BundlerAuditProcessor.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) 2020 Jeremy Long. All Rights Reserved.
  17.  */
  18. package org.owasp.dependencycheck.processing;

  19. import com.github.packageurl.MalformedPackageURLException;
  20. import com.github.packageurl.PackageURL;
  21. import com.github.packageurl.PackageURLBuilder;
  22. import io.github.jeremylong.openvulnerability.client.nvd.CvssV2;
  23. import io.github.jeremylong.openvulnerability.client.nvd.CvssV2Data;
  24. import org.owasp.dependencycheck.Engine;
  25. import org.owasp.dependencycheck.data.nvdcve.CveDB;
  26. import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
  27. import org.owasp.dependencycheck.dependency.Confidence;
  28. import org.owasp.dependencycheck.dependency.Dependency;
  29. import org.owasp.dependencycheck.dependency.EvidenceType;
  30. import org.owasp.dependencycheck.dependency.Reference;
  31. import org.owasp.dependencycheck.dependency.Vulnerability;
  32. import org.owasp.dependencycheck.dependency.VulnerableSoftware;
  33. import org.owasp.dependencycheck.dependency.VulnerableSoftwareBuilder;
  34. import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
  35. import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
  36. import org.owasp.dependencycheck.utils.Checksum;
  37. import org.owasp.dependencycheck.utils.processing.Processor;
  38. import org.slf4j.Logger;
  39. import org.slf4j.LoggerFactory;
  40. import us.springett.parsers.cpe.exceptions.CpeValidationException;
  41. import us.springett.parsers.cpe.values.Part;

  42. import java.io.BufferedReader;
  43. import java.io.File;
  44. import java.io.IOException;
  45. import java.io.InputStream;
  46. import java.io.InputStreamReader;
  47. import java.nio.charset.StandardCharsets;
  48. import java.util.HashMap;
  49. import java.util.Map;

  50. import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.ADVISORY;
  51. import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.CRITICALITY;
  52. import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.CVE;
  53. import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.DEPENDENCY_ECOSYSTEM;
  54. import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.NAME;
  55. import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.VERSION;

  56. /**
  57.  * Processor for the output of bundler-audit.
  58.  *
  59.  * @author Jeremy Long
  60.  */
  61. public class BundlerAuditProcessor extends Processor<InputStream> {

  62.     /**
  63.      * The logger.
  64.      */
  65.     private static final Logger LOGGER = LoggerFactory.getLogger(BundlerAuditProcessor.class);
  66.     /**
  67.      * Reference to the gem lock dependency.
  68.      */
  69.     private final Dependency gemDependency;
  70.     /**
  71.      * Reference to the dependency-check engine.
  72.      */
  73.     private final Engine engine;
  74.     /**
  75.      * Temporary storage for an exception if it occurs during the processing.
  76.      */
  77.     private IOException ioException;
  78.     /**
  79.      * Temporary storage for an exception if it occurs during the processing.
  80.      */
  81.     private CpeValidationException cpeException;

  82.     /**
  83.      * Constructs a new processor to consume the output of `bundler-audit`.
  84.      *
  85.      * @param gemDependency a reference to `gem.lock` dependency
  86.      * @param engine a reference to the dependency-check engine
  87.      */
  88.     public BundlerAuditProcessor(Dependency gemDependency, Engine engine) {
  89.         this.gemDependency = gemDependency;
  90.         this.engine = engine;
  91.     }

  92.     /**
  93.      * Throws any exceptions that occurred during processing.
  94.      *
  95.      * @throws IOException thrown if an IO Exception occurred
  96.      * @throws CpeValidationException thrown if a CPE validation exception
  97.      * occurred
  98.      */
  99.     @Override
  100.     public void close() throws IOException, CpeValidationException {
  101.         if (ioException != null) {
  102.             addSuppressedExceptions(ioException, cpeException);
  103.             throw ioException;
  104.         }
  105.         if (cpeException != null) {
  106.             throw cpeException;
  107.         }
  108.     }

  109.     @Override
  110.     public void run() {
  111.         final String parentName = gemDependency.getActualFile().getParentFile().getName();
  112.         final String fileName = gemDependency.getFileName();
  113.         final String filePath = gemDependency.getFilePath();
  114.         Dependency dependency = null;
  115.         Vulnerability vulnerability = null;
  116.         String gem = null;
  117.         final Map<String, Dependency> map = new HashMap<>();
  118.         boolean appendToDescription = false;

  119.         try (InputStreamReader ir = new InputStreamReader(getInput(), StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(ir)) {

  120.             String nextLine;
  121.             while ((nextLine = br.readLine()) != null) {
  122.                 if (nextLine.startsWith(NAME)) {
  123.                     appendToDescription = false;
  124.                     gem = nextLine.substring(NAME.length());
  125.                     if (!map.containsKey(gem)) {
  126.                         map.put(gem, createDependencyForGem(engine, gemDependency.getActualFile(), parentName, fileName, filePath, gem));
  127.                     }
  128.                     dependency = map.get(gem);
  129.                     LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
  130.                 } else if (nextLine.startsWith(VERSION)) {
  131.                     vulnerability = createVulnerability(parentName, dependency, gem, nextLine);
  132.                 } else if (nextLine.startsWith(ADVISORY) || nextLine.startsWith(CVE)) {
  133.                     setVulnerabilityName(parentName, dependency, vulnerability, nextLine);
  134.                 } else if (nextLine.startsWith(CRITICALITY)) {
  135.                     addCriticalityToVulnerability(parentName, vulnerability, nextLine);
  136.                 } else if (nextLine.startsWith("URL: ")) {
  137.                     addReferenceToVulnerability(parentName, vulnerability, nextLine);
  138.                 } else if (nextLine.startsWith("Description:") || nextLine.startsWith("Title:")) {
  139.                     appendToDescription = true;
  140.                     if (null != vulnerability) {
  141.                         vulnerability.setDescription("*** Vulnerability obtained from bundle-audit verbose report. "
  142.                                 + "Title link may not work. CPE below is guessed. CVSS score is estimated (-1.0 "
  143.                                 + " indicates unknown). See link below for full details. *** ");
  144.                     }
  145.                 } else if (appendToDescription && null != vulnerability) {
  146.                     vulnerability.setDescription(vulnerability.getDescription() + nextLine + "\n");
  147.                 }
  148.             }
  149.         } catch (IOException ex) {
  150.             this.ioException = ex;
  151.         } catch (CpeValidationException ex) {
  152.             this.cpeException = ex;
  153.         }
  154.     }

  155.     /**
  156.      * Sets the vulnerability name.
  157.      *
  158.      * @param parentName the parent name
  159.      * @param dependency the dependency
  160.      * @param vulnerability the vulnerability
  161.      * @param nextLine the line to parse
  162.      */
  163.     private void setVulnerabilityName(String parentName, Dependency dependency, Vulnerability vulnerability, String nextLine) {
  164.         final String advisory;
  165.         if (nextLine.startsWith(CVE)) {
  166.             advisory = nextLine.substring(CVE.length());
  167.         } else {
  168.             advisory = nextLine.substring(ADVISORY.length());
  169.         }
  170.         if (null != vulnerability) {
  171.             vulnerability.setName(advisory);
  172.         }
  173.         if (null != dependency) {
  174.             dependency.addVulnerability(vulnerability);
  175.         }
  176.         LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
  177.     }

  178.     /**
  179.      * Adds a reference to the vulnerability.
  180.      *
  181.      * @param parentName the parent name
  182.      * @param vulnerability the vulnerability
  183.      * @param nextLine the line to parse
  184.      */
  185.     private void addReferenceToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
  186.         final String url = nextLine.substring("URL: ".length());
  187.         if (null != vulnerability) {
  188.             final Reference ref = new Reference();
  189.             ref.setName(vulnerability.getName());
  190.             ref.setSource("bundle-audit");
  191.             ref.setUrl(url);
  192.             vulnerability.addReference(ref);
  193.         }
  194.         LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
  195.     }

  196.     /**
  197.      * Adds the criticality to the vulnerability
  198.      *
  199.      * @param parentName the parent name
  200.      * @param vulnerability the vulnerability
  201.      * @param nextLine the line to parse
  202.      */
  203.     private void addCriticalityToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
  204.         if (null != vulnerability) {
  205.             final String criticality = nextLine.substring(CRITICALITY.length()).trim();
  206.             Double score = -1.0;
  207.             Vulnerability v = null;
  208.             final CveDB cvedb = engine.getDatabase();
  209.             if (cvedb != null) {
  210.                 try {
  211.                     v = cvedb.getVulnerability(vulnerability.getName());
  212.                 } catch (DatabaseException ex) {
  213.                     LOGGER.debug("Unable to look up vulnerability {}", vulnerability.getName());
  214.                 }
  215.             }
  216.             if (v != null && (v.getCvssV2() != null || v.getCvssV3() != null)) {
  217.                 if (v.getCvssV2() != null) {
  218.                     vulnerability.setCvssV2(v.getCvssV2());
  219.                 }
  220.                 if (v.getCvssV3() != null) {
  221.                     vulnerability.setCvssV3(v.getCvssV3());
  222.                 }
  223.             } else {
  224.                 if ("High".equalsIgnoreCase(criticality)) {
  225.                     score = 8.5;
  226.                 } else if ("Medium".equalsIgnoreCase(criticality)) {
  227.                     score = 5.5;
  228.                 } else if ("Low".equalsIgnoreCase(criticality)) {
  229.                     score = 2.0;
  230.                 }
  231.                 LOGGER.debug("bundle-audit vulnerability missing CVSS data: {}", vulnerability.getName());
  232.                 final CvssV2Data cvssData = new CvssV2Data("2.0", null, null, null, null, null, null, null, score, criticality.toUpperCase(),
  233.                         null, null, null, null, null, null, null, null, null, null);
  234.                 final CvssV2 cvssV2 = new CvssV2(null, null, cvssData, criticality.toUpperCase(), null, null, null, null, null, null, null);
  235.                 vulnerability.setCvssV2(cvssV2);
  236.                 vulnerability.setUnscoredSeverity(null);
  237.             }
  238.         }
  239.         LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
  240.     }

  241.     /**
  242.      * Creates a vulnerability.
  243.      *
  244.      * @param parentName the parent name
  245.      * @param dependency the dependency
  246.      * @param gem the gem name
  247.      * @param nextLine the line to parse
  248.      * @return the vulnerability
  249.      * @throws CpeValidationException thrown if there is an error building the
  250.      * CPE vulnerability object
  251.      */
  252.     private Vulnerability createVulnerability(String parentName, Dependency dependency, String gem, String nextLine) throws CpeValidationException {
  253.         Vulnerability vulnerability = null;
  254.         if (null != dependency) {
  255.             final String version = nextLine.substring(VERSION.length());
  256.             dependency.addEvidence(EvidenceType.VERSION,
  257.                     "bundler-audit",
  258.                     "Version",
  259.                     version,
  260.                     Confidence.HIGHEST);
  261.             dependency.setVersion(version);
  262.             dependency.setName(gem);
  263.             try {
  264.                 final PackageURL purl = PackageURLBuilder.aPackageURL().withType("gem").withName(dependency.getName())
  265.                         .withVersion(dependency.getVersion()).build();
  266.                 dependency.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
  267.             } catch (MalformedPackageURLException ex) {
  268.                 LOGGER.debug("Unable to build package url for python", ex);
  269.                 final GenericIdentifier id = new GenericIdentifier("gem:" + dependency.getName() + "@" + dependency.getVersion(),
  270.                         Confidence.HIGHEST);
  271.                 dependency.addSoftwareIdentifier(id);
  272.             }

  273.             vulnerability = new Vulnerability(); // don't add to dependency until we have name set later
  274.             vulnerability.setSource(Vulnerability.Source.BUNDLEAUDIT);
  275.             final VulnerableSoftwareBuilder builder = new VulnerableSoftwareBuilder();
  276.             final VulnerableSoftware vs = builder.part(Part.APPLICATION)
  277.                     .vendor(gem)
  278.                     .product(String.format("%s_project", gem))
  279.                     .version(version).build();
  280.             vulnerability.addVulnerableSoftware(vs);
  281.             vulnerability.setMatchedVulnerableSoftware(vs);
  282.             vulnerability.setUnscoredSeverity("UNKNOWN");
  283.         }
  284.         LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
  285.         return vulnerability;
  286.     }

  287.     /**
  288.      * Creates the dependency based off of the gem.
  289.      *
  290.      * @param engine the engine used for scanning
  291.      * @param gemFile the gem file
  292.      * @param parentName the gem parent
  293.      * @param fileName the file name
  294.      * @param filePath the file path
  295.      * @param gem the gem name
  296.      * @return the dependency to add
  297.      * @throws IOException thrown if a temporary gem file could not be written
  298.      */
  299.     private Dependency createDependencyForGem(Engine engine, File gemFile, String parentName, String fileName,
  300.             String filePath, String gem) throws IOException {
  301.         final String displayFileName = String.format("%s%c%s:%s", parentName, File.separatorChar, fileName, gem);
  302.         final Dependency dependency = new Dependency(gemFile, true);
  303.         dependency.setSha1sum(Checksum.getSHA1Checksum(displayFileName));
  304.         dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
  305.         dependency.addEvidence(EvidenceType.PRODUCT, "bundler-audit", "Name", gem, Confidence.HIGHEST);
  306.         dependency.addEvidence(EvidenceType.VENDOR, "bundler-audit", "Name", gem, Confidence.HIGH);
  307.         //TODO add package URL - note, this may require parsing the gemfile.lock and getting the version for each entry

  308.         dependency.setDisplayFileName(displayFileName);
  309.         dependency.setFileName(fileName);
  310.         dependency.setFilePath(filePath);
  311.         //sha1sum is used for anchor links in the HtML report
  312.         dependency.setSha1sum(Checksum.getSHA1Checksum(displayFileName));
  313.         engine.addDependency(dependency);
  314.         return dependency;
  315.     }
  316. }