From 9ec85dbcd247623ab3c5e1c5b3c9b3d046fb8c04 Mon Sep 17 00:00:00 2001 From: artale Date: Mon, 15 Jun 2026 04:40:50 +0200 Subject: [PATCH] feat: add hidden unicode security scan --- COMMANDS.md | 5 ++++ src/core/unicode-safety.test.ts | 3 +- src/core/unicode-safety.ts | 8 ++++++ src/index.ts | 45 ++++++++++++++++++++++++++++++ src/upgrades/familiar-knowledge.ts | 2 +- 5 files changed, 61 insertions(+), 2 deletions(-) diff --git a/COMMANDS.md b/COMMANDS.md index 9f7c4b2..b27890a 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -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 ` + ### `pai` - `pai status` diff --git a/src/core/unicode-safety.test.ts b/src/core/unicode-safety.test.ts index d2b3a7e..6581144 100644 --- a/src/core/unicode-safety.test.ts +++ b/src/core/unicode-safety.test.ts @@ -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"]); }); }); diff --git a/src/core/unicode-safety.ts b/src/core/unicode-safety.ts index bb5299d..6b6fbc3 100644 --- a/src/core/unicode-safety.ts +++ b/src/core/unicode-safety.ts @@ -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")}`, + })); +} diff --git a/src/index.ts b/src/index.ts index 2470070..0910fb1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2566,6 +2566,51 @@ plinius } }); + +// ── Security ──────────────────────────────────────────────── + +const security = program + .command("security") + .description("Security scanners"); + +security + .command("scan ") + .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 diff --git a/src/upgrades/familiar-knowledge.ts b/src/upgrades/familiar-knowledge.ts index e810798..32c451a 100644 --- a/src/upgrades/familiar-knowledge.ts +++ b/src/upgrades/familiar-knowledge.ts @@ -154,7 +154,7 @@ export class FamiliarKnowledge { body, "", processed.contradictions.length > 0 - ? `> ⚠️ Contradictions: ${processed.contradictions.join("; ")}` + ? `> ⚠ Contradictions: ${processed.contradictions.join("; ")}` : "", ].filter(Boolean).join("\n");