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