Law/.github/workflows/security-ci.yml
2026-03-02 16:22:07 +03:00

183 lines
5.8 KiB
YAML

name: security-ci
on:
workflow_dispatch:
pull_request:
push:
branches:
- master
- main
schedule:
- cron: "17 2 * * 1"
permissions:
contents: read
security-events: write
env:
BANDIT_MAX_HIGH: "0"
DEP_MAX_VULNS: "0"
TRIVY_MAX_HIGH: "0"
TRIVY_MAX_CRITICAL: "0"
jobs:
sast-and-dependencies:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install scanners
run: |
python -m pip install --upgrade pip
pip install bandit pip-audit
- name: Prepare reports directory
run: mkdir -p reports/security
- name: Run Bandit (SAST)
run: |
bandit -r app scripts -f json -o reports/security/bandit.json || true
- name: Run pip-audit (dependencies)
run: |
pip-audit -r requirements.txt --format json --output reports/security/pip-audit.json || true
- name: Build SAST/dependency summary
id: sast_deps_summary
run: |
set -euo pipefail
BANDIT_HIGH=$(jq '[.results[]? | select(.issue_severity == "HIGH")] | length' reports/security/bandit.json)
DEP_VULNS=$(jq '
if type == "array" then length
elif has("vulnerabilities") then (.vulnerabilities | length)
elif has("dependencies") then ([.dependencies[]?.vulns[]?] | length)
else 0
end
' reports/security/pip-audit.json)
{
echo "SAST/Dependency Security Summary"
echo "bandit.high=${BANDIT_HIGH}"
echo "deps.vulns=${DEP_VULNS}"
echo "threshold.bandit.high=${BANDIT_MAX_HIGH}"
echo "threshold.deps.vulns=${DEP_MAX_VULNS}"
} | tee reports/security/sast-deps-summary.txt
echo "bandit_high=${BANDIT_HIGH}" >> "$GITHUB_OUTPUT"
echo "dep_vulns=${DEP_VULNS}" >> "$GITHUB_OUTPUT"
- name: Upload SAST/dependency reports
if: always()
uses: actions/upload-artifact@v4
with:
name: security-sast-deps-reports
path: |
reports/security/bandit.json
reports/security/pip-audit.json
reports/security/sast-deps-summary.txt
if-no-files-found: error
retention-days: 30
- name: Enforce SAST/dependency thresholds
run: |
set -euo pipefail
BANDIT_HIGH="${{ steps.sast_deps_summary.outputs.bandit_high }}"
DEP_VULNS="${{ steps.sast_deps_summary.outputs.dep_vulns }}"
if [ "${BANDIT_HIGH}" -gt "${BANDIT_MAX_HIGH}" ]; then
echo "Bandit HIGH findings: ${BANDIT_HIGH} (max ${BANDIT_MAX_HIGH})"
exit 1
fi
if [ "${DEP_VULNS}" -gt "${DEP_MAX_VULNS}" ]; then
echo "Dependency vulnerabilities: ${DEP_VULNS} (max ${DEP_MAX_VULNS})"
exit 1
fi
container-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build backend image
run: |
docker build -t law-backend-security:${{ github.sha }} .
- name: Prepare reports directory
run: mkdir -p reports/security
- name: Run Trivy image scan (JSON report)
uses: aquasecurity/trivy-action@0.24.0
with:
image-ref: law-backend-security:${{ github.sha }}
format: json
output: reports/security/trivy-image.json
severity: HIGH,CRITICAL
exit-code: "0"
- name: Run Trivy image scan (SARIF)
uses: aquasecurity/trivy-action@0.24.0
with:
image-ref: law-backend-security:${{ github.sha }}
format: sarif
output: reports/security/trivy-image.sarif
severity: HIGH,CRITICAL
exit-code: "0"
- name: Build container scan summary
id: trivy_summary
run: |
set -euo pipefail
TRIVY_HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length' reports/security/trivy-image.json)
TRIVY_CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' reports/security/trivy-image.json)
{
echo "Container Security Summary"
echo "trivy.high=${TRIVY_HIGH}"
echo "trivy.critical=${TRIVY_CRITICAL}"
echo "threshold.trivy.high=${TRIVY_MAX_HIGH}"
echo "threshold.trivy.critical=${TRIVY_MAX_CRITICAL}"
} | tee reports/security/trivy-summary.txt
echo "trivy_high=${TRIVY_HIGH}" >> "$GITHUB_OUTPUT"
echo "trivy_critical=${TRIVY_CRITICAL}" >> "$GITHUB_OUTPUT"
- name: Upload Trivy reports
if: always()
uses: actions/upload-artifact@v4
with:
name: security-container-reports
path: |
reports/security/trivy-image.json
reports/security/trivy-image.sarif
reports/security/trivy-summary.txt
if-no-files-found: error
retention-days: 30
- name: Upload SARIF to Security tab
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: reports/security/trivy-image.sarif
- name: Enforce container scan thresholds
run: |
set -euo pipefail
TRIVY_HIGH="${{ steps.trivy_summary.outputs.trivy_high }}"
TRIVY_CRITICAL="${{ steps.trivy_summary.outputs.trivy_critical }}"
if [ "${TRIVY_HIGH}" -gt "${TRIVY_MAX_HIGH}" ]; then
echo "Trivy HIGH findings: ${TRIVY_HIGH} (max ${TRIVY_MAX_HIGH})"
exit 1
fi
if [ "${TRIVY_CRITICAL}" -gt "${TRIVY_MAX_CRITICAL}" ]; then
echo "Trivy CRITICAL findings: ${TRIVY_CRITICAL} (max ${TRIVY_MAX_CRITICAL})"
exit 1
fi