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