-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5442949
commit e6f45cd
Showing
6 changed files
with
311 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
146
pkg/fanal/analyzer/language/python/packaging/egg_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} | ||
} |
Oops, something went wrong.