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) 2017 Steve Springett. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.data.nodeaudit;
19  
20  import org.owasp.dependencycheck.analyzer.NodePackageAnalyzer;
21  
22  import java.util.Map;
23  import java.util.Objects;
24  import java.util.Optional;
25  import java.util.TreeMap;
26  import java.util.stream.Collectors;
27  import javax.json.Json;
28  import javax.json.JsonObject;
29  import javax.json.JsonObjectBuilder;
30  import javax.json.JsonString;
31  import javax.json.JsonValue;
32  import javax.annotation.concurrent.ThreadSafe;
33  import org.apache.commons.collections4.MultiValuedMap;
34  
35  /**
36   * Class used to create the payload to submit to the NPM Audit API service.
37   *
38   * @author Steve Springett
39   * @author Jeremy Long
40   */
41  @ThreadSafe
42  public final class NpmPayloadBuilder {
43  
44      /**
45       * Private constructor for utility class.
46       */
47      private NpmPayloadBuilder() {
48          //empty
49      }
50  
51      /**
52       * Builds an npm audit API payload.
53       *
54       * @param lockJson the package-lock.json
55       * @param packageJson the package.json
56       * @param dependencyMap a collection of module/version pairs that is
57       * populated while building the payload
58       * @param skipDevDependencies whether devDependencies should be skipped
59       * @return the npm audit API payload
60       */
61      public static JsonObject build(JsonObject lockJson, JsonObject packageJson,
62              MultiValuedMap<String, String> dependencyMap, boolean skipDevDependencies) {
63          final JsonObjectBuilder payloadBuilder = Json.createObjectBuilder();
64          addProjectInfo(packageJson, payloadBuilder);
65  
66          // NPM Audit expects 'requires' to be an object containing key/value
67          // pairs corresponding to the module name (key) and version (value).
68          final JsonObjectBuilder requiresBuilder = Json.createObjectBuilder();
69  
70          if (packageJson.containsKey("dependencies")) {
71              packageJson.getJsonObject("dependencies").entrySet()
72                      .stream()
73                      .collect(Collectors.toMap(
74                              Map.Entry::getKey,
75                              Map.Entry::getValue,
76                              (oldValue, newValue) -> newValue, TreeMap::new))
77                      .forEach((key, value) -> {
78                          if (NodePackageAnalyzer.shouldSkipDependency(key, ((JsonString) value).getString())) {
79                              return;
80                          }
81                          requiresBuilder.add(key, value);
82                          dependencyMap.put(key, value.toString());
83                      });
84          }
85  
86          if (!skipDevDependencies && packageJson.containsKey("devDependencies")) {
87              packageJson.getJsonObject("devDependencies").entrySet()
88                      .stream()
89                      .collect(Collectors.toMap(
90                              Map.Entry::getKey,
91                              Map.Entry::getValue,
92                              (oldValue, newValue) -> newValue, TreeMap::new))
93                      .forEach((key, value) -> {
94                          if (NodePackageAnalyzer.shouldSkipDependency(key, ((JsonString) value).getString())) {
95                              return;
96                          }
97                          requiresBuilder.add(key, value);
98                          dependencyMap.put(key, value.toString());
99                      });
100         }
101 
102         payloadBuilder.add("requires", requiresBuilder.build());
103 
104         final JsonObjectBuilder dependenciesBuilder = Json.createObjectBuilder();
105         final int lockJsonVersion = lockJson.containsKey("lockfileVersion") ? lockJson.getInt("lockfileVersion") : 1;
106         JsonObject dependencies = lockJson.getJsonObject("dependencies");
107         if (lockJsonVersion >= 2 && dependencies == null) {
108             dependencies = lockJson.getJsonObject("packages");
109         }
110 
111         if (dependencies != null) {
112             dependencies.forEach((k, value) -> {
113                 String key = k;
114                 final int indexOfNodeModule = key.lastIndexOf(NodePackageAnalyzer.NODE_MODULES_DIRNAME + "/");
115                 if (indexOfNodeModule >= 0) {
116                     key = key.substring(indexOfNodeModule + NodePackageAnalyzer.NODE_MODULES_DIRNAME.length() + 1);
117                 }
118 
119                 JsonObject dep = ((JsonObject) value);
120 
121                 //After Version 3, dependencies can't be taken directly from package-lock.json
122                 if (lockJsonVersion > 2 && dep.containsKey("dependencies") && dep.get("dependencies") instanceof JsonObject) {
123                     final JsonObjectBuilder depBuilder = Json.createObjectBuilder(dep);
124                     depBuilder.remove("dependencies");
125                     depBuilder.add("requires", dep.get("dependencies"));
126                     dep = depBuilder.build();
127                 }
128 
129                 final String version = dep.getString("version", "");
130                 final boolean isDev = dep.getBoolean("dev", false);
131                 if (skipDevDependencies && isDev) {
132                     return;
133                 }
134                 if (NodePackageAnalyzer.shouldSkipDependency(key, version)) {
135                     return;
136                 }
137                 dependencyMap.put(key, version);
138                 dependenciesBuilder.add(key, buildDependencies(dep, dependencyMap));
139             });
140         }
141         payloadBuilder.add("dependencies", dependenciesBuilder.build());
142 
143         addConstantElements(payloadBuilder);
144         return payloadBuilder.build();
145     }
146 
147     /**
148      * Attempts to build the request data for NPM Audit API call. This may
149      * produce a payload that will fail.
150      *
151      * @param packageJson a raw package-lock.json file
152      * @param dependencyMap a collection of module/version pairs that is
153      * @param skipDevDependencies whether devDependencies should be skipped
154      * populated while building the payload
155      * @return the JSON payload for NPN Audit
156      */
157     public static JsonObject build(JsonObject packageJson, MultiValuedMap<String, String> dependencyMap,
158             final boolean skipDevDependencies) {
159         final JsonObjectBuilder payloadBuilder = Json.createObjectBuilder();
160         addProjectInfo(packageJson, payloadBuilder);
161 
162         // NPM Audit expects 'requires' to be an object containing key/value
163         // pairs corresponding to the module name (key) and version (value).
164         final JsonObjectBuilder requiresBuilder = Json.createObjectBuilder();
165         final JsonObjectBuilder dependenciesBuilder = Json.createObjectBuilder();
166 
167         final JsonObject dependencies = packageJson.getJsonObject("dependencies");
168         if (dependencies != null) {
169             dependencies.forEach((name, value) -> {
170                 final String version;
171                 if (value.getValueType() == JsonValue.ValueType.OBJECT) {
172                     final JsonObject dep = ((JsonObject) value);
173                     version = Optional.ofNullable(dep.getJsonString("version"))
174                             .map(JsonString::getString)
175                             .orElse(null);
176 
177                     final boolean isDev = dep.getBoolean("dev", false);
178                     if (skipDevDependencies && isDev) {
179                         return;
180                     }
181                     if (NodePackageAnalyzer.shouldSkipDependency(name, version)) {
182                         return;
183                     }
184                     dependencyMap.put(name, version);
185                     dependenciesBuilder.add(name, buildDependencies(dep, dependencyMap));
186                 } else {
187                     //TODO I think the following is dead code and no real "dependencies"
188                     //     section in a lock file will look like this
189                     final String tmp = value.toString();
190                     if (tmp.startsWith("\"")) {
191                         version = tmp.substring(1, tmp.length() - 1);
192                     } else {
193                         version = tmp;
194                     }
195                 }
196                 requiresBuilder.add(name, Objects.isNull(version) ? "*" : "^" + version);
197             });
198         }
199         payloadBuilder.add("requires", requiresBuilder.build());
200 
201         payloadBuilder.add("dependencies", dependenciesBuilder.build());
202 
203         addConstantElements(payloadBuilder);
204         return payloadBuilder.build();
205     }
206 
207     /**
208      * Adds the project name and version to the npm audit API payload.
209      *
210      * @param packageJson a reference to the package-lock.json
211      * @param payloadBuilder a reference to the npm audit API payload builder
212      */
213     private static void addProjectInfo(JsonObject packageJson, final JsonObjectBuilder payloadBuilder) {
214         final String projectName = packageJson.getString("name", "");
215         final String projectVersion = packageJson.getString("version", "");
216         if (!projectName.isEmpty()) {
217             payloadBuilder.add("name", projectName);
218         }
219         if (!projectVersion.isEmpty()) {
220             payloadBuilder.add("version", projectVersion);
221         }
222     }
223 
224     /**
225      * Adds the constant data elements to the npm audit API payload.
226      *
227      * @param payloadBuilder a reference to the npm audit API payload builder
228      */
229     private static void addConstantElements(final JsonObjectBuilder payloadBuilder) {
230         payloadBuilder.add("install", Json.createArrayBuilder().build());
231         payloadBuilder.add("remove", Json.createArrayBuilder().build());
232         payloadBuilder.add("metadata", Json.createObjectBuilder()
233                 .add("npm_version", "6.9.0")
234                 .add("node_version", "v10.5.0")
235                 .add("platform", "linux")
236         );
237     }
238 
239     /**
240      * Recursively builds the dependency structure - copying only the needed
241      * items from the package-lock.json into the npm audit API payload.
242      *
243      * @param dep the parent dependency
244      * @param dependencyMap the collection of child dependencies
245      * @return the dependencies structure needed for the npm audit API payload
246      */
247     private static JsonObject buildDependencies(JsonObject dep, MultiValuedMap<String, String> dependencyMap) {
248         final JsonObjectBuilder depBuilder = Json.createObjectBuilder();
249         Optional.ofNullable(dep.getJsonString("version"))
250                 .map(JsonString::getString)
251                 .ifPresent(version -> depBuilder.add("version", version));
252 
253         //not installed package (like, dependency of an optional dependency) doesn't contains integrity
254         if (dep.containsKey("integrity")) {
255             depBuilder.add("integrity", dep.getString("integrity"));
256         }
257         if (dep.containsKey("requires")) {
258             final JsonObjectBuilder requiresBuilder = Json.createObjectBuilder();
259             dep.getJsonObject("requires").forEach((key, value) -> {
260                 if (NodePackageAnalyzer.shouldSkipDependency(key, ((JsonString) value).getString())) {
261                     return;
262                 }
263 
264                 requiresBuilder.add(key, value);
265             });
266             depBuilder.add("requires", requiresBuilder.build());
267         }
268         if (dep.containsKey("dependencies")) {
269             final JsonObjectBuilder dependeciesBuilder = Json.createObjectBuilder();
270             dep.getJsonObject("dependencies").forEach((key, value) -> {
271                 if (value.getValueType() == JsonValue.ValueType.OBJECT) {
272                     final JsonObject currentDep = (JsonObject) value;
273                     final String v = currentDep.getString("version");
274                     dependencyMap.put(key, v);
275                     dependeciesBuilder.add(key, buildDependencies(currentDep, dependencyMap));
276                 } else {
277                     final String tmp = value.toString();
278                     final String v;
279                     if (tmp.startsWith("\"")) {
280                         v = tmp.substring(1, tmp.length() - 1);
281                     } else {
282                         v = tmp;
283                     }
284                     dependencyMap.put(key, v);
285                     dependeciesBuilder.add(key, v);
286                 }
287             });
288             depBuilder.add("dependencies", dependeciesBuilder.build());
289         }
290         return depBuilder.build();
291     }
292 }