Skip to content
Merged
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
75 changes: 36 additions & 39 deletions .github/workflows/container-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
TRIVY_SCA_SARIF_OUTPUT: sca-trivy-container.sarif
OSV_SCA_SARIF_OUTPUT: sca-osv-container.sarif

# SAST /Linting
# SAST / Linting
SEMGREP_CONFIG_RULESETS: >-
semgrep-rules/dockerfile auto
OPENGREP_SAST_SARIF_OUTPUT: sast-opengrep-dockerfile.sarif
Expand Down Expand Up @@ -51,52 +51,49 @@ jobs:
run: python ci/container_scan.py --scan-type sast

- name: Run SCA scanning
if: always()
if: ${{ !cancelled() }}
run: python ci/container_scan.py --scan-type sca --image ${{ env.IMAGE_NAME }}

- name: Upload Trivy SARIF to GitHub Security tab
id: upload_trivy
if: always()
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 #v4.38.0
with:
sarif_file: ${{ env.TRIVY_SCA_SARIF_OUTPUT }}
category: trivy-container-scanning

- name: Upload OSV Scanner SARIF to GitHub Security tab
id: upload_osv
if: always()
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 #v4.38.0
with:
sarif_file: ${{ env.OSV_SCA_SARIF_OUTPUT }}
category: osv-scanner-container-scanning

- name: Upload OpenGrep SARIF to GitHub Security tab
id: upload_opengrep
if: always()
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 #v4.38.0
with:
sarif_file: ${{ env.OPENGREP_SAST_SARIF_OUTPUT }}
category: opengrep-sast

- name: Upload Hadolint SARIF to GitHub Security tab
id: upload_hadolint
if: always()
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 #v4.38.0
with:
sarif_file: ${{ env.HADOLINT_SAST_SARIF_OUTPUT }}
category: hadolint-sast

- name: Merge all SARIF reports
if: always()
if: ${{ !cancelled() }}
run: |
python ci/container_scan.py \
--merge-sarif "${{ env.TRIVY_SCA_SARIF_OUTPUT }}" "${{ env.OSV_SCA_SARIF_OUTPUT }}" "${{ env.OPENGREP_SAST_SARIF_OUTPUT }}" "${{ env.HADOLINT_SAST_SARIF_OUTPUT }}" \
--merge-output "${{ env.CONTAINER_SCAN_MERGED_SARIF_OUTPUT }}"

- name: Upload SARIF artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
- name: Upload merged SARIF to GitHub Security tab
if: ${{ !cancelled() && hashFiles(env.CONTAINER_SCAN_MERGED_SARIF_OUTPUT) != '' }}
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.2
with:
sarif_file: ${{ env.CONTAINER_SCAN_MERGED_SARIF_OUTPUT }}
category: container-scan

- name: Encrypt SARIF report
id: encrypt
if: ${{ !cancelled() && hashFiles(env.CONTAINER_SCAN_MERGED_SARIF_OUTPUT) != '' }}
env:
PW: ${{ secrets.ARTIFACT_PASSWORD }}
OUT: ${{ env.CONTAINER_SCAN_MERGED_SARIF_OUTPUT }}.gpg
run: |
if [ -z "$PW" ]; then
echo "::warning title=SARIF report not uploaded::ARTIFACT_PASSWORD is not available in this run (Dependabot, Renovate or fork PR). Skipping encryption and upload."
rm -f -- "$CONTAINER_SCAN_MERGED_SARIF_OUTPUT"
exit 0
fi
printf '%s' "$PW" | gpg --symmetric --batch --yes --quiet \
--pinentry-mode loopback --passphrase-fd 0 \
--no-symkey-cache \
--cipher-algo AES256 \
--s2k-mode 3 --s2k-digest-algo SHA512 --s2k-count 65011712 \
--output "$OUT" -- "$CONTAINER_SCAN_MERGED_SARIF_OUTPUT"
rm -f -- "$CONTAINER_SCAN_MERGED_SARIF_OUTPUT"
echo "encrypted=$OUT" >> "$GITHUB_OUTPUT"

