View Javadoc
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.File;
21  import java.io.FileFilter;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InputStreamReader;
26  import java.io.OutputStream;
27  import java.io.Reader;
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.Enumeration;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Map.Entry;
35  import java.util.Properties;
36  import java.util.Set;
37  import java.util.StringTokenizer;
38  import java.util.jar.Attributes;
39  import java.util.jar.JarEntry;
40  import java.util.jar.JarFile;
41  import java.util.jar.Manifest;
42  import java.util.regex.Pattern;
43  import java.util.zip.ZipEntry;
44  import org.apache.commons.compress.utils.IOUtils;
45  import org.apache.commons.io.FilenameUtils;
46  import org.jsoup.Jsoup;
47  import org.owasp.dependencycheck.Engine;
48  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
49  import org.owasp.dependencycheck.dependency.Confidence;
50  import org.owasp.dependencycheck.dependency.Dependency;
51  import org.owasp.dependencycheck.dependency.EvidenceCollection;
52  import org.owasp.dependencycheck.exception.InitializationException;
53  import org.owasp.dependencycheck.utils.FileFilterBuilder;
54  import org.owasp.dependencycheck.xml.pom.License;
55  import org.owasp.dependencycheck.xml.pom.PomUtils;
56  import org.owasp.dependencycheck.xml.pom.Model;
57  import org.owasp.dependencycheck.utils.FileUtils;
58  import org.owasp.dependencycheck.utils.Settings;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  /**
63   * Used to load a JAR file and collect information that can be used to determine
64   * the associated CPE.
65   *
66   * @author Jeremy Long
67   */
68  public class JarAnalyzer extends AbstractFileTypeAnalyzer {
69  
70      //<editor-fold defaultstate="collapsed" desc="Constants and Member Variables">
71      /**
72       * The logger.
73       */
74      private static final Logger LOGGER = LoggerFactory.getLogger(JarAnalyzer.class);
75      /**
76       * The count of directories created during analysis. This is used for
77       * creating temporary directories.
78       */
79      private static int dirCount = 0;
80      /**
81       * The system independent newline character.
82       */
83      private static final String NEWLINE = System.getProperty("line.separator");
84      /**
85       * A list of values in the manifest to ignore as they only result in false
86       * positives.
87       */
88      private static final Set<String> IGNORE_VALUES = newHashSet(
89              "Sun Java System Application Server");
90      /**
91       * A list of elements in the manifest to ignore.
92       */
93      private static final Set<String> IGNORE_KEYS = newHashSet(
94              "built-by",
95              "created-by",
96              "builtby",
97              "createdby",
98              "build-jdk",
99              "buildjdk",
100             "ant-version",
101             "antversion",
102             "dynamicimportpackage",
103             "dynamicimport-package",
104             "dynamic-importpackage",
105             "dynamic-import-package",
106             "import-package",
107             "ignore-package",
108             "export-package",
109             "importpackage",
110             "ignorepackage",
111             "exportpackage",
112             "sealed",
113             "manifest-version",
114             "archiver-version",
115             "manifestversion",
116             "archiverversion",
117             "classpath",
118             "class-path",
119             "tool",
120             "bundle-manifestversion",
121             "bundlemanifestversion",
122             "bundle-vendor",
123             "include-resource",
124             "embed-dependency",
125             "ipojo-components",
126             "ipojo-extension",
127             "eclipse-sourcereferences");
128     /**
129      * Deprecated Jar manifest attribute, that is, nonetheless, useful for
130      * analysis.
131      */
132     @SuppressWarnings("deprecation")
133     private static final String IMPLEMENTATION_VENDOR_ID = Attributes.Name.IMPLEMENTATION_VENDOR_ID
134             .toString();
135     /**
136      * item in some manifest, should be considered medium confidence.
137      */
138     private static final String BUNDLE_VERSION = "Bundle-Version"; //: 2.1.2
139     /**
140      * item in some manifest, should be considered medium confidence.
141      */
142     private static final String BUNDLE_DESCRIPTION = "Bundle-Description"; //: Apache Struts 2
143     /**
144      * item in some manifest, should be considered medium confidence.
145      */
146     private static final String BUNDLE_NAME = "Bundle-Name"; //: Struts 2 Core
147     /**
148      * A pattern to detect HTML within text.
149      */
150     private static final Pattern HTML_DETECTION_PATTERN = Pattern.compile("\\<[a-z]+.*/?\\>", Pattern.CASE_INSENSITIVE);
151 
152     //</editor-fold>
153     /**
154      * Constructs a new JarAnalyzer.
155      */
156     public JarAnalyzer() {
157     }
158 
159     //<editor-fold defaultstate="collapsed" desc="All standard implmentation details of Analyzer">
160     /**
161      * The name of the analyzer.
162      */
163     private static final String ANALYZER_NAME = "Jar Analyzer";
164     /**
165      * The phase that this analyzer is intended to run in.
166      */
167     private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
168     /**
169      * The set of file extensions supported by this analyzer.
170      */
171     private static final String[] EXTENSIONS = {"jar", "war"};
172 
173     /**
174      * The file filter used to determine which files this analyzer supports.
175      */
176     private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build();
177 
178     /**
179      * Returns the FileFilter.
180      *
181      * @return the FileFilter
182      */
183     @Override
184     protected FileFilter getFileFilter() {
185         return FILTER;
186     }
187 
188     /**
189      * Returns the name of the analyzer.
190      *
191      * @return the name of the analyzer.
192      */
193     @Override
194     public String getName() {
195         return ANALYZER_NAME;
196     }
197 
198     /**
199      * Returns the phase that the analyzer is intended to run in.
200      *
201      * @return the phase that the analyzer is intended to run in.
202      */
203     @Override
204     public AnalysisPhase getAnalysisPhase() {
205         return ANALYSIS_PHASE;
206     }
207     //</editor-fold>
208 
209     /**
210      * Returns the key used in the properties file to reference the analyzer's
211      * enabled property.
212      *
213      * @return the analyzer's enabled property setting key
214      */
215     @Override
216     protected String getAnalyzerEnabledSettingKey() {
217         return Settings.KEYS.ANALYZER_JAR_ENABLED;
218     }
219 
220     /**
221      * Loads a specified JAR file and collects information from the manifest and
222      * checksums to identify the correct CPE information.
223      *
224      * @param dependency the dependency to analyze.
225      * @param engine the engine that is scanning the dependencies
226      * @throws AnalysisException is thrown if there is an error reading the JAR
227      * file.
228      */
229     @Override
230     public void analyzeFileType(Dependency dependency, Engine engine) throws AnalysisException {
231         try {
232             final List<ClassNameInformation> classNames = collectClassNames(dependency);
233             final String fileName = dependency.getFileName().toLowerCase();
234             if (classNames.isEmpty()
235                     && (fileName.endsWith("-sources.jar")
236                     || fileName.endsWith("-javadoc.jar")
237                     || fileName.endsWith("-src.jar")
238                     || fileName.endsWith("-doc.jar"))) {
239                 engine.getDependencies().remove(dependency);
240             }
241             final boolean hasManifest = parseManifest(dependency, classNames);
242             final boolean hasPOM = analyzePOM(dependency, classNames, engine);
243             final boolean addPackagesAsEvidence = !(hasManifest && hasPOM);
244             analyzePackageNames(classNames, dependency, addPackagesAsEvidence);
245         } catch (IOException ex) {
246             throw new AnalysisException("Exception occurred reading the JAR file.", ex);
247         }
248     }
249 
250     /**
251      * Attempts to find a pom.xml within the JAR file. If found it extracts
252      * information and adds it to the evidence. This will attempt to interpolate
253      * the strings contained within the pom.properties if one exists.
254      *
255      * @param dependency the dependency being analyzed
256      * @param classes a collection of class name information
257      * @param engine the analysis engine, used to add additional dependencies
258      * @throws AnalysisException is thrown if there is an exception parsing the
259      * pom
260      * @return whether or not evidence was added to the dependency
261      */
262     protected boolean analyzePOM(Dependency dependency, List<ClassNameInformation> classes, Engine engine) throws AnalysisException {
263         boolean foundSomething = false;
264         final JarFile jar;
265         try {
266             jar = new JarFile(dependency.getActualFilePath());
267         } catch (IOException ex) {
268             LOGGER.warn("Unable to read JarFile '{}'.", dependency.getActualFilePath());
269             LOGGER.trace("", ex);
270             return false;
271         }
272         List<String> pomEntries;
273         try {
274             pomEntries = retrievePomListing(jar);
275         } catch (IOException ex) {
276             LOGGER.warn("Unable to read Jar file entries in '{}'.", dependency.getActualFilePath());
277             LOGGER.trace("", ex);
278             return false;
279         }
280         File externalPom = null;
281         if (pomEntries.isEmpty()) {
282             final String pomPath = FilenameUtils.removeExtension(dependency.getActualFilePath()) + ".pom";
283             externalPom = new File(pomPath);
284             if (externalPom.isFile()) {
285                 pomEntries.add(pomPath);
286             } else {
287                 return false;
288             }
289         }
290         for (String path : pomEntries) {
291             LOGGER.debug("Reading pom entry: {}", path);
292             Properties pomProperties = null;
293             try {
294                 if (externalPom == null) {
295                     pomProperties = retrievePomProperties(path, jar);
296                 }
297             } catch (IOException ex) {
298                 LOGGER.trace("ignore this, failed reading a non-existent pom.properties", ex);
299             }
300             Model pom = null;
301             try {
302                 if (pomEntries.size() > 1) {
303                     //extract POM to its own directory and add it as its own dependency
304                     final Dependency newDependency = new Dependency();
305                     pom = extractPom(path, jar, newDependency);
306 
307                     final String displayPath = String.format("%s%s%s",
308                             dependency.getFilePath(),
309                             File.separator,
310                             path);
311                     final String displayName = String.format("%s%s%s",
312                             dependency.getFileName(),
313                             File.separator,
314                             path);
315 
316                     newDependency.setFileName(displayName);
317                     newDependency.setFilePath(displayPath);
318                     pom.processProperties(pomProperties);
319                     setPomEvidence(newDependency, pom, null);
320                     engine.getDependencies().add(newDependency);
321                     Collections.sort(engine.getDependencies());
322                 } else {
323                     if (externalPom == null) {
324                         pom = PomUtils.readPom(path, jar);
325                     } else {
326                         pom = PomUtils.readPom(externalPom);
327                     }
328                     if (pom != null) {
329                         pom.processProperties(pomProperties);
330                         foundSomething |= setPomEvidence(dependency, pom, classes);
331                     }
332                 }
333             } catch (AnalysisException ex) {
334                 LOGGER.warn("An error occurred while analyzing '{}'.", dependency.getActualFilePath());
335                 LOGGER.trace("", ex);
336             }
337         }
338         return foundSomething;
339     }
340 
341     /**
342      * Given a path to a pom.xml within a JarFile, this method attempts to load
343      * a sibling pom.properties if one exists.
344      *
345      * @param path the path to the pom.xml within the JarFile
346      * @param jar the JarFile to load the pom.properties from
347      * @return a Properties object or null if no pom.properties was found
348      * @throws IOException thrown if there is an exception reading the
349      * pom.properties
350      */
351     private Properties retrievePomProperties(String path, final JarFile jar) throws IOException {
352         Properties pomProperties = null;
353         final String propPath = path.substring(0, path.length() - 7) + "pom.properies";
354         final ZipEntry propEntry = jar.getEntry(propPath);
355         if (propEntry != null) {
356             Reader reader = null;
357             try {
358                 reader = new InputStreamReader(jar.getInputStream(propEntry), "UTF-8");
359                 pomProperties = new Properties();
360                 pomProperties.load(reader);
361                 LOGGER.debug("Read pom.properties: {}", propPath);
362             } finally {
363                 if (reader != null) {
364                     try {
365                         reader.close();
366                     } catch (IOException ex) {
367                         LOGGER.trace("close error", ex);
368                     }
369                 }
370             }
371         }
372         return pomProperties;
373     }
374 
375     /**
376      * Searches a JarFile for pom.xml entries and returns a listing of these
377      * entries.
378      *
379      * @param jar the JarFile to search
380      * @return a list of pom.xml entries
381      * @throws IOException thrown if there is an exception reading a JarEntry
382      */
383     private List<String> retrievePomListing(final JarFile jar) throws IOException {
384         final List<String> pomEntries = new ArrayList<String>();
385         final Enumeration<JarEntry> entries = jar.entries();
386         while (entries.hasMoreElements()) {
387             final JarEntry entry = entries.nextElement();
388             final String entryName = (new File(entry.getName())).getName().toLowerCase();
389             if (!entry.isDirectory() && "pom.xml".equals(entryName)) {
390                 LOGGER.trace("POM Entry found: {}", entry.getName());
391                 pomEntries.add(entry.getName());
392             }
393         }
394         return pomEntries;
395     }
396 
397     /**
398      * Retrieves the specified POM from a jar file and converts it to a Model.
399      *
400      * @param path the path to the pom.xml file within the jar file
401      * @param jar the jar file to extract the pom from
402      * @param dependency the dependency being analyzed
403      * @return returns the POM object
404      * @throws AnalysisException is thrown if there is an exception extracting
405      * or parsing the POM {@link org.owasp.dependencycheck.xml.pom.Model} object
406      */
407     private Model extractPom(String path, JarFile jar, Dependency dependency) throws AnalysisException {
408         InputStream input = null;
409         FileOutputStream fos = null;
410         final File tmpDir = getNextTempDirectory();
411         final File file = new File(tmpDir, "pom.xml");
412         try {
413             final ZipEntry entry = jar.getEntry(path);
414             if (entry == null) {
415                 throw new AnalysisException(String.format("Pom (%s)does not exist in %s", path, jar.getName()));
416             }
417             input = jar.getInputStream(entry);
418             fos = new FileOutputStream(file);
419             IOUtils.copy(input, fos);
420             dependency.setActualFilePath(file.getAbsolutePath());
421         } catch (IOException ex) {
422             LOGGER.warn("An error occurred reading '{}' from '{}'.", path, dependency.getFilePath());
423             LOGGER.error("", ex);
424         } finally {
425             closeStream(fos);
426             closeStream(input);
427         }
428         return PomUtils.readPom(file);
429     }
430 
431     /**
432      * Silently closes an input stream ignoring errors.
433      *
434      * @param stream an input stream to close
435      */
436     private void closeStream(InputStream stream) {
437         if (stream != null) {
438             try {
439                 stream.close();
440             } catch (IOException ex) {
441                 LOGGER.trace("", ex);
442             }
443         }
444     }
445 
446     /**
447      * Silently closes an output stream ignoring errors.
448      *
449      * @param stream an output stream to close
450      */
451     private void closeStream(OutputStream stream) {
452         if (stream != null) {
453             try {
454                 stream.close();
455             } catch (IOException ex) {
456                 LOGGER.trace("", ex);
457             }
458         }
459     }
460 
461     /**
462      * Sets evidence from the pom on the supplied dependency.
463      *
464      * @param dependency the dependency to set data on
465      * @param pom the information from the pom
466      * @param classes a collection of ClassNameInformation - containing data
467      * about the fully qualified class names within the JAR file being analyzed
468      * @return true if there was evidence within the pom that we could use;
469      * otherwise false
470      */
471     public static boolean setPomEvidence(Dependency dependency, Model pom, List<ClassNameInformation> classes) {
472         boolean foundSomething = false;
473         boolean addAsIdentifier = true;
474         if (pom == null) {
475             return foundSomething;
476         }
477         String groupid = pom.getGroupId();
478         String parentGroupId = pom.getParentGroupId();
479         String artifactid = pom.getArtifactId();
480         String parentArtifactId = pom.getParentArtifactId();
481         String version = pom.getVersion();
482         String parentVersion = pom.getParentVersion();
483 
484         if ("org.sonatype.oss".equals(parentGroupId) && "oss-parent".equals(parentArtifactId)) {
485             parentGroupId = null;
486             parentArtifactId = null;
487             parentVersion = null;
488         }
489 
490         if ((groupid == null || groupid.isEmpty()) && parentGroupId != null && !parentGroupId.isEmpty()) {
491             groupid = parentGroupId;
492         }
493 
494         final String originalGroupID = groupid;
495         if (groupid != null && (groupid.startsWith("org.") || groupid.startsWith("com."))) {
496             groupid = groupid.substring(4);
497         }
498 
499         if ((artifactid == null || artifactid.isEmpty()) && parentArtifactId != null && !parentArtifactId.isEmpty()) {
500             artifactid = parentArtifactId;
501         }
502 
503         final String originalArtifactID = artifactid;
504         if (artifactid != null && (artifactid.startsWith("org.") || artifactid.startsWith("com."))) {
505             artifactid = artifactid.substring(4);
506         }
507 
508         if ((version == null || version.isEmpty()) && parentVersion != null && !parentVersion.isEmpty()) {
509             version = parentVersion;
510         }
511 
512         if (groupid != null && !groupid.isEmpty()) {
513             foundSomething = true;
514             dependency.getVendorEvidence().addEvidence("pom", "groupid", groupid, Confidence.HIGHEST);
515             dependency.getProductEvidence().addEvidence("pom", "groupid", groupid, Confidence.LOW);
516             addMatchingValues(classes, groupid, dependency.getVendorEvidence());
517             addMatchingValues(classes, groupid, dependency.getProductEvidence());
518             if (parentGroupId != null && !parentGroupId.isEmpty() && !parentGroupId.equals(groupid)) {
519                 dependency.getVendorEvidence().addEvidence("pom", "parent-groupid", parentGroupId, Confidence.MEDIUM);
520                 dependency.getProductEvidence().addEvidence("pom", "parent-groupid", parentGroupId, Confidence.LOW);
521                 addMatchingValues(classes, parentGroupId, dependency.getVendorEvidence());
522                 addMatchingValues(classes, parentGroupId, dependency.getProductEvidence());
523             }
524         } else {
525             addAsIdentifier = false;
526         }
527 
528         if (artifactid != null && !artifactid.isEmpty()) {
529             foundSomething = true;
530             dependency.getProductEvidence().addEvidence("pom", "artifactid", artifactid, Confidence.HIGHEST);
531             dependency.getVendorEvidence().addEvidence("pom", "artifactid", artifactid, Confidence.LOW);
532             addMatchingValues(classes, artifactid, dependency.getVendorEvidence());
533             addMatchingValues(classes, artifactid, dependency.getProductEvidence());
534             if (parentArtifactId != null && !parentArtifactId.isEmpty() && !parentArtifactId.equals(artifactid)) {
535                 dependency.getProductEvidence().addEvidence("pom", "parent-artifactid", parentArtifactId, Confidence.MEDIUM);
536                 dependency.getVendorEvidence().addEvidence("pom", "parent-artifactid", parentArtifactId, Confidence.LOW);
537                 addMatchingValues(classes, parentArtifactId, dependency.getVendorEvidence());
538                 addMatchingValues(classes, parentArtifactId, dependency.getProductEvidence());
539             }
540         } else {
541             addAsIdentifier = false;
542         }
543 
544         if (version != null && !version.isEmpty()) {
545             foundSomething = true;
546             dependency.getVersionEvidence().addEvidence("pom", "version", version, Confidence.HIGHEST);
547             if (parentVersion != null && !parentVersion.isEmpty() && !parentVersion.equals(version)) {
548                 dependency.getVersionEvidence().addEvidence("pom", "parent-version", version, Confidence.LOW);
549             }
550         } else {
551             addAsIdentifier = false;
552         }
553 
554         if (addAsIdentifier) {
555             dependency.addIdentifier("maven", String.format("%s:%s:%s", originalGroupID, originalArtifactID, version), null, Confidence.HIGH);
556         }
557 
558         // org name
559         final String org = pom.getOrganization();
560         if (org != null && !org.isEmpty()) {
561             dependency.getVendorEvidence().addEvidence("pom", "organization name", org, Confidence.HIGH);
562             dependency.getProductEvidence().addEvidence("pom", "organization name", org, Confidence.LOW);
563             addMatchingValues(classes, org, dependency.getVendorEvidence());
564             addMatchingValues(classes, org, dependency.getProductEvidence());
565         }
566         //pom name
567         final String pomName = pom.getName();
568         if (pomName
569                 != null && !pomName.isEmpty()) {
570             foundSomething = true;
571             dependency.getProductEvidence().addEvidence("pom", "name", pomName, Confidence.HIGH);
572             dependency.getVendorEvidence().addEvidence("pom", "name", pomName, Confidence.HIGH);
573             addMatchingValues(classes, pomName, dependency.getVendorEvidence());
574             addMatchingValues(classes, pomName, dependency.getProductEvidence());
575         }
576 
577         //Description
578         final String description = pom.getDescription();
579         if (description != null && !description.isEmpty() && !description.startsWith("POM was created by")) {
580             foundSomething = true;
581             final String trimmedDescription = addDescription(dependency, description, "pom", "description");
582             addMatchingValues(classes, trimmedDescription, dependency.getVendorEvidence());
583             addMatchingValues(classes, trimmedDescription, dependency.getProductEvidence());
584         }
585 
586         final String projectURL = pom.getProjectURL();
587         if (projectURL != null && !projectURL.trim().isEmpty()) {
588             dependency.getVendorEvidence().addEvidence("pom", "url", projectURL, Confidence.HIGHEST);
589         }
590 
591         extractLicense(pom, dependency);
592         return foundSomething;
593     }
594 
595     /**
596      * Analyzes the path information of the classes contained within the
597      * JarAnalyzer to try and determine possible vendor or product names. If any
598      * are found they are stored in the packageVendor and packageProduct
599      * hashSets.
600      *
601      * @param classNames a list of class names
602      * @param dependency a dependency to analyze
603      * @param addPackagesAsEvidence a flag indicating whether or not package
604      * names should be added as evidence.
605      */
606     protected void analyzePackageNames(List<ClassNameInformation> classNames,
607             Dependency dependency, boolean addPackagesAsEvidence) {
608         final Map<String, Integer> vendorIdentifiers = new HashMap<String, Integer>();
609         final Map<String, Integer> productIdentifiers = new HashMap<String, Integer>();
610         analyzeFullyQualifiedClassNames(classNames, vendorIdentifiers, productIdentifiers);
611 
612         final int classCount = classNames.size();
613         final EvidenceCollection vendor = dependency.getVendorEvidence();
614         final EvidenceCollection product = dependency.getProductEvidence();
615 
616         for (Map.Entry<String, Integer> entry : vendorIdentifiers.entrySet()) {
617             final float ratio = entry.getValue() / (float) classCount;
618             if (ratio > 0.5) {
619                 //TODO remove weighting
620                 vendor.addWeighting(entry.getKey());
621                 if (addPackagesAsEvidence && entry.getKey().length() > 1) {
622                     vendor.addEvidence("jar", "package name", entry.getKey(), Confidence.LOW);
623                 }
624             }
625         }
626         for (Map.Entry<String, Integer> entry : productIdentifiers.entrySet()) {
627             final float ratio = entry.getValue() / (float) classCount;
628             if (ratio > 0.5) {
629                 product.addWeighting(entry.getKey());
630                 if (addPackagesAsEvidence && entry.getKey().length() > 1) {
631                     product.addEvidence("jar", "package name", entry.getKey(), Confidence.LOW);
632                 }
633             }
634         }
635     }
636 
637     /**
638      * <p>
639      * Reads the manifest from the JAR file and collects the entries. Some
640      * vendorKey entries are:</p>
641      * <ul><li>Implementation Title</li>
642      * <li>Implementation Version</li> <li>Implementation Vendor</li>
643      * <li>Implementation VendorId</li> <li>Bundle Name</li> <li>Bundle
644      * Version</li> <li>Bundle Vendor</li> <li>Bundle Description</li> <li>Main
645      * Class</li> </ul>
646      * However, all but a handful of specific entries are read in.
647      *
648      * @param dependency A reference to the dependency
649      * @param classInformation a collection of class information
650      * @return whether evidence was identified parsing the manifest
651      * @throws IOException if there is an issue reading the JAR file
652      */
653     protected boolean parseManifest(Dependency dependency, List<ClassNameInformation> classInformation) throws IOException {
654         boolean foundSomething = false;
655         JarFile jar = null;
656         try {
657             jar = new JarFile(dependency.getActualFilePath());
658             final Manifest manifest = jar.getManifest();
659             if (manifest == null) {
660                 if (!dependency.getFileName().toLowerCase().endsWith("-sources.jar")
661                         && !dependency.getFileName().toLowerCase().endsWith("-javadoc.jar")
662                         && !dependency.getFileName().toLowerCase().endsWith("-src.jar")
663                         && !dependency.getFileName().toLowerCase().endsWith("-doc.jar")) {
664                     LOGGER.debug("Jar file '{}' does not contain a manifest.",
665                             dependency.getFileName());
666                 }
667                 return false;
668             }
669             final EvidenceCollection vendorEvidence = dependency.getVendorEvidence();
670             final EvidenceCollection productEvidence = dependency.getProductEvidence();
671             final EvidenceCollection versionEvidence = dependency.getVersionEvidence();
672             String source = "Manifest";
673             String specificationVersion = null;
674             boolean hasImplementationVersion = false;
675             Attributes atts = manifest.getMainAttributes();
676             for (Entry<Object, Object> entry : atts.entrySet()) {
677                 String key = entry.getKey().toString();
678                 String value = atts.getValue(key);
679                 if (HTML_DETECTION_PATTERN.matcher(value).find()) {
680                     value = Jsoup.parse(value).text();
681                 }
682                 if (IGNORE_VALUES.contains(value)) {
683                     continue;
684                 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_TITLE.toString())) {
685                     foundSomething = true;
686                     productEvidence.addEvidence(source, key, value, Confidence.HIGH);
687                     addMatchingValues(classInformation, value, productEvidence);
688                 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VERSION.toString())) {
689                     hasImplementationVersion = true;
690                     foundSomething = true;
691                     versionEvidence.addEvidence(source, key, value, Confidence.HIGH);
692                 } else if ("specification-version".equalsIgnoreCase(key)) {
693                     specificationVersion = value;
694                 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VENDOR.toString())) {
695                     foundSomething = true;
696                     vendorEvidence.addEvidence(source, key, value, Confidence.HIGH);
697                     addMatchingValues(classInformation, value, vendorEvidence);
698                 } else if (key.equalsIgnoreCase(IMPLEMENTATION_VENDOR_ID)) {
699                     foundSomething = true;
700                     vendorEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
701                     addMatchingValues(classInformation, value, vendorEvidence);
702                 } else if (key.equalsIgnoreCase(BUNDLE_DESCRIPTION)) {
703                     foundSomething = true;
704                     addDescription(dependency, value, "manifest", key);
705                     addMatchingValues(classInformation, value, productEvidence);
706                 } else if (key.equalsIgnoreCase(BUNDLE_NAME)) {
707                     foundSomething = true;
708                     productEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
709                     addMatchingValues(classInformation, value, productEvidence);
710 //                //the following caused false positives.
711 //                } else if (key.equalsIgnoreCase(BUNDLE_VENDOR)) {
712                 } else if (key.equalsIgnoreCase(BUNDLE_VERSION)) {
713                     foundSomething = true;
714                     versionEvidence.addEvidence(source, key, value, Confidence.HIGH);
715                 } else if (key.equalsIgnoreCase(Attributes.Name.MAIN_CLASS.toString())) {
716                     continue;
717                     //skipping main class as if this has important information to add it will be added during class name analysis...
718                 } else {
719                     key = key.toLowerCase();
720                     if (!IGNORE_KEYS.contains(key)
721                             && !key.endsWith("jdk")
722                             && !key.contains("lastmodified")
723                             && !key.endsWith("package")
724                             && !key.endsWith("classpath")
725                             && !key.endsWith("class-path")
726                             && !key.endsWith("-scm") //todo change this to a regex?
727                             && !key.startsWith("scm-")
728                             && !value.trim().startsWith("scm:")
729                             && !isImportPackage(key, value)
730                             && !isPackage(key, value)) {
731                         foundSomething = true;
732                         if (key.contains("version")) {
733                             if (!key.contains("specification")) {
734                                 versionEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
735                             }
736                         } else if ("build-id".equals(key)) {
737                             int pos = value.indexOf('(');
738                             if (pos >= 0) {
739                                 value = value.substring(0, pos - 1);
740                             }
741                             pos = value.indexOf('[');
742                             if (pos >= 0) {
743                                 value = value.substring(0, pos - 1);
744                             }
745                             versionEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
746                         } else if (key.contains("title")) {
747                             productEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
748                             addMatchingValues(classInformation, value, productEvidence);
749                         } else if (key.contains("vendor")) {
750                             if (key.contains("specification")) {
751                                 vendorEvidence.addEvidence(source, key, value, Confidence.LOW);
752                             } else {
753                                 vendorEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
754                                 addMatchingValues(classInformation, value, vendorEvidence);
755                             }
756                         } else if (key.contains("name")) {
757                             productEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
758                             vendorEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
759                             addMatchingValues(classInformation, value, vendorEvidence);
760                             addMatchingValues(classInformation, value, productEvidence);
761                         } else if (key.contains("license")) {
762                             addLicense(dependency, value);
763                         } else if (key.contains("description")) {
764                             addDescription(dependency, value, "manifest", key);
765                         } else {
766                             productEvidence.addEvidence(source, key, value, Confidence.LOW);
767                             vendorEvidence.addEvidence(source, key, value, Confidence.LOW);
768                             addMatchingValues(classInformation, value, vendorEvidence);
769                             addMatchingValues(classInformation, value, productEvidence);
770                             if (value.matches(".*\\d.*")) {
771                                 final StringTokenizer tokenizer = new StringTokenizer(value, " ");
772                                 while (tokenizer.hasMoreElements()) {
773                                     final String s = tokenizer.nextToken();
774                                     if (s.matches("^[0-9.]+$")) {
775                                         versionEvidence.addEvidence(source, key, s, Confidence.LOW);
776                                     }
777                                 }
778                             }
779                         }
780                     }
781                 }
782             }
783             for (Map.Entry<String, Attributes> item : manifest.getEntries().entrySet()) {
784                 final String name = item.getKey();
785                 source = "manifest: " + name;
786                 atts = item.getValue();
787                 for (Entry<Object, Object> entry : atts.entrySet()) {
788                     final String key = entry.getKey().toString();
789                     final String value = atts.getValue(key);
790                     if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_TITLE.toString())) {
791                         foundSomething = true;
792                         productEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
793                         addMatchingValues(classInformation, value, productEvidence);
794                     } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VERSION.toString())) {
795                         foundSomething = true;
796                         versionEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
797                     } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VENDOR.toString())) {
798                         foundSomething = true;
799                         vendorEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
800                         addMatchingValues(classInformation, value, vendorEvidence);
801                     } else if (key.equalsIgnoreCase(Attributes.Name.SPECIFICATION_TITLE.toString())) {
802                         foundSomething = true;
803                         productEvidence.addEvidence(source, key, value, Confidence.MEDIUM);
804                         addMatchingValues(classInformation, value, productEvidence);
805                     }
806                 }
807             }
808             if (specificationVersion != null && !hasImplementationVersion) {
809                 foundSomething = true;
810                 versionEvidence.addEvidence(source, "specification-version", specificationVersion, Confidence.HIGH);
811             }
812         } finally {
813             if (jar != null) {
814                 jar.close();
815             }
816         }
817         return foundSomething;
818     }
819 
820     /**
821      * Adds a description to the given dependency. If the description contains
822      * one of the following strings beyond 100 characters, then the description
823      * used will be trimmed to that position:
824      * <ul><li>"such as"</li><li>"like "</li><li>"will use "</li><li>"* uses
825      * "</li></ul>
826      *
827      * @param dependency a dependency
828      * @param description the description
829      * @param source the source of the evidence
830      * @param key the "name" of the evidence
831      * @return if the description is trimmed, the trimmed version is returned;
832      * otherwise the original description is returned
833      */
834     public static String addDescription(Dependency dependency, String description, String source, String key) {
835         if (dependency.getDescription() == null) {
836             dependency.setDescription(description);
837         }
838         String desc;
839         if (HTML_DETECTION_PATTERN.matcher(description).find()) {
840             desc = Jsoup.parse(description).text();
841         } else {
842             desc = description;
843         }
844         dependency.setDescription(desc);
845         if (desc.length() > 100) {
846             desc = desc.replaceAll("\\s\\s+", " ");
847             final int posSuchAs = desc.toLowerCase().indexOf("such as ", 100);
848             final int posLike = desc.toLowerCase().indexOf("like ", 100);
849             final int posWillUse = desc.toLowerCase().indexOf("will use ", 100);
850             final int posUses = desc.toLowerCase().indexOf(" uses ", 100);
851             int pos = -1;
852             pos = Math.max(pos, posSuchAs);
853             if (pos >= 0 && posLike >= 0) {
854                 pos = Math.min(pos, posLike);
855             } else {
856                 pos = Math.max(pos, posLike);
857             }
858             if (pos >= 0 && posWillUse >= 0) {
859                 pos = Math.min(pos, posWillUse);
860             } else {
861                 pos = Math.max(pos, posWillUse);
862             }
863             if (pos >= 0 && posUses >= 0) {
864                 pos = Math.min(pos, posUses);
865             } else {
866                 pos = Math.max(pos, posUses);
867             }
868 
869             if (pos > 0) {
870                 desc = desc.substring(0, pos) + "...";
871             }
872             dependency.getProductEvidence().addEvidence(source, key, desc, Confidence.LOW);
873             dependency.getVendorEvidence().addEvidence(source, key, desc, Confidence.LOW);
874         } else {
875             dependency.getProductEvidence().addEvidence(source, key, desc, Confidence.MEDIUM);
876             dependency.getVendorEvidence().addEvidence(source, key, desc, Confidence.MEDIUM);
877         }
878         return desc;
879     }
880 
881     /**
882      * Adds a license to the given dependency.
883      *
884      * @param d a dependency
885      * @param license the license
886      */
887     private void addLicense(Dependency d, String license) {
888         if (d.getLicense() == null) {
889             d.setLicense(license);
890         } else if (!d.getLicense().contains(license)) {
891             d.setLicense(d.getLicense() + NEWLINE + license);
892         }
893     }
894 
895     /**
896      * The parent directory for the individual directories per archive.
897      */
898     private File tempFileLocation = null;
899 
900     /**
901      * Initializes the JarAnalyzer.
902      *
903      * @throws InitializationException is thrown if there is an exception
904      * creating a temporary directory
905      */
906     @Override
907     public void initializeFileTypeAnalyzer() throws InitializationException {
908         try {
909             final File baseDir = Settings.getTempDirectory();
910             tempFileLocation = File.createTempFile("check", "tmp", baseDir);
911             if (!tempFileLocation.delete()) {
912                 final String msg = String.format("Unable to delete temporary file '%s'.", tempFileLocation.getAbsolutePath());
913                 setEnabled(false);
914                 throw new InitializationException(msg);
915             }
916             if (!tempFileLocation.mkdirs()) {
917                 final String msg = String.format("Unable to create directory '%s'.", tempFileLocation.getAbsolutePath());
918                 setEnabled(false);
919                 throw new InitializationException(msg);
920             }
921         } catch (IOException ex) {
922             setEnabled(false);
923             throw new InitializationException("Unable to create a temporary file", ex);
924         }
925     }
926 
927     /**
928      * Deletes any files extracted from the JAR during analysis.
929      */
930     @Override
931     public void close() {
932         if (tempFileLocation != null && tempFileLocation.exists()) {
933             LOGGER.debug("Attempting to delete temporary files");
934             final boolean success = FileUtils.delete(tempFileLocation);
935             if (!success) {
936                 LOGGER.warn("Failed to delete some temporary files, see the log for more details");
937             }
938         }
939     }
940 
941     /**
942      * Determines if the key value pair from the manifest is for an "import"
943      * type entry for package names.
944      *
945      * @param key the key from the manifest
946      * @param value the value from the manifest
947      * @return true or false depending on if it is believed the entry is an
948      * "import" entry
949      */
950     private boolean isImportPackage(String key, String value) {
951         final Pattern packageRx = Pattern.compile("^([a-zA-Z0-9_#\\$\\*\\.]+\\s*[,;]\\s*)+([a-zA-Z0-9_#\\$\\*\\.]+\\s*)?$");
952         final boolean matches = packageRx.matcher(value).matches();
953         return matches && (key.contains("import") || key.contains("include") || value.length() > 10);
954     }
955 
956     /**
957      * Cycles through an enumeration of JarEntries, contained within the
958      * dependency, and returns a list of the class names. This does not include
959      * core Java package names (i.e. java.* or javax.*).
960      *
961      * @param dependency the dependency being analyzed
962      * @return an list of fully qualified class names
963      */
964     private List<ClassNameInformation> collectClassNames(Dependency dependency) {
965         final List<ClassNameInformation> classNames = new ArrayList<ClassNameInformation>();
966         JarFile jar = null;
967         try {
968             jar = new JarFile(dependency.getActualFilePath());
969             final Enumeration<JarEntry> entries = jar.entries();
970             while (entries.hasMoreElements()) {
971                 final JarEntry entry = entries.nextElement();
972                 final String name = entry.getName().toLowerCase();
973                 //no longer stripping "|com\\.sun" - there are some com.sun jar files with CVEs.
974                 if (name.endsWith(".class") && !name.matches("^javax?\\..*$")) {
975                     final ClassNameInformation className = new ClassNameInformation(name.substring(0, name.length() - 6));
976                     classNames.add(className);
977                 }
978             }
979         } catch (IOException ex) {
980             LOGGER.warn("Unable to open jar file '{}'.", dependency.getFileName());
981             LOGGER.debug("", ex);
982         } finally {
983             if (jar != null) {
984                 try {
985                     jar.close();
986                 } catch (IOException ex) {
987                     LOGGER.trace("", ex);
988                 }
989             }
990         }
991         return classNames;
992     }
993 
994     /**
995      * Cycles through the list of class names and places the package levels 0-3
996      * into the provided maps for vendor and product. This is helpful when
997      * analyzing vendor/product as many times this is included in the package
998      * name.
999      *
1000      * @param classNames a list of class names
1001      * @param vendor HashMap of possible vendor names from package names (e.g.
1002      * owasp)
1003      * @param product HashMap of possible product names from package names (e.g.
1004      * dependencycheck)
1005      */
1006     private void analyzeFullyQualifiedClassNames(List<ClassNameInformation> classNames,
1007             Map<String, Integer> vendor, Map<String, Integer> product) {
1008         for (ClassNameInformation entry : classNames) {
1009             final List<String> list = entry.getPackageStructure();
1010             addEntry(vendor, list.get(0));
1011 
1012             if (list.size() == 2) {
1013                 addEntry(product, list.get(1));
1014             }
1015             if (list.size() == 3) {
1016                 addEntry(vendor, list.get(1));
1017                 addEntry(product, list.get(1));
1018                 addEntry(product, list.get(2));
1019             }
1020             if (list.size() >= 4) {
1021                 addEntry(vendor, list.get(1));
1022                 addEntry(vendor, list.get(2));
1023                 addEntry(product, list.get(1));
1024                 addEntry(product, list.get(2));
1025                 addEntry(product, list.get(3));
1026             }
1027         }
1028     }
1029 
1030     /**
1031      * Adds an entry to the specified collection and sets the Integer (e.g. the
1032      * count) to 1. If the entry already exists in the collection then the
1033      * Integer is incremented by 1.
1034      *
1035      * @param collection a collection of strings and their occurrence count
1036      * @param key the key to add to the collection
1037      */
1038     private void addEntry(Map<String, Integer> collection, String key) {
1039         if (collection.containsKey(key)) {
1040             collection.put(key, collection.get(key) + 1);
1041         } else {
1042             collection.put(key, 1);
1043         }
1044     }
1045 
1046     /**
1047      * Cycles through the collection of class name information to see if parts
1048      * of the package names are contained in the provided value. If found, it
1049      * will be added as the HIGHEST confidence evidence because we have more
1050      * then one source corroborating the value.
1051      *
1052      * @param classes a collection of class name information
1053      * @param value the value to check to see if it contains a package name
1054      * @param evidence the evidence collection to add new entries too
1055      */
1056     private static void addMatchingValues(List<ClassNameInformation> classes, String value, EvidenceCollection evidence) {
1057         if (value == null || value.isEmpty() || classes == null || classes.isEmpty()) {
1058             return;
1059         }
1060         final String text = value.toLowerCase();
1061         for (ClassNameInformation cni : classes) {
1062             for (String key : cni.getPackageStructure()) {
1063                 final Pattern p = Pattern.compile("\b" + key + "\b");
1064                 if (p.matcher(text).find()) {
1065                     //if (text.contains(key)) { //note, package structure elements are already lowercase.
1066                     evidence.addEvidence("jar", "package name", key, Confidence.HIGHEST);
1067                 }
1068             }
1069         }
1070     }
1071 
1072     /**
1073      * Simple check to see if the attribute from a manifest is just a package
1074      * name.
1075      *
1076      * @param key the key of the value to check
1077      * @param value the value to check
1078      * @return true if the value looks like a java package name, otherwise false
1079      */
1080     private boolean isPackage(String key, String value) {
1081 
1082         return !key.matches(".*(version|title|vendor|name|license|description).*")
1083                 && value.matches("^([a-zA-Z_][a-zA-Z0-9_\\$]*(\\.[a-zA-Z_][a-zA-Z0-9_\\$]*)*)?$");
1084 
1085     }
1086 
1087     /**
1088      * Extracts the license information from the pom and adds it to the
1089      * dependency.
1090      *
1091      * @param pom the pom object
1092      * @param dependency the dependency to add license information too
1093      */
1094     public static void extractLicense(Model pom, Dependency dependency) {
1095         //license
1096         if (pom.getLicenses() != null) {
1097             String license = null;
1098             for (License lic : pom.getLicenses()) {
1099                 String tmp = null;
1100                 if (lic.getName() != null) {
1101                     tmp = lic.getName();
1102                 }
1103                 if (lic.getUrl() != null) {
1104                     if (tmp == null) {
1105                         tmp = lic.getUrl();
1106                     } else {
1107                         tmp += ": " + lic.getUrl();
1108                     }
1109                 }
1110                 if (tmp == null) {
1111                     continue;
1112                 }
1113                 if (HTML_DETECTION_PATTERN.matcher(tmp).find()) {
1114                     tmp = Jsoup.parse(tmp).text();
1115                 }
1116                 if (license == null) {
1117                     license = tmp;
1118                 } else {
1119                     license += "\n" + tmp;
1120                 }
1121             }
1122             if (license != null) {
1123                 dependency.setLicense(license);
1124 
1125             }
1126         }
1127     }
1128 
1129     /**
1130      * Stores information about a class name.
1131      */
1132     protected static class ClassNameInformation {
1133 
1134         /**
1135          * <p>
1136          * Stores information about a given class name. This class will keep the
1137          * fully qualified class name and a list of the important parts of the
1138          * package structure. Up to the first four levels of the package
1139          * structure are stored, excluding a leading "org" or "com".
1140          * Example:</p>
1141          * <code>ClassNameInformation obj = new ClassNameInformation("org.owasp.dependencycheck.analyzer.JarAnalyzer");
1142          * System.out.println(obj.getName());
1143          * for (String p : obj.getPackageStructure())
1144          *     System.out.println(p);
1145          * </code>
1146          * <p>
1147          * Would result in:</p>
1148          * <code>org.owasp.dependencycheck.analyzer.JarAnalyzer
1149          * owasp
1150          * dependencycheck
1151          * analyzer
1152          * jaranalyzer</code>
1153          *
1154          * @param className a fully qualified class name
1155          */
1156         ClassNameInformation(String className) {
1157             name = className;
1158             if (name.contains("/")) {
1159                 final String[] tmp = className.toLowerCase().split("/");
1160                 int start = 0;
1161                 int end = 3;
1162                 if ("com".equals(tmp[0]) || "org".equals(tmp[0])) {
1163                     start = 1;
1164                     end = 4;
1165                 }
1166                 if (tmp.length <= end) {
1167                     end = tmp.length - 1;
1168                 }
1169                 for (int i = start; i <= end; i++) {
1170                     packageStructure.add(tmp[i]);
1171                 }
1172             } else {
1173                 packageStructure.add(name);
1174             }
1175         }
1176         /**
1177          * The fully qualified class name.
1178          */
1179         private String name;
1180 
1181         /**
1182          * Get the value of name
1183          *
1184          * @return the value of name
1185          */
1186         public String getName() {
1187             return name;
1188         }
1189 
1190         /**
1191          * Set the value of name
1192          *
1193          * @param name new value of name
1194          */
1195         public void setName(String name) {
1196             this.name = name;
1197         }
1198         /**
1199          * Up to the first four levels of the package structure, excluding a
1200          * leading "org" or "com".
1201          */
1202         private final ArrayList<String> packageStructure = new ArrayList<String>();
1203 
1204         /**
1205          * Get the value of packageStructure
1206          *
1207          * @return the value of packageStructure
1208          */
1209         public ArrayList<String> getPackageStructure() {
1210             return packageStructure;
1211         }
1212     }
1213 
1214     /**
1215      * Retrieves the next temporary directory to extract an archive too.
1216      *
1217      * @return a directory
1218      * @throws AnalysisException thrown if unable to create temporary directory
1219      */
1220     private File getNextTempDirectory() throws AnalysisException {
1221         dirCount += 1;
1222         final File directory = new File(tempFileLocation, String.valueOf(dirCount));
1223         //getting an exception for some directories not being able to be created; might be because the directory already exists?
1224         if (directory.exists()) {
1225             return getNextTempDirectory();
1226         }
1227         if (!directory.mkdirs()) {
1228             final String msg = String.format("Unable to create temp directory '%s'.", directory.getAbsolutePath());
1229             throw new AnalysisException(msg);
1230         }
1231         return directory;
1232     }
1233 }