Skip to content

Commit 6502d33

Browse files
authored
feat: add new suppression xsd allowing grouping of suppressions (#7957)
1 parent 5eeaa35 commit 6502d33

File tree

6 files changed

+345
-17
lines changed

6 files changed

+345
-17
lines changed

core/src/main/java/org/owasp/dependencycheck/xml/suppression/SuppressionHandler.java

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.ArrayList;
2121
import java.util.Calendar;
2222
import java.util.List;
23+
import java.util.Optional;
2324
import javax.annotation.concurrent.NotThreadSafe;
2425
import org.owasp.dependencycheck.exception.ParseException;
2526
import org.owasp.dependencycheck.utils.DateUtil;
@@ -30,7 +31,8 @@
3031
import org.xml.sax.helpers.DefaultHandler;
3132

3233
/**
33-
* A handler to load suppression rules.
34+
* A handler to load suppression rules. In the input xml a suppression rule can be part of a {@code suppressionGroup}. In that
35+
* case the attributes set on group element will act as default values for child suppressions.
3436
*
3537
* @author Jeremy Long
3638
*/
@@ -42,6 +44,10 @@ public class SuppressionHandler extends DefaultHandler {
4244
*/
4345
private static final Logger LOGGER = LoggerFactory.getLogger(SuppressionHandler.class);
4446

47+
/**
48+
* The suppressionGroup node, indicates the start of a new suppressionGroup.
49+
*/
50+
public static final String SUPPRESSION_GROUP = "suppressionGroup";
4551
/**
4652
* The suppress node, indicates the start of a new rule.
4753
*/
@@ -105,6 +111,10 @@ public class SuppressionHandler extends DefaultHandler {
105111
*/
106112
private StringBuilder currentText;
107113

114+
private Boolean groupBase = null;
115+
private Calendar groupUntil = null;
116+
117+
108118
/**
109119
* Get the value of suppressionRules.
110120
*
@@ -127,22 +137,40 @@ public List<SuppressionRule> getSuppressionRules() {
127137
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
128138
currentAttributes = attributes;
129139
currentText = new StringBuilder();
140+
141+
if (SUPPRESSION_GROUP.equals(qName)) {
142+
groupBase = attributes.getValue("base") != null ? Boolean.parseBoolean(attributes.getValue("base")) : null;
143+
groupUntil = parseUntilAttribute(attributes).orElse(null);
144+
}
145+
130146
if (SUPPRESS.equals(qName)) {
147+
Boolean base = attributes.getValue("base") != null ? Boolean.parseBoolean(attributes.getValue("base")) : null;
148+
Calendar until = parseUntilAttribute(attributes).orElse(null);
149+
131150
rule = new SuppressionRule();
132-
final String base = currentAttributes.getValue("base");
133-
if (base != null) {
134-
rule.setBase(Boolean.parseBoolean(base));
135-
} else {
136-
rule.setBase(false);
137-
}
138-
final String until = currentAttributes.getValue("until");
139-
if (until != null) {
140-
try {
141-
rule.setUntil(DateUtil.parseXmlDate(until));
142-
} catch (ParseException ex) {
143-
throw new SAXException("Unable to parse until date in suppression file: " + until, ex);
144-
}
151+
//If suppression doesn't have attribute set, use that of the group (if in group).
152+
rule.setBase(base != null ? base : groupBase);
153+
rule.setUntil(until != null ? until : groupUntil);
154+
}
155+
}
156+
157+
/**
158+
* Read the provided {@code attributes} for attribute {@code until}. Return {@link Calendar} object if attribute is
159+
* present and can be parsed.
160+
*
161+
* @return empty if attribute {@code until} is not present.
162+
* @throws SAXException if attribute {@code until} is present but value can not be parsed as {@link Calendar}.
163+
*/
164+
private static Optional<Calendar> parseUntilAttribute(Attributes attributes) throws SAXException {
165+
String untilStr = attributes.getValue("until");
166+
if (untilStr != null) {
167+
try {
168+
return Optional.of(DateUtil.parseXmlDate(untilStr));
169+
} catch (ParseException ex) {
170+
throw new SAXException("Unable to parse attribute 'until': " + untilStr, ex);
145171
}
172+
} else {
173+
return Optional.empty();
146174
}
147175
}
148176

@@ -166,6 +194,10 @@ public void endElement(String uri, String localName, String qName) throws SAXExc
166194
}
167195
rule = null;
168196
break;
197+
case SUPPRESSION_GROUP:
198+
groupBase = null;
199+
groupUntil = null;
200+
break;
169201
case FILE_PATH:
170202
rule.setFilePath(processPropertyType());
171203
break;
@@ -191,7 +223,10 @@ public void endElement(String uri, String localName, String qName) throws SAXExc
191223
rule.addVulnerabilityName(processPropertyType());
192224
break;
193225
case NOTES:
194-
rule.setNotes(currentText.toString().trim());
226+
// Check that the notes element is from a suppression and not a suppressionGroup.
227+
if(rule != null) {
228+
rule.setNotes(currentText.toString().trim());
229+
}
195230
break;
196231
case CVSS_BELOW:
197232
final Double cvss = Double.valueOf(currentText.toString().trim());

core/src/main/java/org/owasp/dependencycheck/xml/suppression/SuppressionParser.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public class SuppressionParser {
5454
* The logger.
5555
*/
5656
private static final Logger LOGGER = LoggerFactory.getLogger(SuppressionParser.class);
57+
58+
/**
59+
* The suppression schema file location for v 1.4.
60+
*/
61+
public static final String SUPPRESSION_SCHEMA_1_4 = "schema/dependency-suppression.1.4.xsd";
5762
/**
5863
* The suppression schema file location for v 1.3.
5964
*/
@@ -101,6 +106,7 @@ public List<SuppressionRule> parseSuppressionRules(File file) throws Suppression
101106
public List<SuppressionRule> parseSuppressionRules(InputStream inputStream)
102107
throws SuppressionParseException, SAXException {
103108
try (
109+
InputStream schemaStream14 = FileUtils.getResourceAsStream(SUPPRESSION_SCHEMA_1_4);
104110
InputStream schemaStream13 = FileUtils.getResourceAsStream(SUPPRESSION_SCHEMA_1_3);
105111
InputStream schemaStream12 = FileUtils.getResourceAsStream(SUPPRESSION_SCHEMA_1_2);
106112
InputStream schemaStream11 = FileUtils.getResourceAsStream(SUPPRESSION_SCHEMA_1_1);
@@ -112,7 +118,7 @@ public List<SuppressionRule> parseSuppressionRules(InputStream inputStream)
112118
final String charsetName = bom == null ? defaultEncoding : bom.getCharsetName();
113119

114120
final SuppressionHandler handler = new SuppressionHandler();
115-
final SAXParser saxParser = XmlUtils.buildSecureSaxParser(schemaStream13, schemaStream12, schemaStream11, schemaStream10);
121+
final SAXParser saxParser = XmlUtils.buildSecureSaxParser(schemaStream14, schemaStream13, schemaStream12, schemaStream11, schemaStream10);
116122
final XMLReader xmlReader = saxParser.getXMLReader();
117123
xmlReader.setErrorHandler(new SuppressionErrorHandler());
118124
xmlReader.setContentHandler(handler);
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<xs:schema id="suppressions"
3+
xmlns:xs="http://www.w3.org/2001/XMLSchema"
4+
elementFormDefault="qualified"
5+
targetNamespace="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.4.xsd"
6+
xmlns:dc="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.4.xsd">
7+
8+
<xs:complexType name="regexStringType">
9+
<xs:simpleContent>
10+
<xs:extension base="xs:string">
11+
<xs:attribute name="regex" use="optional" type="xs:boolean" default="false"/>
12+
<xs:attribute name="caseSensitive" use="optional" type="xs:boolean" default="false"/>
13+
</xs:extension>
14+
</xs:simpleContent>
15+
</xs:complexType>
16+
<xs:simpleType name="cvssScoreType">
17+
<xs:restriction base="xs:decimal">
18+
<xs:minInclusive value="0"/>
19+
<xs:maxInclusive value="10"/>
20+
</xs:restriction>
21+
</xs:simpleType>
22+
<xs:simpleType name="cveType">
23+
<xs:restriction base="xs:string">
24+
<xs:pattern value="((\w+\-)?CVE\-\d\d\d\d\-\d+|\d+)"/>
25+
</xs:restriction>
26+
</xs:simpleType>
27+
<xs:simpleType name="sha1Type">
28+
<xs:restriction base="xs:string">
29+
<xs:pattern value="[a-fA-F0-9]{40}"/>
30+
</xs:restriction>
31+
</xs:simpleType>
32+
33+
<xs:complexType name="suppressType">
34+
<xs:sequence>
35+
<xs:sequence minOccurs="0" maxOccurs="1">
36+
<xs:element name="notes" type="xs:string"/>
37+
</xs:sequence>
38+
39+
<xs:choice minOccurs="0" maxOccurs="1">
40+
<xs:element name="filePath" type="dc:regexStringType"/>
41+
<xs:element name="sha1" type="dc:sha1Type"/>
42+
<xs:element name="gav" type="dc:regexStringType"/>
43+
<xs:element name="packageUrl" type="dc:regexStringType"/>
44+
</xs:choice>
45+
46+
<xs:choice minOccurs="1" maxOccurs="unbounded">
47+
<xs:element name="cpe" type="dc:regexStringType"/>
48+
<xs:element name="cve" type="dc:cveType"/>
49+
<xs:element name="vulnerabilityName" type="dc:regexStringType"/>
50+
<xs:element name="cwe" type="xs:positiveInteger"/>
51+
<xs:element name="cvssBelow" type="dc:cvssScoreType"/>
52+
</xs:choice>
53+
</xs:sequence>
54+
<xs:attribute name="until" type="xs:date"/>
55+
</xs:complexType>
56+
57+
<xs:complexType name="suppressionGroupType">
58+
<xs:sequence>
59+
<xs:sequence minOccurs="0" maxOccurs="1">
60+
<xs:element name="notes" type="xs:string"/>
61+
</xs:sequence>
62+
<xs:element name="suppress" minOccurs="1" maxOccurs="unbounded">
63+
<xs:complexType>
64+
<xs:complexContent>
65+
<xs:extension base="dc:suppressType">
66+
<!-- Suppression has no default value for "base" when part of a group. Group value is used as default. -->
67+
<xs:attribute name="base" type="xs:boolean"/>
68+
</xs:extension>
69+
</xs:complexContent>
70+
</xs:complexType>
71+
</xs:element>
72+
</xs:sequence>
73+
<xs:attribute name="name" type="xs:string"/>
74+
<xs:attribute name="until" type="xs:date"/>
75+
<xs:attribute name="base" type="xs:boolean" default="false"/>
76+
</xs:complexType>
77+
78+
<xs:element name="suppressions">
79+
<xs:complexType>
80+
<xs:choice minOccurs="0" maxOccurs="unbounded">
81+
<xs:element name="suppress" >
82+
<xs:complexType>
83+
<xs:complexContent>
84+
<xs:extension base="dc:suppressType">
85+
<!-- Default values present in standalone suppression -->
86+
<xs:attribute use="optional" name="base" default="false"/>
87+
</xs:extension>
88+
</xs:complexContent>
89+
</xs:complexType>
90+
</xs:element>
91+
<xs:element name="suppressionGroup" type="dc:suppressionGroupType"/>
92+
</xs:choice>
93+
</xs:complexType>
94+
</xs:element>
95+
</xs:schema>

core/src/test/java/org/owasp/dependencycheck/xml/suppression/SuppressionParserTest.java

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@
2121
import org.owasp.dependencycheck.BaseTest;
2222

2323
import java.io.File;
24+
import java.time.Instant;
25+
import java.time.LocalDate;
26+
import java.time.ZoneOffset;
2427
import java.util.List;
28+
import java.util.stream.Collectors;
2529

26-
import static org.junit.jupiter.api.Assertions.assertEquals;
30+
import static org.junit.jupiter.api.Assertions.*;
2731

2832
/**
2933
* Test of the suppression parser.
@@ -83,4 +87,78 @@ void testParseSuppressionRulesV1dot3() throws Exception {
8387
List<SuppressionRule> result = instance.parseSuppressionRules(file);
8488
assertEquals(4, result.size());
8589
}
90+
91+
/**
92+
* Test of parseSuppressionRules method, of class SuppressionParser for the
93+
* v1.4 suppression XML Schema.
94+
*/
95+
@Test
96+
void testParseSuppressionRulesV1dot4() throws SuppressionParseException {
97+
File file = BaseTest.getResourceAsFile(this, "suppressions_1_4.xml");
98+
SuppressionParser instance = new SuppressionParser();
99+
List<SuppressionRule> suppressionRules = instance.parseSuppressionRules(file);
100+
101+
assertEquals(7, suppressionRules.size());
102+
}
103+
104+
/**
105+
* Any content that follows Schema 1.3 is also valid content according to Schema 1.4
106+
*/
107+
@Test
108+
void testParseSuppressionRulesV1dot4BackwardsCompability() throws SuppressionParseException {
109+
// 'suppressions_1_4_no_groups.xml' has the same content as 'suppressions_1_3.xml'. But follows schema 1.4
110+
File file = BaseTest.getResourceAsFile(this, "suppressions_1_4_no_groups.xml");
111+
SuppressionParser instance = new SuppressionParser();
112+
List<SuppressionRule> suppressionRules = instance.parseSuppressionRules(file);
113+
114+
assertEquals(4, suppressionRules.size());
115+
}
116+
117+
/**
118+
* If a suppression is present in a group and does not have attributes set, then the ones from the group are used
119+
* as defaults.
120+
*/
121+
@Test
122+
void testParseSuppressionV1dot4Inherits() throws SuppressionParseException {
123+
File file = BaseTest.getResourceAsFile(this, "suppressions_1_4.xml");
124+
SuppressionParser instance = new SuppressionParser();
125+
List<SuppressionRule> suppressionRules = instance.parseSuppressionRules(file);
126+
127+
// CVE-2013-1338 in test xml has no attributes and should inherit the ones set on group level.
128+
List<SuppressionRule> filteredSuppressions = suppressionRules.stream().
129+
filter(s -> s.getCve().contains("CVE-2013-1338"))
130+
.collect(Collectors.toList());
131+
assertEquals(1, filteredSuppressions.size());
132+
SuppressionRule rule = filteredSuppressions.get(0);
133+
134+
Instant expectedTime = LocalDate.of(2026, 1, 1)
135+
.atStartOfDay(ZoneOffset.UTC)
136+
.toInstant();
137+
assertEquals(expectedTime, rule.getUntil().toInstant());
138+
assertTrue(rule.isBase());
139+
}
140+
141+
/**
142+
* If a suppression in a suppression group has attributes set, then those override those of the suppressionGroup.
143+
*/
144+
@Test
145+
void testParseSuppressionV1dot4AttributeOverrides() throws SuppressionParseException {
146+
File file = BaseTest.getResourceAsFile(this, "suppressions_1_4.xml");
147+
SuppressionParser instance = new SuppressionParser();
148+
List<SuppressionRule> suppressionRules = instance.parseSuppressionRules(file);
149+
150+
// CVE-2013-1339 in test xml has attribute {code (until="2027-01-01Z")} set and is present in suppressionGroup.
151+
List<SuppressionRule> filteredSuppressions = suppressionRules.stream().
152+
filter(s -> s.getCve().contains("CVE-2013-1339"))
153+
.collect(Collectors.toList());
154+
assertEquals(1, filteredSuppressions.size());
155+
SuppressionRule rule = filteredSuppressions.get(0);
156+
157+
Instant expectedTime = LocalDate.of(2027, 1, 1)
158+
.atStartOfDay(ZoneOffset.UTC)
159+
.toInstant();
160+
assertEquals(expectedTime, rule.getUntil().toInstant());
161+
assertFalse(rule.isBase());
162+
}
163+
86164
}

0 commit comments

Comments
 (0)