diff --git a/internal/lint/cronjob.go b/internal/lint/cronjob.go index 802b57ed..8785b40f 100644 --- a/internal/lint/cronjob.go +++ b/internal/lint/cronjob.go @@ -40,7 +40,7 @@ func (s *CronJob) Lint(ctx context.Context) error { cj := o.(*batchv1.CronJob) fqn := client.FQN(cj.Namespace, cj.Name) s.InitOutcome(fqn) - ctx = internal.WithSpec(ctx, SpecFor(fqn, cj)) + ctx = internal.WithSpec(ctx, coSpecFor(fqn, cj, cj.Spec.JobTemplate.Spec.Template.Spec)) s.checkCronJob(ctx, fqn, cj) s.checkContainers(ctx, fqn, cj.Spec.JobTemplate.Spec.Template.Spec) s.checkUtilization(ctx, over, fqn) diff --git a/internal/lint/dp.go b/internal/lint/dp.go index c35d02c1..3e884cc5 100644 --- a/internal/lint/dp.go +++ b/internal/lint/dp.go @@ -39,7 +39,7 @@ func (s *Deployment) Lint(ctx context.Context) error { dp := o.(*appsv1.Deployment) fqn := client.FQN(dp.Namespace, dp.Name) s.InitOutcome(fqn) - ctx = internal.WithSpec(ctx, SpecFor(fqn, dp)) + ctx = internal.WithSpec(ctx, coSpecFor(fqn, dp, dp.Spec.Template.Spec)) s.checkDeployment(ctx, dp) s.checkContainers(ctx, fqn, dp.Spec.Template.Spec) s.checkUtilization(ctx, over, dp) diff --git a/internal/lint/ds.go b/internal/lint/ds.go index 523d9904..725fca07 100644 --- a/internal/lint/ds.go +++ b/internal/lint/ds.go @@ -38,7 +38,7 @@ func (s *DaemonSet) Lint(ctx context.Context) error { ds := o.(*appsv1.DaemonSet) fqn := client.FQN(ds.Namespace, ds.Name) s.InitOutcome(fqn) - ctx = internal.WithSpec(ctx, SpecFor(fqn, ds)) + ctx = internal.WithSpec(ctx, coSpecFor(fqn, ds, ds.Spec.Template.Spec)) s.checkDaemonSet(ctx, ds) s.checkContainers(ctx, fqn, ds.Spec.Template.Spec) diff --git a/internal/lint/helper.go b/internal/lint/helper.go index bd74f488..36a7a5ee 100644 --- a/internal/lint/helper.go +++ b/internal/lint/helper.go @@ -28,6 +28,25 @@ const ( type qos = int +func coSpecFor(fqn string, o metav1.ObjectMetaAccessor, spec v1.PodSpec) rules.Spec { + rule := SpecFor(fqn, o) + rule.Containers = fetchContainers(spec) + + return rule +} + +func fetchContainers(podTemplate v1.PodSpec) []string { + containers := make([]string, 0, len(podTemplate.InitContainers)+len(podTemplate.Containers)) + for _, co := range podTemplate.InitContainers { + containers = append(containers, co.Name) + } + for _, co := range podTemplate.Containers { + containers = append(containers, co.Name) + } + + return containers +} + // SpecFor construct a new run spec for a given resource. func SpecFor(fqn string, o metav1.ObjectMetaAccessor) rules.Spec { spec := rules.Spec{ @@ -146,7 +165,7 @@ func asMB(q resource.Quantity) string { return fmt.Sprintf("%vMi", toMB(q)) } -// PodResources computes pod resouces as sum of containers allocations. +// PodResources computes pod resources as sum of containers allocations. func podResources(spec v1.PodSpec) (cpu, mem resource.Quantity) { for _, co := range spec.InitContainers { c, m, _ := containerResources(co) diff --git a/internal/lint/job.go b/internal/lint/job.go index 84b25e2b..72e7dfa1 100644 --- a/internal/lint/job.go +++ b/internal/lint/job.go @@ -39,7 +39,7 @@ func (s *Job) Lint(ctx context.Context) error { j := o.(*batchv1.Job) fqn := client.FQN(j.Namespace, j.Name) s.InitOutcome(fqn) - ctx = internal.WithSpec(ctx, SpecFor(fqn, j)) + ctx = internal.WithSpec(ctx, coSpecFor(fqn, j, j.Spec.Template.Spec)) s.checkJob(ctx, fqn, j) s.checkContainers(ctx, fqn, j.Spec.Template.Spec) s.checkUtilization(ctx, over, fqn) diff --git a/internal/lint/np.go b/internal/lint/np.go index 7abbb6a2..090cebe0 100644 --- a/internal/lint/np.go +++ b/internal/lint/np.go @@ -23,11 +23,13 @@ type direction string const ( dirIn direction = "Ingress" dirOut direction = "Egress" - bothPols = "All" + bothPols = "all" noPols = "" + ingress = "ingress" + egress = "egress" ) -// NetworkPolicy tracks NetworkPolicy sanitizatios. +// NetworkPolicy tracks NetworkPolicy linting. type NetworkPolicy struct { *issues.Collector @@ -57,13 +59,13 @@ func (s *NetworkPolicy) Lint(ctx context.Context) error { s.checkSelector(ctx, fqn, np.Spec.PodSelector) s.checkIngresses(ctx, fqn, np.Spec.Ingress) s.checkEgresses(ctx, fqn, np.Spec.Egress) - s.checkRuleType(ctx, fqn, &np.Spec) + s.checkRuleType(ctx, &np.Spec) } return nil } -func (s *NetworkPolicy) checkRuleType(ctx context.Context, fqn string, spec *netv1.NetworkPolicySpec) { +func (s *NetworkPolicy) checkRuleType(ctx context.Context, spec *netv1.NetworkPolicySpec) { if spec.PodSelector.Size() > 0 { return } @@ -72,15 +74,15 @@ func (s *NetworkPolicy) checkRuleType(ctx context.Context, fqn string, spec *net case isAllowAll(spec): s.AddCode(ctx, 1203, "Allow", bothPols) case isAllowAllIngress(spec): - s.AddCode(ctx, 1203, "Allow All", dirIn) + s.AddCode(ctx, 1203, "Allow all", ingress) case isAllowAllEgress(spec): - s.AddCode(ctx, 1203, "Allow All", dirOut) + s.AddCode(ctx, 1203, "Allow all", egress) case isDenyAll(spec): s.AddCode(ctx, 1203, "Deny", bothPols) case isDenyAllIngress(spec): - s.AddCode(ctx, 1203, "Deny All", dirIn) + s.AddCode(ctx, 1203, "Deny all", ingress) case isDenyAllEgress(spec): - s.AddCode(ctx, 1203, "Deny All", dirOut) + s.AddCode(ctx, 1203, "Deny all", egress) } } @@ -157,7 +159,7 @@ func (s *NetworkPolicy) checkIPBlocks(ctx context.Context, fqn string, b *netv1. s.AddErr(ctx, err) } if !s.matchPips(ns, ipnet) { - s.AddCode(ctx, 1206, d, b.CIDR) + s.AddCode(ctx, 1206, strings.ToLower(string(d)), b.CIDR) } for _, ex := range b.Except { _, ipnet, err := net.ParseCIDR(ex) @@ -166,7 +168,7 @@ func (s *NetworkPolicy) checkIPBlocks(ctx context.Context, fqn string, b *netv1. continue } if !s.matchPips(ns, ipnet) { - s.AddCode(ctx, 1207, d, ex) + s.AddCode(ctx, 1207, strings.ToLower(string(d)), ex) } } } @@ -210,16 +212,16 @@ func (s *NetworkPolicy) checkPodSelector(ctx context.Context, nss []*v1.Namespac } if !found { if len(nn) > 0 { - s.AddCode(ctx, 1208, d, dumpSel(sel), strings.Join(nn, ",")) + s.AddCode(ctx, 1208, strings.ToLower(string(d)), dumpSel(sel), strings.Join(nn, ",")) } else { - s.AddCode(ctx, 1202, d, dumpSel(sel)) + s.AddCode(ctx, 1202, strings.ToLower(string(d)), dumpSel(sel)) } } } func (s *NetworkPolicy) checkNSSelector(ctx context.Context, sel *metav1.LabelSelector, nss []*v1.Namespace, d direction) bool { if len(nss) == 0 { - s.AddCode(ctx, 1201, d, dumpSel(sel)) + s.AddCode(ctx, 1201, strings.ToLower(string(d)), dumpSel(sel)) return false } diff --git a/internal/lint/np_test.go b/internal/lint/np_test.go index 67d99889..617f5bcd 100644 --- a/internal/lint/np_test.go +++ b/internal/lint/np_test.go @@ -31,46 +31,46 @@ func TestNPLintDenyAll(t *testing.T) { ii := np.Outcome()["default/deny-all"] assert.Equal(t, 1, len(ii)) - assert.Equal(t, `[POP-1203] Deny All policy in effect`, ii[0].Message) + assert.Equal(t, `[POP-1203] Deny all policy in effect`, ii[0].Message) assert.Equal(t, rules.InfoLevel, ii[0].Level) ii = np.Outcome()["default/deny-all-ing"] assert.Equal(t, 1, len(ii)) - assert.Equal(t, `[POP-1203] Deny All Ingress policy in effect`, ii[0].Message) + assert.Equal(t, `[POP-1203] Deny all ingress policy in effect`, ii[0].Message) assert.Equal(t, rules.InfoLevel, ii[0].Level) ii = np.Outcome()["default/deny-all-eg"] assert.Equal(t, 1, len(ii)) - assert.Equal(t, `[POP-1203] Deny All Egress policy in effect`, ii[0].Message) + assert.Equal(t, `[POP-1203] Deny all egress policy in effect`, ii[0].Message) assert.Equal(t, rules.InfoLevel, ii[0].Level) ii = np.Outcome()["default/allow-all"] assert.Equal(t, 1, len(ii)) - assert.Equal(t, `[POP-1203] Allow All policy in effect`, ii[0].Message) + assert.Equal(t, `[POP-1203] Allow all policy in effect`, ii[0].Message) assert.Equal(t, rules.InfoLevel, ii[0].Level) ii = np.Outcome()["default/allow-all-ing"] assert.Equal(t, 1, len(ii)) - assert.Equal(t, `[POP-1203] Allow All Ingress policy in effect`, ii[0].Message) + assert.Equal(t, `[POP-1203] Allow all ingress policy in effect`, ii[0].Message) assert.Equal(t, rules.InfoLevel, ii[0].Level) ii = np.Outcome()["default/allow-all-eg"] assert.Equal(t, 1, len(ii)) - assert.Equal(t, `[POP-1203] Allow All Egress policy in effect`, ii[0].Message) + assert.Equal(t, `[POP-1203] Allow all egress policy in effect`, ii[0].Message) assert.Equal(t, rules.InfoLevel, ii[0].Level) ii = np.Outcome()["default/ip-block-all-ing"] assert.Equal(t, 2, len(ii)) - assert.Equal(t, `[POP-1206] No pods matched Egress IPBlock 172.2.0.0/24`, ii[0].Message) + assert.Equal(t, `[POP-1206] No pods matched egress IPBlock 172.2.0.0/24`, ii[0].Message) assert.Equal(t, rules.WarnLevel, ii[0].Level) - assert.Equal(t, `[POP-1203] Deny All Ingress policy in effect`, ii[1].Message) + assert.Equal(t, `[POP-1203] Deny all ingress policy in effect`, ii[1].Message) assert.Equal(t, rules.InfoLevel, ii[1].Level) ii = np.Outcome()["default/ip-block-all-eg"] assert.Equal(t, 2, len(ii)) - assert.Equal(t, `[POP-1206] No pods matched Ingress IPBlock 172.2.0.0/24`, ii[0].Message) + assert.Equal(t, `[POP-1206] No pods matched ingress IPBlock 172.2.0.0/24`, ii[0].Message) assert.Equal(t, rules.WarnLevel, ii[0].Level) - assert.Equal(t, `[POP-1203] Deny All Egress policy in effect`, ii[1].Message) + assert.Equal(t, `[POP-1203] Deny all egress policy in effect`, ii[1].Message) assert.Equal(t, rules.InfoLevel, ii[1].Level) } @@ -93,26 +93,26 @@ func TestNPLint(t *testing.T) { ii = np.Outcome()["default/np2"] assert.Equal(t, 3, len(ii)) - assert.Equal(t, `[POP-1207] No pods matched except Ingress IPBlock 172.1.1.0/24`, ii[0].Message) + assert.Equal(t, `[POP-1207] No pods matched except ingress IPBlock 172.1.1.0/24`, ii[0].Message) assert.Equal(t, rules.WarnLevel, ii[0].Level) - assert.Equal(t, `[POP-1208] No pods match Ingress pod selector: app=p2 in namespace: ns2`, ii[1].Message) + assert.Equal(t, `[POP-1208] No pods match ingress pod selector: app=p2 in namespace: ns2`, ii[1].Message) assert.Equal(t, rules.WarnLevel, ii[1].Level) - assert.Equal(t, `[POP-1206] No pods matched Egress IPBlock 172.0.0.0/24`, ii[2].Message) + assert.Equal(t, `[POP-1206] No pods matched egress IPBlock 172.0.0.0/24`, ii[2].Message) assert.Equal(t, rules.WarnLevel, ii[2].Level) ii = np.Outcome()["default/np3"] assert.Equal(t, 6, len(ii)) assert.Equal(t, `[POP-1200] No pods match pod selector: app=p-bozo`, ii[0].Message) assert.Equal(t, rules.WarnLevel, ii[0].Level) - assert.Equal(t, `[POP-1206] No pods matched Ingress IPBlock 172.2.0.0/16`, ii[1].Message) + assert.Equal(t, `[POP-1206] No pods matched ingress IPBlock 172.2.0.0/16`, ii[1].Message) assert.Equal(t, rules.WarnLevel, ii[1].Level) - assert.Equal(t, `[POP-1207] No pods matched except Ingress IPBlock 172.2.1.0/24`, ii[2].Message) + assert.Equal(t, `[POP-1207] No pods matched except ingress IPBlock 172.2.1.0/24`, ii[2].Message) assert.Equal(t, rules.WarnLevel, ii[2].Level) - assert.Equal(t, `[POP-1201] No namespaces match Ingress namespace selector: app-In-ns-bozo`, ii[3].Message) + assert.Equal(t, `[POP-1201] No namespaces match ingress namespace selector: app-In-ns-bozo`, ii[3].Message) assert.Equal(t, rules.WarnLevel, ii[3].Level) - assert.Equal(t, `[POP-1202] No pods match Ingress pod selector: app=pod-bozo`, ii[4].Message) + assert.Equal(t, `[POP-1202] No pods match ingress pod selector: app=pod-bozo`, ii[4].Message) assert.Equal(t, rules.WarnLevel, ii[4].Level) - assert.Equal(t, `[POP-1208] No pods match Egress pod selector: app=p1-missing in namespace: default`, ii[5].Message) + assert.Equal(t, `[POP-1208] No pods match egress pod selector: app=p1-missing in namespace: default`, ii[5].Message) assert.Equal(t, rules.WarnLevel, ii[5].Level) } diff --git a/internal/lint/pod.go b/internal/lint/pod.go index 22cdbc9e..507e47ef 100644 --- a/internal/lint/pod.go +++ b/internal/lint/pod.go @@ -51,6 +51,7 @@ func NewPod(co *issues.Collector, db *db.DB) *Pod { // Lint cleanse the resource.. func (s *Pod) Lint(ctx context.Context) error { + boundSA := boundDefaultSA(s.db) txn, it := s.db.MustITFor(internal.Glossary[internal.PO]) defer txn.Abort() for o := it.Next(); o != nil; o = it.Next() { @@ -59,7 +60,7 @@ func (s *Pod) Lint(ctx context.Context) error { s.InitOutcome(fqn) defer s.CloseOutcome(ctx, fqn, nil) - ctx = internal.WithSpec(ctx, SpecFor(fqn, po)) + ctx = internal.WithSpec(ctx, coSpecFor(fqn, po, po.Spec)) s.checkStatus(ctx, po) s.checkContainerStatus(ctx, fqn, po) s.checkContainers(ctx, fqn, po) @@ -69,7 +70,7 @@ func (s *Pod) Lint(ctx context.Context) error { s.checkPdb(ctx, po.ObjectMeta.Labels) } s.checkForMultiplePdbMatches(ctx, po.Namespace, po.ObjectMeta.Labels) - s.checkSecure(ctx, fqn, po.Spec) + s.checkSecure(ctx, fqn, po.Spec, boundSA) pmx, err := s.db.FindPMX(fqn) if err != nil { @@ -96,6 +97,10 @@ func (s *Pod) checkNPs(ctx context.Context, pod *v1.Pod) { txn, it := s.db.MustITForNS(internal.Glossary[internal.NP], pod.Namespace) defer txn.Abort() + const ( + in = 0 + out = 1 + ) matches := [2]int{} for o := it.Next(); o != nil; o = it.Next() { np := o.(*netv1.NetworkPolicy) @@ -103,34 +108,41 @@ func (s *Pod) checkNPs(ctx context.Context, pod *v1.Pod) { return } if isDenyAllIngress(&np.Spec) || isAllowAllIngress(&np.Spec) { - matches[0]++ + matches[in]++ if s.checkEgresses(ctx, pod, np.Spec.Egress) { - matches[1]++ + matches[out]++ } continue } if isDenyAllEgress(&np.Spec) || isAllowAllEgress(&np.Spec) { - matches[1]++ + matches[out]++ if s.checkIngresses(ctx, pod, np.Spec.Ingress) { - matches[0]++ + matches[in]++ } continue } if labelsMatch(&np.Spec.PodSelector, pod.Labels) { + if polInclude(np.Spec.PolicyTypes, dirIn) { + matches[in]++ + } + if polInclude(np.Spec.PolicyTypes, dirOut) { + matches[out]++ + } + } else { if s.checkIngresses(ctx, pod, np.Spec.Ingress) { - matches[0]++ + matches[out]++ } if s.checkEgresses(ctx, pod, np.Spec.Egress) { - matches[1]++ + matches[in]++ } } } - if matches[0] == 0 { - s.AddCode(ctx, 1204, dirIn) + if matches[in] == 0 { + s.AddCode(ctx, 1204, ingress) } - if matches[1] == 0 { - s.AddCode(ctx, 1204, dirOut) + if matches[out] == 0 { + s.AddCode(ctx, 1204, egress) } } @@ -285,17 +297,21 @@ func (s *Pod) checkUtilization(ctx context.Context, fqn string, po *v1.Pod, cmx } } -func (s *Pod) checkSecure(ctx context.Context, fqn string, spec v1.PodSpec) { - if err := s.checkSA(ctx, fqn, spec); err != nil { +func (s *Pod) checkSecure(ctx context.Context, fqn string, spec v1.PodSpec, boundSA bool) { + if err := s.checkSA(ctx, fqn, spec, boundSA); err != nil { s.AddErr(ctx, err) } s.checkSecContext(ctx, fqn, spec) } -func (s *Pod) checkSA(ctx context.Context, fqn string, spec v1.PodSpec) error { +func (s *Pod) checkSA(ctx context.Context, fqn string, spec v1.PodSpec, boundSA bool) error { ns, _ := namespaced(fqn) if spec.ServiceAccountName == "default" { - s.AddCode(ctx, 300) + if boundSA { + s.AddCode(ctx, 308) + } else { + s.AddCode(ctx, 300) + } } txn := s.db.Txn(false) diff --git a/internal/lint/pod_test.go b/internal/lint/pod_test.go index 3c0d0d0f..c53f54ed 100644 --- a/internal/lint/pod_test.go +++ b/internal/lint/pod_test.go @@ -5,12 +5,18 @@ package lint import ( "context" + "os" + "path/filepath" "testing" "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" "github.com/derailed/popeye/internal/test" + "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/pkg/config/json" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" v1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" polv1 "k8s.io/api/policy/v1" @@ -37,7 +43,7 @@ func TestPodNPLint(t *testing.T) { ii := po.Outcome()["ns1/p1"] assert.Equal(t, 1, len(ii)) - assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[0].Message) + assert.Equal(t, `[POP-1204] Pod egress is not secured by a network policy`, ii[0].Message) ii = po.Outcome()["ns2/p2"] assert.Equal(t, 0, len(ii)) @@ -106,7 +112,7 @@ func TestPodCheckSecure(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { p := NewPod(test.MakeCollector(t), dba) - p.checkSecure(ctx, "default/p1", u.pod.Spec) + p.checkSecure(ctx, "default/p1", u.pod.Spec, true) assert.Equal(t, u.issues, len(p.Outcome()["default/p1"])) }) } @@ -136,8 +142,8 @@ func TestPodLint(t *testing.T) { assert.Equal(t, 6, len(ii)) assert.Equal(t, `[POP-207] Pod is in an unhappy phase ()`, ii[0].Message) assert.Equal(t, `[POP-208] Unmanaged pod detected. Best to use a controller`, ii[1].Message) - assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[2].Message) - assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[3].Message) + assert.Equal(t, `[POP-1204] Pod ingress is not secured by a network policy`, ii[2].Message) + assert.Equal(t, `[POP-1204] Pod egress is not secured by a network policy`, ii[3].Message) assert.Equal(t, `[POP-206] Pod has no associated PodDisruptionBudget`, ii[4].Message) assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[5].Message) @@ -145,8 +151,8 @@ func TestPodLint(t *testing.T) { assert.Equal(t, 6, len(ii)) assert.Equal(t, `[POP-105] Liveness uses a port#, prefer a named port`, ii[0].Message) assert.Equal(t, `[POP-105] Readiness uses a port#, prefer a named port`, ii[1].Message) - assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[2].Message) - assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[3].Message) + assert.Equal(t, `[POP-1204] Pod ingress is not secured by a network policy`, ii[2].Message) + assert.Equal(t, `[POP-1204] Pod egress is not secured by a network policy`, ii[3].Message) assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[4].Message) assert.Equal(t, `[POP-109] CPU Current/Request (2000m/1000m) reached user 80% threshold (200%)`, ii[5].Message) @@ -163,8 +169,8 @@ func TestPodLint(t *testing.T) { assert.Equal(t, `[POP-113] Container image "zorg:latest" is not hosted on an allowed docker registry`, ii[8].Message) assert.Equal(t, `[POP-107] No resource limits defined`, ii[9].Message) assert.Equal(t, `[POP-208] Unmanaged pod detected. Best to use a controller`, ii[10].Message) - assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[11].Message) - assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[12].Message) + assert.Equal(t, `[POP-1204] Pod ingress is not secured by a network policy`, ii[11].Message) + assert.Equal(t, `[POP-1204] Pod egress is not secured by a network policy`, ii[12].Message) assert.Equal(t, `[POP-300] Uses "default" ServiceAccount`, ii[13].Message) assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[14].Message) @@ -173,12 +179,50 @@ func TestPodLint(t *testing.T) { assert.Equal(t, `[POP-113] Container image "blee:v1.2" is not hosted on an allowed docker registry`, ii[0].Message) assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[1].Message) assert.Equal(t, `[POP-102] No probes defined`, ii[2].Message) - assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[3].Message) - assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[4].Message) + assert.Equal(t, `[POP-1204] Pod ingress is not secured by a network policy`, ii[3].Message) + assert.Equal(t, `[POP-1204] Pod egress is not secured by a network policy`, ii[4].Message) assert.Equal(t, `[POP-209] Pod is managed by multiple PodDisruptionBudgets (pdb4, pdb4-1)`, ii[5].Message) assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[6].Message) } +func TestPodLintExcludes(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/2.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*polv1.PodDisruptionBudget](ctx, l.DB, "pol/pdb/1.yaml", internal.Glossary[internal.PDB])) + assert.NoError(t, test.LoadDB[*netv1.NetworkPolicy](ctx, l.DB, "net/np/1.yaml", internal.Glossary[internal.NP])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + bb, err := os.ReadFile(filepath.Join("testdata", "config", "1.yaml")) + assert.NoError(t, err) + assert.NoError(t, json.NewValidator().Validate(json.SpinachSchema, bb)) + var cfg config.Config + assert.NoError(t, yaml.Unmarshal(bb, &cfg)) + + codes, err := issues.LoadCodes() + assert.NoError(t, err) + cc := issues.NewCollector(codes, &cfg) + po := NewPod(cc, dba) + po.Collector.Config.Registries = []string{"dorker.io"} + assert.Nil(t, po.Lint(test.MakeContext("v1/pods", "pods"))) + assert.Equal(t, 5, len(po.Outcome())) + + ii := po.Outcome()["default/p4"] + + assert.Equal(t, 7, len(ii)) + assert.Equal(t, `[POP-101] Image tagged "latest" in use`, ii[0].Message) + assert.Equal(t, `[POP-107] No resource limits defined`, ii[1].Message) + assert.Equal(t, `[POP-208] Unmanaged pod detected. Best to use a controller`, ii[2].Message) + assert.Equal(t, `[POP-1204] Pod ingress is not secured by a network policy`, ii[3].Message) + assert.Equal(t, `[POP-1204] Pod egress is not secured by a network policy`, ii[4].Message) + assert.Equal(t, `[POP-300] Uses "default" ServiceAccount`, ii[5].Message) + assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[6].Message) +} + // ---------------------------------------------------------------------------- // Helpers... diff --git a/internal/lint/rb.go b/internal/lint/rb.go index 28492a34..b13d46ec 100644 --- a/internal/lint/rb.go +++ b/internal/lint/rb.go @@ -60,3 +60,31 @@ func (r *RoleBinding) checkInUse(ctx context.Context) { } } } + +func boundDefaultSA(db *db.DB) bool { + txn, it := db.MustITFor(internal.Glossary[internal.ROB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + rb := o.(*rbacv1.RoleBinding) + if rb.Namespace != client.DefaultNamespace || rb.RoleRef.Kind == "ClusterRole" { + continue + } + if rb.RoleRef.APIGroup == "" && rb.RoleRef.Kind == "ServiceAccount" && rb.RoleRef.Name == "default" { + return true + } + } + + txn, it = db.MustITFor(internal.Glossary[internal.CRB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + rb := o.(*rbacv1.ClusterRoleBinding) + if rb.RoleRef.Kind == "ClusterRole" { + continue + } + if rb.RoleRef.APIGroup == "" && rb.RoleRef.Kind == "ServiceAccount" && rb.RoleRef.Name == "default" { + return true + } + } + + return false +} diff --git a/internal/lint/rb_test.go b/internal/lint/rb_test.go index 086a405b..1bd43960 100644 --- a/internal/lint/rb_test.go +++ b/internal/lint/rb_test.go @@ -44,3 +44,47 @@ func TestRBLint(t *testing.T) { assert.Equal(t, `[POP-1300] References a ClusterRole (cr-bozo) which does not exist`, ii[0].Message) assert.Equal(t, rules.WarnLevel, ii[0].Level) } + +func TestRB_boundDefaultSA(t *testing.T) { + uu := map[string]struct { + roPath, robPath string + crPath, crbPath string + e bool + }{ + "happy": { + roPath: "auth/ro/1.yaml", + robPath: "auth/rob/1.yaml", + crPath: "auth/cr/1.yaml", + crbPath: "auth/crb/1.yaml", + }, + "role-bound": { + roPath: "auth/ro/1.yaml", + robPath: "auth/rob/2.yaml", + crPath: "auth/cr/1.yaml", + crbPath: "auth/crb/1.yaml", + }, + "cluster-role-bound": { + roPath: "auth/ro/1.yaml", + robPath: "auth/rob/1.yaml", + crPath: "auth/cr/1.yaml", + crbPath: "auth/crb/2.yaml", + }, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.RoleBinding](ctx, l.DB, u.robPath, internal.Glossary[internal.ROB])) + assert.NoError(t, test.LoadDB[*rbacv1.Role](ctx, l.DB, u.roPath, internal.Glossary[internal.RO])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRole](ctx, l.DB, u.crPath, internal.Glossary[internal.CR])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, u.crbPath, internal.Glossary[internal.CRB])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + + assert.Equal(t, u.e, boundDefaultSA(dba)) + }) + } +} diff --git a/internal/lint/rs.go b/internal/lint/rs.go index 2e9763bd..dca34c11 100644 --- a/internal/lint/rs.go +++ b/internal/lint/rs.go @@ -36,7 +36,7 @@ func (s *ReplicaSet) Lint(ctx context.Context) error { rs := o.(*appsv1.ReplicaSet) fqn := client.FQN(rs.Namespace, rs.Name) s.InitOutcome(fqn) - ctx = internal.WithSpec(ctx, SpecFor(fqn, rs)) + ctx = internal.WithSpec(ctx, coSpecFor(fqn, rs, rs.Spec.Template.Spec)) s.checkHealth(ctx, rs) } diff --git a/internal/lint/sts.go b/internal/lint/sts.go index 6d94ea5d..cf34cdf1 100644 --- a/internal/lint/sts.go +++ b/internal/lint/sts.go @@ -48,7 +48,7 @@ func (s *StatefulSet) Lint(ctx context.Context) error { sts := o.(*appsv1.StatefulSet) fqn := client.FQN(sts.Namespace, sts.Name) s.InitOutcome(fqn) - ctx = internal.WithSpec(ctx, SpecFor(fqn, sts)) + ctx = internal.WithSpec(ctx, coSpecFor(fqn, sts, sts.Spec.Template.Spec)) s.checkStatefulSet(ctx, sts) s.checkContainers(ctx, fqn, sts) diff --git a/internal/lint/testdata/auth/crb/2.yaml b/internal/lint/testdata/auth/crb/2.yaml new file mode 100644 index 00000000..df2abcf3 --- /dev/null +++ b/internal/lint/testdata/auth/crb/2.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb1 + subjects: + - kind: ServiceAccount + name: default + namespace: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr1 + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb2 + subjects: + - kind: ServiceAccount + name: sa2 + namespace: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr-bozo + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb3 + subjects: + - kind: ServiceAccount + name: sa-bozo + namespace: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r-bozo + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/internal/lint/testdata/auth/rob/2.yaml b/internal/lint/testdata/auth/rob/2.yaml new file mode 100644 index 00000000..4663a1cf --- /dev/null +++ b/internal/lint/testdata/auth/rob/2.yaml @@ -0,0 +1,45 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb1 + namespace: default + subjects: + - kind: ServiceAccount + name: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r1 + apiGroup: rbac.authorization.k8s.io + + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb2 + namespace: default + subjects: + - kind: ServiceAccount + name: sa-bozo + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r-bozo + apiGroup: rbac.authorization.k8s.io + + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb3 + namespace: default + subjects: + - kind: ServiceAccount + name: sa-bozo + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr-bozo + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/internal/lint/testdata/config/1.yaml b/internal/lint/testdata/config/1.yaml new file mode 100644 index 00000000..d36ab81b --- /dev/null +++ b/internal/lint/testdata/config/1.yaml @@ -0,0 +1,12 @@ +popeye: + excludes: + linters: + pods: + instances: + - codes: + - 100 + - 106 + - 113 + - 204 + containers: + - "rx:c2*" \ No newline at end of file diff --git a/internal/lint/testdata/core/pod/3.yaml b/internal/lint/testdata/core/pod/3.yaml index 49b1419f..86e6d5e6 100644 --- a/internal/lint/testdata/core/pod/3.yaml +++ b/internal/lint/testdata/core/pod/3.yaml @@ -1,7 +1,8 @@ --- apiVersion: v1 -kind: List +kind: PodList items: + - apiVersion: v1 kind: Pod metadata: @@ -62,6 +63,7 @@ items: phase: Running podIPs: - ip: 172.1.0.3 + - apiVersion: v1 kind: Pod metadata: diff --git a/internal/lint/testdata/net/np/1.yaml b/internal/lint/testdata/net/np/1.yaml index e25a10c4..e0c05859 100644 --- a/internal/lint/testdata/net/np/1.yaml +++ b/internal/lint/testdata/net/np/1.yaml @@ -1,6 +1,7 @@ -apiVersion: v1 -kind: List +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicyList items: + - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -35,12 +36,14 @@ items: ports: - protocol: TCP port: 5978 + - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: np2 namespace: default spec: + podSelector: {} ingress: - from: - ipBlock: @@ -66,6 +69,7 @@ items: ports: - protocol: TCP port: 5978 + - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: diff --git a/internal/lint/testdata/net/np/2.yaml b/internal/lint/testdata/net/np/2.yaml index 0d088d36..7ecea392 100644 --- a/internal/lint/testdata/net/np/2.yaml +++ b/internal/lint/testdata/net/np/2.yaml @@ -1,6 +1,7 @@ -apiVersion: v1 -kind: List +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicyList items: + - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -29,6 +30,7 @@ items: podSelector: {} policyTypes: - Egress + - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -54,6 +56,7 @@ items: - {} policyTypes: - Ingress + - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -82,6 +85,7 @@ items: policyTypes: - Ingress - Egress + - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: diff --git a/internal/lint/testdata/net/np/3.yaml b/internal/lint/testdata/net/np/3.yaml index 3ae4bad3..10548950 100644 --- a/internal/lint/testdata/net/np/3.yaml +++ b/internal/lint/testdata/net/np/3.yaml @@ -1,6 +1,7 @@ -apiVersion: v1 -kind: List +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicyList items: + - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -10,15 +11,14 @@ items: podSelector: {} policyTypes: - Ingress + - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-all-egress namespace: ns2 spec: - # podSelector: - # matchLabels: - # app: p2 + podSelector: {} ingress: - from: - namespaceSelector: diff --git a/internal/lint/testdata/net/np/blee.yml b/internal/lint/testdata/net/np/blee.yml new file mode 100644 index 00000000..86a03d3d --- /dev/null +++ b/internal/lint/testdata/net/np/blee.yml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: np1 + namespace: default +spec: + podSelector: + matchLabels: + app: p1 + policyTypes: + - Ingress + - Egress + ingress: + - from: + - ipBlock: + cidr: 172.1.0.0/16 + except: + - 172.1.0.0/24 + - namespaceSelector: + matchLabels: + ns: default + podSelector: + matchLabels: + app: p1 + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - ipBlock: + cidr: 172.1.0.0/16 + ports: + - protocol: TCP + port: 5978