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.Objects;
26  import java.util.Set;
27  import javax.annotation.concurrent.NotThreadSafe;
28  import org.apache.commons.lang3.time.DateFormatUtils;
29  import org.owasp.dependencycheck.dependency.Dependency;
30  import org.owasp.dependencycheck.dependency.Vulnerability;
31  import org.owasp.dependencycheck.dependency.naming.CpeIdentifier;
32  import org.owasp.dependencycheck.dependency.naming.Identifier;
33  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  import us.springett.parsers.cpe.Cpe;
37  import us.springett.parsers.cpe.exceptions.CpeEncodingException;
38  
39  /**
40   *
41   * @author Jeremy Long
42   */
43  @NotThreadSafe
44  public class SuppressionRule {
45  
46      /**
47       * The Logger for use throughout the class.
48       */
49      private static final Logger LOGGER = LoggerFactory.getLogger(SuppressionRule.class);
50      /**
51       * The file path for the suppression.
52       */
53      private PropertyType filePath;
54  
55      /**
56       * The SHA1 hash.
57       */
58      private String sha1;
59      /**
60       * A list of CPEs to suppression
61       */
62      private List<PropertyType> cpe = new ArrayList<>();
63      /**
64       * The list of cvssBelow scores.
65       */
66      private List<Double> cvssBelow = new ArrayList<>();
67      /**
68       * The list of cvssV2Below scores.
69       */
70      private List<Double> cvssV2Below = new ArrayList<>();
71      /**
72       * The list of cvssV3Below scores.
73       */
74      private List<Double> cvssV3Below = new ArrayList<>();
75      /**
76       * The list of cvssV4Below scores.
77       */
78      private List<Double> cvssV4Below = new ArrayList<>();
79      /**
80       * The list of CWE entries to suppress.
81       */
82      private List<String> cwe = new ArrayList<>();
83      /**
84       * The list of CVE entries to suppress.
85       */
86      private List<String> cve = new ArrayList<>();
87      /**
88       * The list of vulnerability name entries to suppress.
89       */
90      private final List<PropertyType> vulnerabilityNames = new ArrayList<>();
91      /**
92       * A Maven GAV to suppression.
93       */
94      private PropertyType gav = null;
95      /**
96       * The list of vulnerability name entries to suppress.
97       */
98      private PropertyType packageUrl = null;
99      /**
100      * The notes added in suppression file
101      */
102 
103     private String notes;
104 
105     /**
106      * A flag indicating whether or not the suppression rule is a core/base rule
107      * that should not be included in the resulting report in the "suppressed"
108      * section.
109      */
110     private boolean base;
111 
112     /**
113      * A date until which the suppression is to be retained. This can be used to
114      * make a temporary suppression that auto-expires to suppress a CVE while
115      * waiting for the vulnerability fix of the dependency to be released.
116      */
117     private Calendar until;
118 
119     /**
120      * A flag whether or not the rule matched a dependency & CPE.
121      */
122     private boolean matched = false;
123 
124     /**
125      * Get the value of matched.
126      *
127      * @return the value of matched
128      */
129     public boolean isMatched() {
130         return matched;
131     }
132 
133     /**
134      * Set the value of matched.
135      *
136      * @param matched new value of matched
137      */
138     public void setMatched(boolean matched) {
139         this.matched = matched;
140     }
141 
142     /**
143      * Get the (@code{nullable}) value of until.
144      *
145      * @return the value of until
146      */
147     public Calendar getUntil() {
148         return until;
149     }
150 
151     /**
152      * Set the value of until.
153      *
154      * @param until new value of until
155      */
156     public void setUntil(Calendar until) {
157         this.until = until;
158     }
159 
160     /**
161      * Get the value of filePath.
162      *
163      * @return the value of filePath
164      */
165     public PropertyType getFilePath() {
166         return filePath;
167     }
168 
169     /**
170      * Set the value of filePath.
171      *
172      * @param filePath new value of filePath
173      */
174     public void setFilePath(PropertyType filePath) {
175         this.filePath = filePath;
176     }
177 
178     /**
179      * Get the value of sha1.
180      *
181      * @return the value of sha1
182      */
183     public String getSha1() {
184         return sha1;
185     }
186 
187     /**
188      * Set the value of SHA1.
189      *
190      * @param sha1 new value of SHA1
191      */
192     public void setSha1(String sha1) {
193         this.sha1 = sha1;
194     }
195 
196     /**
197      * Get the value of CPE.
198      *
199      * @return the value of CPE
200      */
201     public List<PropertyType> getCpe() {
202         return cpe;
203     }
204 
205     /**
206      * Set the value of CPE.
207      *
208      * @param cpe new value of CPE
209      */
210     public void setCpe(List<PropertyType> cpe) {
211         this.cpe = cpe;
212     }
213 
214     /**
215      * Adds the CPE to the CPE list.
216      *
217      * @param cpe the CPE to add
218      */
219     public void addCpe(PropertyType cpe) {
220         this.cpe.add(cpe);
221     }
222 
223     /**
224      * Adds the CPE to the CPE list.
225      *
226      * @param name the vulnerability name to add
227      */
228     public void addVulnerabilityName(PropertyType name) {
229         this.vulnerabilityNames.add(name);
230     }
231 
232     /**
233      * Returns whether or not this suppression rule as CPE entries.
234      *
235      * @return whether or not this suppression rule as CPE entries
236      */
237     public boolean hasCpe() {
238         return !cpe.isEmpty();
239     }
240 
241     /**
242      * Get the value of cvssBelow.
243      *
244      * @return the value of cvssBelow
245      */
246     public List<Double> getCvssBelow() {
247         return cvssBelow;
248     }
249 
250     /**
251      * Set the value of cvssBelow.
252      *
253      * @param cvssBelow new value of cvssBelow
254      */
255     public void setCvssBelow(List<Double> cvssBelow) {
256         this.cvssBelow = cvssBelow;
257     }
258 
259     /**
260      * Adds the CVSS to the cvssBelow list.
261      *
262      * @param cvss the CVSS to add
263      */
264     public void addCvssBelow(Double cvss) {
265         this.cvssBelow.add(cvss);
266     }
267 
268     /**
269      * Returns whether or not this suppression rule has CVSS suppression criteria.
270      *
271      * @return whether or not this suppression rule has CVSS suppression criteria.
272      */
273     public boolean hasCvssBelow() {
274         return !cvssBelow.isEmpty();
275     }
276 
277     /**
278      * Get the value of cvssV2Below.
279      *
280      * @return the value of cvssV2Below
281      */
282     public List<Double> getCvssV2Below() {
283         return cvssV2Below;
284     }
285 
286     /**
287      * Set the value of cvssV2Below.
288      *
289      * @param cvssV2Below new value of cvssV2Below
290      */
291     public void setCvssV2Below(List<Double> cvssV2Below) {
292         this.cvssV2Below = cvssV2Below;
293     }
294 
295     /**
296      * Adds the CVSS to the cvssV2Below list.
297      *
298      * @param cvss the CVSS to add
299      */
300     public void addCvssV2Below(Double cvss) {
301         this.cvssV2Below.add(cvss);
302     }
303 
304     /**
305      * Returns whether or not this suppression rule has CVSS v2 suppression criteria.
306      *
307      * @return whether or not this suppression rule has CVSS v2 suppression criteria.
308      */
309     public boolean hasCvssV2Below() {
310         return !cvssV2Below.isEmpty();
311     }
312 
313     /**
314      * Get the value of cvssV3Below.
315      *
316      * @return the value of cvssV3Below
317      */
318     public List<Double> getCvssV3Below() {
319         return cvssV3Below;
320     }
321 
322     /**
323      * Set the value of cvssV3Below.
324      *
325      * @param cvssV3Below new value of cvssV3Below
326      */
327     public void setCvssV3Below(List<Double> cvssV3Below) {
328         this.cvssV3Below = cvssV3Below;
329     }
330 
331     /**
332      * Adds the CVSS to the cvssV3Below list.
333      *
334      * @param cvss the CVSS to add
335      */
336     public void addCvssV3Below(Double cvss) {
337         this.cvssV3Below.add(cvss);
338     }
339 
340     /**
341      * Returns whether or not this suppression rule has CVSS v3 suppression criteria.
342      *
343      * @return whether or not this suppression rule has CVSS v3 suppression criteria.
344      */
345     public boolean hasCvssV3Below() {
346         return !cvssV3Below.isEmpty();
347     }
348 
349     /**
350      * Get the value of cvssV4Below.
351      *
352      * @return the value of cvssV4Below
353      */
354     public List<Double> getCvssV4Below() {
355         return cvssV4Below;
356     }
357 
358     /**
359      * Set the value of cvssV4Below.
360      *
361      * @param cvssV4Below new value of cvssV4Below
362      */
363     public void setCvssV4Below(List<Double> cvssV4Below) {
364         this.cvssV4Below = cvssV4Below;
365     }
366 
367     /**
368      * Adds the CVSS to the cvssV4Below list.
369      *
370      * @param cvss the CVSS to add
371      */
372     public void addCvssV4Below(Double cvss) {
373         this.cvssV4Below.add(cvss);
374     }
375 
376     /**
377      * Returns whether or not this suppression rule has CVSS v4 suppression criteria.
378      *
379      * @return whether or not this suppression rule has CVSS v4 suppression criteria.
380      */
381     public boolean hasCvssV4Below() {
382         return !cvssV4Below.isEmpty();
383     }
384 
385     /**
386      * Get the value of notes.
387      *
388      * @return the value of notes
389      */
390     public String getNotes() {
391         return notes;
392     }
393 
394     /**
395      * Set the value of notes.
396      *
397      * @param notes new value of notes
398      */
399     public void setNotes(String notes) {
400         this.notes = notes;
401     }
402 
403     /**
404      * Returns whether this suppression rule has notes entries.
405      *
406      * @return whether this suppression rule has notes entries
407      */
408     public boolean hasNotes() {
409         return !notes.isEmpty();
410     }
411 
412     /**
413      * Get the value of CWE.
414      *
415      * @return the value of CWE
416      */
417     public List<String> getCwe() {
418         return cwe;
419     }
420 
421     /**
422      * Set the value of CWE.
423      *
424      * @param cwe new value of CWE
425      */
426     public void setCwe(List<String> cwe) {
427         this.cwe = cwe;
428     }
429 
430     /**
431      * Adds the CWE to the CWE list.
432      *
433      * @param cwe the CWE to add
434      */
435     public void addCwe(String cwe) {
436         this.cwe.add(cwe);
437     }
438 
439     /**
440      * Returns whether this suppression rule has CWE entries.
441      *
442      * @return whether this suppression rule has CWE entries
443      */
444     public boolean hasCwe() {
445         return !cwe.isEmpty();
446     }
447 
448     /**
449      * Get the value of CVE.
450      *
451      * @return the value of CVE
452      */
453     public List<String> getCve() {
454         return cve;
455     }
456 
457     /**
458      * Set the value of CVE.
459      *
460      * @param cve new value of CVE
461      */
462     public void setCve(List<String> cve) {
463         this.cve = cve;
464     }
465 
466     /**
467      * Adds the CVE to the CVE list.
468      *
469      * @param cve the CVE to add
470      */
471     public void addCve(String cve) {
472         this.cve.add(cve);
473     }
474 
475     /**
476      * Returns whether this suppression rule has CVE entries.
477      *
478      * @return whether this suppression rule has CVE entries
479      */
480     public boolean hasCve() {
481         return !cve.isEmpty();
482     }
483 
484     /**
485      * Returns whether this suppression rule has vulnerabilityName entries.
486      *
487      * @return whether this suppression rule has vulnerabilityName entries
488      */
489     public boolean hasVulnerabilityName() {
490         return !vulnerabilityNames.isEmpty();
491     }
492 
493     /**
494      * Get the value of Maven GAV.
495      *
496      * @return the value of GAV
497      */
498     public PropertyType getGav() {
499         return gav;
500     }
501 
502     /**
503      * Set the value of Maven GAV.
504      *
505      * @param gav new value of Maven GAV
506      */
507     public void setGav(PropertyType gav) {
508         this.gav = gav;
509     }
510 
511     /**
512      * Returns whether or not this suppression rule as GAV entries.
513      *
514      * @return whether or not this suppression rule as GAV entries
515      */
516     public boolean hasGav() {
517         return gav != null;
518     }
519 
520     /**
521      * Set the value of Package URL.
522      *
523      * @param purl new value of package URL
524      */
525     public void setPackageUrl(PropertyType purl) {
526         this.packageUrl = purl;
527     }
528 
529     /**
530      * Returns whether or not this suppression rule as packageUrl entries.
531      *
532      * @return whether or not this suppression rule as packageUrl entries
533      */
534     public boolean hasPackageUrl() {
535         return packageUrl != null;
536     }
537 
538     /**
539      * Get the value of base.
540      *
541      * @return the value of base
542      */
543     public boolean isBase() {
544         return base;
545     }
546 
547     /**
548      * Set the value of base.
549      *
550      * @param base new value of base
551      */
552     public void setBase(boolean base) {
553         this.base = base;
554     }
555 
556     /**
557      * Processes a given dependency to determine if any CPE, CVE, CWE, or CVSS
558      * scores should be suppressed. If any should be, they are removed from the
559      * dependency.
560      *
561      * @param dependency a project dependency to analyze
562      */
563     public void process(Dependency dependency) {
564         if (filePath != null && !filePath.matches(dependency.getFilePath())) {
565             return;
566         }
567         if (sha1 != null && !sha1.equalsIgnoreCase(dependency.getSha1sum())) {
568             return;
569         }
570         if (hasGav()) {
571             final Iterator<Identifier> itr = dependency.getSoftwareIdentifiers().iterator();
572             boolean found = false;
573             while (itr.hasNext()) {
574                 final Identifier i = itr.next();
575                 if (identifierMatches(this.gav, i)) {
576                     found = true;
577                     break;
578                 }
579             }
580             if (!found) {
581                 return;
582             }
583         }
584         if (hasPackageUrl()) {
585             final Iterator<Identifier> itr = dependency.getSoftwareIdentifiers().iterator();
586             boolean found = false;
587             while (itr.hasNext()) {
588                 final Identifier i = itr.next();
589                 if (purlMatches(this.packageUrl, i)) {
590                     found = true;
591                     break;
592                 }
593             }
594             if (!found) {
595                 return;
596             }
597         }
598 
599         if (this.hasCpe()) {
600             final Set<Identifier> removalList = new HashSet<>();
601             for (Identifier i : dependency.getVulnerableSoftwareIdentifiers()) {
602                 for (PropertyType c : this.cpe) {
603                     if (identifierMatches(c, i)) {
604                         if (!isBase()) {
605                             matched = true;
606                             if (this.notes != null) {
607                                 i.setNotes(this.notes);
608                             }
609                             dependency.addSuppressedIdentifier(i);
610                         }
611                         removalList.add(i);
612                         break;
613                     }
614                 }
615             }
616             removalList.forEach(dependency::removeVulnerableSoftwareIdentifier);
617         }
618         if (hasCve() || hasVulnerabilityName() || hasCwe() || hasCvssBelow() || hasCvssV2Below() || hasCvssV3Below() || hasCvssV4Below()) {
619             final Set<Vulnerability> removeVulns = new HashSet<>();
620             for (Vulnerability v : dependency.getVulnerabilities()) {
621                 boolean remove = false;
622                 for (String entry : this.cve) {
623                     if (entry.equalsIgnoreCase(v.getName())) {
624                         removeVulns.add(v);
625                         remove = true;
626                         break;
627                     }
628                 }
629                 if (!remove && this.cwe != null && !v.getCwes().isEmpty()) {
630                     for (String entry : this.cwe) {
631                         final String toMatch = String.format("CWE-%s", entry);
632                         if (v.getCwes().stream().anyMatch(toTest -> toMatch.regionMatches(0, toTest, 0, toMatch.length()))) {
633                             remove = true;
634                             removeVulns.add(v);
635                             break;
636                         }
637                     }
638                 }
639                 if (!remove && v.getName() != null) {
640                     for (PropertyType entry : this.vulnerabilityNames) {
641                         if (entry.matches(v.getName())) {
642                             remove = true;
643                             removeVulns.add(v);
644                             break;
645                         }
646                     }
647                 }
648                 if (!remove) {
649                     if (suppressedBasedOnScore(v)) {
650                         remove = true;
651                         removeVulns.add(v);
652                     }
653                 }
654                 if (remove && !isBase()) {
655                     matched = true;
656                     if (this.notes != null) {
657                         v.setNotes(this.notes);
658                     }
659                     dependency.addSuppressedVulnerability(v);
660                 }
661             }
662             removeVulns.forEach(dependency::removeVulnerability);
663         }
664     }
665 
666     boolean suppressedBasedOnScore(Vulnerability v) {
667         if (!cvssBelow.isEmpty()) {
668             for (Double cvss : this.cvssBelow) {
669                 //TODO validate this comparison
670                 if (v.getCvssV2() != null && v.getCvssV2().getCvssData().getBaseScore().compareTo(cvss) < 0) {
671                     return true;
672                 }
673                 if (v.getCvssV3() != null && v.getCvssV3().getCvssData().getBaseScore().compareTo(cvss) < 0) {
674                     return true;
675                 }
676                 if (v.getCvssV4() != null && v.getCvssV4().getCvssData().getBaseScore().compareTo(cvss) < 0) {
677                     return true;
678                 }
679             }
680             return false;
681         }
682 
683         if (hasCvssV2Below() || hasCvssV3Below() || hasCvssV4Below()) {
684             Double v2SuppressionThreshold = this.cvssV2Below.stream().max(Double::compare).orElse(11.0);
685             Double v3SuppressionThreshold = this.cvssV3Below.stream().max(Double::compare).orElse(11.0);
686             Double v4SuppressionThreshold = this.cvssV4Below.stream().max(Double::compare).orElse(11.0);
687 
688             Double v2Score = v.getCvssV2() != null ? v.getCvssV2().getCvssData().getBaseScore() : null;
689             Double v3Score = v.getCvssV3() != null ? v.getCvssV3().getCvssData().getBaseScore() : null;
690             Double v4Score = v.getCvssV4() != null ? v.getCvssV4().getCvssData().getBaseScore() : null;
691 
692             // only if all version indicate suppression will the vulnerability be suppressed
693             // so if we are missing data (score or threshold) for a specific version we assume suppression
694             boolean cvssV2CheckSuppressing = v2Score == null || v2Score < v2SuppressionThreshold;
695             boolean cvssV3CheckSuppressing = v3Score == null || v3Score < v3SuppressionThreshold;
696             boolean cvssV4CheckSuppressing = v4Score == null || v4Score < v4SuppressionThreshold;
697 
698             return cvssV2CheckSuppressing && cvssV3CheckSuppressing && cvssV4CheckSuppressing;
699         }
700 
701         return false;
702     }
703 
704     /**
705      * Identifies if the cpe specified by the cpe suppression rule does not
706      * specify a version.
707      *
708      * @param c a suppression rule identifier
709      * @return true if the property type does not specify a version; otherwise
710      * false
711      */
712     protected boolean cpeHasNoVersion(PropertyType c) {
713         return !c.isRegex() && countCharacter(c.getValue(), ':') <= 3;
714     }
715 
716     /**
717      * Counts the number of occurrences of the character found within the
718      * string.
719      *
720      * @param str the string to check
721      * @param c the character to count
722      * @return the number of times the character is found in the string
723      */
724     private int countCharacter(String str, char c) {
725         int count = 0;
726         int pos = str.indexOf(c) + 1;
727         while (pos > 0) {
728             count += 1;
729             pos = str.indexOf(c, pos) + 1;
730         }
731         return count;
732     }
733 
734     /**
735      * Determines if the cpeEntry specified as a PropertyType matches the given
736      * Identifier.
737      *
738      * @param suppressionEntry a suppression rule entry
739      * @param identifier a CPE identifier to check
740      * @return true if the entry matches; otherwise false
741      */
742     protected boolean purlMatches(PropertyType suppressionEntry, Identifier identifier) {
743         if (identifier instanceof PurlIdentifier) {
744             final PurlIdentifier purl = (PurlIdentifier) identifier;
745             return suppressionEntry.matches(purl.toString());
746         }
747         return false;
748     }
749 
750     /**
751      * Determines if the cpeEntry specified as a PropertyType matches the given
752      * Identifier.
753      *
754      * @param suppressionEntry a suppression rule entry
755      * @param identifier a CPE identifier to check
756      * @return true if the entry matches; otherwise false
757      */
758     protected boolean identifierMatches(PropertyType suppressionEntry, Identifier identifier) {
759         if (identifier instanceof PurlIdentifier) {
760             final PurlIdentifier purl = (PurlIdentifier) identifier;
761             return suppressionEntry.matches(purl.toGav());
762         } else if (identifier instanceof CpeIdentifier) {
763             //TODO check for regex - not just type
764             final Cpe cpeId = ((CpeIdentifier) identifier).getCpe();
765             if (suppressionEntry.isRegex()) {
766                 try {
767                     return suppressionEntry.matches(cpeId.toCpe22Uri());
768                 } catch (CpeEncodingException ex) {
769                     LOGGER.debug("Unable to convert CPE to 22 URI?" + cpeId);
770                 }
771             } else if (suppressionEntry.isCaseSensitive()) {
772                 try {
773                     return cpeId.toCpe22Uri().startsWith(suppressionEntry.getValue());
774                 } catch (CpeEncodingException ex) {
775                     LOGGER.debug("Unable to convert CPE to 22 URI?" + cpeId);
776                 }
777             } else {
778                 final String id;
779                 try {
780                     id = cpeId.toCpe22Uri().toLowerCase();
781                 } catch (CpeEncodingException ex) {
782                     LOGGER.debug("Unable to convert CPE to 22 URI?" + cpeId);
783                     return false;
784                 }
785                 final String check = suppressionEntry.getValue().toLowerCase();
786                 return id.startsWith(check);
787             }
788         }
789         return suppressionEntry.matches(identifier.getValue());
790     }
791 
792     /**
793      * Standard toString implementation.
794      *
795      * @return a string representation of this object
796      */
797     @Override
798     public String toString() {
799         final StringBuilder sb = new StringBuilder(64);
800         sb.append("SuppressionRule{");
801         if (until != null) {
802             final String dt = DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(until);
803             sb.append("until=").append(dt).append(',');
804         }
805         if (filePath != null) {
806             sb.append("filePath=").append(filePath).append(',');
807         }
808         if (sha1 != null) {
809             sb.append("sha1=").append(sha1).append(',');
810         }
811         if (packageUrl != null) {
812             sb.append("packageUrl=").append(packageUrl).append(',');
813         }
814         if (gav != null) {
815             sb.append("gav=").append(gav).append(',');
816         }
817         if (cpe != null && !cpe.isEmpty()) {
818             sb.append("cpe={");
819             cpe.forEach((pt) -> sb.append(pt).append(','));
820             sb.append('}');
821         }
822         if (cwe != null && !cwe.isEmpty()) {
823             sb.append("cwe={");
824             cwe.forEach((s) -> sb.append(s).append(','));
825             sb.append('}');
826         }
827         if (cve != null && !cve.isEmpty()) {
828             sb.append("cve={");
829             cve.forEach((s) -> sb.append(s).append(','));
830             sb.append('}');
831         }
832         if (vulnerabilityNames != null && !vulnerabilityNames.isEmpty()) {
833             sb.append("vulnerabilityName={");
834             vulnerabilityNames.forEach((pt) -> sb.append(pt).append(','));
835             sb.append('}');
836         }
837         if (cvssBelow != null && !cvssBelow.isEmpty()) {
838             sb.append("cvssBelow={");
839             cvssBelow.forEach((s) -> sb.append(s).append(','));
840             sb.append('}');
841         }
842         if (cvssV2Below != null && !cvssV2Below.isEmpty()) {
843             sb.append("cvssV2Below={");
844             cvssV2Below.forEach((s) -> sb.append(s).append(','));
845             sb.append('}');
846         }
847         if (cvssV3Below != null && !cvssV3Below.isEmpty()) {
848             sb.append("cvssV3Below={");
849             cvssV3Below.forEach((s) -> sb.append(s).append(','));
850             sb.append('}');
851         }
852         if (cvssV4Below != null && !cvssV4Below.isEmpty()) {
853             sb.append("cvssV4Below={");
854             cvssV4Below.forEach((s) -> sb.append(s).append(','));
855             sb.append('}');
856         }
857         sb.append('}');
858         return sb.toString();
859     }
860 
861     /**
862      * Suppression rules are considered equal if all properties except the "notes" and mutual "matched"
863      * status are equal.
864      *
865      * @param o   the reference object with which to compare.
866      * @return whether the object is equals to this one
867      */
868     @Override
869     public boolean equals(Object o) {
870         if (o == null || getClass() != o.getClass()) return false;
871         if (this == o) return true;
872         SuppressionRule that = (SuppressionRule) o;
873         return base == that.base
874                 && Objects.equals(filePath, that.filePath)
875                 && Objects.equals(sha1, that.sha1)
876                 && Objects.equals(cpe, that.cpe)
877                 && Objects.equals(cvssBelow, that.cvssBelow)
878                 && Objects.equals(cvssV2Below, that.cvssV2Below)
879                 && Objects.equals(cvssV3Below, that.cvssV3Below)
880                 && Objects.equals(cvssV4Below, that.cvssV4Below)
881                 && Objects.equals(cwe, that.cwe)
882                 && Objects.equals(cve, that.cve)
883                 && Objects.equals(vulnerabilityNames, that.vulnerabilityNames)
884                 && Objects.equals(gav, that.gav)
885                 && Objects.equals(packageUrl, that.packageUrl)
886                 && Objects.equals(until, that.until);
887     }
888 
889     @Override
890     public int hashCode() {
891         return Objects.hash(base, filePath, sha1, cpe, cvssBelow, cvssV2Below, cvssV3Below, cvssV4Below, cwe, cve, vulnerabilityNames, gav, packageUrl, until);
892     }
893 }