Skip to content

Commit 5c9e192

Browse files
msukkariclaude
andcommitted
fix: reopen closed Linear issues on recurring vulnerability findings
Instead of silently skipping completed/cancelled Linear issues or creating duplicates, the vulnerability triage workflow now reopens them by moving the issue back to Triage state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bae8381 commit 5c9e192

1 file changed

Lines changed: 48 additions & 9 deletions

File tree

.github/workflows/trivy-vulnerability-triage.yml

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ jobs:
394394
claude_args: |
395395
--allowedTools Bash,Read,Write,Edit,Glob,Grep,WebSearch,WebFetch
396396
--model claude-sonnet-4-6
397-
--json-schema '{"type":"object","properties":{"cves":{"type":"array","items":{"type":"object","properties":{"cveId":{"type":"string","description":"CVE ID, GHSA ID, or codeql:<rule-id>"},"severity":{"type":"string","enum":["CRITICAL","HIGH","MEDIUM","LOW"]},"source":{"type":"string","enum":["trivy","dependabot","codeql","trivy+dependabot"],"description":"Which scanner(s) reported this finding"},"title":{"type":"string","description":"Short summary for the Linear issue title"},"description":{"type":"string","description":"Markdown analysis: affected packages, direct vs transitive, remediation steps, and references"},"affectedPackage":{"type":"string"},"linearIssueExists":{"type":"boolean"}},"required":["cveId","severity","source","title","description","affectedPackage","linearIssueExists"]}}},"required":["cves"]}'
397+
--json-schema '{"type":"object","properties":{"cves":{"type":"array","items":{"type":"object","properties":{"cveId":{"type":"string","description":"CVE ID, GHSA ID, or codeql:<rule-id>"},"severity":{"type":"string","enum":["CRITICAL","HIGH","MEDIUM","LOW"]},"source":{"type":"string","enum":["trivy","dependabot","codeql","trivy+dependabot"],"description":"Which scanner(s) reported this finding"},"title":{"type":"string","description":"Short summary for the Linear issue title"},"description":{"type":"string","description":"Markdown analysis: affected packages, direct vs transitive, remediation steps, and references"},"affectedPackage":{"type":"string"},"linearIssueExists":{"type":"boolean"},"linearIssueId":{"type":"string","description":"The Linear issue UUID if a matching issue was found, empty string otherwise"},"linearIssueClosed":{"type":"boolean","description":"True if the matching Linear issue is in a completed or canceled state"}},"required":["cveId","severity","source","title","description","affectedPackage","linearIssueExists","linearIssueId","linearIssueClosed"]}}},"required":["cves"]}'
398398
prompt: |
399399
You are a security engineer triaging vulnerabilities and security findings for the Sourcebot Docker image.
400400
You have three data sources to analyze. Each is a JSON array where every entry has a pre-computed
@@ -444,17 +444,19 @@ jobs:
444444
445445
7. **Check Linear for existing issues** for each finding:
446446
- For each `cveId`, run a GraphQL query against the Linear API to search for issues
447-
whose title contains that ID.
448-
- **Important**: Exclude cancelled issues so that previously cancelled/rejected findings
449-
can be re-created. Use a state type filter to only match active issues.
447+
whose title contains that ID. Search ALL issues regardless of state (open, completed, cancelled).
450448
- Use the following curl command pattern:
451449
```
452450
curl -s -X POST https://api.linear.app/graphql \
453451
-H "Content-Type: application/json" \
454452
-H "Authorization: $LINEAR_API_KEY" \
455-
-d '{"query": "query { issues(filter: { team: { id: { eq: \"'$LINEAR_TEAM_ID'\" } }, title: { contains: \"<ID>\" }, state: { type: { nin: [\"canceled\"] } } }) { nodes { id title } } }"}'
453+
-d '{"query": "query { issues(filter: { team: { id: { eq: \"'$LINEAR_TEAM_ID'\" } }, title: { contains: \"<ID>\" } }) { nodes { id title state { type } } } }"}'
456454
```
457455
- Set `linearIssueExists` to `true` if any matching issue is found, `false` otherwise.
456+
- If multiple issues match, prefer the one with an open state (i.e., state type is NOT `"completed"` or `"canceled"`).
457+
Only use a closed issue if no open issue exists for that finding.
458+
- Set `linearIssueId` to the `id` (UUID) of the selected matching issue, or `""` if none found.
459+
- Set `linearIssueClosed` to `true` if the selected issue's `state.type` is `"completed"` or `"canceled"`, `false` otherwise.
458460
459461
8. Return the structured JSON with all findings in the `cves` array.
460462
@@ -473,7 +475,7 @@ jobs:
473475
echo "| ID | Source | Severity | Package | Linear Status |" >> "$GITHUB_STEP_SUMMARY"
474476
echo "|----|--------|----------|---------|---------------|" >> "$GITHUB_STEP_SUMMARY"
475477
476-
echo "$STRUCTURED_OUTPUT" | jq -r '.cves[] | "| \(.cveId) | \(.source) | \(.severity) | \(.affectedPackage) | \(if .linearIssueExists then "Existing" else "New" end) |"' >> "$GITHUB_STEP_SUMMARY"
478+
echo "$STRUCTURED_OUTPUT" | jq -r '.cves[] | "| \(.cveId) | \(.source) | \(.severity) | \(.affectedPackage) | \(if .linearIssueExists then (if .linearIssueClosed then "Reopen" else "Existing") else "New" end) |"' >> "$GITHUB_STEP_SUMMARY"
477479
478480
echo "" >> "$GITHUB_STEP_SUMMARY"
479481
echo "### Details" >> "$GITHUB_STEP_SUMMARY"
@@ -527,6 +529,7 @@ jobs:
527529
528530
CREATED_COUNT=0
529531
SKIPPED_COUNT=0
532+
REOPENED_COUNT=0
530533
FAILED_COUNT=0
531534
532535
echo "## Linear Issue Creation" >> "$GITHUB_STEP_SUMMARY"
@@ -543,13 +546,49 @@ jobs:
543546
TITLE=$(echo "$cve" | jq -r '.title')
544547
DESCRIPTION=$(echo "$cve" | jq -r '.description')
545548
LINEAR_EXISTS=$(echo "$cve" | jq -r '.linearIssueExists')
549+
LINEAR_ISSUE_ID=$(echo "$cve" | jq -r '.linearIssueId')
550+
LINEAR_CLOSED=$(echo "$cve" | jq -r '.linearIssueClosed')
546551
547-
if [ "$LINEAR_EXISTS" = "true" ]; then
548-
echo "Skipping $CVE_ID — Linear issue already exists."
552+
if [ "$LINEAR_EXISTS" = "true" ] && [ "$LINEAR_CLOSED" = "false" ]; then
553+
echo "Skipping $CVE_ID — Linear issue already exists and is open."
549554
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
550555
continue
551556
fi
552557
558+
if [ "$LINEAR_EXISTS" = "true" ] && [ "$LINEAR_CLOSED" = "true" ]; then
559+
# Reopen the closed issue by setting its state back to Triage
560+
if [ -z "$STATE_ID" ]; then
561+
echo "::warning::Cannot reopen $CVE_ID — no Triage state found. Skipping."
562+
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
563+
continue
564+
fi
565+
566+
REOPEN_MUTATION='mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success issue { id identifier url } } }'
567+
REOPEN_VARIABLES=$(jq -n \
568+
--arg issueId "$LINEAR_ISSUE_ID" \
569+
--arg stateId "$STATE_ID" \
570+
'{issueId: $issueId, stateId: $stateId}')
571+
REOPEN_PAYLOAD=$(jq -n --arg query "$REOPEN_MUTATION" --argjson vars "$REOPEN_VARIABLES" '{query: $query, variables: $vars}')
572+
573+
REOPEN_RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \
574+
-H "Content-Type: application/json" \
575+
-H "Authorization: $LINEAR_API_KEY" \
576+
-d "$REOPEN_PAYLOAD")
577+
578+
REOPEN_URL=$(echo "$REOPEN_RESPONSE" | jq -r '.data.issueUpdate.issue.url // empty')
579+
if [ -n "$REOPEN_URL" ]; then
580+
echo "Reopened Linear issue for $CVE_ID: $REOPEN_URL"
581+
echo "- Reopened [$CVE_ID]($REOPEN_URL) (moved back to Triage)" >> "$GITHUB_STEP_SUMMARY"
582+
REOPENED_COUNT=$((REOPENED_COUNT + 1))
583+
else
584+
echo "::error::Failed to reopen Linear issue for $CVE_ID"
585+
echo "$REOPEN_RESPONSE" | jq .
586+
FAILED_COUNT=$((FAILED_COUNT + 1))
587+
fi
588+
continue
589+
fi
590+
591+
# Create new issue
553592
PRIORITY=$(severity_to_priority "$SEVERITY")
554593
ISSUE_TITLE="[$REPOSITORY] $CVE_ID: $TITLE"
555594
@@ -588,7 +627,7 @@ jobs:
588627
done < /tmp/cves.jsonl
589628
590629
echo "" >> "$GITHUB_STEP_SUMMARY"
591-
echo "**Summary:** Created $CREATED_COUNT issue(s), skipped $SKIPPED_COUNT existing issue(s), failed $FAILED_COUNT issue(s)." >> "$GITHUB_STEP_SUMMARY"
630+
echo "**Summary:** Created $CREATED_COUNT issue(s), reopened $REOPENED_COUNT issue(s), skipped $SKIPPED_COUNT existing issue(s), failed $FAILED_COUNT issue(s)." >> "$GITHUB_STEP_SUMMARY"
592631
593632
if [ "$FAILED_COUNT" -gt 0 ]; then
594633
echo "::error::Failed to create $FAILED_COUNT Linear issue(s)"

0 commit comments

Comments
 (0)