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