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       * A reference to SHA1 message digest.
97       */
98      private static MessageDigest sha1 = null;
99  
100     static {
101         try {
102             sha1 = MessageDigest.getInstance("SHA1");
103         } catch (NoSuchAlgorithmException e) {
104             LOGGER.error(e.getMessage());
105         }
106     }
107 
108     /**
109      * Returns the name of the CMake analyzer.
110      *
111      * @return the name of the analyzer
112      *
113      */
114     @Override
115     public String getName() {
116         return "CMake Analyzer";
117     }
118 
119     /**
120      * Tell that we are used for information collection.
121      *
122      * @return INFORMATION_COLLECTION
123      */
124     @Override
125     public AnalysisPhase getAnalysisPhase() {
126         return AnalysisPhase.INFORMATION_COLLECTION;
127     }
128 
129     /**
130      * Returns the set of supported file extensions.
131      *
132      * @return the set of supported file extensions
133      */
134     @Override
135     protected FileFilter getFileFilter() {
136         return FILTER;
137     }
138 
139     /**
140      * No-op initializer implementation.
141      *
142      * @throws InitializationException never thrown
143      */
144     @Override
145     protected void initializeFileTypeAnalyzer() throws InitializationException {
146         // Nothing to do here.
147     }
148 
149     /**
150      * Analyzes python packages and adds evidence to the dependency.
151      *
152      * @param dependency the dependency being analyzed
153      * @param engine the engine being used to perform the scan
154      * @throws AnalysisException thrown if there is an unrecoverable error
155      * analyzing the dependency
156      */
157     @Override
158     protected void analyzeFileType(Dependency dependency, Engine engine)
159             throws AnalysisException {
160         final File file = dependency.getActualFile();
161         final String parentName = file.getParentFile().getName();
162         final String name = file.getName();
163         dependency.setDisplayFileName(String.format("%s%c%s", parentName, File.separatorChar, name));
164         String contents;
165         try {
166             contents = FileUtils.readFileToString(file, Charset.defaultCharset()).trim();
167         } catch (IOException e) {
168             throw new AnalysisException(
169                     "Problem occurred while reading dependency file.", e);
170         }
171 
172         if (StringUtils.isNotBlank(contents)) {
173             final Matcher m = PROJECT.matcher(contents);
174             int count = 0;
175             while (m.find()) {
176                 count++;
177                 LOGGER.debug(String.format(
178                         "Found project command match with %d groups: %s",
179                         m.groupCount(), m.group(0)));
180                 final String group = m.group(1);
181                 LOGGER.debug("Group 1: " + group);
182                 dependency.getProductEvidence().addEvidence(name, "Project",
183                         group, Confidence.HIGH);
184             }
185             LOGGER.debug("Found {} matches.", count);
186             analyzeSetVersionCommand(dependency, engine, contents);
187         }
188     }
189 
190     /**
191      * Extracts the version information from the contents. If more then one
192      * version is found additional dependencies are added to the dependency
193      * list.
194      *
195      * @param dependency the dependency being analyzed
196      * @param engine the dependency-check engine
197      * @param contents the version information
198      */
199     @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(
200             value = "DM_DEFAULT_ENCODING",
201             justification = "Default encoding is only used if UTF-8 is not available")
202     private void analyzeSetVersionCommand(Dependency dependency, Engine engine, String contents) {
203         Dependency currentDep = dependency;
204 
205         final Matcher m = SET_VERSION.matcher(contents);
206         int count = 0;
207         while (m.find()) {
208             count++;
209             LOGGER.debug("Found project command match with {} groups: {}",
210                     m.groupCount(), m.group(0));
211             String product = m.group(1);
212             final String version = m.group(2);
213             LOGGER.debug("Group 1: " + product);
214             LOGGER.debug("Group 2: " + version);
215             final String aliasPrefix = "ALIASOF_";
216             if (product.startsWith(aliasPrefix)) {
217                 product = product.replaceFirst(aliasPrefix, "");
218             }
219             if (count > 1) {
220                 //TODO - refactor so we do not assign to the parameter (checkstyle)
221                 currentDep = new Dependency(dependency.getActualFile());
222                 currentDep.setDisplayFileName(String.format("%s:%s", dependency.getDisplayFileName(), product));
223                 final String filePath = String.format("%s:%s", dependency.getFilePath(), product);
224                 currentDep.setFilePath(filePath);
225 
226                 byte[] path;
227                 try {
228                     path = filePath.getBytes("UTF-8");
229                 } catch (UnsupportedEncodingException ex) {
230                     path = filePath.getBytes();
231                 }
232                 currentDep.setSha1sum(Checksum.getHex(sha1.digest(path)));
233                 engine.getDependencies().add(currentDep);
234             }
235             final String source = currentDep.getDisplayFileName();
236             currentDep.getProductEvidence().addEvidence(source, "Product",
237                     product, Confidence.MEDIUM);
238             currentDep.getVersionEvidence().addEvidence(source, "Version",
239                     version, Confidence.MEDIUM);
240         }
241         LOGGER.debug(String.format("Found %d matches.", count));
242     }
243 
244     @Override
245     protected String getAnalyzerEnabledSettingKey() {
246         return Settings.KEYS.ANALYZER_CMAKE_ENABLED;
247     }
248 }