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

feat: Add binary version parsing for nodejs, php and python #6524

Draft
wants to merge 36 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
105f39e
Add binary version parsing for java, nodejs, php and python
Apr 19, 2024
b6027e8
Add parse test for Python binary
Apr 19, 2024
5b3eec8
go mod tidy
Apr 19, 2024
3ead37c
lint via gci + fix lint errors
Apr 19, 2024
82ae5f5
Remove := because of linter
Apr 19, 2024
407ebd8
Fix parser to grab correct match index
Apr 19, 2024
8e8443c
Normalize to binary go package
Apr 19, 2024
526cd87
Implement standalone python binary scanning in fanal
Apr 19, 2024
1a1425f
Python version parsing
Apr 20, 2024
35a0a66
Remove java and add nodejs and php support for standalone binaries
Apr 20, 2024
699531d
Refactor to remove duplicated code, reflect some review comments
Jun 3, 2024
a708e38
Add binary version parsing for java, nodejs, php and python
Apr 19, 2024
68624ea
Add parse test for Python binary
Apr 19, 2024
d704cda
go mod tidy
Apr 19, 2024
a57944f
lint via gci + fix lint errors
Apr 19, 2024
d7c795b
Remove := because of linter
Apr 19, 2024
41f0857
Fix parser to grab correct match index
Apr 19, 2024
66a008b
Normalize to binary go package
Apr 19, 2024
a9f14c6
Implement standalone python binary scanning in fanal
Apr 19, 2024
5c326db
Python version parsing
Apr 20, 2024
da71578
Remove java and add nodejs and php support for standalone binaries
Apr 20, 2024
39c24d0
Refactor to remove duplicated code, reflect some review comments
Jun 3, 2024
1a78560
Merge branch 'parse-binary-versions' of github.com:kovacs-levent/triv…
Jun 3, 2024
ed91d36
Refactor to remove duplicated code, reflect some review comments
Jun 3, 2024
e6e3165
Fix dependency errors
Jun 3, 2024
9e39387
Merge branch 'parse-binary-versions' of github.com:kovacs-levent/triv…
Jun 3, 2024
5f93328
Resolve previously unfixed conflict
Jun 3, 2024
6ba4de5
Rework fanal tests
Jun 3, 2024
3e71c17
Remove unused import
Jun 3, 2024
5ac66dc
Fix go.mod
Jun 3, 2024
739880a
Fix lint errors
Jun 3, 2024
169e9ae
Add seperate types const for detectable binaries
Jun 4, 2024
f35d924
Remove unused types
Jun 4, 2024
5834ab9
fix lint issues
Jun 4, 2024
3cf74b4
Fix Rekor disabling executable analysis, and only disable Rekor queries
Jun 4, 2024
e0a45ee
Update Exe implementation inspired by updated buildinfo.go
Jun 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 9 additions & 4 deletions pkg/commands/artifact/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
76 changes: 76 additions & 0 deletions pkg/dependency/parser/executable/executable.go
Original file line number Diff line number Diff line change
@@ -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
}
70 changes: 70 additions & 0 deletions pkg/dependency/parser/executable/nodejs/parse.go
Original file line number Diff line number Diff line change
@@ -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
}
54 changes: 54 additions & 0 deletions pkg/dependency/parser/executable/nodejs/parse_test.go
Original file line number Diff line number Diff line change
@@ -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: "[email protected]",
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)
})
}
}
1 change: 1 addition & 0 deletions pkg/dependency/parser/executable/nodejs/testdata/dummy
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Binary file not shown.
71 changes: 71 additions & 0 deletions pkg/dependency/parser/executable/php/parse.go
Original file line number Diff line number Diff line change
@@ -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<version>[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
}
54 changes: 54 additions & 0 deletions pkg/dependency/parser/executable/php/parse_test.go
Original file line number Diff line number Diff line change
@@ -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: "[email protected]",
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)
})
}
}
1 change: 1 addition & 0 deletions pkg/dependency/parser/executable/php/testdata/dummy
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Binary file not shown.