mirror of
https://github.com/ysoftdevs/DependencyCheck.git
synced 2026-01-14 15:53:36 +01:00
Merge branch 'brianf-dependencyNameImprovements'
This commit is contained in:
@@ -58,6 +58,12 @@ import org.owasp.dependencycheck.exception.InitializationException;
|
||||
@Experimental
|
||||
public class CMakeAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
|
||||
/**
|
||||
* A descriptor for the type of dependencies processed or added by this
|
||||
* analyzer
|
||||
*/
|
||||
public static final String DEPENDENCY_ECOSYSTEM = "CMAKE";
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*/
|
||||
@@ -66,8 +72,7 @@ public class CMakeAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
/**
|
||||
* Used when compiling file scanning regex patterns.
|
||||
*/
|
||||
private static final int REGEX_OPTIONS = Pattern.DOTALL
|
||||
| Pattern.CASE_INSENSITIVE | Pattern.MULTILINE;
|
||||
private static final int REGEX_OPTIONS = Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE;
|
||||
|
||||
/**
|
||||
* Regex to extract the product information.
|
||||
@@ -82,10 +87,8 @@ public class CMakeAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
*
|
||||
* Group 2: Version
|
||||
*/
|
||||
private static final Pattern SET_VERSION = Pattern
|
||||
.compile(
|
||||
"^ *set\\s*\\(\\s*(\\w+)_version\\s+\"?(\\d+(?:\\.\\d+)+)[\\s\"]?\\)",
|
||||
REGEX_OPTIONS);
|
||||
private static final Pattern SET_VERSION = Pattern.compile(
|
||||
"^ *set\\s*\\(\\s*(\\w+)_version\\s+\"?(\\d+(?:\\.\\d+)+)[\\s\"]?\\)", REGEX_OPTIONS);
|
||||
|
||||
/**
|
||||
* Detects files that can be analyzed.
|
||||
@@ -149,12 +152,10 @@ public class CMakeAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
* analyzing the dependency
|
||||
*/
|
||||
@Override
|
||||
protected void analyzeDependency(Dependency dependency, Engine engine)
|
||||
throws AnalysisException {
|
||||
protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
|
||||
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
|
||||
final File file = dependency.getActualFile();
|
||||
final String parentName = file.getParentFile().getName();
|
||||
final String name = file.getName();
|
||||
dependency.setDisplayFileName(String.format("%s%c%s", parentName, File.separatorChar, name));
|
||||
String contents;
|
||||
try {
|
||||
contents = FileUtils.readFileToString(file, Charset.defaultCharset()).trim();
|
||||
@@ -162,7 +163,6 @@ public class CMakeAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
throw new AnalysisException(
|
||||
"Problem occurred while reading dependency file.", e);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(contents)) {
|
||||
final Matcher m = PROJECT.matcher(contents);
|
||||
int count = 0;
|
||||
@@ -175,6 +175,7 @@ public class CMakeAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
LOGGER.debug("Group 1: {}", group);
|
||||
dependency.addEvidence(EvidenceType.PRODUCT, name, "Project", group, Confidence.HIGH);
|
||||
dependency.addEvidence(EvidenceType.VENDOR, name, "Project", group, Confidence.HIGH);
|
||||
dependency.setName(group);
|
||||
}
|
||||
LOGGER.debug("Found {} matches.", count);
|
||||
analyzeSetVersionCommand(dependency, engine, contents);
|
||||
@@ -213,7 +214,7 @@ public class CMakeAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
if (count > 1) {
|
||||
//TODO - refactor so we do not assign to the parameter (checkstyle)
|
||||
currentDep = new Dependency(dependency.getActualFile());
|
||||
currentDep.setDisplayFileName(String.format("%s:%s", dependency.getDisplayFileName(), product));
|
||||
currentDep.setEcosystem(DEPENDENCY_ECOSYSTEM);
|
||||
final String filePath = String.format("%s:%s", dependency.getFilePath(), product);
|
||||
currentDep.setFilePath(filePath);
|
||||
|
||||
@@ -227,10 +228,12 @@ public class CMakeAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
currentDep.setSha1sum(Checksum.getHex(sha1.digest(path)));
|
||||
engine.addDependency(currentDep);
|
||||
}
|
||||
final String source = currentDep.getDisplayFileName();
|
||||
final String source = currentDep.getFileName();
|
||||
currentDep.addEvidence(EvidenceType.PRODUCT, source, "Product", product, Confidence.MEDIUM);
|
||||
currentDep.addEvidence(EvidenceType.VENDOR, source, "Vendor", product, Confidence.MEDIUM);
|
||||
currentDep.addEvidence(EvidenceType.VERSION, source, "Version", version, Confidence.MEDIUM);
|
||||
currentDep.setName(product);
|
||||
currentDep.setVersion(version);
|
||||
}
|
||||
LOGGER.debug("Found {} matches.", count);
|
||||
}
|
||||
@@ -241,9 +244,9 @@ public class CMakeAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sha1 message digest.
|
||||
* Returns the SHA1 message digest.
|
||||
*
|
||||
* @return the sha1 message digest
|
||||
* @return the SHA1 message digest
|
||||
*/
|
||||
private MessageDigest getSha1MessageDigest() {
|
||||
try {
|
||||
|
||||
@@ -45,6 +45,12 @@ import org.owasp.dependencycheck.utils.Settings;
|
||||
@ThreadSafe
|
||||
public class CocoaPodsAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
|
||||
/**
|
||||
* A descriptor for the type of dependencies processed or added by this
|
||||
* analyzer
|
||||
*/
|
||||
public static final String DEPENDENCY_ECOSYSTEM = "CocoaPod";
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*/
|
||||
@@ -124,6 +130,7 @@ public class CocoaPodsAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
protected void analyzeDependency(Dependency dependency, Engine engine)
|
||||
throws AnalysisException {
|
||||
|
||||
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
|
||||
String contents;
|
||||
try {
|
||||
contents = FileUtils.readFileToString(dependency.getActualFile(), Charset.defaultCharset());
|
||||
@@ -140,6 +147,7 @@ public class CocoaPodsAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
if (!name.isEmpty()) {
|
||||
dependency.addEvidence(EvidenceType.PRODUCT, PODSPEC, "name_project", name, Confidence.HIGHEST);
|
||||
dependency.addEvidence(EvidenceType.VENDOR, PODSPEC, "name_project", name, Confidence.HIGHEST);
|
||||
dependency.setName(name);
|
||||
}
|
||||
final String summary = determineEvidence(contents, blockVariable, "summary");
|
||||
if (!summary.isEmpty()) {
|
||||
@@ -156,14 +164,14 @@ public class CocoaPodsAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
}
|
||||
final String license = determineEvidence(contents, blockVariable, "licen[cs]es?");
|
||||
if (!license.isEmpty()) {
|
||||
dependency.addEvidence(EvidenceType.VENDOR, PODSPEC, "license", license, Confidence.HIGHEST);
|
||||
dependency.setLicense(license);
|
||||
}
|
||||
|
||||
final String version = determineEvidence(contents, blockVariable, "version");
|
||||
if (!version.isEmpty()) {
|
||||
dependency.addEvidence(EvidenceType.VERSION, PODSPEC, "version", version, Confidence.HIGHEST);
|
||||
dependency.setVersion(version);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setPackagePath(dependency);
|
||||
|
||||
@@ -47,6 +47,12 @@ import org.owasp.dependencycheck.dependency.EvidenceType;
|
||||
@Experimental
|
||||
public class ComposerLockAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
|
||||
/**
|
||||
* A descriptor for the type of dependencies processed or added by this
|
||||
* analyzer
|
||||
*/
|
||||
public static final String DEPENDENCY_ECOSYSTEM = "Composer";
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*/
|
||||
@@ -105,20 +111,33 @@ public class ComposerLockAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
|
||||
try (FileInputStream fis = new FileInputStream(dependency.getActualFile())) {
|
||||
final ComposerLockParser clp = new ComposerLockParser(fis);
|
||||
LOGGER.info("Checking composer.lock file {}", dependency.getActualFilePath());
|
||||
LOGGER.debug("Checking composer.lock file {}", dependency.getActualFilePath());
|
||||
clp.process();
|
||||
//if dependencies are found in the lock, then there is always an empty shell dependency left behind for the
|
||||
//composer.lock. The first pass through, reuse the top level dependency, and add new ones for the rest.
|
||||
boolean processedAtLeastOneDep = false;
|
||||
for (ComposerDependency dep : clp.getDependencies()) {
|
||||
final Dependency d = new Dependency(dependency.getActualFile());
|
||||
d.setDisplayFileName(String.format("%s:%s/%s", dependency.getDisplayFileName(), dep.getGroup(), dep.getProject()));
|
||||
final String filePath = String.format("%s:%s/%s", dependency.getFilePath(), dep.getGroup(), dep.getProject());
|
||||
final String filePath = String.format("%s:%s/%s/%s", dependency.getFilePath(), dep.getGroup(), dep.getProject(), dep.getVersion());
|
||||
d.setName(dep.getProject());
|
||||
d.setVersion(dep.getVersion());
|
||||
d.setEcosystem(DEPENDENCY_ECOSYSTEM);
|
||||
final MessageDigest sha1 = getSha1MessageDigest();
|
||||
d.setFilePath(filePath);
|
||||
d.setSha1sum(Checksum.getHex(sha1.digest(filePath.getBytes(Charset.defaultCharset()))));
|
||||
d.addEvidence(EvidenceType.VENDOR, COMPOSER_LOCK, "vendor", dep.getGroup(), Confidence.HIGHEST);
|
||||
d.addEvidence(EvidenceType.PRODUCT, COMPOSER_LOCK, "product", dep.getProject(), Confidence.HIGHEST);
|
||||
d.addEvidence(EvidenceType.VERSION, COMPOSER_LOCK, "version", dep.getVersion(), Confidence.HIGHEST);
|
||||
LOGGER.info("Adding dependency {}", d);
|
||||
LOGGER.debug("Adding dependency {}", d.getDisplayFileName());
|
||||
engine.addDependency(d);
|
||||
//make sure we only remove the main dependency if we went through this loop at least once.
|
||||
processedAtLeastOneDep = true;
|
||||
}
|
||||
// remove the dependency at the end because it's referenced in the loop itself.
|
||||
// double check the name to be sure we only remove the generic entry.
|
||||
if (processedAtLeastOneDep && dependency.getDisplayFileName().equalsIgnoreCase("composer.lock")) {
|
||||
LOGGER.debug("Removing main redundant dependency {}", dependency.getDisplayFileName());
|
||||
engine.removeDependency(dependency);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
LOGGER.warn("Error opening dependency {}", dependency.getActualFilePath());
|
||||
|
||||
@@ -73,6 +73,10 @@ import org.slf4j.LoggerFactory;
|
||||
public class JarAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
|
||||
//<editor-fold defaultstate="collapsed" desc="Constants and Member Variables">
|
||||
/**
|
||||
* A descriptor for the type of dependencies processed or added by this analyzer
|
||||
*/
|
||||
public static final String DEPENDENCY_ECOSYSTEM = "Java";
|
||||
/**
|
||||
* The logger.
|
||||
*/
|
||||
@@ -258,6 +262,7 @@ public class JarAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
final boolean hasPOM = analyzePOM(dependency, classNames, engine);
|
||||
final boolean addPackagesAsEvidence = !(hasManifest && hasPOM);
|
||||
analyzePackageNames(classNames, dependency, addPackagesAsEvidence);
|
||||
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
|
||||
} catch (IOException ex) {
|
||||
throw new AnalysisException("Exception occurred reading the JAR file (" + dependency.getFileName() + ").", ex);
|
||||
}
|
||||
|
||||
@@ -55,17 +55,19 @@ public class NodePackageAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
* The logger.
|
||||
*/
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(NodePackageAnalyzer.class);
|
||||
|
||||
/**
|
||||
* A descriptor for the type of dependencies processed or added by this
|
||||
* analyzer
|
||||
*/
|
||||
public static final String DEPENDENCY_ECOSYSTEM = "npm";
|
||||
/**
|
||||
* The name of the analyzer.
|
||||
*/
|
||||
private static final String ANALYZER_NAME = "Node.js Package Analyzer";
|
||||
|
||||
/**
|
||||
* The phase that this analyzer is intended to run in.
|
||||
*/
|
||||
private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
|
||||
|
||||
/**
|
||||
* The file name to scan.
|
||||
*/
|
||||
@@ -124,6 +126,7 @@ public class NodePackageAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
|
||||
@Override
|
||||
protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
|
||||
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
|
||||
final File file = dependency.getActualFile();
|
||||
if (!file.isFile() || file.length() == 0) {
|
||||
return;
|
||||
@@ -134,6 +137,7 @@ public class NodePackageAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
final Object value = json.get("name");
|
||||
if (value instanceof JsonString) {
|
||||
final String valueString = ((JsonString) value).getString();
|
||||
dependency.setName(valueString);
|
||||
dependency.addEvidence(EvidenceType.PRODUCT, PACKAGE_JSON, "name", valueString, Confidence.HIGHEST);
|
||||
dependency.addEvidence(EvidenceType.VENDOR, PACKAGE_JSON, "name_project",
|
||||
String.format("%s_project", valueString), Confidence.LOW);
|
||||
@@ -143,8 +147,8 @@ public class NodePackageAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
}
|
||||
addToEvidence(dependency, EvidenceType.PRODUCT, json, "description");
|
||||
addToEvidence(dependency, EvidenceType.VENDOR, json, "author");
|
||||
addToEvidence(dependency, EvidenceType.VERSION, json, "version");
|
||||
dependency.setDisplayFileName(String.format("%s/%s", file.getParentFile().getName(), file.getName()));
|
||||
final String version = addToEvidence(dependency, EvidenceType.VERSION, json, "version");
|
||||
dependency.setVersion(version);
|
||||
} catch (JsonException e) {
|
||||
LOGGER.warn("Failed to parse package.json file.", e);
|
||||
} catch (IOException e) {
|
||||
@@ -159,23 +163,26 @@ public class NodePackageAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
* @param dep the dependency to add the evidence
|
||||
* @param t the type of evidence to add
|
||||
* @param json information from node.js
|
||||
* @return the actual string set into evidence
|
||||
* @param key the key to obtain the data from the json information
|
||||
*/
|
||||
private void addToEvidence(Dependency dep, EvidenceType t, JsonObject json, String key) {
|
||||
private String addToEvidence(Dependency dep, EvidenceType t, JsonObject json, String key) {
|
||||
String evidenceStr = null;
|
||||
if (json.containsKey(key)) {
|
||||
final JsonValue value = json.get(key);
|
||||
if (value instanceof JsonString) {
|
||||
dep.addEvidence(t, PACKAGE_JSON, key, ((JsonString) value).getString(), Confidence.HIGHEST);
|
||||
|
||||
evidenceStr = ((JsonString) value).getString();
|
||||
dep.addEvidence(t, PACKAGE_JSON, key, evidenceStr, Confidence.HIGHEST);
|
||||
} else if (value instanceof JsonObject) {
|
||||
final JsonObject jsonObject = (JsonObject) value;
|
||||
for (final Map.Entry<String, JsonValue> entry : jsonObject.entrySet()) {
|
||||
final String property = entry.getKey();
|
||||
final JsonValue subValue = entry.getValue();
|
||||
if (subValue instanceof JsonString) {
|
||||
evidenceStr = ((JsonString) subValue).getString();
|
||||
dep.addEvidence(t, PACKAGE_JSON,
|
||||
String.format("%s.%s", key, property),
|
||||
((JsonString) subValue).getString(),
|
||||
evidenceStr,
|
||||
Confidence.HIGHEST);
|
||||
} else {
|
||||
LOGGER.warn("JSON sub-value not string as expected: {}", subValue);
|
||||
@@ -185,5 +192,6 @@ public class NodePackageAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
LOGGER.warn("JSON value not string or JSON object as expected: {}", value);
|
||||
}
|
||||
}
|
||||
return evidenceStr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,12 @@ import org.owasp.dependencycheck.dependency.EvidenceType;
|
||||
@ThreadSafe
|
||||
public class PythonDistributionAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
|
||||
/**
|
||||
* A descriptor for the type of dependencies processed or added by this
|
||||
* analyzer
|
||||
*/
|
||||
public static final String DEPENDENCY_ECOSYSTEM = "Python.Dist";
|
||||
|
||||
/**
|
||||
* Name of egg metadata files to analyze.
|
||||
*/
|
||||
@@ -167,6 +173,8 @@ public class PythonDistributionAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
@Override
|
||||
protected void analyzeDependency(Dependency dependency, Engine engine)
|
||||
throws AnalysisException {
|
||||
|
||||
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
|
||||
final File actualFile = dependency.getActualFile();
|
||||
if (WHL_FILTER.accept(actualFile)) {
|
||||
collectMetadataFromArchiveFormat(dependency, DIST_INFO_FILTER,
|
||||
@@ -180,7 +188,6 @@ public class PythonDistributionAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
if (metadata || PKG_INFO.equals(name)) {
|
||||
final File parent = actualFile.getParentFile();
|
||||
final String parentName = parent.getName();
|
||||
dependency.setDisplayFileName(parentName + "/" + name);
|
||||
if (parent.isDirectory()
|
||||
&& (metadata && parentName.endsWith(".dist-info")
|
||||
|| parentName.endsWith(".egg-info") || "EGG-INFO"
|
||||
@@ -281,6 +288,9 @@ public class PythonDistributionAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
final InternetHeaders headers = getManifestProperties(file);
|
||||
addPropertyToEvidence(dependency, EvidenceType.VERSION, Confidence.HIGHEST, headers, "Version");
|
||||
addPropertyToEvidence(dependency, EvidenceType.PRODUCT, Confidence.HIGHEST, headers, "Name");
|
||||
addPropertyToEvidence(dependency, EvidenceType.PRODUCT, Confidence.MEDIUM, headers, "Name");
|
||||
dependency.setName(headers.getHeader("Name", null));
|
||||
dependency.setVersion(headers.getHeader("Version", null));
|
||||
final String url = headers.getHeader("Home-page", null);
|
||||
if (StringUtils.isNotBlank(url)) {
|
||||
if (UrlStringUtils.isUrl(url)) {
|
||||
|
||||
@@ -48,6 +48,12 @@ import org.owasp.dependencycheck.exception.InitializationException;
|
||||
@ThreadSafe
|
||||
public class PythonPackageAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
|
||||
/**
|
||||
* A descriptor for the type of dependencies processed or added by this
|
||||
* analyzer
|
||||
*/
|
||||
public static final String DEPENDENCY_ECOSYSTEM = "Python.Pkg";
|
||||
|
||||
/**
|
||||
* Used when compiling file scanning regex patterns.
|
||||
*/
|
||||
@@ -183,6 +189,7 @@ public class PythonPackageAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
@Override
|
||||
protected void analyzeDependency(Dependency dependency, Engine engine)
|
||||
throws AnalysisException {
|
||||
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
|
||||
final File file = dependency.getActualFile();
|
||||
final File parent = file.getParentFile();
|
||||
final String parentName = parent.getName();
|
||||
@@ -190,8 +197,8 @@ public class PythonPackageAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
//by definition, the containing folder of __init__.py is considered the package, even the file is empty:
|
||||
//"The __init__.py files are required to make Python treat the directories as containing packages"
|
||||
//see section "6.4 Packages" from https://docs.python.org/2/tutorial/modules.html;
|
||||
dependency.setDisplayFileName(parentName + "/__init__.py");
|
||||
dependency.addEvidence(EvidenceType.PRODUCT, file.getName(), "PackageName", parentName, Confidence.HIGHEST);
|
||||
dependency.setName(parentName);
|
||||
|
||||
final File[] fileList = parent.listFiles(PY_FILTER);
|
||||
if (fileList != null) {
|
||||
@@ -312,6 +319,9 @@ public class PythonPackageAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
final boolean found = matcher.find();
|
||||
if (found) {
|
||||
dependency.addEvidence(type, source, name, matcher.group(4), confidence);
|
||||
if (type == EvidenceType.VERSION) {
|
||||
dependency.setVersion(matcher.group(4));
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,11 @@ import org.owasp.dependencycheck.dependency.Dependency;
|
||||
@ThreadSafe
|
||||
public class RubyBundlerAnalyzer extends RubyGemspecAnalyzer {
|
||||
|
||||
/**
|
||||
* A descriptor for the type of dependencies processed or added by this analyzer
|
||||
*/
|
||||
public static final String DEPENDENCY_ECOSYSTEM = "Ruby.Bundle";
|
||||
|
||||
/**
|
||||
* The name of the analyzer.
|
||||
*/
|
||||
@@ -99,7 +104,7 @@ public class RubyBundlerAnalyzer extends RubyGemspecAnalyzer {
|
||||
protected void analyzeDependency(Dependency dependency, Engine engine)
|
||||
throws AnalysisException {
|
||||
super.analyzeDependency(dependency, engine);
|
||||
|
||||
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
|
||||
//find the corresponding gem folder for this .gemspec stub by "bundle install --deployment"
|
||||
final File gemspecFile = dependency.getActualFile();
|
||||
final String gemFileName = gemspecFile.getName();
|
||||
|
||||
@@ -50,6 +50,11 @@ import org.slf4j.LoggerFactory;
|
||||
@ThreadSafe
|
||||
public class RubyGemspecAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
|
||||
/**
|
||||
* A descriptor for the type of dependencies processed or added by this
|
||||
* analyzer
|
||||
*/
|
||||
public static final String DEPENDENCY_ECOSYSTEM = "Ruby.Bundle";
|
||||
/**
|
||||
* The logger.
|
||||
*/
|
||||
@@ -58,17 +63,14 @@ public class RubyGemspecAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
* The name of the analyzer.
|
||||
*/
|
||||
private static final String ANALYZER_NAME = "Ruby Gemspec Analyzer";
|
||||
|
||||
/**
|
||||
* The phase that this analyzer is intended to run in.
|
||||
*/
|
||||
private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
|
||||
|
||||
/**
|
||||
* The gemspec file extension.
|
||||
*/
|
||||
private static final String GEMSPEC = "gemspec";
|
||||
|
||||
/**
|
||||
* The file filter containing the list of file extensions that can be
|
||||
* analyzed.
|
||||
@@ -133,6 +135,7 @@ public class RubyGemspecAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
|
||||
@Override
|
||||
protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
|
||||
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
|
||||
String contents;
|
||||
try {
|
||||
contents = FileUtils.readFileToString(dependency.getActualFile(), Charset.defaultCharset());
|
||||
@@ -148,6 +151,7 @@ public class RubyGemspecAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
final String name = addStringEvidence(dependency, EvidenceType.PRODUCT, contents, blockVariable, "name", "name", Confidence.HIGHEST);
|
||||
if (!name.isEmpty()) {
|
||||
dependency.addEvidence(EvidenceType.VENDOR, GEMSPEC, "name_project", name + "_project", Confidence.LOW);
|
||||
dependency.setName(name);
|
||||
}
|
||||
addStringEvidence(dependency, EvidenceType.PRODUCT, contents, blockVariable, "summary", "summary", Confidence.LOW);
|
||||
|
||||
@@ -160,9 +164,10 @@ public class RubyGemspecAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
blockVariable, "version", "version", Confidence.HIGHEST);
|
||||
if (value.length() < 1) {
|
||||
addEvidenceFromVersionFile(dependency, EvidenceType.VERSION, dependency.getActualFile());
|
||||
} else {
|
||||
dependency.setVersion(value);
|
||||
}
|
||||
}
|
||||
|
||||
setPackagePath(dependency);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ import org.owasp.dependencycheck.utils.Settings;
|
||||
@ThreadSafe
|
||||
public class SwiftPackageManagerAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
|
||||
/**
|
||||
* A descriptor for the type of dependencies processed or added by this
|
||||
* analyzer
|
||||
*/
|
||||
public static final String DEPENDENCY_ECOSYSTEM = "Swift.PM";
|
||||
|
||||
/**
|
||||
* The name of the analyzer.
|
||||
*/
|
||||
@@ -121,6 +127,8 @@ public class SwiftPackageManagerAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
protected void analyzeDependency(Dependency dependency, Engine engine)
|
||||
throws AnalysisException {
|
||||
|
||||
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
|
||||
|
||||
String contents;
|
||||
try {
|
||||
contents = FileUtils.readFileToString(dependency.getActualFile(), Charset.defaultCharset());
|
||||
@@ -135,11 +143,18 @@ public class SwiftPackageManagerAnalyzer extends AbstractFileTypeAnalyzer {
|
||||
return;
|
||||
}
|
||||
|
||||
//SPM is currently under development for SWIFT 3. Its current metadata includes package name and dependencies.
|
||||
//Future interesting metadata: version, license, homepage, author, summary, etc.
|
||||
// TODO SPM is currently under development for SWIFT 3. Its current metadata includes
|
||||
// package name and dependencies.
|
||||
// Future interesting metadata: version, license, homepage, author, summary,
|
||||
// etc.
|
||||
final String name = addStringEvidence(dependency, EvidenceType.PRODUCT, packageDescription, "name", "name", Confidence.HIGHEST);
|
||||
if (name != null && !name.isEmpty()) {
|
||||
dependency.addEvidence(EvidenceType.VENDOR, SPM_FILE_NAME, "name_project", name, Confidence.HIGHEST);
|
||||
dependency.setName(name);
|
||||
} else {
|
||||
// if we can't get the name from the meta, then assume the name is the name of
|
||||
// the parent folder containing the package.swift file.
|
||||
dependency.setName(dependency.getActualFile().getParentFile().getName());
|
||||
}
|
||||
}
|
||||
setPackagePath(dependency);
|
||||
|
||||
@@ -60,7 +60,7 @@ public class ComposerLockParser {
|
||||
* @param inputStream the InputStream to parse
|
||||
*/
|
||||
public ComposerLockParser(InputStream inputStream) {
|
||||
LOGGER.info("Creating a ComposerLockParser");
|
||||
LOGGER.debug("Creating a ComposerLockParser");
|
||||
this.jsonReader = Json.createReader(inputStream);
|
||||
this.composerDependencies = new ArrayList<>();
|
||||
}
|
||||
@@ -69,7 +69,7 @@ public class ComposerLockParser {
|
||||
* Process the input stream to create the list of dependencies.
|
||||
*/
|
||||
public void process() {
|
||||
LOGGER.info("Beginning Composer lock processing");
|
||||
LOGGER.debug("Beginning Composer lock processing");
|
||||
try {
|
||||
final JsonObject composer = jsonReader.readObject();
|
||||
if (composer.containsKey("packages")) {
|
||||
|
||||
@@ -126,6 +126,22 @@ public class Dependency extends EvidenceCollection implements Serializable, Comp
|
||||
*/
|
||||
private boolean isVirtual = false;
|
||||
|
||||
/**
|
||||
* Defines the human-recognizable name for the dependency
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Defines the human-recognizable version for the dependency
|
||||
*/
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* A descriptor for the type of dependency based on which analyzer added it
|
||||
* or collected evidence about it
|
||||
*/
|
||||
private String ecosystem;
|
||||
|
||||
/**
|
||||
* Returns the package path.
|
||||
*
|
||||
@@ -249,15 +265,22 @@ public class Dependency extends EvidenceCollection implements Serializable, Comp
|
||||
|
||||
/**
|
||||
* Returns the file name to display in reports; if no display file name has
|
||||
* been set it will default to the actual file name.
|
||||
* been set it will default to constructing a name based on the name and
|
||||
* version fields, otherwise it will return the actual file name.
|
||||
*
|
||||
* @return the file name to display
|
||||
*/
|
||||
public String getDisplayFileName() {
|
||||
if (displayName == null) {
|
||||
return this.fileName;
|
||||
if (displayName != null) {
|
||||
return displayName;
|
||||
}
|
||||
return this.displayName;
|
||||
if (name == null) {
|
||||
return fileName;
|
||||
}
|
||||
if (version == null) {
|
||||
return name;
|
||||
}
|
||||
return name + ":" + version;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -487,6 +510,20 @@ public class Dependency extends EvidenceCollection implements Serializable, Comp
|
||||
this.license = license;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name the name to set
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unmodifiable sorted set of vulnerabilities.
|
||||
*
|
||||
@@ -729,4 +766,32 @@ public class Dependency extends EvidenceCollection implements Serializable, Comp
|
||||
public synchronized void addSuppressedVulnerabilities(List<Vulnerability> vulns) {
|
||||
this.suppressedVulnerabilities.addAll(vulns);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the version
|
||||
*/
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param version the version to set
|
||||
*/
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the ecosystem
|
||||
*/
|
||||
public String getEcosystem() {
|
||||
return ecosystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ecosystem the ecosystem to set
|
||||
*/
|
||||
public void setEcosystem(String ecosystem) {
|
||||
this.ecosystem = ecosystem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ public class CMakeAnalyzerTest extends BaseDBTestCase {
|
||||
/**
|
||||
* Cleanup any resources used.
|
||||
*
|
||||
* @throws Exception if there is a problem
|
||||
*/
|
||||
@After
|
||||
@Override
|
||||
@@ -135,6 +136,21 @@ public class CMakeAnalyzerTest extends BaseDBTestCase {
|
||||
assertProductEvidence(result, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether expected evidence is gathered from OpenCV's CVDetectPython.
|
||||
*
|
||||
* @throws AnalysisException is thrown when an exception occurs.
|
||||
*/
|
||||
@Test
|
||||
public void testAnalyzeCMakeListsPython() throws AnalysisException {
|
||||
final Dependency result = new Dependency(BaseTest.getResourceAsFile(
|
||||
this, "cmake/opencv/cmake/OpenCVDetectPython.cmake"));
|
||||
analyzer.analyze(result, null);
|
||||
|
||||
//this one finds nothing so it falls through to the filename. Can we do better?
|
||||
assertEquals("OpenCVDetectPython.cmake", result.getDisplayFileName());
|
||||
}
|
||||
|
||||
private void assertProductEvidence(Dependency result, String product) {
|
||||
boolean found = false;
|
||||
for (Evidence e : result.getEvidence(EvidenceType.PRODUCT)) {
|
||||
|
||||
@@ -32,10 +32,13 @@ import org.owasp.dependencycheck.exception.InitializationException;
|
||||
import java.io.File;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
|
||||
/**
|
||||
* Unit tests for NodePackageAnalyzer.
|
||||
@@ -92,6 +95,25 @@ public class ComposerLockAnalyzerTest extends BaseDBTestCase {
|
||||
assertTrue(analyzer.accept(new File("composer.lock")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test of basic additions to the dependency list by parsing the
|
||||
* composer.lock file
|
||||
*
|
||||
* @throws AnalysisException is thrown when an exception occurs.
|
||||
*/
|
||||
@Test
|
||||
public void testRemoveRedundantParent() throws Exception {
|
||||
try (Engine engine = new Engine(getSettings())) {
|
||||
final Dependency result = new Dependency(BaseTest.getResourceAsFile(this, "composer.lock"));
|
||||
//test that we don't remove the parent if it's not redundant by name
|
||||
result.setDisplayFileName("NotComposer.Lock");
|
||||
engine.addDependency(result);
|
||||
analyzer.analyze(result, engine);
|
||||
//make sure the composer.lock is not removed
|
||||
assertTrue(ArrayUtils.contains(engine.getDependencies(), result));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test of inspect method, of class PythonDistributionAnalyzer.
|
||||
*
|
||||
@@ -102,7 +124,17 @@ public class ComposerLockAnalyzerTest extends BaseDBTestCase {
|
||||
try (Engine engine = new Engine(getSettings())) {
|
||||
final Dependency result = new Dependency(BaseTest.getResourceAsFile(this,
|
||||
"composer.lock"));
|
||||
//simulate normal operation when the composer.lock is already added to the engine as a dependency
|
||||
engine.addDependency(result);
|
||||
analyzer.analyze(result, engine);
|
||||
//make sure the redundant composer.lock is removed
|
||||
assertFalse(ArrayUtils.contains(engine.getDependencies(), result));
|
||||
assertEquals(30, engine.getDependencies().length);
|
||||
Dependency d = engine.getDependencies()[0];
|
||||
assertEquals("classpreloader", d.getName());
|
||||
assertEquals("2.0.0", d.getVersion());
|
||||
assertThat(d.getDisplayFileName(), equalTo("classpreloader:2.0.0"));
|
||||
assertEquals(ComposerLockAnalyzer.DEPENDENCY_ECOSYSTEM, d.getEcosystem());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ public class JarAnalyzerTest extends BaseTest {
|
||||
file = BaseTest.getResourceAsFile(this, "dwr.jar");
|
||||
result = new Dependency(file);
|
||||
instance.analyze(result, null);
|
||||
assertEquals(JarAnalyzer.DEPENDENCY_ECOSYSTEM,result.getEcosystem());
|
||||
boolean found = false;
|
||||
for (Evidence e : result.getEvidence(EvidenceType.VENDOR)) {
|
||||
if (e.getName().equals("url")) {
|
||||
|
||||
@@ -101,5 +101,8 @@ public class NodePackageAnalyzerTest extends BaseTest {
|
||||
assertThat(vendorString, containsString("dns-sync_project"));
|
||||
assertThat(result.getEvidence(EvidenceType.PRODUCT).toString(), containsString("dns-sync"));
|
||||
assertThat(result.getEvidence(EvidenceType.VERSION).toString(), containsString("0.1.0"));
|
||||
assertEquals(NodePackageAnalyzer.DEPENDENCY_ECOSYSTEM, result.getEcosystem());
|
||||
assertEquals("dns-sync", result.getName());
|
||||
assertEquals("0.1.0", result.getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,8 +120,7 @@ public class PythonDistributionAnalyzerTest extends BaseTest {
|
||||
final Dependency result = new Dependency(BaseTest.getResourceAsFile(
|
||||
this, "python/site-packages/Django-1.7.2.dist-info/METADATA"));
|
||||
djangoAssertions(result);
|
||||
assertEquals("Django-1.7.2.dist-info/METADATA", result.getDisplayFileName());
|
||||
}
|
||||
}
|
||||
|
||||
private void djangoAssertions(final Dependency result)
|
||||
throws AnalysisException {
|
||||
@@ -136,6 +135,10 @@ public class PythonDistributionAnalyzerTest extends BaseTest {
|
||||
}
|
||||
}
|
||||
assertTrue("Version 1.7.2 not found in Django dependency.", found);
|
||||
assertEquals("1.7.2",result.getVersion());
|
||||
assertEquals("Django",result.getName());
|
||||
assertEquals("Django:1.7.2",result.getDisplayFileName());
|
||||
assertEquals(PythonDistributionAnalyzer.DEPENDENCY_ECOSYSTEM,result.getEcosystem());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -188,5 +191,9 @@ public class PythonDistributionAnalyzerTest extends BaseTest {
|
||||
}
|
||||
}
|
||||
assertTrue("Version 0.0.1 not found in EggTest dependency.", found);
|
||||
assertEquals("0.0.1",result.getVersion());
|
||||
assertEquals("EggTest",result.getName());
|
||||
assertEquals("EggTest:0.0.1",result.getDisplayFileName());
|
||||
assertEquals(PythonDistributionAnalyzer.DEPENDENCY_ECOSYSTEM,result.getEcosystem());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,5 +103,9 @@ public class PythonPackageAnalyzerTest extends BaseTest {
|
||||
}
|
||||
}
|
||||
assertTrue("Version 0.0.1 not found in EggTest dependency.", found);
|
||||
assertEquals("0.0.1",result.getVersion());
|
||||
assertEquals("eggtest",result.getName());
|
||||
assertEquals("eggtest:0.0.1",result.getDisplayFileName());
|
||||
assertEquals(PythonPackageAnalyzer.DEPENDENCY_ECOSYSTEM,result.getEcosystem());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ public class RubyBundlerAnalyzerTest extends BaseTest {
|
||||
public void testSupportsFiles() {
|
||||
assertThat(analyzer.accept(new File("test.gemspec")), is(false));
|
||||
assertThat(analyzer.accept(new File("specifications" + File.separator + "test.gemspec")), is(true));
|
||||
assertThat(analyzer.accept(new File("gemspec.lock")), is(false));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +98,7 @@ public class RubyBundlerAnalyzerTest extends BaseTest {
|
||||
final Dependency result = new Dependency(BaseTest.getResourceAsFile(this,
|
||||
"ruby/vulnerable/gems/rails-4.1.15/vendor/bundle/ruby/2.2.0/specifications/dalli-2.7.5.gemspec"));
|
||||
analyzer.analyze(result, null);
|
||||
|
||||
|
||||
final String vendorString = result.getEvidence(EvidenceType.VENDOR).toString();
|
||||
assertThat(vendorString, containsString("Peter M. Goldstein"));
|
||||
assertThat(vendorString, containsString("Mike Perham"));
|
||||
@@ -107,5 +108,9 @@ public class RubyBundlerAnalyzerTest extends BaseTest {
|
||||
assertThat(result.getEvidence(EvidenceType.PRODUCT).toString(), containsString("dalli"));
|
||||
assertThat(result.getEvidence(EvidenceType.PRODUCT).toString(), containsString("High performance memcached client for Ruby"));
|
||||
assertThat(result.getEvidence(EvidenceType.VERSION).toString(), containsString("2.7.5"));
|
||||
assertEquals("dalli", result.getName());
|
||||
assertEquals("2.7.5", result.getVersion());
|
||||
assertEquals(RubyBundlerAnalyzer.DEPENDENCY_ECOSYSTEM, result.getEcosystem());
|
||||
assertEquals("dalli:2.7.5", result.getDisplayFileName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ public class RubyGemspecAnalyzerTest extends BaseTest {
|
||||
@Test
|
||||
public void testSupportsFiles() {
|
||||
assertThat(analyzer.accept(new File("test.gemspec")), is(true));
|
||||
assertThat(analyzer.accept(new File("gemspec.lock")), is(false));
|
||||
// assertThat(analyzer.accept(new File("Rakefile")), is(true));
|
||||
}
|
||||
|
||||
@@ -98,24 +99,33 @@ public class RubyGemspecAnalyzerTest extends BaseTest {
|
||||
"ruby/vulnerable/gems/specifications/rest-client-1.7.2.gemspec"));
|
||||
analyzer.analyze(result, null);
|
||||
final String vendorString = result.getEvidence(EvidenceType.VENDOR).toString();
|
||||
assertEquals(RubyGemspecAnalyzer.DEPENDENCY_ECOSYSTEM, result.getEcosystem());
|
||||
assertThat(vendorString, containsString("REST Client Team"));
|
||||
assertThat(vendorString, containsString("rest-client_project"));
|
||||
assertThat(vendorString, containsString("rest.client@librelist.com"));
|
||||
assertThat(vendorString, containsString("https://github.com/rest-client/rest-client"));
|
||||
assertThat(result.getEvidence(EvidenceType.PRODUCT).toString(), containsString("rest-client"));
|
||||
assertThat(result.getEvidence(EvidenceType.VERSION).toString(), containsString("1.7.2"));
|
||||
assertEquals("rest-client", result.getName());
|
||||
assertEquals("1.7.2", result.getVersion());
|
||||
assertEquals("rest-client:1.7.2", result.getDisplayFileName());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test Rakefile analysis.
|
||||
*
|
||||
* @throws AnalysisException is thrown when an exception occurs.
|
||||
*/
|
||||
//@Test TODO: place holder to test Rakefile support
|
||||
//@Test
|
||||
//TODO: place holder to test Rakefile support
|
||||
public void testAnalyzeRakefile() throws AnalysisException {
|
||||
final Dependency result = new Dependency(BaseTest.getResourceAsFile(this,
|
||||
"ruby/vulnerable/gems/rails-4.1.15/vendor/bundle/ruby/2.2.0/gems/pg-0.18.4/Rakefile"));
|
||||
analyzer.analyze(result, null);
|
||||
assertTrue(result.size()>0);
|
||||
assertTrue(result.size() > 0);
|
||||
assertEquals(RubyGemspecAnalyzer.DEPENDENCY_ECOSYSTEM, result.getEcosystem());
|
||||
assertEquals("pg", result.getName());
|
||||
assertEquals("0.18.4", result.getVersion());
|
||||
assertEquals("pg:0.18.4", result.getDisplayFileName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.owasp.dependencycheck.dependency.Dependency;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
|
||||
import java.io.File;
|
||||
import org.owasp.dependencycheck.dependency.EvidenceType;
|
||||
@@ -60,7 +61,7 @@ public class SwiftAnalyzersTest extends BaseTest {
|
||||
|
||||
spmAnalyzer.close();
|
||||
spmAnalyzer = null;
|
||||
|
||||
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
@@ -110,9 +111,13 @@ public class SwiftAnalyzersTest extends BaseTest {
|
||||
|
||||
assertThat(vendorString, containsString("Carlos Vidal"));
|
||||
assertThat(vendorString, containsString("https://github.com/nakiostudio/EasyPeasy"));
|
||||
assertThat(vendorString, containsString("MIT"));
|
||||
assertThat(result.getEvidence(EvidenceType.PRODUCT).toString(), containsString("EasyPeasy"));
|
||||
assertThat(result.getEvidence(EvidenceType.VERSION).toString(), containsString("0.2.3"));
|
||||
assertThat(result.getName(), equalTo("EasyPeasy"));
|
||||
assertThat(result.getVersion(), equalTo("0.2.3"));
|
||||
assertThat(result.getDisplayFileName(), equalTo("EasyPeasy:0.2.3"));
|
||||
assertThat(result.getLicense(), containsString("MIT"));
|
||||
assertThat(result.getEcosystem(), equalTo(CocoaPodsAnalyzer.DEPENDENCY_ECOSYSTEM));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,5 +132,9 @@ public class SwiftAnalyzersTest extends BaseTest {
|
||||
spmAnalyzer.analyze(result, null);
|
||||
|
||||
assertThat(result.getEvidence(EvidenceType.PRODUCT).toString(), containsString("Gloss"));
|
||||
assertThat(result.getName(), equalTo("Gloss"));
|
||||
//TODO: when version processing is added, update the expected name.
|
||||
assertThat(result.getDisplayFileName(), equalTo("Gloss"));
|
||||
assertThat(result.getEcosystem(), equalTo(SwiftPackageManagerAnalyzer.DEPENDENCY_ECOSYSTEM));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user