mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
9595dad58b
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 8.1.0 to 8.1.1. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/c0f553fe549906ede9cf27b5156039d195d2ece0...5f6978faf089d4d20b00c7766989d076bb2fc7f1) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-version: 8.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
346 lines
14 KiB
YAML
346 lines
14 KiB
YAML
name: Process Community Advisory
|
|
|
|
on:
|
|
issues:
|
|
types: [labeled]
|
|
|
|
permissions: read-all
|
|
|
|
concurrency:
|
|
group: community-advisory
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
FEED_PATH: advisories/feed.json
|
|
FEED_SIG_PATH: advisories/feed.json.sig
|
|
SKILL_FEED_PATH: skills/clawsec-feed/advisories/feed.json
|
|
SKILL_FEED_SIG_PATH: skills/clawsec-feed/advisories/feed.json.sig
|
|
|
|
jobs:
|
|
process-advisory:
|
|
if: github.event.label.name == 'advisory-approved'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Parse issue and create advisory
|
|
id: parse
|
|
env:
|
|
ISSUE_BODY: ${{ github.event.issue.body }}
|
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
ISSUE_URL: ${{ github.event.issue.html_url }}
|
|
ISSUE_TITLE: ${{ github.event.issue.title }}
|
|
ISSUE_CREATED_AT: ${{ github.event.issue.created_at }}
|
|
run: |
|
|
# Generate advisory ID: CLAW-YYYY-{issue_number padded to 4 digits}
|
|
# Use issue creation year, not current year, to ensure consistency
|
|
YEAR=$(echo "$ISSUE_CREATED_AT" | cut -c1-4)
|
|
PADDED_NUM=$(printf "%04d" "$ISSUE_NUMBER")
|
|
ADVISORY_ID="CLAW-${YEAR}-${PADDED_NUM}"
|
|
echo "advisory_id=$ADVISORY_ID" >> $GITHUB_OUTPUT
|
|
echo "Generated advisory ID: $ADVISORY_ID"
|
|
|
|
# Check if advisory already exists by issue URL (dedupe by issue, not by ID)
|
|
# This prevents duplicates when the same issue is labeled in different years
|
|
if jq -e --arg url "$ISSUE_URL" '.advisories[] | select(.github_issue_url == $url)' "$FEED_PATH" > /dev/null 2>&1; then
|
|
echo "Advisory for issue $ISSUE_URL already exists in feed"
|
|
echo "already_exists=true" >> $GITHUB_OUTPUT
|
|
exit 0
|
|
fi
|
|
echo "already_exists=false" >> $GITHUB_OUTPUT
|
|
|
|
# Parse opener type (human vs agent)
|
|
if echo "$ISSUE_BODY" | grep -q '\[x\] Agent'; then
|
|
OPENER_TYPE="agent"
|
|
else
|
|
OPENER_TYPE="human"
|
|
fi
|
|
echo "Opener type: $OPENER_TYPE"
|
|
|
|
# Parse report type
|
|
if echo "$ISSUE_BODY" | grep -q '\[x\] Malicious Prompt'; then
|
|
REPORT_TYPE="prompt_injection"
|
|
elif echo "$ISSUE_BODY" | grep -q '\[x\] Vulnerable Skill'; then
|
|
REPORT_TYPE="vulnerable_skill"
|
|
elif echo "$ISSUE_BODY" | grep -q '\[x\] Tampering Attempt'; then
|
|
REPORT_TYPE="tampering_attempt"
|
|
else
|
|
REPORT_TYPE="unknown"
|
|
fi
|
|
echo "Report type: $REPORT_TYPE"
|
|
|
|
# Parse severity
|
|
if echo "$ISSUE_BODY" | grep -q '\[x\] Critical'; then
|
|
SEVERITY="critical"
|
|
elif echo "$ISSUE_BODY" | grep -q '\[x\] High'; then
|
|
SEVERITY="high"
|
|
elif echo "$ISSUE_BODY" | grep -q '\[x\] Medium'; then
|
|
SEVERITY="medium"
|
|
elif echo "$ISSUE_BODY" | grep -q '\[x\] Low'; then
|
|
SEVERITY="low"
|
|
else
|
|
SEVERITY="medium"
|
|
fi
|
|
echo "Severity: $SEVERITY"
|
|
|
|
# Parse title (between ## Title and ## Description)
|
|
TITLE=$(echo "$ISSUE_BODY" | sed -n '/^## Title/,/^## Description/p' | grep -v '^## ' | grep -v '^<!--' | grep -v '^\s*$' | head -1 | xargs)
|
|
if [ -z "$TITLE" ]; then
|
|
TITLE="$ISSUE_TITLE"
|
|
fi
|
|
echo "Title: $TITLE"
|
|
|
|
# Parse description (between ## Description and ---)
|
|
DESCRIPTION=$(echo "$ISSUE_BODY" | sed -n '/^## Description/,/^---/p' | grep -v '^## Description' | grep -v '^---' | grep -v '^<!--' | sed '/^\s*$/d' | tr '\n' ' ' | xargs)
|
|
if [ -z "$DESCRIPTION" ]; then
|
|
DESCRIPTION="See issue for details."
|
|
fi
|
|
echo "Description: ${DESCRIPTION:0:100}..."
|
|
|
|
# Parse skill name and version
|
|
SKILL_NAME=$(echo "$ISSUE_BODY" | sed -n '/^### Skill Name/,/^### /p' | grep -v '^### ' | grep -v '^<!--' | grep -v '^\s*$' | head -1 | xargs)
|
|
SKILL_VERSION=$(echo "$ISSUE_BODY" | sed -n '/^### Skill Version/,/^### /p' | grep -v '^### ' | grep -v '^<!--' | grep -v '^\s*$' | head -1 | xargs)
|
|
|
|
# Build affected array
|
|
AFFECTED="[]"
|
|
if [ -n "$SKILL_NAME" ] && [ -n "$SKILL_VERSION" ]; then
|
|
AFFECTED=$(jq -n --arg name "$SKILL_NAME" --arg ver "$SKILL_VERSION" '[$name + "@" + $ver]')
|
|
elif [ -n "$SKILL_NAME" ]; then
|
|
AFFECTED=$(jq -n --arg name "$SKILL_NAME" '[$name]')
|
|
fi
|
|
echo "Affected: $AFFECTED"
|
|
|
|
# Build platforms array
|
|
OPENCLAW_SELECTED="false"
|
|
if echo "$ISSUE_BODY" | grep -qi '^[[:space:]]*-[[:space:]]*\[[xX]\][[:space:]]*OpenClaw'; then
|
|
OPENCLAW_SELECTED="true"
|
|
fi
|
|
|
|
OTHER_PLATFORM_RAW=$(echo "$ISSUE_BODY" | sed -n 's/^[[:space:]]*-[[:space:]]*\[[xX]\][[:space:]]*Other:[[:space:]]*\(.*\)$/\1/p' | head -1 | xargs)
|
|
OTHER_PLATFORM=""
|
|
if [ -n "$OTHER_PLATFORM_RAW" ]; then
|
|
OTHER_PLATFORM=$(echo "$OTHER_PLATFORM_RAW" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//')
|
|
if echo "$OTHER_PLATFORM" | grep -q 'nanoclaw'; then
|
|
OTHER_PLATFORM="nanoclaw"
|
|
fi
|
|
fi
|
|
|
|
PLATFORMS=$(jq -n --arg open "$OPENCLAW_SELECTED" --arg other "$OTHER_PLATFORM" '
|
|
[
|
|
(if $open == "true" then "openclaw" else empty end),
|
|
(if ($other | length) > 0 then $other else empty end)
|
|
] | unique
|
|
')
|
|
if [ "$PLATFORMS" = "[]" ]; then
|
|
PLATFORMS='["openclaw","nanoclaw"]'
|
|
fi
|
|
echo "Platforms: $PLATFORMS"
|
|
|
|
# Parse recommended action
|
|
ACTION=$(echo "$ISSUE_BODY" | sed -n '/^## Recommended Action/,/^---/p' | grep -v '^## Recommended Action' | grep -v '^---' | grep -v '^<!--' | sed '/^\s*$/d' | tr '\n' ' ' | xargs)
|
|
if [ -z "$ACTION" ]; then
|
|
ACTION="Review the advisory details and take appropriate action."
|
|
fi
|
|
echo "Action: ${ACTION:0:100}..."
|
|
|
|
# Parse reporter name
|
|
REPORTER_NAME=$(echo "$ISSUE_BODY" | grep -A1 '\*\*Agent/User Name:\*\*' | tail -1 | sed 's/\*\*Contact:\*\*.*//' | xargs)
|
|
if [ -z "$REPORTER_NAME" ]; then
|
|
REPORTER_NAME=$(echo "$ISSUE_BODY" | sed -n 's/.*\*\*Agent\/User Name:\*\*\s*\(.*\)/\1/p' | xargs)
|
|
fi
|
|
echo "Reporter: $REPORTER_NAME"
|
|
|
|
# Get current timestamp
|
|
PUBLISHED=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
|
|
# Create advisory JSON
|
|
jq -n \
|
|
--arg id "$ADVISORY_ID" \
|
|
--arg severity "$SEVERITY" \
|
|
--arg type "$REPORT_TYPE" \
|
|
--arg title "$TITLE" \
|
|
--arg description "$DESCRIPTION" \
|
|
--argjson affected "$AFFECTED" \
|
|
--argjson platforms "$PLATFORMS" \
|
|
--arg action "$ACTION" \
|
|
--arg published "$PUBLISHED" \
|
|
--arg source "Community Report" \
|
|
--arg issue_url "$ISSUE_URL" \
|
|
--arg reporter_name "$REPORTER_NAME" \
|
|
--arg opener_type "$OPENER_TYPE" \
|
|
'{
|
|
id: $id,
|
|
severity: $severity,
|
|
type: $type,
|
|
title: $title,
|
|
description: $description,
|
|
affected: $affected,
|
|
platforms: $platforms,
|
|
action: $action,
|
|
published: $published,
|
|
references: [],
|
|
source: $source,
|
|
github_issue_url: $issue_url,
|
|
reporter: {
|
|
agent_name: $reporter_name,
|
|
opener_type: $opener_type
|
|
}
|
|
}' > tmp_advisory.json
|
|
|
|
echo "Created advisory JSON:"
|
|
cat tmp_advisory.json
|
|
|
|
- name: Set up Python for exploitability analysis
|
|
if: steps.parse.outputs.already_exists != 'true'
|
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: '3.10'
|
|
|
|
- name: Analyze exploitability for community advisory
|
|
if: steps.parse.outputs.already_exists != 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
echo "=== Analyzing exploitability for community advisory ==="
|
|
|
|
scripts/ci/enrich_exploitability.sh \
|
|
--mode single \
|
|
--input tmp_advisory.json \
|
|
--output tmp_advisory.json
|
|
|
|
echo "=== Exploitability analysis complete ==="
|
|
echo "Exploitability score: $(jq -r '.exploitability_score // "unknown"' tmp_advisory.json)"
|
|
|
|
- name: Update feed
|
|
if: steps.parse.outputs.already_exists != 'true'
|
|
run: |
|
|
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
|
|
# Add new advisory to feed
|
|
jq --argjson new "$(cat tmp_advisory.json)" --arg now "$NOW" '
|
|
.updated = $now |
|
|
.advisories = ([$new] + .advisories)
|
|
' "$FEED_PATH" > tmp_feed.json
|
|
|
|
# Validate JSON
|
|
if jq empty tmp_feed.json 2>/dev/null; then
|
|
echo "Feed JSON is valid"
|
|
mv tmp_feed.json "$FEED_PATH"
|
|
|
|
# Sync to skill feed
|
|
mkdir -p "$(dirname "$SKILL_FEED_PATH")"
|
|
cp "$FEED_PATH" "$SKILL_FEED_PATH"
|
|
|
|
echo "Updated feeds:"
|
|
echo " - $FEED_PATH"
|
|
echo " - $SKILL_FEED_PATH"
|
|
|
|
TOTAL=$(jq '.advisories | length' "$FEED_PATH")
|
|
echo "Total advisories: $TOTAL"
|
|
else
|
|
echo "Error: Generated invalid JSON"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Sign advisory feed and verify
|
|
if: steps.parse.outputs.already_exists != 'true'
|
|
uses: ./.github/actions/sign-and-verify
|
|
with:
|
|
private_key: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY }}
|
|
private_key_passphrase: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY_PASSPHRASE }}
|
|
input_file: ${{ env.FEED_PATH }}
|
|
signature_file: ${{ env.FEED_SIG_PATH }}
|
|
verify_files: |
|
|
${{ env.FEED_PATH }}
|
|
${{ env.SKILL_FEED_PATH }}
|
|
|
|
- name: Sync advisory signature to skill feed
|
|
if: steps.parse.outputs.already_exists != 'true'
|
|
run: cp "$FEED_SIG_PATH" "$SKILL_FEED_SIG_PATH"
|
|
|
|
- name: Require automation token for write operations
|
|
env:
|
|
AUTOMATION_TOKEN: ${{ secrets.POLL_NVD_CVES_PAT }}
|
|
run: |
|
|
if [ -z "$AUTOMATION_TOKEN" ]; then
|
|
echo "::error::Set POLL_NVD_CVES_PAT with repo write permissions."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Create Pull Request
|
|
if: steps.parse.outputs.already_exists != 'true'
|
|
id: create-pr
|
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
|
with:
|
|
token: ${{ secrets.POLL_NVD_CVES_PAT }}
|
|
branch: automated/community-advisory-${{ github.event.issue.number }}
|
|
delete-branch: true
|
|
title: "chore: add community advisory ${{ steps.parse.outputs.advisory_id }}"
|
|
body: |
|
|
## Summary
|
|
Add community advisory `${{ steps.parse.outputs.advisory_id }}` from issue #${{ github.event.issue.number }}.
|
|
|
|
- Issue: ${{ github.event.issue.html_url }}
|
|
- Reporter: @${{ github.event.issue.user.login }}
|
|
- Trigger: `advisory-approved` label
|
|
|
|
---
|
|
*This PR was generated by the community advisory workflow.*
|
|
commit-message: |
|
|
chore: add community advisory ${{ steps.parse.outputs.advisory_id }}
|
|
|
|
Added from issue #${{ github.event.issue.number }}
|
|
Issue: ${{ github.event.issue.html_url }}
|
|
add-paths: |
|
|
${{ env.FEED_PATH }}
|
|
${{ env.FEED_SIG_PATH }}
|
|
${{ env.SKILL_FEED_PATH }}
|
|
${{ env.SKILL_FEED_SIG_PATH }}
|
|
|
|
- name: Comment on issue
|
|
if: steps.parse.outputs.already_exists != 'true'
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
github-token: ${{ secrets.POLL_NVD_CVES_PAT }}
|
|
script: |
|
|
const advisoryId = '${{ steps.parse.outputs.advisory_id }}';
|
|
const pullRequestUrl = '${{ steps.create-pr.outputs.pull-request-url }}';
|
|
const operation = '${{ steps.create-pr.outputs.pull-request-operation }}';
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.issue.number,
|
|
body: `## Advisory Pull Request Opened
|
|
|
|
This security report has been prepared for publication in the ClawSec advisory feed.
|
|
|
|
**Advisory ID:** \`${advisoryId}\`
|
|
**Pull Request:** ${pullRequestUrl || 'No PR generated (no file changes detected)'}
|
|
**PR Operation:** \`${operation || 'none'}\`
|
|
|
|
The advisory will be published after the pull request is merged.
|
|
|
|
Thank you for your contribution to community security!`
|
|
});
|
|
|
|
- name: Comment if already exists
|
|
if: steps.parse.outputs.already_exists == 'true'
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
github-token: ${{ secrets.POLL_NVD_CVES_PAT }}
|
|
script: |
|
|
const advisoryId = '${{ steps.parse.outputs.advisory_id }}';
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.issue.number,
|
|
body: `## Advisory Already Exists
|
|
|
|
An advisory with ID \`${advisoryId}\` already exists in the feed. No changes were made.
|
|
|
|
If this is a different issue, please open a new issue.`
|
|
});
|