Skip to content

Malware Signature Scan #9

Malware Signature Scan

Malware Signature Scan #9

Workflow file for this run

name: Malware Signature Scan
on:
push:
pull_request:
schedule:
- cron: '0 6 * * *'
workflow_dispatch:
permissions:
contents: read
issues: write
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scan for malware signatures (A8-1662 / PolinRider incident)
id: scan
shell: bash
run: |
set +e
FOUND=0
SRC=( --include='*.js' --include='*.mjs' --include='*.cjs'
--include='*.ts' --include='*.mts' --include='*.cts'
--include='*.tsx' --include='*.jsx' --include='*.vue' --include='*.svelte'
--include='*.json' --include='*.html' --include='*.yml' --include='*.yaml' --include='*.sh' )
SKIP=( --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build
--exclude-dir=.git --exclude-dir=vendor --exclude-dir=coverage
--exclude='*.min.js' --exclude='*.min.css' --exclude='*.map'
--exclude='package-lock.json' --exclude='yarn.lock' --exclude='pnpm-lock.yaml'
--exclude='malware-scan.yml' )
rule() {
local label="$1" re="$2" out
out=$(grep -rnE "$re" "${SRC[@]}" "${SKIP[@]}" . 2>/dev/null)
echo "== $label =="
if [ -n "$out" ]; then
echo "$out" | cut -c1-200 | head -50
FOUND=1
fi
}
rule "1. known campaign markers" \
"A8-1662|A8-\*#new|global\['r'\] *= *require|global\.i *= *['\"]A8"
# Rule 2: obfuscator string-array dispatch. Two guards against false positives:
# the offset is 2-4 hex digits (a 6/8-digit literal is a colour, e.g.
# THREE.Color(0x1a0a14)), and the call must repeat many times in one file --
# real string-array dispatch appears dozens of times, a colour constant once.
echo "== 2. obfuscator string-array dispatch (repeated) =="
out=$(grep -roE "[A-Za-z_\$][A-Za-z0-9_\$]{2,}\(0x[0-9a-f]{2,4}\)" \
"${SRC[@]}" "${SKIP[@]}" . 2>/dev/null \
| awk -F: '{c[$1]++} END{for(f in c) if(c[f]>=8) printf "%s: %d dispatch-style calls\n", f, c[f]}')
if [ -n "$out" ]; then echo "$out" | head -50; FOUND=1; fi
# Rule 3: code pushed off-screen behind padding. Require what follows the
# padding to look executable (identifier then call or assignment) -- generated
# HTML such as Plotly's pads before markup like "<div id=", which is harmless.
rule "3. whitespace-padding disguise" \
" {120,}[A-Za-z_\$][A-Za-z0-9_\$]*[[:space:]]*[(=]"
# Rules 4a/4b replace the old ".{400,}" length rule. Length alone flags inline
# SVG, data: URIs, minified vendor bundles and JSON data -- all normal. What
# actually indicates a hidden payload is *encoding*, so look for that instead.
rule "4a. long run of hex/unicode escapes (encoded string)" \
"(\\\\x[0-9a-fA-F]{2}){8,}|(\\\\u[0-9a-fA-F]{4}){8,}"
# A base64 blob on its own is not suspicious -- Plotly stores chart data that
# way, and so do inline images and fonts. It matters only when something
# decodes and runs it, so require a decode/exec primitive around the blob.
rule "4b. base64 blob fed to a decode/exec primitive" \
"(atob|eval|Function)[[:space:]]*\([[:space:]]*['\"][A-Za-z0-9+/]{100,}|Buffer\.from\([[:space:]]*['\"][A-Za-z0-9+/]{100,}|(eval|Function)[[:space:]]*\([[:space:]]*(atob|decodeURIComponent|unescape)[[:space:]]*\("
echo "== 5. dangerous primitives in build/config files =="
CFG=$(git ls-files 2>/dev/null | grep -E '(^|/)([A-Za-z0-9._-]*\.config\.(js|cjs|mjs|ts)|\.eslintrc[^/]*)$')
if [ -n "$CFG" ]; then
out=$(echo "$CFG" | tr '\n' '\0' | xargs -0 -r grep -nE \
'\beval\(|new Function\(|child_process|createRequire|\batob\(|Buffer\.from\([^)]*base64' 2>/dev/null)
if [ -n "$out" ]; then echo "$out" | cut -c1-200; FOUND=1; fi
fi
echo "== 6. vscode auto-run triggers =="
if [ -f .vscode/tasks.json ] && grep -qE "folderOpen" .vscode/tasks.json; then
echo "VSCODE: tasks.json runs on folderOpen"; FOUND=1
fi
if [ -f .vscode/settings.json ] && grep -qE "allowAutomaticTasks" .vscode/settings.json; then
echo "VSCODE: allowAutomaticTasks set"; FOUND=1
fi
echo "== 7. disguised binaries (font/image files that are actually text/JS) =="
for f in $(git ls-files 2>/dev/null | grep -iE '\.(woff2?|ttf|otf|png|jpe?g|ico|gif|webp)$'); do
if file "$f" | grep -qiE 'text|javascript'; then echo "DISGUISED: $f"; FOUND=1; fi
done
echo "RESULT FOUND=$FOUND"
echo "found=$FOUND" >> "$GITHUB_OUTPUT"
exit 0
- name: Fail the check if a signature was found
if: steps.scan.outputs.found == '1'
run: |
echo "::error::Malware signature detected in this repository. See the scan step output above. Do not trust removal commit messages -- read file content directly."
exit 1
- name: Open an alert issue
if: failure() && steps.scan.outputs.found == '1'
uses: actions/github-script@v7
with:
script: |
const title = `Malware signature detected — ${context.sha.substring(0,7)}`;
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const body = [
`Automated scan found a malicious code signature (campaign A8-1662 / obfuscated payload / disguised binary) at commit ${context.sha}.`,
'',
`Run: ${runUrl}`,
'',
'Do not trust commit messages claiming removal -- in the original incident the commit titled "Remove malicious code injected into eslint.config.js" did not remove the payload, it swapped in a larger variant. Read file content directly at the current ref before concluding anything is clean.'
].join('\n');
const existing = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
const dup = existing.find(i => !i.pull_request && i.title.startsWith('Malware signature detected'));
if (!dup) {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body
});
} else {
core.info(`Alert issue already open: #${dup.number}`);
}