Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 234 additions & 0 deletions .github/workflows/security-review-diff.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
name: Security Review (Diff)

on:
workflow_dispatch:
inputs:
pull_request_number:
description: "Optional pull request number to review"
required: false
default: ""
# pull_request:
# types:
# - opened
# - synchronize
# - reopened
# - ready_for_review
# - labeled

concurrency:
group: security-review-diff-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

jobs:
pr-security-review:
name: Pull Request Security Review
runs-on: ubuntu-24.04
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
permissions:
contents: read
pull-requests: write
issues: write

env:
SECURITY_BLOCK_LABEL: "security:blocked"
SECURITY_RISK_LABEL_PREFIX: "security:risk:"

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Install Task
uses: arduino/setup-task@v2
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}

- name: Collect updated pin targets
id: pins
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.sha }}"

if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.pull_request_number }}" ]; then
pr_json=$(gh pr view "${{ github.event.inputs.pull_request_number }}" --json baseRefOid,headRefOid)
base_sha=$(echo "$pr_json" | jq -r '.baseRefOid')
head_sha=$(echo "$pr_json" | jq -r '.headRefOid')
fi

if [ -z "$base_sha" ] || [ -z "$head_sha" ]; then
echo "Unable to resolve base/head SHA for review." >&2
exit 0
fi

task ci -- collect-updated-pins \
--base "$base_sha" \
--head "$head_sha" \
--workspace "${{ github.workspace }}" \
--output-json pins-context.json \
--summary-md pins-summary.md

if [ -s pins-context.json ]; then
echo "has_targets=true" >> "$GITHUB_OUTPUT"
echo "context=pins-context.json" >> "$GITHUB_OUTPUT"
echo "summary=pins-summary.md" >> "$GITHUB_OUTPUT"
else
echo "has_targets=false" >> "$GITHUB_OUTPUT"
fi

- name: Collect new local servers
id: newservers
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.sha }}"

if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.pull_request_number }}" ]; then
pr_json=$(gh pr view "${{ github.event.inputs.pull_request_number }}" --json baseRefOid,headRefOid)
base_sha=$(echo "$pr_json" | jq -r '.baseRefOid')
head_sha=$(echo "$pr_json" | jq -r '.headRefOid')
fi

if [ -z "$base_sha" ] || [ -z "$head_sha" ]; then
echo "Unable to resolve base/head SHA for review." >&2
exit 0
fi

task ci -- collect-new-servers \
--base "$base_sha" \
--head "$head_sha" \
--workspace "${{ github.workspace }}" \
--output-json new-servers-context.json \
--summary-md new-servers-summary.md

if [ -s new-servers-context.json ]; then
echo "has_targets=true" >> "$GITHUB_OUTPUT"
echo "context=new-servers-context.json" >> "$GITHUB_OUTPUT"
echo "summary=new-servers-summary.md" >> "$GITHUB_OUTPUT"
else
echo "has_targets=false" >> "$GITHUB_OUTPUT"
fi

- name: Ensure security labels exist
if: steps.pins.outputs.has_targets == 'true' || steps.newservers.outputs.has_targets == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh label create "${SECURITY_BLOCK_LABEL}" \
--color B60205 \
--description "Security automation detected blocking issues." \
|| echo "Label ${SECURITY_BLOCK_LABEL} already exists."

for risk in critical high medium low info; do
label="${SECURITY_RISK_LABEL_PREFIX}${risk}"
gh label create "$label" \
--color 0E8A16 \
--description "Security automation risk assessment: ${risk}." \
|| echo "Label $label already exists."
done
Comment on lines +121 to +137
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just do this manually, once.


- name: Remove stale security labels
if: steps.pins.outputs.has_targets == 'true' || steps.newservers.outputs.has_targets == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for label in "${SECURITY_BLOCK_LABEL}" \
"${SECURITY_RISK_LABEL_PREFIX}critical" \
"${SECURITY_RISK_LABEL_PREFIX}high" \
"${SECURITY_RISK_LABEL_PREFIX}medium" \
"${SECURITY_RISK_LABEL_PREFIX}low" \
"${SECURITY_RISK_LABEL_PREFIX}info"
do
gh pr edit "${{ github.event.pull_request.number }}" \
--repo "${{ github.repository }}" \
--remove-label "$label" || true
done

- name: Prepare review context
if: steps.pins.outputs.has_targets == 'true' || steps.newservers.outputs.has_targets == 'true'
run: |
mkdir -p /tmp/security-review/pins
mkdir -p /tmp/security-review/new

if [ "${{ steps.pins.outputs.has_targets }}" = "true" ]; then
task ci -- prepare-updated-pins \
--context-file "${{ steps.pins.outputs.context }}" \
--output-dir /tmp/security-review/pins
fi

if [ "${{ steps.newservers.outputs.has_targets }}" = "true" ]; then
task ci -- prepare-new-servers \
--context-file "${{ steps.newservers.outputs.context }}" \
--output-dir /tmp/security-review/new
fi

task ci -- compose-pr-summary \
--pins-summary "${{ steps.pins.outputs.summary }}" \
--new-summary "${{ steps.newservers.outputs.summary }}" \
--output summary.md

