Security Scan #933
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Security Scan | |
| on: | |
| push: | |
| branches: [ main, dev, develop ] | |
| pull_request: | |
| branches: [ main, dev, develop ] | |
| schedule: | |
| # Run security scans daily at 2 AM UTC | |
| - cron: '0 2 * * *' | |
| jobs: | |
| dependency-scan: | |
| name: Dependency Security Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install pip-audit | |
| - name: Run pip-audit on locked requirements | |
| # CVE-2025-3000: memory corruption in torch.jit.script, local-host attack | |
| # only, no patched torch release exists. torch is a transitive dependency | |
| # (transformers / sentence-transformers) and the app never calls | |
| # torch.jit.script on any input — not reachable. Accepted-risk suppression; | |
| # revisit when an upstream fix ships. See TODO.md "CI And Security Evidence". | |
| run: | | |
| pip-audit -r requirements.txt --desc --format json --output pip-audit-report.json --ignore-vuln CVE-2025-3000 | |
| - name: Upload dependency scan report | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pip-audit-report | |
| path: pip-audit-report.json | |
| npm-audit: | |
| name: NPM Security Audit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24' | |
| - name: Run npm audit | |
| env: | |
| # npm audit only needs the dependency tree, not the binaries. Skipping | |
| # the electron/chromedriver postinstall downloads keeps this job from | |
| # flaking on transient CDN errors (e.g. HTTP 502 from electron install.js). | |
| ELECTRON_SKIP_BINARY_DOWNLOAD: 1 | |
| CHROMEDRIVER_SKIP_DOWNLOAD: true | |
| run: | | |
| cd frontend | |
| npm ci | |
| # || true: npm audit exits non-zero when vulnerabilities are found at the | |
| # specified level. We capture the report as an artifact for review without | |
| # hard-failing the job on known/accepted findings. | |
| npm audit --audit-level=high --json > ../npm-audit.json || true | |
| cat ../npm-audit.json | |
| - name: Upload npm audit report | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: npm-audit-report | |
| path: npm-audit.json | |
| code-security: | |
| name: Code Security Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install bandit | |
| run: pip install bandit[toml] | |
| - name: Run Bandit full report | |
| run: | | |
| bandit -r backend/ core/ --exit-zero -f json -o bandit-full-report.json | |
| - name: Validate Bandit baseline exists | |
| run: | | |
| test -f .bandit-baseline.json | |
| - name: Normalize Bandit baseline paths | |
| run: | | |
| python - <<'PY' | |
| import json | |
| from pathlib import Path | |
| source = Path(".bandit-baseline.json") | |
| target = Path(".bandit-baseline.ci.json") | |
| data = json.loads(source.read_text()) | |
| metrics = data.get("metrics", {}) | |
| data["metrics"] = { | |
| key.replace("\\", "/") if key != "_totals" else key: value | |
| for key, value in metrics.items() | |
| } | |
| for result in data.get("results", []): | |
| filename = result.get("filename") | |
| if isinstance(filename, str): | |
| result["filename"] = filename.replace("\\", "/") | |
| target.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n") | |
| PY | |
| - name: Run Bandit delta gate (new high-confidence issues) | |
| run: | | |
| bandit -r backend/ core/ -b .bandit-baseline.ci.json --exit-zero -f json -o bandit-report.json | |
| bandit -r backend/ core/ -b .bandit-baseline.ci.json -ll -ii -f screen | |
| - name: Upload Bandit report | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: bandit-report | |
| path: | | |
| bandit-report.json | |
| bandit-full-report.json | |
| secret-scan: | |
| name: Secret Scanning | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Resolve TruffleHog diff range | |
| if: github.event_name != 'schedule' | |
| id: trufflehog-diff | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const before = context.payload.before || ''; | |
| const isZero = /^0+$/.test(before); | |
| let base = before; | |
| if (context.eventName === 'pull_request') { | |
| base = context.payload.pull_request.base.sha; | |
| } else if (!base || isZero) { | |
| const commits = (await github.rest.repos.listCommits({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| per_page: 2, | |
| sha: context.sha, | |
| })).data; | |
| base = commits.length > 1 ? commits[commits.length - 1].sha : context.sha; | |
| } | |
| core.setOutput('base', base); | |
| core.setOutput('head', context.eventName === 'pull_request' | |
| ? context.payload.pull_request.head.sha | |
| : context.sha); | |
| - name: TruffleHog OSS (push/pr diff) | |
| if: github.event_name != 'schedule' | |
| uses: trufflesecurity/trufflehog@v3.93.1 | |
| with: | |
| path: ./ | |
| base: ${{ steps.trufflehog-diff.outputs.base }} | |
| head: ${{ steps.trufflehog-diff.outputs.head }} | |
| extra_args: --debug --only-verified | |
| - name: TruffleHog OSS (scheduled full scan) | |
| if: github.event_name == 'schedule' | |
| uses: trufflesecurity/trufflehog@v3.93.1 | |
| with: | |
| path: ./ | |
| extra_args: --debug --only-verified | |
| codeql-analysis: | |
| name: CodeQL Analysis | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: read | |
| security-events: write | |
| strategy: | |
| matrix: | |
| language: [ 'python', 'javascript' ] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Initialize CodeQL | |
| uses: github/codeql-action/init@v4 | |
| with: | |
| languages: ${{ matrix.language }} | |
| - name: Autobuild | |
| uses: github/codeql-action/autobuild@v4 | |
| - name: Perform CodeQL Analysis | |
| uses: github/codeql-action/analyze@v4 | |
| sbom: | |
| name: SBOM Generation | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install CycloneDX tools | |
| run: pip install cyclonedx-bom | |
| - name: Generate Backend SBOM | |
| run: | | |
| if [ -f requirements-enterprise.txt ]; then | |
| cyclonedx-py requirements requirements-enterprise.txt --output-file sbom-backend.json | |
| else | |
| cyclonedx-py requirements requirements.txt --output-file sbom-backend.json | |
| fi | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24' | |
| - name: Generate Frontend SBOM | |
| run: | | |
| if [ -f frontend/package-lock.json ]; then | |
| cd frontend | |
| npx @cyclonedx/cyclonedx-npm --package-lock-only --ignore-npm-errors --output-file ../sbom-frontend.json | |
| fi | |
| - name: Upload SBOMs | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sbom-reports | |
| path: | | |
| sbom-backend.json | |
| sbom-frontend.json | |
| if-no-files-found: error | |
| artifact-signing: | |
| name: Artifact Signing (Cosign) | |
| needs: [sbom] | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Download SBOM artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: sbom-reports | |
| path: . | |
| - name: Install Cosign | |
| uses: sigstore/cosign-installer@v4.0.0 | |
| - name: Sign SBOM artifacts (keyless OIDC) | |
| env: | |
| COSIGN_EXPERIMENTAL: "1" | |
| run: | | |
| set +e | |
| cosign version | |
| test -f sbom-backend.json | |
| cosign sign-blob --yes sbom-backend.json \ | |
| --output-signature sbom-backend.json.sig \ | |
| --output-certificate sbom-backend.json.pem | |
| backend_status=$? | |
| if [ "$backend_status" -ne 0 ]; then | |
| echo "::warning::Cosign SBOM signing is unavailable for this run; continuing without signatures." | |
| rm -f sbom-backend.json.sig sbom-backend.json.pem sbom-frontend.json.sig sbom-frontend.json.pem | |
| exit 0 | |
| fi | |
| if [ -f sbom-frontend.json ]; then | |
| cosign sign-blob --yes sbom-frontend.json \ | |
| --output-signature sbom-frontend.json.sig \ | |
| --output-certificate sbom-frontend.json.pem | |
| frontend_status=$? | |
| if [ "$frontend_status" -ne 0 ]; then | |
| echo "::warning::Cosign frontend SBOM signing failed; backend SBOM signature was preserved." | |
| rm -f sbom-frontend.json.sig sbom-frontend.json.pem | |
| fi | |
| fi | |
| - name: Upload signing artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sbom-signatures | |
| path: | | |
| sbom-backend.json.sig | |
| sbom-backend.json.pem | |
| sbom-frontend.json.sig | |
| sbom-frontend.json.pem | |
| if-no-files-found: warn | |
| crash-reporting-probe: | |
| name: Crash Reporting Probe | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'schedule' || github.ref == 'refs/heads/main' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| - name: Check crash reporting configuration | |
| id: crash-reporting-config | |
| env: | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| run: | | |
| if [ -n "$SENTRY_DSN" ]; then | |
| echo "configured=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "configured=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::SENTRY_DSN is not configured; skipping crash-reporting probe." | |
| fi | |
| - name: Send Sentry probe event | |
| if: steps.crash-reporting-config.outputs.configured == 'true' | |
| env: | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| FLASK_ENV: "production" | |
| APP_VERSION: "security-workflow-probe" | |
| run: | | |
| python scripts/send_sentry_test_event.py --message "DataLogicEngine security workflow crash probe" --tag source=security --tag workflow=security |