security(deps)(deps): bump the nodejs-minor-patch group across 1 directory with 8 updates #2379
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 Checks | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| pull_request: | |
| branches: [ main ] | |
| schedule: | |
| # Run security checks daily at 2 AM UTC | |
| - cron: '0 2 * * *' | |
| permissions: | |
| contents: read | |
| security-events: write | |
| actions: read | |
| pull-requests: write # Required for commenting on PRs | |
| jobs: | |
| dependency-scan: | |
| name: Dependency Vulnerability Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '18' | |
| cache: 'npm' | |
| cache-dependency-path: frontend/package-lock.json | |
| # Python dependency scanning with Safety | |
| - name: Install Python dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install safety bandit semgrep | |
| pip install -e ".[dev]" | |
| - name: Python Safety Check | |
| continue-on-error: true | |
| run: | | |
| timeout 120 safety check --json --output safety-report.json || true | |
| if [ -f safety-report.json ]; then | |
| echo "Safety vulnerabilities found. Check the report:" | |
| cat safety-report.json | |
| fi | |
| # Node.js dependency scanning with npm audit | |
| - name: Install Node.js dependencies | |
| run: | | |
| cd frontend | |
| npm ci | |
| - name: Node.js Audit | |
| run: | | |
| cd frontend | |
| npm audit --audit-level moderate --json > npm-audit-report.json || true | |
| if [ -s npm-audit-report.json ]; then | |
| echo "NPM vulnerabilities found. Check the report:" | |
| cat npm-audit-report.json | |
| fi | |
| # Advanced dependency scanning with Snyk (requires SNYK_TOKEN) | |
| # - name: Run Snyk to check for vulnerabilities | |
| # uses: snyk/actions/setup@master | |
| # | |
| # - name: Snyk Python scan | |
| # run: | | |
| # snyk test --file=pyproject.toml --json > snyk-python-report.json || true | |
| # env: | |
| # SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | |
| # continue-on-error: true | |
| # - name: Snyk Node.js scan | |
| # run: | | |
| # cd frontend | |
| # snyk test --json > snyk-node-report.json || true | |
| # env: | |
| # SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | |
| # continue-on-error: true | |
| # Upload dependency scan artifacts | |
| - name: Upload dependency scan reports | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: dependency-scan-reports | |
| path: | | |
| safety-report.json | |
| frontend/npm-audit-report.json | |
| sast-scan: | |
| name: Static Application Security Testing | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| # Bandit for Python SAST | |
| - name: Install Bandit | |
| run: pip install bandit[toml] | |
| - name: Run Bandit Python SAST | |
| run: | | |
| bandit -r . -f json -o bandit-report.json || true | |
| if [ -f bandit-report.json ]; then | |
| echo "Bandit found security issues:" | |
| cat bandit-report.json | |
| fi | |
| # Semgrep for multi-language SAST (requires SEMGREP_APP_TOKEN) | |
| # - name: Run Semgrep | |
| # uses: returntocorp/semgrep-action@v1 | |
| # with: | |
| # config: >- | |
| # p/security-audit | |
| # p/secrets | |
| # p/python | |
| # p/javascript | |
| # p/typescript | |
| # p/react | |
| # generateSarif: "1" | |
| # env: | |
| # SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} | |
| # CodeQL Analysis | |
| - name: Initialize CodeQL | |
| uses: github/codeql-action/init@v3 | |
| with: | |
| languages: javascript, python | |
| queries: security-extended,security-and-quality | |
| config-file: ./.github/codeql/codeql-config.yml | |
| - name: Autobuild | |
| uses: github/codeql-action/autobuild@v3 | |
| - name: Perform CodeQL Analysis | |
| uses: github/codeql-action/analyze@v3 | |
| with: | |
| category: "/language:javascript,python" | |
| # Upload SAST artifacts | |
| - name: Upload SAST reports | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: sast-reports | |
| path: | | |
| bandit-report.json | |
| secrets-scan: | |
| name: Secrets Scanning | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 # Full history for better secret detection | |
| # TruffleHog for secrets scanning | |
| - name: TruffleHog OSS | |
| uses: trufflesecurity/trufflehog@main | |
| with: | |
| path: ./ | |
| extra_args: --debug --only-verified | |
| # GitLeaks for additional secret detection (requires GITLEAKS_LICENSE) | |
| # - name: Run GitLeaks | |
| # uses: gitleaks/gitleaks-action@v2 | |
| # env: | |
| # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} | |
| container-scan: | |
| name: Container Security Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Build Docker image for scanning | |
| run: | | |
| docker build -t security-scan:latest . | |
| # Trivy for container vulnerability scanning | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| image-ref: 'security-scan:latest' | |
| format: 'sarif' | |
| output: 'trivy-results.sarif' | |
| - name: Upload Trivy scan results to GitHub Security tab | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() | |
| with: | |
| sarif_file: 'trivy-results.sarif' | |
| # Docker Scout for additional container scanning | |
| - name: Docker Scout scan | |
| uses: docker/scout-action@v1 | |
| if: always() | |
| continue-on-error: true | |
| with: | |
| command: cves | |
| image: security-scan:latest | |
| only-severities: critical,high | |
| format: sarif | |
| output: scout-results.sarif | |
| - name: Upload Scout scan results | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() && hashFiles('scout-results.sarif') != '' | |
| with: | |
| sarif_file: 'scout-results.sarif' | |
| # SBOM generation with Syft (Anchore) — bundled into the container scan artifact | |
| - name: Generate SBOM (Syft) | |
| uses: anchore/sbom-action@v0 | |
| with: | |
| image: security-scan:latest | |
| format: spdx-json | |
| output-file: sbom.spdx.json | |
| upload-artifact: false | |
| # Grype for image vulnerability scanning. Report-first defaults: fail-build=false, | |
| # only-fixed=true so signal stays focused on actionable, fixable CVEs. | |
| - name: Run Grype vulnerability scanner | |
| id: grype | |
| uses: anchore/scan-action@v7 | |
| continue-on-error: true | |
| with: | |
| image: security-scan:latest | |
| fail-build: false | |
| severity-cutoff: high | |
| only-fixed: true | |
| output-format: sarif | |
| output-file: grype-results.sarif | |
| - name: Upload Grype scan results to GitHub Security tab | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() && hashFiles('grype-results.sarif') != '' | |
| with: | |
| sarif_file: grype-results.sarif | |
| category: grype | |
| # Upload container scan artifacts | |
| - name: Upload container scan reports | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: container-scan-reports | |
| path: | | |
| trivy-results.sarif | |
| scout-results.sarif | |
| grype-results.sarif | |
| sbom.spdx.json | |
| if-no-files-found: ignore | |
| security-summary: | |
| name: Security Summary | |
| runs-on: ubuntu-latest | |
| needs: [dependency-scan, sast-scan, secrets-scan, container-scan] | |
| if: always() | |
| steps: | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v8 | |
| - name: Install jq | |
| run: sudo apt-get install -y jq | |
| - name: Generate Security Summary | |
| run: | | |
| echo "# Security Scan Summary" > security-summary.md | |
| echo "" >> security-summary.md | |
| # --- Python Dependencies (Safety) --- | |
| echo "### Python Dependencies (Safety)" >> security-summary.md | |
| REPORT="dependency-scan-reports/safety-report.json" | |
| if [ -f "$REPORT" ] && [ -s "$REPORT" ]; then | |
| VULN_COUNT=$(jq '[.vulnerabilities // [] | length] | add // 0' "$REPORT" 2>/dev/null || echo "0") | |
| if [ "$VULN_COUNT" -gt 0 ] 2>/dev/null; then | |
| echo "**$VULN_COUNT vulnerabilities found**" >> security-summary.md | |
| echo "" >> security-summary.md | |
| echo "| Package | Installed | Vuln ID | Severity | Description |" >> security-summary.md | |
| echo "|---------|-----------|---------|----------|-------------|" >> security-summary.md | |
| jq -r '.vulnerabilities[]? | "| \(.package_name // "?") | \(.analyzed_version // "?") | \(.vulnerability_id // "?") | \(.severity // "?") | \(.advisory[0:80] // "?") |"' "$REPORT" 2>/dev/null >> security-summary.md || true | |
| else | |
| echo "No known vulnerabilities found." >> security-summary.md | |
| fi | |
| else | |
| echo "Report not available." >> security-summary.md | |
| fi | |
| echo "" >> security-summary.md | |
| # --- Node.js Dependencies (npm audit) --- | |
| echo "### Node.js Dependencies (npm audit)" >> security-summary.md | |
| REPORT="dependency-scan-reports/npm-audit-report.json" | |
| if [ -f "$REPORT" ] && [ -s "$REPORT" ]; then | |
| TOTAL=$(jq '.metadata.vulnerabilities // {} | to_entries | map(.value) | add // 0' "$REPORT" 2>/dev/null || echo "0") | |
| if [ "$TOTAL" -gt 0 ] 2>/dev/null; then | |
| CRIT=$(jq '.metadata.vulnerabilities.critical // 0' "$REPORT" 2>/dev/null) | |
| HIGH=$(jq '.metadata.vulnerabilities.high // 0' "$REPORT" 2>/dev/null) | |
| MOD=$(jq '.metadata.vulnerabilities.moderate // 0' "$REPORT" 2>/dev/null) | |
| LOW=$(jq '.metadata.vulnerabilities.low // 0' "$REPORT" 2>/dev/null) | |
| echo "**$TOTAL vulnerabilities:** $CRIT critical, $HIGH high, $MOD moderate, $LOW low" >> security-summary.md | |
| echo "" >> security-summary.md | |
| # List top-level vulnerable packages | |
| echo "| Package | Severity | Via | Fix Available |" >> security-summary.md | |
| echo "|---------|----------|----|---------------|" >> security-summary.md | |
| jq -r '.vulnerabilities // {} | to_entries[] | "| \(.key) | \(.value.severity // "?") | \(.value.via | if type == "array" then [.[] | if type == "string" then . else .name // "?" end] | join(", ") else "?" end) | \(if .value.fixAvailable then "Yes" else "No" end) |"' "$REPORT" 2>/dev/null | head -20 >> security-summary.md || true | |
| else | |
| echo "No vulnerabilities found." >> security-summary.md | |
| fi | |
| else | |
| echo "Report not available." >> security-summary.md | |
| fi | |
| echo "" >> security-summary.md | |
| # --- Python SAST (Bandit) --- | |
| echo "### Python SAST (Bandit)" >> security-summary.md | |
| REPORT="sast-reports/bandit-report.json" | |
| if [ -f "$REPORT" ] && [ -s "$REPORT" ]; then | |
| ISSUE_COUNT=$(jq '.results | length' "$REPORT" 2>/dev/null || echo "0") | |
| if [ "$ISSUE_COUNT" -gt 0 ] 2>/dev/null; then | |
| HIGH_SEV=$(jq '[.results[] | select(.issue_severity == "HIGH")] | length' "$REPORT" 2>/dev/null || echo "0") | |
| MED_SEV=$(jq '[.results[] | select(.issue_severity == "MEDIUM")] | length' "$REPORT" 2>/dev/null || echo "0") | |
| LOW_SEV=$(jq '[.results[] | select(.issue_severity == "LOW")] | length' "$REPORT" 2>/dev/null || echo "0") | |
| echo "**$ISSUE_COUNT issues:** $HIGH_SEV high, $MED_SEV medium, $LOW_SEV low severity" >> security-summary.md | |
| echo "" >> security-summary.md | |
| # Show high/medium issues with file locations | |
| HIGH_MED=$(jq '[.results[] | select(.issue_severity == "HIGH" or .issue_severity == "MEDIUM")] | length' "$REPORT" 2>/dev/null || echo "0") | |
| if [ "$HIGH_MED" -gt 0 ] 2>/dev/null; then | |
| echo "**High/Medium severity issues:**" >> security-summary.md | |
| echo "" >> security-summary.md | |
| echo "| Severity | Confidence | File | Line | Issue |" >> security-summary.md | |
| echo "|----------|------------|------|------|-------|" >> security-summary.md | |
| jq -r '.results[] | select(.issue_severity == "HIGH" or .issue_severity == "MEDIUM") | "| \(.issue_severity) | \(.issue_confidence) | \(.filename | split("/") | .[-2:] | join("/")) | \(.line_number) | \(.issue_text[0:80]) |"' "$REPORT" 2>/dev/null | head -30 >> security-summary.md || true | |
| fi | |
| else | |
| echo "No security issues found." >> security-summary.md | |
| fi | |
| else | |
| echo "Report not available." >> security-summary.md | |
| fi | |
| echo "" >> security-summary.md | |
| # --- Job Status --- | |
| echo "### Scan Status" >> security-summary.md | |
| echo "| Scan | Status |" >> security-summary.md | |
| echo "|------|--------|" >> security-summary.md | |
| echo "| Dependency Scan | ${{ needs.dependency-scan.result }} |" >> security-summary.md | |
| echo "| SAST Scan | ${{ needs.sast-scan.result }} |" >> security-summary.md | |
| echo "| Secrets Scan | ${{ needs.secrets-scan.result }} |" >> security-summary.md | |
| echo "| Container Scan | ${{ needs.container-scan.result }} |" >> security-summary.md | |
| cat security-summary.md | |
| - name: Upload Security Summary | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: security-summary | |
| path: security-summary.md | |
| # Comment on PR with security summary | |
| - name: Comment PR with Security Summary | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const summary = fs.readFileSync('security-summary.md', 'utf8'); | |
| // Find and update existing security comment, or create a new one | |
| const comments = await github.rest.issues.listComments({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| }); | |
| const marker = '<!-- security-scan-summary -->'; | |
| const body = `${marker}\n## Security Scan Results\n\n${summary}`; | |
| const existing = comments.data.find(c => c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body: body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body, | |
| }); | |
| } |