Ctrl K
ComunidadGuías…

internal/checks/imports_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package checks

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

	"github.com/sophotechlabs/spinoza/internal/api"
)

const nativeImport = `{"tool":"myscan","findings":[
 {"id":"no-latest","title":"Image tag is latest","severity":"medium","category":"security",
  "wrong":"A floating tag means the running image can change under the deployment.",
  "remedy":"Pin the image to a digest or a version.",
  "detail":"container app runs registry.example/app:latest","container":"app",
  "object":{"apiVersion":"apps/v1","kind":"Deployment","namespace":"apps","name":"api"}},
 {"id":"no-latest","detail":"container web runs registry.example/web:latest","container":"web",
  "object":{"apiVersion":"apps/v1","kind":"Deployment","namespace":"apps","name":"ghost"}}
]}`

func importing(t *testing.T, paths []string, objects ...*unstructured.Unstructured) api.CheckReport {
	t.Helper()
	keep := wholeCluster()
	keep.Imports = paths
	return Run(t.Context(), newLister(objects...), descriptors(), api.Metrics{}, keep, 0)
}

func writeImport(t *testing.T, name, body string) string {
	t.Helper()
	path := filepath.Join(t.TempDir(), name)
	if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
		t.Fatalf("writing %s: %v", name, err)
	}
	return path
}

func fixtureImport(name string) string {
	return filepath.Join("testdata", "imports", name)
}

func clusterWide(kind, apiVersion, name string) *unstructured.Unstructured {
	return &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": apiVersion,
		"kind":       kind,
		"metadata":   map[string]any{"name": name},
	}}
}

func namespacedObj(kind, apiVersion, name string) *unstructured.Unstructured {
	return &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": apiVersion,
		"kind":       kind,
		"metadata":   metadata(name),
	}}
}

func takenAt(t *testing.T, path string) string {
	t.Helper()
	info, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat %s: %v", path, err)
	}
	return info.ModTime().UTC().Format(time.RFC3339)
}

// what a file's finding becomes in the report

func TestAnImportedFindingLandsOnTheObjectItNames(t *testing.T) {
	path := writeImport(t, "myscan.json", nativeImport)

	found := importing(t, []string{path}, deployment("api", podSpec(container("app", nil))))

	group := groupNamed(t, found, "myscan/no-latest")
	if group.Title != "Image tag is latest" || group.Severity != severityMedium || group.Category != categorySecurity {
		t.Fatalf("group = %+v, want the file's title, severity and category", group)
	}
	if !strings.HasPrefix(group.Wrong, "A floating tag") || !strings.HasPrefix(group.Remedy, "Pin the image") {
		t.Fatalf("wrong = %q, remedy = %q, want the file's words", group.Wrong, group.Remedy)
	}
	if len(group.Sources) != 1 || group.Sources[0] != path || group.Taken != takenAt(t, path) {
		t.Fatalf("sources = %v taken = %q, want the file and when it was written", group.Sources, group.Taken)
	}
	if len(group.Findings) != 2 {
		t.Fatalf("findings = %d, want the matched and the unmatched one", len(group.Findings))
	}
	matched := group.Findings[0]
	object := objectFor(t, found, matched)
	if object.Name != "api" || object.Kind != "Deployment" || object.Resource != "deployments" || object.Namespace != testNamespace {
		t.Fatalf("matched object = %+v, want the deployment the audit read", object)
	}
	if matched.Unmatched || matched.Container != "app" || matched.Detail != "container app runs registry.example/app:latest" {
		t.Fatalf("matched finding = %+v", matched)
	}
	ghost := group.Findings[1]
	if !ghost.Unmatched {
		t.Fatal("a finding about an object the audit never saw was not marked")
	}
	if shape := objectFor(t, found, ghost); shape.Name != "ghost" || shape.Kind != "Deployment" || shape.Resource != "" {
		t.Fatalf("unmatched object = %+v, want the identity the file gave and no resource", shape)
	}
}

