mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 12:41:04 +00:00
162 lines
5.8 KiB
YAML
162 lines
5.8 KiB
YAML
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, dev]
|
|
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
|