Skip to content
Merged
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
11 changes: 6 additions & 5 deletions .github/scripts/filter-unlinked-cve-issues.jq
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
def has_linked_github_pr:
def has_open_linked_github_pr:
any(
.attachments.nodes[]?.url?;
type == "string"
and test("^https://github\\.com/[^/]+/[^/]+/pull/[0-9]+(?:[/?#].*)?$")
.attachments.nodes[]?;
(.url? | type == "string"
and test("^https://github\\.com/[^/]+/[^/]+/pull/[0-9]+(?:[/?#].*)?$"))
and .githubPrState? == "open"
);

[
.[]
| select(any(.labels.nodes[]?; .name == "CVE"))
| select(has_linked_github_pr | not)
| select(has_open_linked_github_pr | not)
| {
id,
identifier,
Expand Down
52 changes: 52 additions & 0 deletions .github/scripts/find-unlinked-cve-issues.sh
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,56 @@ while true; do
fi
done

github_pr_states='[]'
while IFS= read -r pr_url; do
if [[ ! "$pr_url" =~ ^https://github\.com/([^/]+)/([^/]+)/pull/([0-9]+)([/?#].*)?$ ]]; then
echo "Could not parse linked GitHub pull request URL: $pr_url" >&2
exit 1
fi

owner="${BASH_REMATCH[1]}"
repository="${BASH_REMATCH[2]}"
pull_number="${BASH_REMATCH[3]}"
if ! pr_state=$(gh api "repos/$owner/$repository/pulls/$pull_number" --jq '.state'); then
echo "Could not fetch linked GitHub pull request: $pr_url" >&2
exit 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PR fetch failure aborts all discovery

Medium Severity

Any failed gh api lookup (or unparsable linked PR URL) exits the whole discovery script before filtering. One deleted, private companion, or temporarily unreachable attachment then blocks remediation for every CVE that night, including issues with no linked PRs. That conflicts with only suppressing issues that still have an open PR.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8b240a4. Configure here.

fi

if [[ "$pr_state" != "open" && "$pr_state" != "closed" ]]; then
echo "GitHub returned an unexpected state for $pr_url: $pr_state" >&2
exit 1
fi

github_pr_states=$(jq -cn \
--argjson states "$github_pr_states" \
--arg url "$pr_url" \
--arg state "$pr_state" \
'$states + [{url: $url, state: $state}]')
done < <(jq -r '
[
.[]
| select(any(.labels.nodes[]?; .name == "CVE"))
| .attachments.nodes[]?.url?
| strings
| select(test("^https://github\\.com/[^/]+/[^/]+/pull/[0-9]+(?:[/?#].*)?$"))
]
| unique[]
' <<<"$all_issues")

all_issues=$(jq -cn \
--argjson issues "$all_issues" \
--argjson states "$github_pr_states" '
($states | map({key: .url, value: .state}) | from_entries) as $states_by_url
| $issues
| map(
.attachments.nodes |= map(
if $states_by_url[.url] != null then
. + {githubPrState: $states_by_url[.url]}
else
.
end
)
)
')

jq -c -f "$FILTER" <<<"$all_issues"
81 changes: 74 additions & 7 deletions .github/scripts/test-cve-remediation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,22 @@ ISSUES='[
{
"id": "issue-2",
"identifier": "SOU-2",
"title": "[sourcebot-dev/example] CVE-2: linked in this repository",
"title": "[sourcebot-dev/example] CVE-2: open PR linked in this repository",
"url": "https://linear.app/sourcebot/issue/SOU-2/test",
"priority": 2,
"state": {"name": "In Progress", "type": "started"},
"labels": {"nodes": [{"name": "CVE"}]},
"attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/example/pull/42"}]}
"attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/example/pull/42", "githubPrState": "open"}]}
},
{
"id": "issue-3",
"identifier": "SOU-3",
"title": "[sourcebot-dev/example] CVE-3: linked in a companion repository",
"title": "[sourcebot-dev/example] CVE-3: closed PR linked in a companion repository",
"url": "https://linear.app/sourcebot/issue/SOU-3/test",
"priority": 1,
"state": {"name": "Todo", "type": "unstarted"},
"labels": {"nodes": [{"name": "CVE"}]},
"attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/companion/pull/9/files"}]}
"attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/companion/pull/9/files", "githubPrState": "closed"}]}
},
{
"id": "issue-4",
Expand All @@ -100,6 +100,15 @@ ISSUES='[
]'

