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 com.github.packageurl.MalformedPackageURLException;
21  import com.github.packageurl.PackageURL;
22  import com.github.packageurl.PackageURLBuilder;
23  import com.google.common.base.Strings;
24  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
25  import java.io.File;
26  import java.io.FileFilter;
27  import java.io.FileInputStream;
28  import java.io.FileOutputStream;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.io.InputStreamReader;
32  import java.io.Reader;
33  import java.io.UnsupportedEncodingException;
34  import java.nio.charset.StandardCharsets;
35  import java.nio.file.Paths;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.Enumeration;
39  import java.util.HashMap;
40  import java.util.HashSet;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.Map.Entry;
44  import java.util.Properties;
45  import java.util.Set;
46  import java.util.StringTokenizer;
47  import java.util.concurrent.atomic.AtomicInteger;
48  import java.util.jar.Attributes;
49  import java.util.jar.JarEntry;
50  import java.util.jar.JarFile;
51  import java.util.jar.Manifest;
52  import java.util.regex.Pattern;
53  import java.util.zip.ZipEntry;
54  
55  import org.apache.commons.io.FilenameUtils;
56  import org.apache.commons.lang3.StringUtils;
57  import org.apache.commons.io.IOUtils;
58  import org.jsoup.Jsoup;
59  import org.owasp.dependencycheck.Engine;
60  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
61  import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
62  import org.owasp.dependencycheck.dependency.Confidence;
63  import org.owasp.dependencycheck.dependency.Dependency;
64  import org.owasp.dependencycheck.dependency.EvidenceType;
65  import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
66  import org.owasp.dependencycheck.dependency.naming.Identifier;
67  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
68  import org.owasp.dependencycheck.exception.InitializationException;
69  import org.owasp.dependencycheck.utils.FileFilterBuilder;
70  import org.owasp.dependencycheck.utils.FileUtils;
71  import org.owasp.dependencycheck.utils.Settings;
72  import org.owasp.dependencycheck.xml.pom.Developer;
73  import org.owasp.dependencycheck.xml.pom.License;
74  import org.owasp.dependencycheck.xml.pom.Model;
75  import org.owasp.dependencycheck.xml.pom.PomUtils;
76  import org.slf4j.Logger;
77  import org.slf4j.LoggerFactory;
78  
79  /**
80   * Used to load a JAR file and collect information that can be used to determine
81   * the associated CPE.
82   *
83   * @author Jeremy Long
84   */
85  public class JarAnalyzer extends AbstractFileTypeAnalyzer {
86  
87      //<editor-fold defaultstate="collapsed" desc="Constants and Member Variables">
88      /**
89       * A descriptor for the type of dependencies processed or added by this
90       * analyzer.
91       */
92      public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.JAVA;
93      /**
94       * The logger.
95       */
96      private static final Logger LOGGER = LoggerFactory.getLogger(JarAnalyzer.class);
97      /**
98       * The count of directories created during analysis. This is used for
99       * creating temporary directories.
100      */
101     private static final AtomicInteger DIR_COUNT = new AtomicInteger(0);
102     /**
103      * The system independent newline character.
104      */
105     private static final String NEWLINE = System.getProperty("line.separator");
106     /**
107      * A list of values in the manifest to ignore as they only result in false
108      * positives.
109      */
110     private static final Set<String> IGNORE_VALUES = newHashSet(
111             "Sun Java System Application Server");
112     /**
113      * A list of elements in the manifest to ignore.
114      */
115     private static final Set<String> IGNORE_KEYS = newHashSet(
116             "built-by",
117             "created-by",
118             "builtby",
119             "built-with",
120             "builtwith",
121             "createdby",
122             "build-jdk",
123             "buildjdk",
124             "ant-version",
125             "antversion",
126             "dynamicimportpackage",
127             "dynamicimport-package",
128             "dynamic-importpackage",
129             "dynamic-import-package",
130             "import-package",
131             "ignore-package",
132             "export-package",
133             "importpackage",
134             "import-template",
135             "importtemplate",
136             "java-vendor",
137             "export-template",
138             "exporttemplate",
139             "ignorepackage",
140             "exportpackage",
141             "sealed",
142             "manifest-version",
143             "archiver-version",
144             "manifestversion",
145             "archiverversion",
146             "classpath",
147             "class-path",
148             "tool",
149             "bundle-manifestversion",
150             "bundlemanifestversion",
151             "bundle-vendor",
152             "include-resource",
153             "embed-dependency",
154             "embedded-artifacts",
155             "ipojo-components",
156             "ipojo-extension",
157             "plugin-dependencies",
158             "today",
159             "tstamp",
160             "dstamp",
161             "eclipse-sourcereferences",
162             "kotlin-version",
163             "require-capability",
164             "require-bundle");
165     /**
166      * Deprecated Jar manifest attribute, that is, nonetheless, useful for
167      * analysis.
168      */
169     @SuppressWarnings("deprecation")
170     private static final String IMPLEMENTATION_VENDOR_ID = Attributes.Name.IMPLEMENTATION_VENDOR_ID
171             .toString();
172     /**
173      * item in some manifest, should be considered medium confidence.
174      */
175     private static final String BUNDLE_VERSION = "Bundle-Version"; //: 2.1.2
176     /**
177      * item in some manifest, should be considered medium confidence.
178      */
179     private static final String BUNDLE_DESCRIPTION = "Bundle-Description"; //: Apache Struts 2
180     /**
181      * item in some manifest, should be considered medium confidence.
182      */
183     private static final String BUNDLE_NAME = "Bundle-Name"; //: Struts 2 Core
184     /**
185      * A pattern to detect HTML within text.
186      */
187     private static final Pattern HTML_DETECTION_PATTERN = Pattern.compile("\\<[a-z]+.*/?\\>", Pattern.CASE_INSENSITIVE);
188     /**
189      * The name of the analyzer.
190      */
191     private static final String ANALYZER_NAME = "Jar Analyzer";
192     /**
193      * The phase that this analyzer is intended to run in.
194      */
195     private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
196     /**
197      * The set of jar files to exclude from analysis.
198      */
199     private static final List<String> EXCLUDE_JARS = Arrays.asList("-doc.jar", "-src.jar", "-javadoc.jar", "-sources.jar");
200     /**
201      * The set of file extensions supported by this analyzer.
202      */
203     private static final String[] EXTENSIONS = {"jar", "war", "aar"};
204     /**
205      * The file filter used to determine which files this analyzer supports.
206      */
207     private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build();
208 
209     /**
210      * The expected first bytes when reading a zip file.
211      */
212     private static final byte[] ZIP_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x03, 0x04};
213 
214     /**
215      * The expected first bytes when reading an empty zip file.
216      */
217     private static final byte[] ZIP_EMPTY_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x05, 0x06};
218 
219     /**
220      * The expected first bytes when reading a spanned zip file.
221      */
222     private static final byte[] ZIP_SPANNED_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x07, 0x08};
223 
224     //</editor-fold>
225     /**
226      * The parent directory for the individual directories per archive.
227      */
228     private File tempFileLocation = null;
229     /**
230      * Maven group id and artifact ids must match the regex to be considered
231      * valid. In some cases ODC cannot interpolate a variable and it produced
232      * invalid names.
233      */
234     private static final String VALID_NAME = "^[A-Za-z0-9_\\-.]+$";
235 
236     //<editor-fold defaultstate="collapsed" desc="All standard implmentation details of Analyzer">
237     /**
238      * Returns the FileFilter.
239      *
240      * @return the FileFilter
241      */
242     @Override
243     protected FileFilter getFileFilter() {
244         return FILTER;
245     }
246 
247     /**
248      * Returns the name of the analyzer.
249      *
250      * @return the name of the analyzer.
251      */
252     @Override
253     public String getName() {
254         return ANALYZER_NAME;
255     }
256 
257     /**
258      * Returns the phase that the analyzer is intended to run in.
259      *
260      * @return the phase that the analyzer is intended to run in.
261      */
262     @Override
263     public AnalysisPhase getAnalysisPhase() {
264         return ANALYSIS_PHASE;
265     }
266 
267     @Override
268     public boolean accept(File pathname) {
269         final boolean accepted = super.accept(pathname);
270         return accepted && !isExcludedJar(pathname);
271     }
272 
273     /**
274      * Returns true if the JAR is a `*-sources.jar` or `*-javadoc.jar`;
275      * otherwise false.
276      *
277      * @param path the path to the dependency
278      * @return true if the JAR is a `*-sources.jar` or `*-javadoc.jar`;
279      * otherwise false.
280      */
281     private boolean isExcludedJar(File path) {
282         final String fileName = path.getName().toLowerCase();
283         return EXCLUDE_JARS.stream().anyMatch(fileName::endsWith);
284     }
285     //</editor-fold>
286 
287     /**
288      * Returns the key used in the properties file to reference the analyzer's
289      * enabled property.
290      *
291      * @return the analyzer's enabled property setting key
292      */
293     @Override
294     protected String getAnalyzerEnabledSettingKey() {
295         return Settings.KEYS.ANALYZER_JAR_ENABLED;
296     }
297 
298     /**
299      * Loads a specified JAR file and collects information from the manifest and
300      * checksums to identify the correct CPE information.
301      *
302      * @param dependency the dependency to analyze.
303      * @param engine the engine that is scanning the dependencies
304      * @throws AnalysisException is thrown if there is an error reading the JAR
305      * file.
306      */
307     @Override
308     public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
309         final List<ClassNameInformation> classNames = collectClassNames(dependency);
310         final String fileName = dependency.getFileName().toLowerCase();
311         if ((classNames.isEmpty()
312                 && (fileName.endsWith("-sources.jar")
313                 || fileName.endsWith("-javadoc.jar")
314                 || fileName.endsWith("-src.jar")
315                 || fileName.endsWith("-doc.jar")
316                 || isMacOSMetaDataFile(dependency, engine)))
317                 || !isZipFile(dependency)) {
318             engine.removeDependency(dependency);
319             return;
320         }
321         Exception exception = null;
322         boolean hasManifest = false;
323         try {
324             hasManifest = parseManifest(dependency, classNames);
325         } catch (IOException ex) {
326             LOGGER.debug("Invalid Manifest", ex);
327             exception = ex;
328         }
329         boolean hasPOM = false;
330         try {
331             hasPOM = analyzePOM(dependency, classNames, engine);
332         } catch (AnalysisException ex) {
333             LOGGER.debug("Error parsing pom.xml", ex);
334             exception = ex;
335         }
336         final boolean addPackagesAsEvidence = !(hasManifest && hasPOM);
337         analyzePackageNames(classNames, dependency, addPackagesAsEvidence);
338         dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
339 
340         if (exception != null) {
341             throw new AnalysisException(String.format("An error occurred extracting evidence from "
342                     + "%s, analysis may be incomplete; please see the log for more details.",
343                     dependency.getDisplayFileName()), exception);
344         }
345     }
346 
347     /**
348      * Checks if the given dependency appears to be a macOS meta-data file,
349      * returning true if its filename starts with a ._ prefix and if there is
350      * another dependency with the same filename minus the ._ prefix, otherwise
351      * it returns false.
352      *
353      * @param dependency the dependency to check if it's a macOS meta-data file
354      * @param engine the engine that is scanning the dependencies
355      * @return whether or not the given dependency appears to be a macOS
356      * meta-data file
357      */
358     @SuppressFBWarnings(justification = "If actual file path is not null the path will have elements and getFileName will not be called on a null",
359             value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"})
360     private boolean isMacOSMetaDataFile(final Dependency dependency, final Engine engine) {
361         if (dependency.getActualFilePath() != null) {
362             final String fileName = Paths.get(dependency.getActualFilePath()).getFileName().toString();
363             return fileName.startsWith("._") && hasDependencyWithFilename(engine.getDependencies(), fileName.substring(2));
364         }
365         return false;
366     }
367 
368     /**
369      * Iterates through the given list of dependencies and returns true when it
370      * finds a dependency with a filename matching the given filename, otherwise
371      * returns false.
372      *
373      * @param dependencies the dependencies to search within
374      * @param fileName the filename to search for
375      * @return whether or not the given dependencies contain a dependency with
376      * the given filename
377      */
378     @SuppressFBWarnings(justification = "If actual file path is not null the path will have elements and getFileName will not be called on a null",
379             value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"})
380     private boolean hasDependencyWithFilename(final Dependency[] dependencies, final String fileName) {
381         for (final Dependency dependency : dependencies) {
382             if (dependency.getActualFilePath() != null
383                     && Paths.get(dependency.getActualFilePath()).getFileName().toString().equalsIgnoreCase(fileName)) {
384                 return true;
385             }
386         }
387         return false;
388     }
389 
390     /**
391      * Attempts to read the first bytes of the given dependency (using its
392      * actual file path) and returns true if they match the expected first bytes
393      * of a zip file, which may be empty or spanned. If they don't match, or if
394      * the file could not be read, then it returns false.
395      *
396      * @param dependency the dependency to check if it's a zip file
397      * @return whether or not the given dependency appears to be a zip file from
398      * its first bytes
399      */
400     @SuppressFBWarnings(justification = "try with resources will clean up the output stream", value = {"OBL_UNSATISFIED_OBLIGATION"})
401     private boolean isZipFile(final Dependency dependency) {
402         final byte[] buffer = new byte[4];
403         try (FileInputStream fileInputStream = new FileInputStream(dependency.getActualFilePath())) {
404             if (fileInputStream.read(buffer) > 0
405                     && (Arrays.equals(buffer, ZIP_FIRST_BYTES)
406                     || Arrays.equals(buffer, ZIP_EMPTY_FIRST_BYTES)
407                     || Arrays.equals(buffer, ZIP_SPANNED_FIRST_BYTES))) {
408                 return true;
409             }
410         } catch (Exception e) {
411             LOGGER.warn("Unable to check if '{}' is a zip file", dependency.getActualFilePath());
412             LOGGER.trace("", e);
413         }
414         return false;
415     }
416 
417     /**
418      * Attempts to find a pom.xml within the JAR file. If found it extracts
419      * information and adds it to the evidence. This will attempt to interpolate
420      * the strings contained within the pom.properties if one exists.
421      *
422      * @param dependency the dependency being analyzed
423      * @param classes a collection of class name information
424      * @param engine the analysis engine, used to add additional dependencies
425      * @throws AnalysisException is thrown if there is an exception parsing the
426      * pom
427      * @return whether or not evidence was added to the dependency
428      */
429     protected boolean analyzePOM(Dependency dependency, List<ClassNameInformation> classes, Engine engine) throws AnalysisException {
430 
431         //TODO add breakpoint on groov-all to find out why commons-cli is not added as a new dependency?
432         boolean evidenceAdded = false;
433         try (JarFile jar = new JarFile(dependency.getActualFilePath(), false)) {
434             //check if we are scanning in a repo directory - so the pom is adjacent to the jar
435             final String repoPomName = FilenameUtils.removeExtension(dependency.getActualFilePath()) + ".pom";
436             final File repoPom = new File(repoPomName);
437             if (repoPom.isFile()) {
438                 final Model pom = PomUtils.readPom(repoPom);
439                 evidenceAdded |= setPomEvidence(dependency, pom, classes, true);
440             }
441 
442             final List<String> pomEntries = retrievePomListing(jar);
443 
444             for (String path : pomEntries) {
445                 LOGGER.debug("Reading pom entry: {}", path);
446                 try {
447                     //extract POM to its own directory and add it as its own dependency
448                     final Properties pomProperties = retrievePomProperties(path, jar);
449                     final File pomFile = extractPom(path, jar);
450                     final Model pom = PomUtils.readPom(pomFile);
451                     pom.setGAVFromPomDotProperties(pomProperties);
452                     pom.processProperties(pomProperties);
453 
454                     final String artifactId = new File(path).getParentFile().getName();
455                     if (dependency.getActualFile().getName().startsWith(artifactId)) {
456                         evidenceAdded |= setPomEvidence(dependency, pom, classes, true);
457                     } else {
458                         final String displayPath = String.format("%s%s%s",
459                                 dependency.getFilePath(),
460                                 File.separator,
461                                 path);
462                         final String displayName = String.format("%s%s%s",
463                                 dependency.getFileName(),
464                                 File.separator,
465                                 path);
466                         final Dependency newDependency = new Dependency();
467                         newDependency.setActualFilePath(pomFile.getAbsolutePath());
468                         newDependency.setFileName(displayName);
469                         newDependency.setFilePath(displayPath);
470                         newDependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
471                         String groupId = pom.getGroupId();
472                         String version = pom.getVersion();
473                         if (groupId == null) {
474                             groupId = pom.getParentGroupId();
475                         }
476                         if (version == null) {
477                             version = pom.getParentVersion();
478                         }
479                         if (groupId == null) {
480                             newDependency.setName(pom.getArtifactId());
481                             newDependency.setPackagePath(String.format("%s:%s", pom.getArtifactId(), version));
482                         } else {
483                             newDependency.setName(String.format("%s:%s", groupId, pom.getArtifactId()));
484                             newDependency.setPackagePath(String.format("%s:%s:%s", groupId, pom.getArtifactId(), version));
485                         }
486                         newDependency.setDisplayFileName(String.format("%s (shaded: %s)",
487                                 dependency.getDisplayFileName(), newDependency.getPackagePath()));
488                         newDependency.setVersion(version);
489                         setPomEvidence(newDependency, pom, null, true);
490                         if (dependency.getProjectReferences().size() > 0) {
491                             newDependency.addAllProjectReferences(dependency.getProjectReferences());
492                         }
493                         engine.addDependency(newDependency);
494                     }
495                 } catch (AnalysisException ex) {
496                     LOGGER.warn("An error occurred while analyzing '{}'.", dependency.getActualFilePath());
497                     LOGGER.trace("", ex);
498                 }
499             }
500         } catch (IOException ex) {
501             LOGGER.warn("Unable to read JarFile '{}'.", dependency.getActualFilePath());
502             LOGGER.trace("", ex);
503         }
504         return evidenceAdded;
505     }
506 
507     /**
508      * Given a path to a pom.xml within a JarFile, this method attempts to load
509      * a sibling pom.properties if one exists.
510      *
511      * @param path the path to the pom.xml within the JarFile
512      * @param jar the JarFile to load the pom.properties from
513      * @return a Properties object or null if no pom.properties was found
514      */
515     private Properties retrievePomProperties(String path, final JarFile jar) {
516         final Properties pomProperties = new Properties();
517         final String propPath = path.substring(0, path.length() - 7) + "pom.properties";
518         final ZipEntry propEntry = jar.getEntry(propPath);
519         if (propEntry != null) {
520             try (Reader reader = new InputStreamReader(jar.getInputStream(propEntry), StandardCharsets.UTF_8)) {
521                 pomProperties.load(reader);
522                 LOGGER.debug("Read pom.properties: {}", propPath);
523             } catch (UnsupportedEncodingException ex) {
524                 LOGGER.trace("UTF-8 is not supported", ex);
525             } catch (IOException ex) {
526                 LOGGER.trace("Unable to read the POM properties", ex);
527             }
528         }
529         return pomProperties;
530     }
531 
532     /**
533      * Searches a JarFile for pom.xml entries and returns a listing of these
534      * entries.
535      *
536      * @param jar the JarFile to search
537      * @return a list of pom.xml entries
538      * @throws IOException thrown if there is an exception reading a JarEntry
539      */
540     private List<String> retrievePomListing(final JarFile jar) throws IOException {
541         final List<String> pomEntries = new ArrayList<>();
542         final Enumeration<JarEntry> entries = jar.entries();
543         while (entries.hasMoreElements()) {
544             final JarEntry entry = entries.nextElement();
545             final String entryName = new File(entry.getName()).getName().toLowerCase();
546             if (!entry.isDirectory() && "pom.xml".equals(entryName)
547                     && entry.getName().toUpperCase().startsWith("META-INF")) {
548                 pomEntries.add(entry.getName());
549             }
550         }
551         return pomEntries;
552     }
553 
554     /**
555      * Retrieves the specified POM from a jar.
556      *
557      * @param path the path to the pom.xml file within the jar file
558      * @param jar the jar file to extract the pom from
559      * @return returns the POM file
560      * @throws AnalysisException is thrown if there is an exception extracting
561      * the file
562      */
563     private File extractPom(String path, JarFile jar) throws AnalysisException {
564         final File tmpDir = getNextTempDirectory();
565         final File file = new File(tmpDir, "pom.xml");
566         final ZipEntry entry = jar.getEntry(path);
567         if (entry == null) {
568             throw new AnalysisException(String.format("Pom (%s) does not exist in %s", path, jar.getName()));
569         }
570         try (InputStream input = jar.getInputStream(entry);
571                 FileOutputStream fos = new FileOutputStream(file)) {
572             IOUtils.copy(input, fos);
573         } catch (IOException ex) {
574             LOGGER.warn("An error occurred reading '{}' from '{}'.", path, jar.getName());
575             LOGGER.error("", ex);
576         }
577         return file;
578     }
579 
580     /**
581      * Sets evidence from the pom on the supplied dependency.
582      *
583      * @param dependency the dependency to set data on
584      * @param pom the information from the pom
585      * @param classes a collection of ClassNameInformation - containing data
586      * about the fully qualified class names within the JAR file being analyzed
587      * @param isMainPom a flag indicating if this is the primary pom.
588      * @return true if there was evidence within the pom that we could use;
589      * otherwise false
590      */
591     public static boolean setPomEvidence(Dependency dependency, Model pom,
592             List<ClassNameInformation> classes, boolean isMainPom) {
593         if (pom == null) {
594             return false;
595         }
596         boolean foundSomething = false;
597         boolean addAsIdentifier = true;
598         String groupid = intepolationFailCheck(pom.getGroupId());
599         String parentGroupId = intepolationFailCheck(pom.getParentGroupId());
600         String artifactid = intepolationFailCheck(pom.getArtifactId());
601         String parentArtifactId = intepolationFailCheck(pom.getParentArtifactId());
602         String version = intepolationFailCheck(pom.getVersion());
603         String parentVersion = intepolationFailCheck(pom.getParentVersion());
604 
605         if (("org.sonatype.oss".equals(parentGroupId) && "oss-parent".equals(parentArtifactId))
606                 || ("org.springframework.boot".equals(parentGroupId) && "spring-boot-starter-parent".equals(parentArtifactId))) {
607             parentGroupId = null;
608             parentArtifactId = null;
609             parentVersion = null;
610         }
611 
612         if ((groupid == null || groupid.isEmpty()) && parentGroupId != null && !parentGroupId.isEmpty()) {
613             groupid = parentGroupId;
614         }
615 
616         final String originalGroupID = groupid;
617 
618         if ((artifactid == null || artifactid.isEmpty()) && parentArtifactId != null && !parentArtifactId.isEmpty()) {
619             artifactid = parentArtifactId;
620         }
621 
622         final String originalArtifactID = artifactid;
623         if (artifactid != null && (artifactid.startsWith("org.") || artifactid.startsWith("com."))) {
624             artifactid = artifactid.substring(4);
625         }
626 
627         if ((version == null || version.isEmpty()) && parentVersion != null && !parentVersion.isEmpty()) {
628             version = parentVersion;
629         }
630 
631         if (isMainPom && dependency.getName() == null && originalArtifactID != null && !originalArtifactID.isEmpty()) {
632             if (originalGroupID != null && !originalGroupID.isEmpty()) {
633                 dependency.setName(String.format("%s:%s", originalGroupID, originalArtifactID));
634             } else {
635                 dependency.setName(originalArtifactID);
636             }
637         }
638         if (isMainPom && dependency.getVersion() == null && version != null && !version.isEmpty()) {
639             dependency.setVersion(version);
640         }
641 
642         if (groupid != null && !groupid.isEmpty()) {
643             foundSomething = true;
644             dependency.addEvidence(EvidenceType.VENDOR, "pom", "groupid", groupid, Confidence.HIGHEST);
645             //In several cases we are seeing the product name at the end of the group identifier.
646             // This may cause several FP on products that have a collection of dependencies (e.g. jetty).
647             //dependency.addEvidence(EvidenceType.PRODUCT, "pom", "groupid", groupid, Confidence.LOW);
648             dependency.addEvidence(EvidenceType.PRODUCT, "pom", "groupid", groupid, Confidence.HIGHEST);
649             addMatchingValues(classes, groupid, dependency, EvidenceType.VENDOR);
650             addMatchingValues(classes, groupid, dependency, EvidenceType.PRODUCT);
651             if (parentGroupId != null && !parentGroupId.isEmpty() && !parentGroupId.equals(groupid)) {
652                 dependency.addEvidence(EvidenceType.VENDOR, "pom", "parent-groupid", parentGroupId, Confidence.MEDIUM);
653                 //see note above for groupid
654                 //dependency.addEvidence(EvidenceType.PRODUCT, "pom", "parent-groupid", parentGroupId, Confidence.LOW);
655                 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "parent-groupid", parentGroupId, Confidence.MEDIUM);
656                 addMatchingValues(classes, parentGroupId, dependency, EvidenceType.VENDOR);
657                 addMatchingValues(classes, parentGroupId, dependency, EvidenceType.PRODUCT);
658             }
659         } else {
660             addAsIdentifier = false;
661         }
662 
663         if (artifactid != null && !artifactid.isEmpty()) {
664             foundSomething = true;
665             dependency.addEvidence(EvidenceType.PRODUCT, "pom", "artifactid", artifactid, Confidence.HIGHEST);
666             dependency.addEvidence(EvidenceType.VENDOR, "pom", "artifactid", artifactid, Confidence.LOW);
667             addMatchingValues(classes, artifactid, dependency, EvidenceType.VENDOR);
668             addMatchingValues(classes, artifactid, dependency, EvidenceType.PRODUCT);
669             if (parentArtifactId != null && !parentArtifactId.isEmpty() && !parentArtifactId.equals(artifactid)) {
670                 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "parent-artifactid", parentArtifactId, Confidence.MEDIUM);
671                 dependency.addEvidence(EvidenceType.VENDOR, "pom", "parent-artifactid", parentArtifactId, Confidence.LOW);
672                 addMatchingValues(classes, parentArtifactId, dependency, EvidenceType.VENDOR);
673                 addMatchingValues(classes, parentArtifactId, dependency, EvidenceType.PRODUCT);
674             }
675         } else {
676             addAsIdentifier = false;
677         }
678 
679         if (version != null && !version.isEmpty()) {
680             foundSomething = true;
681             dependency.addEvidence(EvidenceType.VERSION, "pom", "version", version, Confidence.HIGHEST);
682             if (parentVersion != null && !parentVersion.isEmpty() && !parentVersion.equals(version)) {
683                 dependency.addEvidence(EvidenceType.VERSION, "pom", "parent-version", version, Confidence.LOW);
684             }
685         } else {
686             addAsIdentifier = false;
687         }
688 
689         if (addAsIdentifier && isMainPom) {
690             Identifier id = null;
691             try {
692                 if (originalArtifactID != null && originalArtifactID.matches(VALID_NAME)
693                         && originalGroupID != null && originalGroupID.matches(VALID_NAME)) {
694                     final PackageURL purl = PackageURLBuilder.aPackageURL().withType("maven").withNamespace(originalGroupID)
695                             .withName(originalArtifactID).withVersion(version).build();
696                     id = new PurlIdentifier(purl, Confidence.HIGH);
697                 } else {
698                     LOGGER.debug("Invalid maven identifier identified: " + originalGroupID + ":" + originalArtifactID);
699                 }
700             } catch (MalformedPackageURLException ex) {
701                 final String gav = String.format("%s:%s:%s", originalGroupID, originalArtifactID, version);
702                 LOGGER.debug("Error building package url for " + gav + "; using generic identifier instead.", ex);
703                 id = new GenericIdentifier("maven:" + gav, Confidence.HIGH);
704             }
705             if (id != null) {
706                 dependency.addSoftwareIdentifier(id);
707             }
708         }
709 
710         // org name
711         final String org = pom.getOrganization();
712         if (org != null && !org.isEmpty()) {
713             dependency.addEvidence(EvidenceType.VENDOR, "pom", "organization name", org, Confidence.HIGH);
714             dependency.addEvidence(EvidenceType.PRODUCT, "pom", "organization name", org, Confidence.LOW);
715             addMatchingValues(classes, org, dependency, EvidenceType.VENDOR);
716             addMatchingValues(classes, org, dependency, EvidenceType.PRODUCT);
717         }
718         // org name
719         String orgUrl = pom.getOrganizationUrl();
720         if (orgUrl != null && !orgUrl.isEmpty()) {
721             if (orgUrl.startsWith("https://github.com/") || orgUrl.startsWith("https://gitlab.com/")) {
722                 orgUrl = orgUrl.substring(19);
723                 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "url", orgUrl, Confidence.HIGH);
724             } else {
725                 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "organization url", orgUrl, Confidence.LOW);
726             }
727             dependency.addEvidence(EvidenceType.VENDOR, "pom", "organization url", orgUrl, Confidence.MEDIUM);
728         }
729         //pom name
730         final String pomName = pom.getName();
731         if (pomName != null && !pomName.isEmpty() && !"${project.groupId}:${project.artifactId}".equals(pomName)) {
732             foundSomething = true;
733             dependency.addEvidence(EvidenceType.PRODUCT, "pom", "name", pomName, Confidence.HIGH);
734             dependency.addEvidence(EvidenceType.VENDOR, "pom", "name", pomName, Confidence.HIGH);
735             addMatchingValues(classes, pomName, dependency, EvidenceType.VENDOR);
736             addMatchingValues(classes, pomName, dependency, EvidenceType.PRODUCT);
737         }
738 
739         //Description
740         final String description = pom.getDescription();
741         if (description != null && !description.isEmpty()
742                 && !description.startsWith("POM was created by")
743                 && !description.startsWith("Sonatype helps open source projects")
744                 && !description.endsWith("project for Spring Boot")) {
745             foundSomething = true;
746             final String trimmedDescription = addDescription(dependency, description, "pom", "description");
747             addMatchingValues(classes, trimmedDescription, dependency, EvidenceType.VENDOR);
748             addMatchingValues(classes, trimmedDescription, dependency, EvidenceType.PRODUCT);
749         }
750 
751         String projectURL = pom.getProjectURL();
752         if (projectURL != null && !projectURL.trim().isEmpty()) {
753             if (projectURL.startsWith("https://github.com/") || projectURL.startsWith("https://gitlab.com/")) {
754                 projectURL = projectURL.substring(19);
755                 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "url", projectURL, Confidence.HIGH);
756             } else {
757                 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "url", projectURL, Confidence.MEDIUM);
758             }
759             dependency.addEvidence(EvidenceType.VENDOR, "pom", "url", projectURL, Confidence.HIGHEST);
760 
761         }
762 
763         if (pom.getDevelopers() != null && !pom.getDevelopers().isEmpty()) {
764             for (Developer dev : pom.getDevelopers()) {
765                 if (!Strings.isNullOrEmpty(dev.getId())) {
766                     dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer id", dev.getId(), Confidence.MEDIUM);
767                     dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer id", dev.getId(), Confidence.LOW);
768                 }
769                 if (!Strings.isNullOrEmpty(dev.getName())) {
770                     dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer name", dev.getName(), Confidence.MEDIUM);
771                     dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer name", dev.getName(), Confidence.LOW);
772                 }
773                 if (!Strings.isNullOrEmpty(dev.getEmail())) {
774                     dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer email", dev.getEmail(), Confidence.LOW);
775                     dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer email", dev.getEmail(), Confidence.LOW);
776                 }
777                 if (!Strings.isNullOrEmpty(dev.getOrganizationUrl())) {
778                     dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer org URL", dev.getOrganizationUrl(), Confidence.MEDIUM);
779                     dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer org URL", dev.getOrganizationUrl(), Confidence.LOW);
780                 }
781                 final String devOrg = dev.getOrganization();
782                 if (!Strings.isNullOrEmpty(devOrg)) {
783                     dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer org", devOrg, Confidence.MEDIUM);
784                     dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer org", devOrg, Confidence.LOW);
785                     addMatchingValues(classes, devOrg, dependency, EvidenceType.VENDOR);
786                     addMatchingValues(classes, devOrg, dependency, EvidenceType.PRODUCT);
787                 }
788             }
789         }
790 
791         extractLicense(pom, dependency);
792         return foundSomething;
793     }
794 
795     /**
796      * Analyzes the path information of the classes contained within the
797      * JarAnalyzer to try and determine possible vendor or product names. If any
798      * are found they are stored in the packageVendor and packageProduct
799      * hashSets.
800      *
801      * @param classNames a list of class names
802      * @param dependency a dependency to analyze
803      * @param addPackagesAsEvidence a flag indicating whether or not package
804      * names should be added as evidence.
805      */
806     protected void analyzePackageNames(List<ClassNameInformation> classNames,
807             Dependency dependency, boolean addPackagesAsEvidence) {
808         final Map<String, Integer> vendorIdentifiers = new HashMap<>();
809         final Map<String, Integer> productIdentifiers = new HashMap<>();
810         analyzeFullyQualifiedClassNames(classNames, vendorIdentifiers, productIdentifiers);
811 
812         final int classCount = classNames.size();
813 
814         vendorIdentifiers.forEach((key, value) -> {
815             final float ratio = value / (float) classCount;
816             if (ratio > 0.5) {
817                 //TODO remove weighting?
818                 dependency.addVendorWeighting(key);
819                 if (addPackagesAsEvidence && key.length() > 1) {
820                     dependency.addEvidence(EvidenceType.VENDOR, "jar", "package name", key, Confidence.LOW);
821                 }
822             }
823         });
824         productIdentifiers.forEach((key, value) -> {
825             final float ratio = value / (float) classCount;
826             if (ratio > 0.5) {
827                 //todo remove weighting
828                 dependency.addProductWeighting(key);
829                 if (addPackagesAsEvidence && key.length() > 1) {
830                     dependency.addEvidence(EvidenceType.PRODUCT, "jar", "package name", key, Confidence.LOW);
831                 }
832             }
833         });
834     }
835 
836     /**
837      * <p>
838      * Reads the manifest from the JAR file and collects the entries. Some
839      * vendorKey entries are:</p>
840      * <ul><li>Implementation Title</li>
841      * <li>Implementation Version</li> <li>Implementation Vendor</li>
842      * <li>Implementation VendorId</li> <li>Bundle Name</li> <li>Bundle
843      * Version</li> <li>Bundle Vendor</li> <li>Bundle Description</li> <li>Main
844      * Class</li> </ul>
845      * However, all but a handful of specific entries are read in.
846      *
847      * @param dependency A reference to the dependency
848      * @param classInformation a collection of class information
849      * @return whether evidence was identified parsing the manifest
850      * @throws IOException if there is an issue reading the JAR file
851      */
852     //CSOFF: MethodLength
853     protected boolean parseManifest(Dependency dependency, List<ClassNameInformation> classInformation)
854             throws IOException {
855         boolean foundSomething = false;
856         try (JarFile jar = new JarFile(dependency.getActualFilePath(), false)) {
857             final Manifest manifest = jar.getManifest();
858             if (manifest == null) {
859                 if (!dependency.getFileName().toLowerCase().endsWith("-sources.jar")
860                         && !dependency.getFileName().toLowerCase().endsWith("-javadoc.jar")
861                         && !dependency.getFileName().toLowerCase().endsWith("-src.jar")
862                         && !dependency.getFileName().toLowerCase().endsWith("-doc.jar")) {
863                     LOGGER.debug("Jar file '{}' does not contain a manifest.", dependency.getFileName());
864                 }
865                 return false;
866             }
867             String source = "Manifest";
868             String specificationVersion = null;
869             boolean hasImplementationVersion = false;
870             Attributes atts = manifest.getMainAttributes();
871             for (Entry<Object, Object> entry : atts.entrySet()) {
872                 String key = entry.getKey().toString();
873                 String value = atts.getValue(key);
874                 if (HTML_DETECTION_PATTERN.matcher(value).find()) {
875                     value = Jsoup.parse(value).text();
876                 }
877                 if (value.startsWith("git@github.com:") || value.startsWith("git@gitlab.com:")) {
878                     value = value.substring(15);
879                 }
880                 if (IGNORE_VALUES.contains(value)) {
881                     continue;
882                 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_TITLE.toString())) {
883                     foundSomething = true;
884                     dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.HIGH);
885                     addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
886                 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VERSION.toString())) {
887                     hasImplementationVersion = true;
888                     foundSomething = true;
889                     dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.HIGH);
890                 } else if ("specification-version".equalsIgnoreCase(key)) {
891                     specificationVersion = value;
892                 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VENDOR.toString())) {
893                     foundSomething = true;
894                     dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.HIGH);
895                     addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
896                 } else if (key.equalsIgnoreCase(IMPLEMENTATION_VENDOR_ID)) {
897                     foundSomething = true;
898                     dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
899                     addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
900                 } else if (key.equalsIgnoreCase(BUNDLE_DESCRIPTION)) {
901                     if (!value.startsWith("Sonatype helps open source projects")) {
902                         foundSomething = true;
903                         addDescription(dependency, value, "manifest", key);
904                         addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
905                     }
906                 } else if (key.equalsIgnoreCase(BUNDLE_NAME)) {
907                     foundSomething = true;
908                     dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
909                     addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
910 //                //the following caused false positives.
911 //                } else if (key.equalsIgnoreCase(BUNDLE_VENDOR)) {
912                 } else if (key.equalsIgnoreCase(BUNDLE_VERSION)) {
913                     foundSomething = true;
914                     dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.HIGH);
915                 } else if (key.equalsIgnoreCase(Attributes.Name.MAIN_CLASS.toString())) {
916                     //noinspection UnnecessaryContinue
917                     continue;
918                     //skipping main class as if this has important information to add it will be added during class name analysis...
919                 } else if ("implementation-url".equalsIgnoreCase(key)
920                         && value != null
921                         && value.startsWith("https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/parent/")) {
922                     continue;
923                 } else {
924                     key = key.toLowerCase();
925                     if (!IGNORE_KEYS.contains(key)
926                             && !key.endsWith("jdk")
927                             && !key.contains("lastmodified")
928                             && !key.endsWith("package")
929                             && !key.endsWith("classpath")
930                             && !key.endsWith("class-path")
931                             && !key.endsWith("-scm") //todo change this to a regex?
932                             && !key.startsWith("scm-")
933                             && !value.trim().startsWith("scm:")
934                             && !isImportPackage(key, value)
935                             && !isPackage(key, value)) {
936                         foundSomething = true;
937                         if (key.contains("version")) {
938                             if (!key.contains("specification")) {
939                                 dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM);
940                             }
941                         } else if ("build-id".equals(key)) {
942                             int pos = value.indexOf('(');
943                             if (pos > 0) {
944                                 value = value.substring(0, pos - 1);
945                             }
946                             pos = value.indexOf('[');
947                             if (pos > 0) {
948                                 value = value.substring(0, pos - 1);
949                             }
950                             dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM);
951                         } else if (key.contains("title")) {
952                             dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
953                             addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
954                         } else if (key.contains("vendor")) {
955                             if (key.contains("specification")) {
956                                 dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.LOW);
957                             } else {
958                                 dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
959                                 addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
960                             }
961                         } else if (key.contains("name")) {
962                             dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
963                             dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
964                             addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
965                             addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
966                         } else if (key.contains("license")) {
967                             addLicense(dependency, value);
968                         } else if (key.contains("description")) {
969                             if (!value.startsWith("Sonatype helps open source projects")) {
970                                 final String trimmedDescription = addDescription(dependency, value, "manifest", key);
971                                 addMatchingValues(classInformation, trimmedDescription, dependency, EvidenceType.VENDOR);
972                                 addMatchingValues(classInformation, trimmedDescription, dependency, EvidenceType.PRODUCT);
973                             }
974                         } else {
975                             dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.LOW);
976                             dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.LOW);
977                             addMatchingValues(classInformation, value, dependency, EvidenceType.VERSION);
978                             addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
979                             if (value.matches(".*\\d.*")) {
980                                 final StringTokenizer tokenizer = new StringTokenizer(value, " ");
981                                 while (tokenizer.hasMoreElements()) {
982                                     final String s = tokenizer.nextToken();
983                                     if (s.matches("^[0-9.]+$")) {
984                                         dependency.addEvidence(EvidenceType.VERSION, source, key, s, Confidence.LOW);
985                                     }
986                                 }
987                             }
988                         }
989                     }
990                 }
991             }
992             for (Map.Entry<String, Attributes> item : manifest.getEntries().entrySet()) {
993                 final String name = item.getKey();
994                 source = "manifest: " + name;
995                 atts = item.getValue();
996                 for (Entry<Object, Object> entry : atts.entrySet()) {
997                     final String key = entry.getKey().toString();
998                     final String value = atts.getValue(key);
999                     if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_TITLE.toString())) {
1000                         foundSomething = true;
1001                         dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
1002                         addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
1003                     } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VERSION.toString())) {
1004                         foundSomething = true;
1005                         dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM);
1006                     } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VENDOR.toString())) {
1007                         foundSomething = true;
1008                         dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
1009                         addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
1010                     } else if (key.equalsIgnoreCase(Attributes.Name.SPECIFICATION_TITLE.toString())) {
1011                         foundSomething = true;
1012                         dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
1013                         addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
1014                     }
1015                 }
1016             }
1017             if (specificationVersion != null && !hasImplementationVersion) {
1018                 foundSomething = true;
1019                 dependency.addEvidence(EvidenceType.VERSION, source, "specification-version", specificationVersion, Confidence.HIGH);
1020             }
1021         }
1022         return foundSomething;
1023     }
1024     //CSON: MethodLength
1025 
1026     /**
1027      * Adds a description to the given dependency. If the description contains
1028      * one of the following strings beyond 100 characters, then the description
1029      * used will be trimmed to that position:
1030      * <ul><li>"such as"</li><li>"like "</li><li>"will use "</li><li>"* uses
1031      * "</li></ul>
1032      *
1033      * @param dependency a dependency
1034      * @param description the description
1035      * @param source the source of the evidence
1036      * @param key the "name" of the evidence
1037      * @return if the description is trimmed, the trimmed version is returned;
1038      * otherwise the original description is returned
1039      */
1040     public static String addDescription(Dependency dependency, String description, String source, String key) {
1041         if (dependency.getDescription() == null) {
1042             dependency.setDescription(description);
1043         }
1044         String desc;
1045         if (HTML_DETECTION_PATTERN.matcher(description).find()) {
1046             desc = Jsoup.parse(description).text();
1047         } else {
1048             desc = description;
1049         }
1050         dependency.setDescription(desc);
1051         if (desc.length() > 100) {
1052             desc = desc.replaceAll("\\s\\s+", " ");
1053             final int posSuchAs = desc.toLowerCase().indexOf("such as ", 100);
1054             final int posLike = desc.toLowerCase().indexOf("like ", 100);
1055             final int posWillUse = desc.toLowerCase().indexOf("will use ", 100);
1056             final int posUses = desc.toLowerCase().indexOf(" uses ", 100);
1057 
1058             int pos = -1;
1059             pos = Math.max(pos, posSuchAs);
1060             if (pos >= 0 && posLike >= 0) {
1061                 pos = Math.min(pos, posLike);
1062             } else {
1063                 pos = Math.max(pos, posLike);
1064             }
1065             if (pos >= 0 && posWillUse >= 0) {
1066                 pos = Math.min(pos, posWillUse);
1067             } else {
1068                 pos = Math.max(pos, posWillUse);
1069             }
1070             if (pos >= 0 && posUses >= 0) {
1071                 pos = Math.min(pos, posUses);
1072             } else {
1073                 pos = Math.max(pos, posUses);
1074             }
1075             if (pos > 0) {
1076                 desc = desc.substring(0, pos) + "...";
1077             }
1078 //            //no longer add description directly. Use matching terms in other parts of the evidence collection
1079 //            //but description adds too many FP
1080 //            dependency.addEvidence(EvidenceType.PRODUCT, source, key, desc, Confidence.LOW);
1081 //            dependency.addEvidence(EvidenceType.VENDOR, source, key, desc, Confidence.LOW);
1082 //        } else {
1083 //            dependency.addEvidence(EvidenceType.PRODUCT, source, key, desc, Confidence.MEDIUM);
1084 //            dependency.addEvidence(EvidenceType.VENDOR, source, key, desc, Confidence.MEDIUM);
1085         }
1086         return desc;
1087     }
1088 
1089     /**
1090      * Adds a license to the given dependency.
1091      *
1092      * @param d a dependency
1093      * @param license the license
1094      */
1095     private void addLicense(Dependency d, String license) {
1096         if (d.getLicense() == null) {
1097             d.setLicense(license);
1098         } else if (!d.getLicense().contains(license)) {
1099             d.setLicense(d.getLicense() + NEWLINE + license);
1100         }
1101     }
1102 
1103     /**
1104      * Initializes the JarAnalyzer.
1105      *
1106      * @param engine a reference to the dependency-check engine
1107      * @throws InitializationException is thrown if there is an exception
1108      * creating a temporary directory
1109      */
1110     @Override
1111     public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
1112         try {
1113             final File baseDir = getSettings().getTempDirectory();
1114             tempFileLocation = File.createTempFile("check", "tmp", baseDir);
1115             if (!tempFileLocation.delete()) {
1116                 final String msg = String.format("Unable to delete temporary file '%s'.", tempFileLocation.getAbsolutePath());
1117                 setEnabled(false);
1118                 throw new InitializationException(msg);
1119             }
1120             if (!tempFileLocation.mkdirs()) {
1121                 final String msg = String.format("Unable to create directory '%s'.", tempFileLocation.getAbsolutePath());
1122                 setEnabled(false);
1123                 throw new InitializationException(msg);
1124             }
1125         } catch (IOException ex) {
1126             setEnabled(false);
1127             throw new InitializationException("Unable to create a temporary file", ex);
1128         }
1129     }
1130 
1131     /**
1132      * Deletes any files extracted from the JAR during analysis.
1133      */
1134     @Override
1135     public void closeAnalyzer() {
1136         if (tempFileLocation != null && tempFileLocation.exists()) {
1137             LOGGER.debug("Attempting to delete temporary files from `{}`", tempFileLocation.toString());
1138             final boolean success = FileUtils.delete(tempFileLocation);
1139             if (!success && tempFileLocation.exists()) {
1140                 final String[] l = tempFileLocation.list();
1141                 if (l != null && l.length > 0) {
1142                     LOGGER.warn("Failed to delete the JAR Analyzder's temporary files from `{}`, "
1143                             + "see the log for more details", tempFileLocation.getAbsolutePath());
1144                 }
1145             }
1146         }
1147     }
1148 
1149     /**
1150      * Determines if the key value pair from the manifest is for an "import"
1151      * type entry for package names.
1152      *
1153      * @param key the key from the manifest
1154      * @param value the value from the manifest
1155      * @return true or false depending on if it is believed the entry is an
1156      * "import" entry
1157      */
1158     private boolean isImportPackage(String key, String value) {
1159         final Pattern packageRx = Pattern.compile("^(\\s*[a-zA-Z0-9_#\\$\\*\\.]+\\s*[,;])+(\\s*[a-zA-Z0-9_#\\$\\*\\.]+\\s*)?$");
1160         final boolean matches = packageRx.matcher(value).matches();
1161         return matches && (key.contains("import") || key.contains("include") || value.length() > 10);
1162     }
1163 
1164     /**
1165      * Cycles through an enumeration of JarEntries, contained within the
1166      * dependency, and returns a list of the class names. This does not include
1167      * core Java package names (i.e. java.* or javax.*).
1168      *
1169      * @param dependency the dependency being analyzed
1170      * @return an list of fully qualified class names
1171      */
1172     protected List<ClassNameInformation> collectClassNames(Dependency dependency) {
1173         final List<ClassNameInformation> classNames = new ArrayList<>();
1174         try (JarFile jar = new JarFile(dependency.getActualFilePath(), false)) {
1175             final Enumeration<JarEntry> entries = jar.entries();
1176             while (entries.hasMoreElements()) {
1177                 final JarEntry entry = entries.nextElement();
1178                 final String name = entry.getName().toLowerCase();
1179                 //no longer stripping "|com\\.sun" - there are some com.sun jar files with CVEs.
1180                 if (name.endsWith(".class") && !name.matches("^javax?\\..*$")) {
1181                     final ClassNameInformation className = new ClassNameInformation(name.substring(0, name.length() - 6));
1182                     classNames.add(className);
1183                 }
1184             }
1185         } catch (IOException ex) {
1186             LOGGER.warn("Unable to open jar file '{}'.", dependency.getFileName());
1187             LOGGER.debug("", ex);
1188         }
1189         return classNames;
1190     }
1191 
1192     /**
1193      * Cycles through the list of class names and places the package levels 0-3
1194      * into the provided maps for vendor and product. This is helpful when
1195      * analyzing vendor/product as many times this is included in the package
1196      * name.
1197      *
1198      * @param classNames a list of class names
1199      * @param vendor HashMap of possible vendor names from package names (e.g.
1200      * owasp)
1201      * @param product HashMap of possible product names from package names (e.g.
1202      * dependencycheck)
1203      */
1204     private void analyzeFullyQualifiedClassNames(List<ClassNameInformation> classNames,
1205             Map<String, Integer> vendor, Map<String, Integer> product) {
1206         for (ClassNameInformation entry : classNames) {
1207             final List<String> list = entry.getPackageStructure();
1208             addEntry(vendor, list.get(0));
1209 
1210             if (list.size() == 2) {
1211                 addEntry(product, list.get(1));
1212             } else if (list.size() == 3) {
1213                 addEntry(vendor, list.get(1));
1214                 addEntry(product, list.get(1));
1215                 addEntry(product, list.get(2));
1216             } else if (list.size() >= 4) {
1217                 addEntry(vendor, list.get(1));
1218                 addEntry(vendor, list.get(2));
1219                 addEntry(product, list.get(1));
1220                 addEntry(product, list.get(2));
1221                 addEntry(product, list.get(3));
1222             }
1223         }
1224     }
1225 
1226     /**
1227      * Adds an entry to the specified collection and sets the Integer (e.g. the
1228      * count) to 1. If the entry already exists in the collection then the
1229      * Integer is incremented by 1.
1230      *
1231      * @param collection a collection of strings and their occurrence count
1232      * @param key the key to add to the collection
1233      */
1234     private void addEntry(Map<String, Integer> collection, String key) {
1235         if (collection.containsKey(key)) {
1236             collection.put(key, collection.get(key) + 1);
1237         } else {
1238             collection.put(key, 1);
1239         }
1240     }
1241 
1242     /**
1243      * Cycles through the collection of class name information to see if parts
1244      * of the package names are contained in the provided value. If found, it
1245      * will be added as the HIGHEST confidence evidence because we have more
1246      * then one source corroborating the value.
1247      *
1248      * @param classes a collection of class name information
1249      * @param value the value to check to see if it contains a package name
1250      * @param dep the dependency to add new entries too
1251      * @param type the type of evidence (vendor, product, or version)
1252      */
1253     protected static void addMatchingValues(List<ClassNameInformation> classes, String value, Dependency dep, EvidenceType type) {
1254         if (value == null || value.isEmpty() || classes == null || classes.isEmpty()) {
1255             return;
1256         }
1257         final HashSet<String> tested = new HashSet<>();
1258         //TODO add a hashSet and only analyze any given key once.
1259         for (ClassNameInformation cni : classes) {
1260             //classes.forEach((cni) -> {
1261             for (String key : cni.getPackageStructure()) {
1262                 //cni.getPackageStructure().forEach((key) -> {
1263                 if (!tested.contains(key)) {
1264                     tested.add(key);
1265                     final int pos = StringUtils.indexOfIgnoreCase(value, key);
1266                     if ((pos == 0 && (key.length() == value.length() || (key.length() < value.length()
1267                             && !Character.isLetterOrDigit(value.charAt(key.length())))))
1268                             || (pos > 0 && !Character.isLetterOrDigit(value.charAt(pos - 1))
1269                             && (pos + key.length() == value.length() || (key.length() < value.length()
1270                             && !Character.isLetterOrDigit(value.charAt(pos + key.length())))))) {
1271                         dep.addEvidence(type, "jar", "package name", key, Confidence.HIGHEST);
1272                     }
1273                 }
1274             }
1275         }
1276     }
1277 
1278     /**
1279      * Simple check to see if the attribute from a manifest is just a package
1280      * name.
1281      *
1282      * @param key the key of the value to check
1283      * @param value the value to check
1284      * @return true if the value looks like a java package name, otherwise false
1285      */
1286     private boolean isPackage(String key, String value) {
1287 
1288         return !key.matches(".*(version|title|vendor|name|license|description).*")
1289                 && value.matches("^[a-zA-Z_][a-zA-Z0-9_\\$]*\\.([a-zA-Z_][a-zA-Z0-9_\\$]*\\.)*([a-zA-Z_][a-zA-Z0-9_\\$]*)$");
1290 
1291     }
1292 
1293     /**
1294      * Returns null if the value starts with `${` and ends with `}`.
1295      *
1296      * @param value the value to check
1297      * @return the correct value which may be null
1298      */
1299     private static String intepolationFailCheck(String value) {
1300         if (value != null && value.contains("${")) {
1301             return null;
1302         }
1303         return value;
1304     }
1305 
1306     /**
1307      * Extracts the license information from the pom and adds it to the
1308      * dependency.
1309      *
1310      * @param pom the pom object
1311      * @param dependency the dependency to add license information too
1312      */
1313     public static void extractLicense(Model pom, Dependency dependency) {
1314         //license
1315         if (pom.getLicenses() != null) {
1316             StringBuilder license = null;
1317             for (License lic : pom.getLicenses()) {
1318                 String tmp = null;
1319                 if (lic.getName() != null) {
1320                     tmp = lic.getName();
1321                 }
1322                 if (lic.getUrl() != null) {
1323                     if (tmp == null) {
1324                         tmp = lic.getUrl();
1325                     } else {
1326                         tmp += ": " + lic.getUrl();
1327                     }
1328                 }
1329                 if (tmp == null) {
1330                     continue;
1331                 }
1332                 if (HTML_DETECTION_PATTERN.matcher(tmp).find()) {
1333                     tmp = Jsoup.parse(tmp).text();
1334                 }
1335                 if (license == null) {
1336                     license = new StringBuilder(tmp);
1337                 } else {
1338                     license.append("\n").append(tmp);
1339                 }
1340             }
1341             if (license != null) {
1342                 dependency.setLicense(license.toString());
1343 
1344             }
1345         }
1346     }
1347 
1348     /**
1349      * Stores information about a class name.
1350      */
1351     protected static class ClassNameInformation {
1352 
1353         /**
1354          * The fully qualified class name.
1355          */
1356         private String name;
1357         /**
1358          * Up to the first four levels of the package structure, excluding a
1359          * leading "org" or "com".
1360          */
1361         private final ArrayList<String> packageStructure = new ArrayList<>();
1362 
1363         /**
1364          * <p>
1365          * Stores information about a given class name. This class will keep the
1366          * fully qualified class name and a list of the important parts of the
1367          * package structure. Up to the first four levels of the package
1368          * structure are stored, excluding a leading "org" or "com".
1369          * Example:</p>
1370          * <code>ClassNameInformation obj = new ClassNameInformation("org/owasp/dependencycheck/analyzer/JarAnalyzer");
1371          * System.out.println(obj.getName());
1372          * for (String p : obj.getPackageStructure())
1373          *     System.out.println(p);
1374          * </code>
1375          * <p>
1376          * Would result in:</p>
1377          * <code>org.owasp.dependencycheck.analyzer.JarAnalyzer
1378          * owasp
1379          * dependencycheck
1380          * analyzer
1381          * jaranalyzer</code>
1382          *
1383          * @param className a fully qualified class name
1384          */
1385         ClassNameInformation(String className) {
1386             name = className;
1387             if (name.contains("/")) {
1388                 final String[] tmp = StringUtils.split(className.toLowerCase(), '/');
1389                 int start = 0;
1390                 int end = 3;
1391                 if ("com".equals(tmp[0]) || "org".equals(tmp[0])) {
1392                     start = 1;
1393                     end = 4;
1394                 }
1395                 if (tmp.length <= end) {
1396                     end = tmp.length - 1;
1397                 }
1398                 packageStructure.addAll(Arrays.asList(tmp).subList(start, end + 1));
1399             } else {
1400                 packageStructure.add(name);
1401             }
1402         }
1403 
1404         /**
1405          * Get the value of name
1406          *
1407          * @return the value of name
1408          */
1409         public String getName() {
1410             return name;
1411         }
1412 
1413         /**
1414          * Set the value of name
1415          *
1416          * @param name new value of name
1417          */
1418         public void setName(String name) {
1419             this.name = name;
1420         }
1421 
1422         /**
1423          * Get the value of packageStructure
1424          *
1425          * @return the value of packageStructure
1426          */
1427         public ArrayList<String> getPackageStructure() {
1428             return packageStructure;
1429         }
1430     }
1431 
1432     /**
1433      * Retrieves the next temporary directory to extract an archive too.
1434      *
1435      * @return a directory
1436      * @throws AnalysisException thrown if unable to create temporary directory
1437      */
1438     private File getNextTempDirectory() throws AnalysisException {
1439         final int dirCount = DIR_COUNT.incrementAndGet();
1440         final File directory = new File(tempFileLocation, String.valueOf(dirCount));
1441         //getting an exception for some directories not being able to be created; might be because the directory already exists?
1442         if (directory.exists()) {
1443             return getNextTempDirectory();
1444         }
1445         if (!directory.mkdirs()) {
1446             final String msg = String.format("Unable to create temp directory '%s'.", directory.getAbsolutePath());
1447             throw new AnalysisException(msg);
1448         }
1449         return directory;
1450     }
1451 }