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                 final JsonObject dep = ((JsonObject) value);
119                 final String version = dep.getString("version", "");
120                 final boolean isDev = dep.getBoolean("dev", false);
121                 if (skipDevDependencies && isDev) {
122                     return;
123                 }
124                 if (NodePackageAnalyzer.shouldSkipDependency(key, version)) {
125                     return;
126                 }
127                 dependencyMap.put(key, version);
128                 dependenciesBuilder.add(key, buildDependencies(dep, dependencyMap));
129             });
130         }
131         payloadBuilder.add("dependencies", dependenciesBuilder.build());
132 
133         addConstantElements(payloadBuilder);
134         return payloadBuilder.build();
135     }
136 
137     /**
138      * Attempts to build the request data for NPM Audit API call. This may
139      * produce a payload that will fail.
140      *
141      * @param packageJson a raw package-lock.json file
142      * @param dependencyMap a collection of module/version pairs that is
143      * @param skipDevDependencies whether devDependencies should be skipped
144      * populated while building the payload
145      * @return the JSON payload for NPN Audit
146      */
147     public static JsonObject build(JsonObject packageJson, MultiValuedMap<String, String> dependencyMap,
148                                    final boolean skipDevDependencies) {
149         final JsonObjectBuilder payloadBuilder = Json.createObjectBuilder();
150         addProjectInfo(packageJson, payloadBuilder);
151 
152         // NPM Audit expects 'requires' to be an object containing key/value
153         // pairs corresponding to the module name (key) and version (value).
154         final JsonObjectBuilder requiresBuilder = Json.createObjectBuilder();
155         final JsonObjectBuilder dependenciesBuilder = Json.createObjectBuilder();
156 
157         final JsonObject dependencies = packageJson.getJsonObject("dependencies");
158         if (dependencies != null) {
159             dependencies.forEach((name, value) -> {
160                 final String version;
161                 if (value.getValueType() == JsonValue.ValueType.OBJECT) {
162                     final JsonObject dep = ((JsonObject) value);
163                     version = Optional.ofNullable(dep.getJsonString("version"))
164                             .map(JsonString::getString)
165                             .orElse(null);
166 
167                     final boolean isDev = dep.getBoolean("dev", false);
168                     if (skipDevDependencies && isDev) {
169                         return;
170                     }
171                     if (NodePackageAnalyzer.shouldSkipDependency(name, version)) {
172                         return;
173                     }
174                     dependencyMap.put(name, version);
175                     dependenciesBuilder.add(name, buildDependencies(dep, dependencyMap));
176                 } else {
177                     //TODO I think the following is dead code and no real "dependencies"
178                     //     section in a lock file will look like this
179                     final String tmp = value.toString();
180                     if (tmp.startsWith("\"")) {
181                         version = tmp.substring(1, tmp.length() - 1);
182                     } else {
183                         version = tmp;
184                     }
185                 }
186                 requiresBuilder.add(name, Objects.isNull(version) ? "*" : "^" + version);
187             });
188         }
189         payloadBuilder.add("requires", requiresBuilder.build());
190 
191         payloadBuilder.add("dependencies", dependenciesBuilder.build());
192 
193         addConstantElements(payloadBuilder);
194         return payloadBuilder.build();
195     }
196 
197     /**
198      * Adds the project name and version to the npm audit API payload.
199      *
200      * @param packageJson a reference to the package-lock.json
201      * @param payloadBuilder a reference to the npm audit API payload builder
202      */
203     private static void addProjectInfo(JsonObject packageJson, final JsonObjectBuilder payloadBuilder) {
204         final String projectName = packageJson.getString("name", "");
205         final String projectVersion = packageJson.getString("version", "");
206         if (!projectName.isEmpty()) {
207             payloadBuilder.add("name", projectName);
208         }
209         if (!projectVersion.isEmpty()) {
210             payloadBuilder.add("version", projectVersion);
211         }
212     }
213 
214     /**
215      * Adds the constant data elements to the npm audit API payload.
216      *
217      * @param payloadBuilder a reference to the npm audit API payload builder
218      */
219     private static void addConstantElements(final JsonObjectBuilder payloadBuilder) {
220         payloadBuilder.add("install", Json.createArrayBuilder().build());
221         payloadBuilder.add("remove", Json.createArrayBuilder().build());
222         payloadBuilder.add("metadata", Json.createObjectBuilder()
223                 .add("npm_version", "6.9.0")
224                 .add("node_version", "v10.5.0")
225                 .add("platform", "linux")
226         );
227     }
228 
229     /**
230      * Recursively builds the dependency structure - copying only the needed
231      * items from the package-lock.json into the npm audit API payload.
232      *
233      * @param dep the parent dependency
234      * @param dependencyMap the collection of child dependencies
235      * @return the dependencies structure needed for the npm audit API payload
236      */
237     private static JsonObject buildDependencies(JsonObject dep, MultiValuedMap<String, String> dependencyMap) {
238         final JsonObjectBuilder depBuilder = Json.createObjectBuilder();
239         Optional.ofNullable(dep.getJsonString("version"))
240                         .map(JsonString::getString)
241                         .ifPresent(version -> depBuilder.add("version", version));
242 
243         //not installed package (like, dependency of an optional dependency) doesn't contains integrity
244         if (dep.containsKey("integrity")) {
245             depBuilder.add("integrity", dep.getString("integrity"));
246         }
247         if (dep.containsKey("requires")) {
248             final JsonObjectBuilder requiresBuilder = Json.createObjectBuilder();
249             dep.getJsonObject("requires").forEach((key, value) -> {
250                 if (NodePackageAnalyzer.shouldSkipDependency(key, ((JsonString) value).getString())) {
251                     return;
252                 }
253 
254                 requiresBuilder.add(key, value);
255             });
256             depBuilder.add("requires", requiresBuilder.build());
257         }
258         if (dep.containsKey("dependencies")) {
259             final JsonObjectBuilder dependeciesBuilder = Json.createObjectBuilder();
260             dep.getJsonObject("dependencies").forEach((key, value) -> {
261                 if (value.getValueType() == JsonValue.ValueType.OBJECT) {
262                     final JsonObject currentDep = (JsonObject) value;
263                     final String v = currentDep.getString("version");
264                     dependencyMap.put(key, v);
265                     dependeciesBuilder.add(key, buildDependencies(currentDep, dependencyMap));
266                 } else {
267                     final String tmp = value.toString();
268                     final String v;
269                     if (tmp.startsWith("\"")) {
270                         v = tmp.substring(1, tmp.length() - 1);
271                     } else {
272                         v = tmp;
273                     }
274                     dependencyMap.put(key, v);
275                     dependeciesBuilder.add(key, v);
276                 }
277             });
278             depBuilder.add("dependencies", dependeciesBuilder.build());
279         }
280         return depBuilder.build();
281     }
282 }