diff --git a/fcli-core/fcli-app/build.gradle.kts b/fcli-core/fcli-app/build.gradle.kts index 12c38c2a221..3cbddfca67c 100644 --- a/fcli-core/fcli-app/build.gradle.kts +++ b/fcli-core/fcli-app/build.gradle.kts @@ -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" ) diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/cli/converter/SourceDecoderConverter.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/cli/converter/SourceDecoderConverter.java new file mode 100644 index 00000000000..083d74bbfe1 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/cli/converter/SourceDecoderConverter.java @@ -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 { + @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 + "'"); + } + } +} diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/cli/mixin/SourceEncodingsMixin.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/cli/mixin/SourceEncodingsMixin.java new file mode 100644 index 00000000000..e58f0e86d0d --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/cli/mixin/SourceEncodingsMixin.java @@ -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 sourceDecoders; + + /** + * Returns a single decoder that tries the configured candidates in order. + */ + public ISourceDecoder getSourceDecoder() { + return SourceDecoders.of(sourceDecoders); + } +} diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/applyRemediation/ApplyAutoRemediationOnSource.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/applyRemediation/ApplyAutoRemediationOnSource.java index e2db9d777cb..23b77f0119a 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/applyRemediation/ApplyAutoRemediationOnSource.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/applyRemediation/ApplyAutoRemediationOnSource.java @@ -12,6 +12,8 @@ */ package com.fortify.cli.aviator.applyRemediation; +import java.util.Objects; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -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; @@ -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()); @@ -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(); - } } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/AuditFPR.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/AuditFPR.java index 4571af0bc7f..a89dc206597 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/AuditFPR.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/AuditFPR.java @@ -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; @@ -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; @@ -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 issueCategoryLookup = tagMappingConfig.requiresCategoryForSuppressionEvaluation() ? buildIssueCategoryLookup(parsedData.vulnerabilities) @@ -69,22 +74,21 @@ public static FPRAuditResult auditFPR(AuditFprOptions options) Map 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 auditIssueMap = auditProcessor.processAuditXML(); FPRProcessor fprProcessor = new FPRProcessor(fprHandle, auditIssueMap, auditProcessor); @@ -126,7 +130,8 @@ private static Map buildIssueCategoryLookup(List private static AuditOutcome performAviatorAudit( ParsedFprData parsedData, IAviatorLogger logger, String token, String appVersion, String url, String sscAppName, String sscAppVersion, - Map auditResponsesToFill, FilterSelection filterSelection, FprHandle fprHandle, List folderPriorityOrder) { + Map auditResponsesToFill, FilterSelection filterSelection, FprHandle fprHandle, + List folderPriorityOrder, ISourceDecoder sourceDecoder) { SourceLanguageResolver sourceLanguageResolver = new SourceLanguageResolver(parsedData.streamingFVDLProcessor.getFvdlMetadata()); parsedData.streamingFVDLProcessor.getFvdlMetadata().clearSourceFileTypeIndexes(); @@ -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 @@ -151,7 +158,7 @@ private static AuditOutcome performAviatorAudit( private static FPRAuditResult finalizeFprAudit( AuditOutcome auditOutcome, Map auditResponses, AuditProcessor auditProcessor, TagMappingConfig tagMappingConfig, - Map issueCategoryLookup, FPRInfo fprInfo) { + Map issueCategoryLookup, FPRInfo fprInfo, StreamingFVDLProcessor streamingFVDLProcessor) { int totalIssuesToAudit = auditOutcome.getTotalIssuesToAudit(); if (auditResponses.isEmpty()) { @@ -167,6 +174,8 @@ private static FPRAuditResult finalizeFprAudit( long issuesSuccessfullyAudited = auditResponses.values().stream() .filter(response -> "SUCCESS".equalsIgnoreCase(response.getStatus())) .count(); + Map skippedByReason = getSkippedAuditReasons(auditResponses, totalIssuesToAudit); + int issuesSkipped = skippedByReason.values().stream().mapToInt(Integer::intValue).sum(); String status; String message = null; @@ -192,10 +201,67 @@ private static FPRAuditResult finalizeFprAudit( 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, + issuesSkipped, skippedByReason, remediationGenerationMetric.skippedRemediations(), + remediationGenerationMetric.skippedByReason()); + } + + private static Map getSkippedAuditReasons(Map auditResponses, int totalIssuesToAudit) { + Map skippedByReason = new LinkedHashMap<>(); + auditResponses.values().stream() + .filter(response -> !"SUCCESS".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) { + String statusMessage = response == null ? null : response.getStatusMessage(); + String message = statusMessage == null || statusMessage.isBlank() + ? response == null ? null : response.getStatus() + : statusMessage; + if (message == null || message.isBlank()) { + return "Unknown audit failure"; + } + if (message.startsWith("Client-side pre-processing error: ")) { + message = message.substring("Client-side pre-processing error: ".length()); + } + if (message.startsWith("Could not decode source file")) { + return "Source file decode failed"; + } + if (message.contains("was not found in the FPR")) { + return "Source file not found in FPR"; + } + if (message.contains("could not be read from the FPR")) { + return "Source file read failed"; + } + if ("FAILED".equalsIgnoreCase(message)) { + return "Audit failed"; + } + if ("SKIPPED".equalsIgnoreCase(message)) { + return "Skipped by Aviator"; + } + return message; + } + + private static void recordSkipped(Map skippedByReason, String reason) { + skippedByReason.merge(reason, 1, Integer::sum); } } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java index d25c1286183..dc5b6897892 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -48,7 +49,10 @@ import com.fortify.cli.aviator.fpr.filter.VulnerabilityFilterer; import com.fortify.cli.aviator.fpr.model.AuditIssue; import com.fortify.cli.aviator.fpr.model.FPRInfo; +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; import com.fortify.cli.aviator.fpr.processor.AuditProcessor; +import com.fortify.cli.aviator.fpr.utils.ISourceDecoder; +import com.fortify.cli.aviator.fpr.utils.SourceDecoders; import com.fortify.cli.aviator.grpc.AviatorGrpcClient; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper; import com.fortify.cli.aviator.util.Constants; @@ -80,6 +84,8 @@ public class IssueAuditor { private TagDefinition humanAuditTag; private TagDefinition aviatorStatusTag; private final SourceLanguageResolver sourceLanguageResolver; + private final ISourceDecoder sourceDecoder; + private final FVDLMetadata fvdlMetadata; private final IAviatorLogger logger; private final List customPriorityOrder; @@ -88,6 +94,15 @@ public IssueAuditor(List vulnerabilities, AuditProcessor auditPro FPRInfo fprInfo, String SSCApplicationName, String SSCApplicationVersion, FilterSelection filterSelection, IAviatorLogger logger, List customPriorityOrder, SourceLanguageResolver sourceLanguageResolver) { + this(vulnerabilities, auditProcessor, auditIssueMap, fprInfo, SSCApplicationName, SSCApplicationVersion, + filterSelection, logger, customPriorityOrder, sourceLanguageResolver, SourceDecoders.defaults(), null); + } + + public IssueAuditor(List vulnerabilities, AuditProcessor auditProcessor, Map auditIssueMap, + FPRInfo fprInfo, String SSCApplicationName, String SSCApplicationVersion, + FilterSelection filterSelection, IAviatorLogger logger, List customPriorityOrder, + SourceLanguageResolver sourceLanguageResolver, ISourceDecoder sourceDecoder, + FVDLMetadata fvdlMetadata) { this.logger = logger; this.customPriorityOrder = customPriorityOrder; this.MAX_PER_CATEGORY = Constants.MAX_PER_CATEGORY; @@ -103,6 +118,8 @@ public IssueAuditor(List vulnerabilities, AuditProcessor auditPro this.SSCApplicationName = SSCApplicationName; this.SSCApplicationVersion = SSCApplicationVersion; this.sourceLanguageResolver = sourceLanguageResolver; + this.sourceDecoder = Objects.requireNonNull(sourceDecoder, "sourceDecoder"); + this.fvdlMetadata = fvdlMetadata; this.analysisTag = fprInfo.getFilterTemplate().getTagDefinitions().stream().filter(t -> "Analysis".equalsIgnoreCase(t.getName())).findFirst().orElse(null); this.resultsTag = resolveResultTag("", "", analysisTag); } @@ -162,7 +179,8 @@ public AuditOutcome performAudit(Map auditResponses, Stri } else { try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(url, logger, DEFAULT_PING_INTERVAL_SECONDS)) { CompletableFuture> future = - client.processBatchRequests(promptsToAudit, projectName, fprInfo.getBuildId(), SSCApplicationName, SSCApplicationVersion, token, fprHandle, customPriorityOrder); + client.processBatchRequests(promptsToAudit, projectName, fprInfo.getBuildId(), SSCApplicationName, + SSCApplicationVersion, token, fprHandle, customPriorityOrder, sourceDecoder, fvdlMetadata); Map responses = future.get(500, TimeUnit.MINUTES); responses.forEach((requestId, response) -> auditResponses.put(response.getIssueId(), response)); logger.progress("Audit completed"); diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/AuditFprOptions.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/AuditFprOptions.java index 54b8c803038..8fa13198222 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/AuditFprOptions.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/AuditFprOptions.java @@ -14,6 +14,8 @@ import java.util.List; import com.fortify.cli.aviator.config.IAviatorLogger; +import com.fortify.cli.aviator.fpr.utils.ISourceDecoder; +import com.fortify.cli.aviator.fpr.utils.SourceDecoders; import com.fortify.cli.aviator.util.FprHandle; import lombok.Builder; @@ -34,4 +36,5 @@ public class AuditFprOptions { private final boolean noFilterSet; private final List folderNames; private final List folderPriorityOrder; + @Builder.Default private final ISourceDecoder sourceDecoder = SourceDecoders.defaults(); } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/FPRAuditResult.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/FPRAuditResult.java index 94d8b9a4e18..e2f55d51b62 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/FPRAuditResult.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/FPRAuditResult.java @@ -13,6 +13,7 @@ package com.fortify.cli.aviator.audit.model; import java.io.File; +import java.util.Map; import lombok.Data; @@ -23,13 +24,29 @@ public class FPRAuditResult { private String message; private int issuesSuccessfullyAudited; private int totalIssuesToAudit; + private int issuesSkipped; + private Map skippedByReason; + private int remediationGenerationSkipped; + private Map remediationGenerationSkippedByReason; public FPRAuditResult(File updatedFile, String status, String message, int issuesSuccessfullyAudited, int totalIssuesToAudit) { + this(updatedFile, status, message, issuesSuccessfullyAudited, totalIssuesToAudit, + Math.max(0, totalIssuesToAudit - issuesSuccessfullyAudited), Map.of(), 0, Map.of()); + } + + public FPRAuditResult(File updatedFile, String status, String message, + int issuesSuccessfullyAudited, int totalIssuesToAudit, int issuesSkipped, + Map skippedByReason, int remediationGenerationSkipped, + Map remediationGenerationSkippedByReason) { this.updatedFile = updatedFile; this.status = status; this.message = message; this.issuesSuccessfullyAudited = issuesSuccessfullyAudited; this.totalIssuesToAudit = totalIssuesToAudit; + this.issuesSkipped = issuesSkipped; + this.skippedByReason = skippedByReason == null ? Map.of() : Map.copyOf(skippedByReason); + this.remediationGenerationSkipped = remediationGenerationSkipped; + this.remediationGenerationSkippedByReason = remediationGenerationSkippedByReason == null ? Map.of() : Map.copyOf(remediationGenerationSkippedByReason); } } \ No newline at end of file diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuditProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuditProcessor.java index 602367fbf03..d919ff321b6 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuditProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuditProcessor.java @@ -27,8 +27,10 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -59,7 +61,10 @@ import com.fortify.cli.aviator.config.TagMappingConfig; import com.fortify.cli.aviator.fpr.model.AuditIssue; import com.fortify.cli.aviator.fpr.model.FPRInfo; +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; import com.fortify.cli.aviator.fpr.utils.FileUtils; +import com.fortify.cli.aviator.fpr.utils.ISourceDecoder; +import com.fortify.cli.aviator.fpr.utils.SourceDecoders; import com.fortify.cli.aviator.util.Constants; import com.fortify.cli.aviator.util.FprHandle; @@ -83,9 +88,39 @@ public class AuditProcessor { private final Map auditIssueMap = new HashMap<>(); private final FprHandle fprHandle; + private final ISourceDecoder sourceDecoder; + private RemediationGenerationMetric lastRemediationGenerationMetric = RemediationGenerationMetric.empty(); + + public record RemediationGenerationMetric(int skippedRemediations, Map skippedByReason) { + public static RemediationGenerationMetric empty() { + return new RemediationGenerationMetric(0, Map.of()); + } + } + + private enum RemediationSkipReason { + SOURCE_READ_OR_DECODE_FAILED("Source file read/decode failed"), + INVALID_LINE_NUMBER("Invalid line number"), + STRUCTURALLY_INVALID("Structurally invalid remediation"), + NO_VALID_CHANGES("No valid changes"); + + private final String label; + + RemediationSkipReason(String label) { + this.label = label; + } + } public AuditProcessor(FprHandle fprHandle) { + this(fprHandle, SourceDecoders.defaults()); + } + + public AuditProcessor(FprHandle fprHandle, ISourceDecoder sourceDecoder) { this.fprHandle = fprHandle; + this.sourceDecoder = Objects.requireNonNull(sourceDecoder, "sourceDecoder"); + } + + public RemediationGenerationMetric getLastRemediationGenerationMetric() { + return lastRemediationGenerationMetric; } /** @@ -709,6 +744,13 @@ private String addCommentToIssueElement(Element issueElement, String commentText public File updateAndSaveAuditAndRemediationsXml(Map auditResponses, TagMappingConfig tagMappingConfig, Map issueCategoryLookup, FPRInfo fprInfo) throws AviatorTechnicalException { + return updateAndSaveAuditAndRemediationsXml(auditResponses, tagMappingConfig, issueCategoryLookup, fprInfo, null); + } + + public File updateAndSaveAuditAndRemediationsXml(Map auditResponses, + TagMappingConfig tagMappingConfig, Map issueCategoryLookup, + FPRInfo fprInfo, FVDLMetadata fvdlMetadata) throws AviatorTechnicalException { + lastRemediationGenerationMetric = RemediationGenerationMetric.empty(); // Step 1: Apply this save's audit responses. writtenInstanceIds is the local retain set. Map effectiveIssueCategoryLookup = issueCategoryLookup == null ? Map.of() : issueCategoryLookup; AuditXmlUpdateResult updateResult = updateAuditXml( @@ -728,7 +770,10 @@ public File updateAndSaveAuditAndRemediationsXml(Map audi // Step 4: Generate the in-memory remediations.xml document if needed. if (hasRemediations && !remediationCommentTimestamps.isEmpty()) { - this.remediationsDoc = generateRemediationsXml(auditResponses, remediationCommentTimestamps, fprInfo); + Map skippedByReason = new LinkedHashMap<>(); + this.remediationsDoc = generateRemediationsXml(auditResponses, remediationCommentTimestamps, fprInfo, + fvdlMetadata, skippedByReason); + lastRemediationGenerationMetric = toRemediationGenerationMetric(skippedByReason); } else { this.remediationsDoc = null; if (hasRemediations) { @@ -765,7 +810,8 @@ public File updateAndSaveAuditAndRemediationsXml(Map audi private Document generateRemediationsXml(Map auditResponses, Map remediationCommentTimestamps, - FPRInfo fprInfo) throws AviatorTechnicalException { + FPRInfo fprInfo, FVDLMetadata fvdlMetadata, + Map skippedByReason) throws AviatorTechnicalException { try { DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); docFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); @@ -794,6 +840,7 @@ private Document generateRemediationsXml(Map auditRespons Element remediationListElement = finalDoc.createElementNS(REMEDIATIONS_NAMESPACE_URI, "RemediationList"); rootElement.appendChild(remediationListElement); + FileUtils fileUtils = new FileUtils(this.sourceDecoder, fvdlMetadata); int validRemediationCount = 0; for (Map.Entry entry : auditResponses.entrySet()) { @@ -828,12 +875,11 @@ private Document generateRemediationsXml(Map auditRespons filenameElement.setTextContent(filename); fileChangesElement.appendChild(filenameElement); - //Optional originalFileContentOptional = fvdlProcessor.getSourceFileContent(filename); - FileUtils fileUtils = new FileUtils(); - Optional originalFileContentOptional = fileUtils.getSourceFileContent(fprHandle, filename); + Optional originalFileContentOptional = fileUtils.getSourceFileContent(fprHandle, filename); if (originalFileContentOptional.isEmpty()) { logger.warn("WARN: Could not retrieve source code for file '{}'. Skipping remediation generation for this file for instanceId '{}'.", filename, instanceId); + recordSkipped(skippedByReason, RemediationSkipReason.SOURCE_READ_OR_DECODE_FAILED); continue; } @@ -900,6 +946,7 @@ private Document generateRemediationsXml(Map auditRespons fileChangesElement.appendChild(changeElement); } catch (NumberFormatException e) { logger.error("Skipping change for issue {} due to invalid line number format. Details: {}", instanceId, e.getMessage()); + recordSkipped(skippedByReason, RemediationSkipReason.INVALID_LINE_NUMBER); } } if (fileChangesElement.getElementsByTagNameNS(REMEDIATIONS_NAMESPACE_URI, "Change").getLength() > 0) { @@ -915,9 +962,11 @@ private Document generateRemediationsXml(Map auditRespons validRemediationCount++; } else { logger.warn("WARN: Skipping structurally invalid remediation for issue instanceId: {}", instanceId); + recordSkipped(skippedByReason, RemediationSkipReason.STRUCTURALLY_INVALID); } } else { logger.warn("WARN: Skipping remediation for instanceId '{}' because all of its proposed changes were invalid and could not be processed.", instanceId); + recordSkipped(skippedByReason, RemediationSkipReason.NO_VALID_CHANGES); } } @@ -929,6 +978,15 @@ private Document generateRemediationsXml(Map auditRespons } } + private RemediationGenerationMetric toRemediationGenerationMetric(Map skippedByReason) { + int skippedRemediations = skippedByReason.values().stream().mapToInt(Integer::intValue).sum(); + return new RemediationGenerationMetric(skippedRemediations, Map.copyOf(skippedByReason)); + } + + private void recordSkipped(Map skippedByReason, RemediationSkipReason reason) { + skippedByReason.merge(reason.label, 1, Integer::sum); + } + private String calculateHashBase64(String content, String algorithm) { if (content == null) return ""; try { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java index 97c1ffce863..2a059b8380b 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java @@ -14,11 +14,7 @@ import java.io.IOException; import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; -import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -32,6 +28,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.zip.ZipFile; @@ -49,6 +46,12 @@ import com.fortify.cli.aviator._common.exception.AviatorSimpleException; import com.fortify.cli.aviator._common.exception.AviatorTechnicalException; import com.fortify.cli.aviator.fpr.model.FVDLMetadata; +import com.fortify.cli.aviator.fpr.utils.ISourceDecoder; +import com.fortify.cli.aviator.fpr.utils.ISourceDecoder.DecodeResult; +import com.fortify.cli.aviator.fpr.utils.ISourceDecoder.SourceDecodeException; +import com.fortify.cli.aviator.fpr.utils.SourceDecoders; +import com.fortify.cli.aviator.fpr.utils.SourceEncoder; +import com.fortify.cli.aviator.fpr.utils.SourceEncoder.SourceEncodeException; import com.fortify.cli.aviator.util.FprHandle; import com.fortify.cli.aviator.util.FuzzyContextSearcher; @@ -58,6 +61,7 @@ public class RemediationProcessor { private final FprHandle fprHandle; private final String sourceCodeDirectory; + private final ISourceDecoder sourceDecoder; public record RemediationMetric(int totalRemediations, int appliedRemediations, int skippedRemediations, Set modifiedFiles, Map skippedByReason) { @@ -66,16 +70,14 @@ public RemediationMetric(int totalRemediations, int appliedRemediations, int ski } } - private record FvdlMetadataResult(FVDLMetadata metadata, SkipReason skipReason) {} + private record SourceFileContent(String content, Charset charset, String encodingSource) {} - private record PendingFileWrite(String filename, Path filePath, String content, byte[] updatedBytes) {} + private record PendingFileWrite(String filename, Path filePath, String content, Charset charset, String encodingSource, + byte[] updatedBytes) {} private record RollbackFileWrite(String filename, Path filePath, byte[] originalBytes) {} private enum SkipReason { - FVDL_METADATA_UNAVAILABLE("FVDL metadata unavailable"), - FVDL_ENCODING_MISSING("FVDL source encoding missing"), - FVDL_ENCODING_UNSUPPORTED("FVDL source encoding unsupported"), SOURCE_FILE_MISSING("Source file missing"), SOURCE_FILE_OUTSIDE_SOURCE_DIR("Source file outside source directory"), SOURCE_READ_FAILED("Source file read failed"), @@ -136,8 +138,13 @@ private static class RollbackRemediationException extends AviatorTechnicalExcept } public RemediationProcessor(FprHandle fprHandle, String sourceCodeDirectory) { + this(fprHandle, sourceCodeDirectory, SourceDecoders.defaults()); + } + + public RemediationProcessor(FprHandle fprHandle, String sourceCodeDirectory, ISourceDecoder sourceDecoder) { this.fprHandle = fprHandle; this.sourceCodeDirectory = sourceCodeDirectory; + this.sourceDecoder = Objects.requireNonNull(sourceDecoder, "sourceDecoder"); } public RemediationMetric processRemediationXML() { @@ -157,7 +164,7 @@ public RemediationMetric processRemediationXML() { } final Path sourceBasePath = Paths.get(trimmedSourceDir).toAbsolutePath().normalize(); LOG.debug("Applying remediations from {} to source directory {}", remediationPath, sourceBasePath); - final FvdlMetadataResult fvdlMetadataResult = loadFvdlMetadata(); + final FVDLMetadata fvdlMetadata = loadFvdlMetadata(); try (InputStream remediationStream = Files.newInputStream(remediationPath)) { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); @@ -176,7 +183,7 @@ public RemediationMetric processRemediationXML() { appliedRemediations = 0; for (int i = 0; i < remediationNodes.getLength(); i++) { Element remediation = (Element) remediationNodes.item(i); - if (processRemediation(remediation, sourceBasePath, fvdlMetadataResult, modifiedFiles, skippedByReason)) { + if (processRemediation(remediation, sourceBasePath, fvdlMetadata, modifiedFiles, skippedByReason)) { appliedRemediations++; } } @@ -198,11 +205,11 @@ public RemediationMetric processRemediationXML() { return new RemediationMetric(totalRemediations, appliedRemediations, skippedRemediations, modifiedFiles, skippedByReason); } - private boolean processRemediation(Element remediation, Path sourceBasePath, FvdlMetadataResult fvdlMetadataResult, + private boolean processRemediation(Element remediation, Path sourceBasePath, FVDLMetadata fvdlMetadata, Set modifiedFiles, Map skippedByReason) { String instanceId = remediation.getAttribute("instanceId"); try { - Map pendingWrites = prepareFileChanges(remediation, sourceBasePath, fvdlMetadataResult); + Map pendingWrites = prepareFileChanges(remediation, sourceBasePath, fvdlMetadata); if (pendingWrites.isEmpty()) { recordSkipped(skippedByReason, SkipReason.NO_CHANGES); return false; @@ -230,7 +237,7 @@ private boolean processRemediation(Element remediation, Path sourceBasePath, Fvd } private Map prepareFileChanges(Element remediation, Path sourceBasePath, - FvdlMetadataResult fvdlMetadataResult) { + FVDLMetadata fvdlMetadata) { NodeList fileChangesNodes = remediation.getElementsByTagNameNS(NAMESPACE_URI, "FileChanges"); if (fileChangesNodes.getLength() == 0) { throw new SkipRemediationException(SkipReason.NO_CHANGES, "No file changes found"); @@ -238,12 +245,12 @@ private Map prepareFileChanges(Element remediation, Path Map pendingWrites = new LinkedHashMap<>(); for (int j = 0; j < fileChangesNodes.getLength(); j++) { - processFileChanges(remediation, (Element) fileChangesNodes.item(j), sourceBasePath, fvdlMetadataResult, pendingWrites); + processFileChanges(remediation, (Element) fileChangesNodes.item(j), sourceBasePath, fvdlMetadata, pendingWrites); } return pendingWrites; } - private boolean processFileChanges(Element remediation, Element fileChanges, Path sourceBasePath, FvdlMetadataResult fvdlMetadataResult, + private boolean processFileChanges(Element remediation, Element fileChanges, Path sourceBasePath, FVDLMetadata fvdlMetadata, Map pendingWrites) { String instanceId = remediation.getAttribute("instanceId"); String filename = getRequiredElementText(fileChanges, "Filename"); @@ -260,23 +267,25 @@ private boolean processFileChanges(Element remediation, Element fileChanges, Pat } String fileHash = getRequiredElementText(fileChanges, "Hash"); - Charset sourceEncoding = getRequiredSourceEncoding(filename, fvdlMetadataResult); NodeList changesNodes = fileChanges.getElementsByTagNameNS(NAMESPACE_URI, "Change"); if (changesNodes.getLength() == 0) { throw new SkipRemediationException(SkipReason.NO_CHANGES, "No changes found for file: " + filename); } - LOG.debug("Remediation {} has {} change(s) for '{}' using FVDL encoding {}", instanceId, changesNodes.getLength(), filename, - sourceEncoding.name()); + SourceFileContent sourceFileContent = getPendingOrSourceContent(filePath, filename, fvdlMetadata, pendingWrites); + Charset sourceEncoding = sourceFileContent.charset(); + LOG.debug("Remediation {} has {} change(s) for '{}' using source encoding {}", instanceId, changesNodes.getLength(), filename, + sourceFileContent.encodingSource()); - String updatedContent = getPendingOrSourceContent(filePath, filename, sourceEncoding, pendingWrites); + String updatedContent = sourceFileContent.content(); for (int k = 0; k < changesNodes.getLength(); k++) { updatedContent = applyChange(instanceId, filename, fileHash, sourceEncoding, updatedContent, (Element) changesNodes.item(k), k + 1); } - byte[] updatedBytes = encodeStrict(updatedContent, sourceEncoding, filename); - pendingWrites.put(filePath, new PendingFileWrite(filename, filePath, updatedContent, updatedBytes)); - LOG.debug("Staged remediation {} for '{}' using FVDL encoding {}; changes={}, encodedBytes={}", instanceId, filename, - sourceEncoding.name(), changesNodes.getLength(), updatedBytes.length); + byte[] updatedBytes = encodeSourceFile(updatedContent, sourceEncoding, filename); + pendingWrites.put(filePath, new PendingFileWrite(filename, filePath, updatedContent, sourceEncoding, + sourceFileContent.encodingSource(), updatedBytes)); + LOG.debug("Staged remediation {} for '{}' using source encoding {}; changes={}, encodedBytes={}", instanceId, filename, + sourceFileContent.encodingSource(), changesNodes.getLength(), updatedBytes.length); return true; } @@ -334,10 +343,12 @@ private String applyChange(String instanceId, String filename, String fileHash, return String.join(lineSeparator, updatedLines); } - private String getPendingOrSourceContent(Path filePath, String filename, Charset sourceEncoding, + private SourceFileContent getPendingOrSourceContent(Path filePath, String filename, FVDLMetadata fvdlMetadata, Map pendingWrites) { PendingFileWrite pendingWrite = pendingWrites.get(filePath); - return pendingWrite == null ? readSourceFile(filePath, filename, sourceEncoding) : pendingWrite.content(); + return pendingWrite == null + ? readSourceFile(filePath, filename, fvdlMetadata) + : new SourceFileContent(pendingWrite.content(), pendingWrite.charset(), pendingWrite.encodingSource()); } private void commitRemediationWrites(String instanceId, Map pendingWrites, Set modifiedFiles) @@ -392,84 +403,46 @@ private boolean isFilePresent(Path path) { return Files.exists(path) && Files.isRegularFile(path); } - private FvdlMetadataResult loadFvdlMetadata() { + /** Nullable: missing/unreadable FVDL means FPR encoding candidate is skipped. */ + private FVDLMetadata loadFvdlMetadata() { if (!Files.exists(fprHandle.getPath("/audit.fvdl"))) { - LOG.warn("FVDL file '/audit.fvdl' is missing; source remediations will be skipped"); - return new FvdlMetadataResult(null, SkipReason.FVDL_METADATA_UNAVAILABLE); + LOG.warn("FVDL file '/audit.fvdl' is missing; FPR encoding candidate will be skipped"); + return null; } try (ZipFile zipFile = new ZipFile(fprHandle.getFprPath().toFile())) { LOG.debug("Loading FVDL build metadata from '{}' to resolve source encodings", fprHandle.getFprPath()); - StreamingFVDLProcessor processor = new StreamingFVDLProcessor(fprHandle); + // Decoder unused for metadata-only parse; ctor requires one for FileUtils wiring. + StreamingFVDLProcessor processor = new StreamingFVDLProcessor(fprHandle, sourceDecoder); processor.parseBuildMetadata(zipFile, "audit.fvdl"); LOG.debug("Loaded FVDL build metadata from '{}'", fprHandle.getFprPath()); - return new FvdlMetadataResult(processor.getFvdlMetadata(), null); + return processor.getFvdlMetadata(); } catch (Exception e) { - LOG.warn("Error reading source file encodings from audit.fvdl; source remediations will be skipped", e); - return new FvdlMetadataResult(null, SkipReason.FVDL_METADATA_UNAVAILABLE); + LOG.warn("Error reading source file encodings from audit.fvdl; FPR encoding candidate will be skipped", e); + return null; } } - private Charset getRequiredSourceEncoding(String filename, FvdlMetadataResult fvdlMetadataResult) { - if (fvdlMetadataResult.skipReason() != null || fvdlMetadataResult.metadata() == null) { - throw new SkipRemediationException(SkipReason.FVDL_METADATA_UNAVAILABLE, - "FVDL metadata is unavailable; cannot determine source encoding for file '" + filename + "'"); - } - - String encoding = fvdlMetadataResult.metadata().findSourceFileEncodingForFileName(filename); - if (encoding == null || encoding.isBlank()) { - LOG.debug("FVDL source encoding lookup failed for '{}'", filename); - throw new SkipRemediationException(SkipReason.FVDL_ENCODING_MISSING, - "FVDL does not declare a source encoding for file '" + filename + "'"); - } - - try { - Charset charset = Charset.forName(encoding); - LOG.debug("FVDL source encoding for '{}' resolved to '{}'", filename, charset.name()); - return charset; - } catch (Exception e) { - throw new SkipRemediationException(SkipReason.FVDL_ENCODING_UNSUPPORTED, - "FVDL declares unsupported source encoding '" + encoding + "' for file '" + filename + "'", e); - } - } - - private String readSourceFile(Path filePath, String filename, Charset sourceEncoding) { + private SourceFileContent readSourceFile(Path filePath, String filename, FVDLMetadata fvdlMetadata) { try { byte[] sourceBytes = Files.readAllBytes(filePath); - String decodedContent = decodeStrict(sourceBytes, sourceEncoding); - LOG.debug("Strict decoded '{}' using {}; sourceBytes={}, decodedChars={}", filename, sourceEncoding.name(), sourceBytes.length, - decodedContent.length()); - return decodedContent; - } catch (CharacterCodingException e) { - throw new SkipRemediationException(SkipReason.SOURCE_DECODE_FAILED, - "FVDL declares source encoding '" + sourceEncoding.name() + "' for file '" + filename + - "', but the source file cannot be decoded using that encoding", e); + // Metadata may be null (FVDL missing); FPR candidate fails and other encodings are tried. + DecodeResult decodeResult = sourceDecoder.decode(sourceBytes, filename, fvdlMetadata); + LOG.debug("Strict decoded '{}' using {}; sourceBytes={}, decodedChars={}", filename, decodeResult.source(), sourceBytes.length, + decodeResult.content().length()); + return new SourceFileContent(decodeResult.content(), decodeResult.charset(), decodeResult.source()); + } catch (SourceDecodeException e) { + throw new SkipRemediationException(SkipReason.SOURCE_DECODE_FAILED, e.getMessage(), e); } catch (IOException e) { throw new SkipRemediationException(SkipReason.SOURCE_READ_FAILED, "Error reading source code file '" + filePath + "'", e); } } - private String decodeStrict(byte[] bytes, Charset charset) throws CharacterCodingException { - return charset.newDecoder() - .onMalformedInput(CodingErrorAction.REPORT) - .onUnmappableCharacter(CodingErrorAction.REPORT) - .decode(ByteBuffer.wrap(bytes)) - .toString(); - } - - private byte[] encodeStrict(String content, Charset charset, String filename) { + private byte[] encodeSourceFile(String content, Charset charset, String filename) { try { - ByteBuffer buffer = charset.newEncoder() - .onMalformedInput(CodingErrorAction.REPORT) - .onUnmappableCharacter(CodingErrorAction.REPORT) - .encode(CharBuffer.wrap(content)); - byte[] result = new byte[buffer.remaining()]; - buffer.get(result); - return result; - } catch (CharacterCodingException e) { - throw new SkipRemediationException(SkipReason.REMEDIATION_ENCODE_FAILED, - "Remediation content for file '" + filename + "' cannot be encoded using FVDL source encoding '" + - charset.name() + "'", e); + return SourceEncoder.encode(content, charset, filename); + } catch (SourceEncodeException e) { + throw new SkipRemediationException(SkipReason.REMEDIATION_ENCODE_FAILED, e.getMessage(), e); } } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/StreamingFVDLProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/StreamingFVDLProcessor.java index 9a3f8ffa9e8..3dace7f6e75 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/StreamingFVDLProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/StreamingFVDLProcessor.java @@ -32,6 +32,8 @@ import com.fortify.cli.aviator.fpr.filter.AnalyzerType; import com.fortify.cli.aviator.fpr.model.*; import com.fortify.cli.aviator.fpr.utils.FileUtils; +import com.fortify.cli.aviator.fpr.utils.ISourceDecoder; +import com.fortify.cli.aviator.fpr.utils.SourceDecoders; import com.fortify.cli.aviator.fpr.utils.XmlUtils; import com.fortify.cli.aviator.util.FprHandle; import com.fortify.cli.aviator.util.StringUtil; @@ -70,17 +72,26 @@ public class StreamingFVDLProcessor { private long peakMemoryPass2 = 0; private long peakMemoryPostProcessing = 0; - public StreamingFVDLProcessor(FprHandle fprHandle){ + public StreamingFVDLProcessor(FprHandle fprHandle) { + this(fprHandle, SourceDecoders.defaults()); + } + + /** + * @param sourceDecoder used for stack-trace line/fragment source reads; shares {@link #fvdlMetadata} + * so FPR encoding candidates resolve after build metadata is parsed. + */ + public StreamingFVDLProcessor(FprHandle fprHandle, ISourceDecoder sourceDecoder) { this.vulnFinalizer = new VulnFinalizer(); - this.fileUtils = new FileUtils(); this.fprHandle = fprHandle; this.sourceFileMap = fprHandle.getSourceFileMap(); this.xmlInputFactory = XMLInputFactory.newInstance(); // Security: Disable external entity processing xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); - //this.parsingMetadata = new ParsingMetadata(); this.fvdlMetadata = new FVDLMetadata(); + // Same metadata instance FileUtils will see once encodings are registered during parse. + this.fileUtils = new FileUtils( + Objects.requireNonNull(sourceDecoder, "sourceDecoder"), this.fvdlMetadata); this.rawVulnerabilities = new ArrayList<>(); this.vulnerabilities = new ArrayList<>(); this.descriptionProcessor = new DescriptionProcessor(); @@ -92,8 +103,6 @@ public StreamingFVDLProcessor(FprHandle fprHandle){ this.traceParser.setNodeParser(nodeParser); // Circular dependency for Reason parsing this.descriptionParser = new DescriptionParser(); this.metadataParser = new MetadataParser(); - /*this.extractedPath = extractedPath; - this.indexXMLProcessor = new IndexXMLProcessor(extractedPath, sourceFileMap);*/ } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/CharsetSourceDecoder.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/CharsetSourceDecoder.java new file mode 100644 index 00000000000..b0cf5d06621 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/CharsetSourceDecoder.java @@ -0,0 +1,45 @@ +/* + * 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.fpr.utils; + +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.util.Objects; + +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; + +/** Fixed-{@link Charset} decoder. Created via {@link SourceDecoders}. */ +final class CharsetSourceDecoder implements ISourceDecoder { + private final Charset charset; + private final String label; + + CharsetSourceDecoder(String charsetName) { + this.charset = Charset.forName(charsetName); + this.label = charsetName; + } + + @Override + public DecodeResult decode(byte[] bytes, String filename, FVDLMetadata fvdlMetadata) { + Objects.requireNonNull(bytes, "bytes must not be null"); + try { + return new DecodeResult(SourceEncoder.decodeStrict(bytes, charset), charset, label); + } catch (CharacterCodingException e) { + throw new SourceDecodeException(label + " failed to decode source bytes", e); + } + } + + @Override + public String describe() { + return label; + } +} diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/CompositeSourceDecoder.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/CompositeSourceDecoder.java new file mode 100644 index 00000000000..9c335cb550f --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/CompositeSourceDecoder.java @@ -0,0 +1,53 @@ +/* + * 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.fpr.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; + +/** Tries candidates in order. Created via {@link SourceDecoders#of}. */ +final class CompositeSourceDecoder implements ISourceDecoder { + private final List decoders; + + CompositeSourceDecoder(List decoders) { + Objects.requireNonNull(decoders, "decoders must not be null"); + if (decoders.isEmpty()) { + throw new IllegalArgumentException("decoders must not be empty"); + } + this.decoders = List.copyOf(decoders); + } + + @Override + public DecodeResult decode(byte[] bytes, String filename, FVDLMetadata fvdlMetadata) { + Objects.requireNonNull(bytes, "bytes must not be null"); + List failures = new ArrayList<>(); + for (ISourceDecoder decoder : decoders) { + try { + return decoder.decode(bytes, filename, fvdlMetadata); + } catch (SourceDecodeException e) { + failures.add(e.getMessage() != null ? e.getMessage() : decoder.describe() + " failed"); + } + } + throw new SourceDecodeException("Could not decode source file '" + filename + "' using source encodings " + + describe() + "; attempted: " + String.join("; ", failures)); + } + + @Override + public String describe() { + return decoders.stream().map(ISourceDecoder::describe).collect(Collectors.joining(",")); + } +} diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/FileUtils.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/FileUtils.java index 6ecd49aed6c..b45ea13f4f2 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/FileUtils.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/FileUtils.java @@ -14,13 +14,13 @@ import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -28,15 +28,31 @@ import org.slf4j.LoggerFactory; import com.fortify.cli.aviator.audit.model.Fragment; +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; import com.fortify.cli.aviator.util.FileTypeLanguageMapperUtil; import com.fortify.cli.aviator.util.FileUtil; import com.fortify.cli.aviator.util.FprHandle; import com.fortify.cli.aviator.util.LanguageCommentMapperUtil; import com.fortify.cli.aviator.util.StringUtil; +/** + * Source file helpers. Decode failures are soft (empty result + log): snippets/lines are best-effort. + * Callers that must fail or skip with metrics (e.g. remediation apply) should decode themselves. + */ public class FileUtils { private static final Logger logger = LoggerFactory.getLogger(FileUtils.class); private final Map> fileContentCache = new ConcurrentHashMap<>(); + private final ISourceDecoder sourceDecoder; + private final FVDLMetadata fvdlMetadata; + + public FileUtils() { + this(SourceDecoders.defaults(), null); + } + + public FileUtils(ISourceDecoder sourceDecoder, FVDLMetadata fvdlMetadata) { + this.sourceDecoder = Objects.requireNonNull(sourceDecoder, "sourceDecoder"); + this.fvdlMetadata = fvdlMetadata; + } /** * Reads all lines from a file, caching the result to avoid repeated reads. @@ -46,12 +62,16 @@ public class FileUtils { * @return List of lines, or empty list if file not found or error occurs */ public List readFileWithFallback(Path filePath) { + return readFileWithFallback(filePath, filePath.getFileName().toString()); + } + + private List readFileWithFallback(Path filePath, String filename) { return fileContentCache.computeIfAbsent(filePath, path -> { try { byte[] fileBytes = Files.readAllBytes(path); - String content = new String(fileBytes, StandardCharsets.UTF_8); + String content = sourceDecoder.decode(fileBytes, filename, fvdlMetadata).content(); return Arrays.asList(content.split("\\r?\\n")); - } catch (IOException e) { + } catch (IOException | ISourceDecoder.SourceDecodeException e) { logger.error("Failed to read file: {}", path, e); return Collections.emptyList(); } @@ -80,7 +100,7 @@ public String getLineFromFile(FprHandle fprHandle, String relativePath, int line Path fullSourcePath = resolveFullPath(fprHandle, relativePath); if (fullSourcePath == null) return ""; - List lines = readFileWithFallback(fullSourcePath); + List lines = readFileWithFallback(fullSourcePath, relativePath); if (lineNumber > 0 && lines.size() >= lineNumber) { return appendLineNumbers(lines.get(lineNumber - 1), relativePath, lineNumber - 1); } @@ -97,7 +117,7 @@ public Fragment getFragmentFromFile(FprHandle fprHandle, String relativePath, in return new Fragment("", 0, 0); } - List lines = readFileWithFallback(fullSourcePath); + List lines = readFileWithFallback(fullSourcePath, relativePath); if (lines.isEmpty() || lineNumber <= 0) { return new Fragment("", 0, 0); } @@ -136,8 +156,9 @@ public Optional getSourceFileContent(FprHandle fprHandle, String relativ } try { - return Optional.of(String.join(System.lineSeparator(), readFileWithFallback(actualSourcePath))); - } catch (Exception e) { + byte[] fileBytes = Files.readAllBytes(actualSourcePath); + return Optional.of(sourceDecoder.decode(fileBytes, relativePath, fvdlMetadata).content()); + } catch (IOException | ISourceDecoder.SourceDecodeException e) { logger.warn("Could not read source file content for path: {}", relativePath, e); return Optional.empty(); } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/FprSourceDecoder.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/FprSourceDecoder.java new file mode 100644 index 00000000000..f7241d3ed73 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/FprSourceDecoder.java @@ -0,0 +1,57 @@ +/* + * 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.fpr.utils; + +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.util.Objects; + +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; + +/** Resolves encoding from FVDL metadata, then strict-decodes. Created via {@link SourceDecoders}. */ +final class FprSourceDecoder implements ISourceDecoder { + static final String TOKEN = "FPR"; + + @Override + public DecodeResult decode(byte[] bytes, String filename, FVDLMetadata fvdlMetadata) { + Objects.requireNonNull(bytes, "bytes must not be null"); + Charset charset = resolveCharset(filename, fvdlMetadata); + try { + String content = SourceEncoder.decodeStrict(bytes, charset); + return new DecodeResult(content, charset, TOKEN + "(" + charset.name() + ")"); + } catch (CharacterCodingException e) { + throw new SourceDecodeException(TOKEN + "(" + charset.name() + ") failed to decode source bytes", e); + } + } + + private static Charset resolveCharset(String filename, FVDLMetadata fvdlMetadata) { + if (fvdlMetadata == null) { + throw new SourceDecodeException("FPR metadata unavailable"); + } + String encodingName = fvdlMetadata.findSourceFileEncodingForFileName(filename); + if (encodingName == null || encodingName.isBlank()) { + throw new SourceDecodeException("FPR encoding missing for '" + filename + "'"); + } + try { + return Charset.forName(encodingName); + } catch (Exception e) { + throw new SourceDecodeException( + TOKEN + " resolved to unsupported encoding '" + encodingName + "'", e); + } + } + + @Override + public String describe() { + return TOKEN; + } +} diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/ISourceDecoder.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/ISourceDecoder.java new file mode 100644 index 00000000000..96e76154a8b --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/ISourceDecoder.java @@ -0,0 +1,55 @@ +/* + * 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.fpr.utils; + +import java.nio.charset.Charset; + +import com.fortify.cli.aviator._common.exception.AviatorSimpleException; +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; + +/** + * Decodes source file bytes using a specific encoding strategy. + * Implementations may resolve the encoding from FVDL metadata, use a fixed + * {@link Charset}, or try multiple candidate decoders in order. + */ +public interface ISourceDecoder { + /** + * Decode source file bytes into text. + * + * @param bytes raw source file bytes + * @param filename source file name (used for FPR encoding lookup and error messages) + * @param fvdlMetadata optional FVDL metadata; required only by FPR-based decoders + * @return decoded content together with the charset that produced it + * @throws SourceDecodeException if this decoder cannot decode the bytes + */ + DecodeResult decode(byte[] bytes, String filename, FVDLMetadata fvdlMetadata); + + /** + * Human-readable description of this decoder (used in CLI help and error messages). + */ + String describe(); + + record DecodeResult(String content, Charset charset, String source) {} + + class SourceDecodeException extends AviatorSimpleException { + private static final long serialVersionUID = 1L; + + public SourceDecodeException(String message) { + super(message); + } + + public SourceDecodeException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/SourceCodeEnricher.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/SourceCodeEnricher.java index 57e28043765..235c3d60512 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/SourceCodeEnricher.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/SourceCodeEnricher.java @@ -12,18 +12,18 @@ */ package com.fortify.cli.aviator.fpr.utils; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fortify.cli.aviator.audit.model.File; import com.fortify.cli.aviator.audit.model.StackTraceElement; +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; import com.fortify.cli.aviator.util.FprHandle; import com.fortify.cli.aviator.util.StringUtil; @@ -39,24 +39,17 @@ public class SourceCodeEnricher { private static final Logger logger = LoggerFactory.getLogger(SourceCodeEnricher.class); - /*private final Path extractedPath; - private final Map sourceFileMap;*/ private final FprHandle fprHandle; private final FileUtils fileUtils; - /** - * Creates a new SourceCodeEnricher with the required dependencies. - * @param fprHandle Utility for file operations (line numbering, line counting) - */ - /*public SourceCodeEnricher(Path extractedPath, Map sourceFileMap, FileUtils fileUtils) { - this.extractedPath = extractedPath; - this.sourceFileMap = sourceFileMap; - this.fileUtils = fileUtils; - }*/ + public SourceCodeEnricher(FprHandle fprHandle) { + this(fprHandle, SourceDecoders.defaults(), null); + } - public SourceCodeEnricher(FprHandle fprHandle){ + public SourceCodeEnricher(FprHandle fprHandle, ISourceDecoder sourceDecoder, FVDLMetadata fvdlMetadata) { this.fprHandle = fprHandle; - this.fileUtils = new FileUtils(); + // Single soft-fail decode path via FileUtils (same policy as snippets/lines). + this.fileUtils = new FileUtils(Objects.requireNonNull(sourceDecoder, "sourceDecoder"), fvdlMetadata); } /** @@ -122,34 +115,19 @@ private void processFileForElement(StackTraceElement element, Map String filename = element.getFilename(); if (!StringUtil.isEmpty(filename) && fprHandle.getSourceFileMap().containsKey(filename) && !uniqueFiles.containsKey(filename)) { - String internalPath = fprHandle.getSourceFileMap().get(filename); - if (internalPath == null) { return; } // Should not happen due to containsKey check, but safe. - - Path actualSourcePath = fprHandle.getPath("/" + internalPath); - + // Soft-fail decode via FileUtils: omit file rather than fail the whole issue. + Optional contentOpt = fileUtils.getSourceFileContent(fprHandle, filename); + if (contentOpt.isEmpty()) { + return; + } + String content = contentOpt.get(); File file = new File(); file.setName(filename); file.setSegment(false); file.setStartLine(1); - - try { - if (Files.exists(actualSourcePath)) { - byte[] encodedBytes = Files.readAllBytes(actualSourcePath); - String content = new String(encodedBytes); - // Keep line markers in prompt file content; downstream gRPC/template rendering is pass-through. - file.setContent(fileUtils.appendLineNumbers(content, filename, 0)); - file.setEndLine(fileUtils.countLines(actualSourcePath)); - } else { - // This warning is now more accurate. - logger.warn("Source file not found at internal path: {}. This may indicate a corrupt FPR.", actualSourcePath); - file.setContent(""); - file.setEndLine(0); - } - } catch (IOException e) { - logger.warn("Error processing file: {}", filename, e); - file.setContent(""); - file.setEndLine(0); - } + // Keep line markers in prompt file content; downstream gRPC/template rendering is pass-through. + file.setContent(fileUtils.appendLineNumbers(content, filename, 0)); + file.setEndLine(content.split("\\R", -1).length); uniqueFiles.put(filename, file); } } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/SourceDecoders.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/SourceDecoders.java new file mode 100644 index 00000000000..7c047085fe0 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/SourceDecoders.java @@ -0,0 +1,82 @@ +/* + * 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.fpr.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Domain factory for {@link ISourceDecoder} instances. Single source of truth for + * token/CSV parsing and default candidate order. CLI converters should delegate here. + */ +public final class SourceDecoders { + public static final String DEFAULT_SOURCE_ENCODINGS = "FPR,UTF-8,CP850"; + + private SourceDecoders() {} + + /** + * Map a single token to a decoder: {@code FPR} or a charset name. + * + * @throws IllegalArgumentException if token is blank + * @throws java.nio.charset.IllegalCharsetNameException if charset name is illegal + * @throws java.nio.charset.UnsupportedCharsetException if charset is unsupported + */ + public static ISourceDecoder fromToken(String token) { + if (token == null || token.isBlank()) { + throw new IllegalArgumentException("Source encoding must not be blank"); + } + String trimmed = token.trim(); + if (FprSourceDecoder.TOKEN.equalsIgnoreCase(trimmed)) { + return new FprSourceDecoder(); + } + return new CharsetSourceDecoder(trimmed); + } + + /** + * Parse a comma-separated list of encoding tokens into a single decoder + * (composite when more than one candidate). + */ + public static ISourceDecoder fromCsv(String csv) { + String effective = csv == null || csv.isBlank() ? DEFAULT_SOURCE_ENCODINGS : csv; + List decoders = new ArrayList<>(); + for (String part : effective.split(",")) { + String trimmed = part.trim(); + if (!trimmed.isEmpty()) { + decoders.add(fromToken(trimmed)); + } + } + if (decoders.isEmpty()) { + return defaults(); + } + return of(decoders); + } + + /** + * Compose an ordered candidate list into one decoder. + */ + public static ISourceDecoder of(List decoders) { + Objects.requireNonNull(decoders, "decoders must not be null"); + if (decoders.isEmpty()) { + return defaults(); + } + if (decoders.size() == 1) { + return decoders.get(0); + } + return new CompositeSourceDecoder(decoders); + } + + public static ISourceDecoder defaults() { + return fromCsv(DEFAULT_SOURCE_ENCODINGS); + } +} diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/SourceEncoder.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/SourceEncoder.java new file mode 100644 index 00000000000..93668218dd1 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/SourceEncoder.java @@ -0,0 +1,64 @@ +/* + * 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.fpr.utils; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; + +import com.fortify.cli.aviator._common.exception.AviatorSimpleException; + +/** + * Strict source encode/decode helpers ({@link CodingErrorAction#REPORT}). + * Public {@link #encode} is for write-back; package-private decode is shared by decoders. + */ +public final class SourceEncoder { + private SourceEncoder() {} + + public static byte[] encode(String content, Charset charset, String filename) { + try { + return encodeStrict(content, charset); + } catch (CharacterCodingException e) { + throw new SourceEncodeException( + "Source file '" + filename + "' cannot be encoded using " + charset.name(), e); + } + } + + static String decodeStrict(byte[] bytes, Charset charset) throws CharacterCodingException { + return charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + .decode(ByteBuffer.wrap(bytes)) + .toString(); + } + + private static byte[] encodeStrict(String content, Charset charset) throws CharacterCodingException { + ByteBuffer buffer = charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + .encode(CharBuffer.wrap(content)); + byte[] result = new byte[buffer.remaining()]; + buffer.get(result); + return result; + } + + public static class SourceEncodeException extends AviatorSimpleException { + private static final long serialVersionUID = 1L; + + public SourceEncodeException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java index 874e192e1f2..e3766515ecd 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java @@ -52,6 +52,8 @@ import com.fortify.cli.aviator.audit.model.AuditResponse; import com.fortify.cli.aviator.audit.model.UserPrompt; import com.fortify.cli.aviator.config.IAviatorLogger; +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; +import com.fortify.cli.aviator.fpr.utils.ISourceDecoder; import com.fortify.cli.aviator.util.Constants; import com.fortify.cli.aviator.util.FprHandle; import com.fortify.grpc.token.DeleteTokenRequest; @@ -123,8 +125,11 @@ public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAv this(channel, defaultTimeoutSeconds, logger, 30); } - public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, FprHandle fprHandle, List customPriorityOrder) { - AviatorStreamProcessor processor = new AviatorStreamProcessor(this, logger, asyncStub, processingExecutor, pingScheduler, pingIntervalSeconds, defaultTimeoutSeconds, fprHandle); + public CompletableFuture> processBatchRequests(Queue requests, String projectName, + String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, FprHandle fprHandle, + List customPriorityOrder, ISourceDecoder sourceDecoder, FVDLMetadata fvdlMetadata) { + AviatorStreamProcessor processor = new AviatorStreamProcessor(this, logger, asyncStub, processingExecutor, pingScheduler, + pingIntervalSeconds, defaultTimeoutSeconds, fprHandle, sourceDecoder, fvdlMetadata); CompletableFuture> future = processor.processBatchRequests(requests, projectName, FPRBuildId, SSCApplicationName, SSCApplicationVersion, token, customPriorityOrder); future.whenComplete((res, th) -> processor.close()); return future.exceptionally(ex -> { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java index 89d376801f5..31dcef6ad58 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java @@ -17,6 +17,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Queue; import java.util.Set; import java.util.UUID; @@ -48,6 +49,8 @@ import com.fortify.cli.aviator.audit.model.AuditResponse; import com.fortify.cli.aviator.audit.model.UserPrompt; import com.fortify.cli.aviator.config.IAviatorLogger; +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; +import com.fortify.cli.aviator.fpr.utils.ISourceDecoder; import com.fortify.cli.aviator.fpr.utils.SourceCodeEnricher; import com.fortify.cli.aviator.util.Constants; import com.fortify.cli.aviator.util.FprHandle; @@ -92,8 +95,13 @@ class AviatorStreamProcessor implements AutoCloseable { private volatile Future processingTask; private final Object retryLock = new Object(); private final FprHandle fprHandle; + private final ISourceDecoder sourceDecoder; + private final FVDLMetadata fvdlMetadata; - public AviatorStreamProcessor(AviatorGrpcClient client, IAviatorLogger logger, AuditorServiceGrpc.AuditorServiceStub asyncStub, ExecutorService processingExecutor, ScheduledExecutorService pingScheduler, long pingIntervalSeconds, long defaultTimeoutSeconds, FprHandle fprHandle) { + public AviatorStreamProcessor(AviatorGrpcClient client, IAviatorLogger logger, AuditorServiceGrpc.AuditorServiceStub asyncStub, + ExecutorService processingExecutor, ScheduledExecutorService pingScheduler, long pingIntervalSeconds, + long defaultTimeoutSeconds, FprHandle fprHandle, ISourceDecoder sourceDecoder, + FVDLMetadata fvdlMetadata) { this.client = client; this.logger = logger; this.asyncStub = asyncStub; @@ -102,6 +110,8 @@ public AviatorStreamProcessor(AviatorGrpcClient client, IAviatorLogger logger, A this.pingIntervalSeconds = pingIntervalSeconds; this.defaultTimeoutSeconds = defaultTimeoutSeconds; this.fprHandle = fprHandle; + this.sourceDecoder = Objects.requireNonNull(sourceDecoder, "sourceDecoder"); + this.fvdlMetadata = fvdlMetadata; } public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, List customPriorityOrder) { @@ -708,7 +718,7 @@ private void processRequestQueue(int totalRequests, AtomicInteger processedReque String instanceId = wrapper.userPrompt.getIssueData().getInstanceID(); // Lazy Loading of source code files for individual issue - SourceCodeEnricher sourceCodeEnricher = new SourceCodeEnricher(fprHandle); + SourceCodeEnricher sourceCodeEnricher = new SourceCodeEnricher(fprHandle, sourceDecoder, fvdlMetadata); Map enrichedFiles = sourceCodeEnricher.enrichWithSourceCode(wrapper.userPrompt.getStackTrace()); diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/cli/converter/SourceDecoderConverterTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/cli/converter/SourceDecoderConverterTest.java new file mode 100644 index 00000000000..0ad64fab89d --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/cli/converter/SourceDecoderConverterTest.java @@ -0,0 +1,41 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import picocli.CommandLine.TypeConversionException; + +/** + * Only the CLI adapter layer: domain factory exceptions become {@link TypeConversionException}. + * Happy-path token semantics are covered by {@code SourceDecodersTest}. + */ +class SourceDecoderConverterTest { + + private final SourceDecoderConverter converter = new SourceDecoderConverter(); + + @Test + void convert_delegatesHappyPathToFactory() { + assertEquals("FPR", converter.convert("FPR").describe()); + assertEquals("UTF-8", converter.convert("UTF-8").describe()); + } + + @Test + void convert_mapsFactoryFailuresToTypeConversionException() { + assertThrows(TypeConversionException.class, () -> converter.convert(" ")); + assertThrows(TypeConversionException.class, () -> converter.convert("NOT-A-REAL-CHARSET")); + } +} diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/utils/SourceDecodersTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/utils/SourceDecodersTest.java new file mode 100644 index 00000000000..fea7f98fed1 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/utils/SourceDecodersTest.java @@ -0,0 +1,102 @@ +/* + * 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.fpr.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; + +import org.junit.jupiter.api.Test; + +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; +import com.fortify.cli.aviator.fpr.utils.ISourceDecoder.DecodeResult; +import com.fortify.cli.aviator.fpr.utils.ISourceDecoder.SourceDecodeException; +import com.fortify.cli.aviator.fpr.utils.SourceEncoder.SourceEncodeException; + +/** + * Behavior tests for source encoding factory + decode/encode. + * Prefer real byte decode outcomes over describe()-only checks. + */ +class SourceDecodersTest { + + @Test + void fromToken_rejectsBlankAndUnknownCharset() { + assertThrows(IllegalArgumentException.class, () -> SourceDecoders.fromToken(" ")); + assertThrows(UnsupportedCharsetException.class, () -> SourceDecoders.fromToken("NOT-A-CHARSET")); + } + + @Test + void defaults_matchDocumentedCandidateOrder() { + assertEquals("FPR,UTF-8,CP850", SourceDecoders.DEFAULT_SOURCE_ENCODINGS); + assertEquals(SourceDecoders.DEFAULT_SOURCE_ENCODINGS, SourceDecoders.defaults().describe()); + } + + @Test + void decode_fallsThroughFprWhenMetadataMissing() { + byte[] utf8 = "hello".getBytes(StandardCharsets.UTF_8); + DecodeResult result = SourceDecoders.defaults().decode(utf8, "Main.java", null); + assertEquals("hello", result.content()); + assertEquals(StandardCharsets.UTF_8, result.charset()); + assertEquals("UTF-8", result.source()); + } + + @Test + void decode_usesFprEncodingWhenMetadataPresent() { + FVDLMetadata metadata = new FVDLMetadata(); + metadata.registerSourceFileEncoding("src/Main.java", "ISO-8859-1"); + byte[] latin1 = "caf\u00e9".getBytes(StandardCharsets.ISO_8859_1); + + DecodeResult result = SourceDecoders.defaults().decode(latin1, "src/Main.java", metadata); + + assertEquals("caf\u00e9", result.content()); + assertEquals(StandardCharsets.ISO_8859_1, result.charset()); + assertTrue(result.source().startsWith("FPR("), result.source()); + } + + @Test + void decode_allCandidatesFail_messageListsAttempts() { + ISourceDecoder decoder = SourceDecoders.fromCsv("UTF-8,US-ASCII"); + byte[] invalid = new byte[] {(byte) 0xFF, (byte) 0xFE, (byte) 0x00}; + + SourceDecodeException ex = assertThrows(SourceDecodeException.class, + () -> decoder.decode(invalid, "bad.bin", null)); + + assertTrue(ex.getMessage().contains("bad.bin"), ex.getMessage()); + assertTrue(ex.getMessage().contains("UTF-8"), ex.getMessage()); + assertTrue(ex.getMessage().contains("US-ASCII"), ex.getMessage()); + } + + @Test + void encode_roundTripAndRejectsUnmappable() { + String content = "secure code"; + byte[] encoded = SourceEncoder.encode(content, StandardCharsets.UTF_8, "a.java"); + assertEquals(content, SourceDecoders.fromToken("UTF-8").decode(encoded, "a.java", null).content()); + + assertThrows(SourceEncodeException.class, + () -> SourceEncoder.encode("caf\u00e9", StandardCharsets.US_ASCII, "a.java")); + } + + @Test + void fromToken_fprIsCaseInsensitive() { + assertEquals("FPR", SourceDecoders.fromToken("fpr").describe()); + } + + @Test + void fromCsv_compositesMultipleCandidates() { + assertEquals("UTF-8,CP850", SourceDecoders.fromCsv("UTF-8,CP850").describe()); + assertEquals("UTF-8", SourceDecoders.fromCsv("UTF-8").describe()); + } +} diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCApplyRemediationsCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCApplyRemediationsCommand.java index 30fe3af0ae2..c6bff9c596e 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCApplyRemediationsCommand.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCApplyRemediationsCommand.java @@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.aviator._common.cli.mixin.SourceEncodingsMixin; import com.fortify.cli.aviator._common.exception.AviatorSimpleException; import com.fortify.cli.aviator.applyRemediation.ApplyAutoRemediationOnSource; import com.fortify.cli.aviator.config.AviatorLoggerImpl; @@ -63,6 +64,7 @@ public class AviatorSSCApplyRemediationsCommand extends AbstractSSCJsonNodeOutpu private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCApplyRemediationsCommand.class); @Option(names = {"--source-dir"}, descriptionKey = "fcli.aviator.ssc.apply-remediations.source-dir") private String sourceCodeDirectory = System.getProperty("user.dir"); + @Mixin private SourceEncodingsMixin sourceEncodingsMixin; @Override @SneakyThrows @@ -128,7 +130,8 @@ JsonNode processAllAviatorArtifacts(OffsetDateTime sinceDate) { try { fprPath = downloadArtifactFpr(ad); try (FprHandle fprHandle = new FprHandle(fprPath)) { - var metric = ApplyAutoRemediationOnSource.applyRemediations(fprHandle, sourceCodeDirectory, logger); + var metric = ApplyAutoRemediationOnSource.applyRemediations(fprHandle, sourceCodeDirectory, + sourceEncodingsMixin.getSourceDecoder(), logger); totalRemediations += metric.totalRemediations(); appliedRemediations += metric.appliedRemediations(); skippedRemediations += metric.skippedRemediations(); @@ -181,7 +184,8 @@ JsonNode processFprRemediations(SSCArtifactDescriptor ad) { try { logger.progress("Status: Processing FPR with Aviator for Applying Auto Remediations"); try (FprHandle fprHandle = new FprHandle(fprPath)) { - var remediationMetric = ApplyAutoRemediationOnSource.applyRemediations(fprHandle, sourceCodeDirectory, logger); + var remediationMetric = ApplyAutoRemediationOnSource.applyRemediations(fprHandle, sourceCodeDirectory, + sourceEncodingsMixin.getSourceDecoder(), logger); String status = remediationMetric.appliedRemediations() > 0 ? "Remediation-Applied" : "No-Remediation-Applied"; diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java index 2a4e73281df..5a0c876ef9e 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.aviator._common.cli.mixin.SourceEncodingsMixin; import com.fortify.cli.aviator._common.config.AviatorConfigManager; import com.fortify.cli.aviator._common.session.user.cli.mixin.AviatorUserSessionDescriptorSupplier; import com.fortify.cli.aviator._common.session.user.helper.AviatorUserSessionDescriptor; @@ -79,6 +80,7 @@ public class AviatorSSCAuditCommand extends AbstractSSCJsonNodeOutputCommand imp @ArgGroup(exclusive = true, multiplicity = "0..1") private QuotaHandlingArgGroup quotaHandlingArgGroup = new QuotaHandlingArgGroup(); @Option(names = {"--test-exceeding-quota"}) private boolean testExceedingQuota; @Option(names = {"--default-quota-fallback"}) private boolean defaultQuotaFallback; + @Mixin private SourceEncodingsMixin sourceEncodingsMixin; private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCAuditCommand.class); private Long checkedQuotaBefore; @@ -275,6 +277,7 @@ private JsonNode processFpr(UnirestInstance unirest, SSCAppVersionDescriptor av, .noFilterSet(isNoFilterSet()) .folderNames(folderNames) .folderPriorityOrder(getFolderPriorityOrder()) + .sourceDecoder(sourceEncodingsMixin.getSourceDecoder()) .build()); } catch (Exception e) { LOG.error("FPR audit failed for {}:{}: {}", av.getApplicationName(), av.getVersionName(), e.getMessage(), e); diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java index 542ee85bb86..2596476ec1d 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java @@ -137,10 +137,32 @@ public static void setAuditStats(ObjectNode result, FPRAuditResult auditResult) audit.put("message", message); audit.put("submitted", auditResult.getTotalIssuesToAudit()); audit.put("succeeded", auditResult.getIssuesSuccessfullyAudited()); - audit.put("skipped", Math.max(0, auditResult.getTotalIssuesToAudit() - auditResult.getIssuesSuccessfullyAudited())); + audit.put("skipped", auditResult.getIssuesSkipped()); + audit.put("skippedReasons", formatSkippedReasons(auditResult.getSkippedByReason())); + audit.set("skippedByReason", toObjectNode(auditResult.getSkippedByReason())); + audit.put("remediationGenerationSkipped", auditResult.getRemediationGenerationSkipped()); + audit.put("remediationGenerationSkippedReasons", formatSkippedReasons(auditResult.getRemediationGenerationSkippedByReason())); + audit.set("remediationGenerationSkippedByReason", toObjectNode(auditResult.getRemediationGenerationSkippedByReason())); ((ObjectNode) result.get("operation")).set("audit", audit); } + private static ObjectNode toObjectNode(Map skippedByReason) { + ObjectNode object = JsonHelper.getObjectMapper().createObjectNode(); + if (skippedByReason != null) { + skippedByReason.forEach(object::put); + } + return object; + } + + private static String formatSkippedReasons(Map skippedByReason) { + if (skippedByReason == null || skippedByReason.isEmpty()) { + return ""; + } + List parts = new ArrayList<>(); + skippedByReason.forEach((reason, count) -> parts.add(reason + "=" + count)); + return String.join(", ", parts); + } + /** * Sets only the {@code operation.audit.message} field without audit stats. * Used for code paths that don't perform an actual audit (SKIPPED, FAILED, QUOTA_EXCEEDED, etc.). diff --git a/fcli-core/fcli-aviator/src/main/resources/com/fortify/cli/aviator/i18n/AviatorMessages.properties b/fcli-core/fcli-aviator/src/main/resources/com/fortify/cli/aviator/i18n/AviatorMessages.properties index da3da97cfb2..7a2c64f68e3 100644 --- a/fcli-core/fcli-aviator/src/main/resources/com/fortify/cli/aviator/i18n/AviatorMessages.properties +++ b/fcli-core/fcli-aviator/src/main/resources/com/fortify/cli/aviator/i18n/AviatorMessages.properties @@ -129,6 +129,7 @@ fcli.aviator.ssc.audit.skip-if-exceeding-quota = Skip audit if the number of ope fcli.aviator.ssc.audit.test-exceeding-quota = Check whether the number of open issues exceeds the available Fortify Remediation Aviator quota and report the result without performing an audit. fcli.aviator.ssc.audit.default-quota-fallback = (Internal) When the Fortify Aviator application does not exist, use the tenant default quota instead of reporting app not found. Used by bulk audit. fcli.aviator.ssc.audit.folder-priority-order = Custom priority order for folder-based filtering when quota is exceeded (comma-separated, highest priority first). Example: Critical,High,Medium,Low. If not specified, uses default priority order. +fcli.aviator.source-encodings = Comma-separated source encoding candidates to try in order when decoding source files. Use FPR to try the source encoding recorded in audit.fvdl. When writing remediated files, the accepted encoding is used. Default value: ${DEFAULT-VALUE}. fcli.aviator.ssc.audit.refresh = By default, this command will refresh the source application version's metrics when copying from it. \ Note that for large applications this can lead to an error if the timeout expires. fcli.aviator.ssc.audit.refresh-timeout = Time-out, for example 30s (30 seconds), 5m (5 minutes), 1h (1 hour). Default value: ${DEFAULT-VALUE} @@ -204,7 +205,7 @@ fcli.env.default.prefix=FCLI_DEFAULT # Table output columns configuration fcli.aviator.session.output.table.args = name,type,url,created,expires,expired fcli.aviator.admin-config.output.table.args = name,type,url,created -fcli.aviator.ssc.audit.output.table.args = id,application.name,name,artifactId,action +fcli.aviator.ssc.audit.output.table.args = id,application.name,name,artifactId,action,operation.audit.skippedReasons fcli.aviator.app.create.output.table.args = id,name,entitlement_id,disclaimer,quota_last_updated,quota fcli.aviator.app.add-entitlement.output.table.args = id,name,entitlement_id,entitlement_multiplier,entitlements_consumed,quota_last_updated,quota fcli.aviator.app.delete.output.table.args = message diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/aviator/cmd/FoDAviatorApplyRemediationsCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/aviator/cmd/FoDAviatorApplyRemediationsCommand.java index f792edbef1e..471232a1e03 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/aviator/cmd/FoDAviatorApplyRemediationsCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/aviator/cmd/FoDAviatorApplyRemediationsCommand.java @@ -20,6 +20,7 @@ import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.aviator._common.cli.mixin.SourceEncodingsMixin; import com.fortify.cli.aviator.applyRemediation.ApplyAutoRemediationOnSource; import com.fortify.cli.aviator.config.AviatorLoggerImpl; import com.fortify.cli.aviator.util.FprHandle; @@ -56,6 +57,7 @@ public class FoDAviatorApplyRemediationsCommand extends AbstractFoDJsonNodeOutpu @Mixin private FoDReleaseByQualifiedNameOrIdResolverMixin.RequiredOption releaseResolver; private static final Logger LOG = LoggerFactory.getLogger(FoDAviatorApplyRemediationsCommand.class); @Option(names = {"--source-dir"}) private String sourceCodeDirectory = System.getProperty("user.dir"); + @Mixin private SourceEncodingsMixin sourceEncodingsMixin; @Override @SneakyThrows public JsonNode getJsonNode(UnirestInstance unirest) { @@ -82,7 +84,8 @@ private JsonNode processFprRemediations(UnirestInstance unirest, FoDReleaseDescr logger.progress("Status: Processing FPR with Aviator for Applying Auto Remediations"); try (FprHandle fprHandle = new FprHandle(downloadedFprPath)) { - var remediationMetric = ApplyAutoRemediationOnSource.applyRemediations(fprHandle, sourceCodeDirectory, logger); + var remediationMetric = ApplyAutoRemediationOnSource.applyRemediations(fprHandle, sourceCodeDirectory, + sourceEncodingsMixin.getSourceDecoder(), logger); LOG.info("Applied remediation {}", remediationMetric.appliedRemediations()); LOG.info("Total remediation {}", remediationMetric.totalRemediations()); String status = remediationMetric.appliedRemediations() > 0 ? "Remediation-Applied" : "No-Remediation-Applied"; diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties index 62dcf5b6c6d..0fcff19749e 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties @@ -1038,6 +1038,7 @@ fcli.fod.attribute.update.values = List of picklist values (only for Picklist da fcli.fod.aviator.usage.header = Use Fortify Remediation Aviator with FoD. fcli.fod.aviator.apply-remediations.usage.header = Apply Fortify Remediation Aviator auto-remediations to source code. fcli.fod.aviator.apply-remediations.source-dir = Directory containing source code to apply remediations to. Default value: ${DEFAULT-VALUE}. +fcli.aviator.source-encodings = Comma-separated source encoding candidates to try in order when decoding source files. Use FPR to try the source encoding recorded in audit.fvdl. When writing remediated files, the accepted encoding is used. Default value: ${DEFAULT-VALUE}. # various messages displayed during execution