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.VERSION, "msbuild", "version", version, Confidence.HIGHEST);
189 
190                 if (id.indexOf('.') > 0) {
191                     final String[] parts = id.split("\\.");
192 
193                     // example: Microsoft.EntityFrameworkCore
194                     child.addEvidence(EvidenceType.VENDOR, "msbuild", "id", parts[0], Confidence.MEDIUM);
195                     child.addEvidence(EvidenceType.PRODUCT, "msbuild", "id", parts[1], Confidence.MEDIUM);
196 
197                     if (parts.length > 2) {
198                         final String rest = id.substring(id.indexOf('.') + 1);
199                         child.addEvidence(EvidenceType.PRODUCT, "msbuild", "id", rest, Confidence.MEDIUM);
200                     }
201                 } else {
202                     // example: jQuery
203                     child.addEvidence(EvidenceType.VENDOR, "msbuild", "id", id, Confidence.LOW);
204                 }
205 
206                 engine.addDependency(child);
207             }
208 
209         } catch (Throwable e) {
210             throw new AnalysisException(e);
211         }
212     }
213 
214     /**
215      * Attempts to load the `Directory.Build.props` file.
216      *
217      * @param directory the project directory.
218      * @return the properties from the Directory.Build.props.
219      * @throws MSBuildProjectParseException thrown if there is an error parsing
220      * the Directory.Build.props files.
221      */
222     private Properties loadDirectoryBuildProps(File directory) throws MSBuildProjectParseException {
223         final Properties props = new Properties();
224         if (directory == null || !directory.isDirectory()) {
225             return props;
226         }
227 
228         final File directoryProps = locateDirectoryBuildFile(DIRECTORY_BUILDPROPS, directory);
229         if (directoryProps != null) {
230             final Map<String, String> entries = readDirectoryBuildProps(directoryProps);
231 
232             if (entries != null) {
233                 for (Map.Entry<String, String> entry : entries.entrySet()) {
234                     props.put(entry.getKey(), entry.getValue());
235                 }
236             }
237         }
238         return props;
239     }
240 
241     /**
242      * Walk the current directory up to find `Directory.Build.props`.
243      *
244      * @param name the name of the build file to load.
245      * @param directory the directory to begin searching at.
246      * @return the `Directory.Build.props` file if found; otherwise null.
247      */
248     private File locateDirectoryBuildFile(String name, File directory) {
249         File search = directory;
250         while (search != null && search.isDirectory()) {
251             final File props = new File(search, name);
252             if (props.isFile()) {
253                 return props;
254             }
255             search = search.getParentFile();
256         }
257         return null;
258     }
259 
260     /**
261      * Exceedingly naive processing of MSBuild Import statements. Only four
262      * cases are supported:
263      * <ul>
264      * <li>A relative path to the import</li>
265      * <li>$(MSBuildThisFileDirectory)../path.to.props</li>
266      * <li>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props',
267      * '$(MSBuildThisFileDirectory)../'))</li>
268      * <li>$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..,
269      * Directory.Build.props))\Directory.Build.props</li>
270      * </ul>
271      *
272      * @param importStatement the import statement
273      * @param currentFile the props file containing the import
274      * @return a reference to the file if it could be found, otherwise null.
275      */
276     private File getImport(String importStatement, File currentFile) {
277         //<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
278         //<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory).., Directory.Build.props))\Directory.Build.props" />
279         if (importStatement == null || importStatement.isEmpty()) {
280             return null;
281         }
282         if (importStatement.startsWith("$")) {
283             final String compact = importStatement.replaceAll("\\s", "");
284             if (IMPORT_GET_PATH_OF_FILE.equalsIgnoreCase(compact)
285                     || IMPORT_GET_DIRECTORY.equalsIgnoreCase(compact)) {
286                 return locateDirectoryBuildFile("Directory.Build.props", currentFile.getParentFile().getParentFile());
287             } else if (importStatement.startsWith("$(MSBuildThisFileDirectory)")) {
288                 final String path = importStatement.substring(27);
289                 final File currentDirectory = currentFile.getParentFile();
290                 final Path p = Paths.get(currentDirectory.getAbsolutePath(),
291                         path.replace('\\', File.separatorChar).replace('/', File.separatorChar));
292                 final File f = p.normalize().toFile();
293                 if (f.isFile() && !f.equals(currentFile)) {
294                     return f;
295                 }
296             }
297         } else {
298             final File currentDirectory = currentFile.getParentFile();
299             final Path p = Paths.get(currentDirectory.getAbsolutePath(),
300                     importStatement.replace('\\', File.separatorChar).replace('/', File.separatorChar));
301 
302             final File f = p.normalize().toFile();
303 
304             if (f.isFile() && !f.equals(currentFile)) {
305                 return f;
306             }
307         }
308         LOGGER.warn("Unable to import Directory.Build.props import `{}` in `{}`", importStatement, currentFile);
309         return null;
310     }
311 
312     private Map<String, String> readDirectoryBuildProps(File directoryProps) throws MSBuildProjectParseException {
313         Map<String, String> entries = null;
314         final Set<String> imports = new HashSet<>();
315         if (directoryProps != null && directoryProps.isFile()) {
316             final DirectoryBuildPropsParser parser = new DirectoryBuildPropsParser();
317             try (FileInputStream fis = new FileInputStream(directoryProps);
318                     BOMInputStream bis = BOMInputStream.builder().setInputStream(fis).get()) {
319                 //skip BOM if it exists
320                 bis.getBOM();
321                 entries = parser.parse(bis);
322                 imports.addAll(parser.getImports());
323             } catch (IOException ex) {
324                 throw new MSBuildProjectParseException("Error reading Directory.Build.props", ex);
325             }
326 
327             for (String importStatement : imports) {
328                 final File parentBuildProps = getImport(importStatement, directoryProps);
329                 if (parentBuildProps != null && !directoryProps.equals(parentBuildProps)) {
330                     final Map<String, String> parentEntries = readDirectoryBuildProps(parentBuildProps);
331                     if (parentEntries != null) {
332                         parentEntries.putAll(entries);
333                         entries = parentEntries;
334                     }
335                 }
336             }
337             return entries;
338         }
339         return null;
340     }
341 
342     private Map<String, String> loadCentrallyManaged(File folder, Properties props) throws MSBuildProjectParseException {
343         final File packages = locateDirectoryBuildFile(DIRECTORY_PACKAGESPROPS, folder);
344         if (packages != null && packages.isFile()) {
345             final DirectoryPackagesPropsParser parser = new DirectoryPackagesPropsParser();
346             try (FileInputStream fis = new FileInputStream(packages);
347                     BOMInputStream bis = BOMInputStream.builder().setInputStream(fis).get()) {
348                 //skip BOM if it exists
349                 bis.getBOM();
350                 return parser.parse(bis, props);
351             } catch (IOException ex) {
352                 throw new MSBuildProjectParseException("Error reading Directory.Build.props", ex);
353             }
354         }
355         return new HashMap<>();
356     }
357 
358 }