plugins/node-cve/skills/report-findings/SKILL.md
Generate triage reports and post findings to Jira and Slack
npx skillsauth add openshift-eng/ai-helpers report-findingsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 (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/tree/main/plugins/node-cve]_
COMMENT
)"
Use the footer line exactly as shown. Do not append a date; the Jira comment timestamp already covers that.
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"
]
}
testing
Snapshot OpenShift payload data (release controller, PR diffs, comments, CI jobs, JUnit results, regression tracking) to a local directory for offline analysis
research
Shared engine for analyzing Jira issue activity and generating status summaries
tools
This skill should be used before any Snowflake command to verify MCP connectivity, guide users through access provisioning, and set the session context. Invoke this skill proactively whenever a command needs Snowflake data access.
development
Analyze a payload snapshot to identify root causes of blocking job failures, score candidate PRs, and produce an HTML report with revert recommendations