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) 2015 Institute for Defense Analyses. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.analyzer;
19  
20  import org.apache.commons.io.FileUtils;
21  import org.apache.commons.lang3.StringUtils;
22  import org.owasp.dependencycheck.Engine;
23  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
24  import org.owasp.dependencycheck.dependency.Confidence;
25  import org.owasp.dependencycheck.dependency.Dependency;
26  import org.owasp.dependencycheck.utils.Checksum;
27  import org.owasp.dependencycheck.utils.FileFilterBuilder;
28  import org.owasp.dependencycheck.utils.Settings;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  import java.io.File;
33  import java.io.FileFilter;
34  import java.io.IOException;
35  import java.io.UnsupportedEncodingException;
36  import java.nio.charset.Charset;
37  import java.security.MessageDigest;
38  import java.security.NoSuchAlgorithmException;
39  import java.util.regex.Matcher;
40  import java.util.regex.Pattern;
41  import org.owasp.dependencycheck.exception.InitializationException;
42  
43  /**
44   * <p>
45   * Used to analyze CMake build files, and collect information that can be used
46   * to determine the associated CPE.</p>
47   * <p>
48   * Note: This analyzer catches straightforward invocations of the project
49   * command, plus some other observed patterns of version inclusion in real CMake
50   * projects. Many projects make use of older versions of CMake and/or use custom
51   * "homebrew" ways to insert version information. Hopefully as the newer CMake
52   * call pattern grows in usage, this analyzer allow more CPEs to be
53   * identified.</p>
54   *
55   * @author Dale Visser
56   */
57  @Experimental
58  public class CMakeAnalyzer extends AbstractFileTypeAnalyzer {
59  
60      /**
61       * The logger.
62       */
63      private static final Logger LOGGER = LoggerFactory.getLogger(CMakeAnalyzer.class);
64  
65      /**
66       * Used when compiling file scanning regex patterns.
67       */
68      private static final int REGEX_OPTIONS = Pattern.DOTALL
69              | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE;
70  
71      /**
72       * Regex to extract the product information.
73       */
74      private static final Pattern PROJECT = Pattern.compile(
75              "^ *project *\\([ \\n]*(\\w+)[ \\n]*.*?\\)", REGEX_OPTIONS);
76  
77      /**
78       * Regex to extract product and version information.
79       *
80       * Group 1: Product
81       *
82       * Group 2: Version
83       */
84      private static final Pattern SET_VERSION = Pattern
85              .compile(
86                      "^ *set\\s*\\(\\s*(\\w+)_version\\s+\"?(\\d+(?:\\.\\d+)+)[\\s\"]?\\)",
87                      REGEX_OPTIONS);
88  
89      /**
90       * Detects files that can be analyzed.
91       */
92      private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(".cmake")
93              .addFilenames("CMakeLists.txt").build();
94  
95      /**
96       * Returns the name of the CMake analyzer.
97       *
98       * @return the name of the analyzer
99       */
100     @Override
101     public String getName() {
102         return "CMake Analyzer";
103     }
104 
105     /**
106      * Tell that we are used for information collection.
107      *
108      * @return INFORMATION_COLLECTION
109      */
110     @Override
111     public AnalysisPhase getAnalysisPhase() {
112         return AnalysisPhase.INFORMATION_COLLECTION;
113     }
114 
115     /**
116      * Returns the set of supported file extensions.
117      *
118      * @return the set of supported file extensions
119      */
120     @Override
121     protected FileFilter getFileFilter() {
122         return FILTER;
123     }
124 
125     /**
126      * Initializes the analyzer.
127      *
128      * @throws InitializationException thrown if an exception occurs getting an
129      * instance of SHA1
130      */
131     @Override
132     protected void initializeFileTypeAnalyzer() throws InitializationException {
133         try {
134             getSha1MessageDigest();
135         } catch (IllegalStateException ex) {
136             setEnabled(false);
137             throw new InitializationException("Unable to create SHA1 MessageDigest", ex);
138         }
139     }
140 
141     /**
142      * Analyzes python packages and adds evidence to the dependency.
143      *
144      * @param dependency the dependency being analyzed
145      * @param engine the engine being used to perform the scan
146      * @throws AnalysisException thrown if there is an unrecoverable error
147      * analyzing the dependency
148      */
149     @Override
150     protected void analyzeDependency(Dependency dependency, Engine engine)
151             throws AnalysisException {
152         final File file = dependency.getActualFile();
153         final String parentName = file.getParentFile().getName();
154         final String name = file.getName();
155         dependency.setDisplayFileName(String.format("%s%c%s", parentName, File.separatorChar, name));
156         String contents;
157         try {
158             contents = FileUtils.readFileToString(file, Charset.defaultCharset()).trim();
159         } catch (IOException e) {
160             throw new AnalysisException(
161                     "Problem occurred while reading dependency file.", e);
162         }
163 
164         if (StringUtils.isNotBlank(contents)) {
165             final Matcher m = PROJECT.matcher(contents);
166             int count = 0;
167             while (m.find()) {
168                 count++;
169                 LOGGER.debug(String.format(
170                         "Found project command match with %d groups: %s",
171                         m.groupCount(), m.group(0)));
172                 final String group = m.group(1);
173                 LOGGER.debug("Group 1: " + group);
174                 dependency.getProductEvidence().addEvidence(name, "Project",
175                         group, Confidence.HIGH);
176             }
177             LOGGER.debug("Found {} matches.", count);
178             analyzeSetVersionCommand(dependency, engine, contents);
179         }
180     }
181 
182     /**
183      * Extracts the version information from the contents. If more then one
184      * version is found additional dependencies are added to the dependency
185      * list.
186      *
187      * @param dependency the dependency being analyzed
188      * @param engine the dependency-check engine
189      * @param contents the version information
190      */
191     @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(
192             value = "DM_DEFAULT_ENCODING",
193             justification = "Default encoding is only used if UTF-8 is not available")
194     private void analyzeSetVersionCommand(Dependency dependency, Engine engine, String contents) {
195         Dependency currentDep = dependency;
196 
197         final Matcher m = SET_VERSION.matcher(contents);
198         int count = 0;
199         while (m.find()) {
200             count++;
201             LOGGER.debug("Found project command match with {} groups: {}",
202                     m.groupCount(), m.group(0));
203             String product = m.group(1);
204             final String version = m.group(2);
205             LOGGER.debug("Group 1: " + product);
206             LOGGER.debug("Group 2: " + version);
207             final String aliasPrefix = "ALIASOF_";
208             if (product.startsWith(aliasPrefix)) {
209                 product = product.replaceFirst(aliasPrefix, "");
210             }
211             if (count > 1) {
212                 //TODO - refactor so we do not assign to the parameter (checkstyle)
213                 currentDep = new Dependency(dependency.getActualFile());
214                 currentDep.setDisplayFileName(String.format("%s:%s", dependency.getDisplayFileName(), product));
215                 final String filePath = String.format("%s:%s", dependency.getFilePath(), product);
216                 currentDep.setFilePath(filePath);
217 
218                 byte[] path;
219                 try {
220                     path = filePath.getBytes("UTF-8");
221                 } catch (UnsupportedEncodingException ex) {
222                     path = filePath.getBytes();
223                 }
224                 final MessageDigest sha1 = getSha1MessageDigest();
225                 currentDep.setSha1sum(Checksum.getHex(sha1.digest(path)));
226                 engine.getDependencies().add(currentDep);
227             }
228             final String source = currentDep.getDisplayFileName();
229             currentDep.getProductEvidence().addEvidence(source, "Product",
230                     product, Confidence.MEDIUM);
231             currentDep.getVersionEvidence().addEvidence(source, "Version",
232                     version, Confidence.MEDIUM);
233         }
234         LOGGER.debug(String.format("Found %d matches.", count));
235     }
236 
237     @Override
238     protected String getAnalyzerEnabledSettingKey() {
239         return Settings.KEYS.ANALYZER_CMAKE_ENABLED;
240     }
241 
242     /**
243      * Returns the sha1 message digest.
244      *
245      * @return the sha1 message digest
246      */
247     private MessageDigest getSha1MessageDigest() {
248         try {
249             return MessageDigest.getInstance("SHA1");
250         } catch (NoSuchAlgorithmException e) {
251             LOGGER.error(e.getMessage());
252             throw new IllegalStateException("Failed to obtain the SHA1 message digest.", e);
253         }
254     }
255 }