1   
2   
3   
4   
5   
6   
7   
8   
9   
10  
11  
12  
13  
14  
15  
16  
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  
49  
50  
51  
52  
53  
54  @Experimental
55  @ThreadSafe
56  public class RubyGemspecAnalyzer extends AbstractFileTypeAnalyzer {
57  
58      
59  
60  
61  
62      public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.RUBY;
63      
64  
65  
66      private static final Logger LOGGER = LoggerFactory.getLogger(RubyGemspecAnalyzer.class);
67      
68  
69  
70      private static final String ANALYZER_NAME = "Ruby Gemspec Analyzer";
71      
72  
73  
74      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
75      
76  
77  
78      private static final String GEMSPEC = "gemspec";
79      
80  
81  
82  
83      private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(GEMSPEC).build();
84      
85      
86  
87      
88  
89  
90      private static final String VERSION_FILE_NAME = "VERSION";
91  
92      
93  
94  
95      private static final Pattern GEMSPEC_BLOCK_INIT = Pattern.compile("Gem::Specification\\.new\\s+?do\\s+?\\|(.+?)\\|");
96  
97      
98  
99  
100     @Override
101     protected FileFilter getFileFilter() {
102         return FILTER;
103     }
104 
105     @Override
106     protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
107         
108     }
109 
110     
111 
112 
113 
114 
115     @Override
116     public String getName() {
117         return ANALYZER_NAME;
118     }
119 
120     
121 
122 
123 
124 
125     @Override
126     public AnalysisPhase getAnalysisPhase() {
127         return ANALYSIS_PHASE;
128     }
129 
130     
131 
132 
133 
134 
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 
215 
216 
217 
218 
219 
220 
221 
222 
223 
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         
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(); 
235         } else { 
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 
251 
252 
253 
254 
255 
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) { 
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 
291 
292 
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 }