From cd44bb48f8ff3cca20bb9b4c6ab3dce1907fc528 Mon Sep 17 00:00:00 2001 From: Nikita Pivkin Date: Fri, 18 Oct 2024 03:50:12 +0600 Subject: [PATCH] refactor(misconf): simplify k8s scanner (#7717) Signed-off-by: nikpivkin --- pkg/iac/scanners/helm/scanner.go | 2 +- pkg/iac/scanners/kubernetes/parser/parser.go | 57 +- pkg/iac/scanners/kubernetes/scanner.go | 112 +--- pkg/iac/scanners/kubernetes/scanner_test.go | 666 ++++--------------- 4 files changed, 132 insertions(+), 705 deletions(-) diff --git a/pkg/iac/scanners/helm/scanner.go b/pkg/iac/scanners/helm/scanner.go index bfa63a5d2c00..daf8f3108628 100644 --- a/pkg/iac/scanners/helm/scanner.go +++ b/pkg/iac/scanners/helm/scanner.go @@ -130,7 +130,7 @@ func (s *Scanner) getScanResults(path string, ctx context.Context, target fs.FS) file := file s.logger.Debug("Processing rendered chart file", log.FilePath(file.TemplateFilePath)) - manifests, err := kparser.New().Parse(strings.NewReader(file.ManifestContent), file.TemplateFilePath) + manifests, err := kparser.Parse(ctx, strings.NewReader(file.ManifestContent), file.TemplateFilePath) if err != nil { return nil, fmt.Errorf("unmarshal yaml: %w", err) } diff --git a/pkg/iac/scanners/kubernetes/parser/parser.go b/pkg/iac/scanners/kubernetes/parser/parser.go index f3ca7d613562..57d723801c98 100644 --- a/pkg/iac/scanners/kubernetes/parser/parser.go +++ b/pkg/iac/scanners/kubernetes/parser/parser.go @@ -5,69 +5,14 @@ import ( "encoding/json" "fmt" "io" - "io/fs" - "path/filepath" "regexp" "strings" "gopkg.in/yaml.v3" kyaml "sigs.k8s.io/yaml" - - "github.com/aquasecurity/trivy/pkg/log" ) -type Parser struct { - logger *log.Logger -} - -// New creates a new K8s parser -func New() *Parser { - return &Parser{ - logger: log.WithPrefix("k8s parser"), - } -} - -func (p *Parser) ParseFS(ctx context.Context, target fs.FS, path string) (map[string][]any, error) { - files := make(map[string][]any) - if err := fs.WalkDir(target, filepath.ToSlash(path), func(path string, entry fs.DirEntry, err error) error { - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - if err != nil { - return err - } - if entry.IsDir() { - return nil - } - - parsed, err := p.ParseFile(ctx, target, path) - if err != nil { - p.logger.Error("Parse error", log.FilePath(path), log.Err(err)) - return nil - } - - files[path] = parsed - return nil - }); err != nil { - return nil, err - } - return files, nil -} - -// ParseFile parses Kubernetes manifest from the provided filesystem path. -func (p *Parser) ParseFile(_ context.Context, fsys fs.FS, path string) ([]any, error) { - f, err := fsys.Open(filepath.ToSlash(path)) - if err != nil { - return nil, err - } - defer func() { _ = f.Close() }() - return p.Parse(f, path) -} - -func (p *Parser) Parse(r io.Reader, path string) ([]any, error) { - +func Parse(_ context.Context, r io.Reader, path string) ([]any, error) { contents, err := io.ReadAll(r) if err != nil { return nil, err diff --git a/pkg/iac/scanners/kubernetes/scanner.go b/pkg/iac/scanners/kubernetes/scanner.go index dfd248cc8b1d..f79d59aaaf12 100644 --- a/pkg/iac/scanners/kubernetes/scanner.go +++ b/pkg/iac/scanners/kubernetes/scanner.go @@ -3,119 +3,17 @@ package kubernetes import ( "context" "io" - "io/fs" - "path/filepath" - "sort" - "sync" - "github.com/liamg/memoryfs" - - "github.com/aquasecurity/trivy/pkg/iac/framework" - "github.com/aquasecurity/trivy/pkg/iac/rego" - "github.com/aquasecurity/trivy/pkg/iac/scan" - "github.com/aquasecurity/trivy/pkg/iac/scanners" + "github.com/aquasecurity/trivy/pkg/iac/scanners/generic" "github.com/aquasecurity/trivy/pkg/iac/scanners/kubernetes/parser" "github.com/aquasecurity/trivy/pkg/iac/scanners/options" "github.com/aquasecurity/trivy/pkg/iac/types" - "github.com/aquasecurity/trivy/pkg/log" ) -var _ scanners.FSScanner = (*Scanner)(nil) -var _ options.ConfigurableScanner = (*Scanner)(nil) - -type Scanner struct { - mu sync.Mutex - logger *log.Logger - options []options.ScannerOption - regoScanner *rego.Scanner - parser *parser.Parser -} - -func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} -func (s *Scanner) SetRegoOnly(bool) {} -func (s *Scanner) SetFrameworks(frameworks []framework.Framework) {} - -func NewScanner(opts ...options.ScannerOption) *Scanner { - s := &Scanner{ - options: opts, - logger: log.WithPrefix("k8s scanner"), - parser: parser.New(), - } - for _, opt := range opts { - opt(s) - } - return s -} - -func (s *Scanner) Name() string { - return "Kubernetes" -} - -func (s *Scanner) initRegoScanner(srcFS fs.FS) (*rego.Scanner, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.regoScanner != nil { - return s.regoScanner, nil - } - regoScanner := rego.NewScanner(types.SourceKubernetes, s.options...) - if err := regoScanner.LoadPolicies(srcFS); err != nil { - return nil, err - } - s.regoScanner = regoScanner - return regoScanner, nil +func NewScanner(opts ...options.ScannerOption) *generic.GenericScanner { + return generic.NewScanner("Kubernetes", types.SourceKubernetes, generic.ParseFunc(parse), opts...) } -func (s *Scanner) ScanReader(ctx context.Context, filename string, reader io.Reader) (scan.Results, error) { - memfs := memoryfs.New() - if err := memfs.MkdirAll(filepath.Base(filename), 0o700); err != nil { - return nil, err - } - data, err := io.ReadAll(reader) - if err != nil { - return nil, err - } - if err := memfs.WriteFile(filename, data, 0o644); err != nil { - return nil, err - } - return s.ScanFS(ctx, memfs, ".") -} - -func (s *Scanner) ScanFS(ctx context.Context, target fs.FS, dir string) (scan.Results, error) { - - k8sFilesets, err := s.parser.ParseFS(ctx, target, dir) - if err != nil { - return nil, err - } - - if len(k8sFilesets) == 0 { - return nil, nil - } - - var inputs []rego.Input - for path, k8sFiles := range k8sFilesets { - for _, content := range k8sFiles { - inputs = append(inputs, rego.Input{ - Path: path, - FS: target, - Contents: content, - }) - } - } - - regoScanner, err := s.initRegoScanner(target) - if err != nil { - return nil, err - } - - s.logger.Debug("Scanning files", log.Int("count", len(inputs))) - results, err := regoScanner.ScanInput(ctx, inputs...) - if err != nil { - return nil, err - } - results.SetSourceAndFilesystem("", target, false) - - sort.Slice(results, func(i, j int) bool { - return results[i].Rule().AVDID < results[j].Rule().AVDID - }) - return results, nil +func parse(ctx context.Context, r io.Reader, path string) (any, error) { + return parser.Parse(ctx, r, path) } diff --git a/pkg/iac/scanners/kubernetes/scanner_test.go b/pkg/iac/scanners/kubernetes/scanner_test.go index ef1ac38560f4..c9186a7f9c40 100644 --- a/pkg/iac/scanners/kubernetes/scanner_test.go +++ b/pkg/iac/scanners/kubernetes/scanner_test.go @@ -1,24 +1,23 @@ -package kubernetes +package kubernetes_test import ( "context" - "os" + "io/fs" "strings" "testing" + "testing/fstest" + "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/aquasecurity/trivy/internal/testutil" - "github.com/aquasecurity/trivy/pkg/iac/framework" "github.com/aquasecurity/trivy/pkg/iac/rego" "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners/kubernetes" ) -func Test_BasicScan_YAML(t *testing.T) { - - fs := testutil.CreateFS(t, map[string]string{ - "/code/example.yaml": ` +func Test_ScanYAML(t *testing.T) { + file := ` apiVersion: v1 kind: Pod metadata: @@ -28,148 +27,48 @@ spec: - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] image: busybox name: hello -`, - "/rules/rule.rego": ` +` + fsys := buildFS(map[string]string{ + "code/example.yaml": file, + "checks/rule.rego": `# METADATA +# title: test check +# custom: +# id: KSV011 +# avd_id: AVD-KSV-0011 +# severity: LOW +# input: +# selector: +# - type: kubernetes package builtin.kubernetes.KSV011 import data.lib.kubernetes -import data.lib.utils - -default failLimitsCPU = false - -__rego_metadata__ := { - "id": "KSV011", - "avd_id": "AVD-KSV-0011", - "title": "CPU not limited", - "short_code": "limit-cpu", - "version": "v1.0.0", - "severity": "LOW", - "type": "Kubernetes Security Check", - "description": "Enforcing CPU limits prevents DoS via resource exhaustion.", - "recommended_actions": "Set a limit value under 'containers[].resources.limits.cpu'.", - "url": "https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits", -} - -__rego_input__ := { - "combine": false, - "selector": [{"type": "kubernetes"}], -} - -# getLimitsCPUContainers returns all containers which have set resources.limits.cpu -getLimitsCPUContainers[container] { - allContainers := kubernetes.containers[_] - utils.has_key(allContainers.resources.limits, "cpu") - container := allContainers.name -} - -# getNoLimitsCPUContainers returns all containers which have not set -# resources.limits.cpu -getNoLimitsCPUContainers[container] { - container := kubernetes.containers[_].name - not getLimitsCPUContainers[container] -} - -# failLimitsCPU is true if containers[].resources.limits.cpu is not set -# for ANY container -failLimitsCPU { - count(getNoLimitsCPUContainers) > 0 -} deny[res] { - failLimitsCPU - - msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'resources.limits.cpu'", [getNoLimitsCPUContainers[_], kubernetes.kind, kubernetes.name])) - - res := { - "msg": msg, - "id": __rego_metadata__.id, - "title": __rego_metadata__.title, - "severity": __rego_metadata__.severity, - "type": __rego_metadata__.type, - "startline": 6, - "endline": 10, - } + container := kubernetes.containers[_] + res := result.new("fail", container) } `, }) - scanner := NewScanner( - rego.WithPolicyDirs("rules"), + scanner := kubernetes.NewScanner( + rego.WithPolicyFilesystem(fsys), + rego.WithPolicyDirs("checks"), rego.WithEmbeddedLibraries(true), ) - results, err := scanner.ScanFS(context.TODO(), fs, "code") + results, err := scanner.ScanFS(context.TODO(), fsys, "code") require.NoError(t, err) - require.Len(t, results.GetFailed(), 1) - - assert.Equal(t, scan.Rule{ - AVDID: "AVD-KSV-0011", - Aliases: []string{"KSV011"}, - ShortCode: "limit-cpu", - Summary: "CPU not limited", - Explanation: "Enforcing CPU limits prevents DoS via resource exhaustion.", - Impact: "", - Resolution: "Set a limit value under 'containers[].resources.limits.cpu'.", - Provider: "kubernetes", - Service: "general", - Links: []string{"https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits"}, - Severity: "LOW", - Terraform: &scan.EngineMetadata{}, - CloudFormation: &scan.EngineMetadata{}, - CustomChecks: scan.CustomChecks{Terraform: (*scan.TerraformCustomCheck)(nil)}, - RegoPackage: "data.builtin.kubernetes.KSV011", - Frameworks: map[framework.Framework][]string{ - framework.Default: {}, - }, - }, results.GetFailed()[0].Rule()) + failed := results.GetFailed() + require.Len(t, failed, 1) - failure := results.GetFailed()[0] - actualCode, err := failure.GetCode() - require.NoError(t, err) - for i := range actualCode.Lines { - actualCode.Lines[i].Highlighted = "" - } - assert.Equal(t, []scan.Line{ - { - Number: 6, - Content: "spec: ", - IsCause: true, - FirstCause: true, - Annotation: "", - }, - { - Number: 7, - Content: " containers: ", - IsCause: true, - Annotation: "", - }, - { - Number: 8, - Content: " - command: [\"sh\", \"-c\", \"echo 'Hello' && sleep 1h\"]", - IsCause: true, - Annotation: "", - }, - { - Number: 9, - Content: " image: busybox", - IsCause: true, - Annotation: "", - }, - { - Number: 10, - Content: " name: hello", - IsCause: true, - LastCause: true, - Annotation: "", - }, - }, actualCode.Lines) + assert.Equal(t, "AVD-KSV-0011", failed[0].Rule().AVDID) + assertLines(t, file, failed) } -func Test_BasicScan_JSON(t *testing.T) { +func Test_ScanJSON(t *testing.T) { - fs := testutil.CreateFS(t, map[string]string{ - "/code/example.json": ` + file := ` { "apiVersion": "v1", "kind": "Pod", @@ -190,165 +89,58 @@ func Test_BasicScan_JSON(t *testing.T) { ] } } -`, - "/rules/rule.rego": ` +` + + fsys := buildFS(map[string]string{ + "code/example.json": file, + "checks/rule.rego": `# METADATA +# title: test check +# custom: +# id: KSV011 +# avd_id: AVD-KSV-0011 +# severity: LOW +# input: +# selector: +# - type: kubernetes package builtin.kubernetes.KSV011 import data.lib.kubernetes -import data.lib.utils - -default failLimitsCPU = false - -__rego_metadata__ := { - "id": "KSV011", - "avd_id": "AVD-KSV-0011", - "title": "CPU not limited", - "short_code": "limit-cpu", - "version": "v1.0.0", - "severity": "LOW", - "type": "Kubernetes Security Check", - "description": "Enforcing CPU limits prevents DoS via resource exhaustion.", - "recommended_actions": "Set a limit value under 'containers[].resources.limits.cpu'.", - "url": "https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits", -} - -__rego_input__ := { - "combine": false, - "selector": [{"type": "kubernetes"}], -} - -# getLimitsCPUContainers returns all containers which have set resources.limits.cpu -getLimitsCPUContainers[container] { - allContainers := kubernetes.containers[_] - utils.has_key(allContainers.resources.limits, "cpu") - container := allContainers.name -} - -# getNoLimitsCPUContainers returns all containers which have not set -# resources.limits.cpu -getNoLimitsCPUContainers[container] { - container := kubernetes.containers[_].name - not getLimitsCPUContainers[container] -} - -# failLimitsCPU is true if containers[].resources.limits.cpu is not set -# for ANY container -failLimitsCPU { - count(getNoLimitsCPUContainers) > 0 -} deny[res] { - failLimitsCPU - - msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'resources.limits.cpu'", [getNoLimitsCPUContainers[_], kubernetes.kind, kubernetes.name])) - - res := { - "msg": msg, - "id": __rego_metadata__.id, - "title": __rego_metadata__.title, - "severity": __rego_metadata__.severity, - "type": __rego_metadata__.type, - "startline": 6, - "endline": 10, - } + container := kubernetes.containers[_] + res := result.new("fail", container) } `, }) - scanner := NewScanner( - rego.WithPolicyDirs("rules"), + scanner := kubernetes.NewScanner( + rego.WithPolicyFilesystem(fsys), + rego.WithPolicyDirs("checks"), rego.WithEmbeddedLibraries(true), ) - results, err := scanner.ScanFS(context.TODO(), fs, "code") + results, err := scanner.ScanFS(context.TODO(), fsys, "code") require.NoError(t, err) require.Len(t, results.GetFailed(), 1) - assert.Equal(t, scan.Rule{ - AVDID: "AVD-KSV-0011", - Aliases: []string{"KSV011"}, - ShortCode: "limit-cpu", - Summary: "CPU not limited", - Explanation: "Enforcing CPU limits prevents DoS via resource exhaustion.", - Impact: "", - Resolution: "Set a limit value under 'containers[].resources.limits.cpu'.", - Provider: "kubernetes", - Service: "general", - Links: []string{"https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits"}, - Severity: "LOW", - Terraform: &scan.EngineMetadata{}, - CloudFormation: &scan.EngineMetadata{}, - CustomChecks: scan.CustomChecks{Terraform: (*scan.TerraformCustomCheck)(nil)}, - RegoPackage: "data.builtin.kubernetes.KSV011", - Frameworks: map[framework.Framework][]string{ - framework.Default: {}, - }, - }, results.GetFailed()[0].Rule()) + failed := results.GetFailed() + require.Len(t, failed, 1) - failure := results.GetFailed()[0] - actualCode, err := failure.GetCode() - require.NoError(t, err) - for i := range actualCode.Lines { - actualCode.Lines[i].Highlighted = "" - } - assert.Equal(t, []scan.Line{ - { - Number: 6, - Content: ` "name": "hello-cpu-limit"`, - IsCause: true, - FirstCause: true, - Annotation: "", - }, - { - Number: 7, - Content: ` },`, - IsCause: true, - Annotation: "", - }, - { - Number: 8, - Content: ` "spec": {`, - IsCause: true, - Annotation: "", - }, - { - Number: 9, - Content: ` "containers": [`, - IsCause: true, - Annotation: "", - }, - { - Number: 10, - Content: ` {`, - IsCause: true, - LastCause: true, - Annotation: "", - }, - }, actualCode.Lines) + assert.Equal(t, "AVD-KSV-0011", failed[0].Rule().AVDID) + assertLines(t, file, failed) } -func Test_FileScan(t *testing.T) { +func Test_YamlWithSeparator(t *testing.T) { - results, err := NewScanner(rego.WithEmbeddedPolicies(true), rego.WithEmbeddedLibraries(true), rego.WithEmbeddedLibraries(true)).ScanReader(context.TODO(), "k8s.yaml", strings.NewReader(` -apiVersion: v1 -kind: Pod -metadata: - name: hello-cpu-limit -spec: - containers: - - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] - image: busybox - name: hello -`)) - require.NoError(t, err) - - assert.NotEmpty(t, results.GetFailed()) -} - -func Test_FileScan_WithSeparator(t *testing.T) { + fsys := buildFS(map[string]string{ + "check.rego": `package defsec - results, err := NewScanner(rego.WithEmbeddedPolicies(true), rego.WithEmbeddedLibraries(true)).ScanReader(context.TODO(), "k8s.yaml", strings.NewReader(` +deny[res] { + input.kind == "Pod" + res := result.new("fail", input) +}`, + "k8s.yaml": ` --- --- apiVersion: v1 @@ -360,13 +152,21 @@ spec: - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] image: busybox name: hello -`)) +`, + }) + + scanner := kubernetes.NewScanner( + rego.WithPolicyFilesystem(fsys), + rego.WithPolicyDirs("."), + rego.WithEmbeddedLibraries(true), + ) + results, err := scanner.ScanFS(context.TODO(), fsys, ".") require.NoError(t, err) assert.NotEmpty(t, results.GetFailed()) } -func Test_FileScan_MultiManifests(t *testing.T) { +func Test_YamlMultiDocument(t *testing.T) { file := ` --- apiVersion: v1 @@ -389,306 +189,71 @@ spec: image: busybox name: hello2 ` - - results, err := NewScanner( - rego.WithEmbeddedPolicies(true), - rego.WithEmbeddedLibraries(true), - rego.WithEmbeddedLibraries(true)).ScanReader(context.TODO(), "k8s.yaml", strings.NewReader(file)) - require.NoError(t, err) - - assert.Greater(t, len(results.GetFailed()), 1) - fileLines := strings.Split(file, "\n") - for _, failure := range results.GetFailed() { - actualCode, err := failure.GetCode() - require.NoError(t, err) - assert.NotEmpty(t, actualCode.Lines) - for _, line := range actualCode.Lines { - assert.Greater(t, len(fileLines), line.Number) - assert.Equal(t, line.Content, fileLines[line.Number-1]) - } - } -} - -func Test_FileScanWithPolicyReader(t *testing.T) { - - results, err := NewScanner(rego.WithPolicyReader(strings.NewReader(`package defsec - -deny[msg] { - msg = "fail" -} -`))).ScanReader(context.TODO(), "k8s.yaml", strings.NewReader(` -apiVersion: v1 -kind: Pod -metadata: - name: hello-cpu-limit -spec: - containers: - - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] - image: busybox - name: hello -`)) - require.NoError(t, err) - - assert.Len(t, results.GetFailed(), 1) -} - -func Test_FileScanJSON(t *testing.T) { - - results, err := NewScanner(rego.WithPolicyReader(strings.NewReader(`package defsec - -deny[msg] { - input.kind == "Pod" - msg = "fail" -} -`))).ScanReader(context.TODO(), "k8s.json", strings.NewReader(` -{ - "kind": "Pod", - "apiVersion": "v1", - "metadata": { - "name": "mongo", - "labels": { - "name": "mongo", - "role": "mongo" - } - }, - "spec": { - "volumes": [ - { - "name": "mongo-disk", - "gcePersistentDisk": { - "pdName": "mongo-disk", - "fsType": "ext4" - } - } - ], - "containers": [ - { - "name": "mongo", - "image": "mongo:latest", - "ports": [ - { - "name": "mongo", - "containerPort": 27017 - } - ], - "volumeMounts": [ - { - "name": "mongo-disk", - "mountPath": "/data/db" - } - ] - } - ] - } -} -`)) - require.NoError(t, err) - - assert.Len(t, results.GetFailed(), 1) -} - -func Test_FileScanWithMetadata(t *testing.T) { - - results, err := NewScanner( - rego.WithTrace(os.Stdout), - rego.WithPolicyReader(strings.NewReader(`package defsec - -deny[msg] { - input.kind == "Pod" - msg := { - "msg": "fail", - "startline": 2, - "endline": 2, - "filepath": "chartname/template/serviceAccount.yaml" - } -} -`))).ScanReader( - context.TODO(), - "k8s.yaml", - strings.NewReader(` -apiVersion: v1 -kind: Pod -metadata: - name: hello-cpu-limit -spec: - containers: - - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] - image: busybox - name: hello -`)) - require.NoError(t, err) - - assert.NotEmpty(t, results.GetFailed()) - - firstResult := results.GetFailed()[0] - assert.Equal(t, 2, firstResult.Metadata().Range().GetStartLine()) - assert.Equal(t, 2, firstResult.Metadata().Range().GetEndLine()) - assert.Equal(t, "chartname/template/serviceAccount.yaml", firstResult.Metadata().Range().GetFilename()) -} - -func Test_FileScanExampleWithResultFunction(t *testing.T) { - - results, err := NewScanner( - rego.WithEmbeddedPolicies(true), rego.WithEmbeddedLibraries(true), - rego.WithPolicyReader(strings.NewReader(`package defsec - -import data.lib.kubernetes - -default checkCapsDropAll = false - -__rego_metadata__ := { -"id": "KSV003", -"avd_id": "AVD-KSV-0003", -"title": "Default capabilities not dropped", -"short_code": "drop-default-capabilities", -"version": "v1.0.0", -"severity": "LOW", -"type": "Kubernetes Security Check", -"description": "The container should drop all default capabilities and add only those that are needed for its execution.", -"recommended_actions": "Add 'ALL' to containers[].securityContext.capabilities.drop.", -"url": "https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/", -} - -__rego_input__ := { -"combine": false, -"selector": [{"type": "kubernetes"}], -} - -# Get all containers which include 'ALL' in security.capabilities.drop -getCapsDropAllContainers[container] { -allContainers := kubernetes.containers[_] -lower(allContainers.securityContext.capabilities.drop[_]) == "all" -container := allContainers.name -} - -# Get all containers which don't include 'ALL' in security.capabilities.drop -getCapsNoDropAllContainers[container] { -container := kubernetes.containers[_] -not getCapsDropAllContainers[container.name] -} + fsys := buildFS(map[string]string{ + "check.rego": `package defsec deny[res] { -output := getCapsNoDropAllContainers[_] - -msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should add 'ALL' to 'securityContext.capabilities.drop'", [output.name, kubernetes.kind, kubernetes.name])) + input.kind == "Pod" + res := result.new("fail", input) +}`, + "k8s.yaml": file, + }) -res := result.new(msg, output) -} + scanner := kubernetes.NewScanner( + rego.WithPolicyFilesystem(fsys), + rego.WithPolicyDirs("."), + rego.WithEmbeddedLibraries(true), + ) -`))).ScanReader( - context.TODO(), - "k8s.yaml", - strings.NewReader(` -apiVersion: v1 -kind: Pod -metadata: - name: hello-cpu-limit -spec: - containers: - - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] - image: busybox - name: hello - securityContext: - capabilities: - drop: - - nothing -`)) + results, err := scanner.ScanFS(context.TODO(), fsys, ".") require.NoError(t, err) - require.NotEmpty(t, results.GetFailed()) - - firstResult := results.GetFailed()[0] - assert.Equal(t, 8, firstResult.Metadata().Range().GetStartLine()) - assert.Equal(t, 14, firstResult.Metadata().Range().GetEndLine()) - assert.Equal(t, "k8s.yaml", firstResult.Metadata().Range().GetFilename()) + assertLines(t, file, results) } -func Test_checkPolicyIsApplicable(t *testing.T) { - srcFS := testutil.CreateFS(t, map[string]string{ - "policies/pod_policy.rego": `# METADATA -# title: "Process can elevate its own privileges" -# description: "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node." +func Test_CheckWithSubtype(t *testing.T) { + fsys := buildFS(map[string]string{ + "checks/pod_policy.rego": `# METADATA +# title: test check # scope: package # schemas: # - input: schema["kubernetes"] -# related_resources: -# - https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted # custom: # id: KSV001 -# avd_id: AVD-KSV-0999 +# avd_id: AVD-KSV-0001 # severity: MEDIUM -# short_code: no-self-privesc -# recommended_action: "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'." # input: # selector: # - type: kubernetes # subtypes: # - kind: Pod -package builtin.kubernetes.KSV999 +package builtin.kubernetes.KSV001 import data.lib.kubernetes -import data.lib.utils - -default checkAllowPrivilegeEscalation = false - -# getNoPrivilegeEscalationContainers returns the names of all containers which have -# securityContext.allowPrivilegeEscalation set to false. -getNoPrivilegeEscalationContainers[container] { - allContainers := kubernetes.containers[_] - allContainers.securityContext.allowPrivilegeEscalation == false - container := allContainers.name -} - -# getPrivilegeEscalationContainers returns the names of all containers which have -# securityContext.allowPrivilegeEscalation set to true or not set. -getPrivilegeEscalationContainers[container] { - containerName := kubernetes.containers[_].name - not getNoPrivilegeEscalationContainers[containerName] - container := kubernetes.containers[_] -} deny[res] { - output := getPrivilegeEscalationContainers[_] - msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'securityContext.allowPrivilegeEscalation' to false", [output.name, kubernetes.kind, kubernetes.name])) - res := result.new(msg, output) + res := result.new("fail", input) } - `, - "policies/namespace_policy.rego": `# METADATA -# title: "The default namespace should not be used" -# description: "ensure that default namespace should not be used" + "checks/namespace_policy.rego": `# METADATA +# title: test check 2 # scope: package # schemas: # - input: schema["kubernetes"] -# related_resources: -# - https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ # custom: -# id: KSV110 -# avd_id: AVD-KSV-0888 +# id: KSV002 +# avd_id: AVD-KSV-0002 # severity: LOW -# short_code: default-namespace-should-not-be-used -# recommended_action: "Ensure that namespaces are created to allow for appropriate segregation of Kubernetes resources and that all new resources are created in a specific namespace." # input: # selector: # - type: kubernetes # subtypes: # - kind: Namespace -package builtin.kubernetes.KSV888 - -import data.lib.kubernetes - -default defaultNamespaceInUse = false - -defaultNamespaceInUse { - kubernetes.namespace == "default" -} +package builtin.kubernetes.KSV002 deny[res] { - defaultNamespaceInUse - msg := sprintf("%s '%s' should not be set with 'default' namespace", [kubernetes.kind, kubernetes.name]) - res := result.new(msg, input.metadata.namespace) + res := result.new("fail", input) } - `, "test/KSV001/pod.yaml": `apiVersion: v1 kind: Pod @@ -706,18 +271,37 @@ spec: `, }) - scanner := NewScanner( - // rego.WithEmbeddedPolicies(true), rego.WithEmbeddedLibraries(true), + scanner := kubernetes.NewScanner( rego.WithEmbeddedLibraries(true), - rego.WithPolicyDirs("policies/"), - rego.WithPolicyFilesystem(srcFS), + rego.WithPolicyDirs("checks"), + rego.WithPolicyFilesystem(fsys), ) - results, err := scanner.ScanFS(context.TODO(), srcFS, "test/KSV001") + results, err := scanner.ScanFS(context.TODO(), fsys, "test/KSV001") require.NoError(t, err) require.NoError(t, err) require.Len(t, results.GetFailed(), 1) - failure := results.GetFailed()[0].Rule() - assert.Equal(t, "Process can elevate its own privileges", failure.Summary) + failure := results.GetFailed()[0] + + assert.Equal(t, "AVD-KSV-0001", failure.Rule().AVDID) +} + +func assertLines(t *testing.T, content string, results scan.Results) { + lines := strings.Split(content, "\n") + for _, res := range results { + actualCode, err := res.GetCode() + require.NoError(t, err) + assert.NotEmpty(t, actualCode.Lines) + for _, line := range actualCode.Lines { + assert.Greater(t, len(lines), line.Number) + assert.Equal(t, line.Content, lines[line.Number-1]) + } + } +} + +func buildFS(files map[string]string) fs.FS { + return fstest.MapFS(lo.MapValues(files, func(val string, _ string) *fstest.MapFile { + return &fstest.MapFile{Data: []byte(val)} + })) }