From ae30a1c797de2a60cff6c27dd38973f605e0fe0f Mon Sep 17 00:00:00 2001 From: cdatla Date: Tue, 21 Apr 2026 17:47:10 +0530 Subject: [PATCH 1/2] fix: populate analysis type during streaming FVDL parsing --- .../cli/aviator/fpr/Vulnerability.java | 8 ++-- .../cli/aviator/fpr/model/FVDLMetadata.java | 1 + .../aviator/fpr/processor/MetadataParser.java | 48 +++++++++++++------ .../fpr/processor/StreamingFVDLProcessor.java | 34 ++++++++++++- .../filter/engine/FilterEngineTest.java | 19 ++++++++ 5 files changed, 91 insertions(+), 19 deletions(-) diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/Vulnerability.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/Vulnerability.java index fdb9f0a316..a006b244b5 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/Vulnerability.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/Vulnerability.java @@ -69,6 +69,7 @@ public class Vulnerability implements Searchable { private String audience; private String buildId; private String uuid; + private String analysisType; @Builder.Default private List> stackTrace = new ArrayList<>(); @Builder.Default private List files = new ArrayList<>(); @@ -100,7 +101,7 @@ 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, + filetype, likelihood, priority, projectName, analysis, analysisType, category, subcategory, shortDescription, explanation, audience, requestMethod, attackType, vulnerableParameter) || taintFlags.stream().anyMatch(f -> f.toLowerCase().contains(lowerSearch)) || knowledge.values().stream().anyMatch(v -> v != null && v.toLowerCase().contains(lowerSearch)) || @@ -112,7 +113,7 @@ 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, + filetype, likelihood, priority, projectName, analysis, analysisType, category, subcategory, shortDescription, explanation, audience, requestMethod, attackType, vulnerableParameter) || taintFlags.stream().anyMatch(f -> f.equalsIgnoreCase(matchString)) || knowledge.values().stream().anyMatch(v -> v != null && v.equalsIgnoreCase(matchString)) || @@ -124,7 +125,7 @@ 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, + filetype, likelihood, priority, projectName, analysis, analysisType, category, subcategory, shortDescription, explanation, audience, requestMethod, attackType, vulnerableParameter) || taintFlags.stream().anyMatch(f -> pattern.matcher(f).matches()) || knowledge.values().stream().anyMatch(v -> v != null && pattern.matcher(v).matches()) || @@ -190,6 +191,7 @@ public Object getAttributeValue(String attributeName) { case "filetype": valueToReturn = this.getFiletype(); break; case "issuestatus": valueToReturn = this.getIssueStatus(); break; case "audience": valueToReturn = this.getAudience(); break; + case "analysistype": valueToReturn = this.getAnalysisType(); break; // --- Complex / Aggregated Fields --- case "filename": valueToReturn = this.getFiles().stream().map(File::getName).collect(Collectors.joining(" ")); break; diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/FVDLMetadata.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/FVDLMetadata.java index 49e3b9f0a3..880e1f2aeb 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/FVDLMetadata.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/FVDLMetadata.java @@ -28,6 +28,7 @@ public class FVDLMetadata { private String projectName; private String projectVersion; private String engineVersion; + private String analysisType = "SCA"; // Rule metadata cache: classId -> metadata map private Map> ruleMetadata = new ConcurrentHashMap<>(); diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/MetadataParser.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/MetadataParser.java index 2ea579da8f..81cd07ed16 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/MetadataParser.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/MetadataParser.java @@ -13,6 +13,7 @@ package com.fortify.cli.aviator.fpr.processor; import static com.fortify.cli.aviator.fpr.processor.XmlParserUtils.readElementText; +import static com.fortify.cli.aviator.fpr.processor.XmlParserUtils.skipSection; import java.util.HashMap; import java.util.Map; @@ -24,6 +25,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; + /** * Parses metadata sections from FVDL XML. * Handles EngineData with rule metadata. @@ -35,36 +38,51 @@ public class MetadataParser { * Parse EngineData section for rule metadata. * Collects Rule metadata with format: ruleId -> Map */ - public void parseEngineData(XMLStreamReader reader, Map> ruleMetadata) + public void parseEngineData(XMLStreamReader reader, FVDLMetadata fvdlMetadata) throws XMLStreamException { logger.debug("Streaming EngineData parsing started"); - int depth = 1; String currentRuleId = null; Map currentMetadata = null; + Map> ruleMetadata = fvdlMetadata.getRuleMetadata(); - while (reader.hasNext() && depth > 0) { + while (reader.hasNext()) { int event = reader.next(); if (event == XMLStreamConstants.START_ELEMENT) { String localName = reader.getLocalName(); - if ("Rule".equals(localName)) { - currentRuleId = reader.getAttributeValue(null, "id"); - currentMetadata = new HashMap<>(); - } else if ("Group".equals(localName) && currentMetadata != null) { - String groupName = reader.getAttributeValue(null, "name"); - String groupValue = readElementText(reader); - if (groupName != null && groupValue != null) { - currentMetadata.put(groupName, groupValue); - } + switch (localName) { + case "EngineVersion": + fvdlMetadata.setEngineVersion(readElementText(reader)); + break; + case "Properties": + skipSection(reader, localName); + break; + case "Rule": + currentRuleId = reader.getAttributeValue(null, "id"); + currentMetadata = new HashMap<>(); + break; + case "Group": + if (currentMetadata != null) { + String groupName = reader.getAttributeValue(null, "name"); + String groupValue = readElementText(reader); + if (groupName != null && groupValue != null) { + currentMetadata.put(groupName, groupValue); + } + } + break; + default: + break; } - depth++; } else if (event == XMLStreamConstants.END_ELEMENT) { - depth--; - if ("Rule".equals(reader.getLocalName()) && currentRuleId != null && currentMetadata != null) { + String localName = reader.getLocalName(); + if ("Rule".equals(localName) && currentRuleId != null && currentMetadata != null) { ruleMetadata.put(currentRuleId, currentMetadata); currentRuleId = null; currentMetadata = null; + } else if ("EngineData".equals(localName)) { + logger.debug("Streaming Engine Data parsing completed"); + return; } } } 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 bb8f8cdda5..d406455128 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 @@ -297,6 +297,14 @@ private void parseMetadataAndPools(InputStream is) throws XMLStreamException { logger.debug("Pass 1: Parsing EngineData"); parseEngineData(reader); break; + case "Run": + logger.debug("Pass 1: Parsing Run for analysis type"); + parseRun(reader); + break; + case "RuntimeConfiguration": + logger.debug("Pass 1: Parsing RuntimeConfiguration for analysis type"); + parseRuntimeConfiguration(reader); + break; case "Build": logger.debug("Pass 1: Skipping Build"); // Build is already skipped in parseEngineData @@ -374,7 +382,30 @@ private void parseVulnerabilitiesOnly(InputStream is) throws XMLStreamException * Delegates to MetadataParser. */ private void parseEngineData(XMLStreamReader reader) throws XMLStreamException { - metadataParser.parseEngineData(reader, fvdlMetadata.getRuleMetadata()); + metadataParser.parseEngineData(reader, fvdlMetadata); + } + + private void parseRun(XMLStreamReader reader) throws XMLStreamException { + while (reader.hasNext()) { + int event = reader.next(); + + if (event == XMLStreamConstants.START_ELEMENT && "EngineName".equals(reader.getLocalName())) { + updateAnalysisType(readElementText(reader)); + } else if (event == XMLStreamConstants.END_ELEMENT && "Run".equals(reader.getLocalName())) { + return; + } + } + } + + private void parseRuntimeConfiguration(XMLStreamReader reader) throws XMLStreamException { + updateAnalysisType("SECURITYSCOPE"); + skipSection(reader, "RuntimeConfiguration"); + } + + private void updateAnalysisType(String analysisType) { + if (analysisType != null && !analysisType.isBlank()) { + fvdlMetadata.setAnalysisType(analysisType.trim()); + } } /** @@ -1075,6 +1106,7 @@ public Vulnerability processVulnerability( .instanceSeverity(streamedVuln.getInstanceSeverity()) .confidence(streamedVuln.getConfidence()) .analysis(streamedVuln.getShortDescription()) + .analysisType(fvdlMetadata.getAnalysisType()) .build(); // 1. Get the base metadata from streaming-parsed ruleMetadata in FVDLMetadata diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/filter/engine/FilterEngineTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/filter/engine/FilterEngineTest.java index 988b3619ca..e2900bcf4b 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/filter/engine/FilterEngineTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/filter/engine/FilterEngineTest.java @@ -128,6 +128,25 @@ void testRegexMatch() { assertEquals(1, result.size()); } + @Test + void testAnalysisTypeFilterMatchesOnlyMatchingVulnerability() { + Vulnerability scaVuln = new Vulnerability(); + scaVuln.setInstanceID("SCA_VULN"); + scaVuln.setAnalysisType("SCA"); + + Vulnerability webInspectVuln = new Vulnerability(); + webInspectVuln.setInstanceID("WEBINSPECT_VULN"); + webInspectVuln.setAnalysisType("WEBINSPECT"); + + List result = VulnerabilityFilterer.filter( + List.of(scaVuln, webInspectVuln), + "[analysis type]:SCA" + ); + + assertEquals(1, result.size()); + assertEquals("SCA_VULN", result.get(0).getInstanceID()); + } + // Null Attr Handling @Test void testNullAttrNotContains() { From 13d1456bda49e66045f6281dba84d4e469e4b16e Mon Sep 17 00:00:00 2001 From: cdatla Date: Wed, 22 Apr 2026 14:33:44 +0530 Subject: [PATCH 2/2] fix: improve streaming FVDL filter support --- .../fortify/cli/aviator/fpr/FPRProcessor.java | 92 ++++ .../cli/aviator/fpr/Vulnerability.java | 134 +++++- .../cli/aviator/fpr/VulnerabilityMapper.java | 4 +- .../aviator/fpr/filter/AttributeMapper.java | 5 + .../comparer/ContainsSearchComparer.java | 4 +- .../filter/comparer/ExactMatchComparer.java | 4 +- .../fpr/filter/engine/FilterParser.java | 27 +- .../cli/aviator/fpr/jaxb/ObjectFactory.java | 6 +- .../cli/aviator/fpr/model/FVDLMetadata.java | 49 +++ .../cli/aviator/fpr/model/StreamedTrace.java | 4 +- .../fpr/model/StreamedVulnerability.java | 11 +- .../fpr/processor/AuxiliaryProcessor.java | 6 +- .../fpr/processor/ReplacementParser.java | 2 +- .../fpr/processor/StreamingFVDLProcessor.java | 197 ++++++++- .../aviator/fpr/processor/VulnFinalizer.java | 4 +- .../filter/engine/FilterEngineTest.java | 103 +++++ .../cli/aviator/fpr/FPRProcessorTest.java | 176 ++++++++ .../fpr/processor/MetadataParserTest.java | 95 +++++ .../processor/StreamingFVDLProcessorTest.java | 397 ++++++++++++++++++ 19 files changed, 1258 insertions(+), 62 deletions(-) create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/FPRProcessorTest.java create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/MetadataParserTest.java create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/StreamingFVDLProcessorTest.java diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/FPRProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/FPRProcessor.java index 0219fbed94..b0899d4f59 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/FPRProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/FPRProcessor.java @@ -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; @@ -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; @@ -91,6 +94,7 @@ public List process(StreamingFVDLProcessor streamingFVDLProcessor //List vulnerabilities = fvdlProcessor.processXML(); List vulnerabilities = streamingFVDLProcessor.getVulnerabilities(); + applyAuditIssueData(vulnerabilities); logger.info("Parsed {} vulnerabilities from FVDL.", vulnerabilities.size()); return vulnerabilities; @@ -101,4 +105,92 @@ public List process(StreamingFVDLProcessor streamingFVDLProcessor throw new AviatorTechnicalException("Unexpected error during FPR processing.", e); } } + + private void applyAuditIssueData(List 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 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 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 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; + } } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/Vulnerability.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/Vulnerability.java index a006b244b5..c3a0b5bbec 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/Vulnerability.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/Vulnerability.java @@ -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; @@ -85,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"); @@ -101,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, analysisType, 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))); @@ -113,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, analysisType, 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)); @@ -125,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, analysisType, 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()); @@ -175,34 +191,67 @@ 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 --- @@ -210,17 +259,7 @@ public Object getAttributeValue(String attributeName) { if (valueToReturn == null) { for (Map.Entry 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; } } @@ -239,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> traces) { if (traces == null || traces.isEmpty()) return ""; StringBuilder sb = new StringBuilder(); diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/VulnerabilityMapper.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/VulnerabilityMapper.java index ad6a841dd9..dc62f499d6 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/VulnerabilityMapper.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/VulnerabilityMapper.java @@ -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. *

* This class encapsulates the logic for handling nulls and providing * sensible defaults, ensuring that the rest of the application works with @@ -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. diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/AttributeMapper.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/AttributeMapper.java index 6bb8d4ecf3..851d40d9ad 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/AttributeMapper.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/AttributeMapper.java @@ -58,12 +58,15 @@ public final class AttributeMapper { {"instance id", "instanceid"}, {"issue age", "issueage"}, {"issue state", "issuestate"}, + {"issue status", "issuestatus"}, {"[issue state]", "issuestate"}, {"kingdom", "kingdom"}, {"likelihood", "likelihood"}, {"line", "linenumber"}, {"manual", "manual"}, {"mapped category", "mappedcategory"}, + {"min virtual call confidence", "minvirtualcallconfidence"}, + {"min_virtual_call_confidence", "minvirtualcallconfidence"}, {"method", "requestmethod"}, {"package", "package"}, {"parameters", "requestparameters"}, @@ -80,6 +83,7 @@ public final class AttributeMapper { {"shortfilename", "shortfilename"}, {"sink", "sinkfunction"}, {"[sink function]", "sinkfunction"}, + {"sink context", "sinkcontext"}, {"source", "sourcefunction"}, {"[source function]", "sourcefunction"}, {"source context", "sourcecontext"}, @@ -96,6 +100,7 @@ public final class AttributeMapper { {"trigger", "trigger"}, {"url", "url"}, {"user", "user"}, + {"virtconf", "minvirtualcallconfidence"}, // --- Custom Tag Style Attributes --- {"pci 4.0", "pci 4.0"}, diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/comparer/ContainsSearchComparer.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/comparer/ContainsSearchComparer.java index f548fc8e5d..7c3c62c77a 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/comparer/ContainsSearchComparer.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/comparer/ContainsSearchComparer.java @@ -22,10 +22,10 @@ public ContainsSearchComparer(String searchTerm) { @Override public boolean matches(Object attributeValue) { - if (!(attributeValue instanceof String)) { + if (attributeValue == null) { return false; } - boolean result = ((String) attributeValue).toLowerCase().contains(searchTerm); + boolean result = attributeValue.toString().toLowerCase().contains(searchTerm); return result; } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/comparer/ExactMatchComparer.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/comparer/ExactMatchComparer.java index 833e096fb4..5b808a0a22 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/comparer/ExactMatchComparer.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/comparer/ExactMatchComparer.java @@ -27,10 +27,10 @@ public boolean matches(Object attributeValue) { if (searchTerm == null) { return attributeValue == null; } - if (!(attributeValue instanceof String)) { + if (attributeValue == null) { return false; } - boolean result = searchTerm.equalsIgnoreCase((String) attributeValue); + boolean result = searchTerm.equalsIgnoreCase(attributeValue.toString()); return result; } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/engine/FilterParser.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/engine/FilterParser.java index 738989c934..bed8b600ed 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/engine/FilterParser.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/filter/engine/FilterParser.java @@ -166,14 +166,19 @@ private static SearchQuery buildLegacyQuery(String modifier, List values attributeName = modifier; } if (values.size() == 1) { - return new SearchQuery(attributeName, createComparer(values.get(0))); + SearchComparer comparer = createComparer(values.get(0)); + if (isSpecialModifier(modifier)) { + attributeName = getSpecialAttributeName(modifier); + comparer = createSpecialComparer(modifier, values.get(0)); + } + return new SearchQuery(attributeName, comparer); } BooleanComparer combinedComparer = new BooleanComparer(); for (String value : values) { combinedComparer.addComparer(createComparer(value)); } if (isSpecialModifier(modifier)) { - attributeName = "confidence"; + attributeName = getSpecialAttributeName(modifier); } return new SearchQuery(attributeName, combinedComparer); } @@ -212,7 +217,7 @@ private static SearchQuery parseTerm(String term) { SearchComparer comparer = createComparer(valuePart); if (isSpecialModifier(cleanedModifier)) { comparer = createSpecialComparer(cleanedModifier, valuePart); - attributeName = "confidence"; // like library + attributeName = getSpecialAttributeName(cleanedModifier); // like library } return new SearchQuery(attributeName, comparer); } @@ -289,7 +294,10 @@ private static SearchTree.Node applyOp(SearchTree.LogicalOperator op, SearchTree // NEW: Specials like library (e.g., maxconf as range on confidence) private static boolean isSpecialModifier(String modifier) { - return "maxconf".equalsIgnoreCase(modifier) || "minconf".equalsIgnoreCase(modifier); // add more as needed + return "maxconf".equalsIgnoreCase(modifier) + || "minconf".equalsIgnoreCase(modifier) + || "maxvirtconf".equalsIgnoreCase(modifier) + || "minvirtconf".equalsIgnoreCase(modifier); } private static SearchComparer createSpecialComparer(String modifier, String value) { @@ -297,7 +305,18 @@ private static SearchComparer createSpecialComparer(String modifier, String valu return new NumberRangeComparer("[0," + value + "]"); } else if ("minconf".equalsIgnoreCase(modifier)) { return new NumberRangeComparer("[" + value + ",5]"); + } else if ("maxvirtconf".equalsIgnoreCase(modifier)) { + return new NumberRangeComparer("[0," + value + "]"); + } else if ("minvirtconf".equalsIgnoreCase(modifier)) { + return new NumberRangeComparer("[" + value + ",1]"); } return null; } + + private static String getSpecialAttributeName(String modifier) { + if ("maxvirtconf".equalsIgnoreCase(modifier) || "minvirtconf".equalsIgnoreCase(modifier)) { + return "minvirtualcallconfidence"; + } + return "confidence"; + } } \ No newline at end of file diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/jaxb/ObjectFactory.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/jaxb/ObjectFactory.java index 8214cce783..a8172e61f0 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/jaxb/ObjectFactory.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/jaxb/ObjectFactory.java @@ -17,13 +17,13 @@ /** * This object contains factory methods for each * Java content interface and Java element interface - * generated in the com.fortify.aviator.fpr.jaxb package. + * generated in this package. *

An ObjectFactory allows you to programatically * construct new instances of the Java representation * for XML content. The Java representation of XML * content can consist of schema derived interfaces * and classes representing the binding of schema - * type definitions, element declarations and model + * type definitions, element declarations and * groups. Factory methods for each of these are * provided in this class. * @@ -62,7 +62,7 @@ public class ObjectFactory { private final static QName _RulePackListRulePackMAC_QNAME = new QName("xmlns://www.fortifysoftware.com/schema/fvdl", "MAC"); /** - * Create a new ObjectFactory that can be used to create new instances of schema derived classes for package: com.fortify.aviator.fpr.jaxb + * Create a new ObjectFactory that can be used to create new instances of schema-derived classes for this package. * */ public ObjectFactory() { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/FVDLMetadata.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/FVDLMetadata.java index 880e1f2aeb..5b23e63d31 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/FVDLMetadata.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/FVDLMetadata.java @@ -14,6 +14,7 @@ import java.util.Map; +import java.util.StringJoiner; import java.util.concurrent.ConcurrentHashMap; import lombok.Data; @@ -39,6 +40,9 @@ public class FVDLMetadata { // Trace pool: traceId -> StreamedTrace private Map tracePool = new ConcurrentHashMap<>(); + // Context pool: contextId -> parsed context information + private Map contextPool = new ConcurrentHashMap<>(); + // Description cache: classID -> StreamedDescription private Map descriptionCache = new ConcurrentHashMap<>(); @@ -59,4 +63,49 @@ public static class NodeData { private String actionType; private String label; } + + @Data + public static class ContextInfo { + private String id; + private String namespace; + private String className; + private String functionName; + private String filename; + private Integer startLine; + + public String getContextString() { + return joinNonBlank(namespace, className, functionName); + } + + public String getQualifiedClassName() { + return joinNonBlank(namespace, className); + } + + public String getQualifiedFunctionName() { + if (functionName == null || functionName.isBlank()) { + return getQualifiedClassName(); + } + + String qualifiedClassName = getQualifiedClassName(); + StringBuilder builder = new StringBuilder(); + if (!qualifiedClassName.isBlank()) { + builder.append(qualifiedClassName).append('.'); + } + builder.append(functionName); + if (!functionName.startsWith("http")) { + builder.append("()"); + } + return builder.toString(); + } + + private String joinNonBlank(String... parts) { + StringJoiner joiner = new StringJoiner("."); + for (String part : parts) { + if (part != null && !part.isBlank()) { + joiner.add(part); + } + } + return joiner.toString(); + } + } } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/StreamedTrace.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/StreamedTrace.java index b4258f58ca..51a3b8c386 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/StreamedTrace.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/StreamedTrace.java @@ -12,8 +12,6 @@ */ package com.fortify.cli.aviator.fpr.model; - - import java.util.ArrayList; import java.util.List; @@ -53,7 +51,7 @@ public static class Primary { @Builder public static class Entry { private String nodeId; // For backward compatibility and debugging (can be null for inline nodes) - private Node node; // CHANGED: Full Node object (from com.fortify.aviator.cli.fpr.models.Node) + private Node node; // Parsed node data, including inline nodes without IDs private boolean isDefault; /** diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/StreamedVulnerability.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/StreamedVulnerability.java index eb47b09bf0..2613e5882e 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/StreamedVulnerability.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/StreamedVulnerability.java @@ -22,7 +22,7 @@ import lombok.Data; /** - * Lightweight vulnerability model for streaming parsing. + * Lightweight vulnerability representation for streaming parsing. * Contains only essential fields to minimize memory footprint. */ @Data @@ -44,6 +44,7 @@ public class StreamedVulnerability { private Double defaultSeverity; private Double instanceSeverity; private Double confidence; + private Double minVirtualCallConfidence; private Double impact; private Double probability; private String priority; @@ -55,6 +56,11 @@ public class StreamedVulnerability { private Integer primaryColStart; private Integer primaryColEnd; + // Vulnerability-level context information + private String contextNamespace; + private String contextClassName; + private String contextFunctionName; + // Trace information (hierarchical structure matching FVDL schema) // Each vulnerability can have multiple traces representing different data flow paths @Builder.Default @@ -129,8 +135,7 @@ public static class Trace { // Optional trace ID (used when trace is referenced from TracePool) private String id; - // List of nodes in this trace's data flow path - // Uses existing Node model from com.fortify.aviator.cli.fpr.models.Node + // List of nodes in this trace's data flow path. @Builder.Default private List nodes = new ArrayList<>(); } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuxiliaryProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuxiliaryProcessor.java index f9a1794e6d..e99fdb7f0a 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuxiliaryProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuxiliaryProcessor.java @@ -29,8 +29,8 @@ import com.fortify.cli.aviator.fpr.jaxb.Vulnerability; /** - * Processor for AuxiliaryData and ExternalEntries in FVDL Vulnerability. - * Populates auxiliaryData and externalEntries in the custom Vulnerability model. + * Processor for AuxiliaryData and ExternalEntries in an FVDL vulnerability. + * Populates auxiliaryData and externalEntries on the target Vulnerability instance. */ public class AuxiliaryProcessor { private static final Logger logger = LoggerFactory.getLogger(AuxiliaryProcessor.class); @@ -39,7 +39,7 @@ public class AuxiliaryProcessor { * Processes AuxiliaryData and ExternalEntries for a vulnerability. * * @param vulnJAXB JAXB Vulnerability object - * @param vulnCustom Custom Vulnerability model to populate + * @param vulnCustom Target Vulnerability instance to populate */ public void process(Vulnerability vulnJAXB, com.fortify.cli.aviator.fpr.Vulnerability vulnCustom) { // Process AuxiliaryData diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/ReplacementParser.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/ReplacementParser.java index b9ca5accdb..f11e57f265 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/ReplacementParser.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/ReplacementParser.java @@ -21,7 +21,7 @@ /** * Parses the section of a vulnerability's AnalysisInfo - * and populates the clean ReplacementData model. This class acts as the bridge + * and populates a ReplacementData instance. This class acts as the bridge * between the raw JAXB format and the application's internal data structure. */ public final class ReplacementParser { 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 d406455128..6c0d424321 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 @@ -101,7 +101,7 @@ public StreamingFVDLProcessor(FprHandle fprHandle){ * Parse an FPR file using two-pass parsing strategy. * * TWO-PASS PARSING STRATEGY: - * Pass 1: Parse metadata and pools (NodePool, TracePool, Descriptions, EngineData) + * Pass 1: Parse metadata and pools (NodePool, TracePool, ContextPool, Descriptions, EngineData) * This populates the reference pools needed for vulnerability parsing. * Pass 2: Parse Vulnerabilities with fully populated pools * NodeRef lookups now succeed because NodePool is populated. @@ -133,7 +133,7 @@ public void parse(ZipFile zipFile, String entryName) throws Exception { // ======================================== // PASS 1: Parse Metadata and Pools // ======================================== - logger.info(">>> PASS 1: Parsing metadata and pools (NodePool, TracePool, Descriptions, EngineData)"); + logger.info(">>> PASS 1: Parsing metadata and pools (NodePool, TracePool, ContextPool, Descriptions, EngineData)"); long pass1Start = System.currentTimeMillis(); memoryTracker.initializePass1Peak(); @@ -275,7 +275,8 @@ public void parse(ZipFile zipFile, String entryName) throws Exception { * - EngineData (rule metadata) * - UnifiedNodePool (node definitions) * - UnifiedTracePool (trace definitions) - * - Description (vulnerability descriptions) + * - ContextPool (context definitions) + * - Description (vulnerability descriptions) * - Build (skipped) * * Skipped sections: @@ -317,6 +318,10 @@ private void parseMetadataAndPools(InputStream is) throws XMLStreamException { logger.debug("Pass 1: Parsing UnifiedTracePool"); parseTracePool(reader); break; + case "ContextPool": + logger.debug("Pass 1: Parsing ContextPool"); + parseContextPool(reader); + break; case "Description": logger.debug("Pass 1: Parsing Description"); parseDescriptions(reader); @@ -364,6 +369,7 @@ private void parseVulnerabilitiesOnly(InputStream is) throws XMLStreamException case "Build": case "UnifiedNodePool": case "UnifiedTracePool": + case "ContextPool": case "Description": logger.debug("Pass 2: Skipping {} (already parsed in Pass 1)", localName); skipSection(reader, localName); @@ -482,6 +488,64 @@ private void parseTracePool(XMLStreamReader reader) throws XMLStreamException { logger.debug("Trace processed are {} ", fvdlMetadata.getTracePool().size()); } + /** + * Parse ContextPool section. + */ + private void parseContextPool(XMLStreamReader reader) throws XMLStreamException { + logger.debug("Start parse ContextPool"); + + while (reader.hasNext()) { + int event = reader.next(); + + if (event == XMLStreamConstants.START_ELEMENT && "Context".equals(reader.getLocalName())) { + FVDLMetadata.ContextInfo contextInfo = parseContext(reader); + if (contextInfo != null && contextInfo.getId() != null && !contextInfo.getId().isBlank()) { + fvdlMetadata.getContextPool().put(contextInfo.getId(), contextInfo); + } + } else if (event == XMLStreamConstants.END_ELEMENT && "ContextPool".equals(reader.getLocalName())) { + break; + } + } + + logger.info("Contexts processed: {} ", fvdlMetadata.getContextPool().size()); + } + + private FVDLMetadata.ContextInfo parseContext(XMLStreamReader reader) throws XMLStreamException { + FVDLMetadata.ContextInfo contextInfo = new FVDLMetadata.ContextInfo(); + contextInfo.setId(reader.getAttributeValue(null, "id")); + + while (reader.hasNext()) { + int event = reader.next(); + + if (event == XMLStreamConstants.START_ELEMENT) { + switch (reader.getLocalName()) { + case "NamespaceIdent": + contextInfo.setNamespace(reader.getAttributeValue(null, "name")); + break; + case "ClassIdent": + contextInfo.setNamespace(reader.getAttributeValue(null, "namespace")); + contextInfo.setClassName(reader.getAttributeValue(null, "name")); + break; + case "Function": + contextInfo.setNamespace(reader.getAttributeValue(null, "namespace")); + contextInfo.setClassName(reader.getAttributeValue(null, "enclosingClass")); + contextInfo.setFunctionName(reader.getAttributeValue(null, "name")); + break; + case "FunctionDeclarationSourceLocation": + contextInfo.setFilename(reader.getAttributeValue(null, "path")); + contextInfo.setStartLine(parseIntSafe(reader.getAttributeValue(null, "line"))); + break; + default: + break; + } + } else if (event == XMLStreamConstants.END_ELEMENT && "Context".equals(reader.getLocalName())) { + return contextInfo; + } + } + + return contextInfo; + } + /** * Parse Descriptions section for vulnerability descriptions. * Delegates to DescriptionParser. @@ -534,6 +598,7 @@ private StreamedVulnerability parseVulnerability(XMLStreamReader reader) throws break; case "InstanceInfo": currentSection = "InstanceInfo"; + builder.minVirtualCallConfidence(parseDoubleSafe(reader.getAttributeValue(null, "MinVirtualCallConfidence"))); break; case "AnalysisInfo": currentSection = "AnalysisInfo"; @@ -584,6 +649,14 @@ private StreamedVulnerability parseVulnerability(XMLStreamReader reader) throws // Parse ReplacementDefinitions from AnalysisInfo->Unified->ReplacementDefinitions builder.replacementData(parseReplacementDefinitions(reader)); break; + case "Context": + if ("AnalysisInfo".equals(currentSection)) { + FVDLMetadata.ContextInfo contextInfo = parseContext(reader); + builder.contextNamespace(contextInfo.getNamespace()); + builder.contextClassName(contextInfo.getClassName()); + builder.contextFunctionName(contextInfo.getFunctionName()); + } + break; case "AuxiliaryData": parseAuxiliaryData(reader, auxiliaryDataList); break; @@ -1105,8 +1178,12 @@ public Vulnerability processVulnerability( .defaultSeverity(streamedVuln.getDefaultSeverity()) .instanceSeverity(streamedVuln.getInstanceSeverity()) .confidence(streamedVuln.getConfidence()) + .minVirtualCallConfidence(streamedVuln.getMinVirtualCallConfidence()) .analysis(streamedVuln.getShortDescription()) .analysisType(fvdlMetadata.getAnalysisType()) + .packageName(streamedVuln.getContextNamespace()) + .className(streamedVuln.getContextClassName()) + .functionName(streamedVuln.getContextFunctionName()) .build(); // 1. Get the base metadata from streaming-parsed ruleMetadata in FVDLMetadata @@ -1149,7 +1226,6 @@ public Vulnerability processVulnerability( vulnCustom.setStackTrace(stackTraces); - //Map uniqueFiles = new java.util.LinkedHashMap<>(); if (!stackTraces.isEmpty()) { List firstStackTrace = stackTraces.get(0); List lastStackTrace = stackTraces.get(stackTraces.size() - 1); @@ -1160,6 +1236,8 @@ public Vulnerability processVulnerability( vulnCustom.setLongestStackTrace(findLongestList(stackTraces)); } + applyContextAttributes(vulnCustom, streamedVuln); + aggregateFromTraces(vulnCustom); // Process DAST / Auxiliary data - replicate auxiliaryProcessor.process() and processRequestRelated() @@ -1171,14 +1249,13 @@ public Vulnerability processVulnerability( vulnCustom.setShortDescription(StringUtil.stripTags(descs[0], true)); vulnCustom.setExplanation(StringUtil.stripTags(descs[1], true)); - // Set projectName from first file path if not already set - // Since file content population is disabled, we derive it from stack trace - if ((vulnCustom.getProjectName() == null || vulnCustom.getProjectName().isEmpty()) + // Set package name from the first file path when the FVDL doesn't provide namespace data. + if ((vulnCustom.getPackageName() == null || vulnCustom.getPackageName().isEmpty()) && !stackTraces.isEmpty() && !stackTraces.get(0).isEmpty()) { com.fortify.cli.aviator.audit.model.StackTraceElement firstElement = stackTraces.get(0).get(0); if (firstElement != null && firstElement.getFilename() != null) { String firstFilePath = firstElement.getFilename(); - vulnCustom.setProjectName(initPackageName(firstFilePath)); + vulnCustom.setPackageName(createPackageName(firstFilePath)); } } @@ -1188,6 +1265,80 @@ public Vulnerability processVulnerability( return vulnCustom; } + private void applyContextAttributes(Vulnerability vulnerability, StreamedVulnerability streamedVulnerability) { + FVDLMetadata.ContextInfo sourceContextInfo = resolveContextInfo(getSourceNode(streamedVulnerability)); + FVDLMetadata.ContextInfo sinkContextInfo = resolveContextInfo(getSinkNode(streamedVulnerability)); + + if (sourceContextInfo != null) { + vulnerability.setSourceContext(sourceContextInfo.getContextString()); + vulnerability.setSourceFunction(sourceContextInfo.getQualifiedFunctionName()); + if (StringUtil.isEmpty(vulnerability.getPackageName())) { + vulnerability.setPackageName(sourceContextInfo.getNamespace()); + } + if (StringUtil.isEmpty(vulnerability.getClassName())) { + vulnerability.setClassName(sourceContextInfo.getClassName()); + } + if (StringUtil.isEmpty(vulnerability.getFunctionName())) { + vulnerability.setFunctionName(sourceContextInfo.getFunctionName()); + } + } + + if (sinkContextInfo != null) { + vulnerability.setSinkContext(sinkContextInfo.getContextString()); + vulnerability.setSinkFunction(sinkContextInfo.getQualifiedFunctionName()); + if (StringUtil.isEmpty(vulnerability.getPackageName())) { + vulnerability.setPackageName(sinkContextInfo.getNamespace()); + } + if (StringUtil.isEmpty(vulnerability.getClassName())) { + vulnerability.setClassName(sinkContextInfo.getClassName()); + } + if (StringUtil.isEmpty(vulnerability.getFunctionName())) { + vulnerability.setFunctionName(sinkContextInfo.getFunctionName()); + } + } + } + + private FVDLMetadata.ContextInfo resolveContextInfo(Node node) { + if (node == null || StringUtil.isEmpty(node.getContextId())) { + return null; + } + return fvdlMetadata.getContextPool().get(node.getContextId()); + } + + private Node getSourceNode(StreamedVulnerability streamedVulnerability) { + List sourceTraceNodes = getSourceTraceNodes(streamedVulnerability); + return sourceTraceNodes.isEmpty() ? null : sourceTraceNodes.get(0); + } + + private Node getSinkNode(StreamedVulnerability streamedVulnerability) { + List sinkTraceNodes = getSinkTraceNodes(streamedVulnerability); + return sinkTraceNodes.isEmpty() ? null : sinkTraceNodes.get(sinkTraceNodes.size() - 1); + } + + private List getSourceTraceNodes(StreamedVulnerability streamedVulnerability) { + return getTraceNodes(streamedVulnerability, 0); + } + + private List getSinkTraceNodes(StreamedVulnerability streamedVulnerability) { + if (streamedVulnerability.getTraces() == null || streamedVulnerability.getTraces().isEmpty()) { + return Collections.emptyList(); + } + return getTraceNodes(streamedVulnerability, streamedVulnerability.getTraces().size() - 1); + } + + private List getTraceNodes(StreamedVulnerability streamedVulnerability, int traceIndex) { + if (streamedVulnerability.getTraces() == null || streamedVulnerability.getTraces().isEmpty()) { + return Collections.emptyList(); + } + + StreamedVulnerability.Trace trace = streamedVulnerability.getTraces().get(traceIndex); + if (trace.getNodes() == null || trace.getNodes().isEmpty()) { + return Collections.emptyList(); + } + + return trace.getNodes(); + } + /** * Initialize package name from a file path. * Extracts the first directory component as the project name. @@ -1195,12 +1346,31 @@ public Vulnerability processVulnerability( * @param filePath File path to derive package from * @return Derived package name or the full path if no separator found */ - private String initPackageName(String filePath) { + static String createPackageName(String filePath) { if (filePath == null || filePath.trim().isEmpty()) { return ""; } - int separatorIndex = filePath.indexOf('/'); - return separatorIndex > 0 ? filePath.substring(0, separatorIndex) : filePath; + + String shortFileName = new java.io.File(filePath).getName(); + String dirName = filePath.substring(0, Math.max(0, filePath.length() - shortFileName.length())); + if (dirName.isEmpty()) { + return ""; + } + + String packageName = dirName.replace('\\', '.').replace('/', '.'); + if (packageName.endsWith(".")) { + packageName = packageName.substring(0, packageName.length() - 1); + } + + String[] packageHeaders = new String[]{"com", "org", "net", "src"}; + for (String packageHeader : packageHeaders) { + String marker = "." + packageHeader + "."; + int index = packageName.lastIndexOf(marker); + if (index != -1) { + return packageName.substring(index + 1); + } + } + return packageName; } /** @@ -1234,8 +1404,7 @@ private void processAuxiliaryAndRequestData( } /** - * Convert StreamedVulnerability.ExternalEntry objects to Entry objects. - * Needed because Vulnerability class expects com.fortify.aviator.cli.fpr.models.Entry. + * Convert streamed external entries to the Entry type used by Vulnerability. * * @param streamedEntries List of StreamedVulnerability.ExternalEntry * @return List of converted Entry objects @@ -1662,7 +1831,7 @@ private int countNodesRecursive(com.fortify.cli.aviator.audit.model.StackTraceEl */ private void aggregateFromTraces(Vulnerability vulnCustom) { Set allTaintFlags = new HashSet<>(); - Map allKnowledge = new HashMap<>(); + Map allKnowledge = new HashMap<>(vulnCustom.getKnowledge()); for (List trace : vulnCustom.getStackTrace()) { for (com.fortify.cli.aviator.audit.model.StackTraceElement ste : trace) { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/VulnFinalizer.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/VulnFinalizer.java index 6cdc09aed4..86315e7115 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/VulnFinalizer.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/VulnFinalizer.java @@ -112,10 +112,10 @@ private void deriveFields(Vulnerability vuln) { vuln.setPriority(priority); // 4. Derive package name (logic remains the same) - if (vuln.getProjectName() == null || vuln.getProjectName().isEmpty()) { + if (vuln.getPackageName() == null || vuln.getPackageName().isEmpty()) { if (vuln.getFiles() != null && !vuln.getFiles().isEmpty()) { String firstFilePath = vuln.getFiles().get(0).getName(); - vuln.setProjectName(initPackageName(firstFilePath)); + vuln.setPackageName(initPackageName(firstFilePath)); } } } diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/filter/engine/FilterEngineTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/filter/engine/FilterEngineTest.java index e2900bcf4b..9d8d77442b 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/filter/engine/FilterEngineTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/filter/engine/FilterEngineTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import com.fortify.cli.aviator.audit.model.StackTraceElement; import com.fortify.cli.aviator.fpr.Vulnerability; import com.fortify.cli.aviator.fpr.filter.Filter; import com.fortify.cli.aviator.fpr.filter.FilterSet; @@ -147,6 +148,102 @@ void testAnalysisTypeFilterMatchesOnlyMatchingVulnerability() { assertEquals("SCA_VULN", result.get(0).getInstanceID()); } + @Test + void testStreamingBackedAttributesAreFilterable() { + Vulnerability matchingVuln = new Vulnerability(); + matchingVuln.setInstanceID("MATCHING_VULN"); + matchingVuln.setClassID("RULE-123"); + matchingVuln.setKingdom("Dataflow"); + matchingVuln.setProbability(2.5); + matchingVuln.setRequestBody("username=admin"); + matchingVuln.setRequestMethod("POST"); + matchingVuln.setAttackPayload("drop table users"); + matchingVuln.setResponse("HTTP/1.1 500 Internal Server Error"); + matchingVuln.setSource(new StackTraceElement("src/main/java/com/acme/SqlExample.java", 41, "dangerousCall()", "Call", null, "", "")); + + Vulnerability nonMatchingVuln = new Vulnerability(); + nonMatchingVuln.setInstanceID("NON_MATCHING_VULN"); + nonMatchingVuln.setClassID("RULE-999"); + nonMatchingVuln.setKingdom("Control Flow"); + nonMatchingVuln.setProbability(0.5); + nonMatchingVuln.setRequestBody("username=guest"); + nonMatchingVuln.setRequestMethod("GET"); + nonMatchingVuln.setAttackPayload("benign payload"); + nonMatchingVuln.setResponse("HTTP/1.1 200 OK"); + nonMatchingVuln.setSource(new StackTraceElement("src/test/java/com/acme/SafeExample.java", 12, "safeCall()", "Call", null, "", "")); + + List vulnerabilities = List.of(matchingVuln, nonMatchingVuln); + + assertMatchesOnly("ruleid:RULE-123", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("kingdom:Dataflow", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("probability:[2.0,3.0]", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("body:username=admin", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("method:POST", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("[attack payload]:\"drop table users\"", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("response:500 Internal Server Error", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("file:SqlExample.java", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("sourcefile:src/main/java/com/acme/SqlExample.java", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("shortfilename:SqlExample.java", vulnerabilities, "MATCHING_VULN"); + } + + @Test + void testCodeSnippetFilterUsesSourceCode() { + Vulnerability matchingVuln = new Vulnerability(); + matchingVuln.setInstanceID("MATCHING_VULN"); + matchingVuln.setSource(new StackTraceElement("src/Main.java", 42, "dangerousCall()", "Call", null, "", "")); + + Vulnerability nonMatchingVuln = new Vulnerability(); + nonMatchingVuln.setInstanceID("NON_MATCHING_VULN"); + nonMatchingVuln.setSource(new StackTraceElement("src/Main.java", 43, "safeCall()", "Call", null, "", "")); + + assertMatchesOnly( + "codesnippet:\"dangerousCall()\"", + List.of(matchingVuln, nonMatchingVuln), + "MATCHING_VULN" + ); + } + + @Test + void testVirtualConfidenceFilters() { + Vulnerability mediumVirtConf = new Vulnerability(); + mediumVirtConf.setInstanceID("MEDIUM_VIRT_CONF"); + mediumVirtConf.setMinVirtualCallConfidence(0.58); + + Vulnerability highVirtConf = new Vulnerability(); + highVirtConf.setInstanceID("HIGH_VIRT_CONF"); + highVirtConf.setMinVirtualCallConfidence(0.95); + + List vulnerabilities = List.of(mediumVirtConf, highVirtConf); + + assertMatchesOnly("virtconf:0.58", vulnerabilities, "MEDIUM_VIRT_CONF"); + assertMatchesOnly("maxVirtConf:0.60", vulnerabilities, "MEDIUM_VIRT_CONF"); + assertMatchesOnly("minVirtConf:0.90", vulnerabilities, "HIGH_VIRT_CONF"); + } + + @Test + void testLegacyFortifyAliasesRemainSupported() { + Vulnerability matchingVuln = new Vulnerability(); + matchingVuln.setInstanceID("MATCHING_VULN"); + matchingVuln.getKnowledge().put("EnginePriority", "5"); + matchingVuln.getKnowledge().put("IssueAge", "7"); + matchingVuln.getKnowledge().put("MappedCategory", "OWASP A1"); + matchingVuln.getKnowledge().put("SecondaryRequests", "3"); + + Vulnerability nonMatchingVuln = new Vulnerability(); + nonMatchingVuln.setInstanceID("NON_MATCHING_VULN"); + nonMatchingVuln.getKnowledge().put("EnginePriority", "2"); + nonMatchingVuln.getKnowledge().put("IssueAge", "1"); + nonMatchingVuln.getKnowledge().put("MappedCategory", "OWASP A3"); + nonMatchingVuln.getKnowledge().put("SecondaryRequests", "0"); + + List vulnerabilities = List.of(matchingVuln, nonMatchingVuln); + + assertMatchesOnly("[engine priority]:[4,5]", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("[issue age]:[6,8]", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("[mapped category]:\"OWASP A1\"", vulnerabilities, "MATCHING_VULN"); + assertMatchesOnly("[secondary requests]:[2,4]", vulnerabilities, "MATCHING_VULN"); + } + // Null Attr Handling @Test void testNullAttrNotContains() { @@ -156,6 +253,12 @@ void testNullAttrNotContains() { assertEquals(1, result.size()); // !false (null not contains) = true, matches } + private void assertMatchesOnly(String query, List vulnerabilities, String expectedInstanceId) { + List result = VulnerabilityFilterer.filter(vulnerabilities, query); + assertEquals(1, result.size(), "Unexpected match count for query: " + query); + assertEquals(expectedInstanceId, result.get(0).getInstanceID(), "Unexpected match for query: " + query); + } + private Filter createFolderFilter(String query) { Filter f = new Filter(); f.setAction("setFolder"); f.setQuery(query); return f; } private Filter createHideFilter(String query) { Filter f = new Filter(); f.setAction("hide"); f.setQuery(query); return f; } } diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/FPRProcessorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/FPRProcessorTest.java new file mode 100644 index 0000000000..7b7a94f007 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/FPRProcessorTest.java @@ -0,0 +1,176 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import com.fortify.cli.aviator.fpr.filter.VulnerabilityFilterer; +import com.fortify.cli.aviator.fpr.model.AuditIssue; +import com.fortify.cli.aviator.fpr.processor.AuditProcessor; +import com.fortify.cli.aviator.fpr.processor.StreamingFVDLProcessor; +import com.fortify.cli.aviator.util.Constants; +import com.fortify.cli.aviator.util.FprHandle; + +class FPRProcessorTest { + private static final String CUSTOM_TAG_ID = "11111111-2222-3333-4444-555555555555"; + + private Path tempFprFile; + private FprHandle fprHandle; + + @AfterEach + void tearDown() throws Exception { + if (fprHandle != null) { + fprHandle.close(); + } + if (tempFprFile != null) { + Files.deleteIfExists(tempFprFile); + } + } + + @Test + void testProcessMergesAuditDerivedStateIntoStreamingVulnerabilities() throws Exception { + createTestFpr(minimalAuditFvdl()); + + AuditIssue auditIssue = AuditIssue.builder() + .instanceId("instance-1") + .suppressed(true) + .tags(Map.of(Constants.AUDITOR_STATUS_TAG_ID, Constants.NOT_AN_ISSUE)) + .threadedComments(List.of(AuditIssue.Comment.builder() + .content("Reviewed by analyst") + .username("analyst") + .timestamp("2026-04-22T00:00:00Z") + .build())) + .build(); + + FPRProcessor fprProcessor = new FPRProcessor(fprHandle, Map.of("instance-1", auditIssue), null); + List vulnerabilities = fprProcessor.process(new StreamingFVDLProcessor(fprHandle)); + + assertEquals(1, vulnerabilities.size()); + + Vulnerability vulnerability = vulnerabilities.get(0); + assertTrue(vulnerability.isSuppressed()); + assertTrue(vulnerability.isAudited()); + assertEquals(Constants.NOT_AN_ISSUE, vulnerability.getIssueStatus()); + assertEquals("Reviewed by analyst", vulnerability.getLastComment()); + assertEquals(1, VulnerabilityFilterer.filter(vulnerabilities, "suppressed:true").size()); + assertEquals(1, VulnerabilityFilterer.filter(vulnerabilities, "audited:true").size()); + assertEquals(1, VulnerabilityFilterer.filter(vulnerabilities, "[issue status]:\"Not an Issue\"").size()); + assertEquals(1, VulnerabilityFilterer.filter(vulnerabilities, "commentuser:analyst").size()); + assertEquals(1, VulnerabilityFilterer.filter(vulnerabilities, "historyuser:analyst").size()); + } + + @Test + void testProcessDoesNotMarkCommentOnlyIssueAsAudited() throws Exception { + createTestFpr(minimalAuditFvdl()); + + AuditIssue auditIssue = AuditIssue.builder() + .instanceId("instance-1") + .threadedComments(List.of(AuditIssue.Comment.builder() + .content("Investigating") + .username("analyst") + .timestamp("2026-04-22T00:00:00Z") + .build())) + .build(); + + Vulnerability vulnerability = processSingleVulnerability(auditIssue); + + assertFalse(vulnerability.isAudited()); + } + + @Test + void testProcessDoesNotMarkCustomTagOnlyIssueAsAudited() throws Exception { + createTestFpr(minimalAuditFvdl()); + + AuditIssue auditIssue = AuditIssue.builder() + .instanceId("instance-1") + .tags(Map.of(CUSTOM_TAG_ID, "High")) + .build(); + + Vulnerability vulnerability = processSingleVulnerability(auditIssue); + + assertFalse(vulnerability.isAudited()); + } + + @Test + void testProcessDoesNotMarkPendingReviewDefaultAuditorStatusAsAudited() throws Exception { + createTestFpr(minimalAuditFvdl()); + + AuditIssue auditIssue = AuditIssue.builder() + .instanceId("instance-1") + .tags(Map.of(Constants.AUDITOR_STATUS_TAG_ID, Constants.PENDING_REVIEW)) + .build(); + + Vulnerability vulnerability = processSingleVulnerability(auditIssue); + + assertFalse(vulnerability.isAudited()); + } + + private String minimalAuditFvdl() { + return """ + + + + + + RULE-1 + Dataflow + Cross-Site Scripting + Reflected + Dataflow + 3.0 + + + instance-1 + 3.0 + 4.0 + + + + + """; + } + + private void createTestFpr(String auditFvdlXml) throws Exception { + tempFprFile = Files.createTempFile("fpr-processor", ".fpr"); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(tempFprFile))) { + zipOutputStream.putNextEntry(new ZipEntry("audit.fvdl")); + zipOutputStream.write(auditFvdlXml.getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + + zipOutputStream.putNextEntry(new ZipEntry("src-archive/index.xml")); + zipOutputStream.write("".getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + } + fprHandle = new FprHandle(tempFprFile); + } + + private Vulnerability processSingleVulnerability(AuditIssue auditIssue) throws Exception { + FPRProcessor fprProcessor = new FPRProcessor(fprHandle, Map.of("instance-1", auditIssue), new AuditProcessor(fprHandle)); + List vulnerabilities = fprProcessor.process(new StreamingFVDLProcessor(fprHandle)); + assertEquals(1, vulnerabilities.size()); + return vulnerabilities.get(0); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/MetadataParserTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/MetadataParserTest.java new file mode 100644 index 0000000000..d85c86da63 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/MetadataParserTest.java @@ -0,0 +1,95 @@ +/* + * 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.processor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.StringReader; + +import javax.xml.stream.XMLInputFactory; + +import org.junit.jupiter.api.Test; + +import com.fortify.cli.aviator.fpr.model.FVDLMetadata; + +class MetadataParserTest { + private final XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); + private final MetadataParser metadataParser = new MetadataParser(); + + @Test + void testParseEngineDataExtractsRuleMetadataWithoutChangingDefaultAnalysisType() throws Exception { + String xml = """ + + 23.2.0.0123 + + + com.fortify.example + value + + + + + + Cross-Site Scripting + + + + + """; + + FVDLMetadata fvdlMetadata = parseEngineData(xml); + + assertEquals("23.2.0.0123", fvdlMetadata.getEngineVersion()); + assertEquals("SCA", fvdlMetadata.getAnalysisType()); + assertEquals("Cross-Site Scripting", fvdlMetadata.getRuleMetadata().get("RULE-1").get("Category")); + } + + @Test + void testParseEngineDataIgnoresEngineDataPropertyGroupsForAnalysisType() throws Exception { + String xml = """ + + + + java.version + 17 + + + + + com.fortify.example + value + + + + + com.fortify.locale + en + + + + """; + + FVDLMetadata fvdlMetadata = parseEngineData(xml); + + assertEquals("SCA", fvdlMetadata.getAnalysisType()); + } + + private FVDLMetadata parseEngineData(String xml) throws Exception { + FVDLMetadata fvdlMetadata = new FVDLMetadata(); + var reader = xmlInputFactory.createXMLStreamReader(new StringReader(xml)); + reader.nextTag(); + metadataParser.parseEngineData(reader, fvdlMetadata); + reader.close(); + return fvdlMetadata; + } +} \ No newline at end of file diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/StreamingFVDLProcessorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/StreamingFVDLProcessorTest.java new file mode 100644 index 0000000000..2ef464544f --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/StreamingFVDLProcessorTest.java @@ -0,0 +1,397 @@ +/* + * 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.processor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import com.fortify.cli.aviator.fpr.Vulnerability; +import com.fortify.cli.aviator.fpr.filter.VulnerabilityFilterer; +import com.fortify.cli.aviator.util.FprHandle; + +class StreamingFVDLProcessorTest { + private Path tempFprFile; + private FprHandle fprHandle; + + @AfterEach + void tearDown() throws Exception { + if (fprHandle != null) { + fprHandle.close(); + } + if (tempFprFile != null) { + Files.deleteIfExists(tempFprFile); + } + } + + @Test + void testParseUsesRunEngineNameAndIgnoresEngineDataPropertiesForAnalysisType() throws Exception { + String xml = """ + + + + + + com.fortify.example + value + + + + + PTA + + + + """; + + createTestFpr(xml); + + StreamingFVDLProcessor processor = new StreamingFVDLProcessor(fprHandle); + try (ZipFile zipFile = new ZipFile(tempFprFile.toFile())) { + processor.parse(zipFile, "audit.fvdl"); + } + + assertEquals("PTA", processor.getFvdlMetadata().getAnalysisType()); + } + + @Test + void testParsePreservesRuleMetadataWithoutTracesAndKeepsDefaultAnalysisType() throws Exception { + String xml = """ + + + + + + + 79 + Cross-Site Scripting + + + + + + + + RULE-1 + Dataflow + Cross-Site Scripting + Reflected + Dataflow + 3.0 + + + instance-1 + 3.0 + 4.0 + + + + + """; + + createTestFpr(xml); + + StreamingFVDLProcessor processor = new StreamingFVDLProcessor(fprHandle); + try (ZipFile zipFile = new ZipFile(tempFprFile.toFile())) { + processor.parse(zipFile, "audit.fvdl"); + } + + assertEquals(1, processor.getVulnerabilities().size()); + + Vulnerability vulnerability = processor.getVulnerabilities().get(0); + assertEquals("SCA", vulnerability.getAnalysisType()); + assertEquals("79", vulnerability.getKnowledge().get("CWE")); + assertEquals("Cross-Site Scripting", vulnerability.getKnowledge().get("Category")); + assertNotNull(vulnerability.getKnowledge()); + assertEquals(1, VulnerabilityFilterer.filter(processor.getVulnerabilities(), "cwe:79").size()); + } + + @Test + void testParseExposesMinVirtualCallConfidenceForFiltering() throws Exception { + String xml = """ + + + + + + RULE-1 + Dataflow + Cross-Site Scripting + Reflected + 3.0 + + + instance-1 + 3.0 + 4.0 + + + + + """; + + createTestFpr(xml); + + StreamingFVDLProcessor processor = new StreamingFVDLProcessor(fprHandle); + try (ZipFile zipFile = new ZipFile(tempFprFile.toFile())) { + processor.parse(zipFile, "audit.fvdl"); + } + + Vulnerability vulnerability = processor.getVulnerabilities().get(0); + assertEquals(0.58, vulnerability.getMinVirtualCallConfidence()); + assertEquals(1, VulnerabilityFilterer.filter(processor.getVulnerabilities(), "virtconf:0.58").size()); + assertEquals(1, VulnerabilityFilterer.filter(processor.getVulnerabilities(), "maxVirtConf:0.60").size()); + } + + @Test + void testCreatePackageNameMatchesModelSemantics() { + String filename = "leffe/rules/runtime/hybrid/head/demo/dotnet/CommerceAD/App_Code/Components/CustomersDB.cs"; + assertEquals( + "leffe.rules.runtime.hybrid.head.demo.dotnet.CommerceAD.App_Code.Components", + StreamingFVDLProcessor.createPackageName(filename) + ); + } + + @Test + void testParseExposesContextBackedAttributesForFiltering() throws Exception { + String xml = """ + + + + + + RULE-1 + Dataflow + SQL Injection + Dynamic + Dataflow + 3.0 + + + instance-1 + 3.0 + 4.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + Assignment to input + + + + executeQuery(0) + + + + """; + + createTestFpr(xml); + + StreamingFVDLProcessor processor = new StreamingFVDLProcessor(fprHandle); + try (ZipFile zipFile = new ZipFile(tempFprFile.toFile())) { + processor.parse(zipFile, "audit.fvdl"); + } + + Vulnerability vulnerability = processor.getVulnerabilities().get(0); + assertEquals("com.example.issue", vulnerability.getPackageName()); + assertEquals("IssueController", vulnerability.getClassName()); + assertEquals("com.example.source.SourceController.readInput()", vulnerability.getSourceFunction()); + assertEquals("com.example.sink.SqlSink.executeQuery()", vulnerability.getSinkFunction()); + assertEquals("com.example.source.SourceController.readInput", vulnerability.getSourceContext()); + assertEquals("com.example.sink.SqlSink.executeQuery", vulnerability.getSinkContext()); + assertEquals(1, VulnerabilityFilterer.filter(processor.getVulnerabilities(), "package:com.example.issue").size()); + assertEquals(1, VulnerabilityFilterer.filter(processor.getVulnerabilities(), "class:IssueController").size()); + assertEquals(1, VulnerabilityFilterer.filter(processor.getVulnerabilities(), "source:readInput").size()); + assertEquals(1, VulnerabilityFilterer.filter(processor.getVulnerabilities(), "sink:executeQuery").size()); + assertEquals(1, VulnerabilityFilterer.filter(processor.getVulnerabilities(), "[source context]:com.example.source.SourceController.readInput").size()); + assertEquals(1, VulnerabilityFilterer.filter(processor.getVulnerabilities(), "[sink context]:com.example.sink.SqlSink.executeQuery").size()); + } + + @Test + void testParseUsesFirstTraceForSourceContextAndLastTraceForSinkContext() throws Exception { + String xml = """ + + + + + + RULE-1 + Dataflow + SQL Injection + Dynamic + Dataflow + 3.0 + + + instance-1 + 3.0 + 4.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Assignment to input + + + + sanitize(0) + + + + prepareQuery(0) + + + + executeQuery(0) + + + + """; + + createTestFpr(xml); + + StreamingFVDLProcessor processor = new StreamingFVDLProcessor(fprHandle); + try (ZipFile zipFile = new ZipFile(tempFprFile.toFile())) { + processor.parse(zipFile, "audit.fvdl"); + } + + Vulnerability vulnerability = processor.getVulnerabilities().get(0); + assertEquals("com.example.source.SourceController.readInput", vulnerability.getSourceContext()); + assertEquals("com.example.sink.SqlSink.executeQuery", vulnerability.getSinkContext()); + assertEquals("com.example.source.SourceController.readInput()", vulnerability.getSourceFunction()); + assertEquals("com.example.sink.SqlSink.executeQuery()", vulnerability.getSinkFunction()); + } + + @Test + void testParseExposesRequestIdFromRequestHeadersForFiltering() throws Exception { + String xml = """ + + + + + + RULE-1 + Dataflow + SQL Injection + Dynamic + Dataflow + 3.0 + + + instance-1 + 3.0 + 4.0 + + + + + + + + + + + + + """; + + createTestFpr(xml); + + StreamingFVDLProcessor processor = new StreamingFVDLProcessor(fprHandle); + try (ZipFile zipFile = new ZipFile(tempFprFile.toFile())) { + processor.parse(zipFile, "audit.fvdl"); + } + + Vulnerability vulnerability = processor.getVulnerabilities().get(0); + assertEquals("ABC123", vulnerability.getAttributeValue("requestid")); + assertEquals(1, VulnerabilityFilterer.filter(processor.getVulnerabilities(), "[request id]:ABC123").size()); + } + + private void createTestFpr(String auditFvdlXml) throws Exception { + tempFprFile = Files.createTempFile("streaming-fvdl", ".fpr"); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(tempFprFile))) { + zipOutputStream.putNextEntry(new ZipEntry("audit.fvdl")); + zipOutputStream.write(auditFvdlXml.getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + + zipOutputStream.putNextEntry(new ZipEntry("src-archive/index.xml")); + zipOutputStream.write("".getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + } + fprHandle = new FprHandle(tempFprFile); + } +} \ No newline at end of file