diff --git a/core/src/main/java/org/owasp/dependencycheck/xml/suppression/SuppressionHandler.java b/core/src/main/java/org/owasp/dependencycheck/xml/suppression/SuppressionHandler.java index 50f4df32a9f..4fc0bbb7493 100644 --- a/core/src/main/java/org/owasp/dependencycheck/xml/suppression/SuppressionHandler.java +++ b/core/src/main/java/org/owasp/dependencycheck/xml/suppression/SuppressionHandler.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.List; +import java.util.Optional; import javax.annotation.concurrent.NotThreadSafe; import org.owasp.dependencycheck.exception.ParseException; import org.owasp.dependencycheck.utils.DateUtil; @@ -30,7 +31,8 @@ import org.xml.sax.helpers.DefaultHandler; /** - * A handler to load suppression rules. + * A handler to load suppression rules. In the input xml a suppression rule can be part of a {@code suppressionGroup}. In that + * case the attributes set on group element will act as default values for child suppressions. * * @author Jeremy Long */ @@ -42,6 +44,10 @@ public class SuppressionHandler extends DefaultHandler { */ private static final Logger LOGGER = LoggerFactory.getLogger(SuppressionHandler.class); + /** + * The suppressionGroup node, indicates the start of a new suppressionGroup. + */ + public static final String SUPPRESSION_GROUP = "suppressionGroup"; /** * The suppress node, indicates the start of a new rule. */ @@ -105,6 +111,10 @@ public class SuppressionHandler extends DefaultHandler { */ private StringBuilder currentText; + private Boolean groupBase = null; + private Calendar groupUntil = null; + + /** * Get the value of suppressionRules. * @@ -127,22 +137,40 @@ public List getSuppressionRules() { public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { currentAttributes = attributes; currentText = new StringBuilder(); + + if (SUPPRESSION_GROUP.equals(qName)) { + groupBase = attributes.getValue("base") != null ? Boolean.parseBoolean(attributes.getValue("base")) : null; + groupUntil = parseUntilAttribute(attributes).orElse(null); + } + if (SUPPRESS.equals(qName)) { + Boolean base = attributes.getValue("base") != null ? Boolean.parseBoolean(attributes.getValue("base")) : null; + Calendar until = parseUntilAttribute(attributes).orElse(null); + rule = new SuppressionRule(); - final String base = currentAttributes.getValue("base"); - if (base != null) { - rule.setBase(Boolean.parseBoolean(base)); - } else { - rule.setBase(false); - } - final String until = currentAttributes.getValue("until"); - if (until != null) { - try { - rule.setUntil(DateUtil.parseXmlDate(until)); - } catch (ParseException ex) { - throw new SAXException("Unable to parse until date in suppression file: " + until, ex); - } + //If suppression doesn't have attribute set, use that of the group (if in group). + rule.setBase(base != null ? base : groupBase); + rule.setUntil(until != null ? until : groupUntil); + } + } + + /** + * Read the provided {@code attributes} for attribute {@code until}. Return {@link Calendar} object if attribute is + * present and can be parsed. + * + * @return empty if attribute {@code until} is not present. + * @throws SAXException if attribute {@code until} is present but value can not be parsed as {@link Calendar}. + */ + private static Optional parseUntilAttribute(Attributes attributes) throws SAXException { + String untilStr = attributes.getValue("until"); + if (untilStr != null) { + try { + return Optional.of(DateUtil.parseXmlDate(untilStr)); + } catch (ParseException ex) { + throw new SAXException("Unable to parse attribute 'until': " + untilStr, ex); } + } else { + return Optional.empty(); } } @@ -166,6 +194,10 @@ public void endElement(String uri, String localName, String qName) throws SAXExc } rule = null; break; + case SUPPRESSION_GROUP: + groupBase = null; + groupUntil = null; + break; case FILE_PATH: rule.setFilePath(processPropertyType()); break; @@ -191,7 +223,10 @@ public void endElement(String uri, String localName, String qName) throws SAXExc rule.addVulnerabilityName(processPropertyType()); break; case NOTES: - rule.setNotes(currentText.toString().trim()); + // Check that the notes element is from a suppression and not a suppressionGroup. + if(rule != null) { + rule.setNotes(currentText.toString().trim()); + } break; case CVSS_BELOW: final Double cvss = Double.valueOf(currentText.toString().trim()); diff --git a/core/src/main/java/org/owasp/dependencycheck/xml/suppression/SuppressionParser.java b/core/src/main/java/org/owasp/dependencycheck/xml/suppression/SuppressionParser.java index edafaa044b2..ed823bc6720 100644 --- a/core/src/main/java/org/owasp/dependencycheck/xml/suppression/SuppressionParser.java +++ b/core/src/main/java/org/owasp/dependencycheck/xml/suppression/SuppressionParser.java @@ -54,6 +54,11 @@ public class SuppressionParser { * The logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(SuppressionParser.class); + + /** + * The suppression schema file location for v 1.4. + */ + public static final String SUPPRESSION_SCHEMA_1_4 = "schema/dependency-suppression.1.4.xsd"; /** * The suppression schema file location for v 1.3. */ @@ -101,6 +106,7 @@ public List parseSuppressionRules(File file) throws Suppression public List parseSuppressionRules(InputStream inputStream) throws SuppressionParseException, SAXException { try ( + InputStream schemaStream14 = FileUtils.getResourceAsStream(SUPPRESSION_SCHEMA_1_4); InputStream schemaStream13 = FileUtils.getResourceAsStream(SUPPRESSION_SCHEMA_1_3); InputStream schemaStream12 = FileUtils.getResourceAsStream(SUPPRESSION_SCHEMA_1_2); InputStream schemaStream11 = FileUtils.getResourceAsStream(SUPPRESSION_SCHEMA_1_1); @@ -112,7 +118,7 @@ public List parseSuppressionRules(InputStream inputStream) final String charsetName = bom == null ? defaultEncoding : bom.getCharsetName(); final SuppressionHandler handler = new SuppressionHandler(); - final SAXParser saxParser = XmlUtils.buildSecureSaxParser(schemaStream13, schemaStream12, schemaStream11, schemaStream10); + final SAXParser saxParser = XmlUtils.buildSecureSaxParser(schemaStream14, schemaStream13, schemaStream12, schemaStream11, schemaStream10); final XMLReader xmlReader = saxParser.getXMLReader(); xmlReader.setErrorHandler(new SuppressionErrorHandler()); xmlReader.setContentHandler(handler); diff --git a/core/src/main/resources/schema/dependency-suppression.1.4.xsd b/core/src/main/resources/schema/dependency-suppression.1.4.xsd new file mode 100644 index 00000000000..09c648ca0f1 --- /dev/null +++ b/core/src/main/resources/schema/dependency-suppression.1.4.xsd @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/test/java/org/owasp/dependencycheck/xml/suppression/SuppressionParserTest.java b/core/src/test/java/org/owasp/dependencycheck/xml/suppression/SuppressionParserTest.java index a4f236271ed..8bcc0e9e832 100644 --- a/core/src/test/java/org/owasp/dependencycheck/xml/suppression/SuppressionParserTest.java +++ b/core/src/test/java/org/owasp/dependencycheck/xml/suppression/SuppressionParserTest.java @@ -21,9 +21,13 @@ import org.owasp.dependencycheck.BaseTest; import java.io.File; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.List; +import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; /** * Test of the suppression parser. @@ -83,4 +87,78 @@ void testParseSuppressionRulesV1dot3() throws Exception { List result = instance.parseSuppressionRules(file); assertEquals(4, result.size()); } + + /** + * Test of parseSuppressionRules method, of class SuppressionParser for the + * v1.4 suppression XML Schema. + */ + @Test + void testParseSuppressionRulesV1dot4() throws SuppressionParseException { + File file = BaseTest.getResourceAsFile(this, "suppressions_1_4.xml"); + SuppressionParser instance = new SuppressionParser(); + List suppressionRules = instance.parseSuppressionRules(file); + + assertEquals(7, suppressionRules.size()); + } + + /** + * Any content that follows Schema 1.3 is also valid content according to Schema 1.4 + */ + @Test + void testParseSuppressionRulesV1dot4BackwardsCompability() throws SuppressionParseException { + // 'suppressions_1_4_no_groups.xml' has the same content as 'suppressions_1_3.xml'. But follows schema 1.4 + File file = BaseTest.getResourceAsFile(this, "suppressions_1_4_no_groups.xml"); + SuppressionParser instance = new SuppressionParser(); + List suppressionRules = instance.parseSuppressionRules(file); + + assertEquals(4, suppressionRules.size()); + } + + /** + * If a suppression is present in a group and does not have attributes set, then the ones from the group are used + * as defaults. + */ + @Test + void testParseSuppressionV1dot4Inherits() throws SuppressionParseException { + File file = BaseTest.getResourceAsFile(this, "suppressions_1_4.xml"); + SuppressionParser instance = new SuppressionParser(); + List suppressionRules = instance.parseSuppressionRules(file); + + // CVE-2013-1338 in test xml has no attributes and should inherit the ones set on group level. + List filteredSuppressions = suppressionRules.stream(). + filter(s -> s.getCve().contains("CVE-2013-1338")) + .collect(Collectors.toList()); + assertEquals(1, filteredSuppressions.size()); + SuppressionRule rule = filteredSuppressions.get(0); + + Instant expectedTime = LocalDate.of(2026, 1, 1) + .atStartOfDay(ZoneOffset.UTC) + .toInstant(); + assertEquals(expectedTime, rule.getUntil().toInstant()); + assertTrue(rule.isBase()); + } + + /** + * If a suppression in a suppression group has attributes set, then those override those of the suppressionGroup. + */ + @Test + void testParseSuppressionV1dot4AttributeOverrides() throws SuppressionParseException { + File file = BaseTest.getResourceAsFile(this, "suppressions_1_4.xml"); + SuppressionParser instance = new SuppressionParser(); + List suppressionRules = instance.parseSuppressionRules(file); + + // CVE-2013-1339 in test xml has attribute {code (until="2027-01-01Z")} set and is present in suppressionGroup. + List filteredSuppressions = suppressionRules.stream(). + filter(s -> s.getCve().contains("CVE-2013-1339")) + .collect(Collectors.toList()); + assertEquals(1, filteredSuppressions.size()); + SuppressionRule rule = filteredSuppressions.get(0); + + Instant expectedTime = LocalDate.of(2027, 1, 1) + .atStartOfDay(ZoneOffset.UTC) + .toInstant(); + assertEquals(expectedTime, rule.getUntil().toInstant()); + assertFalse(rule.isBase()); + } + } diff --git a/core/src/test/resources/suppressions_1_4.xml b/core/src/test/resources/suppressions_1_4.xml new file mode 100644 index 00000000000..652f1a3a8a1 --- /dev/null +++ b/core/src/test/resources/suppressions_1_4.xml @@ -0,0 +1,71 @@ + + + + + + + .*\btest\.jar + cpe:/a:jboss:jboss + + + + + .*\btest\.jar + CVE-2013-1337 + + + + + 7 + + + + + + 384FAA82E193D4E4B0546059CA09572654BC3970 + CVE-2013-1337 + + + + + + Temporary Suppressions to be reexamined later + + + 384FAA82E193D4E4B0546059CA09572654BC3970 + CVE-2013-1338 + + + + + 384FAA82E193D4E4B0546059CA09572654BC3970 + CVE-2013-1339 + + + + + + + + c:\path\to\some.jar + cpe:/a:csv:csv:1.0 + + + + .*\btest\.jar + CVE-2013-1337 + + + + diff --git a/core/src/test/resources/suppressions_1_4_no_groups.xml b/core/src/test/resources/suppressions_1_4_no_groups.xml new file mode 100644 index 00000000000..4af449f9842 --- /dev/null +++ b/core/src/test/resources/suppressions_1_4_no_groups.xml @@ -0,0 +1,43 @@ + + + + + + c:\path\to\some.jar + cpe:/a:csv:csv:1.0 + + + + .*\btest\.jar + cpe:/a:jboss:jboss + + + + .*\btest\.jar + CVE-2013-1337 + + + + 384FAA82E193D4E4B0546059CA09572654BC3970 + CVE-2013-1337 + + + + 7 + +