diff --git a/.github/scripts/samples-linters/ignore-list/k8s b/.github/scripts/samples-linters/ignore-list/k8s index 4032fc36b8f..78810f188af 100644 --- a/.github/scripts/samples-linters/ignore-list/k8s +++ b/.github/scripts/samples-linters/ignore-list/k8s @@ -1 +1,5 @@ assets/queries/k8s/hpa_targets_invalid_object/test/positive.yaml +assets/queries/k8s/network_policy_ingress_not_restricted/test/positive.yaml +assets/queries/k8s/network_policy_ingress_not_restricted/test/negative.yaml +assets/queries/k8s/ingress_whitelist_open_to_all/test/positive.yaml +assets/queries/k8s/ingress_whitelist_open_to_all/test/negative.yaml diff --git a/.github/workflows/validate-k8s-samples.yml b/.github/workflows/validate-k8s-samples.yml index 68901309a7c..4e229d97966 100644 --- a/.github/workflows/validate-k8s-samples.yml +++ b/.github/workflows/validate-k8s-samples.yml @@ -28,6 +28,6 @@ jobs: run: | python3 -u .github/scripts/samples-linters/validate-syntax.py \ "assets/queries/k8s/**/test/*.yaml" \ - --extra ' --skip-kinds CustomResourceDefinition,KubeletConfiguration,Policy,EncryptionConfiguration,KubeSchedulerConfiguration,SecretProviderClass,Service,Configuration,ContainerSource,Revision' \ + --extra ' --skip-kinds CustomResourceDefinition,KubeletConfiguration,Policy,EncryptionConfiguration,KubeSchedulerConfiguration,SecretProviderClass,Service,Configuration,ContainerSource,Revision --ignore-missing-schemas' \ --linter .bin/kubeval \ --skip '.github/scripts/samples-linters/ignore-list/k8s' -v diff --git a/Dockerfile b/Dockerfile index 6b9c0685fd6..2196280d65c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,8 @@ RUN go mod download -x COPY . . # Build the Go app +USER root +RUN mkdir -p bin RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \ -ldflags "-s -w -X github.com/Checkmarx/kics/v2/internal/constants.Version=${VERSION} -X github.com/Checkmarx/kics/v2/internal/constants.SCMCommit=${COMMIT} -X github.com/Checkmarx/kics/v2/internal/constants.SentryDSN=${SENTRY_DSN} -X github.com/Checkmarx/kics/v2/internal/constants.BaseURL=${DESCRIPTIONS_URL}" \ -a -installsuffix cgo \ diff --git a/assets/queries/k8s/ingress_whitelist_open_to_all/metadata.json b/assets/queries/k8s/ingress_whitelist_open_to_all/metadata.json new file mode 100644 index 00000000000..2dd6fd07338 --- /dev/null +++ b/assets/queries/k8s/ingress_whitelist_open_to_all/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "3a7d4239-f768-438f-a30b-23a26f885966", + "queryName": "Ingress Whitelist Open To All IPs", + "severity": "HIGH", + "category": "Networking and Firewall", + "descriptionText": "Kubernetes Ingress resources should not set the 'nginx.ingress.kubernetes.io/whitelist-source-range' annotation to '0.0.0.0/0' or '::/0'. These values allow traffic from all IPv4 or IPv6 addresses, effectively disabling the source IP restriction and exposing the service to the entire internet.", + "descriptionUrl": "https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#whitelist-source-range", + "platform": "Kubernetes", + "descriptionID": "3a7d4239", + "cloudProvider": "common", + "cwe": "668", + "riskScore": "7.5" +} diff --git a/assets/queries/k8s/ingress_whitelist_open_to_all/query.rego b/assets/queries/k8s/ingress_whitelist_open_to_all/query.rego new file mode 100644 index 00000000000..ded9bc672bd --- /dev/null +++ b/assets/queries/k8s/ingress_whitelist_open_to_all/query.rego @@ -0,0 +1,29 @@ +package Cx + +# Ingress whitelist-source-range annotation set to 0.0.0.0/0 or ::/0 allows all IPs. +CxPolicy[result] { + document := input.document[i] + document.kind == "Ingress" + metadata := document.metadata + + whitelist := metadata.annotations["nginx.ingress.kubernetes.io/whitelist-source-range"] + open_cidr_in_whitelist(whitelist) + + result := { + "documentId": input.document[i].id, + "resourceType": document.kind, + "resourceName": metadata.name, + "searchKey": sprintf("metadata.name={{%s}}.annotations", [metadata.name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("Ingress '%s' whitelist-source-range should restrict access to specific IP ranges", [metadata.name]), + "keyActualValue": sprintf("Ingress '%s' whitelist-source-range is set to '%s', allowing access from all IP addresses", [metadata.name, whitelist]), + } +} + +open_cidr_in_whitelist(whitelist) { + contains(whitelist, "0.0.0.0/0") +} + +open_cidr_in_whitelist(whitelist) { + contains(whitelist, "::/0") +} diff --git a/assets/queries/k8s/ingress_whitelist_open_to_all/test/negative.yaml b/assets/queries/k8s/ingress_whitelist_open_to_all/test/negative.yaml new file mode 100644 index 00000000000..4d959283b88 --- /dev/null +++ b/assets/queries/k8s/ingress_whitelist_open_to_all/test/negative.yaml @@ -0,0 +1,37 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: restricted-whitelist + annotations: + nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/16,192.168.0.0/24" +spec: + rules: + - host: example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: myservice + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: single-ip-whitelist + annotations: + nginx.ingress.kubernetes.io/whitelist-source-range: "203.0.113.42/32" +spec: + rules: + - host: internal.example.com + http: + paths: + - path: /admin + pathType: Prefix + backend: + service: + name: admin-service + port: + number: 8080 diff --git a/assets/queries/k8s/ingress_whitelist_open_to_all/test/positive.yaml b/assets/queries/k8s/ingress_whitelist_open_to_all/test/positive.yaml new file mode 100644 index 00000000000..000b8539627 --- /dev/null +++ b/assets/queries/k8s/ingress_whitelist_open_to_all/test/positive.yaml @@ -0,0 +1,56 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: open-whitelist-ipv4 + annotations: + nginx.ingress.kubernetes.io/whitelist-source-range: "0.0.0.0/0" +spec: + rules: + - host: example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: myservice + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: open-whitelist-ipv6 + annotations: + nginx.ingress.kubernetes.io/whitelist-source-range: "::/0" +spec: + rules: + - host: example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: myservice + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: open-whitelist-combined + annotations: + nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/16,0.0.0.0/0" +spec: + rules: + - host: example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: myservice + port: + number: 443 diff --git a/assets/queries/k8s/ingress_whitelist_open_to_all/test/positive_expected_result.json b/assets/queries/k8s/ingress_whitelist_open_to_all/test/positive_expected_result.json new file mode 100644 index 00000000000..e7dd6fd3813 --- /dev/null +++ b/assets/queries/k8s/ingress_whitelist_open_to_all/test/positive_expected_result.json @@ -0,0 +1,17 @@ +[ + { + "queryName": "Ingress Whitelist Open To All IPs", + "severity": "HIGH", + "line": 5 + }, + { + "queryName": "Ingress Whitelist Open To All IPs", + "severity": "HIGH", + "line": 24 + }, + { + "queryName": "Ingress Whitelist Open To All IPs", + "severity": "HIGH", + "line": 43 + } +] diff --git a/assets/queries/k8s/network_policy_ingress_not_restricted/metadata.json b/assets/queries/k8s/network_policy_ingress_not_restricted/metadata.json new file mode 100644 index 00000000000..d5be58cc244 --- /dev/null +++ b/assets/queries/k8s/network_policy_ingress_not_restricted/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "d44bdf72-1b6c-44a7-bd67-4933211fe36c", + "queryName": "Network Policy Ingress Not Restricted", + "severity": "HIGH", + "category": "Networking and Firewall", + "descriptionText": "Kubernetes NetworkPolicy ingress rules should define a 'from' block to restrict which sources can send traffic to the selected pods. An ingress rule without a 'from' block allows traffic from all sources, including any IP address on the internet.", + "descriptionUrl": "https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors", + "platform": "Kubernetes", + "descriptionID": "d44bdf72", + "cloudProvider": "common", + "cwe": "668", + "riskScore": "7.5" +} diff --git a/assets/queries/k8s/network_policy_ingress_not_restricted/query.rego b/assets/queries/k8s/network_policy_ingress_not_restricted/query.rego new file mode 100644 index 00000000000..486ce570652 --- /dev/null +++ b/assets/queries/k8s/network_policy_ingress_not_restricted/query.rego @@ -0,0 +1,37 @@ +package Cx + +import data.generic.common as common_lib + +# An ingress rule without a 'from' block allows traffic from ALL sources (any IP). +CxPolicy[result] { + document := input.document[i] + document.kind == "NetworkPolicy" + metadata := document.metadata + spec := document.spec + + ingress_in_scope(spec) + + ingress_rule := spec.ingress[j] + not common_lib.valid_key(ingress_rule, "from") + + result := { + "documentId": input.document[i].id, + "resourceType": document.kind, + "resourceName": metadata.name, + "searchKey": sprintf("metadata.name={{%s}}.spec.ingress", [metadata.name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("NetworkPolicy '%s' ingress rule [%d] should define a 'from' block to restrict source IPs", [metadata.name, j]), + "keyActualValue": sprintf("NetworkPolicy '%s' ingress rule [%d] has no 'from' block, allowing traffic from all sources", [metadata.name, j]), + } +} + +# policyTypes explicitly includes Ingress +ingress_in_scope(spec) { + lower(spec.policyTypes[_]) == "ingress" +} + +# policyTypes is absent — K8s defaults to controlling Ingress when ingress rules are present +ingress_in_scope(spec) { + not common_lib.valid_key(spec, "policyTypes") + common_lib.valid_key(spec, "ingress") +} diff --git a/assets/queries/k8s/network_policy_ingress_not_restricted/test/negative.yaml b/assets/queries/k8s/network_policy_ingress_not_restricted/test/negative.yaml new file mode 100644 index 00000000000..d1bc0aeaee6 --- /dev/null +++ b/assets/queries/k8s/network_policy_ingress_not_restricted/test/negative.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: restricted-ingress +spec: + podSelector: + matchLabels: + app: myapp + policyTypes: + - Ingress + ingress: + - from: + - ipBlock: + cidr: 10.0.0.0/24 + - namespaceSelector: + matchLabels: + project: myproject + - podSelector: + matchLabels: + role: frontend + ports: + - protocol: TCP + port: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-all-ingress +spec: + podSelector: + matchLabels: + app: isolated + policyTypes: + - Ingress diff --git a/assets/queries/k8s/network_policy_ingress_not_restricted/test/positive.yaml b/assets/queries/k8s/network_policy_ingress_not_restricted/test/positive.yaml new file mode 100644 index 00000000000..ba5e3350187 --- /dev/null +++ b/assets/queries/k8s/network_policy_ingress_not_restricted/test/positive.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-all-ingress-empty-rule +spec: + podSelector: + matchLabels: + app: myapp + policyTypes: + - Ingress + ingress: + - {} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-any-source-with-ports +spec: + podSelector: {} + policyTypes: + - Ingress + ingress: + - ports: + - protocol: TCP + port: 80 diff --git a/assets/queries/k8s/network_policy_ingress_not_restricted/test/positive_expected_result.json b/assets/queries/k8s/network_policy_ingress_not_restricted/test/positive_expected_result.json new file mode 100644 index 00000000000..c897856ef3f --- /dev/null +++ b/assets/queries/k8s/network_policy_ingress_not_restricted/test/positive_expected_result.json @@ -0,0 +1,12 @@ +[ + { + "queryName": "Network Policy Ingress Not Restricted", + "severity": "HIGH", + "line": 11 + }, + { + "queryName": "Network Policy Ingress Not Restricted", + "severity": "HIGH", + "line": 22 + } +] diff --git a/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/metadata.json b/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/metadata.json new file mode 100644 index 00000000000..97d7e72ca80 --- /dev/null +++ b/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "9b848928-9211-4bbe-9a7f-6bb651447593", + "queryName": "Security Group Ingress With Wide CIDR Range", + "severity": "HIGH", + "category": "Networking and Firewall", + "descriptionText": "AWS Security Group ingress rules should not use overly broad CIDR blocks. A CIDR prefix length of /8 or shorter (e.g., 10.0.0.0/1, 0.0.0.0/8) allows millions to billions of IP addresses to reach your resources. Use the most specific CIDR range required.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group", + "platform": "Terraform", + "descriptionID": "9b848928", + "cloudProvider": "aws", + "cwe": "668", + "riskScore": "7.2" +} diff --git a/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/query.rego b/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/query.rego new file mode 100644 index 00000000000..39e74d65544 --- /dev/null +++ b/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/query.rego @@ -0,0 +1,95 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# aws_security_group — single ingress block +CxPolicy[result] { + resource := input.document[i].resource.aws_security_group[name] + ingress_list := tf_lib.get_ingress_list(resource.ingress) + ingress_list.is_unique_element + + cidr := ingress_list.value[_].cidr_blocks[_] + is_wide_cidr(cidr) + + result := { + "documentId": input.document[i].id, + "resourceType": "aws_security_group", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_security_group[%s].ingress.cidr_blocks", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("aws_security_group[%s].ingress.cidr_blocks should use a CIDR prefix length greater than /8", [name]), + "keyActualValue": sprintf("aws_security_group[%s].ingress.cidr_blocks '%s' has prefix /%s, granting access to a very large IP range", [name, cidr, split(cidr, "/")[1]]), + "searchLine": common_lib.build_search_line(["resource", "aws_security_group", name, "ingress", "cidr_blocks"], []), + } +} + +# aws_security_group — multiple ingress blocks +CxPolicy[result] { + resource := input.document[i].resource.aws_security_group[name] + ingress_list := tf_lib.get_ingress_list(resource.ingress) + not ingress_list.is_unique_element + + ingress := ingress_list.value[j] + cidr := ingress.cidr_blocks[_] + is_wide_cidr(cidr) + + result := { + "documentId": input.document[i].id, + "resourceType": "aws_security_group", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_security_group[%s].ingress[%d].cidr_blocks", [name, j]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("aws_security_group[%s].ingress[%d].cidr_blocks should use a CIDR prefix length greater than /8", [name, j]), + "keyActualValue": sprintf("aws_security_group[%s].ingress[%d].cidr_blocks '%s' has prefix /%s, granting access to a very large IP range", [name, j, cidr, split(cidr, "/")[1]]), + "searchLine": common_lib.build_search_line(["resource", "aws_security_group", name, "ingress", j, "cidr_blocks"], []), + } +} + +# aws_vpc_security_group_ingress_rule +CxPolicy[result] { + rule := input.document[i].resource.aws_vpc_security_group_ingress_rule[name] + cidr := rule.cidr_ipv4 + is_wide_cidr(cidr) + + result := { + "documentId": input.document[i].id, + "resourceType": "aws_vpc_security_group_ingress_rule", + "resourceName": tf_lib.get_resource_name(rule, name), + "searchKey": sprintf("aws_vpc_security_group_ingress_rule[%s].cidr_ipv4", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("aws_vpc_security_group_ingress_rule[%s].cidr_ipv4 should use a CIDR prefix length greater than /8", [name]), + "keyActualValue": sprintf("aws_vpc_security_group_ingress_rule[%s].cidr_ipv4 '%s' has prefix /%s, granting access to a very large IP range", [name, cidr, split(cidr, "/")[1]]), + "searchLine": common_lib.build_search_line(["resource", "aws_vpc_security_group_ingress_rule", name, "cidr_ipv4"], []), + } +} + +# aws_security_group_rule (type = ingress) +CxPolicy[result] { + rule := input.document[i].resource.aws_security_group_rule[name] + tf_lib.is_security_group_ingress("aws_security_group_rule", rule) + cidr := rule.cidr_blocks[_] + is_wide_cidr(cidr) + + result := { + "documentId": input.document[i].id, + "resourceType": "aws_security_group_rule", + "resourceName": tf_lib.get_resource_name(rule, name), + "searchKey": sprintf("aws_security_group_rule[%s].cidr_blocks", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("aws_security_group_rule[%s].cidr_blocks should use a CIDR prefix length greater than /8", [name]), + "keyActualValue": sprintf("aws_security_group_rule[%s].cidr_blocks '%s' has prefix /%s, granting access to a very large IP range", [name, cidr, split(cidr, "/")[1]]), + "searchLine": common_lib.build_search_line(["resource", "aws_security_group_rule", name, "cidr_blocks"], []), + } +} + +# CIDR prefix length 1–8: covers hundreds of millions to billions of IPs. +# Prefix 0 (0.0.0.0/0) is already caught by unrestricted_security_group_ingress. +# 10.0.0.0/8 is the standard RFC 1918 Class-A private range and is excluded. +is_wide_cidr(cidr) { + contains(cidr, "/") + prefix := to_number(split(cidr, "/")[1]) + prefix >= 1 + prefix <= 8 + cidr != "10.0.0.0/8" +} diff --git a/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/negative1.tf b/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/negative1.tf new file mode 100644 index 00000000000..3ea813397d9 --- /dev/null +++ b/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/negative1.tf @@ -0,0 +1,53 @@ +resource "aws_security_group" "negative1" { + name = "negative1" + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["10.0.0.0/16"] + } +} + +resource "aws_security_group" "negative2" { + name = "negative2" + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["192.168.0.0/24"] + } +} + +# 10.0.0.0/8 is the standard RFC 1918 Class-A private range — excluded +resource "aws_security_group" "negative3" { + name = "negative3" + + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["10.0.0.0/8"] + } +} + +# 0.0.0.0/0 is already detected by unrestricted_security_group_ingress +resource "aws_security_group" "negative4" { + name = "negative4" + + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_vpc_security_group_ingress_rule" "negative5" { + security_group_id = aws_security_group.negative1.id + cidr_ipv4 = "10.0.0.0/16" + from_port = 443 + ip_protocol = "tcp" + to_port = 443 +} diff --git a/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/positive1.tf b/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/positive1.tf new file mode 100644 index 00000000000..33479d90fbb --- /dev/null +++ b/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/positive1.tf @@ -0,0 +1,29 @@ +resource "aws_security_group" "positive1" { + name = "positive1" + + ingress { + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = ["10.0.0.0/1"] + } +} + +resource "aws_security_group" "positive2" { + name = "positive2" + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/7"] + } +} + +resource "aws_vpc_security_group_ingress_rule" "positive3" { + security_group_id = aws_security_group.positive1.id + cidr_ipv4 = "128.0.0.0/1" + from_port = 80 + ip_protocol = "tcp" + to_port = 80 +} diff --git a/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/positive_expected_result.json b/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/positive_expected_result.json new file mode 100644 index 00000000000..a49df60d022 --- /dev/null +++ b/assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/positive_expected_result.json @@ -0,0 +1,17 @@ +[ + { + "queryName": "Security Group Ingress With Wide CIDR Range", + "severity": "HIGH", + "line": 8 + }, + { + "queryName": "Security Group Ingress With Wide CIDR Range", + "severity": "HIGH", + "line": 19 + }, + { + "queryName": "Security Group Ingress With Wide CIDR Range", + "severity": "HIGH", + "line": 25 + } +] diff --git a/e2e/tmp-kics-ar/194604684.tf b/e2e/tmp-kics-ar/194604684.tf new file mode 100755 index 00000000000..ff7e89a967e --- /dev/null +++ b/e2e/tmp-kics-ar/194604684.tf @@ -0,0 +1,20 @@ +resource "alicloud_ram_account_password_policy" "corporate1" { + require_lowercase_characters = false + require_uppercase_characters = false + require_numbers = false + require_symbols = false + hard_expiry = true + password_reuse_prevention = 5 + max_login_attempts = 3 +} + +resource "alicloud_ram_account_password_policy" "corporate2" { + minimum_password_length = 14 + require_lowercase_characters = false + require_uppercase_characters = false + require_numbers = false + require_symbols = false + hard_expiry = true + password_reuse_prevention = 5 + max_login_attempts = 3 +} diff --git a/e2e/tmp-kics-ar/985110139.tf b/e2e/tmp-kics-ar/985110139.tf new file mode 100755 index 00000000000..ff7e89a967e --- /dev/null +++ b/e2e/tmp-kics-ar/985110139.tf @@ -0,0 +1,20 @@ +resource "alicloud_ram_account_password_policy" "corporate1" { + require_lowercase_characters = false + require_uppercase_characters = false + require_numbers = false + require_symbols = false + hard_expiry = true + password_reuse_prevention = 5 + max_login_attempts = 3 +} + +resource "alicloud_ram_account_password_policy" "corporate2" { + minimum_password_length = 14 + require_lowercase_characters = false + require_uppercase_characters = false + require_numbers = false + require_symbols = false + hard_expiry = true + password_reuse_prevention = 5 + max_login_attempts = 3 +} diff --git a/e2e/tmp-kics-ar/results-remediate-all.json b/e2e/tmp-kics-ar/results-remediate-all.json new file mode 100755 index 00000000000..25eff795466 --- /dev/null +++ b/e2e/tmp-kics-ar/results-remediate-all.json @@ -0,0 +1,160 @@ +{ + "kics_version": "development", + "files_scanned": 1, + "lines_scanned": 0, + "files_parsed": 1, + "lines_parsed": 0, + "lines_ignored": 0, + "files_failed_to_scan": 0, + "queries_total": 3, + "queries_failed_to_execute": 0, + "queries_failed_to_compute_similarity_id": 0, + "scan_id": "console", + "severity_counters": { + "CRITICAL": 0, + "HIGH": 1, + "INFO": 0, + "LOW": 0, + "MEDIUM": 4 + }, + "total_counter": 5, + "total_bom_resources": 0, + "start": "0001-01-01T00:00:00Z", + "end": "0001-01-01T00:00:00Z", + "paths": [ + "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/194604684.tf" + ], + "queries": [ + { + "query_name": "Ram Account Password Policy Not Required Minimum Length", + "query_id": "a9dfec39-a740-4105-bbd6-721ba163c053", + "query_url": "", + "severity": "HIGH", + "platform": "Terraform", + "cloud_provider": "ALICLOUD", + "category": "Secret Management", + "experimental": false, + "description": "Ram Account Password Policy should have 'minimum_password_length' defined and set to 14 or above", + "description_id": "a8b47743", + "cis_description_id": "testCISID", + "cis_description_title": "testCISTitle", + "cis_description_text": "testCISDescription", + "files": [ + { + "file_name": "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/194604684.tf", + "similarity_id": "f282fa13cf5e4ffd4bbb0ee2059f8d0240edcd2ca54b3bb71633145d961de5ce", + "line": 1, + "vuln_lines": null, + "resource_type": "alicloud_ram_account_password_policy", + "resource_name": "corporate1", + "issue_type": "MissingAttribute", + "search_key": "alicloud_ram_account_password_policy[corporate1]", + "search_line": 0, + "search_value": "", + "expected_value": "'minimum_password_length' is defined and set to 14 or above ", + "actual_value": "'minimum_password_length' is not defined", + "remediation": "minimum_password_length = 14", + "remediation_type": "addition" + } + ] + }, + { + "query_name": "RAM Account Password Policy Not Required Symbols", + "query_id": "41a38329-d81b-4be4-aef4-55b2615d3282", + "query_url": "", + "severity": "MEDIUM", + "platform": "Terraform", + "cloud_provider": "ALICLOUD", + "category": "Secret Management", + "experimental": false, + "description": "RAM account password security should require at least one symbol", + "description_id": "f3616c34", + "cis_description_id": "testCISID", + "cis_description_title": "testCISTitle", + "cis_description_text": "testCISDescription", + "files": [ + { + "file_name": "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/194604684.tf", + "similarity_id": "87abbee5d0ec977ba193371c702dca2c040ea902d2e606806a63b66119ff89bc", + "line": 5, + "vuln_lines": null, + "resource_type": "alicloud_ram_account_password_policy", + "resource_name": "corporate1", + "issue_type": "IncorrectValue", + "search_key": "resource.alicloud_ram_account_password_policy[corporate1].require_symbols", + "search_line": 0, + "search_value": "", + "expected_value": "resource.alicloud_ram_account_password_policy[corporate1].require_symbols is set to 'true'", + "actual_value": "resource.alicloud_ram_account_password_policy[corporate1].require_symbols is configured as 'false'", + "remediation": "{\"after\":\"true\",\"before\":\"false\"}", + "remediation_type": "replacement" + }, + { + "file_name": "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/194604684.tf", + "similarity_id": "2628457bdb548986936dbd7d8479524f2079f26d36b9faa9f34423e796fe62c8", + "line": 16, + "vuln_lines": null, + "resource_type": "alicloud_ram_account_password_policy", + "resource_name": "corporate2", + "issue_type": "IncorrectValue", + "search_key": "resource.alicloud_ram_account_password_policy[corporate2].require_symbols", + "search_line": 0, + "search_value": "", + "expected_value": "resource.alicloud_ram_account_password_policy[corporate2].require_symbols is set to 'true'", + "actual_value": "resource.alicloud_ram_account_password_policy[corporate2].require_symbols is configured as 'false'", + "remediation": "{\"after\":\"true\",\"before\":\"false\"}", + "remediation_type": "replacement" + } + ] + }, + { + "query_name": "Ram Account Password Policy Max Password Age Unrecommended", + "query_id": "2bb13841-7575-439e-8e0a-cccd9ede2fa8", + "query_url": "", + "severity": "MEDIUM", + "platform": "Terraform", + "cloud_provider": "ALICLOUD", + "category": "Secret Management", + "experimental": false, + "description": "Ram Account Password Policy Password 'max_password_age' should be higher than 0 and lower than 91", + "description_id": "6056f5ca", + "cis_description_id": "testCISID", + "cis_description_title": "testCISTitle", + "cis_description_text": "testCISDescription", + "files": [ + { + "file_name": "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/194604684.tf", + "similarity_id": "f1d17b3513439e03cd0a25690acc44755d4e68decfaa6c03522b20a65b26b617", + "line": 5, + "vuln_lines": null, + "resource_type": "alicloud_ram_account_password_policy", + "resource_name": "corporate1", + "issue_type": "MissingAttribute", + "search_key": "alicloud_ram_account_password_policy[corporate1]", + "search_line": 0, + "search_value": "", + "expected_value": "'max_password_age' should be higher than 0 and lower than 91", + "actual_value": "'max_password_age' is not defined", + "remediation": "max_password_age = 12", + "remediation_type": "addition" + }, + { + "file_name": "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/194604684.tf", + "similarity_id": "404ad93f4a485d0dd1b1621489c38be9c98dcc0b94396701ecad162e28db97fd", + "line": 11, + "vuln_lines": null, + "resource_type": "alicloud_ram_account_password_policy", + "resource_name": "corporate2", + "issue_type": "MissingAttribute", + "search_key": "alicloud_ram_account_password_policy[corporate2]", + "search_line": 0, + "search_value": "", + "expected_value": "'max_password_age' should be higher than 0 and lower than 91", + "actual_value": "'max_password_age' is not defined", + "remediation": "max_password_age = 12", + "remediation_type": "addition" + } + ] + } + ] +} diff --git a/e2e/tmp-kics-ar/results-remediate-include-ids.json b/e2e/tmp-kics-ar/results-remediate-include-ids.json new file mode 100755 index 00000000000..bc62590d6b9 --- /dev/null +++ b/e2e/tmp-kics-ar/results-remediate-include-ids.json @@ -0,0 +1,160 @@ +{ + "kics_version": "development", + "files_scanned": 1, + "lines_scanned": 0, + "files_parsed": 1, + "lines_parsed": 0, + "lines_ignored": 0, + "files_failed_to_scan": 0, + "queries_total": 3, + "queries_failed_to_execute": 0, + "queries_failed_to_compute_similarity_id": 0, + "scan_id": "console", + "severity_counters": { + "CRITICAL": 0, + "HIGH": 1, + "INFO": 0, + "LOW": 0, + "MEDIUM": 4 + }, + "total_counter": 5, + "total_bom_resources": 0, + "start": "0001-01-01T00:00:00Z", + "end": "0001-01-01T00:00:00Z", + "paths": [ + "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/985110139.tf" + ], + "queries": [ + { + "query_name": "Ram Account Password Policy Not Required Minimum Length", + "query_id": "a9dfec39-a740-4105-bbd6-721ba163c053", + "query_url": "", + "severity": "HIGH", + "platform": "Terraform", + "cloud_provider": "ALICLOUD", + "category": "Secret Management", + "experimental": false, + "description": "Ram Account Password Policy should have 'minimum_password_length' defined and set to 14 or above", + "description_id": "a8b47743", + "cis_description_id": "testCISID", + "cis_description_title": "testCISTitle", + "cis_description_text": "testCISDescription", + "files": [ + { + "file_name": "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/985110139.tf", + "similarity_id": "f282fa13cf5e4ffd4bbb0ee2059f8d0240edcd2ca54b3bb71633145d961de5ce", + "line": 1, + "vuln_lines": null, + "resource_type": "alicloud_ram_account_password_policy", + "resource_name": "corporate1", + "issue_type": "MissingAttribute", + "search_key": "alicloud_ram_account_password_policy[corporate1]", + "search_line": 0, + "search_value": "", + "expected_value": "'minimum_password_length' is defined and set to 14 or above ", + "actual_value": "'minimum_password_length' is not defined", + "remediation": "minimum_password_length = 14", + "remediation_type": "addition" + } + ] + }, + { + "query_name": "RAM Account Password Policy Not Required Symbols", + "query_id": "41a38329-d81b-4be4-aef4-55b2615d3282", + "query_url": "", + "severity": "MEDIUM", + "platform": "Terraform", + "cloud_provider": "ALICLOUD", + "category": "Secret Management", + "experimental": false, + "description": "RAM account password security should require at least one symbol", + "description_id": "f3616c34", + "cis_description_id": "testCISID", + "cis_description_title": "testCISTitle", + "cis_description_text": "testCISDescription", + "files": [ + { + "file_name": "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/985110139.tf", + "similarity_id": "87abbee5d0ec977ba193371c702dca2c040ea902d2e606806a63b66119ff89bc", + "line": 5, + "vuln_lines": null, + "resource_type": "alicloud_ram_account_password_policy", + "resource_name": "corporate1", + "issue_type": "IncorrectValue", + "search_key": "resource.alicloud_ram_account_password_policy[corporate1].require_symbols", + "search_line": 0, + "search_value": "", + "expected_value": "resource.alicloud_ram_account_password_policy[corporate1].require_symbols is set to 'true'", + "actual_value": "resource.alicloud_ram_account_password_policy[corporate1].require_symbols is configured as 'false'", + "remediation": "{\"after\":\"true\",\"before\":\"false\"}", + "remediation_type": "replacement" + }, + { + "file_name": "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/985110139.tf", + "similarity_id": "2628457bdb548986936dbd7d8479524f2079f26d36b9faa9f34423e796fe62c8", + "line": 16, + "vuln_lines": null, + "resource_type": "alicloud_ram_account_password_policy", + "resource_name": "corporate2", + "issue_type": "IncorrectValue", + "search_key": "resource.alicloud_ram_account_password_policy[corporate2].require_symbols", + "search_line": 0, + "search_value": "", + "expected_value": "resource.alicloud_ram_account_password_policy[corporate2].require_symbols is set to 'true'", + "actual_value": "resource.alicloud_ram_account_password_policy[corporate2].require_symbols is configured as 'false'", + "remediation": "{\"after\":\"true\",\"before\":\"false\"}", + "remediation_type": "replacement" + } + ] + }, + { + "query_name": "Ram Account Password Policy Max Password Age Unrecommended", + "query_id": "2bb13841-7575-439e-8e0a-cccd9ede2fa8", + "query_url": "", + "severity": "MEDIUM", + "platform": "Terraform", + "cloud_provider": "ALICLOUD", + "category": "Secret Management", + "experimental": false, + "description": "Ram Account Password Policy Password 'max_password_age' should be higher than 0 and lower than 91", + "description_id": "6056f5ca", + "cis_description_id": "testCISID", + "cis_description_title": "testCISTitle", + "cis_description_text": "testCISDescription", + "files": [ + { + "file_name": "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/985110139.tf", + "similarity_id": "f1d17b3513439e03cd0a25690acc44755d4e68decfaa6c03522b20a65b26b617", + "line": 5, + "vuln_lines": null, + "resource_type": "alicloud_ram_account_password_policy", + "resource_name": "corporate1", + "issue_type": "MissingAttribute", + "search_key": "alicloud_ram_account_password_policy[corporate1]", + "search_line": 0, + "search_value": "", + "expected_value": "'max_password_age' should be higher than 0 and lower than 91", + "actual_value": "'max_password_age' is not defined", + "remediation": "max_password_age = 12", + "remediation_type": "addition" + }, + { + "file_name": "/Users/anterosilva/Documents/PM/Monthly Data Review/kics/e2e/tmp-kics-ar/985110139.tf", + "similarity_id": "404ad93f4a485d0dd1b1621489c38be9c98dcc0b94396701ecad162e28db97fd", + "line": 11, + "vuln_lines": null, + "resource_type": "alicloud_ram_account_password_policy", + "resource_name": "corporate2", + "issue_type": "MissingAttribute", + "search_key": "alicloud_ram_account_password_policy[corporate2]", + "search_line": 0, + "search_value": "", + "expected_value": "'max_password_age' should be higher than 0 and lower than 91", + "actual_value": "'max_password_age' is not defined", + "remediation": "max_password_age = 12", + "remediation_type": "addition" + } + ] + } + ] +} diff --git a/internal/console/assets/scan-flags.json b/internal/console/assets/scan-flags.json index 7367bc81107..14a312c2491 100644 --- a/internal/console/assets/scan-flags.json +++ b/internal/console/assets/scan-flags.json @@ -103,6 +103,12 @@ "defaultValue": "false", "usage": "simplified version of CLI output" }, + "no-logo": { + "flagType": "bool", + "shorthandFlag": "", + "defaultValue": "false", + "usage": "hides the KICS ASCII logo" + }, "no-progress": { "flagType": "bool", "shorthandFlag": "", diff --git a/internal/console/flags/scan_flags.go b/internal/console/flags/scan_flags.go index 9e45168efad..8801b63d02f 100644 --- a/internal/console/flags/scan_flags.go +++ b/internal/console/flags/scan_flags.go @@ -17,6 +17,7 @@ const ( FailOnFlag = "fail-on" IgnoreOnExitFlag = "ignore-on-exit" MinimalUIFlag = "minimal-ui" + NoLogoFlag = "no-logo" NoProgressFlag = "no-progress" OutputNameFlag = "output-name" OutputPathFlag = "output-path" diff --git a/internal/console/pre_scan.go b/internal/console/pre_scan.go index db0a1b090f4..732ff47b6ff 100644 --- a/internal/console/pre_scan.go +++ b/internal/console/pre_scan.go @@ -139,7 +139,9 @@ func (console *console) preScan() { } printer := internalPrinter.NewPrinter(flags.GetBoolFlag(flags.MinimalUIFlag)) - printer.Success.Printf("\n%s\n", banner) + if !flags.GetBoolFlag(flags.NoLogoFlag) { + printer.Success.Printf("\n%s\n", banner) + } versionMsg := fmt.Sprintf("\nScanning with %s\n\n", constants.GetVersion()) fmt.Println(versionMsg) diff --git a/pkg/engine/inspector.go b/pkg/engine/inspector.go index 83ad82302ae..d985733e0b9 100644 --- a/pkg/engine/inspector.go +++ b/pkg/engine/inspector.go @@ -605,7 +605,9 @@ func getVulnerabilitiesFromQuery(ctx *QueryContext, c *Inspector, queryResultIte log.Debug(). Msgf("Excluding result SimilarityID: %s", vulnerability.SimilarityID) return nil, false - } else if checkComment(vulnerability.Line, file.LinesIgnore) { + } + if checkComment(vulnerability.Line, file.LinesIgnore) || + checkQueryComment(vulnerability.Line, vulnerability.QueryID, file.QueryLinesIgnore) { log.Debug(). Msgf("Excluding result Comment: %s", vulnerability.SimilarityID) return nil, false @@ -624,6 +626,23 @@ func checkComment(line int, ignoreLines []int) bool { return false } +// checkQueryComment returns true if the given line is suppressed for the specific queryID. +func checkQueryComment(line int, queryID string, queryIgnoreLines model.QueryIgnoreLines) bool { + if queryIgnoreLines == nil { + return false + } + lines, ok := queryIgnoreLines[queryID] + if !ok { + return false + } + for _, ignoreLine := range lines { + if line == ignoreLine { + return true + } + } + return false +} + // contains is a simple method to check if a slice // contains an entry func contains(s []string, e string) bool { diff --git a/pkg/engine/inspector_test.go b/pkg/engine/inspector_test.go index 26ea37e56a8..1c4e6decec5 100644 --- a/pkg/engine/inspector_test.go +++ b/pkg/engine/inspector_test.go @@ -1318,3 +1318,22 @@ func TestFilterOutDuplicatedHelmVulnerabilities(t *testing.T) { }) } } + +// TestCheckQueryComment tests the checkQueryComment helper function. +func TestCheckQueryComment(t *testing.T) { + qil := model.QueryIgnoreLines{ + "abc-uuid": []int{10, 11, 12}, + "def-uuid": []int{20}, + } + // Should suppress + assert.True(t, checkQueryComment(10, "abc-uuid", qil)) + assert.True(t, checkQueryComment(11, "abc-uuid", qil)) + assert.True(t, checkQueryComment(12, "abc-uuid", qil)) + assert.True(t, checkQueryComment(20, "def-uuid", qil)) + // Should NOT suppress (wrong query) + assert.False(t, checkQueryComment(10, "def-uuid", qil)) + // Should NOT suppress (wrong line) + assert.False(t, checkQueryComment(99, "abc-uuid", qil)) + // Should NOT suppress (nil map) + assert.False(t, checkQueryComment(10, "abc-uuid", nil)) +} diff --git a/pkg/engine/provider/filesystem_test.go b/pkg/engine/provider/filesystem_test.go index c20163c3765..865280962b5 100644 --- a/pkg/engine/provider/filesystem_test.go +++ b/pkg/engine/provider/filesystem_test.go @@ -10,7 +10,6 @@ import ( "testing" "github.com/Checkmarx/kics/v2/pkg/model" - dockerParser "github.com/Checkmarx/kics/v2/pkg/parser/docker" "github.com/Checkmarx/kics/v2/test" "github.com/pkg/errors" "github.com/stretchr/testify/require" @@ -102,7 +101,7 @@ func TestFileSystemSourceProvider_GetSources(t *testing.T) { //nolint args: args{ ctx: nil, extensions: model.Extensions{ - ".dockerfile": dockerParser.Parser{}, + ".dockerfile": struct{}{}, }, sink: mockSink, resolverSink: mockErrResolverSink, @@ -118,7 +117,7 @@ func TestFileSystemSourceProvider_GetSources(t *testing.T) { //nolint args: args{ ctx: nil, extensions: model.Extensions{ - ".dockerfile": dockerParser.Parser{}, + ".dockerfile": struct{}{}, }, sink: mockErrSink, resolverSink: mockErrResolverSink, @@ -134,7 +133,7 @@ func TestFileSystemSourceProvider_GetSources(t *testing.T) { //nolint args: args{ ctx: nil, extensions: model.Extensions{ - ".dockerfile": dockerParser.Parser{}, + ".dockerfile": struct{}{}, }, sink: mockSink, resolverSink: mockResolverSink, @@ -150,7 +149,7 @@ func TestFileSystemSourceProvider_GetSources(t *testing.T) { //nolint args: args{ ctx: nil, extensions: model.Extensions{ - ".dockerfile": dockerParser.Parser{}, + ".dockerfile": struct{}{}, }, sink: mockSink, resolverSink: mockResolverSink, @@ -182,7 +181,7 @@ func TestFileSystemSourceProvider_GetSources(t *testing.T) { //nolint ctx: nil, queryName: "template", extensions: model.Extensions{ - ".dockerfile": dockerParser.Parser{}, + ".dockerfile": struct{}{}, }, sink: mockSink, resolverSink: mockErrResolverSink, @@ -198,7 +197,7 @@ func TestFileSystemSourceProvider_GetSources(t *testing.T) { //nolint args: args{ ctx: nil, extensions: model.Extensions{ - ".dockerfile": dockerParser.Parser{}, + ".dockerfile": struct{}{}, }, sink: mockSink, resolverSink: mockResolverSink, @@ -312,7 +311,7 @@ func TestFileSystemSourceProvider_checkConditions(t *testing.T) { args: args{ info: infoFile, extensions: model.Extensions{ - ".dockerfile": dockerParser.Parser{}, + ".dockerfile": struct{}{}, }, path: filepath.FromSlash("assets/queries"), }, @@ -364,7 +363,7 @@ func TestFileSystemSourceProvider_checkConditions(t *testing.T) { args: args{ info: infoFile, extensions: model.Extensions{ - ".dockerfile": dockerParser.Parser{}, + ".dockerfile": struct{}{}, }, path: filepath.FromSlash("assets/queries"), }, diff --git a/pkg/kics/resolver_sink.go b/pkg/kics/resolver_sink.go index 907f68a6b38..d49d7c8ed31 100644 --- a/pkg/kics/resolver_sink.go +++ b/pkg/kics/resolver_sink.go @@ -91,6 +91,7 @@ func (s *Service) resolverSink( Commands: fileCommands, IDInfo: rfile.IDInfo, LinesIgnore: documents.IgnoreLines, + QueryLinesIgnore: documents.QueryIgnoreLines, ResolvedFiles: documents.ResolvedFiles, LinesOriginalData: utils.SplitLines(string(rfile.OriginalData)), IsMinified: documents.IsMinified, diff --git a/pkg/kics/sink.go b/pkg/kics/sink.go index ce7e054f26c..b443670f224 100644 --- a/pkg/kics/sink.go +++ b/pkg/kics/sink.go @@ -92,6 +92,7 @@ func (s *Service) sink(ctx context.Context, filename, scanID string, FilePath: filename, Commands: fileCommands, LinesIgnore: documents.IgnoreLines, + QueryLinesIgnore: documents.QueryIgnoreLines, ResolvedFiles: documents.ResolvedFiles, LinesOriginalData: utils.SplitLines(documents.Content), IsMinified: documents.IsMinified, diff --git a/pkg/model/comment_yaml.go b/pkg/model/comment_yaml.go index 3c5f504b653..bbd775980b3 100644 --- a/pkg/model/comment_yaml.go +++ b/pkg/model/comment_yaml.go @@ -15,6 +15,8 @@ type comment string type Ignore struct { // Lines is the lines to ignore Lines []int + // QueryLines maps a query UUID to the set of line numbers suppressed for that query + QueryLines QueryIgnoreLines } var ( @@ -30,14 +32,37 @@ func (i *Ignore) build(lines []int) { i.Lines = append(i.Lines, lines...) } +// buildQuery appends lines to QueryLines[queryID] +func (i *Ignore) buildQuery(queryID string, lines []int) { + defer memoryMu.Unlock() + memoryMu.Lock() + if i.QueryLines == nil { + i.QueryLines = make(QueryIgnoreLines) + } + i.QueryLines[queryID] = append(i.QueryLines[queryID], lines...) +} + // GetLines returns the lines to ignore func (i *Ignore) GetLines() []int { return RemoveDuplicates(i.Lines) } +// GetQueryLines returns the per-query lines to ignore +func (i *Ignore) GetQueryLines() QueryIgnoreLines { + if i.QueryLines == nil { + return QueryIgnoreLines{} + } + result := make(QueryIgnoreLines, len(i.QueryLines)) + for k, v := range i.QueryLines { + result[k] = RemoveDuplicates(v) + } + return result +} + // Reset resets the ignore struct func (i *Ignore) Reset() { i.Lines = make([]int, 0) + i.QueryLines = make(QueryIgnoreLines) } // ignoreCommentsYAML sets the lines to ignore for a yaml file @@ -66,11 +91,22 @@ func ignoreCommentsYAML(node *yaml.Node) { // processCommentYAML returns the lines to ignore func processCommentYAML(comment *comment, position int, content *yaml.Node, kind yaml.Kind, isFooter bool) (linesIgnore []int) { linesIgnore = make([]int, 0) - switch com := (*comment).value(); com { + com, queryID := (*comment).value() + switch com { case IgnoreLine: linesIgnore = append(linesIgnore, processLine(kind, content, position)...) case IgnoreBlock: linesIgnore = append(linesIgnore, processBlock(kind, content.Content, position)...) + case IgnoreLineQuery: + if queryID != "" { + lines := processLine(kind, content, position) + NewIgnore.buildQuery(queryID, lines) + } + case IgnoreBlockQuery: + if queryID != "" { + lines := processBlock(kind, content.Content, position) + NewIgnore.buildQuery(queryID, lines) + } default: linesIgnore = append(linesIgnore, processRegularLine(string(*comment), content, position, isFooter)...) } @@ -193,8 +229,8 @@ func getNodeLastLine(node *yaml.Node) (lastLine int) { return } -// value returns the value of the comment -func (c *comment) value() (value CommentCommand) { +// value returns the value of the comment and, for query-specific commands, the query UUID. +func (c *comment) value() (value CommentCommand, queryID string) { comment := strings.ToLower(string(*c)) if isHelm(comment) { res := KICSGetContentCommentRgxp.FindString(comment) @@ -207,10 +243,10 @@ func (c *comment) value() (value CommentCommand) { comment = KICSCommentRgxp.ReplaceAllString(comment, "") comment = strings.Trim(comment, "\n") commands := strings.Split(strings.Trim(comment, "\r"), " ") - value = ProcessCommands(commands) + value, queryID = ProcessCommands(commands) return } - return CommentCommand(comment) + return CommentCommand(comment), "" } func isHelm(comment string) bool { diff --git a/pkg/model/comment_yaml_test.go b/pkg/model/comment_yaml_test.go index 667ffddcb67..1c09e40ec3a 100644 --- a/pkg/model/comment_yaml_test.go +++ b/pkg/model/comment_yaml_test.go @@ -748,7 +748,7 @@ func Test_value(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - res := tt.input.value() + res, _ := tt.input.value() assert.Equal(t, string(res), tt.want) }) } diff --git a/pkg/model/comments.go b/pkg/model/comments.go index 646526a05df..b8ae4a5743b 100644 --- a/pkg/model/comments.go +++ b/pkg/model/comments.go @@ -14,19 +14,29 @@ func RemoveDuplicates(lines []int) []int { } // ProcessCommands processes a slice of commands. -func ProcessCommands(commands []string) CommentCommand { +// Returns the matched CommentCommand and, for query-specific commands, the query UUID (empty otherwise). +func ProcessCommands(commands []string) (CommentCommand, string) { for _, command := range commands { + // Check for query-specific ignore-line= or ignore-block= + if m := KICSIgnoreQueryRgxp.FindStringSubmatch(command); m != nil { + prefix := m[1] + uuid := m[2] + if prefix == string(IgnoreLine) { + return IgnoreLineQuery, uuid + } + return IgnoreBlockQuery, uuid + } switch com := CommentCommand(command); com { case IgnoreLine: - return IgnoreLine + return IgnoreLine, "" case IgnoreBlock: - return IgnoreBlock + return IgnoreBlock, "" default: continue } } - return CommentCommand(commands[0]) + return CommentCommand(commands[0]), "" } // Range returns a slice of lines between the start and end line numbers. diff --git a/pkg/model/comments_test.go b/pkg/model/comments_test.go index 717ee772391..3e73607e730 100644 --- a/pkg/model/comments_test.go +++ b/pkg/model/comments_test.go @@ -66,7 +66,7 @@ func TestProcessCommands(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := ProcessCommands(tt.args.commands); got != tt.want { + if got, _ := ProcessCommands(tt.args.commands); got != tt.want { t.Errorf("ProcessCommands() = %v, want %v", got, tt.want) } }) diff --git a/pkg/model/model.go b/pkg/model/model.go index c8cca5744a7..30ec00a0a4c 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -26,9 +26,11 @@ const ( // Constants to describe commands given from comments const ( - IgnoreLine CommentCommand = "ignore-line" - IgnoreBlock CommentCommand = "ignore-block" - IgnoreComment CommentCommand = "ignore-comment" + IgnoreLine CommentCommand = "ignore-line" + IgnoreBlock CommentCommand = "ignore-block" + IgnoreComment CommentCommand = "ignore-comment" + IgnoreLineQuery CommentCommand = "ignore-line-query" + IgnoreBlockQuery CommentCommand = "ignore-block-query" ) // Constants to describe vulnerability's severity @@ -72,7 +74,9 @@ var ( // KICSGetContentCommentRgxp to gets the kics comment on the hel case KICSGetContentCommentRgxp = regexp.MustCompile(`(^|\n)((/{2})|#|;)*\s*kics-scan([^\n]*)\n`) // KICSCommentRgxpYaml is the regexp to identify if the comment has KICS comment at the end of the comment in YAML - KICSCommentRgxpYaml = regexp.MustCompile(`((/{2})|#)*\s*kics-scan\s*(ignore-line|ignore-block)\s*\n*$`) + KICSCommentRgxpYaml = regexp.MustCompile(`((/{2})|#)*\s*kics-scan\s*(ignore-line|ignore-block)(=[0-9a-fA-F-]+)?\s*\n*$`) + // KICSIgnoreQueryRgxp is the regexp to extract the query UUID from an ignore-line= or ignore-block= command + KICSIgnoreQueryRgxp = regexp.MustCompile(`^(ignore-line|ignore-block)=([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$`) ) // Version - is the model for the version response @@ -103,8 +107,8 @@ type IssueType string // CodeLine is the lines containing and adjacent to the vulnerability line with their respective positions type CodeLine struct { - Position int - Line string + Position int `json:"line_no"` + Line string `json:"line"` } // ExtractedPathObject is the struct that contains the path location of extracted source @@ -117,6 +121,9 @@ type ExtractedPathObject struct { // CommentsCommands list of commands on a file that will be parsed type CommentsCommands map[string]string +// QueryIgnoreLines maps a query UUID to the set of line numbers suppressed for that query. +type QueryIgnoreLines map[string][]int + // FileMetadata is a representation of basic information and content of a file type FileMetadata struct { ID string `db:"id"` @@ -131,6 +138,7 @@ type FileMetadata struct { IDInfo map[int]interface{} Commands CommentsCommands LinesIgnore []int + QueryLinesIgnore QueryIgnoreLines ResolvedFiles map[string]ResolvedFile LinesOriginalData *[]string IsMinified bool diff --git a/pkg/model/model_yaml.go b/pkg/model/model_yaml.go index 5639b29e972..7903c951530 100644 --- a/pkg/model/model_yaml.go +++ b/pkg/model/model_yaml.go @@ -57,6 +57,29 @@ func GetIgnoreLines(file *FileMetadata) []int { return ignoreLines } +// GetQueryIgnoreLines returns a map of query UUID to suppressed lines for the given YAML file. +func GetQueryIgnoreLines(file *FileMetadata) QueryIgnoreLines { + if !utils.Contains(filepath.Ext(file.FilePath), []string{".yml", ".yaml"}) { + return file.QueryLinesIgnore + } + + NewIgnore.Reset() + var node yaml.Node + + if err := yaml.Unmarshal([]byte(file.OriginalData), &node); err != nil { + log.Info().Msgf("failed to unmarshal file: %s", err) + return file.QueryLinesIgnore + } + + if node.Kind == 1 && len(node.Content) == 1 { + visited := make(map[*yaml.Node]interface{}) + _ = unmarshalWithVisited(node.Content[0], visited) + return NewIgnore.GetQueryLines() + } + + return file.QueryLinesIgnore +} + /* YAML Node TYPES diff --git a/pkg/model/summary.go b/pkg/model/summary.go index fd139f2266c..0e5186ead56 100644 --- a/pkg/model/summary.go +++ b/pkg/model/summary.go @@ -25,7 +25,7 @@ type VulnerableFile struct { SimilarityID string `json:"similarity_id"` OldSimilarityID string `json:"old_similarity_id,omitempty"` Line int `json:"line"` - VulnLines *[]CodeLine `json:"-"` + VulnLines *[]CodeLine `json:"vuln_lines"` ResourceType string `json:"resource_type,omitempty"` ResourceName string `json:"resource_name,omitempty"` IssueType IssueType `json:"issue_type"` diff --git a/pkg/parser/docker/comments.go b/pkg/parser/docker/comments.go index 52f0d6cda75..a35ce48c360 100644 --- a/pkg/parser/docker/comments.go +++ b/pkg/parser/docker/comments.go @@ -9,15 +9,17 @@ import ( // ignore is a structure that contains information about the lines that are being ignored. type ignore struct { - from map[string]bool - lines []int + from map[string]bool + lines []int + queryLines model.QueryIgnoreLines } // newIgnore returns a new ignore struct. func newIgnore() *ignore { return &ignore{ - from: make(map[string]bool), - lines: make([]int, 0), + from: make(map[string]bool), + lines: make([]int, 0), + queryLines: make(model.QueryIgnoreLines), } } @@ -45,12 +47,24 @@ func (i *ignore) getIgnoreComments(node *parser.Node) (ignore bool) { } for idx, comment := range node.PrevComment { - switch processComment(comment) { + cmd, queryID := processComment(comment) + switch cmd { case model.IgnoreLine: i.lines = append(i.lines, model.Range(node.StartLine-(idx+1), node.EndLine)...) case model.IgnoreBlock: i.lines = append(i.lines, node.StartLine-(idx+1)) ignore = true + case model.IgnoreLineQuery: + if queryID != "" { + lines := model.Range(node.StartLine-(idx+1), node.EndLine) + i.queryLines[queryID] = append(i.queryLines[queryID], lines...) + } + i.lines = append(i.lines, node.StartLine-(idx+1)) + case model.IgnoreBlockQuery: + if queryID != "" { + i.queryLines[queryID] = append(i.queryLines[queryID], node.StartLine-(idx+1)) + } + i.lines = append(i.lines, node.StartLine-(idx+1)) default: i.lines = append(i.lines, node.StartLine-(idx+1)) } @@ -59,15 +73,24 @@ func (i *ignore) getIgnoreComments(node *parser.Node) (ignore bool) { return } -// processComment returns the type of comment given. -func processComment(comment string) (value model.CommentCommand) { +// getQueryIgnoreLines returns the per-query suppressed lines. +func (i *ignore) getQueryIgnoreLines() model.QueryIgnoreLines { + result := make(model.QueryIgnoreLines, len(i.queryLines)) + for k, v := range i.queryLines { + result[k] = model.RemoveDuplicates(v) + } + return result +} + +// processComment returns the type of comment given and, for query-specific commands, the query UUID. +func processComment(comment string) (value model.CommentCommand, queryID string) { commentLower := strings.ToLower(comment) if model.KICSCommentRgxp.MatchString(commentLower) { commentLower = model.KICSCommentRgxp.ReplaceAllString(commentLower, "") commands := strings.Split(strings.Trim(commentLower, "\n"), " ") - value = model.ProcessCommands(commands) + value, queryID = model.ProcessCommands(commands) return } - return model.CommentCommand(comment) + return model.CommentCommand(comment), "" } diff --git a/pkg/parser/docker/comments_test.go b/pkg/parser/docker/comments_test.go index e9ce74e82f2..c16ea367488 100644 --- a/pkg/parser/docker/comments_test.go +++ b/pkg/parser/docker/comments_test.go @@ -4,6 +4,7 @@ import ( "reflect" "testing" + "github.com/Checkmarx/kics/v2/pkg/model" "github.com/moby/buildkit/frontend/dockerfile/parser" "github.com/stretchr/testify/require" ) @@ -17,8 +18,9 @@ func Test_newIgnore(t *testing.T) { { name: "new ignore", want: &ignore{ - from: make(map[string]bool), - lines: make([]int, 0), + from: make(map[string]bool), + lines: make([]int, 0), + queryLines: make(model.QueryIgnoreLines), }, }, } diff --git a/pkg/parser/docker/parser.go b/pkg/parser/docker/parser.go index 7f97835b07e..f9c800679df 100644 --- a/pkg/parser/docker/parser.go +++ b/pkg/parser/docker/parser.go @@ -13,6 +13,7 @@ import ( // Parser is a Dockerfile parser type Parser struct { + queryIgnoreLines model.QueryIgnoreLines } // Resource Separates the list of commands by file @@ -122,10 +123,16 @@ func (p *Parser) Parse(_ string, fileContent []byte) ([]model.Document, []int, e documents = append(documents, *doc) ignoreLines := ignoreStruct.getIgnoreLines() + p.queryIgnoreLines = ignoreStruct.getQueryIgnoreLines() return documents, ignoreLines, nil } +// GetQueryIgnoreLines returns the per-query suppressed lines from the last Parse call. +func (p *Parser) GetQueryIgnoreLines() model.QueryIgnoreLines { + return p.queryIgnoreLines +} + // GetKind returns the kind of the parser func (p *Parser) GetKind() model.FileKind { return model.KindDOCKER diff --git a/pkg/parser/grpc/converter/converter.go b/pkg/parser/grpc/converter/converter.go index 58a7acaa37a..4443b87c5d7 100644 --- a/pkg/parser/grpc/converter/converter.go +++ b/pkg/parser/grpc/converter/converter.go @@ -582,7 +582,7 @@ func (j *JSONProto) processCommentProto(comment *proto.Comment, lineStart int, e comment = model.KICSCommentRgxp.ReplaceAllString(comment, "") comment = strings.Trim(comment, "\n") commands := strings.Split(strings.Trim(comment, "\r"), " ") - value = model.ProcessCommands(commands) + value, _ = model.ProcessCommands(commands) } continue } diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 4615b735a84..81176e13ed8 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -22,6 +22,12 @@ type kindParser interface { GetResolvedFiles() map[string]model.ResolvedFile } +// queryIgnoreLinesProvider is an optional interface that parsers can implement +// to provide per-query suppressed line information. +type queryIgnoreLinesProvider interface { + GetQueryIgnoreLines() model.QueryIgnoreLines +} + // Builder is a representation of parsers that will be construct type Builder struct { parsers []kindParser @@ -76,13 +82,14 @@ type Parser struct { // ParsedDocument is a struct containing data retrieved from parsing type ParsedDocument struct { - Docs []model.Document - Kind model.FileKind - Content string - IgnoreLines []int - CountLines int - ResolvedFiles map[string]model.ResolvedFile - IsMinified bool + Docs []model.Document + Kind model.FileKind + Content string + IgnoreLines []int + QueryIgnoreLines model.QueryIgnoreLines + CountLines int + ResolvedFiles map[string]model.ResolvedFile + IsMinified bool } // CommentsCommands gets commands on comments in the file beginning, before the code starts @@ -146,7 +153,7 @@ func (c *Parser) Parse( cont = string(fileContent) } - return ParsedDocument{ + pd := ParsedDocument{ Docs: obj, Kind: c.parsers.GetKind(), Content: cont, @@ -154,7 +161,11 @@ func (c *Parser) Parse( CountLines: bytes.Count(resolved, []byte{'\n'}) + 1, ResolvedFiles: c.parsers.GetResolvedFiles(), IsMinified: isMinified, - }, nil + } + if qp, ok := c.parsers.(queryIgnoreLinesProvider); ok { + pd.QueryIgnoreLines = qp.GetQueryIgnoreLines() + } + return pd, nil } return ParsedDocument{ Docs: nil, diff --git a/pkg/parser/terraform/comment/comment.go b/pkg/parser/terraform/comment/comment.go index 63c394f2263..ff742824ce4 100644 --- a/pkg/parser/terraform/comment/comment.go +++ b/pkg/parser/terraform/comment/comment.go @@ -16,18 +16,18 @@ func (c *comment) position() hcl.Pos { return hcl.Pos{Line: c.Range.End.Line + 1, Column: c.Range.End.Column, Byte: c.Range.End.Byte} } -// value returns the value of a comment -func (c *comment) value() (value model.CommentCommand) { +// value returns the value of a comment and, for query-specific commands, the query UUID. +func (c *comment) value() (value model.CommentCommand, queryID string) { comment := strings.ToLower(string(c.Bytes)) // check if we are working with kics command if model.KICSCommentRgxp.MatchString(comment) { comment = model.KICSCommentRgxp.ReplaceAllString(comment, "") comment = strings.Trim(comment, "\n") commands := strings.Split(strings.Trim(comment, "\r"), " ") - value = model.ProcessCommands(commands) + value, queryID = model.ProcessCommands(commands) return } - return model.CommentCommand(comment) + return model.CommentCommand(comment), "" } // Ignore is a map of commands to ignore @@ -44,6 +44,9 @@ func (i *Ignore) build(ignoreLine, ignoreBlock, ignoreComment []hcl.Pos) { *i = ignoreStruct } +// QueryIgnore maps a query UUID to command-specific positions. +type QueryIgnore map[string]map[model.CommentCommand][]hcl.Pos + // /////////////////////////// // LINES TO IGNORE // // /////////////////////////// @@ -114,22 +117,23 @@ func checkBlockRange(block *hcl.Block, position hcl.Pos) bool { // /////////////////////////// // ParseComments parses the comments and returns the kics commands -func ParseComments(src []byte, filename string) (Ignore, error) { +func ParseComments(src []byte, filename string) (Ignore, QueryIgnore, error) { comments, diags := hclsyntax.LexConfig(src, filename, hcl.Pos{Line: 0, Column: 0}) if diags != nil && diags.HasErrors() { - return Ignore{}, diags.Errs()[0] + return Ignore{}, QueryIgnore{}, diags.Errs()[0] } - ig := processTokens(comments) + ig, qi := processTokens(comments) - return ig, nil + return ig, qi, nil } // processTokens goes over the tokens and returns the kics commands -func processTokens(tokens hclsyntax.Tokens) (ig Ignore) { +func processTokens(tokens hclsyntax.Tokens) (ig Ignore, qi QueryIgnore) { ignoreLines := make([]hcl.Pos, 0) ignoreBlocks := make([]hcl.Pos, 0) ignoreComments := make([]hcl.Pos, 0) + qi = make(QueryIgnore) for i := range tokens { // token is not a comment if tokens[i].Type != hclsyntax.TokenComment || i+1 > len(tokens) { @@ -140,27 +144,52 @@ func processTokens(tokens hclsyntax.Tokens) (ig Ignore) { continue } ignoreLines, ignoreBlocks, ignoreComments = processComment((*comment)(&tokens[i]), - (*comment)(&tokens[i+1]), ignoreLines, ignoreBlocks, ignoreComments) + (*comment)(&tokens[i+1]), ignoreLines, ignoreBlocks, ignoreComments, qi) } ig = make(map[model.CommentCommand][]hcl.Pos) ig.build(ignoreLines, ignoreBlocks, ignoreComments) - return ig + return ig, qi } // processComment analyzes the comment to determine which type of kics command the comment is func processComment(comment *comment, tokenToIgnore *comment, - ignoreLine, ignoreBlock, ignoreComments []hcl.Pos) (ignoreLineR, ignoreBlockR, ignoreCommentsR []hcl.Pos) { + ignoreLine, ignoreBlock, ignoreComments []hcl.Pos, qi QueryIgnore) (ignoreLineR, ignoreBlockR, ignoreCommentsR []hcl.Pos) { ignoreLineR = ignoreLine ignoreBlockR = ignoreBlock ignoreCommentsR = ignoreComments - switch comment.value() { + cmd, queryID := comment.value() + switch cmd { case model.IgnoreLine: // comment is of type kics ignore-line ignoreLineR = append(ignoreLineR, tokenToIgnore.position(), hcl.Pos{Line: comment.position().Line - 1}) case model.IgnoreBlock: // comment is of type kics ignore-block ignoreBlockR = append(ignoreBlockR, tokenToIgnore.position(), hcl.Pos{Line: comment.position().Line - 1}) + case model.IgnoreLineQuery: + // comment is of type kics ignore-line= + if queryID != "" { + if qi[queryID] == nil { + qi[queryID] = make(map[model.CommentCommand][]hcl.Pos) + } + qi[queryID][model.IgnoreLineQuery] = append(qi[queryID][model.IgnoreLineQuery], + tokenToIgnore.position(), hcl.Pos{Line: comment.position().Line - 1}) + } + // treat as a comment line (not a global ignore) + ignoreCommentsR = append(ignoreCommentsR, hcl.Pos{Line: comment.position().Line - 1}) + return + case model.IgnoreBlockQuery: + // comment is of type kics ignore-block= + if queryID != "" { + if qi[queryID] == nil { + qi[queryID] = make(map[model.CommentCommand][]hcl.Pos) + } + qi[queryID][model.IgnoreBlockQuery] = append(qi[queryID][model.IgnoreBlockQuery], + tokenToIgnore.position(), hcl.Pos{Line: comment.position().Line - 1}) + } + // treat as a comment line (not a global ignore) + ignoreCommentsR = append(ignoreCommentsR, hcl.Pos{Line: comment.position().Line - 1}) + return default: // comment is not of type kics ignore ignoreCommentsR = append(ignoreCommentsR, hcl.Pos{Line: comment.position().Line - 1}) @@ -169,3 +198,23 @@ func processComment(comment *comment, tokenToIgnore *comment, return } + +// GetQueryIgnoreLines resolves QueryIgnore positions to line numbers per query UUID +func GetQueryIgnoreLines(qi QueryIgnore, body *hclsyntax.Body) model.QueryIgnoreLines { + result := make(model.QueryIgnoreLines) + for queryID, cmdMap := range qi { + lines := make([]int, 0) + for cmd, positions := range cmdMap { + switch cmd { + case model.IgnoreLineQuery: + lines = append(lines, getLinesFromPos(positions)...) + case model.IgnoreBlockQuery: + for _, position := range positions { + lines = append(lines, checkBlock(body, position)...) + } + } + } + result[queryID] = model.RemoveDuplicates(lines) + } + return result +} diff --git a/pkg/parser/terraform/comment/comment_test.go b/pkg/parser/terraform/comment/comment_test.go index 1c0a8c0ad5d..775c6feda2f 100644 --- a/pkg/parser/terraform/comment/comment_test.go +++ b/pkg/parser/terraform/comment/comment_test.go @@ -123,7 +123,7 @@ func TestComment_ParseComments(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ParseComments(tt.content, tt.filename) + got, _, err := ParseComments(tt.content, tt.filename) if (err != nil) != tt.wantErr { t.Errorf("ParseComments() error = %v, wantErr %v", err, tt.wantErr) return @@ -177,7 +177,7 @@ func TestComment_GetIgnoreLines(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ignore, err := ParseComments(tt.content, tt.filename) + ignore, _, err := ParseComments(tt.content, tt.filename) require.NoError(t, err) file, diagnostics := hclsyntax.ParseConfig(tt.content, tt.filename, hcl.Pos{Byte: 0, Line: 1, Column: 1}) require.False(t, diagnostics.HasErrors()) diff --git a/pkg/parser/terraform/terraform.go b/pkg/parser/terraform/terraform.go index 6354f6bfae5..b40b34d6d1f 100644 --- a/pkg/parser/terraform/terraform.go +++ b/pkg/parser/terraform/terraform.go @@ -25,9 +25,10 @@ type Converter func(file *hcl.File, inputVariables converter.VariableMap) (model // Parser struct that contains the function to parse file and the number of retries if something goes wrong type Parser struct { - convertFunc Converter - numOfRetries int - terraformVarsPath string + convertFunc Converter + numOfRetries int + terraformVarsPath string + queryIgnoreLines model.QueryIgnoreLines } // NewDefault initializes a parser with Parser default values @@ -179,12 +180,14 @@ func (p *Parser) Parse(path string, content []byte) ([]model.Document, []int, er return nil, []int{}, err } - ignore, err := comment.ParseComments(content, path) + ignore, qi, err := comment.ParseComments(content, path) if err != nil { log.Err(err).Msg("failed to parse comments") } - linesToIgnore := comment.GetIgnoreLines(ignore, file.Body.(*hclsyntax.Body)) + body := file.Body.(*hclsyntax.Body) + linesToIgnore := comment.GetIgnoreLines(ignore, body) + p.queryIgnoreLines = comment.GetQueryIgnoreLines(qi, body) fc, parseErr := p.convertFunc(file, inputVariableMap) json, err := addExtraInfo([]model.Document{fc}, path) @@ -224,3 +227,8 @@ func (p *Parser) StringifyContent(content []byte) (string, error) { func (p *Parser) GetResolvedFiles() map[string]model.ResolvedFile { return make(map[string]model.ResolvedFile) } + +// GetQueryIgnoreLines returns the per-query suppressed lines from the last Parse call. +func (p *Parser) GetQueryIgnoreLines() model.QueryIgnoreLines { + return p.queryIgnoreLines +} diff --git a/pkg/parser/yaml/parser.go b/pkg/parser/yaml/parser.go index 7999f7ab8e1..ebbdedfdaa2 100644 --- a/pkg/parser/yaml/parser.go +++ b/pkg/parser/yaml/parser.go @@ -196,6 +196,11 @@ func emptyDocument() *model.Document { return &model.Document{} } +// GetQueryIgnoreLines returns the per-query suppressed lines collected during the last Parse call. +func (p *Parser) GetQueryIgnoreLines() model.QueryIgnoreLines { + return model.NewIgnore.GetQueryLines() +} + // GetResolvedFiles returns resolved files func (p *Parser) GetResolvedFiles() map[string]model.ResolvedFile { return p.resolvedFiles diff --git a/tmp/kics-test/not-suppressed.yaml b/tmp/kics-test/not-suppressed.yaml new file mode 100644 index 00000000000..dd2290e587e --- /dev/null +++ b/tmp/kics-test/not-suppressed.yaml @@ -0,0 +1,9 @@ +on: + pull_request_target: +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} \ No newline at end of file diff --git a/tmp/kics-test/suppressed.yaml b/tmp/kics-test/suppressed.yaml new file mode 100644 index 00000000000..1e63d4c2b29 --- /dev/null +++ b/tmp/kics-test/suppressed.yaml @@ -0,0 +1,11 @@ +on: + pull_request_target: +jobs: + build: + runs-on: ubuntu-latest + steps: + # kics-scan ignore-block=d3a8b4c1-f2e7-4a9b-8c5d-1e6f0a2b3c4d + - uses: actions/checkout@v3 + with: + # kics-scan ignore-line=d3a8b4c1-f2e7-4a9b-8c5d-1e6f0a2b3c4d + ref: ${{ github.event.pull_request.head.sha }} \ No newline at end of file