EXPECTED='[
{
"id": "issue-3",
"identifier": "SOU-3",
"title": "[sourcebot-dev/example] CVE-3: closed PR linked in a companion repository",
"url": "https://linear.app/sourcebot/issue/SOU-3/test",
"priority": 1,
"status": "Todo",
"statusType": "unstarted"
},
{
"id": "issue-5",
"identifier": "SOU-5",
Expand All @@ -121,14 +130,15 @@ EXPECTED='[
]'

assert_json \
"keeps only CVEs without a linked GitHub pull request and sorts by priority" \
"keeps CVEs without an open linked GitHub pull request and sorts by priority" \
"$(jq -c -f "$FILTER" <<<"$ISSUES")" \
"$EXPECTED"

FAKE_CURL_DIR=$(mktemp -d)
FAKE_CURL_COUNT=$(mktemp)
FAKE_CURL_PAYLOAD_DIR=$(mktemp -d)
trap 'rm -rf "$FAKE_CURL_DIR" "$FAKE_CURL_PAYLOAD_DIR"; rm -f "$FAKE_CURL_COUNT"' EXIT
FAKE_GH_LOG=$(mktemp)
trap 'rm -rf "$FAKE_CURL_DIR" "$FAKE_CURL_PAYLOAD_DIR"; rm -f "$FAKE_CURL_COUNT" "$FAKE_GH_LOG"' EXIT
printf '0\n' > "$FAKE_CURL_COUNT"

cat > "$FAKE_CURL_DIR/curl" <<'EOF'
Expand Down Expand Up @@ -181,12 +191,22 @@ elif ((count == 2)); then
{
"id": "page-1-linked",
"identifier": "SOU-21",
"title": "[sourcebot-dev/example] CVE-21: linked",
"title": "[sourcebot-dev/example] CVE-21: open PR linked",
"url": "https://linear.app/sourcebot/issue/SOU-21/test",
"priority": 1,
"state": {"name": "Backlog", "type": "backlog"},
"labels": {"nodes": [{"name": "CVE"}]},
"attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/example/pull/21"}]}
},
{
"id": "page-1-closed-pr",
"identifier": "SOU-23",
"title": "[sourcebot-dev/example] CVE-23: closed PR linked",
"url": "https://linear.app/sourcebot/issue/SOU-23/test",
"priority": 2,
"state": {"name": "Backlog", "type": "backlog"},
"labels": {"nodes": [{"name": "CVE"}]},
"attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/companion/pull/23/files"}]}
}
],
"pageInfo": {"hasNextPage": true, "endCursor": "next-page"}
Expand Down Expand Up @@ -220,10 +240,38 @@ printf '200'
EOF
chmod +x "$FAKE_CURL_DIR/curl"

cat > "$FAKE_CURL_DIR/gh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

if [[ "$1" != "api" ]]; then
echo "Unexpected gh command: $*" >&2
exit 1
fi

endpoint="$2"
printf '%s\n' "$endpoint" >> "$FAKE_GH_LOG"
case "$endpoint" in
repos/sourcebot-dev/example/pulls/21)
printf 'open\n'
;;
repos/sourcebot-dev/companion/pulls/23)
printf 'closed\n'
;;
*)
echo "Unexpected GitHub API endpoint: $endpoint" >&2
exit 1
;;
esac
EOF
chmod +x "$FAKE_CURL_DIR/gh"

