github.com/aquasecurity/trivy-iac@v0.8.1-0.20240127024015-3d8e412cf0ab/pkg/scanners/terraform/scanner_test.go (about) 1 package terraform 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "strconv" 8 "testing" 9 10 "github.com/aquasecurity/defsec/pkg/providers" 11 "github.com/aquasecurity/defsec/pkg/rules" 12 "github.com/aquasecurity/defsec/pkg/scan" 13 "github.com/aquasecurity/defsec/pkg/scanners/options" 14 "github.com/aquasecurity/defsec/pkg/severity" 15 "github.com/aquasecurity/defsec/pkg/state" 16 "github.com/aquasecurity/defsec/pkg/terraform" 17 "github.com/stretchr/testify/assert" 18 "github.com/stretchr/testify/require" 19 20 "github.com/aquasecurity/trivy-iac/test/testutil" 21 ) 22 23 var alwaysFailRule = scan.Rule{ 24 Provider: providers.AWSProvider, 25 Service: "service", 26 ShortCode: "abc", 27 Severity: severity.High, 28 CustomChecks: scan.CustomChecks{ 29 Terraform: &scan.TerraformCustomCheck{ 30 RequiredTypes: []string{}, 31 RequiredLabels: []string{}, 32 Check: func(resourceBlock *terraform.Block, _ *terraform.Module) (results scan.Results) { 33 results.Add("oh no", resourceBlock) 34 return 35 }, 36 }, 37 }, 38 } 39 40 const emptyBucketRule = ` 41 # METADATA 42 # schemas: 43 # - input: schema.input 44 # custom: 45 # avd_id: AVD-AWS-0001 46 # input: 47 # selector: 48 # - type: cloud 49 # subtypes: 50 # - service: s3 51 # provider: aws 52 package defsec.test.aws1 53 deny[res] { 54 bucket := input.aws.s3.buckets[_] 55 bucket.name.value == "" 56 res := result.new("The name of the bucket must not be empty", bucket) 57 } 58 ` 59 60 func scanWithOptions(t *testing.T, code string, opt ...options.ScannerOption) scan.Results { 61 62 fs := testutil.CreateFS(t, map[string]string{ 63 "project/main.tf": code, 64 }) 65 66 scanner := New(opt...) 67 results, _, err := scanner.ScanFSWithMetrics(context.TODO(), fs, "project") 68 require.NoError(t, err) 69 return results 70 } 71 72 func Test_OptionWithAlternativeIDProvider(t *testing.T) { 73 reg := rules.Register(alwaysFailRule) 74 defer rules.Deregister(reg) 75 76 options := []options.ScannerOption{ 77 ScannerWithAlternativeIDProvider(func(s string) []string { 78 return []string{"something", "altid", "blah"} 79 }), 80 } 81 results := scanWithOptions(t, ` 82 //tfsec:ignore:altid 83 resource "something" "else" {} 84 `, options...) 85 require.Len(t, results.GetFailed(), 0) 86 require.Len(t, results.GetIgnored(), 1) 87 88 } 89 90 func Test_TrivyOptionWithAlternativeIDProvider(t *testing.T) { 91 reg := rules.Register(alwaysFailRule) 92 defer rules.Deregister(reg) 93 94 options := []options.ScannerOption{ 95 ScannerWithAlternativeIDProvider(func(s string) []string { 96 return []string{"something", "altid", "blah"} 97 }), 98 } 99 results := scanWithOptions(t, ` 100 //trivy:ignore:altid 101 resource "something" "else" {} 102 `, options...) 103 require.Len(t, results.GetFailed(), 0) 104 require.Len(t, results.GetIgnored(), 1) 105 106 } 107 108 func Test_OptionWithSeverityOverrides(t *testing.T) { 109 reg := rules.Register(alwaysFailRule) 110 defer rules.Deregister(reg) 111 112 options := []options.ScannerOption{ 113 ScannerWithSeverityOverrides(map[string]string{"aws-service-abc": "LOW"}), 114 } 115 results := scanWithOptions(t, ` 116 resource "something" "else" {} 117 `, options...) 118 require.Len(t, results.GetFailed(), 1) 119 assert.Equal(t, severity.Low, results.GetFailed()[0].Severity()) 120 } 121 122 func Test_OptionWithDebugWriter(t *testing.T) { 123 reg := rules.Register(alwaysFailRule) 124 defer rules.Deregister(reg) 125 126 buffer := bytes.NewBuffer([]byte{}) 127 128 scannerOpts := []options.ScannerOption{ 129 options.ScannerWithDebug(buffer), 130 } 131 _ = scanWithOptions(t, ` 132 resource "something" "else" {} 133 `, scannerOpts...) 134 require.Greater(t, buffer.Len(), 0) 135 } 136 137 func Test_OptionNoIgnores(t *testing.T) { 138 reg := rules.Register(alwaysFailRule) 139 defer rules.Deregister(reg) 140 141 scannerOpts := []options.ScannerOption{ 142 ScannerWithNoIgnores(), 143 } 144 results := scanWithOptions(t, ` 145 //tfsec:ignore:aws-service-abc 146 resource "something" "else" {} 147 `, scannerOpts...) 148 require.Len(t, results.GetFailed(), 1) 149 require.Len(t, results.GetIgnored(), 0) 150 151 } 152 153 func Test_OptionExcludeRules(t *testing.T) { 154 reg := rules.Register(alwaysFailRule) 155 defer rules.Deregister(reg) 156 157 options := []options.ScannerOption{ 158 ScannerWithExcludedRules([]string{"aws-service-abc"}), 159 } 160 results := scanWithOptions(t, ` 161 resource "something" "else" {} 162 `, options...) 163 require.Len(t, results.GetFailed(), 0) 164 require.Len(t, results.GetIgnored(), 1) 165 166 } 167 168 func Test_OptionIncludeRules(t *testing.T) { 169 reg := rules.Register(alwaysFailRule) 170 defer rules.Deregister(reg) 171 172 scannerOpts := []options.ScannerOption{ 173 ScannerWithIncludedRules([]string{"this-only"}), 174 } 175 results := scanWithOptions(t, ` 176 resource "something" "else" {} 177 `, scannerOpts...) 178 require.Len(t, results.GetFailed(), 0) 179 require.Len(t, results.GetIgnored(), 1) 180 181 } 182 183 func Test_OptionWithMinimumSeverity(t *testing.T) { 184 reg := rules.Register(alwaysFailRule) 185 defer rules.Deregister(reg) 186 187 scannerOpts := []options.ScannerOption{ 188 ScannerWithMinimumSeverity(severity.Critical), 189 } 190 results := scanWithOptions(t, ` 191 resource "something" "else" {} 192 `, scannerOpts...) 193 require.Len(t, results.GetFailed(), 0) 194 require.Len(t, results.GetIgnored(), 1) 195 196 } 197 198 func Test_OptionWithPolicyDirs(t *testing.T) { 199 200 fs := testutil.CreateFS(t, map[string]string{ 201 "/code/main.tf": ` 202 resource "aws_s3_bucket" "my-bucket" { 203 bucket = "evil" 204 } 205 `, 206 "/rules/test.rego": ` 207 package defsec.abcdefg 208 209 __rego_metadata__ := { 210 "id": "TEST123", 211 "avd_id": "AVD-TEST-0123", 212 "title": "Buckets should not be evil", 213 "short_code": "no-evil-buckets", 214 "severity": "CRITICAL", 215 "type": "DefSec Security Check", 216 "description": "You should not allow buckets to be evil", 217 "recommended_actions": "Use a good bucket instead", 218 "url": "https://google.com/search?q=is+my+bucket+evil", 219 } 220 221 __rego_input__ := { 222 "combine": false, 223 "selector": [{"type": "defsec", "subtypes": [{"service": "s3", "provider": "aws"}]}], 224 } 225 226 deny[cause] { 227 bucket := input.aws.s3.buckets[_] 228 bucket.name.value == "evil" 229 cause := bucket.name 230 } 231 `, 232 }) 233 234 debugLog := bytes.NewBuffer([]byte{}) 235 scanner := New( 236 options.ScannerWithDebug(debugLog), 237 options.ScannerWithPolicyFilesystem(fs), 238 options.ScannerWithPolicyDirs("rules"), 239 options.ScannerWithRegoOnly(true), 240 ) 241 242 results, err := scanner.ScanFS(context.TODO(), fs, "code") 243 require.NoError(t, err) 244 245 require.Len(t, results.GetFailed(), 1) 246 247 failure := results.GetFailed()[0] 248 249 assert.Equal(t, "AVD-TEST-0123", failure.Rule().AVDID) 250 251 actualCode, err := failure.GetCode() 252 require.NoError(t, err) 253 for i := range actualCode.Lines { 254 actualCode.Lines[i].Highlighted = "" 255 } 256 assert.Equal(t, []scan.Line{ 257 { 258 Number: 2, 259 Content: "resource \"aws_s3_bucket\" \"my-bucket\" {", 260 IsCause: false, 261 FirstCause: false, 262 LastCause: false, 263 Annotation: "", 264 }, 265 { 266 Number: 3, 267 Content: "\tbucket = \"evil\"", 268 IsCause: true, 269 FirstCause: true, 270 LastCause: true, 271 Annotation: "", 272 }, 273 { 274 Number: 4, 275 Content: "}", 276 IsCause: false, 277 FirstCause: false, 278 LastCause: false, 279 Annotation: "", 280 }, 281 }, actualCode.Lines) 282 283 if t.Failed() { 284 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 285 } 286 287 } 288 289 func Test_OptionWithPolicyNamespaces(t *testing.T) { 290 291 tests := []struct { 292 includedNamespaces []string 293 policyNamespace string 294 wantFailure bool 295 }{ 296 { 297 includedNamespaces: nil, 298 policyNamespace: "blah", 299 wantFailure: false, 300 }, 301 { 302 includedNamespaces: nil, 303 policyNamespace: "appshield.something", 304 wantFailure: true, 305 }, 306 { 307 includedNamespaces: nil, 308 policyNamespace: "defsec.blah", 309 wantFailure: true, 310 }, 311 { 312 includedNamespaces: []string{"user"}, 313 policyNamespace: "users", 314 wantFailure: false, 315 }, 316 { 317 includedNamespaces: []string{"users"}, 318 policyNamespace: "something.users", 319 wantFailure: false, 320 }, 321 { 322 includedNamespaces: []string{"users"}, 323 policyNamespace: "users", 324 wantFailure: true, 325 }, 326 { 327 includedNamespaces: []string{"users"}, 328 policyNamespace: "users.my_rule", 329 wantFailure: true, 330 }, 331 { 332 includedNamespaces: []string{"a", "users", "b"}, 333 policyNamespace: "users", 334 wantFailure: true, 335 }, 336 { 337 includedNamespaces: []string{"user"}, 338 policyNamespace: "defsec", 339 wantFailure: true, 340 }, 341 } 342 343 for i, test := range tests { 344 345 t.Run(strconv.Itoa(i), func(t *testing.T) { 346 347 fs := testutil.CreateFS(t, map[string]string{ 348 "/code/main.tf": ` 349 resource "aws_s3_bucket" "my-bucket" { 350 bucket = "evil" 351 } 352 `, 353 "/rules/test.rego": fmt.Sprintf(` 354 # METADATA 355 # custom: 356 # input: 357 # selector: 358 # - type: cloud 359 # subtypes: 360 # - service: s3 361 # provider: aws 362 package %s 363 364 deny[cause] { 365 bucket := input.aws.s3.buckets[_] 366 bucket.name.value == "evil" 367 cause := bucket.name 368 } 369 370 `, test.policyNamespace), 371 }) 372 373 scanner := New( 374 options.ScannerWithPolicyDirs("rules"), 375 options.ScannerWithPolicyNamespaces(test.includedNamespaces...), 376 ) 377 378 results, _, err := scanner.ScanFSWithMetrics(context.TODO(), fs, "code") 379 require.NoError(t, err) 380 381 var found bool 382 for _, result := range results.GetFailed() { 383 if result.RegoNamespace() == test.policyNamespace && result.RegoRule() == "deny" { 384 found = true 385 break 386 } 387 } 388 assert.Equal(t, test.wantFailure, found) 389 390 }) 391 } 392 393 } 394 395 func Test_OptionWithStateFunc(t *testing.T) { 396 397 fs := testutil.CreateFS(t, map[string]string{ 398 "code/main.tf": ` 399 resource "aws_s3_bucket" "my-bucket" { 400 bucket = "evil" 401 } 402 `, 403 }) 404 405 var actual state.State 406 407 debugLog := bytes.NewBuffer([]byte{}) 408 scanner := New( 409 options.ScannerWithDebug(debugLog), 410 ScannerWithStateFunc(func(s *state.State) { 411 require.NotNil(t, s) 412 actual = *s 413 }), 414 ) 415 416 _, _, err := scanner.ScanFSWithMetrics(context.TODO(), fs, "code") 417 require.NoError(t, err) 418 419 assert.Equal(t, 1, len(actual.AWS.S3.Buckets)) 420 421 if t.Failed() { 422 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 423 } 424 425 } 426 427 func Test_OptionWithRegoOnly(t *testing.T) { 428 429 fs := testutil.CreateFS(t, map[string]string{ 430 "/code/main.tf": ` 431 resource "aws_s3_bucket" "my-bucket" { 432 bucket = "evil" 433 } 434 `, 435 "/rules/test.rego": ` 436 package defsec.abcdefg 437 438 __rego_metadata__ := { 439 "id": "TEST123", 440 "avd_id": "AVD-TEST-0123", 441 "title": "Buckets should not be evil", 442 "short_code": "no-evil-buckets", 443 "severity": "CRITICAL", 444 "type": "DefSec Security Check", 445 "description": "You should not allow buckets to be evil", 446 "recommended_actions": "Use a good bucket instead", 447 "url": "https://google.com/search?q=is+my+bucket+evil", 448 } 449 450 __rego_input__ := { 451 "combine": false, 452 "selector": [{"type": "defsec", "subtypes": [{"service": "s3", "provider": "aws"}]}], 453 } 454 455 deny[cause] { 456 bucket := input.aws.s3.buckets[_] 457 bucket.name.value == "evil" 458 cause := bucket.name 459 } 460 `, 461 }) 462 463 debugLog := bytes.NewBuffer([]byte{}) 464 scanner := New( 465 options.ScannerWithDebug(debugLog), 466 options.ScannerWithPolicyDirs("rules"), 467 options.ScannerWithRegoOnly(true), 468 ) 469 470 results, err := scanner.ScanFS(context.TODO(), fs, "code") 471 require.NoError(t, err) 472 473 require.Len(t, results.GetFailed(), 1) 474 assert.Equal(t, "AVD-TEST-0123", results[0].Rule().AVDID) 475 476 if t.Failed() { 477 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 478 } 479 } 480 481 func Test_OptionWithRegoOnly_CodeHighlighting(t *testing.T) { 482 483 fs := testutil.CreateFS(t, map[string]string{ 484 "/code/main.tf": ` 485 resource "aws_s3_bucket" "my-bucket" { 486 bucket = "evil" 487 } 488 `, 489 "/rules/test.rego": ` 490 package defsec.abcdefg 491 492 __rego_metadata__ := { 493 "id": "TEST123", 494 "avd_id": "AVD-TEST-0123", 495 "title": "Buckets should not be evil", 496 "short_code": "no-evil-buckets", 497 "severity": "CRITICAL", 498 "type": "DefSec Security Check", 499 "description": "You should not allow buckets to be evil", 500 "recommended_actions": "Use a good bucket instead", 501 "url": "https://google.com/search?q=is+my+bucket+evil", 502 } 503 504 __rego_input__ := { 505 "combine": false, 506 "selector": [{"type": "defsec", "subtypes": [{"service": "s3", "provider": "aws"}]}], 507 } 508 509 deny[res] { 510 bucket := input.aws.s3.buckets[_] 511 bucket.name.value == "evil" 512 res := result.new("oh no", bucket.name) 513 } 514 `, 515 }) 516 517 debugLog := bytes.NewBuffer([]byte{}) 518 scanner := New( 519 options.ScannerWithDebug(debugLog), 520 options.ScannerWithPolicyDirs("rules"), 521 options.ScannerWithRegoOnly(true), 522 options.ScannerWithEmbeddedLibraries(true), 523 ) 524 525 results, err := scanner.ScanFS(context.TODO(), fs, "code") 526 require.NoError(t, err) 527 528 require.Len(t, results.GetFailed(), 1) 529 assert.Equal(t, "AVD-TEST-0123", results[0].Rule().AVDID) 530 assert.NotNil(t, results[0].Metadata().Range().GetFS()) 531 532 if t.Failed() { 533 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 534 } 535 } 536 537 func Test_IAMPolicyRego(t *testing.T) { 538 fs := testutil.CreateFS(t, map[string]string{ 539 "/code/main.tf": ` 540 resource "aws_sqs_queue_policy" "bad_example" { 541 queue_url = aws_sqs_queue.q.id 542 543 policy = <<POLICY 544 { 545 "Statement": [ 546 { 547 "Effect": "Allow", 548 "Principal": "*", 549 "Action": "*" 550 } 551 ] 552 } 553 POLICY 554 }`, 555 "/rules/test.rego": ` 556 # METADATA 557 # title: Buckets should not be evil 558 # description: You should not allow buckets to be evil 559 # scope: package 560 # schemas: 561 # - input: schema.input 562 # related_resources: 563 # - https://google.com/search?q=is+my+bucket+evil 564 # custom: 565 # id: TEST123 566 # avd_id: AVD-TEST-0123 567 # short_code: no-evil-buckets 568 # severity: CRITICAL 569 # recommended_action: Use a good bucket instead 570 # input: 571 # selector: 572 # - type: cloud 573 # subtypes: 574 # - service: sqs 575 # provider: aws 576 package defsec.abcdefg 577 578 579 deny[res] { 580 queue := input.aws.sqs.queues[_] 581 policy := queue.policies[_] 582 doc := json.unmarshal(policy.document.value) 583 statement = doc.Statement[_] 584 action := statement.Action[_] 585 action == "*" 586 res := result.new("SQS Policy contains wildcard in action", policy.document) 587 } 588 `, 589 }) 590 591 debugLog := bytes.NewBuffer([]byte{}) 592 scanner := New( 593 options.ScannerWithDebug(debugLog), 594 options.ScannerWithTrace(debugLog), 595 options.ScannerWithPolicyDirs("rules"), 596 options.ScannerWithRegoOnly(true), 597 options.ScannerWithEmbeddedLibraries(true), 598 ) 599 600 defer func() { 601 if t.Failed() { 602 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 603 } 604 }() 605 606 results, err := scanner.ScanFS(context.TODO(), fs, "code") 607 require.NoError(t, err) 608 609 require.Len(t, results.GetFailed(), 1) 610 assert.Equal(t, "AVD-TEST-0123", results[0].Rule().AVDID) 611 assert.NotNil(t, results[0].Metadata().Range().GetFS()) 612 613 } 614 615 func Test_ContainerDefinitionRego(t *testing.T) { 616 fs := testutil.CreateFS(t, map[string]string{ 617 "/code/main.tf": ` 618 resource "aws_ecs_task_definition" "test" { 619 family = "test" 620 container_definitions = <<TASK_DEFINITION 621 [ 622 { 623 "privileged": true, 624 "cpu": 10, 625 "command": ["sleep", "10"], 626 "entryPoint": ["/"], 627 "environment": [ 628 {"name": "VARNAME", "value": "VARVAL"} 629 ], 630 "essential": true, 631 "image": "jenkins", 632 "memory": 128, 633 "name": "jenkins", 634 "portMappings": [ 635 { 636 "containerPort": 80, 637 "hostPort": 8080 638 } 639 ], 640 "resourceRequirements":[ 641 { 642 "type":"InferenceAccelerator", 643 "value":"device_1" 644 } 645 ] 646 } 647 ] 648 TASK_DEFINITION 649 650 inference_accelerator { 651 device_name = "device_1" 652 device_type = "eia1.medium" 653 } 654 }`, 655 "/rules/test.rego": ` 656 package defsec.abcdefg 657 658 659 __rego_metadata__ := { 660 "id": "TEST123", 661 "avd_id": "AVD-TEST-0123", 662 "title": "Buckets should not be evil", 663 "short_code": "no-evil-buckets", 664 "severity": "CRITICAL", 665 "type": "DefSec Security Check", 666 "description": "You should not allow buckets to be evil", 667 "recommended_actions": "Use a good bucket instead", 668 "url": "https://google.com/search?q=is+my+bucket+evil", 669 } 670 671 __rego_input__ := { 672 "combine": false, 673 "selector": [{"type": "defsec", "subtypes": [{"service": "ecs", "provider": "aws"}]}], 674 } 675 676 deny[res] { 677 definition := input.aws.ecs.taskdefinitions[_].containerdefinitions[_] 678 definition.privileged.value == true 679 res := result.new("Privileged container detected", definition.privileged) 680 } 681 `, 682 }) 683 684 debugLog := bytes.NewBuffer([]byte{}) 685 scanner := New( 686 options.ScannerWithDebug(debugLog), 687 options.ScannerWithPolicyDirs("rules"), 688 options.ScannerWithRegoOnly(true), 689 options.ScannerWithEmbeddedLibraries(true), 690 ) 691 692 results, err := scanner.ScanFS(context.TODO(), fs, "code") 693 require.NoError(t, err) 694 695 require.Len(t, results.GetFailed(), 1) 696 assert.Equal(t, "AVD-TEST-0123", results[0].Rule().AVDID) 697 assert.NotNil(t, results[0].Metadata().Range().GetFS()) 698 699 if t.Failed() { 700 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 701 } 702 } 703 704 func Test_S3_Linking(t *testing.T) { 705 706 code := ` 707 ## tfsec:ignore:aws-s3-enable-bucket-encryption 708 ## tfsec:ignore:aws-s3-enable-bucket-logging 709 ## tfsec:ignore:aws-s3-enable-versioning 710 resource "aws_s3_bucket" "blubb" { 711 bucket = "test" 712 } 713 714 resource "aws_s3_bucket_public_access_block" "audit_logs_athena" { 715 bucket = aws_s3_bucket.blubb.id 716 717 block_public_acls = true 718 block_public_policy = true 719 ignore_public_acls = true 720 restrict_public_buckets = true 721 } 722 723 # tfsec:ignore:aws-s3-enable-bucket-encryption 724 # tfsec:ignore:aws-s3-enable-bucket-logging 725 # tfsec:ignore:aws-s3-enable-versioning 726 resource "aws_s3_bucket" "foo" { 727 bucket = "prefix-" # remove this variable and it works; does not report 728 force_destroy = true 729 } 730 731 resource "aws_s3_bucket_public_access_block" "foo" { 732 bucket = aws_s3_bucket.foo.id 733 734 block_public_acls = true 735 block_public_policy = true 736 ignore_public_acls = true 737 restrict_public_buckets = true 738 } 739 740 ` 741 742 fs := testutil.CreateFS(t, map[string]string{ 743 "code/main.tf": code, 744 }) 745 746 debugLog := bytes.NewBuffer([]byte{}) 747 scanner := New( 748 options.ScannerWithDebug(debugLog), 749 ) 750 751 results, err := scanner.ScanFS(context.TODO(), fs, "code") 752 require.NoError(t, err) 753 754 failed := results.GetFailed() 755 for _, result := range failed { 756 // public access block 757 assert.NotEqual(t, "AVD-AWS-0094", result.Rule().AVDID, "AVD-AWS-0094 should not be reported - was found at "+result.Metadata().Range().String()) 758 // encryption 759 assert.NotEqual(t, "AVD-AWS-0088", result.Rule().AVDID) 760 // logging 761 assert.NotEqual(t, "AVD-AWS-0089", result.Rule().AVDID) 762 // versioning 763 assert.NotEqual(t, "AVD-AWS-0090", result.Rule().AVDID) 764 } 765 766 if t.Failed() { 767 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 768 } 769 } 770 771 func Test_S3_Linking_PublicAccess(t *testing.T) { 772 773 code := ` 774 resource "aws_s3_bucket" "testA" { 775 bucket = "com.test.testA" 776 } 777 778 resource "aws_s3_bucket_acl" "testA" { 779 bucket = aws_s3_bucket.testA.id 780 acl = "private" 781 } 782 783 resource "aws_s3_bucket_public_access_block" "testA" { 784 bucket = aws_s3_bucket.testA.id 785 786 block_public_acls = true 787 block_public_policy = true 788 ignore_public_acls = true 789 restrict_public_buckets = true 790 } 791 792 resource "aws_s3_bucket" "testB" { 793 bucket = "com.test.testB" 794 } 795 796 resource "aws_s3_bucket_acl" "testB" { 797 bucket = aws_s3_bucket.testB.id 798 acl = "private" 799 } 800 801 resource "aws_s3_bucket_public_access_block" "testB" { 802 bucket = aws_s3_bucket.testB.id 803 804 block_public_acls = true 805 block_public_policy = true 806 ignore_public_acls = true 807 restrict_public_buckets = true 808 } 809 810 ` 811 812 fs := testutil.CreateFS(t, map[string]string{ 813 "code/main.tf": code, 814 }) 815 816 debugLog := bytes.NewBuffer([]byte{}) 817 scanner := New( 818 options.ScannerWithDebug(debugLog), 819 ) 820 821 results, err := scanner.ScanFS(context.TODO(), fs, "code") 822 require.NoError(t, err) 823 824 for _, result := range results.GetFailed() { 825 // public access block 826 assert.NotEqual(t, "AVD-AWS-0094", result.Rule().AVDID) 827 } 828 829 if t.Failed() { 830 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 831 } 832 } 833 834 func Test_RegoInput(t *testing.T) { 835 836 var regoInput interface{} 837 838 opts := []options.ScannerOption{ 839 ScannerWithStateFunc(func(s *state.State) { 840 regoInput = s.ToRego() 841 }), 842 } 843 _ = scanWithOptions(t, ` 844 resource "aws_security_group" "example_security_group" { 845 name = "example_security_group" 846 847 description = "Example SG" 848 849 ingress { 850 description = "Allow SSH" 851 from_port = 22 852 to_port = 22 853 protocol = "tcp" 854 cidr_blocks = ["1.2.3.4", "5.6.7.8"] 855 } 856 857 } 858 `, opts...) 859 860 outer, ok := regoInput.(map[string]interface{}) 861 require.True(t, ok) 862 aws, ok := outer["aws"].(map[string]interface{}) 863 require.True(t, ok) 864 ec2, ok := aws["ec2"].(map[string]interface{}) 865 require.True(t, ok) 866 sgs, ok := ec2["securitygroups"].([]interface{}) 867 require.True(t, ok) 868 require.Len(t, sgs, 1) 869 sg0, ok := sgs[0].(map[string]interface{}) 870 require.True(t, ok) 871 ingress, ok := sg0["ingressrules"].([]interface{}) 872 require.True(t, ok) 873 require.Len(t, ingress, 1) 874 ingress0, ok := ingress[0].(map[string]interface{}) 875 require.True(t, ok) 876 cidrs, ok := ingress0["cidrs"].([]interface{}) 877 require.True(t, ok) 878 require.Len(t, cidrs, 2) 879 880 cidr0, ok := cidrs[0].(map[string]interface{}) 881 require.True(t, ok) 882 883 cidr1, ok := cidrs[1].(map[string]interface{}) 884 require.True(t, ok) 885 886 assert.Equal(t, "1.2.3.4", cidr0["value"]) 887 assert.Equal(t, "5.6.7.8", cidr1["value"]) 888 } 889 890 // PoC for replacing Go with Rego: AVD-AWS-0001 891 func Test_RegoRules(t *testing.T) { 892 893 fs := testutil.CreateFS(t, map[string]string{ 894 "/code/main.tf": ` 895 resource "aws_apigatewayv2_stage" "bad_example" { 896 api_id = aws_apigatewayv2_api.example.id 897 name = "example-stage" 898 } 899 `, 900 "/rules/test.rego": `# METADATA 901 # schemas: 902 # - input: schema.input 903 # custom: 904 # avd_id: AVD-AWS-0001 905 # input: 906 # selector: 907 # - type: cloud 908 # subtypes: 909 # - service: apigateway 910 # provider: aws 911 package builtin.cloud.AWS0001 912 913 deny[res] { 914 api := input.aws.apigateway.v1.apis[_] 915 stage := api.stages[_] 916 isManaged(stage) 917 stage.accesslogging.cloudwatchloggrouparn.value == "" 918 res := result.new("Access logging is not configured.", stage.accesslogging.cloudwatchloggrouparn) 919 } 920 921 deny[res] { 922 api := input.aws.apigateway.v2.apis[_] 923 stage := api.stages[_] 924 isManaged(stage) 925 stage.accesslogging.cloudwatchloggrouparn.value == "" 926 res := result.new("Access logging is not configured.", stage.accesslogging.cloudwatchloggrouparn) 927 } 928 `, 929 }) 930 931 debugLog := bytes.NewBuffer([]byte{}) 932 scanner := New( 933 options.ScannerWithDebug(debugLog), 934 options.ScannerWithPolicyFilesystem(fs), 935 options.ScannerWithPolicyDirs("rules"), 936 options.ScannerWithRegoOnly(true), 937 ) 938 939 results, err := scanner.ScanFS(context.TODO(), fs, "code") 940 require.NoError(t, err) 941 942 require.Len(t, results.GetFailed(), 1) 943 944 failure := results.GetFailed()[0] 945 946 assert.Equal(t, "AVD-AWS-0001", failure.Rule().AVDID) 947 948 actualCode, err := failure.GetCode() 949 require.NoError(t, err) 950 for i := range actualCode.Lines { 951 actualCode.Lines[i].Highlighted = "" 952 } 953 assert.Equal(t, []scan.Line{ 954 { 955 Number: 2, 956 Content: "resource \"aws_apigatewayv2_stage\" \"bad_example\" {", 957 IsCause: true, 958 FirstCause: true, 959 LastCause: false, 960 Annotation: "", 961 }, 962 { 963 Number: 3, 964 Content: " api_id = aws_apigatewayv2_api.example.id", 965 IsCause: true, 966 FirstCause: false, 967 LastCause: false, 968 Annotation: "", 969 }, 970 { 971 Number: 4, 972 Content: " name = \"example-stage\"", 973 IsCause: true, 974 FirstCause: false, 975 LastCause: false, 976 Annotation: "", 977 }, 978 { 979 Number: 5, 980 Content: "}", 981 IsCause: true, 982 FirstCause: false, 983 LastCause: true, 984 Annotation: "", 985 }, 986 }, actualCode.Lines) 987 988 if t.Failed() { 989 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 990 } 991 992 } 993 994 func Test_OptionWithConfigsFileSystem(t *testing.T) { 995 fs := testutil.CreateFS(t, map[string]string{ 996 "code/main.tf": ` 997 variable "bucket_name" { 998 type = string 999 } 1000 resource "aws_s3_bucket" "main" { 1001 bucket = var.bucket_name 1002 } 1003 `, 1004 "rules/bucket_name.rego": emptyBucketRule, 1005 }) 1006 1007 configsFS := testutil.CreateFS(t, map[string]string{ 1008 "main.tfvars": ` 1009 bucket_name = "test" 1010 `, 1011 }) 1012 1013 debugLog := bytes.NewBuffer([]byte{}) 1014 scanner := New( 1015 options.ScannerWithDebug(debugLog), 1016 options.ScannerWithPolicyDirs("rules"), 1017 options.ScannerWithPolicyFilesystem(fs), 1018 options.ScannerWithRegoOnly(true), 1019 options.ScannerWithEmbeddedLibraries(false), 1020 options.ScannerWithEmbeddedPolicies(false), 1021 ScannerWithAllDirectories(true), 1022 ScannerWithTFVarsPaths("main.tfvars"), 1023 ScannerWithConfigsFileSystem(configsFS), 1024 ) 1025 1026 results, err := scanner.ScanFS(context.TODO(), fs, "code") 1027 require.NoError(t, err) 1028 1029 assert.Len(t, results, 1) 1030 assert.Len(t, results.GetPassed(), 1) 1031 1032 if t.Failed() { 1033 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 1034 } 1035 } 1036 1037 func Test_OptionWithConfigsFileSystem_ConfigInCode(t *testing.T) { 1038 fs := testutil.CreateFS(t, map[string]string{ 1039 "code/main.tf": ` 1040 variable "bucket_name" { 1041 type = string 1042 } 1043 resource "aws_s3_bucket" "main" { 1044 bucket = var.bucket_name 1045 } 1046 `, 1047 "rules/bucket_name.rego": emptyBucketRule, 1048 "main.tfvars": ` 1049 bucket_name = "test" 1050 `, 1051 }) 1052 1053 debugLog := bytes.NewBuffer([]byte{}) 1054 scanner := New( 1055 options.ScannerWithDebug(debugLog), 1056 options.ScannerWithPolicyDirs("rules"), 1057 options.ScannerWithPolicyFilesystem(fs), 1058 options.ScannerWithRegoOnly(true), 1059 options.ScannerWithEmbeddedLibraries(false), 1060 options.ScannerWithEmbeddedPolicies(false), 1061 ScannerWithAllDirectories(true), 1062 ScannerWithTFVarsPaths("main.tfvars"), 1063 ScannerWithConfigsFileSystem(fs), 1064 ) 1065 1066 results, err := scanner.ScanFS(context.TODO(), fs, "code") 1067 require.NoError(t, err) 1068 1069 assert.Len(t, results, 1) 1070 assert.Len(t, results.GetPassed(), 1) 1071 1072 if t.Failed() { 1073 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 1074 } 1075 } 1076 1077 func Test_DoNotScanNonRootModules(t *testing.T) { 1078 fs := testutil.CreateFS(t, map[string]string{ 1079 "/code/app1/main.tf": ` 1080 module "s3" { 1081 source = "./modules/s3" 1082 bucket_name = "test" 1083 } 1084 `, 1085 "/code/app1/modules/s3/main.tf": ` 1086 variable "bucket_name" { 1087 type = string 1088 } 1089 1090 resource "aws_s3_bucket" "main" { 1091 bucket = var.bucket_name 1092 } 1093 `, 1094 "/code/app1/app2/main.tf": ` 1095 module "s3" { 1096 source = "../modules/s3" 1097 bucket_name = "test" 1098 } 1099 1100 module "ec2" { 1101 source = "./modules/ec2" 1102 } 1103 `, 1104 "/code/app1/app2/modules/ec2/main.tf": ` 1105 variable "security_group_description" { 1106 type = string 1107 } 1108 resource "aws_security_group" "main" { 1109 description = var.security_group_description 1110 } 1111 `, 1112 "/rules/bucket_name.rego": ` 1113 # METADATA 1114 # schemas: 1115 # - input: schema.input 1116 # custom: 1117 # avd_id: AVD-AWS-0001 1118 # input: 1119 # selector: 1120 # - type: cloud 1121 # subtypes: 1122 # - service: s3 1123 # provider: aws 1124 package defsec.test.aws1 1125 deny[res] { 1126 bucket := input.aws.s3.buckets[_] 1127 bucket.name.value == "" 1128 res := result.new("The name of the bucket must not be empty", bucket) 1129 } 1130 `, 1131 "/rules/sec_group_description.rego": ` 1132 # METADATA 1133 # schemas: 1134 # - input: schema.input 1135 # custom: 1136 # avd_id: AVD-AWS-0002 1137 # input: 1138 # selector: 1139 # - type: cloud 1140 # subtypes: 1141 # - service: ec2 1142 # provider: aws 1143 package defsec.test.aws2 1144 deny[res] { 1145 group := input.aws.ec2.securitygroups[_] 1146 group.description.value == "" 1147 res := result.new("The description of the security group must not be empty", group) 1148 } 1149 `, 1150 }) 1151 1152 debugLog := bytes.NewBuffer([]byte{}) 1153 scanner := New( 1154 options.ScannerWithDebug(debugLog), 1155 options.ScannerWithPolicyFilesystem(fs), 1156 options.ScannerWithPolicyDirs("rules"), 1157 options.ScannerWithEmbeddedPolicies(false), 1158 options.ScannerWithEmbeddedLibraries(false), 1159 options.ScannerWithRegoOnly(true), 1160 ScannerWithAllDirectories(true), 1161 ) 1162 1163 results, err := scanner.ScanFS(context.TODO(), fs, "code") 1164 require.NoError(t, err) 1165 1166 assert.Len(t, results.GetPassed(), 2) 1167 require.Len(t, results.GetFailed(), 1) 1168 assert.Equal(t, "AVD-AWS-0002", results.GetFailed()[0].Rule().AVDID) 1169 1170 if t.Failed() { 1171 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 1172 } 1173 } 1174 1175 func Test_RoleRefToOutput(t *testing.T) { 1176 fs := testutil.CreateFS(t, map[string]string{ 1177 "code/main.tf": ` 1178 module "this" { 1179 source = "./modules/iam" 1180 } 1181 1182 resource "aws_iam_role_policy" "bad-policy" { 1183 name = "bad-policy" 1184 role = module.this.role_name 1185 policy = jsonencode({ 1186 Version = "2012-10-17", 1187 Statement = [ 1188 { 1189 Effect = "Allow" 1190 Action = "*" 1191 Resource = "*" 1192 }, 1193 ] 1194 }) 1195 } 1196 `, 1197 "code/modules/iam/main.tf": ` 1198 resource "aws_iam_role" "example" { 1199 name = "example" 1200 assume_role_policy = jsonencode({}) 1201 } 1202 1203 output "role_name" { 1204 value = aws_iam_role.example.id 1205 } 1206 `, 1207 "rules/test.rego": ` 1208 # METADATA 1209 # schemas: 1210 # - input: schema.input 1211 # custom: 1212 # avd_id: AVD-AWS-0001 1213 # input: 1214 # selector: 1215 # - type: cloud 1216 # subtypes: 1217 # - service: iam 1218 # provider: aws 1219 package defsec.test.aws1 1220 deny[res] { 1221 policy := input.aws.iam.roles[_].policies[_] 1222 policy.name.value == "bad-policy" 1223 res := result.new("Deny!", policy) 1224 } 1225 `, 1226 }) 1227 1228 debugLog := bytes.NewBuffer([]byte{}) 1229 scanner := New( 1230 options.ScannerWithDebug(debugLog), 1231 options.ScannerWithPolicyDirs("rules"), 1232 options.ScannerWithPolicyFilesystem(fs), 1233 options.ScannerWithRegoOnly(true), 1234 options.ScannerWithEmbeddedLibraries(false), 1235 options.ScannerWithEmbeddedPolicies(false), 1236 ScannerWithAllDirectories(true), 1237 ) 1238 1239 results, err := scanner.ScanFS(context.TODO(), fs, "code") 1240 require.NoError(t, err) 1241 1242 assert.Len(t, results, 1) 1243 assert.Len(t, results.GetFailed(), 1) 1244 } 1245 1246 func Test_RegoRefToAwsProviderAttributes(t *testing.T) { 1247 fs := testutil.CreateFS(t, map[string]string{ 1248 "code/providers.tf": ` 1249 provider "aws" { 1250 region = "us-east-2" 1251 default_tags { 1252 tags = { 1253 Environment = "Local" 1254 Name = "LocalStack" 1255 } 1256 } 1257 } 1258 `, 1259 "rules/region.rego": ` 1260 # METADATA 1261 # schemas: 1262 # - input: schema.input 1263 # custom: 1264 # avd_id: AVD-AWS-0001 1265 # input: 1266 # selector: 1267 # - type: cloud 1268 # subtypes: 1269 # - service: meta 1270 # provider: aws 1271 package defsec.test.aws1 1272 deny[res] { 1273 region := input.aws.meta.tfproviders[_].region 1274 region.value != "us-east-1" 1275 res := result.new("Only the 'us-east-1' region is allowed!", region) 1276 } 1277 `, 1278 "rules/tags.rego": ` 1279 # METADATA 1280 # schemas: 1281 # - input: schema.input 1282 # custom: 1283 # avd_id: AVD-AWS-0002 1284 # input: 1285 # selector: 1286 # - type: cloud 1287 # subtypes: 1288 # - service: meta 1289 # provider: aws 1290 package defsec.test.aws2 1291 deny[res] { 1292 provider := input.aws.meta.tfproviders[_] 1293 tags = provider.defaulttags.tags.value 1294 not tags.Environment 1295 res := result.new("provider should have the following default tags: 'Environment'", tags) 1296 }`, 1297 }) 1298 1299 debugLog := bytes.NewBuffer([]byte{}) 1300 scanner := New( 1301 options.ScannerWithDebug(debugLog), 1302 options.ScannerWithPolicyDirs("rules"), 1303 options.ScannerWithPolicyFilesystem(fs), 1304 options.ScannerWithRegoOnly(true), 1305 options.ScannerWithEmbeddedLibraries(false), 1306 options.ScannerWithEmbeddedPolicies(false), 1307 ScannerWithAllDirectories(true), 1308 ) 1309 1310 results, err := scanner.ScanFS(context.TODO(), fs, "code") 1311 require.NoError(t, err) 1312 1313 require.Len(t, results, 2) 1314 1315 require.Len(t, results.GetFailed(), 1) 1316 assert.Equal(t, "AVD-AWS-0001", results.GetFailed()[0].Rule().AVDID) 1317 1318 require.Len(t, results.GetPassed(), 1) 1319 assert.Equal(t, "AVD-AWS-0002", results.GetPassed()[0].Rule().AVDID) 1320 1321 if t.Failed() { 1322 fmt.Printf("Debug logs:\n%s\n", debugLog.String()) 1323 } 1324 }