feat: add hidden unicode security scan

This commit is contained in:
artale 2026-06-15 04:40:50 +02:00
parent 4499b860be
commit 9ec85dbcd2
5 changed files with 61 additions and 2 deletions

View File

@ -26,6 +26,7 @@ fable-agent plinius godmode "improve explanation quality"
| `session` | Session lifecycle |
| `skills` | Skill registry ops |
| `state` | Knowledge state / stats view |
| `security` | Security scanners |
| `pai` | PAI/bridge operations |
| `status` | Runtime surface summary |
| `workflow` | Load workflow JSON |
@ -85,6 +86,10 @@ fable-agent plinius godmode "improve explanation quality"
- `status`
- Displays command availability, env bootstrap, and runtime wiring summary.
### `security`
- `security scan <target>`
### `pai`
- `pai status`

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { hasHiddenUnicode, stripHiddenUnicode } from "./unicode-safety.js";
import { findHiddenUnicode, hasHiddenUnicode, stripHiddenUnicode } from "./unicode-safety.js";
describe("unicode safety", () => {
it("strips hidden instruction carriers", () => {
@ -8,5 +8,6 @@ describe("unicode safety", () => {
expect(hasHiddenUnicode(text)).toBe(true);
expect(stripHiddenUnicode(text)).toBe("safetext");
expect(findHiddenUnicode(text).map((hit) => hit.codePoint)).toEqual(["U+E0069", "U+200B", "U+202E"]);
});
});

View File

@ -9,3 +9,11 @@ export function hasHiddenUnicode(text: string): boolean {
HIDDEN_UNICODE.lastIndex = 0;
return HIDDEN_UNICODE.test(text);
}
export function findHiddenUnicode(text: string): { index: number; codePoint: string }[] {
HIDDEN_UNICODE.lastIndex = 0;
return [...text.matchAll(HIDDEN_UNICODE)].map((match) => ({
index: match.index,
codePoint: `U+${match[0].codePointAt(0)!.toString(16).toUpperCase().padStart(4, "0")}`,
}));
}

View File

@ -2566,6 +2566,51 @@ plinius
}
});
// ── Security ────────────────────────────────────────────────
const security = program
.command("security")
.description("Security scanners");
security
.command("scan <target>")
.description("Scan files for hidden Unicode prompt-injection carriers")
.action(async (target: string) => {
const { findHiddenUnicode } = await import("./core/unicode-safety.js");
const root = path.resolve(target);
const files: string[] = [];
const walk = (file: string) => {
const st = fs.statSync(file);
if (st.isDirectory()) {
for (const name of fs.readdirSync(file)) {
if ([".git", "node_modules", "dist"].includes(name)) continue;
walk(path.join(file, name));
}
return;
}
if (/\.(md|txt|ts|js|json|yaml|yml)$/i.test(file)) files.push(file);
};
walk(root);
let hits = 0;
for (const file of files) {
const found = findHiddenUnicode(fs.readFileSync(file, "utf-8"));
if (found.length === 0) continue;
hits += found.length;
console.log(`${file}: ${found.map((h) => `${h.codePoint}@${h.index}`).join(", ")}`);
}
if (hits > 0) {
console.error(`
Hidden Unicode found: ${hits}`);
process.exit(1);
}
console.log(`
No hidden Unicode found in ${files.length} file(s)`);
});
program.parse(process.argv);
// Show help if no args

View File

@ -154,7 +154,7 @@ export class FamiliarKnowledge {
body,
"",
processed.contradictions.length > 0
? `> ⚠ Contradictions: ${processed.contradictions.join("; ")}`
? `> ⚠ Contradictions: ${processed.contradictions.join("; ")}`
: "",
].filter(Boolean).join("\n");