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