diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go index afdbf2ee696..f80d6cd7b20 100644 --- a/pkg/commands/artifact/run.go +++ b/pkg/commands/artifact/run.go @@ -509,12 +509,16 @@ func disabledAnalyzers(opts flag.Options) []analyzer.Type { analyzers = append(analyzers, analyzer.TypeHistoryDockerfile) } - // Skip executable file analysis if Rekor isn't a specified SBOM source. + return analyzers +} + +func disabledHandlers(opts flag.Options) []ftypes.HandlerType { + var handlers []ftypes.HandlerType + // Skip unpackaged executable file analysis with Rekor if Rekor isn't a specificed SBOM source if !slices.Contains(opts.SBOMSources, types.SBOMSourceRekor) { - analyzers = append(analyzers, analyzer.TypeExecutable) + handlers = append(handlers, ftypes.UnpackagedPostHandler) } - - return analyzers + return handlers } func filterMisconfigAnalyzers(included, all []analyzer.Type) ([]analyzer.Type, error) { @@ -626,6 +630,7 @@ func initScannerConfig(opts flag.Options, cacheClient cache.Cache) (ScannerConfi }, ArtifactOption: artifact.Option{ DisabledAnalyzers: disabledAnalyzers(opts), + DisabledHandlers: disabledHandlers(opts), FilePatterns: opts.FilePatterns, Parallel: opts.Parallel, Offline: opts.OfflineScan, diff --git a/pkg/dependency/parser/executable/executable.go b/pkg/dependency/parser/executable/executable.go new file mode 100644 index 00000000000..de0b622f144 --- /dev/null +++ b/pkg/dependency/parser/executable/executable.go @@ -0,0 +1,76 @@ +// Ported from https://github.com/golang/go/blob/b5a861782312d2b3a4f71e33d9a0c2b01a40fe5f/src/debug/buildinfo/buildinfo.go + +package executable + +import ( + "bytes" + "debug/elf" + "errors" + "fmt" + "io" +) + +var errUnrecognizedFormat = errors.New("unrecognized file format") + +// An exe is a generic interface to an OS executable (ELF, Mach-O, PE, XCOFF). +type Exe interface { + // ReadData reads and returns up to size byte starting at virtual address addr. + ReadData(addr, size uint64) ([]byte, error) + + // DataStart returns the writable data segment start address. + DataStart() (uint64, uint64) +} + +// openExe opens file and returns it as an exe. +func OpenExe(r io.ReaderAt) (Exe, error) { + ident := make([]byte, 16) + if n, err := r.ReadAt(ident, 0); n < len(ident) || err != nil { + return nil, errUnrecognizedFormat + } + + switch { + case bytes.HasPrefix(ident, []byte("\x7FELF")): + f, err := elf.NewFile(r) + if err != nil { + return nil, errUnrecognizedFormat + } + return &elfExe{f}, nil + default: + return nil, errUnrecognizedFormat + } + + return nil, errUnrecognizedFormat +} + +// elfExe is the ELF implementation of the exe interface. +type elfExe struct { + f *elf.File +} + +func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) { + for _, prog := range x.f.Progs { + if prog.Vaddr > addr || addr > prog.Vaddr+prog.Filesz-1 { + continue + } + n := prog.Vaddr + prog.Filesz - addr + if n > size { + n = size + } + data := make([]byte, n) + _, err := prog.ReadAt(data, int64(addr-prog.Vaddr)) + if err != nil { + return nil, err + } + return data, nil + } + return nil, fmt.Errorf("address not mapped") +} + +func (x *elfExe) DataStart() (uint64, uint64) { + for _, s := range x.f.Sections { + if s.Name == ".rodata" { + return s.Addr, s.SectionHeader.Size + } + } + return 0, 0 +} diff --git a/pkg/dependency/parser/executable/nodejs/parse.go b/pkg/dependency/parser/executable/nodejs/parse.go new file mode 100644 index 00000000000..df27cbc6aea --- /dev/null +++ b/pkg/dependency/parser/executable/nodejs/parse.go @@ -0,0 +1,70 @@ +// Ported from https://github.com/golang/go/blob/e9c96835971044aa4ace37c7787de231bbde05d9/src/cmd/go/internal/version/version.go + +package nodejsparser + +import ( + "bytes" + "regexp" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + exe "github.com/aquasecurity/trivy/pkg/dependency/parser/executable" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +var ( + ErrUnrecognizedExe = xerrors.New("unrecognized executable format") +) + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +// Parse scans file to try to report the NodeJS version. +func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + x, err := exe.OpenExe(r) + if err != nil { + return nil, nil, ErrUnrecognizedExe + } + + mod, vers := findVers(x) + if vers == "" { + return nil, nil, nil + } + + var libs []ftypes.Package + libs = append(libs, ftypes.Package{ + ID: dependency.ID(ftypes.NodeJsExecutable, mod, vers), + Name: mod, + Version: vers, + }) + + return libs, nil, nil +} + +// findVers finds and returns the NodeJS version in the executable x. +func findVers(x exe.Exe) (vers, mod string) { + text, size := x.DataStart() + data, err := x.ReadData(text, size) + if err != nil { + return + } + + re := regexp.MustCompile(`node\.js\/v(\d{1,3}\.\d{1,3}\.\d{1,3})`) + // split by null characters + items := bytes.Split(data, []byte("\000")) + for _, s := range items { + // Extract the version number + match := re.FindSubmatch(s) + if match != nil { + vers = string(match[1]) + break + } + } + + return "node", vers +} diff --git a/pkg/dependency/parser/executable/nodejs/parse_test.go b/pkg/dependency/parser/executable/nodejs/parse_test.go new file mode 100644 index 00000000000..3bf07081cdd --- /dev/null +++ b/pkg/dependency/parser/executable/nodejs/parse_test.go @@ -0,0 +1,54 @@ +package nodejsparser + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + inputFile string + want []types.Package + wantDep []types.Dependency + wantErr string + }{ + { + name: "ELF12", + inputFile: "testdata/node.12.elf", + want: []types.Package{ + { + ID: "node@12.16.3", + Name: "node", + Version: "12.16.3", + }, + }, + }, + { + name: "sad path", + inputFile: "testdata/dummy", + wantErr: "unrecognized executable format", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + parser := NewParser() + got, _, err := parser.Parse(f) + if tt.wantErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/executable/nodejs/testdata/dummy b/pkg/dependency/parser/executable/nodejs/testdata/dummy new file mode 100644 index 00000000000..26bf640459d --- /dev/null +++ b/pkg/dependency/parser/executable/nodejs/testdata/dummy @@ -0,0 +1 @@ +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ No newline at end of file diff --git a/pkg/dependency/parser/executable/nodejs/testdata/node.12.elf b/pkg/dependency/parser/executable/nodejs/testdata/node.12.elf new file mode 100644 index 00000000000..f91f9c958c6 Binary files /dev/null and b/pkg/dependency/parser/executable/nodejs/testdata/node.12.elf differ diff --git a/pkg/dependency/parser/executable/php/parse.go b/pkg/dependency/parser/executable/php/parse.go new file mode 100644 index 00000000000..f662a4c728f --- /dev/null +++ b/pkg/dependency/parser/executable/php/parse.go @@ -0,0 +1,71 @@ +// Ported from https://github.com/golang/go/blob/e9c96835971044aa4ace37c7787de231bbde05d9/src/cmd/go/internal/version/version.go + +package phpparser + +import ( + "bytes" + "regexp" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + exe "github.com/aquasecurity/trivy/pkg/dependency/parser/executable" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +var ( + ErrUnrecognizedExe = xerrors.New("unrecognized executable format") + ErrNonPythonBinary = xerrors.New("non Python binary") +) + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +// Parse scans file to try to report the Python version. +func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + x, err := exe.OpenExe(r) + if err != nil { + return nil, nil, ErrUnrecognizedExe + } + + name, vers := findVers(x) + if vers == "" { + return nil, nil, nil + } + + var libs []ftypes.Package + libs = append(libs, ftypes.Package{ + ID: dependency.ID(ftypes.PhpExecutable, name, vers), + Name: name, + Version: vers, + }) + + return libs, nil, nil +} + +// findVers finds and returns the PHP version in the executable x. +func findVers(x exe.Exe) (vers, mod string) { + text, size := x.DataStart() + data, err := x.ReadData(text, size) + if err != nil { + return + } + + re := regexp.MustCompile(`(?m)X-Powered-By: PHP\/(?P[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`) + // split by null characters + items := bytes.Split(data, []byte("\000")) + for _, s := range items { + // Extract the version number + match := re.FindSubmatch(s) + if match != nil { + vers = string(match[1]) + break + } + } + + return "php", vers +} diff --git a/pkg/dependency/parser/executable/php/parse_test.go b/pkg/dependency/parser/executable/php/parse_test.go new file mode 100644 index 00000000000..a08552fe8ef --- /dev/null +++ b/pkg/dependency/parser/executable/php/parse_test.go @@ -0,0 +1,54 @@ +package phpparser + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + inputFile string + want []types.Package + wantErr string + }{ + { + name: "ELF", + inputFile: "testdata/php.elf", + want: []types.Package{ + { + ID: "php@8.0.7", + Name: "php", + Version: "8.0.7", + }, + }, + }, + { + name: "sad path", + inputFile: "testdata/dummy", + wantErr: "unrecognized executable format", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + + parser := NewParser() + got, _, err := parser.Parse(f) + if tt.wantErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/executable/php/testdata/dummy b/pkg/dependency/parser/executable/php/testdata/dummy new file mode 100644 index 00000000000..26bf640459d --- /dev/null +++ b/pkg/dependency/parser/executable/php/testdata/dummy @@ -0,0 +1 @@ +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ No newline at end of file diff --git a/pkg/dependency/parser/executable/php/testdata/php.elf b/pkg/dependency/parser/executable/php/testdata/php.elf new file mode 100644 index 00000000000..6f559229913 Binary files /dev/null and b/pkg/dependency/parser/executable/php/testdata/php.elf differ diff --git a/pkg/dependency/parser/executable/python/parse_test.go b/pkg/dependency/parser/executable/python/parse_test.go new file mode 100644 index 00000000000..4b9bd7fa2cf --- /dev/null +++ b/pkg/dependency/parser/executable/python/parse_test.go @@ -0,0 +1,76 @@ +package pythonparser + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + inputFile string + want []types.Package + wantDep []types.Dependency + wantErr string + }{ + { + name: "ELF2.7", + inputFile: "testdata/python2.7.elf", + want: []types.Package{ + { + ID: "python@2.7.18", + Name: "python", + Version: "2.7.18", + }, + }, + }, + { + name: "ELF3.9", + inputFile: "testdata/python3.9.elf", + want: []types.Package{ + { + ID: "python@3.9.19", + Name: "python", + Version: "3.9.19", + }, + }, + }, + { + name: "ELF3.10", + inputFile: "testdata/python3.10.elf", + want: []types.Package{ + { + ID: "python@3.10.12", + Name: "python", + Version: "3.10.12", + }, + }, + }, + { + name: "sad path", + inputFile: "testdata/dummy", + wantErr: "unrecognized executable format", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + parser := NewParser() + got, _, err := parser.Parse(f) + if tt.wantErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/executable/python/parser.go b/pkg/dependency/parser/executable/python/parser.go new file mode 100644 index 00000000000..267ed9ab98f --- /dev/null +++ b/pkg/dependency/parser/executable/python/parser.go @@ -0,0 +1,72 @@ +// Ported from https://github.com/golang/go/blob/e9c96835971044aa4ace37c7787de231bbde05d9/src/cmd/go/internal/version/version.go + +package pythonparser + +import ( + "bytes" + "regexp" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + exe "github.com/aquasecurity/trivy/pkg/dependency/parser/executable" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +var ( + ErrUnrecognizedExe = xerrors.New("unrecognized executable format") + ErrNonPythonBinary = xerrors.New("non Python binary") +) + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +// Parse scans file to try to report the Python version. +func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + x, err := exe.OpenExe(r) + if err != nil { + return nil, nil, ErrUnrecognizedExe + } + + name, vers := findVers(x) + if vers == "" { + return nil, nil, nil + } + + var libs []ftypes.Package + libs = append(libs, ftypes.Package{ + ID: dependency.ID(ftypes.PythonExecutable, name, vers), + Name: name, + Version: vers, + }) + + return libs, nil, nil +} + +// findVers finds and returns the Python version in the executable x. +func findVers(x exe.Exe) (mod, vers string) { + text, size := x.DataStart() + data, err := x.ReadData(text, size) + if err != nil { + return + } + + // Python's version pattern is [NUL]3.11.2[NUL] + re := regexp.MustCompile(`^\d{1,4}\.\d{1,4}\.\d{1,4}[-._a-zA-Z0-9]*$`) + // split by null characters, this is important so that we don't match for version number-like strings without the null character + items := bytes.Split(data, []byte("\000")) + for _, s := range items { + // Extract the version number + match := re.FindSubmatch(s) + if match != nil { + vers = string(match[0]) + break + } + } + + return "python", vers +} diff --git a/pkg/dependency/parser/executable/python/testdata/dummy b/pkg/dependency/parser/executable/python/testdata/dummy new file mode 100644 index 00000000000..26bf640459d --- /dev/null +++ b/pkg/dependency/parser/executable/python/testdata/dummy @@ -0,0 +1 @@ +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ No newline at end of file diff --git a/pkg/dependency/parser/executable/python/testdata/python2.7.elf b/pkg/dependency/parser/executable/python/testdata/python2.7.elf new file mode 100644 index 00000000000..4cfee55c3d4 Binary files /dev/null and b/pkg/dependency/parser/executable/python/testdata/python2.7.elf differ diff --git a/pkg/dependency/parser/executable/python/testdata/python3.10.elf b/pkg/dependency/parser/executable/python/testdata/python3.10.elf new file mode 100644 index 00000000000..a7ac65d3c15 Binary files /dev/null and b/pkg/dependency/parser/executable/python/testdata/python3.10.elf differ diff --git a/pkg/dependency/parser/executable/python/testdata/python3.9.elf b/pkg/dependency/parser/executable/python/testdata/python3.9.elf new file mode 100644 index 00000000000..d75bbfa4fcf Binary files /dev/null and b/pkg/dependency/parser/executable/python/testdata/python3.9.elf differ diff --git a/pkg/fanal/analyzer/executable/executable.go b/pkg/fanal/analyzer/executable/executable.go index b484cb0fde3..6f0e3969496 100644 --- a/pkg/fanal/analyzer/executable/executable.go +++ b/pkg/fanal/analyzer/executable/executable.go @@ -3,11 +3,17 @@ package executable import ( "context" "os" + "regexp" "golang.org/x/xerrors" + "github.com/aquasecurity/trivy/pkg/dependency/parser/executable/nodejs" + "github.com/aquasecurity/trivy/pkg/dependency/parser/executable/php" + "github.com/aquasecurity/trivy/pkg/dependency/parser/executable/python" "github.com/aquasecurity/trivy/pkg/digest" "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/fanal/utils" ) @@ -21,6 +27,50 @@ const version = 1 // so that it can search for SBOM attestation in post-handler. type executableAnalyzer struct{} +// Returns boolean argument in first argument, indicating whether the Executable version is detectable +func isDetectableLibraryExecutable(fileInfo os.FileInfo) (bool, types.TargetType, error) { + isPythonExecutable := isDetectablePythonExecutable(fileInfo) + if isPythonExecutable { + return true, types.PythonExecutable, nil + } + isNodeJsExecutable := isDetectableNodeJsExecutable(fileInfo) + if isNodeJsExecutable { + return true, types.NodeJsExecutable, nil + } + isPhpExecutable := isDetectablePhpExecutable(fileInfo) + if isPhpExecutable { + return true, types.PhpExecutable, nil + } + return false, types.TargetType(""), nil +} + +func isDetectablePythonExecutable(fileInfo os.FileInfo) bool { + pythonLibNameRegex := regexp.MustCompile("^libpython[0-9]+(?:[.0-9])+[a-z]?[.]so.*$") + pythonExecutableNameRegex := regexp.MustCompile("(?:.*/|^)python(?P[0-9]+(?:[.0-9])+)?$") + isPythonExecutable := pythonExecutableNameRegex.FindSubmatch([]byte(fileInfo.Name())) + isPythonLibSo := pythonLibNameRegex.FindSubmatch([]byte(fileInfo.Name())) + return (isPythonExecutable != nil || isPythonLibSo != nil) +} + +func isDetectableNodeJsExecutable(fileInfo os.FileInfo) bool { + nodejsExecutableNameRegex := regexp.MustCompile("(?:.*/|^)node(?P[0-9]+(?:[.0-9])+)?$") + isNodeJsExecutable := nodejsExecutableNameRegex.FindSubmatch([]byte(fileInfo.Name())) + return (isNodeJsExecutable != nil) +} + +func isDetectablePhpExecutable(fileInfo os.FileInfo) bool { + phpExecutableNameRegex := regexp.MustCompile("(.*/|^)php[0-9]*$") + phpLibNameRegex := regexp.MustCompile("(.*/|^)libphp[0-9a-z.-]*[.]so$") + phpFpmNameRegex := regexp.MustCompile("(.*/|^)php-fpm[0-9]*$") + phpCgiNameRegex := regexp.MustCompile("(.*/|^)php-cgi[0-9]*$") + + isPHPExecutable := phpExecutableNameRegex.FindSubmatch([]byte(fileInfo.Name())) + isPHPLib := phpLibNameRegex.FindSubmatch([]byte(fileInfo.Name())) + isPHPFpm := phpFpmNameRegex.FindSubmatch([]byte(fileInfo.Name())) + isPHPCgi := phpCgiNameRegex.FindSubmatch([]byte(fileInfo.Name())) + return (isPHPExecutable != nil || isPHPLib != nil || isPHPFpm != nil || isPHPCgi != nil) +} + func (a executableAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { // Skip non-binaries isBinary, err := utils.IsBinary(input.Content, input.Info.Size()) @@ -32,6 +82,25 @@ func (a executableAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisIn if err != nil { return nil, xerrors.Errorf("sha256 error: %w", err) } + isDetectableLib, binaryType, err := isDetectableLibraryExecutable(input.Info) + if isDetectableLib && binaryType != "" && err == nil { + var res *analyzer.AnalysisResult = nil + switch binaryType { + case types.PythonExecutable: + res, err = language.Analyze(types.PythonExecutable, input.FilePath, input.Content, pythonparser.NewParser()) + case types.NodeJsExecutable: + res, err = language.Analyze(types.NodeJsExecutable, input.FilePath, input.Content, nodejsparser.NewParser()) + case types.PhpExecutable: + res, err = language.Analyze(types.PhpExecutable, input.FilePath, input.Content, phpparser.NewParser()) + } + if err != nil { + return nil, err + } + if res != nil { + res.Digests = map[string]string{input.FilePath: dig.String()} + return res, nil + } + } return &analyzer.AnalysisResult{ Digests: map[string]string{ diff --git a/pkg/fanal/analyzer/executable/executable_test.go b/pkg/fanal/analyzer/executable/executable_test.go index 774359163a6..4fef9ef12ce 100644 --- a/pkg/fanal/analyzer/executable/executable_test.go +++ b/pkg/fanal/analyzer/executable/executable_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" ) func Test_executableAnalyzer_Analyze(t *testing.T) { @@ -31,6 +32,72 @@ func Test_executableAnalyzer_Analyze(t *testing.T) { filePath: "testdata/hello.txt", want: nil, }, + { + name: "Python binary", + filePath: "testdata/python2.7", + want: &analyzer.AnalysisResult{ + Digests: map[string]string{ + "testdata/python2.7": "sha256:c43714431f84c27aa30b4b2368d6570fcafdced12e2e9aa0efb10aeb5cbe5a6b", + }, + Applications: []types.Application{ + { + Type: types.PythonExecutable, + FilePath: "testdata/python2.7", + Packages: types.Packages{ + { + ID: "python@2.7.18", + Name: "python", + Version: "2.7.18", + }, + }, + }, + }, + }, + }, + { + name: "Php Binary", + filePath: "testdata/php", + want: &analyzer.AnalysisResult{ + Digests: map[string]string{ + "testdata/php": "sha256:38afd180eaa357b320cffa30293052b7c732d2e4f8fa8cef9250ef00eef6491c", + }, + Applications: []types.Application{ + { + Type: types.PhpExecutable, + FilePath: "testdata/php", + Packages: types.Packages{ + { + ID: "php@8.0.7", + Name: "php", + Version: "8.0.7", + }, + }, + }, + }, + }, + }, + { + name: "NodeJS Binary", + filePath: "testdata/node", + want: &analyzer.AnalysisResult{ + Digests: map[string]string{ + "testdata/node": "sha256:a96e9711ed4fc86ede60e8992dac02e32edfb41949c8edc36d09318a26ac8c10", + }, + Applications: []types.Application{ + { + Type: types.NodeJsExecutable, + FilePath: "testdata/node", + Packages: types.Packages{ + { + ID: "node@12.16.3", + Name: "node", + Version: "12.16.3", + }, + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/fanal/analyzer/executable/testdata/node b/pkg/fanal/analyzer/executable/testdata/node new file mode 100755 index 00000000000..f91f9c958c6 Binary files /dev/null and b/pkg/fanal/analyzer/executable/testdata/node differ diff --git a/pkg/fanal/analyzer/executable/testdata/php b/pkg/fanal/analyzer/executable/testdata/php new file mode 100755 index 00000000000..6f559229913 Binary files /dev/null and b/pkg/fanal/analyzer/executable/testdata/php differ diff --git a/pkg/fanal/analyzer/executable/testdata/python2.7 b/pkg/fanal/analyzer/executable/testdata/python2.7 new file mode 100755 index 00000000000..4cfee55c3d4 Binary files /dev/null and b/pkg/fanal/analyzer/executable/testdata/python2.7 differ diff --git a/pkg/fanal/types/const.go b/pkg/fanal/types/const.go index 6874b8a40b0..7130fb27804 100644 --- a/pkg/fanal/types/const.go +++ b/pkg/fanal/types/const.go @@ -148,3 +148,10 @@ const ( JuliaProject = "Project.toml" JuliaManifest = "Manifest.toml" ) + +// Detectable executable types +const ( + PythonExecutable LangType = "python" + NodeJsExecutable LangType = "nodejs" + PhpExecutable LangType = "php" +) diff --git a/pkg/purl/purl.go b/pkg/purl/purl.go index 92ce07be974..4f8c70fcf39 100644 --- a/pkg/purl/purl.go +++ b/pkg/purl/purl.go @@ -476,6 +476,8 @@ func purlType(t ftypes.TargetType) string { ftypes.Amazon, ftypes.Fedora, ftypes.Oracle, ftypes.OpenSUSE, ftypes.OpenSUSELeap, ftypes.OpenSUSETumbleweed, ftypes.SLES, ftypes.Photon: return packageurl.TypeRPM + case ftypes.PythonExecutable, ftypes.PhpExecutable, ftypes.NodeJsExecutable: + return packageurl.TypeGeneric case TypeOCI: return packageurl.TypeOCI case ftypes.Julia: