Skip to content

Commit 76e41aa

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 76e41aa

3 files changed

Lines changed: 360 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: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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 com.fasterxml.jackson.annotation.JsonInclude;
19+
import com.fasterxml.jackson.core.JsonProcessingException;
20+
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import com.redhat.exhort.api.v4.AnalysisReport;
22+
import com.redhat.exhort.api.v4.ProviderReport;
23+
import com.redhat.exhort.api.v4.Source;
24+
import com.redhat.exhort.impl.ExhortApi;
25+
import java.io.IOException;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.nio.file.Paths;
29+
import java.util.concurrent.CompletableFuture;
30+
import java.util.concurrent.ExecutionException;
31+
32+
public class Cli {
33+
34+
private static final ObjectMapper MAPPER = new ObjectMapper();
35+
36+
static {
37+
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
38+
}
39+
40+
private enum Command {
41+
STACK,
42+
COMPONENT
43+
}
44+
45+
private enum OutputFormat {
46+
JSON,
47+
SUMMARY,
48+
HTML
49+
}
50+
51+
public static void main(String[] args) {
52+
if (args.length == 0 || isHelpRequested(args)) {
53+
printHelp();
54+
return;
55+
}
56+
57+
try {
58+
CliArgs cliArgs = parseArgs(args);
59+
String result = executeCommand(cliArgs).get();
60+
System.out.println(result);
61+
} catch (IllegalArgumentException e) {
62+
System.err.println("Error: " + e.getMessage());
63+
System.err.println();
64+
printHelp();
65+
System.exit(1);
66+
} catch (IOException | InterruptedException | ExecutionException e) {
67+
System.err.println("Unexpected error: " + e.getMessage());
68+
System.exit(1);
69+
}
70+
}
71+
72+
private static boolean isHelpRequested(String[] args) {
73+
for (String arg : args) {
74+
if ("--help".equals(arg) || "-h".equals(arg)) {
75+
return true;
76+
}
77+
}
78+
return false;
79+
}
80+
81+
private static CliArgs parseArgs(String[] args) {
82+
if (args.length < 2) {
83+
throw new IllegalArgumentException("Missing required arguments");
84+
}
85+
86+
String commandStr = args[0].toLowerCase();
87+
Command command;
88+
89+
switch (commandStr) {
90+
case "stack":
91+
command = Command.STACK;
92+
break;
93+
case "component":
94+
command = Command.COMPONENT;
95+
break;
96+
default:
97+
throw new IllegalArgumentException(
98+
"Unknown command: " + commandStr + ". Use 'stack' or 'component'");
99+
}
100+
101+
String filePath = args[1];
102+
Path path = Paths.get(filePath);
103+
104+
if (!Files.exists(path)) {
105+
throw new IllegalArgumentException("File does not exist: " + filePath);
106+
}
107+
108+
if (!Files.isRegularFile(path)) {
109+
throw new IllegalArgumentException("Path is not a file: " + filePath);
110+
}
111+
112+
OutputFormat outputFormat = OutputFormat.JSON; // default
113+
114+
// Parse additional options for stack command
115+
if (args.length > 2) {
116+
for (int i = 2; i < args.length; i++) {
117+
switch (args[i]) {
118+
case "--summary":
119+
outputFormat = OutputFormat.SUMMARY;
120+
break;
121+
case "--html":
122+
if (command != Command.STACK) {
123+
throw new IllegalArgumentException("HTML format is only supported for stack command");
124+
}
125+
outputFormat = OutputFormat.HTML;
126+
break;
127+
default:
128+
throw new IllegalArgumentException("Unknown option for stack command: " + args[i]);
129+
}
130+
}
131+
} else if (command == Command.COMPONENT && args.length > 2) {
132+
throw new IllegalArgumentException("Component command does not accept additional options");
133+
}
134+
135+
return new CliArgs(command, path, outputFormat);
136+
}
137+
138+
private static CompletableFuture<String> executeCommand(CliArgs args) throws IOException {
139+
switch (args.command) {
140+
case STACK:
141+
return executeStackAnalysis(args.filePath.toAbsolutePath().toString(), args.outputFormat);
142+
case COMPONENT:
143+
return executeComponentAnalysis(
144+
args.filePath.toAbsolutePath().toString(), args.outputFormat);
145+
default:
146+
throw new AssertionError();
147+
}
148+
}
149+
150+
private static CompletableFuture<String> executeStackAnalysis(
151+
String filePath, OutputFormat outputFormat) throws IOException {
152+
Api api = new ExhortApi();
153+
switch (outputFormat) {
154+
case JSON:
155+
return api.stackAnalysis(filePath).thenApply(Cli::toJsonString);
156+
case HTML:
157+
return api.stackAnalysisHtml(filePath).thenApply(bytes -> new String(bytes));
158+
case SUMMARY:
159+
return api.stackAnalysis(filePath)
160+
.thenApply(Cli::extractSummary)
161+
.thenApply(Cli::toJsonString);
162+
default:
163+
throw new AssertionError();
164+
}
165+
}
166+
167+
private static CompletableFuture<String> executeComponentAnalysis(
168+
String filePath, OutputFormat outputFormat) throws IOException {
169+
Api api = new ExhortApi();
170+
CompletableFuture<AnalysisReport> analysis = api.componentAnalysis(filePath);
171+
if (outputFormat.equals(OutputFormat.SUMMARY)) {
172+
analysis = analysis.thenApply(Cli::extractSummary);
173+
}
174+
return analysis.thenApply(Cli::toJsonString);
175+
}
176+
177+
private static String toJsonString(Object obj) {
178+
try {
179+
return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
180+
} catch (JsonProcessingException e) {
181+
throw new RuntimeException("Failed to serialize to JSON", e);
182+
}
183+
}
184+
185+
private static AnalysisReport extractSummary(AnalysisReport report) {
186+
AnalysisReport summary = new AnalysisReport();
187+
summary.setScanned(report.getScanned());
188+
if (report.getProviders() == null) {
189+
return summary;
190+
}
191+
report
192+
.getProviders()
193+
.entrySet()
194+
.forEach(
195+
entry -> {
196+
var provider = new ProviderReport();
197+
provider.setStatus(entry.getValue().getStatus());
198+
if (entry.getValue().getSources() != null) {
199+
entry
200+
.getValue()
201+
.getSources()
202+
.entrySet()
203+
.forEach(
204+
sourceEntry -> {
205+
var source = new Source();
206+
source.setSummary(sourceEntry.getValue().getSummary());
207+
provider.putSourcesItem(sourceEntry.getKey(), source);
208+
});
209+
}
210+
summary.putProvidersItem(entry.getKey(), provider);
211+
});
212+
return summary;
213+
}
214+
215+
private static void printHelp() {
216+
System.out.println("Exhort Java API CLI");
217+
System.out.println();
218+
System.out.println("USAGE:");
219+
System.out.println(" java -jar exhort-java-api.jar <COMMAND> <FILE_PATH> [OPTIONS]");
220+
System.out.println();
221+
System.out.println("COMMANDS:");
222+
System.out.println(" stack <file_path> [--summary|--html]");
223+
System.out.println(" Perform stack analysis on the specified manifest file");
224+
System.out.println(" Options:");
225+
System.out.println(" --summary Output summary in JSON format");
226+
System.out.println(" --html Output full report in HTML format");
227+
System.out.println(" (default) Output full report in JSON format");
228+
System.out.println();
229+
System.out.println(" component <file_path> [--summary]");
230+
System.out.println(" Perform component analysis on the specified manifest file");
231+
System.out.println(" Options:");
232+
System.out.println(" --summary Output summary in JSON format");
233+
System.out.println(" (default) Output full report in JSON format");
234+
System.out.println();
235+
System.out.println("OPTIONS:");
236+
System.out.println(" -h, --help Show this help message");
237+
System.out.println();
238+
System.out.println("EXAMPLES:");
239+
System.out.println(" java -jar exhort-java-api.jar stack /path/to/pom.xml");
240+
System.out.println(" java -jar exhort-java-api.jar stack /path/to/package.json --summary");
241+
System.out.println(" java -jar exhort-java-api.jar stack /path/to/build.gradle --html");
242+
System.out.println(" java -jar exhort-java-api.jar component /path/to/requirements.txt");
243+
}
244+
245+
private static class CliArgs {
246+
final Command command;
247+
final Path filePath;
248+
final OutputFormat outputFormat;
249+
250+
CliArgs(Command command, Path filePath, OutputFormat outputFormat) {
251+
this.command = command;
252+
this.filePath = filePath;
253+
this.outputFormat = outputFormat;
254+
}
255+
}
256+
}

0 commit comments

Comments
 (0)