mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
add malware scan
This commit is contained in:
161
.github/workflows/malware-scan.yml
vendored
Normal file
161
.github/workflows/malware-scan.yml
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
name: Malware Scan
|
||||
|
||||
# Supply-chain malware gate for the PolinRider / Famous Chollima campaign.
|
||||
#
|
||||
# Runs standalone on every push and pull request, and is also called by
|
||||
# deploy.yml as a required first job — a detection fails this workflow, which
|
||||
# blocks every downstream deploy job from starting.
|
||||
|
||||
on:
|
||||
push:
|
||||
# dev and staging are already gated through deploy.yml's required
|
||||
# malware-scan job — no need to scan those pushes twice.
|
||||
branches-ignore:
|
||||
- dev
|
||||
- staging
|
||||
pull_request:
|
||||
workflow_call:
|
||||
secrets:
|
||||
TELEGRAM_BOT_TOKEN:
|
||||
required: false
|
||||
TELEGRAM_CHAT_ID:
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# A detection on a ref should not be raced by a newer run of the same ref.
|
||||
concurrency:
|
||||
group: malware-scan-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
name: Scan for PolinRider malware
|
||||
# Plain `self-hosted` — GitHub applies this label to every self-hosted
|
||||
# runner automatically. The scan is host-agnostic, unlike the deploy jobs
|
||||
# which pin to a branch-specific runner.
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
infected: ${{ steps.scan.outputs.infected }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Verify scanner rules still work
|
||||
# Fails if someone weakens a detection rule or introduces a false
|
||||
# positive against minified vendor bundles.
|
||||
run: node .github/scripts/scan.js --self-test
|
||||
|
||||
- name: Scan repository
|
||||
id: scan
|
||||
run: |
|
||||
set -uo pipefail
|
||||
|
||||
# Actions runs this with `bash -e`, so the non-zero exit must be
|
||||
# caught with `||` rather than read back from $? afterwards.
|
||||
STATUS=0
|
||||
node .github/scripts/scan.js --json --output malware-report.json . || STATUS=$?
|
||||
|
||||
if [ "$STATUS" -eq 0 ]; then
|
||||
echo "infected=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No malware detected."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "infected=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Human-readable run for the log, so the failure is legible in the UI.
|
||||
node .github/scripts/scan.js . || true
|
||||
exit 1
|
||||
|
||||
- name: Build alert message
|
||||
id: message
|
||||
if: failure() && steps.scan.outputs.infected == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
FILES=$(jq -r '.results[].filePath' malware-report.json | head -20)
|
||||
COUNT=$(jq -r '.infectedFiles' malware-report.json)
|
||||
RULES=$(jq -r '[.results[].findings[] | select(.severity=="CRITICAL") | .id] | unique | join(", ")' malware-report.json)
|
||||
|
||||
{
|
||||
echo "message<<EOF"
|
||||
echo "🚨 POLINRIDER MALWARE DETECTED — DEPLOY BLOCKED"
|
||||
echo ""
|
||||
echo "Repo: ${GITHUB_REPOSITORY}"
|
||||
echo "Branch: ${GITHUB_REF_NAME}"
|
||||
echo "Commit: ${GITHUB_SHA}"
|
||||
echo "Author: ${GITHUB_ACTOR}"
|
||||
echo ""
|
||||
echo "Infected files (${COUNT}):"
|
||||
echo "${FILES}"
|
||||
echo ""
|
||||
echo "Critical rules: ${RULES:-none}"
|
||||
echo ""
|
||||
echo "Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||
echo ""
|
||||
echo "Do NOT run pnpm install or any build on this checkout."
|
||||
echo "Rotate every secret this repo's CI can reach."
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Notify Telegram
|
||||
if: failure() && steps.scan.outputs.infected == 'true'
|
||||
env:
|
||||
BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
TEXT: ${{ steps.message.outputs.message }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
|
||||
if [ -z "${BOT_TOKEN:-}" ] || [ -z "${CHAT_ID:-}" ]; then
|
||||
echo "::warning::TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID not set — skipping notification."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# No parse_mode: the payload contains characters Telegram's Markdown
|
||||
# parser would reject, and a failed notification is worse than plain text.
|
||||
HTTP=$(curl -sS -o /tmp/tg.out -w '%{http_code}' \
|
||||
-X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
--data-urlencode "chat_id=${CHAT_ID}" \
|
||||
--data-urlencode "text=${TEXT}" \
|
||||
--data-urlencode "disable_web_page_preview=true") || true
|
||||
|
||||
if [ "${HTTP:-000}" != "200" ]; then
|
||||
echo "::warning::Telegram notification failed (HTTP ${HTTP:-000}): $(cat /tmp/tg.out 2>/dev/null | head -c 300)"
|
||||
else
|
||||
echo "Telegram alert sent."
|
||||
fi
|
||||
rm -f /tmp/tg.out
|
||||
|
||||
- name: Upload scan report
|
||||
if: always() && hashFiles('malware-report.json') != ''
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: malware-report-${{ github.run_id }}
|
||||
path: malware-report.json
|
||||
retention-days: 30
|
||||
|
||||
- name: Job summary
|
||||
if: always()
|
||||
run: |
|
||||
set -uo pipefail
|
||||
RESULT="${{ steps.scan.outputs.infected }}"
|
||||
|
||||
if [ "$RESULT" = "true" ]; then
|
||||
{
|
||||
echo "## 🚨 Malware detected — deployment blocked"
|
||||
echo ""
|
||||
echo '```'
|
||||
jq -r '.results[] | .filePath, (.findings[] | " [\(.id)] \(.severity) — \(.description)")' \
|
||||
malware-report.json 2>/dev/null | head -100 || true
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
elif [ "$RESULT" = "false" ]; then
|
||||
echo "## ✅ No malware detected" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
# The scan step never produced a verdict — treat as inconclusive
|
||||
# rather than clean, so a broken scanner is never read as a pass.
|
||||
echo "## ⚠️ Scan did not complete — verdict unknown" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
Reference in New Issue
Block a user