Skip to content

Commit 1b2b88a

Browse files
authored
fix: throw error on gradle command failure instead of returning empty… (#362)
… sbom ## Description GradleProvider used Operations.runProcessGetOutput() which never checked the process exit code. When Gradle evaluation failed (e.g., unresolvable version catalogs), the error was silently ignored and an empty SBOM was returned to the user. Switched getDependencies() and getProperties() to use runProcessGetFullOutput() which properly captures stdout, stderr, and exit code. Non-zero exit codes now throw a RuntimeException with the Gradle error message. Also added null-safety defaults in getRoot() for edge cases with partial properties output. **Related issue (if any):** fixes #361 ## Checklist - [x] I have followed this repository's contributing guidelines. - [x] I will adhere to the project's code of conduct. ## Additional information
1 parent d6c4b54 commit 1b2b88a

3 files changed

Lines changed: 267 additions & 88 deletions

File tree

src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java

Lines changed: 81 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ public final class GradleProvider extends BaseJavaProvider {
5959

6060
private static final Logger log = LoggersFactory.getLogger(GradleProvider.class.getName());
6161

62+
private static final long TIMEOUT =
63+
Long.parseLong(System.getProperty("trustify.gradle.timeout.seconds", "120"));
64+
6265
private final String gradleExecutable = Operations.getExecutable("gradle", "--version");
6366

6467
public GradleProvider(Path manifest) {
@@ -73,20 +76,24 @@ public String readLicenseFromManifest() {
7376
@Override
7477
public Content provideStack() throws IOException {
7578
Path tempFile = getDependencies(manifest);
76-
if (debugLoggingIsNeeded()) {
77-
String stackAnalysisDependencyTree = Files.readString(tempFile);
78-
log.info(
79-
String.format(
80-
"Package Manager Gradle Stack Analysis Dependency Tree Output: %s %s",
81-
System.lineSeparator(), stackAnalysisDependencyTree));
82-
}
83-
Map<String, String> propertiesMap = extractProperties(manifest);
79+
try {
80+
if (debugLoggingIsNeeded()) {
81+
String stackAnalysisDependencyTree = Files.readString(tempFile);
82+
log.info(
83+
String.format(
84+
"Package Manager Gradle Stack Analysis Dependency Tree Output: %s %s",
85+
System.lineSeparator(), stackAnalysisDependencyTree));
86+
}
87+
Map<String, String> propertiesMap = extractProperties(manifest);
8488

85-
var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, AnalysisType.STACK);
86-
var ignored = getIgnoredDeps(manifest);
89+
var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, AnalysisType.STACK);
90+
var ignored = getIgnoredDeps(manifest);
8791

88-
return new Content(
89-
sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE);
92+
return new Content(
93+
sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE);
94+
} finally {
95+
Files.deleteIfExists(tempFile);
96+
}
9097
}
9198

9299
private List<String> getIgnoredDeps(Path manifestPath) throws IOException {
@@ -228,28 +235,42 @@ private String extractPackageName(String line) {
228235
}
229236

230237
private Path getDependencies(Path manifestPath) throws IOException {
231-
// create a temp file for storing the dependency tree in
232-
var tempFile = Files.createTempFile("TRUSTIFY_DA_graph_", null);
233-
// the command will create the dependency tree in the temp file
234238
String gradleCommand = gradleExecutable + " dependencies";
235-
236239
String[] cmdList = gradleCommand.split("\\s+");
237-
String gradleOutput =
238-
Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), cmdList);
239-
Files.writeString(tempFile, gradleOutput);
240240

241+
var result =
242+
Operations.runProcessGetFullOutput(
243+
Path.of(manifestPath.getParent().toString()), cmdList, null, TIMEOUT);
244+
245+
if (result.getExitCode() != 0) {
246+
throw new RuntimeException(
247+
String.format(
248+
"gradle dependencies command failed with exit code %d for manifest '%s': %s",
249+
result.getExitCode(), manifestPath, result.getError()));
250+
}
251+
252+
var tempFile = Files.createTempFile("TRUSTIFY_DA_graph_", null);
253+
Files.writeString(tempFile, result.getOutput());
241254
return tempFile;
242255
}
243256

244257
private Path getProperties(Path manifestPath) throws IOException {
245-
Path propsTempFile = Files.createTempFile("propsfile", ".txt");
246258
String propCmd = gradleExecutable + " properties";
247259
String[] propCmdList = propCmd.split("\\s+");
248-
String properties =
249-
Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), propCmdList);
250-
// Create a temporary file
251-
Files.writeString(propsTempFile, properties);
252260

