Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fcli-core/fcli-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
// Inter-project dependencies
val refs = listOf(
"fcliCommonRef","fcliCommonThirdpartyRef","fcliCommonCiRef","fcliCommonActionRef","fcliCommonToolRef",
"fcliActionRef","fcliAiAssistRef","fcliAviatorRef","fcliConfigRef",
"fcliActionRef","fcliAiAssistRef","fcliAviatorCommonRef","fcliAviatorRef","fcliConfigRef",
"fcliFoDRef","fcliSSCRef","fcliSCSastRef","fcliSCDastRef",
"fcliToolRef","fcliLicenseRef","fcliUtilRef"
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2021-2026 Open Text.
*
* The only warranties for products and services of Open Text
* and its affiliates and licensors ("Open Text") are as may
* be set forth in the express warranty statements accompanying
* such products and services. Nothing herein should be construed
* as constituting an additional warranty. Open Text shall not be
* liable for technical or editorial errors or omissions contained
* herein. The information contained herein is subject to change
* without notice.
*/
package com.fortify.cli.aviator._common.cli.converter;

import com.fortify.cli.aviator.fpr.utils.ISourceDecoder;
import com.fortify.cli.aviator.fpr.utils.SourceDecoders;

import picocli.CommandLine.ITypeConverter;
import picocli.CommandLine.TypeConversionException;

/**
* Picocli adapter: maps a single {@code --source-encodings} token to an
* {@link ISourceDecoder} via the domain factory {@link SourceDecoders}.
*/
public final class SourceDecoderConverter implements ITypeConverter<ISourceDecoder> {
@Override
public ISourceDecoder convert(String value) {
try {
return SourceDecoders.fromToken(value);
} catch (IllegalArgumentException e) {
// Covers blank tokens, IllegalCharsetNameException, UnsupportedCharsetException
throw new TypeConversionException(
e.getMessage() != null ? e.getMessage() : "Invalid source encoding '" + value + "'");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2021-2026 Open Text.
*
* The only warranties for products and services of Open Text
* and its affiliates and licensors ("Open Text") are as may
* be set forth in the express warranty statements accompanying
* such products and services. Nothing herein should be construed
* as constituting an additional warranty. Open Text shall not be
* liable for technical or editorial errors or omissions contained
* herein. The information contained herein is subject to change
* without notice.
*/
package com.fortify.cli.aviator._common.cli.mixin;

import java.util.List;

import com.fortify.cli.aviator._common.cli.converter.SourceDecoderConverter;
import com.fortify.cli.aviator.fpr.utils.ISourceDecoder;
import com.fortify.cli.aviator.fpr.utils.SourceDecoders;

import lombok.Getter;
import picocli.CommandLine.Option;

/**
* Shared {@code --source-encodings} option for Aviator commands that decode
* (and optionally re-encode) source files from an FPR.
*/
public class SourceEncodingsMixin {
@Getter
@Option(names = {"--source-encodings"},
split = ",",
converter = SourceDecoderConverter.class,
defaultValue = SourceDecoders.DEFAULT_SOURCE_ENCODINGS,
paramLabel = "encoding",
descriptionKey = "fcli.aviator.source-encodings")
private List<ISourceDecoder> sourceDecoders;

/**
* Returns a single decoder that tries the configured candidates in order.
*/
public ISourceDecoder getSourceDecoder() {
return SourceDecoders.of(sourceDecoders);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
package com.fortify.cli.aviator.applyRemediation;

import java.util.Objects;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -20,6 +22,8 @@
import com.fortify.cli.aviator.config.IAviatorLogger;
import com.fortify.cli.aviator.fpr.processor.RemediationProcessor;
import com.fortify.cli.aviator.fpr.processor.RemediationProcessor.RemediationMetric;
import com.fortify.cli.aviator.fpr.utils.ISourceDecoder;
import com.fortify.cli.aviator.fpr.utils.SourceDecoders;
import com.fortify.cli.aviator.util.FprHandle;


Expand All @@ -28,6 +32,12 @@ public class ApplyAutoRemediationOnSource {

public static RemediationMetric applyRemediations(FprHandle fprHandle, String sourceCodeDirectory, IAviatorLogger logger)
throws AviatorSimpleException, AviatorTechnicalException {
return applyRemediations(fprHandle, sourceCodeDirectory, SourceDecoders.defaults(), logger);
}

public static RemediationMetric applyRemediations(FprHandle fprHandle, String sourceCodeDirectory,
ISourceDecoder sourceDecoder, IAviatorLogger logger)
throws AviatorSimpleException, AviatorTechnicalException {

LOG.info("Starting apply auto-remediation process for file: {}", fprHandle.getFprPath());

Expand All @@ -37,8 +47,8 @@ public static RemediationMetric applyRemediations(FprHandle fprHandle, String so
}
LOG.info("FPR validation successful");

RemediationProcessor remediationProcessor = new RemediationProcessor(fprHandle, sourceCodeDirectory);
RemediationProcessor remediationProcessor = new RemediationProcessor(fprHandle, sourceCodeDirectory,
Objects.requireNonNull(sourceDecoder, "sourceDecoder"));
return remediationProcessor.processRemediationXML();

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.io.File;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -39,6 +40,7 @@
import com.fortify.cli.aviator.fpr.model.FPRInfo;
import com.fortify.cli.aviator.fpr.processor.AuditProcessor;
import com.fortify.cli.aviator.fpr.processor.StreamingFVDLProcessor;
import com.fortify.cli.aviator.fpr.utils.ISourceDecoder;
import com.fortify.cli.aviator.util.FprHandle;
import com.fortify.cli.aviator.util.ResourceUtil;

Expand All @@ -52,8 +54,11 @@ public static FPRAuditResult auditFPR(AuditFprOptions options)
options.getFprHandle().validate();
AviatorConfigManager.getInstance();

// Non-null: AuditFprOptions defaults via @Builder.Default; CLI mixin always supplies a decoder.
ISourceDecoder sourceDecoder = options.getSourceDecoder();

// --- STAGE 1: PARSING ---
ParsedFprData parsedData = prepareAndParseFpr(options.getFprHandle());
ParsedFprData parsedData = prepareAndParseFpr(options.getFprHandle(), sourceDecoder);
TagMappingConfig tagMappingConfig = loadTagMappingConfig(options.getTagMappingPath());
Map<String, String> issueCategoryLookup = tagMappingConfig.requiresCategoryForSuppressionEvaluation()
? buildIssueCategoryLookup(parsedData.vulnerabilities)
Expand All @@ -69,22 +74,21 @@ public static FPRAuditResult auditFPR(AuditFprOptions options)
Map<String, AuditResponse> auditResponses = new ConcurrentHashMap<>();
AuditOutcome auditOutcome = performAviatorAudit(
parsedData, options.getLogger(), options.getToken(), options.getAppVersion(), options.getUrl(), options.getSscAppName(), options.getSscAppVersion(),
auditResponses, filterSelection, options.getFprHandle(), options.getFolderPriorityOrder()
auditResponses, filterSelection, options.getFprHandle(), options.getFolderPriorityOrder(), sourceDecoder
);

// --- STAGE 4: FINALIZATION ---
return finalizeFprAudit(
auditOutcome, auditResponses, parsedData.auditProcessor,
tagMappingConfig, issueCategoryLookup, parsedData.fprInfo
tagMappingConfig, issueCategoryLookup, parsedData.fprInfo, parsedData.streamingFVDLProcessor
);
}

private static ParsedFprData prepareAndParseFpr(FprHandle fprHandle) {
private static ParsedFprData prepareAndParseFpr(FprHandle fprHandle, ISourceDecoder sourceDecoder) {
try {
// Processors now take the FprHandle directly, no more extracted path
AuditProcessor auditProcessor = new AuditProcessor(fprHandle);
//FVDLProcessor fvdlProcessor = new FVDLProcessor(fprHandle);
StreamingFVDLProcessor streamingFVDLProcessor = new StreamingFVDLProcessor(fprHandle);
AuditProcessor auditProcessor = new AuditProcessor(fprHandle, sourceDecoder);
StreamingFVDLProcessor streamingFVDLProcessor = new StreamingFVDLProcessor(fprHandle, sourceDecoder);

Map<String, AuditIssue> auditIssueMap = auditProcessor.processAuditXML();
FPRProcessor fprProcessor = new FPRProcessor(fprHandle, auditIssueMap, auditProcessor);
Expand Down Expand Up @@ -126,7 +130,8 @@ private static Map<String, String> buildIssueCategoryLookup(List<Vulnerability>
private static AuditOutcome performAviatorAudit(
ParsedFprData parsedData, IAviatorLogger logger,
String token, String appVersion, String url, String sscAppName, String sscAppVersion,
Map<String, AuditResponse> auditResponsesToFill, FilterSelection filterSelection, FprHandle fprHandle, List<String> folderPriorityOrder) {
Map<String, AuditResponse> auditResponsesToFill, FilterSelection filterSelection, FprHandle fprHandle,
List<String> folderPriorityOrder, ISourceDecoder sourceDecoder) {
SourceLanguageResolver sourceLanguageResolver =
new SourceLanguageResolver(parsedData.streamingFVDLProcessor.getFvdlMetadata());
parsedData.streamingFVDLProcessor.getFvdlMetadata().clearSourceFileTypeIndexes();
Expand All @@ -141,7 +146,9 @@ private static AuditOutcome performAviatorAudit(
filterSelection,
logger,
folderPriorityOrder,
sourceLanguageResolver
sourceLanguageResolver,
sourceDecoder,
parsedData.streamingFVDLProcessor.getFvdlMetadata()
);
return issueAuditor.performAudit(
auditResponsesToFill, token, appVersion, parsedData.fprInfo.getBuildId(), url, fprHandle
Expand All @@ -151,33 +158,37 @@ private static AuditOutcome performAviatorAudit(
private static FPRAuditResult finalizeFprAudit(
AuditOutcome auditOutcome, Map<String, AuditResponse> auditResponses,
AuditProcessor auditProcessor, TagMappingConfig tagMappingConfig,
Map<String, String> issueCategoryLookup, FPRInfo fprInfo) {
Map<String, String> issueCategoryLookup, FPRInfo fprInfo, StreamingFVDLProcessor streamingFVDLProcessor) {

int totalIssuesToAudit = auditOutcome.getTotalIssuesToAudit();
int issuesSubmitted = getSubmittedAuditCount(auditResponses);
if (auditResponses.isEmpty()) {
if (totalIssuesToAudit == 0) {
LOG.info("No issues were audited, skipping update and upload");
return new FPRAuditResult(null, "SKIPPED", "No issues to audit", 0, totalIssuesToAudit);
return new FPRAuditResult(null, "SKIPPED", "No issues to audit", 0, totalIssuesToAudit,
issuesSubmitted, 0, Map.of(), 0, Map.of());
} else {
LOG.error("No audit responses received for {} issues", totalIssuesToAudit);
return new FPRAuditResult(null, "FAILED", "No audit responses received from server", 0, totalIssuesToAudit);
return new FPRAuditResult(null, "FAILED", "No audit responses received from server", 0, totalIssuesToAudit,
issuesSubmitted, 0, Map.of(), 0, Map.of());
}
}

long issuesSuccessfullyAudited = auditResponses.values().stream()
.filter(response -> "SUCCESS".equalsIgnoreCase(response.getStatus()))
.count();
Map<String, Integer> skippedByReason = getSkippedAuditReasons(auditResponses, totalIssuesToAudit);
int issuesSkipped = skippedByReason.values().stream().mapToInt(Integer::intValue).sum();

String status;
String message = null;

if (issuesSuccessfullyAudited == totalIssuesToAudit) {
status = "AUDITED";
} else if (issuesSuccessfullyAudited > 0) {
status = "PARTIALLY_AUDITED";
} else {
status = "FAILED";
status = determineAuditStatus(issuesSuccessfullyAudited, issuesSkipped, totalIssuesToAudit, auditResponses.size());
if ("SKIPPED".equals(status)) {
message = String.format("All %d issues were skipped", totalIssuesToAudit);
} else if ("FAILED".equals(status)) {
String commonFailureReason = auditResponses.values().stream()
.filter(response -> !"SKIPPED".equalsIgnoreCase(response.getStatus()))
.map(AuditResponse::getStatusMessage)
.filter(msg -> msg != null && !msg.isBlank())
.findFirst()
Expand All @@ -186,16 +197,69 @@ private static FPRAuditResult finalizeFprAudit(
if (commonFailureReason.startsWith("Client-side pre-processing error: ")) {
commonFailureReason = commonFailureReason.substring("Client-side pre-processing error: ".length());
}
message = String.format("All %d issues failed (%s)", totalIssuesToAudit, commonFailureReason);
message = String.format("No issues were audited (%d skipped; failure details: %s)",
issuesSkipped, commonFailureReason);
}

File updatedFile = null;
if (issuesSuccessfullyAudited > 0) {
updatedFile = auditProcessor.updateAndSaveAuditAndRemediationsXml(
auditResponses, tagMappingConfig, issueCategoryLookup, fprInfo);
auditResponses, tagMappingConfig, issueCategoryLookup, fprInfo,
streamingFVDLProcessor.getFvdlMetadata());
}
AuditProcessor.RemediationGenerationMetric remediationGenerationMetric = auditProcessor.getLastRemediationGenerationMetric();

if (!skippedByReason.isEmpty()) {
LOG.info("Skipped audit issues by reason: {}", skippedByReason);
}
if (!remediationGenerationMetric.skippedByReason().isEmpty()) {
LOG.info("Skipped audit remediation generation by reason: {}", remediationGenerationMetric.skippedByReason());
}

LOG.info("FPR audit process completed with status: {}", status);
return new FPRAuditResult(updatedFile, status, message, (int) issuesSuccessfullyAudited, totalIssuesToAudit);
return new FPRAuditResult(updatedFile, status, message, (int) issuesSuccessfullyAudited, totalIssuesToAudit,
issuesSubmitted, issuesSkipped, skippedByReason, remediationGenerationMetric.skippedRemediations(),
remediationGenerationMetric.skippedByReason());
}

static int getSubmittedAuditCount(Map<String, AuditResponse> auditResponses) {
return (int) auditResponses.values().stream()
.filter(AuditResponse::isSubmittedToAviator)
.count();
}

static String determineAuditStatus(long issuesSuccessfullyAudited, int issuesSkipped,
int totalIssuesToAudit, int responseCount) {
if (issuesSuccessfullyAudited == totalIssuesToAudit) {
return "AUDITED";
}
if (issuesSuccessfullyAudited > 0) {
return "PARTIALLY_AUDITED";
}
if (issuesSkipped == totalIssuesToAudit && responseCount == totalIssuesToAudit) {
return "SKIPPED";
}
return "FAILED";
}

static Map<String, Integer> getSkippedAuditReasons(Map<String, AuditResponse> auditResponses, int totalIssuesToAudit) {
Map<String, Integer> skippedByReason = new LinkedHashMap<>();
auditResponses.values().stream()
.filter(response -> "SKIPPED".equalsIgnoreCase(response.getStatus()))
.map(AuditFPR::getSkippedAuditReason)
.forEach(reason -> recordSkipped(skippedByReason, reason));
int missingResponses = Math.max(0, totalIssuesToAudit - auditResponses.size());
if (missingResponses > 0) {
skippedByReason.merge("No audit response received", missingResponses, Integer::sum);
}
return skippedByReason;
}

private static String getSkippedAuditReason(AuditResponse response) {
return response.getAuditSkipReason().getDisplayMessage();
}

private static void recordSkipped(Map<String, Integer> skippedByReason, String reason) {
skippedByReason.merge(reason, 1, Integer::sum);
}
}
Loading
Loading