View Javadoc
1   /*
2    * This file is part of dependency-check-core.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2013 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.xml.suppression;
19  
20  import java.util.ArrayList;
21  import java.util.Calendar;
22  import java.util.HashSet;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Set;
26  import javax.annotation.concurrent.NotThreadSafe;
27  import org.apache.commons.lang3.time.DateFormatUtils;
28  import org.owasp.dependencycheck.dependency.Dependency;
29  import org.owasp.dependencycheck.dependency.Vulnerability;
30  import org.owasp.dependencycheck.dependency.naming.CpeIdentifier;
31  import org.owasp.dependencycheck.dependency.naming.Identifier;
32  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  import us.springett.parsers.cpe.Cpe;
36  import us.springett.parsers.cpe.exceptions.CpeEncodingException;
37  
38  /**
39   *
40   * @author Jeremy Long
41   */
42  @NotThreadSafe
43  public class SuppressionRule {
44  
45      /**
46       * The Logger for use throughout the class.
47       */
48      private static final Logger LOGGER = LoggerFactory.getLogger(SuppressionRule.class);
49      /**
50       * The file path for the suppression.
51       */
52      private PropertyType filePath;
53  
54      /**
55       * The SHA1 hash.
56       */
57      private String sha1;
58      /**
59       * A list of CPEs to suppression
60       */
61      private List<PropertyType> cpe = new ArrayList<>();
62      /**
63       * The list of cvssBelow scores.
64       */
65      private List<Double> cvssBelow = new ArrayList<>();
66      /**
67       * The list of CWE entries to suppress.
68       */
69      private List<String> cwe = new ArrayList<>();
70      /**
71       * The list of CVE entries to suppress.
72       */
73      private List<String> cve = new ArrayList<>();
74      /**
75       * The list of vulnerability name entries to suppress.
76       */
77      private final List<PropertyType> vulnerabilityNames = new ArrayList<>();
78      /**
79       * A Maven GAV to suppression.
80       */
81      private PropertyType gav = null;
82      /**
83       * The list of vulnerability name entries to suppress.
84       */
85      private PropertyType packageUrl = null;
86      /**
87       * The notes added in suppression file
88       */
89  
90      private String notes;
91  
92      /**
93       * A flag indicating whether or not the suppression rule is a core/base rule
94       * that should not be included in the resulting report in the "suppressed"
95       * section.
96       */
97      private boolean base;
98  
99      /**
100      * A date until which the suppression is to be retained. This can be used to
101      * make a temporary suppression that auto-expires to suppress a CVE while
102      * waiting for the vulnerability fix of the dependency to be released.
103      */
104     private Calendar until;
105 
106     /**
107      * A flag whether or not the rule matched a dependency & CPE.
108      */
109     private boolean matched = false;
110 
111     /**
112      * Get the value of matched.
113      *
114      * @return the value of matched
115      */
116     public boolean isMatched() {
117         return matched;
118     }
119 
120     /**
121      * Set the value of matched.
122      *
123      * @param matched new value of matched
124      */
125     public void setMatched(boolean matched) {
126         this.matched = matched;
127     }
128 
129     /**
130      * Get the (@code{nullable}) value of until.
131      *
132      * @return the value of until
133      */
134     public Calendar getUntil() {
135         return until;
136     }
137 
138     /**
139      * Set the value of until.
140      *
141      * @param until new value of until
142      */
143     public void setUntil(Calendar until) {
144         this.until = until;
145     }
146 
147     /**
148      * Get the value of filePath.
149      *
150      * @return the value of filePath
151      */
152     public PropertyType getFilePath() {
153         return filePath;
154     }
155 
156     /**
157      * Set the value of filePath.
158      *
159      * @param filePath new value of filePath
160      */
161     public void setFilePath(PropertyType filePath) {
162         this.filePath = filePath;
163     }
164 
165     /**
166      * Get the value of sha1.
167      *
168      * @return the value of sha1
169      */
170     public String getSha1() {
171         return sha1;
172     }
173 
174     /**
175      * Set the value of SHA1.
176      *
177      * @param sha1 new value of SHA1
178      */
179     public void setSha1(String sha1) {
180         this.sha1 = sha1;
181     }
182 
183     /**
184      * Get the value of CPE.
185      *
186      * @return the value of CPE
187      */
188     public List<PropertyType> getCpe() {
189         return cpe;
190     }
191 
192     /**
193      * Set the value of CPE.
194      *
195      * @param cpe new value of CPE
196      */
197     public void setCpe(List<PropertyType> cpe) {
198         this.cpe = cpe;
199     }
200 
201     /**
202      * Adds the CPE to the CPE list.
203      *
204      * @param cpe the CPE to add
205      */
206     public void addCpe(PropertyType cpe) {
207         this.cpe.add(cpe);
208     }
209 
210     /**
211      * Adds the CPE to the CPE list.
212      *
213      * @param name the vulnerability name to add
214      */
215     public void addVulnerabilityName(PropertyType name) {
216         this.vulnerabilityNames.add(name);
217     }
218 
219     /**
220      * Returns whether or not this suppression rule as CPE entries.
221      *
222      * @return whether or not this suppression rule as CPE entries
223      */
224     public boolean hasCpe() {
225         return !cpe.isEmpty();
226     }
227 
228     /**
229      * Get the value of cvssBelow.
230      *
231      * @return the value of cvssBelow
232      */
233     public List<Double> getCvssBelow() {
234         return cvssBelow;
235     }
236 
237     /**
238      * Set the value of cvssBelow.
239      *
240      * @param cvssBelow new value of cvssBelow
241      */
242     public void setCvssBelow(List<Double> cvssBelow) {
243         this.cvssBelow = cvssBelow;
244     }
245 
246     /**
247      * Adds the CVSS to the cvssBelow list.
248      *
249      * @param cvss the CVSS to add
250      */
251     public void addCvssBelow(Double cvss) {
252         this.cvssBelow.add(cvss);
253     }
254 
255     /**
256      * Returns whether or not this suppression rule has CVSS suppression criteria.
257      *
258      * @return whether or not this suppression rule has CVSS suppression criteria.
259      */
260     public boolean hasCvssBelow() {
261         return !cvssBelow.isEmpty();
262     }
263 
264     /**
265      * Get the value of notes.
266      *
267      * @return the value of notes
268      */
269     public String getNotes() {
270         return notes;
271     }
272 
273     /**
274      * Set the value of notes.
275      *
276      * @param notes new value of notes
277      */
278     public void setNotes(String notes) {
279         this.notes = notes;
280     }
281 
282     /**
283      * Returns whether this suppression rule has notes entries.
284      *
285      * @return whether this suppression rule has notes entries
286      */
287     public boolean hasNotes() {
288         return !notes.isEmpty();
289     }
290 
291     /**
292      * Get the value of CWE.
293      *
294      * @return the value of CWE
295      */
296     public List<String> getCwe() {
297         return cwe;
298     }
299 
300     /**
301      * Set the value of CWE.
302      *
303      * @param cwe new value of CWE
304      */
305     public void setCwe(List<String> cwe) {
306         this.cwe = cwe;
307     }
308 
309     /**
310      * Adds the CWE to the CWE list.
311      *
312      * @param cwe the CWE to add
313      */
314     public void addCwe(String cwe) {
315         this.cwe.add(cwe);
316     }
317 
318     /**
319      * Returns whether this suppression rule has CWE entries.
320      *
321      * @return whether this suppression rule has CWE entries
322      */
323     public boolean hasCwe() {
324         return !cwe.isEmpty();
325     }
326 
327     /**
328      * Get the value of CVE.
329      *
330      * @return the value of CVE
331      */
332     public List<String> getCve() {
333         return cve;
334     }
335 
336     /**
337      * Set the value of CVE.
338      *
339      * @param cve new value of CVE
340      */
341     public void setCve(List<String> cve) {
342         this.cve = cve;
343     }
344 
345     /**
346      * Adds the CVE to the CVE list.
347      *
348      * @param cve the CVE to add
349      */
350     public void addCve(String cve) {
351         this.cve.add(cve);
352     }
353 
354     /**
355      * Returns whether this suppression rule has CVE entries.
356      *
357      * @return whether this suppression rule has CVE entries
358      */
359     public boolean hasCve() {
360         return !cve.isEmpty();
361     }
362 
363     /**
364      * Returns whether this suppression rule has vulnerabilityName entries.
365      *
366      * @return whether this suppression rule has vulnerabilityName entries
367      */
368     public boolean hasVulnerabilityName() {
369         return !vulnerabilityNames.isEmpty();
370     }
371 
372     /**
373      * Get the value of Maven GAV.
374      *
375      * @return the value of GAV
376      */
377     public PropertyType getGav() {
378         return gav;
379     }
380 
381     /**
382      * Set the value of Maven GAV.
383      *
384      * @param gav new value of Maven GAV
385      */
386     public void setGav(PropertyType gav) {
387         this.gav = gav;
388     }
389 
390     /**
391      * Returns whether or not this suppression rule as GAV entries.
392      *
393      * @return whether or not this suppression rule as GAV entries
394      */
395     public boolean hasGav() {
396         return gav != null;
397     }
398 
399     /**
400      * Set the value of Package URL.
401      *
402      * @param purl new value of package URL
403      */
404     public void setPackageUrl(PropertyType purl) {
405         this.packageUrl = purl;
406     }
407 
408     /**
409      * Returns whether or not this suppression rule as packageUrl entries.
410      *
411      * @return whether or not this suppression rule as packageUrl entries
412      */
413     public boolean hasPackageUrl() {
414         return packageUrl != null;
415     }
416 
417     /**
418      * Get the value of base.
419      *
420      * @return the value of base
421      */
422     public boolean isBase() {
423         return base;
424     }
425 
426     /**
427      * Set the value of base.
428      *
429      * @param base new value of base
430      */
431     public void setBase(boolean base) {
432         this.base = base;
433     }
434 
435     /**
436      * Processes a given dependency to determine if any CPE, CVE, CWE, or CVSS
437      * scores should be suppressed. If any should be, they are removed from the
438      * dependency.
439      *
440      * @param dependency a project dependency to analyze
441      */
442     public void process(Dependency dependency) {
443         if (filePath != null && !filePath.matches(dependency.getFilePath())) {
444             return;
445         }
446         if (sha1 != null && !sha1.equalsIgnoreCase(dependency.getSha1sum())) {
447             return;
448         }
449         if (hasGav()) {
450             final Iterator<Identifier> itr = dependency.getSoftwareIdentifiers().iterator();
451             boolean found = false;
452             while (itr.hasNext()) {
453                 final Identifier i = itr.next();
454                 if (identifierMatches(this.gav, i)) {
455                     found = true;
456                     break;
457                 }
458             }
459             if (!found) {
460                 return;
461             }
462         }
463         if (hasPackageUrl()) {
464             final Iterator<Identifier> itr = dependency.getSoftwareIdentifiers().iterator();
465             boolean found = false;
466             while (itr.hasNext()) {
467                 final Identifier i = itr.next();
468                 if (purlMatches(this.packageUrl, i)) {
469                     found = true;
470                     break;
471                 }
472             }
473             if (!found) {
474                 return;
475             }
476         }
477 
478         if (this.hasCpe()) {
479             final Set<Identifier> removalList = new HashSet<>();
480             for (Identifier i : dependency.getVulnerableSoftwareIdentifiers()) {
481                 for (PropertyType c : this.cpe) {
482                     if (identifierMatches(c, i)) {
483                         if (!isBase()) {
484                             matched = true;
485                             if (this.notes != null) {
486                                 i.setNotes(this.notes);
487                             }
488                             dependency.addSuppressedIdentifier(i);
489                         }
490                         removalList.add(i);
491                         break;
492                     }
493                 }
494             }
495             removalList.forEach(dependency::removeVulnerableSoftwareIdentifier);
496         }
497         if (hasCve() || hasVulnerabilityName() || hasCwe() || hasCvssBelow()) {
498             final Set<Vulnerability> removeVulns = new HashSet<>();
499             for (Vulnerability v : dependency.getVulnerabilities()) {
500                 boolean remove = false;
501                 for (String entry : this.cve) {
502                     if (entry.equalsIgnoreCase(v.getName())) {
503                         removeVulns.add(v);
504                         remove = true;
505                         break;
506                     }
507                 }
508                 if (!remove && this.cwe != null && !v.getCwes().isEmpty()) {
509                     for (String entry : this.cwe) {
510                         final String toMatch = String.format("CWE-%s", entry);
511                         if (v.getCwes().stream().anyMatch(toTest -> toMatch.regionMatches(0, toTest, 0, toMatch.length()))) {
512                             remove = true;
513                             removeVulns.add(v);
514                             break;
515                         }
516                     }
517                 }
518                 if (!remove && v.getName() != null) {
519                     for (PropertyType entry : this.vulnerabilityNames) {
520                         if (entry.matches(v.getName())) {
521                             remove = true;
522                             removeVulns.add(v);
523                             break;
524                         }
525                     }
526                 }
527                 if (!remove) {
528                     for (Double cvss : this.cvssBelow) {
529                         //TODO validate this comparison
530                         if (v.getCvssV2() != null && v.getCvssV2().getCvssData().getBaseScore().compareTo(cvss) < 0) {
531                             remove = true;
532                             removeVulns.add(v);
533                             break;
534                         }
535                         if (v.getCvssV3() != null && v.getCvssV3().getCvssData().getBaseScore().compareTo(cvss) < 0) {
536                             remove = true;
537                             removeVulns.add(v);
538                             break;
539                         }
540                     }
541                 }
542                 if (remove && !isBase()) {
543                     matched = true;
544                     if (this.notes != null) {
545                         v.setNotes(this.notes);
546                     }
547                     dependency.addSuppressedVulnerability(v);
548                 }
549             }
550             removeVulns.forEach(dependency::removeVulnerability);
551         }
552     }
553 
554     /**
555      * Identifies if the cpe specified by the cpe suppression rule does not
556      * specify a version.
557      *
558      * @param c a suppression rule identifier
559      * @return true if the property type does not specify a version; otherwise
560      * false
561      */
562     protected boolean cpeHasNoVersion(PropertyType c) {
563         return !c.isRegex() && countCharacter(c.getValue(), ':') <= 3;
564     }
565 
566     /**
567      * Counts the number of occurrences of the character found within the
568      * string.
569      *
570      * @param str the string to check
571      * @param c the character to count
572      * @return the number of times the character is found in the string
573      */
574     private int countCharacter(String str, char c) {
575         int count = 0;
576         int pos = str.indexOf(c) + 1;
577         while (pos > 0) {
578             count += 1;
579             pos = str.indexOf(c, pos) + 1;
580         }
581         return count;
582     }
583 
584     /**
585      * Determines if the cpeEntry specified as a PropertyType matches the given
586      * Identifier.
587      *
588      * @param suppressionEntry a suppression rule entry
589      * @param identifier a CPE identifier to check
590      * @return true if the entry matches; otherwise false
591      */
592     protected boolean purlMatches(PropertyType suppressionEntry, Identifier identifier) {
593         if (identifier instanceof PurlIdentifier) {
594             final PurlIdentifier purl = (PurlIdentifier) identifier;
595             return suppressionEntry.matches(purl.toString());
596         }
597         return false;
598     }
599 
600     /**
601      * Determines if the cpeEntry specified as a PropertyType matches the given
602      * Identifier.
603      *
604      * @param suppressionEntry a suppression rule entry
605      * @param identifier a CPE identifier to check
606      * @return true if the entry matches; otherwise false
607      */
608     protected boolean identifierMatches(PropertyType suppressionEntry, Identifier identifier) {
609         if (identifier instanceof PurlIdentifier) {
610             final PurlIdentifier purl = (PurlIdentifier) identifier;
611             return suppressionEntry.matches(purl.toGav());
612         } else if (identifier instanceof CpeIdentifier) {
613             //TODO check for regex - not just type
614             final Cpe cpeId = ((CpeIdentifier) identifier).getCpe();
615             if (suppressionEntry.isRegex()) {
616                 try {
617                     return suppressionEntry.matches(cpeId.toCpe22Uri());
618                 } catch (CpeEncodingException ex) {
619                     LOGGER.debug("Unable to convert CPE to 22 URI?" + cpeId);
620                 }
621             } else if (suppressionEntry.isCaseSensitive()) {
622                 try {
623                     return cpeId.toCpe22Uri().startsWith(suppressionEntry.getValue());
624                 } catch (CpeEncodingException ex) {
625                     LOGGER.debug("Unable to convert CPE to 22 URI?" + cpeId);
626                 }
627             } else {
628                 final String id;
629                 try {
630                     id = cpeId.toCpe22Uri().toLowerCase();
631                 } catch (CpeEncodingException ex) {
632                     LOGGER.debug("Unable to convert CPE to 22 URI?" + cpeId);
633                     return false;
634                 }
635                 final String check = suppressionEntry.getValue().toLowerCase();
636                 return id.startsWith(check);
637             }
638         }
639         return suppressionEntry.matches(identifier.getValue());
640     }
641 
642     /**
643      * Standard toString implementation.
644      *
645      * @return a string representation of this object
646      */
647     @Override
648     public String toString() {
649         final StringBuilder sb = new StringBuilder(64);
650         sb.append("SuppressionRule{");
651         if (until != null) {
652             final String dt = DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(until);
653             sb.append("until=").append(dt).append(',');
654         }
655         if (filePath != null) {
656             sb.append("filePath=").append(filePath).append(',');
657         }
658         if (sha1 != null) {
659             sb.append("sha1=").append(sha1).append(',');
660         }
661         if (packageUrl != null) {
662             sb.append("packageUrl=").append(packageUrl).append(',');
663         }
664         if (gav != null) {
665             sb.append("gav=").append(gav).append(',');
666         }
667         if (cpe != null && !cpe.isEmpty()) {
668             sb.append("cpe={");
669             cpe.forEach((pt) -> sb.append(pt).append(','));
670             sb.append('}');
671         }
672         if (cwe != null && !cwe.isEmpty()) {
673             sb.append("cwe={");
674             cwe.forEach((s) -> sb.append(s).append(','));
675             sb.append('}');
676         }
677         if (cve != null && !cve.isEmpty()) {
678             sb.append("cve={");
679             cve.forEach((s) -> sb.append(s).append(','));
680             sb.append('}');
681         }
682         if (vulnerabilityNames != null && !vulnerabilityNames.isEmpty()) {
683             sb.append("vulnerabilityName={");
684             vulnerabilityNames.forEach((pt) -> sb.append(pt).append(','));
685             sb.append('}');
686         }
687         if (cvssBelow != null && !cvssBelow.isEmpty()) {
688             sb.append("cvssBelow={");
689             cvssBelow.forEach((s) -> sb.append(s).append(','));
690             sb.append('}');
691         }
692         sb.append('}');
693         return sb.toString();
694     }
695 }