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