1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
69
70
71
72
73 public class OssIndexAnalyzer extends AbstractAnalyzer {
74
75
76
77
78 private static final Logger LOG = LoggerFactory.getLogger(OssIndexAnalyzer.class);
79
80
81
82
83 private static final Pattern CVE_PATTERN = Pattern.compile("\\bCVE-\\d{4}-\\d{4,10}\\b");
84
85
86
87
88 public static final String REFERENCE_TYPE = "OSSINDEX";
89
90
91
92
93 private static Map<PackageUrl, ComponentReport> reports;
94
95
96
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
117
118
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
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
194 if (reports != null) {
195 enrich(dependency);
196 }
197 }
198
199 }
200
201
202
203
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
215
216
217
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
231
232
233
234
235
236 private Map<PackageUrl, ComponentReport> requestReports(final Dependency[] dependencies) throws Exception {
237 LOG.debug("Requesting component-reports for {} dependencies", dependencies.length);
238
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
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
262
263
264
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
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
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
308
309
310
311
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
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
399 result.addReference(REFERENCE_TYPE, source.getTitle(), source.getReference().toString());
400
401
402 source.getExternalReferences().forEach(externalReference
403 -> result.addReference("OSSIndex", externalReference.toString(), externalReference.toString()));
404
405
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
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 }