chore: import upstream snapshot with attribution
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Has been cancelled
Java Checkstyle / java-checkstyle (push) Has been cancelled
Maven Collate Tests / maven-collate-ci (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Has been cancelled
Publish Package to Maven Central Repository / publish-maven-packages (push) Has been cancelled
OpenMetadata Service Unit Tests / Detect Changes (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Has been cancelled
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Has been cancelled
Java Checkstyle / java-checkstyle (push) Has been cancelled
Maven Collate Tests / maven-collate-ci (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Has been cancelled
Publish Package to Maven Central Repository / publish-maven-packages (push) Has been cancelled
OpenMetadata Service Unit Tests / Detect Changes (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Has been cancelled
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Has been cancelled
This commit is contained in:
Executable
+247
@@ -0,0 +1,247 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user