@thinkingsage/kanon 0.8.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/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bridge/mcp-server.cjs +14171 -0
- package/package.json +98 -0
- package/src/adapters/capabilities.ts +178 -0
- package/src/adapters/claude-code.ts +110 -0
- package/src/adapters/cline.ts +98 -0
- package/src/adapters/codex.ts +173 -0
- package/src/adapters/copilot.ts +106 -0
- package/src/adapters/cursor.ts +97 -0
- package/src/adapters/degradation.ts +95 -0
- package/src/adapters/index.ts +324 -0
- package/src/adapters/kiro-frontmatter.ts +139 -0
- package/src/adapters/kiro-inclusion.ts +86 -0
- package/src/adapters/kiro.ts +412 -0
- package/src/adapters/qdeveloper.ts +115 -0
- package/src/adapters/types.ts +81 -0
- package/src/adapters/windsurf.ts +96 -0
- package/src/admin.ts +283 -0
- package/src/asset-conventions.ts +118 -0
- package/src/attribution-backfill.ts +319 -0
- package/src/attribution-report.ts +95 -0
- package/src/attribution.ts +239 -0
- package/src/backends/github.ts +194 -0
- package/src/backends/http.ts +122 -0
- package/src/backends/index.ts +39 -0
- package/src/backends/local.ts +47 -0
- package/src/backends/s3.ts +157 -0
- package/src/backends/types.ts +59 -0
- package/src/base-cache.ts +270 -0
- package/src/browse-ui.ts +3754 -0
- package/src/browse.ts +1038 -0
- package/src/build.ts +1108 -0
- package/src/catalog.ts +204 -0
- package/src/cli-deprecated.ts +29 -0
- package/src/cli.ts +773 -0
- package/src/collection-admin.ts +287 -0
- package/src/collection-builder.ts +464 -0
- package/src/collections.ts +116 -0
- package/src/compatibility.ts +105 -0
- package/src/config.ts +743 -0
- package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
- package/src/eval.ts +1169 -0
- package/src/file-writer.ts +61 -0
- package/src/format-registry.ts +141 -0
- package/src/guild/auto-updater.ts +163 -0
- package/src/guild/backend-resolver.ts +49 -0
- package/src/guild/cli.ts +592 -0
- package/src/guild/collection-expander.ts +47 -0
- package/src/guild/global-cache.ts +247 -0
- package/src/guild/hook-generator.ts +100 -0
- package/src/guild/manifest.ts +154 -0
- package/src/guild/path-utils.ts +12 -0
- package/src/guild/sync.ts +622 -0
- package/src/guild/version-resolver.ts +42 -0
- package/src/help/metadata.ts +445 -0
- package/src/help/renderer.ts +265 -0
- package/src/help/typo-suggester.ts +25 -0
- package/src/hooks/expression.ts +493 -0
- package/src/hooks/pipeline.ts +141 -0
- package/src/import.ts +773 -0
- package/src/importers/claude-code.ts +134 -0
- package/src/importers/cline.ts +103 -0
- package/src/importers/codex.ts +140 -0
- package/src/importers/copilot.ts +103 -0
- package/src/importers/cursor.ts +105 -0
- package/src/importers/index.ts +390 -0
- package/src/importers/kiro.ts +110 -0
- package/src/importers/qdeveloper.ts +103 -0
- package/src/importers/types.ts +54 -0
- package/src/importers/windsurf.ts +104 -0
- package/src/install.ts +1005 -0
- package/src/manifest-admin.ts +306 -0
- package/src/mcp-bridge.ts +240 -0
- package/src/mutation/delta.ts +50 -0
- package/src/mutation/history.ts +66 -0
- package/src/mutation/operators.ts +524 -0
- package/src/mutation/runner.ts +332 -0
- package/src/new.ts +106 -0
- package/src/outcomes/collision.ts +127 -0
- package/src/outcomes/normalize.ts +208 -0
- package/src/outcomes/registry.ts +173 -0
- package/src/parser.ts +446 -0
- package/src/provenance-backfill-cli.ts +319 -0
- package/src/provenance-backfill.ts +520 -0
- package/src/publish.ts +354 -0
- package/src/reconcile-orchestrator.ts +502 -0
- package/src/reconcile-report-renderer.ts +176 -0
- package/src/resolve-body.ts +15 -0
- package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
- package/src/rosetta/builtins/contracts.ts +1033 -0
- package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
- package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
- package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
- package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
- package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
- package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
- package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
- package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
- package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
- package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
- package/src/rosetta/builtins/sources/cline-native.ts +176 -0
- package/src/rosetta/builtins/sources/codex-native.ts +343 -0
- package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
- package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
- package/src/rosetta/builtins/sources/index.ts +95 -0
- package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
- package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
- package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
- package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
- package/src/rosetta/builtins/sources/superpowers.ts +240 -0
- package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
- package/src/rosetta/builtins/targets/claude-code.ts +181 -0
- package/src/rosetta/builtins/targets/cline.ts +87 -0
- package/src/rosetta/builtins/targets/codex.ts +226 -0
- package/src/rosetta/builtins/targets/copilot.ts +103 -0
- package/src/rosetta/builtins/targets/cursor.ts +87 -0
- package/src/rosetta/builtins/targets/index.ts +60 -0
- package/src/rosetta/builtins/targets/kiro.ts +278 -0
- package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
- package/src/rosetta/builtins/targets/windsurf.ts +87 -0
- package/src/rosetta/canonical.ts +729 -0
- package/src/rosetta/compatibility.ts +432 -0
- package/src/rosetta/contracts.ts +329 -0
- package/src/rosetta/detector.ts +724 -0
- package/src/rosetta/diagnostics.ts +630 -0
- package/src/rosetta/engine-bootstrap.ts +103 -0
- package/src/rosetta/engine.ts +744 -0
- package/src/rosetta/index.ts +381 -0
- package/src/rosetta/inspection.ts +530 -0
- package/src/rosetta/plan.ts +448 -0
- package/src/rosetta/provenance-digest.ts +369 -0
- package/src/rosetta/reconcile.ts +812 -0
- package/src/rosetta/redaction.ts +467 -0
- package/src/rosetta/registry.ts +712 -0
- package/src/rosetta/renderers.ts +571 -0
- package/src/rosetta/request-guard.ts +335 -0
- package/src/rosetta/resolution.ts +419 -0
- package/src/rosetta/source-accounting.ts +233 -0
- package/src/rosetta/templates.ts +129 -0
- package/src/rosetta-cli.ts +717 -0
- package/src/rosetta-docs-generator.ts +793 -0
- package/src/rosetta-profiles-cli.ts +367 -0
- package/src/schemas.ts +1712 -0
- package/src/spec-coordination.ts +1141 -0
- package/src/temper.ts +747 -0
- package/src/template-bundle-loader.ts +312 -0
- package/src/template-engine.ts +53 -0
- package/src/translation-application-policy.ts +496 -0
- package/src/translation-orchestrator.ts +1013 -0
- package/src/translation-plan-applier.ts +473 -0
- package/src/tutorial.ts +305 -0
- package/src/validate.ts +1093 -0
- package/src/versioning.ts +553 -0
- package/src/wizard.ts +660 -0
- package/src/workspace.ts +237 -0
- package/templates/eval-contexts/claude-code.md.njk +6 -0
- package/templates/eval-contexts/cline.md.njk +6 -0
- package/templates/eval-contexts/copilot.md.njk +6 -0
- package/templates/eval-contexts/cursor.md.njk +6 -0
- package/templates/eval-contexts/kiro.md.njk +10 -0
- package/templates/eval-contexts/qdeveloper.md.njk +6 -0
- package/templates/eval-contexts/windsurf.md.njk +6 -0
- package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
- package/templates/harness-adapters/_base/base.md.njk +16 -0
- package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
- package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
- package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
- package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
- package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
- package/templates/harness-adapters/cline/hook.sh.njk +4 -0
- package/templates/harness-adapters/cline/mcp.json.njk +1 -0
- package/templates/harness-adapters/cline/rule.md.njk +1 -0
- package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
- package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
- package/templates/harness-adapters/codex/skill.md.njk +27 -0
- package/templates/harness-adapters/copilot/agents.md.njk +1 -0
- package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
- package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
- package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
- package/templates/harness-adapters/cursor/rule.md.njk +6 -0
- package/templates/harness-adapters/kiro/hook.json.njk +1 -0
- package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
- package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
- package/templates/harness-adapters/kiro/power.md.njk +12 -0
- package/templates/harness-adapters/kiro/steering.md.njk +16 -0
- package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
- package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
- package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
- package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
- package/templates/knowledge/hooks.yaml.njk +4 -0
- package/templates/knowledge/knowledge.md.njk +53 -0
- package/templates/knowledge/mcp-servers.yaml.njk +2 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rosetta Stone — Fail-Closed Sensitive-Value Handling
|
|
3
|
+
*
|
|
4
|
+
* Applies contract security policies (reject, preserve, reference-only),
|
|
5
|
+
* records sensitive locations and fingerprints without raw values,
|
|
6
|
+
* proves structured preview redaction, and suppresses derived diagnostics,
|
|
7
|
+
* plans, and content when completeness cannot be proven.
|
|
8
|
+
*
|
|
9
|
+
* CONSTRAINTS:
|
|
10
|
+
* - Pure function — no filesystem, process, clock, random, Git, or network imports
|
|
11
|
+
* - Uses djb2 hash for non-reversible fingerprinting (no node:crypto)
|
|
12
|
+
*
|
|
13
|
+
* Requirements: 9.6, 9.7, 9.8, 13.9, 13.10, 13.11, 13.12
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { FormatSecurityPolicy, TranslationDiagnostic } from "../schemas";
|
|
17
|
+
import { createDiagnostic } from "./diagnostics";
|
|
18
|
+
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
20
|
+
// Types
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Records the path, field, and a non-reversible fingerprint of a sensitive value.
|
|
25
|
+
* Never stores the raw value itself.
|
|
26
|
+
*/
|
|
27
|
+
export interface SensitiveLocation {
|
|
28
|
+
readonly path: string;
|
|
29
|
+
readonly field: string;
|
|
30
|
+
readonly fingerprint: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Certifies completeness of a redaction pass over registered sensitive locations.
|
|
35
|
+
*/
|
|
36
|
+
export interface RedactionProof {
|
|
37
|
+
readonly complete: boolean;
|
|
38
|
+
readonly coveredLocations: number;
|
|
39
|
+
readonly totalLocations: number;
|
|
40
|
+
readonly uncoveredPaths: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A structured redactor that handles content and diagnostic redaction.
|
|
45
|
+
*/
|
|
46
|
+
export interface StructuredRedactor {
|
|
47
|
+
redactContent(content: string): string;
|
|
48
|
+
redactDiagnostics(
|
|
49
|
+
diagnostics: readonly TranslationDiagnostic[],
|
|
50
|
+
): TranslationDiagnostic[];
|
|
51
|
+
proveCompleteness(): RedactionProof | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
55
|
+
// Non-Reversible Fingerprinting (djb2)
|
|
56
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Produces a non-reversible fingerprint using the djb2 hash algorithm.
|
|
60
|
+
* Returns a truncated hex string suitable for location tracking.
|
|
61
|
+
*/
|
|
62
|
+
export function computeFingerprint(value: string): string {
|
|
63
|
+
let hash = 5381;
|
|
64
|
+
for (let i = 0; i < value.length; i++) {
|
|
65
|
+
// hash * 33 + char
|
|
66
|
+
hash = ((hash << 5) + hash + value.charCodeAt(i)) | 0;
|
|
67
|
+
}
|
|
68
|
+
// Convert to unsigned 32-bit and return as hex
|
|
69
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
73
|
+
// Redaction Registry
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Collects sensitive locations during parsing. Records paths and fingerprints
|
|
78
|
+
* but never raw sensitive values.
|
|
79
|
+
*/
|
|
80
|
+
export class RedactionRegistry {
|
|
81
|
+
private readonly locations: SensitiveLocation[] = [];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Register a sensitive location with its non-reversible fingerprint.
|
|
85
|
+
*/
|
|
86
|
+
registerSensitive(
|
|
87
|
+
location: Omit<SensitiveLocation, "fingerprint">,
|
|
88
|
+
fingerprint: string,
|
|
89
|
+
): void {
|
|
90
|
+
this.locations.push(Object.freeze({ ...location, fingerprint }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns all registered sensitive locations as a frozen array.
|
|
95
|
+
*/
|
|
96
|
+
getLocations(): readonly SensitiveLocation[] {
|
|
97
|
+
return Object.freeze([...this.locations]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if any sensitive content has been registered.
|
|
102
|
+
*/
|
|
103
|
+
hasRegisteredLocations(): boolean {
|
|
104
|
+
return this.locations.length > 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
109
|
+
// Secret Detection Heuristics
|
|
110
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
111
|
+
|
|
112
|
+
/** AWS access key pattern (starts with AKIA) */
|
|
113
|
+
const AWS_KEY_PATTERN = /\bAKIA[0-9A-Z]{16}\b/;
|
|
114
|
+
|
|
115
|
+
/** GitHub token patterns (ghp_, gho_, ghs_, ghr_, github_pat_) */
|
|
116
|
+
const GITHUB_TOKEN_PATTERN =
|
|
117
|
+
/\b(ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36}|ghs_[a-zA-Z0-9]{36}|ghr_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9_]{22,})\b/;
|
|
118
|
+
|
|
119
|
+
/** JWT-like tokens (eyJ prefix followed by base64-ish content) */
|
|
120
|
+
const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/;
|
|
121
|
+
|
|
122
|
+
/** Common password field indicators */
|
|
123
|
+
const PASSWORD_FIELD_PATTERN =
|
|
124
|
+
/(?:password|passwd|pwd|secret|api_key|apikey|private_key|access_token|auth_token)\s*[:=]\s*["']?[^\s"']{8,}/i;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Calculate Shannon entropy of a string.
|
|
128
|
+
* Used to detect high-entropy secrets.
|
|
129
|
+
*/
|
|
130
|
+
function shannonEntropy(str: string): number {
|
|
131
|
+
if (str.length === 0) return 0;
|
|
132
|
+
const freq: Record<string, number> = {};
|
|
133
|
+
for (const ch of str) {
|
|
134
|
+
freq[ch] = (freq[ch] ?? 0) + 1;
|
|
135
|
+
}
|
|
136
|
+
let entropy = 0;
|
|
137
|
+
const len = str.length;
|
|
138
|
+
for (const count of Object.values(freq)) {
|
|
139
|
+
const p = count / len;
|
|
140
|
+
entropy -= p * Math.log2(p);
|
|
141
|
+
}
|
|
142
|
+
return entropy;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Minimum entropy threshold to flag a string as potentially secret */
|
|
146
|
+
const HIGH_ENTROPY_THRESHOLD = 4.0;
|
|
147
|
+
/** Minimum length for high-entropy detection */
|
|
148
|
+
const HIGH_ENTROPY_MIN_LENGTH = 16;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Detects whether a string value looks like a secret/credential.
|
|
152
|
+
* Returns true if any heuristic matches.
|
|
153
|
+
*/
|
|
154
|
+
export function looksLikeSecret(value: string): boolean {
|
|
155
|
+
if (AWS_KEY_PATTERN.test(value)) return true;
|
|
156
|
+
if (GITHUB_TOKEN_PATTERN.test(value)) return true;
|
|
157
|
+
if (JWT_PATTERN.test(value)) return true;
|
|
158
|
+
if (PASSWORD_FIELD_PATTERN.test(value)) return true;
|
|
159
|
+
|
|
160
|
+
// High-entropy string detection for values that don't match patterns
|
|
161
|
+
// but look random enough to be secrets
|
|
162
|
+
if (
|
|
163
|
+
value.length >= HIGH_ENTROPY_MIN_LENGTH &&
|
|
164
|
+
shannonEntropy(value) >= HIGH_ENTROPY_THRESHOLD
|
|
165
|
+
) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check whether a value matches one of the approved reference patterns.
|
|
174
|
+
* Approved patterns use ${ENV_VAR} or similar interpolation syntax.
|
|
175
|
+
*/
|
|
176
|
+
export function matchesApprovedPattern(
|
|
177
|
+
value: string,
|
|
178
|
+
allowedPatterns: readonly string[],
|
|
179
|
+
): boolean {
|
|
180
|
+
for (const pattern of allowedPatterns) {
|
|
181
|
+
try {
|
|
182
|
+
const regex = new RegExp(pattern);
|
|
183
|
+
if (regex.test(value)) return true;
|
|
184
|
+
} catch {
|
|
185
|
+
// Invalid pattern — skip silently
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
192
|
+
// Security Policy Application
|
|
193
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Result of applying a security policy to content.
|
|
197
|
+
*/
|
|
198
|
+
export interface PolicyApplicationResult {
|
|
199
|
+
readonly ok: boolean;
|
|
200
|
+
readonly content: string;
|
|
201
|
+
readonly diagnostics: TranslationDiagnostic[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Applies the contract's security policy to content.
|
|
206
|
+
*
|
|
207
|
+
* - `"reject"` — if content contains literal secrets not matching approved
|
|
208
|
+
* reference patterns, return error diagnostic.
|
|
209
|
+
* - `"preserve"` — pass content through unchanged.
|
|
210
|
+
* - `"reference-only"` — verify all secret-like values use approved reference
|
|
211
|
+
* syntax; reject literal values.
|
|
212
|
+
*/
|
|
213
|
+
export function applySensitivePolicy(
|
|
214
|
+
content: string,
|
|
215
|
+
policy: FormatSecurityPolicy["sensitiveValuePolicy"],
|
|
216
|
+
allowedPatterns: readonly string[],
|
|
217
|
+
): PolicyApplicationResult {
|
|
218
|
+
if (policy === "preserve") {
|
|
219
|
+
return { ok: true, content, diagnostics: [] };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// For "reject" and "reference-only", scan for secrets
|
|
223
|
+
const diagnostics: TranslationDiagnostic[] = [];
|
|
224
|
+
const secretFindings = findSecretsInContent(content);
|
|
225
|
+
|
|
226
|
+
if (policy === "reject") {
|
|
227
|
+
// Any literal secret that isn't an approved reference is rejected
|
|
228
|
+
for (const finding of secretFindings) {
|
|
229
|
+
if (!matchesApprovedPattern(finding.value, allowedPatterns)) {
|
|
230
|
+
diagnostics.push(
|
|
231
|
+
createDiagnostic("RS_SENSITIVE_REJECTED", {
|
|
232
|
+
message: `Literal secret detected at offset ${finding.offset} under reject policy.`,
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (diagnostics.length > 0) {
|
|
239
|
+
return { ok: false, content, diagnostics };
|
|
240
|
+
}
|
|
241
|
+
return { ok: true, content, diagnostics: [] };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// "reference-only": every secret-like value must match approved patterns
|
|
245
|
+
for (const finding of secretFindings) {
|
|
246
|
+
if (!matchesApprovedPattern(finding.value, allowedPatterns)) {
|
|
247
|
+
diagnostics.push(
|
|
248
|
+
createDiagnostic("RS_SENSITIVE_REFERENCE_INVALID", {
|
|
249
|
+
message: `Value at offset ${finding.offset} does not match any approved reference pattern.`,
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (diagnostics.length > 0) {
|
|
256
|
+
return { ok: false, content, diagnostics };
|
|
257
|
+
}
|
|
258
|
+
return { ok: true, content, diagnostics: [] };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
262
|
+
// Secret Finding (Internal)
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
264
|
+
|
|
265
|
+
interface SecretFinding {
|
|
266
|
+
readonly value: string;
|
|
267
|
+
readonly offset: number;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Scans content for potential secret values using detection heuristics.
|
|
272
|
+
* Returns the matched values and their offsets.
|
|
273
|
+
*/
|
|
274
|
+
function findSecretsInContent(content: string): SecretFinding[] {
|
|
275
|
+
const findings: SecretFinding[] = [];
|
|
276
|
+
const patterns: RegExp[] = [
|
|
277
|
+
new RegExp(AWS_KEY_PATTERN.source, "g"),
|
|
278
|
+
new RegExp(GITHUB_TOKEN_PATTERN.source, "g"),
|
|
279
|
+
new RegExp(JWT_PATTERN.source, "g"),
|
|
280
|
+
new RegExp(PASSWORD_FIELD_PATTERN.source, "gi"),
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
for (const pattern of patterns) {
|
|
284
|
+
let match: RegExpExecArray | null;
|
|
285
|
+
// Reset lastIndex for global patterns
|
|
286
|
+
pattern.lastIndex = 0;
|
|
287
|
+
while (true) {
|
|
288
|
+
match = pattern.exec(content);
|
|
289
|
+
if (match === null) break;
|
|
290
|
+
findings.push({ value: match[0], offset: match.index });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Also check for high-entropy tokens (word boundaries)
|
|
295
|
+
const tokenPattern = /\b[A-Za-z0-9_\-/+=]{16,}\b/g;
|
|
296
|
+
let tokenMatch: RegExpExecArray | null;
|
|
297
|
+
while (true) {
|
|
298
|
+
tokenMatch = tokenPattern.exec(content);
|
|
299
|
+
if (tokenMatch === null) break;
|
|
300
|
+
const token = tokenMatch[0];
|
|
301
|
+
if (
|
|
302
|
+
shannonEntropy(token) >= HIGH_ENTROPY_THRESHOLD &&
|
|
303
|
+
!findings.some((f) => f.offset === tokenMatch?.index)
|
|
304
|
+
) {
|
|
305
|
+
findings.push({ value: token, offset: tokenMatch.index });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return findings;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
313
|
+
// Structured Redactor
|
|
314
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
315
|
+
|
|
316
|
+
/** Sentinel used to replace redacted values in content */
|
|
317
|
+
const REDACTION_MARKER = "[REDACTED]";
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Creates a structured redactor for the given sensitive locations.
|
|
321
|
+
* The redactor replaces known sensitive values with `[REDACTED]` markers
|
|
322
|
+
* and can prove completeness of the redaction pass.
|
|
323
|
+
*/
|
|
324
|
+
export function createRedactor(
|
|
325
|
+
locations: readonly SensitiveLocation[],
|
|
326
|
+
): StructuredRedactor {
|
|
327
|
+
// Track which locations have been verified as covered
|
|
328
|
+
const coveredIndices = new Set<number>();
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
redactContent(content: string): string {
|
|
332
|
+
if (locations.length === 0) {
|
|
333
|
+
return content;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let redacted = content;
|
|
337
|
+
// For each registered location, attempt to find and redact
|
|
338
|
+
// We use fingerprints to verify coverage without needing raw values
|
|
339
|
+
const secretFindings = findSecretsInContent(content);
|
|
340
|
+
|
|
341
|
+
for (const finding of secretFindings) {
|
|
342
|
+
const fingerprint = computeFingerprint(finding.value);
|
|
343
|
+
// Check if this finding matches a registered location
|
|
344
|
+
for (let i = 0; i < locations.length; i++) {
|
|
345
|
+
if (locations[i].fingerprint === fingerprint) {
|
|
346
|
+
redacted = redacted.replaceAll(finding.value, REDACTION_MARKER);
|
|
347
|
+
coveredIndices.add(i);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return redacted;
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
redactDiagnostics(
|
|
356
|
+
diagnostics: readonly TranslationDiagnostic[],
|
|
357
|
+
): TranslationDiagnostic[] {
|
|
358
|
+
if (locations.length === 0) {
|
|
359
|
+
return [...diagnostics];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return diagnostics.map((diag) => {
|
|
363
|
+
let message = diag.message;
|
|
364
|
+
let remediation = diag.remediation;
|
|
365
|
+
|
|
366
|
+
// Scan the diagnostic text for potential secrets
|
|
367
|
+
const msgFindings = findSecretsInContent(message);
|
|
368
|
+
for (const finding of msgFindings) {
|
|
369
|
+
message = message.replaceAll(finding.value, REDACTION_MARKER);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const remFindings = findSecretsInContent(remediation);
|
|
373
|
+
for (const finding of remFindings) {
|
|
374
|
+
remediation = remediation.replaceAll(finding.value, REDACTION_MARKER);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return { ...diag, message, remediation };
|
|
378
|
+
});
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
proveCompleteness(): RedactionProof | null {
|
|
382
|
+
if (locations.length === 0) {
|
|
383
|
+
return {
|
|
384
|
+
complete: true,
|
|
385
|
+
coveredLocations: 0,
|
|
386
|
+
totalLocations: 0,
|
|
387
|
+
uncoveredPaths: [],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const uncoveredPaths: string[] = [];
|
|
392
|
+
for (let i = 0; i < locations.length; i++) {
|
|
393
|
+
if (!coveredIndices.has(i)) {
|
|
394
|
+
uncoveredPaths.push(locations[i].path);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const complete = uncoveredPaths.length === 0;
|
|
399
|
+
return {
|
|
400
|
+
complete,
|
|
401
|
+
coveredLocations: coveredIndices.size,
|
|
402
|
+
totalLocations: locations.length,
|
|
403
|
+
uncoveredPaths,
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
410
|
+
// Incomplete Redaction Suppression
|
|
411
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Minimal safe result when redaction completeness cannot be proven.
|
|
415
|
+
* Suppresses all content, content-derived diagnostics, and plans.
|
|
416
|
+
*/
|
|
417
|
+
export interface SuppressedResult {
|
|
418
|
+
readonly suppressed: true;
|
|
419
|
+
readonly diagnostics: TranslationDiagnostic[];
|
|
420
|
+
readonly content: null;
|
|
421
|
+
readonly plan: null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* When the redaction proof is null or incomplete, suppresses all content,
|
|
426
|
+
* content-derived diagnostic fields, and emits RS_REDACTION_UNSAFE.
|
|
427
|
+
*
|
|
428
|
+
* Returns a minimal safe result with only the redaction-unsafe diagnostic
|
|
429
|
+
* and any non-content-derived diagnostics that were already safe.
|
|
430
|
+
*/
|
|
431
|
+
export function suppressOnIncompleteRedaction(
|
|
432
|
+
result: {
|
|
433
|
+
readonly diagnostics: readonly TranslationDiagnostic[];
|
|
434
|
+
readonly content?: string | null;
|
|
435
|
+
readonly plan?: unknown | null;
|
|
436
|
+
},
|
|
437
|
+
proof: RedactionProof | null,
|
|
438
|
+
): SuppressedResult | null {
|
|
439
|
+
// If proof is complete, no suppression needed
|
|
440
|
+
if (proof?.complete) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Create the RS_REDACTION_UNSAFE diagnostic
|
|
445
|
+
const unsafeDiagnostic = createDiagnostic("RS_REDACTION_UNSAFE", {
|
|
446
|
+
message:
|
|
447
|
+
proof === null
|
|
448
|
+
? "Redaction completeness cannot be proven; all output suppressed."
|
|
449
|
+
: `Redaction incomplete: ${proof.coveredLocations}/${proof.totalLocations} locations covered. Output suppressed.`,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Filter diagnostics: remove any that might contain content-derived data
|
|
453
|
+
// Only keep diagnostics from phases that cannot contain sensitive content
|
|
454
|
+
const safeDiagnostics = result.diagnostics.filter(
|
|
455
|
+
(d) =>
|
|
456
|
+
d.phase === "request" ||
|
|
457
|
+
d.phase === "registry" ||
|
|
458
|
+
d.phase === "detection",
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
suppressed: true,
|
|
463
|
+
diagnostics: [unsafeDiagnostic, ...safeDiagnostics],
|
|
464
|
+
content: null,
|
|
465
|
+
plan: null,
|
|
466
|
+
};
|
|
467
|
+
}
|