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