Skip to content

DAST Nightly

DAST Nightly #141

Workflow file for this run

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,
});