- name: Upload encrypted SARIF artifact
if: ${{ !cancelled() && steps.encrypt.outputs.encrypted != '' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: container-scan-sarif-report
path: ${{ env.CONTAINER_SCAN_MERGED_SARIF_OUTPUT }}
path: ${{ steps.encrypt.outputs.encrypted }}
retention-days: 30
if-no-files-found: error
36 changes: 29 additions & 7 deletions .github/workflows/sast.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,39 @@ jobs:
- name: Run SAST scanning
run: python ci/sast_scan.py

- name: Upload Semgrep SARIF to GitHub Security tab
id: upload_semgrep
if: always()
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 #v4.38.0
- name: Upload SARIF to GitHub Security tab
if: ${{ !cancelled() && hashFiles(env.OPENGREP_SARIF_OUTPUT) != '' }}
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.2
with:
sarif_file: ${{ env.OPENGREP_SARIF_OUTPUT }}
category: semgrep-app

- name: Upload SARIF artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
- name: Encrypt SARIF report
id: encrypt
if: ${{ !cancelled() && hashFiles(env.OPENGREP_SARIF_OUTPUT) != '' }}
env:
PW: ${{ secrets.ARTIFACT_PASSWORD }}
OUT: ${{ env.OPENGREP_SARIF_OUTPUT }}.gpg
run: |
if [ -z "$PW" ]; then
echo "::warning title=SARIF report not uploaded::ARTIFACT_PASSWORD is not available in this run (Dependabot, Renovate or fork PR). Skipping encryption and upload."
rm -f -- "$OPENGREP_SARIF_OUTPUT"
exit 0
fi
printf '%s' "$PW" | gpg --symmetric --batch --yes --quiet \
--pinentry-mode loopback --passphrase-fd 0 \
--no-symkey-cache \
--cipher-algo AES256 \
--s2k-mode 3 --s2k-digest-algo SHA512 --s2k-count 65011712 \
--output "$OUT" -- "$OPENGREP_SARIF_OUTPUT"
rm -f -- "$OPENGREP_SARIF_OUTPUT"
echo "encrypted=$OUT" >> "$GITHUB_OUTPUT"

- name: Upload encrypted SARIF artifact
if: ${{ !cancelled() && steps.encrypt.outputs.encrypted != '' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sast-scan-sarif-report
path: ${{ env.OPENGREP_SARIF_OUTPUT }}
path: ${{ steps.encrypt.outputs.encrypted }}
retention-days: 30
if-no-files-found: error
57 changes: 35 additions & 22 deletions .github/workflows/sca.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jobs:
TRIVY_SARIF_OUTPUT: trivy-platform-backend.sarif
OSV_SARIF_OUTPUT: osv-scanner-platform-backend.sarif
SCA_MERGED_SARIF_OUTPUT: SCA-platform-backend-merged.sarif
# CVSS gate: fail at >= FAIL, warn between WARN and FAIL.
GATE_FAIL_THRESHOLD: "8.0"
GATE_WARN_THRESHOLD: "5.0"

steps:
- name: Check out repository
Expand All @@ -32,41 +35,51 @@ jobs:
python-version: '3.14.7'

- name: Cache Maven packages
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.m2/repository # /.m2 only is too broad 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
- name: Setup tools and generate SBOM
run: bash ci/setup-tools.sh --install-tool trivy,osv-scanner --sbom-ecosystem maven

- name: Run SCA tools
run: python ci/sca_scan.py

- name: Upload Trivy SARIF to GitHub Security tab
id: upload_trivy
if: always()
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 #v4.38.0
- name: Upload merged SARIF to GitHub Security tab
if: ${{ !cancelled() && hashFiles(env.SCA_MERGED_SARIF_OUTPUT) != '' }}
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.2
with:
sarif_file: ${{ env.TRIVY_SARIF_OUTPUT }}
category: trivy-app
sarif_file: ${{ env.SCA_MERGED_SARIF_OUTPUT }}
category: sca-app

- name: Upload OSV Scanner SARIF to GitHub Security tab
id: upload_osv
if: always()
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 #v4.38.0
with:
sarif_file: ${{ env.OSV_SARIF_OUTPUT }}
category: osv-scanner-app
- name: Encrypt SARIF report
id: encrypt
if: ${{ !cancelled() && hashFiles(env.SCA_MERGED_SARIF_OUTPUT) != '' }}
env:
PW: ${{ secrets.ARTIFACT_PASSWORD }}
OUT: ${{ env.SCA_MERGED_SARIF_OUTPUT }}.gpg
run: |
if [ -z "$PW" ]; then
echo "::warning title=SARIF report not uploaded::ARTIFACT_PASSWORD is not available in this run (Dependabot, Renovate or fork PR). Skipping encryption and upload."
rm -f -- "$SCA_MERGED_SARIF_OUTPUT"
exit 0
fi
printf '%s' "$PW" | gpg --symmetric --batch --yes --quiet \
--pinentry-mode loopback --passphrase-fd 0 \
--no-symkey-cache \
--cipher-algo AES256 \
--s2k-mode 3 --s2k-digest-algo SHA512 --s2k-count 65011712 \
--output "$OUT" -- "$SCA_MERGED_SARIF_OUTPUT"
rm -f -- "$SCA_MERGED_SARIF_OUTPUT"
echo "encrypted=$OUT" >> "$GITHUB_OUTPUT"

- name: Upload SARIF artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
- name: Upload encrypted SARIF artifact
if: ${{ !cancelled() && steps.encrypt.outputs.encrypted != '' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sca-scan-sarif-report
path: ${{ env.SCA_MERGED_SARIF_OUTPUT }}
path: ${{ steps.encrypt.outputs.encrypted }}
retention-days: 30
if-no-files-found: error
40 changes: 40 additions & 0 deletions .github/workflows/secrets-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Secret Scanning (gitleaks)

on:
pull_request:
push:
branches:
- master
workflow_dispatch:

permissions:
contents: read
security-events: write

jobs:
gitleaks:
runs-on: ubuntu-latest
env:
GITLEAKS_SARIF_OUTPUT: gitleaks.sarif

steps:
- name: Check out repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- name: Install gitleaks
run: bash ci/setup-tools.sh --install-tool gitleaks

- name: Install and run gitleaks
run: >
gitleaks dir .
--config ci/suppress_gitleaks.toml
--redact
--report-format sarif
--report-path "$GITLEAKS_SARIF_OUTPUT"

- name: Upload SARIF to code scanning
if: always()
uses: github/codeql-action/upload-sarif@c35d1b164463ee62a100735382aaaa525c5d3496 #v2.25.6
with:
sarif_file: ${{ env.GITLEAKS_SARIF_OUTPUT }}
category: gitleaks
11 changes: 10 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
Expand All @@ -13,3 +13,12 @@ repos:
- id: pretty-format-json
args: ["--autofix"]
exclude: slack.json
- id: check-xml
- id: detect-private-key
- id: check-case-conflict
- id: mixed-line-ending

- repo: https://github.com/gitleaks/gitleaks
rev: 83d9cd684c87d95d656c1458ef04895a7f1cbd8e # frozen: v8.30.1
hooks:
- id: gitleaks
28 changes: 15 additions & 13 deletions ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,22 @@ Following the OWASP DevSecOps model, scanning is split into three independent pi

```
.github/
├── workflows/
│ ├── container-scan.yml # builds the image, scans the Dockerfile (SAST) and image (SCA)
│ ├── sca.yml # resolves deps, generates SBOM, scans it (SCA)
│ └── sast.yml # scans source code (SAST)
└── scripts/
├── setup-tools.sh # installs trivy, osv-scanner, opengrep, hadolint, semgrep-rules
├── container_scan.py # orchestrator for container-scan.yml
├── sca_scan.py # orchestrator for sca.yml
├── sast_scan.py # orchestrator for sast.yml
├── parse_sarif.py # shared: reads SARIF security-severity scores
├── suppress_trivy.yaml # shared Trivy ignore file
└── suppress_osv_scanner.toml # shared OSV-Scanner ignore file
└── workflows/
├── container-scan.yml # builds the image, scans the Dockerfile (SAST) and image (SCA)
├── sca.yml # resolves deps, generates SBOM, scans it (SCA)
├── sast.yml # scans source code (SAST)
├── publish_images.yml # publishes built images
└── ebrains.yml # EBRAINS-specific pipeline

ci/
├── setup-tools.sh # installs trivy, osv-scanner, opengrep, hadolint, semgrep-rules
├── container_scan.py # orchestrator for container-scan.yml
├── sca_scan.py # orchestrator for sca.yml
├── sast_scan.py # orchestrator for sast.yml
├── parse_sarif.py # shared: reads SARIF security-severity scores
├── suppress_trivy.yaml # shared Trivy ignore file
└── suppress_osv_scanner.toml # shared OSV-Scanner ignore file
```

> **Note:** all three workflows trigger on `pull_request`, `workflow_dispatch`, and a weekly Monday 02:00 UTC schedule, and run independently in parallel. Each has its own gate and its own category in the GitHub Security tab.

## 2. Architecture
Expand Down
13 changes: 6 additions & 7 deletions ci/container_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
import json
import argparse
from parse_sarif import evaluate
from parse_sarif import evaluate, GATE_FAIL_THRESHOLD, GATE_WARN_THRESHOLD


GREEN = '\033[92m'
Expand Down Expand Up @@ -92,25 +92,24 @@ def handle_sca():
eval_result = evaluate(path)

if eval_result.gate_failed:
tool_status[name] = "FAILED" # this tool found CVSS >= 8.0
tool_status[name] = "FAILED"
gate_failed = True
elif eval_result.gate_warn:
tool_status[name] = "WARNING" # this tool found 5.0 <= CVSS < 8.0
tool_status[name] = "WARNING"
else:
tool_status[name] = "PASSED" # this tool found nothing >= 5.0
tool_status[name] = "PASSED"

# Print summary of results
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}")
logger.warning(f"[{name}]: {YELLOW}WARNING (findings between {GATE_WARN_THRESHOLD} and {GATE_FAIL_THRESHOLD}){RESET}")
elif status == "ERROR":
logger.error(f"[{name}]: {RED}ERROR (tool did not run correctly){RESET}")
else:
logger.error(f"[{name}]: {RED}FAILED (CVSS >= 8.0 found){RESET}")
logger.info(f"{BOLD}=========================================={RESET}\n")
logger.error(f"[{name}]: {RED}FAILED (CVSS >= {GATE_FAIL_THRESHOLD} found){RESET}")

# Exit with non-zero code if any tool failed the gate
if gate_failed:
Expand Down
8 changes: 6 additions & 2 deletions ci/parse_sarif.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import json
import os
from dataclasses import dataclass

GATE_FAIL_THRESHOLD = float(os.getenv("GATE_FAIL_THRESHOLD", "8.0"))
GATE_WARN_THRESHOLD = float(os.getenv("GATE_WARN_THRESHOLD", "5.0"))

@dataclass
class EvaluationResult:
gate_failed: bool
Expand Down Expand Up @@ -34,6 +38,6 @@ def evaluate(sarif_paths):
max_score = max(max_score, float(score))

return EvaluationResult(
gate_failed=max_score >= 8,
gate_warn=5 <= max_score < 8,
gate_failed=max_score >= GATE_FAIL_THRESHOLD,
gate_warn=GATE_WARN_THRESHOLD <= max_score < GATE_FAIL_THRESHOLD,
)
2 changes: 1 addition & 1 deletion ci/sast_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
SEMGREP_CONFIG_RULESETS = os.getenv(
"SEMGREP_CONFIG_RULESETS",
" semgrep-rules/generic semgrep-rules/problem-based-packs semgrep-rules/bash "
" semgrep-rules/java auto semgrep-rules/yaml semgrep-rules/package_managers p/default "
" semgrep-rules/java auto semgrep-rules/yaml semgrep-rules/package_managers p/default"
).split()
OPENGREP_EXCLUDE = os.getenv(
"OPENGREP_EXCLUDE",
Expand Down
Loading
Loading