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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,22 @@ Options:
- `--summary` - Output summary in JSON format
- (default) - Output full report in JSON format

**Image Analysis**
```shell
java -jar trustify-da-java-client-cli.jar image <image_ref> [<image_ref>...] [--summary|--html]
```
Perform security analysis on the specified container image(s).

Arguments:
- `<image_ref>` - Container image reference (e.g., `nginx:latest`, `registry.io/image:tag`)
- Multiple images can be analyzed at once
- Optionally specify platform with `^^` notation (e.g., `image:tag^^linux/amd64`)

Options:
- `--summary` - Output summary in JSON format
- `--html` - Output full report in HTML format
- (default) - Output full report in JSON format

#### Backend Configuration

The client requires the backend URL to be configured through the environment variable:
Expand Down Expand Up @@ -637,6 +653,18 @@ 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

# Container image analysis with JSON output (default)
java -jar trustify-da-java-client-cli.jar image nginx:latest

# Multiple container image analysis
java -jar trustify-da-java-client-cli.jar image nginx:latest docker.io/library/node:18

# Container image analysis with platform specification
java -jar trustify-da-java-client-cli.jar image nginx:latest^^linux/amd64 --summary

# Container image analysis with HTML output
java -jar trustify-da-java-client-cli.jar image quay.io/redhat/ubi8:latest --html

