Skip to content

Commit

Permalink
refactor: split .egg and packaging analyzers (#7514)
Browse files Browse the repository at this point in the history
  • Loading branch information
DmitriyLewen authored Sep 16, 2024
1 parent 5442949 commit e6f45cd
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 108 deletions.
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

0 comments on commit e6f45cd

Please sign in to comment.