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