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..e804fed8406 --- /dev/null +++ b/.github/scripts/validate-search-line/validate-search-line.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 + +import json +import os +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent + + +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", + ] + + 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 != 0: + 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: + - Sort results by: file_name, line, search_key, search_value, resource_type, + resource_name, query_name, expected_value, actual_value + - 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()) + + # Flatten results from all queries + all_results = [] + for query in data.get("queries", []): + for entry in query.get("files", []): + all_results.append({ + "file_name": entry.get("file_name", ""), + "line": entry.get("line", 0), + "search_line": entry.get("search_line", 0), + }) + + if not all_results: + print(" [OK] No results to validate") + return True + + # Validate each result + valid = True + for idx, r in enumerate(all_results): + sl = r["search_line"] + ln = r["line"] + fn = r["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(): + query_dirs = get_changed_queries() + + if not query_dirs: + print("No query.rego were changes - 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 e9029c36282..9bdf86e30c4 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -90,6 +90,40 @@ jobs: with: name: unit-test-${{ runner.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: Set up Python + if: steps.filter.outputs.queries == 'true' + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.13' + - name: Set up Go + if: steps.filter.outputs.queries == 'true' + run: make build + - name: Validate searchLine in modified queries + if: steps.filter.outputs.queries == 'true' + 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/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 diff --git a/docker/Dockerfile.ubi8 b/docker/Dockerfile.ubi8 index e9caa31353f..85a39dc998e 100644 --- a/docker/Dockerfile.ubi8 +++ b/docker/Dockerfile.ubi8 @@ -4,10 +4,10 @@ WORKDIR /build ENV PATH=$PATH:/usr/local/go/bin -ADD https://golang.org/dl/go1.25.7.linux-amd64.tar.gz . +ADD https://golang.org/dl/go1.25.8.linux-amd64.tar.gz . RUN yum install git gcc -y \ - && rm -rf /usr/local/go && tar -C /usr/local -xzf go1.25.7.linux-amd64.tar.gz \ - && rm -f go1.25.7.linux-amd64.tar.gz + && rm -rf /usr/local/go && tar -C /usr/local -xzf go1.25.8.linux-amd64.tar.gz \ + && rm -f go1.25.8.linux-amd64.tar.gz ENV GOPRIVATE=github.com/Checkmarx/* ARG VERSION="development"