github.com/khulnasoft-lab/defsec@v1.0.5-0.20230827010352-5e9f46893d95/pkg/scanners/kubernetes/scanner_test.go (about) 1 package kubernetes 2 3 import ( 4 "context" 5 "os" 6 "strings" 7 "testing" 8 9 "github.com/khulnasoft-lab/defsec/pkg/framework" 10 "github.com/khulnasoft-lab/defsec/pkg/scanners/options" 11 12 "github.com/khulnasoft-lab/defsec/pkg/scan" 13 14 "github.com/khulnasoft-lab/defsec/test/testutil" 15 16 "github.com/stretchr/testify/assert" 17 18 "github.com/stretchr/testify/require" 19 ) 20 21 func Test_BasicScan(t *testing.T) { 22 23 fs := testutil.CreateFS(t, map[string]string{ 24 "/code/example.yaml": ` 25 apiVersion: v1 26 kind: Pod 27 metadata: 28 name: hello-cpu-limit 29 spec: 30 containers: 31 - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] 32 image: busybox 33 name: hello 34 `, 35 "/rules/lib.k8s.rego": ` 36 package lib.kubernetes 37 38 default is_gatekeeper = false 39 40 is_gatekeeper { 41 has_field(input, "review") 42 has_field(input.review, "object") 43 } 44 45 object = input { 46 not is_gatekeeper 47 } 48 49 object = input.review.object { 50 is_gatekeeper 51 } 52 53 format(msg) = gatekeeper_format { 54 is_gatekeeper 55 gatekeeper_format = {"msg": msg} 56 } 57 58 format(msg) = msg { 59 not is_gatekeeper 60 } 61 62 name = object.metadata.name 63 64 default namespace = "default" 65 66 namespace = object.metadata.namespace 67 68 #annotations = object.metadata.annotations 69 70 kind = object.kind 71 72 is_pod { 73 kind = "Pod" 74 } 75 76 is_cronjob { 77 kind = "CronJob" 78 } 79 80 default is_controller = false 81 82 is_controller { 83 kind = "Deployment" 84 } 85 86 is_controller { 87 kind = "StatefulSet" 88 } 89 90 is_controller { 91 kind = "DaemonSet" 92 } 93 94 is_controller { 95 kind = "ReplicaSet" 96 } 97 98 is_controller { 99 kind = "ReplicationController" 100 } 101 102 is_controller { 103 kind = "Job" 104 } 105 106 split_image(image) = [image, "latest"] { 107 not contains(image, ":") 108 } 109 110 split_image(image) = [image_name, tag] { 111 [image_name, tag] = split(image, ":") 112 } 113 114 pod_containers(pod) = all_containers { 115 keys = {"containers", "initContainers"} 116 all_containers = [c | keys[k]; c = pod.spec[k][_]] 117 } 118 119 containers[container] { 120 pods[pod] 121 all_containers = pod_containers(pod) 122 container = all_containers[_] 123 } 124 125 containers[container] { 126 all_containers = pod_containers(object) 127 container = all_containers[_] 128 } 129 130 pods[pod] { 131 is_pod 132 pod = object 133 } 134 135 pods[pod] { 136 is_controller 137 pod = object.spec.template 138 } 139 140 pods[pod] { 141 is_cronjob 142 pod = object.spec.jobTemplate.spec.template 143 } 144 145 volumes[volume] { 146 pods[pod] 147 volume = pod.spec.volumes[_] 148 } 149 150 dropped_capability(container, cap) { 151 container.securityContext.capabilities.drop[_] == cap 152 } 153 154 added_capability(container, cap) { 155 container.securityContext.capabilities.add[_] == cap 156 } 157 158 has_field(obj, field) { 159 obj[field] 160 } 161 162 no_read_only_filesystem(c) { 163 not has_field(c, "securityContext") 164 } 165 166 no_read_only_filesystem(c) { 167 has_field(c, "securityContext") 168 not has_field(c.securityContext, "readOnlyRootFilesystem") 169 } 170 171 privilege_escalation_allowed(c) { 172 not has_field(c, "securityContext") 173 } 174 175 privilege_escalation_allowed(c) { 176 has_field(c, "securityContext") 177 has_field(c.securityContext, "allowPrivilegeEscalation") 178 } 179 180 annotations[annotation] { 181 pods[pod] 182 annotation = pod.metadata.annotations 183 } 184 185 host_ipcs[host_ipc] { 186 pods[pod] 187 host_ipc = pod.spec.hostIPC 188 } 189 190 host_networks[host_network] { 191 pods[pod] 192 host_network = pod.spec.hostNetwork 193 } 194 195 host_pids[host_pid] { 196 pods[pod] 197 host_pid = pod.spec.hostPID 198 } 199 200 host_aliases[host_alias] { 201 pods[pod] 202 host_alias = pod.spec 203 } 204 `, 205 "/rules/lib.util.rego": ` 206 package lib.utils 207 208 has_key(x, k) { 209 _ = x[k] 210 }`, 211 "/rules/rule.rego": ` 212 package builtin.kubernetes.KSV011 213 214 import data.lib.kubernetes 215 import data.lib.utils 216 217 default failLimitsCPU = false 218 219 __rego_metadata__ := { 220 "id": "KSV011", 221 "avd_id": "AVD-KSV-0011", 222 "title": "CPU not limited", 223 "short_code": "limit-cpu", 224 "version": "v1.0.0", 225 "severity": "LOW", 226 "type": "Kubernetes Security Check", 227 "description": "Enforcing CPU limits prevents DoS via resource exhaustion.", 228 "recommended_actions": "Set a limit value under 'containers[].resources.limits.cpu'.", 229 "url": "https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits", 230 } 231 232 __rego_input__ := { 233 "combine": false, 234 "selector": [{"type": "kubernetes"}], 235 } 236 237 # getLimitsCPUContainers returns all containers which have set resources.limits.cpu 238 getLimitsCPUContainers[container] { 239 allContainers := kubernetes.containers[_] 240 utils.has_key(allContainers.resources.limits, "cpu") 241 container := allContainers.name 242 } 243 244 # getNoLimitsCPUContainers returns all containers which have not set 245 # resources.limits.cpu 246 getNoLimitsCPUContainers[container] { 247 container := kubernetes.containers[_].name 248 not getLimitsCPUContainers[container] 249 } 250 251 # failLimitsCPU is true if containers[].resources.limits.cpu is not set 252 # for ANY container 253 failLimitsCPU { 254 count(getNoLimitsCPUContainers) > 0 255 } 256 257 deny[res] { 258 failLimitsCPU 259 260 msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'resources.limits.cpu'", [getNoLimitsCPUContainers[_], kubernetes.kind, kubernetes.name])) 261 262 res := { 263 "msg": msg, 264 "id": __rego_metadata__.id, 265 "title": __rego_metadata__.title, 266 "severity": __rego_metadata__.severity, 267 "type": __rego_metadata__.type, 268 "startline": 6, 269 "endline": 10, 270 } 271 } 272 `, 273 }) 274 275 scanner := NewScanner(options.ScannerWithPolicyDirs("rules")) 276 277 results, err := scanner.ScanFS(context.TODO(), fs, "code") 278 require.NoError(t, err) 279 280 require.Len(t, results.GetFailed(), 1) 281 282 assert.Equal(t, scan.Rule{ 283 AVDID: "AVD-KSV-0011", 284 Aliases: []string{"KSV011"}, 285 ShortCode: "limit-cpu", 286 Summary: "CPU not limited", 287 Explanation: "Enforcing CPU limits prevents DoS via resource exhaustion.", 288 Impact: "", 289 Resolution: "Set a limit value under 'containers[].resources.limits.cpu'.", 290 Provider: "kubernetes", 291 Service: "general", 292 Links: []string{"https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits"}, 293 Severity: "LOW", 294 Terraform: &scan.EngineMetadata{}, 295 CloudFormation: &scan.EngineMetadata{}, 296 CustomChecks: scan.CustomChecks{Terraform: (*scan.TerraformCustomCheck)(nil)}, 297 RegoPackage: "data.builtin.kubernetes.KSV011", 298 Frameworks: map[framework.Framework][]string{}, 299 }, results.GetFailed()[0].Rule()) 300 301 failure := results.GetFailed()[0] 302 actualCode, err := failure.GetCode() 303 require.NoError(t, err) 304 for i := range actualCode.Lines { 305 actualCode.Lines[i].Highlighted = "" 306 } 307 assert.Equal(t, []scan.Line{ 308 { 309 Number: 6, 310 Content: "spec: ", 311 IsCause: true, 312 FirstCause: true, 313 Annotation: "", 314 }, 315 { 316 Number: 7, 317 Content: " containers: ", 318 IsCause: true, 319 Annotation: "", 320 }, 321 { 322 Number: 8, 323 Content: " - command: [\"sh\", \"-c\", \"echo 'Hello' && sleep 1h\"]", 324 IsCause: true, 325 Annotation: "", 326 }, 327 { 328 Number: 9, 329 Content: " image: busybox", 330 IsCause: true, 331 Annotation: "", 332 }, 333 { 334 Number: 10, 335 Content: " name: hello", 336 IsCause: true, 337 LastCause: true, 338 Annotation: "", 339 }, 340 }, actualCode.Lines) 341 } 342 343 func Test_FileScan(t *testing.T) { 344 345 results, err := NewScanner(options.ScannerWithEmbeddedPolicies(true), options.ScannerWithEmbeddedLibraries(true), options.ScannerWithEmbeddedLibraries(true)).ScanReader(context.TODO(), "k8s.yaml", strings.NewReader(` 346 apiVersion: v1 347 kind: Pod 348 metadata: 349 name: hello-cpu-limit 350 spec: 351 containers: 352 - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] 353 image: busybox 354 name: hello 355 `)) 356 require.NoError(t, err) 357 358 assert.Greater(t, len(results.GetFailed()), 0) 359 } 360 361 func Test_FileScan_WithSeparator(t *testing.T) { 362 363 results, err := NewScanner(options.ScannerWithEmbeddedPolicies(true), options.ScannerWithEmbeddedLibraries(true)).ScanReader(context.TODO(), "k8s.yaml", strings.NewReader(` 364 --- 365 --- 366 apiVersion: v1 367 kind: Pod 368 metadata: 369 name: hello-cpu-limit 370 spec: 371 containers: 372 - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] 373 image: busybox 374 name: hello 375 `)) 376 require.NoError(t, err) 377 378 assert.Greater(t, len(results.GetFailed()), 0) 379 } 380 381 func Test_FileScan_MultiManifests(t *testing.T) { 382 file := ` 383 --- 384 apiVersion: v1 385 kind: Pod 386 metadata: 387 name: hello1-cpu-limit 388 spec: 389 containers: 390 - command: ["sh", "-c", "echo 'Hello1' && sleep 1h"] 391 image: busybox 392 name: hello1 393 --- 394 apiVersion: v1 395 kind: Pod 396 metadata: 397 name: hello2-cpu-limit 398 spec: 399 containers: 400 - command: ["sh", "-c", "echo 'Hello2' && sleep 1h"] 401 image: busybox 402 name: hello2 403 ` 404 405 results, err := NewScanner( 406 options.ScannerWithEmbeddedPolicies(true), 407 options.ScannerWithEmbeddedLibraries(true), 408 options.ScannerWithEmbeddedLibraries(true)).ScanReader(context.TODO(), "k8s.yaml", strings.NewReader(file)) 409 require.NoError(t, err) 410 411 assert.Greater(t, len(results.GetFailed()), 1) 412 fileLines := strings.Split(file, "\n") 413 for _, failure := range results.GetFailed() { 414 actualCode, err := failure.GetCode() 415 require.NoError(t, err) 416 assert.Greater(t, len(actualCode.Lines), 0) 417 for _, line := range actualCode.Lines { 418 assert.Greater(t, len(fileLines), line.Number) 419 assert.Equal(t, line.Content, fileLines[line.Number-1]) 420 } 421 } 422 } 423 424 func Test_FileScanWithPolicyReader(t *testing.T) { 425 426 results, err := NewScanner(options.ScannerWithPolicyReader(strings.NewReader(`package defsec 427 428 deny[msg] { 429 msg = "fail" 430 } 431 `))).ScanReader(context.TODO(), "k8s.yaml", strings.NewReader(` 432 apiVersion: v1 433 kind: Pod 434 metadata: 435 name: hello-cpu-limit 436 spec: 437 containers: 438 - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] 439 image: busybox 440 name: hello 441 `)) 442 require.NoError(t, err) 443 444 assert.Equal(t, 1, len(results.GetFailed())) 445 } 446 447 func Test_FileScanJSON(t *testing.T) { 448 449 results, err := NewScanner(options.ScannerWithPolicyReader(strings.NewReader(`package defsec 450 451 deny[msg] { 452 input.kind == "Pod" 453 msg = "fail" 454 } 455 `))).ScanReader(context.TODO(), "k8s.json", strings.NewReader(` 456 { 457 "kind": "Pod", 458 "apiVersion": "v1", 459 "metadata": { 460 "name": "mongo", 461 "labels": { 462 "name": "mongo", 463 "role": "mongo" 464 } 465 }, 466 "spec": { 467 "volumes": [ 468 { 469 "name": "mongo-disk", 470 "gcePersistentDisk": { 471 "pdName": "mongo-disk", 472 "fsType": "ext4" 473 } 474 } 475 ], 476 "containers": [ 477 { 478 "name": "mongo", 479 "image": "mongo:latest", 480 "ports": [ 481 { 482 "name": "mongo", 483 "containerPort": 27017 484 } 485 ], 486 "volumeMounts": [ 487 { 488 "name": "mongo-disk", 489 "mountPath": "/data/db" 490 } 491 ] 492 } 493 ] 494 } 495 } 496 `)) 497 require.NoError(t, err) 498 499 assert.Equal(t, 1, len(results.GetFailed())) 500 } 501 502 func Test_FileScanWithMetadata(t *testing.T) { 503 504 results, err := NewScanner( 505 options.ScannerWithDebug(os.Stdout), 506 507 options.ScannerWithPolicyReader(strings.NewReader(`package defsec 508 509 deny[msg] { 510 input.kind == "Pod" 511 msg := { 512 "msg": "fail", 513 "startline": 2, 514 "endline": 2, 515 "filepath": "chartname/template/serviceAccount.yaml" 516 } 517 } 518 `))).ScanReader( 519 context.TODO(), 520 "k8s.yaml", 521 strings.NewReader(` 522 apiVersion: v1 523 kind: Pod 524 metadata: 525 name: hello-cpu-limit 526 spec: 527 containers: 528 - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] 529 image: busybox 530 name: hello 531 `)) 532 require.NoError(t, err) 533 534 assert.Greater(t, len(results.GetFailed()), 0) 535 536 firstResult := results.GetFailed()[0] 537 assert.Equal(t, 2, firstResult.Metadata().Range().GetStartLine()) 538 assert.Equal(t, 2, firstResult.Metadata().Range().GetEndLine()) 539 assert.Equal(t, "chartname/template/serviceAccount.yaml", firstResult.Metadata().Range().GetFilename()) 540 } 541 542 func Test_FileScanExampleWithResultFunction(t *testing.T) { 543 544 results, err := NewScanner( 545 options.ScannerWithDebug(os.Stdout), 546 options.ScannerWithTrace(os.Stdout), 547 options.ScannerWithEmbeddedPolicies(true), options.ScannerWithEmbeddedLibraries(true), 548 options.ScannerWithPolicyReader(strings.NewReader(`package defsec 549 550 import data.lib.kubernetes 551 552 default checkCapsDropAll = false 553 554 __rego_metadata__ := { 555 "id": "KSV003", 556 "avd_id": "AVD-KSV-0003", 557 "title": "Default capabilities not dropped", 558 "short_code": "drop-default-capabilities", 559 "version": "v1.0.0", 560 "severity": "LOW", 561 "type": "Kubernetes Security Check", 562 "description": "The container should drop all default capabilities and add only those that are needed for its execution.", 563 "recommended_actions": "Add 'ALL' to containers[].securityContext.capabilities.drop.", 564 "url": "https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/", 565 } 566 567 __rego_input__ := { 568 "combine": false, 569 "selector": [{"type": "kubernetes"}], 570 } 571 572 # Get all containers which include 'ALL' in security.capabilities.drop 573 getCapsDropAllContainers[container] { 574 allContainers := kubernetes.containers[_] 575 lower(allContainers.securityContext.capabilities.drop[_]) == "all" 576 container := allContainers.name 577 } 578 579 # Get all containers which don't include 'ALL' in security.capabilities.drop 580 getCapsNoDropAllContainers[container] { 581 container := kubernetes.containers[_] 582 not getCapsDropAllContainers[container.name] 583 } 584 585 deny[res] { 586 output := getCapsNoDropAllContainers[_] 587 588 msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should add 'ALL' to 'securityContext.capabilities.drop'", [output.name, kubernetes.kind, kubernetes.name])) 589 590 res := result.new(msg, output) 591 } 592 593 `))).ScanReader( 594 context.TODO(), 595 "k8s.yaml", 596 strings.NewReader(` 597 apiVersion: v1 598 kind: Pod 599 metadata: 600 name: hello-cpu-limit 601 spec: 602 containers: 603 - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] 604 image: busybox 605 name: hello 606 securityContext: 607 capabilities: 608 drop: 609 - nothing 610 `)) 611 require.NoError(t, err) 612 613 require.Greater(t, len(results.GetFailed()), 0) 614 615 firstResult := results.GetFailed()[0] 616 assert.Equal(t, 8, firstResult.Metadata().Range().GetStartLine()) 617 assert.Equal(t, 14, firstResult.Metadata().Range().GetEndLine()) 618 assert.Equal(t, "k8s.yaml", firstResult.Metadata().Range().GetFilename()) 619 } 620 621 /* 622 // TODO(simar): Uncomment once all k8s policies have subtype selector added 623 func Test_checkPolicyIsApplicable(t *testing.T) { 624 srcFS := testutil.CreateFS(t, map[string]string{ 625 "policies/pod_policy.rego": `# METADATA 626 # title: "Process can elevate its own privileges" 627 # 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." 628 # scope: package 629 # schemas: 630 # - input: schema["kubernetes"] 631 # related_resources: 632 # - https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 633 # custom: 634 # id: KSV001 635 # avd_id: AVD-KSV-0999 636 # severity: MEDIUM 637 # short_code: no-self-privesc 638 # recommended_action: "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'." 639 # input: 640 # selector: 641 # - type: kubernetes 642 # subtypes: 643 # - kind: Pod 644 package builtin.kubernetes.KSV999 645 646 import data.lib.kubernetes 647 import data.lib.utils 648 649 default checkAllowPrivilegeEscalation = false 650 651 # getNoPrivilegeEscalationContainers returns the names of all containers which have 652 # securityContext.allowPrivilegeEscalation set to false. 653 getNoPrivilegeEscalationContainers[container] { 654 allContainers := kubernetes.containers[_] 655 allContainers.securityContext.allowPrivilegeEscalation == false 656 container := allContainers.name 657 } 658 659 # getPrivilegeEscalationContainers returns the names of all containers which have 660 # securityContext.allowPrivilegeEscalation set to true or not set. 661 getPrivilegeEscalationContainers[container] { 662 containerName := kubernetes.containers[_].name 663 not getNoPrivilegeEscalationContainers[containerName] 664 container := kubernetes.containers[_] 665 } 666 667 deny[res] { 668 output := getPrivilegeEscalationContainers[_] 669 msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'securityContext.allowPrivilegeEscalation' to false", [output.name, kubernetes.kind, kubernetes.name])) 670 res := result.new(msg, output) 671 } 672 673 `, 674 "policies/namespace_policy.rego": `# METADATA 675 # title: "The default namespace should not be used" 676 # description: "ensure that default namespace should not be used" 677 # scope: package 678 # schemas: 679 # - input: schema["kubernetes"] 680 # related_resources: 681 # - https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ 682 # custom: 683 # id: KSV110 684 # avd_id: AVD-KSV-0888 685 # severity: LOW 686 # short_code: default-namespace-should-not-be-used 687 # 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." 688 # input: 689 # selector: 690 # - type: kubernetes 691 # subtypes: 692 # - kind: Namespace 693 package builtin.kubernetes.KSV888 694 695 import data.lib.kubernetes 696 697 default defaultNamespaceInUse = false 698 699 defaultNamespaceInUse { 700 kubernetes.namespace == "default" 701 } 702 703 deny[res] { 704 defaultNamespaceInUse 705 msg := sprintf("%s '%s' should not be set with 'default' namespace", [kubernetes.kind, kubernetes.name]) 706 res := result.new(msg, input.metadata.namespace) 707 } 708 709 `, 710 "test/KSV001/pod.yaml": `apiVersion: v1 711 kind: Pod 712 metadata: 713 name: hello-cpu-limit 714 spec: 715 containers: 716 - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] 717 image: busybox 718 name: hello 719 securityContext: 720 capabilities: 721 drop: 722 - all 723 `, 724 }) 725 726 scanner := NewScanner( 727 //options.ScannerWithEmbeddedPolicies(true), options.ScannerWithEmbeddedLibraries(true), 728 options.ScannerWithEmbeddedLibraries(true), 729 options.ScannerWithPolicyDirs("policies/"), 730 options.ScannerWithPolicyFilesystem(srcFS), 731 ) 732 results, err := scanner.ScanFS(context.TODO(), srcFS, "test/KSV001") 733 require.NoError(t, err) 734 735 require.NoError(t, err) 736 require.Len(t, results.GetFailed(), 1) 737 738 failure := results.GetFailed()[0].Rule() 739 assert.Equal(t, "Process can elevate its own privileges", failure.Summary) 740 } 741 */