mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 07:40:48 -08:00
708 lines
16 KiB
Go
708 lines
16 KiB
Go
package analyzer_test
|
|
|
|
import (
|
|
"os"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sync/semaphore"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
|
"github.com/aquasecurity/trivy/pkg/fanal/types"
|
|
"github.com/aquasecurity/trivy/pkg/javadb"
|
|
"github.com/aquasecurity/trivy/pkg/mapfs"
|
|
xio "github.com/aquasecurity/trivy/pkg/x/io"
|
|
|
|
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/imgconf/apk"
|
|
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/java/jar"
|
|
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/poetry"
|
|
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/ruby/bundler"
|
|
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/alpine"
|
|
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/ubuntu"
|
|
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/apk"
|
|
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/repo/apk"
|
|
_ "github.com/aquasecurity/trivy/pkg/fanal/handler/all"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
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: types.Debian,
|
|
Name: "9.8",
|
|
},
|
|
PackageInfos: []types.PackageInfo{
|
|
{
|
|
FilePath: "var/lib/dpkg/status.d/libc",
|
|
Packages: types.Packages{
|
|
{
|
|
Name: "libc",
|
|
Version: "1.2.3",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Applications: []types.Application{
|
|
{
|
|
Type: "bundler",
|
|
FilePath: "app/Gemfile.lock",
|
|
Packages: types.Packages{
|
|
{
|
|
Name: "rails",
|
|
Version: "5.0.0",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
new: &analyzer.AnalysisResult{
|
|
PackageInfos: []types.PackageInfo{
|
|
{
|
|
FilePath: "var/lib/dpkg/status.d/openssl",
|
|
Packages: types.Packages{
|
|
{
|
|
Name: "openssl",
|
|
Version: "1.1.1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Applications: []types.Application{
|
|
{
|
|
Type: "bundler",
|
|
FilePath: "app2/Gemfile.lock",
|
|
Packages: types.Packages{
|
|
{
|
|
Name: "nokogiri",
|
|
Version: "1.0.0",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: analyzer.AnalysisResult{
|
|
OS: types.OS{
|
|
Family: types.Debian,
|
|
Name: "9.8",
|
|
},
|
|
PackageInfos: []types.PackageInfo{
|
|
{
|
|
FilePath: "var/lib/dpkg/status.d/libc",
|
|
Packages: types.Packages{
|
|
{
|
|
Name: "libc",
|
|
Version: "1.2.3",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
FilePath: "var/lib/dpkg/status.d/openssl",
|
|
Packages: types.Packages{
|
|
{
|
|
Name: "openssl",
|
|
Version: "1.1.1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Applications: []types.Application{
|
|
{
|
|
Type: "bundler",
|
|
FilePath: "app/Gemfile.lock",
|
|
Packages: types.Packages{
|
|
{
|
|
Name: "rails",
|
|
Version: "5.0.0",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "bundler",
|
|
FilePath: "app2/Gemfile.lock",
|
|
Packages: types.Packages{
|
|
{
|
|
Name: "nokogiri",
|
|
Version: "1.0.0",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "redhat must be replaced with oracle",
|
|
fields: fields{
|
|
OS: types.OS{
|
|
Family: types.RedHat, // this must be overwritten
|
|
Name: "8.0",
|
|
},
|
|
},
|
|
args: args{
|
|
new: &analyzer.AnalysisResult{
|
|
OS: types.OS{
|
|
Family: types.Oracle,
|
|
Name: "8.0",
|
|
},
|
|
},
|
|
},
|
|
want: analyzer.AnalysisResult{
|
|
OS: types.OS{
|
|
Family: types.Oracle,
|
|
Name: "8.0",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "debian must be replaced with ubuntu",
|
|
fields: fields{
|
|
OS: types.OS{
|
|
Family: types.Debian, // this must be overwritten
|
|
Name: "9.0",
|
|
},
|
|
},
|
|
args: args{
|
|
new: &analyzer.AnalysisResult{
|
|
OS: types.OS{
|
|
Family: types.Ubuntu,
|
|
Name: "18.04",
|
|
},
|
|
},
|
|
},
|
|
want: analyzer.AnalysisResult{
|
|
OS: types.OS{
|
|
Family: types.Ubuntu,
|
|
Name: "18.04",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "merge extended flag",
|
|
fields: fields{
|
|
// This must be overwritten
|
|
OS: types.OS{
|
|
Family: types.Ubuntu,
|
|
Name: "16.04",
|
|
},
|
|
},
|
|
args: args{
|
|
new: &analyzer.AnalysisResult{
|
|
OS: types.OS{
|
|
Family: types.Ubuntu,
|
|
Extended: true,
|
|
},
|
|
},
|
|
},
|
|
want: analyzer.AnalysisResult{
|
|
OS: types.OS{
|
|
Family: types.Ubuntu,
|
|
Name: "16.04",
|
|
Extended: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "alpine OS needs to be extended with apk repositories",
|
|
fields: fields{
|
|
OS: types.OS{
|
|
Family: types.Alpine,
|
|
Name: "3.15.3",
|
|
},
|
|
},
|
|
args: args{
|
|
new: &analyzer.AnalysisResult{
|
|
Repository: &types.Repository{
|
|
Family: types.Alpine,
|
|
Release: "edge",
|
|
},
|
|
},
|
|
},
|
|
want: analyzer.AnalysisResult{
|
|
OS: types.OS{
|
|
Family: types.Alpine,
|
|
Name: "3.15.3",
|
|
},
|
|
Repository: &types.Repository{
|
|
Family: types.Alpine,
|
|
Release: "edge",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "alpine must not be replaced with oracle",
|
|
fields: fields{
|
|
OS: types.OS{
|
|
Family: types.Alpine, // this must not be overwritten
|
|
Name: "3.11",
|
|
},
|
|
},
|
|
args: args{
|
|
new: &analyzer.AnalysisResult{
|
|
OS: types.OS{
|
|
Family: types.Oracle,
|
|
Name: "8.0",
|
|
},
|
|
},
|
|
},
|
|
want: analyzer.AnalysisResult{
|
|
OS: types.OS{
|
|
Family: types.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 TestAnalyzerGroup_AnalyzeFile(t *testing.T) {
|
|
type args struct {
|
|
filePath string
|
|
testFilePath string
|
|
disabledAnalyzers []analyzer.Type
|
|
filePatterns []string
|
|
}
|
|
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.Packages{
|
|
{
|
|
ID: "musl@1.1.24-r2",
|
|
Name: "musl",
|
|
Version: "1.1.24-r2",
|
|
SrcName: "musl",
|
|
SrcVersion: "1.1.24-r2",
|
|
Licenses: []string{"MIT"},
|
|
Arch: "x86_64",
|
|
Digest: "sha1:cb2316a189ebee5282c4a9bd98794cc2477a74c6",
|
|
InstalledFiles: []string{
|
|
"lib/libc.musl-x86_64.so.1",
|
|
"lib/ld-musl-x86_64.so.1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
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",
|
|
Packages: types.Packages{
|
|
{
|
|
ID: "actioncable@5.2.3",
|
|
Name: "actioncable",
|
|
Version: "5.2.3",
|
|
Indirect: false,
|
|
Relationship: types.RelationshipDirect,
|
|
DependsOn: []string{
|
|
"actionpack@5.2.3",
|
|
},
|
|
Locations: []types.Location{
|
|
{
|
|
StartLine: 4,
|
|
EndLine: 4,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "actionpack@5.2.3",
|
|
Name: "actionpack",
|
|
Version: "5.2.3",
|
|
Indirect: true,
|
|
Relationship: types.RelationshipIndirect,
|
|
Locations: []types.Location{
|
|
{
|
|
StartLine: 6,
|
|
EndLine: 6,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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: "happy path with library analyzer file pattern regex",
|
|
args: args{
|
|
filePath: "/app/Gemfile-dev.lock",
|
|
testFilePath: "testdata/app/Gemfile.lock",
|
|
filePatterns: []string{"bundler:Gemfile(-.*)?\\.lock"},
|
|
},
|
|
want: &analyzer.AnalysisResult{
|
|
Applications: []types.Application{
|
|
{
|
|
Type: "bundler",
|
|
FilePath: "/app/Gemfile-dev.lock",
|
|
Packages: types.Packages{
|
|
{
|
|
ID: "actioncable@5.2.3",
|
|
Name: "actioncable",
|
|
Version: "5.2.3",
|
|
Indirect: false,
|
|
Relationship: types.RelationshipDirect,
|
|
DependsOn: []string{
|
|
"actionpack@5.2.3",
|
|
},
|
|
Locations: []types.Location{
|
|
{
|
|
StartLine: 4,
|
|
EndLine: 4,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "actionpack@5.2.3",
|
|
Name: "actionpack",
|
|
Version: "5.2.3",
|
|
Indirect: true,
|
|
Relationship: types.RelationshipIndirect,
|
|
Locations: []types.Location{
|
|
{
|
|
StartLine: 6,
|
|
EndLine: 6,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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",
|
|
},
|
|
{
|
|
name: "sad path with broken file pattern regex",
|
|
args: args{
|
|
filePath: "/app/Gemfile-dev.lock",
|
|
testFilePath: "testdata/app/Gemfile.lock",
|
|
filePatterns: []string{"bundler:Gemfile(-.*?\\.lock"},
|
|
},
|
|
wantErr: "error parsing regexp",
|
|
},
|
|
{
|
|
name: "sad path with broken file pattern",
|
|
args: args{
|
|
filePath: "/app/Gemfile-dev.lock",
|
|
testFilePath: "testdata/app/Gemfile.lock",
|
|
filePatterns: []string{"Gemfile(-.*)?\\.lock"},
|
|
},
|
|
wantErr: "invalid file pattern",
|
|
},
|
|
}
|
|
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, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{
|
|
FilePatterns: tt.args.filePatterns,
|
|
DisabledAnalyzers: tt.args.disabledAnalyzers,
|
|
})
|
|
if err != nil && tt.wantErr != "" {
|
|
require.ErrorContains(t, err, tt.wantErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
info, err := os.Stat(tt.args.testFilePath)
|
|
require.NoError(t, err)
|
|
|
|
ctx := t.Context()
|
|
err = a.AnalyzeFile(ctx, &wg, limit, got, "", tt.args.filePath, info,
|
|
func() (xio.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, 0o000)
|
|
t.Cleanup(func() {
|
|
os.Chmod(tt.args.testFilePath, 0o644)
|
|
})
|
|
}
|
|
return os.Open(tt.args.testFilePath)
|
|
},
|
|
nil, analyzer.AnalysisOptions{},
|
|
)
|
|
|
|
wg.Wait()
|
|
if tt.wantErr != "" {
|
|
require.ErrorContains(t, err, tt.wantErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAnalyzerGroup_PostAnalyze(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dir string
|
|
analyzerType analyzer.Type
|
|
filePatterns []string
|
|
want *analyzer.AnalysisResult
|
|
}{
|
|
{
|
|
name: "jars with invalid jar",
|
|
dir: "testdata/post-apps/jar/",
|
|
analyzerType: analyzer.TypeJar,
|
|
want: &analyzer.AnalysisResult{
|
|
Applications: []types.Application{
|
|
{
|
|
Type: types.Jar,
|
|
FilePath: "testdata/post-apps/jar/jackson-annotations-2.15.0-rc2.jar",
|
|
Packages: types.Packages{
|
|
{
|
|
Name: "com.fasterxml.jackson.core:jackson-annotations",
|
|
Version: "2.15.0-rc2",
|
|
FilePath: "testdata/post-apps/jar/jackson-annotations-2.15.0-rc2.jar",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "poetry files with file from pattern and invalid file",
|
|
dir: "testdata/post-apps/poetry/",
|
|
filePatterns: []string{
|
|
"poetry:poetry-pattern.lock",
|
|
},
|
|
analyzerType: analyzer.TypePoetry,
|
|
want: &analyzer.AnalysisResult{
|
|
Applications: []types.Application{
|
|
{
|
|
Type: types.Poetry,
|
|
FilePath: "testdata/post-apps/poetry/happy/poetry-pattern.lock",
|
|
Packages: types.Packages{
|
|
{
|
|
ID: "certifi@2022.12.7",
|
|
Name: "certifi",
|
|
Version: "2022.12.7",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: types.Poetry,
|
|
FilePath: "testdata/post-apps/poetry/happy/poetry.lock",
|
|
Packages: types.Packages{
|
|
{
|
|
ID: "certifi@2022.12.7",
|
|
Name: "certifi",
|
|
Version: "2022.12.7",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{
|
|
FilePatterns: tt.filePatterns,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create a virtual filesystem
|
|
composite, err := analyzer.NewCompositeFS()
|
|
require.NoError(t, err)
|
|
|
|
mfs := mapfs.New()
|
|
require.NoError(t, mfs.CopyFilesUnder(tt.dir))
|
|
composite.Set(tt.analyzerType, mfs)
|
|
|
|
if tt.analyzerType == analyzer.TypeJar {
|
|
// init java-trivy-db with skip update
|
|
repo, err := name.NewTag(javadb.DefaultGHCRRepository)
|
|
require.NoError(t, err)
|
|
javadb.Init("./language/java/jar/testdata", []name.Reference{repo}, true, false, types.RegistryOptions{Insecure: false})
|
|
}
|
|
|
|
ctx := t.Context()
|
|
got := new(analyzer.AnalysisResult)
|
|
err = a.PostAnalyze(ctx, composite, got, analyzer.AnalysisOptions{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAnalyzerGroup_AnalyzerVersions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
disabled []analyzer.Type
|
|
want analyzer.Versions
|
|
}{
|
|
{
|
|
name: "happy path",
|
|
disabled: []analyzer.Type{},
|
|
want: analyzer.Versions{
|
|
Analyzers: map[string]int{
|
|
"alpine": 1,
|
|
"apk-repo": 1,
|
|
"apk": 2,
|
|
"bundler": 1,
|
|
"ubuntu": 1,
|
|
"ubuntu-esm": 1,
|
|
},
|
|
PostAnalyzers: map[string]int{
|
|
"jar": 1,
|
|
"poetry": 1,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "disable analyzers",
|
|
disabled: []analyzer.Type{
|
|
analyzer.TypeAlpine,
|
|
analyzer.TypeApkRepo,
|
|
analyzer.TypeUbuntu,
|
|
analyzer.TypeUbuntuESM,
|
|
analyzer.TypeJar,
|
|
},
|
|
want: analyzer.Versions{
|
|
Analyzers: map[string]int{
|
|
"apk": 2,
|
|
"bundler": 1,
|
|
},
|
|
PostAnalyzers: map[string]int{
|
|
"poetry": 1,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{
|
|
DisabledAnalyzers: tt.disabled,
|
|
})
|
|
require.NoError(t, err)
|
|
got := a.AnalyzerVersions()
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|