OpenMetadata/.github/workflows/security-scan.yml
Harsh Vador 86e1d88386
security: Include branch name in security scan Slack alerts and fail only on high vulnerabilities (#27977)
* Add branch context to security scan Slack alerts and upload CSV findings summary

* change failing severity from medium to  high & address gitar

* fix csv formatting

* revert flattening changes
2026-05-11 10:41:48 +05:30

254 lines
9.5 KiB
YAML

# Copyright 2021 Collate
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: security-scan
on:
schedule:
- cron: '0 0 */2 * *'
workflow_dispatch:
jobs:
vulnerability-scan:
runs-on: ubuntu-latest
environment: security-scan
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: 'openmetadata-ui/src/main/resources/ui/.nvmrc'
- name: Enable yarn
run: corepack enable
- name: Install UI dependencies
working-directory: openmetadata-ui/src/main/resources/ui
run: yarn install --frozen-lockfile --ignore-scripts
- name: Run Retire.js scan
id: retire-scan
continue-on-error: true
working-directory: openmetadata-ui/src/main/resources/ui
run: |
npx retire@5 \
--path node_modules/ \
--severity high \
--outputformat json \
--outputpath retire-report.json
- name: Verify report was generated
working-directory: openmetadata-ui/src/main/resources/ui
run: |
if [ ! -f retire-report.json ]; then
echo '::error::retire-report.json was not generated — retire scan may have crashed'
exit 1
fi
- name: Upload Retire.js Report
if: success()
uses: actions/upload-artifact@v4
with:
name: retire-js-report
path: openmetadata-ui/src/main/resources/ui/retire-report.json
retention-days: 30
- name: Publish Retire.js Summary
if: success()
working-directory: openmetadata-ui/src/main/resources/ui
run: |
python3 - << 'EOF' >> $GITHUB_STEP_SUMMARY
import json
SEVERITY_ICON = {"critical": "🚨", "high": "🔴", "medium": "🟠", "low": "🟡"}
SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}
NM = "node_modules/"
def escape(text):
return str(text).replace('|', '\\|').replace('`', "'")
try:
with open("retire-report.json") as f:
data = json.load(f)
except FileNotFoundError:
print("## Retire.js Scan Results\n\n> Report file not found — scan may not have run.")
raise SystemExit(0)
findings = data.get("data", [])
libs = {}
for item in findings:
filepath = item.get("file", "")
short = filepath[filepath.find(NM) + len(NM):] if NM in filepath else filepath
for result in item.get("results", []):
key = (result.get("component", ""), result.get("version", ""))
if key not in libs:
libs[key] = {"files": [], "vulns": result.get("vulnerabilities", [])}
if short not in libs[key]["files"]:
libs[key]["files"].append(short)
print("## Retire.js Scan Results\n")
if not libs:
print("✅ No vulnerable libraries found.")
else:
total_vulns = sum(len(v["vulns"]) for v in libs.values())
print(f"> **{len(libs)} vulnerable librar{'y' if len(libs) == 1 else 'ies'} · {total_vulns} CVE{'s' if total_vulns != 1 else ''} found**\n")
for (component, version), info in sorted(libs.items(), key=lambda x: min(
(SEVERITY_ORDER.get(v.get("severity", "low"), 3) for v in x[1]["vulns"]), default=3)):
top_sev = min(info["vulns"], key=lambda v: SEVERITY_ORDER.get(v.get("severity", "low"), 3))
icon = SEVERITY_ICON.get(top_sev.get("severity", "low"), "⚪")
print(f"### {icon} {component} {version}\n")
print("| Severity | CVE | Summary |")
print("|---|---|---|")
for vuln in sorted(info["vulns"], key=lambda v: SEVERITY_ORDER.get(v.get("severity", "low"), 3)):
sev = vuln.get("severity", "")
ids = vuln.get("identifiers", {})
cves = ids.get("CVE", [])
summary = ids.get("summary", "").split("\n")[0][:120]
cve_str = ", ".join(f"[{c}](https://nvd.nist.gov/vuln/detail/{c})" for c in cves) if cves else ids.get("githubID", "—")
print(f"| {SEVERITY_ICON.get(sev, '')} {sev} | {escape(cve_str)} | {escape(summary)} |")
print("\n**Bundled in:**")
for f in info["files"]:
print(f"- `{f}`")
print()
EOF
- name: Slack on Failure
if: steps.retire-scan.outcome == 'failure'
uses: slackapi/slack-github-action@v1.23.0
with:
channel-id: ${{ secrets.SLACK_CHANNEL_IDS }}
payload: |
{
"text": "🚨 Vulnerability scan failed on branch `${{ github.ref_name }}`, please check it <https://github.com/open-metadata/OpenMetadata/actions/runs/${{ github.run_id }}|here>. 🚨"
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
- name: Slack on Success
if: steps.retire-scan.outcome == 'success'
uses: slackapi/slack-github-action@v1.23.0
with:
channel-id: ${{ secrets.SLACK_CHANNEL_IDS }}
payload: |
{
"text": "🟢 Vulnerability scan passed for OpenMetadata Repo on branch `${{ github.ref_name }}`, please check it <https://github.com/open-metadata/OpenMetadata/actions/runs/${{ github.run_id }}|here>."
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
- name: Force failure on vulnerabilities found
if: steps.retire-scan.outcome == 'failure'
run: exit 1
security-scan:
runs-on: ubuntu-latest
environment: security-scan
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
SNYK_ORGANIZATION: ${{ secrets.SNYK_ORGANIZATION_ID }}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: false
docker-images: true
swap-storage: true
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Install Ubuntu dependencies
run: |
# stop relying on apt cache of GitHub runners
sudo apt-get update
sudo apt-get install -y unixodbc-dev python3-venv librdkafka-dev gcc libsasl2-dev build-essential libssl-dev libffi-dev \
librdkafka-dev unixodbc-dev libevent-dev wkhtmltopdf libkrb5-dev
# Install and Authenticate to Snyk
- name: Install Snyk & Authenticate
run: |
sudo make install_antlr_cli
sudo npm install -g snyk
snyk auth ${SNYK_TOKEN}
snyk config set org=${SNYK_ORGANIZATION}
- name: Install Python dependencies
run: |
python3 -m venv env
source env/bin/activate
make install_all install_apis
- name: Maven build
id: maven-build
continue-on-error: true
run: mvn -DskipTests clean install
- name: Run Scan
id: security-report
if: steps.maven-build.outcome == 'success'
continue-on-error: true
run: |
source env/bin/activate
make snyk-report
- name: Slack on Failure
if: steps.security-report.outcome != 'success'
uses: slackapi/slack-github-action@v1.23.0
with:
channel-id: ${{ secrets.SLACK_CHANNEL_IDS }}
payload: |
{
"text": "🚨 Security report failed on branch `${{ github.ref_name }}`, please check it <https://github.com/open-metadata/OpenMetadata/actions/runs/${{ github.run_id }}|here>. 🚨"
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
- name: Slack on Success
if: steps.security-report.outcome == 'success'
uses: slackapi/slack-github-action@v1.23.0
with:
channel-id: ${{ secrets.SLACK_CHANNEL_IDS }}
payload: |
{
"text": "🟢 Security report generated for OpenMetadata Repo on branch `${{ github.ref_name }}`, please check it <https://github.com/open-metadata/OpenMetadata/actions/runs/${{ github.run_id }}|here>."
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
- name: Upload Snyk Report HTML files
if: steps.security-report.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: security-report
path: security-report
- name: Force failure
if: steps.maven-build.outcome != 'success' || steps.security-report.outcome != 'success'
run: |
exit 1