diff --git a/external/config_scan.go b/external/config_scan.go new file mode 100644 index 000000000..8a14de97c --- /dev/null +++ b/external/config_scan.go @@ -0,0 +1,66 @@ +package external + +import ( + "context" + "errors" + + "github.com/aquasecurity/fanal/analyzer" + "github.com/aquasecurity/fanal/analyzer/config" + "github.com/aquasecurity/fanal/applier" + "github.com/aquasecurity/fanal/artifact/local" + "github.com/aquasecurity/fanal/cache" + "github.com/aquasecurity/fanal/types" +) + +type ConfigScanner struct { + cache cache.FSCache + policyPaths []string + dataPaths []string + namespaces []string +} + +func NewConfigScanner(cacheDir string, policyPaths, dataPaths, namespaces []string) (*ConfigScanner, error) { + // Initialize local cache + cacheClient, err := cache.NewFSCache(cacheDir) + if err != nil { + return nil, err + } + + return &ConfigScanner{ + cache: cacheClient, + policyPaths: policyPaths, + dataPaths: dataPaths, + namespaces: namespaces, + }, nil +} + +func (s ConfigScanner) Scan(dir string) ([]types.Misconfiguration, error) { + art, err := local.NewArtifact(dir, s.cache, nil, config.ScannerOption{ + PolicyPaths: s.policyPaths, + DataPaths: s.dataPaths, + Namespaces: s.namespaces, + }) + if err != nil { + return nil, err + } + + // Scan config files + result, err := art.Inspect(context.Background()) + if err != nil { + return nil, err + } + + // Merge layers + a := applier.NewApplier(s.cache) + mergedLayer, err := a.ApplyLayers(result.ID, result.BlobIDs) + if !errors.Is(err, analyzer.ErrUnknownOS) && !errors.Is(err, analyzer.ErrNoPkgsDetected) { + return nil, err + } + + // Do not assert successes and layer + for i := range mergedLayer.Misconfigurations { + mergedLayer.Misconfigurations[i].Layer = types.Layer{} + } + + return mergedLayer.Misconfigurations, nil +} diff --git a/external/config_scan_test.go b/external/config_scan_test.go new file mode 100644 index 000000000..a3f58e7b7 --- /dev/null +++ b/external/config_scan_test.go @@ -0,0 +1,88 @@ +package external_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/fanal/external" + "github.com/aquasecurity/fanal/types" +) + +func TestConfigScanner_Scan(t *testing.T) { + type fields struct { + policyPaths []string + dataPaths []string + namespaces []string + } + tests := []struct { + name string + fields fields + inputDir string + want []types.Misconfiguration + }{ + { + name: "deny", + fields: fields{ + policyPaths: []string{"testdata/deny"}, + namespaces: []string{"testdata"}, + }, + inputDir: "testdata/deny", + want: []types.Misconfiguration{ + { + FileType: "dockerfile", + FilePath: "Dockerfile", + Failures: types.MisconfResults{ + { + Namespace: "testdata.xyz_200", + Message: "Old image", + PolicyMetadata: types.PolicyMetadata{ + ID: "XYZ-200", + Type: "Docker Security Check", + Title: "Old FROM", + Severity: "LOW", + }, + }, + }, + }, + }, + }, + { + name: "allow", + fields: fields{ + policyPaths: []string{"testdata/allow"}, + namespaces: []string{"testdata"}, + }, + inputDir: "testdata/allow", + want: []types.Misconfiguration{ + { + FileType: "dockerfile", + FilePath: "Dockerfile", + Successes: types.MisconfResults{ + { + Namespace: "testdata.xyz_200", + PolicyMetadata: types.PolicyMetadata{ + ID: "XYZ-200", + Type: "Docker Security Check", + Title: "Old FROM", + Severity: "LOW", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, err := external.NewConfigScanner(t.TempDir(), + tt.fields.policyPaths, tt.fields.dataPaths, tt.fields.namespaces) + require.NoError(t, err) + + got, err := s.Scan(tt.inputDir) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/external/testdata/allow/Dockerfile b/external/testdata/allow/Dockerfile new file mode 100644 index 000000000..fb7daf440 --- /dev/null +++ b/external/testdata/allow/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:3.14 + +ADD foo.txt . \ No newline at end of file diff --git a/external/testdata/allow/docker.rego b/external/testdata/allow/docker.rego new file mode 100644 index 000000000..0ef57f138 --- /dev/null +++ b/external/testdata/allow/docker.rego @@ -0,0 +1,20 @@ +package testdata.xyz_200 + +__rego_metadata__ := { + "id": "XYZ-200", + "title": "Old FROM", + "version": "v1.0.0", + "severity": "LOW", + "type": "Docker Security Check", +} + +__rego_input__ := { + "combine": false, + "selector": [{"type": "dockerfile"}], +} + +deny[msg] { + input.stages[from] + from == "alpine:3.10" + msg := "Old image" +} diff --git a/external/testdata/deny/Dockerfile b/external/testdata/deny/Dockerfile new file mode 100644 index 000000000..b3be16e28 --- /dev/null +++ b/external/testdata/deny/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:3.10 + +ADD foo.txt . \ No newline at end of file diff --git a/external/testdata/deny/docker.rego b/external/testdata/deny/docker.rego new file mode 100644 index 000000000..0ef57f138 --- /dev/null +++ b/external/testdata/deny/docker.rego @@ -0,0 +1,20 @@ +package testdata.xyz_200 + +__rego_metadata__ := { + "id": "XYZ-200", + "title": "Old FROM", + "version": "v1.0.0", + "severity": "LOW", + "type": "Docker Security Check", +} + +__rego_input__ := { + "combine": false, + "selector": [{"type": "dockerfile"}], +} + +deny[msg] { + input.stages[from] + from == "alpine:3.10" + msg := "Old image" +}