Skip to content

Commit c3db035

Browse files
committed
feat: adapto to inspector v2 results
1 parent f79e2d8 commit c3db035

3 files changed

Lines changed: 206 additions & 47 deletions

File tree

cli/cage/audit/scanner.go

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"strings"
78

9+
ecrtypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
810
smithy "github.com/aws/smithy-go"
911
"github.com/loilo-inc/canarycage/v5/awsiface"
1012
)
@@ -55,11 +57,7 @@ func scanImage(ctx context.Context, ecrTool EcrTool, info ImageInfo) ScanResult
5557
} else if findings, err := ecrTool.GetImageScanFindings(ctx, &info, imageID); err != nil {
5658
return ScanResult{ImageInfo: info, Err: parseError(err)}
5759
} else {
58-
var cves []CVE
59-
for _, f := range findings.Findings {
60-
cve := findingToCVE(f)
61-
cves = append(cves, cve)
62-
}
60+
cves := scanFindingsToCVEs(findings)
6361
return ScanResult{ImageInfo: info, Cves: cves}
6462
}
6563
}
@@ -71,3 +69,134 @@ func parseError(err error) error {
7169
}
7270
return err
7371
}
72+
73+
func scanFindingsToCVEs(findings *ecrtypes.ImageScanFindings) []CVE {
74+
var cves []CVE
75+
if findings.Findings != nil {
76+
for _, f := range findings.Findings {
77+
cves = append(cves, findingToCVE(f))
78+
}
79+
return cves
80+
} else if findings.EnhancedFindings != nil {
81+
for _, f := range findings.EnhancedFindings {
82+
cves = append(cves, enhancedFindingToCVE(f))
83+
}
84+
}
85+
return cves
86+
}
87+
88+
func findingToCVE(finding ecrtypes.ImageScanFinding) CVE {
89+
cve := CVE{
90+
Name: "unknown",
91+
PackageName: "unknown",
92+
PackageVersion: "unknown",
93+
Severity: finding.Severity,
94+
}
95+
if finding.Name != nil {
96+
cve.Name = *finding.Name
97+
}
98+
attrs := unwrapAttributes(finding.Attributes)
99+
if val, ok := attrs["package_name"]; ok {
100+
cve.PackageName = val
101+
}
102+
if val, ok := attrs["package_version"]; ok {
103+
cve.PackageVersion = val
104+
}
105+
if finding.Uri != nil {
106+
cve.Uri = *finding.Uri
107+
}
108+
if finding.Description != nil {
109+
cve.Description = *finding.Description
110+
}
111+
return cve
112+
}
113+
114+
func enhancedFindingToCVE(finding ecrtypes.EnhancedImageScanFinding) CVE {
115+
analysis := &EnchancedAnalysis{Score: finding.Score}
116+
cve := CVE{
117+
Name: "unknown",
118+
PackageName: "unknown",
119+
PackageVersion: "unknown",
120+
Severity: ecrtypes.FindingSeverityUndefined,
121+
EnchancedAnalysis: analysis,
122+
}
123+
if finding.Description != nil {
124+
cve.Description = *finding.Description
125+
}
126+
if finding.Severity != nil {
127+
cve.Severity = ecrtypes.FindingSeverity(*finding.Severity)
128+
}
129+
if finding.Status != nil {
130+
analysis.Status = *finding.Status
131+
}
132+
if finding.ExploitAvailable != nil {
133+
analysis.ExploitAvailable = *finding.ExploitAvailable
134+
}
135+
if finding.FixAvailable != nil {
136+
analysis.FixAvailable = *finding.FixAvailable
137+
}
138+
if finding.PackageVulnerabilityDetails == nil {
139+
return cve
140+
}
141+
details := finding.PackageVulnerabilityDetails
142+
if details.VulnerabilityId != nil {
143+
cve.Name = *details.VulnerabilityId
144+
}
145+
if cve.Severity == ecrtypes.FindingSeverityUndefined && details.VendorSeverity != nil {
146+
cve.Severity = ecrtypes.FindingSeverity(*details.VendorSeverity)
147+
}
148+
if details.SourceUrl != nil {
149+
cve.Uri = *details.SourceUrl
150+
} else if len(details.ReferenceUrls) > 0 {
151+
cve.Uri = details.ReferenceUrls[0]
152+
}
153+
cve.PackageName, cve.PackageVersion = vulnerablePackagesToNameVersion(details.VulnerablePackages)
154+
analysis.FixedInVersion = strings.Join(uniquePackageValues(details.VulnerablePackages, func(pkg ecrtypes.VulnerablePackage) *string {
155+
return pkg.FixedInVersion
156+
}), ", ")
157+
return cve
158+
}
159+
160+
func unwrapAttributes(attrs []ecrtypes.Attribute) map[string]string {
161+
m := make(map[string]string)
162+
for _, attr := range attrs {
163+
if attr.Key != nil && attr.Value != nil {
164+
m[*attr.Key] = *attr.Value
165+
}
166+
}
167+
return m
168+
}
169+
170+
func vulnerablePackagesToNameVersion(packages []ecrtypes.VulnerablePackage) (string, string) {
171+
names := uniquePackageValues(packages, func(pkg ecrtypes.VulnerablePackage) *string {
172+
return pkg.Name
173+
})
174+
versions := uniquePackageValues(packages, func(pkg ecrtypes.VulnerablePackage) *string {
175+
return pkg.Version
176+
})
177+
return joinOrUnknown(names), joinOrUnknown(versions)
178+
}
179+
180+
func uniquePackageValues(packages []ecrtypes.VulnerablePackage, value func(ecrtypes.VulnerablePackage) *string) []string {
181+
seen := make(map[string]struct{})
182+
var values []string
183+
for _, pkg := range packages {
184+
v := value(pkg)
185+
if v == nil || *v == "" {
186+
continue
187+
}
188+
if _, ok := seen[*v]; ok {
189+
continue
190+
}
191+
seen[*v] = struct{}{}
192+
values = append(values, *v)
193+
}
194+
return values
195+
}
196+
197+
func joinOrUnknown(values []string) string {
198+
if len(values) == 0 {
199+
return "unknown"
200+
}
201+
return strings.Join(values, ", ")
202+
}

