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