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 @@ -15,6 +15,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.zip.ZipFile;

import org.slf4j.Logger;
Expand All @@ -28,7 +29,9 @@
import com.fortify.cli.aviator.fpr.processor.AuditProcessor;
import com.fortify.cli.aviator.fpr.processor.FilterTemplateParser;
import com.fortify.cli.aviator.fpr.processor.StreamingFVDLProcessor;
import com.fortify.cli.aviator.util.Constants;
import com.fortify.cli.aviator.util.FprHandle;
import com.fortify.cli.aviator.util.StringUtil;

import lombok.Getter;

Expand Down Expand Up @@ -91,6 +94,7 @@ public List<Vulnerability> process(StreamingFVDLProcessor streamingFVDLProcessor
//List<Vulnerability> vulnerabilities = fvdlProcessor.processXML();

List<Vulnerability> vulnerabilities = streamingFVDLProcessor.getVulnerabilities();
applyAuditIssueData(vulnerabilities);
logger.info("Parsed {} vulnerabilities from FVDL.", vulnerabilities.size());

return vulnerabilities;
Expand All @@ -101,4 +105,92 @@ public List<Vulnerability> process(StreamingFVDLProcessor streamingFVDLProcessor
throw new AviatorTechnicalException("Unexpected error during FPR processing.", e);
}
}

private void applyAuditIssueData(List<Vulnerability> vulnerabilities) {
vulnerabilities.stream()
.filter(vulnerability -> vulnerability != null && vulnerability.getInstanceID() != null)
.forEach(this::applyAuditIssueData);
}

private void applyAuditIssueData(Vulnerability vulnerability) {
AuditIssue auditIssue = auditIssueMap.get(vulnerability.getInstanceID());
if (auditIssue == null) {
return;
}

vulnerability.setSuppressed(auditIssue.isSuppressed());
vulnerability.setAudited(isAudited(auditIssue));

String issueStatus = resolveIssueStatus(auditIssue);
if (issueStatus != null) {
vulnerability.setIssueStatus(issueStatus);
}

List<AuditIssue.Comment> threadedComments = auditIssue.getThreadedComments();
if (threadedComments != null && !threadedComments.isEmpty()) {
vulnerability.setLastComment(threadedComments.get(threadedComments.size() - 1).getContent());
String commentUsers = threadedComments.stream()
.map(AuditIssue.Comment::getUsername)
.filter(username -> username != null && !username.isBlank())
.distinct()
.collect(Collectors.joining(" "));
vulnerability.setCommentUsers(commentUsers);
vulnerability.setHistoryUsers(commentUsers);
}
}

private boolean isAudited(AuditIssue auditIssue) {
Map<String, String> tags = auditIssue.getTags();
if (tags == null) {
return false;
}

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

if (auditIssue.isSuppressed()) {
return true;
}

if (tags.containsKey(Constants.AVIATOR_EXPECTED_OUTCOME_TAG_ID)) {
return true;
}

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);
}

