Skip to content
Merged
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
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ public class TrustifyExample {
var result = componentWithLicense.get();
var report = result.report(); // standard AnalysisReport
var licenseSummary = result.licenseSummary(); // license compatibility summary (may be null)

// generate a CycloneDX SBOM locally (no backend call required)
String sbomJson = exhortApi.generateSbom("/path/to/pom.xml");
}
}
```
Expand Down Expand Up @@ -670,6 +673,16 @@ java -jar trustify-da-java-client-cli.jar license <file_path>
```
Display project license information from manifest and LICENSE file in JSON format.

**SBOM Generation**
```shell
java -jar trustify-da-java-client-cli.jar sbom <file_path> [--output <path>]
```
Generate a CycloneDX SBOM from the specified manifest file locally, without sending anything to the backend.

Options:
- `--output <path>` - Write SBOM JSON to the specified file
- (default) - Print SBOM JSON to stdout

**Image Analysis**
```shell
java -jar trustify-da-java-client-cli.jar image <image_ref> [<image_ref>...] [--summary|--html]
Expand All @@ -690,9 +703,9 @@ Options:

The client requires the backend URL to be configured through the environment variable:

- **Environment variable**: `TRUSTIFY_DA_BACKEND_URL=https://backend.url` (required)
- **Environment variable**: `TRUSTIFY_DA_BACKEND_URL=https://backend.url` (required for analysis commands)

The application will fail to start if this environment variable is not set.
The application will fail to start if this environment variable is not set, except for the `sbom` command which operates locally without a backend connection.

#### Examples

Expand Down Expand Up @@ -720,6 +733,10 @@ 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

# SBOM generation (no backend required)
java -jar trustify-da-java-client-cli.jar sbom /path/to/pom.xml
java -jar trustify-da-java-client-cli.jar sbom /path/to/package.json --output sbom.json

# License information
java -jar trustify-da-java-client-cli.jar license /path/to/pom.xml

Expand Down
10 changes: 10 additions & 0 deletions src/main/java/io/github/guacsec/trustifyda/Api.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,14 @@ CompletableFuture<Map<ImageRef, AnalysisReport>> imageAnalysis(Set<ImageRef> ima
throws IOException;

CompletableFuture<byte[]> imageAnalysisHtml(Set<ImageRef> imageRefs) throws IOException;

/**
* Generate a CycloneDX SBOM from a manifest file locally without sending anything to the backend.
*
* @param manifestFile the path for the manifest file
* @return the CycloneDX JSON SBOM content as a String
* @throws IOException when failed to load the manifest file
Comment thread
soul2zimate marked this conversation as resolved.
* @throws IllegalStateException when the manifest file type is not supported
*/
String generateSbom(String manifestFile) throws IOException;
}
38 changes: 37 additions & 1 deletion src/main/java/io/github/guacsec/trustifyda/cli/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ private static CliArgs parseArgs(String[] args) {
return switch (command) {
case STACK, COMPONENT, LICENSE -> parseFileBasedArgs(command, args);
case IMAGE -> parseImageBasedArgs(command, args);
case SBOM -> parseSbomArgs(args);
};
}

Expand Down Expand Up @@ -153,17 +154,40 @@ private static CliArgs parseImageBasedArgs(Command command, String[] args) {
return new CliArgs(command, imageRefs, outputFormat);
}

private static CliArgs parseSbomArgs(String[] args) {
if (args.length < 2) {
throw new IllegalArgumentException("Missing required file path for sbom command");
}

Path path = validateFile(args[1]);
Path outputPath = null;

for (int i = 2; i < args.length; i++) {
if ("--output".equals(args[i])) {
if (i + 1 >= args.length) {
throw new IllegalArgumentException("Missing value for --output flag");
}
outputPath = Paths.get(args[++i]);
} else {
throw new IllegalArgumentException("Unknown option for sbom command: " + args[i]);
}
}

return new CliArgs(Command.SBOM, path, outputPath);
}

private static Command parseCommand(String commandStr) {
return switch (commandStr) {
case "stack" -> Command.STACK;
case "component" -> Command.COMPONENT;
case "image" -> Command.IMAGE;
case "license" -> Command.LICENSE;
case "sbom" -> Command.SBOM;
default ->
throw new IllegalArgumentException(
"Unknown command: "
+ commandStr
+ ". Use 'stack', 'component', 'image', or 'license'");
+ ". Use 'stack', 'component', 'image', 'license', or 'sbom'");
};
}

