Skip to content
This repository has been archived by the owner on Jun 21, 2022. It is now read-only.

Move file patterns to a higher level to be able to use it on any analyzer #372

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions analyzer/all/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package all
import (
_ "github.com/aquasecurity/fanal/analyzer/buildinfo"
_ "github.com/aquasecurity/fanal/analyzer/command/apk"
_ "github.com/aquasecurity/fanal/analyzer/config/all"
_ "github.com/aquasecurity/fanal/analyzer/language/dotnet/nuget"
_ "github.com/aquasecurity/fanal/analyzer/language/golang/binary"
_ "github.com/aquasecurity/fanal/analyzer/language/golang/mod"
Expand Down
47 changes: 42 additions & 5 deletions analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"io/fs"
"os"
"regexp"
"sort"
"strings"
"sync"
Expand Down Expand Up @@ -91,6 +92,7 @@ type Opener func() (dio.ReadSeekCloserAt, error)
type AnalyzerGroup struct {
analyzers []analyzer
configAnalyzers []configAnalyzer
filePatterns map[Type][]*regexp.Regexp
}

type AnalysisResult struct {
Expand Down Expand Up @@ -243,12 +245,37 @@ func belongToGroup(groupName Group, analyzerType Type, disabledAnalyzers []Type,
return true
}

func NewAnalyzerGroup(groupName Group, disabledAnalyzers []Type) AnalyzerGroup {
const separator = ":"

func NewAnalyzerGroup(groupName Group, disabledAnalyzers []Type, filePatterns []string) (AnalyzerGroup, error) {
if groupName == "" {
groupName = GroupBuiltin
}

var group AnalyzerGroup
group := AnalyzerGroup{
filePatterns: map[Type][]*regexp.Regexp{},
}

for _, p := range filePatterns {
// e.g. "dockerfile:my_dockerfile_*"
s := strings.SplitN(p, separator, 2)
if len(s) != 2 {
return group, xerrors.Errorf("invalid file pattern (%s)", p)
}

fileType, pattern := s[0], s[1]
r, err := regexp.Compile(pattern)
if err != nil {
return group, xerrors.Errorf("invalid file regexp (%s): %w", p, err)
}

if _, ok := group.filePatterns[Type(fileType)]; !ok {
group.filePatterns[Type(fileType)] = []*regexp.Regexp{}
}

group.filePatterns[Type(fileType)] = append(group.filePatterns[Type(fileType)], r)
}

for analyzerType, a := range analyzers {
if !belongToGroup(groupName, analyzerType, disabledAnalyzers, a) {
continue
Expand All @@ -263,7 +290,7 @@ func NewAnalyzerGroup(groupName Group, disabledAnalyzers []Type) AnalyzerGroup {
group.configAnalyzers = append(group.configAnalyzers, a)
}

return group
return group, nil
}

// AnalyzerVersions returns analyzer version identifier used for cache keys.
Expand All @@ -290,14 +317,24 @@ func (ag AnalyzerGroup) AnalyzeFile(ctx context.Context, wg *sync.WaitGroup, lim
return nil
}

// filepath extracted from tar file doesn't have the prefix "/"
cleanPath := strings.TrimLeft(filePath, "/")

for _, a := range ag.analyzers {
// Skip disabled analyzers
if slices.Contains(disabled, a.Type()) {
continue
}

// filepath extracted from tar file doesn't have the prefix "/"
if !a.Required(strings.TrimLeft(filePath, "/"), info) {
filePatternMatch := false
for _, pattern := range ag.filePatterns[a.Type()] {
if pattern.MatchString(cleanPath) {
filePatternMatch = true
break
}
}

if !filePatternMatch && !a.Required(cleanPath, info) {
continue
}
rc, err := opener()
Expand Down
62 changes: 58 additions & 4 deletions analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ func TestAnalyzeFile(t *testing.T) {
filePath string
testFilePath string
disabledAnalyzers []analyzer.Type
filePatterns []string
}
tests := []struct {
name string
Expand Down Expand Up @@ -377,6 +378,28 @@ func TestAnalyzeFile(t *testing.T) {
},
want: &analyzer.AnalysisResult{},
},
{
name: "happy path with library analyzer file pattern regex",
args: args{
filePath: "/app/Gemfile-dev.lock",
testFilePath: "testdata/app/Gemfile.lock",
filePatterns: []string{"bundler:Gemfile(-.*)?\\.lock"},
},
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: "bundler",
FilePath: "/app/Gemfile-dev.lock",
Libraries: []types.Package{
{
Name: "actioncable",
Version: "5.2.3",
},
},
},
},
},
},
{
name: "ignore permission error",
args: args{
Expand All @@ -393,14 +416,38 @@ func TestAnalyzeFile(t *testing.T) {
},
wantErr: "unable to open /lib/apk/db/installed",
},
{
name: "sad path with broken file pattern regex",
args: args{
filePath: "/app/Gemfile-dev.lock",
testFilePath: "testdata/app/Gemfile.lock",
filePatterns: []string{"bundler:Gemfile(-.*?\\.lock"},
},
wantErr: "error parsing regexp",
},
{
name: "sad path with broken file pattern",
args: args{
filePath: "/app/Gemfile-dev.lock",
testFilePath: "testdata/app/Gemfile.lock",
filePatterns: []string{"Gemfile(-.*)?\\.lock"},
},
wantErr: "invalid file pattern",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var wg sync.WaitGroup
limit := semaphore.NewWeighted(3)

got := new(analyzer.AnalysisResult)
a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.args.disabledAnalyzers)
a, err := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.args.disabledAnalyzers, tt.args.filePatterns)
if err != nil && tt.wantErr != "" {
require.NotNil(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)

info, err := os.Stat(tt.args.testFilePath)
require.NoError(t, err)
Expand Down Expand Up @@ -440,6 +487,7 @@ func TestAnalyzeConfig(t *testing.T) {
targetOS types.OS
configBlob []byte
disabledAnalyzers []analyzer.Type
filePatterns []string
}
tests := []struct {
name string
Expand Down Expand Up @@ -482,7 +530,9 @@ func TestAnalyzeConfig(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.args.disabledAnalyzers)
a, err := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.args.disabledAnalyzers, tt.args.filePatterns)
require.NoError(t, err)

got := a.AnalyzeImageConfig(tt.args.targetOS, tt.args.configBlob)
assert.Equal(t, tt.want, got)
})
Expand Down Expand Up @@ -517,7 +567,9 @@ func TestAnalyzer_AnalyzerVersions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.disabled)
a, err := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.disabled, nil)
require.NoError(t, err)

got := a.AnalyzerVersions()
fmt.Printf("%v\n", got)
assert.Equal(t, tt.want, got)
Expand Down Expand Up @@ -549,7 +601,9 @@ func TestAnalyzer_ImageConfigAnalyzerVersions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.disabled)
a, err := analyzer.NewAnalyzerGroup(analyzer.GroupBuiltin, tt.disabled, nil)
require.NoError(t, err)

