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  import org.apache.commons.lang3.StringUtils;
63  import org.owasp.dependencycheck.utils.CvssUtil;
64  import org.sonatype.goodies.packageurl.InvalidException;
65  import org.sonatype.ossindex.service.client.transport.Transport.TransportException;
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 required: https://ossindex.sonatype.org/doc/auth-required");
138           setEnabled(false);
139         }
140       }
141     }
142 
143     @Override
144     protected void analyzeDependency(final Dependency dependency, final Engine engine) throws AnalysisException {
145         // batch request component-reports for all dependencies
146         synchronized (FETCH_MUTIX) {
147             if (reports == null) {
148                 try {
149                     requestDelay();
150                     reports = requestReports(engine.getDependencies());
151                 } catch (SocketTimeoutException e) {
152                     final boolean warnOnly = getSettings().getBoolean(Settings.KEYS.ANALYZER_OSSINDEX_WARN_ONLY_ON_REMOTE_ERRORS, false);
153                     this.setEnabled(false);
154                     if (warnOnly) {
155                         LOG.warn("OSS Index socket timeout, disabling the analyzer", e);
156                     } else {
157                         LOG.debug("OSS Index socket timeout", e);
158                         throw new AnalysisException("Failed to establish socket to OSS Index", e);
159                     }
160                 } catch (Exception ex) {
161                     final String message = ex.getMessage();
162                     final boolean warnOnly = getSettings().getBoolean(Settings.KEYS.ANALYZER_OSSINDEX_WARN_ONLY_ON_REMOTE_ERRORS, false);
163                     this.setEnabled(false);
164                     if (StringUtils.contains(message, "401")) {
165                         if (warnOnly) {
166                             LOG.warn("Invalid credentials for the OSS Index, disabling the analyzer");
167                         } else {
168                             LOG.error("Invalid credentials for the OSS Index, disabling the analyzer");
169                             throw new AnalysisException("Invalid credentials provided for OSS Index", ex);
170                         }
171                     } else if (StringUtils.contains(message, "403")) {
172                         if (warnOnly) {
173                             LOG.warn("OSS Index access forbidden, disabling the analyzer");
174                         } else {
175                             LOG.error("OSS Index access forbidden, disabling the analyzer");
176                             throw new AnalysisException("OSS Index access forbidden", ex);
177                         }
178                     } else if (StringUtils.contains(message, "429")) {
179                         if (warnOnly) {
180                             LOG.warn("OSS Index rate limit exceeded, disabling the analyzer", ex);
181                         } else {
182                             throw new AnalysisException("OSS Index rate limit exceeded, disabling the analyzer", ex);
183                         }
184                     } else if (warnOnly) {
185                         LOG.warn("Error requesting component reports, disabling the analyzer. " + ex.getMessage(), ex);
186                     } else {
187                         LOG.debug("Error requesting component reports, disabling the analyzer", ex);
188                         throw new AnalysisException("Failed to request component-reports. " + ex.getMessage(), ex);
189                     }
190                 }
191             }
192 
193             // skip enrichment if we failed to fetch reports
194             if (reports != null) {
195                 enrich(dependency);
196             }
197         }
198 
199     }
200 
201     /**
202      * Delays each request (thread) by the configured amount of seconds, if the
203      * configuration is present.
204      */
205     private void requestDelay() throws InterruptedException {
206         final int delay = getSettings().getInt(Settings.KEYS.ANALYZER_OSSINDEX_REQUEST_DELAY, 0);
207         if (delay > 0) {
208             LOG.debug("Request delay: " + delay);
209             TimeUnit.SECONDS.sleep(delay);
210         }
211     }
212 
213     /**
214      * Helper to complain if unable to parse Package-URL.
215      *
216      * @param value the url to parse
217      * @return the package url
218      */
219     @Nullable
220     private PackageUrl parsePackageUrl(final String value) {
221         try {
222             return PackageUrl.parse(value);
223         } catch (InvalidException e) {
224             LOG.debug("Invalid Package-URL: {}", value, e);
225             return null;
226         }
227     }
228 
229     /**
230      * Batch request component-reports for all dependencies.
231      *
232      * @param dependencies the collection of dependencies
233      * @return the map of dependency to OSS Index's component-report
234      * @throws Exception thrown if there is an exception requesting the report
235      */
236     private Map<PackageUrl, ComponentReport> requestReports(final Dependency[] dependencies) throws Exception {
237         LOG.debug("Requesting component-reports for {} dependencies", dependencies.length);
238         // create requests for each dependency which has a PURL identifier
239         final List<PackageUrl> packages = new ArrayList<>();
240         Arrays.stream(dependencies).forEach(dependency -> dependency.getSoftwareIdentifiers().stream()
241                 .filter(id -> id instanceof PurlIdentifier)
242                 .map(id -> parsePackageUrl(id.getValue()))
243                 .filter(id -> id != null && StringUtils.isNotBlank(id.getVersion()))
244                 .forEach(packages::add));
245         // only attempt if we have been able to collect some packages
246         if (!packages.isEmpty()) {
247             try (OssindexClient client = newOssIndexClient()) {
248                 LOG.debug("OSS Index Analyzer submitting: " + packages);
249                 return client.requestComponentReports(packages);
250             }
251         }
252         LOG.warn("Unable to determine Package-URL identifiers for {} dependencies", dependencies.length);
253         return Collections.emptyMap();
254     }
255 
256     OssindexClient newOssIndexClient() {
257         return OssindexClientFactory.create(getSettings());
258     }
259 
260     /**
261      * Attempt to enrich given dependency with vulnerability details from OSS
262      * Index component-report.
263      *
264      * @param dependency the dependency to enrich
265      */
266     void enrich(final Dependency dependency) {
267         LOG.debug("Enrich dependency: {}", dependency);
268 
269         for (Identifier id : dependency.getSoftwareIdentifiers()) {
270             if (id instanceof PurlIdentifier) {
271                 LOG.debug("  Package: {} -> {}", id, id.getConfidence());
272 
273                 final PackageUrl purl = parsePackageUrl(id.getValue());
274                 if (purl != null && StringUtils.isNotBlank(purl.getVersion())) {
275                     try {
276                         final ComponentReport report = reports.get(purl);
277                         if (report == null) {
278                             LOG.debug("Missing component-report for: " + purl);
279                             continue;
280                         }
281 
282                         // expose the URL to the package details for report generation
283                         id.setUrl(report.getReference().toString());
284 
285                         report.getVulnerabilities().stream()
286                                 .map((vuln) -> transform(report, vuln))
287                                 .forEachOrdered((v) -> {
288                                     final Vulnerability existing = dependency.getVulnerabilities().stream()
289                                             .filter(e -> e.getName().equals(v.getName())).findFirst()
290                                             .orElse(null);
291                                     if (existing != null) {
292                                         //TODO - can we enhance anything other than the references?
293                                         existing.addReferences(v.getReferences());
294                                     } else {
295                                         dependency.addVulnerability(v);
296                                     }
297                                 });
298                     } catch (Exception e) {
299                         LOG.warn("Failed to fetch component-report for: {}", purl, e);
300                     }
301                 }
302             }
303         }
304     }
305 
306     /**
307      * Transform OSS Index component-report to ODC vulnerability.
308      *
309      * @param report the component report
310      * @param source the vulnerability from the report to transform
311      * @return the transformed vulnerability
312      */
313     private Vulnerability transform(final ComponentReport report, final ComponentReportVulnerability source) {
314         final Vulnerability result = new Vulnerability();
315         result.setSource(Vulnerability.Source.OSSINDEX);
316 
317         if (source.getCve() != null) {
318             result.setName(source.getCve());
319         } else {
320             String cve = null;
321             if (source.getTitle() != null) {
322                 final Matcher matcher = CVE_PATTERN.matcher(source.getTitle());
323                 if (matcher.find()) {
324                     cve = matcher.group();
325                 } else {
326                     cve = source.getTitle();
327                 }
328             }
329             if (cve == null && source.getReference() != null) {
330                 final Matcher matcher = CVE_PATTERN.matcher(source.getReference().toString());
331                 if (matcher.find()) {
332                     cve = matcher.group();
333                 }
334             }
335             result.setName(cve != null ? cve : source.getId());
336         }
337         result.setDescription(source.getDescription());
338         result.addCwe(source.getCwe());
339 
340         final double cvssScore = source.getCvssScore() != null ? source.getCvssScore().doubleValue() : -1;
341 
342         if (source.getCvssVector() != null) {
343             if (source.getCvssVector().startsWith("CVSS:4")) {
344                 result.setCvssV4(CvssUtil.vectorToCvssV4("ossindex", CvssV4.Type.PRIMARY, cvssScore, source.getCvssVector()));
345             } else if (source.getCvssVector().startsWith("CVSS:3")) {
346                 result.setCvssV3(CvssUtil.vectorToCvssV3(source.getCvssVector(), cvssScore));
347             } else {
348                 // convert cvss details
349                 final CvssVector cvssVector = CvssVectorFactory.create(source.getCvssVector());
350                 final Map<String, String> metrics = cvssVector.getMetrics();
351                 if (cvssVector instanceof Cvss2Vector) {
352                     String tmp = metrics.get(Cvss2Vector.ACCESS_VECTOR);
353                     CvssV2Data.AccessVectorType accessVector = null;
354                     if (tmp != null) {
355                         accessVector = CvssV2Data.AccessVectorType.fromValue(tmp);
356                     }
357                     tmp = metrics.get(Cvss2Vector.ACCESS_COMPLEXITY);
358                     CvssV2Data.AccessComplexityType accessComplexity = null;
359                     if (tmp != null) {
360                         accessComplexity = CvssV2Data.AccessComplexityType.fromValue(tmp);
361                     }
362                     tmp = metrics.get(Cvss2Vector.AUTHENTICATION);
363                     CvssV2Data.AuthenticationType authentication = null;
364                     if (tmp != null) {
365                         authentication = CvssV2Data.AuthenticationType.fromValue(tmp);
366                     }
367                     tmp = metrics.get(Cvss2Vector.CONFIDENTIALITY_IMPACT);
368                     CvssV2Data.CiaType confidentialityImpact = null;
369                     if (tmp != null) {
370                         confidentialityImpact = CvssV2Data.CiaType.fromValue(tmp);
371                     }
372                     tmp = metrics.get(Cvss2Vector.INTEGRITY_IMPACT);
373                     CvssV2Data.CiaType integrityImpact = null;
374                     if (tmp != null) {
375                         integrityImpact = CvssV2Data.CiaType.fromValue(tmp);
376                     }
377                     tmp = metrics.get(Cvss2Vector.AVAILABILITY_IMPACT);
378                     CvssV2Data.CiaType availabilityImpact = null;
379                     if (tmp != null) {
380                         availabilityImpact = CvssV2Data.CiaType.fromValue(tmp);
381                     }
382                     final String severity = Cvss2Severity.of((float) cvssScore).name().toUpperCase();
383                     final CvssV2Data cvssData = new CvssV2Data(CvssV2Data.Version._2_0, source.getCvssVector(), accessVector,
384                             accessComplexity, authentication, confidentialityImpact,
385                             integrityImpact, availabilityImpact, cvssScore,
386                             severity, null, null, null, null, null, null, null, null, null, null);
387                     final CvssV2 cvssV2 = new CvssV2(null, null, cvssData, severity, null, null, null, null, null, null, null);
388                     result.setCvssV2(cvssV2);
389                 } else {
390                     LOG.warn("Unsupported CVSS vector: {}", cvssVector);
391                     result.setUnscoredSeverity(Double.toString(cvssScore));
392                 }
393             }
394         } else {
395             LOG.debug("OSS has no vector for {}", result.getName());
396             result.setUnscoredSeverity(Double.toString(cvssScore));
397         }
398         // generate a reference to the vulnerability details on OSS Index
399         result.addReference(REFERENCE_TYPE, source.getTitle(), source.getReference().toString());
400 
401         // generate references to other references reported by OSS Index
402         source.getExternalReferences().forEach(externalReference
403                 -> result.addReference("OSSIndex", externalReference.toString(), externalReference.toString()));
404 
405         // attach vulnerable software details as best we can
406         final PackageUrl purl = report.getCoordinates();
407         try {
408             final VulnerableSoftwareBuilder builder = new VulnerableSoftwareBuilder()
409                     .part(Part.APPLICATION)
410                     .vendor(purl.getNamespaceAsString())
411                     .product(purl.getName())
412                     .version(purl.getVersion());
413 
414             // TODO: consider if we want/need to extract version-ranges to apply to vulnerable-software?
415             final VulnerableSoftware software = builder.build();
416             result.addVulnerableSoftware(software);
417             result.setMatchedVulnerableSoftware(software);
418         } catch (CpeValidationException e) {
419             LOG.warn("Unable to construct vulnerable-software for: {}", purl, e);
420         }
421 
422         return result;
423     }
424 }