diff --git a/.github/workflows/malware-scan.yml b/.github/workflows/malware-scan.yml new file mode 100644 index 0000000..40a8eed --- /dev/null +++ b/.github/workflows/malware-scan.yml @@ -0,0 +1,166 @@ +name: PolinRider Malware Scan + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + malware-scan: + name: Scan for PolinRider malware indicators + runs-on: ubuntu-latest + steps: + - name: Checkout (full history for rewritten-commit detection) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run PolinRider malware scan + shell: bash + run: | + set +e + FAIL=0 + + # ── Helpers ───────────────────────────────────────────────────────── + flag() { + echo "::error file=$1::$2" + FAIL=1 + } + + # ── 1. Known malware string markers ───────────────────────────────── + echo "--- Check 1: known malware strings ---" + while IFS= read -r match; do + [ -z "$match" ] && continue + file=$(echo "$match" | cut -d: -f1) + flag "$file" "Malware marker found: $match" + done < <( + grep -rn \ + -e 'rmcej%otb%' \ + -e 'Cot%3t=shtP' \ + -e '_\$_1e42' \ + --include='*.js' --include='*.ts' --include='*.mjs' \ + --include='*.cjs' --include='*.json' --include='*.html' \ + --include='*.sh' \ + . 2>/dev/null | grep -v '^\./.git/' + ) + + # ── 2. .vscode/tasks.json with runOn: folderOpen ───────────────────── + echo "--- Check 2: .vscode/tasks.json runOn folderOpen ---" + find . -path '*/.vscode/tasks.json' ! -path '*/.git/*' | while read -r f; do + if grep -q '"runOn"' "$f" && grep -q 'folderOpen' "$f"; then + flag "$f" "Suspicious .vscode/tasks.json with runOn:folderOpen — classic PolinRider trigger" + fi + done + + # ── 3. .vscode/settings.json with allowAutomaticTasks ──────────────── + echo "--- Check 3: .vscode/settings.json allowAutomaticTasks ---" + find . -path '*/.vscode/settings.json' ! -path '*/.git/*' | while read -r f; do + if grep -q 'allowAutomaticTasks' "$f" && grep -q '"on"' "$f"; then + flag "$f" "task.allowAutomaticTasks is enabled — required to auto-run malicious VSCode tasks" + fi + done + + # ── 4. Suspicious font files ───────────────────────────────────────── + echo "--- Check 4: suspicious font files ---" + while IFS= read -r f; do + flag "$f" "Suspicious font file: matches known PolinRider payload filenames" + done < <( + find . ! -path '*/.git/*' \( \ + -name 'fa-solid-500.woff2' -o \ + -name 'fa-solid-600.eot' -o \ + -name 'fa-solid.lfk' -o \ + -name 'fa-solid-300.llf' -o \ + -name '*.lfk' -o \ + -name '*.llf' \ + \) 2>/dev/null + ) + + # ── 5. dist/setup.js ───────────────────────────────────────────────── + echo "--- Check 5: dist/setup.js ---" + find . -path '*/dist/setup.js' ! -path '*/.git/*' | while read -r f; do + flag "$f" "dist/setup.js present — PolinRider uses this as a dropper payload" + done + + # ── 6. package.json preinstall running dist/setup.js ───────────────── + echo "--- Check 6: package.json preinstall hook ---" + find . -name 'package.json' ! -path '*/.git/*' ! -path '*/node_modules/*' | while read -r f; do + if python3 -c " + import json, sys + try: + d = json.load(open('$f')) + pre = d.get('scripts', {}).get('preinstall', '') + sys.exit(0 if ('setup.js' in pre or 'dist/setup' in pre) else 1) + except: sys.exit(1) + " 2>/dev/null; then + flag "$f" "preinstall script references dist/setup.js — PolinRider runs payload on npm install" + fi + done + + # ── 7. api.js in repo root / src, plus start/dev prepended with it ─── + echo "--- Check 7: rogue api.js with hijacked dev/start script ---" + find . -maxdepth 3 -name 'api.js' ! -path '*/.git/*' ! -path '*/node_modules/*' | while read -r f; do + dir=$(dirname "$f") + pkg="$dir/package.json" + if [ -f "$pkg" ]; then + if python3 -c " + import json, sys + try: + d = json.load(open('$pkg')) + scripts = d.get('scripts', {}) + for key in ('dev', 'start'): + if scripts.get(key, '').startswith('node api.js'): + sys.exit(0) + sys.exit(1) + except: sys.exit(1) + " 2>/dev/null; then + flag "$f" "api.js present and dev/start script begins with 'node api.js' — PolinRider execution vector" + fi + fi + done + + # ── 8. Config files with trailing code after export default ────────── + echo "--- Check 8: config files with appended code after export ---" + for pattern in \ + 'postcss.config.*' 'tailwind.config.*' 'eslint.config.*' \ + 'next.config.*' 'jest.config.*' 'vite.config.*'; do + find . -name "$pattern" ! -path '*/.git/*' ! -path '*/node_modules/*' | while read -r f; do + # Get the byte offset of last export default / module.exports + last_export=$(grep -n 'export default\|module\.exports' "$f" | tail -1 | cut -d: -f1) + [ -z "$last_export" ] && continue + total=$(wc -l < "$f") + # Allow up to 10 lines after the last export statement (closing braces, etc.) + tail_lines=$(( total - last_export )) + if [ "$tail_lines" -gt 10 ]; then + # Check if the extra lines contain non-whitespace, non-comment content + extra=$(tail -n "$tail_lines" "$f" | grep -v '^\s*$' | grep -v '^\s*//' | grep -c '.') + if [ "$extra" -gt 3 ]; then + flag "$f" "Suspicious: $extra non-empty lines after 'export default' — PolinRider hides payload after config exports" + fi + fi + done + done + + # ── 9. Binary blobs disguised as font files ────────────────────────── + echo "--- Check 9: binary files with font extensions that are not fonts ---" + find . ! -path '*/.git/*' ! -path '*/node_modules/*' \ + \( -name '*.woff' -o -name '*.woff2' -o -name '*.eot' -o -name '*.ttf' \) \ + | while read -r f; do + # Real font files start with known magic bytes; node/JS starts with common ASCII + first_bytes=$(xxd -l 4 "$f" 2>/dev/null | awk '{print $2$3}' | head -1) + # wOFF = 774f4646, wOF2 = 774f4632, OTTO = 4f54544f, true = 00010000, ttcf = 74746366 + # If it starts with something else entirely (like 0a2f2f 0a76617 etc = JS/text), flag it + case "$first_bytes" in + 774f4646*|774f4632*|4f54544f*|00010000*|74746366*|000100*) ;; # valid font magic + *) flag "$f" "File has font extension but does not have valid font magic bytes — possible binary payload disguised as font" ;; + esac + done + + # ── Summary ────────────────────────────────────────────────────────── + echo "" + if [ "$FAIL" -eq 0 ]; then + echo "✅ No PolinRider malware indicators found." + else + echo "🚨 One or more malware indicators were detected. See annotations above." + exit 1 + fi