name: Claude Full Codebase Security Scan on: workflow_dispatch: inputs: prompt_path: description: "Path to the repo prompt file Claude should read" required: true default: "resources/specs/claude-security-scan.md" target_ref: description: "Branch, tag, or SHA to scan" required: false default: "" #schedule: # Weekly scan: Sunday 02:00 UTC # - cron: "0 2 * * 0" permissions: contents: read id-token: write concurrency: group: claude-full-codebase-security-scan-${{ github.ref }} cancel-in-progress: false jobs: scan: name: Full codebase security scan runs-on: ubuntu-latest timeout-minutes: 60 env: DEFAULT_PROMPT_PATH: "resources/specs/claude-security-scan.md" steps: - name: Resolve scan inputs id: vars shell: bash run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then PROMPT_PATH="${{ inputs.prompt_path }}" TARGET_REF="${{ inputs.target_ref }}" else PROMPT_PATH="${DEFAULT_PROMPT_PATH}" TARGET_REF="" fi if [ -z "$TARGET_REF" ]; then TARGET_REF="${{ github.ref }}" fi echo "prompt_path=$PROMPT_PATH" >> "$GITHUB_OUTPUT" echo "target_ref=$TARGET_REF" >> "$GITHUB_OUTPUT" - name: Checkout repository uses: actions/checkout@v6 with: ref: ${{ steps.vars.outputs.target_ref }} fetch-depth: 0 - name: Verify prompt file exists shell: bash run: | test -f "${{ steps.vars.outputs.prompt_path }}" || { echo "Prompt file not found: ${{ steps.vars.outputs.prompt_path }}" exit 1 } - name: Run Claude full codebase security scan id: claude_scan uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.CLAUDE_API_KEY }} prompt: | REPO: ${{ github.repository }} REF: ${{ steps.vars.outputs.target_ref }} PROMPT_FILE: ${{ steps.vars.outputs.prompt_path }} You are running a full-codebase security scan in GitHub Actions. First, read the prompt file at: ${{ steps.vars.outputs.prompt_path }} Treat that file as the authoritative security-review instruction set. Scan the checked-out repository as a whole codebase. Do not modify files. Do not create commits. Do not open pull requests. Do not post GitHub comments. Do not create GitHub issues. claude_args: | --max-turns 20 --allowedTools "Read,Glob,Grep,Bash(git:*),Bash(find:*),Bash(grep:*),Bash(sed:*),Bash(cat:*)" --model claude-opus-4-8 - name: Prepare private security scan outputs if: always() && steps.claude_scan.outputs.execution_file != '' id: prepare_outputs shell: bash run: | set -u mkdir -p private-scan-results TODAY="$(date -u +%Y%m%d)" EXECUTION_OUTPUT_FILE="claude-execution-output_${TODAY}.json" FINDINGS_FILE="security-findings_${TODAY}.json" HAS_EXECUTION_OUTPUT="false" HAS_JSON_FINDINGS="false" echo "date_suffix=${TODAY}" >> "$GITHUB_OUTPUT" echo "execution_output_file=${EXECUTION_OUTPUT_FILE}" >> "$GITHUB_OUTPUT" echo "findings_file=${FINDINGS_FILE}" >> "$GITHUB_OUTPUT" # First check whether the raw Claude execution output exists. if [ -f "${{ steps.claude_scan.outputs.execution_file }}" ]; then jq ' map(select(.type == "result")) ' "${{ steps.claude_scan.outputs.execution_file }}" > "private-scan-results/${EXECUTION_OUTPUT_FILE}" HAS_EXECUTION_OUTPUT="true" if jq -r ' .[] | select(.type == "result") | .result | sub("^[^{]*"; "") ' "${{ steps.claude_scan.outputs.execution_file }}" \ | jq . > "private-scan-results/${FINDINGS_FILE}" then if jq -e '.findings and .analysis_summary' "private-scan-results/${FINDINGS_FILE}" > /dev/null; then HAS_JSON_FINDINGS="true" else rm -f "private-scan-results/${FINDINGS_FILE}" fi else rm -f "private-scan-results/${FINDINGS_FILE}" fi fi echo "has_execution_output=${HAS_EXECUTION_OUTPUT}" >> "$GITHUB_OUTPUT" echo "has_json_findings=${HAS_JSON_FINDINGS}" >> "$GITHUB_OUTPUT" - name: Push scan outputs to private security repo if: always() && steps.prepare_outputs.outputs.has_execution_output == 'true' env: SECURITY_RESULTS_TOKEN: ${{ secrets.SECURITY_RESULTS_TOKEN }} shell: bash run: | set -euo pipefail RESULTS_REPO="cesanta/security" WORKDIR="$(mktemp -d)" git clone "https://x-access-token:${SECURITY_RESULTS_TOKEN}@github.com/${RESULTS_REPO}.git" "$WORKDIR" mkdir -p "$WORKDIR/files" cp "private-scan-results/${{ steps.prepare_outputs.outputs.execution_output_file }}" "$WORKDIR/files/" if [ "${{ steps.prepare_outputs.outputs.has_json_findings }}" = "true" ]; then cp "private-scan-results/${{ steps.prepare_outputs.outputs.findings_file }}" "$WORKDIR/files/" fi cd "$WORKDIR" git config user.name "security-scan-bot" git config user.email "security-scan-bot@users.noreply.github.com" git add files/ if git diff --cached --quiet; then echo "No scan outputs to commit." exit 0 fi git commit -m "Add security scan results ${{ steps.prepare_outputs.outputs.date_suffix }}" git push