Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/scripts/parse_sarif.py
Original file line number Diff line number Diff line change
@@ -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,
)
10 changes: 10 additions & 0 deletions .github/scripts/readme.md
Original file line number Diff line number Diff line change
@@ -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.
110 changes: 110 additions & 0 deletions .github/scripts/run_sca_app.py
Original file line number Diff line number Diff line change
@@ -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()
109 changes: 109 additions & 0 deletions .github/scripts/run_sca_image.py
Original file line number Diff line number Diff line change
@@ -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()
31 changes: 31 additions & 0 deletions .github/scripts/setup-tools.sh
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions .github/scripts/supress_osv_scanner.toml
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 3 additions & 0 deletions .github/scripts/supress_trivy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
vulnerabilities:
- id: CVE-2026-54515
statement: "The proposed fix version 2.21.5 not yet released"
58 changes: 58 additions & 0 deletions .github/workflows/sca_app.yml
Original file line number Diff line number Diff line change
@@ -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
Loading