Skip to content

Security Agent

Security Agent #5

name: Security Agent
# Triggers Cursor's Security Cloud Agent via API in two modes:
# 1. PR mode — Dependabot/dep-labeled PR → workflow waits + posts report
# 2. Cron mode — weekly Mondays 06:00 UTC, sandbox creates fix PRs/issues
# (Cursor sandbox can create new PRs/issues but cannot comment on existing
# PRs, so PR-mode requires the workflow to post the report itself.)
on:
pull_request:
types: [opened, synchronize, labeled]
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
jobs:
# Gate the agent on CURSOR_API_KEY being present. Secrets can't be used in
# job-level `if:`, so we surface presence as an output here and skip cleanly
# (neutral, not failed) on repos that haven't set the key.
guard:
name: Check Cursor API key
runs-on: ubuntu-latest
outputs:
has_key: ${{ steps.check.outputs.has_key }}
steps:
- id: check
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
run: |
if [ -n "$CURSOR_API_KEY" ]; then
echo "has_key=true" >> "$GITHUB_OUTPUT"
else
echo "has_key=false" >> "$GITHUB_OUTPUT"
echo "::notice title=Cursor agent skipped::CURSOR_API_KEY is not set on this repo — skipping. Set it with: gh secret set CURSOR_API_KEY --repo <owner>/<repo>"
fi
pr-mode:
name: Run Security Agent (PR mode)
needs: guard
if: >
needs.guard.outputs.has_key == 'true' &&
github.event_name == 'pull_request' && (
github.event.pull_request.user.login == 'dependabot[bot]' ||
contains(github.event.pull_request.labels.*.name, 'dependencies')
)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- name: Trigger Cursor agent
id: trigger
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
REPO: ${{ github.repository }}
PR_URL: ${{ github.event.pull_request.html_url }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ', ') }}
PR_HEAD: ${{ github.event.pull_request.head.ref }}
run: |
set -euo pipefail
PROMPT=$(jq -Rs \
--arg repo "$REPO" \
--arg mode "dependabot-pr" \
--arg url "$PR_URL" \
--arg author "$PR_AUTHOR" \
--arg labels "$PR_LABELS" \
'
gsub("\\{\\{repo\\}\\}"; $repo)
| gsub("\\{\\{mode\\}\\}"; $mode)
| gsub("\\{\\{pr.url\\}\\}"; $url)
| gsub("\\{\\{pr.author\\}\\}"; $author)
| gsub("\\{\\{pr.labels\\}\\}"; $labels)
' .cursor/agent-prompts/security.md)
PAYLOAD=$(jq -n \
--arg prompt "$PROMPT" \
--arg repo "$REPO" \
--arg ref "$PR_HEAD" \
'{
prompt: { text: $prompt },
source: { repository: ("github.com/" + $repo), ref: $ref },
target: { autoCreatePr: false, openAsCursorGithubApp: true },
model: "composer-2.5"
}')
response=$(curl -sS -X POST "https://api.cursor.com/v0/agents" \
-H "Authorization: Bearer $CURSOR_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
echo "$response" | jq .
AGENT_ID=$(echo "$response" | jq -er '.id')
echo "agent_id=$AGENT_ID" >> "$GITHUB_OUTPUT"
- name: Wait for agent to finish
id: wait
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
AGENT_ID: ${{ steps.trigger.outputs.agent_id }}
run: |
set -euo pipefail
for i in $(seq 1 30); do
agent_status=$(curl -sS "https://api.cursor.com/v0/agents/$AGENT_ID" \
-H "Authorization: Bearer $CURSOR_API_KEY" | jq -r '.status')
echo "[$i/30] status: $agent_status"
if [[ "$agent_status" != "RUNNING" && "$agent_status" != "CREATING" ]]; then
echo "final_status=$agent_status" >> "$GITHUB_OUTPUT"
exit 0
fi
sleep 30
done
echo "final_status=TIMEOUT" >> "$GITHUB_OUTPUT"
- name: Post agent report + label
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
AGENT_ID: ${{ steps.trigger.outputs.agent_id }}
AGENT_STATUS: ${{ steps.wait.outputs.final_status }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
REPORT=$(curl -sS "https://api.cursor.com/v0/agents/$AGENT_ID/conversation" \
-H "Authorization: Bearer $CURSOR_API_KEY" \
| jq -r '[.messages[] | select(.type == "assistant_message") | .text] | last')
{
echo "## Security review (Cursor Security agent — IaC)"
echo ""
echo "**Agent run:** [\`$AGENT_ID\`](https://cursor.com/agents/$AGENT_ID)"
echo "**Status:** \`$AGENT_STATUS\`"
echo ""
echo "$REPORT"
} > /tmp/sec-comment.md
gh pr comment "$PR" --repo "$REPO" --body-file /tmp/sec-comment.md
if echo "$REPORT" | grep -qiE "safe to merge"; then
gh pr edit "$PR" --repo "$REPO" --add-label auto-merge-safe || true
elif echo "$REPORT" | grep -qiE "needs.human|risky|breaking"; then
gh pr edit "$PR" --repo "$REPO" --add-label needs-human || true
fi
cron-mode:
name: Run Security Agent (weekly cron)
needs: guard
if: >
needs.guard.outputs.has_key == 'true' &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Trigger Cursor agent
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
PROMPT=$(jq -Rs \
--arg repo "$REPO" \
--arg mode "weekly-cron" \
--arg empty "" \
'
gsub("\\{\\{repo\\}\\}"; $repo)
| gsub("\\{\\{mode\\}\\}"; $mode)
| gsub("\\{\\{pr.url\\}\\}"; $empty)
| gsub("\\{\\{pr.author\\}\\}"; $empty)
| gsub("\\{\\{pr.labels\\}\\}"; $empty)
' .cursor/agent-prompts/security.md)
PAYLOAD=$(jq -n \
--arg prompt "$PROMPT" \
--arg repo "$REPO" \
'{
prompt: { text: $prompt },
source: { repository: ("github.com/" + $repo), ref: "main" },
target: { autoCreatePr: true, openAsCursorGithubApp: true },
model: "composer-2.5"
}')
response=$(curl -sS -X POST "https://api.cursor.com/v0/agents" \
-H "Authorization: Bearer $CURSOR_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
echo "$response" | jq .
echo "$response" | jq -e '.id' >/dev/null