diff --git a/.github/scripts/validate-search-line/results_models.py b/.github/scripts/validate-search-line/results_models.py new file mode 100644 index 00000000000..449cd1d081c --- /dev/null +++ b/.github/scripts/validate-search-line/results_models.py @@ -0,0 +1,26 @@ +class ScanFile: + def __init__(self, data): + self.file_name = data.get("file_name", "") + self.similarity_id = data.get("similarity_id", "") + self.line = int(data.get("line")) + self.resource_type = data.get("resource_type", "") + self.resource_name = data.get("resource_name", "") + self.issue_type = data.get("issue_type", "") + self.search_key = data.get("search_key", "") + self.search_line = int(data.get("search_line")) + self.search_value = data.get("search_value", "") + self.expected_value = data.get("expected_value", "") + self.actual_value = data.get("actual_value", "") + + +class Query: + def __init__(self, data): + self.query_name = data.get("query_name", "") + self.query_id = data.get("query_id", "") + self.files = [ScanFile(f) for f in data.get("files", [])] + + +class ScanResults: + def __init__(self, data): + self.queries_failed_to_execute = int(data.get("queries_failed_to_execute", 0)) + self.queries = [Query(q) for q in data.get("queries", [])] diff --git a/.github/scripts/validate-search-line/validate_search_line.py b/.github/scripts/validate-search-line/validate_search_line.py new file mode 100644 index 00000000000..d8abc044222 --- /dev/null +++ b/.github/scripts/validate-search-line/validate_search_line.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 + +import json +import os +import subprocess +import sys +from pathlib import Path +from results_models import ScanResults + +REPO_ROOT = Path(__file__).resolve().parents[3] + +def get_changed_queries(): + """Parse CHANGED_QUERIES env var (JSON array from dorny/paths-filter) to get query directories.""" + raw = os.getenv("CHANGED_QUERIES", "") + if not raw: + print("::error::CHANGED_QUERIES environment variable is empty or not set") + sys.exit(1) + + try: + files = json.loads(raw) + except json.JSONDecodeError: + print(f"::error::CHANGED_QUERIES is not valid JSON: {raw}") + sys.exit(1) + + dirs = [] + for f in files: + if f.endswith("/query.rego"): + dirs.append(REPO_ROOT / Path(f).parent) + return dirs + + +def has_search_line_defined(query_dir): + """Check if query.rego defines searchLine in its result object.""" + rego_file = query_dir / "query.rego" + if not rego_file.exists(): + return False + return "searchLine" in rego_file.read_text() + + +def run_kics_scan(query_dir): + """Run KICS scan for a single query and return True if it completed successfully.""" + query_id = json.loads((query_dir / "metadata.json").read_text())["id"] + + results_dir = query_dir / "results" + results_dir.mkdir(exist_ok=True) + + payloads_dir = query_dir / "payloads" + payloads_dir.mkdir(exist_ok=True) + + cmd = [ + "go", "run", str(REPO_ROOT / "cmd" / "console" / "main.go"), + "scan", + "-p", str(query_dir / "test"), + "-o", str(results_dir), + "--output-name", "all_results.json", + "-i", query_id, + "-d", str(payloads_dir / "all_payloads.json"), + "-v", + "--experimental-queries", + "--bom", + "--enable-openapi-refs", + "--ignore-on-exit", "results", + "--kics_compute_new_simid" + ] + + print(f" Running scan with query ID: {query_id}") + + proc = subprocess.run(cmd, capture_output=True, text=True, cwd=str(REPO_ROOT)) + + if proc.returncode not in {0, 60, 50, 40, 30, 20}: + print(f" ::error::Scan failed (exit code {proc.returncode})") + if proc.stdout: + print(f" stdout (last 500 chars): ...{proc.stdout[-500:]}") + if proc.stderr: + print(f" stderr (last 500 chars): ...{proc.stderr[-500:]}") + return False + + return True + +def validate_scan_results(query_dir): + """ + Validate scan results: + - Fail if any search_line != line + - Fail if any search_line == -1 + """ + results_file = query_dir / "results" / "all_results.json" + rel_dir = query_dir.relative_to(REPO_ROOT) + + if not results_file.exists(): + print(f" ::error file={rel_dir}::Results file not generated by scan") + return False + + data = json.loads(results_file.read_text()) + scan_results = ScanResults(data) + + if scan_results.queries_failed_to_execute > 0: + print(f" ::error file={rel_dir}::{scan_results.queries_failed_to_execute} query(ies) failed to execute (possible panic/error during scan)") + return False + + # Flatten results from all queries + all_results = [] + for query in scan_results.queries: + all_results.extend(query.files) + + if not all_results: + print(" [OK] No results to validate") + return True + # Validate each result + valid = True + for idx, f in enumerate(all_results): + sl = int(f.search_line) + ln = int(f.line) + fn = f.file_name + + if sl == -1: + print(f" ::error::Result [{idx}] {fn}: search_line is -1") + valid = False + elif sl != ln: + print(f" ::error::Result [{idx}] {fn}: search_line ({sl}) != line ({ln})") + valid = False + else: + print(f" [OK] Result [{idx}] {fn}: search_line={sl} == line={ln}") + + return valid + + +def validate_query(query_dir): + """Validate a single query directory.""" + + if not has_search_line_defined(query_dir): + print(" [SKIP] searchLine not defined in query.rego - PASS") + return True + + print(" searchLine is defined in query.rego - running scan...") + + if not run_kics_scan(query_dir): + return False + + return validate_scan_results(query_dir) + + +def main(): + print("Running script validate_search_line.py") + query_dirs = get_changed_queries() + + if not query_dirs: + print("No query.rego were changed - nothing to validate") + sys.exit(0) + + all_valid = True + for qd in query_dirs: + if not validate_query(qd): + all_valid = False + + if all_valid: + print("All searchLine validations passed!") + sys.exit(0) + else: + print("::error::Some searchLine validations failed. See errors above.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index 10441389326..61dbb43317b 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -90,6 +90,42 @@ jobs: with: name: unit-test-${{ matrix.os }}-${{ github.event.pull_request.head.sha }}.log path: unit-test.log + validate-search-line: + name: validate-search-line + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + - name: Detect changed query.rego files + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + with: + list-files: json + filters: | + queries: + - 'assets/queries/**/query.rego' + - name: Debug filter outputs + run: | + echo "queries changed: ${{ steps.filter.outputs.queries }}" + echo "changed files: ${{ steps.filter.outputs.queries_files }}" + - name: Set up Python + id: setup-python + if: steps.filter.outputs.queries == 'true' + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.13' + - name: Validate searchLine in modified queries + if: steps.setup-python.outcome == 'success' + env: + CHANGED_QUERIES: ${{ steps.filter.outputs.queries_files }} + KICS_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + KICS_PR_NUMBER: ${{ github.event.number }} + working-directory: .github/scripts/validate-search-line/ + run: python3 validate_search_line.py + security-scan: name: security-scan runs-on: ubuntu-latest diff --git a/assets/queries/googleDeploymentManager/gcp/bigquery_database_is_public/query.rego b/assets/queries/googleDeploymentManager/gcp/bigquery_database_is_public/query.rego index f0c85ed0ef2..dad6c151063 100644 --- a/assets/queries/googleDeploymentManager/gcp/bigquery_database_is_public/query.rego +++ b/assets/queries/googleDeploymentManager/gcp/bigquery_database_is_public/query.rego @@ -16,6 +16,6 @@ CxPolicy[result] { "issueType": "IncorrectValue", "keyExpectedValue": sprintf("'access[%d].specialGroup' should not equal to 'allAuthenticatedUsers'", [j]), "keyActualValue": sprintf("'access[%d].specialGroup' is equal to 'allAuthenticatedUsers'", [j]), - "searchLine": common_lib.build_search_line(["resources", idx, "properties", "access", j, "specialGroup"], []), + "searchLine": common_lib.build_search_line(["potato"], []), } } diff --git a/assets/queries/serverlessFW/serverless_api_access_logging_setting_undefined/query.rego b/assets/queries/serverlessFW/serverless_api_access_logging_setting_undefined/query.rego index d29c3c5f8da..d2d3ed8ae7c 100644 --- a/assets/queries/serverlessFW/serverless_api_access_logging_setting_undefined/query.rego +++ b/assets/queries/serverlessFW/serverless_api_access_logging_setting_undefined/query.rego @@ -18,6 +18,6 @@ CxPolicy[result] { "issueType": "IncorrectValue", "keyExpectedValue": "provider.logs.restApi should have 'accessLogging' set to true", "keyActualValue": "provider.logs.restApi has 'accessLogging' set to false", - "searchLine": common_lib.build_search_line(["provider", "logs", "restApi", "accessLogging"], []), + "searchLine": common_lib.build_search_line(["fjwoijfewijf"], []), } } diff --git a/assets/queries/terraform/aws/alb_deletion_protection_disabled/query.rego b/assets/queries/terraform/aws/alb_deletion_protection_disabled/query.rego index d3dac6f5cf3..a4afe6b6aeb 100644 --- a/assets/queries/terraform/aws/alb_deletion_protection_disabled/query.rego +++ b/assets/queries/terraform/aws/alb_deletion_protection_disabled/query.rego @@ -19,7 +19,7 @@ CxPolicy[result] { "issueType": "MissingAttribute", "keyExpectedValue": "'enable_deletion_protection' should be defined and set to true", "keyActualValue": "'enable_deletion_protection' is undefined or null", - "searchLine": common_lib.build_search_line(["resource", loadBalancer, name], []), + "searchLine": "-1", "remediation": "enable_deletion_protection = true", "remediationType": "addition", } diff --git a/assets/queries/terraform/aws/amazon_dms_replication_instance_is_publicly_accessible/query.rego b/assets/queries/terraform/aws/amazon_dms_replication_instance_is_publicly_accessible/query.rego index b5d42201a6d..4752d4f75e0 100644 --- a/assets/queries/terraform/aws/amazon_dms_replication_instance_is_publicly_accessible/query.rego +++ b/assets/queries/terraform/aws/amazon_dms_replication_instance_is_publicly_accessible/query.rego @@ -6,6 +6,7 @@ import data.generic.terraform as tf_lib CxPolicy[result] { resource := input.document[i].resource aws_dms_replication_instance := resource.aws_dms_replication_instance[name] + common_lib.valid_key(aws_dms_replication_instance, publicly_accessible) aws_dms_replication_instance.publicly_accessible == true result := { @@ -17,4 +18,4 @@ CxPolicy[result] { "keyExpectedValue": sprintf("aws_dms_replication_instance[%s].publicly_accessible should be set to false", [name]), "keyActualValue": sprintf("aws_dms_replication_instance[%s].publicly_accessible is set to true", [name]), } -} \ No newline at end of file +} diff --git a/assets/queries/terraform/aws/cmk_is_unusable/query.rego b/assets/queries/terraform/aws/cmk_is_unusable/query.rego index 84cefba6e51..7e65756a667 100644 --- a/assets/queries/terraform/aws/cmk_is_unusable/query.rego +++ b/assets/queries/terraform/aws/cmk_is_unusable/query.rego @@ -13,7 +13,7 @@ CxPolicy[result] { "resourceType": "aws_kms_key", "resourceName": tf_lib.get_resource_name(resource, name), "searchKey": sprintf("aws_kms_key[%s].is_enabled", [name]), - "searchLine": common_lib.build_search_line(["resource", "aws_kms_key", name, "is_enabled"], []), + "searchLine": common_lib.build_search_line(["fweoiwenfgewoi"], []), "issueType": "IncorrectValue", "keyExpectedValue": sprintf("aws_kms_key[%s].is_enabled should be set to true", [name]), "keyActualValue": sprintf("aws_kms_key[%s].is_enabled is set to false", [name]), diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 74659e3d16a..47caa36a8ea 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.25.7-alpine AS build_env +FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.25.8-alpine AS build_env # Install build dependencies RUN apk add --no-cache git diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index db9f7bd30c8..f74b5517dd8 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -3,7 +3,7 @@ # it does not define an ENTRYPOINT as this is a requirement described here: # https://docs.microsoft.com/en-us/azure/devops/pipelines/process/container-phases?view=azure-devops#linux-based-containers # -FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.25.7-bookworm as build_env +FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.25.8-bookworm as build_env # Create a group and user RUN groupadd checkmarx && useradd -g checkmarx -M -s /bin/bash checkmarx USER checkmarx