2727import io .github .guacsec .trustifyda .api .v5 .AnalysisReport ;
2828import io .github .guacsec .trustifyda .api .v5 .ProviderReport ;
2929import io .github .guacsec .trustifyda .api .v5 .SourceSummary ;
30+ import io .github .guacsec .trustifyda .image .ImageRef ;
31+ import io .github .guacsec .trustifyda .image .ImageUtils ;
3032import io .github .guacsec .trustifyda .impl .ExhortApi ;
3133import java .io .IOException ;
3234import java .nio .file .Files ;
3335import java .nio .file .Path ;
3436import java .nio .file .Paths ;
3537import java .util .HashMap ;
38+ import java .util .HashSet ;
3639import java .util .Map ;
40+ import java .util .Set ;
3741import java .util .concurrent .CompletableFuture ;
3842import java .util .concurrent .ExecutionException ;
3943
@@ -82,25 +86,81 @@ private static CliArgs parseArgs(String[] args) {
8286
8387 Command command = parseCommand (args [0 ]);
8488
89+ switch (command ) {
90+ case STACK :
91+ case COMPONENT :
92+ return parseFileBasedArgs (command , args );
93+ case IMAGE :
94+ return parseImageBasedArgs (command , args );
95+ default :
96+ throw new IllegalArgumentException ("Unsupported command: " + command );
97+ }
98+ }
99+
100+ private static CliArgs parseFileBasedArgs (Command command , String [] args ) {
101+ if (args .length < 2 ) {
102+ throw new IllegalArgumentException ("Missing required file path for " + command + " command" );
103+ }
104+
85105 Path path = validateFile (args [1 ]);
86106
87107 OutputFormat outputFormat = OutputFormat .JSON ;
88108 if (args .length == 3 ) {
89109 outputFormat = parseOutputFormat (command , args [2 ]);
110+ } else if (args .length > 3 ) {
111+ throw new IllegalArgumentException ("Too many arguments for " + command + " command" );
90112 }
91113
92114 return new CliArgs (command , path , outputFormat );
93115 }
94116
117+ private static CliArgs parseImageBasedArgs (Command command , String [] args ) {
118+ if (args .length < 2 ) {
119+ throw new IllegalArgumentException (
120+ "Missing required image references for " + command + " command" );
121+ }
122+
123+ OutputFormat outputFormat = OutputFormat .JSON ;
124+ int imageArgCount = args .length - 1 ;
125+
126+ if (args .length >= 3 ) {
127+ String lastArg = args [args .length - 1 ];
128+ if (lastArg .startsWith ("--" )) {
129+ outputFormat = parseOutputFormat (command , lastArg );
130+ imageArgCount = args .length - 2 ;
131+ }
132+ }
133+
134+ if (imageArgCount < 1 ) {
135+ throw new IllegalArgumentException (
136+ "At least one image reference is required for " + command + " command" );
137+ }
138+
139+ Set <ImageRef > imageRefs = new HashSet <>();
140+ for (int i = 1 ; i <= imageArgCount ; i ++) {
141+ try {
142+ ImageRef imageRef = ImageUtils .parseImageRef (args [i ]);
143+ imageRefs .add (imageRef );
144+ } catch (Exception e ) {
145+ throw new IllegalArgumentException (
146+ "Invalid image reference '" + args [i ] + "': " + e .getMessage (), e );
147+ }
148+ }
149+
150+ return new CliArgs (command , imageRefs , outputFormat );
151+ }
152+
95153 private static Command parseCommand (String commandStr ) {
96154 switch (commandStr ) {
97155 case "stack" :
98156 return Command .STACK ;
99157 case "component" :
100158 return Command .COMPONENT ;
159+ case "image" :
160+ return Command .IMAGE ;
101161 default :
102162 throw new IllegalArgumentException (
103- "Unknown command: " + commandStr + ". Use 'stack' or 'component '" );
163+ "Unknown command: " + commandStr + ". Use 'stack', 'component', or 'image '" );
104164 }
105165 }
106166
@@ -109,8 +169,9 @@ private static OutputFormat parseOutputFormat(Command command, String formatArg)
109169 case "--summary" :
110170 return OutputFormat .SUMMARY ;
111171 case "--html" :
112- if (command != Command .STACK ) {
113- throw new IllegalArgumentException ("HTML format is only supported for stack command" );
172+ if (command != Command .STACK && command != Command .IMAGE ) {
173+ throw new IllegalArgumentException (
174+ "HTML format is only supported for stack and image commands" );
114175 }
115176 return OutputFormat .HTML ;
116177 default :
@@ -137,6 +198,8 @@ private static CompletableFuture<String> executeCommand(CliArgs args) throws IOE
137198 case COMPONENT :
138199 return executeComponentAnalysis (
139200 args .filePath .toAbsolutePath ().toString (), args .outputFormat );
201+ case IMAGE :
202+ return executeImageAnalysis (args .imageRefs , args .outputFormat );
140203 default :
141204 throw new AssertionError ();
142205 }
@@ -178,6 +241,44 @@ private static String toJsonString(Object obj) {
178241 }
179242 }
180243
244+ private static CompletableFuture <String > executeImageAnalysis (
245+ Set <ImageRef > imageRefs , OutputFormat outputFormat ) throws IOException {
246+ Api api = new ExhortApi ();
247+ switch (outputFormat ) {
248+ case JSON :
249+ return api .imageAnalysis (imageRefs ).thenApply (App ::formatImageAnalysisResult );
250+ case HTML :
251+ return api .imageAnalysisHtml (imageRefs ).thenApply (bytes -> new String (bytes ));
252+ case SUMMARY :
253+ return api .imageAnalysis (imageRefs )
254+ .thenApply (App ::extractImageSummary )
255+ .thenApply (App ::toJsonString );
256+ default :
257+ throw new AssertionError ();
258+ }
259+ }
260+
261+ private static String formatImageAnalysisResult (Map <ImageRef , AnalysisReport > analysisResults ) {
262+ try {
263+ return MAPPER .writeValueAsString (analysisResults );
264+ } catch (JsonProcessingException e ) {
265+ throw new RuntimeException ("Failed to serialize image analysis results" , e );
266+ }
267+ }
268+
269+ private static Map <String , Map <String , SourceSummary >> extractImageSummary (
270+ Map <ImageRef , AnalysisReport > analysisResults ) {
271+ Map <String , Map <String , SourceSummary >> imageSummaries = new HashMap <>();
272+
273+ for (Map .Entry <ImageRef , AnalysisReport > entry : analysisResults .entrySet ()) {
274+ String imageKey = entry .getKey ().toString ();
275+ Map <String , SourceSummary > imageSummary = extractSummary (entry .getValue ());
276+ imageSummaries .put (imageKey , imageSummary );
277+ }
278+
279+ return imageSummaries ;
280+ }
281+
181282 private static Map <String , SourceSummary > extractSummary (AnalysisReport report ) {
182283 Map <String , SourceSummary > summary = new HashMap <>();
183284 if (report .getProviders () == null ) {
0 commit comments