CMakeAnalyzer.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) 2015 Institute for Defense Analyses. All Rights Reserved.
*/
package org.owasp.dependencycheck.analyzer;
import org.apache.commons.lang3.StringUtils;
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.exception.InitializationException;
import org.owasp.dependencycheck.utils.Checksum;
import org.owasp.dependencycheck.utils.DependencyVersion;
import org.owasp.dependencycheck.utils.DependencyVersionUtil;
import org.owasp.dependencycheck.utils.FileFilterBuilder;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>
* Used to analyze CMake build files, and collect information that can be used
* to determine the associated CPE.</p>
* <p>
* Note: This analyzer catches straightforward invocations of the project
* command, plus some other observed patterns of version inclusion in real CMake
* projects. Many projects make use of older versions of CMake and/or use custom
* "homebrew" ways to insert version information. Hopefully as the newer CMake
* call pattern grows in usage, this analyzer allow more CPEs to be
* identified.</p>
*
* @author Dale Visser
*/
@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 = Ecosystem.NATIVE;
/**
* The logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(CMakeAnalyzer.class);
/**
* Used when compiling file scanning regex patterns.
*/
private static final int REGEX_OPTIONS = Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE;
/**
* Regex to obtain the project version.
*/
private static final Pattern PROJECT_VERSION = Pattern.compile("^\\s*set\\s*\\(\\s*VERSION\\s*\"([^\"]*)\"\\)",
REGEX_OPTIONS);
/**
* Regex to obtain variables.
*/
private static final Pattern SET_VAR_REGEX = Pattern.compile(
"^\\s*set\\s*\\(\\s*([a-zA-Z\\d_\\-]*)\\s+\"?([a-zA-Z\\d_\\-.${}]*)\"?\\s*\\)", REGEX_OPTIONS);
/**
* Regex to find inlined variables to replace them.
*/
private static final Pattern INL_VAR_REGEX = Pattern.compile("(\\$\\s*\\{([^}]*)\\s*})", REGEX_OPTIONS);
/**
* Regex to extract the product information.
*/
private static final Pattern PROJECT = Pattern.compile("^ *project *\\([ \\n]*(\\w+)[ \\n]*.*?\\)", REGEX_OPTIONS);
/**
* Regex to extract product and version information.
*
* <p>Group 1: Product</p>
* <p>Group 2: Version</p>
*/
private static final Pattern SET_VERSION = Pattern
.compile("^\\s*set\\s*\\(\\s*(\\w+)_version\\s+\"?([^\")]*)\\s*\"?\\)", REGEX_OPTIONS);
/**
* Detects files that can be analyzed.
*/
private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(".cmake")
.addFilenames("CMakeLists.txt").build();
/**
* Returns the name of the CMake analyzer.
*
* @return the name of the analyzer
*/
@Override
public String getName() {
return "CMake Analyzer";
}
/**
* Tell that we are used for information collection.
*
* @return INFORMATION_COLLECTION
*/
@Override
public AnalysisPhase getAnalysisPhase() {
return AnalysisPhase.INFORMATION_COLLECTION;
}
/**
* Returns the set of supported file extensions.
*
* @return the set of supported file extensions
*/
@Override
protected FileFilter getFileFilter() {
return FILTER;
}
/**
* Initializes the analyzer.
*
* @param engine a reference to the dependency-check engine
* @throws InitializationException thrown if an exception occurs getting an
* instance of SHA1
*/
@Override
protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
//do nothing
}
/**
* Analyzes python packages and adds evidence to the dependency.
*
* @param dependency the dependency being analyzed
* @param engine the engine being used to perform the scan
* @throws AnalysisException thrown if there is an unrecoverable error
* analyzing the dependency
*/
@Override
protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
final File file = dependency.getActualFile();
final String name = file.getName();
final String contents;
try {
contents = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8).trim();
} catch (IOException e) {
throw new AnalysisException(
"Problem occurred while reading dependency file.", e);
}
if (StringUtils.isNotBlank(contents)) {
final Map<String, String> vars = collectDefinedVariables(contents);
String contentsReplacer = contents;
Matcher r = INL_VAR_REGEX.matcher(contents);
while (r.find()) {
boolean leastOne = false;
if (vars.containsKey(r.group(2))) {
if (!vars.get(r.group(2)).contains(r.group(2))) {
contentsReplacer = contentsReplacer.replace(r.group(1), vars.get(r.group(2)));
r = INL_VAR_REGEX.matcher(contentsReplacer);
leastOne = true;
}
}
while (r.find()) {
if (vars.containsKey(r.group(2))) {
if (!vars.get(r.group(2)).contains(r.group(2))) {
contentsReplacer = contentsReplacer.replace(r.group(1), vars.get(r.group(2)));
r = INL_VAR_REGEX.matcher(contentsReplacer);
leastOne = true;
}
}
}
if (!leastOne) {
break;
}
r = INL_VAR_REGEX.matcher(contentsReplacer);
}
final String contentsReplaced = contentsReplacer;
final Matcher m = PROJECT.matcher(contentsReplaced);
int count = 0;
while (m.find()) {
count++;
LOGGER.debug(String.format(
"Found project command match with %d groups: %s",
m.groupCount(), m.group(0)));
final String group = m.group(1);
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);
dependency.setDisplayFileName(group);
}
if (count > 0) {
dependency.addEvidence(EvidenceType.VENDOR, "CmakeAnalyzer", "hint", "gnu", Confidence.MEDIUM);
}
LOGGER.debug("Found {} matches.", count);
final Matcher mVersion = PROJECT_VERSION.matcher(contentsReplaced);
while (mVersion.find()) {
LOGGER.debug(String.format(
"Found set version command match with %d groups: %s",
mVersion.groupCount(), mVersion.group(0)));
final String group = mVersion.group(1);
LOGGER.debug("Group 1: {}", group);
dependency.addEvidence(EvidenceType.VERSION, name, "VERSION", group, Confidence.HIGH);
final DependencyVersion vers = DependencyVersionUtil.parseVersion(group, true);
if (vers != null) {
dependency.setVersion(vers.toString());
}
}
analyzeSetVersionCommand(dependency, engine, contentsReplaced);
}
}
/**
* Collect defined CMake variables
*
* @param contents the version information
*
* @return a map referencing identified variables
*/
private Map<String, String> collectDefinedVariables(String contents) {
final Map<String, String> vars = new HashMap<>();
final Matcher m = SET_VAR_REGEX.matcher(contents);
int count = 0;
while (m.find()) {
count++;
LOGGER.debug("Found set variable command match with {} groups: {}",
m.groupCount(), m.group(0));
final String name = m.group(1);
final String value = m.group(2);
LOGGER.debug("Group 1: {}", name);
LOGGER.debug("Group 2: {}", value);
vars.put(name, value);
}
LOGGER.debug("Found {} matches.", count);
return removeSelfReferences(vars);
}
/**
* Extracts the version information from the contents. If more then one
* version is found additional dependencies are added to the dependency
* list.
*
* @param dependency the dependency being analyzed
* @param engine the dependency-check engine
* @param contents the version information
*/
private void analyzeSetVersionCommand(Dependency dependency, Engine engine, String contents) {
Dependency currentDep = dependency;
final Matcher m = SET_VERSION.matcher(contents);
int count = 0;
while (m.find()) {
count++;
LOGGER.debug("Found project command match with {} groups: {}",
m.groupCount(), m.group(0));
String product = m.group(1);
final String version = m.group(2);
LOGGER.debug("Group 1: {}", product);
LOGGER.debug("Group 2: {}", version);
final String aliasPrefix = "ALIASOF_";
if (product.startsWith(aliasPrefix)) {
product = product.replaceFirst(aliasPrefix, "");
}
if (product.startsWith("_")) {
product = product.substring(1);
}
if (count > 1) {
currentDep = new Dependency(dependency.getActualFile(), true);
currentDep.setEcosystem(DEPENDENCY_ECOSYSTEM);
final String filePath = String.format("%s:%s", dependency.getFilePath(), product);
currentDep.setFilePath(filePath);
currentDep.setSha1sum(Checksum.getSHA1Checksum(filePath));
currentDep.setSha256sum(Checksum.getSHA256Checksum(filePath));
currentDep.setMd5sum(Checksum.getMD5Checksum(filePath));
engine.addDependency(currentDep);
}
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);
if (product.toLowerCase().endsWith("lib")) {
currentDep = new Dependency(dependency.getActualFile(), true);
currentDep.setEcosystem(DEPENDENCY_ECOSYSTEM);
final String filePath = String.format("%s:%s", dependency.getFilePath(), product);
currentDep.setFilePath(filePath);
currentDep.setSha1sum(Checksum.getSHA1Checksum(filePath));
currentDep.setSha256sum(Checksum.getSHA256Checksum(filePath));
currentDep.setMd5sum(Checksum.getMD5Checksum(filePath));
engine.addDependency(currentDep);
product = "lib" + product.toLowerCase().substring(0, product.length() - 3);
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);
}
if (StringUtils.isBlank(currentDep.getName())) {
currentDep.setName(product);
currentDep.setDisplayFileName(product);
}
if (StringUtils.isBlank(currentDep.getVersion())) {
final DependencyVersion vers = DependencyVersionUtil.parseVersion(version, true);
if (vers != null) {
currentDep.setVersion(vers.toString());
}
}
}
LOGGER.debug("Found {} matches.", count);
}
@Override
protected String getAnalyzerEnabledSettingKey() {
return Settings.KEYS.ANALYZER_CMAKE_ENABLED;
}
/**
* This method prevents to generate an infinite loop when variables are
* initialized by other variables and end up forming an unresolvable
* chain.
*
* <p>This method takes the resolved variables map as an input and will return
* a new map, without the keys generating an infinite resolution chain.</p>
*
* @param vars variables initialization detected in the CMake build file
*
* @return a new map without infinite chain variables
*/
Map<String, String> removeSelfReferences(final Map<String, String> vars) {
final Map<String, String> resolvedVars = new HashMap<>();
vars.forEach((key, value) -> {
if (!isVariableSelfReferencing(vars, key)) {
resolvedVars.put(key, value);
}
});
return resolvedVars;
}
private boolean isVariableSelfReferencing(Map<String, String> vars, String key) {
final List<String> resolutionChain = new ArrayList<>();
resolutionChain.add(key);
String nextKey = resolutionChain.get(0);
do {
final Matcher matcher = INL_VAR_REGEX.matcher(vars.get(nextKey));
if (!matcher.find()) {
break;
}
nextKey = matcher.group(2);
if (Objects.nonNull(nextKey) && resolutionChain.contains(nextKey)) {
return true;
}
resolutionChain.add(nextKey);
} while (Objects.nonNull(nextKey) && vars.containsKey(nextKey) && !key.equals(nextKey));
return resolutionChain.size() != 1 && key.equals(nextKey);
}
}