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