DAST Nightly #150
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: DAST Nightly | |
| on: | |
| push: | |
| branches: [main] | |
| schedule: | |
| - cron: '0 2 * * *' | |
| concurrency: | |
| group: dast-nightly-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| nightly-scan: | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| issues: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 | |
| with: | |
| fetch-depth: 1 | |
| - name: Setup Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: demo-app/package-lock.json | |
| - name: Install dependencies | |
| working-directory: demo-app | |
| run: npm ci | |
| - name: Install script tooling (YAML profile merge) | |
| run: npm ci --prefix scripts | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Build nightly image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: ./demo-app | |
| load: true | |
| tags: zerodast-demo-app:${{ github.sha }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Build surgical ZAP image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: ./docker | |
| file: ./docker/Dockerfile.zerodast-scanner | |
| load: true | |
| tags: zerodast-scanner:${{ github.sha }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Pre-pull trusted images | |
| run: | | |
| docker pull postgres:16-alpine & | |
| docker pull node:20-alpine & | |
| wait | |
| - name: Prepare post-scan hook | |
| run: | | |
| cat <<'EOF' > /tmp/post-scan.sh | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| bash scripts/verify-admin-coverage.sh reports/zap-report.json | |
| bash scripts/verify-canaries.sh reports/zap-report.json | |
| EOF | |
| chmod +x /tmp/post-scan.sh | |
| - name: Run nightly DAST | |
| env: | |
| ZAP_IMAGE: zerodast-scanner:${{ github.sha }} | |
| SCAN_PROFILE: "" | |
| ZERODAST_TARGET_NAME: zerodast-demo-app | |
| ZERODAST_SCAN_PROFILE: nightly-full | |
| ZERODAST_SCAN_TRIGGER: push-or-schedule | |
| ZERODAST_SCAN_MODE: core | |
| APP_IMAGE: zerodast-demo-app:${{ github.sha }} | |
| SCHEMA_SQL: ${{ github.workspace }}/db/seed/schema.sql | |
| MOCK_DATA_SQL: ${{ github.workspace }}/db/seed/mock_data.sql | |
| ZAP_CONFIG_PATH: ${{ github.workspace }}/security/zap/automation.yaml | |
| ZAP_FAIL_LEVEL: High | |
| BASELINE_SUPPRESSIONS_PATH: ${{ github.workspace }}/security/zap/.zap-baseline.json | |
| FINDING_BASELINE_PATH: ${{ github.workspace }}/security/zap/.zap-result-baseline.json | |
| AUTH_BOOTSTRAP_MODE: adapter | |
| AUTH_ADAPTER_SCRIPT: ${{ github.workspace }}/scripts/auth-adapters/json-token-login.sh | |
| AUTH_TOKEN_PATH: /tmp/zap-auth-token.txt | |
| ADMIN_AUTH_TOKEN_PATH: /tmp/zap-auth-token-admin.txt | |
| RUN_AUTHZ_NETWORK: 'true' | |
| POST_SCAN_SCRIPT: /tmp/post-scan.sh | |
| REPORTS_DIR: ${{ github.workspace }}/reports | |
| run: bash security/run-dast-env.sh | |
| - name: Upload nightly report artifact | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: nightly-dast-report | |
| path: reports | |
| retention-days: 30 | |
| if-no-files-found: warn | |
| - name: Parse report | |
| if: always() | |
| id: parse_report | |
| run: | | |
| set +e | |
| if [ -f reports/zap-report.json ]; then | |
| node scripts/parse-zap-report.js reports/zap-report.json | tee report-summary.md | |
| status=${PIPESTATUS[0]} | |
| else | |
| echo 'ZAP report was not produced.' | tee report-summary.md | |
| status=1 | |
| fi | |
| echo "parse_exit=$status" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| - name: Create issue for failing threshold | |
| if: always() && steps.parse_report.outputs.parse_exit != '0' | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const policy = JSON.parse(fs.readFileSync('security/report-policy.json', 'utf8')); | |
| const issuePolicy = policy.nightlyIssue || {}; | |
| if (issuePolicy.enabled === false) { | |
| console.log('Skipping nightly issue creation because policy disabled it.'); | |
| return; | |
| } | |
| const summary = fs.readFileSync('report-summary.md', 'utf8'); | |
| const resultState = fs.existsSync('reports/result-state.json') | |
| ? JSON.parse(fs.readFileSync('reports/result-state.json', 'utf8')) | |
| : null; | |
| const comparison = resultState?.comparison || {}; | |
| const mode = issuePolicy.mode || 'threshold_or_new_findings'; | |
| const parseFailed = '${{ steps.parse_report.outputs.parse_exit }}' !== '0'; | |
| const shouldOpen = | |
| mode === 'always' || | |
| (mode === 'threshold_only' && parseFailed) || | |
| (mode === 'new_findings' && (comparison.newFindingCount || 0) > 0) || | |
| (mode === 'threshold_or_new_findings' && | |
| (parseFailed || (comparison.newFindingCount || 0) > 0)); | |
| if (!shouldOpen) { | |
| console.log(`Skipping nightly issue creation due to policy mode=${mode}`); | |
| return; | |
| } | |
| const rollupLabel = issuePolicy.rollupLabel || 'dast-nightly-rollup'; | |
| const rollupTitle = | |
| issuePolicy.rollupTitle || | |
| issuePolicy.titlePrefix || | |
| 'ZeroDAST nightly DAST rollup'; | |
| const issueLabels = Array.isArray(issuePolicy.labels) | |
| ? issuePolicy.labels | |
| : ['security', 'dast', rollupLabel]; | |
| const body = [ | |
| 'Nightly DAST produced an actionable result according to report policy.', | |
| '', | |
| `**Commit:** \`${context.sha}\``, | |
| '', | |
| '### Policy Summary', | |
| '', | |
| `- Policy mode: ${mode}`, | |
| `- State: ${resultState?.state || 'unknown'}`, | |
| `- New findings vs baseline: ${comparison.newFindingCount || 0}`, | |
| `- Persisting findings vs baseline: ${comparison.persistingFindingCount || 0}`, | |
| `- Resolved findings vs baseline: ${comparison.resolvedFindingCount || 0}`, | |
| '', | |
| summary, | |
| '', | |
| `Workflow run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| '', | |
| '_This issue is the single rollup for nightly DAST; CI updates it instead of opening a new issue each run._', | |
| ].join('\n'); | |
| async function listOpenRollupIssues() { | |
| const out = []; | |
| for (let page = 1; page <= 20; page += 1) { | |
| const { data } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: rollupLabel, | |
| per_page: 100, | |
| page, | |
| }); | |
| out.push(...data); | |
| if (data.length < 100) break; | |
| } | |
| return out; | |
| } | |
| const rollupIssues = await listOpenRollupIssues(); | |
| rollupIssues.sort((a, b) => a.number - b.number); | |
| const existing = rollupIssues[0]; | |
| if (existing) { | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: existing.number, | |
| title: rollupTitle, | |
| body, | |
| state: 'open', | |
| }); | |
| if (rollupIssues.length > 1) { | |
| const dupes = rollupIssues.slice(1); | |
| for (const dupe of dupes) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: dupe.number, | |
| body: | |
| 'Closed automatically: duplicate nightly rollup. Canonical rollup: #' + | |
| existing.number + | |
| '.', | |
| }); | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: dupe.number, | |
| state: 'closed', | |
| state_reason: 'not_planned', | |
| }); | |
| } | |
| } | |
| return; | |
| } | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: rollupTitle, | |
| body, | |
| labels: issueLabels, | |
| }); |