func TestAnImportedFindingRanksByReachLikeABuiltInOne(t *testing.T) {
	path := writeImport(t, "myscan.json", nativeImport)

	found := importing(t, []string{path}, replicas(deployment("api", podSpec(container("app", nil))), 3))

	group := groupNamed(t, found, "myscan/no-latest")
	if group.Findings[0].Severity != severityHigh {
		t.Fatalf("a finding on a three-replica deployment ranked %q, want high like a built-in check", group.Findings[0].Severity)
	}
	if group.Findings[1].Severity != severityMedium {
		t.Fatalf("an unmatched finding ranked %q, want the file's own medium with no reach applied", group.Findings[1].Severity)
	}
}

func TestImportedGroupsComeAfterTheBuiltInAndPersonalOnes(t *testing.T) {
	path := writeImport(t, "myscan.json", nativeImport)

	found := importing(t, []string{path})

	if last := found.Groups[len(found.Groups)-1].ID; last != "myscan/no-latest" {
		t.Fatalf("last group = %s, want the imported one", last)
	}
}

func TestAnImportedRuleCanBeDisabledLikeAnyOther(t *testing.T) {
	path := writeImport(t, "myscan.json", nativeImport)
	keep := wholeCluster()
	keep.Imports = []string{path}
	keep.Disabled = []string{"myscan/no-latest"}

	found := Run(t.Context(), newLister(), descriptors(), api.Metrics{}, keep, 0)

	for _, group := range found.Groups {
		if group.ID == "myscan/no-latest" {
			t.Fatal("a disabled imported rule was still reported")
		}
	}
}

func TestTwoFilesFromOneToolShareARule(t *testing.T) {
	first := writeImport(t, "first.json", nativeImport)
	second := writeImport(t, "second.json", `{"tool":"myscan","findings":[{"id":"no-latest","detail":"container job runs registry.example/job:latest","object":{"kind":"Job","namespace":"apps","name":"nightly"}}]}`)
	older := time.Now().Add(-2 * time.Hour).Truncate(time.Second)
	if err := os.Chtimes(first, older, older); err != nil {
		t.Fatal(err)
	}

	found := importing(t, []string{second, first})

	group := groupNamed(t, found, "myscan/no-latest")
	if len(group.Sources) != 2 || group.Sources[0] != second || group.Sources[1] != first {
		t.Fatalf("sources = %v, want both files in the order they were listed", group.Sources)
	}
	if group.Taken != older.UTC().Format(time.RFC3339) {
		t.Fatalf("taken = %q, want the older file's time %s", group.Taken, older.UTC().Format(time.RFC3339))
	}
	if len(group.Findings) != 3 {
		t.Fatalf("findings = %d, want the three from both files", len(group.Findings))
	}
}

// how the file is read

func TestAnImportIsReadAgainOnceTheFileChanges(t *testing.T) {
	path := writeImport(t, "myscan.json", `{"tool":"myscan","findings":[{"id":"no-latest","object":{"kind":"Deployment","namespace":"apps","name":"api"}}]}`)
	if len(groupNamed(t, importing(t, []string{path}), "myscan/no-latest").Findings) != 1 {
		t.Fatal("the first read did not report the one finding")
	}

	if err := os.WriteFile(path, []byte(nativeImport), 0o600); err != nil {
		t.Fatal(err)
	}
	later := time.Now().Add(time.Hour)
	if err := os.Chtimes(path, later, later); err != nil {
		t.Fatal(err)
	}

	if got := len(groupNamed(t, importing(t, []string{path}), "myscan/no-latest").Findings); got != 2 {
		t.Fatalf("after the file changed the report carried %d findings, want 2", got)
	}
}

