Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 124 additions & 17 deletions .github/workflows/malware-guard.yml
Original file line number Diff line number Diff line change
@@ -1,33 +1,140 @@
# Blocks the obfuscated-payload worm seen in Drips Wave repos (Sep-Oct 2026):
# code hidden after long runs of whitespace in config files, plus
# branch_structure.json / temp_*_push.bat entries added to .gitignore.
name: Malware guard

on:
pull_request:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
scan:
name: Malware guard
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Check out source without persisted credentials
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Scan for known payload signatures
- name: Inspect tracked Git objects without executing repository code
shell: bash
run: |
set -u
fail=0
check() {
hits=$(git grep -nIP "$1" -- "${@:2}" ':!.github/workflows/malware-guard.yml' ':!*lock*' ':!*.min.js' | cut -c1-200)
if [ -n "$hits" ]; then echo "::error::$1"; echo "$hits"; fail=1; fi
}
check '\S[ \t]{150,}\S|^[ \t]{150,}\S' '*.js' '*.cjs' '*.mjs' '*.ts' '*.jsx' '*.tsx'
check "global\.i\s*=\s*'|global\[['\"]!['\"]\]" '*.js' '*.cjs' '*.mjs' '*.ts' '*.jsx' '*.tsx'
check 'temp_auto_push|temp_interactive_push|branch_structure\.json' '.gitignore' '**/.gitignore'
check '"runOn"\s*:\s*"folderOpen"' '.vscode/*'
exit $fail
python3 -I - <<'PY'
"""Dependency-free checks for the Drips/Wave malware delivery patterns."""
import json
from pathlib import PurePosixPath
import re
import subprocess
import sys

MARKERS = (b'rmcej%otb%', b'Cot%3T=sthP', b'Cot%3t=shtP', b'_$__1e42', b'_$_1e42')
SUSPICIOUS = {'fa-solid-500.woff2', 'fa-solid-600.eot', 'fa-solid.lik',
'fa-solid-300.ttf', 'fa-solid-300.llf', 'branch_structure.json',
'temp_auto_push.bat', 'temp_interactive_push.bat'}
EXECUTABLE = {'.js', '.cjs', '.mjs', '.ts', '.jsx', '.tsx', '.ps1', '.sh', '.bat'}
FONTS = {'.woff', '.woff2', '.ttf', '.eot', '.otf', '.lik', '.llf'}

def check_file(name, data):
path = PurePosixPath(name.lower())
hits = []
if path.name in SUSPICIOUS or name.lower().endswith('dist/setup.js'):
hits.append('Known suspicious payload or propagation filename')
lowercase = data.lower()
if any(marker.lower() in lowercase for marker in MARKERS):
hits.append('Known obfuscated-loader marker')
text = data.decode('utf-8', 'replace')
if path.suffix in FONTS and re.search(r'\b(?:require\s*\(|global\s*[.\[]|child_process|function\s*\()', text):
hits.append('JavaScript stored in a font-like file')
if path.suffix in EXECUTABLE | FONTS:
for match in re.finditer(r'[ \t]{150,}', text):
if match.end() < len(text) and not text[match.end()].isspace():
hits.append('Code hidden after unusually long whitespace')
break
if re.search(r"global\s{0,64}(?:\.\s{0,64}i\s{0,64}=|\[\s{0,64}['\"](?:!|_V)['\"]\s{0,64}\])", text):
hits.append('Known injected-global loader pattern')
if path.name == 'tasks.json' and path.parent.name == '.vscode':
if re.search(r'"runOn"\s*:\s*"folderOpen"', text):
hits.append('VS Code task executes automatically on folder open')
if path.name == 'settings.json' and path.parent.name == '.vscode':
if re.search(r'"task\.allowAutomaticTasks"\s*:\s*"on"', text):
hits.append('VS Code automatic tasks explicitly enabled')
if name.startswith('.github/workflows/'):
if 'DORIS_PAT' in text or ('pull_request_target' in text and re.search(r'gh\s+pr\s+(?:merge|review)', text)):
hits.append('Unsafe privileged PR approval or merge automation')
if path.name == 'package.json':
try:
scripts = json.loads(text).get('scripts', {})
except (ValueError, AttributeError):
hits.append('Package manifest cannot be parsed for inspection')
scripts = {}
if not isinstance(scripts, dict):
hits.append('Package scripts must be an object')
scripts = {}
for script, command in scripts.items():
if not isinstance(command, str):
hits.append('Package command must be a string')
continue
if re.search(r'@p+enapitools/openapi-generator-cli', command):
hits.append(f'Misspelled OpenAPI package namespace: {script}')
if re.search(r'(?:dist[/\\]setup\.js|fa-solid-[^\s]+|\b(?:curl|wget)\b[^\n]*\|\s*(?:ba)?sh\b)', command):
hits.append(f'Suspicious package entry point: {script}')
if script in {'start', 'dev', 'prestart', 'predev'} and re.search(r'\bapi\.js\b', command):
hits.append(f'API loader entry point requires security review: {script}')
return hits

def scan_git(revision='HEAD'):
listing = subprocess.check_output(['git', 'ls-tree', '-r', '-z', revision])
objects = []
for record in listing.split(b'\0'):
if not record:
continue
info, raw_name = record.split(b'\t', 1)
mode, kind, oid = info.split()
if kind == b'blob':
name = raw_name.decode('utf-8', 'replace')
# This trusted inline scanner necessarily contains its own signatures.
if name != '.github/workflows/malware-guard.yml':
objects.append((name, oid))
proc = subprocess.Popen(['git', 'cat-file', '--batch'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
errors = []
try:
for name, oid in objects:
proc.stdin.write(oid + b'\n')
proc.stdin.flush()
header = proc.stdout.readline().split()
if len(header) != 3 or header[1] != b'blob':
raise RuntimeError('Git blob could not be inspected')
size = int(header[2])
data = proc.stdout.read(size)
if len(data) != size or proc.stdout.read(1) != b'\n':
raise RuntimeError('Incomplete Git blob read')
errors.extend(f'{name}: {hit}' for hit in check_file(name, data))
finally:
proc.stdin.close()
proc.stdout.close()
code = proc.wait()
if code:
raise RuntimeError('Git blob reader failed')
for error in errors:
# Do not echo untrusted file content or GitHub Actions command syntax.
print(json.dumps(error, ensure_ascii=True))
print(f'Inspected {len(objects)} tracked files; {len(errors)} malware-indicator findings.')
return 1 if errors else 0

def self_test():
for marker in MARKERS:
assert check_file('config.cjs', marker), 'Marker detection failed'
assert check_file('.vscode/tasks.json', b'{"runOn":"folderOpen"}')
assert check_file('.vscode/settings.json', b'{"task.allowAutomaticTasks":"on"}')
assert check_file('public/custom.woff2', b"require('child_process')")
assert check_file('package.json', b'{"scripts":{"preinstall":"node dist/setup.js"}}')
assert check_file('config.js', b'module.exports={};' + b' '*200 + b'global.i="x"')
assert not check_file('package.json', b'{"scripts":{"dev":"next dev"}}')
assert not check_file('config.js', b'module.exports = {};\n')

if __name__ == '__main__':
self_test()
sys.exit(scan_git())
PY
Loading