diff --git a/.github/scripts/parse_sarif.py b/.github/scripts/parse_sarif.py new file mode 100644 index 00000000..16af3b4a --- /dev/null +++ b/.github/scripts/parse_sarif.py @@ -0,0 +1,30 @@ +import json +from dataclasses import dataclass + +@dataclass +class EvaluationResult: + gate_failed: bool + gate_warn: bool + +def evaluate(sarif_paths): + if isinstance(sarif_paths, str): + sarif_paths = [sarif_paths] + + max_score = 0.0 + for path in sarif_paths: + with open(path) as f: + sarif = json.load(f, strict=False) + + for run in sarif.get("runs", []): + rules = run["tool"]["driver"].get("rules", []) + severities = {r["id"]: r.get("properties", {}).get("security-severity") for r in rules} + + for result in run.get("results", []): + score = severities.get(result["ruleId"]) + if score is not None: + max_score = max(max_score, float(score)) + + return EvaluationResult( + gate_failed=max_score >= 8, + gate_warn=5 <= max_score < 8, + ) \ No newline at end of file diff --git a/.github/scripts/readme.md b/.github/scripts/readme.md new file mode 100644 index 00000000..2061ca7a --- /dev/null +++ b/.github/scripts/readme.md @@ -0,0 +1,10 @@ +# SCA Pipeline + +Security scanning runs automatically on every pull request to detect vulnerable dependencies before merging. +We generate a CycloneDX SBOM from the project to ensure accurate results with no false positives. + +## Files + +- **setup-tools.sh** : Installs the scanning tools: Trivy, OSV Scanner, and OWASP Dependency-Check. +- **run_sca.py** : Orchestrates all three tools and prints a pass/fail summary at the end. +- **osv-scanner.toml** : OSV Scanner suppression list for vulnerabilities that cannot be fixed yet, with reasons and expiry dates. \ No newline at end of file diff --git a/.github/scripts/run_sca_app.py b/.github/scripts/run_sca_app.py new file mode 100644 index 00000000..455e652c --- /dev/null +++ b/.github/scripts/run_sca_app.py @@ -0,0 +1,110 @@ +import subprocess +import os +import sys +import logging +import json +from parse_sarif import evaluate + +GREEN = '\033[92m' +RED = '\033[91m' +RESET = '\033[0m' +BOLD = '\033[1m' +YELLOW = '\033[93m' + +logging.basicConfig( + level=logging.INFO, + format='%(message)s' # Clean format to prevent double-timestamps in CI logs +) +logger = logging.getLogger("sca-orchestrator") + +def run_trivy(): + cmd = [ + "trivy", "sbom", + "target/bom.json", + "--format", "sarif", + "--ignorefile", ".github/scripts/supress_trivy.yaml", + "--output", "trivy-app.sarif" + ] + + return subprocess.run(cmd).returncode + +def run_osv_scanner(): + cmd = [ + "osv-scanner", "scan", "source", + "--lockfile", "target/bom.json", + "--config", ".github/scripts/supress_osv_scanner.toml", + "--format", "sarif", + "--output-file", "osv-scanner-app.sarif" + ] + + return subprocess.run(cmd).returncode + +def merge_sarifs(): + merged = { + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [], + } + + for path in ("trivy-app.sarif", "osv-scanner-app.sarif"): + if not os.path.exists(path): + logger.warning(f"{path} not found, skipping in merge") + continue + with open(path) as f: + sarif = json.load(f, strict=False) + merged["runs"].extend(sarif.get("runs", [])) + + with open("merged-SCA-platform-backend-app.sarif", "w") as f: + json.dump(merged, f) + + logger.info("SARIF files merged successfully.") + + + +def main(): + tools = [run_trivy, run_osv_scanner] + + exit_codes = {} + for tool in tools: + exit_codes[tool.__name__] = tool() + logger.info("-" * 40) + + merge_sarifs() # combined artifact only, not used for the gate decision + + sarif_files = {"trivy": "trivy-app.sarif", "osv-scanner": "osv-scanner-app.sarif"} + tool_status = {} # "PASSED" | "WARNING" | "FAILED" + gate_failed = False + + for name, path in sarif_files.items(): + if not os.path.exists(path): + logger.error(f"{RED}[!] {name} SARIF file missing, skipping evaluation: {path}{RESET}") + tool_status[name] = "FAILED, Something is wrong with the tool execution, please check the logs." + gate_failed = True + continue + + eval_result = evaluate(path) + + if eval_result.gate_failed: + tool_status[name] = "FAILED" # this tool found CVSS >= 8.0 + gate_failed = True + elif eval_result.gate_warn: + tool_status[name] = "WARNING" # this tool found 5.0 <= CVSS < 8.0 + else: + tool_status[name] = "PASSED" # this tool found nothing >= 5.0 + + logger.info(f"\n{BOLD}========== SCA PIPELINE SUMMARY =========={RESET}") + for name, status in tool_status.items(): + if status == "PASSED": + logger.info(f"[{name}]: {GREEN}PASSED{RESET}") + elif status == "WARNING": + logger.warning(f"[{name}]: {YELLOW}WARNING (findings between 5.0 and 8.0){RESET}") + else: + logger.error(f"[{name}]: {RED}FAILED (CVSS >= 8.0 found){RESET}") + logger.info(f"{BOLD}=========================================={RESET}\n") + + if gate_failed: + logger.error(f"{RED}One or more SCA tools failed the gate check.{RESET}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/scripts/run_sca_image.py b/.github/scripts/run_sca_image.py new file mode 100644 index 00000000..a20a8457 --- /dev/null +++ b/.github/scripts/run_sca_image.py @@ -0,0 +1,109 @@ +import subprocess +import os +import sys +import logging +import json +from parse_sarif import evaluate + +GREEN = '\033[92m' +RED = '\033[91m' +RESET = '\033[0m' +BOLD = '\033[1m' +YELLOW = '\033[93m' + +logging.basicConfig( + level=logging.INFO, + format='%(message)s' # Clean format to prevent double-timestamps in CI logs +) +logger = logging.getLogger("sca-orchestrator") +def run_trivy(): + cmd = [ + "trivy", "image", + "platform-backend:testing", + "--format", "sarif", + "--ignorefile", ".github/scripts/supress_trivy.yaml", + "--output", "trivy-image.sarif" + ] + return subprocess.run(cmd).returncode + + +def run_osv_scanner(): + cmd = [ + "osv-scanner", "scan", "image", + "platform-backend:testing", + "--config", ".github/scripts/supress_osv_scanner.toml", + "--format", "sarif", + "--output-file", "osv-scanner-image.sarif" + ] + return subprocess.run(cmd).returncode + +def merge_sarifs(): + + merged = { + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [], + } + + for path in ("trivy-image.sarif", "osv-scanner-image.sarif"): + if not os.path.exists(path): + logger.warning(f"{path} not found, skipping in merge") + continue + with open(path) as f: + sarif = json.load(f, strict=False) + merged["runs"].extend(sarif.get("runs", [])) + + with open("merged-SCA-platform-backend-image.sarif", "w") as f: + json.dump(merged, f) + + logger.info("SARIF files merged successfully.") + + + +def main(): + tools = [run_trivy, run_osv_scanner] + + exit_codes = {} + for tool in tools: + exit_codes[tool.__name__] = tool() + logger.info("-" * 40) + + merge_sarifs() # combined artifact only, not used for the gate decision + + sarif_files = {"trivy": "trivy-image.sarif", "osv-scanner": "osv-scanner-image.sarif"} + tool_status = {} # "PASSED" | "WARNING" | "FAILED" + gate_failed = False + + for name, path in sarif_files.items(): + if not os.path.exists(path): + logger.error(f"{RED}[!] {name} SARIF file missing, skipping evaluation: {path}{RESET}") + tool_status[name] = "FAILED, Something is wrong with the tool execution, please check the logs." + gate_failed = True + continue + + eval_result = evaluate(path) + + if eval_result.gate_failed: + tool_status[name] = "FAILED" # this tool found CVSS >= 8.0 + gate_failed = True # Fail the gate if any tool fails + elif eval_result.gate_warn: + tool_status[name] = "WARNING" # this tool found 5.0 <= CVSS < 8.0 + else: + tool_status[name] = "PASSED" # this tool found nothing >= 5.0 + + logger.info(f"\n{BOLD}========== SCA PIPELINE SUMMARY =========={RESET}") + for name, status in tool_status.items(): + if status == "PASSED": + logger.info(f"[{name}]: {GREEN}PASSED{RESET}") + elif status == "WARNING": + logger.warning(f"[{name}]: {YELLOW}WARNING (findings between 5.0 and 8.0){RESET}") + else: + logger.error(f"[{name}]: {RED}FAILED (CVSS >= 8.0 found){RESET}") + logger.info(f"{BOLD}=========================================={RESET}\n") + + if gate_failed: + logger.error(f"{RED}One or more SCA tools failed the gate check.{RESET}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/scripts/setup-tools.sh b/.github/scripts/setup-tools.sh new file mode 100644 index 00000000..d502f886 --- /dev/null +++ b/.github/scripts/setup-tools.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e + +# Install Trivy +curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.71.1 + +# Install OSV Scanner +curl -L https://github.com/google/osv-scanner/releases/download/v2.4.0/osv-scanner_linux_amd64 -o /usr/local/bin/osv-scanner +chmod +x /usr/local/bin/osv-scanner + + +# Generate SBOM based on project type + +PROJECT_TYPE="${1:-none}" # maven | npm | none +case "$PROJECT_TYPE" in + maven) + echo "Generating SBOM for Maven project" + mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom -q + ;; + npm) + echo "Generating SBOM for NPM project" + npx --yes @cyclonedx/cyclonedx-npm --output-file bom.json + ;; + none) + echo "No SBOM generation needed for image pipelines" + ;; + *) + echo "Unknown PROJECT_TYPE: $PROJECT_TYPE" >&2 # redirect error message to stderr + exit 1 + ;; +esac \ No newline at end of file diff --git a/.github/scripts/supress_osv_scanner.toml b/.github/scripts/supress_osv_scanner.toml new file mode 100644 index 00000000..b5075b31 --- /dev/null +++ b/.github/scripts/supress_osv_scanner.toml @@ -0,0 +1,4 @@ +[[IgnoredVulns]] +id = "GHSA-5jmj-h7xm-6q6v" +ignoreUntil = 2026-09-30 +reason = "The proposed fix version 2.21.5 not yet released" \ No newline at end of file diff --git a/.github/scripts/supress_trivy.yaml b/.github/scripts/supress_trivy.yaml new file mode 100644 index 00000000..5c8cae89 --- /dev/null +++ b/.github/scripts/supress_trivy.yaml @@ -0,0 +1,3 @@ +vulnerabilities: + - id: CVE-2026-54515 + statement: "The proposed fix version 2.21.5 not yet released" \ No newline at end of file diff --git a/.github/workflows/sca_app.yml b/.github/workflows/sca_app.yml new file mode 100644 index 00000000..2b560ffd --- /dev/null +++ b/.github/workflows/sca_app.yml @@ -0,0 +1,58 @@ +name: SCA CI - app + +on: + pull_request: + +jobs: + sca: + name: Software Composition Analysis + runs-on: ubuntu-latest # TODO: Migrate back to debian:trixie once pipeline MVP is fully stable + permissions: + contents: read + security-events: write # required for uploading SCA results to github security + + steps: + - name: Check out repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0 + with: + python-version: '3.14.4' + + - name: Cache Maven packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0 + with: + path: ~/.m2/repository # /.m2 only is too braod and can cause cache corruption issues + key: ${{ runner.os }}-m2-v1-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2-v1- + + - name: Resolve Maven dependencies + run: mvn dependency:resolve -q + + - name: Setup tools + run: bash .github/scripts/setup-tools.sh maven + + - name: Run SCA tools + run: python .github/scripts/run_sca_app.py + + - name: Upload Trivy SARIF to GitHub Security tab + id: upload_trivy + if: always() + uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + with: + sarif_file: trivy-app.sarif + + - name: Upload OSV Scanner SARIF to GitHub Security tab + id: upload_osv + if: always() + uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + with: + sarif_file: osv-scanner-app.sarif + + - name: Upload merged SARIF to GitHub Security tab + if: ${{ always() && steps.upload_trivy.outcome == 'success' && steps.upload_osv.outcome == 'success' }} + uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + with: + sarif_file: merged-SCA-platform-backend-app.sarif + category: merged-sca \ No newline at end of file diff --git a/.github/workflows/sca_image.yml b/.github/workflows/sca_image.yml new file mode 100644 index 00000000..6b2e614a --- /dev/null +++ b/.github/workflows/sca_image.yml @@ -0,0 +1,51 @@ +name: SCA CI - image + +on: + pull_request: + +jobs: + sca: + name: Software Composition Analysis + runs-on: ubuntu-latest # TODO: Migrate back to debian:trixie once pipeline MVP is fully stable + permissions: + contents: read + security-events: write # required for uploading SCA results to github security + + steps: + - name: Check out repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0 + with: + python-version: '3.14.4' + + - name: Build Docker image + run: docker build -t platform-backend:testing . + + - name: Setup tools + run: bash .github/scripts/setup-tools.sh + + - name: Run SCA tools + run: python .github/scripts/run_sca_image.py + + - name: Upload Trivy SARIF to GitHub Security tab + id: upload_trivy + if: always() + uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + with: + sarif_file: trivy-image.sarif + + - name: Upload OSV Scanner SARIF to GitHub Security tab + id: upload_osv + if: always() + uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + with: + sarif_file: osv-scanner-image.sarif + + - name: Upload merged SARIF to GitHub Security tab + if: ${{ always() && steps.upload_trivy.outcome == 'success' && steps.upload_osv.outcome == 'success' }} + uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6 + with: + sarif_file: merged-SCA-platform-backend-image.sarif + category: merged-sca \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index aa9e3710..2fb4aa23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ RUN apk add --no-cache curl ####################################################### # Install dockerize ####################################################### -ENV DOCKERIZE_VERSION=v0.10.1 +ENV DOCKERIZE_VERSION=v0.13.0 RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ && rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz diff --git a/pom.xml b/pom.xml index b8a333a0..7d5b41a8 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,8 @@ 11.0.22 2.6 10.9.1 + 2.21.4 + 3.1.4