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) 2017 Steve Springett. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.analyzer;
19  
20  import com.esotericsoftware.minlog.Log;
21  import com.github.packageurl.MalformedPackageURLException;
22  import com.github.packageurl.PackageURL;
23  import com.github.packageurl.PackageURLBuilder;
24  import com.h3xstream.retirejs.repo.JsLibrary;
25  import com.h3xstream.retirejs.repo.JsLibraryResult;
26  import com.h3xstream.retirejs.repo.JsVulnerability;
27  import com.h3xstream.retirejs.repo.ScannerFacade;
28  import com.h3xstream.retirejs.repo.VulnerabilitiesRepository;
29  import com.h3xstream.retirejs.repo.VulnerabilitiesRepositoryLoader;
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.commons.validator.routines.UrlValidator;
32  import org.json.JSONException;
33  import org.owasp.dependencycheck.Engine;
34  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
35  import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
36  import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
37  import org.owasp.dependencycheck.data.update.RetireJSDataSource;
38  import org.owasp.dependencycheck.data.update.exception.UpdateException;
39  import org.owasp.dependencycheck.dependency.Confidence;
40  import org.owasp.dependencycheck.dependency.Dependency;
41  import org.owasp.dependencycheck.dependency.EvidenceType;
42  import org.owasp.dependencycheck.dependency.Reference;
43  import org.owasp.dependencycheck.dependency.Vulnerability;
44  import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
45  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
46  import org.owasp.dependencycheck.exception.InitializationException;
47  import org.owasp.dependencycheck.exception.WriteLockException;
48  import org.owasp.dependencycheck.utils.FileFilterBuilder;
49  import org.owasp.dependencycheck.utils.Settings;
50  import org.owasp.dependencycheck.utils.WriteLock;
51  import org.owasp.dependencycheck.utils.search.FileContentSearch;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  import javax.annotation.concurrent.ThreadSafe;
56  import java.io.File;
57  import java.io.FileFilter;
58  import java.io.FileInputStream;
59  import java.io.FileNotFoundException;
60  import java.io.IOException;
61  import java.io.InputStream;
62  import java.net.URL;
63  import java.nio.file.Files;
64  import java.util.ArrayList;
65  import java.util.List;
66  import java.util.Map;
67  import org.apache.commons.io.IOUtils;
68  
69  /**
70   * The RetireJS analyzer uses the manually curated list of vulnerabilities from
71   * the RetireJS community along with the necessary information to assist in
72   * identifying vulnerable components. Vulnerabilities documented by the RetireJS
73   * community usually originate from other sources such as the NVD, OSVDB, NSP,
74   * and various issue trackers.
75   *
76   * @author Steve Springett
77   */
78  @ThreadSafe
79  public class RetireJsAnalyzer extends AbstractFileTypeAnalyzer {
80  
81      /**
82       * A descriptor for the type of dependencies processed or added by this
83       * analyzer.
84       */
85      public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.JAVASCRIPT;
86      /**
87       * The logger.
88       */
89      private static final Logger LOGGER = LoggerFactory.getLogger(RetireJsAnalyzer.class);
90      /**
91       * The name of the analyzer.
92       */
93      private static final String ANALYZER_NAME = "RetireJS Analyzer";
94      /**
95       * The phase that this analyzer is intended to run in.
96       */
97      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.FINDING_ANALYSIS;
98      /**
99       * The set of file extensions supported by this analyzer.
100      */
101     private static final String[] EXTENSIONS = {"js"};
102     /**
103      * The file filter used to determine which files this analyzer supports.
104      */
105     private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build();
106     /**
107      * An instance of the local VulnerabilitiesRepository
108      */
109     private VulnerabilitiesRepository jsRepository;
110     /**
111      * The list of filters used to exclude files by file content; the intent is
112      * that this could be used to filter out a companies custom files by filter
113      * on their own copyright statements.
114      */
115     private String[] filters = null;
116 
117     /**
118      * Flag indicating whether non-vulnerable JS should be excluded if they are
119      * contained in a JAR.
120      */
121     //TODO implement this
122     @SuppressWarnings("FieldMayBeFinal")
123     private boolean skipNonVulnerableInJAR = true;
124 
125     /**
126      * Returns the FileFilter.
127      *
128      * @return the FileFilter
129      */
130     @Override
131     protected FileFilter getFileFilter() {
132         return FILTER;
133     }
134 
135     /**
136      * Determines if the file can be analyzed by the analyzer.
137      *
138      * @param pathname the path to the file
139      * @return true if the file can be analyzed by the given analyzer; otherwise
140      * false
141      */
142     @Override
143     public boolean accept(File pathname) {
144         try {
145             final boolean accepted = super.accept(pathname);
146             if (accepted && !pathname.exists()) {
147                 //file may not yet have been extracted from an archive
148                 super.setFilesMatched(true);
149                 return true;
150             }
151             if (accepted && filters != null && FileContentSearch.contains(pathname, filters)) {
152                 return false;
153             }
154             return accepted;
155         } catch (IOException ex) {
156             LOGGER.warn(String.format("Error testing file %s", pathname), ex);
157         }
158         return false;
159     }
160 
161     /**
162      * Initializes the analyzer with the configured settings.
163      *
164      * @param settings the configured settings to use
165      */
166     @Override
167     public void initialize(Settings settings) {
168         super.initialize(settings);
169         if (this.isEnabled()) {
170             this.filters = settings.getArray(Settings.KEYS.ANALYZER_RETIREJS_FILTERS);
171         }
172     }
173 
174     /**
175      * {@inheritDoc}
176      *
177      * @param engine a reference to the dependency-check engine
178      * @throws InitializationException thrown if there is an exception during
179      * initialization
180      */
181     @Override
182     protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
183         // RetireJS outputs a bunch of repeated output like the following for
184         // vulnerable dependencies, with little context:
185         //
186         // INFO: Vulnerability found: jquery below 1.6.3
187         //
188         // This logging is suppressed because it isn't particularly useful, and
189         // it aligns with other analyzers that don't log such information.
190         Log.set(Log.LEVEL_WARN);
191 
192         File repoFile = null;
193         boolean repoEmpty = false;
194         try {
195             final String configuredUrl = getSettings().getString(Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_URL, RetireJSDataSource.DEFAULT_JS_URL);
196             final URL url = new URL(configuredUrl);
197             final File filepath = new File(url.getPath());
198             repoFile = new File(getSettings().getDataDirectory(), filepath.getName());
199             if (!repoFile.isFile() || repoFile.length() <= 1L) {
200                 LOGGER.warn("Retire JS repository is empty or missing - attempting to force the update");
201                 repoEmpty = true;
202                 getSettings().setBoolean(Settings.KEYS.ANALYZER_RETIREJS_FORCEUPDATE, true);
203             }
204         } catch (FileNotFoundException ex) {
205             this.setEnabled(false);
206             throw new InitializationException(String.format("RetireJS repo does not exist locally (%s)", repoFile), ex);
207         } catch (IOException ex) {
208             this.setEnabled(false);
209             throw new InitializationException("Failed to initialize the RetireJS", ex);
210         }
211 
212         final boolean autoupdate = getSettings().getBoolean(Settings.KEYS.AUTO_UPDATE, true);
213         final boolean forceupdate = getSettings().getBoolean(Settings.KEYS.ANALYZER_RETIREJS_FORCEUPDATE, false);
214         if ((!autoupdate && forceupdate) || (autoupdate && repoEmpty)) {
215             final RetireJSDataSource ds = new RetireJSDataSource();
216             try {
217                 ds.update(engine);
218             } catch (UpdateException ex) {
219                 throw new InitializationException("Unable to initialize the Retire JS repository", ex);
220             }
221         }
222 
223         //several users are reporting that the retire js repository is getting corrupted.
224         try (WriteLock lock = new WriteLock(getSettings(), true, repoFile.getName() + ".lock")) {
225             final File temp = getSettings().getTempDirectory();
226             final File tempRepo = new File(temp, repoFile.getName());
227             LOGGER.debug("copying retireJs repo {} to {}", repoFile.toPath(), tempRepo.toPath());
228             Files.copy(repoFile.toPath(), tempRepo.toPath());
229             repoFile = tempRepo;
230         } catch (WriteLockException | IOException ex) {
231             this.setEnabled(false);
232             throw new InitializationException("Failed to copy the RetireJS repo", ex);
233         }
234         try (FileInputStream in = new FileInputStream(repoFile)) {
235             this.jsRepository = new VulnerabilitiesRepositoryLoader().loadFromInputStream(in);
236         } catch (JSONException ex) {
237             this.setEnabled(false);
238             throw new InitializationException("Failed to initialize the RetireJS repo: `" + repoFile
239                     + "` appears to be malformed. Please delete the file or run the dependency-check purge "
240                     + "command and re-try running dependency-check.", ex);
241         } catch (IOException ex) {
242             this.setEnabled(false);
243             throw new InitializationException("Failed to initialize the RetireJS repo", ex);
244         }
245     }
246 
247     /**
248      * Returns the name of the analyzer.
249      *
250      * @return the name of the analyzer.
251      */
252     @Override
253     public String getName() {
254         return ANALYZER_NAME;
255     }
256 
257     /**
258      * Returns the phase that the analyzer is intended to run in.
259      *
260      * @return the phase that the analyzer is intended to run in.
261      */
262     @Override
263     public AnalysisPhase getAnalysisPhase() {
264         return ANALYSIS_PHASE;
265     }
266 
267     /**
268      * Returns the key used in the properties file to reference the analyzer's
269      * enabled property.
270      *
271      * @return the analyzer's enabled property setting key
272      */
273     @Override
274     protected String getAnalyzerEnabledSettingKey() {
275         return Settings.KEYS.ANALYZER_RETIREJS_ENABLED;
276     }
277 
278     /**
279      * Analyzes the specified JavaScript file.
280      *
281      * @param dependency the dependency to analyze.
282      * @param engine the engine that is scanning the dependencies
283      * @throws AnalysisException is thrown if there is an error reading the file
284      * file.
285      */
286     @Override
287     public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
288         if (dependency.isVirtual()) {
289             return;
290         }
291         try (InputStream fis = new FileInputStream(dependency.getActualFile())) {
292             final byte[] fileContent = IOUtils.toByteArray(fis);
293             final ScannerFacade scanner = new ScannerFacade(jsRepository);
294             final List<JsLibraryResult> results;
295             try {
296                 results = scanner.scanScript(dependency.getActualFile().getAbsolutePath(), fileContent, 0);
297             } catch (StackOverflowError ex) {
298                 final String msg = String.format("An error occured trying to analyze %s. "
299                         + "To resolve this error please try increasing the Java stack size to "
300                         + "8mb and re-run dependency-check:%n%n"
301                         + "(win) : set JAVA_OPTS=\"-Xss8192k\"%n"
302                         + "(*nix): export JAVA_OPTS=\"-Xss8192k\"%n%n",
303                         dependency.getDisplayFileName());
304                 throw new AnalysisException(msg, ex);
305             }
306             if (results.size() > 0) {
307                 for (JsLibraryResult libraryResult : results) {
308 
309                     final JsLibrary lib = libraryResult.getLibrary();
310                     dependency.setName(lib.getName());
311                     dependency.setVersion(libraryResult.getDetectedVersion());
312                     try {
313                         final PackageURL purl = PackageURLBuilder.aPackageURL().withType("javascript")
314                                 .withName(lib.getName()).withVersion(libraryResult.getDetectedVersion()).build();
315                         dependency.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
316                     } catch (MalformedPackageURLException ex) {
317                         LOGGER.debug("Unable to build package url for retireJS", ex);
318                         final GenericIdentifier id = new GenericIdentifier("javascript:" + lib.getName() + "@"
319                                 + libraryResult.getDetectedVersion(), Confidence.HIGHEST);
320                         dependency.addSoftwareIdentifier(id);
321                     }
322 
323                     dependency.addEvidence(EvidenceType.VERSION, "file", "version", libraryResult.getDetectedVersion(), Confidence.HIGH);
324                     dependency.addEvidence(EvidenceType.PRODUCT, "file", "name", libraryResult.getLibrary().getName(), Confidence.HIGH);
325                     dependency.addEvidence(EvidenceType.VENDOR, "file", "name", libraryResult.getLibrary().getName(), Confidence.HIGH);
326 
327                     final List<Vulnerability> vulns = new ArrayList<>();
328                     final JsVulnerability jsVuln = libraryResult.getVuln();
329 
330                     if (jsVuln.getIdentifiers().containsKey("CVE") || jsVuln.getIdentifiers().containsKey("osvdb")) {
331                         /* CVEs and OSVDB are an array of Strings - each one a unique vulnerability.
332                          * So the JsVulnerability we are operating on may actually be representing
333                          * multiple vulnerabilities. */
334 
335                         //TODO - can we refactor this to avoid russian doll syndrome (i.e. nesting)?
336                         //CSOFF: NestedForDepth
337                         for (Map.Entry<String, List<String>> entry : jsVuln.getIdentifiers().entrySet()) {
338                             final String key = entry.getKey();
339                             final List<String> value = entry.getValue();
340                             if ("CVE".equals(key)) {
341                                 for (String cve : value) {
342                                     Vulnerability vuln = engine.getDatabase().getVulnerability(StringUtils.trim(cve));
343                                     if (vuln == null) {
344                                         /* The CVE does not exist in the database and is likely in a
345                                          * reserved state. Create a new one without adding it to the
346                                          * database and populate it as best as possible. */
347                                         vuln = new Vulnerability();
348                                         vuln.setName(cve);
349                                         vuln.setUnscoredSeverity(jsVuln.getSeverity());
350                                         vuln.setSource(Vulnerability.Source.RETIREJS);
351                                     }
352                                     jsVuln.getInfo().stream().map((info) -> {
353                                         if (UrlValidator.getInstance().isValid(info)) {
354                                             return new Reference(info, "info", info);
355                                         }
356                                         return new Reference(info, "info", null);
357                                     }).forEach(vuln::addReference);
358                                     vulns.add(vuln);
359                                 }
360                             } else if ("osvdb".equals(key)) {
361                                 //todo - convert to map/collect
362                                 value.forEach((osvdb) -> {
363                                     final Vulnerability vuln = new Vulnerability();
364                                     vuln.setName(osvdb);
365                                     vuln.setSource(Vulnerability.Source.RETIREJS);
366                                     vuln.setUnscoredSeverity(jsVuln.getSeverity());
367                                     jsVuln.getInfo().stream().map((info) -> {
368                                         if (UrlValidator.getInstance().isValid(info)) {
369                                             return new Reference(info, "info", info);
370                                         }
371                                         return new Reference(info, "info", null);
372                                     }).forEach(vuln::addReference);
373                                     vulns.add(vuln);
374                                 });
375                             }
376                             dependency.addVulnerabilities(vulns);
377                         }
378                         //CSON: NestedForDepth
379                     } else {
380                         final Vulnerability individualVuln = new Vulnerability();
381                         /* ISSUE, BUG, etc are all individual vulnerabilities. The result of this
382                          * iteration will be one vulnerability. */
383                         for (Map.Entry<String, List<String>> entry : jsVuln.getIdentifiers().entrySet()) {
384                             final String key = entry.getKey();
385                             final List<String> value = entry.getValue();
386                             // CSOFF: NeedBraces
387                             if (null != key) {
388                                 switch (key) {
389                                     case "summary":
390                                         if (null == individualVuln.getName()) {
391                                             individualVuln.setName(value.get(0));
392                                         }
393                                         individualVuln.setDescription(value.get(0));
394                                         break;
395                                     case "issue":
396                                         individualVuln.setName(libraryResult.getLibrary().getName() + " issue: " + value.get(0));
397                                         if (UrlValidator.getInstance().isValid(value.get(0))) {
398                                             individualVuln.addReference(key, key, value.get(0));
399                                         } else {
400                                             individualVuln.addReference(key, value.get(0), null);
401                                         }
402                                         break;
403                                     case "bug":
404                                         individualVuln.setName(libraryResult.getLibrary().getName() + " bug: " + value.get(0));
405                                         if (UrlValidator.getInstance().isValid(value.get(0))) {
406                                             individualVuln.addReference(key, key, value.get(0));
407                                         } else {
408                                             individualVuln.addReference(key, value.get(0), null);
409                                         }
410                                         break;
411                                     case "pr":
412                                         individualVuln.setName(libraryResult.getLibrary().getName() + " pr: " + value.get(0));
413                                         if (UrlValidator.getInstance().isValid(value.get(0))) {
414                                             individualVuln.addReference(key, key, value.get(0));
415                                         } else {
416                                             individualVuln.addReference(key, value.get(0), null);
417                                         }
418                                         break;
419                                     //case "release":
420                                     default:
421                                         if (UrlValidator.getInstance().isValid(value.get(0))) {
422                                             individualVuln.addReference(key, key, value.get(0));
423                                         } else {
424                                             individualVuln.addReference(key, value.get(0), null);
425                                         }
426                                         break;
427                                 }
428                             }
429                             // CSON: NeedBraces
430                         }
431                         if (StringUtils.isBlank(individualVuln.getName())) {
432                             individualVuln.setName("Vulnerability in " + libraryResult.getLibrary().getName());
433                         }
434                         individualVuln.setSource(Vulnerability.Source.RETIREJS);
435                         individualVuln.setUnscoredSeverity(jsVuln.getSeverity());
436                         jsVuln.getInfo().stream().map((info) -> {
437                             if (UrlValidator.getInstance().isValid(info)) {
438                                 return new Reference(info, "info", info);
439                             }
440                             return new Reference(info, "info", null);
441                         }).forEach(individualVuln::addReference);
442 
443                         dependency.addVulnerability(individualVuln);
444                     }
445                 }
446             } else if (getSettings().getBoolean(Settings.KEYS.ANALYZER_RETIREJS_FILTER_NON_VULNERABLE, false)) {
447                 engine.removeDependency(dependency);
448             }
449         } catch (IOException | DatabaseException e) {
450             throw new AnalysisException(e);
451         }
452     }
453 
454     @Override
455     protected void closeAnalyzer() throws Exception {
456         Log.set(Log.LEVEL_INFO);
457     }
458 }