Skip to content
Merged
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 @@ -31,7 +31,6 @@
import com.fortify.cli.aviator.audit.model.FPRAuditResult;
import com.fortify.cli.aviator.audit.model.FilterSelection;
import com.fortify.cli.aviator.audit.model.ParsedFprData;
import com.fortify.cli.aviator.config.IAviatorLogger;
import com.fortify.cli.aviator.config.TagMappingConfig;
import com.fortify.cli.aviator.fpr.FPRProcessor;
import com.fortify.cli.aviator.fpr.Vulnerability;
Expand Down Expand Up @@ -72,10 +71,7 @@ public static FPRAuditResult auditFPR(AuditFprOptions options)

// --- STAGE 3: AUDITING ---
Map<String, AuditResponse> auditResponses = new ConcurrentHashMap<>();
AuditOutcome auditOutcome = performAviatorAudit(
parsedData, options.getLogger(), options.getToken(), options.getAppVersion(), options.getUrl(), options.getSscAppName(), options.getSscAppVersion(),
auditResponses, filterSelection, options.getFprHandle(), options.getFolderPriorityOrder(), sourceDecoder
);
AuditOutcome auditOutcome = performAviatorAudit(parsedData, auditResponses, filterSelection, options);

// --- STAGE 4: FINALIZATION ---
return finalizeFprAudit(
Expand Down Expand Up @@ -127,32 +123,23 @@ private static Map<String, String> buildIssueCategoryLookup(List<Vulnerability>
return issueCategoryLookup;
}

private static AuditOutcome performAviatorAudit(
ParsedFprData parsedData, IAviatorLogger logger,
String token, String appVersion, String url, String sscAppName, String sscAppVersion,
Map<String, AuditResponse> auditResponsesToFill, FilterSelection filterSelection, FprHandle fprHandle,
List<String> folderPriorityOrder, ISourceDecoder sourceDecoder) {
private static AuditOutcome performAviatorAudit(ParsedFprData parsedData, Map<String, AuditResponse> auditResponsesToFill,
FilterSelection filterSelection, AuditFprOptions options) {
SourceLanguageResolver sourceLanguageResolver =
new SourceLanguageResolver(parsedData.streamingFVDLProcessor.getFvdlMetadata());
parsedData.streamingFVDLProcessor.getFvdlMetadata().clearSourceFileTypeIndexes();

IssueAuditor issueAuditor = new IssueAuditor(
parsedData.vulnerabilities,
parsedData.auditProcessor,
parsedData.auditIssueMap,
parsedData.fprInfo,
sscAppName,
sscAppVersion,
filterSelection,
logger,
folderPriorityOrder,
sourceLanguageResolver,
sourceDecoder,
parsedData.streamingFVDLProcessor.getFvdlMetadata()
);
return issueAuditor.performAudit(
auditResponsesToFill, token, appVersion, parsedData.fprInfo.getBuildId(), url, fprHandle
);
IssueAuditor issueAuditor = IssueAuditor.builder()
.vulnerabilities(parsedData.vulnerabilities)
.auditProcessor(parsedData.auditProcessor)
.auditIssueMap(parsedData.auditIssueMap)
.fprInfo(parsedData.fprInfo)
.filterSelection(filterSelection)
.sourceLanguageResolver(sourceLanguageResolver)
.fvdlMetadata(parsedData.streamingFVDLProcessor.getFvdlMetadata())
.options(options)
.build();
return issueAuditor.performAudit(auditResponsesToFill);
}

private static FPRAuditResult finalizeFprAudit(
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ public class AuditFprOptions {
private final boolean noFilterSet;
private final List<String> folderNames;
private final List<String> folderPriorityOrder;
@Builder.Default private final boolean forceReaudit = false;
@Builder.Default private final ISourceDecoder sourceDecoder = SourceDecoders.defaults();
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ private boolean isAudited(AuditIssue auditIssue) {
}

String auditorStatusValue = tags.get(Constants.AUDITOR_STATUS_TAG_ID);
if (!isPendingReviewValue(auditorStatusValue)) {
if (!StringUtil.isPendingReviewValue(auditorStatusValue)) {
return true;
}

Expand All @@ -159,16 +159,7 @@ private boolean isAudited(AuditIssue auditIssue) {
}

String analysisTagValue = tags.get(Constants.ANALYSIS_TAG_ID);
return analysisTagValue != null
&& !analysisTagValue.equalsIgnoreCase("Not Set")
&& !analysisTagValue.equalsIgnoreCase(Constants.PENDING_REVIEW)
&& !StringUtil.isEmpty(analysisTagValue);
}

private boolean isPendingReviewValue(String value) {
return StringUtil.isEmpty(value)
|| value.equalsIgnoreCase("Pending Review")
|| value.equalsIgnoreCase(Constants.PENDING_REVIEW);
return !StringUtil.isPendingReviewValue(analysisTagValue);
}

private String resolveIssueStatus(AuditIssue auditIssue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class AuditIssue {
private boolean suppressed;
private int revision;
@Builder.Default private Map<String, String> tags = new HashMap<>();
@Builder.Default private Map<String, String> lastTagUsernames = new HashMap<>();
@Builder.Default private List<Comment> threadedComments = new ArrayList<>();

public void addTag(String tagId, String tagValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ private AuditIssue processAuditIssue(Element issueElement) {
tags.put(tagId, tagValue);
}
auditIssueBuilder.tags(tags);
auditIssueBuilder.lastTagUsernames(lastTagUsernames(issueElement));

List<AuditIssue.Comment> threadedComments = new ArrayList<>();
NodeList commentNodes = issueElement.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "Comment");
Expand All @@ -267,6 +268,24 @@ private AuditIssue processAuditIssue(Element issueElement) {
}


private Map<String, String> lastTagUsernames(Element issueElement) {
Map<String, String> lastTagUsernames = new HashMap<>();
NodeList tagHistories = issueElement.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "TagHistory");
for (int index = 0; index < tagHistories.getLength(); index++) {
Element tagHistory = (Element) tagHistories.item(index);
NodeList tags = tagHistory.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "Tag");
if (tags.getLength() == 0) {
continue;
}
String tagId = ((Element) tags.item(0)).getAttribute("id");
String username = Optional.ofNullable(getFirstElementContentNS(tagHistory, "Username")).orElse("");
if (tagId != null && !tagId.isBlank()) {
lastTagUsernames.put(tagId, username);
}
}
return lastTagUsernames;
}

private String getTagValue(Element tagElement) {
NodeList valueNodes = tagElement.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "Value");
if (valueNodes.getLength() > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipFile;

import javax.xml.parsers.DocumentBuilder;
Expand Down Expand Up @@ -85,6 +86,7 @@ private enum SkipReason {
REMEDIATION_DATA_INVALID("Remediation data invalid"),
REMEDIATION_LINE_RANGE_INVALID("Remediation line range invalid"),
SOURCE_CONTEXT_NOT_FOUND("Source context not found"),
SOURCE_CONTEXT_AMBIGUOUS("Source context matched multiple locations"),
ORIGINAL_CODE_NOT_FOUND("Original code not found"),
REMEDIATION_ENCODE_FAILED("Remediation encode failed"),
SOURCE_WRITE_FAILED("Source file write failed"),
Expand Down Expand Up @@ -211,7 +213,7 @@ private boolean processRemediation(Element remediation, Path sourceBasePath, FVD
try {
Map<Path, PendingFileWrite> pendingWrites = prepareFileChanges(remediation, sourceBasePath, fvdlMetadata);
if (pendingWrites.isEmpty()) {
recordSkipped(skippedByReason, SkipReason.NO_CHANGES);
recordSkipped(skippedByReason, SkipReason.NO_CHANGES.displayName);
return false;
}
try {
Expand All @@ -222,15 +224,15 @@ private boolean processRemediation(Element remediation, Path sourceBasePath, FVD
throw new SkipRemediationException(SkipReason.SOURCE_WRITE_FAILED, e.getMessage(), e);
}
} catch (SkipRemediationException e) {
recordSkipped(skippedByReason, e.reason);
LOG.info("Skipping remediation {}: {}", instanceId, e.getMessage());
recordSkipped(skippedByReason, skipReasonLabel(e));
LOG.warn("Skipping remediation {}: {}", instanceId, e.getMessage());
LOG.debug("Skip reason for remediation {}: {}", instanceId, e.reason.displayName, e);
return false;
} catch (RollbackRemediationException e) {
throw e;
} catch (Exception e) {
recordSkipped(skippedByReason, SkipReason.UNEXPECTED_ERROR);
LOG.info("Skipping remediation {} due to an unexpected processing error", instanceId);
recordSkipped(skippedByReason, SkipReason.UNEXPECTED_ERROR.displayName);
LOG.warn("Skipping remediation {} due to an unexpected processing error", instanceId);
LOG.debug("Unexpected error while processing remediation {}", instanceId, e);
return false;
}
Expand Down Expand Up @@ -307,7 +309,8 @@ private String applyChange(String instanceId, String filename, String fileHash,
LOG.debug("Remediation {} hash check for '{}': {}", instanceId, filename, fileHashMatches ? "matched" : "mismatched");
if (!fileHashMatches) {
LOG.debug("File hash mismatch for remediation {} in {}; searching changed source content", instanceId, filename);
String contextText = getRequiredElementText(change, "Context");
Element contextElement = getRequiredElement(change, "Context");
String contextText = contextElement.getTextContent();
List<String> contextLine = Arrays.asList(contextText.split("\\r?\\n"));
int contextLineFrom = fuzzySearchContext(instanceId, filename, originalLines, contextLine);
if (contextLineFrom == -1) {
Expand All @@ -320,7 +323,10 @@ private String applyChange(String instanceId, String filename, String fileHash,

String originalCodeText = getRequiredElementText(change, "OriginalCode");
List<String> originalCodeLine = Arrays.asList(originalCodeText.split("\\r?\\n"));
int[] lineFromTo = fuzzySearchOriginalCode(instanceId, filename, originalLines, originalCodeLine, contextLineFrom);
int contextBefore = parseRequiredContextAttribute(contextElement, "before");
int contextAfter = parseRequiredContextAttribute(contextElement, "after");
int[] lineFromTo = fuzzySearchOriginalCode(instanceId, filename, originalLines, originalCodeLine,
contextLineFrom, contextLine.size(), contextBefore, contextAfter);
if (lineFromTo[0] == -1 || lineFromTo[1] == -1) {
LOG.debug("Original code search failed for remediation {} in {}; context line={}, original code lines={}, source lines={}",
instanceId, filename, contextLineFrom + 1, originalCodeLine.size(), originalLines.size());
Expand Down Expand Up @@ -387,16 +393,35 @@ private void rollbackRemediationWrites(String instanceId, List<RollbackFileWrite

private int fuzzySearchContext(String instanceId, String filename, List<String> originalLines, List<String> contextLine) {
try {
return FuzzyContextSearcher.fuzzySearchContext(originalLines, contextLine, 0);
List<Integer> matches = FuzzyContextSearcher.fuzzySearchContextMatches(originalLines, contextLine, 0);
if (matches.size() > 1) {
String candidateLines = matches.stream()
.map(line -> String.valueOf(line + 1))
.collect(Collectors.joining(", "));
throw new SkipRemediationException(SkipReason.SOURCE_CONTEXT_AMBIGUOUS,
"Source context matched multiple locations in file '" + filename + "'; candidate lines: " + candidateLines);
}
return matches.isEmpty() ? -1 : matches.get(0);
} catch (IOException e) {
throw new SkipRemediationException(SkipReason.SOURCE_CONTEXT_NOT_FOUND,
"Error searching source context for remediation '" + instanceId + "' in file '" + filename + "'", e);
}
}

private int[] fuzzySearchOriginalCode(String instanceId, String filename, List<String> originalLines, List<String> originalCodeLine,
int contextLineFrom) {
return FuzzyContextSearcher.fuzzySearchOriginalCode(originalLines, originalCodeLine, 0, contextLineFrom);
int contextLineFrom, int contextLineCount, int contextBefore, int contextAfter) {
int contextStart = contextLineFrom + contextBefore;
int contextEnd = contextLineFrom + contextLineCount - contextAfter;
if (contextStart < 0 || contextStart >= contextEnd || contextEnd > originalLines.size()) {
return new int[] {-1, -1};
}

int[] lineFromTo = FuzzyContextSearcher.fuzzySearchOriginalCode(
originalLines.subList(contextStart, contextEnd), originalCodeLine, 0, 0);
if (lineFromTo[0] == -1 || lineFromTo[1] == -1) {
return lineFromTo;
}
return new int[] {lineFromTo[0] + contextStart, lineFromTo[1] + contextStart};
}

private boolean isFilePresent(Path path) {
Expand Down Expand Up @@ -447,12 +472,34 @@ private byte[] encodeSourceFile(String content, Charset charset, String filename
}

private String getRequiredElementText(Element parent, String elementName) {
return getRequiredElement(parent, elementName).getTextContent();
}

private Element getRequiredElement(Element parent, String elementName) {
NodeList nodes = parent.getElementsByTagNameNS(NAMESPACE_URI, elementName);
if (nodes.getLength() == 0 || nodes.item(0) == null) {
throw new SkipRemediationException(SkipReason.REMEDIATION_DATA_INVALID,
"Missing required remediation element '" + elementName + "'");
}
return nodes.item(0).getTextContent();
return (Element) nodes.item(0);
}

private int parseRequiredContextAttribute(Element context, String attributeName) {
String value = context.getAttribute(attributeName);
if (value == null || value.isBlank()) {
throw new SkipRemediationException(SkipReason.REMEDIATION_DATA_INVALID,
"Missing required remediation context attribute '" + attributeName + "'");
}
try {
int parsedValue = Integer.parseInt(value);
if (parsedValue < 0) {
throw new NumberFormatException("negative value");
}
return parsedValue;
} catch (NumberFormatException e) {
throw new SkipRemediationException(SkipReason.REMEDIATION_DATA_INVALID,
"Invalid remediation context attribute '" + attributeName + "': " + value, e);
}
}

private int parseRequiredInt(Element parent, String elementName) {
Expand Down Expand Up @@ -517,8 +564,12 @@ private String calculateHashBase64(String content, String algorithm) {
}
}

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

private String skipReasonLabel(SkipRemediationException exception) {
return exception.reason.displayName;
}

private String formatSkippedReasons(Map<String, Integer> skippedByReason) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
*/
package com.fortify.cli.aviator.util;

import java.util.Locale;
import java.util.Set;

public class Constants {

// Audit Result Values
Expand Down Expand Up @@ -45,8 +48,19 @@ public class Constants {
public static final String AUDITOR_STATUS_TAG_ID = "ACB05E55-E74D-468C-8501-52E1FDC27D71";
public static final String FOD_TAG_ID = "604f0fbe-b5fe-47cd-a9cb-587ad8ebe93a";

// User Names
// User Names written into audit.xml TagHistory / comments
public static final String USER_NAME = "Fortify Remediation Aviator";
public static final String USER_NAME_LEGACY_FORTIFY_AVIATOR = "Fortify Aviator";
public static final String USER_NAME_LEGACY_CORE_SAST_AVIATOR = "Core SAST Aviator";
private static final Set<String> AVIATOR_AUDIT_USERNAMES = Set.of(
USER_NAME.toLowerCase(Locale.ROOT),
USER_NAME_LEGACY_FORTIFY_AVIATOR.toLowerCase(Locale.ROOT),
USER_NAME_LEGACY_CORE_SAST_AVIATOR.toLowerCase(Locale.ROOT)
);

public static boolean isAviatorAuditUsername(String username) {
return username != null && AVIATOR_AUDIT_USERNAMES.contains(username.trim().toLowerCase(Locale.ROOT));
}

// Other Constants
public static final String AUDIT_NAMESPACE_URI = "xmlns://www.fortify.com/schema/audit";
Expand Down
Loading
Loading