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 com.github.packageurl.MalformedPackageURLException;
21  import com.github.packageurl.PackageURL;
22  import com.github.packageurl.PackageURLBuilder;
23  import org.owasp.dependencycheck.Engine;
24  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
25  import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
26  import org.owasp.dependencycheck.dependency.Confidence;
27  import org.owasp.dependencycheck.dependency.Dependency;
28  import org.owasp.dependencycheck.dependency.EvidenceType;
29  import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
30  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
31  import org.owasp.dependencycheck.exception.InitializationException;
32  import org.owasp.dependencycheck.utils.FileFilterBuilder;
33  import org.owasp.dependencycheck.utils.Settings;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import javax.annotation.concurrent.ThreadSafe;
38  import java.io.File;
39  import java.io.FileFilter;
40  import java.io.IOException;
41  import java.nio.charset.StandardCharsets;
42  import java.nio.file.Files;
43  import java.util.List;
44  import java.util.regex.Matcher;
45  import java.util.regex.Pattern;
46  
47  /**
48   * Used to analyze Ruby Gem specifications and collect information that can be
49   * used to determine the associated CPE. Regular expressions are used to parse
50   * the well-defined Ruby syntax that forms the specification.
51   *
52   * @author Dale Visser
53   */
54  @Experimental
55  @ThreadSafe
56  public class RubyGemspecAnalyzer extends AbstractFileTypeAnalyzer {
57  
58      /**
59       * A descriptor for the type of dependencies processed or added by this
60       * analyzer.
61       */
62      public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.RUBY;
63      /**
64       * The logger.
65       */
66      private static final Logger LOGGER = LoggerFactory.getLogger(RubyGemspecAnalyzer.class);
67      /**
68       * The name of the analyzer.
69       */
70      private static final String ANALYZER_NAME = "Ruby Gemspec Analyzer";
71      /**
72       * The phase that this analyzer is intended to run in.
73       */
74      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
75      /**
76       * The gemspec file extension.
77       */
78      private static final String GEMSPEC = "gemspec";
79      /**
80       * The file filter containing the list of file extensions that can be
81       * analyzed.
82       */
83      private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(GEMSPEC).build();
84      //TODO: support Rakefile
85      //= FileFilterBuilder.newInstance().addExtensions(GEMSPEC).addFilenames("Rakefile").build();
86  
87      /**
88       * The name of the version file.
89       */
90      private static final String VERSION_FILE_NAME = "VERSION";
91  
92      /**
93       * The capture group #1 is the block variable.
94       */
95      private static final Pattern GEMSPEC_BLOCK_INIT = Pattern.compile("Gem::Specification\\.new\\s+?do\\s+?\\|(.+?)\\|");
96  
97      /**
98       * @return a filter that accepts files matching the glob pattern, *.gemspec
99       */
100     @Override
101     protected FileFilter getFileFilter() {
102         return FILTER;
103     }
104 
105     @Override
106     protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
107         // NO-OP
108     }
109 
110     /**
111      * Returns the name of the analyzer.
112      *
113      * @return the name of the analyzer.
114      */
115     @Override
116     public String getName() {
117         return ANALYZER_NAME;
118     }
119 
120     /**
121      * Returns the phase that the analyzer is intended to run in.
122      *
123      * @return the phase that the analyzer is intended to run in.
124      */
125     @Override
126     public AnalysisPhase getAnalysisPhase() {
127         return ANALYSIS_PHASE;
128     }
129 
130     /**
131      * Returns the key used in the properties file to reference the analyzer's
132      * enabled property.
133      *
134      * @return the analyzer's enabled property setting key
135      */
136     @Override
137     protected String getAnalyzerEnabledSettingKey() {
138         return Settings.KEYS.ANALYZER_RUBY_GEMSPEC_ENABLED;
139     }
140 
141     @Override
142     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
143         dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
144         String contents;
145         try {
146             contents = new String(Files.readAllBytes(dependency.getActualFile().toPath()), StandardCharsets.UTF_8);
147         } catch (IOException e) {
148             throw new AnalysisException(
149                     "Problem occurred while reading dependency file.", e);
150         }
151         final Matcher matcher = GEMSPEC_BLOCK_INIT.matcher(contents);
152         if (matcher.find()) {
153             contents = contents.substring(matcher.end());
154             final String blockVariable = matcher.group(1);
155 
156             final String name = addStringEvidence(dependency, EvidenceType.PRODUCT, contents, blockVariable, "name", "name", Confidence.HIGHEST);
157             if (!name.isEmpty()) {
158                 dependency.addEvidence(EvidenceType.VENDOR, GEMSPEC, "name_project", name + "_project", Confidence.LOW);
159                 dependency.setName(name);
160             }
161             final String description = addStringEvidence(dependency, EvidenceType.PRODUCT, contents, blockVariable,
162                     "summary", "summary", Confidence.LOW);
163             if (description != null && !description.isEmpty()) {
164                 dependency.setDescription(description);
165             }
166             addStringEvidence(dependency, EvidenceType.VENDOR, contents, blockVariable,
167                     "author", "authors?", Confidence.HIGHEST);
168             addStringEvidence(dependency, EvidenceType.VENDOR, contents, blockVariable,
169                     "email", "emails?", Confidence.MEDIUM);
170             addStringEvidence(dependency, EvidenceType.VENDOR, contents, blockVariable,
171                     "homepage", "homepage", Confidence.HIGHEST);
172             final String license = addStringEvidence(dependency, EvidenceType.VENDOR, contents, blockVariable,
173                     "license", "licen[cs]es?", Confidence.HIGHEST);
174             if (license != null && !license.isEmpty()) {
175                 dependency.setLicense(license);
176             }
177             final String value = addStringEvidence(dependency, EvidenceType.VERSION, contents,
178                     blockVariable, "version", "version", Confidence.HIGHEST);
179             if (value.length() < 1) {
180                 final String version = addEvidenceFromVersionFile(dependency, EvidenceType.VERSION, dependency.getActualFile());
181                 if (version != null) {
182                     dependency.setVersion(version);
183                 }
184             } else {
185                 dependency.setVersion(value);
186             }
187         }
188         if (dependency.getName() != null && dependency.getVersion() != null) {
189             dependency.setDisplayFileName(String.format("%s:%s", dependency.getName(), dependency.getVersion()));
190         }
191 
192         try {
193             final PackageURLBuilder builder = PackageURLBuilder.aPackageURL().withType("gem").withName(dependency.getName());
194             if (dependency.getVersion() != null) {
195                 builder.withVersion(dependency.getVersion());
196             }
197             final PackageURL purl = builder.build();
198             dependency.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
199         } catch (MalformedPackageURLException ex) {
200             LOGGER.debug("Unable to build package url for python", ex);
201             final GenericIdentifier id;
202             if (dependency.getVersion() != null) {
203                 id = new GenericIdentifier("gem:" + dependency.getName() + "@" + dependency.getVersion(), Confidence.HIGHEST);
204             } else {
205                 id = new GenericIdentifier("gem:" + dependency.getName(), Confidence.HIGHEST);
206             }
207             dependency.addSoftwareIdentifier(id);
208         }
209 
210         setPackagePath(dependency);
211     }
212 
213     /**
214      * Adds the specified evidence to the given evidence collection.
215      *
216      * @param dependency the dependency being analyzed
217      * @param type the type of evidence to add
218      * @param contents the evidence contents
219      * @param blockVariable the variable
220      * @param field the field
221      * @param fieldPattern the field pattern
222      * @param confidence the confidence of the evidence
223      * @return the evidence string value added
224      */
225     private String addStringEvidence(Dependency dependency, EvidenceType type, String contents,
226             String blockVariable, String field, String fieldPattern, Confidence confidence) {
227         String value = "";
228 
229         //capture array value between [ ]
230         final Matcher arrayMatcher = Pattern.compile(
231                 String.format("\\s*?%s\\.%s\\s*?=\\s*?\\[(.*?)\\]", blockVariable, fieldPattern), Pattern.CASE_INSENSITIVE).matcher(contents);
232         if (arrayMatcher.find()) {
233             final String arrayValue = arrayMatcher.group(1);
234             value = arrayValue.replaceAll("['\"]", "").trim(); //strip quotes
235         } else { //capture single value between quotes
236             final Matcher matcher = Pattern.compile(
237                     String.format("\\s*?%s\\.%s\\s*?=\\s*?(['\"])(.*?)\\1", blockVariable, fieldPattern), Pattern.CASE_INSENSITIVE).matcher(contents);
238             if (matcher.find()) {
239                 value = matcher.group(2);
240             }
241         }
242         if (value.length() > 0) {
243             dependency.addEvidence(type, GEMSPEC, field, value, confidence);
244         }
245 
246         return value;
247     }
248 
249     /**
250      * Adds evidence from the version file.
251      *
252      * @param dependency the dependency being analyzed
253      * @param type the type of evidence to add
254      * @param dependencyFile the dependency being analyzed
255      * @return the version number added
256      */
257     private String addEvidenceFromVersionFile(Dependency dependency, EvidenceType type, File dependencyFile) {
258         final File parentDir = dependencyFile.getParentFile();
259         String version = null;
260         int versionCount = 0;
261         if (parentDir != null) {
262             final File[] matchingFiles = parentDir.listFiles((dir, name) -> name.contains(VERSION_FILE_NAME));
263             if (matchingFiles == null) {
264                 return null;
265             }
266             for (File f : matchingFiles) {
267                 try {
268                     final List<String> lines = Files.readAllLines(f.toPath(), StandardCharsets.UTF_8);
269                     if (lines.size() == 1) { //TODO other checking?
270                         final String value = lines.get(0).trim();
271                         if (version == null || !version.equals(value)) {
272                             version = value;
273                             versionCount++;
274                         }
275 
276                         dependency.addEvidence(type, GEMSPEC, "version", value, Confidence.HIGH);
277                     }
278                 } catch (IOException e) {
279                     LOGGER.debug("Error reading gemspec", e);
280                 }
281             }
282         }
283         if (versionCount == 1) {
284             return version;
285         }
286         return null;
287     }
288 
289     /**
290      * Sets the package path on the dependency.
291      *
292      * @param dep the dependency to alter
293      */
294     private void setPackagePath(Dependency dep) {
295         final File file = new File(dep.getFilePath());
296         final String parent = file.getParent();
297         if (parent != null) {
298             dep.setPackagePath(parent);
299         }
300     }
301 }