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 413b6d9c..2f8832bf 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java @@ -202,13 +202,60 @@ private void handleVirtualWorkspace( + metadata.workspaceMembers()); } for (String memberId : metadata.workspaceMembers()) { + Set memberIgnoredDeps = getMemberIgnoredDeps(memberId, packageMap, ignoredDeps); processWorkspaceMember( - sbom, root, memberId, nodeMap, packageMap, ignoredDeps, analysisType); + sbom, root, memberId, nodeMap, packageMap, memberIgnoredDeps, analysisType); } } } } + /** + * Builds the full set of ignored dependencies for a workspace member by reading the member's own + * Cargo.toml for ignore patterns and merging them with the workspace-level ignored deps. + */ + private Set getMemberIgnoredDeps( + String memberId, Map packageMap, Set workspaceIgnoredDeps) { + CargoPackage memberPkg = packageMap.get(memberId); + if (memberPkg == null || memberPkg.manifestPath() == null) { + return workspaceIgnoredDeps; + } + Path memberManifest = Path.of(memberPkg.manifestPath()); + if (!Files.isRegularFile(memberManifest)) { + return workspaceIgnoredDeps; + } + try { + TomlParseResult memberToml = Toml.parse(memberManifest); + if (memberToml.hasErrors()) { + return workspaceIgnoredDeps; + } + String memberContent = Files.readString(memberManifest, StandardCharsets.UTF_8); + Set memberIgnored = getIgnoredDependencies(memberToml, memberContent); + if (memberIgnored.isEmpty()) { + return workspaceIgnoredDeps; + } + if (debugLoggingIsNeeded()) { + log.info( + "Found " + + memberIgnored.size() + + " ignored dependencies in member " + + memberPkg.name() + + ": " + + memberIgnored); + } + Set merged = new HashSet<>(workspaceIgnoredDeps); + merged.addAll(memberIgnored); + return merged; + } catch (IOException e) { + log.warning( + "Failed to read member Cargo.toml for ignore patterns: " + + memberManifest + + ": " + + e.getMessage()); + return workspaceIgnoredDeps; + } + } + void processWorkspaceDependencies( Sbom sbom, PackageURL root, @@ -714,7 +761,7 @@ private ProjectInfo parseCargoToml(TomlParseResult result) throws IOException { throw new IOException("Invalid Cargo.toml: no [package] or [workspace] section found"); } - private Set getIgnoredDependencies(TomlParseResult result, String content) { + Set getIgnoredDependencies(TomlParseResult result, String content) { Set normalDependencies = collectNormalDependencies(result); if (debugLoggingIsNeeded()) { log.info("Found " + normalDependencies.size() + " normal dependencies in Cargo.toml"); diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/rust/model/CargoPackage.java b/src/main/java/io/github/guacsec/trustifyda/providers/rust/model/CargoPackage.java index 0a734b2f..5fc8a438 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/rust/model/CargoPackage.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/rust/model/CargoPackage.java @@ -24,4 +24,5 @@ public record CargoPackage( @JsonProperty("name") String name, @JsonProperty("version") String version, @JsonProperty("id") String id, + @JsonProperty("manifest_path") String manifestPath, @JsonProperty("dependencies") List dependencies) {} diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java b/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java index 1d3c922c..caf3a18a 100644 --- a/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java +++ b/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java @@ -405,23 +405,11 @@ public void testComplexDependencySyntaxWithIgnorePatterns(@TempDir Path tempDir) """; Files.writeString(cargoToml, content); - // Create RustProvider and test ignore detection CargoProvider provider = new CargoProvider(cargoToml); - - // Read the file content for the updated method signature String cargoContent = Files.readString(cargoToml, StandardCharsets.UTF_8); + TomlParseResult tomlResult = Toml.parse(cargoToml); - // Parse TOML using TOMLJ (matching the optimized implementation) - org.tomlj.TomlParseResult tomlResult = org.tomlj.Toml.parse(cargoToml); - - // Use reflection to test the private getIgnoredDependencies method with new signature - java.lang.reflect.Method method = - CargoProvider.class.getDeclaredMethod( - "getIgnoredDependencies", org.tomlj.TomlParseResult.class, String.class); - method.setAccessible(true); - - @SuppressWarnings("unchecked") - Set ignoredDeps = (Set) method.invoke(provider, tomlResult, cargoContent); + Set ignoredDeps = provider.getIgnoredDependencies(tomlResult, cargoContent); System.out.println("Complex syntax test - Ignored dependencies found:"); for (String dep : ignoredDeps) { @@ -627,20 +615,10 @@ public void testEdgeCaseCargoTomlFormats(@TempDir Path tempDir) throws Exception assertTrue(sbomContent.contains("edge-case-project"), "Should contain project name"); // Test ignore detection with edge case formatting - // Read the file content for the updated method signature String edgeCargoContent = Files.readString(edgeCaseCargoToml, StandardCharsets.UTF_8); + TomlParseResult edgeTomlResult = Toml.parse(edgeCaseCargoToml); - // Parse TOML using TOMLJ (matching the optimized implementation) - org.tomlj.TomlParseResult edgeTomlResult = org.tomlj.Toml.parse(edgeCaseCargoToml); - - java.lang.reflect.Method method = - CargoProvider.class.getDeclaredMethod( - "getIgnoredDependencies", org.tomlj.TomlParseResult.class, String.class); - method.setAccessible(true); - - @SuppressWarnings("unchecked") - Set ignoredDeps = - (Set) method.invoke(provider, edgeTomlResult, edgeCargoContent); + Set ignoredDeps = provider.getIgnoredDependencies(edgeTomlResult, edgeCargoContent); // Should detect ignore patterns despite varying spacing and formatting assertTrue(ignoredDeps.contains("dep1"), "Should ignore dep1 (extra spaces)"); @@ -766,4 +744,69 @@ public void testVirtualWorkspaceWithoutWorkspaceDepsDoesNotThrowNPE(@TempDir Pat sbom, root, new HashMap<>(), new HashSet<>(), tomlResult), "processWorkspaceDependencies should handle missing [workspace.dependencies] gracefully"); } + + @Test + public void testMemberCargoTomlIgnorePatternsDetected(@TempDir Path tempDir) throws Exception { + // Simulate a member's Cargo.toml with exhortignore on a dependency + Path memberDir = tempDir.resolve("crate-a"); + Files.createDirectories(memberDir); + Path memberCargoToml = memberDir.resolve("Cargo.toml"); + String memberContent = + """ + [package] + name = "crate-a" + version = "0.1.0" + edition = "2021" + + [dependencies] + serde = "1.0" # exhortignore + tokio = "1.0" + reqwest = "0.11" # trustify-da-ignore + """; + Files.writeString(memberCargoToml, memberContent); + + CargoProvider provider = new CargoProvider(memberCargoToml); + TomlParseResult tomlResult = Toml.parse(memberCargoToml); + String content = Files.readString(memberCargoToml, StandardCharsets.UTF_8); + + Set ignoredDeps = provider.getIgnoredDependencies(tomlResult, content); + + assertTrue(ignoredDeps.contains("serde"), "serde should be ignored (exhortignore)"); + assertFalse(ignoredDeps.contains("tokio"), "tokio should NOT be ignored"); + assertTrue(ignoredDeps.contains("reqwest"), "reqwest should be ignored (trustify-da-ignore)"); + assertEquals(2, ignoredDeps.size(), "Should find exactly 2 ignored dependencies in member"); + } + + @Test + public void testMemberIgnorePatternsWithTableFormat(@TempDir Path tempDir) throws Exception { + Path memberDir = tempDir.resolve("crate-b"); + Files.createDirectories(memberDir); + Path memberCargoToml = memberDir.resolve("Cargo.toml"); + String memberContent = + """ + [package] + name = "crate-b" + version = "0.1.0" + edition = "2021" + + [dependencies] + serde-json-wasm = "1.0" + + [dependencies.aho-corasick] # trustify-da-ignore + version = "1.0.0" + """; + Files.writeString(memberCargoToml, memberContent); + + CargoProvider provider = new CargoProvider(memberCargoToml); + TomlParseResult tomlResult = Toml.parse(memberCargoToml); + String content = Files.readString(memberCargoToml, StandardCharsets.UTF_8); + + Set ignoredDeps = provider.getIgnoredDependencies(tomlResult, content); + + assertFalse(ignoredDeps.contains("serde-json-wasm"), "serde-json-wasm should NOT be ignored"); + assertTrue( + ignoredDeps.contains("aho-corasick"), + "aho-corasick should be ignored (table format with trustify-da-ignore)"); + assertEquals(1, ignoredDeps.size(), "Should find exactly 1 ignored dependency in member"); + } }