diff --git a/plugin/src/main/java/org/owasp/benchmarkutils/score/parsers/MendReader.java b/plugin/src/main/java/org/owasp/benchmarkutils/score/parsers/MendReader.java index e049a075..be4a21e3 100644 --- a/plugin/src/main/java/org/owasp/benchmarkutils/score/parsers/MendReader.java +++ b/plugin/src/main/java/org/owasp/benchmarkutils/score/parsers/MendReader.java @@ -12,7 +12,7 @@ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * - * @author Sascha Knoop + * @author Sascha Knoop, Jan Kühl * @created 2022 */ package org.owasp.benchmarkutils.score.parsers; @@ -20,51 +20,111 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import java.util.ArrayList; import java.util.List; +import java.util.Set; import org.owasp.benchmarkutils.score.BenchmarkScore; +import org.owasp.benchmarkutils.score.CweNumber; import org.owasp.benchmarkutils.score.ResultFile; import org.owasp.benchmarkutils.score.TestCaseResult; import org.owasp.benchmarkutils.score.TestSuiteResults; public class MendReader extends Reader { + /* legacy version */ + private static final String ROOT_NODE_REPORT_MODEL = "ReportModel"; + /* since 2023 */ + private static final String ROOT_NODE_REPORTS_WITH_PROJECT = "reportsWithProject"; + private static final Set SUPPORTED_ROOT_NODES = + Set.of(ROOT_NODE_REPORT_MODEL, ROOT_NODE_REPORTS_WITH_PROJECT); + @Override public boolean canRead(ResultFile resultFile) { return resultFile.filename().endsWith(".xml") - && resultFile.xmlRootNodeName().equals("ReportModel"); + && SUPPORTED_ROOT_NODES.contains(resultFile.xmlRootNodeName()); } @Override public TestSuiteResults parse(ResultFile resultFile) throws Exception { - TestSuiteResults tr = new TestSuiteResults("Mend", true, TestSuiteResults.ToolType.SAST); + TestSuiteResults tr = + new TestSuiteResults("Mend SAST", true, TestSuiteResults.ToolType.SAST); Report report = xmlMapper.readValue(resultFile.content(), Report.class); tr.setTime(report.stats.duration); + String rootNodeName = resultFile.xmlRootNodeName(); + for (Report.EngineResults engineResults : report.engineResults) { for (Report.EngineResults.Result result : engineResults.results) { - for (Report.EngineResults.Result.Vulnerability vulnerability : - result.vulnerabilities) { - try { - String testfile = extractFilenameWithoutEnding(vulnerability.filename); + switch (rootNodeName) { + case ROOT_NODE_REPORT_MODEL: + parseVulnerabilities(result, tr); + break; + case ROOT_NODE_REPORTS_WITH_PROJECT: + // Findings replaced Vulnerabilities as Mend's finding element, since 2023 + parseFindings(result, tr); + break; + default: + continue; + } + } + } + return tr; + } - if (testfile.startsWith(BenchmarkScore.TESTCASENAME)) { - TestCaseResult tcr = new TestCaseResult(); + private void parseVulnerabilities(Report.EngineResults.Result result, TestSuiteResults tr) { + for (Report.EngineResults.Result.Vulnerability vulnerability : result.vulnerabilities) { + try { + String testfile = extractFilenameWithoutEnding(vulnerability.filename); - tcr.setCategory(result.type.name); - tcr.setCWE(result.type.cwe.asNumber()); - tcr.setNumber(testNumber(testfile)); + if (testfile.startsWith(BenchmarkScore.TESTCASENAME)) { + TestCaseResult tcr = new TestCaseResult(); - tr.put(tcr); - } - } catch (Exception e) { - e.printStackTrace(); - } + tcr.setCategory(result.type.name); + tcr.setCWE(result.type.cwe.asNumber()); + tcr.setNumber(testNumber(testfile)); + + tr.put(tcr); } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void parseFindings(Report.EngineResults.Result result, TestSuiteResults tr) { + int cwe = mapCwe(result.type.cwe.asNumber()); + + for (Report.EngineResults.Result.Finding finding : result.findings) { + try { + String testfile = extractFilenameWithoutEnding(finding.sharedStep.file); + + if (testfile.startsWith(BenchmarkScore.TESTCASENAME)) { + TestCaseResult tcr = new TestCaseResult(); + + tcr.setCategory(result.type.name); + tcr.setCWE(cwe); + tcr.setNumber(testNumber(testfile)); + + tr.put(tcr); + } + } catch (Exception e) { + e.printStackTrace(); } } - return tr; + } + + // CWE remap required for the Findings-based report format, since 2023 + private static int mapCwe(int cwe) { + switch (cwe) { + case 338: + return CweNumber.WEAK_RANDOM; + case 1004: + return CweNumber.INSECURE_COOKIE; + default: + return cwe; + } } @JsonIgnoreProperties(ignoreUnknown = true) @@ -75,7 +135,7 @@ private static class Report { @JacksonXmlProperty(localName = "Results") @JacksonXmlElementWrapper(useWrapping = false) - List engineResults; + List engineResults = new ArrayList<>(); @JsonIgnoreProperties(ignoreUnknown = true) private static class Stats { @@ -92,7 +152,7 @@ private static class EngineResults { @JacksonXmlProperty(localName = "Results") @JacksonXmlElementWrapper(useWrapping = false) - List results; + List results = new ArrayList<>(); @JsonIgnoreProperties(ignoreUnknown = true) private static class Result { @@ -102,7 +162,12 @@ private static class Result { @JacksonXmlElementWrapper(localName = "Vulnerabilities") @JacksonXmlProperty(localName = "Vulnerability") - List vulnerabilities; + List vulnerabilities = new ArrayList<>(); + + // Findings replaced Vulnerabilities as Mend's finding element, since 2023 + @JacksonXmlProperty(localName = "Findings") + @JacksonXmlElementWrapper(useWrapping = false) + List findings = new ArrayList<>(); @JsonIgnoreProperties(ignoreUnknown = true) private static class Type { @@ -131,6 +196,21 @@ private static class Vulnerability { @JacksonXmlProperty(localName = "SinkFile") String filename; } + + // New finding element, since 2023 (replaces Vulnerability) + @JsonIgnoreProperties(ignoreUnknown = true) + private static class Finding { + + @JacksonXmlProperty(localName = "SharedStep") + SharedStep sharedStep; + + @JsonIgnoreProperties(ignoreUnknown = true) + private static class SharedStep { + + @JacksonXmlProperty(localName = "File") + String file; + } + } } } } diff --git a/plugin/src/main/java/org/owasp/benchmarkutils/score/parsers/Reader.java b/plugin/src/main/java/org/owasp/benchmarkutils/score/parsers/Reader.java index 39be5154..d3513708 100644 --- a/plugin/src/main/java/org/owasp/benchmarkutils/score/parsers/Reader.java +++ b/plugin/src/main/java/org/owasp/benchmarkutils/score/parsers/Reader.java @@ -31,11 +31,12 @@ import org.owasp.benchmarkutils.score.parsers.csv.SemgrepCSVReader; import org.owasp.benchmarkutils.score.parsers.csv.WhiteHatDynamicReader; import org.owasp.benchmarkutils.score.parsers.sarif.BanditReader; -import org.owasp.benchmarkutils.score.parsers.sarif.CogniumReader; import org.owasp.benchmarkutils.score.parsers.sarif.CodeQLReader; +import org.owasp.benchmarkutils.score.parsers.sarif.CogniumReader; import org.owasp.benchmarkutils.score.parsers.sarif.ContrastScanReader; import org.owasp.benchmarkutils.score.parsers.sarif.DatadogSastReader; import org.owasp.benchmarkutils.score.parsers.sarif.FortifySarifReader; +import org.owasp.benchmarkutils.score.parsers.sarif.MendSarifReader; import org.owasp.benchmarkutils.score.parsers.sarif.OpenTaintReader; import org.owasp.benchmarkutils.score.parsers.sarif.PTAIReader; import org.owasp.benchmarkutils.score.parsers.sarif.PrecautionReader; @@ -92,6 +93,7 @@ public static List allReaders() { new KlocworkCSVReader(), new KiuwanReader(), new MendReader(), + new MendSarifReader(), new NetsparkerReader(), new NJSScanReader(), new NoisyCricketReader(), diff --git a/plugin/src/main/java/org/owasp/benchmarkutils/score/parsers/sarif/MendSarifReader.java b/plugin/src/main/java/org/owasp/benchmarkutils/score/parsers/sarif/MendSarifReader.java new file mode 100644 index 00000000..2a6977bc --- /dev/null +++ b/plugin/src/main/java/org/owasp/benchmarkutils/score/parsers/sarif/MendSarifReader.java @@ -0,0 +1,99 @@ +/** + * OWASP Benchmark Project + * + *

This file is part of the Open Web Application Security Project (OWASP) Benchmark Project For + * details, please see https://owasp.org/www-project-benchmark/. + * + *

The OWASP Benchmark is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Foundation, version 2. + * + *

The OWASP Benchmark is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * @author Jan Kühl + * @created 2026 + */ +package org.owasp.benchmarkutils.score.parsers.sarif; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.owasp.benchmarkutils.score.CweNumber; +import org.owasp.benchmarkutils.score.ResultFile; +import org.owasp.benchmarkutils.score.TestSuiteResults; + +/** + * This reader is made for Mend SAST reports using the SARIF format. Mend rules don't carry a CWE + * tag or field - the CWE is only present as the trailing number of each rule's {@code helpUri} + * (e.g. https://cwe.mitre.org/data/definitions/78.html), so CWEs are scraped from there + * automatically instead of providing a mapping of Mend rule id to CWE. + */ +public class MendSarifReader extends SarifReader { + + public MendSarifReader() { + super("mend.sast.", true, CweSourceType.CUSTOM); + } + + @Override + public String toolName(ResultFile resultFile) { + return "Mend SAST"; + } + + /** + * Mend's SARIF driver reports its version as a descriptive, comma-separated string per language + * (e.g. {@code "26.6.1.2 (Java*)"}). + */ + @Override + public void setVersion(ResultFile resultFile, TestSuiteResults testSuiteResults) { + super.setVersion(resultFile, testSuiteResults); + + String version = testSuiteResults.getToolVersion(); + + if (version == null) { + return; + } + + Matcher matcher = Pattern.compile("^[0-9][0-9.]*").matcher(version); + + if (matcher.find()) { + testSuiteResults.setToolVersion(matcher.group()); + } + } + + @Override + public Map customRuleCweMappings(JSONObject tool) { + Map ruleCweMap = new HashMap<>(); + + JSONArray rules = tool.getJSONObject("driver").getJSONArray("rules"); + + for (int i = 0; i < rules.length(); i++) { + try { + JSONObject rule = rules.getJSONObject(i); + + ruleCweMap.put(rule.getString("id"), mapCwe(extractCwe(rule.getString("helpUri")))); + } catch (JSONException e) { + // Skip rules without a helpUri-based CWE reference. + } + } + + return ruleCweMap; + } + + @Override + public int mapCwe(int cwe) { + switch (cwe) { + case 338: + return CweNumber.WEAK_RANDOM; + case 1004: + return CweNumber.INSECURE_COOKIE; + default: + return cwe; + } + } +} diff --git a/plugin/src/test/java/org/owasp/benchmarkutils/score/parsers/MendReaderTest.java b/plugin/src/test/java/org/owasp/benchmarkutils/score/parsers/MendReaderTest.java index e998ee5f..54bc971e 100644 --- a/plugin/src/test/java/org/owasp/benchmarkutils/score/parsers/MendReaderTest.java +++ b/plugin/src/test/java/org/owasp/benchmarkutils/score/parsers/MendReaderTest.java @@ -12,7 +12,7 @@ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * - * @author Sascha Knoop + * @author Sascha Knoop, Jan Kühl * @created 2022 */ package org.owasp.benchmarkutils.score.parsers; @@ -30,27 +30,34 @@ public class MendReaderTest extends ReaderTestBase { - private ResultFile resultFile; + private ResultFile resultFileLegacy; + private ResultFile resultFile2023; @BeforeEach void setUp() { - resultFile = TestHelper.resultFileOf("testfiles/Benchmark_Mend.xml"); + resultFileLegacy = TestHelper.resultFileOf("testfiles/Benchmark_Mend.xml"); + resultFile2023 = TestHelper.resultFileOf("testfiles/Benchmark_Mend_2023.xml"); BenchmarkScore.TESTCASENAME = "BenchmarkTest"; } @Test - public void onlyMendReaderReportsCanReadAsTrue() { - assertOnlyMatcherClassIs(this.resultFile, MendReader.class); + public void onlyMendReaderReportsCanReadAsTrueForLegacyFormat() { + assertOnlyMatcherClassIs(this.resultFileLegacy, MendReader.class); } @Test - void readerHandlesGivenResultFile() throws Exception { + public void onlyMendReaderReportsCanReadAsTrueForFormat2023() { + assertOnlyMatcherClassIs(this.resultFile2023, MendReader.class); + } + + @Test + void readerHandlesLegacyReportFormat() throws Exception { MendReader reader = new MendReader(); - TestSuiteResults result = reader.parse(resultFile); + TestSuiteResults result = reader.parse(resultFileLegacy); assertEquals(TestSuiteResults.ToolType.SAST, result.getToolType()); assertTrue(result.isCommercial()); - assertEquals("Mend", result.getToolName()); + assertEquals("Mend SAST", result.getToolName()); assertEquals("01:23:45", result.getTime()); assertEquals(2, result.getTotalResults()); @@ -58,4 +65,21 @@ void readerHandlesGivenResultFile() throws Exception { assertEquals(CweNumber.SQL_INJECTION, result.get(1).get(0).getCWE()); assertEquals(CweNumber.COMMAND_INJECTION, result.get(2).get(0).getCWE()); } + + @Test + void readerHandlesReportFormat2023() throws Exception { + MendReader reader = new MendReader(); + TestSuiteResults result = reader.parse(resultFile2023); + + assertEquals(TestSuiteResults.ToolType.SAST, result.getToolType()); + assertTrue(result.isCommercial()); + assertEquals("Mend SAST", result.getToolName()); + assertEquals("01:23:45", result.getTime()); + + assertEquals(3, result.getTotalResults()); + + assertEquals(CweNumber.SQL_INJECTION, result.get(1).get(0).getCWE()); + assertEquals(CweNumber.COMMAND_INJECTION, result.get(2).get(0).getCWE()); + assertEquals(CweNumber.WEAK_RANDOM, result.get(3).get(0).getCWE()); + } } diff --git a/plugin/src/test/java/org/owasp/benchmarkutils/score/parsers/sarif/MendSarifReaderTest.java b/plugin/src/test/java/org/owasp/benchmarkutils/score/parsers/sarif/MendSarifReaderTest.java new file mode 100644 index 00000000..19618aef --- /dev/null +++ b/plugin/src/test/java/org/owasp/benchmarkutils/score/parsers/sarif/MendSarifReaderTest.java @@ -0,0 +1,59 @@ +/** + * OWASP Benchmark Project + * + *

This file is part of the Open Web Application Security Project (OWASP) Benchmark Project For + * details, please see https://owasp.org/www-project-benchmark/. + * + *

The OWASP Benchmark is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Foundation, version 2. + * + *

The OWASP Benchmark is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * @author Jan Kühl + * @created 2026 + */ +package org.owasp.benchmarkutils.score.parsers.sarif; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.owasp.benchmarkutils.score.*; +import org.owasp.benchmarkutils.score.parsers.ReaderTestBase; + +public class MendSarifReaderTest extends ReaderTestBase { + + private ResultFile resultFile; + + @BeforeEach + void setUp() { + resultFile = TestHelper.resultFileOf("testfiles/Benchmark_Mend.sarif"); + BenchmarkScore.TESTCASENAME = "BenchmarkTest"; + } + + @Test + public void onlyMendSarifReaderReportsCanReadAsTrue() { + assertOnlyMatcherClassIs(this.resultFile, MendSarifReader.class); + } + + @Test + void readerHandlesGivenResultFile() throws Exception { + MendSarifReader reader = new MendSarifReader(); + TestSuiteResults result = reader.parse(resultFile); + + assertEquals(TestSuiteResults.ToolType.SAST, result.getToolType()); + assertTrue(result.isCommercial()); + assertEquals("Mend SAST", result.getToolName()); + assertEquals("26.6.1.2", result.getToolVersion()); + + assertEquals(3, result.getTotalResults()); + + assertEquals(CweNumber.COMMAND_INJECTION, result.get(1).get(0).getCWE()); + assertEquals(CweNumber.SQL_INJECTION, result.get(2).get(0).getCWE()); + assertEquals(CweNumber.WEAK_RANDOM, result.get(3).get(0).getCWE()); + } +} diff --git a/plugin/src/test/resources/testfiles/Benchmark_Mend.sarif b/plugin/src/test/resources/testfiles/Benchmark_Mend.sarif new file mode 100644 index 00000000..69fc28ad --- /dev/null +++ b/plugin/src/test/resources/testfiles/Benchmark_Mend.sarif @@ -0,0 +1,163 @@ +{ + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "informationUri": "https://www.mend.io/sast/", + "name": "mend.sast.java-driver-exec.jar", + "rules": [ + { + "id": "java-cmd-inj", + "name": "Java-Driver-Exec.JarCommandInjection", + "shortDescription": { + "text": "Command Injection vulnerability (Java*)" + }, + "helpUri": "https://cwe.mitre.org/data/definitions/78.html", + "help": { + "text": "Command Injection vulnerability." + }, + "properties": { + "precision": "high" + } + }, + { + "id": "java-sqli", + "name": "Java-Driver-Exec.JarSqlInjection", + "shortDescription": { + "text": "SQL Injection vulnerability (Java*)" + }, + "helpUri": "https://cwe.mitre.org/data/definitions/89.html", + "help": { + "text": "SQL Injection vulnerability." + }, + "properties": { + "precision": "high" + } + }, + { + "id": "java-weak-rnd", + "name": "Java-Driver-Exec.JarWeakPseudoRandom", + "shortDescription": { + "text": "Weak Pseudo-Random vulnerability (Java*)" + }, + "helpUri": "https://cwe.mitre.org/data/definitions/338.html", + "help": { + "text": "Weak Pseudo-Random vulnerability." + }, + "properties": { + "precision": "high" + } + }, + { + "id": "java-predictable-seed", + "name": "Java-Driver-Exec.JarPredictableSeed", + "shortDescription": { + "text": "Predictable Seed vulnerability (Java*)" + }, + "helpUri": "https://cwe.mitre.org/data/definitions/335.html", + "help": { + "text": "Predictable Seed vulnerability." + }, + "properties": { + "precision": "high" + } + } + ], + "version": "26.6.1.2 (Java*)" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "startTimeUtc": "2026-07-13T09:24:55.479Z" + }, + { + "endTimeUtc": "2026-07-13T09:24:55.479Z", + "executionSuccessful": true + } + ], + "results": [ + { + "properties": { + "detectionTime": "2026-07-13T09:50:56.793Z", + "findingId": "11111111-1111-1111-1111-111111111111", + "isExploitable": false, + "probability": "HIGH", + "severity": "High" + }, + "ruleId": "java-cmd-inj", + "level": "error", + "message": { + "text": "'exec' method of 'java.lang.Runtime' object could be abused to perform an arbitrary Command Execution attack." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src/main/java/org/owasp/benchmark/testcode/BenchmarkTest00001.java" + }, + "region": { + "startLine": 90 + } + } + } + ] + }, + { + "properties": { + "detectionTime": "2026-07-13T09:50:56.793Z", + "findingId": "22222222-2222-2222-2222-222222222222", + "isExploitable": false, + "probability": "HIGH", + "severity": "High" + }, + "ruleId": "java-sqli", + "level": "error", + "message": { + "text": "Tainted input used to construct a SQL query." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src/main/java/org/owasp/benchmark/testcode/BenchmarkTest00002.java" + }, + "region": { + "startLine": 60 + } + } + } + ] + }, + { + "properties": { + "detectionTime": "2026-07-13T09:50:56.793Z", + "findingId": "33333333-3333-3333-3333-333333333333", + "isExploitable": false, + "probability": "HIGH", + "severity": "Medium" + }, + "ruleId": "java-weak-rnd", + "level": "warning", + "message": { + "text": "Use of a weak pseudo-random number generator." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src/main/java/org/owasp/benchmark/testcode/BenchmarkTest00003.java" + }, + "region": { + "startLine": 45 + } + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/plugin/src/test/resources/testfiles/Benchmark_Mend_2023.xml b/plugin/src/test/resources/testfiles/Benchmark_Mend_2023.xml new file mode 100644 index 00000000..d2d941fd --- /dev/null +++ b/plugin/src/test/resources/testfiles/Benchmark_Mend_2023.xml @@ -0,0 +1,60 @@ + + + 01:23:45 + + + Java + + + + CWE-78 + + + + + + + CWE-89 + CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') + https://cwe.mitre.org/data/definitions/89.html + + + + 11111111-1111-1111-1111-111111111111 + + src/main/java/org/owasp/benchmark/testcode/BenchmarkTest00001.java + + + + + + + CWE-78 + CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') + https://cwe.mitre.org/data/definitions/78.html + + + + 22222222-2222-2222-2222-222222222222 + + src/main/java/org/owasp/benchmark/testcode/BenchmarkTest00002.java + + + + + + + CWE-338 + CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG) + https://cwe.mitre.org/data/definitions/338.html + + + + 33333333-3333-3333-3333-333333333333 + + src/main/java/org/owasp/benchmark/testcode/BenchmarkTest00003.java + + + + +