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