DISCOVERED=$(
PATH="$FAKE_CURL_DIR:$PATH" \
FAKE_CURL_COUNT="$FAKE_CURL_COUNT" \
FAKE_CURL_PAYLOAD_DIR="$FAKE_CURL_PAYLOAD_DIR" \
FAKE_GH_LOG="$FAKE_GH_LOG" \
GH_TOKEN="test-token" \
LINEAR_API_KEY="test-key" \
LINEAR_TEAM_ID="team-key" \
LINEAR_GRAPHQL_ATTEMPTS=1 \
Expand All @@ -240,6 +288,15 @@ EXPECTED_DISCOVERED='[
"status": "Todo",
"statusType": "unstarted"
},
{
"id": "page-1-closed-pr",
"identifier": "SOU-23",
"title": "[sourcebot-dev/example] CVE-23: closed PR linked",
"url": "https://linear.app/sourcebot/issue/SOU-23/test",
"priority": 2,
"status": "Backlog",
"statusType": "backlog"
},
{
"id": "page-1-unlinked",
"identifier": "SOU-20",
Expand All @@ -251,6 +308,10 @@ EXPECTED_DISCOVERED='[
}
]'
assert_json "paginates Linear results and filters before invoking Claude" "$DISCOVERED" "$EXPECTED_DISCOVERED"
assert_json \
"checks every unique linked GitHub pull request state" \
"$(jq -Rsc 'split("\n") | map(select(length > 0))' "$FAKE_GH_LOG")" \
'["repos/sourcebot-dev/companion/pulls/23","repos/sourcebot-dev/example/pulls/21"]'
assert_json \
"resolves the configured Linear team identifier" \
"$(jq -c '.variables.teamId' "$FAKE_CURL_PAYLOAD_DIR/1.json")" \
Expand All @@ -275,6 +336,12 @@ assert_workflow_contains \
assert_workflow_contains \
"only invokes Claude when discovery found work" \
"if: needs.discover.outputs.has_issues == 'true'"
assert_workflow_contains \
"grants discovery read access to pull request state" \
'pull-requests: read'
assert_workflow_contains \
"authenticates GitHub API requests with the workflow token" \
'GH_TOKEN: ${{ github.token }}'
assert_workflow_contains \
"passes only Linear UUIDs between jobs to avoid secret redaction" \
"issue_ids=\$(jq -c 'map(.id)'"
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/_cve-remediation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
type: string
default: ''
max_issues:
description: Maximum number of unlinked CVEs to pass to Claude in one run.
description: Maximum number of CVEs without an open linked PR to pass to Claude in one run.
required: false
type: number
default: 50
Expand All @@ -28,10 +28,11 @@ on:

jobs:
discover:
name: Find unlinked CVEs
name: Find CVEs without an open PR
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
has_issues: ${{ steps.discover.outputs.has_issues }}
issue_ids: ${{ steps.discover.outputs.issue_ids }}
Expand All @@ -49,9 +50,10 @@ jobs:
path: .cve-remediation-workflow
persist-credentials: false

- name: Find open CVEs without a linked PR
- name: Find open CVEs without an open linked PR
id: discover
env:
GH_TOKEN: ${{ github.token }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
LINEAR_TEAM_ID: ${{ secrets.LINEAR_TEAM_ID }}
REPOSITORY: ${{ github.repository }}
Expand Down Expand Up @@ -82,7 +84,7 @@ jobs:
{
echo "## CVE remediation discovery"
echo
echo "Found **$total_issue_count** open CVE(s) for \`$REPOSITORY\` without a linked GitHub PR."
echo "Found **$total_issue_count** open CVE(s) for \`$REPOSITORY\` without an open linked GitHub PR."
if ((total_issue_count > issue_count)); then
echo "This run will process the first **$issue_count** by Linear priority."
fi
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cve-remediation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
workflow_dispatch:
inputs:
max_issues:
description: Maximum number of unlinked CVEs to process.
description: Maximum number of CVEs without an open linked PR to process.
required: false
type: number
default: 50
Expand Down
Loading