bf2343b7e4
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Waiting to run
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Blocked by required conditions
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Waiting to run
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Blocked by required conditions
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Waiting to run
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Blocked by required conditions
Java Checkstyle / java-checkstyle (push) Waiting to run
Maven Collate Tests / maven-collate-ci (push) Waiting to run
OpenMetadata Service Unit Tests / Detect Changes (push) Waiting to run
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Blocked by required conditions
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Blocked by required conditions
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Blocked by required conditions
Publish Package to Maven Central Repository / publish-maven-packages (push) Waiting to run
248 lines
7.9 KiB
Python
Executable File
248 lines
7.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Render a Retire.js JSON report as Markdown + Slack-friendly summary.
|
|
|
|
Usage:
|
|
python3 scripts/retire_slack_summary.py <retire-report.json | dir> \\
|
|
[--counts-file PATH] [--slack-file PATH] [--top N]
|
|
|
|
Mirrors the surface of scripts/snyk_summary.py so the notify job can
|
|
consume `_retire_counts.json` and `_retire_slack.txt` identically.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
SEV_ICON = {"critical": "🚨", "high": "🔴", "medium": "🟠", "low": "🟡"}
|
|
SEV_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
NM = "node_modules/"
|
|
|
|
|
|
def esc(s):
|
|
return str(s).replace("|", "\\|").replace("`", "'").replace("\n", " ")
|
|
|
|
|
|
def sev_key(s):
|
|
return SEV_RANK.get((s or "low").lower(), 3)
|
|
|
|
|
|
def resolve_report_path(src):
|
|
if os.path.isdir(src):
|
|
return os.path.join(src, "retire-report.json")
|
|
return src
|
|
|
|
|
|
def short_path(filepath):
|
|
if NM in filepath:
|
|
return filepath[filepath.find(NM) + len(NM):]
|
|
return filepath
|
|
|
|
|
|
def vuln_id(vuln):
|
|
"""Stable identifier for dedup across files."""
|
|
ids = vuln.get("identifiers") or {}
|
|
return json.dumps(ids, sort_keys=True)
|
|
|
|
|
|
def vuln_title(vuln):
|
|
ids = vuln.get("identifiers") or {}
|
|
cves = ids.get("CVE") or []
|
|
if cves:
|
|
return cves[0]
|
|
ghsa = ids.get("githubID")
|
|
if ghsa:
|
|
return ghsa
|
|
summary = ids.get("summary") or ""
|
|
return summary.split("\n")[0][:60] or "—"
|
|
|
|
|
|
def vuln_summary(vuln):
|
|
ids = vuln.get("identifiers") or {}
|
|
summary = ids.get("summary") or ""
|
|
return summary.split("\n")[0][:80]
|
|
|
|
|
|
def vuln_fix(vuln):
|
|
"""Retire.js encodes fix as `below: <first-fixed-version>`."""
|
|
below = vuln.get("below")
|
|
if below:
|
|
return str(below)
|
|
return "no fix"
|
|
|
|
|
|
def collect_libs(data):
|
|
"""Aggregate vulnerabilities per (component, version), deduped."""
|
|
libs = {}
|
|
findings = data.get("data") or []
|
|
for item in findings:
|
|
filepath = item.get("file", "")
|
|
short = short_path(filepath)
|
|
for result in item.get("results") or []:
|
|
key = (result.get("component", "?"), result.get("version", "?"))
|
|
entry = libs.setdefault(key, {
|
|
"files": [],
|
|
"vulns": [],
|
|
"seen": set(),
|
|
})
|
|
if short and short not in entry["files"]:
|
|
entry["files"].append(short)
|
|
for v in result.get("vulnerabilities") or []:
|
|
vid = vuln_id(v)
|
|
if vid in entry["seen"]:
|
|
continue
|
|
entry["seen"].add(vid)
|
|
entry["vulns"].append(v)
|
|
return libs
|
|
|
|
|
|
def lib_top_severity(info):
|
|
if not info["vulns"]:
|
|
return "low"
|
|
return min(
|
|
(v.get("severity", "low") for v in info["vulns"]),
|
|
key=sev_key,
|
|
)
|
|
|
|
|
|
def count_severities(libs):
|
|
counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
|
for info in libs.values():
|
|
for v in info["vulns"]:
|
|
sev = (v.get("severity") or "low").lower()
|
|
if sev in counts:
|
|
counts[sev] += 1
|
|
return counts
|
|
|
|
|
|
def render_md(libs):
|
|
out = ["## 🛡️ Vulnerability JS Scan\n"]
|
|
if not libs:
|
|
out.append("✅ No vulnerable libraries found.\n")
|
|
return "\n".join(out)
|
|
total_vulns = sum(len(info["vulns"]) for info in libs.values())
|
|
out.append(
|
|
f"> **{len(libs)} vulnerable librar"
|
|
f"{'y' if len(libs) == 1 else 'ies'} · {total_vulns} "
|
|
f"CVE{'s' if total_vulns != 1 else ''} found**\n"
|
|
)
|
|
ordered = sorted(libs.items(), key=lambda kv: sev_key(lib_top_severity(kv[1])))
|
|
for (component, version), info in ordered:
|
|
top_icon = SEV_ICON.get(lib_top_severity(info), "⚪")
|
|
out.append(f"### {top_icon} {component} {version}\n")
|
|
out.append("| Severity | CVE | Summary | Fix |")
|
|
out.append("|---|---|---|---|")
|
|
for v in sorted(info["vulns"], key=lambda x: sev_key(x.get("severity"))):
|
|
sev = v.get("severity", "")
|
|
icon = SEV_ICON.get(sev, "⚪")
|
|
ids = v.get("identifiers") or {}
|
|
cves = ids.get("CVE") or []
|
|
if cves:
|
|
cve_str = ", ".join(
|
|
f"[{c}](https://nvd.nist.gov/vuln/detail/{c})" for c in cves
|
|
)
|
|
else:
|
|
cve_str = ids.get("githubID") or "—"
|
|
summary = vuln_summary(v) or "—"
|
|
fix = vuln_fix(v)
|
|
out.append(
|
|
f"| {icon} {sev} | {esc(cve_str)} | {esc(summary)} | {esc(fix)} |"
|
|
)
|
|
if info["files"]:
|
|
out.append("\n**Bundled in:**")
|
|
for f in info["files"]:
|
|
out.append(f"- `{f}`")
|
|
out.append("")
|
|
return "\n".join(out)
|
|
|
|
|
|
def render_lib_slack(component, version, info, top):
|
|
head = f"📦 *{component}@{version}* — {len(info['vulns'])} CVE{'s' if len(info['vulns']) != 1 else ''}"
|
|
rows = []
|
|
ordered = sorted(info["vulns"], key=lambda v: sev_key(v.get("severity")))
|
|
for v in ordered[:top]:
|
|
icon = SEV_ICON.get(v.get("severity", "low"), "⚪")
|
|
title = vuln_title(v)
|
|
fix = vuln_fix(v)
|
|
fix_label = "no fix" if fix == "no fix" else f"fix in {fix}"
|
|
rows.append(f" {icon} {title} · {fix_label}")
|
|
extra = len(info["vulns"]) - top
|
|
if extra > 0:
|
|
rows.append(f" … +{extra} more")
|
|
return head + "\n" + "\n".join(rows) if rows else head
|
|
|
|
|
|
def render_slack(libs, totals, top):
|
|
header = (
|
|
f"*🛡️ Vulnerability JS Scan*\n"
|
|
f"🚨 {totals['critical']} critical · 🔴 {totals['high']} high · "
|
|
f"🟠 {totals['medium']} medium · 🟡 {totals['low']} low"
|
|
)
|
|
if not libs:
|
|
return f"{header}\n> ✅ No vulnerable libraries found."
|
|
ordered = sorted(libs.items(), key=lambda kv: sev_key(lib_top_severity(kv[1])))
|
|
sections = [
|
|
render_lib_slack(component, version, info, top)
|
|
for (component, version), info in ordered
|
|
]
|
|
body = "\n\n".join([header] + sections)
|
|
if len(body) > 2800:
|
|
cut = body.rfind("\n", 0, 2750)
|
|
if cut < 0:
|
|
cut = 2750
|
|
body = body[:cut].rstrip() + "\n…truncated. See Job Summary for full report."
|
|
return body
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("src", nargs="?", default="retire-report.json")
|
|
ap.add_argument("--counts-file")
|
|
ap.add_argument("--slack-file")
|
|
ap.add_argument("--top", type=int, default=5)
|
|
args = ap.parse_args()
|
|
|
|
path = resolve_report_path(args.src)
|
|
totals = {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
|
|
|
if not os.path.exists(path):
|
|
md = f"## 🛡️ Vulnerability JS Scan\n\n> Report file not found at `{path}`.\n"
|
|
sys.stdout.write(md)
|
|
if args.counts_file:
|
|
with open(args.counts_file, "w") as f:
|
|
json.dump({**totals, "total": 0}, f)
|
|
if args.slack_file:
|
|
with open(args.slack_file, "w") as f:
|
|
f.write("*🛡️ Vulnerability JS Scan*\n> Report file not found.")
|
|
return
|
|
|
|
try:
|
|
with open(path) as f:
|
|
data = json.load(f)
|
|
except Exception as e:
|
|
md = f"## 🛡️ Vulnerability JS Scan\n\n> Failed to parse `{path}`: {e}\n"
|
|
sys.stdout.write(md)
|
|
if args.counts_file:
|
|
with open(args.counts_file, "w") as f:
|
|
json.dump({**totals, "total": 0}, f)
|
|
if args.slack_file:
|
|
with open(args.slack_file, "w") as f:
|
|
f.write(f"*🛡️ Vulnerability JS Scan*\n> Parse error: {e}")
|
|
return
|
|
|
|
libs = collect_libs(data)
|
|
totals.update(count_severities(libs))
|
|
|
|
sys.stdout.write(render_md(libs) + "\n")
|
|
|
|
if args.counts_file:
|
|
with open(args.counts_file, "w") as f:
|
|
json.dump({**totals, "total": sum(totals.values())}, f)
|
|
|
|
if args.slack_file:
|
|
with open(args.slack_file, "w") as f:
|
|
f.write(render_slack(libs, totals, args.top))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|