private String resolveIssueStatus(AuditIssue auditIssue) {
Map<String, String> tags = auditIssue.getTags();
if (tags == null || tags.isEmpty()) {
return null;
}

String[] candidateTagIds = {
Constants.AUDITOR_STATUS_TAG_ID,
Constants.FOD_TAG_ID,
Constants.ANALYSIS_TAG_ID,
Constants.AVIATOR_STATUS_TAG_ID
};

for (String tagId : candidateTagIds) {
String value = tags.get(tagId);
if (value != null && !value.trim().isEmpty()) {
return value;
}
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ public class Vulnerability implements Searchable {
private Double accuracy;
private Double impact;
private Double probability;
private Double minVirtualCallConfidence;
private String filetype;
private String likelihood;
private String priority;
private String packageName;
private String projectName;
private String category;
private String subcategory;
Expand All @@ -69,6 +71,7 @@ public class Vulnerability implements Searchable {
private String audience;
private String buildId;
private String uuid;
private String analysisType;

@Builder.Default private List<List<StackTraceElement>> stackTrace = new ArrayList<>();
@Builder.Default private List<File> files = new ArrayList<>();
Expand All @@ -84,11 +87,19 @@ public class Vulnerability implements Searchable {

private String requestHeaders, requestParameters, requestBody, requestMethod, requestCookies, requestHttpVersion;
private String attackPayload, attackType, response, trigger, vulnerableParameter;
private String className;
private String functionName;
private String sourceFunction;
private String sinkFunction;
private String sourceContext;
private String sinkContext;

@Builder.Default private boolean isSuppressed = false;
@Builder.Default private boolean isAudited = false;
@Builder.Default private String issueStatus = "New";
private String lastComment;
private String commentUsers;
private String historyUsers;

private static final Logger filterLogger = LoggerFactory.getLogger(Vulnerability.class.getName() + ".Filter");

Expand All @@ -100,8 +111,10 @@ public boolean contains(String searchString) {
String lowerSearch = searchString.toLowerCase();

return anyStringContains(lowerSearch, analyzerName, classID, kingdom, type, subType, instanceID,
filetype, likelihood, priority, projectName, analysis, category, subcategory,
shortDescription, explanation, audience, requestMethod, attackType, vulnerableParameter) ||
filetype, likelihood, priority, packageName, projectName, analysis, analysisType, category,
subcategory, shortDescription, explanation, audience, requestMethod, attackType,
vulnerableParameter, className, functionName, sourceFunction, sinkFunction, sourceContext,
sinkContext) ||
taintFlags.stream().anyMatch(f -> f.toLowerCase().contains(lowerSearch)) ||
knowledge.values().stream().anyMatch(v -> v != null && v.toLowerCase().contains(lowerSearch)) ||
files.stream().anyMatch(f -> f.getName().toLowerCase().contains(lowerSearch) || (f.getContent() != null && f.getContent().toLowerCase().contains(lowerSearch)));
Expand All @@ -112,8 +125,10 @@ public boolean matches(String matchString) {
if (matchString == null || matchString.isEmpty()) return false;

return anyStringEquals(matchString, analyzerName, classID, kingdom, type, subType, instanceID,
filetype, likelihood, priority, projectName, analysis, category, subcategory,
shortDescription, explanation, audience, requestMethod, attackType, vulnerableParameter) ||
filetype, likelihood, priority, packageName, projectName, analysis, analysisType, category,
subcategory, shortDescription, explanation, audience, requestMethod, attackType,
vulnerableParameter, className, functionName, sourceFunction, sinkFunction, sourceContext,
sinkContext) ||
taintFlags.stream().anyMatch(f -> f.equalsIgnoreCase(matchString)) ||
knowledge.values().stream().anyMatch(v -> v != null && v.equalsIgnoreCase(matchString)) ||
files.stream().anyMatch(f -> f.getName().equalsIgnoreCase(matchString));
Expand All @@ -124,8 +139,10 @@ public boolean matchesPattern(Pattern pattern) {
if (pattern == null) return false;

return anyStringMatchesPattern(pattern, analyzerName, classID, kingdom, type, subType, instanceID,
filetype, likelihood, priority, projectName, analysis, category, subcategory,
shortDescription, explanation, audience, requestMethod, attackType, vulnerableParameter) ||
filetype, likelihood, priority, packageName, projectName, analysis, analysisType, category,
subcategory, shortDescription, explanation, audience, requestMethod, attackType,
vulnerableParameter, className, functionName, sourceFunction, sinkFunction, sourceContext,
sinkContext) ||
taintFlags.stream().anyMatch(f -> pattern.matcher(f).matches()) ||
knowledge.values().stream().anyMatch(v -> v != null && pattern.matcher(v).matches()) ||
files.stream().anyMatch(f -> pattern.matcher(f.getName()).matches());
Expand Down Expand Up @@ -174,51 +191,75 @@ public Object getAttributeValue(String attributeName) {
// --- Strategy 1: Check direct, derived, or instance-specific fields first ---
switch (lowerAttributeName) {
// --- Direct Numeric Fields ---
case "accuracy": valueToReturn = this.getAccuracy(); break;
case "impact": valueToReturn = this.getImpact(); break;
case "likelihood": valueToReturn = (this.getLikelihood() != null && !this.getLikelihood().isEmpty()) ? Double.parseDouble(this.getLikelihood()) : null; break;
case "probability": valueToReturn = this.getProbability(); break;
case "minvirtualcallconfidence": valueToReturn = this.getMinVirtualCallConfidence(); break;
case "confidence": valueToReturn = this.getConfidence(); break;
case "instanceseverity": valueToReturn = this.getInstanceSeverity(); break;
case "linenumber": valueToReturn = (double) (this.getSource() != null ? this.getSource().getLine() : 0); break;
case "sourceline": valueToReturn = (this.getSource() != null) ? (double) this.getSource().getLine() : 0.0; break;

// --- Direct String Fields ---
case "classid": valueToReturn = this.getClassID(); break;
case "kingdom": valueToReturn = this.getKingdom(); break;
case "priority": valueToReturn = this.getPriority(); break;
case "category": valueToReturn = this.getCategory(); break;
case "analyzer": valueToReturn = this.getAnalyzerName(); break;
case "instanceid": valueToReturn = this.getInstanceID(); break;
case "package": valueToReturn = this.getProjectName(); break;
case "package": valueToReturn = this.getPackageName(); break;
case "filetype": valueToReturn = this.getFiletype(); break;
case "issuestatus": valueToReturn = this.getIssueStatus(); break;
case "issuestate": valueToReturn = this.getAnalysis(); break;
case "audience": valueToReturn = this.getAudience(); break;
case "analysistype": valueToReturn = this.getAnalysisType(); break;
case "classname": valueToReturn = this.getClassName(); break;
case "functionname": valueToReturn = this.getFunctionName(); break;
case "sourcefunction": valueToReturn = this.getSourceFunction(); break;
case "sinkfunction": valueToReturn = this.getSinkFunction(); break;
case "sourcecontext": valueToReturn = this.getSourceContext(); break;
case "sinkcontext": valueToReturn = this.getSinkContext(); break;
case "comment": valueToReturn = this.getLastComment(); break;
case "commentuser": valueToReturn = this.getCommentUsers(); break;
case "historyuser": valueToReturn = this.getHistoryUsers(); break;
case "requestheaders": valueToReturn = this.getRequestHeaders(); break;
case "requestid": valueToReturn = getRequestId(); break;
case "requestparameters": valueToReturn = this.getRequestParameters(); break;
case "requestbody": valueToReturn = this.getRequestBody(); break;
case "requestmethod": valueToReturn = this.getRequestMethod(); break;
case "requestcookies": valueToReturn = this.getRequestCookies(); break;
case "requesthttpversion": valueToReturn = this.getRequestHttpVersion(); break;
case "attackpayload": valueToReturn = this.getAttackPayload(); break;
case "attacktype": valueToReturn = this.getAttackType(); break;
case "response": valueToReturn = this.getResponse(); break;
case "trigger": valueToReturn = this.getTrigger(); break;
case "vulnerableparameter": valueToReturn = this.getVulnerableParameter(); break;

// --- Complex / Aggregated Fields ---
case "filename": valueToReturn = this.getFiles().stream().map(File::getName).collect(Collectors.joining(" ")); break;
case "filename": valueToReturn = getPrimaryFilename(); break;
case "taintflags": valueToReturn = String.join(" ", this.getTaintFlags()); break;
case "tracenode": valueToReturn = getFullTraceAsText(this.getStackTrace()); break;
case "sourcefile": valueToReturn = (this.getSource() != null) ? this.getSource().getFilename() : ""; break;
case "shortfilename": valueToReturn = (this.getSource() != null && this.getSource().getFilename() != null) ? new java.io.File(this.getSource().getFilename()).getName() : ""; break;
case "codesnippet": valueToReturn = (this.getSource() != null) ? this.getSource().getCode() : ""; break;
case "sourcefile": valueToReturn = (this.getSource() != null && this.getSource().getFilename() != null) ? this.getSource().getFilename() : getPrimaryFilename(); break;
case "shortfilename": valueToReturn = getShortFilename(); break;
case "url": valueToReturn = this.getExternalEntries().stream()
.map(Entry::getUrl)
.filter(url -> url != null && !url.isBlank())
.collect(Collectors.joining(" "));
break;

// --- Boolean Fields ---
case "suppressed": valueToReturn = this.isSuppressed(); break;
case "audited": valueToReturn = this.isAudited(); break;
case "suppressed": valueToReturn = Boolean.toString(this.isSuppressed()); break;
case "audited": valueToReturn = Boolean.toString(this.isAudited()); break;
}

// --- Strategy 2: If not found above, fall back to the knowledge map ---
// This covers all rule-based metadata like 'Accuracy', 'Probability', 'Kingdom', 'CWE', etc.
if (valueToReturn == null) {
for (Map.Entry<String, String> entry : this.getKnowledge().entrySet()) {
if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(lowerAttributeName)) {
String stringValue = entry.getValue();
// Attempt to convert to a number if it looks like one, for numeric comparisons.
if (stringValue != null && stringValue.matches("^-?[0-9.]+$")) {
try {
valueToReturn = Double.parseDouble(stringValue);
} catch (NumberFormatException e) {
valueToReturn = stringValue; // It wasn't a valid double, treat as string
}
} else {
valueToReturn = stringValue;
}
valueToReturn = entry.getValue();
break;
}
}
Expand All @@ -237,6 +278,55 @@ public Object getAttributeValue(String attributeName) {

return valueToReturn;
}

private String getPrimaryFilename() {
if (this.getSource() != null && this.getSource().getFilename() != null && !this.getSource().getFilename().isBlank()) {
return this.getSource().getFilename();
}
if (this.getLastStackTraceElement() != null && this.getLastStackTraceElement().getFilename() != null
&& !this.getLastStackTraceElement().getFilename().isBlank()) {
return this.getLastStackTraceElement().getFilename();
}
return this.getFiles().stream()
.map(File::getName)
.filter(Objects::nonNull)
.filter(name -> !name.isBlank())
.findFirst()
.orElse("");
}

private String getShortFilename() {
String filename = getPrimaryFilename();
return filename.isBlank() ? "" : new java.io.File(filename).getName();
}

private String getRequestId() {
if (requestHeaders == null || requestHeaders.isBlank()) {
return null;
}

Pattern headerPattern = Pattern.compile("(?i)(?:x-scan-memo|memo)\\s*:\\s*([^\\r\\n]+)");
var headerMatcher = headerPattern.matcher(requestHeaders);
while (headerMatcher.find()) {
String requestId = extractSid(headerMatcher.group(1));
if (requestId != null) {
return requestId;
}
}

return extractSid(requestHeaders);
}

private String extractSid(String value) {
if (value == null || value.isBlank()) {
return null;
}

Pattern sidPattern = Pattern.compile("(?i)\\bSID\\s*[:=]\\s*\"?([^\";,\\s]+)\"?");
var sidMatcher = sidPattern.matcher(value);
return sidMatcher.find() ? sidMatcher.group(1) : null;
}

private String getFullTraceAsText(List<List<StackTraceElement>> traces) {
if (traces == null || traces.isEmpty()) return "";
StringBuilder sb = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

/**
* A factory and mapper class responsible for safely converting a JAXB-generated
* FVDL Vulnerability into our clean, internal application model.
* FVDL Vulnerability into the internal Vulnerability representation.
* <p>
* This class encapsulates the logic for handling nulls and providing
* sensible defaults, ensuring that the rest of the application works with
Expand All @@ -34,7 +34,7 @@ public final class VulnerabilityMapper {
private VulnerabilityMapper() {}

/**
* Maps a JAXB Vulnerability to the internal application's Vulnerability model.
* Maps a JAXB Vulnerability to the internal Vulnerability type.
* This method is the single point of entry for converting raw FVDL data.
*
* @param jaxbVuln The raw vulnerability object from JAXB, which may contain nulls.
Expand Down
Loading
Loading