diff --git a/assets/libraries/common.rego b/assets/libraries/common.rego index a6c663a4865..fda7a83e193 100644 --- a/assets/libraries/common.rego +++ b/assets/libraries/common.rego @@ -338,13 +338,15 @@ find_selector_by_value(filter, str) = rtn { get_tag_name_if_exists(resource) = name { name := resource.tags.Name } else = name { - name := "" + name := "unknown" } get_encryption_if_exists(resource) = encryption { - encryption := resource.encrypted + resource.encrypted == true + encryption := "encrypted" } else = encryption { - valid_key(resource.encryption_info, "encryption_at_rest_kms_key_arn") + options := {"encryption_at_rest_kms_key_arn", "encryption_in_transit"} + valid_key(resource.encryption_info, options[_]) encryption := "encrypted" } else = encryption { fields := {"sqs_managed_sse_enabled", "kms_master_key_id", "encryption_options", "server_side_encryption_configuration"} @@ -745,8 +747,18 @@ remove_last_point(searchKey) = sk { sk := searchKey } -# This function is based on this docs(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-optimized.html#describe-ebs-optimization) +# if accessibility is "hasPolicy", bom_output should also display the policy content +get_bom_output(bom_output, policy) = output { + bom_output.resource_accessibility == "hasPolicy" + + out := {"policy": policy} + + output := object.union(bom_output, out) +} else = output { + output := bom_output +} +# This function is based on this docs(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-optimized.html#describe-ebs-optimization) is_aws_ebs_optimized_by_default(instanceType) { inArray(data.common_lib.aws_ebs_optimized_by_default, instanceType) } diff --git a/assets/libraries/terraform.rego b/assets/libraries/terraform.rego index 374f0ba378a..af5695b4dd4 100644 --- a/assets/libraries/terraform.rego +++ b/assets/libraries/terraform.rego @@ -454,23 +454,7 @@ is_publicly_accessible(policy) { get_accessibility(resource, name, resourcePolicyName, resourceTarget) = info { policy := common_lib.json_unmarshal(resource.policy) - is_publicly_accessible(policy) - info = {"accessibility": "public", "policy": policy} -} else = info { - policy := common_lib.json_unmarshal(resource.policy) - not is_publicly_accessible(policy) - - info = {"accessibility": "restrict", "policy": policy} -} else = info { - not common_lib.valid_key(resource, "policy") - - resourcePolicy := input.document[_].resource[resourcePolicyName][_] - split(resourcePolicy[resourceTarget], ".")[1] == name - - policy := common_lib.json_unmarshal(resourcePolicy.policy) - is_publicly_accessible(policy) - - info = {"accessibility": "public", "policy": policy} + info = {"accessibility": "hasPolicy", "policy": policy} } else = info { not common_lib.valid_key(resource, "policy") @@ -478,8 +462,7 @@ get_accessibility(resource, name, resourcePolicyName, resourceTarget) = info { split(resourcePolicy[resourceTarget], ".")[1] == name policy := common_lib.json_unmarshal(resourcePolicy.policy) - not is_publicly_accessible(policy) - info = {"accessibility": "restrict", "policy": policy} + info = {"accessibility": "hasPolicy", "policy": policy} } else = info { info = {"accessibility": "unknown", "policy": ""} } diff --git a/assets/queries/terraform/aws_bom/ebs/test/positive2.tf b/assets/queries/terraform/aws_bom/ebs/test/positive2.tf new file mode 100644 index 00000000000..8dd549ff88f --- /dev/null +++ b/assets/queries/terraform/aws_bom/ebs/test/positive2.tf @@ -0,0 +1,10 @@ +resource "aws_ebs_volume" "positive2" { + availability_zone = "us-west-2a" + size = 40 + + tags = { + Name = "HelloWorld2" + } + + encrypted = true +} diff --git a/assets/queries/terraform/aws_bom/ebs/test/positive_expected_result.json b/assets/queries/terraform/aws_bom/ebs/test/positive_expected_result.json index a4aa83ee6cb..d8e95f01d7b 100644 --- a/assets/queries/terraform/aws_bom/ebs/test/positive_expected_result.json +++ b/assets/queries/terraform/aws_bom/ebs/test/positive_expected_result.json @@ -4,5 +4,11 @@ "severity": "TRACE", "line": 1, "fileName": "positive1.tf" + }, + { + "queryName": "BOM - AWS EBS", + "severity": "TRACE", + "line": 1, + "fileName": "positive2.tf" } ] diff --git a/assets/queries/terraform/aws_bom/efs/query.rego b/assets/queries/terraform/aws_bom/efs/query.rego index f9e0c95f194..f9e07ea289c 100644 --- a/assets/queries/terraform/aws_bom/efs/query.rego +++ b/assets/queries/terraform/aws_bom/efs/query.rego @@ -11,13 +11,14 @@ CxPolicy[result] { bom_output = { "resource_type": "aws_efs_file_system", "resource_name": common_lib.get_tag_name_if_exists(efs_file_system), - "resource_accessibility": info.accessibility, + "resource_accessibility": info.accessibility, "resource_encryption": common_lib.get_encryption_if_exists(efs_file_system), "resource_vendor": "AWS", "resource_category": "Storage", - "policy": info.policy, } + final_bom_output := common_lib.get_bom_output(bom_output, info.policy) + result := { "documentId": input.document[i].id, "searchKey": sprintf("aws_efs_file_system[%s]", [name]), @@ -25,6 +26,6 @@ CxPolicy[result] { "keyExpectedValue": "", "keyActualValue": "", "searchLine": common_lib.build_search_line(["resource", "aws_efs_file_system", name], []), - "value": json.marshal(bom_output), + "value": json.marshal(final_bom_output), } } diff --git a/assets/queries/terraform/aws_bom/elasticache/query.rego b/assets/queries/terraform/aws_bom/elasticache/query.rego index cceb4179ccb..90f8164d56c 100644 --- a/assets/queries/terraform/aws_bom/elasticache/query.rego +++ b/assets/queries/terraform/aws_bom/elasticache/query.rego @@ -28,7 +28,7 @@ CxPolicy[result] { } get_engine_type(aws_elasticache_cluster) = engine_type { - engine_type := aws_elasticache_cluster.engine_type + engine_type := aws_elasticache_cluster.engine } else = engine_type { not aws_elasticache_cluster.replication_group_id engine_type := "unknown" diff --git a/assets/queries/terraform/aws_bom/mq/query.rego b/assets/queries/terraform/aws_bom/mq/query.rego index 024ca4ead43..a7fd4de4d05 100644 --- a/assets/queries/terraform/aws_bom/mq/query.rego +++ b/assets/queries/terraform/aws_bom/mq/query.rego @@ -15,8 +15,8 @@ CxPolicy[result] { "resource_encryption": common_lib.get_encryption_if_exists(aws_mq_broker_resource), "resource_vendor": "AWS", "resource_category": "Queues", - "user_name": aws_mq_broker_resource.user.username, - "is_default_password": terra_lib.is_default_password(aws_mq_broker_resource.user.password), + # "user_name": aws_mq_broker_resource.user.username, # needs attention in the future + # "is_default_password": terra_lib.is_default_password(aws_mq_broker_resource.user.password), # needs attention in the future } result := { @@ -31,7 +31,8 @@ CxPolicy[result] { } check_publicly_accessible(resource) = accessibility { - accessibility := resource.publicly_accessible + resource.publicly_accessible == true + accessibility := "public" } else = accessibility { - accessibility := false + accessibility := "private" } diff --git a/assets/queries/terraform/aws_bom/mq/test/positive2.tf b/assets/queries/terraform/aws_bom/mq/test/positive2.tf index d5e583884c5..2eb0c9569e6 100644 --- a/assets/queries/terraform/aws_bom/mq/test/positive2.tf +++ b/assets/queries/terraform/aws_bom/mq/test/positive2.tf @@ -16,6 +16,11 @@ resource "aws_mq_broker" "positive2" { password = "111111111111" } + user { + username = "ExampleUser" + password = "MindTheGap" + } + encryption_options { kms_key_id = var.encryption_options.kms_key_id } diff --git a/assets/queries/terraform/aws_bom/s3_bucket/query.rego b/assets/queries/terraform/aws_bom/s3_bucket/query.rego index 5398de4c8a3..fd6775043af 100644 --- a/assets/queries/terraform/aws_bom/s3_bucket/query.rego +++ b/assets/queries/terraform/aws_bom/s3_bucket/query.rego @@ -6,16 +6,20 @@ import data.generic.terraform as terra_lib CxPolicy[result] { bucket_resource := input.document[i].resource.aws_s3_bucket[name] + info := get_accessibility(bucket_resource, name) + bom_output = { "resource_type": "aws_s3_bucket", "resource_name": get_bucket_name(bucket_resource), - "resource_accessibility": get_accessibility(bucket_resource, name), + "resource_accessibility": info.accessibility, "resource_encryption": common_lib.get_encryption_if_exists(bucket_resource), "resource_vendor": "AWS", "resource_category": "Storage", "acl": get_bucket_acl(bucket_resource), } + final_bom_output = common_lib.get_bom_output(bom_output, info.policy) + result := { "documentId": input.document[i].id, "searchKey": sprintf("aws_s3_bucket[%s]", [name]), @@ -23,7 +27,7 @@ CxPolicy[result] { "keyExpectedValue": "", "keyActualValue": "", "searchLine": common_lib.build_search_line(["resource", "aws_s3_bucket", name], []), - "value": json.marshal(bom_output), + "value": json.marshal(final_bom_output), } } @@ -49,17 +53,19 @@ get_accessibility(bucket, bucketName) = accessibility { s3BucketPublicAccessBlock := input.document[i].resource.aws_s3_bucket_public_access_block[_] split(s3BucketPublicAccessBlock.bucket, ".")[1] == bucketName is_public_access_blocked(s3BucketPublicAccessBlock) - accessibility := "private" + accessibility = {"accessibility": "private", "policy": ""} } else = accessibility { # cases when there is a unrestriced policy - accessibility := terra_lib.get_accessibility(bucket, bucketName, "aws_s3_bucket_policy", "bucket").accessibility - accessibility != "unknown" + acc := terra_lib.get_accessibility(bucket, bucketName, "aws_s3_bucket_policy", "bucket") + acc.accessibility == "hasPolicy" + + accessibility = {"accessibility": "hasPolicy", "policy": acc.policy} } else = accessibility { # last cases: acl definition - accessibility := bucket.acl + accessibility = {"accessibility": bucket.acl, "policy": ""} } else = accessibility { # last cases: acl definition not common_lib.valid_key(bucket, "acl") - accessibility := "private" + accessibility = {"accessibility": "private", "policy": ""} } diff --git a/assets/queries/terraform/aws_bom/sns/query.rego b/assets/queries/terraform/aws_bom/sns/query.rego index 596cf21222b..90abd2a0c1f 100644 --- a/assets/queries/terraform/aws_bom/sns/query.rego +++ b/assets/queries/terraform/aws_bom/sns/query.rego @@ -10,14 +10,15 @@ CxPolicy[result] { bom_output = { "resource_type": "aws_sns_topic", - "resource_name": get_queue_name(aws_sns_topic_resource), + "resource_name": get_topic_name(aws_sns_topic_resource), "resource_accessibility": info.accessibility, "resource_encryption": common_lib.get_encryption_if_exists(aws_sns_topic_resource), "resource_vendor": "AWS", "resource_category": "Messaging", - "policy": info.policy, } + final_bom_output := common_lib.get_bom_output(bom_output, info.policy) + result := { "documentId": input.document[i].id, "searchKey": sprintf("aws_sns_topic[%s]", [name]), @@ -25,14 +26,14 @@ CxPolicy[result] { "keyExpectedValue": "", "keyActualValue": "", "searchLine": common_lib.build_search_line(["resource", "aws_sns_topic", name], []), - "value": json.marshal(bom_output), + "value": json.marshal(final_bom_output), } } -get_queue_name(aws_sns_topic_resource) = name { +get_topic_name(aws_sns_topic_resource) = name { name := aws_sns_topic_resource.name } else = name { name := sprintf("%s", [aws_sns_topic_resource.name_prefix]) } else = name { - name := "unknown" + name := common_lib.get_tag_name_if_exists(aws_sns_topic_resource) } diff --git a/assets/queries/terraform/aws_bom/sns/test/positive5.tf b/assets/queries/terraform/aws_bom/sns/test/positive5.tf index fd5177915de..106c5ec8095 100644 --- a/assets/queries/terraform/aws_bom/sns/test/positive5.tf +++ b/assets/queries/terraform/aws_bom/sns/test/positive5.tf @@ -1,5 +1,7 @@ resource "aws_sns_topic" "positive5" { - name = "user-updates-topic" + tags = { + Name = "SNS Topic" + } kms_master_key_id = "alias/aws/sns" diff --git a/assets/queries/terraform/aws_bom/sqs/query.rego b/assets/queries/terraform/aws_bom/sqs/query.rego index 0cbc5590953..fafa338de3f 100644 --- a/assets/queries/terraform/aws_bom/sqs/query.rego +++ b/assets/queries/terraform/aws_bom/sqs/query.rego @@ -15,9 +15,10 @@ CxPolicy[result] { "resource_encryption": common_lib.get_encryption_if_exists(aws_sqs_queue_resource), "resource_vendor": "AWS", "resource_category": "Queues", - "policy": info.policy, } + final_bom_output := common_lib.get_bom_output(bom_output, info.policy) + result := { "documentId": input.document[i].id, "searchKey": sprintf("aws_sqs_queue[%s]", [name]), @@ -25,7 +26,7 @@ CxPolicy[result] { "keyExpectedValue": "", "keyActualValue": "", "searchLine": common_lib.build_search_line(["resource", "aws_sqs_queue", name], []), - "value": json.marshal(bom_output), + "value": json.marshal(final_bom_output), } } @@ -34,5 +35,5 @@ get_queue_name(aws_sqs_queue_resource) = name { } else = name { name := sprintf("%s", [aws_sqs_queue_resource.name_prefix]) } else = name { - name := "unknown" + name := common_lib.get_tag_name_if_exists(aws_sqs_queue_resource) } diff --git a/docs/bom.md b/docs/bom.md index 9175e6ba18c..5fbb0535691 100644 --- a/docs/bom.md +++ b/docs/bom.md @@ -1,4 +1,4 @@ -## Bill Of Materials +## [Terraform] Bill Of Materials This feature uses Rego queries to extract a list of used Terraform resources along with its metadata in the scanned IaC. @@ -9,19 +9,33 @@ BoM queries extracts metadata about the resources and organizes it in the follow ```go billOfMaterialsRequiredFields := map[string]bool{ "acl": false, - "is_default_password": false, "policy": false, - "resource_type": true, - "resource_name": true, - "resource_engine": false, "resource_accessibility": true, - "resource_vendor": true, "resource_category": true, - "user_name": false, + "resource_encryption": true, + "resource_engine": false, + "resource_name": true, + "resource_type": true, + "resource_vendor": true, } ``` -After extracting the information, the query stores the stringified JSON structure inside the `value` field in the `result`: +Observe more detailed information about it in the table below. + +| **Field** | **Possible Values** | **Required** | **Resources** | **Type** | +|:----------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:------------:|:------------------------------------------------------------------------:|:---------------:| +| acl | private,
public-read,
public-read-write,
aws-exec-read,
authenticated-read,
bucket-owner-read,
bucket-owner-full-control,
log-delivery-write | No | `aws_s3_bucket` | string | +| policy | policy content (in case `resource_accessibility` equals hasPolicy) | No | `aws_efs_file_system`,
`aws_s3_bucket`,
`aws_sns_topic`,
`aws_sqs_queue` | JSON marshalled | +| resource_accessibility | public, private, hasPolicy or unknown for `aws_ebs_volume`, `aws_efs_file_system`, `aws_mq_broker`, `aws_msk_cluster`, `aws_s3_bucket`, `aws_sns_topic`, and `aws_sqs_queue`

at least one security group associated with the elasticache is unrestricted, all security groups associated with the elasticache are restricted or unknown for `aws_elasticache_cluster` | Yes | all | string | +| resource_category | In Memory Data Structure for `aws_elasticache_cluster`

Messaging for `aws_sns_topic`

Queues for `aws_mq_broker` and `aws_sqs_queue`

Storage for `aws_ebs_volume`, `aws_efs_file_system`, and `aws_s3_bucket`

Streaming for `aws_msk_cluster` | Yes | all | string | +| resource_encryption | encrypted,
unencrypted,
unknown | Yes | all | string | +| resource_engine | memcached, redis or unknown for `aws_elasticache_cluster`

ActiveMQ or RabbitMQ for `aws_mq_broker` | No | `aws_elasticache_cluster`,
`aws_mq_broker` | string | +| resource_name | anything (if the name is defined),
unknown (if the name is not defined) | Yes | all | string | +| resource_type | aws_ebs_volume for `aws_ebs_volume`,
aws_efs_file_system for `aws_efs_file_system`,
aws_elasticache_cluster for `aws_elasticache_cluster`,
aws_mq_broker for `aws_mq_broker`,
aws_msk_cluster for `aws_msk_cluster`,
aws_s3_bucket for `aws_s3_bucket`,
aws_sns_topic for `aws_sns_topic`, aws_sqs_queue for `aws_sqs_queue` | Yes | all | string | +| resource_vendor | AWS | Yes | all | string | + + +### BoM query example ```rego CxPolicy[result] { diff --git a/test/queries_test.go b/test/queries_test.go index 8a92714c439..9a41e6d17ac 100644 --- a/test/queries_test.go +++ b/test/queries_test.go @@ -227,8 +227,8 @@ func validateQueryResultFields(tb testing.TB, vulnerabilies []model.Vulnerabilit bomResult := make(map[string]string) json.Unmarshal([]byte(*vulnerabilies[idx].Value), &bomResult) bomOutputRequiredFields := map[string]bool{ - "acl": false, - "is_default_password": false, + "acl": false, + // "is_default_password": false, "policy": false, "resource_type": true, "resource_name": true, @@ -236,8 +236,8 @@ func validateQueryResultFields(tb testing.TB, vulnerabilies []model.Vulnerabilit "resource_accessibility": true, "resource_vendor": true, "resource_category": true, - "user_name": false, - "resource_encryption": true, + // "user_name": false, + "resource_encryption": true, } for key := range bomOutputRequiredFields { _, ok := bomResult[key]