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.
This commit is contained in:
46
extensions/secret-scanner.ts
Normal file
46
extensions/secret-scanner.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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(", ")})` };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user