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) 2020 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.processing;
19  
20  import com.github.packageurl.MalformedPackageURLException;
21  import com.github.packageurl.PackageURL;
22  import com.github.packageurl.PackageURLBuilder;
23  import io.github.jeremylong.openvulnerability.client.nvd.CvssV2;
24  import io.github.jeremylong.openvulnerability.client.nvd.CvssV2Data;
25  import org.owasp.dependencycheck.Engine;
26  import org.owasp.dependencycheck.data.nvdcve.CveDB;
27  import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
28  import org.owasp.dependencycheck.dependency.Confidence;
29  import org.owasp.dependencycheck.dependency.Dependency;
30  import org.owasp.dependencycheck.dependency.EvidenceType;
31  import org.owasp.dependencycheck.dependency.Reference;
32  import org.owasp.dependencycheck.dependency.Vulnerability;
33  import org.owasp.dependencycheck.dependency.VulnerableSoftware;
34  import org.owasp.dependencycheck.dependency.VulnerableSoftwareBuilder;
35  import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
36  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
37  import org.owasp.dependencycheck.utils.Checksum;
38  import org.owasp.dependencycheck.utils.processing.Processor;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  import us.springett.parsers.cpe.exceptions.CpeValidationException;
42  import us.springett.parsers.cpe.values.Part;
43  
44  import java.io.BufferedReader;
45  import java.io.File;
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.io.InputStreamReader;
49  import java.nio.charset.StandardCharsets;
50  import java.util.HashMap;
51  import java.util.Map;
52  
53  import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.ADVISORY;
54  import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.CRITICALITY;
55  import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.CVE;
56  import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.DEPENDENCY_ECOSYSTEM;
57  import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.NAME;
58  import static org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer.VERSION;
59  
60  /**
61   * Processor for the output of bundler-audit.
62   *
63   * @author Jeremy Long
64   */
65  public class BundlerAuditProcessor extends Processor<InputStream> {
66  
67      /**
68       * The logger.
69       */
70      private static final Logger LOGGER = LoggerFactory.getLogger(BundlerAuditProcessor.class);
71      /**
72       * Reference to the gem lock dependency.
73       */
74      private final Dependency gemDependency;
75      /**
76       * Reference to the dependency-check engine.
77       */
78      private final Engine engine;
79      /**
80       * Temporary storage for an exception if it occurs during the processing.
81       */
82      private IOException ioException;
83      /**
84       * Temporary storage for an exception if it occurs during the processing.
85       */
86      private CpeValidationException cpeException;
87  
88      /**
89       * Constructs a new processor to consume the output of `bundler-audit`.
90       *
91       * @param gemDependency a reference to `gem.lock` dependency
92       * @param engine a reference to the dependency-check engine
93       */
94      public BundlerAuditProcessor(Dependency gemDependency, Engine engine) {
95          this.gemDependency = gemDependency;
96          this.engine = engine;
97      }
98  
99      /**
100      * Throws any exceptions that occurred during processing.
101      *
102      * @throws IOException thrown if an IO Exception occurred
103      * @throws CpeValidationException thrown if a CPE validation exception
104      * occurred
105      */
106     @Override
107     public void close() throws IOException, CpeValidationException {
108         if (ioException != null) {
109             addSuppressedExceptions(ioException, cpeException);
110             throw ioException;
111         }
112         if (cpeException != null) {
113             throw cpeException;
114         }
115     }
116 
117     @Override
118     public void run() {
119         final String parentName = gemDependency.getActualFile().getParentFile().getName();
120         final String fileName = gemDependency.getFileName();
121         final String filePath = gemDependency.getFilePath();
122         Dependency dependency = null;
123         Vulnerability vulnerability = null;
124         String gem = null;
125         final Map<String, Dependency> map = new HashMap<>();
126         boolean appendToDescription = false;
127 
128         try (InputStreamReader ir = new InputStreamReader(getInput(), StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(ir)) {
129 
130             String nextLine;
131             while ((nextLine = br.readLine()) != null) {
132                 if (nextLine.startsWith(NAME)) {
133                     appendToDescription = false;
134                     gem = nextLine.substring(NAME.length());
135                     if (!map.containsKey(gem)) {
136                         map.put(gem, createDependencyForGem(engine, gemDependency.getActualFile(), parentName, fileName, filePath, gem));
137                     }
138                     dependency = map.get(gem);
139                     LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
140                 } else if (nextLine.startsWith(VERSION)) {
141                     vulnerability = createVulnerability(parentName, dependency, gem, nextLine);
142                 } else if (nextLine.startsWith(ADVISORY) || nextLine.startsWith(CVE)) {
143                     setVulnerabilityName(parentName, dependency, vulnerability, nextLine);
144                 } else if (nextLine.startsWith(CRITICALITY)) {
145                     addCriticalityToVulnerability(parentName, vulnerability, nextLine);
146                 } else if (nextLine.startsWith("URL: ")) {
147                     addReferenceToVulnerability(parentName, vulnerability, nextLine);
148                 } else if (nextLine.startsWith("Description:") || nextLine.startsWith("Title:")) {
149                     appendToDescription = true;
150                     if (null != vulnerability) {
151                         vulnerability.setDescription("*** Vulnerability obtained from bundle-audit verbose report. "
152                                 + "Title link may not work. CPE below is guessed. CVSS score is estimated (-1.0 "
153                                 + " indicates unknown). See link below for full details. *** ");
154                     }
155                 } else if (appendToDescription && null != vulnerability) {
156                     vulnerability.setDescription(vulnerability.getDescription() + nextLine + "\n");
157                 }
158             }
159         } catch (IOException ex) {
160             this.ioException = ex;
161         } catch (CpeValidationException ex) {
162             this.cpeException = ex;
163         }
164     }
165 
166     /**
167      * Sets the vulnerability name.
168      *
169      * @param parentName the parent name
170      * @param dependency the dependency
171      * @param vulnerability the vulnerability
172      * @param nextLine the line to parse
173      */
174     private void setVulnerabilityName(String parentName, Dependency dependency, Vulnerability vulnerability, String nextLine) {
175         final String advisory;
176         if (nextLine.startsWith(CVE)) {
177             advisory = nextLine.substring(CVE.length());
178         } else {
179             advisory = nextLine.substring(ADVISORY.length());
180         }
181         if (null != vulnerability) {
182             vulnerability.setName(advisory);
183         }
184         if (null != dependency) {
185             dependency.addVulnerability(vulnerability);
186         }
187         LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
188     }
189 
190     /**
191      * Adds a reference to the vulnerability.
192      *
193      * @param parentName the parent name
194      * @param vulnerability the vulnerability
195      * @param nextLine the line to parse
196      */
197     private void addReferenceToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
198         final String url = nextLine.substring("URL: ".length());
199         if (null != vulnerability) {
200             final Reference ref = new Reference();
201             ref.setName(vulnerability.getName());
202             ref.setSource("bundle-audit");
203             ref.setUrl(url);
204             vulnerability.getReferences().add(ref);
205         }
206         LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
207     }
208 
209     /**
210      * Adds the criticality to the vulnerability
211      *
212      * @param parentName the parent name
213      * @param vulnerability the vulnerability
214      * @param nextLine the line to parse
215      */
216     private void addCriticalityToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
217         if (null != vulnerability) {
218             final String criticality = nextLine.substring(CRITICALITY.length()).trim();
219             Double score = -1.0;
220             Vulnerability v = null;
221             final CveDB cvedb = engine.getDatabase();
222             if (cvedb != null) {
223                 try {
224                     v = cvedb.getVulnerability(vulnerability.getName());
225                 } catch (DatabaseException ex) {
226                     LOGGER.debug("Unable to look up vulnerability {}", vulnerability.getName());
227                 }
228             }
229             if (v != null && (v.getCvssV2() != null || v.getCvssV3() != null)) {
230                 if (v.getCvssV2() != null) {
231                     vulnerability.setCvssV2(v.getCvssV2());
232                 }
233                 if (v.getCvssV3() != null) {
234                     vulnerability.setCvssV3(v.getCvssV3());
235                 }
236             } else {
237                 if ("High".equalsIgnoreCase(criticality)) {
238                     score = 8.5;
239                 } else if ("Medium".equalsIgnoreCase(criticality)) {
240                     score = 5.5;
241                 } else if ("Low".equalsIgnoreCase(criticality)) {
242                     score = 2.0;
243                 }
244                 LOGGER.debug("bundle-audit vulnerability missing CVSS data: {}", vulnerability.getName());
245                 final CvssV2Data cvssData = new CvssV2Data("2.0", null, null, null, null, null, null, null, score, criticality.toUpperCase(),
246                         null, null, null, null, null, null, null, null, null, null);
247                 final CvssV2 cvssV2 = new CvssV2(null, null, cvssData, criticality.toUpperCase(), null, null, null, null, null, null, null);
248                 vulnerability.setCvssV2(cvssV2);
249                 vulnerability.setUnscoredSeverity(null);
250             }
251         }
252         LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
253     }
254 
255     /**
256      * Creates a vulnerability.
257      *
258      * @param parentName the parent name
259      * @param dependency the dependency
260      * @param gem the gem name
261      * @param nextLine the line to parse
262      * @return the vulnerability
263      * @throws CpeValidationException thrown if there is an error building the
264      * CPE vulnerability object
265      */
266     private Vulnerability createVulnerability(String parentName, Dependency dependency, String gem, String nextLine) throws CpeValidationException {
267         Vulnerability vulnerability = null;
268         if (null != dependency) {
269             final String version = nextLine.substring(VERSION.length());
270             dependency.addEvidence(EvidenceType.VERSION,
271                     "bundler-audit",
272                     "Version",
273                     version,
274                     Confidence.HIGHEST);
275             dependency.setVersion(version);
276             dependency.setName(gem);
277             try {
278                 final PackageURL purl = PackageURLBuilder.aPackageURL().withType("gem").withName(dependency.getName())
279                         .withVersion(dependency.getVersion()).build();
280                 dependency.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
281             } catch (MalformedPackageURLException ex) {
282                 LOGGER.debug("Unable to build package url for python", ex);
283                 final GenericIdentifier id = new GenericIdentifier("gem:" + dependency.getName() + "@" + dependency.getVersion(),
284                         Confidence.HIGHEST);
285                 dependency.addSoftwareIdentifier(id);
286             }
287 
288             vulnerability = new Vulnerability(); // don't add to dependency until we have name set later
289             vulnerability.setSource(Vulnerability.Source.BUNDLEAUDIT);
290             final VulnerableSoftwareBuilder builder = new VulnerableSoftwareBuilder();
291             final VulnerableSoftware vs = builder.part(Part.APPLICATION)
292                     .vendor(gem)
293                     .product(String.format("%s_project", gem))
294                     .version(version).build();
295             vulnerability.addVulnerableSoftware(vs);
296             vulnerability.setMatchedVulnerableSoftware(vs);
297             vulnerability.setUnscoredSeverity("UNKNOWN");
298         }
299         LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
300         return vulnerability;
301     }
302 
303     /**
304      * Creates the dependency based off of the gem.
305      *
306      * @param engine the engine used for scanning
307      * @param gemFile the gem file
308      * @param parentName the gem parent
309      * @param fileName the file name
310      * @param filePath the file path
311      * @param gem the gem name
312      * @return the dependency to add
313      * @throws IOException thrown if a temporary gem file could not be written
314      */
315     private Dependency createDependencyForGem(Engine engine, File gemFile, String parentName, String fileName,
316             String filePath, String gem) throws IOException {
317         final String displayFileName = String.format("%s%c%s:%s", parentName, File.separatorChar, fileName, gem);
318         final Dependency dependency = new Dependency(gemFile, true);
319         dependency.setSha1sum(Checksum.getSHA1Checksum(displayFileName));
320         dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
321         dependency.addEvidence(EvidenceType.PRODUCT, "bundler-audit", "Name", gem, Confidence.HIGHEST);
322         //TODO add package URL - note, this may require parsing the gemfile.lock and getting the version for each entry
323 
324         dependency.setDisplayFileName(displayFileName);
325         dependency.setFileName(fileName);
326         dependency.setFilePath(filePath);
327         //sha1sum is used for anchor links in the HtML report
328         dependency.setSha1sum(Checksum.getSHA1Checksum(displayFileName));
329         engine.addDependency(dependency);
330         return dependency;
331     }
332 }