From f728f197341097fecaba6ebb088ea528f0ceb53b Mon Sep 17 00:00:00 2001
From: Teppei Fukuda <knqyf263@gmail.com>
Date: Sat, 10 Jul 2021 11:07:53 +0300
Subject: [PATCH] feat(config): add external scanner (#206)

---
 external/config_scan.go             | 66 ++++++++++++++++++++++
 external/config_scan_test.go        | 88 +++++++++++++++++++++++++++++
 external/testdata/allow/Dockerfile  |  3 +
 external/testdata/allow/docker.rego | 20 +++++++
 external/testdata/deny/Dockerfile   |  3 +
 external/testdata/deny/docker.rego  | 20 +++++++
 6 files changed, 200 insertions(+)
 create mode 100644 external/config_scan.go
 create mode 100644 external/config_scan_test.go
 create mode 100644 external/testdata/allow/Dockerfile
 create mode 100644 external/testdata/allow/docker.rego
 create mode 100644 external/testdata/deny/Dockerfile
 create mode 100644 external/testdata/deny/docker.rego

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"
+}