feat: add hidden unicode security scan
This commit is contained in:
parent
4499b860be
commit
9ec85dbcd2
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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")}`,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
45
src/index.ts
45
src/index.ts
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ export class FamiliarKnowledge {
|
|||
body,
|
||||
"",
|
||||
processed.contradictions.length > 0
|
||||
? `> ⚠️ Contradictions: ${processed.contradictions.join("; ")}`
|
||||
? `> ⚠ Contradictions: ${processed.contradictions.join("; ")}`
|
||||
: "",
|
||||
].filter(Boolean).join("\n");
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue