13
13
import org .jdbi .v3 .core .statement .Query ;
14
14
import org .jdbi .v3 .sqlite3 .SQLitePlugin ;
15
15
import picocli .CommandLine .Command ;
16
+ import picocli .CommandLine .Help .Ansi ;
16
17
import picocli .CommandLine .Option ;
17
18
import picocli .CommandLine .Parameters ;
18
19
import us .springett .parsers .cpe .Cpe ;
23
24
import java .nio .file .Files ;
24
25
import java .nio .file .Path ;
25
26
import java .util .ArrayList ;
27
+ import java .util .Comparator ;
26
28
import java .util .HashMap ;
27
29
import java .util .HashSet ;
28
30
import java .util .List ;
31
+ import java .util .Map ;
29
32
import java .util .Set ;
33
+ import java .util .TreeMap ;
30
34
import java .util .concurrent .Callable ;
31
35
32
36
@ Command (name = "scan" , description = "Test a database by scanning BOMs." )
@@ -47,49 +51,65 @@ public Integer call() throws Exception {
47
51
.create ("jdbc:sqlite:%s" .formatted (databaseFilePath ))
48
52
.installPlugin (new SQLitePlugin ());
49
53
50
- if (ensureIndexes ) {
51
- jdbi .useHandle (handle -> {
52
- handle .execute ("""
53
- create index if not exists matching_criteria_purl_ns_idx
54
- on matching_criteria(purl_type, purl_namespace, purl_name)
55
- where purl_namespace is not null;
56
- """ );
57
-
58
- handle .execute ("""
59
- create index if not exists matching_criteria_purl_idx
60
- on matching_criteria(purl_type, purl_name)
61
- where purl_namespace is null;
62
- """ );
63
- });
64
- }
54
+ maybeCreateIndexes (jdbi );
65
55
66
56
final byte [] bomBytes = Files .readAllBytes (bomFilePath );
67
57
final Bom bom = BomParserFactory .createParser (bomBytes ).parse (bomBytes );
68
58
59
+ final var matchesByComponentByVulnId = new TreeMap <String , Map <Component , Set <MatchMetadata >>>();
60
+
69
61
try (final Handle handle = jdbi .open ()) {
70
62
// TODO: Consider metadata.component, nested components etc.
71
63
72
64
for (final Component component : bom .getComponents ()) {
73
65
final Set <MatchMetadata > matches = scan (handle , component );
74
- if (!matches .isEmpty ()) {
75
- String componentName = component .getName ();
76
- if (component .getGroup () != null ) {
77
- componentName = component .getGroup () + "/" + componentName ;
78
- }
79
- if (component .getVersion () != null ) {
80
- componentName = componentName + "@" + component .getVersion ();
81
- }
66
+ if (matches .isEmpty ()) {
67
+ continue ;
68
+ }
82
69
83
- // TODO: Move reporting to the very end.
84
- System .out .println (componentName + ":" );
85
- for (final MatchMetadata match : matches ) {
86
- System .out .println ("- %s\n Matched range: %s\n Source: %s" .formatted (
87
- match .vulnId (),
88
- match .criteriaVers (),
89
- match .criteriaSource ()));
90
- }
91
- System .out .println ();
70
+ for (final MatchMetadata match : matches ) {
71
+ final Map <Component , Set <MatchMetadata >> matchesByComponent =
72
+ matchesByComponentByVulnId .computeIfAbsent (
73
+ match .vulnId (), ignored -> new TreeMap <>(
74
+ Comparator .comparing (Component ::getName )
75
+ .thenComparing (Component ::getVersion )));
76
+
77
+ matchesByComponent .computeIfAbsent (
78
+ component , ignored -> new HashSet <>()).add (match );
79
+ }
80
+ }
81
+ }
82
+
83
+ if (matchesByComponentByVulnId .isEmpty ()) {
84
+ System .out .println ("@|bold,green no vulnerabilities identified|@" );
85
+ return 0 ;
86
+ }
87
+
88
+ for (final String vulnId : matchesByComponentByVulnId .keySet ()) {
89
+ final Map <Component , Set <MatchMetadata >> matchesByComponent = matchesByComponentByVulnId .get (vulnId );
90
+
91
+ System .out .println (Ansi .AUTO .string ("@|bold,red,underline %s|@" .formatted (vulnId )));
92
+
93
+ for (final Map .Entry <Component , Set <MatchMetadata >> entry : matchesByComponent .entrySet ()) {
94
+ final Component component = entry .getKey ();
95
+ final Set <MatchMetadata > matches = entry .getValue ();
96
+
97
+ String componentName = component .getName ();
98
+ if (component .getGroup () != null ) {
99
+ componentName = component .getGroup () + "/" + componentName ;
92
100
}
101
+ if (component .getVersion () != null ) {
102
+ componentName = componentName + "@" + component .getVersion ();
103
+ }
104
+
105
+ System .out .println ("- " + componentName );
106
+
107
+ for (final MatchMetadata match : matches ) {
108
+ System .out .println (Ansi .AUTO .string (" + matched: @|italic %s|@ (source: %s)" .formatted (
109
+ match .criteriaVers (), match .criteriaSource ())));
110
+ }
111
+
112
+ System .out .println ();
93
113
}
94
114
}
95
115
@@ -100,6 +120,26 @@ on matching_criteria(purl_type, purl_name)
100
120
return 0 ;
101
121
}
102
122
123
+ private void maybeCreateIndexes (final Jdbi jdbi ) {
124
+ if (!ensureIndexes ) {
125
+ return ;
126
+ }
127
+
128
+ jdbi .useHandle (handle -> {
129
+ handle .execute ("""
130
+ create index if not exists matching_criteria_purl_ns_idx
131
+ on matching_criteria(purl_type, purl_namespace, purl_name)
132
+ where purl_namespace is not null;
133
+ """ );
134
+
135
+ handle .execute ("""
136
+ create index if not exists matching_criteria_purl_idx
137
+ on matching_criteria(purl_type, purl_name)
138
+ where purl_namespace is null;
139
+ """ );
140
+ });
141
+ }
142
+
103
143
private record MatchMetadata (
104
144
String vulnId ,
105
145
String criteriaSource ,
0 commit comments