Skip to content

Commit ec4735a

Browse files
committed
feat: add cli entrypoint and fat jar to use standalone
Signed-off-by: Ruben Romero Montes <rromerom@redhat.com>
1 parent bf42636 commit ec4735a

3 files changed

Lines changed: 352 additions & 3 deletions

File tree

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,73 @@ By Default, The API algorithm will use native commands of PIP installer as data
503503
It's also possible, to use lightweight Python PIP utility [pipdeptree](https://pypi.org/project/pipdeptree/) as data source instead, in order to activate this,
504504
Need to set environment variable/system property - `EXHORT_PIP_USE_DEP_TREE` to true.
505505

506+
### CLI Support
507+
508+
The Exhort Java API includes a command-line interface for standalone usage.
509+
510+
#### Building the CLI
511+
512+
To build the CLI JAR with all dependencies included:
513+
514+
```shell
515+
mvn clean package
516+
```
517+
518+
This creates two JAR files in the `target/` directory:
519+
- `exhort-java-api.jar` - Library JAR (for programmatic use)
520+
- `exhort-java-api-cli.jar` - CLI JAR (includes all dependencies)
521+
522+
#### Usage
523+
524+
```shell
525+
java -jar target/exhort-java-api-cli.jar <COMMAND> <FILE_PATH> [OPTIONS]
526+
```
527+
528+
#### Commands
529+
530+
**Stack Analysis**
531+
```shell
532+
java -jar exhort-java-api-cli.jar stack <file_path> [--summary|--html]
533+
```
534+
Perform stack analysis on the specified manifest file.
535+
536+
Options:
537+
- `--summary` - Output summary in JSON format
538+
- `--html` - Output full report in HTML format
539+
- (default) - Output full report in JSON format
540+
541+
**Component Analysis**
542+
```shell
543+
java -jar exhort-java-api-cli.jar component <file_path> [--summary]
544+
```
545+
Perform component analysis on the specified manifest file.
546+
547+
Options:
548+
- `--summary` - Output summary in JSON format
549+
- (default) - Output full report in JSON format
550+
551+
#### Examples
552+
553+
```shell
554+
# Stack analysis with JSON output (default)
555+
java -jar exhort-java-api-cli.jar stack /path/to/pom.xml
556+
557+
# Stack analysis with summary
558+
java -jar exhort-java-api-cli.jar stack /path/to/package.json --summary
559+
560+
# Stack analysis with HTML output
561+
java -jar exhort-java-api-cli.jar stack /path/to/build.gradle --html
562+
563+
# Component analysis with JSON output (default)
564+
java -jar exhort-java-api-cli.jar component /path/to/requirements.txt
565+
566+
# Component analysis with summary
567+
java -jar exhort-java-api-cli.jar component /path/to/go.mod --summary
568+
569+
# Show help
570+
java -jar exhort-java-api-cli.jar --help
571+
```
572+
506573
### Image Support
507574

508575
Generate vulnerability analysis report for container images.

pom.xml

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
<maven-site-plugin.version>4.0.0-M6</maven-site-plugin.version>
4545
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
4646
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
47+
<maven-shade-plugin.version>3.4.1</maven-shade-plugin.version>
4748
<build-helper-maven-plugin.version>3.4.0</build-helper-maven-plugin.version>
4849
<extra-enforcer-rules.version>1.6.2</extra-enforcer-rules.version>
4950
<flatten-maven-plugin.version>1.4.1</flatten-maven-plugin.version>
@@ -454,6 +455,11 @@ limitations under the License.]]>
454455
<artifactId>spotless-maven-plugin</artifactId>
455456
<version>${spotless-maven-plugin.version}</version>
456457
</plugin>
458+
<plugin>
459+
<groupId>org.apache.maven.plugins</groupId>
460+
<artifactId>maven-shade-plugin</artifactId>
461+
<version>${maven-shade-plugin.version}</version>
462+
</plugin>
457463
</plugins>
458464
</pluginManagement>
459465

