with one click
with one click
Safely query and report on OpenShift CI prow job and test data in BigQuery with cost controls, dry-run validation, and local caching of results
Analyze CVE reachability against downstream repository forks at version-specific release branches
Query and deduplicate open CVE vulnerability issues from OCPBUGS for Node team components
Categorize Jira issues into Red Hat Sankey Activity Type categories using MCP Jira tools. Supports single-issue and batch modes. Use when the user wants to categorize or set activity types on Jira issues, or mentions activity types, work types, Sankey, or capacity allocation.
Jira conventions for the CNTRLPLANE project used by OpenShift teams
Implementation guide for creating well-formed Jira bug reports
| name | report-findings |
| description | Generate triage reports and post findings to Jira and Slack |
Use this skill when Phase 3 of the node-cve:triage command needs to generate the triage report, post comments to Jira tracker issues, and send Slack notifications.
jira CLI (for --notify-jira)curl (for --notify-slack)JIRA_API_TOKEN, JIRA_USERNAME (for Jira)SLACK_API_TOKEN + SLACK_CHANNEL (preferred, enables threading) or SLACK_WEBHOOK (simpler, no threading)Write the report to .work/node-cve/triage-YYYY-MM-DD/report.md:
# Node CVE Triage Report - YYYY-MM-DD
## Summary
| Metric | Count |
|--------|-------|
| Total unique CVEs | N |
| Reachable | N |
| Present | N |
| Unaffected | N |
| Uncertain | N |
## Action Required
List CVEs that are Reachable or Uncertain with unassigned owners. These need immediate attention.
## Detailed Findings
### CVE-XXXX-XXXXX: <short description>
| Field | Value |
|-------|-------|
| Component | Node / CRI-O |
| Repository | openshift/cri-o |
| Overall classification | Reachable / Present but not exploitable / Present but not reachable / Unaffected / Uncertain |
| Overall confidence | High / Medium / Low |
| Assignee | <name or Unassigned> |
| Affected versions | 4.12.z - 4.19 |
| Tracker issues | [OCPBUGS-XXXXX](https://redhat.atlassian.net/browse/OCPBUGS-XXXXX), [OCPBUGS-XXXXX](https://redhat.atlassian.net/browse/OCPBUGS-XXXXX), ... |
**Per-branch results:**
| Branch | OCP Version | Classification | Confidence |
|--------|-------------|----------------|------------|
| release-1.28 | 4.15 | Reachable | High |
| release-1.29 | 4.16 | Reachable | High |
| release-1.30 | 4.17 | Unaffected | High |
| ... | ... | ... | ... |
**Evidence (worst-case branch):**
<source code analysis summary>
<call path if found>
**Recommended action:** <specific action>
---
(repeat for each CVE)
For each unique CVE, post a comment on ALL its tracker issues. Each tracker issue receives the analysis result for its specific OCP version/branch (not a blanket result). Use Atlassian wiki markup (not Markdown):
jira issue comment add OCPBUGS-XXXXX "$(cat <<'COMMENT'
h3. Automated CVE Reachability Analysis
||Field||Value||
|CVE|CVE-XXXX-XXXXX|
|Repository|[openshift/cri-o|https://github.com/openshift/cri-o]|
|Branch|release-1.31|
|Classification|Reachable / Present but not exploitable / Present but not reachable / Unaffected / Uncertain|
|Confidence|High / Medium / Low|
h4. Results across all analyzed branches
||Branch||OCP Version||Classification||Confidence||
|release-1.28|4.15|Reachable|High|
|release-1.29|4.16|Reachable|High|
|release-1.30|4.17|Unaffected|High|
h4. Evidence
{noformat}
<source code analysis summary for this tracker's specific branch>
{noformat}
h4. Recommended Action
<specific next step>
----
_Generated by [node-cve:triage|https://github.com/openshift-eng/ai-helpers]_
COMMENT
)"
Deduplication: Before posting a comment, check for existing node-cve:triage comments on the issue:
jira issue comment list OCPBUGS-XXXXX --plain --no-headers
Search the output for comments containing _Generated by [node-cve:triage|. If a prior comment exists:
Important:
Two modes are supported depending on which credentials are available:
Mode A: Slack API ($SLACK_API_TOKEN + $SLACK_CHANNEL) - enables threaded messages.
Step 3a: Post summary (main message)
RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer $SLACK_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(cat <<'SLACK'
{
"channel": "$SLACK_CHANNEL",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Node CVE Triage (N CVEs analyzed)"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":red_circle: Reachable: <https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX)|N> (<https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX)%20AND%20assignee%20is%20EMPTY|M> unassigned)\n:large_yellow_circle: Present: <https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX)|N> (<https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX)%20AND%20assignee%20is%20EMPTY|M> unassigned)\n:large_green_circle: Unaffected: <https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX)|N>\n:grey_question: Uncertain: <https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX)|N>"
}
}
]
}
SLACK
)"
Build the JQL URLs by collecting all tracker keys per classification group. Use key in (OCPBUGS-XXXXX, OCPBUGS-YYYYY, ...) as the filter. For the unassigned link, add AND (assignee is EMPTY OR assignee = "ocp-sustaining-blocked-trackers") to also count placeholder assignees as unassigned. URL-encode the JQL query. Omit the "(M unassigned)" part when M is 0. Omit empty classification lines.
On subsequent runs with cached results, change the header to "Node CVE Triage (N CVEs, M new)" or "Node CVE Triage (N CVEs, M new, K updated)".
Extract ts from the JSON response (jq -r '.ts') to use as thread_ts for the reply.
Step 3b: Post detailed findings (thread reply)
curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer $SLACK_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(cat <<'SLACK'
{
"channel": "$SLACK_CHANNEL",
"thread_ts": "<ts-from-step-3a>",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Reachable (action required):*\n⢠<https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX%2COCPBUGS-YYYYY)|CVE-XXXX-XXXXX> - <short description>. (CRI-O, high confidence, N trackers[, M unassigned])\n\n*Present (no action needed):*\n⢠<https://redhat.atlassian.net/browse/OCPBUGS-XXXXX|CVE-XXXX-XXXXX> - <short description>. (kubernetes, high confidence, 1 tracker)\n\n*Unaffected:*\n⢠...\n\n*Uncertain:*\n⢠..."
}
}
]
}
SLACK
)"
Mode B: Webhook ($SLACK_WEBHOOK) - simpler setup, no threading.
Post a single message containing both summary and detailed findings:
curl -s -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "$(cat <<'SLACK'
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Node CVE Triage (N CVEs analyzed)"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":red_circle: Reachable: <https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX)|N> (<https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX)%20AND%20assignee%20is%20EMPTY|M> unassigned)\n:large_yellow_circle: Present: <https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX)|N>\n:large_green_circle: Unaffected: <https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX)|N>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Reachable (action required):*\n⢠<https://redhat.atlassian.net/issues/?jql=key%20in%20(OCPBUGS-XXXXX%2COCPBUGS-YYYYY)|CVE-XXXX-XXXXX> - <short description>. (CRI-O, high confidence, N trackers[, M unassigned])\n\n*Present (no action needed):*\n⢠...\n\n*Unaffected:*\n⢠..."
}
}
]
}
SLACK
)"
Both modes: Omit empty classification sections. If the total text exceeds the Slack character limit (3000 chars per text block), split across multiple blocks or truncate with "... and N more. See full report." If Slack returns a non-200 status, log a warning but do not fail the command.
Write cves.json to .work/node-cve/triage-YYYY-MM-DD/cves.json containing the full analysis results in machine-readable format:
{
"date": "YYYY-MM-DD",
"total_cves": 6,
"cves": [
{
"cve_id": "CVE-XXXX-XXXXX",
"summary": "...",
"components": ["Node / CRI-O"],
"repo": "https://github.com/openshift/cri-o",
"overall_classification": "REACHABLE",
"overall_confidence": "HIGH",
"assignee": "...",
"tracker_keys": ["OCPBUGS-XXXXX"],
"affected_versions": ["4.12.z", "4.19"],
"per_branch_results": [
{
"branch": "release-1.28",
"ocp_version": "4.15",
"classification": "REACHABLE",
"confidence": "HIGH",
"evidence_summary": "..."
},
{
"branch": "release-1.30",
"ocp_version": "4.17",
"classification": "NOT_AFFECTED",
"confidence": "HIGH",
"evidence_summary": "..."
}
],
"recommended_action": "..."
}
]
}
Ensure all generated files exist under .work/node-cve/triage-YYYY-MM-DD/:
report.md - full reportcves.json - structured CVE data (for programmatic consumption)<CVE-ID>-<branch>-analysis.md - per-CVE per-branch source code analysis (from Phase 2){
"skill": "report-findings",
"status": "success",
"report_path": ".work/node-cve/triage-2026-05-20/report.md",
"jira_comments_posted": 45,
"jira_comments_failed": 0,
"slack_notified": true,
"artifacts": [
".work/node-cve/triage-2026-05-20/report.md",
".work/node-cve/triage-2026-05-20/cves.json"
]
}