func TestAnImportNobodyCanReadIsNamedInTheReport(t *testing.T) {
	missing := filepath.Join(t.TempDir(), "gone.json")
	folder := t.TempDir()
	huge := filepath.Join(t.TempDir(), "huge.json")
	if err := os.WriteFile(huge, []byte("{"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.Truncate(huge, maxImportBytes+1); err != nil {
		t.Fatal(err)
	}

	found := importing(t, []string{missing, folder, huge}, deployment("api", podSpec(container("app", nil))))

	for _, want := range []string{
		"import " + missing + ": ",
		"import " + folder + ": not a regular file",
		"import " + huge + ": larger than 32 MiB",
	} {
		if !strings.Contains(found.Error, want) {
			t.Fatalf("error = %q, want it to carry %q", found.Error, want)
		}
	}
	if findingCount(t, found, "privileged-containers") != 0 || len(found.Groups) != len(registry()) {
		t.Fatal("an unreadable import disturbed the built-in audit")
	}
}

func TestAnImportInAFormatSpinozaDoesNotReadIsNamed(t *testing.T) {
	cases := []struct {
		name string
		body string
		want string
	}{
		{name: "unknown object", body: `{"hello": 1}`, want: "not a format spinoza reads"},
		{name: "not json", body: `hello`, want: "not a JSON object"},
		{name: "finding without identity", body: `{"findings":[{"id":"x"}]}`, want: "finding 1 needs an id and an object with a kind and a name"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			path := writeImport(t, "odd.json", tc.body)
			found := importing(t, []string{path})
			if !strings.Contains(found.Error, "import "+path+": "+tc.want) {
				t.Fatalf("error = %q, want %q", found.Error, tc.want)
			}
		})
	}
}

func TestParseImportsKeepsOnePathPerLine(t *testing.T) {
	got := ParseImports(" /a.json \n\n# a note\n/a.json\n/b.json\r\n")
	if len(got) != 2 || got[0] != "/a.json" || got[1] != "/b.json" {
		t.Fatalf("paths = %v", got)
	}
	many := make([]string, 0, 40)
	for at := range 40 {
		many = append(many, "/file-"+itoa(at)+".json")
	}
	if got := ParseImports(strings.Join(many, "\n")); len(got) != maxImportPaths {
		t.Fatalf("%d paths kept, want the cap of %d", len(got), maxImportPaths)
	}
	if got := ParseImports("  \n"); len(got) != 0 {
		t.Fatalf("blank input gave %v", got)
	}
}

// the formats scanners write

func TestATrivyKubernetesReportImportsWhatFailed(t *testing.T) {
	found := importing(t, []string{fixtureImport("trivy-k8s.json")},
		deployment("api", podSpec(container("app", nil))),
		clusterWide("ClusterRole", "rbac.authorization.k8s.io/v1", "admin"))

	escalation := groupNamed(t, found, "trivy/KSV-0001")
	if escalation.Title != "Can elevate its own privileges" || escalation.Severity != severityMedium {
		t.Fatalf("group = %+v", escalation)
	}
	if !strings.HasPrefix(escalation.Wrong, "A program inside the container") || !strings.HasPrefix(escalation.Remedy, "Set 'set containers[]") {
		t.Fatalf("wrong = %q remedy = %q, want trivy's description and resolution", escalation.Wrong, escalation.Remedy)
	}
	finding := onlyFinding(t, found, "trivy/KSV-0001")
	if finding.Unmatched || objectFor(t, found, finding).Name != "api" {
		t.Fatalf("finding = %+v, want it on the deployment the audit read", finding)
	}
	if finding.Detail != "Container 'app' of Deployment 'api' should set 'securityContext.allowPrivilegeEscalation' to false" {
		t.Fatalf("detail = %q, want trivy's message", finding.Detail)
	}

	secrets := groupNamed(t, found, "trivy/KSV-0041")
	if secrets.Severity != severityHigh {
		t.Fatalf("a CRITICAL trivy finding ranked %q, want high", secrets.Severity)
	}
	if role := onlyObject(t, found, "trivy/KSV-0041"); role.Kind != "ClusterRole" || role.Name != "admin" || role.Resource != "clusterroles" {
		t.Fatalf("object = %+v, want the cluster role the audit read", role)
	}

	if low := groupNamed(t, found, "trivy/KSV-0113"); low.Severity != severityLow {
		t.Fatalf("a LOW trivy finding ranked %q, want low", low.Severity)
	}
	for _, group := range found.Groups {
		if group.ID == "trivy/KSV-0003" {
			t.Fatal("a misconfiguration trivy marked PASS was imported")
		}
	}
}

func TestAFileWhoseShapeLiesAboutItsFormatIsNamed(t *testing.T) {
	cases := []struct {
		name string
		body string
	}{
		{name: "native", body: `{"findings": 5}`},
		{name: "trivy", body: `{"Resources": 5}`},
		{name: "kubescape", body: `{"summaryDetails": {}, "results": 5}`},
		{name: "sarif", body: `{"runs": 5}`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			path := writeImport(t, "odd.json", tc.body)
			found := importing(t, []string{path})
			if !strings.Contains(found.Error, "import "+path+": ") || !strings.Contains(found.Error, "cannot unmarshal") {
				t.Fatalf("error = %q, want the decode failure named", found.Error)
			}
		})
	}
}

