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 org.apache.commons.io.FileUtils;
21 import org.owasp.dependencycheck.Engine;
22 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
23 import org.owasp.dependencycheck.data.nsp.Advisory;
24 import org.owasp.dependencycheck.data.nsp.NspSearch;
25 import org.owasp.dependencycheck.data.nsp.SanitizePackage;
26 import org.owasp.dependencycheck.dependency.Dependency;
27 import org.owasp.dependencycheck.dependency.Vulnerability;
28 import org.owasp.dependencycheck.dependency.VulnerableSoftware;
29 import org.owasp.dependencycheck.utils.FileFilterBuilder;
30 import org.owasp.dependencycheck.utils.Settings;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33 import java.io.File;
34 import java.io.FileFilter;
35 import java.io.IOException;
36 import java.net.MalformedURLException;
37 import java.util.Arrays;
38 import java.util.HashSet;
39 import java.util.List;
40 import javax.annotation.concurrent.ThreadSafe;
41 import javax.json.Json;
42 import javax.json.JsonException;
43 import javax.json.JsonObject;
44 import javax.json.JsonObjectBuilder;
45 import javax.json.JsonReader;
46 import org.owasp.dependencycheck.exception.InitializationException;
47 import org.owasp.dependencycheck.utils.InvalidSettingException;
48 import org.owasp.dependencycheck.utils.URLConnectionFailureException;
49 
50 /**
51  * Used to analyze Node Package Manager (npm) package.json files via Node
52  * Security Platform (nsp).
53  *
54  * @author Steve Springett
55  */
56 @ThreadSafe
57 public class NspAnalyzer extends AbstractNpmAnalyzer {
58 
59     /**
60      * The logger.
61      */
62     private static final Logger LOGGER = LoggerFactory.getLogger(NspAnalyzer.class);
63 
64     /**
65      * The default URL to the NSP check API.
66      */
67     public static final String DEFAULT_URL = "https://api.nodesecurity.io/check";
68     /**
69      * A descriptor for the type of dependencies processed or added by this
70      * analyzer.
71      */
72     public static final String DEPENDENCY_ECOSYSTEM = NPM_DEPENDENCY_ECOSYSTEM;
73     /**
74      * The file name to scan.
75      */
76     private static final String PACKAGE_JSON = "package.json";
77 
78     /**
79      * Filter that detects files named "package.json".
80      */
81     private static final FileFilter PACKAGE_JSON_FILTER = FileFilterBuilder.newInstance()
82             .addFilenames(PACKAGE_JSON).build();
83 
84     /**
85      * The NSP Searcher.
86      */
87     private NspSearch searcher;
88 
89     /**
90      * Returns the FileFilter
91      *
92      * @return the FileFilter
93      */
94     @Override
getFileFilter()95     protected FileFilter getFileFilter() {
96         return PACKAGE_JSON_FILTER;
97     }
98 
99     /**
100      * Initializes the analyzer once before any analysis is performed.
101      *
102      * @param engine a reference to the dependency-check engine
103      * @throws InitializationException if there's an error during initialization
104      */
105     @Override
prepareFileTypeAnalyzer(Engine engine)106     public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
107         LOGGER.debug("Initializing {}", getName());
108         try {
109             searcher = new NspSearch(getSettings());
110         } catch (MalformedURLException ex) {
111             setEnabled(false);
112             throw new InitializationException("The configured URL to Node Security Platform is malformed", ex);
113         }
114         try {
115             final Settings settings = engine.getSettings();
116             final boolean nodeEnabled = settings.getBoolean(Settings.KEYS.ANALYZER_NODE_PACKAGE_ENABLED);
117             if (!nodeEnabled) {
118                 LOGGER.warn("The Node Package Analyzer has been disabled; the resulting report will only "
119                         + " contain the known vulnerable dependency - not a bill of materials for the node project.");
120             }
121         } catch (InvalidSettingException ex) {
122             throw new InitializationException("Unable to read configuration settings", ex);
123         }
124     }
125 
126     /**
127      * Returns the name of the analyzer.
128      *
129      * @return the name of the analyzer.
130      */
131     @Override
getName()132     public String getName() {
133         return "Node Security Platform Analyzer";
134     }
135 
136     /**
137      * Returns the phase that the analyzer is intended to run in.
138      *
139      * @return the phase that the analyzer is intended to run in.
140      */
141     @Override
getAnalysisPhase()142     public AnalysisPhase getAnalysisPhase() {
143         return AnalysisPhase.FINDING_ANALYSIS;
144     }
145 
146     /**
147      * Returns the key used in the properties file to determine if the analyzer
148      * is enabled.
149      *
150      * @return the enabled property setting key for the analyzer
151      */
152     @Override
getAnalyzerEnabledSettingKey()153     protected String getAnalyzerEnabledSettingKey() {
154         return Settings.KEYS.ANALYZER_NSP_PACKAGE_ENABLED;
155     }
156 
157     @Override
analyzeDependency(Dependency dependency, Engine engine)158     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
159         engine.removeDependency(dependency);
160         final File file = dependency.getActualFile();
161         if (!file.isFile() || file.length() == 0 || !shouldProcess(file)) {
162             return;
163         }
164 
165         try (JsonReader jsonReader = Json.createReader(FileUtils.openInputStream(file))) {
166 
167             // Retrieves the contents of package.json from the Dependency
168             final JsonObject packageJson = jsonReader.readObject();
169 
170             // Create a sanitized version of the package.json
171             final JsonObject sanitizedJson = SanitizePackage.sanitize(packageJson);
172 
173             // Create a new 'package' object that acts as a container for the sanitized package.json
174             final JsonObjectBuilder builder = Json.createObjectBuilder();
175             final JsonObject nspPayload = builder.add("package", sanitizedJson).build();
176 
177             // Submits the package payload to the nsp check service
178             final List<Advisory> advisories = searcher.submitPackage(nspPayload);
179 
180             for (Advisory advisory : advisories) {
181                 /*
182                  * Create a new vulnerability out of the advisory returned by nsp.
183                  */
184                 final Vulnerability vuln = new Vulnerability();
185                 vuln.setCvssScore(advisory.getCvssScore());
186                 vuln.setDescription(advisory.getOverview());
187                 vuln.setName(String.valueOf(advisory.getId()));
188                 vuln.setSource(Vulnerability.Source.NSP);
189                 vuln.addReference(
190                         "NSP",
191                         "Advisory " + advisory.getId() + ": " + advisory.getTitle(),
192                         advisory.getAdvisory()
193                 );
194 
195                 /*
196                  * Create a single vulnerable software object - these do not use CPEs unlike the NVD.
197                  */
198                 final VulnerableSoftware vs = new VulnerableSoftware();
199                 //TODO consider changing this to available versions on the dependency
200                 //  - the update is a part of the version, not versions to update to
201                 //vs.setUpdate(advisory.getPatchedVersions());
202 
203                 vs.setName(advisory.getModule() + ":" + advisory.getVulnerableVersions());
204                 vuln.setVulnerableSoftware(new HashSet<>(Arrays.asList(vs)));
205 
206                 final Dependency existing = findDependency(engine, advisory.getModule(), advisory.getVersion());
207                 if (existing == null) {
208                     final Dependency nodeModule = createDependency(dependency, advisory.getModule(), advisory.getVersion(), "transitive");
209                     nodeModule.addVulnerability(vuln);
210                     engine.addDependency(nodeModule);
211                 } else {
212                     existing.addVulnerability(vuln);
213                 }
214             }
215         } catch (URLConnectionFailureException e) {
216             this.setEnabled(false);
217             throw new AnalysisException(e.getMessage(), e);
218         } catch (IOException e) {
219             LOGGER.debug("Error reading dependency or connecting to Node Security Platform - check API", e);
220             this.setEnabled(false);
221             throw new AnalysisException(e.getMessage(), e);
222         } catch (JsonException e) {
223             throw new AnalysisException(String.format("Failed to parse %s file.", file.getPath()), e);
224         }
225     }
226 }
227