name: Online search for vulns on: schedule: - cron: '42 8 * * *' workflow_dispatch: {} # allow manual trigger permissions: contents: read actions: read # needed to list/download previous run artifacts id-token: write # needed to mint OIDC token concurrency: group: vuln-scan cancel-in-progress: true jobs: scan: runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout repository (for grep-based dedup against existing checks) uses: actions/checkout@v5 with: fetch-depth: 1 persist-credentials: false # ---- Load previous state --------------------------------------------- # Find the most recent successful run of THIS workflow (other than the # current one) and pull its `vuln-scan-state` artifact. On the very # first run there will be none — that's fine, we start empty. - name: Find previous successful run id id: prev env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -e run_id=$(gh run list \ --workflow="${{ github.workflow }}" \ --status=success \ --limit 1 \ --json databaseId \ --jq '.[0].databaseId // empty') echo "run_id=${run_id}" >> "$GITHUB_OUTPUT" if [ -n "$run_id" ]; then echo "Found previous successful run: $run_id" else echo "No previous successful run — starting from empty state." fi - name: Download previous state artifact if: steps.prev.outputs.run_id != '' uses: actions/download-artifact@v4 continue-on-error: true # tolerate retention expiry with: name: vuln-scan-state path: state/ run-id: ${{ steps.prev.outputs.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Ensure state file exists run: | mkdir -p state if [ ! -f state/seen.json ]; then echo '{"last_run": null, "seen": {}}' > state/seen.json echo "Initialized empty state." fi echo "State size: $(wc -c < state/seen.json) bytes" # ---- Run the scan ---------------------------------------------------- # Runs Claude Code against daily_vuln_scan_prompt.md. # That prompt file fully specifies: sources to poll, how to read # state/seen.json, the 25-hour window, the output files to write, # and how to rewrite state/seen.json at the end of the run. - name: Research for online mentions of new vulns id: scan uses: anthropics/claude-code-action@v1 env: SCAN_DATE: ${{ github.run_started_at }} with: claude_args: | --model claude-opus-4-7 --allowedTools "Read,Write,Edit,Bash,Grep,Glob,WebFetch" prompt: | Read the full task instructions from .github/workflows/daily_vuln_scan_prompt.md and execute them end-to-end. That file fully specifies: sources to poll, how to read and update state/seen.json, the 25-hour window, which rss_YYYY-MM-DD_*.md files to write, and the run guardrails. Use $SCAN_DATE (env var) as "now" for time-window decisions. claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - name: Upload Claude execution log if: ${{ always() && steps.scan.outputs.execution_file != '' }} uses: actions/upload-artifact@v4 with: name: claude-execution-log-${{ github.run_id }} path: ${{ steps.scan.outputs.execution_file }} retention-days: 30 if-no-files-found: warn # ---- Persist outputs ------------------------------------------------- - name: Prune state (keep only entries from the last 30 days) run: | python3 - <<'PY' import json, datetime, pathlib p = pathlib.Path("state/seen.json") data = json.loads(p.read_text()) cutoff = (datetime.datetime.utcnow() - datetime.timedelta(days=30)).isoformat() before = len(data.get("seen", {})) data["seen"] = { k: v for k, v in data.get("seen", {}).items() if v.get("seen_at", "9999") >= cutoff } after = len(data["seen"]) p.write_text(json.dumps(data, indent=2, sort_keys=True)) print(f"Pruned state: {before} -> {after} entries") PY - name: Upload new state artifact uses: actions/upload-artifact@v4 with: name: vuln-scan-state path: state/seen.json retention-days: 90 if-no-files-found: error - name: Upload daily report uses: actions/upload-artifact@v4 with: name: vuln-scan-report-${{ github.run_id }} path: rss_*.md retention-days: 90 if-no-files-found: warn