From aefffd7d9d88fe50bcc848e3b7d715695e6a849f Mon Sep 17 00:00:00 2001 From: Jonathan Haas Date: Thu, 21 May 2026 13:37:17 -0700 Subject: [PATCH] ci: remove code scanning sarif uploads --- .github/workflows/ci.yml | 24 --- scripts/upload-sarif-to-code-scanning.py | 216 -------------------- tests/test_upload_sarif_to_code_scanning.py | 51 ----- 3 files changed, 291 deletions(-) delete mode 100644 scripts/upload-sarif-to-code-scanning.py delete mode 100644 tests/test_upload_sarif_to_code_scanning.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71ab44a..a1c7635 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,7 +111,6 @@ jobs: permissions: actions: read contents: read - security-events: write steps: - name: Checkout code uses: actions/checkout@v6 @@ -128,29 +127,6 @@ jobs: - name: Run security checks run: make security - - name: Run gosec with SARIF output - run: gosec -fmt sarif -out gosec.sarif ./... - - - name: Upload gosec results - continue-on-error: true - env: - GITHUB_TOKEN: ${{ github.token }} - run: python3 scripts/upload-sarif-to-code-scanning.py --sarif-file gosec.sarif --category gosec - - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master - with: - scan-type: 'fs' - scan-ref: '.' - format: 'sarif' - output: 'trivy-results.sarif' - - - name: Upload Trivy results - continue-on-error: true - env: - GITHUB_TOKEN: ${{ github.token }} - run: python3 scripts/upload-sarif-to-code-scanning.py --sarif-file trivy-results.sarif --category trivy - build: name: Build and Test Docker Images runs-on: ubuntu-latest diff --git a/scripts/upload-sarif-to-code-scanning.py b/scripts/upload-sarif-to-code-scanning.py deleted file mode 100644 index 13c7985..0000000 --- a/scripts/upload-sarif-to-code-scanning.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python3 -"""Upload a SARIF file to GitHub Code Scanning without the CodeQL action.""" - -from __future__ import annotations - -import argparse -import base64 -import gzip -import hashlib -import json -import os -from pathlib import Path -import sys -import time -import urllib.error -import urllib.request - - -def code_scanning_ref() -> str: - ref = os.environ["GITHUB_REF"] - if os.environ.get("GITHUB_EVENT_NAME") == "merge_group": - with open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8") as event_file: - event = json.load(event_file) - base_ref = event.get("merge_group", {}).get("base_ref", "") - if base_ref.startswith("refs/heads/"): - return base_ref - if base_ref: - return f"refs/heads/{base_ref}" - if ref.startswith(("refs/heads/", "refs/pull/")): - return ref - raise ValueError(f"unsupported GITHUB_REF for Code Scanning SARIF upload: {ref}") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--sarif-file", required=True, type=Path) - parser.add_argument("--category") - return parser.parse_args() - - -def request_headers() -> dict[str, str]: - return { - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}", - "Content-Type": "application/json", - "X-GitHub-Api-Version": "2022-11-28", - } - - -def is_code_scanning_disabled_error(status_code: int, response_body: str) -> bool: - response_body_lower = response_body.lower() - return status_code == 403 and ( - "advanced security must be enabled" in response_body_lower - or "code scanning is not enabled" in response_body_lower - or "code security must be enabled" in response_body_lower - ) - - -def handle_code_scanning_http_error(error: urllib.error.HTTPError) -> None: - response_body = error.read().decode("utf-8") - if is_code_scanning_disabled_error(error.code, response_body): - print("::warning::Code Security is not enabled; skipping SARIF upload.") - return - sys.stderr.write(response_body) - raise error - - -def artifact_uri(run: dict[str, object], artifact_location: object) -> object: - if not isinstance(artifact_location, dict): - return None - uri = artifact_location.get("uri") - if uri: - return uri - index = artifact_location.get("index") - artifacts = run.get("artifacts") - if not isinstance(index, int) or not isinstance(artifacts, list): - return None - if index < 0 or index >= len(artifacts): - return None - artifact = artifacts[index] - if not isinstance(artifact, dict): - return None - location = artifact.get("location") - if not isinstance(location, dict): - return None - return location.get("uri") - - -def result_location_key(result: dict[str, object], run: dict[str, object]) -> dict[str, object]: - locations = result.get("locations") - if not isinstance(locations, list) or not locations: - return {} - first_location = locations[0] - if not isinstance(first_location, dict): - return {} - physical_location = first_location.get("physicalLocation") - if not isinstance(physical_location, dict): - return {} - region = physical_location.get("region") - if not isinstance(region, dict): - region = {} - - return { - "uri": artifact_uri(run, physical_location.get("artifactLocation")), - "startLine": region.get("startLine"), - "startColumn": region.get("startColumn"), - "endLine": region.get("endLine"), - "endColumn": region.get("endColumn"), - } - - -def result_fingerprint(result: dict[str, object], run: dict[str, object]) -> str: - key = { - "ruleId": result.get("ruleId"), - "message": result.get("message"), - "location": result_location_key(result, run), - } - encoded = json.dumps(key, sort_keys=True, separators=(",", ":")).encode("utf-8") - return hashlib.sha256(encoded).hexdigest() - - -def apply_category(sarif: dict[str, object], category: str | None) -> None: - if not category: - return - runs = sarif.get("runs", []) - if not isinstance(runs, list): - return - for index, run in enumerate(runs): - if not isinstance(run, dict): - continue - automation_details = run.setdefault("automationDetails", {}) - if not isinstance(automation_details, dict): - automation_details = {} - run["automationDetails"] = automation_details - if automation_details.get("id"): - continue - automation_details["id"] = category if len(runs) == 1 else f"{category}/run-{index + 1}" - - -def sarif_upload_bytes(path: Path, category: str | None) -> bytes: - sarif = json.loads(path.read_text(encoding="utf-8")) - apply_category(sarif, category) - for run in sarif.get("runs", []): - if not isinstance(run, dict): - continue - for result in run.get("results", []): - if not isinstance(result, dict): - continue - if result.get("partialFingerprints"): - continue - result["partialFingerprints"] = { - "primaryLocationLineHash": result_fingerprint(result, run) - } - return json.dumps(sarif, separators=(",", ":"), ensure_ascii=False).encode("utf-8") - - -def wait_for_sarif_processing(sarif_id: str) -> None: - deadline = time.monotonic() + 120 - status_url = ( - f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}" - f"/code-scanning/sarifs/{sarif_id}" - ) - while True: - request = urllib.request.Request(status_url, headers=request_headers()) - try: - with urllib.request.urlopen(request) as response: - status_body = json.loads(response.read().decode("utf-8")) - except urllib.error.HTTPError as error: - handle_code_scanning_http_error(error) - return - processing_status = status_body.get("processing_status") - if processing_status == "complete": - print(json.dumps(status_body)) - return - if processing_status == "failed": - raise RuntimeError(f"SARIF processing failed: {json.dumps(status_body)}") - if time.monotonic() >= deadline: - raise TimeoutError(f"Timed out waiting for SARIF processing: {json.dumps(status_body)}") - print(f"SARIF processing status is {processing_status}; waiting...") - time.sleep(5) - - -def main() -> int: - args = parse_args() - sarif_payload = base64.b64encode( - gzip.compress(sarif_upload_bytes(args.sarif_file, args.category)) - ).decode("ascii") - - body = { - "commit_sha": os.environ["GITHUB_SHA"], - "ref": code_scanning_ref(), - "sarif": sarif_payload, - "checkout_uri": f"file://{os.environ['GITHUB_WORKSPACE']}", - } - - request = urllib.request.Request( - f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/code-scanning/sarifs", - data=json.dumps(body).encode("utf-8"), - method="POST", - headers=request_headers(), - ) - try: - with urllib.request.urlopen(request) as response: - response_body = json.loads(response.read().decode("utf-8")) - print(json.dumps(response_body)) - except urllib.error.HTTPError as error: - handle_code_scanning_http_error(error) - return 0 - sarif_id = response_body.get("id") - if sarif_id: - wait_for_sarif_processing(str(sarif_id)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tests/test_upload_sarif_to_code_scanning.py b/tests/test_upload_sarif_to_code_scanning.py deleted file mode 100644 index fe55d8b..0000000 --- a/tests/test_upload_sarif_to_code_scanning.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -import importlib.util -import io -from pathlib import Path -import urllib.error - - -def load_upload_sarif_module(): - module_path = ( - Path(__file__).resolve().parents[1] / "scripts" / "upload-sarif-to-code-scanning.py" - ) - spec = importlib.util.spec_from_file_location("upload_sarif_to_code_scanning", module_path) - assert spec is not None - module = importlib.util.module_from_spec(spec) - assert spec.loader is not None - spec.loader.exec_module(module) - return module - - -def test_polling_http_error_can_skip_when_code_scanning_is_disabled(capsys): - module = load_upload_sarif_module() - error = urllib.error.HTTPError( - "https://api.github.com/repos/evalops/keep/code-scanning/sarifs/1", - 403, - "Forbidden", - {}, - io.BytesIO(b'{"message":"Code scanning is not enabled"}'), - ) - - assert module.handle_code_scanning_http_error(error) is None - assert "Code Security is not enabled" in capsys.readouterr().out - - -def test_non_code_scanning_http_error_is_reraised(capsys): - module = load_upload_sarif_module() - error = urllib.error.HTTPError( - "https://api.github.com/repos/evalops/keep/code-scanning/sarifs/1", - 500, - "Server Error", - {}, - io.BytesIO(b'{"message":"temporary outage"}'), - ) - - try: - module.handle_code_scanning_http_error(error) - except urllib.error.HTTPError as raised: - assert raised is error - else: - raise AssertionError("expected HTTPError to be reraised") - assert "temporary outage" in capsys.readouterr().err