-
Notifications
You must be signed in to change notification settings - Fork 0
139 lines (122 loc) · 6.72 KB
/
Copy pathmalware-scan.yml
File metadata and controls
139 lines (122 loc) · 6.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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}`);
}