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