Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: split .egg and packaging analyzers #7514

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions pkg/fanal/analyzer/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ const (
TypeCondaEnv Type = "conda-environment"

// Python
TypePythonPkg Type = "python-pkg"
TypePip Type = "pip"
TypePipenv Type = "pipenv"
TypePoetry Type = "poetry"
TypePythonPkg Type = "python-pkg"
TypePythonPkgEgg Type = "python-egg"
TypePip Type = "pip"
TypePipenv Type = "pipenv"
TypePoetry Type = "poetry"

// Go
TypeGoBinary Type = "gobinary"
Expand Down
126 changes: 126 additions & 0 deletions pkg/fanal/analyzer/language/python/packaging/egg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package packaging

import (
"archive/zip"
"context"
"io"
"os"
"path"
"path/filepath"

"github.com/samber/lo"
"golang.org/x/xerrors"

"github.com/aquasecurity/trivy/pkg/dependency/parser/python/packaging"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer/language"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
xio "github.com/aquasecurity/trivy/pkg/x/io"
)

func init() {
analyzer.RegisterAnalyzer(&eggAnalyzer{})
}

const (
eggAnalyzerVersion = 1
eggExt = ".egg"
)

type eggAnalyzer struct {
logger *log.Logger
licenseClassifierConfidenceLevel float64
}

func (a *eggAnalyzer) Init(opt analyzer.AnalyzerOptions) error {
a.logger = log.WithPrefix("python")
a.licenseClassifierConfidenceLevel = opt.LicenseScannerOption.ClassifierConfidenceLevel
return nil
}

// Analyze analyzes egg archive files
func (a *eggAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
// .egg file is zip format and PKG-INFO needs to be extracted from the zip file.
pkginfoInZip, err := findFileInZip(input.Content, input.Info.Size(), isEggFile)
if err != nil {
return nil, xerrors.Errorf("unable to open `.egg` archive: %w", err)
}

// Egg archive may not contain required files, then we will get nil. Skip this archives
if pkginfoInZip == nil {
return nil, nil
}

rsa, err := xio.NewReadSeekerAt(pkginfoInZip)
if err != nil {
return nil, xerrors.Errorf("unable to convert PKG-INFO reader: %w", err)
}

app, err := language.ParsePackage(types.PythonPkg, input.FilePath, rsa, packaging.NewParser(), input.Options.FileChecksum)
if err != nil {
return nil, xerrors.Errorf("parse error: %w", err)
} else if app == nil {
return nil, nil
}

opener := func(licPath string) (io.ReadCloser, error) {
required := func(filePath string) bool {
return path.Base(filePath) == licPath
}

f, err := findFileInZip(input.Content, input.Info.Size(), required)
if err != nil {
return nil, xerrors.Errorf("unable to find license file in `*.egg` file: %w", err)
} else if f == nil { // zip doesn't contain license file
return nil, nil
}

return f, nil
}

if err = fillAdditionalData(opener, app, a.licenseClassifierConfidenceLevel); err != nil {
a.logger.Warn("Unable to collect additional info", log.Err(err))
}

return &analyzer.AnalysisResult{
Applications: []types.Application{*app},
}, nil
}

func findFileInZip(r xio.ReadSeekerAt, zipSize int64, required func(filePath string) bool) (io.ReadCloser, error) {
if _, err := r.Seek(0, io.SeekStart); err != nil {
return nil, xerrors.Errorf("file seek error: %w", err)
}

zr, err := zip.NewReader(r, zipSize)
if err != nil {
return nil, xerrors.Errorf("zip reader error: %w", err)
}

found, ok := lo.Find(zr.File, func(f *zip.File) bool {
return required(f.Name)
})
if !ok {
return nil, nil
}

f, err := found.Open()
if err != nil {
return nil, xerrors.Errorf("unable to open file in zip: %w", err)
}

return f, nil
}

func (a *eggAnalyzer) Required(filePath string, _ os.FileInfo) bool {
return filepath.Ext(filePath) == eggExt
}

func (a *eggAnalyzer) Type() analyzer.Type {
return analyzer.TypePythonPkgEgg
}

func (a *eggAnalyzer) Version() int {
return eggAnalyzerVersion
}
146 changes: 146 additions & 0 deletions pkg/fanal/analyzer/language/python/packaging/egg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package packaging

import (
"context"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/types"
)

func Test_eggAnalyzer_Analyze(t *testing.T) {
tests := []struct {
name string
inputFile string
includeChecksum bool
want *analyzer.AnalysisResult
wantErr string
}{
{
name: "egg zip",
inputFile: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.PythonPkg,
FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
Packages: types.Packages{
{
Name: "kitchen",
Version: "1.2.6",
Licenses: []string{
"LGPL-2.1-only",
},
FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
},
},
},
},
},
},
{
name: "egg zip with checksum",
inputFile: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
includeChecksum: true,
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.PythonPkg,
FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
Packages: types.Packages{
{
Name: "kitchen",
Version: "1.2.6",
Licenses: []string{
"LGPL-2.1-only",
},
FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg",
Digest: "sha1:4e13b6e379966771e896ee43cf8e240bf6083dca",
},
},
},
},
},
},
{
name: "egg zip with license file",
inputFile: "testdata/egg-zip-with-license-file/sample_package.egg",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.PythonPkg,
FilePath: "testdata/egg-zip-with-license-file/sample_package.egg",
Packages: types.Packages{
{
Name: "sample_package",
Version: "0.1",
Licenses: []string{
"MIT",
},
FilePath: "testdata/egg-zip-with-license-file/sample_package.egg",
},
},
},
},
},
},
{
name: "egg zip doesn't contain required files",
inputFile: "testdata/no-req-files/no-required-files.egg",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := os.Open(tt.inputFile)
require.NoError(t, err)
defer f.Close()
fileInfo, err := os.Lstat(tt.inputFile)
require.NoError(t, err)

a := &eggAnalyzer{}
got, err := a.Analyze(context.Background(), analyzer.AnalysisInput{
Content: f,
FilePath: tt.inputFile,
Info: fileInfo,
Options: analyzer.AnalysisOptions{
FileChecksum: tt.includeChecksum,
},
})

require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}

}

func Test_eggAnalyzer_Required(t *testing.T) {
tests := []struct {
name string
filePath string
want bool
}{
{
name: "egg zip",
filePath: "python2.7/site-packages/cssutils-1.0-py2.7.egg",
want: true,
},
{
name: "egg-info PKG-INFO",
filePath: "python3.8/site-packages/wrapt-1.12.1.egg-info/PKG-INFO",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := eggAnalyzer{}
got := a.Required(tt.filePath, nil)
assert.Equal(t, tt.want, got)
})
}
}
Loading