Repository navigation
ci: add extended security scan workflow #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |