mirror of
https://github.com/kubescape/kubescape.git
synced 2026-03-03 10:10:36 +00:00
500 lines
15 KiB
Go
500 lines
15 KiB
Go
package resourcehandler
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/armosec/armoapi-go/armotypes"
|
|
"github.com/kubescape/k8s-interface/k8sinterface"
|
|
"github.com/kubescape/k8s-interface/workloadinterface"
|
|
"github.com/kubescape/opa-utils/reporthandling"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func mockMatch(i int) reporthandling.RuleMatchObjects {
|
|
switch i {
|
|
case 1:
|
|
return reporthandling.RuleMatchObjects{
|
|
APIGroups: []string{"apps"},
|
|
APIVersions: []string{"v1", "v1beta"},
|
|
Resources: []string{"Pod"},
|
|
}
|
|
case 2:
|
|
return reporthandling.RuleMatchObjects{
|
|
APIGroups: []string{"apps"},
|
|
APIVersions: []string{"v1"},
|
|
Resources: []string{"Deployment", "ReplicaSet"},
|
|
}
|
|
case 3:
|
|
return reporthandling.RuleMatchObjects{
|
|
APIGroups: []string{"core"},
|
|
APIVersions: []string{"v1"},
|
|
Resources: []string{"Secret"},
|
|
}
|
|
case 4:
|
|
return reporthandling.RuleMatchObjects{
|
|
APIGroups: []string{"core"},
|
|
APIVersions: []string{"v1"},
|
|
Resources: []string{"Secret"},
|
|
FieldSelector: []string{"metadata.name=secret1", "metadata.name=secret2,metadata.namespace=default"},
|
|
}
|
|
case 5:
|
|
return reporthandling.RuleMatchObjects{
|
|
APIGroups: []string{"rbac.authorization.k8s.io"},
|
|
APIVersions: []string{"v1"},
|
|
Resources: []string{"ClusterRoleBinding", "RoleBinding"},
|
|
FieldSelector: []string{"metadata.name=test123"},
|
|
}
|
|
case 6:
|
|
return reporthandling.RuleMatchObjects{
|
|
APIGroups: []string{""},
|
|
APIVersions: []string{"v1"},
|
|
Resources: []string{"Namespace"},
|
|
FieldSelector: []string{},
|
|
}
|
|
case 7:
|
|
return reporthandling.RuleMatchObjects{
|
|
APIGroups: []string{""},
|
|
APIVersions: []string{"v1"},
|
|
Resources: []string{"Node"},
|
|
FieldSelector: []string{},
|
|
}
|
|
|
|
default:
|
|
panic("invalid index")
|
|
}
|
|
}
|
|
|
|
func mockRule(ruleName string, matches []reporthandling.RuleMatchObjects, ruleRego string) reporthandling.PolicyRule {
|
|
rule := reporthandling.PolicyRule{
|
|
PortalBase: *armotypes.MockPortalBase("aaaaaaaa-bbbb-cccc-dddd-000000000001", ruleName, nil),
|
|
RuleLanguage: reporthandling.RegoLanguage,
|
|
Match: matches,
|
|
RuleDependencies: []reporthandling.RuleDependency{
|
|
{
|
|
PackageName: "kubernetes.api.client",
|
|
},
|
|
},
|
|
}
|
|
if ruleRego != "" {
|
|
rule.Rule = ruleRego
|
|
} else {
|
|
rule.Rule = reporthandling.MockRegoPrivilegedPods()
|
|
}
|
|
return rule
|
|
}
|
|
|
|
func mockControl(controlName string, rules []reporthandling.PolicyRule) reporthandling.Control {
|
|
return reporthandling.Control{
|
|
PortalBase: *armotypes.MockPortalBase("aaaaaaaa-bbbb-cccc-dddd-000000000001", controlName, nil),
|
|
Rules: rules,
|
|
}
|
|
|
|
}
|
|
|
|
func mockFramework(frameworkName string, controls []reporthandling.Control) *reporthandling.Framework {
|
|
return &reporthandling.Framework{
|
|
PortalBase: *armotypes.MockPortalBase("aaaaaaaa-bbbb-cccc-dddd-000000000001", frameworkName, nil),
|
|
CreationTime: "",
|
|
Description: "mock framework description",
|
|
Controls: controls,
|
|
}
|
|
}
|
|
|
|
func mockWorkload(apiVersion, kind, namespace, name string) workloadinterface.IWorkload {
|
|
mock := workloadinterface.NewWorkloadMock(nil)
|
|
mock.SetKind(kind)
|
|
mock.SetApiVersion(apiVersion)
|
|
mock.SetName(name)
|
|
mock.SetNamespace(namespace)
|
|
|
|
if ok := k8sinterface.IsTypeWorkload(mock.GetObject()); !ok {
|
|
panic("mocked object is not a valid workload")
|
|
}
|
|
|
|
return mock
|
|
}
|
|
|
|
func TestGetQueryableResourceMapFromPolicies(t *testing.T) {
|
|
k8sinterface.InitializeMapResourcesMock()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
workload workloadinterface.IWorkload
|
|
controls []reporthandling.Control
|
|
expectedResourceGroups []string
|
|
expectedExcludedRules []string
|
|
}{
|
|
{
|
|
name: "no workload - all resources groups are queryable",
|
|
workload: nil,
|
|
controls: []reporthandling.Control{
|
|
mockControl("1", []reporthandling.PolicyRule{
|
|
mockRule("rule-a", []reporthandling.RuleMatchObjects{
|
|
mockMatch(1), mockMatch(2), mockMatch(3), mockMatch(4),
|
|
}, ""),
|
|
mockRule("rule-b", []reporthandling.RuleMatchObjects{
|
|
mockMatch(6),
|
|
}, ""),
|
|
}),
|
|
},
|
|
expectedExcludedRules: []string{},
|
|
expectedResourceGroups: []string{
|
|
"/v1/namespaces",
|
|
"apps/v1/deployments",
|
|
"apps/v1/pods",
|
|
"apps/v1/replicasets",
|
|
"apps/v1beta/pods",
|
|
"core/v1/secrets",
|
|
"core/v1/secrets/metadata.name=secret1",
|
|
"core/v1/secrets/metadata.name=secret2,metadata.namespace=default",
|
|
},
|
|
},
|
|
{
|
|
name: "workload - Namespace",
|
|
workload: mockWorkload("v1", "Namespace", "", "ns1"),
|
|
controls: []reporthandling.Control{
|
|
mockControl("1", []reporthandling.PolicyRule{
|
|
mockRule("rule-a", []reporthandling.RuleMatchObjects{
|
|
mockMatch(1), mockMatch(2), mockMatch(3), mockMatch(4),
|
|
}, ""),
|
|
mockRule("rule-b", []reporthandling.RuleMatchObjects{
|
|
mockMatch(6), mockMatch(3), mockMatch(2), mockMatch(7),
|
|
}, ""),
|
|
}),
|
|
},
|
|
expectedExcludedRules: []string{
|
|
"rule-a",
|
|
},
|
|
expectedResourceGroups: []string{
|
|
"/v1/nodes",
|
|
"core/v1/secrets/metadata.namespace=ns1",
|
|
"apps/v1/deployments/metadata.namespace=ns1",
|
|
"apps/v1/replicasets/metadata.namespace=ns1",
|
|
},
|
|
},
|
|
{
|
|
name: "workload - Deployment",
|
|
workload: mockWorkload("apps/v1", "Deployment", "ns1", "deploy1"),
|
|
controls: []reporthandling.Control{
|
|
mockControl("1", []reporthandling.PolicyRule{
|
|
mockRule("rule-b", []reporthandling.RuleMatchObjects{
|
|
mockMatch(6), mockMatch(3), mockMatch(2), mockMatch(7),
|
|
}, ""),
|
|
}),
|
|
},
|
|
expectedExcludedRules: []string{},
|
|
expectedResourceGroups: []string{
|
|
"core/v1/secrets/metadata.namespace=ns1",
|
|
"/v1/namespaces/metadata.name=ns1",
|
|
"/v1/nodes",
|
|
},
|
|
},
|
|
{
|
|
name: "workload - Node",
|
|
workload: mockWorkload("v1", "Node", "", "node1"),
|
|
controls: []reporthandling.Control{
|
|
mockControl("1", []reporthandling.PolicyRule{
|
|
mockRule("rule-b", []reporthandling.RuleMatchObjects{
|
|
mockMatch(6), mockMatch(3), mockMatch(2), mockMatch(7),
|
|
}, ""),
|
|
}),
|
|
},
|
|
expectedExcludedRules: []string{},
|
|
expectedResourceGroups: []string{
|
|
"core/v1/secrets",
|
|
"/v1/namespaces",
|
|
"apps/v1/deployments",
|
|
"apps/v1/replicasets",
|
|
},
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
resourceGroups, excludedRulesMap := getQueryableResourceMapFromPolicies([]reporthandling.Framework{*mockFramework("test", testCase.controls)}, testCase.workload, reporthandling.ScopeCluster) // TODO check second param
|
|
assert.Equalf(t, len(testCase.expectedExcludedRules), len(excludedRulesMap), "excludedRulesMap length is not as expected")
|
|
for _, expectedExcludedRuleName := range testCase.expectedExcludedRules {
|
|
assert.Contains(t, excludedRulesMap, expectedExcludedRuleName, "excludedRulesMap does not contain expected rule name")
|
|
}
|
|
|
|
assert.Equalf(t, len(testCase.expectedResourceGroups), len(resourceGroups), "queryableResourceMap length is not as expected")
|
|
for _, expected := range testCase.expectedResourceGroups {
|
|
assert.Contains(t, resourceGroups, expected, "queryableResourceMap does not contain expected resource group")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateQueryableResourcesMapFromRuleMatchObject(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
matches []reporthandling.RuleMatchObjects
|
|
resourcesFilterMap map[string]bool
|
|
namespace string
|
|
expectedQueryableResourceGroups []string
|
|
expectedK8SResourceGroups []string
|
|
}{
|
|
{
|
|
name: "filter map is nil - query all",
|
|
matches: []reporthandling.RuleMatchObjects{
|
|
mockMatch(1), mockMatch(2), mockMatch(3), mockMatch(4),
|
|
},
|
|
resourcesFilterMap: nil,
|
|
namespace: "",
|
|
expectedQueryableResourceGroups: []string{
|
|
"apps/v1/pods",
|
|
"apps/v1beta/pods",
|
|
"apps/v1/deployments",
|
|
"apps/v1/replicasets",
|
|
"core/v1/secrets",
|
|
"core/v1/secrets/metadata.name=secret1",
|
|
"core/v1/secrets/metadata.name=secret2,metadata.namespace=default",
|
|
},
|
|
expectedK8SResourceGroups: []string{
|
|
"apps/v1/pods",
|
|
"apps/v1beta/pods",
|
|
"apps/v1/deployments",
|
|
"apps/v1/replicasets",
|
|
"core/v1/secrets",
|
|
},
|
|
},
|
|
{
|
|
name: "filter map not nil - query only secrets and pods",
|
|
matches: []reporthandling.RuleMatchObjects{
|
|
mockMatch(1), mockMatch(2), mockMatch(3), mockMatch(4),
|
|
},
|
|
namespace: "",
|
|
resourcesFilterMap: map[string]bool{
|
|
"Secret": true,
|
|
"Pod": true,
|
|
"ReplicaSet": false,
|
|
"Deployment": false,
|
|
},
|
|
expectedQueryableResourceGroups: []string{
|
|
"apps/v1/pods",
|
|
"apps/v1beta/pods",
|
|
"core/v1/secrets",
|
|
"core/v1/secrets/metadata.name=secret1",
|
|
"core/v1/secrets/metadata.name=secret2,metadata.namespace=default",
|
|
},
|
|
expectedK8SResourceGroups: []string{
|
|
"apps/v1/pods",
|
|
"apps/v1beta/pods",
|
|
"core/v1/secrets",
|
|
},
|
|
},
|
|
{
|
|
name: "namespace field selector for namespaced resources",
|
|
matches: []reporthandling.RuleMatchObjects{
|
|
mockMatch(5),
|
|
},
|
|
namespace: "ns1",
|
|
resourcesFilterMap: map[string]bool{
|
|
"RoleBinding": true,
|
|
"ClusterRoleBinding": true,
|
|
},
|
|
expectedQueryableResourceGroups: []string{
|
|
|
|
"rbac.authorization.k8s.io/v1/clusterrolebindings/metadata.name=test123",
|
|
"rbac.authorization.k8s.io/v1/rolebindings/metadata.namespace=ns1,metadata.name=test123",
|
|
},
|
|
expectedK8SResourceGroups: []string{
|
|
"rbac.authorization.k8s.io/v1/clusterrolebindings",
|
|
"rbac.authorization.k8s.io/v1/rolebindings",
|
|
},
|
|
},
|
|
{
|
|
name: "name field selector for Namespace resource",
|
|
matches: []reporthandling.RuleMatchObjects{
|
|
mockMatch(2), mockMatch(6),
|
|
},
|
|
namespace: "ns1",
|
|
resourcesFilterMap: map[string]bool{
|
|
"Deployment": true,
|
|
"ReplicaSet": false,
|
|
"Namespace": true,
|
|
},
|
|
expectedQueryableResourceGroups: []string{
|
|
"apps/v1/deployments/metadata.namespace=ns1",
|
|
"/v1/namespaces/metadata.name=ns1",
|
|
},
|
|
expectedK8SResourceGroups: []string{
|
|
"apps/v1/deployments",
|
|
"/v1/namespaces",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
queryableResources := make(QueryableResources)
|
|
for i := range testCase.matches {
|
|
updateQueryableResourcesMapFromRuleMatchObject(&testCase.matches[i], testCase.resourcesFilterMap, queryableResources, testCase.namespace)
|
|
}
|
|
|
|
assert.Equal(t, len(testCase.expectedQueryableResourceGroups), len(queryableResources))
|
|
for _, resourceGroup := range testCase.expectedQueryableResourceGroups {
|
|
assert.Contains(t, queryableResources, resourceGroup)
|
|
}
|
|
|
|
k8sResources := queryableResources.ToK8sResourceMap()
|
|
assert.Equal(t, len(testCase.expectedK8SResourceGroups), len(k8sResources))
|
|
for _, resourceGroup := range testCase.expectedK8SResourceGroups {
|
|
assert.Contains(t, k8sResources, resourceGroup)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilterRuleMatchesForResource(t *testing.T) {
|
|
testCases := []struct {
|
|
resourceKind string
|
|
matchResources []string
|
|
expectedMap map[string]bool
|
|
}{
|
|
{
|
|
resourceKind: "Pod",
|
|
matchResources: []string{
|
|
"Node", "Pod", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet", "CronJob", "Job", "PodSecurityPolicy",
|
|
},
|
|
expectedMap: map[string]bool{
|
|
"Node": true,
|
|
"PodSecurityPolicy": true,
|
|
"Pod": false,
|
|
"DaemonSet": false,
|
|
"Deployment": false,
|
|
"ReplicaSet": false,
|
|
"StatefulSet": false,
|
|
"CronJob": false,
|
|
"Job": false,
|
|
},
|
|
},
|
|
{
|
|
resourceKind: "Deployment",
|
|
matchResources: []string{
|
|
"Node", "Pod", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet", "CronJob", "Job", "PodSecurityPolicy",
|
|
},
|
|
expectedMap: map[string]bool{
|
|
"Node": true,
|
|
"PodSecurityPolicy": true,
|
|
"Pod": false,
|
|
"DaemonSet": false,
|
|
"Deployment": false,
|
|
"ReplicaSet": false,
|
|
"StatefulSet": false,
|
|
"CronJob": false,
|
|
"Job": false,
|
|
},
|
|
},
|
|
{
|
|
resourceKind: "Deployment",
|
|
matchResources: []string{
|
|
"Deployment", "ReplicaSet",
|
|
},
|
|
expectedMap: map[string]bool{
|
|
"Deployment": false,
|
|
"ReplicaSet": false,
|
|
},
|
|
},
|
|
{
|
|
resourceKind: "ReplicaSet",
|
|
matchResources: []string{
|
|
"Node", "Pod", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet", "CronJob", "Job", "PodSecurityPolicy",
|
|
},
|
|
expectedMap: map[string]bool{
|
|
"Node": true,
|
|
"PodSecurityPolicy": true,
|
|
"Pod": false,
|
|
"DaemonSet": false,
|
|
"Deployment": false,
|
|
"ReplicaSet": false,
|
|
"StatefulSet": false,
|
|
"CronJob": false,
|
|
"Job": false,
|
|
},
|
|
},
|
|
{
|
|
resourceKind: "ClusterRole",
|
|
matchResources: []string{
|
|
"Node", "Pod", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet", "CronJob", "Job", "PodSecurityPolicy",
|
|
},
|
|
expectedMap: nil, // rule does not apply to workload
|
|
},
|
|
{
|
|
resourceKind: "Node",
|
|
matchResources: []string{
|
|
"Node", "Pod", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet", "CronJob", "Job", "PodSecurityPolicy",
|
|
},
|
|
expectedMap: map[string]bool{
|
|
"Node": false,
|
|
"PodSecurityPolicy": true,
|
|
"Pod": true,
|
|
"DaemonSet": true,
|
|
"Deployment": true,
|
|
"ReplicaSet": true,
|
|
"StatefulSet": true,
|
|
"CronJob": true,
|
|
"Job": true,
|
|
},
|
|
},
|
|
{
|
|
resourceKind: "Pod",
|
|
matchResources: []string{
|
|
"PodSecurityPolicy", "Pod",
|
|
},
|
|
expectedMap: map[string]bool{
|
|
"PodSecurityPolicy": true,
|
|
"Pod": false,
|
|
},
|
|
},
|
|
{
|
|
resourceKind: "Pod",
|
|
matchResources: []string{
|
|
"PodSecurityPolicy", "Pod", "ReplicaSet",
|
|
},
|
|
expectedMap: map[string]bool{
|
|
"PodSecurityPolicy": true,
|
|
"Pod": false,
|
|
"ReplicaSet": false,
|
|
},
|
|
},
|
|
{
|
|
resourceKind: "Deployment",
|
|
matchResources: []string{
|
|
"PodSecurityPolicy", "Pod",
|
|
},
|
|
expectedMap: nil, // rule does not apply to workload
|
|
},
|
|
{
|
|
resourceKind: "PodSecurityPolicy",
|
|
matchResources: []string{
|
|
"PodSecurityPolicy", "Pod",
|
|
},
|
|
expectedMap: map[string]bool{
|
|
"PodSecurityPolicy": false,
|
|
"Pod": true,
|
|
},
|
|
},
|
|
}
|
|
for i, testCase := range testCases {
|
|
t.Run(fmt.Sprintf("%v", i), func(t *testing.T) {
|
|
matches := []reporthandling.RuleMatchObjects{
|
|
{
|
|
Resources: testCase.matchResources,
|
|
},
|
|
}
|
|
|
|
result := filterRuleMatchesForResource(testCase.resourceKind, matches)
|
|
if testCase.expectedMap == nil {
|
|
assert.Nil(t, result, "expected nil (rule does not apply to the resource)")
|
|
return
|
|
}
|
|
|
|
if !reflect.DeepEqual(result, testCase.expectedMap) {
|
|
t.Errorf("expected %v, got %v", testCase.expectedMap, result)
|
|
}
|
|
})
|
|
}
|
|
}
|