diff --git a/README.md b/README.md index 4ad526dc..546c72b0 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,7 @@ public class TrustifyExample {
  • Golang - Go Modules
  • Python - pip Installer
  • Gradle - Gradle Installation
  • +
  • Rust - Cargo
  • @@ -315,6 +316,19 @@ test { ``` +
  • +Rust Cargo users can add a comment with #trustify-da-ignore next to the package to be ignored in Cargo.toml: + +```toml +[dependencies] +serde = "1.0.136" # trustify-da-ignore +tokio = { version = "1.0", features = ["full"] } + +[workspace.dependencies] +regex = "1.5.4" # trustify-da-ignore +``` +
  • + #### Ignore Strategies - experimental @@ -337,6 +351,7 @@ System.setProperty("TRUSTIFY_DA_PNPM_PATH", "/path/to/custom/pnpm"); System.setProperty("TRUSTIFY_DA_YARN_PATH", "/path/to/custom/yarn"); System.setProperty("TRUSTIFY_DA_GO_PATH", "/path/to/custom/go"); System.setProperty("TRUSTIFY_DA_GRADLE_PATH", "/path/to/custom/gradle"); +System.setProperty("TRUSTIFY_DA_CARGO_PATH", "/path/to/custom/cargo"); //python - python3, pip3 take precedence if python version > 3 installed System.setProperty("TRUSTIFY_DA_PYTHON3_PATH", "/path/to/python3"); System.setProperty("TRUSTIFY_DA_PIP3_PATH", "/path/to/pip3"); @@ -452,6 +467,11 @@ following keys for setting custom paths for the said executables. pip TRUSTIFY_DA_PIP_PATH + +Cargo Package Manager +cargo +TRUSTIFY_DA_CARGO_PATH + @@ -653,6 +673,9 @@ java -jar trustify-da-java-client-cli.jar component /path/to/requirements.txt # Component analysis with summary java -jar trustify-da-java-client-cli.jar component /path/to/go.mod --summary +# Rust Cargo analysis +java -jar trustify-da-java-client-cli.jar stack /path/to/Cargo.toml --summary + # Container image analysis with JSON output (default) java -jar trustify-da-java-client-cli.jar image nginx:latest 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 e15269ae..52ca76d4 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java @@ -45,7 +45,12 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.logging.Logger; import org.tomlj.Toml; import org.tomlj.TomlParseResult; @@ -341,23 +346,64 @@ private CargoMetadata executeCargoMetadata() throws IOException, InterruptedExce .directory(workingDir.toFile()) .start(); + // Use bounded executor to read streams concurrently to avoid two potential deadlocks: + // 1. buffer deadlock (process blocks on write when output buffers fill up while Java waits for + // process completion) + // 2. stalled process deadlock (readAllBytes() hangs forever when cargo process stalls + // completely with no timeout protection) + // Bounded executor allows proper cancellation and cleanup vs CompletableFuture common pool + ExecutorService streamExecutor = Executors.newFixedThreadPool(2); String output; - try (InputStream is = process.getInputStream()) { - output = new String(is.readAllBytes(), StandardCharsets.UTF_8); - } + String errorOutput; + + try { + Future outputFuture = + streamExecutor.submit( + () -> { + try (InputStream is = process.getInputStream()) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + log.warning("Failed to read stdout from cargo metadata: " + e.getMessage()); + return ""; + } + }); + + Future errorFuture = + streamExecutor.submit( + () -> { + try (InputStream is = process.getErrorStream()) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + log.warning("Failed to read stderr from cargo metadata: " + e.getMessage()); + return ""; + } + }); + + boolean finished = process.waitFor(TIMEOUT, TimeUnit.SECONDS); + + if (!finished) { + process.destroyForcibly(); + outputFuture.cancel(true); + errorFuture.cancel(true); + throw new InterruptedException("cargo metadata timed out after " + TIMEOUT + " seconds"); + } - boolean finished = process.waitFor(TIMEOUT, TimeUnit.SECONDS); + try { + // Short timeout since process already finished + output = outputFuture.get(1, TimeUnit.SECONDS); + errorOutput = errorFuture.get(1, TimeUnit.SECONDS); + } catch (ExecutionException | TimeoutException e) { + log.warning("Failed to read process output: " + e.getMessage()); + return null; + } + } finally { + streamExecutor.shutdownNow(); + } + // Safe to call exitValue() - we confirmed the process finished int exitCode = process.exitValue(); if (exitCode != 0) { - String errorOutput = ""; - try (InputStream errorStream = process.getErrorStream()) { - errorOutput = new String(errorStream.readAllBytes(), StandardCharsets.UTF_8); - } catch (IOException e) { - log.warning("Failed to read error stream: " + e.getMessage()); - } - String errorMessage = "cargo metadata failed with exit code: " + exitCode; if (!errorOutput.isEmpty()) { errorMessage += ". Error: " + errorOutput.trim(); @@ -373,11 +419,6 @@ private CargoMetadata executeCargoMetadata() throws IOException, InterruptedExce return null; } - if (!finished) { - process.destroyForcibly(); - throw new InterruptedException("cargo metadata timed out after " + TIMEOUT + " seconds"); - } - try { CargoMetadata metadata = MAPPER.readValue(output, CargoMetadata.class); if (debugLoggingIsNeeded()) { @@ -552,12 +593,14 @@ private DependencyInfo getPackageInfo(String packageId, Map