diff --git a/src/main/java/io/github/guacsec/trustifyda/Provider.java b/src/main/java/io/github/guacsec/trustifyda/Provider.java index 03f6a17c..95a3d686 100644 --- a/src/main/java/io/github/guacsec/trustifyda/Provider.java +++ b/src/main/java/io/github/guacsec/trustifyda/Provider.java @@ -47,13 +47,13 @@ public Content(byte[] buffer, String type) { /** The ecosystem of this provider, i.e. maven. */ public final Ecosystem.Type ecosystem; - public final Path manifest; + public final Path manifestPath; protected final ObjectMapper objectMapper = new ObjectMapper(); - protected Provider(Ecosystem.Type ecosystem, Path manifest) { + protected Provider(Ecosystem.Type ecosystem, Path manifestPath) { this.ecosystem = ecosystem; - this.manifest = manifest; + this.manifestPath = manifestPath; } /** diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java index e82b1247..d8862be6 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java @@ -146,7 +146,7 @@ private void addDependencies( handleSingleCrate(sbom, root, metadata, nodeMap, packageMap, ignoredDeps, analysisType); } - } catch (Exception e) { + } catch (IOException | InterruptedException e) { log.severe("Unexpected error during " + analysisType + " analysis: " + e.getMessage()); } } @@ -385,7 +385,7 @@ private Path findOutermostCargoTomlDirectory(Path startDir) { } private CargoMetadata executeCargoMetadata() throws IOException, InterruptedException { - Path workingDir = manifest.getParent(); + Path workingDir = manifestPath.getParent(); if (debugLoggingIsNeeded()) { log.info("Executing cargo metadata for full dependency resolution with resolved versions"); @@ -657,20 +657,21 @@ public CargoProvider(Path manifest) { @Override public String readLicenseFromManifest() { String manifestLicense = readLicenseFromToml(null); - return LicenseUtils.getLicense(manifestLicense, manifest); + return LicenseUtils.getLicense(manifestLicense, manifestPath); } private String readLicenseFromToml(TomlParseResult existingResult) { try { - TomlParseResult tomlResult = existingResult != null ? existingResult : Toml.parse(manifest); + TomlParseResult tomlResult = + existingResult != null ? existingResult : Toml.parse(manifestPath); if (tomlResult.hasErrors()) { return null; } String license = tomlResult.getString("package.license"); - return LicenseUtils.getLicense(license, manifest); + return LicenseUtils.getLicense(license, manifestPath); } catch (IOException e) { log.warning("Failed to read license from Cargo.toml: " + e.getMessage()); - return LicenseUtils.getLicense(null, manifest); + return LicenseUtils.getLicense(null, manifestPath); } } @@ -687,11 +688,11 @@ public Content provideStack() throws IOException { } private Sbom createSbom(AnalysisType analysisType) throws IOException { - if (!Files.exists(manifest) || !Files.isRegularFile(manifest)) { - throw new IOException("Cargo.toml not found: " + manifest); + if (!Files.exists(manifestPath) || !Files.isRegularFile(manifestPath)) { + throw new IOException("Cargo.toml not found: " + manifestPath); } - TomlParseResult tomlResult = Toml.parse(manifest); + TomlParseResult tomlResult = Toml.parse(manifestPath); if (tomlResult.hasErrors()) { throw new IOException( "Invalid Cargo.toml format: " + tomlResult.errors().get(0).getMessage()); @@ -706,7 +707,7 @@ private Sbom createSbom(AnalysisType analysisType) throws IOException { Type.CARGO.getType(), null, projectInfo.name(), projectInfo.version(), null, null); sbom.addRoot(root, readLicenseFromToml(tomlResult)); - String cargoContent = Files.readString(manifest, StandardCharsets.UTF_8); + String cargoContent = Files.readString(manifestPath, StandardCharsets.UTF_8); Set ignoredDeps = getIgnoredDependencies(tomlResult, cargoContent); addDependencies(sbom, root, ignoredDeps, tomlResult, analysisType); return sbom; @@ -744,7 +745,7 @@ private ProjectInfo parseCargoToml(TomlParseResult result) throws IOException { boolean hasWorkspace = result.contains("workspace"); if (hasWorkspace) { String workspaceVersion = result.getString(WORKSPACE_PACKAGE_VERSION); - String dirName = manifest.toAbsolutePath().getParent().getFileName().toString(); + String dirName = manifestPath.toAbsolutePath().getParent().getFileName().toString(); if (debugLoggingIsNeeded()) { log.info( "Using workspace fallback: name=" diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java index 7ff822e9..7203c112 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java @@ -73,27 +73,27 @@ public GoModulesProvider(Path manifest) { @Override public String readLicenseFromManifest() { - return LicenseUtils.readLicenseFile(manifest); + return LicenseUtils.readLicenseFile(manifestPath); } @Override public Content provideStack() throws IOException { // check for custom executable - Sbom sbom = getDependenciesSbom(manifest, true); + Sbom sbom = getDependenciesSbom(manifestPath, true); return new Content( sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); } @Override public Content provideComponent() throws IOException { - if (!Files.exists(manifest)) { - throw new IllegalArgumentException("Missing required go.mod file: " + manifest); + if (!Files.exists(manifestPath)) { + throw new IllegalArgumentException("Missing required go.mod file: " + manifestPath); } - if (!Files.isRegularFile(manifest)) { + if (!Files.isRegularFile(manifestPath)) { throw new IllegalArgumentException( - "The provided manifest is not a regular file: " + manifest); + "The provided manifest is not a regular file: " + manifestPath); } - var sbom = getDependenciesSbom(manifest, false); + var sbom = getDependenciesSbom(manifestPath, false); return new Content( sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); } diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java index e990059f..79b1d258 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java @@ -70,12 +70,12 @@ public GradleProvider(Path manifest) { @Override public String readLicenseFromManifest() { - return LicenseUtils.readLicenseFile(manifest); + return LicenseUtils.readLicenseFile(manifestPath); } @Override public Content provideStack() throws IOException { - Path tempFile = getDependencies(manifest); + Path tempFile = getDependencies(manifestPath); try { if (debugLoggingIsNeeded()) { String stackAnalysisDependencyTree = Files.readString(tempFile); @@ -84,10 +84,10 @@ public Content provideStack() throws IOException { "Package Manager Gradle Stack Analysis Dependency Tree Output: %s %s", System.lineSeparator(), stackAnalysisDependencyTree)); } - Map propertiesMap = extractProperties(manifest); + Map propertiesMap = extractProperties(manifestPath); var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, AnalysisType.STACK); - var ignored = getIgnoredDeps(manifest); + var ignored = getIgnoredDeps(manifestPath); return new Content( sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); @@ -605,12 +605,12 @@ private List extractLines(Path inputFilePath, String startMarker) throws @Override public Content provideComponent() throws IOException { - Path tempFile = getDependencies(manifest); + Path tempFile = getDependencies(manifestPath); try { - Map propertiesMap = extractProperties(manifest); + Map propertiesMap = extractProperties(manifestPath); Sbom sbom = buildSbomFromTextFormat(tempFile, propertiesMap, AnalysisType.COMPONENT); - var ignored = getIgnoredDeps(manifest); + var ignored = getIgnoredDeps(manifestPath); return new Content( sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java index fd944a20..c21acae3 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java @@ -68,8 +68,8 @@ public JavaMavenProvider(Path manifest) { @Override public String readLicenseFromManifest() { - String manifestLicense = readLicenseFromPom(manifest); - return LicenseUtils.getLicense(manifestLicense, manifest); + String manifestLicense = readLicenseFromPom(manifestPath); + return LicenseUtils.getLicense(manifestLicense, manifestPath); } /** @@ -124,10 +124,11 @@ private String readLicenseFromPom(Path pomPath) { @Override public Content provideStack() throws IOException { - var mvnCleanCmd = buildMvnCommandArgs("clean", "-f", manifest.toString(), "--batch-mode", "-q"); + var mvnCleanCmd = + buildMvnCommandArgs("clean", "-f", manifestPath.toString(), "--batch-mode", "-q"); var mvnEnvs = getMvnExecEnvs(); // execute the clean command - Operations.runProcess(manifest.getParent(), mvnCleanCmd.toArray(String[]::new), mvnEnvs); + Operations.runProcess(manifestPath.getParent(), mvnCleanCmd.toArray(String[]::new), mvnEnvs); // create a temp file for storing the dependency tree in var tmpFile = Files.createTempFile("TRUSTIFY_DA_dot_graph_", null); // the tree command will build the project and create the dependency tree in the temp file @@ -140,12 +141,12 @@ public Content provideStack() throws IOException { "-DoutputType=text", String.format("-DoutputFile=%s", tmpFile.toString()), "-f", - manifest.toString(), + manifestPath.toString(), "--batch-mode", "-q")); // if we have dependencies marked as ignored, exclude them from the tree command var ignored = - getDependencies(manifest).stream() + getDependencies(manifestPath).stream() .filter(d -> d.ignored) .map(DependencyAggregator::toPurlWithoutVersion) .map(PackageURL::getCoordinates) @@ -157,7 +158,7 @@ public Content provideStack() throws IOException { // execute the tree command var mvnTreeCmd = buildMvnCommandArgs(mvnTreeCmdArgs.toArray(String[]::new)); - Operations.runProcess(manifest.getParent(), mvnTreeCmd.toArray(String[]::new), mvnEnvs); + Operations.runProcess(manifestPath.getParent(), mvnTreeCmd.toArray(String[]::new), mvnEnvs); if (debugLoggingIsNeeded()) { String stackAnalysisDependencyTree = Files.readString(tmpFile); log.info( @@ -201,12 +202,12 @@ private Content generateSbomFromEffectivePom() throws IOException { "help:effective-pom", String.format("-Doutput=%s", tmpEffPom.toString()), "-f", - manifest.toString(), + manifestPath.toString(), "--batch-mode", "-q"); // execute the effective pom command Operations.runProcess( - manifest.getParent(), mvnEffPomCmd.toArray(String[]::new), getMvnExecEnvs()); + manifestPath.getParent(), mvnEffPomCmd.toArray(String[]::new), getMvnExecEnvs()); if (debugLoggingIsNeeded()) { String CaEffectivePoM = Files.readString(tmpEffPom); log.info( @@ -216,7 +217,7 @@ private Content generateSbomFromEffectivePom() throws IOException { } // if we have dependencies marked as ignored grab ignored dependencies from the original pom // the effective-pom goal doesn't carry comments - List dependencies = getDependencies(manifest); + List dependencies = getDependencies(manifestPath); var ignored = dependencies.stream() .filter(d -> d.ignored) @@ -437,7 +438,7 @@ private String selectMvnRuntime(final Path manifestPath) { if (mvnw != null) { try { // verify maven wrapper is accessible - Operations.runProcess(manifest.getParent(), mvnw, ARG_VERSION); + Operations.runProcess(manifestPath.getParent(), mvnw, ARG_VERSION); if (debugLoggingIsNeeded()) { log.info(String.format("using maven wrapper from : %s", mvnw)); } diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java index acb29a0c..bedf36c0 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java @@ -38,7 +38,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; -import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; @@ -128,9 +127,7 @@ private void addDependenciesOf(Sbom sbom, PackageURL from, JsonNode node) { if (dependencies == null) { return; } - Iterator> fields = dependencies.fields(); - while (fields.hasNext()) { - Entry e = fields.next(); + for (var e : dependencies.properties()) { String name = e.getKey(); JsonNode versionNode = e.getValue().get("version"); if (versionNode == null) { @@ -174,14 +171,13 @@ private void addDependenciesFromKey(Sbom sbom, JsonNode depTree, String key) { if (deps == null) { return; } - deps.fields() - .forEachRemaining( + deps.properties() + .forEach( e -> { JsonNode versionNode = e.getValue().get("version"); - if (versionNode == null || versionNode.isNull()) { - return; // skip entries without a resolved version - } - var target = toPurl(e.getKey(), versionNode.asText()); + String version = + (versionNode != null && !versionNode.isNull()) ? versionNode.asText() : null; + var target = toPurl(e.getKey(), version); sbom.addDependency(manifest.root, target, null); addDependenciesOf(sbom, target, e.getValue()); }); @@ -216,16 +212,15 @@ private void addRootDependenciesFromKey( if (node == null) { return; } - node.fields() - .forEachRemaining( + node.properties() + .forEach( e -> { String name = e.getKey(); JsonNode versionNode = e.getValue().get("version"); - if (versionNode != null) { - String version = versionNode.asText(); - PackageURL purl = toPurl(name, version); - direct.put(name, purl); - } + String version = + (versionNode != null && !versionNode.isNull()) ? versionNode.asText() : null; + PackageURL purl = toPurl(name, version); + direct.put(name, purl); }); } diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java index 7d12c540..72bd2b0d 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java @@ -57,7 +57,8 @@ public void setPythonController(PythonControllerBase pythonController) { @Override public Content provideStack() throws IOException { PythonControllerBase controller = getPythonController(); - List> dependencies = controller.getDependencies(manifest.toString(), true); + List> dependencies = + controller.getDependencies(manifestPath.toString(), true); printDependenciesTree(dependencies); Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); sbom.addRoot( @@ -65,7 +66,7 @@ public Content provideStack() throws IOException { for (Map component : dependencies) { addAllDependencies(sbom.getRoot(), component, sbom); } - String manifestContent = Files.readString(manifest); + String manifestContent = Files.readString(manifestPath); handleIgnoredDependencies(manifestContent, sbom); return new Content( sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); @@ -74,7 +75,8 @@ public Content provideStack() throws IOException { @Override public Content provideComponent() throws IOException { PythonControllerBase controller = getPythonController(); - List> dependencies = controller.getDependencies(manifest.toString(), false); + List> dependencies = + controller.getDependencies(manifestPath.toString(), false); printDependenciesTree(dependencies); Sbom sbom = SbomFactory.newInstance(); sbom.addRoot( @@ -85,7 +87,7 @@ public Content provideComponent() throws IOException { sbom.getRoot(), toPurl((String) component.get("name"), (String) component.get("version")), null)); - String manifestContent = Files.readString(manifest); + String manifestContent = Files.readString(manifestPath); handleIgnoredDependencies(manifestContent, sbom); return new Content( sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/PythonProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/PythonProvider.java index aaa69924..12b7efd0 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/PythonProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/PythonProvider.java @@ -43,7 +43,7 @@ protected PythonProvider(Path manifest) { @Override public String readLicenseFromManifest() { - return LicenseUtils.readLicenseFile(manifest); + return LicenseUtils.readLicenseFile(manifestPath); } protected String getRootComponentName() { diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/PythonPyprojectProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/PythonPyprojectProvider.java index fd8d2cba..37c242a5 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/PythonPyprojectProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/PythonPyprojectProvider.java @@ -80,7 +80,7 @@ public PythonPyprojectProvider(Path manifest) { public Content provideStack() throws IOException { rejectPoetryDependencies(); collectIgnoredDeps(); - String reportJson = getPipReportOutput(manifest.toAbsolutePath().getParent()); + String reportJson = getPipReportOutput(manifestPath.toAbsolutePath().getParent()); PipReportData data = parsePipReport(reportJson); Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); @@ -94,7 +94,7 @@ public Content provideStack() throws IOException { } } - String manifestContent = Files.readString(manifest); + String manifestContent = Files.readString(manifestPath); handleIgnoredDependencies(manifestContent, sbom); return new Content( sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); @@ -104,7 +104,7 @@ public Content provideStack() throws IOException { public Content provideComponent() throws IOException { rejectPoetryDependencies(); collectIgnoredDeps(); - String reportJson = getPipReportOutput(manifest.toAbsolutePath().getParent()); + String reportJson = getPipReportOutput(manifestPath.toAbsolutePath().getParent()); PipReportData data = parsePipReport(reportJson); Sbom sbom = SbomFactory.newInstance(); @@ -118,7 +118,7 @@ public Content provideComponent() throws IOException { } } - String manifestContent = Files.readString(manifest); + String manifestContent = Files.readString(manifestPath); handleIgnoredDependencies(manifestContent, sbom); return new Content( sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); @@ -342,7 +342,7 @@ static String canonicalize(String name) { private TomlParseResult getToml() throws IOException { if (cachedToml == null) { - TomlParseResult parsed = Toml.parse(manifest); + TomlParseResult parsed = Toml.parse(manifestPath); if (parsed.hasErrors()) { throw new IOException( "Invalid pyproject.toml format: " + parsed.errors().get(0).getMessage()); @@ -396,7 +396,7 @@ public String readLicenseFromManifest() { } catch (IOException e) { log.fine("Failed to parse pyproject.toml for license: " + e.getMessage()); } - return LicenseUtils.readLicenseFile(manifest); + return LicenseUtils.readLicenseFile(manifestPath); } @Override @@ -425,7 +425,7 @@ private void rejectPoetryDependencies() throws IOException { private void collectIgnoredDeps() throws IOException { TomlParseResult toml = getToml(); - List rawLines = Files.readAllLines(manifest); + List rawLines = Files.readAllLines(manifestPath); collectedIgnoredDeps = new HashSet<>(); // [project.dependencies] - PEP 621 diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/Javascript_Provider_Test.java b/src/test/java/io/github/guacsec/trustifyda/providers/Javascript_Provider_Test.java index 8aac71c8..74303b45 100644 --- a/src/test/java/io/github/guacsec/trustifyda/providers/Javascript_Provider_Test.java +++ b/src/test/java/io/github/guacsec/trustifyda/providers/Javascript_Provider_Test.java @@ -24,6 +24,8 @@ import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mockStatic; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import io.github.guacsec.trustifyda.Api; import io.github.guacsec.trustifyda.ExhortTest; import io.github.guacsec.trustifyda.tools.Ecosystem; @@ -32,6 +34,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -194,6 +197,74 @@ void test_the_provideComponent_with_Path(String pkgManager, String testFolder) t } } + /** + * TC-3818 / TC-4128: Verifies that root-level dependencies without a version field (e.g., file: + * deps, workspace packages, linked packages) are processed correctly, including their transitive + * dependency subtrees. + * + *

Reproducer for a bug in {@code JavaScriptProvider.addDependenciesFromKey()} where a + * root-level dependency with {@code versionNode == null} caused an early return, skipping both + * the entry itself and its entire transitive dependency subtree. Fixed by treating null version + * as a valid case (versionless PURL) and always recursing into children. + * + *

This test uses a synthetic npm-ls output where a root-level dependency ("my-local-lib") has + * no version field but contains 3 transitive dependencies (lodash, debug, ms). All 7 components + * must be present in the SBOM. + */ + @Test + void test_provideStack_includes_deps_of_root_entry_without_version() throws IOException { + // Given a package.json with a file: dependency that has no version in npm ls output + var testFolder = "deps_with_no_version_root_dep"; + var pkgManager = Ecosystem.Type.NPM.getType(); + + var tmpFolder = Files.createTempDirectory("TRUSTIFY_DA_test_"); + var tmpFile = Files.createFile(tmpFolder.resolve("package.json")); + var tmpLockFile = Files.createFile(tmpFolder.resolve(JavaScriptNpmProvider.LOCK_FILE)); + try (var is = + getResourceAsStreamDecision( + this.getClass(), String.format("tst_manifests/npm/%s/package.json", testFolder))) { + Files.write(tmpFile, is.readAllBytes()); + } + try (var is = + getResourceAsStreamDecision( + this.getClass(), String.format("tst_manifests/npm/%s/package-lock.json", testFolder))) { + Files.write(tmpLockFile, is.readAllBytes()); + } + + String listingStack; + try (var is = + getResourceAsStreamDecision( + this.getClass(), String.format("tst_manifests/npm/%s/npm-ls-stack.json", testFolder))) { + listingStack = new String(is.readAllBytes()); + } + + // When providing stack analysis SBOM + try (MockedStatic mockedOperations = + mockOperations(pkgManager, listingStack, false)) { + var content = JavaScriptProviderFactory.create(tmpFile).provideStack(); + + // Then parse the SBOM and count components + var mapper = new ObjectMapper(); + JsonNode sbom = mapper.readTree(new String(content.buffer)); + JsonNode components = sbom.get("components"); + int componentCount = (components != null) ? components.size() : 0; + + // The npm-ls fixture has 7 unique dependencies: + // express@4.18.2, accepts@1.3.8, content-type@1.0.5 (express subtree) + // my-local-lib (no version, file: dep), lodash@4.17.21, debug@4.3.4, ms@2.1.2 (subtree) + assertThat(componentCount) + .as( + "Root-level deps without version (e.g., file: deps) and their transitive " + + "deps must be included in the SBOM.") + .isEqualTo(7); + } finally { + // Cleanup + Files.deleteIfExists(tmpFile); + Files.deleteIfExists(tmpLockFile); + Files.deleteIfExists(tmpFolder); + } + } + private String dropIgnored(String s) { return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); } diff --git a/src/test/resources/tst_manifests/npm/deps_with_no_version_root_dep/npm-ls-stack.json b/src/test/resources/tst_manifests/npm/deps_with_no_version_root_dep/npm-ls-stack.json new file mode 100644 index 00000000..5a353de2 --- /dev/null +++ b/src/test/resources/tst_manifests/npm/deps_with_no_version_root_dep/npm-ls-stack.json @@ -0,0 +1,46 @@ +{ + "version": "1.0.0", + "name": "test-app", + "dependencies": { + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "overridden": false, + "dependencies": { + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "overridden": false + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "overridden": false + } + } + }, + "my-local-lib": { + "resolved": "file:../my-local-lib", + "overridden": false, + "dependencies": { + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "overridden": false + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "overridden": false, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "overridden": false + } + } + } + } + } + } +} diff --git a/src/test/resources/tst_manifests/npm/deps_with_no_version_root_dep/package-lock.json b/src/test/resources/tst_manifests/npm/deps_with_no_version_root_dep/package-lock.json new file mode 100644 index 00000000..de610599 --- /dev/null +++ b/src/test/resources/tst_manifests/npm/deps_with_no_version_root_dep/package-lock.json @@ -0,0 +1,16 @@ +{ + "name": "test-app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test-app", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0", + "my-local-lib": "file:../my-local-lib" + } + } + } +} diff --git a/src/test/resources/tst_manifests/npm/deps_with_no_version_root_dep/package.json b/src/test/resources/tst_manifests/npm/deps_with_no_version_root_dep/package.json new file mode 100644 index 00000000..2984124e --- /dev/null +++ b/src/test/resources/tst_manifests/npm/deps_with_no_version_root_dep/package.json @@ -0,0 +1,9 @@ +{ + "name": "test-app", + "version": "1.0.0", + "description": "Test app with a file: dependency that has no version in npm ls output", + "dependencies": { + "express": "^4.18.0", + "my-local-lib": "file:../my-local-lib" + } +}