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