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 jakarta.json.Json;
39  import jakarta.json.JsonException;
40  import jakarta.json.JsonObject;
41  import jakarta.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 }