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) 2012 Jeremy Long. All Rights Reserved.
17  */
18 package org.owasp.dependencycheck.analyzer;
19 
20 import java.io.FileFilter;
21 import java.io.UnsupportedEncodingException;
22 import java.net.URLEncoder;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.ListIterator;
28 import java.util.Set;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
31 import javax.annotation.concurrent.ThreadSafe;
32 import org.owasp.dependencycheck.Engine;
33 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
34 import org.owasp.dependencycheck.dependency.Dependency;
35 import org.owasp.dependencycheck.dependency.Evidence;
36 import org.owasp.dependencycheck.dependency.EvidenceType;
37 import org.owasp.dependencycheck.dependency.Identifier;
38 import org.owasp.dependencycheck.dependency.VulnerableSoftware;
39 import org.owasp.dependencycheck.utils.FileFilterBuilder;
40 import org.owasp.dependencycheck.utils.Settings;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 
44 /**
45  * This analyzer attempts to remove some well known false positives -
46  * specifically regarding the java runtime.
47  *
48  * @author Jeremy Long
49  */
50 @ThreadSafe
51 public class FalsePositiveAnalyzer extends AbstractAnalyzer {
52 
53     /**
54      * The Logger.
55      */
56     private static final Logger LOGGER = LoggerFactory.getLogger(FalsePositiveAnalyzer.class);
57     /**
58      * The file filter used to find DLL and EXE.
59      */
60     private static final FileFilter DLL_EXE_FILTER = FileFilterBuilder.newInstance().addExtensions("dll", "exe").build();
61     /**
62      * Regex to identify core java libraries and a few other commonly
63      * misidentified ones.
64      */
65     public static final Pattern CORE_JAVA = Pattern.compile("^cpe:/a:(sun|oracle|ibm):(j2[ems]e|"
66             + "java(_platform_micro_edition|_runtime_environment|_se|virtual_machine|se_development_kit|fx)?|"
67             + "jdk|jre|jsse)($|:.*)");
68     /**
69      * Regex to identify core jsf libraries.
70      */
71     public static final Pattern CORE_JAVA_JSF = Pattern.compile("^cpe:/a:(sun|oracle|ibm):jsf($|:.*)");
72     /**
73      * Regex to identify core java library files. This is currently incomplete.
74      */
75     public static final Pattern CORE_FILES = Pattern.compile("(^|/)((alt[-])?rt|jsse|jfxrt|jfr|jce|javaws|deploy|charsets)\\.jar$");
76     /**
77      * Regex to identify core jsf java library files. This is currently
78      * incomplete.
79      */
80     public static final Pattern CORE_JSF_FILES = Pattern.compile("(^|/)jsf[-][^/]*\\.jar$");
81 
82     //<editor-fold defaultstate="collapsed" desc="All standard implementation details of Analyzer">
83     /**
84      * The name of the analyzer.
85      */
86     private static final String ANALYZER_NAME = "False Positive Analyzer";
87     /**
88      * The phase that this analyzer is intended to run in.
89      */
90     private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.POST_IDENTIFIER_ANALYSIS;
91 
92     /**
93      * Returns the name of the analyzer.
94      *
95      * @return the name of the analyzer.
96      */
97     @Override
getName()98     public String getName() {
99         return ANALYZER_NAME;
100     }
101 
102     /**
103      * Returns the phase that the analyzer is intended to run in.
104      *
105      * @return the phase that the analyzer is intended to run in.
106      */
107     @Override
getAnalysisPhase()108     public AnalysisPhase getAnalysisPhase() {
109         return ANALYSIS_PHASE;
110     }
111 
112     /**
113      * <p>
114      * Returns the setting key to determine if the analyzer is enabled.</p>
115      *
116      * @return the key for the analyzer's enabled property
117      */
118     @Override
getAnalyzerEnabledSettingKey()119     protected String getAnalyzerEnabledSettingKey() {
120         return Settings.KEYS.ANALYZER_FALSE_POSITIVE_ENABLED;
121     }
122     //</editor-fold>
123 
124     /**
125      * Analyzes the dependencies and removes bad/incorrect CPE associations
126      * based on various heuristics.
127      *
128      * @param dependency the dependency to analyze.
129      * @param engine the engine that is scanning the dependencies
130      * @throws AnalysisException is thrown if there is an error reading the JAR
131      * file.
132      */
133     @Override
analyzeDependency(Dependency dependency, Engine engine)134     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
135         removeJreEntries(dependency);
136         removeBadMatches(dependency);
137         removeBadSpringMatches(dependency);
138         removeWrongVersionMatches(dependency);
139         removeSpuriousCPE(dependency);
140         removeDuplicativeEntriesFromJar(dependency, engine);
141         addFalseNegativeCPEs(dependency);
142     }
143 
144     /**
145      * Removes inaccurate matches on springframework CPEs.
146      *
147      * @param dependency the dependency to test for and remove known inaccurate
148      * CPE matches
149      */
removeBadSpringMatches(Dependency dependency)150     private void removeBadSpringMatches(Dependency dependency) {
151         String mustContain = null;
152         for (Identifier i : dependency.getIdentifiers()) {
153             if ("maven".contains(i.getType())
154                     && i.getValue() != null && i.getValue().startsWith("org.springframework.")) {
155                 final int endPoint = i.getValue().indexOf(':', 19);
156                 if (endPoint >= 0) {
157                     mustContain = i.getValue().substring(19, endPoint).toLowerCase();
158                     break;
159                 }
160             }
161         }
162         if (mustContain != null) {
163             final Set<Identifier> removalSet = new HashSet<>();
164             for (Identifier i : dependency.getIdentifiers()) {
165                 if ("cpe".contains(i.getType())
166                         && i.getValue() != null
167                         && i.getValue().startsWith("cpe:/a:springsource:")
168                         && !i.getValue().toLowerCase().contains(mustContain)) {
169                     removalSet.add(i);
170                 }
171             }
172             for (Identifier i : removalSet) {
173                 dependency.removeIdentifier(i);
174             }
175         }
176     }
177 
178     /**
179      * <p>
180      * Intended to remove spurious CPE entries. By spurious we mean duplicate,
181      * less specific CPE entries.</p>
182      * <p>
183      * Example:</p>
184      * <code>
185      * cpe:/a:some-vendor:some-product
186      * cpe:/a:some-vendor:some-product:1.5
187      * cpe:/a:some-vendor:some-product:1.5.2
188      * </code>
189      * <p>
190      * Should be trimmed to:</p>
191      * <code>
192      * cpe:/a:some-vendor:some-product:1.5.2
193      * </code>
194      *
195      * @param dependency the dependency being analyzed
196      */
197     @SuppressWarnings("null")
removeSpuriousCPE(Dependency dependency)198     private void removeSpuriousCPE(Dependency dependency) {
199         final List<Identifier> ids = new ArrayList<>(dependency.getIdentifiers());
200         Collections.sort(ids);
201         final ListIterator<Identifier> mainItr = ids.listIterator();
202         while (mainItr.hasNext()) {
203             final Identifier currentId = mainItr.next();
204             final VulnerableSoftware currentCpe = parseCpe(currentId.getType(), currentId.getValue());
205             if (currentCpe == null) {
206                 continue;
207             }
208             final ListIterator<Identifier> subItr = ids.listIterator(mainItr.nextIndex());
209             while (subItr.hasNext()) {
210                 final Identifier nextId = subItr.next();
211                 final VulnerableSoftware nextCpe = parseCpe(nextId.getType(), nextId.getValue());
212                 if (nextCpe == null) {
213                     continue;
214                 }
215                 //TODO fix the version problem below
216                 if (currentCpe.getVendor().equals(nextCpe.getVendor())) {
217                     if (currentCpe.getProduct().equals(nextCpe.getProduct())) {
218                         // see if one is contained in the other.. remove the contained one from dependency.getIdentifier
219                         final String currentVersion = currentCpe.getVersion();
220                         final String nextVersion = nextCpe.getVersion();
221                         if (currentVersion == null && nextVersion == null) {
222                             //how did we get here?
223                             LOGGER.debug("currentVersion and nextVersion are both null?");
224                         } else if (currentVersion == null && nextVersion != null) {
225                             dependency.removeIdentifier(currentId);
226                         } else if (nextVersion == null && currentVersion != null) {
227                             dependency.removeIdentifier(nextId);
228                         } else if (currentVersion.length() < nextVersion.length()) {
229                             if (nextVersion.startsWith(currentVersion) || "-".equals(currentVersion)) {
230                                 dependency.removeIdentifier(currentId);
231                             }
232                         } else if (currentVersion.startsWith(nextVersion) || "-".equals(nextVersion)) {
233                             dependency.removeIdentifier(nextId);
234                         }
235                     }
236                 }
237             }
238         }
239     }
240 
241     /**
242      * Removes any CPE entries for the JDK/JRE unless the filename ends with
243      * rt.jar
244      *
245      * @param dependency the dependency to remove JRE CPEs from
246      */
removeJreEntries(Dependency dependency)247     private void removeJreEntries(Dependency dependency) {
248         final Set<Identifier> removalSet = new HashSet<>();
249         for (Identifier i : dependency.getIdentifiers()) {
250             final Matcher coreCPE = CORE_JAVA.matcher(i.getValue());
251             final Matcher coreFiles = CORE_FILES.matcher(dependency.getFileName());
252             if (coreCPE.matches() && !coreFiles.matches()) {
253                 removalSet.add(i);
254             }
255             final Matcher coreJsfCPE = CORE_JAVA_JSF.matcher(i.getValue());
256             final Matcher coreJsfFiles = CORE_JSF_FILES.matcher(dependency.getFileName());
257             if (coreJsfCPE.matches() && !coreJsfFiles.matches()) {
258                 removalSet.add(i);
259             }
260         }
261         for (Identifier i : removalSet) {
262             dependency.removeIdentifier(i);
263         }
264     }
265 
266     /**
267      * Parses a CPE string into an IndexEntry.
268      *
269      * @param type the type of identifier
270      * @param value the cpe identifier to parse
271      * @return an VulnerableSoftware object constructed from the identifier
272      */
parseCpe(String type, String value)273     private VulnerableSoftware parseCpe(String type, String value) {
274         if (!"cpe".equals(type)) {
275             return null;
276         }
277         final VulnerableSoftware cpe = new VulnerableSoftware();
278         try {
279             cpe.parseName(value);
280         } catch (UnsupportedEncodingException ex) {
281             LOGGER.trace("", ex);
282             return null;
283         }
284         return cpe;
285     }
286 
287     /**
288      * Removes bad CPE matches for a dependency. Unfortunately, right now these
289      * are hard-coded patches for specific problems identified when testing this
290      * on a LARGE volume of jar files.
291      *
292      * @param dependency the dependency to analyze
293      */
removeBadMatches(Dependency dependency)294     protected void removeBadMatches(Dependency dependency) {
295 
296         /* TODO - can we utilize the pom's groupid and artifactId to filter??? most of
297          * these are due to low quality data.  Other idea would be to say any CPE
298          * found based on LOW confidence evidence should have a different CPE type? (this
299          * might be a better solution then just removing the URL for "best-guess" matches).
300          */
301         //Set<Evidence> groupId = dependency.getVendorEvidence().getEvidence("pom", "groupid");
302         //Set<Evidence> artifactId = dependency.getVendorEvidence().getEvidence("pom", "artifactid");
303         for (Identifier i : dependency.getIdentifiers()) {
304             //TODO move this startsWith expression to the base suppression file
305             if ("cpe".equals(i.getType())) {
306                 if ((i.getValue().matches(".*c\\+\\+.*")
307                         || i.getValue().startsWith("cpe:/a:file:file")
308                         || i.getValue().startsWith("cpe:/a:mozilla:mozilla")
309                         || i.getValue().startsWith("cpe:/a:cvs:cvs")
310                         || i.getValue().startsWith("cpe:/a:ftp:ftp")
311                         || i.getValue().startsWith("cpe:/a:tcp:tcp")
312                         || i.getValue().startsWith("cpe:/a:ssh:ssh")
313                         || i.getValue().startsWith("cpe:/a:lookup:lookup"))
314                         && (dependency.getFileName().toLowerCase().endsWith(".jar")
315                         || dependency.getFileName().toLowerCase().endsWith("pom.xml")
316                         || dependency.getFileName().toLowerCase().endsWith(".dll")
317                         || dependency.getFileName().toLowerCase().endsWith(".exe")
318                         || dependency.getFileName().toLowerCase().endsWith(".nuspec")
319                         || dependency.getFileName().toLowerCase().endsWith(".zip")
320                         || dependency.getFileName().toLowerCase().endsWith(".sar")
321                         || dependency.getFileName().toLowerCase().endsWith(".apk")
322                         || dependency.getFileName().toLowerCase().endsWith(".tar")
323                         || dependency.getFileName().toLowerCase().endsWith(".gz")
324                         || dependency.getFileName().toLowerCase().endsWith(".tgz")
325                         || dependency.getFileName().toLowerCase().endsWith(".ear")
326                         || dependency.getFileName().toLowerCase().endsWith(".war"))) {
327                     //itr.remove();
328                     dependency.removeIdentifier(i);
329                 } else if ((i.getValue().startsWith("cpe:/a:jquery:jquery")
330                         || i.getValue().startsWith("cpe:/a:prototypejs:prototype")
331                         || i.getValue().startsWith("cpe:/a:yahoo:yui"))
332                         && (dependency.getFileName().toLowerCase().endsWith(".jar")
333                         || dependency.getFileName().toLowerCase().endsWith("pom.xml")
334                         || dependency.getFileName().toLowerCase().endsWith(".dll")
335                         || dependency.getFileName().toLowerCase().endsWith(".exe"))) {
336                     //itr.remove();
337                     dependency.removeIdentifier(i);
338                 } else if ((i.getValue().startsWith("cpe:/a:microsoft:excel")
339                         || i.getValue().startsWith("cpe:/a:microsoft:word")
340                         || i.getValue().startsWith("cpe:/a:microsoft:visio")
341                         || i.getValue().startsWith("cpe:/a:microsoft:powerpoint")
342                         || i.getValue().startsWith("cpe:/a:microsoft:office")
343                         || i.getValue().startsWith("cpe:/a:core_ftp:core_ftp"))
344                         && (dependency.getFileName().toLowerCase().endsWith(".jar")
345                         || dependency.getFileName().toLowerCase().endsWith(".ear")
346                         || dependency.getFileName().toLowerCase().endsWith(".war")
347                         || dependency.getFileName().toLowerCase().endsWith("pom.xml"))) {
348                     //itr.remove();
349                     dependency.removeIdentifier(i);
350                 } else if (i.getValue().startsWith("cpe:/a:apache:maven")
351                         && !dependency.getFileName().toLowerCase().matches("maven-core-[\\d\\.]+\\.jar")) {
352                     //itr.remove();
353                     dependency.removeIdentifier(i);
354                 } else if (i.getValue().startsWith("cpe:/a:m-core:m-core")) {
355                     boolean found = false;
356                     for (Evidence e : dependency.getEvidence(EvidenceType.PRODUCT)) {
357                         if ("m-core".equalsIgnoreCase(e.getValue())) {
358                             found = true;
359                             break;
360                         }
361                     }
362                     if (!found) {
363                         for (Evidence e : dependency.getEvidence(EvidenceType.VENDOR)) {
364                             if ("m-core".equalsIgnoreCase(e.getValue())) {
365                                 found = true;
366                                 break;
367                             }
368                         }
369                     }
370                     if (!found) {
371                         //itr.remove();
372                         dependency.removeIdentifier(i);
373                     }
374                 } else if (i.getValue().startsWith("cpe:/a:jboss:jboss")
375                         && !dependency.getFileName().toLowerCase().matches("jboss-?[\\d\\.-]+(GA)?\\.jar")) {
376                     //itr.remove();
377                     dependency.removeIdentifier(i);
378                 }
379             }
380         }
381     }
382 
383     /**
384      * Removes CPE matches for the wrong version of a dependency. Currently,
385      * this only covers Axis 1 & 2.
386      *
387      * @param dependency the dependency to analyze
388      */
removeWrongVersionMatches(Dependency dependency)389     private void removeWrongVersionMatches(Dependency dependency) {
390         final Set<Identifier> identifiersToRemove = new HashSet<>();
391         final String fileName = dependency.getFileName();
392         if (fileName != null && fileName.contains("axis2")) {
393             for (Identifier i : dependency.getIdentifiers()) {
394                 if ("cpe".equals(i.getType())) {
395                     final String cpe = i.getValue();
396                     if (cpe != null && (cpe.startsWith("cpe:/a:apache:axis:") || "cpe:/a:apache:axis".equals(cpe))) {
397                         identifiersToRemove.add(i);
398                     }
399                 }
400             }
401         } else if (fileName != null && fileName.contains("axis")) {
402             for (Identifier i : dependency.getIdentifiers()) {
403                 if ("cpe".equals(i.getType())) {
404                     final String cpe = i.getValue();
405                     if (cpe != null && (cpe.startsWith("cpe:/a:apache:axis2:") || "cpe:/a:apache:axis2".equals(cpe))) {
406                         identifiersToRemove.add(i);
407                     }
408                 }
409             }
410         }
411         for (Identifier i : identifiersToRemove) {
412             dependency.removeIdentifier(i);
413         }
414     }
415 
416     /**
417      * There are some known CPE entries, specifically regarding sun and oracle
418      * products due to the acquisition and changes in product names, that based
419      * on given evidence we can add the related CPE entries to ensure a complete
420      * list of CVE entries.
421      *
422      * @param dependency the dependency being analyzed
423      */
addFalseNegativeCPEs(Dependency dependency)424     private void addFalseNegativeCPEs(Dependency dependency) {
425         //TODO move this to the hint analyzer
426         for (final Identifier identifier : dependency.getIdentifiers()) {
427             if ("cpe".equals(identifier.getType()) && identifier.getValue() != null
428                     && (identifier.getValue().startsWith("cpe:/a:oracle:opensso:")
429                     || identifier.getValue().startsWith("cpe:/a:oracle:opensso_enterprise:")
430                     || identifier.getValue().startsWith("cpe:/a:sun:opensso_enterprise:")
431                     || identifier.getValue().startsWith("cpe:/a:sun:opensso:"))) {
432                 final String[] parts = identifier.getValue().split(":");
433                 final int pos = parts[0].length() + parts[1].length() + parts[2].length() + parts[3].length() + 4;
434                 final String newCpe = String.format("cpe:/a:sun:opensso_enterprise:%s", identifier.getValue().substring(pos));
435                 final String newCpe2 = String.format("cpe:/a:oracle:opensso_enterprise:%s", identifier.getValue().substring(pos));
436                 final String newCpe3 = String.format("cpe:/a:sun:opensso:%s", identifier.getValue().substring(pos));
437                 final String newCpe4 = String.format("cpe:/a:oracle:opensso:%s", identifier.getValue().substring(pos));
438                 try {
439                     dependency.addIdentifier("cpe", newCpe,
440                             String.format(CPEAnalyzer.NVD_SEARCH_URL, URLEncoder.encode(newCpe, "UTF-8")),
441                             identifier.getConfidence());
442                     dependency.addIdentifier("cpe", newCpe2,
443                             String.format(CPEAnalyzer.NVD_SEARCH_URL, URLEncoder.encode(newCpe2, "UTF-8")),
444                             identifier.getConfidence());
445                     dependency.addIdentifier("cpe", newCpe3,
446                             String.format(CPEAnalyzer.NVD_SEARCH_URL, URLEncoder.encode(newCpe3, "UTF-8")),
447                             identifier.getConfidence());
448                     dependency.addIdentifier("cpe", newCpe4,
449                             String.format(CPEAnalyzer.NVD_SEARCH_URL, URLEncoder.encode(newCpe4, "UTF-8")),
450                             identifier.getConfidence());
451                 } catch (UnsupportedEncodingException ex) {
452                     LOGGER.debug("", ex);
453                 }
454             }
455             if ("cpe".equals(identifier.getType()) && identifier.getValue() != null
456                     && identifier.getValue().startsWith("cpe:/a:apache:santuario_xml_security_for_java:")) {
457                 final String[] parts = identifier.getValue().split(":");
458                 final int pos = parts[0].length() + parts[1].length() + parts[2].length() + parts[3].length() + 4;
459                 final String newCpe = String.format("cpe:/a:apache:xml_security_for_java:%s", identifier.getValue().substring(pos));
460                 try {
461                     dependency.addIdentifier("cpe", newCpe,
462                             String.format(CPEAnalyzer.NVD_SEARCH_URL, URLEncoder.encode(newCpe, "UTF-8")),
463                             identifier.getConfidence());
464                 } catch (UnsupportedEncodingException ex) {
465                     LOGGER.debug("", ex);
466                 }
467             }
468         }
469     }
470 
471     /**
472      * Removes duplicate entries identified that are contained within JAR files.
473      * These occasionally crop up due to POM entries or other types of files
474      * (such as DLLs and EXEs) being contained within the JAR.
475      *
476      * @param dependency the dependency that might be a duplicate
477      * @param engine the engine used to scan all dependencies
478      */
removeDuplicativeEntriesFromJar(Dependency dependency, Engine engine)479     private synchronized void removeDuplicativeEntriesFromJar(Dependency dependency, Engine engine) {
480         if (dependency.getFileName().toLowerCase().endsWith("pom.xml")
481                 || DLL_EXE_FILTER.accept(dependency.getActualFile())) {
482             String parentPath = dependency.getFilePath().toLowerCase();
483             if (parentPath.contains(".jar")) {
484                 parentPath = parentPath.substring(0, parentPath.indexOf(".jar") + 4);
485                 final Dependency[] dependencies = engine.getDependencies();
486                 final Dependency parent = findDependency(parentPath, dependencies);
487                 if (parent != null) {
488                     boolean remove = false;
489                     for (Identifier i : dependency.getIdentifiers()) {
490                         if ("cpe".equals(i.getType())) {
491                             final String trimmedCPE = trimCpeToVendor(i.getValue());
492                             for (Identifier parentId : parent.getIdentifiers()) {
493                                 if ("cpe".equals(parentId.getType()) && parentId.getValue().startsWith(trimmedCPE)) {
494                                     remove |= true;
495                                 }
496                             }
497                         }
498                         if (!remove) { //we can escape early
499                             return;
500                         }
501                     }
502                     if (remove) {
503                         engine.removeDependency(dependency);
504                     }
505                 }
506             }
507         }
508     }
509 
510     /**
511      * Retrieves a given dependency, based on a given path, from a list of
512      * dependencies.
513      *
514      * @param dependencyPath the path of the dependency to return
515      * @param dependencies the array of dependencies to search
516      * @return the dependency object for the given path, otherwise null
517      */
findDependency(String dependencyPath, Dependency[] dependencies)518     private Dependency findDependency(String dependencyPath, Dependency[] dependencies) {
519         for (Dependency d : dependencies) {
520             if (d.getFilePath().equalsIgnoreCase(dependencyPath)) {
521                 return d;
522             }
523         }
524         return null;
525     }
526 
527     /**
528      * Takes a full CPE and returns the CPE trimmed to include only vendor and
529      * product.
530      *
531      * @param value the CPE value to trim
532      * @return a CPE value that only includes the vendor and product
533      */
trimCpeToVendor(String value)534     private String trimCpeToVendor(String value) {
535         //cpe:/a:jruby:jruby:1.0.8
536         final int pos1 = value.indexOf(':', 7); //right of vendor
537         final int pos2 = value.indexOf(':', pos1 + 1); //right of product
538         if (pos2 < 0) {
539             return value;
540         } else {
541             return value.substring(0, pos2);
542         }
543     }
544 }
545