View Javadoc
1   /*
2    * This file is part of dependency-check-core.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2018 Paul Irwin. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.analyzer;
19  
20  import com.github.packageurl.MalformedPackageURLException;
21  import com.github.packageurl.PackageURL;
22  import com.github.packageurl.PackageURLBuilder;
23  import java.io.File;
24  import org.owasp.dependencycheck.Engine;
25  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
26  import org.owasp.dependencycheck.data.nuget.MSBuildProjectParseException;
27  import org.owasp.dependencycheck.data.nuget.NugetPackageReference;
28  import org.owasp.dependencycheck.data.nuget.XPathMSBuildProjectParser;
29  import org.owasp.dependencycheck.dependency.Confidence;
30  import org.owasp.dependencycheck.dependency.Dependency;
31  import org.owasp.dependencycheck.dependency.EvidenceType;
32  import org.owasp.dependencycheck.exception.InitializationException;
33  import org.owasp.dependencycheck.utils.FileFilterBuilder;
34  import org.owasp.dependencycheck.utils.Settings;
35  import org.owasp.dependencycheck.utils.Checksum;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  import javax.annotation.concurrent.ThreadSafe;
40  import java.io.FileFilter;
41  import java.io.FileInputStream;
42  import java.io.FileNotFoundException;
43  import java.io.IOException;
44  import java.nio.file.Path;
45  import java.nio.file.Paths;
46  import java.util.HashMap;
47  import java.util.HashSet;
48  import java.util.List;
49  import java.util.Map;
50  import java.util.Properties;
51  import java.util.Set;
52  import org.apache.commons.io.input.BOMInputStream;
53  
54  import static org.owasp.dependencycheck.analyzer.NuspecAnalyzer.DEPENDENCY_ECOSYSTEM;
55  import org.owasp.dependencycheck.data.nuget.DirectoryBuildPropsParser;
56  import org.owasp.dependencycheck.data.nuget.DirectoryPackagesPropsParser;
57  import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
58  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
59  
60  /**
61   * Analyzes MS Project files for dependencies.
62   *
63   * @author Paul Irwin
64   */
65  @ThreadSafe
66  public class MSBuildProjectAnalyzer extends AbstractFileTypeAnalyzer {
67  
68      /**
69       * The logger.
70       */
71      private static final Logger LOGGER = LoggerFactory.getLogger(NuspecAnalyzer.class);
72  
73      /**
74       * The name of the analyzer.
75       */
76      private static final String ANALYZER_NAME = "MSBuild Project Analyzer";
77  
78      /**
79       * The phase in which the analyzer runs.
80       */
81      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
82  
83      /**
84       * The types of files on which this will work.
85       */
86      private static final String[] SUPPORTED_EXTENSIONS = new String[]{"csproj", "vbproj"};
87  
88      /**
89       * The file filter used to determine which files this analyzer supports.
90       */
91      private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(SUPPORTED_EXTENSIONS).build();
92      /**
93       * The import value to compare for GetDirectoryNameOfFileAbove.
94       */
95      private static final String IMPORT_GET_DIRECTORY = "$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..,"
96              + "Directory.Build.props))\\Directory.Build.props";
97      /**
98       * The import value to compare for GetPathOfFileAbove.
99       */
100     private static final String IMPORT_GET_PATH_OF_FILE = "$([MSBuild]::GetPathOfFileAbove('Directory.Build.props','"
101             + "$(MSBuildThisFileDirectory)../'))";
102     /**
103      * The msbuild properties file name.
104      */
105     private static final String DIRECTORY_BUILDPROPS = "Directory.Build.props";
106     /**
107      * The nuget centrally managed props file.
108      */
109     private static final String DIRECTORY_PACKAGESPROPS = "Directory.Packages.props";
110 
111     @Override
112     public String getName() {
113         return ANALYZER_NAME;
114     }
115 
116     @Override
117     public AnalysisPhase getAnalysisPhase() {
118         return ANALYSIS_PHASE;
119     }
120 
121     @Override
122     protected FileFilter getFileFilter() {
123         return FILTER;
124     }
125 
126     @Override
127     protected String getAnalyzerEnabledSettingKey() {
128         return Settings.KEYS.ANALYZER_MSBUILD_PROJECT_ENABLED;
129     }
130 
131     @Override
132     protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
133         // intentionally left blank
134     }
135 
136     @Override
137     @SuppressWarnings("StringSplitter")
138     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
139         final File parent = dependency.getActualFile().getParentFile();
140 
141         try {
142             //TODO while we are supporting props - we still do not support Directory.Build.targets
143             final Properties props = loadDirectoryBuildProps(parent);
144 
145             final Map<String, String> centrallyManaged = loadCentrallyManaged(parent, props);
146 
147             LOGGER.debug("Checking MSBuild project file {}", dependency);
148 
149             final XPathMSBuildProjectParser parser = new XPathMSBuildProjectParser();
150             final List<NugetPackageReference> packages;
151 
152             try (FileInputStream fis = new FileInputStream(dependency.getActualFilePath());
153                     BOMInputStream bis = BOMInputStream.builder().setInputStream(fis).get()) {
154                 //skip BOM if it exists
155                 bis.getBOM();
156                 packages = parser.parse(bis, props, centrallyManaged);
157             } catch (MSBuildProjectParseException | FileNotFoundException ex) {
158                 throw new AnalysisException(ex);
159             }
160 
161             if (packages == null || packages.isEmpty()) {
162                 return;
163             }
164 
165             for (NugetPackageReference npr : packages) {
166                 final Dependency child = new Dependency(dependency.getActualFile(), true);
167 
168                 final String id = npr.getId();
169                 final String version = npr.getVersion();
170 
171                 child.setEcosystem(DEPENDENCY_ECOSYSTEM);
172                 child.setName(id);
173                 child.setVersion(version);
174                 try {
175                     final PackageURL purl = PackageURLBuilder.aPackageURL().withType("nuget").withName(id).withVersion(version).build();
176                     child.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
177                 } catch (MalformedPackageURLException ex) {
178                     LOGGER.debug("Unable to build package url for msbuild", ex);
179                     final GenericIdentifier gid = new GenericIdentifier("msbuild:" + id + "@" + version, Confidence.HIGHEST);
180                     child.addSoftwareIdentifier(gid);
181                 }
182                 child.setPackagePath(String.format("%s:%s", id, version));
183                 child.setSha1sum(Checksum.getSHA1Checksum(String.format("%s:%s", id, version)));
184                 child.setSha256sum(Checksum.getSHA256Checksum(String.format("%s:%s", id, version)));
185                 child.setMd5sum(Checksum.getMD5Checksum(String.format("%s:%s", id, version)));
186 
187                 child.addEvidence(EvidenceType.PRODUCT, "msbuild", "id", id, Confidence.HIGHEST);
188                 child.addEvidence(EvidenceType.VENDOR, "msbuild", "id", id, Confidence.MEDIUM);
189                 child.addEvidence(EvidenceType.VERSION, "msbuild", "version", version, Confidence.HIGHEST);
190 
191                 if (id.indexOf('.') > 0) {
192                     final String[] parts = id.split("\\.");
193 
194                     // example: Microsoft.EntityFrameworkCore
195                     child.addEvidence(EvidenceType.VENDOR, "msbuild", "id", parts[0], Confidence.MEDIUM);
196                     child.addEvidence(EvidenceType.PRODUCT, "msbuild", "id", parts[1], Confidence.MEDIUM);
197                     child.addEvidence(EvidenceType.VENDOR, "msbuild", "id", parts[1], Confidence.LOW);
198 
199                     if (parts.length > 2) {
200                         final String rest = id.substring(id.indexOf('.') + 1);
201                         child.addEvidence(EvidenceType.PRODUCT, "msbuild", "id", rest, Confidence.MEDIUM);
202                         child.addEvidence(EvidenceType.VENDOR, "msbuild", "id", rest, Confidence.LOW);
203                     }
204                 } else {
205                     // example: jQuery
206                     child.addEvidence(EvidenceType.VENDOR, "msbuild", "id", id, Confidence.LOW);
207                 }
208 
209                 engine.addDependency(child);
210             }
211 
212         } catch (Throwable e) {
213             throw new AnalysisException(e);
214         }
215     }
216 
217     /**
218      * Attempts to load the `Directory.Build.props` file.
219      *
220      * @param directory the project directory.
221      * @return the properties from the Directory.Build.props.
222      * @throws MSBuildProjectParseException thrown if there is an error parsing
223      * the Directory.Build.props files.
224      */
225     private Properties loadDirectoryBuildProps(File directory) throws MSBuildProjectParseException {
226         final Properties props = new Properties();
227         if (directory == null || !directory.isDirectory()) {
228             return props;
229         }
230 
231         final File directoryProps = locateDirectoryBuildFile(DIRECTORY_BUILDPROPS, directory);
232         if (directoryProps != null) {
233             final Map<String, String> entries = readDirectoryBuildProps(directoryProps);
234 
235             if (entries != null) {
236                 for (Map.Entry<String, String> entry : entries.entrySet()) {
237                     props.put(entry.getKey(), entry.getValue());
238                 }
239             }
240         }
241         return props;
242     }
243 
244     /**
245      * Walk the current directory up to find `Directory.Build.props`.
246      *
247      * @param name the name of the build file to load.
248      * @param directory the directory to begin searching at.
249      * @return the `Directory.Build.props` file if found; otherwise null.
250      */
251     private File locateDirectoryBuildFile(String name, File directory) {
252         File search = directory;
253         while (search != null && search.isDirectory()) {
254             final File props = new File(search, name);
255             if (props.isFile()) {
256                 return props;
257             }
258             search = search.getParentFile();
259         }
260         return null;
261     }
262 
263     /**
264      * Exceedingly naive processing of MSBuild Import statements. Only four
265      * cases are supported:
266      * <ul>
267      * <li>A relative path to the import</li>
268      * <li>$(MSBuildThisFileDirectory)../path.to.props</li>
269      * <li>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props',
270      * '$(MSBuildThisFileDirectory)../'))</li>
271      * <li>$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..,
272      * Directory.Build.props))\Directory.Build.props</li>
273      * </ul>
274      *
275      * @param importStatement the import statement
276      * @param currentFile the props file containing the import
277      * @return a reference to the file if it could be found, otherwise null.
278      */
279     private File getImport(String importStatement, File currentFile) {
280         //<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
281         //<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory).., Directory.Build.props))\Directory.Build.props" />
282         if (importStatement == null || importStatement.isEmpty()) {
283             return null;
284         }
285         if (importStatement.startsWith("$")) {
286             final String compact = importStatement.replaceAll("\\s", "");
287             if (IMPORT_GET_PATH_OF_FILE.equalsIgnoreCase(compact)
288                     || IMPORT_GET_DIRECTORY.equalsIgnoreCase(compact)) {
289                 return locateDirectoryBuildFile("Directory.Build.props", currentFile.getParentFile().getParentFile());
290             } else if (importStatement.startsWith("$(MSBuildThisFileDirectory)")) {
291                 final String path = importStatement.substring(27);
292                 final File currentDirectory = currentFile.getParentFile();
293                 final Path p = Paths.get(currentDirectory.getAbsolutePath(),
294                         path.replace('\\', File.separatorChar).replace('/', File.separatorChar));
295                 final File f = p.normalize().toFile();
296                 if (f.isFile() && !f.equals(currentFile)) {
297                     return f;
298                 }
299             }
300         } else {
301             final File currentDirectory = currentFile.getParentFile();
302             final Path p = Paths.get(currentDirectory.getAbsolutePath(),
303                     importStatement.replace('\\', File.separatorChar).replace('/', File.separatorChar));
304 
305             final File f = p.normalize().toFile();
306 
307             if (f.isFile() && !f.equals(currentFile)) {
308                 return f;
309             }
310         }
311         LOGGER.warn("Unable to import Directory.Build.props import `{}` in `{}`", importStatement, currentFile);
312         return null;
313     }
314 
315     private Map<String, String> readDirectoryBuildProps(File directoryProps) throws MSBuildProjectParseException {
316         Map<String, String> entries = null;
317         final Set<String> imports = new HashSet<>();
318         if (directoryProps != null && directoryProps.isFile()) {
319             final DirectoryBuildPropsParser parser = new DirectoryBuildPropsParser();
320             try (FileInputStream fis = new FileInputStream(directoryProps);
321                     BOMInputStream bis = BOMInputStream.builder().setInputStream(fis).get()) {
322                 //skip BOM if it exists
323                 bis.getBOM();
324                 entries = parser.parse(bis);
325                 imports.addAll(parser.getImports());
326             } catch (IOException ex) {
327                 throw new MSBuildProjectParseException("Error reading Directory.Build.props", ex);
328             }
329 
330             for (String importStatement : imports) {
331                 final File parentBuildProps = getImport(importStatement, directoryProps);
332                 if (parentBuildProps != null && !directoryProps.equals(parentBuildProps)) {
333                     final Map<String, String> parentEntries = readDirectoryBuildProps(parentBuildProps);
334                     if (parentEntries != null) {
335                         parentEntries.putAll(entries);
336                         entries = parentEntries;
337                     }
338                 }
339             }
340             return entries;
341         }
342         return null;
343     }
344 
345     private Map<String, String> loadCentrallyManaged(File folder, Properties props) throws MSBuildProjectParseException {
346         final File packages = locateDirectoryBuildFile(DIRECTORY_PACKAGESPROPS, folder);
347         if (packages != null && packages.isFile()) {
348             final DirectoryPackagesPropsParser parser = new DirectoryPackagesPropsParser();
349             try (FileInputStream fis = new FileInputStream(packages);
350                     BOMInputStream bis = BOMInputStream.builder().setInputStream(fis).get()) {
351                 //skip BOM if it exists
352                 bis.getBOM();
353                 return parser.parse(bis, props);
354             } catch (IOException ex) {
355                 throw new MSBuildProjectParseException("Error reading Directory.Build.props", ex);
356             }
357         }
358         return new HashMap<>();
359     }
360 
361 }