@raishin/vanguard-frontier-agentic 3.14.0 → 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +1 -1
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/codex.toml +1 -1
- package/agents/dotnet/dotnet-aspire-cloud-native-review-agent/harnesses/codex.toml +1 -1
- package/agents/dotnet/dotnet-aspnetcore-api-review-agent/harnesses/codex.toml +1 -1
- package/agents/dotnet/dotnet-aspnetcore-identity-authz-review-agent/harnesses/codex.toml +1 -1
- package/agents/dotnet/dotnet-csharp-runtime-review-agent/harnesses/codex.toml +1 -1
- package/agents/dotnet/dotnet-efcore-data-access-review-agent/harnesses/codex.toml +1 -1
- package/agents/dotnet/dotnet-maestro-agent/harnesses/codex.toml +1 -1
- package/agents/dotnet/dotnet-observability-otel-review-agent/harnesses/codex.toml +1 -1
- package/agents/dotnet/dotnet-performance-aot-review-agent/harnesses/codex.toml +1 -1
- package/agents/dotnet/dotnet-supply-chain-review-agent/harnesses/codex.toml +1 -1
- package/agents/dotnet/dotnet-testing-quality-review-agent/harnesses/codex.toml +1 -1
- package/agents/frontend/nextjs-specialist-agent/harnesses/codex.toml +1 -1
- package/agents/frontend/svelte-sveltekit-specialist-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-analytics-people-data-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-benefits-payroll-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-compensation-equity-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-culture-dei-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-employee-relations-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-hris-process-controls-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-learning-policy-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-leave-accommodation-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-maestro-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-performance-management-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-recruiting-selection-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-risk-triage-review-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-termination-readiness-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-workforce-planning-rif-agent/harnesses/codex.toml +1 -1
- package/agents/hr/hr-workplace-investigations-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-application-server-exit-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-concurrency-and-virtual-thread-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-container-and-kubernetes-readiness-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-database-migration-safety-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-deserialization-and-parser-security-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-framework-production-readiness-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-jdk-lifecycle-and-upgrade-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-jpa-hibernate-performance-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-jvm-performance-and-gc-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-kafka-reliability-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-maestro-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-resilience-pattern-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-spring-security-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-test-architecture-agent/harnesses/codex.toml +1 -1
- package/agents/java/java-transaction-and-consistency-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-contract-review-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-counsel-review-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-employment-law-risk-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-ethics-investigations-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-ip-open-source-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-knowledge-management-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-litigation-discovery-hold-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-maestro-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-policy-governance-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-privacy-data-protection-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-public-disclosure-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-regulatory-compliance-agent/harnesses/codex.toml +1 -1
- package/agents/legal/legal-vendor-procurement-risk-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-administrator-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-ai-connector-mcp-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-ai-foundations-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-application-developer-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-audit-controls-sox-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-bi-reporting-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-data-governance-privacy-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-enterprise-architecture-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-erp-consultant-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-evidence-release-drift-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-financial-foundations-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-identity-access-role-permission-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-integration-migration-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-live-org-mutation-guard-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-maestro-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-oneworld-multisubsidiary-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-sandbox-nonproduction-governance-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-saved-searches-workbook-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-sdf-devops-release-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-sso-oauth-tba-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-suitecloud-developer-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-suiteflow-automation-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-suitefoundation-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-suitescript-secure-code-review-agent/harnesses/codex.toml +1 -1
- package/agents/netsuite/netsuite-web-services-integration-agent/harnesses/codex.toml +1 -1
- package/agents/qa/ci-test-pipeline-review-agent/harnesses/codex.toml +1 -1
- package/agents/qa/helm-chart-quality-review-agent/harnesses/codex.toml +1 -1
- package/agents/qa/kubernetes-manifest-quality-review-agent/harnesses/codex.toml +1 -1
- package/agents/qa/llm-ai-pipeline-test-review-agent/harnesses/codex.toml +1 -1
- package/agents/qa/playwright-e2e-suite-review-agent/harnesses/codex.toml +1 -1
- package/agents/qa/plc-control-logic-safety-review-agent/harnesses/codex.toml +1 -1
- package/agents/qa/rpa-workflow-resilience-review-agent/harnesses/codex.toml +1 -1
- package/agents/qa/test-coverage-quality-review-agent/harnesses/codex.toml +1 -1
- package/agents/qa/test-flakiness-triage-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-adaptive-access-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-agentforce-ai-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-analytics-tableau-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-app-builder-automation-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-business-analyst-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-certificate-lifecycle-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-change-impact-analyst-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-code-analyzer-orchestrator-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-compliance-privacy-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-continuous-verification-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-data-architecture-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-development-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-devops-release-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-enterprise-architect-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-experience-cloud-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-hyperforce-security-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-industry-cloud-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-integration-mulesoft-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-live-guard-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-maestro-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-marketing-cloud-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-network-policy-architect-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-platform-admin-review-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-sales-cloud-revenue-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-sandbox-governance-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-sandbox-isolation-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-security-identity-access-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-service-field-service-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-session-governance-agent/harnesses/codex.toml +1 -1
- package/agents/salesforce/salesforce-slack-collaboration-agent/harnesses/codex.toml +1 -1
- package/catalog/AGENTS.md +2 -1
- package/catalog/asset-integrity.json +723 -276
- package/catalog/index.json +3 -2
- package/catalog/model-assignments.json +121 -121
- package/catalog/model-policy.json +9 -9
- package/catalog/model-registry.json +28 -13
- package/catalog/mods.json +131 -0
- package/mods/README.md +220 -0
- package/mods/audit-trail/README.md +70 -0
- package/mods/audit-trail/bin/hook.mjs +65 -0
- package/mods/audit-trail/claude-code/UNSUPPORTED.md +11 -0
- package/mods/audit-trail/claude-code/settings.hooks.json +14 -0
- package/mods/audit-trail/codex/hooks.json +14 -0
- package/mods/audit-trail/kiro/UNSUPPORTED.md +7 -0
- package/mods/audit-trail/kiro/audit-trail-post-tool-use-hook.json +14 -0
- package/mods/audit-trail/lib/common.mjs +213 -0
- package/mods/audit-trail/lib/core.mjs +50 -0
- package/mods/audit-trail/lib/runtime.mjs +140 -0
- package/mods/audit-trail/mod.toml +37 -0
- package/mods/blast-radius-guard/.claude-plugin/plugin.json +5 -0
- package/mods/blast-radius-guard/README.md +53 -0
- package/mods/blast-radius-guard/bin/hook.mjs +18 -0
- package/mods/blast-radius-guard/claude-code/settings.hooks.json +14 -0
- package/mods/blast-radius-guard/codex/UNSUPPORTED.md +7 -0
- package/mods/blast-radius-guard/codex/hooks.json +14 -0
- package/mods/blast-radius-guard/hooks/hooks.json +5 -0
- package/mods/blast-radius-guard/hooks/register.test.ts +97 -0
- package/mods/blast-radius-guard/hooks/register.ts +13 -0
- package/mods/blast-radius-guard/kiro/UNSUPPORTED.md +7 -0
- package/mods/blast-radius-guard/kiro/blast-radius-guard-pre-tool-use-hook.json +14 -0
- package/mods/blast-radius-guard/lib/common.mjs +213 -0
- package/mods/blast-radius-guard/lib/core.mjs +66 -0
- package/mods/blast-radius-guard/lib/patterns.mjs +102 -0
- package/mods/blast-radius-guard/lib/runtime.mjs +140 -0
- package/mods/blast-radius-guard/mod.toml +34 -0
- package/mods/blast-radius-guard/patterns.json +101 -0
- package/mods/context-burn/.claude-plugin/plugin.json +5 -0
- package/mods/context-burn/README.md +66 -0
- package/mods/context-burn/bin/hook.mjs +38 -0
- package/mods/context-burn/claude-code/UNSUPPORTED.md +7 -0
- package/mods/context-burn/claude-code/settings.hooks.json +24 -0
- package/mods/context-burn/codex/UNSUPPORTED.md +7 -0
- package/mods/context-burn/codex/hooks.json +24 -0
- package/mods/context-burn/hooks/hooks.json +5 -0
- package/mods/context-burn/hooks/register.test.ts +151 -0
- package/mods/context-burn/hooks/register.ts +35 -0
- package/mods/context-burn/kiro/UNSUPPORTED.md +7 -0
- package/mods/context-burn/kiro/context-burn-post-tool-use-hook.json +14 -0
- package/mods/context-burn/kiro/context-burn-session-start-hook.json +14 -0
- package/mods/context-burn/lib/common.mjs +213 -0
- package/mods/context-burn/lib/core.mjs +79 -0
- package/mods/context-burn/lib/runtime.mjs +140 -0
- package/mods/context-burn/mod.toml +37 -0
- package/mods/live-guard-gate/.claude-plugin/plugin.json +5 -0
- package/mods/live-guard-gate/README.md +43 -0
- package/mods/live-guard-gate/bin/hook.mjs +12 -0
- package/mods/live-guard-gate/claude-code/settings.hooks.json +14 -0
- package/mods/live-guard-gate/codex/UNSUPPORTED.md +7 -0
- package/mods/live-guard-gate/codex/hooks.json +14 -0
- package/mods/live-guard-gate/hooks/hooks.json +5 -0
- package/mods/live-guard-gate/hooks/register.test.ts +59 -0
- package/mods/live-guard-gate/hooks/register.ts +11 -0
- package/mods/live-guard-gate/kiro/UNSUPPORTED.md +7 -0
- package/mods/live-guard-gate/kiro/live-guard-gate-pre-tool-use-hook.json +14 -0
- package/mods/live-guard-gate/lib/common.mjs +213 -0
- package/mods/live-guard-gate/lib/core.mjs +24 -0
- package/mods/live-guard-gate/lib/live-agents.mjs +65 -0
- package/mods/live-guard-gate/lib/patterns.mjs +523 -0
- package/mods/live-guard-gate/lib/runtime.mjs +140 -0
- package/mods/live-guard-gate/mod.toml +34 -0
- package/mods/live-guard-gate/patterns.json +522 -0
- package/mods/secret-redactor/.claude-plugin/plugin.json +5 -0
- package/mods/secret-redactor/README.md +86 -0
- package/mods/secret-redactor/bin/hook.mjs +35 -0
- package/mods/secret-redactor/claude-code/UNSUPPORTED.md +7 -0
- package/mods/secret-redactor/claude-code/settings.hooks.json +24 -0
- package/mods/secret-redactor/codex/UNSUPPORTED.md +11 -0
- package/mods/secret-redactor/codex/hooks.json +14 -0
- package/mods/secret-redactor/hooks/hooks.json +5 -0
- package/mods/secret-redactor/hooks/register.test.ts +150 -0
- package/mods/secret-redactor/hooks/register.ts +19 -0
- package/mods/secret-redactor/kiro/UNSUPPORTED.md +11 -0
- package/mods/secret-redactor/kiro/secret-redactor-pre-tool-use-hook.json +14 -0
- package/mods/secret-redactor/lib/common.mjs +213 -0
- package/mods/secret-redactor/lib/core.mjs +61 -0
- package/mods/secret-redactor/lib/patterns.mjs +75 -0
- package/mods/secret-redactor/lib/runtime.mjs +140 -0
- package/mods/secret-redactor/mod.toml +43 -0
- package/mods/secret-redactor/patterns.json +74 -0
- package/package.json +9 -5
- package/plugins/vanguard-frontier-agentic/.codex-plugin/plugin.json +1 -1
- package/scripts/generate-mods.mjs +784 -0
- package/scripts/mods/common.mjs +213 -0
- package/scripts/mods/runtime.mjs +140 -0
- package/tests/fixtures/README.md +1 -1
- package/tests/fuzz-properties.test.mjs +254 -0
- package/tests/mods.test.mjs +768 -0
- package/tests/validate-asset-integrity.py +1 -0
- package/tests/validate-mods.py +821 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Command-hook entry for classic Claude Code (PreToolUse, PostToolUse), Kiro and Codex
|
|
2
|
+
// (PreToolUse only: neither can replace tool output).
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { maskValue } from '../lib/common.mjs'
|
|
5
|
+
import { WITHHELD, checkInput, loadPatterns, logLines, redactOutput } from '../lib/core.mjs'
|
|
6
|
+
import { runHook } from '../lib/runtime.mjs'
|
|
7
|
+
|
|
8
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
9
|
+
|
|
10
|
+
function patterns() {
|
|
11
|
+
const file = process.env.VANGUARD_SECRET_REDACTOR_PATTERNS
|
|
12
|
+
return loadPatterns(file ? readFileSync(file, 'utf8') : undefined, file)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Kiro documents no stdin field names: without a tool_input object, scan the whole payload.
|
|
16
|
+
const PreToolUse = (payload) => checkInput(isObject(payload.tool_input) ? payload.tool_input : payload, patterns())
|
|
17
|
+
|
|
18
|
+
const PostToolUse = (payload, { event, harness }) => {
|
|
19
|
+
if (harness !== 'claude-code') throw new Error(`PostToolUse is unsupported on ${harness}`)
|
|
20
|
+
const { value, hits } = redactOutput(payload.tool_response, patterns())
|
|
21
|
+
if (hits.length === 0) return undefined
|
|
22
|
+
return { decision: 'replace', output: value, log: logLines(hits, payload.tool_name, event) }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// No error text in either verdict: a parse error can quote the payload it failed on.
|
|
26
|
+
const onError = (err, payload, { event }) =>
|
|
27
|
+
event === 'PreToolUse'
|
|
28
|
+
? { decision: 'deny', reason: 'secret-redactor: check failed; denied' }
|
|
29
|
+
: {
|
|
30
|
+
decision: 'replace',
|
|
31
|
+
output: maskValue(payload?.tool_response ?? WITHHELD, WITHHELD),
|
|
32
|
+
log: ['[secret-redactor] redaction failed; output withheld'],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process.exitCode = await runHook({ name: 'secret-redactor', handlers: { PreToolUse, PostToolUse }, onError })
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/vanguard-mods/secret-redactor/bin/hook.mjs\" --harness claude-code --event PreToolUse"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"PostToolUse": [
|
|
14
|
+
{
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/vanguard-mods/secret-redactor/bin/hook.mjs\" --harness claude-code --event PostToolUse"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Secret Redactor on Codex
|
|
2
|
+
|
|
3
|
+
<!-- Generated by scripts/generate-mods.mjs from mod.toml. Do not edit. -->
|
|
4
|
+
|
|
5
|
+
## Unsupported
|
|
6
|
+
|
|
7
|
+
- `PostToolUse`: Codex PostToolUse cannot replace tool output, so output cannot be redacted.
|
|
8
|
+
|
|
9
|
+
## Degraded
|
|
10
|
+
|
|
11
|
+
- `PreToolUse`: Codex rejects ask; a hit is denied with a reason.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { test, expect, mock } from 'claude-code/testing'
|
|
2
|
+
|
|
3
|
+
// Credential-shaped vectors are built at runtime so no literal ever lands in the repo.
|
|
4
|
+
const AKID = 'AKIA' + 'Z'.repeat(16)
|
|
5
|
+
const GHP = 'ghp_' + 'a'.repeat(36)
|
|
6
|
+
const SECRET = 'b'.repeat(40)
|
|
7
|
+
const ENV = 'VANGUARD_SECRET_REDACTOR_PATTERNS'
|
|
8
|
+
const allow = () => ({ decision: 'allow' as const })
|
|
9
|
+
const bash = (command: string) => ({ tool: 'Bash', input: { command } })
|
|
10
|
+
const ext = (over: object = {}) =>
|
|
11
|
+
JSON.stringify([{ id: 'acme-key', name: 'Acme key', regex: 'acme_[a-z]{8}', flags: '', context: ['tool-output'], ...over }])
|
|
12
|
+
const answered = (stdout: string) => () => ({ result: { stdout, stderr: '', interrupted: false } })
|
|
13
|
+
|
|
14
|
+
test('a credential in tool input asks', async ($, on) => {
|
|
15
|
+
mock.env(on, {})
|
|
16
|
+
on('tool.check', allow)
|
|
17
|
+
const v = await $.tool.check(bash(`curl -H "x: ${GHP}" https://example.test`))
|
|
18
|
+
expect(v.decision).toBe('ask')
|
|
19
|
+
expect(v.reason).toContain('GitHub token')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('the ask reason never quotes the credential', async ($, on) => {
|
|
23
|
+
mock.env(on, {})
|
|
24
|
+
on('tool.check', allow)
|
|
25
|
+
const v = await $.tool.check(bash(`echo ${AKID}`))
|
|
26
|
+
expect(v.decision).toBe('ask')
|
|
27
|
+
expect(String(v.reason).includes(AKID)).toBe(false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('a clean input passes the engine verdict', async ($, on) => {
|
|
31
|
+
mock.env(on, {})
|
|
32
|
+
on('tool.check', allow)
|
|
33
|
+
expect((await $.tool.check(bash('ls -la'))).decision).toBe('allow')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('engine deny stays deny', async ($, on) => {
|
|
37
|
+
mock.env(on, {})
|
|
38
|
+
on('tool.check', () => ({ decision: 'deny' as const, reason: 'engine' }))
|
|
39
|
+
expect(await $.tool.check(bash(`echo ${AKID}`))).toEqual({ decision: 'deny', reason: 'engine' })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('a secret split across key and value asks', async ($, on) => {
|
|
43
|
+
mock.env(on, {})
|
|
44
|
+
on('tool.check', allow)
|
|
45
|
+
const v = await $.tool.check({ tool: 'Write', input: { file_path: 'x.json', aws_secret_access_key: SECRET } })
|
|
46
|
+
expect(v.decision).toBe('ask')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('an extension pattern with tool-input context asks', async ($, on) => {
|
|
50
|
+
mock.env(on, { [ENV]: 'ext.json' })
|
|
51
|
+
on('fs.read', () => ({ value: ext({ context: ['tool-input'] }) }))
|
|
52
|
+
on('tool.check', allow)
|
|
53
|
+
expect((await $.tool.check(bash('echo acme_abcdefgh'))).decision).toBe('ask')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('an anchored extension asks for object and argv input leaves', async ($, on) => {
|
|
57
|
+
mock.env(on, { [ENV]: 'ext.json' })
|
|
58
|
+
on('fs.read', () => ({ value: ext({ regex: '^acme_[a-z]{8}$', context: ['tool-input'] }) }))
|
|
59
|
+
on('tool.check', allow)
|
|
60
|
+
for (const input of [{ command: 'acme_abcdefgh' }, { argv: ['prefix', 'acme_abcdefgh'] }]) {
|
|
61
|
+
expect((await $.tool.check({ tool: 'Bash', input })).decision).toBe('ask')
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('an unreadable extension file denies', async ($, on) => {
|
|
66
|
+
mock.env(on, { [ENV]: 'missing.json' })
|
|
67
|
+
on('fs.read', () => {
|
|
68
|
+
throw new Error('ENOENT')
|
|
69
|
+
})
|
|
70
|
+
on('tool.check', allow)
|
|
71
|
+
expect((await $.tool.check(bash('ls'))).decision).toBe('deny')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('an extension reusing a built-in id denies', async ($, on) => {
|
|
75
|
+
mock.env(on, { [ENV]: 'ext.json' })
|
|
76
|
+
on('fs.read', () => ({ value: ext({ id: 'github-pat' }) }))
|
|
77
|
+
on('tool.check', allow)
|
|
78
|
+
expect((await $.tool.check(bash('ls'))).decision).toBe('deny')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('an extension with an unknown context denies', async ($, on) => {
|
|
82
|
+
mock.env(on, { [ENV]: 'ext.json' })
|
|
83
|
+
on('fs.read', () => ({ value: ext({ context: ['bash'] }) }))
|
|
84
|
+
on('tool.check', allow)
|
|
85
|
+
expect((await $.tool.check(bash('ls'))).decision).toBe('deny')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('a credential in tool output is redacted', async ($, on) => {
|
|
89
|
+
mock.env(on, {})
|
|
90
|
+
on('tool.call', answered(`key=${AKID}\n`))
|
|
91
|
+
const r = await $.tool.call({ tool: 'Bash', command: 'cat creds' })
|
|
92
|
+
expect(r).toEqual({ result: { stdout: 'key=[REDACTED:aws-access-key]\n', stderr: '', interrupted: false } })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('a clean result passes through unchanged', async ($, on) => {
|
|
96
|
+
mock.env(on, {})
|
|
97
|
+
on('tool.call', () => ({ result: { stdout: 'hello', stderr: '', interrupted: false }, context: ['note'] }))
|
|
98
|
+
const r = await $.tool.call({ tool: 'Bash', command: 'echo hello' })
|
|
99
|
+
expect(r).toEqual({ result: { stdout: 'hello', stderr: '', interrupted: false }, context: ['note'] })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('a credential in context withholds the result with the redacted context', async ($, on) => {
|
|
103
|
+
mock.env(on, {})
|
|
104
|
+
on('tool.call', () => ({ result: 'ok', context: [`token ${GHP}`] }))
|
|
105
|
+
const r = await $.tool.call({ tool: 'Bash', command: 'true' })
|
|
106
|
+
expect(r).toEqual({ deny: 'secret-redactor: credentials redacted from this result: ok\ntoken [REDACTED:github-pat]' })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('a redacted result keeps the context entries from below', async ($, on) => {
|
|
110
|
+
mock.env(on, {})
|
|
111
|
+
on('tool.call', () => ({ result: { stdout: AKID, stderr: '', interrupted: false }, context: ['note'] }))
|
|
112
|
+
const r = await $.tool.call({ tool: 'Bash', command: 'x' })
|
|
113
|
+
expect(r).toEqual({ result: { stdout: '[REDACTED:aws-access-key]', stderr: '', interrupted: false }, context: ['note'] })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('an errored result with a credential is withheld with the redacted text', async ($, on) => {
|
|
117
|
+
mock.env(on, {})
|
|
118
|
+
on('tool.call', () => ({ isError: true as const, result: `failed: ${GHP}`, text: `failed: ${GHP}` }))
|
|
119
|
+
const r = await $.tool.call({ tool: 'Bash', command: 'false' })
|
|
120
|
+
expect(r).toEqual({ deny: 'secret-redactor: credentials redacted from this result: failed: [REDACTED:github-pat]' })
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('a deny from below is redacted', async ($, on) => {
|
|
124
|
+
mock.env(on, {})
|
|
125
|
+
on('tool.call', () => ({ deny: `refused ${AKID}` }))
|
|
126
|
+
expect(await $.tool.call({ tool: 'Bash', command: 'x' })).toEqual({ deny: 'refused [REDACTED:aws-access-key]' })
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('an extension output pattern redacts', async ($, on) => {
|
|
130
|
+
mock.env(on, { [ENV]: 'ext.json' })
|
|
131
|
+
on('fs.read', () => ({ value: ext() }))
|
|
132
|
+
on('tool.call', answered('acme_abcdefgh'))
|
|
133
|
+
const r = await $.tool.call({ tool: 'Bash', command: 'x' })
|
|
134
|
+
expect(r).toEqual({ result: { stdout: '[REDACTED:acme-key]', stderr: '', interrupted: false } })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('an anchored extension redacts structured native output', async ($, on) => {
|
|
138
|
+
mock.env(on, { [ENV]: 'ext.json' })
|
|
139
|
+
on('fs.read', () => ({ value: ext({ regex: '^acme_[a-z]{8}$' }) }))
|
|
140
|
+
on('tool.call', answered('acme_abcdefgh'))
|
|
141
|
+
expect(await $.tool.call({ tool: 'Bash', command: 'x' })).toEqual({ result: { stdout: '[REDACTED:acme-key]', stderr: '', interrupted: false } })
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('a bad extension withholds the output', async ($, on) => {
|
|
145
|
+
mock.env(on, { [ENV]: 'ext.json' })
|
|
146
|
+
on('fs.read', () => ({ value: 'not json' }))
|
|
147
|
+
on('tool.call', answered(`key=${AKID}`))
|
|
148
|
+
const r = await $.tool.call({ tool: 'Bash', command: 'x' })
|
|
149
|
+
expect(r).toEqual({ deny: '[secret-redactor: output withheld; redaction failed]' })
|
|
150
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EngineInterface, Register } from 'claude-code'
|
|
2
|
+
import { WITHHELD, checkInput, loadPatterns, redactResult } from '../lib/core.mjs'
|
|
3
|
+
|
|
4
|
+
// Top level, not inside register: `claude plugin validate` refuses a $-taking helper there.
|
|
5
|
+
async function patterns($: EngineInterface) {
|
|
6
|
+
const file = await $.env.get('VANGUARD_SECRET_REDACTOR_PATTERNS')
|
|
7
|
+
return loadPatterns(file ? await $.fs.read(file) : undefined, file)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const register: Register = (on) => {
|
|
11
|
+
on('tool.check', async ($, e, next) => {
|
|
12
|
+
const v = await next(e)
|
|
13
|
+
if (v.decision === 'deny') return v
|
|
14
|
+
return checkInput(e.input, await patterns($)) ?? v
|
|
15
|
+
}).catch(() => ({ decision: 'deny', reason: 'secret-redactor: check failed; denied' }))
|
|
16
|
+
|
|
17
|
+
// The tool has already run when redaction fails, so the catch withholds rather than replays.
|
|
18
|
+
on('tool.call', async ($, e, next) => redactResult(await next(e), await patterns($))).catch(() => ({ deny: WITHHELD }))
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Secret Redactor on Kiro
|
|
2
|
+
|
|
3
|
+
<!-- Generated by scripts/generate-mods.mjs from mod.toml. Do not edit. -->
|
|
4
|
+
|
|
5
|
+
## Unsupported
|
|
6
|
+
|
|
7
|
+
- `PostToolUse`: Kiro documents no way for a hook to replace tool output, so output cannot be redacted.
|
|
8
|
+
|
|
9
|
+
## Degraded
|
|
10
|
+
|
|
11
|
+
- `PreToolUse`: Kiro documents no confirmation protocol or stdin field names; the whole payload is scanned and a hit is denied.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "v1",
|
|
3
|
+
"hooks": [
|
|
4
|
+
{
|
|
5
|
+
"name": "secret-redactor:PreToolUse",
|
|
6
|
+
"description": "Asks before tool calls whose input carries a credential and redacts credentials from tool output.",
|
|
7
|
+
"trigger": "PreToolUse",
|
|
8
|
+
"action": {
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "node .kiro/vanguard-mods/secret-redactor/bin/hook.mjs --harness kiro --event PreToolUse"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Shared helpers for the mods under mods/. scripts/generate-mods.mjs copies this file
|
|
2
|
+
// byte-for-byte to mods/<id>/lib/common.mjs, so edit it here and run `npm run mods:write`.
|
|
3
|
+
//
|
|
4
|
+
// Pure on purpose: no imports, no environment, no I/O. Claude Code's native mod runtime
|
|
5
|
+
// imports it and has no Node, and every harness must reach the same verdict on the same
|
|
6
|
+
// input. Regexes compile without the `u` flag so Node and Python `re.ASCII`
|
|
7
|
+
// (tests/validate-mods.py) read them the same way.
|
|
8
|
+
|
|
9
|
+
export const LIMITS = Object.freeze({
|
|
10
|
+
// A redaction that still changes after this many passes is withheld as a whole.
|
|
11
|
+
maxPasses: 8,
|
|
12
|
+
// A guard candidate longer than this is not evaluated and counts as a hit.
|
|
13
|
+
maxCandidateChars: 65536,
|
|
14
|
+
// Guard candidates are not evaluated past this combined length; the call counts as a hit.
|
|
15
|
+
maxTotalChars: 262144,
|
|
16
|
+
// Only the tail of a leaf's context (key or previous sibling) is prefixed to it.
|
|
17
|
+
maxContextChars: 256,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const MARKER_PREFIX = '[REDACTED:'
|
|
21
|
+
export const OVERFLOW_MARKER = '[REDACTED:overflow]'
|
|
22
|
+
const OVERFLOW_HIT = Object.freeze({ id: 'overflow', name: 'redaction did not converge' })
|
|
23
|
+
export const TOO_LARGE_HIT = Object.freeze({ id: 'too-large', name: 'input too large to evaluate' })
|
|
24
|
+
|
|
25
|
+
const ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
26
|
+
const PATTERN_KEYS = new Set(['id', 'name', 'description', 'regex', 'flags', 'context', 'severity', 'agents'])
|
|
27
|
+
const isStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string' && s !== '')
|
|
28
|
+
|
|
29
|
+
// Parser diagnostics can quote credential-bearing data; never expose it in hook logs.
|
|
30
|
+
export function parseJson(text, source) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(text)
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(`${source}: invalid JSON`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Validates a patterns.json-shaped list and compiles it. Throws on anything malformed so a
|
|
39
|
+
// bad extension file fails closed in the caller instead of silently matching nothing.
|
|
40
|
+
export function compilePatterns(list, source = 'patterns') {
|
|
41
|
+
if (!Array.isArray(list)) throw new Error(`${source}: expected an array of patterns`)
|
|
42
|
+
const seen = new Set()
|
|
43
|
+
return list.map((p, i) => {
|
|
44
|
+
const where = `${source}[${i}]`
|
|
45
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) throw new Error(`${where}: expected an object`)
|
|
46
|
+
for (const k of Object.keys(p)) if (!PATTERN_KEYS.has(k)) throw new Error(`${where}: unknown key ${k}`)
|
|
47
|
+
if (typeof p.id !== 'string' || !ID_RE.test(p.id)) throw new Error(`${where}: id must be kebab-case`)
|
|
48
|
+
if (seen.has(p.id)) throw new Error(`${where}: duplicate id ${p.id}`)
|
|
49
|
+
seen.add(p.id)
|
|
50
|
+
if (typeof p.name !== 'string' || p.name === '') throw new Error(`${where}: name must be a non-empty string`)
|
|
51
|
+
if (typeof p.regex !== 'string' || p.regex === '') throw new Error(`${where}: regex must be a non-empty string`)
|
|
52
|
+
if (p.flags !== '' && p.flags !== 'i') throw new Error(`${where}: flags must be "" or "i"`)
|
|
53
|
+
if (!isStringArray(p.context) || p.context.length === 0) throw new Error(`${where}: context must be a non-empty string array`)
|
|
54
|
+
for (const k of ['description', 'severity']) {
|
|
55
|
+
if (p[k] !== undefined && typeof p[k] !== 'string') throw new Error(`${where}: ${k} must be a string`)
|
|
56
|
+
}
|
|
57
|
+
if (p.agents !== undefined && !isStringArray(p.agents)) throw new Error(`${where}: agents must be a string array`)
|
|
58
|
+
let rx, rxg
|
|
59
|
+
try {
|
|
60
|
+
rx = new RegExp(p.regex, p.flags)
|
|
61
|
+
rxg = new RegExp(p.regex, 'g' + p.flags)
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error(`${where}: regex does not compile`)
|
|
64
|
+
}
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
id: p.id,
|
|
67
|
+
name: p.name,
|
|
68
|
+
severity: p.severity,
|
|
69
|
+
context: Object.freeze([...p.context]),
|
|
70
|
+
agents: p.agents ? Object.freeze([...p.agents]) : undefined,
|
|
71
|
+
rx,
|
|
72
|
+
rxg,
|
|
73
|
+
marker: `${MARKER_PREFIX}${p.id}]`,
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Every string value (not key) in depth-first order. The order follows the input, so the
|
|
79
|
+
// same payload always yields the same list.
|
|
80
|
+
export function stringLeaves(value, out = []) {
|
|
81
|
+
if (typeof value === 'string') out.push(value)
|
|
82
|
+
else if (Array.isArray(value)) for (const v of value) stringLeaves(v, out)
|
|
83
|
+
else if (value !== null && typeof value === 'object') for (const v of Object.values(value)) stringLeaves(v, out)
|
|
84
|
+
return out
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Guard candidates: every string leaf, plus every all-string array joined with spaces so
|
|
88
|
+
// an argv-style command (["rm", "-rf", "/"]) reads like the shell line it runs.
|
|
89
|
+
export function candidates(value) {
|
|
90
|
+
const out = stringLeaves(value)
|
|
91
|
+
const joinArrays = (v) => {
|
|
92
|
+
if (Array.isArray(v)) {
|
|
93
|
+
if (v.length > 1 && v.every((s) => typeof s === 'string')) out.push(v.join(' '))
|
|
94
|
+
v.forEach(joinArrays)
|
|
95
|
+
} else if (v !== null && typeof v === 'object') {
|
|
96
|
+
Object.values(v).forEach(joinArrays)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
joinArrays(value)
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The patterns tagged with `context` that match any text, in catalog order. Oversized input
|
|
104
|
+
// is not evaluated (regex cost is unbounded there) and reports TOO_LARGE_HIT instead.
|
|
105
|
+
export function matchPatterns(patterns, texts, context) {
|
|
106
|
+
let total = 0
|
|
107
|
+
for (const t of texts) {
|
|
108
|
+
total += t.length
|
|
109
|
+
if (t.length > LIMITS.maxCandidateChars || total > LIMITS.maxTotalChars) return [TOO_LARGE_HIT]
|
|
110
|
+
}
|
|
111
|
+
return patterns.filter((p) => p.context.includes(context) && texts.some((t) => p.rx.test(t)))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// One pattern pass after another until the text stops changing. Each pass can expose a new
|
|
115
|
+
// match (password-in-url can swallow an earlier marker), so a single pass is not enough.
|
|
116
|
+
function fixedPoint(text, patterns, hits) {
|
|
117
|
+
let x = text
|
|
118
|
+
for (let pass = 0; pass <= LIMITS.maxPasses; pass++) {
|
|
119
|
+
let y = x
|
|
120
|
+
for (const p of patterns) {
|
|
121
|
+
y = y.replace(p.rxg, (m) => {
|
|
122
|
+
if (m === '') return m
|
|
123
|
+
hits.add(p)
|
|
124
|
+
return p.marker
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
if (y === x) return x
|
|
128
|
+
x = y
|
|
129
|
+
}
|
|
130
|
+
hits.add(OVERFLOW_HIT)
|
|
131
|
+
return OVERFLOW_MARKER
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Redacts `s` with `ctx` in front so a secret split across a key and its value, or across
|
|
135
|
+
// argv elements ("Bearer", "<token>"), still matches; then strips the context back off.
|
|
136
|
+
function redactLeaf(s, ctx, patterns, hits) {
|
|
137
|
+
if (ctx === '') return fixedPoint(s, patterns, hits)
|
|
138
|
+
const pre = ctx.slice(-LIMITS.maxContextChars) + ' '
|
|
139
|
+
const y = fixedPoint(pre + s, patterns, hits)
|
|
140
|
+
if (y === OVERFLOW_MARKER) return y
|
|
141
|
+
const r = fixedPoint(pre, patterns, new Set())
|
|
142
|
+
if (y.startsWith(r)) return fixedPoint(y.slice(r.length), patterns, hits)
|
|
143
|
+
// A match spans the boundary: keep from the marker that covers it. Slicing changes
|
|
144
|
+
// anchors and boundaries, so also check the standalone leaf after contextual matching.
|
|
145
|
+
let d = 0
|
|
146
|
+
while (d < r.length && y[d] === r[d]) d++
|
|
147
|
+
const i = y.lastIndexOf(MARKER_PREFIX, d)
|
|
148
|
+
return fixedPoint(i >= 0 ? y.slice(i) : y, patterns, hits)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function walk(v, ctx, patterns, hits) {
|
|
152
|
+
if (typeof v === 'string') {
|
|
153
|
+
const local = new Set()
|
|
154
|
+
const out = redactLeaf(v, ctx, patterns, local)
|
|
155
|
+
if (out !== v) for (const h of local) hits.add(h)
|
|
156
|
+
return out
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(v)) {
|
|
159
|
+
let prev = null
|
|
160
|
+
return v.map((el) => {
|
|
161
|
+
const out = walk(el, typeof prev === 'string' ? prev : ctx, patterns, hits)
|
|
162
|
+
prev = el
|
|
163
|
+
return out
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
if (v !== null && typeof v === 'object') {
|
|
167
|
+
const named = typeof v.name === 'string' ? v.name : typeof v.key === 'string' ? v.key : null
|
|
168
|
+
// fromEntries, not assignment: `out[k] = v` would drop an own "__proto__" key.
|
|
169
|
+
return Object.fromEntries(
|
|
170
|
+
Object.entries(v).map(([k, x]) => [
|
|
171
|
+
walk(k, '', patterns, hits),
|
|
172
|
+
walk(x, k === 'value' && named !== null ? named : k, patterns, hits),
|
|
173
|
+
]),
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
return v
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Returns a copy of `value` with every match replaced by `[REDACTED:<id>]`, plus the
|
|
180
|
+
// patterns that fired (catalog order, unique). Keys are redacted too; two keys that redact
|
|
181
|
+
// to the same marker collapse into one, which loses data but never leaks it.
|
|
182
|
+
export function redactValue(value, patterns) {
|
|
183
|
+
const hits = new Set()
|
|
184
|
+
const out = walk(value, '', patterns, hits)
|
|
185
|
+
const order = [...patterns, OVERFLOW_HIT]
|
|
186
|
+
return { value: out, hits: order.filter((p) => hits.has(p)) }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Same shape as `value`, every string replaced by `text`. The fallback when redaction
|
|
190
|
+
// itself failed: withhold everything rather than pass anything through.
|
|
191
|
+
export function maskValue(value, text) {
|
|
192
|
+
if (typeof value === 'string') return text
|
|
193
|
+
if (Array.isArray(value)) return value.map((v) => maskValue(v, text))
|
|
194
|
+
if (value !== null && typeof value === 'object') {
|
|
195
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, maskValue(v, text)]))
|
|
196
|
+
}
|
|
197
|
+
return value
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// JSON with object keys sorted at every level, so equal values hash equally.
|
|
201
|
+
export function canonicalJson(value) {
|
|
202
|
+
if (Array.isArray(value)) return '[' + value.map((v) => (v === undefined ? 'null' : canonicalJson(v))).join(',') + ']'
|
|
203
|
+
if (value !== null && typeof value === 'object') {
|
|
204
|
+
const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort()
|
|
205
|
+
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(value[k])).join(',') + '}'
|
|
206
|
+
}
|
|
207
|
+
return JSON.stringify(value) ?? 'null'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function sha256Hex(text) {
|
|
211
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(text))
|
|
212
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('')
|
|
213
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Secret Redactor logic, shared by the native module and the Node hook so every harness
|
|
2
|
+
// reaches the same verdict. Pure: no I/O, no environment.
|
|
3
|
+
import { compilePatterns, parseJson, redactValue } from './common.mjs'
|
|
4
|
+
import builtin from './patterns.mjs'
|
|
5
|
+
|
|
6
|
+
const BUILTIN = compilePatterns(builtin, 'patterns.json')
|
|
7
|
+
const CONTEXTS = new Set(['tool-input', 'tool-output'])
|
|
8
|
+
const NOTICE = 'secret-redactor: credentials redacted from this result: '
|
|
9
|
+
export const WITHHELD = '[secret-redactor: output withheld; redaction failed]'
|
|
10
|
+
|
|
11
|
+
const inContext = (patterns, context) => patterns.filter((p) => p.context.includes(context))
|
|
12
|
+
const names = (hits) => hits.map((h) => h.name).join('; ')
|
|
13
|
+
|
|
14
|
+
// Built-in patterns plus the operator's extension, appended (never replacing). Throws on bad
|
|
15
|
+
// JSON or shape, a reused built-in id or an unknown context; callers fail closed.
|
|
16
|
+
export function loadPatterns(extraText, source = 'extension patterns') {
|
|
17
|
+
if (extraText === undefined) return BUILTIN
|
|
18
|
+
const extra = compilePatterns(parseJson(extraText, source), source)
|
|
19
|
+
for (const p of extra) {
|
|
20
|
+
if (BUILTIN.some((b) => b.id === p.id)) throw new Error(`${source}: id ${p.id} is a built-in pattern`)
|
|
21
|
+
if (!p.context.every((c) => CONTEXTS.has(c))) throw new Error(`${source}: ${p.id} context must be tool-input or tool-output`)
|
|
22
|
+
}
|
|
23
|
+
return [...BUILTIN, ...extra]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Asks before a call whose input carries a credential. The reason names the patterns, never
|
|
27
|
+
// the matched text.
|
|
28
|
+
export function checkInput(input, patterns) {
|
|
29
|
+
const { hits } = redactValue(input, inContext(patterns, 'tool-input'))
|
|
30
|
+
return hits.length ? { decision: 'ask', reason: `secret-redactor: tool input contains ${names(hits)}` } : undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function redactOutput(output, patterns) {
|
|
34
|
+
return redactValue(output, inContext(patterns, 'tool-output'))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Native tool.call result. A clean result goes back untouched (core keeps its own messages).
|
|
38
|
+
// A redacted one becomes the hook's own { result }, which core re-validates and re-maps, so
|
|
39
|
+
// the model's text is rebuilt from the redacted value. What cannot be rebuilt that way is
|
|
40
|
+
// withheld with its redacted text instead: an error, a secret that shows only in core's
|
|
41
|
+
// text, or one in `context` (a hook may add context entries but never rewrite them).
|
|
42
|
+
export function redactResult(r, patterns) {
|
|
43
|
+
const out = inContext(patterns, 'tool-output')
|
|
44
|
+
if (typeof r.deny === 'string') {
|
|
45
|
+
const d = redactValue(r.deny, out)
|
|
46
|
+
return d.hits.length ? { deny: d.value } : r
|
|
47
|
+
}
|
|
48
|
+
const result = redactValue(r.result, out)
|
|
49
|
+
const context = redactValue(r.context ?? [], out)
|
|
50
|
+
const text = redactValue(r.text ?? '', out)
|
|
51
|
+
if (result.hits.length + context.hits.length + text.hits.length === 0) return r
|
|
52
|
+
if (r.isError || context.hits.length || result.hits.length === 0) {
|
|
53
|
+
const shown = typeof r.text === 'string' ? text.value : typeof result.value === 'string' ? result.value : (JSON.stringify(result.value) ?? '')
|
|
54
|
+
return { deny: [NOTICE + shown, ...context.value].join('\n') }
|
|
55
|
+
}
|
|
56
|
+
return r.context ? { result: result.value, context: r.context } : { result: result.value }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function logLines(hits, tool, event) {
|
|
60
|
+
return hits.map((h) => `[secret-redactor] redacted ${h.name} in ${tool ?? 'unknown tool'} ${event}`)
|
|
61
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Generated by scripts/generate-mods.mjs from patterns.json. Do not edit.
|
|
2
|
+
export default [
|
|
3
|
+
{
|
|
4
|
+
"id": "github-fine-grained-pat",
|
|
5
|
+
"name": "GitHub fine-grained personal access token",
|
|
6
|
+
"description": "github_pat_ prefix followed by 22+ [A-Za-z0-9_]. Listed before github-pat so a fine-grained body that happens to contain ghp_ is consumed whole.",
|
|
7
|
+
"regex": "github_pat_[A-Za-z0-9_]{22,255}",
|
|
8
|
+
"flags": "",
|
|
9
|
+
"severity": "critical",
|
|
10
|
+
"context": [
|
|
11
|
+
"tool-input",
|
|
12
|
+
"tool-output"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "github-pat",
|
|
17
|
+
"name": "GitHub token (ghp/gho/ghu/ghs/ghr)",
|
|
18
|
+
"description": "GitHub classic PAT / OAuth / user-to-server / server-to-server / refresh token: 3-letter prefix, underscore, 36-255 alphanumerics.",
|
|
19
|
+
"regex": "(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,255}",
|
|
20
|
+
"flags": "",
|
|
21
|
+
"severity": "critical",
|
|
22
|
+
"context": [
|
|
23
|
+
"tool-input",
|
|
24
|
+
"tool-output"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "aws-secret-key",
|
|
29
|
+
"name": "AWS secret access key (context-anchored)",
|
|
30
|
+
"description": "40+ chars of [A-Za-z0-9/+] ONLY when preceded by an AWS secret-key name (aws_secret_access_key, AWS_SECRET_KEY, SecretAccessKey, secretAccessKey, --aws-secret-access-key ...) plus a separator (:, =, =>, or whitespace) with optional quotes/escaped quotes. A bare 40-char base62 run is deliberately NOT matched: it is indistinguishable from a 40-hex git SHA. The match consumes the key name and separator. Listed before aws-access-key so an AKIA-looking prefix inside the value cannot split the secret.",
|
|
31
|
+
"regex": "(?:aws[_.-]?secret[_.-]?(?:access[_.-]?)?key|secret[_.-]?access[_.-]?key)[\\\\\"']{0,4}(?:[ \\t]*(?::|=>?)[ \\t\\r\\n]*|[ \\t]+)[\\\\\"']{0,4}[A-Za-z0-9/+]{40,}",
|
|
32
|
+
"flags": "i",
|
|
33
|
+
"severity": "critical",
|
|
34
|
+
"context": [
|
|
35
|
+
"tool-input",
|
|
36
|
+
"tool-output"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "aws-access-key",
|
|
41
|
+
"name": "AWS access key ID",
|
|
42
|
+
"description": "AKIA (long-term) or ASIA (STS temporary) followed by 16 uppercase alphanumerics. No boundary anchors (no lookbehind available): fails closed, may over-redact inside long uppercase blobs.",
|
|
43
|
+
"regex": "(?:AKIA|ASIA)[0-9A-Z]{16}",
|
|
44
|
+
"flags": "",
|
|
45
|
+
"severity": "high",
|
|
46
|
+
"context": [
|
|
47
|
+
"tool-input",
|
|
48
|
+
"tool-output"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "generic-bearer",
|
|
53
|
+
"name": "HTTP Bearer token",
|
|
54
|
+
"description": "Bearer (any case) + horizontal whitespace + 16+ token chars [A-Za-z0-9._~+/-] + optional = padding. Spec said '+'; 16+ avoids redacting prose such as 'bearer of'. Token class excludes '[' so it can never extend into a [REDACTED:..] marker.",
|
|
55
|
+
"regex": "bearer[ \\t]+[A-Za-z0-9._~+/-]{16,}=*",
|
|
56
|
+
"flags": "i",
|
|
57
|
+
"severity": "high",
|
|
58
|
+
"context": [
|
|
59
|
+
"tool-input",
|
|
60
|
+
"tool-output"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "password-in-url",
|
|
65
|
+
"name": "Credentials embedded in URL",
|
|
66
|
+
"description": "scheme://user:password@ . Scheme bounded to 30 chars (avoids quadratic scans on long alnum runs). User/password classes exclude whitespace, quotes, backslash, '/', '@', angle brackets and backtick, so a match cannot cross whitespace, a JSON string quote, or a path. Must stay LAST in the array: its password class accepts '[' and ':' so it can swallow earlier [REDACTED:..] markers (harmless, single pass), and nothing after it can re-form.",
|
|
67
|
+
"regex": "[A-Za-z][A-Za-z0-9+.-]{0,30}://[^ \\t\\r\\n\"'\\\\/@:?#<>`]*:[^ \\t\\r\\n\"'\\\\/@<>`]+@",
|
|
68
|
+
"flags": "",
|
|
69
|
+
"severity": "high",
|
|
70
|
+
"context": [
|
|
71
|
+
"tool-input",
|
|
72
|
+
"tool-output"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
];
|