refactor(k8s): custom reports (#3076)

This commit is contained in:
Teppei Fukuda
2022-10-26 00:02:33 +03:00
committed by GitHub
parent f4e970f374
commit af89249dea
41 changed files with 1052 additions and 1791 deletions

2
go.mod
View File

@@ -355,7 +355,7 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/neurosnap/sentences.v1 v1.0.6 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools v2.2.0+incompatible
gotest.tools/v3 v3.2.0 // indirect
helm.sh/helm/v3 v3.10.0 // indirect

View File

@@ -18,15 +18,17 @@ func (jw JSONWriter) Write(report *ComplianceReport) error {
var output []byte
var err error
var v interface{}
switch jw.Report {
case allReport:
output, err = json.MarshalIndent(report, "", " ")
v = report
case summaryReport:
output, err = json.MarshalIndent(BuildSummary(report), "", " ")
v = BuildSummary(report)
default:
return xerrors.Errorf(`report %q not supported. Use "summary" or "all"`, jw.Report)
}
output, err = json.MarshalIndent(v, "", " ")
if err != nil {
return xerrors.Errorf("failed to marshal json: %w", err)
}

View File

@@ -1,46 +1,82 @@
package report
package report_test
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/pkg/compliance/report"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/stretchr/testify/assert"
"io"
"os"
"testing"
)
func TestJSONReport(t *testing.T) {
func TestJSONWriter_Write(t *testing.T) {
input := &report.ComplianceReport{
ID: "1234",
Title: "NSA",
RelatedResources: []string{"https://example.com"},
Results: []*report.ControlCheckResult{
{
ID: "1.0",
Name: "Non-root containers",
Severity: "MEDIUM",
Results: types.Results{
{
Misconfigurations: []types.DetectedMisconfiguration{
{AVDID: "AVD-KSV012", Status: types.StatusFailure},
},
},
},
},
{
ID: "1.1",
Name: "Immutable container file systems",
Severity: "LOW",
Results: types.Results{
{
Misconfigurations: []types.DetectedMisconfiguration{
{AVDID: "AVD-KSV013", Status: types.StatusFailure},
},
},
},
},
},
}
tests := []struct {
name string
specPath string
resultPath string
reportType string
wantJsonReportPath string
name string
reportType string
input *report.ComplianceReport
want string
}{
{name: "build summary json output report", specPath: "./testdata/config_spec.yaml", reportType: "summary", resultPath: "./testdata/results_config.json", wantJsonReportPath: "./testdata/json_summary.json"},
{name: "build full json output report", specPath: "./testdata/config_spec.yaml", reportType: "all", resultPath: "./testdata/results_config.json", wantJsonReportPath: "./testdata/json_view.json"},
{
name: "build summary json output report",
reportType: "summary",
input: input,
want: filepath.Join("testdata", "summary.json"),
},
{
name: "build full json output report",
reportType: "all",
input: input,
want: filepath.Join("testdata", "all.json"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
specfile, err := os.ReadFile(tt.specPath)
assert.NoError(t, err)
var res types.Results
resultByte, err := os.ReadFile(tt.resultPath)
err = json.Unmarshal(resultByte, &res)
assert.NoError(t, err)
complianceResults, err := BuildComplianceReport([]types.Results{res}, string(specfile))
assert.NoError(t, err)
ioWriter := new(bytes.Buffer)
tr := JSONWriter{Report: tt.reportType, Output: ioWriter}
err = tr.Write(complianceResults)
assert.NoError(t, err)
bt, err := io.ReadAll(ioWriter)
assert.NoError(t, err)
r, err := os.ReadFile(tt.wantJsonReportPath)
assert.NoError(t, err)
assert.Equal(t, string(bt), string(r))
buf := new(bytes.Buffer)
tr := report.JSONWriter{Report: tt.reportType, Output: buf}
err := tr.Write(tt.input)
require.NoError(t, err)
want, err := os.ReadFile(tt.want)
require.NoError(t, err)
assert.JSONEq(t, string(want), buf.String())
})
}
}

View File

@@ -4,7 +4,6 @@ import (
"io"
"golang.org/x/xerrors"
"gopkg.in/yaml.v2"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/compliance/spec"
@@ -29,37 +28,37 @@ type Option struct {
// ComplianceReport represents a kubernetes scan report
type ComplianceReport struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Version string `json:"severity"`
RelatedResources []string `json:"relatedResources"`
Results []*ControlCheckResult `json:"results"`
ID string
Title string
Description string
Version string
RelatedResources []string
Results []*ControlCheckResult
}
type ControlCheckResult struct {
ControlCheckID string `json:"id"`
ControlName string `json:"name"`
ControlDescription string `json:"description"`
DefaultStatus spec.ControlStatus `json:"defaultStatus,omitempty"`
ControlSeverity string `json:"severity"`
Results types.Results `json:"results"`
ID string
Name string
Description string
DefaultStatus spec.ControlStatus `json:",omitempty"`
Severity string
Results types.Results
}
// ConsolidatedReport represents a kubernetes scan report with consolidated findings
// SummaryReport represents a kubernetes scan report with consolidated findings
type SummaryReport struct {
SchemaVersion int `json:",omitempty"`
ReportID string
ReportTitle string
ID string
Title string
SummaryControls []ControlCheckSummary `json:",omitempty"`
}
type ControlCheckSummary struct {
ControlCheckID string `json:"id"`
ControlName string `json:"name"`
ControlSeverity string `json:"severity"`
TotalPass float32 `json:"totalPass"`
TotalFail float32 `json:"totalFail"`
ID string
Name string
Severity string
TotalPass float32
TotalFail float32
}
// Writer defines the result write operation
@@ -99,16 +98,18 @@ func (r ComplianceReport) empty() bool {
func buildControlCheckResults(checksMap map[string]types.Results, controls []spec.Control) []*ControlCheckResult {
complianceResults := make([]*ControlCheckResult, 0)
for _, control := range controls {
cr := ControlCheckResult{}
cr.ControlName = control.Name
cr.ControlCheckID = control.ID
cr.ControlDescription = control.Description
cr.ControlSeverity = string(control.Severity)
cr.DefaultStatus = control.DefaultStatus
var results types.Results
for _, c := range control.Checks {
cr.Results = append(cr.Results, checksMap[c.ID]...)
results = append(results, checksMap[c.ID]...)
}
complianceResults = append(complianceResults, &cr)
complianceResults = append(complianceResults, &ControlCheckResult{
Name: control.Name,
ID: control.ID,
Description: control.Description,
Severity: string(control.Severity),
DefaultStatus: control.DefaultStatus,
Results: results,
})
}
return complianceResults
}
@@ -126,20 +127,9 @@ func buildComplianceReportResults(checksMap map[string]types.Results, spec spec.
}
}
func BuildComplianceReport(scanResults []types.Results, complianceSpec string) (*ComplianceReport, error) {
// load compliance spec
cs := spec.ComplianceSpec{}
err := yaml.Unmarshal([]byte(complianceSpec), &cs)
if err != nil {
return nil, err
}
// validate scanners types (vuln and config) define in spec supported
err = spec.ValidateScanners(cs.Spec.Controls)
if err != nil {
return nil, err
}
func BuildComplianceReport(scanResults []types.Results, cs spec.ComplianceSpec) (*ComplianceReport, error) {
// aggregate checks by ID
aggregateChecksByID := spec.AggregateAllChecksBySpecID(scanResults, cs.Spec.Controls)
aggregateChecksByID := spec.AggregateAllChecksBySpecID(scanResults, cs)
// build compliance report results
return buildComplianceReportResults(aggregateChecksByID, cs.Spec), nil

View File

@@ -1,84 +1,242 @@
package report
package report_test
import (
"bytes"
"encoding/json"
"io"
"os"
"reflect"
"fmt"
"testing"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/stretchr/testify/assert"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
"github.com/aquasecurity/trivy/pkg/compliance/report"
"github.com/aquasecurity/trivy/pkg/compliance/spec"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/types"
)
func TestReport(t *testing.T) {
func TestBuildComplianceReport(t *testing.T) {
type args struct {
scanResults []types.Results
cs spec.ComplianceSpec
}
tests := []struct {
name string
specPath string
resultPath string
Option Option
wantSummaryReportPath string
expectError bool
name string
args args
want *report.ComplianceReport
wantErr assert.ErrorAssertionFunc
}{
{name: "build table report summary", Option: Option{Report: "summary", Format: "table"}, specPath: "./testdata/config_spec.yaml", resultPath: "./testdata/results_config.json", wantSummaryReportPath: "./testdata/table_summary.txt"},
{name: "build table report full", Option: Option{Report: "all", Format: "table"}, specPath: "./testdata/config_spec.yaml", resultPath: "./testdata/results_config.json", wantSummaryReportPath: "./testdata/table.txt"},
{name: "build json report summary", Option: Option{Report: "summary", Format: "json"}, specPath: "./testdata/config_spec.yaml", resultPath: "./testdata/results_config.json", wantSummaryReportPath: "./testdata/json_summary.json"},
{name: "build json report full", Option: Option{Report: "all", Format: "json"}, specPath: "./testdata/config_spec.yaml", resultPath: "./testdata/results_config.json", wantSummaryReportPath: "./testdata/json_view.json"},
{name: "build report bad format", Option: Option{Report: "all", Format: "aaa"}, specPath: "./testdata/config_spec.yaml", resultPath: "./testdata/results_config.json", wantSummaryReportPath: "./testdata/json_view.json", expectError: true},
{
name: "happy",
args: args{
scanResults: []types.Results{
{
{
Target: "Deployment/metrics-server",
Class: types.ClassConfig,
Type: ftypes.Kubernetes,
MisconfSummary: &types.MisconfSummary{
Successes: 1,
Failures: 0,
Exceptions: 0,
},
Misconfigurations: []types.DetectedMisconfiguration{
{
Type: "Kubernetes Security Check",
ID: "KSV001",
AVDID: "AVD-KSV-0001",
Title: "Process can elevate its own privileges",
Description: "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.",
Message: "Container 'metrics-server' of Deployment 'metrics-server' should set 'securityContext.allowPrivilegeEscalation' to false",
Namespace: "builtin.kubernetes.KSV001",
Query: "data.builtin.kubernetes.KSV001.deny",
Resolution: "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.",
Severity: dbTypes.SeverityMedium.String(),
PrimaryURL: "https://avd.aquasec.com/misconfig/ksv001",
References: []string{
"https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted",
"https://avd.aquasec.com/misconfig/ksv001",
},
Status: types.StatusPassed,
},
{
Type: "Kubernetes Security Check",
ID: "KSV002",
AVDID: "AVD-KSV-9999",
Status: types.StatusFailure,
},
},
},
},
{
{
Target: "rancher/metrics-server:v0.3.6 (debian 9.9)",
Class: types.ClassOSPkg,
Type: "debian",
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "DLA-2424-1",
VendorIDs: []string{"DLA-2424-1"},
PkgName: "tzdata",
InstalledVersion: "2019a-0+deb9u1",
FixedVersion: "2020d-0+deb9u1",
Layer: ftypes.Layer{
DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02",
},
DataSource: &dbTypes.DataSource{
ID: vulnerability.Debian,
Name: "Debian Security Tracker",
URL: "https://salsa.debian.org/security-tracker-team/security-tracker",
},
Vulnerability: dbTypes.Vulnerability{
Title: "tzdata - new upstream version",
Severity: dbTypes.SeverityUnknown.String(),
},
},
},
},
},
},
cs: spec.ComplianceSpec{
Spec: spec.Spec{
ID: "1234",
Title: "NSA",
Description: "National Security Agency - Kubernetes Hardening Guidance",
Version: "1.0",
RelatedResources: []string{
"https://example.com",
},
Controls: []spec.Control{
{
ID: "1.0",
Name: "Non-root containers",
Description: "Check that container is not running as root",
Severity: "MEDIUM",
Checks: []spec.SpecCheck{
{ID: "AVD-KSV-0001"},
},
},
{
ID: "1.1",
Name: "Immutable container file systems",
Description: "Check that container root file system is immutable",
Severity: "LOW",
Checks: []spec.SpecCheck{
{ID: "AVD-KSV-0002"},
},
},
{
ID: "1.2",
Name: "tzdata - new upstream version",
Description: "Bad tzdata package",
Severity: "CRITICAL",
Checks: []spec.SpecCheck{
{ID: "DLA-2424-1"},
},
},
},
},
},
},
want: &report.ComplianceReport{
ID: "1234",
Title: "NSA",
Description: "National Security Agency - Kubernetes Hardening Guidance",
Version: "1.0",
RelatedResources: []string{
"https://example.com",
},
Results: []*report.ControlCheckResult{
{
ID: "1.0",
Name: "Non-root containers",
Description: "Check that container is not running as root",
Severity: "MEDIUM",
Results: types.Results{
{
Target: "Deployment/metrics-server",
Class: types.ClassConfig,
Type: ftypes.Kubernetes,
MisconfSummary: &types.MisconfSummary{
Successes: 1,
Failures: 0,
Exceptions: 0,
},
Misconfigurations: []types.DetectedMisconfiguration{
{
Type: "Kubernetes Security Check",
ID: "KSV001",
AVDID: "AVD-KSV-0001",
Title: "Process can elevate its own privileges",
Description: "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.",
Message: "Container 'metrics-server' of Deployment 'metrics-server' should set 'securityContext.allowPrivilegeEscalation' to false",
Namespace: "builtin.kubernetes.KSV001",
Query: "data.builtin.kubernetes.KSV001.deny",
Resolution: "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.",
Severity: dbTypes.SeverityMedium.String(),
PrimaryURL: "https://avd.aquasec.com/misconfig/ksv001",
References: []string{
"https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted",
"https://avd.aquasec.com/misconfig/ksv001",
},
Status: types.StatusPassed,
},
},
},
},
},
{
ID: "1.1",
Name: "Immutable container file systems",
Description: "Check that container root file system is immutable",
Severity: "LOW",
Results: nil,
},
{
ID: "1.2",
Name: "tzdata - new upstream version",
Description: "Bad tzdata package",
Severity: "CRITICAL",
Results: types.Results{
{
Target: "rancher/metrics-server:v0.3.6 (debian 9.9)",
Class: types.ClassOSPkg,
Type: "debian",
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "DLA-2424-1",
VendorIDs: []string{"DLA-2424-1"},
PkgName: "tzdata",
InstalledVersion: "2019a-0+deb9u1",
FixedVersion: "2020d-0+deb9u1",
Layer: ftypes.Layer{
DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02",
},
DataSource: &dbTypes.DataSource{
ID: vulnerability.Debian,
Name: "Debian Security Tracker",
URL: "https://salsa.debian.org/security-tracker-team/security-tracker",
},
Vulnerability: dbTypes.Vulnerability{
Title: "tzdata - new upstream version",
Severity: dbTypes.SeverityUnknown.String(),
},
},
},
},
},
},
},
},
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
specfile, err := os.ReadFile(tt.specPath)
assert.NoError(t, err)
var res types.Results
resultByte, err := os.ReadFile(tt.resultPath)
err = json.Unmarshal(resultByte, &res)
assert.NoError(t, err)
complianceResults, err := BuildComplianceReport([]types.Results{res}, string(specfile))
ioWriter := new(bytes.Buffer)
tt.Option.Output = ioWriter
err = Write(complianceResults, tt.Option)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
bt, err := io.ReadAll(ioWriter)
assert.NoError(t, err)
r, err := os.ReadFile(tt.wantSummaryReportPath)
assert.NoError(t, err)
assert.Equal(t, string(bt), string(r))
got, err := report.BuildComplianceReport(tt.args.scanResults, tt.args.cs)
if !tt.wantErr(t, err, fmt.Sprintf("BuildComplianceReport(%v, %v)", tt.args.scanResults, tt.args.cs)) {
return
}
})
}
}
func TestBuildComplianceReportResults(t *testing.T) {
tests := []struct {
name string
specPath string
resultPath string
complianceReportPath string
}{
{name: "build report test config and vuln", specPath: "./testdata/config_vuln_spec.yaml", resultPath: "./testdata/results_vul_config.json", complianceReportPath: "./testdata/vuln_config_compliance.json"}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
specFile, err := os.ReadFile(tt.specPath)
assert.NoError(t, err)
var res types.Results
c, err := os.ReadFile(tt.resultPath)
err = json.Unmarshal(c, &res)
assert.NoError(t, err)
pp, err := BuildComplianceReport([]types.Results{res}, string(specFile))
assert.NoError(t, err)
complianceReport, err := os.ReadFile(tt.complianceReportPath)
assert.NoError(t, err)
var cp ComplianceReport
err = json.Unmarshal(complianceReport, &cp)
assert.NoError(t, err)
assert.True(t, reflect.DeepEqual(&cp, pp))
assert.Equalf(t, tt.want, got, "BuildComplianceReport(%v, %v)", tt.args.scanResults, tt.args.cs)
})
}
}

