mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-22 07:10:41 -08:00
refactor: add hook interface for extended functionality (#8585)
This commit is contained in:
@@ -3,12 +3,12 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aquasecurity/trivy/pkg/types"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aquasecurity/trivy/pkg/extension"
|
||||||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
||||||
"github.com/aquasecurity/trivy/pkg/scan/post"
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestModule(t *testing.T) {
|
func TestModule(t *testing.T) {
|
||||||
@@ -52,7 +52,7 @@ func TestModule(t *testing.T) {
|
|||||||
|
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
analyzer.DeregisterAnalyzer("spring4shell")
|
analyzer.DeregisterAnalyzer("spring4shell")
|
||||||
post.DeregisterPostScanner("spring4shell")
|
extension.DeregisterHook("spring4shell")
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run Trivy
|
// Run Trivy
|
||||||
|
|||||||
96
internal/hooktest/hook.go
Normal file
96
internal/hooktest/hook.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package hooktest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aquasecurity/trivy/pkg/extension"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/flag"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testHook struct{}
|
||||||
|
|
||||||
|
func (*testHook) Name() string {
|
||||||
|
return "test"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*testHook) Version() int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunHook implementation
|
||||||
|
func (*testHook) PreRun(ctx context.Context, opts flag.Options) error {
|
||||||
|
if opts.GlobalOptions.ConfigFile == "bad-config" {
|
||||||
|
return errors.New("bad pre-run")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*testHook) PostRun(ctx context.Context, opts flag.Options) error {
|
||||||
|
if opts.GlobalOptions.ConfigFile == "bad-config" {
|
||||||
|
return errors.New("bad post-run")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanHook implementation
|
||||||
|
func (*testHook) PreScan(ctx context.Context, target *types.ScanTarget, options types.ScanOptions) error {
|
||||||
|
if target.Name == "bad-pre" {
|
||||||
|
return errors.New("bad pre-scan")
|
||||||
|
}
|
||||||
|
target.Name += " (pre-scan)"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*testHook) PostScan(ctx context.Context, results types.Results) (types.Results, error) {
|
||||||
|
for i, r := range results {
|
||||||
|
if r.Target == "bad" {
|
||||||
|
return nil, errors.New("bad")
|
||||||
|
}
|
||||||
|
for j := range r.Vulnerabilities {
|
||||||
|
results[i].Vulnerabilities[j].References = []string{
|
||||||
|
"https://example.com/post-scan",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportHook implementation
|
||||||
|
func (*testHook) PreReport(ctx context.Context, report *types.Report, opts flag.Options) error {
|
||||||
|
if report.ArtifactName == "bad-report" {
|
||||||
|
return errors.New("bad pre-report")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the report
|
||||||
|
for i := range report.Results {
|
||||||
|
for j := range report.Results[i].Vulnerabilities {
|
||||||
|
report.Results[i].Vulnerabilities[j].Title = "Modified by pre-report hook"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*testHook) PostReport(ctx context.Context, report *types.Report, opts flag.Options) error {
|
||||||
|
if report.ArtifactName == "bad-report" {
|
||||||
|
return errors.New("bad post-report")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the report
|
||||||
|
for i := range report.Results {
|
||||||
|
for j := range report.Results[i].Vulnerabilities {
|
||||||
|
report.Results[i].Vulnerabilities[j].Description = "Modified by post-report hook"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init(t *testing.T) {
|
||||||
|
h := &testHook{}
|
||||||
|
extension.RegisterHook(h)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
extension.DeregisterHook(h.Name())
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/aquasecurity/trivy/pkg/cache"
|
"github.com/aquasecurity/trivy/pkg/cache"
|
||||||
"github.com/aquasecurity/trivy/pkg/commands/operation"
|
"github.com/aquasecurity/trivy/pkg/commands/operation"
|
||||||
"github.com/aquasecurity/trivy/pkg/db"
|
"github.com/aquasecurity/trivy/pkg/db"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/extension"
|
||||||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
||||||
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
|
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
|
||||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||||
@@ -277,7 +278,6 @@ func (r *runner) Report(ctx context.Context, opts flag.Options, report types.Rep
|
|||||||
if err := pkgReport.Write(ctx, report, opts); err != nil {
|
if err := pkgReport.Write(ctx, report, opts); err != nil {
|
||||||
return xerrors.Errorf("unable to write results: %w", err)
|
return xerrors.Errorf("unable to write results: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,12 +375,32 @@ func Run(ctx context.Context, opts flag.Options, targetKind TargetKind) (err err
|
|||||||
return v.SafeWriteConfigAs("trivy-default.yaml")
|
return v.SafeWriteConfigAs("trivy-default.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call pre-run hooks
|
||||||
|
if err := extension.PreRun(ctx, opts); err != nil {
|
||||||
|
return xerrors.Errorf("pre run error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the application
|
||||||
|
report, err := run(ctx, opts, targetKind)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("run error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call post-run hooks
|
||||||
|
if err := extension.PostRun(ctx, opts); err != nil {
|
||||||
|
return xerrors.Errorf("post run error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return operation.Exit(opts, report.Results.Failed(), report.Metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context, opts flag.Options, targetKind TargetKind) (types.Report, error) {
|
||||||
r, err := NewRunner(ctx, opts)
|
r, err := NewRunner(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, SkipScan) {
|
if errors.Is(err, SkipScan) {
|
||||||
return nil
|
return types.Report{}, nil
|
||||||
}
|
}
|
||||||
return xerrors.Errorf("init error: %w", err)
|
return types.Report{}, xerrors.Errorf("init error: %w", err)
|
||||||
}
|
}
|
||||||
defer r.Close(ctx)
|
defer r.Close(ctx)
|
||||||
|
|
||||||
@@ -395,24 +415,27 @@ func Run(ctx context.Context, opts flag.Options, targetKind TargetKind) (err err
|
|||||||
|
|
||||||
scanFunction, exists := scans[targetKind]
|
scanFunction, exists := scans[targetKind]
|
||||||
if !exists {
|
if !exists {
|
||||||
return xerrors.Errorf("unknown target kind: %s", targetKind)
|
return types.Report{}, xerrors.Errorf("unknown target kind: %s", targetKind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Scan the artifact
|
||||||
report, err := scanFunction(ctx, opts)
|
report, err := scanFunction(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("%s scan error: %w", targetKind, err)
|
return types.Report{}, xerrors.Errorf("%s scan error: %w", targetKind, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Filter the results
|
||||||
report, err = r.Filter(ctx, opts, report)
|
report, err = r.Filter(ctx, opts, report)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("filter error: %w", err)
|
return types.Report{}, xerrors.Errorf("filter error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Report the results
|
||||||
if err = r.Report(ctx, opts, report); err != nil {
|
if err = r.Report(ctx, opts, report); err != nil {
|
||||||
return xerrors.Errorf("report error: %w", err)
|
return types.Report{}, xerrors.Errorf("report error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return operation.Exit(opts, report.Results.Failed(), report.Metadata)
|
return report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func disabledAnalyzers(opts flag.Options) []analyzer.Type {
|
func disabledAnalyzers(opts flag.Options) []analyzer.Type {
|
||||||
|
|||||||
162
pkg/extension/hook.go
Normal file
162
pkg/extension/hook.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package extension
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/aquasecurity/trivy/pkg/flag"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hooks = make(map[string]Hook)
|
||||||
|
|
||||||
|
func RegisterHook(s Hook) {
|
||||||
|
// Avoid duplication
|
||||||
|
hooks[s.Name()] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeregisterHook(name string) {
|
||||||
|
delete(hooks, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook is an interface that defines the methods for a hook.
|
||||||
|
type Hook interface {
|
||||||
|
// Name returns the name of the extension.
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunHook is a extension that is called before and after all the processes.
|
||||||
|
type RunHook interface {
|
||||||
|
Hook
|
||||||
|
|
||||||
|
// PreRun is called before all the processes.
|
||||||
|
PreRun(ctx context.Context, opts flag.Options) error
|
||||||
|
|
||||||
|
// PostRun is called after all the processes.
|
||||||
|
PostRun(ctx context.Context, opts flag.Options) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanHook is a extension that is called before and after the scan.
|
||||||
|
type ScanHook interface {
|
||||||
|
Hook
|
||||||
|
|
||||||
|
// PreScan is called before the scan. It can modify the scan target.
|
||||||
|
// It may be called on the server side in client/server mode.
|
||||||
|
PreScan(ctx context.Context, target *types.ScanTarget, opts types.ScanOptions) error
|
||||||
|
|
||||||
|
// PostScan is called after the scan. It can modify the results.
|
||||||
|
// It may be called on the server side in client/server mode.
|
||||||
|
// NOTE: Wasm modules cannot directly modify the passed results,
|
||||||
|
// so it returns a copy of the results.
|
||||||
|
PostScan(ctx context.Context, results types.Results) (types.Results, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportHook is a extension that is called before and after the report is written.
|
||||||
|
type ReportHook interface {
|
||||||
|
Hook
|
||||||
|
|
||||||
|
// PreReport is called before the report is written.
|
||||||
|
// It can modify the report. It is called on the client side.
|
||||||
|
PreReport(ctx context.Context, report *types.Report, opts flag.Options) error
|
||||||
|
|
||||||
|
// PostReport is called after the report is written.
|
||||||
|
// It can modify the report. It is called on the client side.
|
||||||
|
PostReport(ctx context.Context, report *types.Report, opts flag.Options) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func PreRun(ctx context.Context, opts flag.Options) error {
|
||||||
|
for _, e := range Hooks() {
|
||||||
|
h, ok := e.(RunHook)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := h.PreRun(ctx, opts); err != nil {
|
||||||
|
return xerrors.Errorf("%s pre run error: %w", e.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostRun is a hook that is called after all the processes.
|
||||||
|
func PostRun(ctx context.Context, opts flag.Options) error {
|
||||||
|
for _, e := range Hooks() {
|
||||||
|
h, ok := e.(RunHook)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := h.PostRun(ctx, opts); err != nil {
|
||||||
|
return xerrors.Errorf("%s post run error: %w", e.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreScan is a hook that is called before the scan.
|
||||||
|
func PreScan(ctx context.Context, target *types.ScanTarget, options types.ScanOptions) error {
|
||||||
|
for _, e := range Hooks() {
|
||||||
|
h, ok := e.(ScanHook)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := h.PreScan(ctx, target, options); err != nil {
|
||||||
|
return xerrors.Errorf("%s pre scan error: %w", e.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostScan is a hook that is called after the scan.
|
||||||
|
func PostScan(ctx context.Context, results types.Results) (types.Results, error) {
|
||||||
|
var err error
|
||||||
|
for _, e := range Hooks() {
|
||||||
|
h, ok := e.(ScanHook)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
results, err = h.PostScan(ctx, results)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("%s post scan error: %w", e.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreReport is a hook that is called before the report is written.
|
||||||
|
func PreReport(ctx context.Context, report *types.Report, opts flag.Options) error {
|
||||||
|
for _, e := range Hooks() {
|
||||||
|
h, ok := e.(ReportHook)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := h.PreReport(ctx, report, opts); err != nil {
|
||||||
|
return xerrors.Errorf("%s pre report error: %w", e.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostReport is a hook that is called after the report is written.
|
||||||
|
func PostReport(ctx context.Context, report *types.Report, opts flag.Options) error {
|
||||||
|
for _, e := range Hooks() {
|
||||||
|
h, ok := e.(ReportHook)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := h.PostReport(ctx, report, opts); err != nil {
|
||||||
|
return xerrors.Errorf("%s post report error: %w", e.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hooks returns the list of hooks.
|
||||||
|
func Hooks() []Hook {
|
||||||
|
hooks := lo.Values(hooks)
|
||||||
|
sort.Slice(hooks, func(i, j int) bool {
|
||||||
|
return hooks[i].Name() < hooks[j].Name()
|
||||||
|
})
|
||||||
|
return hooks
|
||||||
|
}
|
||||||
278
pkg/extension/hook_test.go
Normal file
278
pkg/extension/hook_test.go
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
package extension_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||||
|
"github.com/aquasecurity/trivy/internal/hooktest"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/extension"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/flag"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostScan(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
results types.Results
|
||||||
|
want types.Results
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
results: types.Results{
|
||||||
|
{
|
||||||
|
Target: "test",
|
||||||
|
Vulnerabilities: []types.DetectedVulnerability{
|
||||||
|
{
|
||||||
|
VulnerabilityID: "CVE-2022-0001",
|
||||||
|
PkgName: "musl",
|
||||||
|
InstalledVersion: "1.2.3",
|
||||||
|
FixedVersion: "1.2.4",
|
||||||
|
Vulnerability: dbTypes.Vulnerability{
|
||||||
|
Severity: "CRITICAL",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: types.Results{
|
||||||
|
{
|
||||||
|
Target: "test",
|
||||||
|
Vulnerabilities: []types.DetectedVulnerability{
|
||||||
|
{
|
||||||
|
VulnerabilityID: "CVE-2022-0001",
|
||||||
|
PkgName: "musl",
|
||||||
|
InstalledVersion: "1.2.3",
|
||||||
|
FixedVersion: "1.2.4",
|
||||||
|
Vulnerability: dbTypes.Vulnerability{
|
||||||
|
Severity: "CRITICAL",
|
||||||
|
References: []string{
|
||||||
|
"https://example.com/post-scan",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sad path",
|
||||||
|
results: types.Results{
|
||||||
|
{
|
||||||
|
Target: "bad",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Initialize the test hook
|
||||||
|
hooktest.Init(t)
|
||||||
|
|
||||||
|
results, err := extension.PostScan(t.Context(), tt.results)
|
||||||
|
require.Equal(t, tt.wantErr, err != nil)
|
||||||
|
assert.Equal(t, tt.want, results)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreScan(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
target *types.ScanTarget
|
||||||
|
options types.ScanOptions
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
target: &types.ScanTarget{
|
||||||
|
Name: "test",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sad path",
|
||||||
|
target: &types.ScanTarget{
|
||||||
|
Name: "bad-pre",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Initialize the test hook
|
||||||
|
hooktest.Init(t)
|
||||||
|
|
||||||
|
err := extension.PreScan(t.Context(), tt.target, tt.options)
|
||||||
|
require.Equal(t, tt.wantErr, err != nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreRun(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
opts flag.Options
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
opts: flag.Options{},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sad path",
|
||||||
|
opts: flag.Options{
|
||||||
|
GlobalOptions: flag.GlobalOptions{
|
||||||
|
ConfigFile: "bad-config",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Initialize the test hook
|
||||||
|
hooktest.Init(t)
|
||||||
|
|
||||||
|
err := extension.PreRun(t.Context(), tt.opts)
|
||||||
|
require.Equal(t, tt.wantErr, err != nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostRun(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
opts flag.Options
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
opts: flag.Options{},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sad path",
|
||||||
|
opts: flag.Options{
|
||||||
|
GlobalOptions: flag.GlobalOptions{
|
||||||
|
ConfigFile: "bad-config",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Initialize the test extension
|
||||||
|
hooktest.Init(t)
|
||||||
|
|
||||||
|
err := extension.PostRun(t.Context(), tt.opts)
|
||||||
|
require.Equal(t, tt.wantErr, err != nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreReport(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
report *types.Report
|
||||||
|
opts flag.Options
|
||||||
|
wantTitle string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
report: &types.Report{
|
||||||
|
Results: types.Results{
|
||||||
|
{
|
||||||
|
Vulnerabilities: []types.DetectedVulnerability{
|
||||||
|
{
|
||||||
|
VulnerabilityID: "CVE-2022-0001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantTitle: "Modified by pre-report hook",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sad path",
|
||||||
|
report: &types.Report{
|
||||||
|
ArtifactName: "bad-report",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Initialize the test hook
|
||||||
|
hooktest.Init(t)
|
||||||
|
|
||||||
|
err := extension.PreReport(t.Context(), tt.report, tt.opts)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Len(t, tt.report.Results, 1)
|
||||||
|
require.Len(t, tt.report.Results[0].Vulnerabilities, 1)
|
||||||
|
assert.Equal(t, tt.wantTitle, tt.report.Results[0].Vulnerabilities[0].Title)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostReport(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
report *types.Report
|
||||||
|
opts flag.Options
|
||||||
|
wantDescription string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
report: &types.Report{
|
||||||
|
Results: types.Results{
|
||||||
|
{
|
||||||
|
Vulnerabilities: []types.DetectedVulnerability{
|
||||||
|
{
|
||||||
|
VulnerabilityID: "CVE-2022-0001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantDescription: "Modified by post-report hook",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sad path",
|
||||||
|
report: &types.Report{
|
||||||
|
ArtifactName: "bad-report",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Initialize the test hook
|
||||||
|
hooktest.Init(t)
|
||||||
|
|
||||||
|
err := extension.PostReport(t.Context(), tt.report, tt.opts)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Len(t, tt.report.Results, 1)
|
||||||
|
require.Len(t, tt.report.Results[0].Vulnerabilities, 1)
|
||||||
|
assert.Equal(t, tt.wantDescription, tt.report.Results[0].Vulnerabilities[0].Description)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package flag
|
package flag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aquasecurity/trivy/pkg/module"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// e.g. config yaml
|
// e.g. config yaml
|
||||||
@@ -14,7 +16,7 @@ var (
|
|||||||
ModuleDirFlag = Flag[string]{
|
ModuleDirFlag = Flag[string]{
|
||||||
Name: "module-dir",
|
Name: "module-dir",
|
||||||
ConfigName: "module.dir",
|
ConfigName: "module.dir",
|
||||||
Default: module.DefaultDir,
|
Default: filepath.Join(fsutils.HomeDir(), ".trivy", "modules"),
|
||||||
Usage: "specify directory to the wasm modules that will be loaded",
|
Usage: "specify directory to the wasm modules that will be loaded",
|
||||||
Persistent: true,
|
Persistent: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,12 @@ import (
|
|||||||
wasi "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
|
wasi "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/aquasecurity/trivy/pkg/extension"
|
||||||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
||||||
"github.com/aquasecurity/trivy/pkg/log"
|
"github.com/aquasecurity/trivy/pkg/log"
|
||||||
tapi "github.com/aquasecurity/trivy/pkg/module/api"
|
tapi "github.com/aquasecurity/trivy/pkg/module/api"
|
||||||
"github.com/aquasecurity/trivy/pkg/module/serialize"
|
"github.com/aquasecurity/trivy/pkg/module/serialize"
|
||||||
"github.com/aquasecurity/trivy/pkg/scan/post"
|
|
||||||
"github.com/aquasecurity/trivy/pkg/types"
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -33,10 +32,6 @@ var (
|
|||||||
"warn": logWarn,
|
"warn": logWarn,
|
||||||
"error": logError,
|
"error": logError,
|
||||||
}
|
}
|
||||||
|
|
||||||
RelativeDir = filepath.Join(".trivy", "modules")
|
|
||||||
|
|
||||||
DefaultDir = dir()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// logDebug is defined as an api.GoModuleFunc for lower overhead vs reflection.
|
// logDebug is defined as an api.GoModuleFunc for lower overhead vs reflection.
|
||||||
@@ -172,7 +167,7 @@ func (m *Manager) Register() {
|
|||||||
func (m *Manager) Deregister() {
|
func (m *Manager) Deregister() {
|
||||||
for _, mod := range m.modules {
|
for _, mod := range m.modules {
|
||||||
analyzer.DeregisterAnalyzer(analyzer.Type(mod.Name()))
|
analyzer.DeregisterAnalyzer(analyzer.Type(mod.Name()))
|
||||||
post.DeregisterPostScanner(mod.Name())
|
extension.DeregisterHook(mod.Name())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,6 +257,8 @@ func marshal(ctx context.Context, m api.Module, malloc api.Function, v any) (uin
|
|||||||
return ptr, size, nil
|
return ptr, size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ extension.ScanHook = (*wasmModule)(nil)
|
||||||
|
|
||||||
type wasmModule struct {
|
type wasmModule struct {
|
||||||
mod api.Module
|
mod api.Module
|
||||||
memFS *memFS
|
memFS *memFS
|
||||||
@@ -416,7 +413,7 @@ func (m *wasmModule) Register() {
|
|||||||
}
|
}
|
||||||
if m.isPostScanner {
|
if m.isPostScanner {
|
||||||
logger.Debug("Registering custom post scanner")
|
logger.Debug("Registering custom post scanner")
|
||||||
post.RegisterPostScanner(m)
|
extension.RegisterHook(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,8 +483,11 @@ func (m *wasmModule) Analyze(ctx context.Context, input analyzer.AnalysisInput)
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostScan performs post scanning
|
func (m *wasmModule) PreScan(ctx context.Context, target *types.ScanTarget, options types.ScanOptions) error {
|
||||||
// e.g. Remove a vulnerability, change severity, etc.
|
// TODO: Implement
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *wasmModule) PostScan(ctx context.Context, results types.Results) (types.Results, error) {
|
func (m *wasmModule) PostScan(ctx context.Context, results types.Results) (types.Results, error) {
|
||||||
// Find custom resources
|
// Find custom resources
|
||||||
var custom types.Result
|
var custom types.Result
|
||||||
@@ -746,10 +746,6 @@ func isType(ctx context.Context, mod api.Module, name string) (bool, error) {
|
|||||||
return isRes[0] > 0, nil
|
return isRes[0] > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dir() string {
|
|
||||||
return filepath.Join(fsutils.HomeDir(), RelativeDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func modulePostScanSpec(ctx context.Context, mod api.Module, freeFn api.Function) (serialize.PostScanSpec, error) {
|
func modulePostScanSpec(ctx context.Context, mod api.Module, freeFn api.Function) (serialize.PostScanSpec, error) {
|
||||||
postScanSpecFunc := mod.ExportedFunction("post_scan_spec")
|
postScanSpecFunc := mod.ExportedFunction("post_scan_spec")
|
||||||
if postScanSpecFunc == nil {
|
if postScanSpecFunc == nil {
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/aquasecurity/trivy/pkg/extension"
|
||||||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
||||||
"github.com/aquasecurity/trivy/pkg/module"
|
"github.com/aquasecurity/trivy/pkg/module"
|
||||||
"github.com/aquasecurity/trivy/pkg/scan/post"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestManager_Register(t *testing.T) {
|
func TestManager_Register(t *testing.T) {
|
||||||
@@ -24,7 +25,7 @@ func TestManager_Register(t *testing.T) {
|
|||||||
moduleDir string
|
moduleDir string
|
||||||
enabledModules []string
|
enabledModules []string
|
||||||
wantAnalyzerVersions analyzer.Versions
|
wantAnalyzerVersions analyzer.Versions
|
||||||
wantPostScannerVersions map[string]int
|
wantExtentions []string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -36,8 +37,8 @@ func TestManager_Register(t *testing.T) {
|
|||||||
},
|
},
|
||||||
PostAnalyzers: make(map[string]int),
|
PostAnalyzers: make(map[string]int),
|
||||||
},
|
},
|
||||||
wantPostScannerVersions: map[string]int{
|
wantExtentions: []string{
|
||||||
"happy": 1,
|
"happy",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -49,7 +50,7 @@ func TestManager_Register(t *testing.T) {
|
|||||||
},
|
},
|
||||||
PostAnalyzers: make(map[string]int),
|
PostAnalyzers: make(map[string]int),
|
||||||
},
|
},
|
||||||
wantPostScannerVersions: make(map[string]int),
|
wantExtentions: []string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only post scanner",
|
name: "only post scanner",
|
||||||
@@ -58,8 +59,8 @@ func TestManager_Register(t *testing.T) {
|
|||||||
Analyzers: make(map[string]int),
|
Analyzers: make(map[string]int),
|
||||||
PostAnalyzers: make(map[string]int),
|
PostAnalyzers: make(map[string]int),
|
||||||
},
|
},
|
||||||
wantPostScannerVersions: map[string]int{
|
wantExtentions: []string{
|
||||||
"scanner": 2,
|
"scanner",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -69,7 +70,7 @@ func TestManager_Register(t *testing.T) {
|
|||||||
Analyzers: make(map[string]int),
|
Analyzers: make(map[string]int),
|
||||||
PostAnalyzers: make(map[string]int),
|
PostAnalyzers: make(map[string]int),
|
||||||
},
|
},
|
||||||
wantPostScannerVersions: make(map[string]int),
|
wantExtentions: []string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "pass enabled modules",
|
name: "pass enabled modules",
|
||||||
@@ -85,8 +86,8 @@ func TestManager_Register(t *testing.T) {
|
|||||||
},
|
},
|
||||||
PostAnalyzers: make(map[string]int),
|
PostAnalyzers: make(map[string]int),
|
||||||
},
|
},
|
||||||
wantPostScannerVersions: map[string]int{
|
wantExtentions: []string{
|
||||||
"happy": 1,
|
"happy",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -124,9 +125,10 @@ func TestManager_Register(t *testing.T) {
|
|||||||
got := a.AnalyzerVersions()
|
got := a.AnalyzerVersions()
|
||||||
assert.Equal(t, tt.wantAnalyzerVersions, got)
|
assert.Equal(t, tt.wantAnalyzerVersions, got)
|
||||||
|
|
||||||
// Confirm the post scanner is registered
|
hookNames := lo.Map(extension.Hooks(), func(hook extension.Hook, _ int) string {
|
||||||
gotScannerVersions := post.ScannerVersions()
|
return hook.Name()
|
||||||
assert.Equal(t, tt.wantPostScannerVersions, gotScannerVersions)
|
})
|
||||||
|
assert.Equal(t, tt.wantExtentions, hookNames)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
cr "github.com/aquasecurity/trivy/pkg/compliance/report"
|
cr "github.com/aquasecurity/trivy/pkg/compliance/report"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/extension"
|
||||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||||
"github.com/aquasecurity/trivy/pkg/flag"
|
"github.com/aquasecurity/trivy/pkg/flag"
|
||||||
"github.com/aquasecurity/trivy/pkg/log"
|
"github.com/aquasecurity/trivy/pkg/log"
|
||||||
@@ -26,6 +27,11 @@ const (
|
|||||||
|
|
||||||
// Write writes the result to output, format as passed in argument
|
// Write writes the result to output, format as passed in argument
|
||||||
func Write(ctx context.Context, report types.Report, option flag.Options) (err error) {
|
func Write(ctx context.Context, report types.Report, option flag.Options) (err error) {
|
||||||
|
// Call pre-report hooks
|
||||||
|
if err := extension.PreReport(ctx, &report, option); err != nil {
|
||||||
|
return xerrors.Errorf("pre report error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
output, cleanup, err := option.OutputWriter(ctx)
|
output, cleanup, err := option.OutputWriter(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("failed to create a file: %w", err)
|
return xerrors.Errorf("failed to create a file: %w", err)
|
||||||
@@ -106,6 +112,11 @@ func Write(ctx context.Context, report types.Report, option flag.Options) (err e
|
|||||||
return xerrors.Errorf("failed to write results: %w", err)
|
return xerrors.Errorf("failed to write results: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call post-report hooks
|
||||||
|
if err := extension.PostReport(ctx, &report, option); err != nil {
|
||||||
|
return xerrors.Errorf("post report error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package report_test
|
package report_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||||
|
"github.com/aquasecurity/trivy/internal/hooktest"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/flag"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/report"
|
||||||
"github.com/aquasecurity/trivy/pkg/types"
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,3 +88,93 @@ func TestResults_Failed(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWrite(t *testing.T) {
|
||||||
|
testReport := types.Report{
|
||||||
|
SchemaVersion: report.SchemaVersion,
|
||||||
|
ArtifactName: "test-artifact",
|
||||||
|
Results: types.Results{
|
||||||
|
{
|
||||||
|
Target: "test-target",
|
||||||
|
Vulnerabilities: []types.DetectedVulnerability{
|
||||||
|
{
|
||||||
|
VulnerabilityID: "CVE-2021-0001",
|
||||||
|
PkgName: "test-pkg",
|
||||||
|
Vulnerability: dbTypes.Vulnerability{
|
||||||
|
Title: "Test Vulnerability Title",
|
||||||
|
Description: "This is a test description of a vulnerability",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testTemplate := "{{ range . }}{{ range .Vulnerabilities }}- {{ .VulnerabilityID }}: {{ .Title }}\n {{ .Description }}\n{{ end }}{{ end }}"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setUpHook bool
|
||||||
|
report types.Report
|
||||||
|
options flag.Options
|
||||||
|
wantOutput string
|
||||||
|
wantTitle string // Expected title after function call
|
||||||
|
wantDesc string // Expected description after function call
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "template with title and description",
|
||||||
|
report: testReport,
|
||||||
|
options: flag.Options{
|
||||||
|
ReportOptions: flag.ReportOptions{
|
||||||
|
Format: types.FormatTemplate,
|
||||||
|
Template: testTemplate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOutput: "- CVE-2021-0001: Test Vulnerability Title\n This is a test description of a vulnerability\n",
|
||||||
|
wantTitle: "Test Vulnerability Title", // Should remain unchanged
|
||||||
|
wantDesc: "This is a test description of a vulnerability", // Should remain unchanged
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "report modified by hooks",
|
||||||
|
setUpHook: true,
|
||||||
|
report: testReport,
|
||||||
|
options: flag.Options{
|
||||||
|
ReportOptions: flag.ReportOptions{
|
||||||
|
Format: types.FormatTemplate,
|
||||||
|
Template: testTemplate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// The template output only reflects the pre-report hook changes because
|
||||||
|
// the post-report hook runs AFTER the output is written.
|
||||||
|
// However, the report object itself is modified by both pre and post hooks.
|
||||||
|
wantOutput: "- CVE-2021-0001: Modified by pre-report hook\n This is a test description of a vulnerability\n",
|
||||||
|
wantTitle: "Modified by pre-report hook",
|
||||||
|
wantDesc: "Modified by post-report hook",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.setUpHook {
|
||||||
|
hooktest.Init(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a buffer to capture the output
|
||||||
|
output := new(bytes.Buffer)
|
||||||
|
tt.options.SetOutputWriter(output)
|
||||||
|
|
||||||
|
// Execute the Write function
|
||||||
|
err := report.Write(t.Context(), tt.report, tt.options)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify the output matches the expected template rendering
|
||||||
|
got := output.String()
|
||||||
|
assert.Equal(t, tt.wantOutput, got, "Template output does not match wanted value")
|
||||||
|
|
||||||
|
// Verify that the title and description in the report match the expected values
|
||||||
|
require.Len(t, tt.report.Results, 1)
|
||||||
|
require.Len(t, tt.report.Results[0].Vulnerabilities, 1)
|
||||||
|
assert.Equal(t, tt.wantTitle, tt.report.Results[0].Vulnerabilities[0].Title)
|
||||||
|
assert.Equal(t, tt.wantDesc, tt.report.Results[0].Vulnerabilities[0].Description)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||||
ospkgDetector "github.com/aquasecurity/trivy/pkg/detector/ospkg"
|
ospkgDetector "github.com/aquasecurity/trivy/pkg/detector/ospkg"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/extension"
|
||||||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
||||||
"github.com/aquasecurity/trivy/pkg/fanal/applier"
|
"github.com/aquasecurity/trivy/pkg/fanal/applier"
|
||||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||||
@@ -23,7 +24,6 @@ import (
|
|||||||
"github.com/aquasecurity/trivy/pkg/log"
|
"github.com/aquasecurity/trivy/pkg/log"
|
||||||
"github.com/aquasecurity/trivy/pkg/scan/langpkg"
|
"github.com/aquasecurity/trivy/pkg/scan/langpkg"
|
||||||
"github.com/aquasecurity/trivy/pkg/scan/ospkg"
|
"github.com/aquasecurity/trivy/pkg/scan/ospkg"
|
||||||
"github.com/aquasecurity/trivy/pkg/scan/post"
|
|
||||||
"github.com/aquasecurity/trivy/pkg/set"
|
"github.com/aquasecurity/trivy/pkg/set"
|
||||||
"github.com/aquasecurity/trivy/pkg/types"
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
"github.com/aquasecurity/trivy/pkg/vulnerability"
|
"github.com/aquasecurity/trivy/pkg/vulnerability"
|
||||||
@@ -49,7 +49,7 @@ type Service struct {
|
|||||||
vulnClient vulnerability.Client
|
vulnClient vulnerability.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService is the factory method for Scanner
|
// NewService is the factory method for scan service
|
||||||
func NewService(a applier.Applier, osPkgScanner ospkg.Scanner, langPkgScanner langpkg.Scanner,
|
func NewService(a applier.Applier, osPkgScanner ospkg.Scanner, langPkgScanner langpkg.Scanner,
|
||||||
vulnClient vulnerability.Client) Service {
|
vulnClient vulnerability.Client) Service {
|
||||||
return Service{
|
return Service{
|
||||||
@@ -113,6 +113,11 @@ func (s Service) Scan(ctx context.Context, targetName, artifactKey string, blobK
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) ScanTarget(ctx context.Context, target types.ScanTarget, options types.ScanOptions) (types.Results, ftypes.OS, error) {
|
func (s Service) ScanTarget(ctx context.Context, target types.ScanTarget, options types.ScanOptions) (types.Results, ftypes.OS, error) {
|
||||||
|
// Call pre-scan hooks
|
||||||
|
if err := extension.PreScan(ctx, &target, options); err != nil {
|
||||||
|
return nil, ftypes.OS{}, xerrors.Errorf("pre scan error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var results types.Results
|
var results types.Results
|
||||||
|
|
||||||
// Filter packages according to the options
|
// Filter packages according to the options
|
||||||
@@ -148,9 +153,8 @@ func (s Service) ScanTarget(ctx context.Context, target types.ScanTarget, option
|
|||||||
s.vulnClient.FillInfo(results[i].Vulnerabilities, options.VulnSeveritySources)
|
s.vulnClient.FillInfo(results[i].Vulnerabilities, options.VulnSeveritySources)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post scanning
|
// Call post-scan hooks
|
||||||
results, err = post.Scan(ctx, results)
|
if results, err = extension.PostScan(ctx, results); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, ftypes.OS{}, xerrors.Errorf("post scan error: %w", err)
|
return nil, ftypes.OS{}, xerrors.Errorf("post scan error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/aquasecurity/trivy-db/pkg/db"
|
"github.com/aquasecurity/trivy-db/pkg/db"
|
||||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||||
"github.com/aquasecurity/trivy/internal/dbtest"
|
"github.com/aquasecurity/trivy/internal/dbtest"
|
||||||
|
"github.com/aquasecurity/trivy/internal/hooktest"
|
||||||
"github.com/aquasecurity/trivy/pkg/cache"
|
"github.com/aquasecurity/trivy/pkg/cache"
|
||||||
"github.com/aquasecurity/trivy/pkg/fanal/applier"
|
"github.com/aquasecurity/trivy/pkg/fanal/applier"
|
||||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||||
@@ -151,6 +152,7 @@ func TestScanner_Scan(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
fixtures []string
|
fixtures []string
|
||||||
|
setUpHook bool
|
||||||
setupCache func(t *testing.T) cache.Cache
|
setupCache func(t *testing.T) cache.Cache
|
||||||
wantResults types.Results
|
wantResults types.Results
|
||||||
wantOS ftypes.OS
|
wantOS ftypes.OS
|
||||||
@@ -909,6 +911,75 @@ func TestScanner_Scan(t *testing.T) {
|
|||||||
Name: "3.11",
|
Name: "3.11",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "happy path with hooks",
|
||||||
|
args: args{
|
||||||
|
target: "alpine:latest",
|
||||||
|
layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"},
|
||||||
|
options: types.ScanOptions{
|
||||||
|
PkgTypes: []string{types.PkgTypeOS},
|
||||||
|
PkgRelationships: ftypes.Relationships,
|
||||||
|
Scanners: types.Scanners{types.VulnerabilityScanner},
|
||||||
|
VulnSeveritySources: []dbTypes.SourceID{"auto"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fixtures: []string{"testdata/fixtures/happy.yaml"},
|
||||||
|
setUpHook: true,
|
||||||
|
setupCache: func(t *testing.T) cache.Cache {
|
||||||
|
c := cache.NewMemoryCache()
|
||||||
|
require.NoError(t, c.PutBlob("sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{
|
||||||
|
SchemaVersion: ftypes.BlobJSONSchemaVersion,
|
||||||
|
OS: ftypes.OS{
|
||||||
|
Family: ftypes.Alpine,
|
||||||
|
Name: "3.11",
|
||||||
|
},
|
||||||
|
PackageInfos: []ftypes.PackageInfo{
|
||||||
|
{
|
||||||
|
FilePath: "lib/apk/db/installed",
|
||||||
|
Packages: []ftypes.Package{muslPkg},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
return c
|
||||||
|
},
|
||||||
|
wantResults: types.Results{
|
||||||
|
{
|
||||||
|
Target: "alpine:latest (pre-scan) (alpine 3.11)",
|
||||||
|
Class: types.ClassOSPkg,
|
||||||
|
Type: ftypes.Alpine,
|
||||||
|
Packages: ftypes.Packages{
|
||||||
|
muslPkg,
|
||||||
|
},
|
||||||
|
Vulnerabilities: []types.DetectedVulnerability{
|
||||||
|
{
|
||||||
|
VulnerabilityID: "CVE-2020-9999",
|
||||||
|
PkgName: muslPkg.Name,
|
||||||
|
PkgIdentifier: muslPkg.Identifier,
|
||||||
|
InstalledVersion: muslPkg.Version,
|
||||||
|
FixedVersion: "1.2.4",
|
||||||
|
Status: dbTypes.StatusFixed,
|
||||||
|
Layer: ftypes.Layer{
|
||||||
|
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
|
||||||
|
},
|
||||||
|
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-9999",
|
||||||
|
Vulnerability: dbTypes.Vulnerability{
|
||||||
|
Title: "dos",
|
||||||
|
Description: "dos vulnerability",
|
||||||
|
Severity: "HIGH",
|
||||||
|
References: []string{
|
||||||
|
"https://example.com/post-scan", // modified by post-scan hook
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOS: ftypes.OS{
|
||||||
|
Family: "alpine",
|
||||||
|
Name: "3.11",
|
||||||
|
Eosl: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "happy path with misconfigurations",
|
name: "happy path with misconfigurations",
|
||||||
args: args{
|
args: args{
|
||||||
@@ -1242,6 +1313,10 @@ func TestScanner_Scan(t *testing.T) {
|
|||||||
_ = dbtest.InitDB(t, tt.fixtures)
|
_ = dbtest.InitDB(t, tt.fixtures)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
|
if tt.setUpHook {
|
||||||
|
hooktest.Init(t)
|
||||||
|
}
|
||||||
|
|
||||||
c := tt.setupCache(t)
|
c := tt.setupCache(t)
|
||||||
a := applier.NewApplier(c)
|
a := applier.NewApplier(c)
|
||||||
s := NewService(a, ospkg.NewScanner(), langpkg.NewScanner(), vulnerability.NewClient(db.Config{}))
|
s := NewService(a, ospkg.NewScanner(), langpkg.NewScanner(), vulnerability.NewClient(db.Config{}))
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package post
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"golang.org/x/xerrors"
|
|
||||||
|
|
||||||
"github.com/aquasecurity/trivy/pkg/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Scanner interface {
|
|
||||||
Name() string
|
|
||||||
Version() int
|
|
||||||
PostScan(ctx context.Context, results types.Results) (types.Results, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegisterPostScanner(s Scanner) {
|
|
||||||
// Avoid duplication
|
|
||||||
postScanners[s.Name()] = s
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeregisterPostScanner(name string) {
|
|
||||||
delete(postScanners, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ScannerVersions() map[string]int {
|
|
||||||
versions := make(map[string]int)
|
|
||||||
for _, s := range postScanners {
|
|
||||||
versions[s.Name()] = s.Version()
|
|
||||||
}
|
|
||||||
return versions
|
|
||||||
}
|
|
||||||
|
|
||||||
var postScanners = make(map[string]Scanner)
|
|
||||||
|
|
||||||
func Scan(ctx context.Context, results types.Results) (types.Results, error) {
|
|
||||||
var err error
|
|
||||||
for _, s := range postScanners {
|
|
||||||
results, err = s.PostScan(ctx, results)
|
|
||||||
if err != nil {
|
|
||||||
return nil, xerrors.Errorf("%s post scan error: %w", s.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package post_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
|
||||||
"github.com/aquasecurity/trivy/pkg/scan/post"
|
|
||||||
"github.com/aquasecurity/trivy/pkg/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testPostScanner struct{}
|
|
||||||
|
|
||||||
func (testPostScanner) Name() string {
|
|
||||||
return "test"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (testPostScanner) Version() int {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (testPostScanner) PostScan(ctx context.Context, results types.Results) (types.Results, error) {
|
|
||||||
for i, r := range results {
|
|
||||||
if r.Target == "bad" {
|
|
||||||
return nil, errors.New("bad")
|
|
||||||
}
|
|
||||||
for j := range r.Vulnerabilities {
|
|
||||||
results[i].Vulnerabilities[j].Severity = "LOW"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestScan(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
results types.Results
|
|
||||||
want types.Results
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "happy path",
|
|
||||||
results: types.Results{
|
|
||||||
{
|
|
||||||
Target: "test",
|
|
||||||
Vulnerabilities: []types.DetectedVulnerability{
|
|
||||||
{
|
|
||||||
VulnerabilityID: "CVE-2022-0001",
|
|
||||||
PkgName: "musl",
|
|
||||||
InstalledVersion: "1.2.3",
|
|
||||||
FixedVersion: "1.2.4",
|
|
||||||
Vulnerability: dbTypes.Vulnerability{
|
|
||||||
Severity: "CRITICAL",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: types.Results{
|
|
||||||
{
|
|
||||||
Target: "test",
|
|
||||||
Vulnerabilities: []types.DetectedVulnerability{
|
|
||||||
{
|
|
||||||
VulnerabilityID: "CVE-2022-0001",
|
|
||||||
PkgName: "musl",
|
|
||||||
InstalledVersion: "1.2.3",
|
|
||||||
FixedVersion: "1.2.4",
|
|
||||||
Vulnerability: dbTypes.Vulnerability{
|
|
||||||
Severity: "LOW",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sad path",
|
|
||||||
results: types.Results{
|
|
||||||
{
|
|
||||||
Target: "bad",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
s := testPostScanner{}
|
|
||||||
post.RegisterPostScanner(s)
|
|
||||||
defer func() {
|
|
||||||
post.DeregisterPostScanner(s.Name())
|
|
||||||
}()
|
|
||||||
|
|
||||||
results, err := post.Scan(t.Context(), tt.results)
|
|
||||||
require.Equal(t, tt.wantErr, err != nil)
|
|
||||||
assert.Equal(t, tt.want, results)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user