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