PerlCpanfileAnalyzer.java
/*
* This file is part of dependency-check-core.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Copyright (c) 2021 The OWASP Foundation. All Rights Reserved.
*/
package org.owasp.dependencycheck.analyzer;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import com.github.packageurl.PackageURLBuilder;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
import org.owasp.dependencycheck.dependency.Confidence;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.dependency.EvidenceType;
import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
import org.owasp.dependencycheck.dependency.naming.Identifier;
import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
import org.owasp.dependencycheck.exception.InitializationException;
import org.owasp.dependencycheck.utils.Checksum;
import org.owasp.dependencycheck.utils.FileFilterBuilder;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.concurrent.ThreadSafe;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.stream.Collectors.toCollection;
/**
* <p>
* Used to analyze Perl CPAN files. The analyzer does not yet differentiate
* developer and test dependencies from required dependencies. Nor does the
* analyzer support `cpanfile.snapshot` files yet. Finally, version ranges are
* not yet correctly handled either.</p>
* <p>
* Future enhancements should include supporting the snapshot files (which
* should not have version ranges) and correctly parsing the cpanfile DSL so
* that one can differentiate developer and test dependencies - which one may
* not want to include in the analysis.</p>
*
* @author Harjit Sandhu
* @author Jeremy Long
*/
@ThreadSafe
@Experimental
public class PerlCpanfileAnalyzer extends AbstractFileTypeAnalyzer {
/**
* The logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(PerlCpanfileAnalyzer.class);
/**
* The filter used to identify CPAN files.
*/
private static final FileFilter PACKAGE_FILTER = FileFilterBuilder.newInstance().addFilenames("cpanfile").build();
/**
* The pattern to extract version numbers from CPAN files.
*/
private static final Pattern VERSION_PATTERN = Pattern.compile("([0-9\\.]+)");
/**
* Create a new Perl CPAN File Analyzer.
*/
public PerlCpanfileAnalyzer() {
//empty constructor
}
@Override
protected FileFilter getFileFilter() {
return PACKAGE_FILTER;
}
@Override
public String getName() {
return "Perl cpanfile Analyzer";
}
@Override
public AnalysisPhase getAnalysisPhase() {
return AnalysisPhase.INFORMATION_COLLECTION;
}
@Override
protected String getAnalyzerEnabledSettingKey() {
return Settings.KEYS.ANALYZER_CPANFILE_ENABLED;
}
@Override
protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
//nothing to prepare
}
@Override
protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
engine.removeDependency(dependency);
final String contents = tryReadFile(dependency.getActualFile());
final List<String> requires = prepareContents(contents);
if (requires != null && !requires.isEmpty()) {
processFileContents(requires, dependency.getFilePath(), engine);
}
}
private String tryReadFile(File file) throws AnalysisException {
try {
return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8).trim();
} catch (IOException e) {
throw new AnalysisException("Problem occurred while reading dependency file.", e);
}
}
protected List<String> prepareContents(String contents) {
final Pattern pattern = Pattern.compile(";");
return Arrays.stream(contents.split("\n"))
.map(r -> r.indexOf("#") > 0 ? r.substring(0, r.indexOf("#")) : r)
.flatMap(pattern::splitAsStream)
.map(String::trim)
.filter(r -> r.startsWith("requires"))
.collect(toCollection(ArrayList::new));
}
protected void processFileContents(List<String> fileLines, String filePath, Engine engine) throws AnalysisException {
fileLines.stream()
.map(fileLine -> fileLine.split("(,|=>)"))
.map(requires -> {
//LOGGER.debug("perl scanning file:" + fileLine);
final String fqName = requires[0].substring(8)
.replace("'", "")
.replace("\"", "")
.trim();
final String version;
if (requires.length == 1) {
version = "0";
} else {
final Matcher matcher = VERSION_PATTERN.matcher(requires[1]);
if (matcher.find()) {
version = matcher.group(1);
} else {
version = "0";
}
}
final int pos = fqName.lastIndexOf("::");
final String namespace;
final String name;
if (pos > 0) {
namespace = fqName.substring(0, pos);
name = fqName.substring(pos + 2);
} else {
namespace = null;
name = fqName;
}
final Dependency dependency = new Dependency(true);
final File f = new File(filePath);
dependency.setFileName(f.getName());
dependency.setFilePath(filePath);
dependency.setActualFilePath(filePath);
dependency.setDisplayFileName("'" + fqName + "', '" + version + "'");
dependency.setEcosystem(Ecosystem.PERL);
dependency.addEvidence(EvidenceType.VENDOR, "cpanfile", "requires", fqName, Confidence.HIGHEST);
dependency.addEvidence(EvidenceType.PRODUCT, "cpanfile", "requires", fqName, Confidence.HIGHEST);
dependency.addEvidence(EvidenceType.VERSION, "cpanfile", "requires", version, Confidence.HIGHEST);
Identifier id = null;
try {
//note - namespace might be null and that's okay.
final PackageURL purl = PackageURLBuilder.aPackageURL()
.withType("cpan")
.withNamespace(namespace)
.withName(name)
.withVersion(version)
.build();
id = new PurlIdentifier(purl, Confidence.HIGH);
} catch (MalformedPackageURLException ex) {
LOGGER.debug("Error building package url for " + fqName + "; using generic identifier instead.", ex);
id = new GenericIdentifier("cpan:" + fqName + "::" + version, Confidence.HIGH);
}
dependency.setVersion(version);
dependency.setName(fqName);
dependency.addSoftwareIdentifier(id);
//sha1sum is used for anchor links in the HtML report
dependency.setSha1sum(Checksum.getSHA1Checksum(id.getValue()));
return dependency;
}).forEachOrdered(engine::addDependency);
}
}