Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support msbuild Directory.build.props #5475

Merged
merged 16 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import com.github.packageurl.PackageURLBuilder;
import java.io.File;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.data.nuget.MSBuildProjectParseException;
import org.owasp.dependencycheck.data.nuget.MSBuildProjectParser;
import org.owasp.dependencycheck.data.nuget.NugetPackageReference;
import org.owasp.dependencycheck.data.nuget.XPathMSBuildProjectParser;
import org.owasp.dependencycheck.dependency.Confidence;
Expand All @@ -40,10 +40,20 @@
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.io.input.BOMInputStream;

import static org.owasp.dependencycheck.analyzer.NuspecAnalyzer.DEPENDENCY_ECOSYSTEM;
import org.owasp.dependencycheck.data.nuget.DirectoryBuildPropsParser;
import org.owasp.dependencycheck.data.nuget.DirectoryPackagesPropsParser;
import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;

Expand Down Expand Up @@ -79,6 +89,24 @@ public class MSBuildProjectAnalyzer extends AbstractFileTypeAnalyzer {
* The file filter used to determine which files this analyzer supports.
*/
private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(SUPPORTED_EXTENSIONS).build();
/**
* The import value to compare for GetDirectoryNameOfFileAbove.
*/
private static final String IMPORT_GET_DIRECTORY = "$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..,"
+ "Directory.Build.props))\\Directory.Build.props";
/**
* The import value to compare for GetPathOfFileAbove.
*/
private static final String IMPORT_GET_PATH_OF_FILE = "$([MSBuild]::GetPathOfFileAbove('Directory.Build.props','"
+ "$(MSBuildThisFileDirectory)../'))";
/**
* The msbuild properties file name.
*/
private static final String DIRECTORY_BUILDPROPS = "Directory.Build.props";
/**
* The nuget centrally managed props file.
*/
private static final String DIRECTORY_PACKAGESPROPS = "Directory.Packages.props";

@Override
public String getName() {
Expand Down Expand Up @@ -108,16 +136,24 @@ protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationExcep
@Override
@SuppressWarnings("StringSplitter")
protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
LOGGER.debug("Checking MSBuild project file {}", dependency);
final File parent = dependency.getActualFile().getParentFile();

try {
final MSBuildProjectParser parser = new XPathMSBuildProjectParser();
//TODO while we are supporting props - we still do not support Directory.Build.targets
final Properties props = loadDirectoryBuildProps(parent);

final Map<String, String> centrallyManaged = loadCentrallyManaged(parent, props);

LOGGER.debug("Checking MSBuild project file {}", dependency);

final XPathMSBuildProjectParser parser = new XPathMSBuildProjectParser();
final List<NugetPackageReference> packages;

try (FileInputStream fis = new FileInputStream(dependency.getActualFilePath());
BOMInputStream bis = new BOMInputStream(fis)) {
//skip BOM if it exists
bis.getBOM();
packages = parser.parse(bis);
packages = parser.parse(bis, props, centrallyManaged);
} catch (MSBuildProjectParseException | FileNotFoundException ex) {
throw new AnalysisException(ex);
}
Expand Down Expand Up @@ -175,4 +211,145 @@ protected void analyzeDependency(Dependency dependency, Engine engine) throws An
}
}

/**
* Attempts to load the `Directory.Build.props` file.
*
* @param directory the project directory.
* @return the properties from the Directory.Build.props.
* @throws MSBuildProjectParseException thrown if there is an error parsing
* the Directory.Build.props files.
*/
private Properties loadDirectoryBuildProps(File directory) throws MSBuildProjectParseException {
final Properties props = new Properties();
if (directory == null || !directory.isDirectory()) {
return props;
}

final File directoryProps = locateDirectoryBuildFile(DIRECTORY_BUILDPROPS, directory);
final Map<String, String> entries = readDirectoryBuildProps(directoryProps);

for (Map.Entry<String, String> entry : entries.entrySet()) {
props.put(entry.getKey(), entry.getValue());
}

return props;
}

/**
* Walk the current directory up to find `Directory.Build.props`.
*
* @param name the name of the build file to load.
* @param directory the directory to begin searching at.
* @return the `Directory.Build.props` file if found; otherwise null.
*/
private File locateDirectoryBuildFile(String name, File directory) {
File search = directory;
while (search != null && search.isDirectory()) {
final File props = new File(search, name);
if (props.isFile()) {
return props;
}
search = search.getParentFile();
}
return null;
}

/**
* Exceedingly naive processing of MSBuild Import statements. Only four
* cases are supported:
* <ul>
* <li>A relative path to the import</li>
* <li>$(MSBuildThisFileDirectory)../path.to.props</li>
* <li>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props',
* '$(MSBuildThisFileDirectory)../'))</li>
* <li>$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..,
* Directory.Build.props))\Directory.Build.props</li>
* </ul>
*
* @param importStatement the import statement
* @param currentFile the props file containing the import
* @return a reference to the file if it could be found, otherwise null.
*/
private File getImport(String importStatement, File currentFile) {
//<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
//<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory).., Directory.Build.props))\Directory.Build.props" />
if (importStatement == null || importStatement.isEmpty()) {
return null;
}
if (importStatement.startsWith("$")) {
final String compact = importStatement.replaceAll("\\s", "");
if (IMPORT_GET_PATH_OF_FILE.equalsIgnoreCase(compact)
|| IMPORT_GET_DIRECTORY.equalsIgnoreCase(compact)) {
return locateDirectoryBuildFile("Directory.Build.props", currentFile.getParentFile().getParentFile());
} else if (importStatement.startsWith("$(MSBuildThisFileDirectory)")) {
final String path = importStatement.substring(27);
final File currentDirectory = currentFile.getParentFile();
final Path p = Paths.get(currentDirectory.getAbsolutePath(),
path.replace('\\', File.separatorChar).replace('/', File.separatorChar));
final File f = p.normalize().toFile();
if (f.isFile() && !f.equals(currentFile)) {
return f;
}
}
} else {
final File currentDirectory = currentFile.getParentFile();
final Path p = Paths.get(currentDirectory.getAbsolutePath(),
importStatement.replace('\\', File.separatorChar).replace('/', File.separatorChar));

final File f = p.normalize().toFile();

if (f.isFile() && !f.equals(currentFile)) {
return f;
}
}
LOGGER.warn("Unable to import Directory.Build.props import `{}` in `{}`", importStatement, currentFile);
return null;
}

private Map<String, String> readDirectoryBuildProps(File directoryProps) throws MSBuildProjectParseException {
Map<String, String> entries = null;
final Set<String> imports = new HashSet<>();
if (directoryProps != null && directoryProps.isFile()) {
final DirectoryBuildPropsParser parser = new DirectoryBuildPropsParser();
try (FileInputStream fis = new FileInputStream(directoryProps);
BOMInputStream bis = new BOMInputStream(fis)) {
//skip BOM if it exists
bis.getBOM();
entries = parser.parse(bis);
imports.addAll(parser.getImports());
} catch (IOException ex) {
throw new MSBuildProjectParseException("Error reading Directory.Build.props", ex);
}

for (String importStatement : imports) {
final File parentBuildProps = getImport(importStatement, directoryProps);
if (!directoryProps.equals(parentBuildProps)) {
final Map<String, String> parentEntries = readDirectoryBuildProps(parentBuildProps);
if (parentEntries != null) {
parentEntries.putAll(entries);
entries = parentEntries;
}
}
}
return entries;
}
return null;
}

private Map<String, String> loadCentrallyManaged(File folder, Properties props) throws MSBuildProjectParseException {
final File packages = locateDirectoryBuildFile(DIRECTORY_PACKAGESPROPS, folder);
if (packages != null && packages.isFile()) {
final DirectoryPackagesPropsParser parser = new DirectoryPackagesPropsParser();
try (FileInputStream fis = new FileInputStream(packages);
BOMInputStream bis = new BOMInputStream(fis)) {
//skip BOM if it exists
bis.getBOM();
return parser.parse(bis, props);
} catch (IOException ex) {
throw new MSBuildProjectParseException("Error reading Directory.Build.props", ex);
}
}
return new HashMap<>();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.data.nuget.NugetPackageReference;
import org.owasp.dependencycheck.data.nuget.NugetconfParseException;
import org.owasp.dependencycheck.data.nuget.NugetconfParser;
import org.owasp.dependencycheck.data.nuget.XPathNugetconfParser;
import org.owasp.dependencycheck.dependency.Confidence;
import org.owasp.dependencycheck.dependency.Dependency;
Expand Down Expand Up @@ -150,7 +149,7 @@ protected FileFilter getFileFilter() {
public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
LOGGER.debug("Checking packages.config file {}", dependency);
try {
final NugetconfParser parser = new XPathNugetconfParser();
final XPathNugetconfParser parser = new XPathNugetconfParser();
final List<NugetPackageReference> packages;
try (FileInputStream fis = new FileInputStream(dependency.getActualFilePath())) {
packages = parser.parse(fis);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.data.nuget.NugetPackage;
import org.owasp.dependencycheck.data.nuget.NuspecParseException;
import org.owasp.dependencycheck.data.nuget.NuspecParser;
import org.owasp.dependencycheck.data.nuget.XPathNuspecParser;
import org.owasp.dependencycheck.dependency.Confidence;
import org.owasp.dependencycheck.dependency.Dependency;
Expand Down Expand Up @@ -144,7 +143,7 @@ protected FileFilter getFileFilter() {
public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
LOGGER.debug("Checking Nuspec file {}", dependency);
try {
final NuspecParser parser = new XPathNuspecParser();
final XPathNuspecParser parser = new XPathNuspecParser();
final NugetPackage np;
try (FileInputStream fis = new FileInputStream(dependency.getActualFilePath())) {
np = parser.parse(fis);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* 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) 2023 Jeremy Long. All Rights Reserved.
*/
package org.owasp.dependencycheck.data.nuget;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.owasp.dependencycheck.utils.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
* Parses `Directory.Build.props`.
*
* @see
* <a href="https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-your-build?view=vs-2019">Directory.Build.props</a>
* @author Jeremy Long
*/
public class DirectoryBuildPropsParser {

/**
* The collection of imports identified during parsing.
*/
private Set<String> imports = new HashSet<>();

/**
* Returns the imports identified during parsing.
*
* @return the imports identified during parsing.
*/
public Set<String> getImports() {
return imports;
}

/**
* Parse the properties from the `Directory.Build.props` file InputStream.If
* any import nodes are found while parsing, the values will be available
* via `getImports()` after parsing is complete.
*
* @param stream the input stream containing the props file to parse.
* @return the properties.
* @throws MSBuildProjectParseException thrown if there is a parsing error.
*/
public Map<String, String> parse(InputStream stream) throws MSBuildProjectParseException {
try {
final HashMap<String, String> props = new HashMap<>();

final DocumentBuilder db = XmlUtils.buildSecureDocumentBuilder();
final Document d = db.parse(stream);

final XPath xpath = XPathFactory.newInstance().newXPath();

//<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
final NodeList importList = (NodeList) xpath.evaluate("//Import", d, XPathConstants.NODESET);
if (importList != null) {
for (int i = 0; i < importList.getLength(); i++) {
final Node importNode = importList.item(i);
final Node project = importNode.getAttributes().getNamedItem("Project");
imports.add(project.getNodeValue());
}
}
final NodeList propertyGroups = (NodeList) xpath.evaluate("//PropertyGroup", d, XPathConstants.NODESET);
if (propertyGroups != null) {
for (int i = 0; i < propertyGroups.getLength(); i++) {
final Node group = propertyGroups.item(i);
final NodeList propertyNodes = group.getChildNodes();
for (int x = 0; x < propertyNodes.getLength(); x++) {
final Node node = propertyNodes.item(x);
if (node instanceof Element) {
final Element property = (Element) node;
final String name = property.getNodeName();
final Node value = property.getChildNodes().item(0);
if (value != null) {
props.put(name, value.getNodeValue().trim());
}
}
}
}
}
return props;
} catch (ParserConfigurationException | SAXException | IOException | XPathExpressionException ex) {
throw new MSBuildProjectParseException("Error parsing Directory.Build.props", ex);
}
}
}
Loading