Expand Down Expand Up @@ -202,6 +226,7 @@ private static CompletableFuture<String> executeCommand(CliArgs args) throws IOE
executeComponentAnalysis(args.filePath.toAbsolutePath().toString(), args.outputFormat);
case IMAGE -> executeImageAnalysis(args.imageRefs, args.outputFormat);
case LICENSE -> executeLicenseCheck(args.filePath.toAbsolutePath());
case SBOM -> executeSbomGeneration(args);
};
}

Expand Down Expand Up @@ -300,6 +325,17 @@ private static CompletableFuture<String> executeImageAnalysis(
};
}

private static CompletableFuture<String> executeSbomGeneration(CliArgs args) throws IOException {
Api api = new ExhortApi();
String sbomJson = api.generateSbom(args.filePath.toAbsolutePath().toString());
if (args.outputPath != null) {
Files.writeString(args.outputPath, sbomJson);
return CompletableFuture.completedFuture(
"SBOM written to " + args.outputPath.toAbsolutePath());
}
return CompletableFuture.completedFuture(sbomJson);
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
}

private static String formatImageAnalysisResult(Map<ImageRef, AnalysisReport> analysisResults) {
try {
return MAPPER.writeValueAsString(analysisResults);
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/io/github/guacsec/trustifyda/cli/CliArgs.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,29 @@ public class CliArgs {
public final Path filePath;
public final Set<ImageRef> imageRefs;
public final OutputFormat outputFormat;
public final Path outputPath;

public CliArgs(Command command, Path filePath, OutputFormat outputFormat) {
this.command = command;
this.filePath = filePath;
this.imageRefs = null;
this.outputFormat = outputFormat;
this.outputPath = null;
}

public CliArgs(Command command, Set<ImageRef> imageRefs, OutputFormat outputFormat) {
this.command = command;
this.filePath = null;
this.imageRefs = imageRefs;
this.outputFormat = outputFormat;
this.outputPath = null;
}

public CliArgs(Command command, Path filePath, Path outputPath) {
this.command = command;
this.filePath = filePath;
this.imageRefs = null;
this.outputFormat = null;
this.outputPath = outputPath;
}
}
3 changes: 2 additions & 1 deletion src/main/java/io/github/guacsec/trustifyda/cli/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ public enum Command {
STACK,
COMPONENT,
IMAGE,
LICENSE
LICENSE,
SBOM
}
30 changes: 20 additions & 10 deletions src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,13 @@ public final class ExhortApi implements Api {
public static final String S_API_V5_LICENSES_IDENTIFY = "%s/api/v5/licenses/identify";
private static final String TRUSTIFY_DA_LICENSE_CHECK = "TRUSTIFY_DA_LICENSE_CHECK";

private final String endpoint;
private String endpoint;

public String getEndpoint() {
return endpoint;
if (this.endpoint == null) {
this.endpoint = getExhortUrl();
}
return this.endpoint;
}

private final HttpClient client;
Expand Down Expand Up @@ -117,7 +120,6 @@ static HttpClient.Version getHttpVersion() {
commonHookBeginning(true);
this.client = client;
this.mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
this.endpoint = getExhortUrl();
}

public static HttpClient createHttpClient() {
Expand Down Expand Up @@ -346,7 +348,7 @@ public CompletableFuture<AnalysisReport> componentAnalysis(
String exClientTraceId = commonHookBeginning(false);
var manifestPath = Path.of(manifest);
var provider = Ecosystem.getProvider(manifestPath);
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, this.endpoint));
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, getEndpoint()));
var content = provider.provideComponent();
commonHookAfterProviderCreatedSbomAndBeforeExhort();
return getAnalysisReportForComponent(uri, content, exClientTraceId);
Expand Down Expand Up @@ -390,7 +392,7 @@ public CompletableFuture<AnalysisReport> componentAnalysis(String manifestFile)
String exClientTraceId = commonHookBeginning(false);
var manifestPath = Path.of(manifestFile);
var provider = Ecosystem.getProvider(manifestPath);
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, this.endpoint));
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, getEndpoint()));
var content = provider.provideComponent();
commonHookAfterProviderCreatedSbomAndBeforeExhort();
return getAnalysisReportForComponent(uri, content, exClientTraceId);
Expand Down Expand Up @@ -431,13 +433,21 @@ private HttpRequest buildStackRequest(final String manifestFile, final MediaType
throws IOException {
var manifestPath = Path.of(manifestFile);
var provider = Ecosystem.getProvider(manifestPath);
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, this.endpoint));
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, getEndpoint()));
var content = provider.provideStack();
commonHookAfterProviderCreatedSbomAndBeforeExhort();

