mirror of
https://github.com/ysoftdevs/DependencyCheck.git
synced 2026-01-14 15:53:36 +01:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dea5a6937e | ||
|
|
545c324e56 | ||
|
|
535d1e4aff | ||
|
|
8debea384f | ||
|
|
a0b6b66a5f | ||
|
|
37d165d6cb | ||
|
|
5b6eb13cf6 | ||
|
|
5d68c9f1e1 | ||
|
|
faff34a8c6 | ||
|
|
c31be72c8a | ||
|
|
1f0c13b7cb | ||
|
|
f06f1d1c42 | ||
|
|
2eca1f9702 | ||
|
|
ca6cb8811e | ||
|
|
ff14d8344f | ||
|
|
bfb6373742 | ||
|
|
e3f401debb | ||
|
|
c515afd8eb | ||
|
|
e028641861 | ||
|
|
72f9cb2ab2 | ||
|
|
e8694de6fa | ||
|
|
18d38592d4 | ||
|
|
b9767acd02 | ||
|
|
c9060da46e | ||
|
|
ddbcea7abe | ||
|
|
e488767cea | ||
|
|
85cacaf91e | ||
|
|
a038bef7fe | ||
|
|
539d3cbaba | ||
|
|
80784a44c5 | ||
|
|
b1a55e2df3 | ||
|
|
870d345de8 | ||
|
|
2b830dccfa | ||
|
|
9f08cf553b | ||
|
|
7c14017db3 | ||
|
|
e0e85c468a | ||
|
|
6628fc3c33 | ||
|
|
61a1531e7b | ||
|
|
933a8f8ec6 | ||
|
|
f660afc6cb | ||
|
|
a5dc79dffe | ||
|
|
dbc862ad39 | ||
|
|
e6efe6e610 | ||
|
|
9a7fbe44eb | ||
|
|
adfc913a0e | ||
|
|
8813652f0d | ||
|
|
250444dd25 | ||
|
|
a939d0c844 | ||
|
|
577b5ad704 | ||
|
|
7476550356 |
@@ -3,11 +3,11 @@ Copyright (c) 2012-2013 Jeremy Long. All Rights Reserved.
|
||||
|
||||
The licenses for the software listed below can be found in the META-INF/licenses/[dependency name].
|
||||
|
||||
This product includes software developed by
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
This product includes software developed by
|
||||
Jquery.com (http://jquery.com/).
|
||||
This product includes software developed by Jquery.com (http://jquery.com/).
|
||||
|
||||
This product includs software developed by Jonathan Hedley (jsoup.org)
|
||||
|
||||
This software contains unmodified binary redistributions for H2 database engine (http://www.h2database.com/), which is dual licensed and available under a modified version of the MPL 1.1 (Mozilla Public License) or under the (unmodified) EPL 1.0 (Eclipse Public License).
|
||||
An original copy of the license agreement can be found at: http://www.h2database.com/html/license.html
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
DependencyCheck
|
||||
Dependency-Check
|
||||
=========
|
||||
|
||||
DependencyCheck is a utility that attempts to detect publicly disclosed vulnerabilities contained within project dependencies. It does this by determining if there is a Common Platform Enumeration (CPE) identifier for a given dependency. If found, it will generate a report linking to the associated CVE entries..
|
||||
Dependency-Check is a utility that attempts to detect publicly disclosed vulnerabilities contained within project dependencies. It does this by determining if there is a Common Platform Enumeration (CPE) identifier for a given dependency. If found, it will generate a report linking to the associated CVE entries.
|
||||
|
||||
More information can be found on the [wiki].
|
||||
|
||||
|
||||
39
pom.xml
39
pom.xml
@@ -22,7 +22,7 @@ along with DependencyCheck. If not, see <http://www.gnu.org/licenses />.
|
||||
|
||||
<groupId>org.owasp</groupId>
|
||||
<artifactId>dependency-check</artifactId>
|
||||
<version>0.3.1.1</version>
|
||||
<version>0.3.2.2</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>DependencyCheck</name>
|
||||
@@ -37,13 +37,22 @@ along with DependencyCheck. If not, see <http://www.gnu.org/licenses />.
|
||||
<developer>
|
||||
<name>Jeremy Long</name>
|
||||
<email>jeremy.long@owasp.org</email>
|
||||
<organization>owasp</organization>
|
||||
<organization>OWASP</organization>
|
||||
<organizationUrl>https://www.owasp.org/index.php/OWASP_Dependency_Check</organizationUrl>
|
||||
<roles>
|
||||
<role>architect</role>
|
||||
<role>developer</role>
|
||||
</roles>
|
||||
</developer>
|
||||
<developer>
|
||||
<name>Steve Springett</name>
|
||||
<email>Steve.Springett@owasp.org</email>
|
||||
<organization>OWASP</organization>
|
||||
<organizationUrl>https://www.owasp.org/index.php/OWASP_Dependency_Check</organizationUrl>
|
||||
<roles>
|
||||
<role>contributor</role>
|
||||
</roles>
|
||||
</developer>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection>scm:git:git@github.com:jeremylong/DependencyCheck.git</connection>
|
||||
@@ -147,7 +156,6 @@ along with DependencyCheck. If not, see <http://www.gnu.org/licenses />.
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>cobertura-maven-plugin</artifactId>
|
||||
@@ -397,8 +405,13 @@ along with DependencyCheck. If not, see <http://www.gnu.org/licenses />.
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>2.0.1</version>
|
||||
<scope>provided</scope><!-- don't include this in the libs-->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-cli</groupId>
|
||||
<artifactId>commons-cli</artifactId>
|
||||
@@ -410,10 +423,9 @@ along with DependencyCheck. If not, see <http://www.gnu.org/licenses />.
|
||||
<version>2.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- Using the same as Lucene-->
|
||||
<groupId>commons-lang</groupId>
|
||||
<artifactId>commons-lang</artifactId>
|
||||
<version>2.4</version>
|
||||
<version>2.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
@@ -425,18 +437,17 @@ along with DependencyCheck. If not, see <http://www.gnu.org/licenses />.
|
||||
<dependency>
|
||||
<groupId>org.apache.lucene</groupId>
|
||||
<artifactId>lucene-core</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<!--<version>3.5.0</version>-->
|
||||
<version>4.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.lucene</groupId>
|
||||
<artifactId>lucene-analyzers-common</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<version>4.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.lucene</groupId>
|
||||
<artifactId>lucene-queryparser</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<version>4.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
@@ -490,7 +501,13 @@ along with DependencyCheck. If not, see <http://www.gnu.org/licenses />.
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>1.3.171</version>
|
||||
<version>1.3.172</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.7.2</version>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
|
||||
<!-- The following dependencies are only scanned during integration testing -->
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
^ \* You should have received a copy of the GNU General Public License along with\s*$
|
||||
^ \* Dependency-Check\. If not, see http://www.gnu.org/licenses/\.\s*$
|
||||
^ \*\s*$
|
||||
^ \* Copyright \(c\) 201[23] Jeremy Long\. All Rights Reserved\.\s*$
|
||||
^ \* Copyright \(c\) 201[23] (Jeremy Long|Steve Springett)\. All Rights Reserved\.\s*$
|
||||
^ \*/\s*$
|
||||
^package
|
||||
|
||||
@@ -52,28 +52,28 @@ public class Engine {
|
||||
/**
|
||||
* The list of dependencies.
|
||||
*/
|
||||
private List<Dependency> dependencies = new ArrayList<Dependency>();
|
||||
private final List<Dependency> dependencies = new ArrayList<Dependency>();
|
||||
/**
|
||||
* A Map of analyzers grouped by Analysis phase.
|
||||
*/
|
||||
private EnumMap<AnalysisPhase, List<Analyzer>> analyzers =
|
||||
private final EnumMap<AnalysisPhase, List<Analyzer>> analyzers =
|
||||
new EnumMap<AnalysisPhase, List<Analyzer>>(AnalysisPhase.class);
|
||||
/**
|
||||
* A set of extensions supported by the analyzers.
|
||||
*/
|
||||
private Set<String> extensions = new HashSet<String>();
|
||||
private final Set<String> extensions = new HashSet<String>();
|
||||
|
||||
/**
|
||||
* Creates a new Engine.
|
||||
*/
|
||||
public Engine() {
|
||||
boolean autoupdate = true;
|
||||
boolean autoUpdate = true;
|
||||
try {
|
||||
autoupdate = Settings.getBoolean(Settings.KEYS.AUTO_UPDATE);
|
||||
autoUpdate = Settings.getBoolean(Settings.KEYS.AUTO_UPDATE);
|
||||
} catch (InvalidSettingException ex) {
|
||||
Logger.getLogger(Engine.class.getName()).log(Level.WARNING, "Invalid setting for auto-update.");
|
||||
}
|
||||
if (autoupdate) {
|
||||
if (autoUpdate) {
|
||||
doUpdates();
|
||||
}
|
||||
loadAnalyzers();
|
||||
@@ -161,11 +161,13 @@ public class Engine {
|
||||
*/
|
||||
protected void scanDirectory(File dir) {
|
||||
final File[] files = dir.listFiles();
|
||||
for (File f : files) {
|
||||
if (f.isDirectory()) {
|
||||
scanDirectory(f);
|
||||
} else {
|
||||
scanFile(f);
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
if (f.isDirectory()) {
|
||||
scanDirectory(f);
|
||||
} else {
|
||||
scanFile(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,12 @@ import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.owasp.dependencycheck.Engine;
|
||||
import org.owasp.dependencycheck.dependency.Dependency;
|
||||
import org.owasp.dependencycheck.utils.DependencyVersion;
|
||||
import org.owasp.dependencycheck.utils.DependencyVersionUtil;
|
||||
|
||||
/**
|
||||
* <p>This analyzer ensures dependencies that should be grouped together, to
|
||||
@@ -51,6 +55,10 @@ public class DependencyBundlingAnalyzer extends AbstractAnalyzer implements Anal
|
||||
* The phase that this analyzer is intended to run in.
|
||||
*/
|
||||
private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_FINDING_ANALYSIS;
|
||||
/**
|
||||
* A pattern for obtaining the first part of a filename.
|
||||
*/
|
||||
private static final Pattern STARTING_TEXT_PATTERN = Pattern.compile("^[a-zA-Z]*");
|
||||
|
||||
/**
|
||||
* Returns a list of file EXTENSIONS supported by this analyzer.
|
||||
@@ -118,7 +126,8 @@ public class DependencyBundlingAnalyzer extends AbstractAnalyzer implements Anal
|
||||
final Dependency nextDependency = subIterator.next();
|
||||
|
||||
if (identifiersMatch(dependency, nextDependency)
|
||||
&& hasSameBasePath(dependency, nextDependency)) {
|
||||
&& hasSameBasePath(dependency, nextDependency)
|
||||
&& fileNameMatch(dependency, nextDependency)) {
|
||||
|
||||
if (isCore(dependency, nextDependency)) {
|
||||
dependency.addRelatedDependency(nextDependency);
|
||||
@@ -155,7 +164,7 @@ public class DependencyBundlingAnalyzer extends AbstractAnalyzer implements Anal
|
||||
|
||||
/**
|
||||
* Attempts to trim a maven repo to a common base path. This is typically
|
||||
* [drive]\[repolocation\repository\[path1]\[path2].
|
||||
* [drive]\[repo_location]\repository\[path1]\[path2].
|
||||
*
|
||||
* @param path the path to trim
|
||||
* @return a string representing the base path.
|
||||
@@ -179,6 +188,38 @@ public class DependencyBundlingAnalyzer extends AbstractAnalyzer implements Anal
|
||||
return path.substring(0, pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the file names (and version if it exists) of the two
|
||||
* dependencies are sufficiently similiar.
|
||||
* @param dependency1 a dependency2 to compare
|
||||
* @param dependency2 a dependency2 to compare
|
||||
* @return true if the identifiers in the two supplied dependencies are equal
|
||||
*/
|
||||
private boolean fileNameMatch(Dependency dependency1, Dependency dependency2) {
|
||||
if (dependency1 == null || dependency1.getFileName() == null
|
||||
|| dependency2 == null || dependency2.getFileName() == null) {
|
||||
return false;
|
||||
}
|
||||
final String fileName1 = dependency1.getFileName();
|
||||
final String fileName2 = dependency2.getFileName();
|
||||
//version check
|
||||
final DependencyVersion version1 = DependencyVersionUtil.parseVersionFromFileName(fileName1);
|
||||
final DependencyVersion version2 = DependencyVersionUtil.parseVersionFromFileName(fileName2);
|
||||
if (version1 != null && version2 != null) {
|
||||
if (!version1.equals(version2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//filename check
|
||||
final Matcher match1 = STARTING_TEXT_PATTERN.matcher(fileName1);
|
||||
final Matcher match2 = STARTING_TEXT_PATTERN.matcher(fileName2);
|
||||
if (match1.find() && match2.find()) {
|
||||
return match1.group().equals(match2.group());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the identifiers in the two supplied dependencies are equal.
|
||||
* @param dependency1 a dependency2 to compare
|
||||
|
||||
@@ -102,6 +102,7 @@ public class FalsePositiveAnalyzer extends AbstractAnalyzer {
|
||||
*/
|
||||
public void analyze(Dependency dependency, Engine engine) throws AnalysisException {
|
||||
removeJreEntries(dependency);
|
||||
removeBadMatches(dependency);
|
||||
boolean deepScan = false;
|
||||
try {
|
||||
deepScan = Settings.getBoolean(Settings.KEYS.PERFORM_DEEP_SCAN);
|
||||
@@ -182,7 +183,10 @@ public class FalsePositiveAnalyzer extends AbstractAnalyzer {
|
||||
final Iterator<Identifier> itr = identifiers.iterator();
|
||||
while (itr.hasNext()) {
|
||||
final Identifier i = itr.next();
|
||||
|
||||
if ((i.getValue().startsWith("cpe:/a:sun:java:")
|
||||
|| i.getValue().startsWith("cpe:/a:sun:java_se")
|
||||
|| i.getValue().startsWith("cpe:/a:oracle:java_se")
|
||||
|| i.getValue().startsWith("cpe:/a:oracle:jre")
|
||||
|| i.getValue().startsWith("cpe:/a:oracle:jdk"))
|
||||
&& !dependency.getFileName().toLowerCase().endsWith("rt.jar")) {
|
||||
@@ -210,4 +214,24 @@ public class FalsePositiveAnalyzer extends AbstractAnalyzer {
|
||||
}
|
||||
return cpe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes bad CPE matches for a dependency. Unfortunately, right now
|
||||
* these are hard-coded patches for specific problems identified when
|
||||
* testing this ona LARGE volume of jar files.
|
||||
* @param dependency the dependency to analyze
|
||||
*/
|
||||
private void removeBadMatches(Dependency dependency) {
|
||||
final Set<Identifier> identifiers = dependency.getIdentifiers();
|
||||
final Iterator<Identifier> itr = identifiers.iterator();
|
||||
while (itr.hasNext()) {
|
||||
final Identifier i = itr.next();
|
||||
//TODO move this startswith expression to a configuration file?
|
||||
if (i.getValue().startsWith("cpe:/a:apache:xerces-c++:")
|
||||
&& dependency.getFileName().toLowerCase().endsWith(".jar")) {
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
package org.owasp.dependencycheck.analyzer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.util.Enumeration;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.owasp.dependencycheck.Engine;
|
||||
import org.owasp.dependencycheck.dependency.Dependency;
|
||||
import org.owasp.dependencycheck.dependency.Evidence;
|
||||
@@ -39,19 +39,28 @@ import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBElement;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.parsers.SAXParser;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
import javax.xml.transform.sax.SAXSource;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.owasp.dependencycheck.analyzer.pom.MavenNamespaceFilter;
|
||||
import org.owasp.dependencycheck.analyzer.pom.generated.License;
|
||||
import org.owasp.dependencycheck.analyzer.pom.generated.Model;
|
||||
import org.owasp.dependencycheck.analyzer.pom.generated.Organization;
|
||||
import org.owasp.dependencycheck.utils.NonClosingStream;
|
||||
import org.owasp.dependencycheck.utils.Settings;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.XMLFilter;
|
||||
import org.xml.sax.XMLReader;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -99,7 +108,8 @@ public class JarAnalyzer extends AbstractAnalyzer implements Analyzer {
|
||||
"class-path",
|
||||
"tool",
|
||||
"bundle-manifestversion",
|
||||
"bundlemanifestversion");
|
||||
"bundlemanifestversion",
|
||||
"include-resource");
|
||||
/**
|
||||
* The set of file extensions supported by this analyzer.
|
||||
*/
|
||||
@@ -207,6 +217,10 @@ public class JarAnalyzer extends AbstractAnalyzer implements Analyzer {
|
||||
throw new AnalysisException("Exception occurred reading the JAR file.", ex);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A pattern to detect HTML within text.
|
||||
*/
|
||||
private static final Pattern HTML_DETECTION_PATTERN = Pattern.compile("\\<[a-z]+.*/?\\>", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* Attempts to find a pom.xml within the JAR file. If found it extracts
|
||||
@@ -214,154 +228,225 @@ public class JarAnalyzer extends AbstractAnalyzer implements Analyzer {
|
||||
* the strings contained within the pom.properties if one exists.
|
||||
*
|
||||
* @param dependency the dependency being analyzed.
|
||||
* @throws IOException is thrown if there is an error reading the zip file.
|
||||
* @throws AnalysisException is thrown if there is an exception parsing the
|
||||
* pom.
|
||||
* @throws AnalysisException is thrown if there is an exception parsing the pom.
|
||||
* @return whether or not evidence was added to the dependency
|
||||
*/
|
||||
protected boolean analyzePOM(Dependency dependency) throws IOException, AnalysisException {
|
||||
protected boolean analyzePOM(Dependency dependency) throws AnalysisException {
|
||||
boolean foundSomething = false;
|
||||
Properties pomProperties = null;
|
||||
final List<Model> poms = new ArrayList<Model>();
|
||||
FileInputStream fs = null;
|
||||
final JarFile jar;
|
||||
try {
|
||||
fs = new FileInputStream(dependency.getActualFilePath());
|
||||
final ZipInputStream zin = new ZipInputStream(fs);
|
||||
ZipEntry entry = zin.getNextEntry();
|
||||
|
||||
while (entry != null) {
|
||||
final String entryName = (new File(entry.getName())).getName().toLowerCase();
|
||||
|
||||
if (!entry.isDirectory() && "pom.xml".equals(entryName)) {
|
||||
final NonClosingStream stream = new NonClosingStream(zin);
|
||||
Model p = null;
|
||||
try {
|
||||
final JAXBElement obj = (JAXBElement) pomUnmarshaller.unmarshal(stream);
|
||||
p = (Model) obj.getValue();
|
||||
} catch (JAXBException ex) {
|
||||
final String msg = String.format("Unable to parse POM '%s' in '%s'",
|
||||
entry.getName(), dependency.getFilePath());
|
||||
final AnalysisException ax = new AnalysisException(msg, ex);
|
||||
dependency.getAnalysisExceptions().add(ax);
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.INFO, msg);
|
||||
}
|
||||
if (p != null) {
|
||||
poms.add(p);
|
||||
}
|
||||
zin.closeEntry();
|
||||
} else if (!entry.isDirectory() && "pom.properties".equals(entryName)) {
|
||||
//TODO what if there is more then one pom.properties?
|
||||
// need to find the POM, then look to see if there is a sibling
|
||||
// pom.properties and use those together.
|
||||
if (pomProperties == null) {
|
||||
Reader reader;
|
||||
try {
|
||||
reader = new InputStreamReader(zin, "UTF-8");
|
||||
pomProperties = new Properties();
|
||||
pomProperties.load(reader);
|
||||
} finally {
|
||||
//zin.closeEntry closes the reader
|
||||
//reader.close();
|
||||
zin.closeEntry();
|
||||
}
|
||||
} else {
|
||||
final String msg = "JAR file contains multiple pom.properties files - unable to process POM";
|
||||
final AnalysisException ax = new AnalysisException(msg);
|
||||
dependency.getAnalysisExceptions().add(ax);
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.INFO, msg);
|
||||
}
|
||||
}
|
||||
|
||||
entry = zin.getNextEntry();
|
||||
}
|
||||
jar = new JarFile(dependency.getActualFilePath());
|
||||
} catch (IOException ex) {
|
||||
throw new AnalysisException("Error reading JAR file as zip.", ex);
|
||||
} finally {
|
||||
if (fs != null) {
|
||||
fs.close();
|
||||
}
|
||||
final String msg = String.format("Unable to read JarFile '%s'.", dependency.getActualFilePath());
|
||||
final AnalysisException ax = new AnalysisException(msg, ex);
|
||||
dependency.getAnalysisExceptions().add(ax);
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.WARNING, msg, ex);
|
||||
return foundSomething;
|
||||
}
|
||||
List<String> pomEntries;
|
||||
try {
|
||||
pomEntries = retrievePomListing(jar);
|
||||
} catch (IOException ex) {
|
||||
final String msg = String.format("Unable to read JarEntries in '%s'.", dependency.getActualFilePath());
|
||||
final AnalysisException ax = new AnalysisException(msg, ex);
|
||||
dependency.getAnalysisExceptions().add(ax);
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.WARNING, msg, ex);
|
||||
return foundSomething;
|
||||
}
|
||||
|
||||
for (Model pom : poms) {
|
||||
//group id
|
||||
final String groupid = interpolateString(pom.getGroupId(), pomProperties);
|
||||
if (groupid != null) {
|
||||
foundSomething = true;
|
||||
dependency.getVendorEvidence().addEvidence("pom", "groupid", groupid, Evidence.Confidence.HIGH);
|
||||
dependency.getProductEvidence().addEvidence("pom", "groupid", groupid, Evidence.Confidence.LOW);
|
||||
for (String path : pomEntries) {
|
||||
Properties pomProperties = null;
|
||||
try {
|
||||
pomProperties = retrievePomProperties(path, jar);
|
||||
} catch (IOException ex) {
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.FINEST, "ignore this, failed reading a non-existent pom.properties", ex);
|
||||
}
|
||||
//artifact id
|
||||
final String artifactid = interpolateString(pom.getArtifactId(), pomProperties);
|
||||
if (artifactid != null) {
|
||||
foundSomething = true;
|
||||
dependency.getProductEvidence().addEvidence("pom", "artifactid", artifactid, Evidence.Confidence.HIGH);
|
||||
dependency.getVendorEvidence().addEvidence("pom", "artifactid", artifactid, Evidence.Confidence.LOW);
|
||||
Model pom = null;
|
||||
try {
|
||||
pom = retrievePom(path, jar);
|
||||
} catch (JAXBException ex) {
|
||||
final String msg = String.format("Unable to parse POM '%s' in '%s'",
|
||||
path, dependency.getFilePath());
|
||||
final AnalysisException ax = new AnalysisException(msg, ex);
|
||||
dependency.getAnalysisExceptions().add(ax);
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.WARNING, msg);
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.SEVERE, msg, ax);
|
||||
} catch (IOException ex) {
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.SEVERE, null, ex);
|
||||
}
|
||||
//version
|
||||
final String version = interpolateString(pom.getVersion(), pomProperties);
|
||||
if (version != null) {
|
||||
foundSomething = true;
|
||||
dependency.getVersionEvidence().addEvidence("pom", "version", version, Evidence.Confidence.HIGH);
|
||||
}
|
||||
// org name
|
||||
final Organization org = pom.getOrganization();
|
||||
if (org != null && org.getName() != null) {
|
||||
foundSomething = true;
|
||||
final String orgName = interpolateString(org.getName(), pomProperties);
|
||||
dependency.getVendorEvidence().addEvidence("pom", "organization name", orgName, Evidence.Confidence.HIGH);
|
||||
}
|
||||
//pom name
|
||||
final String pomName = interpolateString(pom.getName(), pomProperties);
|
||||
if (pomName != null) {
|
||||
foundSomething = true;
|
||||
dependency.getProductEvidence().addEvidence("pom", "name", pomName, Evidence.Confidence.HIGH);
|
||||
dependency.getVendorEvidence().addEvidence("pom", "name", pomName, Evidence.Confidence.HIGH);
|
||||
foundSomething = setPomEvidence(dependency, pom, pomProperties) || foundSomething;
|
||||
}
|
||||
return foundSomething;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a path to a pom.xml within a JarFile, this method attempts to load
|
||||
* a sibling pom.properties if one exists.
|
||||
* @param path the path to the pom.xml within the JarFile
|
||||
* @param jar the JarFile to load the pom.properties from
|
||||
* @return a Properties object or null if no pom.properties was found
|
||||
* @throws IOException thrown if there is an exception reading the pom.properties
|
||||
*/
|
||||
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "OS_OPEN_STREAM",
|
||||
justification = "The reader is closed by closing the zipEntry")
|
||||
private Properties retrievePomProperties(String path, final JarFile jar) throws IOException {
|
||||
Properties pomProperties = null;
|
||||
final String propPath = path.substring(0, path.length() - 7) + "pom.properies";
|
||||
final ZipEntry propEntry = jar.getEntry(propPath);
|
||||
if (propEntry != null) {
|
||||
final Reader reader = new InputStreamReader(jar.getInputStream(propEntry), "UTF-8");
|
||||
pomProperties = new Properties();
|
||||
pomProperties.load(reader);
|
||||
}
|
||||
return pomProperties;
|
||||
}
|
||||
/**
|
||||
* Searches a JarFile for pom.xml entries and returns a listing of these entries.
|
||||
* @param jar the JarFile to search
|
||||
* @return a list of pom.xml entries
|
||||
* @throws IOException thrown if there is an exception reading a JarEntry
|
||||
*/
|
||||
private List<String> retrievePomListing(final JarFile jar) throws IOException {
|
||||
final List<String> pomEntries = new ArrayList<String>();
|
||||
final Enumeration<JarEntry> entries = jar.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
final JarEntry entry = entries.nextElement();
|
||||
final String entryName = (new File(entry.getName())).getName().toLowerCase();
|
||||
if (!entry.isDirectory() && "pom.xml".equals(entryName)) {
|
||||
pomEntries.add(entry.getName());
|
||||
}
|
||||
}
|
||||
return pomEntries;
|
||||
}
|
||||
/**
|
||||
* Retrieves the specified POM from a jar file and converts it to a Model.
|
||||
* @param path the path to the pom.xml file within the jar file
|
||||
* @param jar the jar file to extract the pom from
|
||||
* @return returns a {@link org.owasp.dependencycheck.analyzer.pom.generated.Model} object
|
||||
* @throws JAXBException is thrown if there is an exception parsing the pom
|
||||
* @throws IOException is thrown if there is an exception reading the jar
|
||||
*/
|
||||
private Model retrievePom(String path, JarFile jar) throws JAXBException, IOException {
|
||||
final ZipEntry entry = jar.getEntry(path);
|
||||
if (entry != null) { //should never be null
|
||||
Model m = null;
|
||||
try {
|
||||
final XMLFilter filter = new MavenNamespaceFilter();
|
||||
final SAXParserFactory spf = SAXParserFactory.newInstance();
|
||||
final SAXParser sp = spf.newSAXParser();
|
||||
final XMLReader xr = sp.getXMLReader();
|
||||
filter.setParent(xr);
|
||||
final NonClosingStream stream = new NonClosingStream(jar.getInputStream(entry));
|
||||
final InputStreamReader reader = new InputStreamReader(stream);
|
||||
final InputSource xml = new InputSource(reader);
|
||||
final SAXSource source = new SAXSource(filter, xml);
|
||||
final JAXBElement<Model> el = pomUnmarshaller.unmarshal(source, Model.class);
|
||||
m = el.getValue();
|
||||
} catch (ParserConfigurationException ex) {
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.SEVERE, null, ex);
|
||||
} catch (SAXException ex) {
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.SEVERE, null, ex);
|
||||
} catch (JAXBException ex) {
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.FINEST, "failure reading pom via jaxb path:'"
|
||||
+ path + "' jar:'" + jar.getName() + "'", ex);
|
||||
}
|
||||
|
||||
//Description
|
||||
if (pom.getDescription() != null) {
|
||||
foundSomething = true;
|
||||
final String description = interpolateString(pom.getDescription(), pomProperties);
|
||||
dependency.setDescription(description);
|
||||
dependency.getProductEvidence().addEvidence("pom", "description", description, Evidence.Confidence.MEDIUM);
|
||||
dependency.getVendorEvidence().addEvidence("pom", "description", description, Evidence.Confidence.MEDIUM);
|
||||
return m;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets evidence from the pom on the supplied dependency.
|
||||
* @param dependency the dependency to set data on
|
||||
* @param pom the information from the pom
|
||||
* @param pomProperties the pom properties file (null if none exists)
|
||||
* @return true if there was evidence within the pom that we could use; otherwise false
|
||||
*/
|
||||
private boolean setPomEvidence(Dependency dependency, Model pom, Properties pomProperties) {
|
||||
boolean foundSomething = false;
|
||||
//group id
|
||||
final String groupid = interpolateString(pom.getGroupId(), pomProperties);
|
||||
if (groupid != null) {
|
||||
foundSomething = true;
|
||||
dependency.getVendorEvidence().addEvidence("pom", "groupid", groupid, Evidence.Confidence.HIGH);
|
||||
dependency.getProductEvidence().addEvidence("pom", "groupid", groupid, Evidence.Confidence.LOW);
|
||||
}
|
||||
//artifact id
|
||||
final String artifactid = interpolateString(pom.getArtifactId(), pomProperties);
|
||||
if (artifactid != null) {
|
||||
foundSomething = true;
|
||||
dependency.getProductEvidence().addEvidence("pom", "artifactid", artifactid, Evidence.Confidence.HIGH);
|
||||
dependency.getVendorEvidence().addEvidence("pom", "artifactid", artifactid, Evidence.Confidence.LOW);
|
||||
}
|
||||
//version
|
||||
final String version = interpolateString(pom.getVersion(), pomProperties);
|
||||
if (version != null) {
|
||||
foundSomething = true;
|
||||
dependency.getVersionEvidence().addEvidence("pom", "version", version, Evidence.Confidence.HIGH);
|
||||
}
|
||||
// org name
|
||||
final Organization org = pom.getOrganization();
|
||||
if (org != null && org.getName() != null) {
|
||||
foundSomething = true;
|
||||
final String orgName = interpolateString(org.getName(), pomProperties);
|
||||
dependency.getVendorEvidence().addEvidence("pom", "organization name", orgName, Evidence.Confidence.HIGH);
|
||||
}
|
||||
//pom name
|
||||
final String pomName = interpolateString(pom.getName(), pomProperties);
|
||||
if (pomName != null) {
|
||||
foundSomething = true;
|
||||
dependency.getProductEvidence().addEvidence("pom", "name", pomName, Evidence.Confidence.HIGH);
|
||||
dependency.getVendorEvidence().addEvidence("pom", "name", pomName, Evidence.Confidence.HIGH);
|
||||
}
|
||||
|
||||
//Description
|
||||
if (pom.getDescription() != null) {
|
||||
foundSomething = true;
|
||||
String description = interpolateString(pom.getDescription(), pomProperties);
|
||||
|
||||
if (HTML_DETECTION_PATTERN.matcher(description).find()) {
|
||||
description = Jsoup.parse(description).text();
|
||||
}
|
||||
|
||||
//license
|
||||
if (pom.getLicenses() != null) {
|
||||
String license = null;
|
||||
for (License lic : pom.getLicenses().getLicense()) {
|
||||
String tmp = null;
|
||||
if (lic.getName() != null) {
|
||||
tmp = interpolateString(lic.getName(), pomProperties);
|
||||
}
|
||||
if (lic.getUrl() != null) {
|
||||
if (tmp == null) {
|
||||
tmp = interpolateString(lic.getUrl(), pomProperties);
|
||||
} else {
|
||||
tmp += ": " + interpolateString(lic.getUrl(), pomProperties);
|
||||
}
|
||||
}
|
||||
dependency.setDescription(description);
|
||||
dependency.getProductEvidence().addEvidence("pom", "description", description, Evidence.Confidence.MEDIUM);
|
||||
dependency.getVendorEvidence().addEvidence("pom", "description", description, Evidence.Confidence.MEDIUM);
|
||||
}
|
||||
|
||||
//license
|
||||
if (pom.getLicenses() != null) {
|
||||
String license = null;
|
||||
for (License lic : pom.getLicenses().getLicense()) {
|
||||
String tmp = null;
|
||||
if (lic.getName() != null) {
|
||||
tmp = interpolateString(lic.getName(), pomProperties);
|
||||
}
|
||||
if (lic.getUrl() != null) {
|
||||
if (tmp == null) {
|
||||
continue;
|
||||
}
|
||||
if (license == null) {
|
||||
license = tmp;
|
||||
tmp = interpolateString(lic.getUrl(), pomProperties);
|
||||
} else {
|
||||
license += "\n" + tmp;
|
||||
tmp += ": " + interpolateString(lic.getUrl(), pomProperties);
|
||||
}
|
||||
}
|
||||
if (license != null) {
|
||||
dependency.setLicense(license);
|
||||
if (tmp == null) {
|
||||
continue;
|
||||
}
|
||||
if (HTML_DETECTION_PATTERN.matcher(tmp).find()) {
|
||||
tmp = Jsoup.parse(tmp).text();
|
||||
}
|
||||
if (license == null) {
|
||||
license = tmp;
|
||||
} else {
|
||||
license += "\n" + tmp;
|
||||
}
|
||||
}
|
||||
if (license != null) {
|
||||
dependency.setLicense(license);
|
||||
}
|
||||
}
|
||||
return foundSomething;
|
||||
}
|
||||
/**
|
||||
* Tracks whether the jar being analyzed contains classes.
|
||||
*/
|
||||
private boolean hasClasses = false;
|
||||
|
||||
/**
|
||||
* Analyzes the path information of the classes contained within the
|
||||
@@ -377,7 +462,6 @@ public class JarAnalyzer extends AbstractAnalyzer implements Analyzer {
|
||||
*/
|
||||
protected boolean analyzePackageNames(Dependency dependency, boolean addPackagesAsEvidence)
|
||||
throws IOException {
|
||||
hasClasses = false;
|
||||
JarFile jar = null;
|
||||
try {
|
||||
jar = new JarFile(dependency.getActualFilePath());
|
||||
@@ -389,7 +473,7 @@ public class JarAnalyzer extends AbstractAnalyzer implements Analyzer {
|
||||
final int count = collectPackageNameInformation(en, level0, level1, level2, level3);
|
||||
|
||||
if (count == 0) {
|
||||
return hasClasses;
|
||||
return false;
|
||||
}
|
||||
final EvidenceCollection vendor = dependency.getVendorEvidence();
|
||||
final EvidenceCollection product = dependency.getProductEvidence();
|
||||
@@ -488,7 +572,7 @@ public class JarAnalyzer extends AbstractAnalyzer implements Analyzer {
|
||||
jar.close();
|
||||
}
|
||||
}
|
||||
return hasClasses;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -511,9 +595,15 @@ public class JarAnalyzer extends AbstractAnalyzer implements Analyzer {
|
||||
|
||||
final Manifest manifest = jar.getManifest();
|
||||
if (manifest == null) {
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.SEVERE,
|
||||
String.format("Jar file '%s' does not contain a manifest.",
|
||||
dependency.getFileName()));
|
||||
//don't log this for javadoc or sources jar files
|
||||
if (!dependency.getFileName().toLowerCase().endsWith("-sources.jar")
|
||||
&& !dependency.getFileName().toLowerCase().endsWith("-javadoc.jar")
|
||||
&& !dependency.getFileName().toLowerCase().endsWith("-src.jar")
|
||||
&& !dependency.getFileName().toLowerCase().endsWith("-doc.jar")) {
|
||||
Logger.getLogger(JarAnalyzer.class.getName()).log(Level.SEVERE,
|
||||
String.format("Jar file '%s' does not contain a manifest.",
|
||||
dependency.getFileName()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
final Attributes atts = manifest.getMainAttributes();
|
||||
@@ -526,7 +616,10 @@ public class JarAnalyzer extends AbstractAnalyzer implements Analyzer {
|
||||
|
||||
for (Entry<Object, Object> entry : atts.entrySet()) {
|
||||
String key = entry.getKey().toString();
|
||||
final String value = atts.getValue(key);
|
||||
String value = atts.getValue(key);
|
||||
if (HTML_DETECTION_PATTERN.matcher(value).find()) {
|
||||
value = Jsoup.parse(value).text();
|
||||
}
|
||||
if (key.equals(Attributes.Name.IMPLEMENTATION_TITLE.toString())) {
|
||||
foundSomething = true;
|
||||
productEvidence.addEvidence(source, key, value, Evidence.Confidence.HIGH);
|
||||
@@ -563,6 +656,8 @@ public class JarAnalyzer extends AbstractAnalyzer implements Analyzer {
|
||||
&& !key.endsWith("jdk")
|
||||
&& !key.contains("lastmodified")
|
||||
&& !key.endsWith("package")
|
||||
&& !key.endsWith("classpath")
|
||||
&& !key.endsWith("class-path")
|
||||
&& !isImportPackage(key, value)) {
|
||||
|
||||
foundSomething = true;
|
||||
@@ -715,10 +810,9 @@ public class JarAnalyzer extends AbstractAnalyzer implements Analyzer {
|
||||
HashMap<String, Integer> level1, HashMap<String, Integer> level2, HashMap<String, Integer> level3) {
|
||||
int count = 0;
|
||||
while (en.hasMoreElements()) {
|
||||
final java.util.jar.JarEntry entry = (java.util.jar.JarEntry) en.nextElement();
|
||||
final JarEntry entry = (JarEntry) en.nextElement();
|
||||
if (entry.getName().endsWith(".class")) {
|
||||
hasClasses = true;
|
||||
String[] path = null;
|
||||
String[] path;
|
||||
if (entry.getName().contains("/")) {
|
||||
path = entry.getName().toLowerCase().split("/");
|
||||
if ("java".equals(path[0])
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
/**
|
||||
* <html>
|
||||
* <head>
|
||||
* <title>org.owasp.dependencycheck.scanner</title>
|
||||
* <title>org.owasp.dependencycheck.analyzer</title>
|
||||
* </head>
|
||||
* <body>
|
||||
* The scanner package contains the utilities to scan files and directories for
|
||||
* dependencies. Analyzers are used to inspect the identified dependencies and
|
||||
* collect Evidence. This evidence is then used to determine if the dependency
|
||||
* has a known CPE.
|
||||
* Analyzers are used to inspect the identified dependencies, collect Evidence,
|
||||
* and process the dependencies.
|
||||
* </body>
|
||||
* </html>
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* This file is part of Dependency-Check.
|
||||
*
|
||||
* Dependency-Check is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* Dependency-Check is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* Dependency-Check. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* Copyright (c) 2013 Jeremy Long. All Rights Reserved.
|
||||
*/
|
||||
package org.owasp.dependencycheck.analyzer.pom;
|
||||
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.helpers.XMLFilterImpl;
|
||||
|
||||
/**
|
||||
* This filter is used when parsing POM documents. Some POM documents
|
||||
* do not specify the xmlns="http://maven.apache.org/POM/4.0.0". This
|
||||
* filter ensures that the correct namespace is added so that both
|
||||
* types of POMs can be read.
|
||||
* @author Jeremy Long (jeremy.long@gmail.com)
|
||||
*/
|
||||
public class MavenNamespaceFilter extends XMLFilterImpl {
|
||||
|
||||
/**
|
||||
* The namespace to add for Maven POMs.
|
||||
*/
|
||||
private static final String NAMESPACE = "http://maven.apache.org/POM/4.0.0";
|
||||
/**
|
||||
* A flag indicating whether or not the namespace (prefix) has been added.
|
||||
*/
|
||||
private boolean namespaceAdded = false;
|
||||
|
||||
/**
|
||||
* Called at the start of the document parsing.
|
||||
* @throws SAXException thrown if there is a SAXException
|
||||
*/
|
||||
@Override
|
||||
public void startDocument() throws SAXException {
|
||||
super.startDocument();
|
||||
startPrefixMapping("", NAMESPACE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an element is started.
|
||||
* @param uri the uri
|
||||
* @param localName the localName
|
||||
* @param qName the qualified name
|
||||
* @param atts the attributes
|
||||
* @throws SAXException thrown if there is a SAXException
|
||||
*/
|
||||
@Override
|
||||
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
|
||||
super.startElement(NAMESPACE, localName, qName, atts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicatees the start of the document.
|
||||
* @param uri the uri
|
||||
* @param localName the localName
|
||||
* @param qName the qualified name
|
||||
* @throws SAXException thrown if there is a SAXException
|
||||
*/
|
||||
@Override
|
||||
public void endElement(String uri, String localName, String qName)
|
||||
throws SAXException {
|
||||
super.endElement(NAMESPACE, localName, qName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when prefix mapping is started.
|
||||
* @param prefix the prefix
|
||||
* @param url the url
|
||||
* @throws SAXException thrown if there is a SAXException
|
||||
*/
|
||||
@Override
|
||||
public void startPrefixMapping(String prefix, String url) throws SAXException {
|
||||
if (!this.namespaceAdded) {
|
||||
namespaceAdded = true;
|
||||
super.startPrefixMapping("", NAMESPACE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* <html>
|
||||
* <head>
|
||||
* <title>org.owasp.dependencycheck.analyzer.pom</title>
|
||||
* </head>
|
||||
* <body>
|
||||
* This package contains utility classes used to parse pom.xml files.
|
||||
* </body>
|
||||
* </html>
|
||||
*/
|
||||
|
||||
package org.owasp.dependencycheck.analyzer.pom;
|
||||
@@ -228,10 +228,7 @@ public class Entry implements Serializable {
|
||||
return false;
|
||||
}
|
||||
final Entry other = (Entry) obj;
|
||||
if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return !((this.name == null) ? (other.name != null) : !this.name.equals(other.name));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.owasp.dependencycheck.data.cpe;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.apache.lucene.analysis.Analyzer;
|
||||
@@ -37,6 +36,7 @@ import org.apache.lucene.store.Directory;
|
||||
import org.apache.lucene.store.FSDirectory;
|
||||
import org.apache.lucene.util.Version;
|
||||
import org.owasp.dependencycheck.data.lucene.AbstractIndex;
|
||||
import org.owasp.dependencycheck.utils.FileUtils;
|
||||
import org.owasp.dependencycheck.utils.Settings;
|
||||
import org.owasp.dependencycheck.data.lucene.FieldAnalyzer;
|
||||
import org.owasp.dependencycheck.data.lucene.SearchFieldAnalyzer;
|
||||
@@ -58,8 +58,7 @@ public class Index extends AbstractIndex {
|
||||
*/
|
||||
public Directory getDirectory() throws IOException {
|
||||
final File path = getDataDirectory();
|
||||
final Directory dir = FSDirectory.open(path);
|
||||
return dir;
|
||||
return FSDirectory.open(path);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,20 +70,9 @@ public class Index extends AbstractIndex {
|
||||
*/
|
||||
public File getDataDirectory() throws IOException {
|
||||
final String fileName = Settings.getString(Settings.KEYS.CPE_INDEX);
|
||||
final String filePath = Index.class.getProtectionDomain().getCodeSource().getLocation().getPath();
|
||||
final String decodedPath = URLDecoder.decode(filePath, "UTF-8");
|
||||
File exePath = new File(decodedPath);
|
||||
if (exePath.getName().toLowerCase().endsWith(".jar")) {
|
||||
exePath = exePath.getParentFile();
|
||||
} else {
|
||||
exePath = new File(".");
|
||||
}
|
||||
File path = new File(exePath.getCanonicalFile() + File.separator + fileName);
|
||||
path = new File(path.getCanonicalPath());
|
||||
if (!path.exists()) {
|
||||
if (!path.mkdirs()) {
|
||||
throw new IOException("Unable to create CPE Data directory");
|
||||
}
|
||||
final File path = FileUtils.getDataDirectory(fileName, Index.class);
|
||||
if (!path.exists() && !path.mkdirs()) {
|
||||
throw new IOException("Unable to create CPE Data directory");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
@@ -102,10 +90,7 @@ public class Index extends AbstractIndex {
|
||||
fieldAnalyzers.put(Fields.VERSION, new VersionAnalyzer(Version.LUCENE_40));
|
||||
fieldAnalyzers.put(Fields.NAME, new KeywordAnalyzer());
|
||||
|
||||
final PerFieldAnalyzerWrapper wrapper = new PerFieldAnalyzerWrapper(
|
||||
new FieldAnalyzer(Version.LUCENE_40), fieldAnalyzers);
|
||||
|
||||
return wrapper;
|
||||
return new PerFieldAnalyzerWrapper(new FieldAnalyzer(Version.LUCENE_40), fieldAnalyzers);
|
||||
}
|
||||
/**
|
||||
* The search field analyzer for the product field.
|
||||
@@ -133,10 +118,7 @@ public class Index extends AbstractIndex {
|
||||
fieldAnalyzers.put(Fields.PRODUCT, productSearchFieldAnalyzer);
|
||||
fieldAnalyzers.put(Fields.VENDOR, vendorSearchFieldAnalyzer);
|
||||
|
||||
final PerFieldAnalyzerWrapper wrapper = new PerFieldAnalyzerWrapper(
|
||||
new FieldAnalyzer(Version.LUCENE_40), fieldAnalyzers);
|
||||
|
||||
return wrapper;
|
||||
return new PerFieldAnalyzerWrapper(new FieldAnalyzer(Version.LUCENE_40), fieldAnalyzers);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,7 +151,6 @@ public class Index extends AbstractIndex {
|
||||
*/
|
||||
public void saveEntry(Entry entry) throws CorruptIndexException, IOException {
|
||||
final Document doc = convertEntryToDoc(entry);
|
||||
//Term term = new Term(Fields.NVDID, LuceneUtils.escapeLuceneQuery(entry.getNvdId()));
|
||||
final Term term = new Term(Fields.NAME, entry.getName());
|
||||
getIndexWriter().updateDocument(term, doc);
|
||||
}
|
||||
@@ -196,7 +177,7 @@ public class Index extends AbstractIndex {
|
||||
|
||||
//TODO revision should likely be its own field
|
||||
if (entry.getVersion() != null) {
|
||||
Field version = null;
|
||||
Field version;
|
||||
if (entry.getRevision() != null) {
|
||||
version = new TextField(Fields.VERSION, entry.getVersion() + " "
|
||||
+ entry.getRevision(), Field.Store.NO);
|
||||
|
||||
@@ -53,9 +53,7 @@ public final class CweDB {
|
||||
final String filePath = "data/cwe.hashmap.serialized";
|
||||
final InputStream input = CweDB.class.getClassLoader().getResourceAsStream(filePath);
|
||||
oin = new ObjectInputStream(input);
|
||||
@SuppressWarnings("unchecked")
|
||||
final HashMap<String, String> data = (HashMap<String, String>) oin.readObject();
|
||||
return data;
|
||||
return (HashMap<String, String>) oin.readObject();
|
||||
} catch (ClassNotFoundException ex) {
|
||||
Logger.getLogger(CweDB.class.getName()).log(Level.SEVERE, null, ex);
|
||||
} catch (IOException ex) {
|
||||
|
||||
@@ -33,7 +33,7 @@ public class CweHandler extends DefaultHandler {
|
||||
/**
|
||||
* a HashMap containing the CWE data.
|
||||
*/
|
||||
private HashMap<String, String> cwe = new HashMap<String, String>();
|
||||
private final HashMap<String, String> cwe = new HashMap<String, String>();
|
||||
|
||||
/**
|
||||
* Returns the HashMap of CWE entries (CWE-ID, Full CWE Name).
|
||||
|
||||
@@ -250,14 +250,11 @@ public abstract class AbstractIndex {
|
||||
* @throws IOException is thrown if there is an issue with the underlying Index
|
||||
*/
|
||||
public TopDocs search(String searchString, int maxQueryResults) throws ParseException, IOException {
|
||||
|
||||
final QueryParser parser = getQueryParser();
|
||||
final Query query = parser.parse(searchString);
|
||||
resetSearchingAnalyzer();
|
||||
final IndexSearcher is = getIndexSearcher();
|
||||
final TopDocs docs = is.search(query, maxQueryResults);
|
||||
|
||||
return docs;
|
||||
return is.search(query, maxQueryResults);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,7 +41,7 @@ public class FieldAnalyzer extends Analyzer {
|
||||
/**
|
||||
* The Lucene Version used.
|
||||
*/
|
||||
private Version version;
|
||||
private final Version version;
|
||||
|
||||
/**
|
||||
* Creates a new FieldAnalyzer.
|
||||
|
||||
@@ -40,6 +40,9 @@ public final class LuceneUtils {
|
||||
* @param text the data to be escaped
|
||||
*/
|
||||
@SuppressWarnings("fallthrough")
|
||||
@edu.umd.cs.findbugs.annotations.SuppressWarnings(
|
||||
value = "SF_SWITCH_NO_DEFAULT",
|
||||
justification = "The switch below does have a default.")
|
||||
public static void appendEscapedLuceneQuery(StringBuilder buf,
|
||||
final CharSequence text) {
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ public class SearchFieldAnalyzer extends Analyzer {
|
||||
/**
|
||||
* The Lucene Version used.
|
||||
*/
|
||||
private Version version;
|
||||
private final Version version;
|
||||
/**
|
||||
* A local reference to the TokenPairConcatenatingFilter so that we
|
||||
* can clear any left over state if this analyzer is re-used.
|
||||
|
||||
@@ -42,7 +42,7 @@ public class SearchVersionAnalyzer extends Analyzer {
|
||||
/**
|
||||
* The Lucene Version used.
|
||||
*/
|
||||
private Version version;
|
||||
private final Version version;
|
||||
|
||||
/**
|
||||
* Creates a new SearchVersionAnalyzer.
|
||||
|
||||
@@ -50,7 +50,7 @@ public final class TokenPairConcatenatingFilter extends TokenFilter {
|
||||
/**
|
||||
* A list of words parsed.
|
||||
*/
|
||||
private LinkedList<String> words;
|
||||
private final LinkedList<String> words;
|
||||
|
||||
/**
|
||||
* Constructs a new TokenPairConcatenatingFilter.
|
||||
|
||||
@@ -42,7 +42,7 @@ public class VersionAnalyzer extends Analyzer {
|
||||
/**
|
||||
* The Lucene Version used.
|
||||
*/
|
||||
private Version version;
|
||||
private final Version version;
|
||||
|
||||
/**
|
||||
* Creates a new VersionAnalyzer.
|
||||
|
||||
@@ -41,7 +41,7 @@ public final class VersionTokenizingFilter extends TokenFilter {
|
||||
/**
|
||||
* A collection of tokens to add to the stream.
|
||||
*/
|
||||
private LinkedList<String> tokens;
|
||||
private final LinkedList<String> tokens;
|
||||
|
||||
/**
|
||||
* Constructs a new VersionTokenizingFilter.
|
||||
|
||||
@@ -21,7 +21,6 @@ package org.owasp.dependencycheck.data.nvdcve;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
@@ -37,6 +36,7 @@ import org.owasp.dependencycheck.data.cwe.CweDB;
|
||||
import org.owasp.dependencycheck.dependency.Reference;
|
||||
import org.owasp.dependencycheck.dependency.Vulnerability;
|
||||
import org.owasp.dependencycheck.dependency.VulnerableSoftware;
|
||||
import org.owasp.dependencycheck.utils.FileUtils;
|
||||
import org.owasp.dependencycheck.utils.Settings;
|
||||
|
||||
/**
|
||||
@@ -181,14 +181,19 @@ public class CveDB {
|
||||
* @throws IOException thrown if there is an IO Exception
|
||||
* @throws SQLException thrown if there is a SQL Exception
|
||||
* @throws DatabaseException thrown if there is an error initializing a new database
|
||||
* @throws ClassNotFoundException thrown if the h2 database driver cannot be loaded
|
||||
*/
|
||||
public void open() throws IOException, SQLException, DatabaseException {
|
||||
@edu.umd.cs.findbugs.annotations.SuppressWarnings(
|
||||
value = "DMI_EMPTY_DB_PASSWORD",
|
||||
justification = "Yes, I know... Blank password.")
|
||||
public void open() throws IOException, SQLException, DatabaseException, ClassNotFoundException {
|
||||
final String fileName = CveDB.getDataDirectory().getCanonicalPath()
|
||||
+ File.separator
|
||||
+ "cve";
|
||||
final File f = new File(fileName);
|
||||
final boolean createTables = !f.exists();
|
||||
final String connStr = "jdbc:h2:file:" + fileName;
|
||||
Class.forName("org.h2.Driver");
|
||||
conn = DriverManager.getConnection(connStr, "sa", "");
|
||||
if (createTables) {
|
||||
createTables();
|
||||
@@ -222,9 +227,9 @@ public class CveDB {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the vulnerabilities associated with the specified CPE cpe.
|
||||
* Retrieves the vulnerabilities associated with the specified CPE.
|
||||
*
|
||||
* @param cpeStr the CPE cpe name
|
||||
* @param cpeStr the CPE name
|
||||
* @return a list of Vulnerabilities
|
||||
* @throws DatabaseException thrown if there is an exception retrieving data
|
||||
*/
|
||||
@@ -305,11 +310,11 @@ public class CveDB {
|
||||
rsS = selectSoftware.executeQuery();
|
||||
while (rsS.next()) {
|
||||
final String cpe = rsS.getString(1);
|
||||
final String prevVers = rsS.getString(2);
|
||||
if (prevVers == null) {
|
||||
final String prevVersion = rsS.getString(2);
|
||||
if (prevVersion == null) {
|
||||
vuln.addVulnerableSoftware(cpe);
|
||||
} else {
|
||||
vuln.addVulnerableSoftware(cpe, prevVers);
|
||||
vuln.addVulnerableSoftware(cpe, prevVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,18 +412,7 @@ public class CveDB {
|
||||
*/
|
||||
public static File getDataDirectory() throws IOException {
|
||||
final String fileName = Settings.getString(Settings.KEYS.CVE_INDEX);
|
||||
final String filePath = CveDB.class.getProtectionDomain().getCodeSource().getLocation().getPath();
|
||||
final String decodedPath = URLDecoder.decode(filePath, "UTF-8");
|
||||
File exePath = new File(decodedPath);
|
||||
|
||||
if (exePath.getName().toLowerCase().endsWith(".jar")) {
|
||||
exePath = exePath.getParentFile();
|
||||
} else {
|
||||
exePath = new File(".");
|
||||
}
|
||||
File path = new File(exePath.getCanonicalFile() + File.separator + fileName);
|
||||
path = new File(path.getCanonicalPath());
|
||||
|
||||
final File path = FileUtils.getDataDirectory(fileName, CveDB.class);
|
||||
if (!path.exists()) {
|
||||
if (!path.mkdirs()) {
|
||||
throw new IOException("Unable to create NVD CVE Data directory");
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.owasp.dependencycheck.dependency.Dependency;
|
||||
import org.owasp.dependencycheck.dependency.Vulnerability;
|
||||
import org.owasp.dependencycheck.dependency.Identifier;
|
||||
import org.owasp.dependencycheck.analyzer.Analyzer;
|
||||
import org.owasp.dependencycheck.dependency.VulnerableSoftware;
|
||||
/**
|
||||
* NvdCveAnalyzer is a utility class that takes a project dependency and
|
||||
* attempts to discern if there is an associated CVEs. It uses the the
|
||||
@@ -53,8 +54,9 @@ public class NvdCveAnalyzer implements Analyzer {
|
||||
* @throws SQLException thrown when there is a SQL Exception
|
||||
* @throws IOException thrown when there is an IO Exception
|
||||
* @throws DatabaseException thrown when there is a database exceptions
|
||||
* @throws ClassNotFoundException thrown if the h2 database driver cannot be loaded
|
||||
*/
|
||||
public void open() throws SQLException, IOException, DatabaseException {
|
||||
public void open() throws SQLException, IOException, DatabaseException, ClassNotFoundException {
|
||||
cveDB = new CveDB();
|
||||
cveDB.open();
|
||||
}
|
||||
@@ -105,7 +107,9 @@ public class NvdCveAnalyzer implements Analyzer {
|
||||
final String value = id.getValue();
|
||||
final List<Vulnerability> vulns = cveDB.getVulnerabilities(value);
|
||||
for (Vulnerability v : vulns) {
|
||||
dependency.addVulnerability(v);
|
||||
if (isValidMatch(dependency, v)) {
|
||||
dependency.addVulnerability(v);
|
||||
}
|
||||
}
|
||||
} catch (DatabaseException ex) {
|
||||
throw new AnalysisException(ex);
|
||||
@@ -159,4 +163,52 @@ public class NvdCveAnalyzer implements Analyzer {
|
||||
public void initialize() throws Exception {
|
||||
this.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Determines if this is a valid vulnerability match for the given dependency.
|
||||
* Specifically, this is concerned with ensuring the version numbers are correct.</p>
|
||||
* <p>Currently, this is focused on the issues with the versions for Struts 1 and Struts 2.
|
||||
* In the future this will due better matching on more version numbers.</p>
|
||||
* @param dependency the dependency
|
||||
* @param v the vulnerability
|
||||
* @return returns true if the vulnerability is for the given dependency
|
||||
*/
|
||||
private boolean isValidMatch(final Dependency dependency, final Vulnerability v) {
|
||||
//right now I only know of the issue with Struts1/2
|
||||
// start with fixing this problem.
|
||||
|
||||
//TODO extend this solution to do better version matching for the vulnerable software.
|
||||
boolean struts1 = false;
|
||||
boolean struts2 = false;
|
||||
for (Identifier i : dependency.getIdentifiers()) {
|
||||
if (i.getValue().startsWith("cpe:/a:apache:struts:")) {
|
||||
final char version = i.getValue().charAt(21);
|
||||
if (version == '1') {
|
||||
struts1 = true;
|
||||
}
|
||||
if (version == '2') {
|
||||
struts2 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!struts1 && !struts2) {
|
||||
return true; //we are not looking at struts, so return true.
|
||||
}
|
||||
if (struts1 && struts2) {
|
||||
return true; //there is a mismatch here, but we can't solve it here so we return valid.
|
||||
}
|
||||
if (struts1) {
|
||||
boolean hasStruts1Vuln = false;
|
||||
boolean hasStruts2PreviousVersion = false;
|
||||
for (VulnerableSoftware vs : v.getVulnerableSoftware()) {
|
||||
hasStruts2PreviousVersion |= vs.hasPreviousVersion() && vs.getName().charAt(21) == '2';
|
||||
hasStruts1Vuln |= vs.getName().charAt(21) == '1';
|
||||
}
|
||||
if (!hasStruts1Vuln && hasStruts2PreviousVersion) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,16 +137,29 @@ public class DatabaseUpdater implements CachedWebDataSource {
|
||||
throw new UpdateException(ex);
|
||||
} catch (DatabaseException ex) {
|
||||
throw new UpdateException(ex);
|
||||
} catch (ClassNotFoundException ex) {
|
||||
throw new UpdateException(ex);
|
||||
} finally {
|
||||
boolean deleted = false;
|
||||
try {
|
||||
if (outputPath != null && outputPath.exists()) {
|
||||
outputPath.delete();
|
||||
deleted = outputPath.delete();
|
||||
}
|
||||
} finally {
|
||||
if (outputPath != null && outputPath.exists()) {
|
||||
if (outputPath != null && (outputPath.exists() || !deleted)) {
|
||||
outputPath.deleteOnExit();
|
||||
}
|
||||
}
|
||||
try {
|
||||
deleted = false;
|
||||
if (outputPath12 != null && outputPath12.exists()) {
|
||||
deleted = outputPath12.delete();
|
||||
}
|
||||
} finally {
|
||||
if (outputPath12 != null && (outputPath12.exists() || !deleted)) {
|
||||
outputPath12.deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,13 +179,14 @@ public class DatabaseUpdater implements CachedWebDataSource {
|
||||
* @param file the file containing the NVD CVE XML
|
||||
* @param oldVersion contains the file containing the NVD CVE XML 1.2
|
||||
* @throws ParserConfigurationException is thrown if there is a parser configuration exception
|
||||
* @throws SAXException is thrown if there is a saxexception
|
||||
* @throws SAXException is thrown if there is a SAXException
|
||||
* @throws IOException is thrown if there is a ioexception
|
||||
* @throws SQLException is thrown if there is a sql exception
|
||||
* @throws DatabaseException is thrown if there is a database exception
|
||||
* @throws ClassNotFoundException thrown if the h2 database driver cannot be loaded
|
||||
*/
|
||||
private void importXML(File file, File oldVersion)
|
||||
throws ParserConfigurationException, SAXException, IOException, SQLException, DatabaseException {
|
||||
throws ParserConfigurationException, SAXException, IOException, SQLException, DatabaseException, ClassNotFoundException {
|
||||
CveDB cveDB = null;
|
||||
Index cpeIndex = null;
|
||||
|
||||
@@ -255,7 +269,14 @@ public class DatabaseUpdater implements CachedWebDataSource {
|
||||
try {
|
||||
out.close();
|
||||
} catch (IOException ex) {
|
||||
Logger.getLogger(DatabaseUpdater.class.getName()).log(Level.SEVERE, null, ex);
|
||||
Logger.getLogger(DatabaseUpdater.class.getName()).log(Level.FINEST, null, ex);
|
||||
}
|
||||
}
|
||||
if (os != null) {
|
||||
try {
|
||||
os.close();
|
||||
} catch (IOException ex) {
|
||||
Logger.getLogger(DatabaseUpdater.class.getName()).log(Level.FINEST, null, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,7 +332,7 @@ public class DatabaseUpdater implements CachedWebDataSource {
|
||||
prop.load(is);
|
||||
|
||||
boolean deleteAndRecreate = false;
|
||||
float version = 0;
|
||||
float version;
|
||||
|
||||
if (prop.getProperty("version") == null) {
|
||||
deleteAndRecreate = true;
|
||||
@@ -333,8 +354,8 @@ public class DatabaseUpdater implements CachedWebDataSource {
|
||||
FileUtils.delete(f);
|
||||
|
||||
//this importer also updates the CPE index and it is also using an old version
|
||||
final Index cpeid = new Index();
|
||||
final File cpeDir = cpeid.getDataDirectory();
|
||||
final Index cpeId = new Index();
|
||||
final File cpeDir = cpeId.getDataDirectory();
|
||||
FileUtils.delete(cpeDir);
|
||||
return currentlyPublished;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public class NvdCve12Handler extends DefaultHandler {
|
||||
/**
|
||||
* The current element.
|
||||
*/
|
||||
private Element current = new Element();
|
||||
private final Element current = new Element();
|
||||
/**
|
||||
* a map of vulnerabilities.
|
||||
*/
|
||||
|
||||
@@ -49,7 +49,7 @@ public class NvdCve20Handler extends DefaultHandler {
|
||||
/**
|
||||
* the current element.
|
||||
*/
|
||||
private Element current = new Element();
|
||||
private final Element current = new Element();
|
||||
/**
|
||||
* the text of the node.
|
||||
*/
|
||||
|
||||
@@ -72,15 +72,15 @@ public class Dependency implements Comparable<Dependency> {
|
||||
/**
|
||||
* A collection of vendor evidence.
|
||||
*/
|
||||
private EvidenceCollection vendorEvidence;
|
||||
private final EvidenceCollection vendorEvidence;
|
||||
/**
|
||||
* A collection of product evidence.
|
||||
*/
|
||||
private EvidenceCollection productEvidence;
|
||||
private final EvidenceCollection productEvidence;
|
||||
/**
|
||||
* A collection of version evidence.
|
||||
*/
|
||||
private EvidenceCollection versionEvidence;
|
||||
private final EvidenceCollection versionEvidence;
|
||||
|
||||
/**
|
||||
* Constructs a new Dependency object.
|
||||
@@ -379,8 +379,8 @@ public class Dependency implements Comparable<Dependency> {
|
||||
if (str == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (vendorEvidence.containsUsedString(str)) {
|
||||
return versionEvidence.containsUsedString(str) || productEvidence.containsUsedString(str) || vendorEvidence.containsUsedString(str);
|
||||
/*if (vendorEvidence.containsUsedString(str)) {
|
||||
return true;
|
||||
}
|
||||
if (productEvidence.containsUsedString(str)) {
|
||||
@@ -390,6 +390,7 @@ public class Dependency implements Comparable<Dependency> {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
*/
|
||||
}
|
||||
/**
|
||||
* A list of vulnerabilities for this dependency.
|
||||
|
||||
@@ -80,7 +80,7 @@ public class EvidenceCollection implements Iterable<Evidence> {
|
||||
*
|
||||
* @param confidence the confidence level for the evidence to be iterated
|
||||
* over.
|
||||
* @return Iterable<Evidence>.
|
||||
* @return Iterable<Evidence> an iterable collectoin of evidence
|
||||
*/
|
||||
public final Iterable<Evidence> iterator(Evidence.Confidence confidence) {
|
||||
if (confidence == Evidence.Confidence.HIGH) {
|
||||
@@ -94,11 +94,11 @@ public class EvidenceCollection implements Iterable<Evidence> {
|
||||
/**
|
||||
* A collection of evidence.
|
||||
*/
|
||||
private Set<Evidence> list;
|
||||
private final Set<Evidence> list;
|
||||
/**
|
||||
* A collection of strings used to adjust Lucene's term weighting.
|
||||
*/
|
||||
private Set<String> weightedStrings;
|
||||
private final Set<String> weightedStrings;
|
||||
|
||||
/**
|
||||
* Creates a new EvidenceCollection.
|
||||
|
||||
@@ -69,11 +69,11 @@ public class ReportGenerator {
|
||||
/**
|
||||
* The Velocity Engine.
|
||||
*/
|
||||
private VelocityEngine engine;
|
||||
private final VelocityEngine engine;
|
||||
/**
|
||||
* The Velocity Engine Context.
|
||||
*/
|
||||
private Context context;
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Constructs a new ReportGenerator.
|
||||
@@ -100,6 +100,7 @@ public class ReportGenerator {
|
||||
*/
|
||||
private VelocityEngine createVelocityEngine() {
|
||||
final VelocityEngine ve = new VelocityEngine();
|
||||
ve.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, VelocityLoggerRedirect.class.getName());
|
||||
ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
|
||||
ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
|
||||
return ve;
|
||||
@@ -110,6 +111,8 @@ public class ReportGenerator {
|
||||
*
|
||||
* @return a Velocity Context.
|
||||
*/
|
||||
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RV_RETURN_VALUE_IGNORED_INFERRED",
|
||||
justification = "No plan to fix this style issue")
|
||||
private Context createContext() {
|
||||
final ToolManager manager = new ToolManager();
|
||||
final Context c = manager.createContext();
|
||||
@@ -193,14 +196,16 @@ public class ReportGenerator {
|
||||
OutputStream outputStream = null;
|
||||
|
||||
try {
|
||||
File foutDir = new File(outFileName).getParentFile();
|
||||
if (!foutDir.exists()) {
|
||||
foutDir.mkdirs();
|
||||
final File outDir = new File(outFileName).getParentFile();
|
||||
if (!outDir.exists()) {
|
||||
final boolean created = outDir.mkdirs();
|
||||
if (!created) {
|
||||
throw new Exception("Unable to create directory '" + outDir.getAbsolutePath() + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
outputStream = new FileOutputStream(outFileName);
|
||||
writer = new OutputStreamWriter(outputStream, "UTF-8");
|
||||
//writer = new BufferedWriter(oswriter);
|
||||
|
||||
if (!engine.evaluate(context, writer, templatePath, reader)) {
|
||||
throw new Exception("Failed to convert the template into html.");
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* This file is part of Dependency-Check.
|
||||
*
|
||||
* Dependency-Check is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* Dependency-Check is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* Dependency-Check. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* Copyright (c) 2013 Steve Springett. All Rights Reserved.
|
||||
*/
|
||||
package org.owasp.dependencycheck.reporting;
|
||||
|
||||
import org.apache.velocity.app.Velocity;
|
||||
import org.apache.velocity.runtime.RuntimeServices;
|
||||
import org.apache.velocity.runtime.log.LogChute;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* <p>DependencyCheck uses {@link java.util.logging.Logger} as a logging framework,
|
||||
* and Apache Velocity uses a custom logging implementation that outputs to a
|
||||
* file named velocity.log by default. This class is an implementation of a
|
||||
* custom Velocity logger that redirects all velocity logging to the Java Logger
|
||||
* class.
|
||||
* </p><p>
|
||||
* This class was written to address permission issues when using Dependency-Check
|
||||
* in a server environment (such as the Jenkins plugin). In some circumstances,
|
||||
* Velocity would attempt to create velocity.log in an un-writable directory.</p>
|
||||
*
|
||||
* @author Steve Springett (steve.springett@owasp.org)
|
||||
*/
|
||||
public class VelocityLoggerRedirect implements LogChute {
|
||||
|
||||
/**
|
||||
* This will be invoked once by the LogManager.
|
||||
* @param rsvc the RuntimeServices
|
||||
*/
|
||||
public void init(RuntimeServices rsvc) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a Velocity log level and message, this method will
|
||||
* call the appropriate Logger level and log the specified values.
|
||||
* @param level the logging level
|
||||
* @param message the message to be logged
|
||||
*/
|
||||
public void log(int level, String message) {
|
||||
Logger.getLogger(Velocity.class.getName()).log(getLevel(level), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a Velocity log level, message and Throwable, this method will
|
||||
* call the appropriate Logger level and log the specified values.
|
||||
* @param level the logging level
|
||||
* @param message the message to be logged
|
||||
* @param t a throwable to log
|
||||
*/
|
||||
public void log(int level, String message, Throwable t) {
|
||||
Logger.getLogger(Velocity.class.getName()).log(getLevel(level), message, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will always return true. The property file will decide what level to log.
|
||||
* @param level the logging level
|
||||
* @return true
|
||||
*/
|
||||
public boolean isLevelEnabled(int level) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps Velocity log levels to {@link Logger} values.
|
||||
* @param velocityLevel the logging level
|
||||
* @return the logging level
|
||||
*/
|
||||
private Level getLevel(int velocityLevel) {
|
||||
switch (velocityLevel) {
|
||||
case TRACE_ID:
|
||||
return Level.ALL;
|
||||
case DEBUG_ID:
|
||||
return Level.FINE;
|
||||
case INFO_ID:
|
||||
return Level.INFO;
|
||||
case WARN_ID:
|
||||
return Level.WARNING;
|
||||
case ERROR_ID:
|
||||
return Level.SEVERE;
|
||||
default:
|
||||
return Level.INFO;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public final class CliParser {
|
||||
/**
|
||||
* The options for the command line parser.
|
||||
*/
|
||||
private Options options = createCommandLineOptions();
|
||||
private final Options options = createCommandLineOptions();
|
||||
/**
|
||||
* Indicates whether the arguments are valid.
|
||||
*/
|
||||
@@ -75,8 +75,7 @@ public final class CliParser {
|
||||
*/
|
||||
private CommandLine parseArgs(String[] args) throws ParseException {
|
||||
final CommandLineParser parser = new PosixParser();
|
||||
final CommandLine ln = parser.parse(options, args);
|
||||
return ln;
|
||||
return parser.parse(options, args);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,7 +101,7 @@ public final class CliParser {
|
||||
+ "the 'out' argument.");
|
||||
}
|
||||
}
|
||||
if (!line.hasOption(ArgumentName.APPNAME)) {
|
||||
if (!line.hasOption(ArgumentName.APP_NAME)) {
|
||||
throw new ParseException("Scan cannot be run without specifying an application "
|
||||
+ "name via the 'app' argument.");
|
||||
}
|
||||
@@ -166,12 +165,12 @@ public final class CliParser {
|
||||
final Option version = new Option(ArgumentName.VERSION_SHORT, ArgumentName.VERSION,
|
||||
false, "print the version information.");
|
||||
|
||||
final Option noupdate = new Option(ArgumentName.DISABLE_AUTO_UPDATE_SHORT, ArgumentName.DISABLE_AUTO_UPDATE,
|
||||
final Option noUpdate = new Option(ArgumentName.DISABLE_AUTO_UPDATE_SHORT, ArgumentName.DISABLE_AUTO_UPDATE,
|
||||
false, "disables the automatic updating of the CPE data.");
|
||||
|
||||
final Option appname = OptionBuilder.withArgName("name").hasArg().withLongOpt(ArgumentName.APPNAME)
|
||||
final Option appName = OptionBuilder.withArgName("name").hasArg().withLongOpt(ArgumentName.APP_NAME)
|
||||
.withDescription("the name of the application being scanned.")
|
||||
.create(ArgumentName.APPNAME_SHORT);
|
||||
.create(ArgumentName.APP_NAME_SHORT);
|
||||
|
||||
final Option connectionTimeout = OptionBuilder.withArgName("timeout").hasArg().withLongOpt(ArgumentName.CONNECTION_TIMEOUT)
|
||||
.withDescription("the connection timeout (in milliseconds) to use when downloading resources.")
|
||||
@@ -197,7 +196,7 @@ public final class CliParser {
|
||||
.withDescription("the folder to write reports to.")
|
||||
.create(ArgumentName.OUT_SHORT);
|
||||
|
||||
final Option outputformat = OptionBuilder.withArgName("format").hasArg().withLongOpt(ArgumentName.OUTPUT_FORMAT)
|
||||
final Option outputFormat = OptionBuilder.withArgName("format").hasArg().withLongOpt(ArgumentName.OUTPUT_FORMAT)
|
||||
.withDescription("the output format to write to (XML, HTML, ALL).")
|
||||
.create(ArgumentName.OUTPUT_FORMAT_SHORT);
|
||||
|
||||
@@ -207,11 +206,11 @@ public final class CliParser {
|
||||
final Options opts = new Options();
|
||||
opts.addOptionGroup(og);
|
||||
opts.addOption(out);
|
||||
opts.addOption(outputformat);
|
||||
opts.addOption(appname);
|
||||
opts.addOption(outputFormat);
|
||||
opts.addOption(appName);
|
||||
opts.addOption(version);
|
||||
opts.addOption(help);
|
||||
opts.addOption(noupdate);
|
||||
opts.addOption(noUpdate);
|
||||
opts.addOption(deepScan);
|
||||
opts.addOption(props);
|
||||
opts.addOption(proxyPort);
|
||||
@@ -301,7 +300,7 @@ public final class CliParser {
|
||||
* @return the application name.
|
||||
*/
|
||||
public String getApplicationName() {
|
||||
return line.getOptionValue(ArgumentName.APPNAME);
|
||||
return line.getOptionValue(ArgumentName.APP_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -405,12 +404,12 @@ public final class CliParser {
|
||||
* The long CLI argument name specifying the name of the application to
|
||||
* be scanned.
|
||||
*/
|
||||
public static final String APPNAME = "app";
|
||||
public static final String APP_NAME = "app";
|
||||
/**
|
||||
* The short CLI argument name specifying the name of the application to
|
||||
* be scanned.
|
||||
*/
|
||||
public static final String APPNAME_SHORT = "a";
|
||||
public static final String APP_NAME_SHORT = "a";
|
||||
/**
|
||||
* The long CLI argument name asking for help.
|
||||
*/
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.apache.commons.lang.StringUtils;
|
||||
* versionParts[2] = 3;
|
||||
* </code></p>
|
||||
* <p>Note, the parser contained in this class expects the version numbers to be
|
||||
* separated by periods. If a different seperator is used the parser will likely
|
||||
* separated by periods. If a different separator is used the parser will likely
|
||||
* fail.</p>
|
||||
* @author Jeremy Long (jeremy.long@owasp.org)
|
||||
*/
|
||||
@@ -111,4 +111,35 @@ public class DependencyVersion implements Iterable {
|
||||
public String toString() {
|
||||
return StringUtils.join(versionParts.toArray(), ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the equality of this object to the one passed in as a parameter.
|
||||
* @param obj the object to compare equality
|
||||
* @return returns true only if the two objects are equal, otherwise false
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
final DependencyVersion other = (DependencyVersion) obj;
|
||||
if (this.versionParts != other.versionParts && (this.versionParts == null || !this.versionParts.equals(other.versionParts))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the hashCode for this object.
|
||||
* @return the hashCode
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 5;
|
||||
hash = 71 * hash + (this.versionParts != null ? this.versionParts.hashCode() : 0);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ public final class Downloader {
|
||||
|
||||
writer = new BufferedOutputStream(new FileOutputStream(outputPath));
|
||||
final byte[] buffer = new byte[4096];
|
||||
int bytesRead = 0;
|
||||
int bytesRead;
|
||||
while ((bytesRead = reader.read(buffer)) > 0) {
|
||||
writer.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ package org.owasp.dependencycheck.utils;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
|
||||
/**
|
||||
* A collection of utilities for processing information about files.
|
||||
@@ -67,4 +68,34 @@ public final class FileUtils {
|
||||
throw new FileNotFoundException("Failed to delete file: " + file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data directory. If a path was specified in dependencycheck.properties
|
||||
* or was specified using the Settings object, and the path exists, that path will be
|
||||
* returned as a File object. If it does not exist, then a File object will be created
|
||||
* based on the file location of the JAR containing the specified class.
|
||||
*
|
||||
* @param configuredFilePath the configured relative or absolute path
|
||||
* @param clazz the class whos path will be resolved
|
||||
* @return a File object
|
||||
* @throws IOException is thrown if the path could not be decoded
|
||||
*/
|
||||
public static File getDataDirectory(String configuredFilePath, Class clazz) throws IOException {
|
||||
final File file = new File(configuredFilePath);
|
||||
if (file.exists() && file.isDirectory() && file.canWrite()) {
|
||||
return new File(file.getCanonicalPath());
|
||||
} else {
|
||||
final String filePath = clazz.getProtectionDomain().getCodeSource().getLocation().getPath();
|
||||
final String decodedPath = URLDecoder.decode(filePath, "UTF-8");
|
||||
File exePath = new File(decodedPath);
|
||||
if (exePath.getName().toLowerCase().endsWith(".jar")) {
|
||||
exePath = exePath.getParentFile();
|
||||
} else {
|
||||
exePath = new File(".");
|
||||
}
|
||||
final File path = new File(exePath.getCanonicalFile() + File.separator + configuredFilePath);
|
||||
return new File(path.getCanonicalPath());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public abstract class Filter<T> {
|
||||
|
||||
private class FilterIterator implements Iterator<T> {
|
||||
|
||||
private Iterator<T> iterator;
|
||||
private final Iterator<T> iterator;
|
||||
private T next;
|
||||
|
||||
private FilterIterator(Iterator<T> iterator) {
|
||||
|
||||
21
src/main/resources/META-INF/licenses/jsoup/LICENSE.txt
Normal file
21
src/main/resources/META-INF/licenses/jsoup/LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2009, 2010, 2011, 2012, 2013 Jonathan Hedley <jonathan@hedley.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -18,20 +18,19 @@
|
||||
*/
|
||||
package org.owasp.dependencycheck.data.cpe;
|
||||
|
||||
import org.owasp.dependencycheck.data.cpe.Index;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.owasp.dependencycheck.utils.FileUtils;
|
||||
import org.owasp.dependencycheck.utils.Settings;
|
||||
|
||||
/**
|
||||
@@ -59,17 +58,7 @@ public abstract class BaseIndexTestCase {
|
||||
|
||||
protected static File getDataDirectory() throws IOException {
|
||||
String fileName = Settings.getString(Settings.KEYS.CPE_INDEX);
|
||||
String filePath = Index.class.getProtectionDomain().getCodeSource().getLocation().getPath();
|
||||
String decodedPath = URLDecoder.decode(filePath, "UTF-8");
|
||||
File exePath = new File(decodedPath);
|
||||
if (exePath.getName().toLowerCase().endsWith(".jar")) {
|
||||
exePath = exePath.getParentFile();
|
||||
} else {
|
||||
exePath = new File(".");
|
||||
}
|
||||
File path = new File(exePath.getCanonicalFile() + File.separator + fileName);
|
||||
path = new File(path.getCanonicalPath());
|
||||
return path;
|
||||
return FileUtils.getDataDirectory(fileName, Index.class);
|
||||
}
|
||||
|
||||
public static void ensureIndexExists() throws Exception {
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
|
||||
/**
|
||||
@@ -61,7 +62,8 @@ public class IndexTest {
|
||||
try {
|
||||
instance.open();
|
||||
} catch (IOException ex) {
|
||||
Assert.fail(ex.getMessage());
|
||||
assertNull(ex.getMessage(), ex);
|
||||
//Assert.fail(ex.getMessage());
|
||||
}
|
||||
instance.close();
|
||||
}
|
||||
@@ -76,6 +78,6 @@ public class IndexTest {
|
||||
Directory result = index.getDirectory();
|
||||
|
||||
String exp = File.separatorChar + "target" + File.separatorChar + "data" + File.separatorChar + "cpe";
|
||||
Assert.assertTrue(result.toString().contains(exp));
|
||||
assertTrue(result.toString().contains(exp));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import junit.framework.TestCase;
|
||||
import org.owasp.dependencycheck.utils.FileUtils;
|
||||
import org.owasp.dependencycheck.utils.Settings;
|
||||
|
||||
/**
|
||||
@@ -49,17 +49,7 @@ public abstract class BaseDBTestCase extends TestCase {
|
||||
|
||||
protected static File getDataDirectory() throws IOException {
|
||||
String fileName = Settings.getString(Settings.KEYS.CVE_INDEX);
|
||||
String filePath = Index.class.getProtectionDomain().getCodeSource().getLocation().getPath();
|
||||
String decodedPath = URLDecoder.decode(filePath, "UTF-8");
|
||||
File exePath = new File(decodedPath);
|
||||
if (exePath.getName().toLowerCase().endsWith(".jar")) {
|
||||
exePath = exePath.getParentFile();
|
||||
} else {
|
||||
exePath = new File(".");
|
||||
}
|
||||
File path = new File(exePath.getCanonicalFile() + File.separator + fileName);
|
||||
path = new File(path.getCanonicalPath());
|
||||
return path;
|
||||
return FileUtils.getDataDirectory(fileName, Index.class);
|
||||
}
|
||||
|
||||
public static void ensureDBExists() throws Exception {
|
||||
|
||||
@@ -72,9 +72,9 @@ public class DependencyVersionUtilTest {
|
||||
|
||||
String[] failingNames = { "no-version-identified.jar", "somelib-04aug2000r7-dev.jar", "no.version15.jar",
|
||||
"lib_1.0_spec-1.1.jar", "lib-api_1.0_spec-1.0.1.jar" };
|
||||
for (int i = 0; i < failingNames.length; i++) {
|
||||
final DependencyVersion version = DependencyVersionUtil.parseVersionFromFileName(failingNames[i]);
|
||||
assertNull("Found version in name that should have failed \"" + failingNames[i] + "\".", version);
|
||||
for (String failingName : failingNames) {
|
||||
final DependencyVersion version = DependencyVersionUtil.parseVersionFromFileName(failingName);
|
||||
assertNull("Found version in name that should have failed \"" + failingName + "\".", version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2706
src/test/resources/nvdcve-2.0-2012.xml
Normal file
2706
src/test/resources/nvdcve-2.0-2012.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
9b5390434d0c6bbf79b5b64c94bff06f497f780c
|
||||
1454
src/test/resources/nvdcve-2012.xml
Normal file
1454
src/test/resources/nvdcve-2012.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
f2ff6066ee3da30900f068dae7819e3bbf5a0618
|
||||
Reference in New Issue
Block a user