How this skill is triggered — by the user, by Claude, or both
Slash command
/node-cve:report-findingsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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)This skill may ONLY post comments to trackers whose component is a Node team component. Many CVEs (especially Go stdlib and vendored-dependency vulnerabilities) span 50-200+ tracker issues across dozens of OpenShift teams (HyperShift, Storage, Networking, Installer, Monitoring, Cloud providers, etc.). Node-specific reachability analysis is meaningless — and confusing — on another team's tracker.
The canonical Node team component list lives in the node-team shared components reference ("Jira Components (OCPBUGS)" section, plus Driver Toolkit and Machine Config Operator). Do not hardcode or duplicate that list here or anywhere else — always read it from the shared reference so it stays in sync as Node team components change. Note: the shared reference also documents pscomponent: label mappings, but those are for Phase 1 CVE discovery and Phase 2 repo mapping only — the posting-time validation in Step 2 below checks the tracker's COMPONENT field exclusively (see Step 2 for why).
Never do this (this is exactly what caused the 2026-07-15 incident where analysis was posted to ~200 non-Node trackers):
summary ~ "CVE-XXXX-XXXXX") to find trackers to comment on. A CVE ID alone is not enough to scope a search — it will return trackers for every team affected by that CVE.tracker_keys produced by the query-open-cves skill in Phase 1.tracker_keys may have been supplemented, cached from a stale run, or copy-pasted into a manual follow-up.Only post to tracker keys that are:
tracker_keys list of the CVE record produced by query-open-cves (Phase 1), ANDIf you ever find yourself constructing a new JQL query or Jira search specifically to find trackers to comment on, STOP — that is the anti-pattern that caused this incident. Reuse the already-filtered tracker list from Phase 1 instead.
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)
VALIDATION (MANDATORY, before posting anything): For each unique CVE, take its tracker_keys list from Phase 1 (query-open-cves) and re-validate every tracker's component immediately before posting — do not trust cached or upstream filtering alone:
# component_is_node_team() checks the tracker's COMPONENT field against the
# canonical Node team component list from the shared components reference
# (link above), NOT a hardcoded list.
for tracker_key in $TRACKER_KEYS; do
component=$(jira issue view "$tracker_key" --plain --no-headers --columns COMPONENT | tail -1)
if ! component_is_node_team "$component"; then
echo "⚠️ SKIPPING $tracker_key: component '$component' is not a Node team component" | tee -a "$SKIPPED_LOG"
sleep 1
continue
fi
# Component validated — proceed with posting for $tracker_key
sleep 1
done
A component is considered a Node team component only if it matches an entry in the "Jira Components (OCPBUGS)" list (plus Driver Toolkit, Machine Config Operator) in the shared components reference. Check the COMPONENT field only — do not treat a pscomponent: label as an alternative pass condition. pscomponent: labels are used in Phase 1/Phase 2 for CVE discovery and repo mapping, not for determining tracker ownership; a non-Node tracker (e.g. component "Security") could incidentally carry a pscomponent:cri-o label, and accepting that as a pass would silently reintroduce the exact cross-team contamination this safeguard exists to prevent. If a tracker's component cannot be confidently classified as Node team, skip it and log the reason — never post "just in case." This check must run even when --component was passed, and even if Phase 1 already filtered by component, since this is the last line of defense before an irreversible write to another team's tracker. Rate limit: sleep 1 second between validation calls, same as the posting calls below, since a CVE with N trackers makes N validation calls before posting even starts.
For each unique CVE, post a comment on every validated tracker issue. 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>
----
_AI-generated analysis by [node-cve:triage|https://github.com/openshift-eng/ai-helpers/tree/main/plugins/node-cve]. Always review prior to use._
COMMENT
)"
Use the footer line exactly as shown. The "AI-generated" label and review notice are required by Red Hat's medium-risk AI agent controls (TR-01, HU-01). 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 [node-cve:triage|. This pattern anchors on the Jira wiki-markup link syntax and matches both the current and legacy footer formats. If a prior comment exists:
Important:
POST-POSTING AUDIT (MANDATORY when --notify-jira is used): Write an audit log to .work/node-cve/triage-$(date +%Y-%m-%d)/posting-audit.log summarizing what was posted and what was skipped, so cross-team contamination is caught immediately instead of discovered days later:
{
echo "=== Node CVE Triage Posting Audit — $(date +%Y-%m-%d) ==="
echo "CVEs processed: $CVE_COUNT"
echo "Trackers commented: $POSTED_COUNT"
echo "Trackers skipped (non-Node component): $SKIPPED_COUNT"
echo ""
echo "Skipped trackers (tracker, component, reason):"
cat "$SKIPPED_LOG"
} > ".work/node-cve/triage-$(date +%Y-%m-%d)/posting-audit.log"
If $SKIPPED_COUNT is greater than zero, print a visible warning in the command summary output (Phase 4) so the operator notices immediately, e.g. "⚠️ Skipped N non-Node-component trackers — see posting-audit.log". A non-zero skip count is expected and healthy for multi-team CVEs; it means the safeguard is working. A skip count that is unexpectedly large relative to Phase 1's tracker count may indicate a bug and should be investigated before re-running.
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>"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": ":robot_face: AI-generated by node-cve:triage. Always review prior to use."
}
]
}
]
}
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• ..."
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": ":robot_face: AI-generated by node-cve:triage. Always review prior to use."
}
]
}
]
}
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• ..."
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": ":robot_face: AI-generated by node-cve:triage. Always review prior to use."
}
]
}
]
}
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)posting-audit.log - only when --notify-jira was used (see Step 2); do not expect this file otherwise<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,
"jira_trackers_skipped_non_node_component": 0,
"slack_notified": true,
"artifacts": [
".work/node-cve/triage-2026-05-20/report.md",
".work/node-cve/triage-2026-05-20/cves.json"
]
}
posting-audit.log is only produced when --notify-jira is used (see Step 2). When present, add it to the artifacts list; omit it entirely on runs without --notify-jira rather than reporting a file that was never written.
npx claudepluginhub jluhrsen/ai-helpers --plugin node-cveGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Synthesizes the current conversation into a structured spec (PRD) and publishes it to the project issue tracker with a ready-for-agent label, without interviewing the user.