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) 2018 Steve Springett. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.analyzer;
19  
20  import org.apache.commons.collections4.MultiValuedMap;
21  import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
22  import org.owasp.dependencycheck.Engine;
23  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
24  import org.owasp.dependencycheck.analyzer.exception.SearchException;
25  import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException;
26  import org.owasp.dependencycheck.data.nodeaudit.Advisory;
27  import org.owasp.dependencycheck.data.nodeaudit.NpmPayloadBuilder;
28  import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
29  import org.owasp.dependencycheck.dependency.Dependency;
30  import org.owasp.dependencycheck.utils.FileFilterBuilder;
31  import org.owasp.dependencycheck.utils.Settings;
32  import org.owasp.dependencycheck.utils.URLConnectionFailureException;
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  import us.springett.parsers.cpe.exceptions.CpeValidationException;
36  
37  import javax.annotation.concurrent.ThreadSafe;
38  import javax.json.Json;
39  import javax.json.JsonException;
40  import javax.json.JsonObject;
41  import javax.json.JsonReader;
42  import java.io.File;
43  import java.io.FileFilter;
44  import java.io.IOException;
45  import java.nio.file.Files;
46  import java.util.List;
47  
48  /**
49   * Used to analyze Node Package Manager (npm) package-lock.json and
50   * npm-shrinkwrap.json files via NPM Audit API.
51   *
52   * @author Steve Springett
53   */
54  @ThreadSafe
55  public class NodeAuditAnalyzer extends AbstractNpmAnalyzer {
56  
57      /**
58       * The logger.
59       */
60      private static final Logger LOGGER = LoggerFactory.getLogger(NodeAuditAnalyzer.class);
61      /**
62       * The default URL to the NPM Audit API.
63       */
64      public static final String DEFAULT_URL = "https://registry.npmjs.org/-/npm/v1/security/audits";
65      /**
66       * A descriptor for the type of dependencies processed or added by this
67       * analyzer.
68       */
69      public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.NODEJS;
70      /**
71       * The file name to scan.
72       */
73      public static final String PACKAGE_LOCK_JSON = "package-lock.json";
74      /**
75       * The file name to scan.
76       */
77      public static final String SHRINKWRAP_JSON = "npm-shrinkwrap.json";
78  
79      /**
80       * Filter that detects files named "package-lock.json or
81       * npm-shrinkwrap.json".
82       */
83      private static final FileFilter PACKAGE_JSON_FILTER = FileFilterBuilder.newInstance()
84              .addFilenames(PACKAGE_LOCK_JSON, SHRINKWRAP_JSON).build();
85  
86      /**
87       * Returns the FileFilter
88       *
89       * @return the FileFilter
90       */
91      @Override
92      protected FileFilter getFileFilter() {
93          return PACKAGE_JSON_FILTER;
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 "Node Audit Analyzer";
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 AnalysisPhase.FINDING_ANALYSIS;
114     }
115 
116     /**
117      * Returns the key used in the properties file to determine if the analyzer
118      * is enabled.
119      *
120      * @return the enabled property setting key for the analyzer
121      */
122     @Override
123     protected String getAnalyzerEnabledSettingKey() {
124         return Settings.KEYS.ANALYZER_NODE_AUDIT_ENABLED;
125     }
126 
127     @Override
128     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
129         if (dependency.getDisplayFileName().equals(dependency.getFileName())) {
130             engine.removeDependency(dependency);
131         }
132         final File packageLock = dependency.getActualFile();
133         final File shrinkwrap = new File(packageLock.getParentFile(), SHRINKWRAP_JSON);
134         if (PACKAGE_LOCK_JSON.equals(dependency.getFileName()) && shrinkwrap.isFile()) {
135             LOGGER.debug("Skipping {} because shrinkwrap lock file exists", dependency.getFilePath());
136             return;
137         }
138         if (!packageLock.isFile() || packageLock.length() == 0 || !shouldProcess(packageLock)) {
139             return;
140         }
141         final File packageJson = new File(packageLock.getParentFile(), "package.json");
142         final List<Advisory> advisories;
143         final MultiValuedMap<String, String> dependencyMap = new HashSetValuedHashMap<>();
144         //final Map<String, String> dependencyMap = new HashMap<>();
145         if (packageJson.isFile()) {
146             advisories = analyzePackage(packageLock, packageJson, dependency, dependencyMap);
147         } else {
148             advisories = legacyAnalysis(packageLock, dependency, dependencyMap);
149         }
150         try {
151             processResults(advisories, engine, dependency, dependencyMap);
152         } catch (CpeValidationException ex) {
153             throw new UnexpectedAnalysisException(ex);
154         }
155     }
156 
157     /**
158      * Analyzes the package and package-lock files by extracting dependency
159      * information, creating a payload to submit to the npm audit API,
160      * submitting the payload, and returning the identified advisories.
161      *
162      * @param lockFile a reference to the package-lock.json
163      * @param packageFile a reference to the package.json
164      * @param dependency a reference to the dependency-object for the
165      * package-lock.json
166      * @param dependencyMap a collection of module/version pairs; during
167      * creation of the payload the dependency map is populated with the
168      * module/version information.
169      * @return a list of advisories
170      * @throws AnalysisException thrown when there is an error creating or
171      * submitting the npm audit API payload
172      */
173     private List<Advisory> analyzePackage(final File lockFile, final File packageFile,
174                                           Dependency dependency, MultiValuedMap<String, String> dependencyMap)
175             throws AnalysisException {
176         try {
177             final JsonReader packageReader = Json.createReader(Files.newInputStream(packageFile.toPath()));
178             final JsonReader lockReader = Json.createReader(Files.newInputStream(lockFile.toPath()));
179             // Retrieves the contents of package-lock.json from the Dependency
180             final JsonObject lockJson = lockReader.readObject();
181             // Retrieves the contents of package-lock.json from the Dependency
182             final JsonObject packageJson = packageReader.readObject();
183 
184             // Modify the payload to meet the NPM Audit API requirements
185             final JsonObject payload = NpmPayloadBuilder.build(lockJson, packageJson, dependencyMap,
186                     getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false));
187 
188             // Submits the package payload to the nsp check service
189             return getSearcher().submitPackage(payload);
190 
191         } catch (URLConnectionFailureException e) {
192             this.setEnabled(false);
193             throw new AnalysisException("Failed to connect to the NPM Audit API (NodeAuditAnalyzer); the analyzer "
194                     + "is being disabled and may result in false negatives.", e);
195         } catch (IOException e) {
196             LOGGER.debug("Error reading dependency or connecting to NPM Audit API", e);
197             this.setEnabled(false);
198             throw new AnalysisException("Failed to read results from the NPM Audit API (NodeAuditAnalyzer); "
199                     + "the analyzer is being disabled and may result in false negatives.", e);
200         } catch (JsonException e) {
201             throw new AnalysisException(String.format("Failed to parse %s file from the NPM Audit API "
202                     + "(NodeAuditAnalyzer).", lockFile.getPath()), e);
203         } catch (SearchException e) {
204             final File yarnCheck = new File(lockFile.getParentFile(), "yarn.lock");
205             if (yarnCheck.exists()) {
206                 final String msg = "NodeAuditAnalyzer failed on " + dependency.getActualFilePath()
207                         + " - yarn.lock was found; if package-lock.json was generated using synp, it may not be in the correct format.";
208                 LOGGER.error(msg);
209                 throw new AnalysisException(msg, e);
210             }
211             LOGGER.error("NodeAuditAnalyzer failed on {}", dependency.getActualFilePath());
212             throw e;
213         }
214     }
215 
216     /**
217      * Analyzes the package and package-lock files by extracting dependency
218      * information, creating a payload to submit to the npm audit API,
219      * submitting the payload, and returning the identified advisories.
220      *
221      * @param file a reference to the package-lock.json
222      * @param dependency a reference to the dependency-object for the
223      * package-lock.json
224      * @param dependencyMap a collection of module/version pairs; during
225      * creation of the payload the dependency map is populated with the
226      * module/version information.
227      * @return a list of advisories
228      * @throws AnalysisException thrown when there is an error creating or
229      * submitting the npm audit API payload
230      */
231     private List<Advisory> legacyAnalysis(final File file, Dependency dependency, MultiValuedMap<String, String> dependencyMap)
232             throws AnalysisException {
233 
234         try (JsonReader jsonReader = Json.createReader(Files.newInputStream(file.toPath()))) {
235 
236             // Retrieves the contents of package-lock.json from the Dependency
237             final JsonObject packageJson = jsonReader.readObject();
238 
239             final String projectName = packageJson.getString("name", "");
240             final String projectVersion = packageJson.getString("version", "");
241             if (!projectName.isEmpty()) {
242                 dependency.setName(projectName);
243             }
244             if (!projectVersion.isEmpty()) {
245                 dependency.setVersion(projectVersion);
246             }
247 
248             // Modify the payload to meet the NPM Audit API requirements
249             final JsonObject payload = NpmPayloadBuilder.build(packageJson, dependencyMap,
250                     getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false));
251 
252             // Submits the package payload to the nsp check service
253             return getSearcher().submitPackage(payload);
254 
255         } catch (URLConnectionFailureException e) {
256             this.setEnabled(false);
257             throw new AnalysisException("Failed to connect to the NPM Audit API (NodeAuditAnalyzer); the analyzer "
258                     + "is being disabled and may result in false negatives.", e);
259         } catch (IOException e) {
260             LOGGER.debug("Error reading dependency or connecting to NPM Audit API", e);
261             this.setEnabled(false);
262             throw new AnalysisException("Failed to read results from the NPM Audit API (NodeAuditAnalyzer); "
263                     + "the analyzer is being disabled and may result in false negatives.", e);
264         } catch (JsonException e) {
265             throw new AnalysisException(String.format("Failed to parse %s file from the NPM Audit API "
266                     + "(NodeAuditAnalyzer).", file.getPath()), e);
267         } catch (SearchException ex) {
268             LOGGER.error("NodeAuditAnalyzer failed on {}", dependency.getActualFilePath());
269             throw ex;
270         }
271     }
272 }