fix(k8s): summary report when when only vulns exit (#2146)

* fix(k8s): summary report when when only vulns exit

Signed-off-by: Jose Donizetti <jdbjunior@gmail.com>

* fix(k8s): return error for not supported report

Signed-off-by: Jose Donizetti <jdbjunior@gmail.com>

* test(k8s): add tests for report Failed()

Signed-off-by: Jose Donizetti <jdbjunior@gmail.com>

* refactor: improve error message

Signed-off-by: Jose Donizetti <jdbjunior@gmail.com>
This commit is contained in:
Jose Donizetti
2022-05-19 10:00:37 -03:00
committed by GitHub
parent 6b95d3857f
commit 3ecc65d626
4 changed files with 211 additions and 19 deletions

View File

@@ -24,7 +24,7 @@ func (jw JSONWriter) Write(report Report) error {
case summaryReport:
output, err = json.MarshalIndent(report.consolidate(), "", " ")
default:
err = fmt.Errorf("report %s not supported", jw.Report)
return xerrors.Errorf(`report %q not supported. Use "summary" or "all"`, jw.Report)
}
if err != nil {

View File

@@ -3,7 +3,9 @@ package k8s
import (
"fmt"
"io"
"strings"
"golang.org/x/exp/maps"
"golang.org/x/xerrors"
ftypes "github.com/aquasecurity/fanal/types"
@@ -57,6 +59,10 @@ type Resource struct {
Report types.Report `json:"-"`
}
func (r Resource) fullname() string {
return strings.ToLower(fmt.Sprintf("%s/%s/%s", r.Namespace, r.Kind, r.Name))
}
// Failed returns whether the k8s report includes any vulnerabilities or misconfigurations
func (r Report) Failed() bool {
for _, r := range r.Vulnerabilities {
@@ -80,25 +86,32 @@ func (r Report) consolidate() ConsolidatedReport {
ClusterName: r.ClusterName,
}
index := make(map[string]Resource)
for _, m := range r.Misconfigurations {
found := false
for _, v := range r.Vulnerabilities {
if v.Kind == m.Kind && v.Name == m.Name && v.Namespace == m.Namespace {
consolidated.Findings = append(consolidated.Findings, Resource{
Namespace: v.Namespace,
Kind: v.Kind,
Name: v.Name,
Results: append(v.Results, m.Results...),
Error: v.Error,
})
found = true
continue
}
}
if !found {
consolidated.Findings = append(consolidated.Findings, m)
}
index[m.fullname()] = m
}
for _, v := range r.Vulnerabilities {
key := v.fullname()
if r, ok := index[key]; ok {
index[key] = Resource{
Namespace: r.Namespace,
Kind: r.Kind,
Name: r.Name,
Results: append(r.Results, v.Results...),
Error: r.Error,
}
continue
}
index[key] = v
}
consolidated.Findings = maps.Values(index)
return consolidated
}
@@ -120,7 +133,7 @@ func write(report Report, option Option) error {
Severities: option.Severities,
}
default:
return xerrors.Errorf("unknown format: %v", option.Format)
return xerrors.Errorf(`unknown format %q. Use "json" or "table"`, option.Format)
}
return writer.Write(report)

173
pkg/k8s/report_test.go Normal file
View File

@@ -0,0 +1,173 @@
package k8s
import (
"testing"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/stretchr/testify/assert"
)
var (
deployOrionWithMisconfigs = Resource{
Namespace: "default",
Kind: "Deploy",
Name: "orion",
Results: types.Results{
{Misconfigurations: []types.DetectedMisconfiguration{{ID: "ID100", Status: types.StatusFailure}}},
},
}
deployOrionWithVulns = Resource{
Namespace: "default",
Kind: "Deploy",
Name: "orion",
Results: types.Results{
{Vulnerabilities: []types.DetectedVulnerability{{VulnerabilityID: "CVE-2020-8888"}}},
},
}
deployOrionWithBothVulnsAndMisconfigs = Resource{
Namespace: "default",
Kind: "Deploy",
Name: "orion",
Results: types.Results{
{Misconfigurations: []types.DetectedMisconfiguration{{ID: "ID100", Status: types.StatusFailure}}},
{Vulnerabilities: []types.DetectedVulnerability{{VulnerabilityID: "CVE-2020-8888"}}},
},
}
cronjobHelloWithVulns = Resource{
Namespace: "default",
Kind: "Cronjob",
Name: "hello",
Results: types.Results{
{Vulnerabilities: []types.DetectedVulnerability{{VulnerabilityID: "CVE-2020-9999"}}},
},
}
podPrometheusWithMisconfigs = Resource{
Namespace: "default",
Kind: "Pod",
Name: "prometheus",
Results: types.Results{
{Misconfigurations: []types.DetectedMisconfiguration{{ID: "ID100"}}},
},
}
)
func TestReport_consolidate(t *testing.T) {
tests := []struct {
name string
report Report
expectedFindings map[string]Resource
}{
{
name: "report with both misconfigs and vulnerabilities",
report: Report{
Vulnerabilities: []Resource{deployOrionWithVulns, cronjobHelloWithVulns},
Misconfigurations: []Resource{deployOrionWithMisconfigs, podPrometheusWithMisconfigs},
},
expectedFindings: map[string]Resource{
"default/deploy/orion": deployOrionWithBothVulnsAndMisconfigs,
"default/cronjob/hello": cronjobHelloWithVulns,
"default/pod/prometheus": podPrometheusWithMisconfigs,
},
},
{
name: "report with only misconfigurations",
report: Report{
Misconfigurations: []Resource{deployOrionWithMisconfigs, podPrometheusWithMisconfigs},
},
expectedFindings: map[string]Resource{
"default/deploy/orion": deployOrionWithMisconfigs,
"default/pod/prometheus": podPrometheusWithMisconfigs,
},
},
{
name: "report with only vulnerabilities",
report: Report{
Vulnerabilities: []Resource{deployOrionWithVulns, cronjobHelloWithVulns},
},
expectedFindings: map[string]Resource{
"default/deploy/orion": deployOrionWithVulns,
"default/cronjob/hello": cronjobHelloWithVulns,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
consolidateReport := tt.report.consolidate()
for _, f := range consolidateReport.Findings {
key := f.fullname()
expected, found := tt.expectedFindings[key]
if !found {
t.Errorf("key not found: %s", key)
}
assert.Equal(t, expected, f)
}
})
}
}
func TestResource_fullname(t *testing.T) {
tests := []struct {
expected string
resource Resource
}{
{"default/deploy/orion", deployOrionWithBothVulnsAndMisconfigs},
{"default/deploy/orion", deployOrionWithMisconfigs},
{"default/cronjob/hello", cronjobHelloWithVulns},
{"default/pod/prometheus", podPrometheusWithMisconfigs},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.resource.fullname())
})
}
}
func TestResourceFailed(t *testing.T) {
tests := []struct {
name string
report Report
expected bool
}{
{
name: "report with both misconfigs and vulnerabilities",
report: Report{
Vulnerabilities: []Resource{deployOrionWithVulns, cronjobHelloWithVulns},
Misconfigurations: []Resource{deployOrionWithMisconfigs, podPrometheusWithMisconfigs},
},
expected: true,
},
{
name: "report with only misconfigurations",
report: Report{
Misconfigurations: []Resource{deployOrionWithMisconfigs, podPrometheusWithMisconfigs},
},
expected: true,
},
{
name: "report with only vulnerabilities",
report: Report{
Vulnerabilities: []Resource{deployOrionWithVulns, cronjobHelloWithVulns},
},
expected: true,
},
{
name: "report without vulnerabilities and misconfigurations",
report: Report{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.report.Failed())
})
}
}

View File

@@ -4,6 +4,9 @@ import (
"io"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"golang.org/x/xerrors"
pkgReport "github.com/aquasecurity/trivy/pkg/report"
)
@@ -36,6 +39,9 @@ func (tw TableWriter) Write(report Report) error {
case summaryReport:
writer := NewSummaryWriter(tw.Output, tw.Severities)
return writer.Write(report)
default:
return xerrors.Errorf(`report %q not supported. Use "summary" or "all"`, tw.Report)
}
return nil
}