func TestAnImportWithTooManyRulesOrFindingsIsRefused(t *testing.T) {
	rules := make([]string, 0, maxImportRules+1)
	for at := range maxImportRules + 1 {
		rules = append(rules, `{"id":"r`+itoa(at)+`","object":{"kind":"Deployment","namespace":"apps","name":"api"}}`)
	}
	tooManyRules := writeImport(t, "rules.json", `{"findings":[`+strings.Join(rules, ",")+`]}`)
	items := make([]string, 0, maxImportItems+1)
	for range maxImportItems + 1 {
		items = append(items, `{"id":"r","object":{"kind":"Deployment","namespace":"apps","name":"api"}}`)
	}
	tooManyItems := writeImport(t, "items.json", `{"findings":[`+strings.Join(items, ",")+`]}`)

	found := importing(t, []string{tooManyRules, tooManyItems})

	if !strings.Contains(found.Error, "import "+tooManyRules+": more than "+itoa(maxImportRules)+" distinct rules") {
		t.Fatalf("error = %q, want the rule cap named", found.Error)
	}
	if !strings.Contains(found.Error, "import "+tooManyItems+": more than "+itoa(maxImportItems)+" findings") {
		t.Fatalf("error = %q, want the finding cap named", found.Error)
	}
}

func TestAKubescapeReportImportsItsFailedControls(t *testing.T) {
	found := importing(t, []string{fixtureImport("kubescape.json")},
		deployment("api", podSpec(container("app", nil))),
		namespacedObj("ServiceAccount", "v1", "robot"))

	escalation := groupNamed(t, found, "kubescape/C-0016")
	if escalation.Title != "Allow privilege escalation" || escalation.Severity != severityMedium {
		t.Fatalf("group = %+v", escalation)
	}
	if !strings.Contains(escalation.Wrong, "C-0016") || !strings.Contains(escalation.Remedy, "hub.armosec.io/docs/c-0016") {
		t.Fatalf("wrong = %q remedy = %q", escalation.Wrong, escalation.Remedy)
	}
	if finding := onlyFinding(t, found, "kubescape/C-0016"); finding.Unmatched || objectFor(t, found, finding).Name != "api" {
		t.Fatalf("finding = %+v, want it on the deployment", finding)
	}
	if account := onlyObject(t, found, "kubescape/C-0053"); account.Kind != "ServiceAccount" || account.Name != "robot" {
		t.Fatalf("object = %+v, want the service account from the composite resource", account)
	}
	for _, group := range found.Groups {
		if group.ID == "kubescape/C-0002" {
			t.Fatal("a control kubescape marked passed was imported")
		}
	}
}

