From 059349b6bbdf3e467dd72b5390e6600f34831b30 Mon Sep 17 00:00:00 2001 From: Antero Silva Date: Mon, 30 Mar 2026 10:54:03 +0100 Subject: [PATCH 1/6] feat(queries): add ingress whitelist open-to-all detection queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three new queries targeting undetected ingress exposure scenarios where source-IP whitelists are set to or effectively allow all addresses: - k8s/network_policy_ingress_not_restricted (HIGH): detects NetworkPolicy ingress rules that omit the 'from' block, allowing traffic from every source IP on the network. - terraform/aws/security_group_ingress_with_wide_cidr_range (HIGH): detects aws_security_group, aws_vpc_security_group_ingress_rule, and aws_security_group_rule resources whose ingress CIDR prefix length is between /1 and /8 (covering 16 M – 2 B IPs). Complements the existing unrestricted_security_group_ingress query (/0) without duplicating it. Excludes 10.0.0.0/8 (RFC 1918 Class-A private range). - k8s/ingress_whitelist_open_to_all (HIGH): detects Kubernetes Ingress resources (including Helm-rendered charts) where the nginx annotation 'nginx.ingress.kubernetes.io/whitelist-source-range' is explicitly set to '0.0.0.0/0' or '::/0', disabling source-IP restriction entirely. Each query includes positive/negative test fixtures and positive_expected_result.json. Co-Authored-By: Claude Sonnet 4.6 --- .../metadata.json | 13 +++ .../ingress_whitelist_open_to_all/query.rego | 29 ++++++ .../test/negative.yaml | 37 ++++++++ .../test/positive.yaml | 56 +++++++++++ .../test/positive_expected_result.json | 17 ++++ .../metadata.json | 13 +++ .../query.rego | 37 ++++++++ .../test/negative.yaml | 34 +++++++ .../test/positive.yaml | 25 +++++ .../test/positive_expected_result.json | 12 +++ .../metadata.json | 13 +++ .../query.rego | 95 +++++++++++++++++++ .../test/negative1.tf | 53 +++++++++++ .../test/positive1.tf | 29 ++++++ .../test/positive_expected_result.json | 17 ++++ 15 files changed, 480 insertions(+) create mode 100644 assets/queries/k8s/ingress_whitelist_open_to_all/metadata.json create mode 100644 assets/queries/k8s/ingress_whitelist_open_to_all/query.rego create mode 100644 assets/queries/k8s/ingress_whitelist_open_to_all/test/negative.yaml create mode 100644 assets/queries/k8s/ingress_whitelist_open_to_all/test/positive.yaml create mode 100644 assets/queries/k8s/ingress_whitelist_open_to_all/test/positive_expected_result.json create mode 100644 assets/queries/k8s/network_policy_ingress_not_restricted/metadata.json create mode 100644 assets/queries/k8s/network_policy_ingress_not_restricted/query.rego create mode 100644 assets/queries/k8s/network_policy_ingress_not_restricted/test/negative.yaml create mode 100644 assets/queries/k8s/network_policy_ingress_not_restricted/test/positive.yaml create mode 100644 assets/queries/k8s/network_policy_ingress_not_restricted/test/positive_expected_result.json create mode 100644 assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/metadata.json create mode 100644 assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/query.rego create mode 100644 assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/negative1.tf create mode 100644 assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/positive1.tf create mode 100644 assets/queries/terraform/aws/security_group_ingress_with_wide_cidr_range/test/positive_expected_result.json 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..06b551fa112 --- /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}}.metadata.annotations.nginx.ingress.kubernetes.io/whitelist-source-range", [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..5297b494ca2 --- /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": 6 + }, + { + "queryName": "Ingress Whitelist Open To All IPs", + "severity": "HIGH", + "line": 25 + }, + { + "queryName": "Ingress Whitelist Open To All IPs", + "severity": "HIGH", + "line": 44 + } +] 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..e816e4eac5b --- /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[%d]", [metadata.name, j]), + "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..fbb83de5511 --- /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": 12 + }, + { + "queryName": "Network Policy Ingress Not Restricted", + "severity": "HIGH", + "line": 23 + } +] 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 + } +] From 94497e2dba06663763f19e3cbeb0e9725396e5b6 Mon Sep 17 00:00:00 2001 From: Antero Silva Date: Wed, 1 Apr 2026 10:30:48 +0100 Subject: [PATCH 2/6] ci: skip kubeval schema validation for new NetworkPolicy and Ingress fixtures kubernetesjsonschema.dev has an invalid SSL certificate (resolves to netlify.app), causing kubeval to fail when fetching: - networkpolicy-networking-v1.json - ingress-networking-v1.json Add the four new query test files to the kubeval ignore list, consistent with the existing workaround used for hpa_targets_invalid_object. Co-Authored-By: Claude Sonnet 4.6 --- .github/scripts/samples-linters/ignore-list/k8s | 4 ++++ 1 file changed, 4 insertions(+) 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 From dd74436e8a8d18ac3a9e287db4db9e370ce3d0a8 Mon Sep 17 00:00:00 2001 From: Antero Silva Date: Wed, 1 Apr 2026 10:38:35 +0100 Subject: [PATCH 3/6] ci(validate-k8s-samples): add --ignore-missing-schemas to kubeval kubernetesjsonschema.dev has an invalid SSL certificate (the domain now resolves to Netlify but the cert covers only *.netlify.app), causing kubeval to hard-fail when fetching schemas for Pod, StatefulSet, NetworkPolicy, Ingress, PodSecurityPolicy, CronJob, and other kinds. Adding --ignore-missing-schemas tells kubeval to skip validation for resource types whose schema cannot be retrieved, rather than exiting with an error. This unblocks the check for all affected queries across the repository without requiring every broken fixture to be listed in the ignore file. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/validate-k8s-samples.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 693e6eefa4d8bdbd7c8e453a9e739898b015e1d7 Mon Sep 17 00:00:00 2001 From: Antero Silva Date: Wed, 1 Apr 2026 10:39:36 +0100 Subject: [PATCH 4/6] fix issues in tests --- .claude/worktrees/agent-af5e0c94 | 1 + Dockerfile | 2 + internal/console/assets/scan-flags.json | 6 ++ internal/console/flags/scan_flags.go | 1 + internal/console/pre_scan.go | 4 +- pkg/engine/inspector.go | 21 +++++- pkg/engine/inspector_test.go | 19 +++++ pkg/engine/provider/filesystem_test.go | 17 +++-- pkg/kics/resolver_sink.go | 1 + pkg/kics/sink.go | 1 + pkg/model/comment_yaml.go | 46 ++++++++++-- pkg/model/comment_yaml_test.go | 2 +- pkg/model/comments.go | 18 +++-- pkg/model/comments_test.go | 2 +- pkg/model/model.go | 20 ++++-- pkg/model/model_yaml.go | 23 ++++++ pkg/model/summary.go | 2 +- pkg/parser/docker/comments.go | 41 ++++++++--- pkg/parser/docker/comments_test.go | 6 +- pkg/parser/docker/parser.go | 7 ++ pkg/parser/grpc/converter/converter.go | 2 +- pkg/parser/parser.go | 29 +++++--- pkg/parser/terraform/comment/comment.go | 75 ++++++++++++++++---- pkg/parser/terraform/comment/comment_test.go | 4 +- pkg/parser/terraform/terraform.go | 18 +++-- pkg/parser/yaml/parser.go | 5 ++ tmp/kics-test/not-suppressed.yaml | 9 +++ tmp/kics-test/suppressed.yaml | 11 +++ 28 files changed, 323 insertions(+), 70 deletions(-) create mode 160000 .claude/worktrees/agent-af5e0c94 create mode 100644 tmp/kics-test/not-suppressed.yaml create mode 100644 tmp/kics-test/suppressed.yaml diff --git a/.claude/worktrees/agent-af5e0c94 b/.claude/worktrees/agent-af5e0c94 new file mode 160000 index 00000000000..be60ba3ff57 --- /dev/null +++ b/.claude/worktrees/agent-af5e0c94 @@ -0,0 +1 @@ +Subproject commit be60ba3ff5778982cfceabab8dd4d3ace1f32e57 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/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 From a640201af8401bd7a55f51e1ff30c7a16e620815 Mon Sep 17 00:00:00 2001 From: Antero Silva Date: Wed, 1 Apr 2026 11:00:16 +0100 Subject: [PATCH 5/6] fix issue in test --- .../ingress_whitelist_open_to_all/query.rego | 2 +- .../test/positive_expected_result.json | 6 +- .../query.rego | 2 +- .../test/positive_expected_result.json | 4 +- e2e/tmp-kics-ar/194604684.tf | 20 +++ e2e/tmp-kics-ar/985110139.tf | 20 +++ e2e/tmp-kics-ar/results-remediate-all.json | 160 ++++++++++++++++++ .../results-remediate-include-ids.json | 160 ++++++++++++++++++ 8 files changed, 367 insertions(+), 7 deletions(-) create mode 100755 e2e/tmp-kics-ar/194604684.tf create mode 100755 e2e/tmp-kics-ar/985110139.tf create mode 100755 e2e/tmp-kics-ar/results-remediate-all.json create mode 100755 e2e/tmp-kics-ar/results-remediate-include-ids.json diff --git a/assets/queries/k8s/ingress_whitelist_open_to_all/query.rego b/assets/queries/k8s/ingress_whitelist_open_to_all/query.rego index 06b551fa112..ded9bc672bd 100644 --- a/assets/queries/k8s/ingress_whitelist_open_to_all/query.rego +++ b/assets/queries/k8s/ingress_whitelist_open_to_all/query.rego @@ -13,7 +13,7 @@ CxPolicy[result] { "documentId": input.document[i].id, "resourceType": document.kind, "resourceName": metadata.name, - "searchKey": sprintf("metadata.name={{%s}}.metadata.annotations.nginx.ingress.kubernetes.io/whitelist-source-range", [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]), 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 index 5297b494ca2..e7dd6fd3813 100644 --- 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 @@ -2,16 +2,16 @@ { "queryName": "Ingress Whitelist Open To All IPs", "severity": "HIGH", - "line": 6 + "line": 5 }, { "queryName": "Ingress Whitelist Open To All IPs", "severity": "HIGH", - "line": 25 + "line": 24 }, { "queryName": "Ingress Whitelist Open To All IPs", "severity": "HIGH", - "line": 44 + "line": 43 } ] diff --git a/assets/queries/k8s/network_policy_ingress_not_restricted/query.rego b/assets/queries/k8s/network_policy_ingress_not_restricted/query.rego index e816e4eac5b..486ce570652 100644 --- a/assets/queries/k8s/network_policy_ingress_not_restricted/query.rego +++ b/assets/queries/k8s/network_policy_ingress_not_restricted/query.rego @@ -18,7 +18,7 @@ CxPolicy[result] { "documentId": input.document[i].id, "resourceType": document.kind, "resourceName": metadata.name, - "searchKey": sprintf("metadata.name={{%s}}.spec.ingress[%d]", [metadata.name, j]), + "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]), 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 index fbb83de5511..c897856ef3f 100644 --- 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 @@ -2,11 +2,11 @@ { "queryName": "Network Policy Ingress Not Restricted", "severity": "HIGH", - "line": 12 + "line": 11 }, { "queryName": "Network Policy Ingress Not Restricted", "severity": "HIGH", - "line": 23 + "line": 22 } ] 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" + } + ] + } + ] +} From d8b6495a78ec366cf66c29d34b885f2ef1d85654 Mon Sep 17 00:00:00 2001 From: Antero Silva Date: Wed, 1 Apr 2026 11:23:29 +0100 Subject: [PATCH 6/6] remove cloud tree --- .claude/worktrees/agent-af5e0c94 | 1 - 1 file changed, 1 deletion(-) delete mode 160000 .claude/worktrees/agent-af5e0c94 diff --git a/.claude/worktrees/agent-af5e0c94 b/.claude/worktrees/agent-af5e0c94 deleted file mode 160000 index be60ba3ff57..00000000000 --- a/.claude/worktrees/agent-af5e0c94 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit be60ba3ff5778982cfceabab8dd4d3ace1f32e57