Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.blackduck.integration.detectable.extraction.Extraction;
import com.blackduck.integration.detectable.util.ToolVersionLogger;
import com.blackduck.integration.executable.ExecutableOutput;
import com.blackduck.integration.util.ExcludedIncludedWildcardFilter;

public class NpmCliExtractor {
public static final String OUTPUT_FILE = "detect_npm_proj_dependencies.json";
Expand All @@ -36,12 +37,20 @@ public class NpmCliExtractor {
private final NpmCliParser npmCliParser;
private final Gson gson;
private final ToolVersionLogger toolVersionLogger;
@Nullable private final ExcludedIncludedWildcardFilter workspaceFilter;

public NpmCliExtractor(DetectableExecutableRunner executableRunner, NpmCliParser npmCliParser, Gson gson, ToolVersionLogger toolVersionLogger) {
this(executableRunner, npmCliParser, gson, toolVersionLogger, null);
}

public NpmCliExtractor(DetectableExecutableRunner executableRunner, NpmCliParser npmCliParser,
Gson gson, ToolVersionLogger toolVersionLogger,
@Nullable ExcludedIncludedWildcardFilter workspaceFilter) {
this.executableRunner = executableRunner;
this.npmCliParser = npmCliParser;
this.gson = gson;
this.toolVersionLogger = toolVersionLogger;
this.workspaceFilter = workspaceFilter;
}

public Extraction extract(File directory, ExecutableTarget npmExe, @Nullable String npmArguments, File packageJsonFile) {
Expand Down Expand Up @@ -77,7 +86,7 @@ public Extraction extract(File directory, ExecutableTarget npmExe, @Nullable Str
} else if (StringUtils.isNotBlank(standardOutput)) {
logger.debug("Parsing npm ls file.");
logger.debug(standardOutput);
NpmPackagerResult result = npmCliParser.generateCodeLocation(standardOutput, combinedPackageJson);
NpmPackagerResult result = npmCliParser.generateCodeLocation(standardOutput, combinedPackageJson, workspaceFilter);
String projectName = result.getProjectName() != null ? result.getProjectName() : combinedPackageJson.getName();
String projectVersion = result.getProjectVersion() != null ? result.getProjectVersion() : combinedPackageJson.getVersion();
return new Extraction.Builder().success(result.getCodeLocation()).projectName(projectName).projectVersion(projectVersion).build();
Expand All @@ -91,7 +100,7 @@ private CombinedPackageJson parsePackageJson(File packageJson) throws IOExceptio
String packageJsonText = FileUtils.readFileToString(packageJson, StandardCharsets.UTF_8);

CombinedPackageJsonExtractor extractor = new CombinedPackageJsonExtractor(gson);
CombinedPackageJson combinedPackageJson = extractor.constructCombinedPackageJson(packageJson.getPath(), packageJsonText);
CombinedPackageJson combinedPackageJson = extractor.constructCombinedPackageJson(packageJson.getPath(), packageJsonText, workspaceFilter);

return combinedPackageJson;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.blackduck.integration.detectable.detectables.npm.cli;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

import com.blackduck.integration.detectable.detectable.util.EnumListFilter;
Expand All @@ -8,10 +10,33 @@
public class NpmCliExtractorOptions {
private final EnumListFilter<NpmDependencyType> npmDependencyTypeFilter;
private final String npmArguments;
private final List<String> excludedWorkspaceNames;
private final List<String> includedWorkspaceNames;
private final boolean ignoreAllWorkspaces;

public NpmCliExtractorOptions(EnumListFilter<NpmDependencyType> npmDependencyTypeFilter, String npmArguments) {
public NpmCliExtractorOptions(EnumListFilter<NpmDependencyType> npmDependencyTypeFilter,
String npmArguments) {
this(npmDependencyTypeFilter, npmArguments,
Collections.emptyList(), Collections.emptyList(), false);
}

public NpmCliExtractorOptions(EnumListFilter<NpmDependencyType> npmDependencyTypeFilter,
String npmArguments,
List<String> excludedWorkspaceNames,
List<String> includedWorkspaceNames) {
this(npmDependencyTypeFilter, npmArguments, excludedWorkspaceNames, includedWorkspaceNames, false);
}

public NpmCliExtractorOptions(EnumListFilter<NpmDependencyType> npmDependencyTypeFilter,
String npmArguments,
List<String> excludedWorkspaceNames,
List<String> includedWorkspaceNames,
boolean ignoreAllWorkspaces) {
this.npmDependencyTypeFilter = npmDependencyTypeFilter;
this.npmArguments = npmArguments;
this.excludedWorkspaceNames = excludedWorkspaceNames;
this.includedWorkspaceNames = includedWorkspaceNames;
this.ignoreAllWorkspaces = ignoreAllWorkspaces;
}

public EnumListFilter<NpmDependencyType> getDependencyTypeFilter() {
Expand All @@ -21,4 +46,16 @@ public EnumListFilter<NpmDependencyType> getDependencyTypeFilter() {
public Optional<String> getNpmArguments() {
return Optional.ofNullable(npmArguments);
}

public List<String> getExcludedWorkspaceNames() {
return excludedWorkspaceNames;
}

public List<String> getIncludedWorkspaceNames() {
return includedWorkspaceNames;
}

public boolean isIgnoreAllWorkspaces() {
return ignoreAllWorkspaces;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -24,6 +25,7 @@
import com.blackduck.integration.detectable.detectables.npm.NpmDependencyType;
import com.blackduck.integration.detectable.detectables.npm.lockfile.result.NpmPackagerResult;
import com.blackduck.integration.detectable.detectables.npm.packagejson.CombinedPackageJson;
import com.blackduck.integration.util.ExcludedIncludedWildcardFilter;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
Expand All @@ -45,41 +47,45 @@ public NpmCliParser(ExternalIdFactory externalIdFactory, EnumListFilter<NpmDepen
}

public NpmPackagerResult generateCodeLocation(String npmLsOutput, CombinedPackageJson combinedPackageJson) {
return generateCodeLocation(npmLsOutput, combinedPackageJson, null);
}

public NpmPackagerResult generateCodeLocation(String npmLsOutput, CombinedPackageJson combinedPackageJson,
@Nullable ExcludedIncludedWildcardFilter workspaceFilter) {
if (StringUtils.isBlank(npmLsOutput)) {
logger.error("Ran into an issue creating and writing to file");
return null;
}

logger.debug("Generating results from npm ls -json");
return convertNpmJsonFileToCodeLocation(npmLsOutput, combinedPackageJson);
return convertNpmJsonFileToCodeLocation(npmLsOutput, combinedPackageJson, workspaceFilter);
}

public NpmPackagerResult convertNpmJsonFileToCodeLocation(String npmLsOutput, CombinedPackageJson combinedPackageJson) {
return convertNpmJsonFileToCodeLocation(npmLsOutput, combinedPackageJson, null);
}

public NpmPackagerResult convertNpmJsonFileToCodeLocation(String npmLsOutput, CombinedPackageJson combinedPackageJson,
@Nullable ExcludedIncludedWildcardFilter workspaceFilter) {
JsonObject npmJson = JsonParser.parseString(npmLsOutput).getAsJsonObject();
DependencyGraph graph = new BasicDependencyGraph();

JsonElement projectNameElement = npmJson.getAsJsonPrimitive(JSON_NAME);
JsonElement projectVersionElement = npmJson.getAsJsonPrimitive(JSON_VERSION);
String projectName = null;
String projectVersion = null;
if (projectNameElement != null) {
projectName = projectNameElement.getAsString();
}
if (projectVersionElement != null) {
projectVersion = projectVersionElement.getAsString();
}
String projectName = projectNameElement != null ? projectNameElement.getAsString() : null;
String projectVersion = projectVersionElement != null ? projectVersionElement.getAsString() : null;

// Build alias mapping once from package.json
Map<String, String> aliasMapping = buildAliasMapping(combinedPackageJson);

populateChildren(graph, null, npmJson.getAsJsonObject(JSON_DEPENDENCIES), true, combinedPackageJson, aliasMapping);
populateChildren(graph, null, npmJson.getAsJsonObject(JSON_DEPENDENCIES), true,
combinedPackageJson, aliasMapping, workspaceFilter);

ExternalId externalId = externalIdFactory.createNameVersionExternalId(Forge.NPMJS, projectName, projectVersion);

CodeLocation codeLocation = new CodeLocation(graph, externalId);

return new NpmPackagerResult(projectName, projectVersion, codeLocation);

}

/**
Expand All @@ -99,7 +105,9 @@ private Map<String, String> buildAliasMapping(CombinedPackageJson combinedPackag
);
}

private void populateChildren(DependencyGraph graph, Dependency parentDependency, JsonObject parentNodeChildren, boolean isRootDependency, CombinedPackageJson combinedPackageJson, Map<String, String> aliasMapping) {
private void populateChildren(DependencyGraph graph, Dependency parentDependency, JsonObject parentNodeChildren,
boolean isRootDependency, CombinedPackageJson combinedPackageJson, Map<String, String> aliasMapping,
@Nullable ExcludedIncludedWildcardFilter workspaceFilter) {
if (parentNodeChildren == null) {
return;
}
Expand All @@ -113,15 +121,16 @@ private void populateChildren(DependencyGraph graph, Dependency parentDependency
// Transitives can be both application and dev/peer/optional dependency graphs, but Detect shouldn't be walking a dev, peer, or optional dependency tree unless it passed the filter already.
return true;
}
boolean excludingBecauseDev = (npmDependencyTypeFilter.shouldExclude(NpmDependencyType.DEV, combinedPackageJson.getDevDependencies()) && combinedPackageJson.getDevDependencies().containsKey(
elementEntry.getKey()));
boolean excludingBecauseDev = (npmDependencyTypeFilter.shouldExclude(NpmDependencyType.DEV, combinedPackageJson.getDevDependencies())
&& combinedPackageJson.getDevDependencies().containsKey(elementEntry.getKey()));
boolean excludingBecausePeer = (npmDependencyTypeFilter.shouldExclude(NpmDependencyType.PEER, combinedPackageJson.getPeerDependencies())
&& combinedPackageJson.getPeerDependencies().containsKey(elementEntry.getKey()));
boolean excludingBecauseOptional = (npmDependencyTypeFilter.shouldExclude(NpmDependencyType.OPTIONAL, combinedPackageJson.getOptionalDependencies())
&& combinedPackageJson.getOptionalDependencies().containsKey(elementEntry.getKey()));
return !excludingBecauseDev && !excludingBecausePeer && !excludingBecauseOptional;
})
.forEach(elementEntry -> processChild(elementEntry, graph, parentDependency, isRootDependency, combinedPackageJson, aliasMapping));
.forEach(elementEntry -> processChild(elementEntry, graph, parentDependency, isRootDependency,
combinedPackageJson, aliasMapping, workspaceFilter));
}

private void processChild(
Expand All @@ -130,7 +139,8 @@ private void processChild(
Dependency parentDependency,
boolean isRootDependency,
CombinedPackageJson combinedPackageJson,
Map<String, String> aliasMapping
Map<String, String> aliasMapping,
@Nullable ExcludedIncludedWildcardFilter workspaceFilter
) {
JsonObject element = elementEntry.getValue().getAsJsonObject();
String name = elementEntry.getKey();
Expand All @@ -150,29 +160,44 @@ private void processChild(
}
ExternalId externalId = externalIdFactory.createNameVersionExternalId(Forge.NPMJS, actualName, version);
Dependency child = new Dependency(actualName, version, externalId);
// Any workspace dependency is considered a direct dependency

// Any workspace dependency is considered a direct dependency (unless filtered out)
boolean directWorkspaceDependency = false;
String possibleWorkspaceDependency = Optional.ofNullable(element.getAsJsonPrimitive("resolved"))
.filter(JsonPrimitive::isString)
.map(JsonPrimitive::getAsString)
.orElse(null);

.filter(JsonPrimitive::isString)
.map(JsonPrimitive::getAsString)
.orElse(null);

// Whether this workspace was detected but explicitly excluded by the filter
boolean workspaceExcludedByFilter = false;

if (combinedPackageJson.getRelativeWorkspaces() != null && possibleWorkspaceDependency != null) {
// workspaces under the root resolve as file../<path to workspace>
// remove that and see if any absolute workspace paths have this subpath
String convertedPossibleWorkspaceDependency =
possibleWorkspaceDependency.replace("file:../", "");

directWorkspaceDependency =
combinedPackageJson.getRelativeWorkspaces().stream().anyMatch(workspace -> workspace.equals(convertedPossibleWorkspaceDependency));
// Strip "file:" prefix and any leading "../" or "./" segments; the remaining
// path is relative to the project root, matching entries in relativeWorkspaces.
String convertedPath = possibleWorkspaceDependency.replaceFirst("^file:(\\.\\./|\\./)*", "");
boolean isWorkspace = combinedPackageJson.getRelativeWorkspaces().stream()
.anyMatch(workspace -> workspace.equals(convertedPath));

if (isWorkspace) {
if (workspaceFilter != null) {
directWorkspaceDependency = workspaceFilter.shouldInclude(convertedPath);
workspaceExcludedByFilter = !directWorkspaceDependency;
} else {
directWorkspaceDependency = true;
}
}
}

populateChildren(graph, child, children, directWorkspaceDependency, combinedPackageJson, aliasMapping);
// When a workspace is excluded by the filter, do not traverse its children —
// treat it as a leaf (regular dependency with no transitive graph)
if (!workspaceExcludedByFilter) {
populateChildren(graph, child, children, directWorkspaceDependency,
combinedPackageJson, aliasMapping, workspaceFilter);
}

if (isRootDependency || directWorkspaceDependency) {
if ((isRootDependency && !workspaceExcludedByFilter) || directWorkspaceDependency) {
graph.addChildToRoot(child);
} else {
} else if (!workspaceExcludedByFilter) {
graph.addParentWithChild(parentDependency, child);
}
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,52 @@
package com.blackduck.integration.detectable.detectables.npm.lockfile;

import java.util.List;

import com.blackduck.integration.detectable.detectable.util.EnumListFilter;
import com.blackduck.integration.detectable.detectables.npm.NpmDependencyType;

// TODO: Identical to NpmPackageJsonParseDetectableOptions. Similar to NpmCliExtractorOptions. Common base Options class? JM-01/2022
public class NpmLockfileOptions {
private final EnumListFilter<NpmDependencyType> npmDependencyTypeFilter;
private final List<String> excludedWorkspaceNames;
private final List<String> includedWorkspaceNames;
private final boolean ignoreAllWorkspaces;

public NpmLockfileOptions(EnumListFilter<NpmDependencyType> npmDependencyTypeFilter) {
this(npmDependencyTypeFilter, java.util.Collections.emptyList(), java.util.Collections.emptyList(), false);
}

public NpmLockfileOptions(
EnumListFilter<NpmDependencyType> npmDependencyTypeFilter,
List<String> excludedWorkspaceNames,
List<String> includedWorkspaceNames) {
this(npmDependencyTypeFilter, excludedWorkspaceNames, includedWorkspaceNames, false);
}

public NpmLockfileOptions(
EnumListFilter<NpmDependencyType> npmDependencyTypeFilter,
List<String> excludedWorkspaceNames,
List<String> includedWorkspaceNames,
boolean ignoreAllWorkspaces) {
this.npmDependencyTypeFilter = npmDependencyTypeFilter;
this.excludedWorkspaceNames = excludedWorkspaceNames;
this.includedWorkspaceNames = includedWorkspaceNames;
this.ignoreAllWorkspaces = ignoreAllWorkspaces;
}

public EnumListFilter<NpmDependencyType> getNpmDependencyTypeFilter() {
return npmDependencyTypeFilter;
}

public List<String> getExcludedWorkspaceNames() {
return excludedWorkspaceNames;
}

public List<String> getIncludedWorkspaceNames() {
return includedWorkspaceNames;
}

public boolean isIgnoreAllWorkspaces() {
return ignoreAllWorkspaces;
}
}
Loading