##### Coverage Report - org.owasp.dependencycheck.analyzer.FalsePositiveAnalyzer

Classes in this File | Line Coverage | Branch Coverage | Complexity
FalsePositiveAnalyzer |
69% | 80/115
46% | 52/112
| 6.454545454545454;6.455

1 |  |
     /*
2 |  |
      * This file is part of dependency-check-core.
3 |  |
      *
4 |  |
      * Dependency-check-core is free software: you can redistribute it and/or modify it
5 |  |
      * under the terms of the GNU General Public License as published by the Free
6 |  |
      * Software Foundation, either version 3 of the License, or (at your option) any
7 |  |
      * later version.
8 |  |
      *
9 |  |
      * Dependency-check-core is distributed in the hope that it will be useful, but
10 |  |
      * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 |  |
      * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12 |  |
      * details.
13 |  |
      *
14 |  |
      * You should have received a copy of the GNU General Public License along with
15 |  |
      * dependency-check-core. If not, see http://www.gnu.org/licenses/.
16 |  |
      *
17 |  |
      * Copyright (c) 2012 Jeremy Long. All Rights Reserved.
18 |  |
      */
19 |  |
     package org.owasp.dependencycheck.analyzer;
20 |  |
21 |  |
     import java.io.UnsupportedEncodingException;
22 |  |
     import java.net.URLEncoder;
23 |  |
     import java.util.ArrayList;
24 |  |
     import java.util.Collections;
25 |  |
     import java.util.Iterator;
26 |  |
     import java.util.List;
27 |  |
     import java.util.ListIterator;
28 |  |
     import java.util.Set;
29 |  |
     import java.util.logging.Level;
30 |  |
     import java.util.logging.Logger;
31 |  |
     import java.util.regex.Matcher;
32 |  |
     import java.util.regex.Pattern;
33 |  |
     import org.owasp.dependencycheck.Engine;
34 |  |
     import org.owasp.dependencycheck.dependency.Dependency;
35 |  |
     import org.owasp.dependencycheck.dependency.Identifier;
36 |  |
     import org.owasp.dependencycheck.dependency.VulnerableSoftware;
37 |  |
38 |  |
     /**
39 |  |
      * This analyzer attempts to remove some well known false positives -
40 |  |
      * specifically regarding the java runtime.
41 |  |
      *
42 |  |
      * @author Jeremy Long (jeremy.long@owasp.org)
43 |  |
      */
