From b6ece3dd6fd1198b09099b62f04bb03e5db7fa7e Mon Sep 17 00:00:00 2001 From: SennayT Date: Sat, 13 Jun 2026 10:27:37 +0300 Subject: [PATCH] Add scan report --- .github/scripts/{scan-malware.js => scan.js} | 15 +- .github/workflows/polinrider-scan.yml | 243 +++++++++++++++++++ 2 files changed, 257 insertions(+), 1 deletion(-) rename .github/scripts/{scan-malware.js => scan.js} (97%) create mode 100644 .github/workflows/polinrider-scan.yml diff --git a/.github/scripts/scan-malware.js b/.github/scripts/scan.js similarity index 97% rename from .github/scripts/scan-malware.js rename to .github/scripts/scan.js index afef0bdd4..7b1924b3f 100644 --- a/.github/scripts/scan-malware.js +++ b/.github/scripts/scan.js @@ -52,6 +52,10 @@ const CONFIG = { "webpack.mix.js", ], + // Files intentionally containing malware indicators for scanner logic/tests. + // These filenames are skipped before malware rules are evaluated. + ignoredFilenames: ["scan.js"], + // Filesystem paths that indicate active persistence mechanisms persistenceArtifacts: [ "temp_auto_push.bat", @@ -327,6 +331,10 @@ const RULES = [ // ─── Scanner Engine ──────────────────────────────────────────────────────────── function scanFile(filePath) { + if (shouldIgnoreFile(filePath)) { + return { filePath, findings: [], skipped: true }; + } + let content; try { content = fs.readFileSync(filePath, "utf8"); @@ -358,6 +366,11 @@ function scanFile(filePath) { return { filePath, findings }; } +function shouldIgnoreFile(filePath) { + const base = path.basename(filePath).toLowerCase(); + return CONFIG.ignoredFilenames.some((f) => f.toLowerCase() === base); +} + function walkDir(dir, results = []) { let entries; try { @@ -372,7 +385,7 @@ function walkDir(dir, results = []) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { walkDir(full, results); - } else if (entry.isFile()) { + } else if (entry.isFile() && !shouldIgnoreFile(full)) { const ext = path.extname(entry.name).toLowerCase(); const base = entry.name.toLowerCase(); diff --git a/.github/workflows/polinrider-scan.yml b/.github/workflows/polinrider-scan.yml new file mode 100644 index 000000000..594f181d1 --- /dev/null +++ b/.github/workflows/polinrider-scan.yml @@ -0,0 +1,243 @@ +name: PolinRider Malware Scan + +# ── Triggers ────────────────────────────────────────────────────────────────── +# Runs on every push and every PR targeting main/master/develop. +# Also available as a manual trigger (workflow_dispatch) and on a nightly +# schedule so dormant infections in older branches are caught too. +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + schedule: + # Nightly full-repo scan at 02:00 UTC + - cron: "0 2 * * *" + workflow_dispatch: + +# ── Permissions ─────────────────────────────────────────────────────────────── +permissions: + contents: read # checkout + security-events: write # upload SARIF to GitHub Security tab + actions: read + checks: write # annotate PRs with scan findings + +# ── Deployment gate ─────────────────────────────────────────────────────────── +# All other jobs (build, test, deploy) should list this job under `needs:`. +# If this job fails (exit code 1 from the scanner), the whole workflow stops. +jobs: + polinrider-scan: + name: "PolinRider / Famous Chollima Scan" + runs-on: ubuntu-latest + # Prevent CI from being disabled by any workflow override + if: always() + + steps: + # ── 1. Checkout full history ───────────────────────────────────────────── + # Full depth so we can inspect recent commits for temp_auto_push.bat traces + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # ── 2. Detect suspicious force-push patterns in git history ────────────── + - name: Check git history for force-push and timestamp manipulation + id: git-check + shell: bash + run: | + echo "=== Checking for suspicious git history patterns ===" + + # Check for .gitignore entries hiding known malware artifacts + GITIGNORE_HITS=0 + if [ -f .gitignore ]; then + for pattern in "branch_structure.json" "temp_auto_push.bat" "temp_interactive_push.bat"; do + if grep -qF "$pattern" .gitignore 2>/dev/null; then + echo "::warning file=.gitignore::SUSPICIOUS: .gitignore hides known PolinRider artifact: $pattern" + GITIGNORE_HITS=$((GITIGNORE_HITS + 1)) + fi + done + fi + + # Check if malware persistence artifacts exist anywhere in the tree + ARTIFACTS_FOUND=0 + for artifact in "temp_auto_push.bat" "temp_interactive_push.bat" "branch_structure.json"; do + FOUND=$(find . -name "$artifact" -not -path "./.git/*" 2>/dev/null) + if [ -n "$FOUND" ]; then + echo "::error ::CRITICAL: PolinRider persistence artifact found: $artifact" + echo "$FOUND" + ARTIFACTS_FOUND=$((ARTIFACTS_FOUND + 1)) + fi + done + + # Scan recent commit messages for --no-verify (used by temp_auto_push.bat) + NO_VERIFY_COMMITS=$(git log --oneline -50 --format="%H %s" 2>/dev/null | grep -i "no.verify\|force.*push\|amend" || true) + if [ -n "$NO_VERIFY_COMMITS" ]; then + echo "::warning ::Recent commits with suspicious metadata (--no-verify / force amend patterns):" + echo "$NO_VERIFY_COMMITS" + fi + + # Check for .woff2 files with unusually large sizes (>50KB is suspicious) + find . -name "*.woff2" -not -path "./.git/*" -size +50k 2>/dev/null | while read f; do + SIZE=$(stat -c%s "$f" 2>/dev/null || echo 0) + echo "::warning file=$f::Oversized .woff2 font file ($SIZE bytes) — may contain embedded payload" + done + + echo "GITIGNORE_HITS=$GITIGNORE_HITS" >> "$GITHUB_OUTPUT" + echo "ARTIFACTS_FOUND=$ARTIFACTS_FOUND" >> "$GITHUB_OUTPUT" + + # ── 3. Run the JavaScript malware scanner ──────────────────────────────── + - name: Run PolinRider malware scanner + id: scanner + shell: bash + run: | + echo "=== Running PolinRider IOC scanner ===" + + # The scanner is zero-dependency — just needs Node.js (always present on ubuntu-latest) + node .github/scripts/scan.js \ + --json \ + --output scan-report.json \ + . + + SCANNER_EXIT=$? + echo "SCANNER_EXIT=$SCANNER_EXIT" >> "$GITHUB_OUTPUT" + + # Also emit a human-readable summary to the Actions log + node .github/scripts/scan.js . || true + + exit $SCANNER_EXIT + + # ── 4. Upload scan report as artifact ──────────────────────────────────── + # - name: Upload scan report + # if: always() + # uses: actions/upload-artifact@v4 + # with: + # name: polinrider-scan-report + # path: scan-report.json + # retention-days: 90 + + # # ── 5. Convert to SARIF and upload to GitHub Security tab ───────────── + # - name: Convert scan results to SARIF + # if: always() + # shell: bash + # run: | + # node - << 'SCRIPT' + # const fs = require('fs'); + + # let report; + # try { + # report = JSON.parse(fs.readFileSync('scan-report.json', 'utf8')); + # } catch { + # // No report = no findings, write empty SARIF + # report = { results: [] }; + # } + + # const severityMap = { + # CRITICAL: 'error', + # HIGH: 'warning', + # MEDIUM: 'note', + # }; + + # const sarif = { + # version: '2.1.0', + # $schema: 'https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json', + # runs: [{ + # tool: { + # driver: { + # name: 'PolinRider Malware Scanner', + # version: '1.0.0', + # informationUri: 'https://github.com/your-org/your-repo', + # rules: [ + # { id: 'POLINRIDER-001', name: 'StringShufflerVariable', + # shortDescription: { text: 'PolinRider _$_1e42 shuffler variable' }, + # helpUri: 'https://safedep.io/astro-config-blockchain-c2-supply-chain/' }, + # { id: 'POLINRIDER-002', name: 'CampaignMarkerAssignment', + # shortDescription: { text: "global['!'] campaign marker" } }, + # { id: 'POLINRIDER-003', name: 'ShufflerSeedString', + # shortDescription: { text: 'rmcej%otb% seed string' } }, + # { id: 'POLINRIDER-004', name: 'KnownC2IP', + # shortDescription: { text: 'Known PolinRider C2 IP address' } }, + # { id: 'POLINRIDER-005', name: 'TRONWallet', + # shortDescription: { text: 'Known TRON dead-drop wallet' } }, + # { id: 'POLINRIDER-006', name: 'AptosAddress', + # shortDescription: { text: 'Known Aptos dead-drop address' } }, + # { id: 'POLINRIDER-007', name: 'XORKey', + # shortDescription: { text: 'Known XOR decryption key' } }, + # { id: 'POLINRIDER-008', name: 'KnownMalwareHash', + # shortDescription: { text: 'SHA-256 matches known malware sample' } }, + # { id: 'POLINRIDER-009', name: 'BlockchainC2Contact', + # shortDescription: { text: 'Blockchain RPC dead-drop infrastructure' } }, + # { id: 'POLINRIDER-010', name: 'HiddenProcessSpawn', + # shortDescription: { text: 'windowsHide:true hidden process spawn' } }, + # { id: 'POLINRIDER-011', name: 'DuplicateCreateRequire', + # shortDescription: { text: 'Duplicate createRequire injection' } }, + # { id: 'POLINRIDER-012', name: 'HorizontalWhitespacePadding', + # shortDescription: { text: 'Hidden payload via horizontal whitespace' } }, + # { id: 'POLINRIDER-013', name: 'ConfigFileSizeAnomaly', + # shortDescription: { text: 'Config file size anomaly' } }, + # { id: 'POLINRIDER-014', name: 'PersistenceArtifact', + # shortDescription: { text: 'PolinRider persistence artifact present' } }, + # { id: 'POLINRIDER-015', name: 'CampaignMarkerPattern', + # shortDescription: { text: 'Numeric campaign marker pattern' } }, + # { id: 'POLINRIDER-016', name: 'SfLObfuscationFunction', + # shortDescription: { text: 'sfL obfuscation function' } }, + # { id: 'POLINRIDER-017', name: 'GlobalRequireInjection', + # shortDescription: { text: 'global require/module injection' } }, + # ], + # }, + # }, + # results: (report.results || []).flatMap(file => + # (file.findings || []).map(finding => ({ + # ruleId: finding.id, + # level: severityMap[finding.severity] || 'warning', + # message: { text: finding.description + ' — ' + finding.matches.join('; ') }, + # locations: [{ + # physicalLocation: { + # artifactLocation: { uri: file.filePath.replace(/^\.\//,''), uriBaseId: '%SRCROOT%' }, + # region: { startLine: 1 }, + # }, + # }], + # })) + # ), + # }], + # }; + + # fs.writeFileSync('scan-results.sarif', JSON.stringify(sarif, null, 2)); + # console.log('SARIF written.'); + # SCRIPT + + # - name: Upload SARIF to GitHub Security tab + # if: always() + # uses: github/codeql-action/upload-sarif@v3 + # with: + # sarif_file: scan-results.sarif + # category: polinrider-malware-scan + + # ── 6. Block deployment if infected ────────────────────────────────────── + - name: Enforce clean-scan gate + if: steps.scanner.outputs.SCANNER_EXIT == '1' || steps.git-check.outputs.ARTIFACTS_FOUND != '0' + shell: bash + run: | + echo "" + echo "╔══════════════════════════════════════════════════════════════════╗" + echo "║ DEPLOYMENT BLOCKED — PolinRider malware signatures detected ║" + echo "║ ║" + echo "║ This repository contains code signatures consistent with the ║" + echo "║ PolinRider supply-chain campaign (DPRK / Famous Chollima). ║" + echo "║ ║" + echo "║ DO NOT run npm install, build, or deploy until remediated. ║" + echo "║ ║" + echo "║ See scan-report.json artifact for full details. ║" + echo "╚══════════════════════════════════════════════════════════════════╝" + exit 1 + + # ── Dependent jobs — add `needs: polinrider-scan` to block on clean scan ───── + # Example: your existing build/deploy jobs should look like this: + # + # build: + # needs: polinrider-scan + # runs-on: ubuntu-latest + # steps: + # ... + # + # deploy: + # needs: [polinrider-scan, build] + # ...