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 java.io.File;
21  import java.io.FileFilter;
22  import java.io.FilenameFilter;
23  import java.io.IOException;
24  import java.nio.charset.Charset;
25  import java.util.List;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import org.apache.commons.io.FileUtils;
30  import org.owasp.dependencycheck.Engine;
31  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
32  import org.owasp.dependencycheck.dependency.Confidence;
33  import org.owasp.dependencycheck.dependency.Dependency;
34  import org.owasp.dependencycheck.dependency.EvidenceCollection;
35  import org.owasp.dependencycheck.exception.InitializationException;
36  import org.owasp.dependencycheck.utils.FileFilterBuilder;
37  import org.owasp.dependencycheck.utils.Settings;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  /**
42   * Used to analyze Ruby Gem specifications and collect information that can be
43   * used to determine the associated CPE. Regular expressions are used to parse
44   * the well-defined Ruby syntax that forms the specification.
45   *
46   * @author Dale Visser
47   */
48  @Experimental
49  public class RubyGemspecAnalyzer extends AbstractFileTypeAnalyzer {
50  
51      /**
52       * The logger.
53       */
54      private static final Logger LOGGER = LoggerFactory.getLogger(RubyGemspecAnalyzer.class);
55      /**
56       * The name of the analyzer.
57       */
58      private static final String ANALYZER_NAME = "Ruby Gemspec Analyzer";
59  
60      /**
61       * The phase that this analyzer is intended to run in.
62       */
63      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
64  
65      /**
66       * The gemspec file extension.
67       */
68      private static final String GEMSPEC = "gemspec";
69  
70      /**
71       * The file filter containing the list of file extensions that can be
72       * analyzed.
73       */
74      private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(GEMSPEC).build();
75      //TODO: support Rakefile
76      //= FileFilterBuilder.newInstance().addExtensions(GEMSPEC).addFilenames("Rakefile").build();
77  
78      /**
79       * The name of the version file.
80       */
81      private static final String VERSION_FILE_NAME = "VERSION";
82  
83      /**
84       * @return a filter that accepts files matching the glob pattern, *.gemspec
85       */
86      @Override
87      protected FileFilter getFileFilter() {
88          return FILTER;
89      }
90  
91      @Override
92      protected void initializeFileTypeAnalyzer() throws InitializationException {
93          // NO-OP
94      }
95  
96      /**
97       * Returns the name of the analyzer.
98       *
99       * @return the name of the analyzer.
100      */
101     @Override
102     public String getName() {
103         return ANALYZER_NAME;
104     }
105 
106     /**
107      * Returns the phase that the analyzer is intended to run in.
108      *
109      * @return the phase that the analyzer is intended to run in.
110      */
111     @Override
112     public AnalysisPhase getAnalysisPhase() {
113         return ANALYSIS_PHASE;
114     }
115 
116     /**
117      * Returns the key used in the properties file to reference the analyzer's
118      * enabled property.
119      *
120      * @return the analyzer's enabled property setting key
121      */
122     @Override
123     protected String getAnalyzerEnabledSettingKey() {
124         return Settings.KEYS.ANALYZER_RUBY_GEMSPEC_ENABLED;
125     }
126 
127     /**
128      * The capture group #1 is the block variable.
129      */
130     private static final Pattern GEMSPEC_BLOCK_INIT = Pattern.compile("Gem::Specification\\.new\\s+?do\\s+?\\|(.+?)\\|");
131 
132     @Override
133     protected void analyzeDependency(Dependency dependency, Engine engine)
134             throws AnalysisException {
135         String contents;
136         try {
137             contents = FileUtils.readFileToString(dependency.getActualFile(), Charset.defaultCharset());
138         } catch (IOException e) {
139             throw new AnalysisException(
140                     "Problem occurred while reading dependency file.", e);
141         }
142         final Matcher matcher = GEMSPEC_BLOCK_INIT.matcher(contents);
143         if (matcher.find()) {
144             contents = contents.substring(matcher.end());
145             final String blockVariable = matcher.group(1);
146 
147             final EvidenceCollection vendor = dependency.getVendorEvidence();
148             final EvidenceCollection product = dependency.getProductEvidence();
149             final String name = addStringEvidence(product, contents, blockVariable, "name", "name", Confidence.HIGHEST);
150             if (!name.isEmpty()) {
151                 vendor.addEvidence(GEMSPEC, "name_project", name + "_project", Confidence.LOW);
152             }
153             addStringEvidence(product, contents, blockVariable, "summary", "summary", Confidence.LOW);
154 
155             addStringEvidence(vendor, contents, blockVariable, "author", "authors?", Confidence.HIGHEST);
156             addStringEvidence(vendor, contents, blockVariable, "email", "emails?", Confidence.MEDIUM);
157             addStringEvidence(vendor, contents, blockVariable, "homepage", "homepage", Confidence.HIGHEST);
158             addStringEvidence(vendor, contents, blockVariable, "license", "licen[cs]es?", Confidence.HIGHEST);
159 
160             final String value = addStringEvidence(dependency.getVersionEvidence(), contents,
161                     blockVariable, "version", "version", Confidence.HIGHEST);
162             if (value.length() < 1) {
163                 addEvidenceFromVersionFile(dependency.getActualFile(), dependency.getVersionEvidence());
164             }
165         }
166 
167         setPackagePath(dependency);
168     }
169 
170     /**
171      * Adds the specified evidence to the given evidence collection.
172      *
173      * @param evidences the collection to add the evidence to
174      * @param contents the evidence contents
175      * @param blockVariable the variable
176      * @param field the field
177      * @param fieldPattern the field pattern
178      * @param confidence the confidence of the evidence
179      * @return the evidence string value added
180      */
181     private String addStringEvidence(EvidenceCollection evidences, String contents,
182             String blockVariable, String field, String fieldPattern, Confidence confidence) {
183         String value = "";
184 
185         //capture array value between [ ]
186         final Matcher arrayMatcher = Pattern.compile(
187                 String.format("\\s*?%s\\.%s\\s*?=\\s*?\\[(.*?)\\]", blockVariable, fieldPattern), Pattern.CASE_INSENSITIVE).matcher(contents);
188         if (arrayMatcher.find()) {
189             final String arrayValue = arrayMatcher.group(1);
190             value = arrayValue.replaceAll("['\"]", "").trim(); //strip quotes
191         } else { //capture single value between quotes
192             final Matcher matcher = Pattern.compile(
193                     String.format("\\s*?%s\\.%s\\s*?=\\s*?(['\"])(.*?)\\1", blockVariable, fieldPattern), Pattern.CASE_INSENSITIVE).matcher(contents);
194             if (matcher.find()) {
195                 value = matcher.group(2);
196             }
197         }
198         if (value.length() > 0) {
199             evidences.addEvidence(GEMSPEC, field, value, confidence);
200         }
201 
202         return value;
203     }
204 
205     /**
206      * Adds evidence from the version file.
207      *
208      * @param dependencyFile the dependency being analyzed
209      * @param versionEvidences the version evidence
210      */
211     private void addEvidenceFromVersionFile(File dependencyFile, EvidenceCollection versionEvidences) {
212         final File parentDir = dependencyFile.getParentFile();
213         if (parentDir != null) {
214             final File[] matchingFiles = parentDir.listFiles(new FilenameFilter() {
215                 @Override
216                 public boolean accept(File dir, String name) {
217                     return name.contains(VERSION_FILE_NAME);
218                 }
219             });
220             if (matchingFiles == null) {
221                 return;
222             }
223             for (File f : matchingFiles) {
224                 try {
225                     final List<String> lines = FileUtils.readLines(f, Charset.defaultCharset());
226                     if (lines.size() == 1) { //TODO other checking?
227                         final String value = lines.get(0).trim();
228                         versionEvidences.addEvidence(GEMSPEC, "version", value, Confidence.HIGH);
229                     }
230                 } catch (IOException e) {
231                     LOGGER.debug("Error reading gemspec", e);
232                 }
233             }
234         }
235     }
236 
237     /**
238      * Sets the package path on the dependency.
239      *
240      * @param dep the dependency to alter
241      */
242     private void setPackagePath(Dependency dep) {
243         final File file = new File(dep.getFilePath());
244         final String parent = file.getParent();
245         if (parent != null) {
246             dep.setPackagePath(parent);
247         }
248     }
249 }