return buildRequest(content, uri, acceptType, "Stack Analysis");
}

@Override
public String generateSbom(final String manifestFile) throws IOException {
var manifestPath = Path.of(manifestFile);
var provider = Ecosystem.getProvider(manifestPath);
var content = provider.provideStack();
return new String(content.buffer, java.nio.charset.StandardCharsets.UTF_8);
}

@Override
public CompletableFuture<Map<ImageRef, AnalysisReport>> imageAnalysis(
final Set<ImageRef> imageRefs) throws IOException {
Expand Down Expand Up @@ -510,7 +520,7 @@ <H, T> CompletableFuture<T> performBatchAnalysis(
final String analysisName)
throws IOException {
String exClientTraceId = commonHookBeginning(false);
var uri = URI.create(String.format(S_API_V_5_BATCH_ANALYSIS, this.endpoint));
var uri = URI.create(String.format(S_API_V_5_BATCH_ANALYSIS, getEndpoint()));
var sboms = sbomsGenerator.get();
var content =
new Provider.Content(
Expand Down Expand Up @@ -572,7 +582,7 @@ public CompletableFuture<ComponentAnalysisResult> componentAnalysisWithLicense(
String exClientTraceId = commonHookBeginning(false);
var manifestPath = Path.of(manifestFile);
var provider = Ecosystem.getProvider(manifestPath);
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, this.endpoint));
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, getEndpoint()));
var content = provider.provideComponent();
String sbomJson = new String(content.buffer);
commonHookAfterProviderCreatedSbomAndBeforeExhort();
Expand Down Expand Up @@ -603,7 +613,7 @@ public CompletableFuture<ComponentAnalysisResult> componentAnalysisWithLicense(
*/
public CompletableFuture<JsonNode> getLicenseDetails(String spdxId) {
String encodedId = URLEncoder.encode(spdxId, StandardCharsets.UTF_8).replace("+", "%20");
URI uri = URI.create(String.format(S_API_V5_LICENSES, this.endpoint, encodedId));
URI uri = URI.create(String.format(S_API_V5_LICENSES, getEndpoint(), encodedId));
HttpRequest request = buildGetRequest(uri, "License Details");

return this.client
Expand Down Expand Up @@ -649,7 +659,7 @@ public CompletableFuture<String> identifyLicense(Path licenseFilePath) {
LOG.warning(String.format("Failed to read license file: %s", e.getMessage()));
return CompletableFuture.completedFuture(null);
}
URI uri = URI.create(String.format(S_API_V5_LICENSES_IDENTIFY, this.endpoint));
URI uri = URI.create(String.format(S_API_V5_LICENSES_IDENTIFY, getEndpoint()));
HttpRequest request =
buildPostRequest(
uri,
Expand Down
10 changes: 10 additions & 0 deletions src/main/resources/cli_help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ COMMANDS:
--summary Output summary in JSON format (without license check)
(default) Output full report in JSON format (includes license summary)

sbom <file_path> [--output <path>]
Generate a CycloneDX SBOM from the specified manifest file (no backend call)
Options:
--output <path> Write SBOM JSON to the specified file
(default) Print SBOM JSON to stdout

license <file_path>
Display project license information from manifest and LICENSE file in JSON format

Expand Down Expand Up @@ -49,6 +55,10 @@ EXAMPLES:
java -jar trustify-da-java-client-cli.jar component /path/to/requirements.txt
java -jar trustify-da-java-client-cli.jar stack /path/to/Cargo.toml

# SBOM generation
java -jar trustify-da-java-client-cli.jar sbom /path/to/pom.xml
java -jar trustify-da-java-client-cli.jar sbom /path/to/package.json --output sbom.json

# License information
java -jar trustify-da-java-client-cli.jar license /path/to/pom.xml

Expand Down
Loading
Loading