NpmPayloadBuilder.java
/*
* This file is part of dependency-check-core.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Copyright (c) 2017 Steve Springett. All Rights Reserved.
*/
package org.owasp.dependencycheck.data.nodeaudit;
import org.owasp.dependencycheck.analyzer.NodePackageAnalyzer;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonString;
import javax.json.JsonValue;
import javax.annotation.concurrent.ThreadSafe;
import org.apache.commons.collections4.MultiValuedMap;
/**
* Class used to create the payload to submit to the NPM Audit API service.
*
* @author Steve Springett
* @author Jeremy Long
*/
@ThreadSafe
public final class NpmPayloadBuilder {
/**
* Private constructor for utility class.
*/
private NpmPayloadBuilder() {
//empty
}
/**
* Builds an npm audit API payload.
*
* @param lockJson the package-lock.json
* @param packageJson the package.json
* @param dependencyMap a collection of module/version pairs that is
* populated while building the payload
* @param skipDevDependencies whether devDependencies should be skipped
* @return the npm audit API payload
*/
public static JsonObject build(JsonObject lockJson, JsonObject packageJson,
MultiValuedMap<String, String> dependencyMap, boolean skipDevDependencies) {
final JsonObjectBuilder payloadBuilder = Json.createObjectBuilder();
addProjectInfo(packageJson, payloadBuilder);
// NPM Audit expects 'requires' to be an object containing key/value
// pairs corresponding to the module name (key) and version (value).
final JsonObjectBuilder requiresBuilder = Json.createObjectBuilder();
if (packageJson.containsKey("dependencies")) {
packageJson.getJsonObject("dependencies").entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(oldValue, newValue) -> newValue, TreeMap::new))
.forEach((key, value) -> {
if (NodePackageAnalyzer.shouldSkipDependency(key, ((JsonString) value).getString())) {
return;
}
requiresBuilder.add(key, value);
dependencyMap.put(key, value.toString());
});
}
if (!skipDevDependencies && packageJson.containsKey("devDependencies")) {
packageJson.getJsonObject("devDependencies").entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(oldValue, newValue) -> newValue, TreeMap::new))
.forEach((key, value) -> {
if (NodePackageAnalyzer.shouldSkipDependency(key, ((JsonString) value).getString())) {
return;
}
requiresBuilder.add(key, value);
dependencyMap.put(key, value.toString());
});
}
payloadBuilder.add("requires", requiresBuilder.build());
final JsonObjectBuilder dependenciesBuilder = Json.createObjectBuilder();
final int lockJsonVersion = lockJson.containsKey("lockfileVersion") ? lockJson.getInt("lockfileVersion") : 1;
JsonObject dependencies = lockJson.getJsonObject("dependencies");
if (lockJsonVersion >= 2 && dependencies == null) {
dependencies = lockJson.getJsonObject("packages");
}
if (dependencies != null) {
dependencies.forEach((k, value) -> {
String key = k;
final int indexOfNodeModule = key.lastIndexOf(NodePackageAnalyzer.NODE_MODULES_DIRNAME + "/");
if (indexOfNodeModule >= 0) {
key = key.substring(indexOfNodeModule + NodePackageAnalyzer.NODE_MODULES_DIRNAME.length() + 1);
}
JsonObject dep = ((JsonObject) value);
//After Version 3, dependencies can't be taken directly from package-lock.json
if (lockJsonVersion > 2 && dep.containsKey("dependencies") && dep.get("dependencies") instanceof JsonObject) {
final JsonObjectBuilder depBuilder = Json.createObjectBuilder(dep);
depBuilder.remove("dependencies");
depBuilder.add("requires", dep.get("dependencies"));
dep = depBuilder.build();
}
final String version = dep.getString("version", "");
final boolean isDev = dep.getBoolean("dev", false);
if (skipDevDependencies && isDev) {
return;
}
if (NodePackageAnalyzer.shouldSkipDependency(key, version)) {
return;
}
dependencyMap.put(key, version);
dependenciesBuilder.add(key, buildDependencies(dep, dependencyMap));
});
}
payloadBuilder.add("dependencies", dependenciesBuilder.build());
addConstantElements(payloadBuilder);
return payloadBuilder.build();
}
/**
* Attempts to build the request data for NPM Audit API call. This may
* produce a payload that will fail.
*
* @param packageJson a raw package-lock.json file
* @param dependencyMap a collection of module/version pairs that is
* @param skipDevDependencies whether devDependencies should be skipped
* populated while building the payload
* @return the JSON payload for NPN Audit
*/
public static JsonObject build(JsonObject packageJson, MultiValuedMap<String, String> dependencyMap,
final boolean skipDevDependencies) {
final JsonObjectBuilder payloadBuilder = Json.createObjectBuilder();
addProjectInfo(packageJson, payloadBuilder);
// NPM Audit expects 'requires' to be an object containing key/value
// pairs corresponding to the module name (key) and version (value).
final JsonObjectBuilder requiresBuilder = Json.createObjectBuilder();
final JsonObjectBuilder dependenciesBuilder = Json.createObjectBuilder();
final JsonObject dependencies = packageJson.getJsonObject("dependencies");
if (dependencies != null) {
dependencies.forEach((name, value) -> {
final String version;
if (value.getValueType() == JsonValue.ValueType.OBJECT) {
final JsonObject dep = ((JsonObject) value);
version = Optional.ofNullable(dep.getJsonString("version"))
.map(JsonString::getString)
.orElse(null);
final boolean isDev = dep.getBoolean("dev", false);
if (skipDevDependencies && isDev) {
return;
}
if (NodePackageAnalyzer.shouldSkipDependency(name, version)) {
return;
}
dependencyMap.put(name, version);
dependenciesBuilder.add(name, buildDependencies(dep, dependencyMap));
} else {
//TODO I think the following is dead code and no real "dependencies"
// section in a lock file will look like this
final String tmp = value.toString();
if (tmp.startsWith("\"")) {
version = tmp.substring(1, tmp.length() - 1);
} else {
version = tmp;
}
}
requiresBuilder.add(name, Objects.isNull(version) ? "*" : "^" + version);
});
}
payloadBuilder.add("requires", requiresBuilder.build());
payloadBuilder.add("dependencies", dependenciesBuilder.build());
addConstantElements(payloadBuilder);
return payloadBuilder.build();
}
/**
* Adds the project name and version to the npm audit API payload.
*
* @param packageJson a reference to the package-lock.json
* @param payloadBuilder a reference to the npm audit API payload builder
*/
private static void addProjectInfo(JsonObject packageJson, final JsonObjectBuilder payloadBuilder) {
final String projectName = packageJson.getString("name", "");
final String projectVersion = packageJson.getString("version", "");
if (!projectName.isEmpty()) {
payloadBuilder.add("name", projectName);
}
if (!projectVersion.isEmpty()) {
payloadBuilder.add("version", projectVersion);
}
}
/**
* Adds the constant data elements to the npm audit API payload.
*
* @param payloadBuilder a reference to the npm audit API payload builder
*/
private static void addConstantElements(final JsonObjectBuilder payloadBuilder) {
payloadBuilder.add("install", Json.createArrayBuilder().build());
payloadBuilder.add("remove", Json.createArrayBuilder().build());
payloadBuilder.add("metadata", Json.createObjectBuilder()
.add("npm_version", "6.9.0")
.add("node_version", "v10.5.0")
.add("platform", "linux")
);
}
/**
* Recursively builds the dependency structure - copying only the needed
* items from the package-lock.json into the npm audit API payload.
*
* @param dep the parent dependency
* @param dependencyMap the collection of child dependencies
* @return the dependencies structure needed for the npm audit API payload
*/
private static JsonObject buildDependencies(JsonObject dep, MultiValuedMap<String, String> dependencyMap) {
final JsonObjectBuilder depBuilder = Json.createObjectBuilder();
Optional.ofNullable(dep.getJsonString("version"))
.map(JsonString::getString)
.ifPresent(version -> depBuilder.add("version", version));
//not installed package (like, dependency of an optional dependency) doesn't contains integrity
if (dep.containsKey("integrity")) {
depBuilder.add("integrity", dep.getString("integrity"));
}
if (dep.containsKey("requires")) {
final JsonObjectBuilder requiresBuilder = Json.createObjectBuilder();
dep.getJsonObject("requires").forEach((key, value) -> {
if (NodePackageAnalyzer.shouldSkipDependency(key, ((JsonString) value).getString())) {
return;
}
requiresBuilder.add(key, value);
});
depBuilder.add("requires", requiresBuilder.build());
}
if (dep.containsKey("dependencies")) {
final JsonObjectBuilder dependeciesBuilder = Json.createObjectBuilder();
dep.getJsonObject("dependencies").forEach((key, value) -> {
if (value.getValueType() == JsonValue.ValueType.OBJECT) {
final JsonObject currentDep = (JsonObject) value;
final String v = currentDep.getString("version");
dependencyMap.put(key, v);
dependeciesBuilder.add(key, buildDependencies(currentDep, dependencyMap));
} else {
final String tmp = value.toString();
final String v;
if (tmp.startsWith("\"")) {
v = tmp.substring(1, tmp.length() - 1);
} else {
v = tmp;
}
dependencyMap.put(key, v);
dependeciesBuilder.add(key, v);
}
});
depBuilder.add("dependencies", dependeciesBuilder.build());
}
return depBuilder.build();
}
}