diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/util/AviatorRemediationMetricsHelper.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/util/AviatorRemediationMetricsHelper.java index e59326bd2e..3d2090d276 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/util/AviatorRemediationMetricsHelper.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/util/AviatorRemediationMetricsHelper.java @@ -137,11 +137,12 @@ public static void putRemediationMetricFields(ObjectNode result, RemediationMetr int total = metric == null ? 0 : metric.totalRemediations(); int applied = metric == null ? 0 : metric.appliedRemediations(); int skipped = metric == null ? 0 : metric.skippedRemediations(); + String appliedFieldName = metric instanceof RemediationMetric.Preview ? "availableRemediation" : "appliedRemediation"; Map skippedByReason = metric == null ? Map.of() : metric.skippedByReason(); Set modifiedFiles = metric == null ? Set.of() : metric.modifiedFiles(); result.put("totalRemediation", total); - result.put("appliedRemediation", applied); + result.put(appliedFieldName, applied); result.put("skippedRemediation", skipped); result.put("skippedReasons", formatSkippedReasons(skippedByReason)); result.set("skippedByReason", toObjectNode(skippedByReason)); 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 79f54eae7e..a101d53c24 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 @@ -384,6 +384,7 @@ private static final class ProcessingState { private final boolean previewMode; private final Map> changesByIssue = new LinkedHashMap<>(); private final Map skipReasonsByIssue = new LinkedHashMap<>(); + private final Map descriptionsByIssue = new LinkedHashMap<>(); private int xmlEntryCount; private int appliedRemediations; @@ -441,6 +442,12 @@ private void addChangeDetails(String instanceId, String filename, String encodin } } + private void recordDescription(String instanceId, String description) { + if (previewMode && instanceId != null && !instanceId.isBlank() && description != null) { + descriptionsByIssue.put(instanceId, description); + } + } + private List buildPreviewDetails() { List details = new ArrayList<>(); @@ -461,7 +468,8 @@ private List buildPreviewDetails() { FilePreview filePreview = new FilePreview(metadata.path, metadata.encoding, fileChanges); fileMap.put(filename, filePreview); } - details.add(PreviewDetail.available(issueId, fileMap)); + String description = descriptionsByIssue.get(issueId); + details.add(PreviewDetail.available(issueId, description, fileMap)); } // Add skipped remediations @@ -470,7 +478,8 @@ private List buildPreviewDetails() { String skipReason = entry.getValue(); // Only add if not already in successful list if (!changesByIssue.containsKey(issueId)) { - details.add(PreviewDetail.skipped(issueId, skipReason)); + String description = descriptionsByIssue.get(issueId); + details.add(PreviewDetail.skipped(issueId, description, skipReason)); } } @@ -517,6 +526,13 @@ private void validateRequestedIssueIds() { private boolean processRemediation( Element remediation, Path sourceBasePath, FvdlMetadataResult fvdlMetadataResult, ProcessingState state) { String instanceId = remediation.getAttribute("instanceId"); + + if (previewMode) { + NodeList nodes = remediation.getElementsByTagNameNS(NAMESPACE_URI, "AuditComment"); + String description = nodes.getLength() > 0 ? ((Element) nodes.item(0)).getTextContent() : null; + state.recordDescription(instanceId, description); + } + try { Map pendingWrites = prepareFileChanges(remediation, sourceBasePath, fvdlMetadataResult); if (pendingWrites.isEmpty()) { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/preview/PreviewDetail.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/preview/PreviewDetail.java index 889f791753..03fed9c74d 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/preview/PreviewDetail.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/preview/PreviewDetail.java @@ -26,14 +26,16 @@ * * @param issueId The issue/remediation ID from the remediations.xml file * @param status Either "available" (successfully processed) or "skipped" (processing failed) + * @param description A detailed explanation of the issue and the suggested remediation * @param files Map of filename to FilePreview objects containing change details * @param skipReason Human-readable reason why remediation was skipped (null if status is "available") */ @Reflectable -@JsonPropertyOrder({"issueId", "status", "files", "available", "skipped", "skipReason"}) +@JsonPropertyOrder({"issueId", "status", "description", "files", "available", "skipped", "skipReason"}) public record PreviewDetail( String issueId, String status, + String description, Map files, String skipReason) { @@ -47,12 +49,12 @@ public record PreviewDetail( files = files == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(files)); } - public static PreviewDetail available(String issueId, Map files) { - return new PreviewDetail(issueId, "available", files, null); + public static PreviewDetail available(String issueId, String description, Map files) { + return new PreviewDetail(issueId, "available", description, files, null); } - public static PreviewDetail skipped(String issueId, String skipReason) { - return new PreviewDetail(issueId, "skipped", Map.of(), skipReason); + public static PreviewDetail skipped(String issueId, String description, String skipReason) { + return new PreviewDetail(issueId, "skipped", description, Map.of(), skipReason); } public boolean isAvailable() { diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/util/AviatorRemediationMetricsHelperTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/util/AviatorRemediationMetricsHelperTest.java index 2bc938f58d..49c4b3fcb1 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/util/AviatorRemediationMetricsHelperTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/_common/util/AviatorRemediationMetricsHelperTest.java @@ -85,7 +85,7 @@ void remainingIssueIdsDropsAlreadyApplied() { void aggregatingAnyPreviewMetricYieldsPreviewResultWithMergedDetails() { RemediationMetric applied = RemediationMetric.unfiltered(1, 1, Set.of("A.java")); RemediationMetric preview = RemediationMetric.previewUnfiltered(1, 0, Set.of(), Map.of(), - List.of(com.fortify.cli.aviator.fpr.processor.preview.PreviewDetail.skipped("ISSUE-2", "Source file missing"))); + List.of(com.fortify.cli.aviator.fpr.processor.preview.PreviewDetail.skipped("ISSUE-2", null, "Source file missing"))); RemediationMetric aggregated = AviatorRemediationMetricsHelper.aggregateMetrics( null, List.of(applied, preview)); diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/preview/PreviewDetailTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/preview/PreviewDetailTest.java index b40841a8e8..85eb67a85c 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/preview/PreviewDetailTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/preview/PreviewDetailTest.java @@ -32,11 +32,12 @@ class PreviewDetailTest { void availablePreviewDetailCreatedCorrectly() { Map files = Map.of("Example.java", new FilePreview("/path/to/Example.java", "UTF-8", java.util.List.of())); - PreviewDetail detail = PreviewDetail.available("ISSUE-123", files); + PreviewDetail detail = PreviewDetail.available("ISSUE-123", "Remediation rationale", files); assertNotNull(detail); assertEquals("ISSUE-123", detail.issueId()); assertEquals("available", detail.status()); + assertEquals("Remediation rationale", detail.description()); assertEquals(1, detail.files().size()); assertEquals(null, detail.skipReason()); assertTrue(detail.isAvailable()); @@ -44,7 +45,7 @@ void availablePreviewDetailCreatedCorrectly() { @Test void skippedPreviewDetailCreatedCorrectly() { - PreviewDetail detail = PreviewDetail.skipped("ISSUE-456", "Source file missing"); + PreviewDetail detail = PreviewDetail.skipped("ISSUE-456", null, "Source file missing"); assertNotNull(detail); assertEquals("ISSUE-456", detail.issueId()); @@ -57,30 +58,30 @@ void skippedPreviewDetailCreatedCorrectly() { @Test void nullIssueIdThrowsException() { assertThrows(AviatorBugException.class, - () -> new PreviewDetail(null, "available", Map.of(), null)); + () -> new PreviewDetail(null, "available", null, Map.of(), null)); } @Test void blankIssueIdThrowsException() { assertThrows(AviatorBugException.class, - () -> new PreviewDetail("", "available", Map.of(), null)); + () -> new PreviewDetail("", "available", null, Map.of(), null)); } @Test void nullStatusThrowsException() { assertThrows(AviatorBugException.class, - () -> new PreviewDetail("ISSUE-1", null, Map.of(), null)); + () -> new PreviewDetail("ISSUE-1", null, null, Map.of(), null)); } @Test void blankStatusThrowsException() { assertThrows(AviatorBugException.class, - () -> new PreviewDetail("ISSUE-1", " ", Map.of(), null)); + () -> new PreviewDetail("ISSUE-1", " ", null, Map.of(), null)); } @Test void nullFilesMapIsConvertedToEmptyMap() { - PreviewDetail detail = new PreviewDetail("ISSUE-1", "available", null, null); + PreviewDetail detail = new PreviewDetail("ISSUE-1", "available", null, null, null); assertNotNull(detail.files()); assertEquals(0, detail.files().size()); } @@ -89,7 +90,7 @@ void nullFilesMapIsConvertedToEmptyMap() { void filesMapIsUnmodifiable() { Map files = new java.util.LinkedHashMap<>(); files.put("Test.java", new FilePreview("/path", "UTF-8", java.util.List.of())); - PreviewDetail detail = new PreviewDetail("ISSUE-1", "available", files, null); + PreviewDetail detail = new PreviewDetail("ISSUE-1", "available", null, files, null); assertThrows(UnsupportedOperationException.class, () -> detail.files().put("Another.java", new FilePreview("/path2", "UTF-8", java.util.List.of())));