got := a.ImageConfigAnalyzerVersions()
assert.Equal(t, tt.want, got)
})
Expand Down
9 changes: 9 additions & 0 deletions analyzer/config/all/import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package all

import (
_ "github.com/aquasecurity/fanal/analyzer/config/dockerfile"
_ "github.com/aquasecurity/fanal/analyzer/config/helm"
_ "github.com/aquasecurity/fanal/analyzer/config/json"
_ "github.com/aquasecurity/fanal/analyzer/config/terraform"
_ "github.com/aquasecurity/fanal/analyzer/config/yaml"
)
51 changes: 0 additions & 51 deletions analyzer/config/config.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
package config

import (
"regexp"
"sort"
"strings"

"github.com/aquasecurity/fanal/analyzer/config/helm"
"golang.org/x/xerrors"

"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/analyzer/config/dockerfile"
"github.com/aquasecurity/fanal/analyzer/config/json"
"github.com/aquasecurity/fanal/analyzer/config/terraform"
"github.com/aquasecurity/fanal/analyzer/config/yaml"
"github.com/aquasecurity/fanal/types"
)

const separator = ":"

type ScannerOption struct {
Trace bool
RegoOnly bool
Expand All @@ -34,40 +20,3 @@ func (o *ScannerOption) Sort() {
sort.Strings(o.PolicyPaths)
sort.Strings(o.DataPaths)
}

func RegisterConfigAnalyzers(filePatterns []string) error {
var dockerRegexp, jsonRegexp, yamlRegexp, helmRegexp *regexp.Regexp
for _, p := range filePatterns {
// e.g. "dockerfile:my_dockerfile_*"
s := strings.SplitN(p, separator, 2)
if len(s) != 2 {
return xerrors.Errorf("invalid file pattern (%s)", p)
}
fileType, pattern := s[0], s[1]
r, err := regexp.Compile(pattern)
if err != nil {
return xerrors.Errorf("invalid file regexp (%s): %w", p, err)
}

switch fileType {
case types.Dockerfile:
dockerRegexp = r
case types.JSON:
jsonRegexp = r
case types.YAML:
yamlRegexp = r
case types.Helm:
helmRegexp = r
default:
return xerrors.Errorf("unknown file type: %s, pattern: %s", fileType, pattern)
}
}

analyzer.RegisterAnalyzer(dockerfile.NewConfigAnalyzer(dockerRegexp))
analyzer.RegisterAnalyzer(terraform.NewConfigAnalyzer())
analyzer.RegisterAnalyzer(json.NewConfigAnalyzer(jsonRegexp))
analyzer.RegisterAnalyzer(yaml.NewConfigAnalyzer(yamlRegexp))
analyzer.RegisterAnalyzer(helm.NewConfigAnalyzer(helmRegexp))

return nil
}
35 changes: 15 additions & 20 deletions analyzer/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import (

func TestScannerOption_Sort(t *testing.T) {
type fields struct {
Namespaces []string
FilePatterns []string
PolicyPaths []string
DataPaths []string
Namespaces []string
PolicyPaths []string
DataPaths []string
}
tests := []struct {
name string
Expand All @@ -23,25 +22,22 @@ func TestScannerOption_Sort(t *testing.T) {
{
name: "happy path",
fields: fields{
Namespaces: []string{"main", "custom", "default"},
FilePatterns: []string{"dockerfile:foo*", "yaml:yml_*"},
PolicyPaths: []string{"policy"},
DataPaths: []string{"data/b", "data/c", "data/a"},
Namespaces: []string{"main", "custom", "default"},
PolicyPaths: []string{"policy"},
DataPaths: []string{"data/b", "data/c", "data/a"},
},
want: config.ScannerOption{
Namespaces: []string{"custom", "default", "main"},
FilePatterns: []string{"dockerfile:foo*", "yaml:yml_*"},
PolicyPaths: []string{"policy"},
DataPaths: []string{"data/a", "data/b", "data/c"},
Namespaces: []string{"custom", "default", "main"},
PolicyPaths: []string{"policy"},
DataPaths: []string{"data/a", "data/b", "data/c"},
},
},
{
name: "missing some fields",
fields: fields{
Namespaces: []string{"main"},
FilePatterns: nil,
PolicyPaths: nil,
DataPaths: nil,
Namespaces: []string{"main"},
PolicyPaths: nil,
DataPaths: nil,
},
want: config.ScannerOption{
Namespaces: []string{"main"},
Expand All @@ -51,10 +47,9 @@ func TestScannerOption_Sort(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := config.ScannerOption{
Namespaces: tt.fields.Namespaces,
FilePatterns: tt.fields.FilePatterns,
PolicyPaths: tt.fields.PolicyPaths,
DataPaths: tt.fields.DataPaths,
Namespaces: tt.fields.Namespaces,
PolicyPaths: tt.fields.PolicyPaths,
DataPaths: tt.fields.DataPaths,
}
o.Sort()

Expand Down
Loading