VulnerableSoftware.java

  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. import java.io.Serializable;
  20. import java.util.regex.Matcher;
  21. import java.util.regex.Pattern;

  22. import javax.annotation.concurrent.ThreadSafe;

  23. import org.apache.commons.lang3.builder.CompareToBuilder;
  24. import org.apache.commons.lang3.builder.EqualsBuilder;
  25. import org.apache.commons.lang3.builder.HashCodeBuilder;
  26. import org.jetbrains.annotations.NotNull;
  27. import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException;
  28. import org.owasp.dependencycheck.utils.DependencyVersion;
  29. import us.springett.parsers.cpe.Cpe;
  30. import us.springett.parsers.cpe.ICpe;
  31. import us.springett.parsers.cpe.exceptions.CpeValidationException;
  32. import us.springett.parsers.cpe.util.Convert;
  33. import us.springett.parsers.cpe.values.LogicalValue;
  34. import us.springett.parsers.cpe.values.Part;

  35. /**
  36.  * A record containing information about vulnerable software. This is referenced
  37.  * from a vulnerability.
  38.  *
  39.  * @author Jeremy Long
  40.  */
  41. @ThreadSafe
  42. public class VulnerableSoftware extends Cpe implements Serializable {

  43.     /**
  44.      * The serial version UID.
  45.      */
  46.     private static final long serialVersionUID = 605319412326651052L;

  47.     /**
  48.      * The ending range, excluding the specified version, for matching
  49.      * vulnerable software
  50.      */
  51.     private final String versionEndExcluding;
  52.     /**
  53.      * The ending range, including the specified version, for matching
  54.      * vulnerable software
  55.      */
  56.     private final String versionEndIncluding;
  57.     /**
  58.      * The starting range, excluding the specified version, for matching
  59.      * vulnerable software
  60.      */
  61.     private final String versionStartExcluding;
  62.     /**
  63.      * the starting range, including the specified version, for matching
  64.      * vulnerable software
  65.      */
  66.     private final String versionStartIncluding;
  67.     /**
  68.      * A flag indicating whether this represents a vulnerable software object.
  69.      */
  70.     private final boolean vulnerable;

  71.     /**
  72.      * Constructs a new immutable VulnerableSoftware object that represents the
  73.      * Well Form Named defined in the CPE 2.3 specification. Specifying
  74.      * <code>null</code> will be set to the default
  75.      * {@link us.springett.parsers.cpe.values.LogicalValue#ANY}. All values
  76.      * passed in must be well formed (i.e. special characters quoted with a
  77.      * backslash).
  78.      *
  79.      * @see <a href="https://cpe.mitre.org/specification/">CPE 2.3</a>
  80.      * @param part the type of entry: application, operating system, or hardware
  81.      * @param vendor the vendor of the CPE entry
  82.      * @param product the product of the CPE entry
  83.      * @param version the version of the CPE entry
  84.      * @param update the update of the CPE entry
  85.      * @param edition the edition of the CPE entry
  86.      * @param language the language of the CPE entry
  87.      * @param swEdition the swEdition of the CPE entry
  88.      * @param targetSw the targetSw of the CPE entry
  89.      * @param targetHw the targetHw of the CPE entry
  90.      * @param other the other of the CPE entry
  91.      * @param versionEndExcluding the ending range, excluding the specified
  92.      * version, for matching vulnerable software
  93.      * @param versionEndIncluding the ending range, including the specified
  94.      * version, for matching vulnerable software
  95.      * @param versionStartExcluding the starting range, excluding the specified
  96.      * version, for matching vulnerable software
  97.      * @param versionStartIncluding the starting range, including the specified
  98.      * version, for matching vulnerable software
  99.      * @param vulnerable whether or not this represents a vulnerable software
  100.      * item
  101.      * @throws CpeValidationException thrown if one of the CPE entries is
  102.      * invalid
  103.      */
  104.     //CSOFF: ParameterNumber
  105.     public VulnerableSoftware(Part part, String vendor, String product, String version,
  106.             String update, String edition, String language, String swEdition,
  107.             String targetSw, String targetHw, String other,
  108.             String versionEndExcluding, String versionEndIncluding, String versionStartExcluding,
  109.             String versionStartIncluding, boolean vulnerable) throws CpeValidationException {
  110.         super(part, vendor, product, version, update, edition, language, swEdition, targetSw, targetHw, other);
  111.         this.versionEndExcluding = versionEndExcluding;
  112.         this.versionEndIncluding = versionEndIncluding;
  113.         this.versionStartExcluding = versionStartExcluding;
  114.         this.versionStartIncluding = versionStartIncluding;
  115.         this.vulnerable = vulnerable;
  116.     }
  117.     //CSON: ParameterNumber

  118.     @Override
  119.     public int compareTo(@NotNull Object o) {
  120.         if (o instanceof VulnerableSoftware) {
  121.             final VulnerableSoftware other = (VulnerableSoftware) o;
  122.             return new CompareToBuilder()
  123.                     .appendSuper(super.compareTo(other))
  124.                     .append(versionStartIncluding, other.versionStartIncluding)
  125.                     .append(versionStartExcluding, other.versionStartExcluding)
  126.                     .append(versionEndIncluding, other.versionEndIncluding)
  127.                     .append(versionEndExcluding, other.versionEndExcluding)
  128.                     .append(this.vulnerable, other.vulnerable)
  129.                     .build();
  130.         } else if (o instanceof Cpe) {
  131.             return super.compareTo(o);
  132.         }
  133.         throw new UnexpectedAnalysisException("Unable to compare " + o.getClass().getCanonicalName());
  134.     }

  135.     @Override
  136.     public int hashCode() {
  137.         // you pick a hard-coded, randomly chosen, non-zero, odd number
  138.         // ideally different for each class
  139.         return new HashCodeBuilder(13, 59)
  140.                 .appendSuper(super.hashCode())
  141.                 .append(versionEndExcluding)
  142.                 .append(versionEndIncluding)
  143.                 .append(versionStartExcluding)
  144.                 .append(versionStartIncluding)
  145.                 .toHashCode();
  146.     }

  147.     @Override
  148.     public boolean equals(Object obj) {
  149.         if (obj == null || !(obj instanceof VulnerableSoftware)) {
  150.             return false;
  151.         }
  152.         if (this == obj) {
  153.             return true;
  154.         }
  155.         final VulnerableSoftware rhs = (VulnerableSoftware) obj;
  156.         return new EqualsBuilder()
  157.                 .appendSuper(super.equals(obj))
  158.                 .append(versionEndExcluding, rhs.versionEndExcluding)
  159.                 .append(versionEndIncluding, rhs.versionEndIncluding)
  160.                 .append(versionStartExcluding, rhs.versionStartExcluding)
  161.                 .append(versionStartIncluding, rhs.versionStartIncluding)
  162.                 .isEquals();
  163.     }

  164.     /**
  165.      * <p>
  166.      * Determines if the VulnerableSoftware matches the given target
  167.      * VulnerableSoftware. This does not follow the CPE 2.3 Specification
  168.      * exactly as there are cases where undefined comparisons will result in
  169.      * either true or false. For instance, 'ANY' will match 'm+wild cards' and
  170.      * NA will return false when the target has 'm+wild cards'.</p>
  171.      * <p>
  172.      * For vulnerable software matching, the implementation also takes into
  173.      * account version ranges as specified within the NVD data feeds.</p>
  174.      *
  175.      * @param target the target CPE to evaluate
  176.      * @return <code>true</code> if the CPE matches the target; otherwise
  177.      * <code>false</code>
  178.      */
  179.     @Override
  180.     public boolean matches(ICpe target) {
  181.         boolean result = this.vulnerable;
  182.         result &= compareAttributes(this.getPart(), target.getPart());
  183.         result &= compareAttributes(this.getVendor(), target.getVendor());
  184.         result &= compareAttributes(this.getProduct(), target.getProduct());

  185.         //TODO implement versionStart etc.
  186.         result &= compareVersionRange(target.getVersion());

  187.         //todo - if the vulnerablity has an update we are might not be collecting it correctly...
  188.         // as such, this check might cause FN if the CVE has an update in the data set
  189.         result &= compareUpdateAttributes(this.getUpdate(), target.getUpdate());
  190.         result &= compareAttributes(this.getEdition(), target.getEdition());
  191.         result &= compareAttributes(this.getLanguage(), target.getLanguage());
  192.         result &= compareAttributes(this.getSwEdition(), target.getSwEdition());
  193.         result &= compareAttributes(this.getTargetSw(), target.getTargetSw());
  194.         result &= compareAttributes(this.getTargetHw(), target.getTargetHw());
  195.         result &= compareAttributes(this.getOther(), target.getOther());
  196.         return result;
  197.     }

  198.     /**
  199.      * Performs the same operation as Cpe.compareAttributes() - except
  200.      * additional rules are applied to match a1 to alpha1 and the comparison of
  201.      * update attributes will also return true if the only difference between
  202.      * the strings is an underscore or hyphen.
  203.      *
  204.      * @param left the left value to compare
  205.      * @param right the right value to compare
  206.      * @return <code>true</code> if there is a match; otherwise
  207.      * <code>false</code>
  208.      */
  209.     protected static boolean compareUpdateAttributes(String left, String right) {
  210.         //the numbers below come from the CPE Matching standard
  211.         //Table 6-2: Enumeration of Attribute Comparison Set Relations
  212.         //https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7696.pdf

  213.         if (left.equalsIgnoreCase(right)) {
  214.             //1 6 9 - equals
  215.             return true;
  216.         } else if (LogicalValue.ANY.getAbbreviation().equals(left)) {
  217.             //2 3 4 - superset (4 is undefined - treating as true)
  218.             return true;
  219.         } else if (LogicalValue.NA.getAbbreviation().equals(left)
  220.                 && LogicalValue.ANY.getAbbreviation().equals(right)) {
  221.             //5 - subset
  222.             return true;
  223.         } else if (LogicalValue.NA.getAbbreviation().equals(left)) {
  224.             //7 8 - disjoint, undefined
  225.             return false;
  226.         } else if (LogicalValue.NA.getAbbreviation().equals(right)) {
  227.             //12 16 - disjoint
  228.             return false;
  229.         } else if (LogicalValue.ANY.getAbbreviation().equals(right)) {
  230.             //13 15 - subset
  231.             return true;
  232.         }
  233.         final String leftValue = left.replace("-", "").replace("_", "");
  234.         final String rightValue = right.replace("-", "").replace("_", "");
  235.         if (leftValue.equalsIgnoreCase(rightValue)) {
  236.             //1 6 9 - equals
  237.             return true;
  238.         }

  239.         boolean results = false;
  240.         //10 11 14 17
  241.         if (containsSpecialCharacter(left)) {
  242.             final Pattern p = Convert.wellFormedToPattern(left.toLowerCase());
  243.             final Matcher m = p.matcher(right.toLowerCase());
  244.             results = m.matches();
  245.         }
  246.         if (!results && rightValue.matches("^[abu]\\d.*") && leftValue.matches("^(update|alpha|beta).*")) {
  247.             switch (right.charAt(0)) {
  248.                 case 'u':
  249.                     results = compareUpdateAttributes(leftValue, "update" + rightValue.substring(1));
  250.                     break;
  251.                 case 'a':
  252.                     results = compareUpdateAttributes(leftValue, "alpha" + rightValue.substring(1));
  253.                     break;
  254.                 case 'b':
  255.                     results = compareUpdateAttributes(leftValue, "beta" + rightValue.substring(1));
  256.                     break;
  257.                 default:
  258.                     break;
  259.             }
  260.         }
  261.         if (!results && leftValue.matches("^[abu]\\d.*") && rightValue.matches("^(update|alpha|beta).*")) {
  262.             switch (left.charAt(0)) {
  263.                 case 'u':
  264.                     results = compareUpdateAttributes("update" + leftValue.substring(1), rightValue);
  265.                     break;
  266.                 case 'a':
  267.                     results = compareUpdateAttributes("alpha" + leftValue.substring(1), rightValue);
  268.                     break;
  269.                 case 'b':
  270.                     results = compareUpdateAttributes("beta" + leftValue.substring(1), rightValue);
  271.                     break;
  272.                 default:
  273.                     break;
  274.             }
  275.         }
  276.         return results;
  277.     }

  278.     /**
  279.      * Determines if the string has an unquoted special character.
  280.      *
  281.      * @param value the string to check
  282.      * @return <code>true</code> if the string contains an unquoted special
  283.      * character; otherwise <code>false</code>
  284.      */
  285.     private static boolean containsSpecialCharacter(String value) {
  286.         for (int x = 0; x < value.length(); x++) {
  287.             final char c = value.charAt(x);
  288.             if (c == '?' || c == '*') {
  289.                 return true;
  290.             } else if (c == '\\') {
  291.                 //skip the next character because it is quoted
  292.                 x += 1;
  293.             }
  294.         }
  295.         return false;
  296.     }

  297.     /**
  298.      * Tests if the left matches the right.
  299.      *
  300.      * @param left the cpe to compare
  301.      * @param right the cpe to check
  302.      * @return <code>true</code> if a match is found; otherwise
  303.      * <code>false</code>
  304.      */
  305.     public static boolean testMatch(ICpe left, ICpe right) {
  306.         boolean result = true;
  307.         result &= compareAttributes(left.getPart(), right.getPart());
  308.         result &= compareAttributes(left.getWellFormedVendor(), right.getWellFormedVendor());
  309.         result &= compareAttributes(left.getWellFormedProduct(), right.getWellFormedProduct());

  310.         if (right instanceof VulnerableSoftware) {
  311.             final VulnerableSoftware vs = (VulnerableSoftware) right;
  312.             result &= vs.vulnerable;
  313.             result &= compareVersions(vs, left.getVersion());
  314.         } else if (left instanceof VulnerableSoftware) {
  315.             final VulnerableSoftware vs = (VulnerableSoftware) left;
  316.             result &= vs.vulnerable;
  317.             result &= compareVersions(vs, right.getVersion());
  318.         } else {
  319.             result &= compareAttributes(left.getWellFormedVersion(), right.getWellFormedVersion());
  320.         }

  321.         //todo - if the vulnerablity has an update we are might not be collecting it correctly...
  322.         // as such, this check might cause FN if the CVE has an update in the data set
  323.         result &= compareUpdateAttributes(left.getWellFormedUpdate(), right.getWellFormedUpdate());
  324.         result &= compareAttributes(left.getWellFormedEdition(), right.getWellFormedEdition());
  325.         result &= compareAttributes(left.getWellFormedLanguage(), right.getWellFormedLanguage());
  326.         result &= compareAttributes(left.getWellFormedSwEdition(), right.getWellFormedSwEdition());
  327.         result &= compareAttributes(left.getWellFormedTargetSw(), right.getWellFormedTargetSw());
  328.         result &= compareAttributes(left.getWellFormedTargetHw(), right.getWellFormedTargetHw());
  329.         result &= compareAttributes(left.getWellFormedOther(), right.getWellFormedOther());
  330.         return result;
  331.     }

  332.     /**
  333.      * <p>
  334.      * Determines if the target VulnerableSoftware matches the
  335.      * VulnerableSoftware. This does not follow the CPE 2.3 Specification
  336.      * exactly as there are cases where undefined comparisons will result in
  337.      * either true or false. For instance, 'ANY' will match 'm+wild cards' and
  338.      * NA will return false when the target has 'm+wild cards'.</p>
  339.      * <p>
  340.      * For vulnerable software matching, the implementation also takes into
  341.      * account version ranges as specified within the NVD data feeds.</p>
  342.      *
  343.      * @param target the VulnerableSoftware to evaluate
  344.      * @return <code>true</code> if the target CPE matches CPE; otherwise
  345.      * <code>false</code>
  346.      */
  347.     @Override
  348.     public boolean matchedBy(ICpe target) {
  349.         return testMatch(target, this);
  350.     }

  351.     /**
  352.      * Evaluates the target against the version and version range checks:
  353.      * versionEndExcluding, versionStartExcluding versionEndIncluding, and
  354.      * versionStartIncluding.
  355.      *
  356.      * @param targetVersion the version to compare
  357.      * @return <code>true</code> if the target version is matched; otherwise
  358.      * <code>false</code>
  359.      */
  360.     protected boolean compareVersionRange(String targetVersion) {
  361.         return compareVersions(this, targetVersion);
  362.     }

  363.     /**
  364.      * Evaluates the target against the version and version range checks:
  365.      * versionEndExcluding, versionStartExcluding versionEndIncluding, and
  366.      * versionStartIncluding.
  367.      *
  368.      * @param vs a reference to the vulnerable software to compare
  369.      * @param targetVersion the version to compare
  370.      * @return <code>true</code> if the target version is matched; otherwise
  371.      * <code>false</code>
  372.      */
  373.     protected static boolean compareVersions(VulnerableSoftware vs, String targetVersion) {
  374.         if (LogicalValue.NA.getAbbreviation().equals(vs.getVersion())) {
  375.             return false;
  376.         }
  377.         //if any of the four conditions will be evaluated - then true;
  378.         boolean result = (vs.versionEndExcluding != null && !vs.versionEndExcluding.isEmpty())
  379.                 || (vs.versionStartExcluding != null && !vs.versionStartExcluding.isEmpty())
  380.                 || (vs.versionEndIncluding != null && !vs.versionEndIncluding.isEmpty())
  381.                 || (vs.versionStartIncluding != null && !vs.versionStartIncluding.isEmpty());

  382.         if (!result && compareAttributes(vs.getVersion(), targetVersion)) {
  383.             return true;
  384.         }

  385.         final DependencyVersion target = new DependencyVersion(targetVersion);
  386.         if (target.getVersionParts().isEmpty()) {
  387.             return false;
  388.         }
  389.         if (result && vs.versionEndExcluding != null && !vs.versionEndExcluding.isEmpty()) {
  390.             final DependencyVersion endExcluding = new DependencyVersion(vs.versionEndExcluding);
  391.             result = endExcluding.compareTo(target) > 0;
  392.         }
  393.         if (result && vs.versionStartExcluding != null && !vs.versionStartExcluding.isEmpty()) {
  394.             final DependencyVersion startExcluding = new DependencyVersion(vs.versionStartExcluding);
  395.             result = startExcluding.compareTo(target) < 0;
  396.         }
  397.         if (result && vs.versionEndIncluding != null && !vs.versionEndIncluding.isEmpty()) {
  398.             final DependencyVersion endIncluding = new DependencyVersion(vs.versionEndIncluding);
  399.             result &= endIncluding.compareTo(target) >= 0;
  400.         }
  401.         if (result && vs.versionStartIncluding != null && !vs.versionStartIncluding.isEmpty()) {
  402.             final DependencyVersion startIncluding = new DependencyVersion(vs.versionStartIncluding);
  403.             result &= startIncluding.compareTo(target) <= 0;
  404.         }
  405.         return result;
  406.     }

  407.     /**
  408.      * Returns the versionEndExcluding.
  409.      *
  410.      * @return the versionEndExcluding
  411.      */
  412.     public String getVersionEndExcluding() {
  413.         return versionEndExcluding;
  414.     }

  415.     /**
  416.      * Returns the versionEndIncluding.
  417.      *
  418.      * @return the versionEndIncluding
  419.      */
  420.     public String getVersionEndIncluding() {
  421.         return versionEndIncluding;
  422.     }

  423.     /**
  424.      * Returns the versionStartExcluding.
  425.      *
  426.      * @return the versionStartExcluding
  427.      */
  428.     public String getVersionStartExcluding() {
  429.         return versionStartExcluding;
  430.     }

  431.     /**
  432.      * Returns the versionStartIncluding.
  433.      *
  434.      * @return the versionStartIncluding
  435.      */
  436.     public String getVersionStartIncluding() {
  437.         return versionStartIncluding;
  438.     }

  439.     /**
  440.      * Returns the value of vulnerable.
  441.      *
  442.      * @return the value of vulnerable
  443.      */
  444.     public boolean isVulnerable() {
  445.         return vulnerable;
  446.     }

  447.     @Override
  448.     public String toString() {
  449.         final StringBuilder sb = new StringBuilder();
  450.         sb.append(this.toCpe23FS());
  451.         boolean textAdded = false;
  452.         if (versionStartIncluding != null && !versionStartIncluding.isEmpty()) {
  453.             sb.append(" versions from (including) ")
  454.                     .append(versionStartIncluding);
  455.             textAdded = true;
  456.         }
  457.         if (versionStartExcluding != null && !versionStartExcluding.isEmpty()) {
  458.             if (textAdded) {
  459.                 sb.append(";");
  460.             }
  461.             sb.append(" versions from (excluding) ")
  462.                     .append(versionStartExcluding);
  463.             textAdded = true;
  464.         }
  465.         if (versionEndIncluding != null && !versionEndIncluding.isEmpty()) {
  466.             if (textAdded) {
  467.                 sb.append(";");
  468.             }
  469.             sb.append(" versions up to (including) ")
  470.                     .append(versionEndIncluding);
  471.             textAdded = true;
  472.         }
  473.         if (versionEndExcluding != null && !versionEndExcluding.isEmpty()) {
  474.             if (textAdded) {
  475.                 sb.append(";");
  476.             }
  477.             sb.append(" versions up to (excluding) ")
  478.                     .append(versionEndExcluding);
  479.             textAdded = true;
  480.         }
  481.         if (!vulnerable) {
  482.             if (textAdded) {
  483.                 sb.append(";");
  484.             }
  485.             sb.append(" version is NOT VULNERABLE");
  486.         }
  487.         return sb.toString();
  488.     }
  489. }