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) 2019 Jason Dillon. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.analyzer;
19  
20  import io.github.jeremylong.openvulnerability.client.nvd.CvssV2;
21  import io.github.jeremylong.openvulnerability.client.nvd.CvssV2Data;
22  import org.sonatype.ossindex.service.api.componentreport.ComponentReport;
23  import org.sonatype.ossindex.service.api.componentreport.ComponentReportVulnerability;
24  import org.sonatype.ossindex.service.api.cvss.Cvss2Severity;
25  import org.sonatype.ossindex.service.api.cvss.Cvss2Vector;
26  import org.sonatype.ossindex.service.api.cvss.CvssVector;
27  import org.sonatype.ossindex.service.api.cvss.CvssVectorFactory;
28  import org.sonatype.ossindex.service.client.OssindexClient;
29  import org.owasp.dependencycheck.Engine;
30  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
31  import org.owasp.dependencycheck.data.ossindex.OssindexClientFactory;
32  
33  import org.owasp.dependencycheck.dependency.Dependency;
34  import org.owasp.dependencycheck.dependency.Vulnerability;
35  import org.owasp.dependencycheck.dependency.VulnerableSoftware;
36  import org.owasp.dependencycheck.dependency.VulnerableSoftwareBuilder;
37  import org.owasp.dependencycheck.dependency.naming.Identifier;
38  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
39  import org.owasp.dependencycheck.utils.Settings;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  import us.springett.parsers.cpe.exceptions.CpeValidationException;
43  import us.springett.parsers.cpe.values.Part;
44  
45  import org.sonatype.goodies.packageurl.PackageUrl;
46  
47  import java.util.ArrayList;
48  import java.util.Arrays;
49  import java.util.Collections;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.concurrent.TimeUnit;
53  import java.util.regex.Matcher;
54  import java.util.regex.Pattern;
55  
56  import java.net.SocketTimeoutException;
57  
58  import javax.annotation.Nullable;
59  import org.apache.commons.lang3.StringUtils;
60  import org.owasp.dependencycheck.utils.CvssUtil;
61  import org.sonatype.goodies.packageurl.InvalidException;
62  import org.sonatype.ossindex.service.client.transport.Transport.TransportException;
63  
64  /**
65   * Enrich dependency information from Sonatype OSS index.
66   *
67   * @author Jason Dillon
68   * @since 5.0.0
69   */
70  public class OssIndexAnalyzer extends AbstractAnalyzer {
71  
72      /**
73       * A reference to the logger.
74       */
75      private static final Logger LOG = LoggerFactory.getLogger(OssIndexAnalyzer.class);
76  
77      /**
78       * A pattern to match CVE identifiers.
79       */
80      private static final Pattern CVE_PATTERN = Pattern.compile("\\bCVE-\\d{4}-\\d{4,10}\\b");
81  
82      /**
83       * The reference type.
84       */
85      public static final String REFERENCE_TYPE = "OSSINDEX";
86  
87      /**
88       * Fetched reports.
89       */
90      private static Map<PackageUrl, ComponentReport> reports;
91  
92      /**
93       * Lock to protect fetching state.
94       */
95      private static final Object FETCH_MUTIX = new Object();
96  
97      @Override
98      public String getName() {
99          return "Sonatype OSS Index Analyzer";
100     }
101 
102     @Override
103     public AnalysisPhase getAnalysisPhase() {
104         return AnalysisPhase.FINDING_ANALYSIS_PHASE2;
105     }
106 
107     @Override
108     protected String getAnalyzerEnabledSettingKey() {
109         return Settings.KEYS.ANALYZER_OSSINDEX_ENABLED;
110     }
111 
112     /**
113      * Run without parallel support.
114      *
115      * @return false
116      */
117     @Override
118     public boolean supportsParallelProcessing() {
119         return true;
120     }
121 
122     @Override
123     protected void closeAnalyzer() throws Exception {
124         synchronized (FETCH_MUTIX) {
125             reports = null;
126         }
127     }
128 
129     @Override
130     protected void analyzeDependency(final Dependency dependency, final Engine engine) throws AnalysisException {
131         // batch request component-reports for all dependencies
132         synchronized (FETCH_MUTIX) {
133             if (reports == null) {
134                 try {
135                     requestDelay();
136                     reports = requestReports(engine.getDependencies());
137                 } catch (TransportException ex) {
138                     final String message = ex.getMessage();
139                     final boolean warnOnly = getSettings().getBoolean(Settings.KEYS.ANALYZER_OSSINDEX_WARN_ONLY_ON_REMOTE_ERRORS, false);
140                     this.setEnabled(false);
141                     if (StringUtils.endsWith(message, "401")) {
142                         LOG.error("Invalid credentials for the OSS Index, disabling the analyzer");
143                         throw new AnalysisException("Invalid credentials provided for OSS Index", ex);
144                     } else if (StringUtils.endsWith(message, "403")) {
145                         LOG.error("OSS Index access forbidden, disabling the analyzer");
146                         throw new AnalysisException("OSS Index access forbidden", ex);
147                     } else if (StringUtils.endsWith(message, "429")) {
148                         if (warnOnly) {
149                             LOG.warn("OSS Index rate limit exceeded, disabling the analyzer", ex);
150                         } else {
151                             throw new AnalysisException("OSS Index rate limit exceeded, disabling the analyzer", ex);
152                         }
153                     } else if (warnOnly) {
154                         LOG.warn("Error requesting component reports, disabling the analyzer", ex);
155                     } else {
156                         LOG.debug("Error requesting component reports, disabling the analyzer", ex);
157                         throw new AnalysisException("Failed to request component-reports", ex);
158                     }
159                 } catch (SocketTimeoutException e) {
160                     final boolean warnOnly = getSettings().getBoolean(Settings.KEYS.ANALYZER_OSSINDEX_WARN_ONLY_ON_REMOTE_ERRORS, false);
161                     this.setEnabled(false);
162                     if (warnOnly) {
163                         LOG.warn("OSS Index socket timeout, disabling the analyzer", e);
164                     } else {
165                         LOG.debug("OSS Index socket timeout", e);
166                         throw new AnalysisException("Failed to establish socket to OSS Index", e);
167                     }
168                 } catch (Exception e) {
169                     LOG.debug("Error requesting component reports", e);
170                     throw new AnalysisException("Failed to request component-reports", e);
171                 }
172             }
173 
174             // skip enrichment if we failed to fetch reports
175             if (reports != null) {
176                 enrich(dependency);
177             }
178         }
179 
180     }
181 
182     /**
183      * Delays each request (thread) by the configured amount of seconds, if the
184      * configuration is present.
185      */
186     private void requestDelay() throws InterruptedException {
187         final int delay = getSettings().getInt(Settings.KEYS.ANALYZER_OSSINDEX_REQUEST_DELAY, 0);
188         if (delay > 0) {
189             LOG.debug("Request delay: " + delay);
190             TimeUnit.SECONDS.sleep(delay);
191         }
192     }
193 
194     /**
195      * Helper to complain if unable to parse Package-URL.
196      *
197      * @param value the url to parse
198      * @return the package url
199      */
200     @Nullable
201     private PackageUrl parsePackageUrl(final String value) {
202         try {
203             return PackageUrl.parse(value);
204         } catch (InvalidException e) {
205             LOG.debug("Invalid Package-URL: {}", value, e);
206             return null;
207         }
208     }
209 
210     /**
211      * Batch request component-reports for all dependencies.
212      *
213      * @param dependencies the collection of dependencies
214      * @return the map of dependency to OSS Index's component-report
215      * @throws Exception thrown if there is an exception requesting the report
216      */
217     private Map<PackageUrl, ComponentReport> requestReports(final Dependency[] dependencies) throws Exception {
218         LOG.debug("Requesting component-reports for {} dependencies", dependencies.length);
219         // create requests for each dependency which has a PURL identifier
220         final List<PackageUrl> packages = new ArrayList<>();
221         Arrays.stream(dependencies).forEach(dependency -> dependency.getSoftwareIdentifiers().stream()
222                 .filter(id -> id instanceof PurlIdentifier)
223                 .map(id -> parsePackageUrl(id.getValue()))
224                 .filter(id -> id != null && StringUtils.isNotBlank(id.getVersion()))
225                 .forEach(packages::add));
226         // only attempt if we have been able to collect some packages
227         if (!packages.isEmpty()) {
228             try (OssindexClient client = newOssIndexClient()) {
229                 LOG.debug("OSS Index Analyzer submitting: " + packages);
230                 return client.requestComponentReports(packages);
231             }
232         }
233         LOG.warn("Unable to determine Package-URL identifiers for {} dependencies", dependencies.length);
234         return Collections.emptyMap();
235     }
236 
237     OssindexClient newOssIndexClient() {
238         return OssindexClientFactory.create(getSettings());
239     }
240 
241     /**
242      * Attempt to enrich given dependency with vulnerability details from OSS
243      * Index component-report.
244      *
245      * @param dependency the dependency to enrich
246      */
247     void enrich(final Dependency dependency) {
248         LOG.debug("Enrich dependency: {}", dependency);
249 
250         for (Identifier id : dependency.getSoftwareIdentifiers()) {
251             if (id instanceof PurlIdentifier) {
252                 LOG.debug("  Package: {} -> {}", id, id.getConfidence());
253 
254                 final PackageUrl purl = parsePackageUrl(id.getValue());
255                 if (purl != null && StringUtils.isNotBlank(purl.getVersion())) {
256                     try {
257                         final ComponentReport report = reports.get(purl);
258                         if (report == null) {
259                             LOG.debug("Missing component-report for: " + purl);
260                             continue;
261                         }
262 
263                         // expose the URL to the package details for report generation
264                         id.setUrl(report.getReference().toString());
265 
266                         report.getVulnerabilities().stream()
267                                 .map((vuln) -> transform(report, vuln))
268                                 .forEachOrdered((v) -> {
269                                     final Vulnerability existing = dependency.getVulnerabilities().stream()
270                                             .filter(e -> e.getName().equals(v.getName())).findFirst()
271                                             .orElse(null);
272                                     if (existing != null) {
273                                         //TODO - can we enhance anything other than the references?
274                                         existing.getReferences().addAll(v.getReferences());
275                                     } else {
276                                         dependency.addVulnerability(v);
277                                     }
278                                 });
279                     } catch (Exception e) {
280                         LOG.warn("Failed to fetch component-report for: {}", purl, e);
281                     }
282                 }
283             }
284         }
285     }
286 
287     /**
288      * Transform OSS Index component-report to ODC vulnerability.
289      *
290      * @param report the component report
291      * @param source the vulnerability from the report to transform
292      * @return the transformed vulnerability
293      */
294     private Vulnerability transform(final ComponentReport report, final ComponentReportVulnerability source) {
295         final Vulnerability result = new Vulnerability();
296         result.setSource(Vulnerability.Source.OSSINDEX);
297 
298         if (source.getCve() != null) {
299             result.setName(source.getCve());
300         } else {
301             String cve = null;
302             if (source.getTitle() != null) {
303                 final Matcher matcher = CVE_PATTERN.matcher(source.getTitle());
304                 if (matcher.find()) {
305                     cve = matcher.group();
306                 } else {
307                     cve = source.getTitle();
308                 }
309             }
310             if (cve == null && source.getReference() != null) {
311                 final Matcher matcher = CVE_PATTERN.matcher(source.getReference().toString());
312                 if (matcher.find()) {
313                     cve = matcher.group();
314                 }
315             }
316             result.setName(cve != null ? cve : source.getId());
317         }
318         result.setDescription(source.getDescription());
319         result.addCwe(source.getCwe());
320 
321         final double cvssScore = source.getCvssScore() != null ? source.getCvssScore().doubleValue() : -1;
322 
323         if (source.getCvssVector() != null) {
324             if (source.getCvssVector().startsWith("CVSS:3")) {
325                 result.setCvssV3(CvssUtil.vectorToCvssV3(source.getCvssVector(), cvssScore));
326             } else {
327                 // convert cvss details
328                 final CvssVector cvssVector = CvssVectorFactory.create(source.getCvssVector());
329                 final Map<String, String> metrics = cvssVector.getMetrics();
330                 if (cvssVector instanceof Cvss2Vector) {
331                     String tmp = metrics.get(Cvss2Vector.ACCESS_VECTOR);
332                     CvssV2Data.AccessVectorType accessVector = null;
333                     if (tmp != null) {
334                         accessVector = CvssV2Data.AccessVectorType.fromValue(tmp);
335                     }
336                     tmp = metrics.get(Cvss2Vector.ACCESS_COMPLEXITY);
337                     CvssV2Data.AccessComplexityType accessComplexity = null;
338                     if (tmp != null) {
339                         accessComplexity = CvssV2Data.AccessComplexityType.fromValue(tmp);
340                     }
341                     tmp = metrics.get(Cvss2Vector.AUTHENTICATION);
342                     CvssV2Data.AuthenticationType authentication = null;
343                     if (tmp != null) {
344                         authentication = CvssV2Data.AuthenticationType.fromValue(tmp);
345                     }
346                     tmp = metrics.get(Cvss2Vector.CONFIDENTIALITY_IMPACT);
347                     CvssV2Data.CiaType confidentialityImpact = null;
348                     if (tmp != null) {
349                         confidentialityImpact = CvssV2Data.CiaType.fromValue(tmp);
350                     }
351                     tmp = metrics.get(Cvss2Vector.INTEGRITY_IMPACT);
352                     CvssV2Data.CiaType integrityImpact = null;
353                     if (tmp != null) {
354                         integrityImpact = CvssV2Data.CiaType.fromValue(tmp);
355                     }
356                     tmp = metrics.get(Cvss2Vector.AVAILABILITY_IMPACT);
357                     CvssV2Data.CiaType availabilityImpact = null;
358                     if (tmp != null) {
359                         availabilityImpact = CvssV2Data.CiaType.fromValue(tmp);
360                     }
361                     final String severity = Cvss2Severity.of((float) cvssScore).name().toUpperCase();
362                     final CvssV2Data cvssData = new CvssV2Data("2.0", source.getCvssVector(), accessVector,
363                             accessComplexity, authentication, confidentialityImpact,
364                             integrityImpact, availabilityImpact, cvssScore,
365                             severity, null, null, null, null, null, null, null, null, null, null);
366                     final CvssV2 cvssV2 = new CvssV2(null, null, cvssData, severity, null, null, null, null, null, null, null);
367                     result.setCvssV2(cvssV2);
368                 } else {
369                     LOG.warn("Unsupported CVSS vector: {}", cvssVector);
370                     result.setUnscoredSeverity(Double.toString(cvssScore));
371                 }
372             }
373         } else {
374             LOG.debug("OSS has no vector for {}", result.getName());
375             result.setUnscoredSeverity(Double.toString(cvssScore));
376         }
377         // generate a reference to the vulnerability details on OSS Index
378         result.addReference(REFERENCE_TYPE, source.getTitle(), source.getReference().toString());
379 
380         // generate references to other references reported by OSS Index
381         source.getExternalReferences().forEach(externalReference
382                 -> result.addReference("OSSIndex", externalReference.toString(), externalReference.toString()));
383 
384         // attach vulnerable software details as best we can
385         final PackageUrl purl = report.getCoordinates();
386         try {
387             final VulnerableSoftwareBuilder builder = new VulnerableSoftwareBuilder()
388                     .part(Part.APPLICATION)
389                     .vendor(purl.getNamespaceAsString())
390                     .product(purl.getName())
391                     .version(purl.getVersion());
392 
393             // TODO: consider if we want/need to extract version-ranges to apply to vulnerable-software?
394             final VulnerableSoftware software = builder.build();
395             result.addVulnerableSoftware(software);
396             result.setMatchedVulnerableSoftware(software);
397         } catch (CpeValidationException e) {
398             LOG.warn("Unable to construct vulnerable-software for: {}", purl, e);
399         }
400 
401         return result;
402     }
403 }