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