- name: Load security review prompt
if: steps.pins.outputs.has_targets == 'true' || steps.newservers.outputs.has_targets == 'true'
run: |
{
echo 'SECURITY_REVIEW_PROMPT<<EOF'
cat prompts/security-review-diff.txt
echo 'EOF'
} >> "$GITHUB_ENV"

- name: Run Claude security review
if: steps.pins.outputs.has_targets == 'true' || steps.newservers.outputs.has_targets == 'true'
id: claude
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
prompt: ${{ env.SECURITY_REVIEW_PROMPT }}
claude_args: |
--add-file ${{ github.workspace }}/summary.md
--add-file ${{ github.workspace }}/templates/security-review-diff.md
--add-dir /tmp/security-review/pins
--add-dir /tmp/security-review/new
--allowed-tools "Read,Write,Bash(git:*),Bash(gh:*),Bash(mkdir)"

- name: Post security review as PR comment
if: always()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ! -f /tmp/security-review.md ]; then
echo "No security review report produced."
exit 0
fi

{
cat /tmp/security-review.md
echo ""
echo "<!-- automated-security-review -->"
} > security-review-comment.md

comment_id=$(gh api \
repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \
--jq '.[] | select(.body | contains("<!-- automated-security-review -->")) | .id' \
|| true)

if [ -n "$comment_id" ]; then
gh api \
-X PATCH \
-H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/issues/comments/$comment_id \
-F body="@security-review-comment.md"
else
gh pr comment ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--body-file security-review-comment.md
Comment on lines +231 to +233

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle workflow_dispatch PR number when posting review comment

The workflow is only triggered via workflow_dispatch, yet the post-review step still references ${{ github.event.pull_request.number }} when calling gh pr comment. In a manually dispatched run this context is unset, so the command fails and the generated security report is never published to the intended pull request (labels also remain stale for the same reason). Consider using the input value (${{ github.event.inputs.pull_request_number }}) when the event is workflow_dispatch so manual executions can target the requested PR.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, I've temporarily disabled the previous step and left it on a manual trigger for now, but it will eventually be triggered on PR events.

fi
126 changes: 126 additions & 0 deletions .github/workflows/security-review-full.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Security Review (Full)

on:
workflow_dispatch:
inputs:
servers:
description: "Comma-separated list of local server names to audit (leave blank for all)."
required: false
default: ""

concurrency:
group: security-review-full-${{ github.run_id }}
cancel-in-progress: false

jobs:
full-audit:
name: Execute Full Audit
runs-on: ubuntu-24.04
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Install Task
uses: arduino/setup-task@v2
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}

- name: Collect audit targets
run: |
task ci -- collect-full-audit \
--workspace "${{ github.workspace }}" \
--servers "${{ github.event.inputs.servers }}" \
--output-json audit-targets.json

if jq -e '. | length > 0' audit-targets.json >/dev/null; then
echo "AUDIT_HAS_TARGETS=true" >> "$GITHUB_ENV"
else
echo "No audit targets identified; exiting." >&2
echo "AUDIT_HAS_TARGETS=false" >> "$GITHUB_ENV"
fi

- name: Prepare audit contexts
if: env.AUDIT_HAS_TARGETS == 'true'
run: |
mkdir -p /tmp/full-audit
rm -f full-audit-summary.md
echo "# Full Audit Targets" >> full-audit-summary.md
echo "" >> full-audit-summary.md

idx=0
jq -c '.[]' audit-targets.json | while read -r target; do
server=$(echo "$target" | jq -r '.server')
echo "$target" > target.json
task ci -- prepare-full-audit \
--target-file target.json \
--output-dir /tmp/full-audit

repo=$(echo "$target" | jq -r '.project')
commit=$(echo "$target" | jq -r '.commit')
directory=$(echo "$target" | jq -r '.directory')
if [ -z "$directory" ] || [ "$directory" = "null" ]; then
directory="(repository root)"
fi

{
echo "## ${server}"
echo "- Repository: ${repo}"
echo "- Commit: \`${commit}\`"
echo "- Directory: ${directory}"
echo ""
} >> full-audit-summary.md
idx=$((idx+1))
done

echo "Prepared ${idx} audit targets."

- name: Load security review prompt
if: env.AUDIT_HAS_TARGETS == 'true'
run: |
{
echo 'SECURITY_REVIEW_PROMPT<<EOF'
cat prompts/security-review-full.txt
echo 'EOF'
} >> "$GITHUB_ENV"

- name: Run Claude security review
if: env.AUDIT_HAS_TARGETS == 'true'
id: claude
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: ${{ env.SECURITY_REVIEW_PROMPT }}
claude_args: |
--add-file ${{ github.workspace }}/full-audit-summary.md
--add-file ${{ github.workspace }}/templates/security-review-full.md
--add-dir /tmp/full-audit
--allowed-tools "Read,Write,Bash(git:*),Bash(mkdir)"

- name: Store security report
if: env.AUDIT_HAS_TARGETS == 'true'
run: |
if [ -f /tmp/security-review.md ]; then
mkdir -p reports
cp /tmp/security-review.md reports/full-audit-report.md
else
echo "warning: no security review produced" >&2
fi

- name: Upload security reports
if: env.AUDIT_HAS_TARGETS == 'true'
uses: actions/upload-artifact@v4
with:
name: security-reports
path: reports/
if-no-files-found: warn
Loading