Skip to content

Commit e6f45cd

Browse files
authored
refactor: split .egg and packaging analyzers (#7514)
1 parent 5442949 commit e6f45cd

File tree

6 files changed

+311
-108
lines changed

6 files changed

+311
-108
lines changed

pkg/fanal/analyzer/const.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,11 @@ const (
7575
TypeCondaEnv Type = "conda-environment"
7676

7777
// Python
78-
TypePythonPkg Type = "python-pkg"
79-
TypePip Type = "pip"
80-
TypePipenv Type = "pipenv"
81-
TypePoetry Type = "poetry"
78+
TypePythonPkg Type = "python-pkg"
79+
TypePythonPkgEgg Type = "python-egg"
80+
TypePip Type = "pip"
81+
TypePipenv Type = "pipenv"
82+
TypePoetry Type = "poetry"
8283

8384
// Go
8485
TypeGoBinary Type = "gobinary"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package packaging
2+
3+
import (
4+
"archive/zip"
5+
"context"
6+
"io"
7+
"os"
8+
"path"
9+
"path/filepath"
10+
11+
"github.com/samber/lo"
12+
"golang.org/x/xerrors"
13+
14+
"github.com/aquasecurity/trivy/pkg/dependency/parser/python/packaging"
15+
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
16+
"github.com/aquasecurity/trivy/pkg/fanal/analyzer/language"
17+
"github.com/aquasecurity/trivy/pkg/fanal/types"
18+
"github.com/aquasecurity/trivy/pkg/log"
19+
xio "github.com/aquasecurity/trivy/pkg/x/io"
20+
)
21+
22+
func init() {
23+
analyzer.RegisterAnalyzer(&eggAnalyzer{})
24+
}
25+
26+
const (
27+
eggAnalyzerVersion = 1
28+
eggExt = ".egg"
29+
)
30+
31+
type eggAnalyzer struct {
32+
logger *log.Logger
33+
licenseClassifierConfidenceLevel float64
34+
}
35+
36+
func (a *eggAnalyzer) Init(opt analyzer.AnalyzerOptions) error {
37+
a.logger = log.WithPrefix("python")
38+
a.licenseClassifierConfidenceLevel = opt.LicenseScannerOption.ClassifierConfidenceLevel
39+
return nil
40+
}
41+
42+
// Analyze analyzes egg archive files
43+
func (a *eggAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
44+
// .egg file is zip format and PKG-INFO needs to be extracted from the zip file.
45+
pkginfoInZip, err := findFileInZip(input.Content, input.Info.Size(), isEggFile)
46+
if err != nil {
47+
return nil, xerrors.Errorf("unable to open `.egg` archive: %w", err)
48+
}
49+
50+
// Egg archive may not contain required files, then we will get nil. Skip this archives
51+
if pkginfoInZip == nil {
52+
return nil, nil
53+
}
54+
55+
rsa, err := xio.NewReadSeekerAt(pkginfoInZip)
56+
if err != nil {
57+
return nil, xerrors.Errorf("unable to convert PKG-INFO reader: %w", err)
58+
}
59+
60+
app, err := language.ParsePackage(types.PythonPkg, input.FilePath, rsa, packaging.NewParser(), input.Options.FileChecksum)
61+
if err != nil {
62+
return nil, xerrors.Errorf("parse error: %w", err)
63+
} else if app == nil {
64+
return nil, nil
65+
}
66+
67+
opener := func(licPath string) (io.ReadCloser, error) {
68+
required := func(filePath string) bool {
69+
return path.Base(filePath) == licPath
70+
}
71+
72+
f, err := findFileInZip(input.Content, input.Info.Size(), required)
73+
if err != nil {
74+
return nil, xerrors.Errorf("unable to find license file in `*.egg` file: %w", err)
75+
} else if f == nil { // zip doesn't contain license file
76+
return nil, nil
77+
}
78+
79+
return f, nil
80+
}
81+
82+
if err = fillAdditionalData(opener, app, a.licenseClassifierConfidenceLevel); err != nil {
83+
a.logger.Warn("Unable to collect additional info", log.Err(err))
84+
}
85+
86+
return &analyzer.AnalysisResult{
87+
Applications: []types.Application{*app},
88+
}, nil
89+
}
90+
91+
func findFileInZip(r xio.ReadSeekerAt, zipSize int64, required func(filePath string) bool) (io.ReadCloser, error) {
92+
if _, err := r.Seek(0, io.SeekStart); err != nil {
93+
return nil, xerrors.Errorf("file seek error: %w", err)
94+
}
95+
96+
zr, err := zip.NewReader(r, zipSize)
97+
if err != nil {
98+
return nil, xerrors.Errorf("zip reader error: %w", err)
99+
}
100+
101+
found, ok := lo.Find(zr.File, func(f *zip.File) bool {
102+
return required(f.Name)
103+
})
104+
if !ok {
105+
return nil, nil
106+
}
107+
108+
f, err := found.Open()
109+
if err != nil {
110+
return nil, xerrors.Errorf("unable to open file in zip: %w", err)
111+
}
112+
113+
return f, nil
114+
}
115+
116+
func (a *eggAnalyzer) Required(filePath string, _ os.FileInfo) bool {
117+
return filepath.Ext(filePath) == eggExt
118+
}
119+
120+
func (a *eggAnalyzer) Type() analyzer.Type {
121+
return analyzer.TypePythonPkgEgg
122+
}
123+
124+
func (a *eggAnalyzer) Version() int {
125+
return eggAnalyzerVersion
126+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package packaging
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
12+
"github.com/aquasecurity/trivy/pkg/fanal/types"
13+
)
14+
15+
func Test_eggAnalyzer_Analyze(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
inputFile string
19+
includeChecksum bool
20+
want *analyzer.AnalysisResult
21+
wantErr string
22+
}{
23+
{
24+
name: "egg zip",
25+
inputFile: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
26+
want: &analyzer.AnalysisResult{
27+
Applications: []types.Application{
28+
{
29+
Type: types.PythonPkg,
30+
FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
31+
Packages: types.Packages{
32+
{
33+
Name: "kitchen",
34+
Version: "1.2.6",
35+
Licenses: []string{
36+
"LGPL-2.1-only",
37+
},
38+
FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
39+
},
40+
},
41+
},
42+
},
43+
},
44+
},
45+
{
46+
name: "egg zip with checksum",
47+
inputFile: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
48+
includeChecksum: true,
49+
want: &analyzer.AnalysisResult{
50+
Applications: []types.Application{
51+
{
52+
Type: types.PythonPkg,
53+
FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
54+
Packages: types.Packages{
55+
{
56+
Name: "kitchen",
57+
Version: "1.2.6",
58+
Licenses: []string{
59+
"LGPL-2.1-only",
60+
},
61+
FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
62+
Digest: "sha1:4e13b6e379966771e896ee43cf8e240bf6083dca",
63+
},
64+
},
65+
},
66+
},
67+
},
68+
},
69+
{
70+
name: "egg zip with license file",
71+
inputFile: "testdata/egg-zip-with-license-file/sample_package.egg",
72+
want: &analyzer.AnalysisResult{
73+
Applications: []types.Application{
74+
{
75+
Type: types.PythonPkg,
76+
FilePath: "testdata/egg-zip-with-license-file/sample_package.egg",
77+
Packages: types.Packages{
78+
{
79+
Name: "sample_package",
80+
Version: "0.1",
81+
Licenses: []string{
82+
"MIT",
83+
},
84+
FilePath: "testdata/egg-zip-with-license-file/sample_package.egg",
85+
},
86+
},
87+
},
88+
},
89+
},
90+
},
91+
{
92+
name: "egg zip doesn't contain required files",
93+
inputFile: "testdata/no-req-files/no-required-files.egg",
94+
want: nil,
95+
},
96+
}
97+
for _, tt := range tests {
98+
t.Run(tt.name, func(t *testing.T) {
99+
f, err := os.Open(tt.inputFile)
100+
require.NoError(t, err)
101+
defer f.Close()
102+
fileInfo, err := os.Lstat(tt.inputFile)
103+
require.NoError(t, err)
104+
105+
a := &eggAnalyzer{}
106+
got, err := a.Analyze(context.Background(), analyzer.AnalysisInput{
107+
Content: f,
108+
FilePath: tt.inputFile,
109+
Info: fileInfo,
110+
Options: analyzer.AnalysisOptions{
111+
FileChecksum: tt.includeChecksum,
112+
},
113+
})
114+
115+
require.NoError(t, err)
116+
assert.Equal(t, tt.want, got)
117+
})
118+
}
119+
120+
}
121+
122+
func Test_eggAnalyzer_Required(t *testing.T) {
123+
tests := []struct {
124+
name string
125+
filePath string
126+
want bool
127+
}{
128+
{
129+
name: "egg zip",
130+
filePath: "python2.7/site-packages/cssutils-1.0-py2.7.egg",
131+
want: true,
132+
},
133+
{
134+
name: "egg-info PKG-INFO",
135+
filePath: "python3.8/site-packages/wrapt-1.12.1.egg-info/PKG-INFO",
136+
want: false,
137+
},
138+
}
139+
for _, tt := range tests {
140+
t.Run(tt.name, func(t *testing.T) {
141+
a := eggAnalyzer{}
142+
got := a.Required(tt.filePath, nil)
143+
assert.Equal(t, tt.want, got)
144+
})
145+
}
146+
}

0 commit comments

Comments
 (0)