mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-21 14:50:53 -08:00
Co-authored-by: Liam Galvin <liam.galvin@aquasec.com> Co-authored-by: Liam Galvin <liamgalvin@protonmail.com>
558 lines
12 KiB
Go
558 lines
12 KiB
Go
package analyzer_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sync/semaphore"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/aquasecurity/fanal/analyzer"
|
|
aos "github.com/aquasecurity/fanal/analyzer/os"
|
|
"github.com/aquasecurity/fanal/types"
|
|
dio "github.com/aquasecurity/go-dep-parser/pkg/io"
|
|
|
|
_ "github.com/aquasecurity/fanal/analyzer/command/apk"
|
|
_ "github.com/aquasecurity/fanal/analyzer/language/ruby/bundler"
|
|
_ "github.com/aquasecurity/fanal/analyzer/os/alpine"
|
|
_ "github.com/aquasecurity/fanal/analyzer/os/ubuntu"
|
|
_ "github.com/aquasecurity/fanal/analyzer/pkg/apk"
|
|
_ "github.com/aquasecurity/fanal/analyzer/repo/apk"
|
|
_ "github.com/aquasecurity/fanal/handler/all"
|
|
)
|
|
|
|
type mockConfigAnalyzer struct{}
|
|
|
|
func (mockConfigAnalyzer) Required(targetOS types.OS) bool {
|
|
return targetOS.Family == "alpine"
|
|
}
|
|
|
|
func (mockConfigAnalyzer) Analyze(targetOS types.OS, configBlob []byte) ([]types.Package, error) {
|
|
if string(configBlob) != `foo` {
|
|
return nil, errors.New("error")
|
|
}
|
|
return []types.Package{
|
|
{Name: "musl", Version: "1.1.24-r2"},
|
|
}, nil
|
|
}
|
|
|
|
func (mockConfigAnalyzer) Type() analyzer.Type {
|
|
return analyzer.Type("test")
|
|
}
|
|
|
|
func (mockConfigAnalyzer) Version() int {
|
|
return 1
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
analyzer.RegisterConfigAnalyzer(mockConfigAnalyzer{})
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestAnalysisResult_Merge(t *testing.T) {
|
|
type fields struct {
|
|
m sync.Mutex
|
|
OS *types.OS
|
|
PackageInfos []types.PackageInfo
|
|
Applications []types.Application
|
|
}
|
|
type args struct {
|
|
new *analyzer.AnalysisResult
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want analyzer.AnalysisResult
|
|
}{
|
|
{
|
|
name: "happy path",
|
|
fields: fields{
|
|
OS: &types.OS{
|
|
Family: aos.Debian,
|
|
Name: "9.8",
|
|
},
|
|
PackageInfos: []types.PackageInfo{
|
|
{
|
|
FilePath: "var/lib/dpkg/status.d/libc",
|
|
Packages: []types.Package{
|
|
{Name: "libc", Version: "1.2.3"},
|
|
},
|
|
},
|
|
},
|
|
Applications: []types.Application{
|
|
{
|
|
Type: "bundler",
|
|
FilePath: "app/Gemfile.lock",
|
|
Libraries: []types.Package{
|
|
{
|
|
Name: "rails",
|
|
Version: "5.0.0",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
new: &analyzer.AnalysisResult{
|
|
PackageInfos: []types.PackageInfo{
|
|
{
|
|
FilePath: "var/lib/dpkg/status.d/openssl",
|
|
Packages: []types.Package{
|
|
{Name: "openssl", Version: "1.1.1"},
|
|
},
|
|
},
|
|
},
|
|
Applications: []types.Application{
|
|
{
|
|
Type: "bundler",
|
|
FilePath: "app2/Gemfile.lock",
|
|
Libraries: []types.Package{
|
|
{
|
|
Name: "nokogiri",
|
|
Version: "1.0.0",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: analyzer.AnalysisResult{
|
|
OS: &types.OS{
|
|
Family: aos.Debian,
|
|
Name: "9.8",
|
|
},
|
|
PackageInfos: []types.PackageInfo{
|
|
{
|
|
FilePath: "var/lib/dpkg/status.d/libc",
|
|
Packages: []types.Package{
|
|
{Name: "libc", Version: "1.2.3"},
|
|
},
|
|
},
|
|
{
|
|
FilePath: "var/lib/dpkg/status.d/openssl",
|
|
Packages: []types.Package{
|
|
{Name: "openssl", Version: "1.1.1"},
|
|
},
|
|
},
|
|
},
|
|
Applications: []types.Application{
|
|
{
|
|
Type: "bundler",
|
|
FilePath: "app/Gemfile.lock",
|
|
Libraries: []types.Package{
|
|
{
|
|
Name: "rails",
|
|
Version: "5.0.0",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "bundler",
|
|
FilePath: "app2/Gemfile.lock",
|
|
Libraries: []types.Package{
|
|
{
|
|
Name: "nokogiri",
|
|
Version: "1.0.0",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "redhat must be replaced with oracle",
|
|
fields: fields{
|
|
OS: &types.OS{
|
|
Family: aos.RedHat, // this must be overwritten
|
|
Name: "8.0",
|
|
},
|
|
},
|
|
args: args{
|
|
new: &analyzer.AnalysisResult{
|
|
OS: &types.OS{
|
|
Family: aos.Oracle,
|
|
Name: "8.0",
|
|
},
|
|
},
|
|
},
|
|
want: analyzer.AnalysisResult{
|
|
OS: &types.OS{
|
|
Family: aos.Oracle,
|
|
Name: "8.0",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "debian must be replaced with ubuntu",
|
|
fields: fields{
|
|
OS: &types.OS{
|
|
Family: aos.Debian, // this must be overwritten
|
|
Name: "9.0",
|
|
},
|
|
},
|
|
args: args{
|
|
new: &analyzer.AnalysisResult{
|
|
OS: &types.OS{
|
|
Family: aos.Ubuntu,
|
|
Name: "18.04",
|
|
},
|
|
},
|
|
},
|
|
want: analyzer.AnalysisResult{
|
|
OS: &types.OS{
|
|
Family: aos.Ubuntu,
|
|
Name: "18.04",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "alpine OS needs to be extended with apk repositories",
|
|
fields: fields{
|
|
OS: &types.OS{
|
|
Family: aos.Alpine,
|
|
Name: "3.15.3",
|
|
},
|
|
},
|
|
args: args{
|
|
new: &analyzer.AnalysisResult{
|
|
Repository: &types.Repository{
|
|
Family: aos.Alpine,
|
|
Release: "edge",
|
|
},
|
|
},
|
|
},
|
|
want: analyzer.AnalysisResult{
|
|
OS: &types.OS{
|
|
Family: aos.Alpine,
|
|
Name: "3.15.3",
|
|
},
|
|
Repository: &types.Repository{
|
|
Family: aos.Alpine,
|
|
Release: "edge",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "alpine must not be replaced with oracle",
|
|
fields: fields{
|
|
OS: &types.OS{
|
|
Family: aos.Alpine, // this must not be overwritten
|
|
Name: "3.11",
|
|
},
|
|
},
|
|
args: args{
|
|
new: &analyzer.AnalysisResult{
|
|
OS: &types.OS{
|
|
Family: aos.Oracle,
|
|
Name: "8.0",
|
|
},
|
|
},
|
|
},
|
|
want: analyzer.AnalysisResult{
|
|
OS: &types.OS{
|
|
Family: aos.Alpine, // this must not be overwritten
|
|
Name: "3.11",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := analyzer.AnalysisResult{
|
|
OS: tt.fields.OS,
|
|
PackageInfos: tt.fields.PackageInfos,
|
|
Applications: tt.fields.Applications,
|
|
}
|
|
r.Merge(tt.args.new)
|
|
assert.Equal(t, tt.want, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeFile(t *testing.T) {
|
|
type args struct {
|
|
filePath string
|
|
testFilePath string
|
|
disabledAnalyzers []analyzer.Type
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want *analyzer.AnalysisResult
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "happy path with os analyzer",
|
|
args: args{
|
|
filePath: "/etc/alpine-release",
|
|
testFilePath: "testdata/etc/alpine-release",
|
|
},
|
|
want: &analyzer.AnalysisResult{
|
|
OS: &types.OS{
|
|
Family: "alpine",
|
|
Name: "3.11.6",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "happy path with disabled os analyzer",
|
|
args: args{
|
|
filePath: "/etc/alpine-release",
|
|
testFilePath: "testdata/etc/alpine-release",
|
|
disabledAnalyzers: []analyzer.Type{analyzer.TypeAlpine},
|
|
},
|
|
want: &analyzer.AnalysisResult{},
|
|
},
|
|
{
|
|
name: "happy path with package analyzer",
|
|
args: args{
|
|
filePath: "/lib/apk/db/installed",
|
|
testFilePath: "testdata/lib/apk/db/installed",
|
|
},
|
|
want: &analyzer.AnalysisResult{
|
|
PackageInfos: []types.PackageInfo{
|
|
{
|
|
FilePath: "/lib/apk/db/installed",
|
|
Packages: []types.Package{
|
|
{Name: "musl", Version: "1.1.24-r2", SrcName: "musl", SrcVersion: "1.1.24-r2", License: "MIT"},
|
|
},
|
|
},
|
|
},
|
|
SystemInstalledFiles: []string{
|
|
"lib/libc.musl-x86_64.so.1",
|
|
"lib/ld-musl-x86_64.so.1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "happy path with disabled package analyzer",
|
|
args: args{
|
|
filePath: "/lib/apk/db/installed",
|
|
testFilePath: "testdata/lib/apk/db/installed",
|
|
disabledAnalyzers: []analyzer.Type{analyzer.TypeApk},
|
|
},
|
|
want: &analyzer.AnalysisResult{},
|
|
},
|
|
{
|
|
name: "happy path with library analyzer",
|
|
args: args{
|
|
filePath: "/app/Gemfile.lock",
|
|
testFilePath: "testdata/app/Gemfile.lock",
|
|
},
|
|
want: &analyzer.AnalysisResult{
|
|
Applications: []types.Application{
|
|
{
|
|
Type: "bundler",
|
|
FilePath: "/app/Gemfile.lock",
|
|
Libraries: []types.Package{
|
|
{
|
|
Name: "actioncable",
|
|
Version: "5.2.3",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "happy path with invalid os information",
|
|
args: args{
|
|
filePath: "/etc/lsb-release",
|
|
testFilePath: "testdata/etc/hostname",
|
|
},
|
|
want: &analyzer.AnalysisResult{},
|
|
},
|
|
{
|
|
name: "happy path with a directory",
|
|
args: args{
|
|
filePath: "/etc/lsb-release",
|
|
testFilePath: "testdata/etc",
|
|
},
|
|
want: &analyzer.AnalysisResult{},
|
|
},
|
|
{
|
|
name: "ignore permission error",
|
|
args: args{
|
|
filePath: "/etc/alpine-release",
|
|
testFilePath: "testdata/no-permission",
|
|
},
|
|
want: &analyzer.AnalysisResult{},
|
|
},
|
|
{
|
|
name: "sad path with opener error",
|
|
args: args{
|
|
filePath: "/lib/apk/db/installed",
|
|
testFilePath: "testdata/error",
|
|
},
|
|
wantErr: "unable to open /lib/apk/db/installed",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
limit := semaphore.NewWeighted(3)
|
|
|
|
got := new(analyzer.AnalysisResult)
|
|
a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.args.disabledAnalyzers)
|
|
|
|
info, err := os.Stat(tt.args.testFilePath)
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
err = a.AnalyzeFile(ctx, &wg, limit, got, "", tt.args.filePath, info,
|
|
func() (dio.ReadSeekCloserAt, error) {
|
|
if tt.args.testFilePath == "testdata/error" {
|
|
return nil, xerrors.New("error")
|
|
} else if tt.args.testFilePath == "testdata/no-permission" {
|
|
os.Chmod(tt.args.testFilePath, 0000)
|
|
t.Cleanup(func() {
|
|
os.Chmod(tt.args.testFilePath, 0644)
|
|
})
|
|
}
|
|
return os.Open(tt.args.testFilePath)
|
|
},
|
|
nil, analyzer.AnalysisOptions{},
|
|
)
|
|
|
|
wg.Wait()
|
|
if tt.wantErr != "" {
|
|
require.NotNil(t, err)
|
|
assert.Contains(t, err.Error(), tt.wantErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeConfig(t *testing.T) {
|
|
|
|
type args struct {
|
|
targetOS types.OS
|
|
configBlob []byte
|
|
disabledAnalyzers []analyzer.Type
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want []types.Package
|
|
}{
|
|
{
|
|
name: "happy path",
|
|
args: args{
|
|
targetOS: types.OS{
|
|
Family: "alpine",
|
|
Name: "3.11.6",
|
|
},
|
|
configBlob: []byte("foo"),
|
|
},
|
|
want: []types.Package{
|
|
{Name: "musl", Version: "1.1.24-r2"},
|
|
},
|
|
},
|
|
{
|
|
name: "non-target OS",
|
|
args: args{
|
|
targetOS: types.OS{
|
|
Family: "debian",
|
|
Name: "9.2",
|
|
},
|
|
configBlob: []byte("foo"),
|
|
},
|
|
},
|
|
{
|
|
name: "Analyze returns an error",
|
|
args: args{
|
|
targetOS: types.OS{
|
|
Family: "alpine",
|
|
Name: "3.11.6",
|
|
},
|
|
configBlob: []byte("bar"),
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.args.disabledAnalyzers)
|
|
got := a.AnalyzeImageConfig(tt.args.targetOS, tt.args.configBlob)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAnalyzer_AnalyzerVersions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
disabled []analyzer.Type
|
|
want map[string]int
|
|
}{
|
|
{
|
|
name: "happy path",
|
|
disabled: []analyzer.Type{},
|
|
want: map[string]int{
|
|
"alpine": 1,
|
|
"apk-repo": 1,
|
|
"apk": 1,
|
|
"bundler": 1,
|
|
"ubuntu": 1,
|
|
},
|
|
},
|
|
{
|
|
name: "disable analyzers",
|
|
disabled: []analyzer.Type{analyzer.TypeAlpine, analyzer.TypeApkRepo, analyzer.TypeUbuntu},
|
|
want: map[string]int{
|
|
"apk": 1,
|
|
"bundler": 1,
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.disabled)
|
|
got := a.AnalyzerVersions()
|
|
fmt.Printf("%v\n", got)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAnalyzer_ImageConfigAnalyzerVersions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
disabled []analyzer.Type
|
|
want map[string]int
|
|
}{
|
|
{
|
|
name: "happy path",
|
|
disabled: []analyzer.Type{},
|
|
want: map[string]int{
|
|
"apk-command": 1,
|
|
"test": 1,
|
|
},
|
|
},
|
|
{
|
|
name: "disable analyzers",
|
|
disabled: []analyzer.Type{analyzer.TypeAlpine, analyzer.TypeApkCommand},
|
|
want: map[string]int{
|
|
"test": 1,
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.disabled)
|
|
got := a.ImageConfigAnalyzerVersions()
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|