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 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
50
51
52
53
54 @ThreadSafe
55 public class NodeAuditAnalyzer extends AbstractNpmAnalyzer {
56
57
58
59
60 private static final Logger LOGGER = LoggerFactory.getLogger(NodeAuditAnalyzer.class);
61
62
63
64 public static final String DEFAULT_URL = "https://registry.npmjs.org/-/npm/v1/security/audits";
65
66
67
68
69 public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.NODEJS;
70
71
72
73 public static final String PACKAGE_LOCK_JSON = "package-lock.json";
74
75
76
77 public static final String SHRINKWRAP_JSON = "npm-shrinkwrap.json";
78
79
80
81
82
83 private static final FileFilter PACKAGE_JSON_FILTER = FileFilterBuilder.newInstance()
84 .addFilenames(PACKAGE_LOCK_JSON, SHRINKWRAP_JSON).build();
85
86
87
88
89
90
91 @Override
92 protected FileFilter getFileFilter() {
93 return PACKAGE_JSON_FILTER;
94 }
95
96
97
98
99
100
101 @Override
102 public String getName() {
103 return "Node Audit Analyzer";
104 }
105
106
107
108
109
110
111 @Override
112 public AnalysisPhase getAnalysisPhase() {
113 return AnalysisPhase.FINDING_ANALYSIS;
114 }
115
116
117
118
119
120
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
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
159
160
161
162
163
164
165
166
167
168
169
170
171
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
180 final JsonObject lockJson = lockReader.readObject();
181
182 final JsonObject packageJson = packageReader.readObject();
183
184
185 final JsonObject payload = NpmPayloadBuilder.build(lockJson, packageJson, dependencyMap,
186 getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false));
187
188
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
218
219
220
221
222
223
224
225
226
227
228
229
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
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
249 final JsonObject payload = NpmPayloadBuilder.build(packageJson, dependencyMap,
250 getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false));
251
252
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 }