cli/cage/audit/scanner_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,62 @@ func TestScanImage(t *testing.T) {
152152
assert.Len(t, result.Cves, 1)
153153
})
154154

155+
t.Run("returns scan result with CVEs from enhanced findings", func(t *testing.T) {
156+
imageID := &ecrtypes.ImageIdentifier{
157+
ImageTag: aws.String("test-tag"),
158+
}
159+
findings := &ecrtypes.ImageScanFindings{
160+
Findings: nil,
161+
EnhancedFindings: []ecrtypes.EnhancedImageScanFinding{
162+
{
163+
Description: aws.String("Enhanced vulnerability"),
164+
Severity: aws.String(string(ecrtypes.FindingSeverityHigh)),
165+
Status: aws.String("ACTIVE"),
166+
ExploitAvailable: aws.String("NO"),
167+
FixAvailable: aws.String("YES"),
168+
Score: 7.5,
169+
PackageVulnerabilityDetails: &ecrtypes.PackageVulnerabilityDetails{
170+
SourceUrl: aws.String("https://example.com/CVE-2026-1234"),
171+
VulnerabilityId: aws.String("CVE-2026-1234"),
172+
VulnerablePackages: []ecrtypes.VulnerablePackage{
173+
{
174+
Name: aws.String("openssl"),
175+
Version: aws.String("1.0.0"),
176+
FixedInVersion: aws.String("1.0.1"),
177+
},
178+
},
179+
},
180+
},
181+
},
182+
}
183+
stub := &stubEcrTool{
184+
imageID: imageID,
185+
findings: findings,
186+
}
187+
188+
result := scanImage(ctx, stub, imageInfo)
189+
190+
assert.NoError(t, result.Err)
191+
assert.Equal(t, imageInfo, result.ImageInfo)
192+
assert.Equal(t, []CVE{
193+
{
194+
Name: "CVE-2026-1234",
195+
Description: "Enhanced vulnerability",
196+
PackageName: "openssl",
197+
PackageVersion: "1.0.0",
198+
Uri: "https://example.com/CVE-2026-1234",
199+
Severity: ecrtypes.FindingSeverityHigh,
200+
EnchancedAnalysis: &EnchancedAnalysis{
201+
Status: "ACTIVE",
202+
ExploitAvailable: "NO",
203+
FixAvailable: "YES",
204+
FixedInVersion: "1.0.1",
205+
Score: 7.5,
206+
},
207+
},
208+
}, result.Cves)
209+
})
210+
155211
t.Run("returns error when GetActualImageIdentifier fails", func(t *testing.T) {
156212
expectedErr := errors.New("image identifier error")
157213
stub := &stubEcrTool{

cli/cage/audit/types.go

Lines changed: 16 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -93,42 +93,6 @@ type ScanResultSummary struct {
9393
ImageURI string `json:"image_uri"`
9494
}
9595

96-
func unwrapAttributes(attrs []ecrtypes.Attribute) map[string]string {
97-
m := make(map[string]string)
98-
for _, attr := range attrs {
99-
if attr.Key != nil && attr.Value != nil {
100-
m[*attr.Key] = *attr.Value
101-
}
102-
}
103-
return m
104-
}
105-
106-
func findingToCVE(finding ecrtypes.ImageScanFinding) CVE {
107-
cve := CVE{
108-
Name: "unknown",
109-
PackageName: "unknown",
110-
PackageVersion: "unknown",
111-
Severity: finding.Severity,
112-
}
113-
if finding.Name != nil {
114-
cve.Name = *finding.Name
115-
}
116-
attrs := unwrapAttributes(finding.Attributes)
117-
if val, ok := attrs["package_name"]; ok {
118-
cve.PackageName = val
119-
}
120-
if val, ok := attrs["package_version"]; ok {
121-
cve.PackageVersion = val
122-
}
123-
if finding.Uri != nil {
124-
cve.Uri = *finding.Uri
125-
}
126-
if finding.Description != nil {
127-
cve.Description = *finding.Description
128-
}
129-
return cve
130-
}
131-
13296
type FinalResult struct {
13397
Target
13498
Result
@@ -152,12 +116,22 @@ type Vuln struct {
152116
}
153117

154118
type CVE struct {
155-
Name string `json:"name"`
156-
Description string `json:"description"`
157-
PackageName string `json:"package_name"`
158-
PackageVersion string `json:"package_version"`
159-
Uri string `json:"uri"`
160-
Severity ecrtypes.FindingSeverity `json:"severity"`
119+
Name string `json:"name"`
120+
Description string `json:"description"`
121+
PackageName string `json:"package_name"`
122+
PackageVersion string `json:"package_version"`
123+
Uri string `json:"uri"`
124+
Severity ecrtypes.FindingSeverity `json:"severity"`
125+
EnchancedAnalysis *EnchancedAnalysis `json:"enchanced_analysis"`
126+
}
127+
128+
type EnchancedAnalysis struct {
129+
// Fields below are populated only from EnhancedImageScanFinding (Inspector v2).
130+
Status string `json:"status"`
131+
ExploitAvailable string `json:"exploit_available"`
132+
FixAvailable string `json:"fix_available"`
133+
FixedInVersion string `json:"fixed_in_version"`
134+
Score float64 `json:"score"`
161135
}
162136

163137
func (a *Result) CriticalCves() []Vuln {

0 commit comments

Comments
 (0)