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) 2018 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.dependency;
19  
20  import java.io.Serializable;
21  import java.util.regex.Matcher;
22  import java.util.regex.Pattern;
23  
24  import javax.annotation.concurrent.ThreadSafe;
25  
26  import org.apache.commons.lang3.builder.CompareToBuilder;
27  import org.apache.commons.lang3.builder.EqualsBuilder;
28  import org.apache.commons.lang3.builder.HashCodeBuilder;
29  import org.jetbrains.annotations.NotNull;
30  import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException;
31  import org.owasp.dependencycheck.dependency.naming.CpeIdentifier;
32  import org.owasp.dependencycheck.utils.DependencyVersion;
33  import us.springett.parsers.cpe.Cpe;
34  import us.springett.parsers.cpe.ICpe;
35  import us.springett.parsers.cpe.exceptions.CpeValidationException;
36  import us.springett.parsers.cpe.util.Convert;
37  import us.springett.parsers.cpe.values.LogicalValue;
38  import us.springett.parsers.cpe.values.Part;
39  
40  /**
41   * A record containing information about vulnerable software. This is referenced
42   * from a vulnerability.
43   *
44   * @author Jeremy Long
45   */
46  @ThreadSafe
47  public class VulnerableSoftware extends Cpe implements Serializable {
48  
49      /**
50       * The serial version UID.
51       */
52      private static final long serialVersionUID = 605319412326651052L;
53  
54      /**
55       * The ending range, excluding the specified version, for matching
56       * vulnerable software
57       */
58      private final String versionEndExcluding;
59      /**
60       * The ending range, including the specified version, for matching
61       * vulnerable software
62       */
63      private final String versionEndIncluding;
64      /**
65       * The starting range, excluding the specified version, for matching
66       * vulnerable software
67       */
68      private final String versionStartExcluding;
69      /**
70       * the starting range, including the specified version, for matching
71       * vulnerable software
72       */
73      private final String versionStartIncluding;
74      /**
75       * A flag indicating whether this represents a vulnerable software object.
76       */
77      private final boolean vulnerable;
78  
79      /**
80       * Constructs a new immutable VulnerableSoftware object that represents the
81       * Well Form Named defined in the CPE 2.3 specification. Specifying
82       * <code>null</code> will be set to the default
83       * {@link us.springett.parsers.cpe.values.LogicalValue#ANY}. All values
84       * passed in must be well formed (i.e. special characters quoted with a
85       * backslash).
86       *
87       * @see <a href="https://cpe.mitre.org/specification/">CPE 2.3</a>
88       * @param part the type of entry: application, operating system, or hardware
89       * @param vendor the vendor of the CPE entry
90       * @param product the product of the CPE entry
91       * @param version the version of the CPE entry
92       * @param update the update of the CPE entry
93       * @param edition the edition of the CPE entry
94       * @param language the language of the CPE entry
95       * @param swEdition the swEdition of the CPE entry
96       * @param targetSw the targetSw of the CPE entry
97       * @param targetHw the targetHw of the CPE entry
98       * @param other the other of the CPE entry
99       * @param versionEndExcluding the ending range, excluding the specified
100      * version, for matching vulnerable software
101      * @param versionEndIncluding the ending range, including the specified
102      * version, for matching vulnerable software
103      * @param versionStartExcluding the starting range, excluding the specified
104      * version, for matching vulnerable software
105      * @param versionStartIncluding the starting range, including the specified
106      * version, for matching vulnerable software
107      * @param vulnerable whether or not this represents a vulnerable software
108      * item
109      * @throws CpeValidationException thrown if one of the CPE entries is
110      * invalid
111      */
112     //CSOFF: ParameterNumber
113     public VulnerableSoftware(Part part, String vendor, String product, String version,
114             String update, String edition, String language, String swEdition,
115             String targetSw, String targetHw, String other,
116             String versionEndExcluding, String versionEndIncluding, String versionStartExcluding,
117             String versionStartIncluding, boolean vulnerable) throws CpeValidationException {
118         super(part, vendor, product, version, update, edition, language, swEdition, targetSw, targetHw, other);
119         this.versionEndExcluding = versionEndExcluding;
120         this.versionEndIncluding = versionEndIncluding;
121         this.versionStartExcluding = versionStartExcluding;
122         this.versionStartIncluding = versionStartIncluding;
123         this.vulnerable = vulnerable;
124     }
125     //CSON: ParameterNumber
126 
127     /**
128      * Normalizes null and empty strings to null for consistent comparison.
129      * @param s the string to normalize
130      * @return null if s is null or empty, otherwise s
131      */
132     private static String normalizeForComparison(String s) {
133         return (s == null || s.isEmpty()) ? null : s;
134     }
135 
136     @Override
137     public int compareTo(@NotNull ICpe o) {
138         if (o instanceof VulnerableSoftware) {
139             final VulnerableSoftware other = (VulnerableSoftware) o;
140             return new CompareToBuilder()
141                     .appendSuper(super.compareTo(other))
142                     .append(normalizeForComparison(versionStartIncluding), normalizeForComparison(other.versionStartIncluding))
143                     .append(normalizeForComparison(versionStartExcluding), normalizeForComparison(other.versionStartExcluding))
144                     .append(normalizeForComparison(versionEndIncluding), normalizeForComparison(other.versionEndIncluding))
145                     .append(normalizeForComparison(versionEndExcluding), normalizeForComparison(other.versionEndExcluding))
146                     .append(this.vulnerable, other.vulnerable)
147                     .build();
148         } else if (o instanceof Cpe) {
149             return super.compareTo(o);
150         }
151         throw new UnexpectedAnalysisException("Unable to compare " + o.getClass().getCanonicalName());
152     }
153 
154     @Override
155     public int hashCode() {
156         // you pick a hard-coded, randomly chosen, non-zero, odd number
157         // ideally different for each class
158         return new HashCodeBuilder(13, 59)
159                 .appendSuper(super.hashCode())
160                 .append(versionEndExcluding)
161                 .append(versionEndIncluding)
162                 .append(versionStartExcluding)
163                 .append(versionStartIncluding)
164                 .toHashCode();
165     }
166 
167     @Override
168     public boolean equals(Object obj) {
169         if (obj == null || !(obj instanceof VulnerableSoftware)) {
170             return false;
171         }
172         if (this == obj) {
173             return true;
174         }
175         final VulnerableSoftware rhs = (VulnerableSoftware) obj;
176         return new EqualsBuilder()
177                 .appendSuper(super.equals(obj))
178                 .append(versionEndExcluding, rhs.versionEndExcluding)
179                 .append(versionEndIncluding, rhs.versionEndIncluding)
180                 .append(versionStartExcluding, rhs.versionStartExcluding)
181                 .append(versionStartIncluding, rhs.versionStartIncluding)
182                 .isEquals();
183     }
184 
185     /**
186      * <p>
187      * Determines if the VulnerableSoftware matches the given target
188      * VulnerableSoftware. This does not follow the CPE 2.3 Specification
189      * exactly as there are cases where undefined comparisons will result in
190      * either true or false. For instance, 'ANY' will match 'm+wild cards' and
191      * NA will return false when the target has 'm+wild cards'.</p>
192      * <p>
193      * For vulnerable software matching, the implementation also takes into
194      * account version ranges as specified within the NVD data feeds.</p>
195      *
196      * @param target the target CPE to evaluate
197      * @return <code>true</code> if the CPE matches the target; otherwise
198      * <code>false</code>
199      */
200     @Override
201     public boolean matches(ICpe target) {
202         boolean result = this.vulnerable;
203         result &= compareAttributes(this.getPart(), target.getPart());
204         result &= compareAttributes(this.getVendor(), target.getVendor());
205         result &= compareAttributes(this.getProduct(), target.getProduct());
206 
207         //TODO implement versionStart etc.
208         result &= compareVersionRange(target.getVersion());
209 
210         //todo - if the vulnerablity has an update we are might not be collecting it correctly...
211         // as such, this check might cause FN if the CVE has an update in the data set
212         result &= compareUpdateAttributes(this.getUpdate(), target.getUpdate());
213         result &= compareAttributes(this.getEdition(), target.getEdition());
214         result &= compareAttributes(this.getLanguage(), target.getLanguage());
215         result &= compareAttributes(this.getSwEdition(), target.getSwEdition());
216         result &= compareAttributes(this.getTargetSw(), target.getTargetSw());
217         result &= compareAttributes(this.getTargetHw(), target.getTargetHw());
218         result &= compareAttributes(this.getOther(), target.getOther());
219         return result;
220     }
221 
222     /**
223      * Performs the same operation as Cpe.compareAttributes() - except
224      * additional rules are applied to match a1 to alpha1 and the comparison of
225      * update attributes will also return true if the only difference between
226      * the strings is an underscore or hyphen.
227      *
228      * @param left the left value to compare
229      * @param right the right value to compare
230      * @return <code>true</code> if there is a match; otherwise
231      * <code>false</code>
232      */
233     protected static boolean compareUpdateAttributes(String left, String right) {
234         //the numbers below come from the CPE Matching standard
235         //Table 6-2: Enumeration of Attribute Comparison Set Relations
236         //https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7696.pdf
237 
238         if (left.equalsIgnoreCase(right)) {
239             //1 6 9 - equals
240             return true;
241         } else if (LogicalValue.ANY.getAbbreviation().equals(left)) {
242             //2 3 4 - superset (4 is undefined - treating as true)
243             return true;
244         } else if (LogicalValue.NA.getAbbreviation().equals(left)
245                 && LogicalValue.ANY.getAbbreviation().equals(right)) {
246             //5 - subset
247             return true;
248         } else if (LogicalValue.NA.getAbbreviation().equals(left)) {
249             //7 8 - disjoint, undefined
250             return false;
251         } else if (LogicalValue.NA.getAbbreviation().equals(right)) {
252             //12 16 - disjoint
253             return false;
254         } else if (LogicalValue.ANY.getAbbreviation().equals(right)) {
255             //13 15 - subset
256             return true;
257         }
258         final String leftValue = left.replace("-", "").replace("_", "");
259         final String rightValue = right.replace("-", "").replace("_", "");
260         if (leftValue.equalsIgnoreCase(rightValue)) {
261             //1 6 9 - equals
262             return true;
263         }
264 
265         boolean results = false;
266         //10 11 14 17
267         if (containsSpecialCharacter(left)) {
268             final Pattern p = Convert.wellFormedToPattern(left.toLowerCase());
269             final Matcher m = p.matcher(right.toLowerCase());
270             results = m.matches();
271         }
272         if (!results && rightValue.matches("^[abu]\\d.*") && leftValue.matches("^(update|alpha|beta).*")) {
273             switch (right.charAt(0)) {
274                 case 'u':
275                     results = compareUpdateAttributes(leftValue, "update" + rightValue.substring(1));
276                     break;
277                 case 'a':
278                     results = compareUpdateAttributes(leftValue, "alpha" + rightValue.substring(1));
279                     break;
280                 case 'b':
281                     results = compareUpdateAttributes(leftValue, "beta" + rightValue.substring(1));
282                     break;
283                 default:
284                     break;
285             }
286         }
287         if (!results && leftValue.matches("^[abu]\\d.*") && rightValue.matches("^(update|alpha|beta).*")) {
288             switch (left.charAt(0)) {
289                 case 'u':
290                     results = compareUpdateAttributes("update" + leftValue.substring(1), rightValue);
291                     break;
292                 case 'a':
293                     results = compareUpdateAttributes("alpha" + leftValue.substring(1), rightValue);
294                     break;
295                 case 'b':
296                     results = compareUpdateAttributes("beta" + leftValue.substring(1), rightValue);
297                     break;
298                 default:
299                     break;
300             }
301         }
302         return results;
303     }
304 
305     /**
306      * Determines if the string has an unquoted special character.
307      *
308      * @param value the string to check
309      * @return <code>true</code> if the string contains an unquoted special
310      * character; otherwise <code>false</code>
311      */
312     private static boolean containsSpecialCharacter(String value) {
313         for (int x = 0; x < value.length(); x++) {
314             final char c = value.charAt(x);
315             if (c == '?' || c == '*') {
316                 return true;
317             } else if (c == '\\') {
318                 //skip the next character because it is quoted
319                 x += 1;
320             }
321         }
322         return false;
323     }
324 
325     /**
326      * Tests if the left matches the right.
327      *
328      * @param left the cpe to compare
329      * @param right the cpe to check
330      * @return <code>true</code> if a match is found; otherwise
331      * <code>false</code>
332      */
333     public static boolean testMatch(ICpe left, ICpe right) {
334         boolean result = true;
335         result &= compareAttributes(left.getPart(), right.getPart());
336         result &= compareAttributes(left.getWellFormedVendor(), right.getWellFormedVendor());
337         result &= compareAttributes(left.getWellFormedProduct(), right.getWellFormedProduct());
338 
339         if (right instanceof VulnerableSoftware) {
340             final VulnerableSoftware vs = (VulnerableSoftware) right;
341             result &= vs.vulnerable;
342             result &= compareVersions(vs, left.getVersion());
343         } else if (left instanceof VulnerableSoftware) {
344             final VulnerableSoftware vs = (VulnerableSoftware) left;
345             result &= vs.vulnerable;
346             result &= compareVersions(vs, right.getVersion());
347         } else {
348             result &= compareAttributes(left.getWellFormedVersion(), right.getWellFormedVersion());
349         }
350 
351         //todo - if the vulnerablity has an update we are might not be collecting it correctly...
352         // as such, this check might cause FN if the CVE has an update in the data set
353         result &= compareUpdateAttributes(left.getWellFormedUpdate(), right.getWellFormedUpdate());
354         result &= compareAttributes(left.getWellFormedEdition(), right.getWellFormedEdition());
355         result &= compareAttributes(left.getWellFormedLanguage(), right.getWellFormedLanguage());
356         result &= compareAttributes(left.getWellFormedSwEdition(), right.getWellFormedSwEdition());
357         result &= compareAttributes(left.getWellFormedTargetSw(), right.getWellFormedTargetSw());
358         result &= compareAttributes(left.getWellFormedTargetHw(), right.getWellFormedTargetHw());
359         result &= compareAttributes(left.getWellFormedOther(), right.getWellFormedOther());
360         return result;
361     }
362 
363     /**
364      * <p>
365      * Determines if the target VulnerableSoftware matches the
366      * VulnerableSoftware. This does not follow the CPE 2.3 Specification
367      * exactly as there are cases where undefined comparisons will result in
368      * either true or false. For instance, 'ANY' will match 'm+wild cards' and
369      * NA will return false when the target has 'm+wild cards'.</p>
370      * <p>
371      * For vulnerable software matching, the implementation also takes into
372      * account version ranges as specified within the NVD data feeds.</p>
373      *
374      * @param target the VulnerableSoftware to evaluate
375      * @return <code>true</code> if the target CPE matches CPE; otherwise
376      * <code>false</code>
377      */
378     @Override
379     public boolean matchedBy(ICpe target) {
380         return testMatch(target, this);
381     }
382 
383     /**
384      * Evaluates the target against the version and version range checks:
385      * versionEndExcluding, versionStartExcluding versionEndIncluding, and
386      * versionStartIncluding.
387      *
388      * @param targetVersion the version to compare
389      * @return <code>true</code> if the target version is matched; otherwise
390      * <code>false</code>
391      */
392     protected boolean compareVersionRange(String targetVersion) {
393         return compareVersions(this, targetVersion);
394     }
395 
396     /**
397      * Evaluates the target against the version and version range checks:
398      * versionEndExcluding, versionStartExcluding versionEndIncluding, and
399      * versionStartIncluding.
400      *
401      * @param vs a reference to the vulnerable software to compare
402      * @param targetVersion the version to compare
403      * @return <code>true</code> if the target version is matched; otherwise
404      * <code>false</code>
405      */
406     protected static boolean compareVersions(VulnerableSoftware vs, String targetVersion) {
407         if (LogicalValue.NA.getAbbreviation().equals(vs.getVersion())) {
408             return false;
409         }
410         //if any of the four conditions will be evaluated - then true;
411         boolean result = (vs.versionEndExcluding != null && !vs.versionEndExcluding.isEmpty())
412                 || (vs.versionStartExcluding != null && !vs.versionStartExcluding.isEmpty())
413                 || (vs.versionEndIncluding != null && !vs.versionEndIncluding.isEmpty())
414                 || (vs.versionStartIncluding != null && !vs.versionStartIncluding.isEmpty());
415 
416         if (!result && compareAttributes(vs.getVersion(), targetVersion)) {
417             return true;
418         }
419 
420         final DependencyVersion target = new DependencyVersion(targetVersion);
421         if (target.getVersionParts().isEmpty()) {
422             return false;
423         }
424         if (result && vs.versionEndExcluding != null && !vs.versionEndExcluding.isEmpty()) {
425             final DependencyVersion endExcluding = new DependencyVersion(vs.versionEndExcluding);
426             result = endExcluding.compareTo(target) > 0;
427         }
428         if (result && vs.versionStartExcluding != null && !vs.versionStartExcluding.isEmpty()) {
429             final DependencyVersion startExcluding = new DependencyVersion(vs.versionStartExcluding);
430             result = startExcluding.compareTo(target) < 0;
431         }
432         if (result && vs.versionEndIncluding != null && !vs.versionEndIncluding.isEmpty()) {
433             final DependencyVersion endIncluding = new DependencyVersion(vs.versionEndIncluding);
434             result &= endIncluding.compareTo(target) >= 0;
435         }
436         if (result && vs.versionStartIncluding != null && !vs.versionStartIncluding.isEmpty()) {
437             final DependencyVersion startIncluding = new DependencyVersion(vs.versionStartIncluding);
438             result &= startIncluding.compareTo(target) <= 0;
439         }
440         return result;
441     }
442 
443     /**
444      * Returns the versionEndExcluding.
445      *
446      * @return the versionEndExcluding
447      */
448     public String getVersionEndExcluding() {
449         return versionEndExcluding;
450     }
451 
452     /**
453      * Returns the versionEndIncluding.
454      *
455      * @return the versionEndIncluding
456      */
457     public String getVersionEndIncluding() {
458         return versionEndIncluding;
459     }
460 
461     /**
462      * Returns the versionStartExcluding.
463      *
464      * @return the versionStartExcluding
465      */
466     public String getVersionStartExcluding() {
467         return versionStartExcluding;
468     }
469 
470     /**
471      * Returns the versionStartIncluding.
472      *
473      * @return the versionStartIncluding
474      */
475     public String getVersionStartIncluding() {
476         return versionStartIncluding;
477     }
478 
479     /**
480      * Returns the value of vulnerable.
481      *
482      * @return the value of vulnerable
483      */
484     public boolean isVulnerable() {
485         return vulnerable;
486     }
487 
488     @Override
489     public String toString() {
490         final StringBuilder sb = new StringBuilder();
491         sb.append(this.toCpe23FS());
492         boolean textAdded = false;
493         if (versionStartIncluding != null && !versionStartIncluding.isEmpty()) {
494             sb.append(" versions from (including) ")
495                     .append(versionStartIncluding);
496             textAdded = true;
497         }
498         if (versionStartExcluding != null && !versionStartExcluding.isEmpty()) {
499             if (textAdded) {
500                 sb.append(";");
501             }
502             sb.append(" versions from (excluding) ")
503                     .append(versionStartExcluding);
504             textAdded = true;
505         }
506         if (versionEndIncluding != null && !versionEndIncluding.isEmpty()) {
507             if (textAdded) {
508                 sb.append(";");
509             }
510             sb.append(" versions up to (including) ")
511                     .append(versionEndIncluding);
512             textAdded = true;
513         }
514         if (versionEndExcluding != null && !versionEndExcluding.isEmpty()) {
515             if (textAdded) {
516                 sb.append(";");
517             }
518             sb.append(" versions up to (excluding) ")
519                     .append(versionEndExcluding);
520             textAdded = true;
521         }
522         if (!vulnerable) {
523             if (textAdded) {
524                 sb.append(";");
525             }
526             sb.append(" version is NOT VULNERABLE");
527         }
528         return sb.toString();
529     }
530 
531     public String toNvdSearchUrl() {
532         return CpeIdentifier.nvdSearchUrlFor(this);
533     }
534 }