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