44 | 7 |
     public class FalsePositiveAnalyzer extends AbstractAnalyzer {
45 |  |
46 |  |
         //<editor-fold defaultstate="collapsed" desc="All standard implmentation details of Analyzer">
47 |  |
         /**
48 |  |
          * The set of file extensions supported by this analyzer.
49 |  |
          */
50 | 1 |
         private static final Set<String> EXTENSIONS = null;
51 |  |
         /**
52 |  |
          * The name of the analyzer.
53 |  |
          */
54 |  |
         private static final String ANALYZER_NAME = "False Positive Analyzer";
55 |  |
         /**
56 |  |
          * The phase that this analyzer is intended to run in.
57 |  |
          */
58 | 1 |
         private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.POST_IDENTIFIER_ANALYSIS;
59 |  |
60 |  |
         /**
61 |  |
          * Returns a list of file EXTENSIONS supported by this analyzer.
62 |  |
          *
63 |  |
          * @return a list of file EXTENSIONS supported by this analyzer.
64 |  |
          */
65 |  |
         public Set<String> getSupportedExtensions() {
66 | 132 |
             return EXTENSIONS;
67 |  |
         }
68 |  |
69 |  |
         /**
70 |  |
          * Returns the name of the analyzer.
71 |  |
          *
72 |  |
          * @return the name of the analyzer.
73 |  |
          */
74 |  |
         public String getName() {
75 | 9 |
             return ANALYZER_NAME;
76 |  |
         }
77 |  |
78 |  |
         /**
79 |  |
          * Returns whether or not this analyzer can process the given extension.
80 |  |
          *
81 |  |
          * @param extension the file extension to test for support
82 |  |
          * @return whether or not the specified file extension is supported by this
83 |  |
          * analyzer.
84 |  |
          */
85 |  |
         public boolean supportsExtension(String extension) {
86 | 9 |
             return true;
87 |  |
         }
88 |  |
89 |  |
         /**
90 |  |
          * Returns the phase that the analyzer is intended to run in.
91 |  |
          *
92 |  |
          * @return the phase that the analyzer is intended to run in.
93 |  |
          */
94 |  |
         public AnalysisPhase getAnalysisPhase() {
95 | 6 |
             return ANALYSIS_PHASE;
96 |  |
         }
97 |  |
         //</editor-fold>
98 |  |
99 |  |
         /**
100 |  |
          * Analyzes the dependencies and removes bad/incorrect CPE associations
101 |  |
          * based on various heuristics.
102 |  |
          *
103 |  |
          * @param dependency the dependency to analyze.
104 |  |
          * @param engine the engine that is scanning the dependencies
105 |  |
          * @throws AnalysisException is thrown if there is an error reading the JAR
106 |  |
          * file.
107 |  |
          */
108 |  |
         @Override
109 |  |
         public void analyze(Dependency dependency, Engine engine) throws AnalysisException {
110 | 15 |
             removeJreEntries(dependency);
111 | 15 |
             removeBadMatches(dependency);
112 | 15 |
             removeWrongVersionMatches(dependency);
113 | 15 |
             removeSpuriousCPE(dependency);
114 | 15 |
             addFalseNegativeCPEs(dependency);
115 | 15 |
         }
116 |  |
117 |  |
         /**
118 |  |
          * <p>Intended to remove spurious CPE entries. By spurious we mean
119 |  |
          * duplicate, less specific CPE entries.</p>
120 |  |
          * <p>Example:</p>
121 |  |
          * <code>
122 |  |
          * cpe:/a:some-vendor:some-product
123 |  |
          * cpe:/a:some-vendor:some-product:1.5
124 |  |
          * cpe:/a:some-vendor:some-product:1.5.2
125 |  |
          * </code>
126 |  |
          * <p>Should be trimmed to:</p>
127 |  |
          * <code>
128 |  |
          * cpe:/a:some-vendor:some-product:1.5.2
129 |  |
          * </code>
130 |  |
          *
131 |  |
          * @param dependency the dependency being analyzed
132 |  |
          */
133 |  |
         @SuppressWarnings("null")
134 |  |
         private void removeSpuriousCPE(Dependency dependency) {
135 | 15 |
             final List<Identifier> ids = new ArrayList<Identifier>();
136 | 15 |
             ids.addAll(dependency.getIdentifiers());
137 | 15 |
             Collections.sort(ids);
138 | 15 |
             final ListIterator<Identifier> mainItr = ids.listIterator();
139 | 36 |
             while (mainItr.hasNext()) {
140 | 21 |
                 final Identifier currentId = mainItr.next();
141 | 21 |
                 final VulnerableSoftware currentCpe = parseCpe(currentId.getType(), currentId.getValue());
142 | 21 |
                 if (currentCpe == null) {
143 | 0 |
                     continue;
144 |  |
                 }
145 | 21 |
                 final ListIterator<Identifier> subItr = ids.listIterator(mainItr.nextIndex());
146 | 32 |
                 while (subItr.hasNext()) {
147 | 11 |
                     final Identifier nextId = subItr.next();
148 | 11 |
                     final VulnerableSoftware nextCpe = parseCpe(nextId.getType(), nextId.getValue());
149 | 11 |
                     if (nextCpe == null) {
150 | 0 |
                         continue;
151 |  |
                     }
152 |  |
                     //TODO fix the version problem below
153 | 11 |
                     if (currentCpe.getVendor().equals(nextCpe.getVendor())) {
154 | 3 |
                         if (currentCpe.getProduct().equals(nextCpe.getProduct())) {
155 |  |
                             // see if one is contained in the other.. remove the contained one from dependency.getIdentifier
156 | 3 |
                             final String currentVersion = currentCpe.getVersion();
157 | 3 |
                             final String nextVersion = nextCpe.getVersion();
158 | 3 |
                             if (currentVersion == null && nextVersion == null) {
159 |  |
                                 //how did we get here?
160 | 0 |
                                 Logger.getLogger(FalsePositiveAnalyzer.class
161 |  |
                                         .getName()).log(Level.FINE, "currentVersion and nextVersion are both null?");
162 | 3 |
                             } else if (currentVersion == null && nextVersion != null) {
163 | 3 |
                                 dependency.getIdentifiers().remove(currentId);
164 | 0 |
                             } else if (nextVersion == null && currentVersion != null) {
165 | 0 |
                                 dependency.getIdentifiers().remove(nextId);
166 | 0 |
                             } else if (currentVersion.length() < nextVersion.length()) {
167 | 0 |
                                 if (nextVersion.startsWith(currentVersion) || "-".equals(currentVersion)) {
168 | 0 |
                                     dependency.getIdentifiers().remove(currentId);
169 |  |
                                 }
170 |  |
                             } else {
171 | 0 |
                                 if (currentVersion.startsWith(nextVersion) || "-".equals(nextVersion)) {
172 | 0 |
                                     dependency.getIdentifiers().remove(nextId);
173 |  |
                                 }
174 |  |
                             }
175 |  |
                         }
176 |  |
                     }
177 | 11 |
                 }
178 | 21 |
             }
179 | 15 |
         }
180 |  |
         /**
181 |  |
          * Regex to identify core java libraries and a few other commonly
182 |  |
          * misidentified ones.
183 |  |
          */
184 | 1 |
         public static final Pattern CORE_JAVA = Pattern.compile("^cpe:/a:(sun|oracle|ibm):(j2[ems]e|"
185 |  |
                 + "java(_platfrom_micro_edition|_runtime_environment|_se|virtual_machine|se_development_kit|fx)?|"
186 |  |
                 + "jdk|jre|jsf|jsse)($|:.*)");
187 |  |
         /**
188 |  |
          * Regex to identify core java library files. This is currently incomplete.
189 |  |
          */
190 | 1 |
         public static final Pattern CORE_FILES = Pattern.compile("^((alt[-])?rt|jsf[-].*|jsse|jfxrt|jfr|jce|javaws|deploy|charsets)\\.jar$");
191 |  |
192 |  |
         /**
193 |  |
          * Removes any CPE entries for the JDK/JRE unless the filename ends with
194 |  |
          * rt.jar
195 |  |
          *
196 |  |
          * @param dependency the dependency to remove JRE CPEs from
197 |  |
          */
198 |  |
         private void removeJreEntries(Dependency dependency) {
199 | 15 |
             final Set<Identifier> identifiers = dependency.getIdentifiers();
200 | 15 |
             final Iterator<Identifier> itr = identifiers.iterator();
201 | 37 |
             while (itr.hasNext()) {
202 | 22 |
                 final Identifier i = itr.next();
203 | 22 |
                 final Matcher coreCPE = CORE_JAVA.matcher(i.getValue());
204 | 22 |
                 final Matcher coreFiles = CORE_FILES.matcher(dependency.getFileName());
205 | 22 |
                 if (coreCPE.matches() && !coreFiles.matches()) {
206 | 0 |
                     itr.remove();
207 |  |
                 }
208 |  |
209 |  |
                 //replacecd with the regex above.
210 |  |
                 //            if (("cpe:/a:sun:java".equals(i.getValue())
211 |  |
                 //                    || "cpe:/a:oracle:java".equals(i.getValue())
212 |  |
                 //                    || "cpe:/a:ibm:java".equals(i.getValue())
213 |  |
                 //                    || "cpe:/a:sun:j2se".equals(i.getValue())
214 |  |
                 //                    || "cpe:/a:oracle:j2se".equals(i.getValue())
215 |  |
                 //                    || i.getValue().startsWith("cpe:/a:sun:java:")
216 |  |
                 //                    || i.getValue().startsWith("cpe:/a:sun:j2se:")
217 |  |
                 //                    || i.getValue().startsWith("cpe:/a:sun:java:jre")
218 |  |
                 //                    || i.getValue().startsWith("cpe:/a:sun:java:jdk")
219 |  |
                 //                    || i.getValue().startsWith("cpe:/a:sun:java_se")
220 |  |
                 //                    || i.getValue().startsWith("cpe:/a:oracle:java_se")
221 |  |
                 //                    || i.getValue().startsWith("cpe:/a:oracle:java:")
222 |  |
                 //                    || i.getValue().startsWith("cpe:/a:oracle:j2se:")
223 |  |
                 //                    || i.getValue().startsWith("cpe:/a:oracle:jre")
224 |  |
                 //                    || i.getValue().startsWith("cpe:/a:oracle:jdk")
225 |  |
                 //                    || i.getValue().startsWith("cpe:/a:ibm:java:"))
226 |  |
                 //                    && !dependency.getFileName().toLowerCase().endsWith("rt.jar")) {
227 |  |
                 //                itr.remove();
228 |  |
                 //            }
229 | 22 |
             }
230 | 15 |
         }
231 |  |
232 |  |
         /**
233 |  |
          * Parses a CPE string into an IndexEntry.
234 |  |
          *
235 |  |
          * @param type the type of identifier
236 |  |
          * @param value the cpe identifier to parse
237 |  |
          * @return an VulnerableSoftware object constructed from the identifier
238 |  |
          */
239 |  |
         private VulnerableSoftware parseCpe(String type, String value) {
240 | 32 |
             if (!"cpe".equals(type)) {
241 | 0 |
                 return null;
242 |  |
             }
243 | 32 |
             final VulnerableSoftware cpe = new VulnerableSoftware();
244 |  |
             try {
245 | 32 |
                 cpe.parseName(value);
246 | 0 |
             } catch (UnsupportedEncodingException ex) {
247 | 0 |
                 Logger.getLogger(FalsePositiveAnalyzer.class.getName()).log(Level.FINEST, null, ex);
248 | 0 |
                 return null;
249 | 32 |
             }
250 | 32 |
             return cpe;
251 |  |
         }
252 |  |
253 |  |
         /**
254 |  |
          * Removes bad CPE matches for a dependency. Unfortunately, right now these
255 |  |
          * are hard-coded patches for specific problems identified when testing this
256 |  |
          * on a LARGE volume of jar files.
257 |  |
          *
258 |  |
          * @param dependency the dependency to analyze
259 |  |
          */
260 |  |
         private void removeBadMatches(Dependency dependency) {
261 | 15 |
             final Set<Identifier> identifiers = dependency.getIdentifiers();
262 | 15 |
             final Iterator<Identifier> itr = identifiers.iterator();
263 |  |
264 |  |
             /* TODO - can we utilize the pom's groupid and artifactId to filter??? most of
265 |  |
              * these are due to low quality data.  Other idea would be to say any CPE
266 |  |
              * found based on LOW confidence evidence should have a different CPE type? (this
267 |  |
              * might be a better solution then just removing the URL for "best-guess" matches).
268 |  |
              */
269 |  |
270 |  |
             //Set<Evidence> groupId = dependency.getVendorEvidence().getEvidence("pom", "groupid");
271 |  |
             //Set<Evidence> artifactId = dependency.getVendorEvidence().getEvidence("pom", "artifactid");
272 |  |
273 | 37 |
             while (itr.hasNext()) {
274 | 22 |
                 final Identifier i = itr.next();
275 |  |
                 //TODO move this startswith expression to a configuration file?
276 | 22 |
                 if ("cpe".equals(i.getType())) {
277 | 22 |
                     if ((i.getValue().matches(".*c\\+\\+.*")
278 |  |
                             || i.getValue().startsWith("cpe:/a:jquery:jquery")
279 |  |
                             || i.getValue().startsWith("cpe:/a:prototypejs:prototype")
280 |  |
                             || i.getValue().startsWith("cpe:/a:yahoo:yui")
281 |  |
                             || i.getValue().startsWith("cpe:/a:file:file")
282 |  |
                             || i.getValue().startsWith("cpe:/a:mozilla:mozilla")
283 |  |
                             || i.getValue().startsWith("cpe:/a:cvs:cvs")
284 |  |
                             || i.getValue().startsWith("cpe:/a:ftp:ftp")
285 |  |
                             || i.getValue().startsWith("cpe:/a:ssh:ssh"))
286 |  |
                             && dependency.getFileName().toLowerCase().endsWith(".jar")) {
287 | 0 |
                         itr.remove();
288 | 22 |
                     } else if (i.getValue().startsWith("cpe:/a:apache:maven")
289 |  |
                             && !dependency.getFileName().toLowerCase().matches("maven-core-[\\d\\.]+\\.jar")) {
290 | 0 |
                         itr.remove();
291 |  |
                     }
292 |  |
                 }
293 | 22 |
             }
294 | 15 |
         }
295 |  |
296 |  |
         /**
297 |  |
          * Removes CPE matches for the wrong version of a dependency. Currently,
298 |  |
          * this only covers Axis 1 & 2.
299 |  |
          *
300 |  |
          * @param dependency the dependency to analyze
301 |  |
          */
302 |  |
         private void removeWrongVersionMatches(Dependency dependency) {
303 | 15 |
             final Set<Identifier> identifiers = dependency.getIdentifiers();
304 | 15 |
             final Iterator<Identifier> itr = identifiers.iterator();
305 |  |
306 | 15 |
             final String fileName = dependency.getFileName();
307 | 15 |
             if (fileName != null && fileName.contains("axis2")) {
308 | 3 |
                 while (itr.hasNext()) {
309 | 2 |
                     final Identifier i = itr.next();
310 | 2 |
                     if ("cpe".equals(i.getType())) {
311 | 2 |
                         final String cpe = i.getValue();
312 | 2 |
                         if (cpe != null && (cpe.startsWith("cpe:/a:apache:axis:") || "cpe:/a:apache:axis".equals(cpe))) {
313 | 1 |
                             itr.remove();
314 |  |
                         }
315 |  |
                     }
316 | 2 |
                 }
317 | 14 |
             } else if (fileName != null && fileName.contains("axis")) {
318 | 0 |
                 while (itr.hasNext()) {
319 | 0 |
                     final Identifier i = itr.next();
320 | 0 |
                     if ("cpe".equals(i.getType())) {
321 | 0 |
                         final String cpe = i.getValue();
322 | 0 |
                         if (cpe != null && (cpe.startsWith("cpe:/a:apache:axis2:") || "cpe:/a:apache:axis2".equals(cpe))) {
323 | 0 |
                             itr.remove();
324 |  |
                         }
325 |  |
                     }
326 | 0 |
                 }
327 |  |
             }
328 | 15 |
         }
329 |  |
330 |  |
         /**
331 |  |
          * There are some known CPE entries, specifically regarding sun and oracle
332 |  |
          * products due to the acquisition and changes in product names, that based
333 |  |
          * on given evidence we can add the related CPE entries to ensure a complete
334 |  |
          * list of CVE entries.
335 |  |
          *
336 |  |
          * @param dependency the dependency being analyzed
337 |  |
          */
338 |  |
         private void addFalseNegativeCPEs(Dependency dependency) {
339 | 15 |
             final Iterator<Identifier> itr = dependency.getIdentifiers().iterator();
340 | 33 |
             while (itr.hasNext()) {
341 | 18 |
                 final Identifier i = itr.next();
342 | 18 |
                 if ("cpe".equals(i.getType()) && i.getValue() != null
343 |  |
                         && (i.getValue().startsWith("cpe:/a:oracle:opensso:")
344 |  |
                         || i.getValue().startsWith("cpe:/a:oracle:opensso_enterprise:")
345 |  |
                         || i.getValue().startsWith("cpe:/a:sun:opensso_enterprise:")
346 |  |
                         || i.getValue().startsWith("cpe:/a:sun:opensso:"))) {
347 | 0 |
                     final String newCpe = String.format("cpe:/a:sun:opensso_enterprise:%s", i.getValue().substring(22));
348 | 0 |
                     final String newCpe2 = String.format("cpe:/a:oracle:opensso_enterprise:%s", i.getValue().substring(22));
349 | 0 |
                     final String newCpe3 = String.format("cpe:/a:sun:opensso:%s", i.getValue().substring(22));
350 | 0 |
                     final String newCpe4 = String.format("cpe:/a:oracle:opensso:%s", i.getValue().substring(22));
351 |  |
                     try {
352 | 0 |
                         dependency.addIdentifier("cpe",
353 |  |
                                 newCpe,
354 |  |
                                 String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe, "UTF-8")));
355 | 0 |
                         dependency.addIdentifier("cpe",
356 |  |
                                 newCpe2,
357 |  |
                                 String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe2, "UTF-8")));
358 | 0 |
                         dependency.addIdentifier("cpe",
359 |  |
                                 newCpe3,
360 |  |
                                 String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe3, "UTF-8")));
361 | 0 |
                         dependency.addIdentifier("cpe",
362 |  |
                                 newCpe4,
363 |  |
                                 String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe4, "UTF-8")));
364 | 0 |
                     } catch (UnsupportedEncodingException ex) {
365 | 0 |
                         Logger.getLogger(FalsePositiveAnalyzer.class
366 |  |
                                 .getName()).log(Level.FINE, null, ex);
367 | 0 |
                     }
368 |  |
                 }
369 | 18 |
             }
370 | 15 |
         }
371 |  |
     }