Skip to content

Commit f4c9398

Browse files
authored
ci(security): add Retire.js workflow to detect bundled JS vulnerabilities (#27315)
* ci(security): add Retire.js workflow to detect bundled JS vulnerabilities * address gitar * add om existing security scan workflow * address gitar * add slack support & remove PR check * address gitar * change job name * address comment * address comment
1 parent c54160f commit f4c9398

1 file changed

Lines changed: 136 additions & 0 deletions

File tree

.github/workflows/security-scan.yml

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,142 @@ on:
1616
workflow_dispatch:
1717

1818
jobs:
19+
vulnerability-scan:
20+
runs-on: ubuntu-latest
21+
environment: security-scan
22+
permissions:
23+
contents: read
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- name: Setup Node.js
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version-file: 'openmetadata-ui/src/main/resources/ui/.nvmrc'
31+
32+
- name: Enable yarn
33+
run: corepack enable
34+
35+
- name: Install UI dependencies
36+
working-directory: openmetadata-ui/src/main/resources/ui
37+
run: yarn install --frozen-lockfile --ignore-scripts
38+
39+
- name: Run Retire.js scan
40+
id: retire-scan
41+
continue-on-error: true
42+
working-directory: openmetadata-ui/src/main/resources/ui
43+
run: |
44+
npx retire@5 \
45+
--path node_modules/ \
46+
--severity medium \
47+
--outputformat json \
48+
--outputpath retire-report.json
49+
50+
- name: Verify report was generated
51+
working-directory: openmetadata-ui/src/main/resources/ui
52+
run: |
53+
if [ ! -f retire-report.json ]; then
54+
echo '::error::retire-report.json was not generated — retire scan may have crashed'
55+
exit 1
56+
fi
57+
58+
- name: Upload Retire.js Report
59+
if: success()
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: retire-js-report
63+
path: openmetadata-ui/src/main/resources/ui/retire-report.json
64+
retention-days: 30
65+
66+
- name: Publish Retire.js Summary
67+
if: success()
68+
working-directory: openmetadata-ui/src/main/resources/ui
69+
run: |
70+
python3 - << 'EOF' >> $GITHUB_STEP_SUMMARY
71+
import json
72+
73+
SEVERITY_ICON = {"critical": "🚨", "high": "🔴", "medium": "🟠", "low": "🟡"}
74+
SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}
75+
NM = "node_modules/"
76+
77+
def escape(text):
78+
return str(text).replace('|', '\\|').replace('`', "'")
79+
80+
try:
81+
with open("retire-report.json") as f:
82+
data = json.load(f)
83+
except FileNotFoundError:
84+
print("## Retire.js Scan Results\n\n> Report file not found — scan may not have run.")
85+
raise SystemExit(0)
86+
87+
findings = data.get("data", [])
88+
libs = {}
89+
for item in findings:
90+
filepath = item.get("file", "")
91+
short = filepath[filepath.find(NM) + len(NM):] if NM in filepath else filepath
92+
for result in item.get("results", []):
93+
key = (result.get("component", ""), result.get("version", ""))
94+
if key not in libs:
95+
libs[key] = {"files": [], "vulns": result.get("vulnerabilities", [])}
96+
if short not in libs[key]["files"]:
97+
libs[key]["files"].append(short)
98+
99+
print("## Retire.js Scan Results\n")
100+
101+
if not libs:
102+
print("✅ No vulnerable libraries found.")
103+
else:
104+
total_vulns = sum(len(v["vulns"]) for v in libs.values())
105+
print(f"> **{len(libs)} vulnerable librar{'y' if len(libs) == 1 else 'ies'} · {total_vulns} CVE{'s' if total_vulns != 1 else ''} found**\n")
106+
107+
for (component, version), info in sorted(libs.items(), key=lambda x: min(
108+
(SEVERITY_ORDER.get(v.get("severity", "low"), 3) for v in x[1]["vulns"]), default=3)):
109+
top_sev = min(info["vulns"], key=lambda v: SEVERITY_ORDER.get(v.get("severity", "low"), 3))
110+
icon = SEVERITY_ICON.get(top_sev.get("severity", "low"), "⚪")
111+
print(f"### {icon} {component} {version}\n")
112+
print("| Severity | CVE | Summary |")
113+
print("|---|---|---|")
114+
for vuln in sorted(info["vulns"], key=lambda v: SEVERITY_ORDER.get(v.get("severity", "low"), 3)):
115+
sev = vuln.get("severity", "")
116+
ids = vuln.get("identifiers", {})
117+
cves = ids.get("CVE", [])
118+
summary = ids.get("summary", "").split("\n")[0][:120]
119+
cve_str = ", ".join(f"[{c}](https://nvd.nist.gov/vuln/detail/{c})" for c in cves) if cves else ids.get("githubID", "—")
120+
print(f"| {SEVERITY_ICON.get(sev, '')} {sev} | {escape(cve_str)} | {escape(summary)} |")
121+
print("\n**Bundled in:**")
122+
for f in info["files"]:
123+
print(f"- `{f}`")
124+
print()
125+
EOF
126+
127+
- name: Slack on Failure
128+
if: steps.retire-scan.outcome == 'failure'
129+
uses: slackapi/slack-github-action@v1.23.0
130+
with:
131+
channel-id: ${{ secrets.SLACK_CHANNEL_IDS }}
132+
payload: |
133+
{
134+
"text": "🚨 Vulnerability scan failed, please check it <https://github.com/open-metadata/OpenMetadata/actions/runs/${{ github.run_id }}|here>. 🚨"
135+
}
136+
env:
137+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
138+
139+
- name: Slack on Success
140+
if: steps.retire-scan.outcome == 'success'
141+
uses: slackapi/slack-github-action@v1.23.0
142+
with:
143+
channel-id: ${{ secrets.SLACK_CHANNEL_IDS }}
144+
payload: |
145+
{
146+
"text": "🟢 Vulnerability scan passed for OpenMetadata Repo, please check it <https://github.com/open-metadata/OpenMetadata/actions/runs/${{ github.run_id }}|here>."
147+
}
148+
env:
149+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
150+
151+
- name: Force failure on vulnerabilities found
152+
if: steps.retire-scan.outcome == 'failure'
153+
run: exit 1
154+
19155
security-scan:
20156
runs-on: ubuntu-latest
21157
environment: security-scan

0 commit comments

Comments
 (0)