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.apache.commons.io.IOUtils;
23 import org.apache.commons.lang3.StringUtils;
24 import org.owasp.dependencycheck.Engine;
25 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
26 import org.owasp.dependencycheck.analyzer.exception.SearchException;
27 import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException;
28 import org.owasp.dependencycheck.data.nodeaudit.Advisory;
29 import org.owasp.dependencycheck.data.nodeaudit.NpmPayloadBuilder;
30 import org.owasp.dependencycheck.dependency.Dependency;
31 import org.owasp.dependencycheck.exception.InitializationException;
32 import org.owasp.dependencycheck.utils.FileFilterBuilder;
33 import org.owasp.dependencycheck.utils.Settings;
34 import org.owasp.dependencycheck.utils.URLConnectionFailureException;
35 import org.owasp.dependencycheck.utils.processing.ProcessReader;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38 import us.springett.parsers.cpe.exceptions.CpeValidationException;
39
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.JsonReader;
45 import java.io.File;
46 import java.io.FileFilter;
47 import java.io.IOException;
48 import java.nio.charset.StandardCharsets;
49 import java.nio.file.Files;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.List;
53
54 @ThreadSafe
55 public class YarnAuditAnalyzer extends AbstractNpmAnalyzer {
56
57
58
59
60 private static final Logger LOGGER = LoggerFactory.getLogger(YarnAuditAnalyzer.class);
61
62
63
64
65 public static final String YARN_PACKAGE_LOCK = "yarn.lock";
66
67
68
69
70 private static final FileFilter LOCK_FILE_FILTER = FileFilterBuilder.newInstance()
71 .addFilenames(YARN_PACKAGE_LOCK).build();
72
73
74
75
76
77 private static final String EXPECTED_ERROR = "{\"type\":\"error\",\"data\":\"Can't make a request in "
78 + "offline mode (\\\"https://registry.yarnpkg.com/-/npm/v1/security/audits\\\")\"}\n";
79
80
81
82
83 private String yarnPath;
84
85
86
87
88
89
90
91
92
93 @Override
94 protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
95 if (dependency.getDisplayFileName().equals(dependency.getFileName())) {
96 engine.removeDependency(dependency);
97 }
98 final File packageLock = dependency.getActualFile();
99 if (!packageLock.isFile() || packageLock.length() == 0 || !shouldProcess(packageLock)) {
100 return;
101 }
102 final File packageJson = new File(packageLock.getParentFile(), "package.json");
103 final List<Advisory> advisories;
104 final MultiValuedMap<String, String> dependencyMap = new HashSetValuedHashMap<>();
105 advisories = analyzePackage(packageLock, packageJson, dependency, dependencyMap);
106 try {
107 processResults(advisories, engine, dependency, dependencyMap);
108 } catch (CpeValidationException ex) {
109 throw new UnexpectedAnalysisException(ex);
110 }
111 }
112
113 @Override
114 protected String getAnalyzerEnabledSettingKey() {
115 return Settings.KEYS.ANALYZER_YARN_AUDIT_ENABLED;
116 }
117
118 @Override
119 protected FileFilter getFileFilter() {
120 return LOCK_FILE_FILTER;
121 }
122
123 @Override
124 public String getName() {
125 return "Yarn Audit Analyzer";
126 }
127
128 @Override
129 public AnalysisPhase getAnalysisPhase() {
130 return AnalysisPhase.FINDING_ANALYSIS;
131 }
132
133
134
135
136
137
138
139 @Override
140 protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
141 super.prepareFileTypeAnalyzer(engine);
142 if (!isEnabled()) {
143 LOGGER.debug("{} Analyzer is disabled skipping yarn executable check", getName());
144 return;
145 }
146 final List<String> args = new ArrayList<>();
147 args.add(getYarn());
148 args.add("--help");
149 final ProcessBuilder builder = new ProcessBuilder(args);
150 LOGGER.debug("Launching: {}", args);
151 try {
152 final Process process = builder.start();
153 try (ProcessReader processReader = new ProcessReader(process)) {
154 processReader.readAll();
155 final int exitValue = process.waitFor();
156 final int expectedExitValue = 0;
157 final int yarnExecutableNotFoundExitValue = 127;
158 switch (exitValue) {
159 case expectedExitValue:
160 LOGGER.debug("{} is enabled.", getName());
161 break;
162 case yarnExecutableNotFoundExitValue:
163 default:
164 this.setEnabled(false);
165 LOGGER.warn("The {} has been disabled. Yarn executable was not found.", getName());
166 }
167 }
168 } catch (Exception ex) {
169 this.setEnabled(false);
170 LOGGER.warn("The {} has been disabled. Yarn executable was not found.", getName());
171 throw new InitializationException("Unable to read yarn audit output.", ex);
172 }
173 }
174
175
176
177
178
179
180 private String getYarn() {
181 final String value;
182 synchronized (this) {
183 if (yarnPath == null) {
184 final String path = getSettings().getString(Settings.KEYS.ANALYZER_YARN_PATH);
185 if (path == null) {
186 yarnPath = "yarn";
187 } else {
188 final File yarnFile = new File(path);
189 if (yarnFile.isFile()) {
190 yarnPath = yarnFile.getAbsolutePath();
191 } else {
192 LOGGER.warn("Provided path to `yarn` executable is invalid.");
193 yarnPath = "yarn";
194 }
195 }
196 }
197 value = yarnPath;
198 }
199 return value;
200 }
201
202 private JsonObject fetchYarnAuditJson(Dependency dependency, boolean skipDevDependencies) throws AnalysisException {
203 final File folder = dependency.getActualFile().getParentFile();
204 if (!folder.isDirectory()) {
205 throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
206 }
207 try {
208 final List<String> args = new ArrayList<>();
209
210 args.add(getYarn());
211 args.add("audit");
212
213 args.add("--offline");
214 if (skipDevDependencies) {
215 args.add("--groups");
216 args.add("dependencies");
217 }
218 args.add("--json");
219 args.add("--verbose");
220 final ProcessBuilder builder = new ProcessBuilder(args);
221 builder.directory(folder);
222 LOGGER.debug("Launching: {}", args);
223
224
225
226 final File tmpFile = getSettings().getTempFile("yarn_audit", "json");
227 builder.redirectOutput(tmpFile);
228 final Process process = builder.start();
229 try (ProcessReader processReader = new ProcessReader(process)) {
230 processReader.readAll();
231 final String errOutput = processReader.getError();
232
233 if (!StringUtils.isBlank(errOutput) && !EXPECTED_ERROR.equals(errOutput)) {
234 LOGGER.debug("Process Error Out: {}", errOutput);
235 LOGGER.debug("Process Out: {}", processReader.getOutput());
236 }
237 final String verboseJson = new String(Files.readAllBytes(tmpFile.toPath()), StandardCharsets.UTF_8);
238 final String auditRequestJson = Arrays.stream(verboseJson.split("\n"))
239 .filter(line -> line.contains("Audit Request"))
240 .findFirst().get();
241 String auditRequest;
242 try (JsonReader reader = Json.createReader(IOUtils.toInputStream(auditRequestJson, StandardCharsets.UTF_8))) {
243 final JsonObject jsonObject = reader.readObject();
244 auditRequest = jsonObject.getString("data");
245 auditRequest = auditRequest.substring(15);
246 }
247 LOGGER.debug("Audit Request: {}", auditRequest);
248
249 return Json.createReader(IOUtils.toInputStream(auditRequest, StandardCharsets.UTF_8)).readObject();
250 } catch (InterruptedException ex) {
251 Thread.currentThread().interrupt();
252 throw new AnalysisException("Yarn audit process was interrupted.", ex);
253 }
254 } catch (IOException ioe) {
255 throw new AnalysisException("yarn audit failure; this error can be ignored if you are not analyzing projects with a yarn lockfile.", ioe);
256 }
257 }
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274 private List<Advisory> analyzePackage(final File lockFile, final File packageFile,
275 Dependency dependency, MultiValuedMap<String, String> dependencyMap)
276 throws AnalysisException {
277 try {
278 final boolean skipDevDependencies = getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false);
279
280 final JsonObject lockJson = fetchYarnAuditJson(dependency, skipDevDependencies);
281
282 final JsonObject packageJson;
283 try (final JsonReader packageReader = Json.createReader(Files.newInputStream(packageFile.toPath()))) {
284 packageJson = packageReader.readObject();
285 }
286
287 final JsonObject payload = NpmPayloadBuilder.build(lockJson, packageJson, dependencyMap, skipDevDependencies);
288
289
290 return getSearcher().submitPackage(payload);
291
292 } catch (URLConnectionFailureException e) {
293 this.setEnabled(false);
294 throw new AnalysisException("Failed to connect to the NPM Audit API (YarnAuditAnalyzer); the analyzer "
295 + "is being disabled and may result in false negatives.", e);
296 } catch (IOException e) {
297 LOGGER.debug("Error reading dependency or connecting to NPM Audit API", e);
298 this.setEnabled(false);
299 throw new AnalysisException("Failed to read results from the NPM Audit API (YarnAuditAnalyzer); "
300 + "the analyzer is being disabled and may result in false negatives.", e);
301 } catch (JsonException e) {
302 throw new AnalysisException(String.format("Failed to parse %s file from the NPM Audit API "
303 + "(YarnAuditAnalyzer).", lockFile.getPath()), e);
304 } catch (SearchException ex) {
305 LOGGER.error("YarnAuditAnalyzer failed on {}", dependency.getActualFilePath());
306 throw ex;
307 }
308 }
309 }