View File

@@ -11,12 +11,17 @@ import (
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/compliance/spec"
pkgReport "github.com/aquasecurity/trivy/pkg/report/table"
"github.com/aquasecurity/trivy/pkg/types"
)
func BuildSummary(cr *ComplianceReport) *SummaryReport {
ccma := make([]ControlCheckSummary, 0)
var ccma []ControlCheckSummary
for _, control := range cr.Results {
ccm := ControlCheckSummary{ControlCheckID: control.ControlCheckID, ControlName: control.ControlName, ControlSeverity: control.ControlSeverity}
ccm := ControlCheckSummary{
ID: control.ID,
Name: control.Name,
Severity: control.Severity,
}
if len(control.Results) == 0 { // this validation is mainly for vuln type
if control.DefaultStatus == spec.PassStatus {
ccm.TotalPass = 1
@@ -25,27 +30,23 @@ func BuildSummary(cr *ComplianceReport) *SummaryReport {
continue
}
for _, check := range control.Results {
for _, cr := range check.Misconfigurations {
if cr.CheckPass() {
ccm.TotalPass++
continue
}
ccm.TotalFail++
}
for _, cr := range check.Vulnerabilities {
if cr.CheckPass() {
for _, m := range check.Misconfigurations {
if m.Status == types.StatusPassed {
ccm.TotalPass++
continue
}
ccm.TotalFail++
}
// Detected vulnerabilities are always failure.
ccm.TotalFail += float32(len(check.Vulnerabilities))
}
ccma = append(ccma, ccm)
}
return &SummaryReport{ReportID: cr.ID,
ReportTitle: cr.Title,
SummaryControls: ccma}
return &SummaryReport{
ID: cr.ID,
Title: cr.Title,
SummaryControls: ccma,
}
}
type SummaryWriter struct {
@@ -69,7 +70,6 @@ func NewSummaryWriter(output io.Writer, requiredSevs []dbTypes.Severity, columnH
// Write writes the results in a summarized table format
func (s SummaryWriter) Write(report *ComplianceReport) error {
if _, err := fmt.Fprintln(s.Output); err != nil {
return xerrors.Errorf("failed to write summary report: %w", err)
}
@@ -102,7 +102,7 @@ func (s SummaryWriter) Write(report *ComplianceReport) error {
func (s SummaryWriter) generateSummary(summaryControls ControlCheckSummary) []string {
percentage := calculatePercentage(summaryControls.TotalFail, summaryControls.TotalPass)
return []string{summaryControls.ControlCheckID, summaryControls.ControlSeverity, summaryControls.ControlName, percentage}
return []string{summaryControls.ID, summaryControls.Severity, summaryControls.Name, percentage}
}
func calculatePercentage(totalFail float32, totalPass float32) string {

View File

@@ -1,62 +1,158 @@
package report
package report_test
import (
"encoding/json"
"os"
"strings"
"testing"
"github.com/aquasecurity/trivy/pkg/compliance/report"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/stretchr/testify/assert"
)
func TestBuildSummary(t *testing.T) {
tests := []struct {
name string
specPath string
resultPath string
wantSummaryReportPath string
name string
reportType string
input *report.ComplianceReport
want *report.SummaryReport
}{
{name: "build report summary config only", specPath: "./testdata/config_spec.yaml", resultPath: "./testdata/results_config.json", wantSummaryReportPath: "./testdata/report_summary.json"},
{name: "build report summary config and vuln", specPath: "./testdata/config_vuln_spec.yaml", resultPath: "./testdata/results_vul_config.json", wantSummaryReportPath: "./testdata/vuln_config_summary.json"}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
specfile, err := os.ReadFile(tt.specPath)
assert.NoError(t, err)
var res types.Results
c, err := os.ReadFile(tt.resultPath)
err = json.Unmarshal(c, &res)
assert.NoError(t, err)
complianceResults, err := BuildComplianceReport([]types.Results{res}, string(specfile))
tk := BuildSummary(complianceResults)
o, err := json.Marshal(tk)
assert.NoError(t, err)
r, err := os.ReadFile(tt.wantSummaryReportPath)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(string(o)), string(r))
})
}
}
func TestCalculatePercentage(t *testing.T) {
tests := []struct {
name string
pass float32
fail float32
want string
}{
{name: "calcuale percentage pass bigger then fail", pass: 10.0, fail: 5.0, want: "66.67%"},
{name: "calcuale percentage pass smaller then fail", pass: 5.0, fail: 10.0, want: "33.33%"},
{name: "calcuale percentage pass zero and fail zero", pass: 0.0, fail: 0.0, want: "0.00%"},
{
name: "build report summary config only",
reportType: "summary",
input: &report.ComplianceReport{
ID: "1234",
Title: "NSA",
RelatedResources: []string{"https://example.com"},
Results: []*report.ControlCheckResult{
{
ID: "1.0",
Name: "Non-root containers",
Severity: "MEDIUM",
Results: types.Results{
{
Misconfigurations: []types.DetectedMisconfiguration{
{AVDID: "AVD-KSV012", Status: types.StatusFailure},
},
},
},
},
{
ID: "1.1",
Name: "Immutable container file systems",
Severity: "LOW",
Results: types.Results{
{
Misconfigurations: []types.DetectedMisconfiguration{
{AVDID: "AVD-KSV013", Status: types.StatusFailure},
},
},
},
},
},
},
want: &report.SummaryReport{
SchemaVersion: 0,
ID: "1234",
Title: "NSA",
SummaryControls: []report.ControlCheckSummary{
{
ID: "1.0",
Name: "Non-root containers",
Severity: "MEDIUM",
TotalPass: 0,
TotalFail: 1,
},
{
ID: "1.1",
Name: "Immutable container file systems",
Severity: "LOW",
TotalPass: 0,
TotalFail: 1,
},
},
},
},
{
name: "build full json output report",
reportType: "all",
input: &report.ComplianceReport{
ID: "1234",
Title: "NSA",
RelatedResources: []string{"https://example.com"},
Results: []*report.ControlCheckResult{
{
ID: "1.0",
Name: "Non-root containers",
Severity: "MEDIUM",
Results: types.Results{
{
Misconfigurations: []types.DetectedMisconfiguration{
{AVDID: "AVD-KSV012", Status: types.StatusFailure},
},
},
},
},
{
ID: "1.1",
Name: "Immutable container file systems",
Severity: "LOW",
Results: types.Results{
{
Misconfigurations: []types.DetectedMisconfiguration{
{AVDID: "AVD-KSV013", Status: types.StatusFailure},
},
},
},
},
{
ID: "1.2",
Name: "tzdata - new upstream version",
Severity: "LOW",
Results: types.Results{
{
Vulnerabilities: []types.DetectedVulnerability{
{VulnerabilityID: "CVE-9999-0001"},
{VulnerabilityID: "CVE-9999-0002"},
},
},
},
},
},
},
want: &report.SummaryReport{
SchemaVersion: 0,
ID: "1234",
Title: "NSA",
SummaryControls: []report.ControlCheckSummary{
{
ID: "1.0",
Name: "Non-root containers",
Severity: "MEDIUM",
TotalPass: 0,
TotalFail: 1,
},
{
ID: "1.1",
Name: "Immutable container file systems",
Severity: "LOW",
TotalPass: 0,
TotalFail: 1,
},
{
ID: "1.2",
Name: "tzdata - new upstream version",
Severity: "LOW",
TotalPass: 0,
TotalFail: 2,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := calculatePercentage(tt.fail, tt.pass)
assert.Equal(t, got, tt.want)
got := report.BuildSummary(tt.input)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -25,7 +25,7 @@ const (
ComplianceColumn = "Compliance"
)
func (tw TableWriter) Columns() []string {
func (tw TableWriter) columns() []string {
return []string{ControlIDColumn, SeverityColumn, ControlNameColumn, ComplianceColumn}
}
@@ -41,7 +41,7 @@ func (tw TableWriter) Write(report *ComplianceReport) error {
}
}
case summaryReport:
writer := NewSummaryWriter(tw.Output, tw.Severities, tw.Columns())
writer := NewSummaryWriter(tw.Output, tw.Severities, tw.columns())
return writer.Write(report)
default:
return xerrors.Errorf(`report %q not supported. Use "summary" or "all"`, tw.Report)

View File

@@ -1,52 +1,75 @@
package report
package report_test
import (
"bytes"
"encoding/json"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/stretchr/testify/assert"
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/pkg/compliance/report"
"github.com/aquasecurity/trivy/pkg/types"
)
func TestTableReport(t *testing.T) {
func TestTableWriter_Write(t *testing.T) {
tests := []struct {
name string
specPath string
resultPath string
reportType string
wantSummaryReportPath string
name string
reportType string
input *report.ComplianceReport
want string
}{
{name: "build table report summary config only", specPath: "./testdata/config_spec.yaml", reportType: "summary", resultPath: "./testdata/results_config.json", wantSummaryReportPath: "./testdata/table_summary.txt"},
{name: "build table report summary config and vuln", specPath: "./testdata/config_vuln_spec.yaml", reportType: "summary", resultPath: "./testdata/results_vul_config.json", wantSummaryReportPath: "./testdata/vuln_conf_table_summary.txt"},
{name: "build table report config only", specPath: "./testdata/config_spec.yaml", reportType: "all", resultPath: "./testdata/results_config.json", wantSummaryReportPath: "./testdata/table.txt"},
{name: "build table report config and vuln", specPath: "./testdata/config_vuln_spec.yaml", reportType: "all", resultPath: "./testdata/results_vul_config.json", wantSummaryReportPath: "./testdata/vuln_conf_table.txt"},
{
name: "build summary table",
reportType: "summary",
input: &report.ComplianceReport{
ID: "1234",
Title: "NSA",
RelatedResources: []string{"https://example.com"},
Results: []*report.ControlCheckResult{
{
ID: "1.0",
Name: "Non-root containers",
Severity: "MEDIUM",
Results: types.Results{
{
Misconfigurations: []types.DetectedMisconfiguration{
{AVDID: "AVD-KSV012", Status: types.StatusFailure},
},
},
},
},
{
ID: "1.1",
Name: "Immutable container file systems",
Severity: "LOW",
Results: types.Results{
{
Misconfigurations: []types.DetectedMisconfiguration{
{AVDID: "AVD-KSV013", Status: types.StatusFailure},
},
},
},
},
},
},
want: filepath.Join("testdata", "table_summary.txt"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
specfile, err := os.ReadFile(tt.specPath)
assert.NoError(t, err)
var res types.Results
resultByte, err := os.ReadFile(tt.resultPath)
err = json.Unmarshal(resultByte, &res)
assert.NoError(t, err)
complianceResults, err := BuildComplianceReport([]types.Results{res}, string(specfile))
ioWriter := new(bytes.Buffer)
tr := TableWriter{Report: tt.reportType, Output: ioWriter}
err = tr.Write(complianceResults)
assert.NoError(t, err)
bt, err := io.ReadAll(ioWriter)
assert.NoError(t, err)
r, err := os.ReadFile(tt.wantSummaryReportPath)
assert.NoError(t, err)
assert.Equal(t, string(bt), string(r))
buf := new(bytes.Buffer)
tr := report.TableWriter{Report: tt.reportType, Output: buf}
err := tr.Write(tt.input)
require.NoError(t, err)
want, err := os.ReadFile(tt.want)
require.NoError(t, err)
assert.Equal(t, string(want), buf.String())
})
}
}
func TestColumn(t *testing.T) {
tr := TableWriter{}
assert.Equal(t, tr.Columns(), []string{ControlIDColumn, SeverityColumn, ControlNameColumn, ComplianceColumn})
}

57
pkg/compliance/report/testdata/all.json vendored Normal file
View File

@@ -0,0 +1,57 @@
{
"ID": "1234",
"Title": "NSA",
"Description": "",
"Version": "",
"RelatedResources": [
"https://example.com"
],
"Results": [
{
"ID": "1.0",
"Name": "Non-root containers",
"Description": "",
"Severity": "MEDIUM",
"Results": [
{
"Target": "",
"Misconfigurations": [
{
"AVDID": "AVD-KSV012",
"Status": "FAIL",
"Layer": {},
"CauseMetadata": {
"Code": {
"Lines": null
}
}
}
]
}
]
},
{
"ID": "1.1",
"Name": "Immutable container file systems",
"Description": "",
"Severity": "LOW",
"Results": [
{
"Target": "",
"Misconfigurations": [
{
"AVDID": "AVD-KSV013",
"Status": "FAIL",
"Layer": {},
"CauseMetadata": {
"Code": {
"Lines": null
}
}
}
]
}
]
}
]
}

View File

@@ -1,23 +0,0 @@
---
spec:
id: "1234"
title: nsa
description: National Security Agency - Kubernetes Hardening Guidance
relatedResources :
- http://related-resource/
version: "1.0"
controls:
- name: Non-root containers
description: 'Check that container is not running as root'
id: '1.0'
checks:
- id: AVD-KSV-0001
severity: 'MEDIUM'
- name: Immutable container file systems
description: 'Check that container root file system is immutable'
id: '1.1'
checks:
- id: AVD-KSV-0003
severity: 'LOW'

View File

@@ -1,29 +0,0 @@
---
spec:
id: "1234"
title: nsa
description: National Security Agency - Kubernetes Hardening Guidance
relatedResources :
- http://related-resource/
version: "1.0"
controls:
- name: Non-root containers
description: 'Check that container is not running as root'
id: '1.0'
checks:
- id: AVD-KSV-0001
severity: 'MEDIUM'
- name: Immutable container file systems
description: 'Check that container root file system is immutable'
id: '1.1'
checks:
- id: AVD-KSV-0003
severity: 'LOW'
- name: tzdata - new upstream version
description: 'Bad tzdata package'
id: '1.2'
checks:
- id: DLA-2424-1
severity: 'CRITICAL'

View File

@@ -1,20 +0,0 @@
{
"ReportID": "1234",
"ReportTitle": "nsa",
"SummaryControls": [
{
"id": "1.0",
"name": "Non-root containers",
"severity": "MEDIUM",
"totalPass": 0,
"totalFail": 1
},
{
"id": "1.1",
"name": "Immutable container file systems",
"severity": "LOW",
"totalPass": 0,
"totalFail": 1
}
]
}

View File

@@ -1,271 +0,0 @@
{
"id": "1234",
"title": "nsa",
"description": "National Security Agency - Kubernetes Hardening Guidance",
"severity": "1.0",
"relatedResources": [
"http://related-resource/"
],
"results": [
{
"id": "1.0",
"name": "Non-root containers",
"description": "Check that container is not running as root",
"severity": "MEDIUM",
"results": [
{
"Target": "Deployment/metrics-server",
"Class": "config",
"Type": "kubernetes",
"MisconfSummary": {
"Successes": 1,
"Failures": 0,
"Exceptions": 0
},
"Misconfigurations": [
{
"Type": "Kubernetes Security Check",
"ID": "KSV001",
"AVDID": "AVD-KSV-0001",
"Title": "Process can elevate its own privileges",
"Description": "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.",
"Message": "Container 'metrics-server' of Deployment 'metrics-server' should set 'securityContext.allowPrivilegeEscalation' to false",
"Namespace": "builtin.kubernetes.KSV001",
"Query": "data.builtin.kubernetes.KSV001.deny",
"Resolution": "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.",
"Severity": "MEDIUM",
"PrimaryURL": "https://avd.aquasec.com/misconfig/ksv001",
"References": [
"https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted",
"https://avd.aquasec.com/misconfig/ksv001"
],
"Status": "FAIL",
"Layer": {},
"CauseMetadata": {
"Provider": "Kubernetes",
"Service": "general",
"StartLine": 132,
"EndLine": 140,
"Code": {
"Lines": [
{
"Number": 132,
"Content": " - image: rancher/metrics-server:v0.3.6",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": true,
"LastCause": false
},
{
"Number": 133,
"Content": " imagePullPolicy: IfNotPresent",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 134,
"Content": " name: metrics-server",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 135,
"Content": " resources: {}",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 136,
"Content": " terminationMessagePath: /dev/termination-log",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 137,
"Content": " terminationMessagePolicy: File",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 138,
"Content": " volumeMounts:",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 139,
"Content": " - mountPath: /tmp",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 140,
"Content": " name: tmp-dir",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": true
}
]
}
}
}
]
}
]
},
{
"id": "1.1",
"name": "Immutable container file systems",
"description": "Check that container root file system is immutable",
"severity": "LOW",
"results": [
{
"Target": "Deployment/metrics-server",
"Class": "config",
"Type": "kubernetes",
"MisconfSummary": {
"Successes": 1,
"Failures": 0,
"Exceptions": 0
},
"Misconfigurations": [
{
"Type": "Kubernetes Security Check",
"ID": "KSV003",
"AVDID": "AVD-KSV-0003",
"Title": "Default capabilities not dropped",
"Description": "The container should drop all default capabilities and add only those that are needed for its execution.",
"Message": "Container 'metrics-server' of Deployment 'metrics-server' should add 'ALL' to 'securityContext.capabilities.drop'",
"Namespace": "builtin.kubernetes.KSV003",
"Query": "data.builtin.kubernetes.KSV003.deny",
"Resolution": "Add 'ALL' to containers[].securityContext.capabilities.drop.",
"Severity": "LOW",
"PrimaryURL": "https://avd.aquasec.com/misconfig/ksv003",
"References": [
"https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/",
"https://avd.aquasec.com/misconfig/ksv003"
],
"Status": "FAIL",
"Layer": {},
"CauseMetadata": {
"Provider": "Kubernetes",
"Service": "general",
"StartLine": 132,
"EndLine": 140,
"Code": {
"Lines": [
{
"Number": 132,
"Content": " - image: rancher/metrics-server:v0.3.6",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": true,
"LastCause": false
},
{
"Number": 133,
"Content": " imagePullPolicy: IfNotPresent",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 134,
"Content": " name: metrics-server",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 135,
"Content": " resources: {}",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 136,
"Content": " terminationMessagePath: /dev/termination-log",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 137,
"Content": " terminationMessagePolicy: File",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 138,
"Content": " volumeMounts:",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 139,
"Content": " - mountPath: /tmp",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 140,
"Content": " name: tmp-dir",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": true
}
]
}
}
}
]
}
]
}
]
}

View File

@@ -1 +0,0 @@
{"ReportID":"1234","ReportTitle":"nsa","SummaryControls":[{"id":"1.0","name":"Non-root containers","severity":"MEDIUM","totalPass":0,"totalFail":1},{"id":"1.1","name":"Immutable container file systems","severity":"LOW","totalPass":0,"totalFail":1}]}

File diff suppressed because one or more lines are too long

View File

@@ -1,150 +0,0 @@
[
{
"Target": "Deployment/metrics-server",
"Class": "config",
"Type": "kubernetes",
"MisconfSummary": {
"Successes": 66,
"Failures": 13,
"Exceptions": 0
},
"Misconfigurations": [
{
"Type": "Kubernetes Security Check",
"ID": "KSV001",
"AVDID": "AVD-KSV-0001",
"Title": "Process can elevate its own privileges",
"Description": "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.",
"Message": "Container 'metrics-server' of Deployment 'metrics-server' should set 'securityContext.allowPrivilegeEscalation' to false",
"Namespace": "builtin.kubernetes.KSV001",
"Query": "data.builtin.kubernetes.KSV001.deny",
"Resolution": "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.",
"Severity": "MEDIUM",
"PrimaryURL": "https://avd.aquasec.com/misconfig/ksv001",
"References": [
"https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted",
"https://avd.aquasec.com/misconfig/ksv001"
],
"Status": "PASS",
"Layer": {},
"CauseMetadata": {
"Provider": "Kubernetes",
"Service": "general",
"StartLine": 132,
"EndLine": 140,
"Code": {
"Lines": [
{
"Number": 132,
"Content": " - image: rancher/metrics-server:v0.3.6",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": true,
"LastCause": false
},
{
"Number": 133,
"Content": " imagePullPolicy: IfNotPresent",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 134,
"Content": " name: metrics-server",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 135,
"Content": " resources: {}",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 136,
"Content": " terminationMessagePath: /dev/termination-log",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 137,
"Content": " terminationMessagePolicy: File",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 138,
"Content": " volumeMounts:",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 139,
"Content": " - mountPath: /tmp",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 140,
"Content": " name: tmp-dir",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": true
}
]
}
}
}
]
},
{
"Target": "rancher/metrics-server:v0.3.6 (debian 9.9)",
"Class": "os-pkgs",
"Type": "debian",
"Vulnerabilities": [
{
"VulnerabilityID": "DLA-2424-1",
"VendorIDs": [
"DLA-2424-1"
],
"PkgName": "tzdata",
"InstalledVersion": "2019a-0+deb9u1",
"FixedVersion": "2020d-0+deb9u1",
"Layer": {
"DiffID": "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02"
},
"DataSource": {
"ID": "debian",
"Name": "Debian Security Tracker",
"URL": "https://salsa.debian.org/security-tracker-team/security-tracker"
},
"Title": "tzdata - new upstream version",
"Severity": "UNKNOWN"
}
]
}
]

View File

@@ -0,0 +1,20 @@
{
"ID": "1234",
"Title": "NSA",
"SummaryControls": [
{
"ID": "1.0",
"Name": "Non-root containers",
"Severity": "MEDIUM",
"TotalPass": 0,
"TotalFail": 1
},
{
"ID": "1.1",
"Name": "Immutable container file systems",
"Severity": "LOW",
"TotalPass": 0,
"TotalFail": 1
}
]
}

View File

@@ -1,5 +1,5 @@
Summary Report for compliance: nsa
Summary Report for compliance: NSA
┌─────┬──────────┬──────────────────────────────────┬────────────┐
│ ID │ Severity │ Control Name │ Compliance │
├─────┼──────────┼──────────────────────────────────┼────────────┤

View File

@@ -1,36 +0,0 @@
Deployment/metrics-server (kubernetes)
======================================
Tests: 1 (SUCCESSES: 1, FAILURES: 0, EXCEPTIONS: 0)
Failures: 0 ()
MEDIUM: Container 'metrics-server' of Deployment 'metrics-server' should set 'securityContext.allowPrivilegeEscalation' to false
════════════════════════════════════════
A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.
See https://avd.aquasec.com/misconfig/ksv001
────────────────────────────────────────
Deployment/metrics-server:132-140
────────────────────────────────────────
132 ┌ - image: rancher/metrics-server:v0.3.6
133 │ imagePullPolicy: IfNotPresent
134 │ name: metrics-server
135 │ resources: {}
136 │ terminationMessagePath: /dev/termination-log
137 │ terminationMessagePolicy: File
138 │ volumeMounts:
139 │ - mountPath: /tmp
140 └ name: tmp-dir
────────────────────────────────────────
rancher/metrics-server:v0.3.6 (debian 9.9)
==========================================
Total: 0 ()
┌─────────┬───────────────┬──────────┬───────────────────┬────────────────┬───────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │
├─────────┼───────────────┼──────────┼───────────────────┼────────────────┼───────────────────────────────┤
│ tzdata │ DLA-2424-1 │ UNKNOWN │ 2019a-0+deb9u1 │ 2020d-0+deb9u1 │ tzdata - new upstream version │
└─────────┴───────────────┴──────────┴───────────────────┴────────────────┴───────────────────────────────┘

View File

@@ -1,11 +0,0 @@
Summary Report for compliance: nsa
┌─────┬──────────┬──────────────────────────────────┬────────────┐
│ ID │ Severity │ Control Name │ Compliance │
├─────┼──────────┼──────────────────────────────────┼────────────┤
│ 1.0 │ MEDIUM │ Non-root containers │ 100.00% │
│ 1.1 │ LOW │ Immutable container file systems │ 0.00% │
│ 1.2 │ CRITICAL │ tzdata - new upstream version │ 0.00% │
└─────┴──────────┴──────────────────────────────────┴────────────┘

View File

@@ -1,182 +0,0 @@
{
"id": "1234",
"title": "nsa",
"description": "National Security Agency - Kubernetes Hardening Guidance",
"severity": "1.0",
"relatedResources": [
"http://related-resource/"
],
"results": [
{
"id": "1.0",
"name": "Non-root containers",
"description": "Check that container is not running as root",
"severity": "MEDIUM",
"results": [
{
"Target": "Deployment/metrics-server",
"Class": "config",
"Type": "kubernetes",
"MisconfSummary": {
"Successes": 1,
"Failures": 0,
"Exceptions": 0
},
"Misconfigurations": [
{
"Type": "Kubernetes Security Check",
"ID": "KSV001",
"AVDID": "AVD-KSV-0001",
"Title": "Process can elevate its own privileges",
"Description": "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.",
"Message": "Container 'metrics-server' of Deployment 'metrics-server' should set 'securityContext.allowPrivilegeEscalation' to false",
"Namespace": "builtin.kubernetes.KSV001",
"Query": "data.builtin.kubernetes.KSV001.deny",
"Resolution": "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.",
"Severity": "MEDIUM",
"PrimaryURL": "https://avd.aquasec.com/misconfig/ksv001",
"References": [
"https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted",
"https://avd.aquasec.com/misconfig/ksv001"
],
"Status": "PASS",
"Layer": {},
"CauseMetadata": {
"Provider": "Kubernetes",
"Service": "general",
"StartLine": 132,
"EndLine": 140,
"Code": {
"Lines": [
{
"Number": 132,
"Content": " - image: rancher/metrics-server:v0.3.6",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": true,
"LastCause": false
},
{
"Number": 133,
"Content": " imagePullPolicy: IfNotPresent",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 134,
"Content": " name: metrics-server",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 135,
"Content": " resources: {}",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 136,
"Content": " terminationMessagePath: /dev/termination-log",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 137,
"Content": " terminationMessagePolicy: File",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 138,
"Content": " volumeMounts:",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 139,
"Content": " - mountPath: /tmp",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 140,
"Content": " name: tmp-dir",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": true
}
]
}
}
}
]
}
]
},
{
"id": "1.1",
"name": "Immutable container file systems",
"description": "Check that container root file system is immutable",
"severity": "LOW",
"results": null
},
{
"id": "1.2",
"name": "tzdata - new upstream version",
"description": "Bad tzdata package",
"severity": "CRITICAL",
"results": [
{
"Target": "rancher/metrics-server:v0.3.6 (debian 9.9)",
"Class": "os-pkgs",
"Type": "debian",
"Vulnerabilities": [
{
"VulnerabilityID": "DLA-2424-1",
"VendorIDs": [
"DLA-2424-1"
],
"PkgName": "tzdata",
"InstalledVersion": "2019a-0+deb9u1",
"FixedVersion": "2020d-0+deb9u1",
"Layer": {
"DiffID": "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02"
},
"DataSource": {
"ID": "debian",
"Name": "Debian Security Tracker",
"URL": "https://salsa.debian.org/security-tracker-team/security-tracker"
},
"Title": "tzdata - new upstream version",
"Severity": "UNKNOWN"
}
]
}
]
}
]
}

View File

@@ -1 +0,0 @@
{"ReportID":"1234","ReportTitle":"nsa","SummaryControls":[{"id":"1.0","name":"Non-root containers","severity":"MEDIUM","totalPass":1,"totalFail":0},{"id":"1.1","name":"Immutable container file systems","severity":"LOW","totalPass":0,"totalFail":0},{"id":"1.2","name":"tzdata - new upstream version","severity":"CRITICAL","totalPass":0,"totalFail":1}]}

View File

@@ -1,17 +1,16 @@
package spec
type Severity string
import (
"strings"
const (
SeverityCritical Severity = "CRITICAL"
SeverityHigh Severity = "HIGH"
SeverityMedium Severity = "MEDIUM"
SeverityLow Severity = "LOW"
"golang.org/x/exp/maps"
"golang.org/x/xerrors"
SeverityNone Severity = "NONE"
SeverityUnknown Severity = "UNKNOWN"
"github.com/aquasecurity/trivy/pkg/types"
)
type Severity string
// ComplianceSpec represent the compliance specification
type ComplianceSpec struct {
Spec Spec `yaml:"spec"`
@@ -58,3 +57,42 @@ const (
PassStatus ControlStatus = "PASS"
WarnStatus ControlStatus = "WARN"
)
// SecurityChecks reads spec control and determines the scanners by check ID prefix
func (cs *ComplianceSpec) SecurityChecks() ([]types.SecurityCheck, error) {
scannerTypes := map[types.SecurityCheck]struct{}{}
for _, control := range cs.Spec.Controls {
for _, check := range control.Checks {
scannerType := securityCheckByCheckID(check.ID)
if scannerType == types.SecurityCheckUnknown {
return nil, xerrors.Errorf("unsupported check ID: %s", check.ID)
}
scannerTypes[scannerType] = struct{}{}
}
}
return maps.Keys(scannerTypes), nil
}
// CheckIDs return list of compliance check IDs
func (cs *ComplianceSpec) CheckIDs() map[types.SecurityCheck][]string {
checkIDsMap := map[types.SecurityCheck][]string{}
for _, control := range cs.Spec.Controls {
for _, check := range control.Checks {
scannerType := securityCheckByCheckID(check.ID)
checkIDsMap[scannerType] = append(checkIDsMap[scannerType], check.ID)
}
}
return checkIDsMap
}
func securityCheckByCheckID(checkID string) types.SecurityCheck {
checkID = strings.ToLower(checkID)
switch {
case strings.HasPrefix(checkID, "cve-") || strings.HasPrefix(checkID, "dla-"):
return types.SecurityCheckVulnerability
case strings.HasPrefix(checkID, "avd-"):
return types.SecurityCheckConfig
default:
return types.SecurityCheckUnknown
}
}

View File

@@ -0,0 +1,236 @@
package spec_test
import (
"fmt"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/aquasecurity/trivy/pkg/compliance/spec"
"github.com/aquasecurity/trivy/pkg/types"
)
func TestComplianceSpec_SecurityChecks(t *testing.T) {
tests := []struct {
name string
spec spec.Spec
want []types.SecurityCheck
wantErr assert.ErrorAssertionFunc
}{
{
name: "get config scanner type by check id prefix",
spec: spec.Spec{
ID: "1234",
Title: "NSA",
Description: "National Security Agency - Kubernetes Hardening Guidance",
RelatedResources: []string{
"https://example.com",
},
Version: "1.0",
Controls: []spec.Control{
{
Name: "Non-root containers",
Description: "Check that container is not running as root",
ID: "1.0",
Checks: []spec.SpecCheck{
{ID: "AVD-KSV012"},
},
},
{
Name: "Check that encryption resource has been set",
Description: "Control checks whether encryption resource has been set",
ID: "1.1",
Checks: []spec.SpecCheck{
{ID: "AVD-1.2.31"},
{ID: "AVD-1.2.32"},
},
},
},
},
want: []types.SecurityCheck{types.SecurityCheckConfig},
wantErr: assert.NoError,
},
{
name: "get config and vuln scanners types by check id prefix",
spec: spec.Spec{
ID: "1234",
Title: "NSA",
Description: "National Security Agency - Kubernetes Hardening Guidance",
RelatedResources: []string{
"https://example.com",
},
Version: "1.0",
Controls: []spec.Control{
{
Name: "Non-root containers",
Description: "Check that container is not running as root",
ID: "1.0",
Checks: []spec.SpecCheck{
{ID: "AVD-KSV012"},
},
},
{
Name: "Check that encryption resource has been set",
Description: "Control checks whether encryption resource has been set",
ID: "1.1",
Checks: []spec.SpecCheck{
{ID: "AVD-1.2.31"},
{ID: "AVD-1.2.32"},
},
},
{
Name: "Ensure no critical vulnerabilities",
Description: "Control checks whether critical vulnerabilities are not found",
ID: "7.0",
Checks: []spec.SpecCheck{
{ID: "CVE-9999-9999"},
},
},
},
},
want: []types.SecurityCheck{types.SecurityCheckConfig, types.SecurityCheckVulnerability},
wantErr: assert.NoError,
},
{
name: "unknown prefix",
spec: spec.Spec{
ID: "1234",
Title: "NSA",
Description: "National Security Agency - Kubernetes Hardening Guidance",
RelatedResources: []string{
"https://example.com",
},
Version: "1.0",
Controls: []spec.Control{
{
Name: "Unknown",
ID: "1.0",
Checks: []spec.SpecCheck{
{ID: "UNKNOWN-001"},
},
},
},
},
wantErr: assert.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cs := &spec.ComplianceSpec{
Spec: tt.spec,
}
got, err := cs.SecurityChecks()
if !tt.wantErr(t, err, fmt.Sprintf("SecurityChecks()")) {
return
}
sort.Strings(got) // for consistency
assert.Equalf(t, tt.want, got, "SecurityChecks()")
})
}
}
func TestComplianceSpec_CheckIDs(t *testing.T) {
tests := []struct {
name string
spec spec.Spec
want map[types.SecurityCheck][]string
}{
{
name: "get config scanner type by check id prefix",
spec: spec.Spec{
ID: "1234",
Title: "NSA",
Description: "National Security Agency - Kubernetes Hardening Guidance",
RelatedResources: []string{
"https://example.com",
},
Version: "1.0",
Controls: []spec.Control{
{
Name: "Non-root containers",
Description: "Check that container is not running as root",
ID: "1.0",
Checks: []spec.SpecCheck{
{ID: "AVD-KSV012"},
},
},
{
Name: "Check that encryption resource has been set",
Description: "Control checks whether encryption resource has been set",
ID: "1.1",
Checks: []spec.SpecCheck{
{ID: "AVD-1.2.31"},
{ID: "AVD-1.2.32"},
},
},
},
},
want: map[types.SecurityCheck][]string{
types.SecurityCheckConfig: {
"AVD-KSV012",
"AVD-1.2.31",
"AVD-1.2.32",
},
},
},
{
name: "get config and vuln scanners types by check id prefix",
spec: spec.Spec{
ID: "1234",
Title: "NSA",
Description: "National Security Agency - Kubernetes Hardening Guidance",
RelatedResources: []string{
"https://example.com",
},
Version: "1.0",
Controls: []spec.Control{
{
Name: "Non-root containers",
Description: "Check that container is not running as root",
ID: "1.0",
Checks: []spec.SpecCheck{
{ID: "AVD-KSV012"},
},
},
{
Name: "Check that encryption resource has been set",
Description: "Control checks whether encryption resource has been set",
ID: "1.1",
Checks: []spec.SpecCheck{
{ID: "AVD-1.2.31"},
{ID: "AVD-1.2.32"},
},
},
{
Name: "Ensure no critical vulnerabilities",
Description: "Control checks whether critical vulnerabilities are not found",
ID: "7.0",
Checks: []spec.SpecCheck{
{ID: "CVE-9999-9999"},
},
},
},
},
want: map[types.SecurityCheck][]string{
types.SecurityCheckConfig: {
"AVD-KSV012",
"AVD-1.2.31",
"AVD-1.2.32",
},
types.SecurityCheckVulnerability: {
"CVE-9999-9999",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cs := &spec.ComplianceSpec{
Spec: tt.spec,
}
got := cs.CheckIDs()
assert.Equalf(t, tt.want, got, "CheckIDs()")
})
}
}

View File

@@ -1,71 +0,0 @@
package spec
import (
"fmt"
"strings"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v2"
"github.com/aquasecurity/trivy/pkg/types"
)
// GetScannerTypes read spec control and detremine the scanners by check ID prefix
func GetScannerTypes(complianceSpec string) ([]types.SecurityCheck, error) {
cs := ComplianceSpec{}
err := yaml.Unmarshal([]byte(complianceSpec), &cs)
if err != nil {
return nil, err
}
scannerTypes := make([]types.SecurityCheck, 0)
for _, control := range cs.Spec.Controls {
for _, check := range control.Checks {
scannerType := scannersByCheckIDPrefix(check.ID)
if !slices.Contains(scannerTypes, scannerType) {
scannerTypes = append(scannerTypes, scannerType)
}
}
}
return scannerTypes, nil
}
// ValidateScanners validate that scanner types are supported
func ValidateScanners(controls []Control) error {
for _, control := range controls {
for _, check := range control.Checks {
scannerType := scannersByCheckIDPrefix(check.ID)
if !slices.Contains(types.SecurityChecks, scannerType) {
return fmt.Errorf("scanner type %v is not supported", scannerType)
}
}
}
return nil
}
// ScannerCheckIDs return list of compliance check IDs
func ScannerCheckIDs(controls []Control) map[string][]string {
scannerChecksMap := make(map[string][]string)
for _, control := range controls {
for _, check := range control.Checks {
scannerType := scannersByCheckIDPrefix(check.ID)
if _, ok := scannerChecksMap[scannerType]; !ok {
scannerChecksMap[scannerType] = make([]string, 0)
}
if !slices.Contains(scannerChecksMap[scannerType], check.ID) {
scannerChecksMap[scannerType] = append(scannerChecksMap[scannerType], check.ID)
}
}
}
return scannerChecksMap
}
func scannersByCheckIDPrefix(checkID string) string {
switch {
case strings.HasPrefix(strings.ToLower(checkID), "cve-") || strings.HasPrefix(strings.ToLower(checkID), "dla-"):
return "vuln"
case strings.HasPrefix(strings.ToLower(checkID), "avd-"):
return "config"
default:
return ""
}
}

View File

@@ -1,86 +0,0 @@
package spec_test
import (
"os"
"testing"
"github.com/aquasecurity/trivy/pkg/compliance/spec"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)
func TestGetScannerTypes(t *testing.T) {
tests := []struct {
name string
specPath string
want []types.SecurityCheck
}{
{name: "get config scanner type by check id prefix", specPath: "./testdata/spec.yaml", want: []types.SecurityCheck{types.SecurityCheckConfig}},
{name: "get config and vuln scanners types by check id prefix", specPath: "./testdata/multi_scanner_spec.yaml", want: []types.SecurityCheck{types.SecurityCheckConfig, types.SecurityCheckVulnerability}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b, err := os.ReadFile(tt.specPath)
assert.NoError(t, err)
got, err := spec.GetScannerTypes(string(b))
assert.NoError(t, err)
assert.Equal(t, got, tt.want)
})
}
}
func TestCheckIDs(t *testing.T) {
tests := []struct {
name string
specPath string
wantConfig int
}{
{name: "get map of scannerType:checkIds array", specPath: "./testdata/spec.yaml", wantConfig: 29},
{name: "get map of scannerType:checkIds array when dup ids", specPath: "./testdata/spec_dup_id.yaml", wantConfig: 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cr, err := ReadSpecFile(tt.specPath)
assert.NoError(t, err)
got := spec.ScannerCheckIDs(cr.Spec.Controls)
assert.Equal(t, len(got["config"]), tt.wantConfig)
})
}
}
func TestValidateScanners(t *testing.T) {
tests := []struct {
name string
specPath string
expectError bool
}{
//{name: "spec with valid scanner", specPath: "./testdata/spec.yaml", expectError: false},
{name: "spec with non valid scanner", specPath: "./testdata/bad_scanner_spec.yaml", expectError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
compliance, err := ReadSpecFile(tt.specPath)
assert.NoError(t, err)
err = spec.ValidateScanners(compliance.Spec.Controls)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func ReadSpecFile(specFilePath string) (*spec.ComplianceSpec, error) {
b, err := os.ReadFile(specFilePath)
if err != nil {
return nil, err
}
cr := spec.ComplianceSpec{}
err = yaml.Unmarshal(b, &cr)
if err != nil {
return nil, err
}
return &cr, nil
}

View File

@@ -1,60 +1,39 @@
package spec
import (
"golang.org/x/exp/slices"
"github.com/aquasecurity/trivy/pkg/types"
)
// TrivyCheck represent checks models : vulnerability , misconfiguration and secret
type TrivyCheck interface {
GetID() string
CheckType() string
CheckPass() bool
}
// Mapper represent scan checks to spec check ids mapper
type Mapper[T TrivyCheck] interface {
FilterScanResultsBySpecCheckIds(trivyChecks []T, scannerCheckIDs map[string][]string) []T
MapSpecCheckIDToFilteredResults(trivyChecks []TrivyCheck, target string, class types.ResultClass, typeN string, scannerCheckIDs map[string][]string) map[string]types.Results
}
type mapper[T TrivyCheck] struct {
}
// NewMapper instansiate new Mapper for specific scanner type
func NewMapper[T TrivyCheck]() Mapper[T] {
return &mapper[T]{}
}
// FilterScanResultsBySpecCheckIds create a array of filtered security checks by spec checks ids
func (m mapper[T]) FilterScanResultsBySpecCheckIds(trivyChecks []T, scannerCheckIDs map[string][]string) []T {
filteredSecurityCheck := make([]T, 0)
for _, tc := range trivyChecks {
for _, id := range scannerCheckIDs[tc.CheckType()] {
if tc.GetID() == id {
filteredSecurityCheck = append(filteredSecurityCheck, tc)
}
}
}
return filteredSecurityCheck
}
// MapSpecCheckIDToFilteredResults map spec check id to filterred scan results
func (m mapper[T]) MapSpecCheckIDToFilteredResults(trivyChecks []TrivyCheck, target string, class types.ResultClass, typeN string, scannerCheckIDs map[string][]string) map[string]types.Results {
// MapSpecCheckIDToFilteredResults map spec check id to filtered scan results
func MapSpecCheckIDToFilteredResults(result types.Result, checkIDs map[types.SecurityCheck][]string) map[string]types.Results {
mapCheckByID := make(map[string]types.Results)
for _, tc := range trivyChecks {
if _, ok := mapCheckByID[tc.GetID()]; !ok {
mapCheckByID[tc.GetID()] = make(types.Results, 0)
for _, vuln := range result.Vulnerabilities {
// Skip irrelevant check IDs
if !slices.Contains(checkIDs[types.SecurityCheckVulnerability], vuln.GetID()) {
continue
}
for _, id := range scannerCheckIDs[tc.CheckType()] {
if tc.GetID() == id {
switch val := tc.(type) {
case types.DetectedMisconfiguration:
mapCheckByID[tc.GetID()] = append(mapCheckByID[tc.GetID()], types.Result{Target: target, Class: class, Type: typeN, MisconfSummary: misconfigSummary(val), Misconfigurations: []types.DetectedMisconfiguration{val}})
case types.DetectedVulnerability:
mapCheckByID[tc.GetID()] = append(mapCheckByID[tc.GetID()], types.Result{Target: target, Class: class, Type: typeN, Vulnerabilities: []types.DetectedVulnerability{val}})
}
}
mapCheckByID[vuln.GetID()] = append(mapCheckByID[vuln.GetID()], types.Result{
Target: result.Target,
Class: result.Class,
Type: result.Type,
Vulnerabilities: []types.DetectedVulnerability{vuln},
})
}
for _, m := range result.Misconfigurations {
// Skip irrelevant check IDs
if !slices.Contains(checkIDs[types.SecurityCheckConfig], m.GetID()) {
continue
}
mapCheckByID[m.GetID()] = append(mapCheckByID[m.GetID()], types.Result{
Target: result.Target,
Class: result.Class,
Type: result.Type,
MisconfSummary: misconfigSummary(m),
Misconfigurations: []types.DetectedMisconfiguration{m},
})
}
return mapCheckByID
}
@@ -65,66 +44,24 @@ func misconfigSummary(misconfig types.DetectedMisconfiguration) *types.MisconfSu
case types.StatusPassed:
rms.Successes = 1
case types.StatusFailure:
rms.Successes = 1
rms.Failures = 1
case types.StatusException:
rms.Exceptions = 1
}
return &rms
}
// FilterResults filter miconfiguration and vulnerabilities results by spec scanner check Ids
func FilterResults(results types.Results, scannerCheckIDs map[string][]string) types.Results {
filteredResults := make(types.Results, 0)
for _, result := range results {
if len(result.Misconfigurations) > 0 {
filteredMisconfig := NewMapper[types.DetectedMisconfiguration]().FilterScanResultsBySpecCheckIds(result.Misconfigurations, scannerCheckIDs)
result.Misconfigurations = filteredMisconfig
}
if len(result.Vulnerabilities) > 0 {
filteredVuln := NewMapper[types.DetectedVulnerability]().FilterScanResultsBySpecCheckIds(result.Vulnerabilities, scannerCheckIDs)
result.Vulnerabilities = filteredVuln
}
filteredResults = append(filteredResults, result)
}
return filteredResults
}
// AggregateAllChecksBySpecID aggregate all scan results and map it to spec ids
func AggregateAllChecksBySpecID(multiResults []types.Results, controls []Control) map[string]types.Results {
scannerCheckIDs := ScannerCheckIDs(controls)
// AggregateAllChecksBySpecID aggregates all scan results and map it to spec ids
func AggregateAllChecksBySpecID(multiResults []types.Results, cs ComplianceSpec) map[string]types.Results {
checkIDs := cs.CheckIDs()
complianceArr := make(map[string]types.Results, 0)
for _, resResult := range multiResults {
filteredResults := FilterResults(resResult, scannerCheckIDs)
for _, result := range filteredResults {
if len(result.Misconfigurations) > 0 {
cMapper := NewMapper[types.DetectedMisconfiguration]()
misconfigMap := cMapper.MapSpecCheckIDToFilteredResults(getTrivyChecks(result.Misconfigurations), result.Target, result.Class, result.Type, scannerCheckIDs)
for id, checks := range misconfigMap {
if _, ok := misconfigMap[id]; !ok {
complianceArr[id] = make(types.Results, 0)
}
complianceArr[id] = append(complianceArr[id], checks...)
}
}
if len(result.Vulnerabilities) > 0 {
vMapper := NewMapper[types.DetectedMisconfiguration]()
vulnsMap := vMapper.MapSpecCheckIDToFilteredResults(getTrivyChecks(result.Vulnerabilities), result.Target, result.Class, result.Type, scannerCheckIDs)
for id, checks := range vulnsMap {
if _, ok := vulnsMap[id]; !ok {
complianceArr[id] = make(types.Results, 0)
}
complianceArr[id] = append(complianceArr[id], checks...)
}
for _, result := range resResult {
m := MapSpecCheckIDToFilteredResults(result, checkIDs)
for id, checks := range m {
complianceArr[id] = append(complianceArr[id], checks...)
}
}
}
return complianceArr
}
func getTrivyChecks[T TrivyCheck](checks []T) []TrivyCheck {
tc := make([]TrivyCheck, 0)
for _, check := range checks {
tc = append(tc, check)
}
return tc
}

View File

@@ -3,83 +3,98 @@ package spec_test
import (
"testing"
"github.com/aquasecurity/trivy/pkg/compliance/spec"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/aquasecurity/trivy/pkg/compliance/spec"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/types"
)
func TestFilterScanResultsBySpecCheckIds(t *testing.T) {
tests := []struct {
name string
specPath string
trivyCheck spec.TrivyCheck
wantMapping map[string][]spec.TrivyCheck
}{
{name: "filter results by check ids for config", specPath: "./testdata/mapping_spec.yaml", trivyCheck: types.DetectedMisconfiguration{AVDID: "KSV012"},
wantMapping: map[string][]spec.TrivyCheck{"KSV012": {types.DetectedMisconfiguration{AVDID: "KSV012"}}}},
{name: "filter results by check ids for vulns", specPath: "./testdata/mapping_spec.yaml", trivyCheck: types.DetectedVulnerability{VulnerabilityID: "KSV014"},
wantMapping: map[string][]spec.TrivyCheck{"KSV014": {types.DetectedVulnerability{VulnerabilityID: "KSV014"}}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cr, err := ReadSpecFile(tt.specPath)
assert.NoError(t, err)
scannerIDMap := spec.ScannerCheckIDs(cr.Spec.Controls)
m := spec.NewMapper[spec.TrivyCheck]()
filteredResults := m.FilterScanResultsBySpecCheckIds([]spec.TrivyCheck{tt.trivyCheck}, scannerIDMap)
for _, v := range filteredResults {
assert.Equal(t, len(tt.wantMapping[v.GetID()]), 1)
}
})
}
}
func TestFilterResults(t *testing.T) {
tests := []struct {
name string
specPath string
results types.Results
wantFiltered types.Results
}{
{name: "filter results by check ids define in spec", specPath: "./testdata/mapping_spec.yaml",
results: types.Results{{Misconfigurations: []types.DetectedMisconfiguration{{AVDID: "AVD-KSV012"}, {AVDID: "AVD-KSV017"}}, Vulnerabilities: []types.DetectedVulnerability{{VulnerabilityID: "CVE-KSV014"}}}},
wantFiltered: types.Results{{Misconfigurations: []types.DetectedMisconfiguration{{AVDID: "AVD-KSV012"}}, Vulnerabilities: []types.DetectedVulnerability{{VulnerabilityID: "CVE-KSV014"}}}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cr, err := ReadSpecFile(tt.specPath)
assert.NoError(t, err)
scannerIDMap := spec.ScannerCheckIDs(cr.Spec.Controls)
r := spec.FilterResults(tt.results, scannerIDMap)
assert.Equal(t, r, tt.wantFiltered)
})
}
}
func TestMapSpecCheckIDtoFilteredResults(t *testing.T) {
tests := []struct {
name string
specPath string
trivyCheck spec.TrivyCheck
wantMapping map[string]types.Results
}{
{name: "map Check by ID config", specPath: "./testdata/mapping_spec.yaml", trivyCheck: types.DetectedMisconfiguration{AVDID: "AVD-KSV012"},
wantMapping: map[string]types.Results{"AVD-KSV012": {types.Result{Target: "target", MisconfSummary: &types.MisconfSummary{Successes: 0, Failures: 0, Exceptions: 0}, Class: "class", Type: "typeN", Misconfigurations: []types.DetectedMisconfiguration{{AVDID: "AVD-KSV012"}}}}},
func TestMapSpecCheckIDToFilteredResults(t *testing.T) {
checkIDs := map[types.SecurityCheck][]string{
types.SecurityCheckConfig: {
"AVD-KSV012",
"AVD-1.2.31",
"AVD-1.2.32",
},
types.SecurityCheckVulnerability: {
"CVE-9999-9999",
},
}
tests := []struct {
name string
checkIDs map[types.SecurityCheck][]string
result types.Result
want map[string]types.Results
}{
{
name: "misconfiguration",
checkIDs: checkIDs,
result: types.Result{
Target: "target",
Class: types.ClassConfig,
Type: ftypes.Kubernetes,
Misconfigurations: []types.DetectedMisconfiguration{
{AVDID: "AVD-KSV012", Status: types.StatusFailure},
{AVDID: "AVD-KSV013", Status: types.StatusFailure},
{AVDID: "AVD-1.2.31", Status: types.StatusFailure},
},
},
want: map[string]types.Results{
"AVD-KSV012": {
{
Target: "target",
Class: types.ClassConfig,
Type: ftypes.Kubernetes,
MisconfSummary: &types.MisconfSummary{Successes: 0, Failures: 1, Exceptions: 0},
Misconfigurations: []types.DetectedMisconfiguration{
{AVDID: "AVD-KSV012", Status: types.StatusFailure},
},
},
},
"AVD-1.2.31": {
{
Target: "target",
Class: types.ClassConfig,
Type: ftypes.Kubernetes,
MisconfSummary: &types.MisconfSummary{Successes: 0, Failures: 1, Exceptions: 0},
Misconfigurations: []types.DetectedMisconfiguration{
{AVDID: "AVD-1.2.31", Status: types.StatusFailure},
},
},
},
},
},
{
name: "vulnerability",
checkIDs: checkIDs,
result: types.Result{
Target: "target",
Class: types.ClassLangPkg,
Type: ftypes.GoModule,
Vulnerabilities: []types.DetectedVulnerability{
{VulnerabilityID: "CVE-9999-0001"},
{VulnerabilityID: "CVE-9999-9999"},
},
},
want: map[string]types.Results{
"CVE-9999-9999": {
{
Target: "target",
Class: types.ClassLangPkg,
Type: ftypes.GoModule,
Vulnerabilities: []types.DetectedVulnerability{
{VulnerabilityID: "CVE-9999-9999"},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cr, err := ReadSpecFile(tt.specPath)
assert.NoError(t, err)
scannerIDMap := spec.ScannerCheckIDs(cr.Spec.Controls)
m := spec.NewMapper[spec.TrivyCheck]()
mapResults := m.MapSpecCheckIDToFilteredResults([]spec.TrivyCheck{tt.trivyCheck}, "target", "class", "typeN", scannerIDMap)
for key, val := range tt.wantMapping {
assert.Equal(t, mapResults[key], val)
}
got := spec.MapSpecCheckIDToFilteredResults(tt.result, tt.checkIDs)
assert.Equalf(t, tt.want, got, "CheckIDs()")
})
}
}

View File

@@ -1,22 +0,0 @@
---
spec:
id: "1234"
title: nsa
description: National Security Agency - Kubernetes Hardening Guidance
version: "1.0"
related_resources :
- http://related-resource/
controls:
- name: Pod and/or namespace Selectors usage
description: 'Control check validate the pod and/or namespace Selectors usage'
id: '2.0'
defaultStatus: 'FAIL'
checks:
- id: AVD-KSV038
severity: 'MEDIUM'
- name: Use CNI plugin that supports NetworkPolicy API
description: 'Control check whether check cni plugin installed'
id: '3.0'
checks:
- id: 5.3.1
severity: 'CRITICAL'

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +0,0 @@
---
spec:
id: "1234"
title: nsa
description: National Security Agency - Kubernetes Hardening Guidance
relatedResources :
- http://related-resource/
version: "1.0"
controls:
- name: Non-root containers
description: 'Check that container is not running as root'
id: '1.0'
checks:
- id: AVD-KSV-0001
severity: 'MEDIUM'
- name: Immutable container file systems
description: 'Check that container root file system is immutable'
id: '1.1'
checks:
- id: AVD-KSV-0003
severity: 'LOW'

View File

@@ -1,23 +0,0 @@
---
spec:
id: "1234"
title: nsa
description: National Security Agency - Kubernetes Hardening Guidance
relatedResources :
- http://related-resource/
version: "1.0"
controls:
- name: Non-root containers
description: 'Check that container is not running as root'
id: '1.0'
checks:
- id: AVD-KSV012
severity: 'MEDIUM'
- name: Immutable container file systems
description: 'Check that container root file system is immutable'
id: '1.1'
checks:
- id: CVE-KSV014
severity: 'LOW'

View File

@@ -1,178 +0,0 @@
---
spec:
name: "1234"
title: nsa
description: National Security Agency - Kubernetes Hardening Guidance
related_resources :
- http://related-resource/
version: "1.0"
controls:
- name: Non-root containers
description: 'Check that container is not running as root'
id: '1.0'
checks:
- id: AVD-KSV012
severity: 'MEDIUM'
- name: Immutable container file systems
description: 'Check that container root file system is immutable'
id: '1.1'
checks:
- id: AVD-KSV014
severity: 'LOW'
- name: Preventing privileged containers
description: 'Controls whether Pods can run privileged containers'
id: '1.2'
checks:
- id: AVD-KSV017
severity: 'HIGH'
- name: Share containers process namespaces
description: 'Controls whether containers can share process namespaces'
id: '1.3'
checks:
- id: AVD-KSV008
severity: 'HIGH'
- name: Share host process namespaces
description: 'Controls whether share host process namespaces'
id: '1.4'
checks:
- id: AVD-KSV009
severity: 'HIGH'
- name: Use the host network
description: 'Controls whether containers can use the host network'
id: '1.5'
checks:
- id: AVD-KSV010
severity: 'HIGH'
- name: Run with root privileges or with root group membership
description: 'Controls whether container applications can run with root privileges or with root group membership'
id: '1.6'
checks:
- id: AVD-KSV029
severity: 'LOW'
- name: Restricts escalation to root privileges
description: 'Control check restrictions escalation to root privileges'
id: '1.7'
checks:
- id: AVD-KSV001
severity: 'MEDIUM'
- name: Sets the SELinux context of the container
description: 'Control checks if pod sets the SELinux context of the container'
id: '1.8'
checks:
- id: AVD-KSV002
severity: 'MEDIUM'
- name: Restrict a container's access to resources with AppArmor
description: 'Control checks the restriction of containers access to resources with AppArmor'
id: '1.9'
checks:
- id: AVD-KSV030
severity: 'MEDIUM'
- name: Sets the seccomp profile used to sandbox containers.
description: 'Control checks the sets the seccomp profile used to sandbox containers'
id: '1.10'
checks:
- id: AVD-KSV030
severity: 'LOW'
- name: Protecting Pod service account tokens
description: 'Control check whether disable secret token been mount ,automountServiceAccountToken: false'
id: '1.11'
checks:
- id: AVD-KSV036
severity: 'MEDIUM'
- name: Namespace kube-system should not be used by users
description: 'Control check whether Namespace kube-system is not be used by users'
id: '1.12'
defaultStatus: 'FAIL'
checks:
- id: AVD-KSV037
severity: 'MEDIUM'
- name: Pod and/or namespace Selectors usage
description: 'Control check validate the pod and/or namespace Selectors usage'
id: '2.0'
defaultStatus: 'FAIL'
checks:
- id: AVD-KSV038
severity: 'MEDIUM'
- name: Use CNI plugin that supports NetworkPolicy API
description: 'Control check whether check cni plugin installed'
id: '3.0'
checks:
- id: AVD-5.3.1
severity: 'CRITICAL'
- name: Use ResourceQuota policies to limit resources
description: 'Control check the use of ResourceQuota policy to limit aggregate resource usage within namespace'
id: '4.0'
defaultStatus: 'FAIL'
checks:
- id: "AVD-KSV040"
severity: 'MEDIUM'
- name: Use LimitRange policies to limit resources
description: 'Control check the use of LimitRange policy limit resource usage for namespaces or nodes'
id: '4.1'
defaultStatus: 'FAIL'
checks:
- id: "AVD-KSV039"
severity: 'MEDIUM'
- name: Control plan disable insecure port
description: 'Control check whether control plan disable insecure port'
id: '5.0'
checks:
- id: AVD-1.2.19
severity: 'CRITICAL'
- name: Encrypt etcd communication
description: 'Control check whether etcd communication is encrypted'
id: '5.1'
checks:
- id: CVE-2.1
severity: 'CRITICAL'
- name: Ensure kube config file permission
description: 'Control check whether kube config file permissions'
id: '6.0'
checks:
- id: AVD-4.1.3
- id: AVD-4.1.4
severity: 'CRITICAL'
- name: Check that encryption resource has been set
description: 'Control checks whether encryption resource has been set'
id: '6.1'
checks:
- id: AVD-1.2.31
- id: AVD-1.2.32
severity: 'CRITICAL'
- name: Check encryption provider
description: 'Control checks whether encryption provider has been set'
id: '6.2'
checks:
- id: AVD-1.2.3
severity: 'CRITICAL'
- name: Make sure anonymous-auth is unset
description: 'Control checks whether anonymous-auth is unset'
id: '7.0'
checks:
- id: AVD-1.2.1
severity: 'CRITICAL'
- name: Make sure -authorization-mode=RBAC
description: 'Control check whether RBAC permission is in use'
id: '7.1'
checks:
- id: AVD-1.2.7
- id: AVD-1.2.8
severity: 'CRITICAL'
- name: Audit policy is configure
description: 'Control check whether audit policy is configure'
id: '8.0'
checks:
- id: AVD-3.2.1
severity: 'HIGH'
- name: Audit log path is configure
description: 'Control check whether audit log path is configure'
id: '8.1'
checks:
- id: AVD-1.2.22
severity: 'MEDIUM'
- name: Audit log aging
description: 'Control check whether audit log aging is configure'
id: '8.2'
checks:
- id: AVD-1.2.23
severity: 'MEDIUM'

File diff suppressed because one or more lines are too long

View File

@@ -1,178 +0,0 @@
---
spec:
id: "1234"
title: nsa
description: National Security Agency - Kubernetes Hardening Guidance
related_resources :
- http://related-resource/
version: "1.0"
controls:
- name: Non-root containers
description: 'Check that container is not running as root'
id: '1.0'
checks:
- id: AVD-KSV012
severity: 'MEDIUM'
- name: Immutable container file systems
description: 'Check that container root file system is immutable'
id: '1.1'
checks:
- id: AVD-KSV014
severity: 'LOW'
- name: Preventing privileged containers
description: 'Controls whether Pods can run privileged containers'
id: '1.2'
checks:
- id: AVD-KSV017
severity: 'HIGH'
- name: Share containers process namespaces
description: 'Controls whether containers can share process namespaces'
id: '1.3'
checks:
- id: AVD-KSV008
severity: 'HIGH'
- name: Share host process namespaces
description: 'Controls whether share host process namespaces'
id: '1.4'
checks:
- id: AVD-KSV009
severity: 'HIGH'
- name: Use the host network
description: 'Controls whether containers can use the host network'
id: '1.5'
checks:
- id: AVD-KSV010
severity: 'HIGH'
- name: Run with root privileges or with root group membership
description: 'Controls whether container applications can run with root privileges or with root group membership'
id: '1.6'
checks:
- id: AVD-KSV029
severity: 'LOW'
- name: Restricts escalation to root privileges
description: 'Control check restrictions escalation to root privileges'
id: '1.7'
checks:
- id: AVD-KSV001
severity: 'MEDIUM'
- name: Sets the SELinux context of the container
description: 'Control checks if pod sets the SELinux context of the container'
id: '1.8'
checks:
- id: AVD-KSV002
severity: 'MEDIUM'
- name: Restrict a container's access to resources with AppArmor
description: 'Control checks the restriction of containers access to resources with AppArmor'
id: '1.9'
checks:
- id: AVD-KSV030
severity: 'MEDIUM'
- name: Sets the seccomp profile used to sandbox containers.
description: 'Control checks the sets the seccomp profile used to sandbox containers'
id: '1.10'
checks:
- id: AVD-KSV030
severity: 'LOW'
- name: Protecting Pod service account tokens
description: 'Control check whether disable secret token been mount ,automountServiceAccountToken: false'
id: '1.11'
checks:
- id: AVD-KSV036
severity: 'MEDIUM'
- name: Namespace kube-system should not be used by users
description: 'Control check whether Namespace kube-system is not be used by users'
id: '1.12'
defaultStatus: 'FAIL'
checks:
- id: AVD-KSV037
severity: 'MEDIUM'
- name: Pod and/or namespace Selectors usage
description: 'Control check validate the pod and/or namespace Selectors usage'
id: '2.0'
defaultStatus: 'FAIL'
checks:
- id: AVD-KSV038
severity: 'MEDIUM'
- name: Use CNI plugin that supports NetworkPolicy API
description: 'Control check whether check cni plugin installed'
id: '3.0'
checks:
- id: AVD-5.3.1
severity: 'CRITICAL'
- name: Use ResourceQuota policies to limit resources
description: 'Control check the use of ResourceQuota policy to limit aggregate resource usage within namespace'
id: '4.0'
defaultStatus: 'FAIL'
checks:
- id: "AVD-KSV040"
severity: 'MEDIUM'
- name: Use LimitRange policies to limit resources
description: 'Control check the use of LimitRange policy limit resource usage for namespaces or nodes'
id: '4.1'
defaultStatus: 'FAIL'
checks:
- id: "AVD-KSV039"
severity: 'MEDIUM'
- name: Control plan disable insecure port
description: 'Control check whether control plan disable insecure port'
id: '5.0'
checks:
- id: AVD-1.2.19
severity: 'CRITICAL'
- name: Encrypt etcd communication
description: 'Control check whether etcd communication is encrypted'
id: '5.1'
checks:
- id: 'AVD-2.1'
severity: 'CRITICAL'
- name: Ensure kube config file permission
description: 'Control check whether kube config file permissions'
id: '6.0'
checks:
- id: AVD-4.1.3
- id: AVD-4.1.4
severity: 'CRITICAL'
- name: Check that encryption resource has been set
description: 'Control checks whether encryption resource has been set'
id: '6.1'
checks:
- id: AVD-1.2.31
- id: AVD-1.2.32
severity: 'CRITICAL'
- name: Check encryption provider
description: 'Control checks whether encryption provider has been set'
id: '6.2'
checks:
- id: AVD-1.2.3
severity: 'CRITICAL'
- name: Make sure anonymous-auth is unset
description: 'Control checks whether anonymous-auth is unset'
id: '7.0'
checks:
- id: AVD-1.2.1
severity: 'CRITICAL'
- name: Make sure -authorization-mode=RBAC
description: 'Control check whether RBAC permission is in use'
id: '7.1'
checks:
- id: AVD-1.2.7
- id: AVD-1.2.8
severity: 'CRITICAL'
- name: Audit policy is configure
description: 'Control check whether audit policy is configure'
id: '8.0'
checks:
- id: AVD-3.2.1
severity: 'HIGH'
- name: Audit log path is configure
description: 'Control check whether audit log path is configure'
id: '8.1'
checks:
- id: AVD-1.2.22
severity: 'MEDIUM'
- name: Audit log aging
description: 'Control check whether audit log aging is configure'
id: '8.2'
checks:
- id: AVD-1.2.23
severity: 'MEDIUM'

View File

@@ -1,22 +0,0 @@
---
spec:
id: "1234"
title: nsa
description: National Security Agency - Kubernetes Hardening Guidance
related_resources :
- http://related-resource/
version: "1.0"
controls:
- name: Non-root containers
description: 'Check that container is not running as root'
id: '1.0'
checks:
- id: AVD-KSV012
severity: 'MEDIUM'
- name: Immutable container file systems
description: 'Check that container root file system is immutable'
id: '1.1'
checks:
- id: AVD-KSV012
severity: 'LOW'

View File

@@ -5,11 +5,8 @@ import (
"errors"
"github.com/spf13/viper"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/aquasecurity/trivy/pkg/types"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
sp "github.com/aquasecurity/defsec/pkg/spec"
"github.com/aquasecurity/trivy-kubernetes/pkg/artifacts"
@@ -17,9 +14,11 @@ import (
cmd "github.com/aquasecurity/trivy/pkg/commands/artifact"
cr "github.com/aquasecurity/trivy/pkg/compliance/report"
"github.com/aquasecurity/trivy/pkg/compliance/spec"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/aquasecurity/trivy/pkg/k8s/report"
"github.com/aquasecurity/trivy/pkg/k8s/scanner"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/types"
)
const (
@@ -81,15 +80,20 @@ func (r *runner) run(ctx context.Context, artifacts []*artifacts.Artifact) error
}()
s := scanner.NewScanner(r.cluster, runner, r.flagOpts)
var complianceSpec string
var complianceSpec spec.ComplianceSpec
// set scanners types by spec
if len(r.flagOpts.ReportOptions.Compliance) > 0 {
complianceSpec = sp.NewSpecLoader().GetSpecByName(r.flagOpts.ReportOptions.Compliance)
scannerTypes, err := spec.GetScannerTypes(complianceSpec)
if err != nil {
return err
if r.flagOpts.ReportOptions.Compliance != "" {
cs := sp.NewSpecLoader().GetSpecByName(r.flagOpts.ReportOptions.Compliance)
if err = yaml.Unmarshal([]byte(cs), &complianceSpec); err != nil {
return xerrors.Errorf("yaml unmarshal error: %w", err)
}
r.flagOpts.ScanOptions.SecurityChecks = scannerTypes
securityChecks, err := complianceSpec.SecurityChecks()
if err != nil {
return xerrors.Errorf("security check error: %w", err)
}
r.flagOpts.ScanOptions.SecurityChecks = securityChecks
}
rpt, err := s.Scan(ctx, artifacts)
@@ -98,17 +102,16 @@ func (r *runner) run(ctx context.Context, artifacts []*artifacts.Artifact) error
}
if len(r.flagOpts.ReportOptions.Compliance) > 0 {
scanResults := make([]types.Results, 0)
for _, rss := range rpt.Misconfigurations {
var scanResults []types.Results
for _, rss := range rpt.Vulnerabilities {
scanResults = append(scanResults, rss.Results)
}
for _, rss := range rpt.Vulnerabilities {
for _, rss := range rpt.Misconfigurations {
scanResults = append(scanResults, rss.Results)
}
complianceReport, err := cr.BuildComplianceReport(scanResults, complianceSpec)
if err != nil {
return err
return xerrors.Errorf("compliance report build error: %w", err)
}
return cr.Write(complianceReport, cr.Option{
Format: r.flagOpts.Format,

View File

@@ -39,16 +39,6 @@ const (
)
// GetID retrun misconfig ID
func (mc DetectedMisconfiguration) GetID() string {
func (mc *DetectedMisconfiguration) GetID() string {
return mc.AVDID
}
// CheckType retrun misconfig check type
func (mc DetectedMisconfiguration) CheckType() string {
return "config"
}
// CheckType retrun misconfig check pass
func (mc DetectedMisconfiguration) CheckPass() bool {
return mc.Status == StatusPassed
}

View File

@@ -29,6 +29,11 @@ type DetectedVulnerability struct {
types.Vulnerability
}
// GetID retrun Vulnerability ID
func (vuln *DetectedVulnerability) GetID() string {
return vuln.VulnerabilityID
}
// BySeverity implements sort.Interface based on the Severity field.
type BySeverity []DetectedVulnerability
@@ -53,18 +58,3 @@ func (v BySeverity) Less(i, j int) bool {
// Swap swaps 2 vulnerability
func (v BySeverity) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
// GetID retrun Vulnerability ID
func (vuln DetectedVulnerability) GetID() string {
return vuln.VulnerabilityID
}
// CheckType retrun vulnerabilies check type
func (mc DetectedVulnerability) CheckType() string {
return "vuln"
}
// CheckType retrun vulnerabilies check pass
func (mc DetectedVulnerability) CheckPass() bool {
return false
}