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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,119 @@
* 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;

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<String> 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

parseFindings and parseVulnerabilities looks very similar. Can you merge them somehow?

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)
Expand All @@ -75,7 +135,7 @@ private static class Report {

@JacksonXmlProperty(localName = "Results")
@JacksonXmlElementWrapper(useWrapping = false)
List<EngineResults> engineResults;
List<EngineResults> engineResults = new ArrayList<>();

@JsonIgnoreProperties(ignoreUnknown = true)
private static class Stats {
Expand All @@ -92,7 +152,7 @@ private static class EngineResults {

@JacksonXmlProperty(localName = "Results")
@JacksonXmlElementWrapper(useWrapping = false)
List<Result> results;
List<Result> results = new ArrayList<>();

@JsonIgnoreProperties(ignoreUnknown = true)
private static class Result {
Expand All @@ -102,7 +162,12 @@ private static class Result {

@JacksonXmlElementWrapper(localName = "Vulnerabilities")
@JacksonXmlProperty(localName = "Vulnerability")
List<Vulnerability> vulnerabilities;
List<Vulnerability> vulnerabilities = new ArrayList<>();

// Findings replaced Vulnerabilities as Mend's finding element, since 2023
@JacksonXmlProperty(localName = "Findings")
@JacksonXmlElementWrapper(useWrapping = false)
List<Finding> findings = new ArrayList<>();

@JsonIgnoreProperties(ignoreUnknown = true)
private static class Type {
Expand Down Expand Up @@ -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;
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -92,6 +93,7 @@ public static List<Reader> allReaders() {
new KlocworkCSVReader(),
new KiuwanReader(),
new MendReader(),
new MendSarifReader(),
new NetsparkerReader(),
new NJSScanReader(),
new NoisyCricketReader(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* OWASP Benchmark Project
*
* <p>This file is part of the Open Web Application Security Project (OWASP) Benchmark Project For
* details, please see <a
* href="https://owasp.org/www-project-benchmark/">https://owasp.org/www-project-benchmark/</a>.
*
* <p>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.
*
* <p>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;

/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Guess this is AI generated comment. It does not add any value, so I propose to remove it.

* 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<String, Integer> customRuleCweMappings(JSONObject tool) {
Map<String, Integer> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,32 +30,56 @@

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

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