# Show help
java -jar trustify-da-java-client-cli.jar --help
```
Expand Down
107 changes: 104 additions & 3 deletions src/main/java/io/github/guacsec/trustifyda/cli/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@
import io.github.guacsec.trustifyda.api.v5.AnalysisReport;
import io.github.guacsec.trustifyda.api.v5.ProviderReport;
import io.github.guacsec.trustifyda.api.v5.SourceSummary;
import io.github.guacsec.trustifyda.image.ImageRef;
import io.github.guacsec.trustifyda.image.ImageUtils;
import io.github.guacsec.trustifyda.impl.ExhortApi;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

Expand Down Expand Up @@ -82,25 +86,81 @@ private static CliArgs parseArgs(String[] args) {

Command command = parseCommand(args[0]);

switch (command) {
case STACK:
case COMPONENT:
return parseFileBasedArgs(command, args);
case IMAGE:
return parseImageBasedArgs(command, args);
default:
throw new IllegalArgumentException("Unsupported command: " + command);
}
}

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

Path path = validateFile(args[1]);

OutputFormat outputFormat = OutputFormat.JSON;
if (args.length == 3) {
outputFormat = parseOutputFormat(command, args[2]);
} else if (args.length > 3) {
throw new IllegalArgumentException("Too many arguments for " + command + " command");
}

return new CliArgs(command, path, outputFormat);
}

private static CliArgs parseImageBasedArgs(Command command, String[] args) {
if (args.length < 2) {
throw new IllegalArgumentException(
"Missing required image references for " + command + " command");
}

OutputFormat outputFormat = OutputFormat.JSON;
int imageArgCount = args.length - 1;

if (args.length >= 3) {
String lastArg = args[args.length - 1];
if (lastArg.startsWith("--")) {
outputFormat = parseOutputFormat(command, lastArg);
imageArgCount = args.length - 2;
}
}

if (imageArgCount < 1) {
throw new IllegalArgumentException(
"At least one image reference is required for " + command + " command");
}

Set<ImageRef> imageRefs = new HashSet<>();
for (int i = 1; i <= imageArgCount; i++) {
try {
ImageRef imageRef = ImageUtils.parseImageRef(args[i]);
imageRefs.add(imageRef);
} catch (Exception e) {
throw new IllegalArgumentException(
"Invalid image reference '" + args[i] + "': " + e.getMessage(), e);
}
}

return new CliArgs(command, imageRefs, outputFormat);
}

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

Expand All @@ -109,8 +169,9 @@ private static OutputFormat parseOutputFormat(Command command, String formatArg)
case "--summary":
return OutputFormat.SUMMARY;
case "--html":
if (command != Command.STACK) {
throw new IllegalArgumentException("HTML format is only supported for stack command");
if (command != Command.STACK && command != Command.IMAGE) {
throw new IllegalArgumentException(
"HTML format is only supported for stack and image commands");
}
return OutputFormat.HTML;
default:
Expand All @@ -137,6 +198,8 @@ private static CompletableFuture<String> executeCommand(CliArgs args) throws IOE
case COMPONENT:
return executeComponentAnalysis(
args.filePath.toAbsolutePath().toString(), args.outputFormat);
case IMAGE:
return executeImageAnalysis(args.imageRefs, args.outputFormat);
default:
throw new AssertionError();
}
Expand Down Expand Up @@ -178,6 +241,44 @@ private static String toJsonString(Object obj) {
}
}

private static CompletableFuture<String> executeImageAnalysis(
Set<ImageRef> imageRefs, OutputFormat outputFormat) throws IOException {
Api api = new ExhortApi();
switch (outputFormat) {
case JSON:
return api.imageAnalysis(imageRefs).thenApply(App::formatImageAnalysisResult);
case HTML:
return api.imageAnalysisHtml(imageRefs).thenApply(bytes -> new String(bytes));
case SUMMARY:
return api.imageAnalysis(imageRefs)
.thenApply(App::extractImageSummary)
.thenApply(App::toJsonString);
default:
throw new AssertionError();
}
}

private static String formatImageAnalysisResult(Map<ImageRef, AnalysisReport> analysisResults) {
try {
return MAPPER.writeValueAsString(analysisResults);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize image analysis results", e);
}
}

private static Map<String, Map<String, SourceSummary>> extractImageSummary(
Map<ImageRef, AnalysisReport> analysisResults) {
Map<String, Map<String, SourceSummary>> imageSummaries = new HashMap<>();

for (Map.Entry<ImageRef, AnalysisReport> entry : analysisResults.entrySet()) {
String imageKey = entry.getKey().toString();
Map<String, SourceSummary> imageSummary = extractSummary(entry.getValue());
imageSummaries.put(imageKey, imageSummary);
}

return imageSummaries;
}

private static Map<String, SourceSummary> extractSummary(AnalysisReport report) {
Map<String, SourceSummary> summary = new HashMap<>();
if (report.getProviders() == null) {
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 @@ -16,16 +16,27 @@
*/
package io.github.guacsec.trustifyda.cli;

import io.github.guacsec.trustifyda.image.ImageRef;
import java.nio.file.Path;
import java.util.Set;

public class CliArgs {
public final Command command;
public final Path filePath;
public final Set<ImageRef> imageRefs;
public final OutputFormat outputFormat;

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

public CliArgs(Command command, Set<ImageRef> imageRefs, OutputFormat outputFormat) {
this.command = command;
this.filePath = null;
this.imageRefs = imageRefs;
this.outputFormat = outputFormat;
}
}
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 @@ -18,5 +18,6 @@

public enum Command {
STACK,
COMPONENT
COMPONENT,
IMAGE
}
39 changes: 39 additions & 0 deletions src/main/java/io/github/guacsec/trustifyda/image/ImageUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,45 @@ static String updatePATHEnv(String execPath) {
}
}

/**
* Parse an image reference string that may contain architecture specification using ^^ notation.
* Examples: - "docker.io/library/node:18" -> ImageRef with no specific platform -
* "docker.io/library/node:18^^amd64" -> ImageRef with amd64 platform -
* "httpd:2.4.49^^linux/amd64" -> ImageRef with linux/amd64 platform
*
* @param imageRefString the image reference string
* @return ImageRef object
* @throws IllegalArgumentException if the format is invalid
*/
public static ImageRef parseImageRef(String imageRefString) {
if (imageRefString == null || imageRefString.trim().isEmpty()) {
throw new IllegalArgumentException("Image reference cannot be null or empty");
}

String[] parts = imageRefString.split("\\^\\^", 2);

if (parts.length == 1) {
// Simple case: "docker.io/library/node:18"
return new ImageRef(parts[0].trim(), null);
} else if (parts.length == 2) {
// Architecture case: "docker.io/library/node:18^^amd64"
String imageRef = parts[0].trim();
String platform = parts[1].trim();
if (imageRef.isEmpty()) {
throw new IllegalArgumentException("Image reference cannot be empty before ^^");
}
if (platform.isEmpty()) {
throw new IllegalArgumentException("Platform specification cannot be empty after ^^");
}
return new ImageRef(imageRef, platform);
} else {
throw new IllegalArgumentException(
"Invalid image reference format: "
+ imageRefString
+ ". Use 'image' or 'image^^platform'");
}
}

public static JsonNode generateImageSBOM(ImageRef imageRef)
throws IOException, MalformedPackageURLException {
var output = execSyft(imageRef);
Expand Down
20 changes: 19 additions & 1 deletion src/main/resources/cli_help.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Dependency Analytics Java API CLI

USAGE:
java -jar trustify-da-java-client-cli.jar <COMMAND> <FILE_PATH> [OPTIONS]
java -jar trustify-da-java-client-cli.jar <COMMAND> <ARGUMENTS> [OPTIONS]

COMMANDS:
stack <file_path> [--summary|--html]
Expand All @@ -17,6 +17,17 @@ COMMANDS:
--summary Output summary in JSON format
(default) Output full report in JSON format

image <image_ref> [<image_ref>...] [--summary|--html]
Perform security analysis on the specified container image(s)
Arguments:
<image_ref> Container image reference (e.g., nginx:latest, registry.io/image:tag)
Multiple images can be analyzed at once
Optionally specify platform with ^^ notation (e.g., image:tag^^linux/amd64)
Options:
--summary Output summary in JSON format
--html Output full report in HTML format
(default) Output full report in JSON format

OPTIONS:
-h, --help Show this help message

Expand All @@ -26,7 +37,14 @@ ENVIRONMENT VARIABLES:
EXAMPLES:
export TRUSTIFY_DA_BACKEND_URL=https://your-backend.url

# File-based analysis
java -jar trustify-da-java-client-cli.jar stack /path/to/pom.xml
java -jar trustify-da-java-client-cli.jar stack /path/to/package.json --summary
java -jar trustify-da-java-client-cli.jar stack /path/to/build.gradle --html
java -jar trustify-da-java-client-cli.jar component /path/to/requirements.txt

# Container image analysis
java -jar trustify-da-java-client-cli.jar image nginx:latest
java -jar trustify-da-java-client-cli.jar image nginx:latest docker.io/library/node:18
java -jar trustify-da-java-client-cli.jar image nginx:latest^^linux/amd64 --summary
java -jar trustify-da-java-client-cli.jar image quay.io/redhat/ubi8:latest --html
Loading
Loading