@@ -651,6 +657,37 @@ limitations under the License.]]>
651657
</execution>
652658
</executions>
653659
</plugin>
660+
<plugin>
661+
<artifactId>maven-jar-plugin</artifactId>
662+
<configuration>
663+
<archive>
664+
<manifest>
665+
<mainClass>com.redhat.exhort.Cli</mainClass>
666+
</manifest>
667+
</archive>
668+
</configuration>
669+
</plugin>
670+
<plugin>
671+
<groupId>org.apache.maven.plugins</groupId>
672+
<artifactId>maven-shade-plugin</artifactId>
673+
<executions>
674+
<execution>
675+
<phase>package</phase>
676+
<goals>
677+
<goal>shade</goal>
678+
</goals>
679+
<configuration>
680+
<shadedArtifactAttached>true</shadedArtifactAttached>
681+
<shadedClassifierName>cli</shadedClassifierName>
682+
<transformers>
683+
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
684+
<mainClass>com.redhat.exhort.Cli</mainClass>
685+
</transformer>
686+
</transformers>
687+
</configuration>
688+
</execution>
689+
</executions>
690+
</plugin>
654691
<plugin>
655692
<groupId>com.diffplug.spotless</groupId>
656693
<artifactId>spotless-maven-plugin</artifactId>
@@ -813,9 +850,6 @@ limitations under the License.]]>
813850
<plugin>
814851
<groupId>de.sormuras.junit</groupId>
815852
<artifactId>junit-platform-maven-plugin</artifactId>
816-
<version>${junit-platform-maven-plugin.version}</version>
817-
<configuration>
818-
</configuration>
819853
</plugin>
820854
</plugins>
821855
</build>
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* Copyright © 2025 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.redhat.exhort;
17+
18+
import java.io.IOException;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
import java.nio.file.Paths;
22+
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.ExecutionException;
24+
25+
import com.fasterxml.jackson.annotation.JsonInclude;
26+
import com.fasterxml.jackson.core.JsonProcessingException;
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
import com.redhat.exhort.api.v4.AnalysisReport;
29+
import com.redhat.exhort.api.v4.ProviderReport;
30+
import com.redhat.exhort.api.v4.Source;
31+
import com.redhat.exhort.impl.ExhortApi;
32+
33+
public class Cli {
34+
35+
private static final ObjectMapper MAPPER = new ObjectMapper();
36+
37+
static {
38+
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
39+
}
40+
41+
private enum Command {
42+
STACK,
43+
COMPONENT
44+
}
45+
46+
private enum OutputFormat {
47+
JSON,
48+
SUMMARY,
49+
HTML
50+
}
51+
52+
public static void main(String[] args) {
53+
if (args.length == 0 || isHelpRequested(args)) {
54+
printHelp();
55+
return;
56+
}
57+
58+
try {
59+
CliArgs cliArgs = parseArgs(args);
60+
String result = executeCommand(cliArgs).get();
61+
System.out.println(result);
62+
} catch (IllegalArgumentException e) {
63+
System.err.println("Error: " + e.getMessage());
64+
System.err.println();
65+
printHelp();
66+
System.exit(1);
67+
} catch (IOException | InterruptedException | ExecutionException e) {
68+
System.err.println("Unexpected error: " + e.getMessage());
69+
System.exit(1);
70+
}
71+
}
72+
73+
private static boolean isHelpRequested(String[] args) {
74+
for (String arg : args) {
75+
if ("--help".equals(arg) || "-h".equals(arg)) {
76+
return true;
77+
}
78+
}
79+
return false;
80+
}
81+
82+
private static CliArgs parseArgs(String[] args) {
83+
if (args.length < 2) {
84+
throw new IllegalArgumentException("Missing required arguments");
85+
}
86+
87+
String commandStr = args[0].toLowerCase();
88+
Command command;
89+
90+
switch (commandStr) {
91+
case "stack":
92+
command = Command.STACK;
93+
break;
94+
case "component":
95+
command = Command.COMPONENT;
96+
break;
97+
default:
98+
throw new IllegalArgumentException(
99+
"Unknown command: " + commandStr + ". Use 'stack' or 'component'");
100+
}
101+
102+
String filePath = args[1];
103+
Path path = Paths.get(filePath);
104+
105+
if (!Files.exists(path)) {
106+
throw new IllegalArgumentException("File does not exist: " + filePath);
107+
}
108+
109+
if (!Files.isRegularFile(path)) {
110+
throw new IllegalArgumentException("Path is not a file: " + filePath);
111+
}
112+
113+
OutputFormat outputFormat = OutputFormat.JSON; // default
114+
115+
// Parse additional options for stack command
116+
if (args.length > 2) {
117+
for (int i = 2; i < args.length; i++) {
118+
switch (args[i]) {
119+
case "--summary":
120+
outputFormat = OutputFormat.SUMMARY;
121+
break;
122+
case "--html":
123+
if (command != Command.STACK) {
124+
throw new IllegalArgumentException("HTML format is only supported for stack command");
125+
}
126+
outputFormat = OutputFormat.HTML;
127+
break;
128+
default:
129+
throw new IllegalArgumentException("Unknown option for stack command: " + args[i]);
130+
}
131+
}
132+
} else if (command == Command.COMPONENT && args.length > 2) {
133+
throw new IllegalArgumentException("Component command does not accept additional options");
134+
}
135+
136+
return new CliArgs(command, path, outputFormat);
137+
}
138+
139+
private static CompletableFuture<String> executeCommand(CliArgs args) throws IOException {
140+
switch (args.command) {
141+
case STACK:
142+
return executeStackAnalysis(args.filePath.toAbsolutePath().toString(), args.outputFormat);
143+
case COMPONENT:
144+
return executeComponentAnalysis(
145+
args.filePath.toAbsolutePath().toString(), args.outputFormat);
146+
default:
147+
throw new AssertionError();
148+
}
149+
}
150+
151+
private static CompletableFuture<String> executeStackAnalysis(
152+
String filePath, OutputFormat outputFormat) throws IOException {
153+
Api api = new ExhortApi();
154+
switch (outputFormat) {
155+
case JSON:
156+
return api.stackAnalysis(filePath).thenApply(Cli::toJsonString);
157+
case HTML:
158+
return api.stackAnalysisHtml(filePath).thenApply(bytes -> new String(bytes));
159+
case SUMMARY:
160+
return api.stackAnalysis(filePath)
161+
.thenApply(Cli::extractSummary)
162+
.thenApply(Cli::toJsonString);
163+
default:
164+
throw new AssertionError();
165+
}
166+
}
167+
168+
private static CompletableFuture<String> executeComponentAnalysis(
169+
String filePath, OutputFormat outputFormat) throws IOException {
170+
Api api = new ExhortApi();
171+
CompletableFuture<AnalysisReport> analysis = api.componentAnalysis(filePath);
172+
if (outputFormat.equals(OutputFormat.SUMMARY)) {
173+
analysis = analysis.thenApply(Cli::extractSummary);
174+
}
175+
return analysis.thenApply(Cli::toJsonString);
176+
}
177+
178+
private static String toJsonString(Object obj) {
179+
try {
180+
return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
181+
} catch (JsonProcessingException e) {
182+
throw new RuntimeException("Failed to serialize to JSON", e);
183+
}
184+
}
185+
186+
private static AnalysisReport extractSummary(AnalysisReport report) {
187+
AnalysisReport summary = new AnalysisReport();
188+
summary.setScanned(report.getScanned());
189+
if(report.getProviders() == null) {
190+
return summary;
191+
}
192+
report.getProviders().entrySet().forEach(entry -> {
193+
var provider = new ProviderReport();
194+
provider.setStatus(entry.getValue().getStatus());
195+
if(entry.getValue().getSources() != null) {
196+
entry.getValue().getSources().entrySet().forEach(sourceEntry -> {
197+
var source = new Source();
198+
source.setSummary(sourceEntry.getValue().getSummary());
199+
provider.putSourcesItem(sourceEntry.getKey(), source);
200+
});
201+
}
202+
summary.putProvidersItem(entry.getKey(), provider);
203+
});
204+
return summary;
205+
}
206+
207+
private static void printHelp() {
208+
System.out.println("Exhort Java API CLI");
209+
System.out.println();
210+
System.out.println("USAGE:");
211+
System.out.println(" java -jar exhort-java-api.jar <COMMAND> <FILE_PATH> [OPTIONS]");
212+
System.out.println();
213+
System.out.println("COMMANDS:");
214+
System.out.println(" stack <file_path> [--summary|--html]");
215+
System.out.println(" Perform stack analysis on the specified manifest file");
216+
System.out.println(" Options:");
217+
System.out.println(" --summary Output summary in JSON format");
218+
System.out.println(" --html Output full report in HTML format");
219+
System.out.println(" (default) Output full report in JSON format");
220+
System.out.println();
221+
System.out.println(" component <file_path> [--summary]");
222+
System.out.println(" Perform component analysis on the specified manifest file");
223+
System.out.println(" Options:");
224+
System.out.println(" --summary Output summary in JSON format");
225+
System.out.println(" (default) Output full report in JSON format");
226+
System.out.println();
227+
System.out.println("OPTIONS:");
228+
System.out.println(" -h, --help Show this help message");
229+
System.out.println();
230+
System.out.println("EXAMPLES:");
231+
System.out.println(" java -jar exhort-java-api.jar stack /path/to/pom.xml");
232+
System.out.println(" java -jar exhort-java-api.jar stack /path/to/package.json --summary");
233+
System.out.println(" java -jar exhort-java-api.jar stack /path/to/build.gradle --html");
234+
System.out.println(" java -jar exhort-java-api.jar component /path/to/requirements.txt");
235+
}
236+
237+
private static class CliArgs {
238+
final Command command;
239+
final Path filePath;
240+
final OutputFormat outputFormat;
241+
242+
CliArgs(Command command, Path filePath, OutputFormat outputFormat) {
243+
this.command = command;
244+
this.filePath = filePath;
245+
this.outputFormat = outputFormat;
246+
}
247+
}
248+
}

0 commit comments

Comments
 (0)