func TestASARIFFileIsReadAsFarAsItNamesObjects(t *testing.T) {
	found := importing(t, []string{fixtureImport("trivy-config.sarif")},
		deployment("api", podSpec(container("app", nil))),
		deployment("twin", podSpec(container("app", nil))),
		namespaced(deployment("twin", podSpec(container("app", nil))), "other"))

	group := groupNamed(t, found, "trivy/KSV-0001")
	if group.Title != "Can elevate its own privileges" || group.Severity != severityMedium || group.Remedy != "Misconfiguration KSV-0001" {
		t.Fatalf("group = %+v", group)
	}
	if len(group.Findings) != 3 {
		t.Fatalf("findings = %d, want 3", len(group.Findings))
	}
	byName := map[string]api.CheckFinding{}
	for _, finding := range group.Findings {
		byName[objectFor(t, found, finding).Name] = finding
	}
	apiFinding := byName["api"]
	if apiFinding.Unmatched || apiFinding.Detail != "Container 'app' of Deployment 'api' should set 'securityContext.allowPrivilegeEscalation' to false" {
		t.Fatalf("a result naming one deployment = %+v, want it matched by kind and name", apiFinding)
	}
	if twin := byName["twin"]; !twin.Unmatched {
		t.Fatal("a name two namespaces share was matched to one of them")
	}
	if file := byName["other.yaml"]; !file.Unmatched || objectFor(t, found, file).Kind != "" {
		t.Fatalf("a result naming no object = %+v, want it kept under its file name", file)
	}
}

func TestASARIFLogicalLocationNamesTheObjectDirectly(t *testing.T) {
	path := writeImport(t, "tool.sarif", `{"runs":[{"tool":{"driver":{"name":"scanner","rules":[]}},"results":[
	 {"ruleId":"R1","level":"error","message":{"text":"broken"},"locations":[{"logicalLocations":[{"fullyQualifiedName":"Deployment/apps/api"}]}]},
	 {"ruleId":"R1","level":"error","message":{"text":"broken"},"locations":[{"logicalLocations":[{"fullyQualifiedName":"Deployment/api"}]}]},
	 {"ruleId":"R1","level":"error","message":{"text":"Deployment '' is unnamed"},"locations":[{"logicalLocations":[{"fullyQualifiedName":"nonsense"}]}]},
	 {"ruleId":"R2","level":"note","message":{"text":"a remark"},"locations":[]}
	]}]}`)

	found := importing(t, []string{path}, deployment("api", podSpec(container("app", nil))))

	group := groupNamed(t, found, "scanner/R1")
	if group.Severity != severityHigh || group.Title != "R1" {
		t.Fatalf("group = %+v, want error as high and the id as title", group)
	}
	if remark := groupNamed(t, found, "scanner/R2"); remark.Severity != severityLow || onlyFinding(t, found, "scanner/R2").Detail != "a remark" {
		t.Fatalf("a note-level result = %+v", remark)
	}
	matched := 0
	for _, finding := range group.Findings {
		name := objectFor(t, found, finding).Name
		if !finding.Unmatched && name == "api" {
			matched++
			continue
		}
		if !finding.Unmatched || name != "unnamed" {
			t.Fatalf("a location nothing reads = %+v on %q", finding, name)
		}
	}
	if matched != 2 {
		t.Fatalf("%d matched, want Kind/namespace/name and Kind/name both to land on the deployment", matched)
	}
}

func TestAKubescapeResultAboutAResourceTheReportDoesNotListIsDropped(t *testing.T) {
	path := writeImport(t, "ks.json", `{"summaryDetails":{"controls":{}},"resources":[],"results":[
	 {"resourceID":"apps/v1/apps/Deployment/api","controls":[{"controlID":"C-0016","name":"Allow privilege escalation","status":{"status":"failed"},"severity":"Medium"}]}
	]}`)

	found := importing(t, []string{path}, deployment("api", podSpec(container("app", nil))))

	if found.Error != "" {
		t.Fatalf("error = %q", found.Error)
	}
	for _, group := range found.Groups {
		if group.ID == "kubescape/C-0016" {
			t.Fatal("a result with no resource entry to identify it became a finding")
		}
	}
}