Files
pi-kit/extensions/secret-scanner.ts
Aleksey Shakhmatov 7e50043984 feat(security): secret-scanner, commit-guard, llm-redaction, audit-log
- lib/secrets.ts: shared SECRET_PATTERNS + scanSecrets/redact (AWS/GitLab/GitHub/Slack tokens, private keys, JWT, generic credentials)
- lib/audit.ts: local JSONL audit (~/.config/pi-kit/audit.jsonl, 600), optional PI_KIT_AUDIT_URL POST; never logs secret values
- secret-scanner: block write/edit whose content matches a secret (complements protected-paths)
- commit-guard: enforce Conventional Commits on 'git commit -m' + scan message for secrets
- llm-redaction: redact secrets from outbound payload via before_provider_request
- audit-log: session_start + slash-command usage; wires company.json auditEndpoint into env
- config: auditEndpoint (null); package.json: npm test; test/guardrails.test.ts (16 cases, all pass)

Verified: all extensions load via pi (lib/ not loaded as extensions); npm test green.
2026-07-16 13:46:22 +03:00

47 lines
1.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Secret Scanner Extension
*
* WHAT: Blocks `write`/`edit` tool calls whose content looks like a secret
* (API keys, tokens, private keys, credentials). Complements
* protected-paths (which blocks by path) by scanning content.
*
* CONFIGURE: edit SECRET_PATTERNS in extensions/lib/secrets.ts.
*
* DISABLE: remove/rename this file from the package `extensions/` folder.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { scanSecrets } from "./lib/secrets.ts";
import { audit } from "./lib/audit.ts";
export default function secretScannerExtension(pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "write" && event.toolName !== "edit") return undefined;
// Scan every string field of the input except the path (content, new_string,
// etc.) so we don't depend on a specific tool's field naming.
const input = event.input as Record<string, unknown>;
const parts: string[] = [];
for (const [key, value] of Object.entries(input)) {
if (key === "path") continue;
if (typeof value === "string") parts.push(value);
}
const hits = scanSecrets(parts.join("\n"));
if (hits.length === 0) return undefined;
const path = typeof input.path === "string" ? input.path : "(unknown)";
audit({ kind: "guardrail_block", guardrail: "secret-scanner", tool: event.toolName, path, patterns: hits });
if (ctx.hasUI) {
const choice = await ctx.ui.select(
`⚠️ Похоже на секрет (${hits.join(", ")}) в записи → ${path}\nВсё равно записать?`,
["Нет", "Да"],
);
if (choice === "Да") return undefined;
}
return { block: true, reason: `Possible secret in content (${hits.join(", ")})` };
});
}