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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ public String updateIssueElement(Element issueElement, AuditResponse response, T
return commentTimestamp;
}


private void updateClientAuditTrail(Element issueElement, AuditResponse response, TagMappingConfig tagMappingConfig, Boolean suppressedHistoryValue) {
Element clientAuditTrail = getClientAuditTrailElement(issueElement);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,4 +384,4 @@ private int parseIntContent(String content) {
return 0;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
Expand All @@ -46,7 +48,7 @@ public class RemediationProcessor {

private final FprHandle fprHandle;
private final String sourceCodeDirectory;
public record RemediationMetric(int totalRemediations, int appliedRemediations, int skippedRemediations){}
public record RemediationMetric(int totalRemediations, int appliedRemediations, int skippedRemediations, Set<String> modifiedFiles){}

public RemediationProcessor(FprHandle fprHandle, String sourceCodeDirectory) {
this.fprHandle = fprHandle;
Expand All @@ -58,10 +60,11 @@ public RemediationMetric processRemediationXML() {
Document remediationDoc;
int totalRemediations;
int appliedRemediations;
Set<String> modifiedFiles = new LinkedHashSet<>();

// Sanitize and normalize the base source directory path once.
String trimmedSourceDir = sourceCodeDirectory.trim();
if (trimmedSourceDir.length() > 1 &&
if (trimmedSourceDir.length() > 1 &&
((trimmedSourceDir.startsWith("\"") && trimmedSourceDir.endsWith("\"")) ||
(trimmedSourceDir.startsWith("'") && trimmedSourceDir.endsWith("'")))) {
trimmedSourceDir = trimmedSourceDir.substring(1, trimmedSourceDir.length() - 1);
Expand Down Expand Up @@ -158,7 +161,8 @@ public RemediationMetric processRemediationXML() {
updatedLines.addAll(newCodeLines);
updatedLines.addAll(originalLines.subList(lineTo, originalLines.size()));
Files.write(filePath, updatedLines);
logger.info("Remediation applied for {}", instanceId);
modifiedFiles.add(filename);
logger.info("Remediation applied for {} in file {}", instanceId, filename);
if(!remediationAppliedOnIssue) {
remediationAppliedOnIssue = true;
appliedRemediations++;
Expand All @@ -177,7 +181,7 @@ public RemediationMetric processRemediationXML() {
logger.error("Unexpected error processing remediation.xml: {}", remediationPath, e);
throw new AviatorTechnicalException("Unexpected error processing remediations.xml.", e);
}
return new RemediationMetric(totalRemediations, appliedRemediations, totalRemediations-appliedRemediations);
return new RemediationMetric(totalRemediations, appliedRemediations, totalRemediations-appliedRemediations, modifiedFiles);
}

private boolean isFilePresent(Path path) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,4 @@ public class Constants {
public static final int MAX_STREAM_RETRIES = 5;
public static final long STREAM_RETRY_BASE_DELAY_MS = 2000;
public static final long STREAM_RETRY_MAX_DELAY_MS = 30000;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -51,7 +53,7 @@

@Command(name = "apply-remediations")
public class AviatorSSCApplyRemediationsCommand extends AbstractSSCJsonNodeOutputCommand implements IRecordTransformer, IActionCommandResultSupplier {
@Getter @Mixin private OutputHelperMixins.TableNoQuery outputHelper;
@Getter @Mixin private OutputHelperMixins.DetailsNoQuery outputHelper;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this change from table to details output? Although users can always switch to a different output type, it might surprise users to suddenly see different output. Potentially it could also break automations that process the default output, although I assume automations would commonly request a specific output format like JSON anyway.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The customer requirement to include the list of modified files in the command output drove this change. Modified file paths can be numerous and lengthy, making them unsuitable for table columns — they would be truncated or break the table layout. DetailsNoQuery renders each field on its own line, which accommodates variable-length lists cleanly. This is also consistent with the audit command, which uses the same output format.

Regarding backward compatibility: since this is a new feature addition (modified files weren't previously in the output), existing automations that parse the default output would already need updates to handle the new fields. As you noted, automations typically request a specific format like --output json, which is unaffected by this change.

@Mixin private ProgressWriterFactoryMixin progressWriterFactoryMixin;
@Mixin private AviatorSSCApplyRemediationsArtifactSelectorMixin artifactSelector;

Expand Down Expand Up @@ -113,6 +115,7 @@ JsonNode processAllAviatorArtifacts(OffsetDateTime sinceDate) {

int totalRemediations = 0, appliedRemediations = 0, skippedRemediations = 0;
int artifactsProcessed = 0, artifactsSkipped = 0;
Set<String> allModifiedFiles = new LinkedHashSet<>();

for (SSCArtifactDescriptor ad : artifacts) {
int artifactIndex = artifactsProcessed + artifactsSkipped + 1;
Expand All @@ -125,6 +128,7 @@ JsonNode processAllAviatorArtifacts(OffsetDateTime sinceDate) {
totalRemediations += metric.totalRemediations();
appliedRemediations += metric.appliedRemediations();
skippedRemediations += metric.skippedRemediations();
allModifiedFiles.addAll(metric.modifiedFiles());
artifactsProcessed++;
}
} catch (AviatorSimpleException e) {
Expand All @@ -144,7 +148,7 @@ JsonNode processAllAviatorArtifacts(OffsetDateTime sinceDate) {
String action = appliedRemediations > 0 ? "Remediation-Applied" : "No-Remediation-Applied";
return AviatorSSCApplyRemediationsHelper.buildAggregatedResultNode(
appVersionId, artifactsProcessed, artifactsSkipped,
totalRemediations, appliedRemediations, skippedRemediations, action);
totalRemediations, appliedRemediations, skippedRemediations, allModifiedFiles, action);
}

@SneakyThrows
Expand All @@ -168,7 +172,7 @@ JsonNode processFprRemediations(SSCArtifactDescriptor ad) {
try (FprHandle fprHandle = new FprHandle(fprPath)) {
var remediationMetric = ApplyAutoRemediationOnSource.applyRemediations(fprHandle, sourceCodeDirectory, logger);
String status = remediationMetric.appliedRemediations() > 0 ? "Remediation-Applied" : "No-Remediation-Applied";
return AviatorSSCApplyRemediationsHelper.buildResultNode(ad, remediationMetric.totalRemediations(), remediationMetric.appliedRemediations(), remediationMetric.skippedRemediations(), status);
return AviatorSSCApplyRemediationsHelper.buildResultNode(ad, remediationMetric.totalRemediations(), remediationMetric.appliedRemediations(), remediationMetric.skippedRemediations(), remediationMetric.modifiedFiles(), status);
}
} finally {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,27 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

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

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
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;
import com.fortify.cli.aviator.audit.AuditFPR;
import com.fortify.cli.aviator.audit.model.AuditFprOptions;
import com.fortify.cli.aviator.audit.model.FPRAuditResult;
import com.fortify.cli.aviator.config.AviatorLoggerImpl;
import com.fortify.cli.aviator.config.TagMappingConfig;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCAuditHelper;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCTagValidator;
import com.fortify.cli.aviator.util.FprHandle;
import com.fortify.cli.aviator.util.ResourceUtil;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
import com.fortify.cli.common.progress.cli.mixin.ProgressWriterFactoryMixin;
Expand Down Expand Up @@ -262,6 +268,7 @@ private JsonNode processFpr(UnirestInstance unirest, SSCAppVersionDescriptor av,

String artifactId = null;
if (auditResult.getUpdatedFile() != null && !"SKIPPED".equals(action) && !"FAILED".equals(action)) {
validateSSCTagsBeforeUpload(unirest, av, logger);
try {
artifactId = uploadAuditedFprToSSC(unirest, auditResult.getUpdatedFile(), av);
} catch (Exception e) {
Expand All @@ -275,6 +282,46 @@ private JsonNode processFpr(UnirestInstance unirest, SSCAppVersionDescriptor av,
return result;
}

/**
* Validates that SSC has the required custom tags and Analysis tag values
* before uploading the audited FPR. Emits warnings for any missing tags or
* values so the user can take corrective action.
*/
private void validateSSCTagsBeforeUpload(UnirestInstance unirest, SSCAppVersionDescriptor av,
AviatorLoggerImpl logger) {
LOG.info("Starting SSC tag validation before FPR upload for app version id={}.", av.getVersionId());
TagMappingConfig tagMappingConfig = loadTagMappingForValidation();
LOG.debug("Tag mapping config loaded: tag_id='{}', mapping={}", tagMappingConfig.getTag_id(), tagMappingConfig.getMapping());
Set<String> analysisTagValues = extractAnalysisTagValues(tagMappingConfig);
LOG.info("Analysis tag values to validate: {}", analysisTagValues);
List<String> warnings = AviatorSSCTagValidator.validatePreUpload(
unirest, av.getVersionId(), tagMappingConfig.getTag_id(), analysisTagValues, logger);
LOG.info("Tag validation complete. {} warning(s) found.", warnings.size());
}

private TagMappingConfig loadTagMappingForValidation() {
if (tagMapping != null && !tagMapping.isBlank()) {
return ResourceUtil.loadYamlFile(new java.io.File(tagMapping), TagMappingConfig.class);
}
return AviatorConfigManager.getInstance().getDefaultTagMappingConfig();
}

private Set<String> extractAnalysisTagValues(TagMappingConfig config) {
Set<String> values = new LinkedHashSet<>();
if (config.getMapping() != null) {
addTierValues(values, config.getMapping().getTier_1());
addTierValues(values, config.getMapping().getTier_2());
}
return values;
}

private void addTierValues(Set<String> values, TagMappingConfig.Tier tier) {
if (tier == null) return;
if (tier.getFp() != null && tier.getFp().getValue() != null) values.add(tier.getFp().getValue());
if (tier.getTp() != null && tier.getTp().getValue() != null) values.add(tier.getTp().getValue());
if (tier.getUnsure() != null && tier.getUnsure().getValue() != null) values.add(tier.getUnsure().getValue());
}

private Path downloadFpr(UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorLoggerImpl logger) throws IOException {
logger.progress("Status: Downloading FPR from SSC for app version: %s:%s (id: %s)", av.getApplicationName(), av.getVersionName(), av.getVersionId());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
*/
package com.fortify.cli.aviator.ssc.helper;

import java.util.Set;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fortify.cli.common.json.JsonHelper;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
Expand All @@ -34,7 +37,7 @@ private AviatorSSCApplyRemediationsHelper() {}
* @param action Final action.
* @return An ObjectNode representing the result.
*/
public static ObjectNode buildResultNode(SSCArtifactDescriptor ad, int totalRemediation, int appliedRemediation, int skippedRemediation, String action) {
public static ObjectNode buildResultNode(SSCArtifactDescriptor ad, int totalRemediation, int appliedRemediation, int skippedRemediation, Set<String> modifiedFiles, String action) {
ObjectNode result = JsonHelper.getObjectMapper().createObjectNode();
result.put("appVersionId", ad.asObjectNode().path("projectVersionId").asText("N/A"));
result.put("artifactId", ad.getId());
Expand All @@ -43,6 +46,7 @@ public static ObjectNode buildResultNode(SSCArtifactDescriptor ad, int totalReme
result.put("totalRemediation", totalRemediation);
result.put("appliedRemediation", appliedRemediation);
result.put("skippedRemediation", skippedRemediation);
result.set("modifiedFiles", toArrayNode(modifiedFiles));
result.put(IActionCommandResultSupplier.actionFieldName, action);
return result;
}
Expand All @@ -60,7 +64,7 @@ public static ObjectNode buildResultNode(SSCArtifactDescriptor ad, int totalReme
* @return An ObjectNode representing the aggregated result.
*/
public static ObjectNode buildAggregatedResultNode(String appVersionId, int artifactsProcessed, int artifactsSkipped,
int totalRemediation, int appliedRemediation, int skippedRemediation, String action) {
int totalRemediation, int appliedRemediation, int skippedRemediation, Set<String> modifiedFiles, String action) {
ObjectNode result = JsonHelper.getObjectMapper().createObjectNode();
result.put("appVersionId", appVersionId);
result.put("artifactId", "N/A");
Expand All @@ -69,8 +73,16 @@ public static ObjectNode buildAggregatedResultNode(String appVersionId, int arti
result.put("totalRemediation", totalRemediation);
result.put("appliedRemediation", appliedRemediation);
result.put("skippedRemediation", skippedRemediation);
result.set("modifiedFiles", toArrayNode(modifiedFiles));
result.put(IActionCommandResultSupplier.actionFieldName, action);
return result;
}

private static ArrayNode toArrayNode(Set<String> files) {
ArrayNode array = JsonHelper.getObjectMapper().createArrayNode();
if (files != null) {
files.forEach(array::add);
}
return array;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,16 @@ public static void writeLastCorrelationTimestamp(UnirestInstance unirest, String
String timestamp = Instant.now().toString();
LOG.debug("Writing last_correlation timestamp '{}' to app version {}", timestamp, versionId);

new SSCAttributeUpdateBuilder(unirest)
.add(Map.of(AviatorSSCCorrelationAttributeDefs.LAST_CORRELATION_ATTR.name(), timestamp))
.buildRequest(versionId)
.asObject(JsonNode.class);
try {
new SSCAttributeUpdateBuilder(unirest)
.add(Map.of(AviatorSSCCorrelationAttributeDefs.LAST_CORRELATION_ATTR.name(), timestamp))
.buildRequest(versionId)
.asObject(JsonNode.class);

LOG.info("last_correlation timestamp '{}' written to app version {}", timestamp, versionId);
LOG.info("last_correlation timestamp '{}' written to app version {}", timestamp, versionId);
} catch (FcliSimpleException e) {
LOG.warn("WARN: Could not write last_correlation timestamp. Run 'fcli aviator ssc prepare' to create the attribute definition.");
}
}

// -------------------------------------------------------------------------
Expand Down
Loading
Loading