Skip to content

Security

Security #34

Workflow file for this run

# ─────────────────────────────────────────────────────────────────────────────
# DevSec Vault — Security Scanning Pipeline
# All actions pinned to full commit SHA (Scorecard: Pinned-Dependencies)
# ─────────────────────────────────────────────────────────────────────────────
name: Security
on:
push:
branches: ["main", "develop"]
pull_request:
schedule:
- cron: "0 3 * * 1" # Weekly Monday 03:00 UTC
# Minimal global permissions — each job declares only what it needs
permissions:
contents: read
jobs:
# ── 1. Secret Scanning ────────────────────────────────────────────────────
secret-scan:
name: Secret Scan (file + history)
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # SARIF upload — scoped to this job only
steps:
- name: Checkout (full history)
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
cache: "pip"
- name: Install DevSec Vault dependencies
run: pip install "click==8.1.8" "rich==13.9.4"
- name: Create reports directory
run: mkdir -p reports
- name: Scan source files
run: |
python src/cli.py scan src/ \
--format json \
--output reports/file-scan.json \
--baseline .devsec-baseline.json \
--fail-on high
env:
DEVSEC_EXCLUDE: "src/fake_secrets.py"
- name: Scan git history (last 300 commits)
run: |
python src/cli.py history \
--max-commits 300 \
--format sarif \
--output reports/history-scan.sarif \
--baseline .devsec-baseline.json
continue-on-error: true
- name: Upload SARIF — history scan
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
if: always() && hashFiles('reports/history-scan.sarif') != ''
with:
sarif_file: reports/history-scan.sarif
category: devsec-vault-history
continue-on-error: true
- name: Upload scan artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: scan-reports
path: reports/
retention-days: 30
# ── 2. Static Analysis (Bandit) ───────────────────────────────────────────
static-analysis:
name: Static Analysis (Bandit)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- name: Install Bandit
run: pip install "bandit==1.8.3"
- name: Create reports directory
run: mkdir -p reports
- name: Run Bandit
run: |
bandit -r src/ \
--exclude src/fake_secrets.py \
--format json \
--output reports/bandit.json \
--severity-level medium \
--confidence-level medium || true
- name: Print Bandit summary
if: always()
run: |
python3 -c "
import json, sys
try:
d = json.load(open('reports/bandit.json'))
m = d.get('metrics', {}).get('_totals', {})
issues = d.get('results', [])
print(f'Bandit: {len(issues)} issue(s) found')
for sev in ['HIGH','MEDIUM']:
count = m.get(f'SEVERITY.{sev}', 0)
if count: print(f' {sev}: {count}')
if any(r[\"issue_severity\"] == \"HIGH\" for r in issues):
print('HIGH severity issues found — failing')
sys.exit(1)
except Exception as e:
print(f'Could not parse bandit report: {e}')
"
- name: Upload Bandit report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: bandit-report
path: reports/bandit.json
retention-days: 30
# ── 3. CodeQL ─────────────────────────────────────────────────────────────
codeql:
name: CodeQL (Python)
runs-on: ubuntu-latest
continue-on-error: true
permissions:
contents: read
actions: read
security-events: write # SARIF upload — scoped to this job only
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Initialize CodeQL
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
languages: python
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
category: "/language:python"
continue-on-error: true
# ── 4. Dependency Audit ───────────────────────────────────────────────────
dependency-audit:
name: Dependency Audit (pip-audit)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- name: Install pip-audit
run: pip install "pip-audit==2.9.0"
- name: Create reports directory
run: mkdir -p reports
- name: Run pip-audit
run: |
pip-audit \
--requirement requirements.txt \
--format json \
--output reports/pip-audit.json \
--progress-spinner off
continue-on-error: true
- name: Print pip-audit summary
if: always()
run: |
python3 -c "
import json
try:
d = json.load(open('reports/pip-audit.json'))
vulns = [dep for dep in d if dep.get('vulns')]
print(f'pip-audit: {len(vulns)} package(s) with vulnerabilities')
for dep in vulns:
for v in dep['vulns']:
print(f' {dep[\"name\"]}=={dep[\"version\"]}: {v[\"id\"]} ({v.get(\"fix_versions\",[])})')
except Exception as e:
print(f'Could not parse pip-audit report: {e}')
"
- name: Upload pip-audit report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: dependency-audit
path: reports/pip-audit.json
retention-days: 30
# ── 5. Unit Tests ─────────────────────────────────────────────────────────
test:
name: Unit Tests
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
cache: "pip"
- name: Install dependencies
run: pip install "click==8.1.8" "rich==13.9.4" "pytest==9.0.3"
- name: Run tests
run: pytest tests/ -v --tb=short