261+
var result =
262+
Operations.runProcessGetFullOutput(
263+
Path.of(manifestPath.getParent().toString()), propCmdList, null, TIMEOUT);
264+
265+
if (result.getExitCode() != 0) {
266+
throw new RuntimeException(
267+
String.format(
268+
"gradle properties command failed with exit code %d for manifest '%s': %s",
269+
result.getExitCode(), manifestPath, result.getError()));
270+
}
271+
272+
Path propsTempFile = Files.createTempFile("propsfile", ".txt");
273+
Files.writeString(propsTempFile, result.getOutput());
253274
return propsTempFile;
254275
}
255276

@@ -509,9 +530,12 @@ private boolean containsVersion(String line) {
509530

510531
private String getRoot(Path textFormatFile, Map<String, String> propertiesMap)
511532
throws IOException {
512-
String group = propertiesMap.get("group");
513-
String version = propertiesMap.get("version");
533+
String group = propertiesMap.getOrDefault("group", "unknown");
534+
String version = propertiesMap.getOrDefault("version", "0.0.0");
514535
String rootName = extractRootProjectValue(textFormatFile);
536+
if (rootName == null || rootName.isEmpty()) {
537+
rootName = "unknown";
538+
}
515539
return group + ':' + rootName + ':' + "jar" + ':' + version;
516540
}
517541

@@ -531,24 +555,28 @@ private String extractRootProjectValue(Path inputFilePath) throws IOException {
531555

532556
private Map<String, String> extractProperties(Path manifestPath) throws IOException {
533557
Path propsTempFile = getProperties(manifestPath);
534-
String content = Files.readString(propsTempFile);
535-
// Define the regular expression pattern for key-value pairs
536-
Pattern pattern = Pattern.compile("([^:]+):\\s+(.+)");
537-
Matcher matcher = pattern.matcher(content);
538-
// Create a Map to store key-value pairs
539-
Map<String, String> keyValueMap = new HashMap<>();
540-
541-
// Iterate through matches and add them to the map
542-
while (matcher.find()) {
543-
String key = matcher.group(1).trim();
544-
String value = matcher.group(2).trim();
545-
keyValueMap.put(key, value);
546-
}
547-
// Check if any key-value pairs were found
548-
if (!keyValueMap.isEmpty()) {
549-
return keyValueMap;
550-
} else {
551-
return Collections.emptyMap();
558+
try {
559+
String content = Files.readString(propsTempFile);
560+
// Define the regular expression pattern for key-value pairs
561+
Pattern pattern = Pattern.compile("([^:]+):\\s+(.+)");
562+
Matcher matcher = pattern.matcher(content);
563+
// Create a Map to store key-value pairs
564+
Map<String, String> keyValueMap = new HashMap<>();
565+
566+
// Iterate through matches and add them to the map
567+
while (matcher.find()) {
568+
String key = matcher.group(1).trim();
569+
String value = matcher.group(2).trim();
570+
keyValueMap.put(key, value);
571+
}
572+
// Check if any key-value pairs were found
573+
if (!keyValueMap.isEmpty()) {
574+
return keyValueMap;
575+
} else {
576+
return Collections.emptyMap();
577+
}
578+
} finally {
579+
Files.deleteIfExists(propsTempFile);
552580
}
553581
}
554582

@@ -577,14 +605,17 @@ private List<String> extractLines(Path inputFilePath, String startMarker) throws
577605

578606
@Override
579607
public Content provideComponent() throws IOException {
580-
581608
Path tempFile = getDependencies(manifest);
582-
Map<String, String> propertiesMap = extractProperties(manifest);
609+
try {
610+
Map<String, String> propertiesMap = extractProperties(manifest);
583611

584-
Sbom sbom = buildSbomFromTextFormat(tempFile, propertiesMap, AnalysisType.COMPONENT);
585-
var ignored = getIgnoredDeps(manifest);
612+
Sbom sbom = buildSbomFromTextFormat(tempFile, propertiesMap, AnalysisType.COMPONENT);
613+
var ignored = getIgnoredDeps(manifest);
586614

587-
return new Content(
588-
sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE);
615+
return new Content(
616+
sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE);
617+
} finally {
618+
Files.deleteIfExists(tempFile);
619+
}
589620
}
590621
}

src/main/java/io/github/guacsec/trustifyda/tools/Operations.java

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import java.util.List;
3030
import java.util.Map;
3131
import java.util.Optional;
32+
import java.util.concurrent.CompletableFuture;
33+
import java.util.concurrent.ExecutionException;
3234
import java.util.concurrent.TimeUnit;
3335
import java.util.logging.Logger;
3436
import java.util.stream.Collectors;
@@ -192,8 +194,15 @@ public static String runProcessGetOutput(Path dir, final String[] cmdList, Strin
192194
}
193195
}
194196

197+
private static final long DEFAULT_PROCESS_TIMEOUT_SECONDS = 120L;
198+
195199
public static ProcessExecOutput runProcessGetFullOutput(
196200
Path dir, final String[] cmdList, String[] envList) {
201+
return runProcessGetFullOutput(dir, cmdList, envList, DEFAULT_PROCESS_TIMEOUT_SECONDS);
202+
}
203+
204+
public static ProcessExecOutput runProcessGetFullOutput(
205+
Path dir, final String[] cmdList, String[] envList, long timeoutSeconds) {
197206
try {
198207
Process process;
199208
if (dir == null) {
@@ -210,34 +219,46 @@ public static ProcessExecOutput runProcessGetFullOutput(
210219
}
211220
}
212221

213-
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
214-
StringBuilder output = new StringBuilder();
215-
String line;
216-
while ((line = reader.readLine()) != null) {
217-
output.append(line);
218-
if (!line.endsWith(System.lineSeparator())) {
219-
output.append("\n");
220-
}
221-
}
222+
CompletableFuture<String> stdoutFuture =
223+
CompletableFuture.supplyAsync(() -> drainStream(process.getInputStream()));
224+
CompletableFuture<String> stderrFuture =
225+
CompletableFuture.supplyAsync(() -> drainStream(process.getErrorStream()));
222226

223-
reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
224-
StringBuilder error = new StringBuilder();
225-
while ((line = reader.readLine()) != null) {
226-
error.append(line);
227-
if (!line.endsWith(System.lineSeparator())) {
228-
error.append("\n");
229-
}
227+
boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
228+
if (!finished) {
229+
process.destroyForcibly();
230+
throw new RuntimeException(
231+
String.format(
232+
"Command '%s' timed out after %d seconds", join(" ", cmdList), timeoutSeconds));
230233
}
231234

232-
process.waitFor(30L, TimeUnit.SECONDS);
233-
234-
return new ProcessExecOutput(output.toString(), error.toString(), process.exitValue());
235-
} catch (IOException | InterruptedException e) {
235+
return new ProcessExecOutput(stdoutFuture.get(), stderrFuture.get(), process.exitValue());
236+
} catch (InterruptedException e) {
237+
Thread.currentThread().interrupt();
238+
throw new RuntimeException(
239+
String.format("Command '%s' was interrupted", join(" ", cmdList)), e);
240+
} catch (ExecutionException e) {
241+
throw new RuntimeException(
242+
String.format("Failed reading output of command '%s'", join(" ", cmdList)), e);
243+
} catch (IOException e) {
236244
throw new RuntimeException(
237245
String.format("Failed to execute command '%s' ", join(" ", cmdList)), e);
238246
}
239247
}
240248

249+
private static String drainStream(java.io.InputStream inputStream) {
250+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
251+
StringBuilder sb = new StringBuilder();
252+
String line;
253+
while ((line = reader.readLine()) != null) {
254+
sb.append(line).append("\n");
255+
}
256+
return sb.toString();
257+
} catch (IOException e) {
258+
throw new RuntimeException("Failed to read process stream", e);
259+
}
260+
}
261+
241262
public static class ProcessExecOutput {
242263
private final String output;
243264
private final String error;

0 commit comments

Comments
 (0)