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
63 import org.apache.commons.lang3.StringUtils;
64 import org.owasp.dependencycheck.utils.CvssUtil;
65 import org.sonatype.goodies.packageurl.InvalidException;
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 " +
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
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
195 if (reports != null) {
196 enrich(dependency);
197 }
198 }
199
200 }
201
202
203
204
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
216
217
218
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
232
233
234
235
236
237 private Map<PackageUrl, ComponentReport> requestReports(final Dependency[] dependencies) throws Exception {
238 LOG.debug("Requesting component-reports for {} dependencies", dependencies.length);
239
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
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
263
264
265
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
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
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
309
310
311
312
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
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
400 result.addReference(REFERENCE_TYPE, source.getTitle(), source.getReference().toString());
401
402
403 source.getExternalReferences().forEach(externalReference
404 -> result.addReference("OSSIndex", externalReference.toString(), externalReference.toString()));
405
406
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
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 }