@akasecurity/ai-tc-claude-code 0.9.3 → 0.9.4
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/plugin.json +1 -1
- package/README.md +1 -1
- package/commands/setup.md +123 -35
- package/hooks/hooks.json +11 -0
- package/package.json +4 -4
- package/scripts/apply-suppressions.js +581 -69
- package/scripts/backfill.js +1954 -147
- package/scripts/filescan.js +602 -76
- package/scripts/firstrun.js +517 -34
- package/scripts/intro.js +176 -20
- package/scripts/message-display.js +28945 -0
- package/scripts/onboard.js +542 -30
- package/scripts/post-tool-use.js +1943 -139
- package/scripts/pre-tool-use.js +2114 -163
- package/scripts/query.js +522 -35
- package/scripts/reconcile.js +2082 -242
- package/scripts/remediate.js +1872 -104
- package/scripts/session-start.js +680 -112
- package/scripts/start-light.js +174 -18
- package/scripts/statusline.js +517 -34
- package/scripts/stop.js +185 -29
- package/scripts/user-prompt-submit.js +1910 -152
package/scripts/remediate.js
CHANGED
|
@@ -492,11 +492,11 @@ var require_ignore = __commonJS({
|
|
|
492
492
|
});
|
|
493
493
|
|
|
494
494
|
// src/remediation/entry.ts
|
|
495
|
-
import { readFileSync as
|
|
495
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
496
496
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
497
497
|
|
|
498
498
|
// ../../packages/persistence/src/database.ts
|
|
499
|
-
import { randomUUID as
|
|
499
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
500
500
|
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
501
501
|
import { join, sep } from "path";
|
|
502
502
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -562,6 +562,22 @@ var SQLITE_MIGRATIONS = [
|
|
|
562
562
|
{
|
|
563
563
|
tag: "0014_drop_legacy_events_findings",
|
|
564
564
|
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
tag: "0015_busy_vengeance",
|
|
568
|
+
sql: "CREATE TABLE `secret_vault` (\n `pointer_id` text PRIMARY KEY NOT NULL,\n `value_fingerprint` text NOT NULL,\n `fingerprint_key_version` integer NOT NULL,\n `key_version` integer NOT NULL,\n `category` text NOT NULL,\n `rule_id` text NOT NULL,\n `masked_match` text NOT NULL,\n `provider` text,\n `ciphertext` text NOT NULL,\n `nonce` text NOT NULL,\n `auth_tag` text NOT NULL,\n `occurrence_count` integer DEFAULT 1 NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_value` ON `secret_vault` (`value_fingerprint`);--> statement-breakpoint\nCREATE TABLE `secret_vault_deref` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `at` integer NOT NULL,\n `target` text NOT NULL,\n `reason` text NOT NULL,\n `outcome` text NOT NULL,\n `grant_id` text,\n `pointer_count` integer DEFAULT 1 NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_pointer` ON `secret_vault_deref` (`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_reason_at` ON `secret_vault_deref` (`reason`,`at`);"
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
tag: "0016_breezy_zodiak",
|
|
572
|
+
sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
tag: "0017_rainy_kat_farrell",
|
|
576
|
+
sql: "CREATE TABLE `secret_vault_sighting` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `location` text NOT NULL,\n `kind` text NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_sighting` ON `secret_vault_sighting` (`pointer_id`,`location`);"
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
tag: "0018_serious_tana_nile",
|
|
580
|
+
sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
|
|
565
581
|
}
|
|
566
582
|
];
|
|
567
583
|
|
|
@@ -16218,6 +16234,7 @@ var ExceptionConditions = external_exports.object({
|
|
|
16218
16234
|
sourceTool: external_exports.string().optional(),
|
|
16219
16235
|
provider: external_exports.string().optional()
|
|
16220
16236
|
}).strict();
|
|
16237
|
+
var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
|
|
16221
16238
|
var DetectionException = external_exports.object({
|
|
16222
16239
|
id: external_exports.guid(),
|
|
16223
16240
|
ruleId: external_exports.string(),
|
|
@@ -16234,6 +16251,7 @@ var DetectionException = external_exports.object({
|
|
|
16234
16251
|
keyVersion: external_exports.number().int().positive(),
|
|
16235
16252
|
// maskMatch() preview of the approved value — never the raw value.
|
|
16236
16253
|
maskedValue: external_exports.string(),
|
|
16254
|
+
capability: ExceptionCapability.default("suppress"),
|
|
16237
16255
|
scope: ExceptionScope,
|
|
16238
16256
|
expiresAt: external_exports.iso.datetime().nullable(),
|
|
16239
16257
|
maxUses: external_exports.number().int().positive().nullable(),
|
|
@@ -16257,6 +16275,7 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16257
16275
|
ruleId: true,
|
|
16258
16276
|
valueFingerprint: true,
|
|
16259
16277
|
keyVersion: true,
|
|
16278
|
+
capability: true,
|
|
16260
16279
|
expiresAt: true,
|
|
16261
16280
|
maxUses: true,
|
|
16262
16281
|
useCount: true,
|
|
@@ -17361,8 +17380,128 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17361
17380
|
message: "At least one field must be provided"
|
|
17362
17381
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17363
17382
|
|
|
17383
|
+
// ../../packages/schema/src/zod/vault.ts
|
|
17384
|
+
var POINTER_FORMAT_VERSION = 2;
|
|
17385
|
+
var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
|
|
17386
|
+
var POINTER_TOKEN_PATTERN = new RegExp(
|
|
17387
|
+
`\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
|
|
17388
|
+
);
|
|
17389
|
+
var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
|
|
17390
|
+
function pointerTokenScanner() {
|
|
17391
|
+
return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
|
|
17392
|
+
}
|
|
17393
|
+
var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
|
|
17394
|
+
var ParsedPointer = external_exports.object({
|
|
17395
|
+
category: DetectionCategory,
|
|
17396
|
+
keyVersion: external_exports.number().int().positive(),
|
|
17397
|
+
pointerId: external_exports.string(),
|
|
17398
|
+
tag: external_exports.string()
|
|
17399
|
+
});
|
|
17400
|
+
var VaultEntry = external_exports.object({
|
|
17401
|
+
pointerId: external_exports.string(),
|
|
17402
|
+
// The keyed HMAC of the raw value under `exception.key`, and the epoch it was
|
|
17403
|
+
// derived under. This is what a reveal-to-model grant matches on, and it rotates
|
|
17404
|
+
// independently of the vault encryption key below.
|
|
17405
|
+
valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
17406
|
+
fingerprintKeyVersion: external_exports.number().int().positive(),
|
|
17407
|
+
// The vault-key epoch this row's ciphertext was sealed under.
|
|
17408
|
+
keyVersion: external_exports.number().int().positive(),
|
|
17409
|
+
// Fixed at first mint and never updated: the same value detected later under a
|
|
17410
|
+
// different rule's category keeps the category it was minted with, so one
|
|
17411
|
+
// value always produces exactly one wire token.
|
|
17412
|
+
category: DetectionCategory,
|
|
17413
|
+
ruleId: external_exports.string(),
|
|
17414
|
+
// Partial-reveal preview for badges and listings. Never the raw value.
|
|
17415
|
+
maskedMatch: external_exports.string(),
|
|
17416
|
+
provider: external_exports.string().optional(),
|
|
17417
|
+
ciphertext: external_exports.string(),
|
|
17418
|
+
nonce: external_exports.string(),
|
|
17419
|
+
authTag: external_exports.string(),
|
|
17420
|
+
// How many times this value has been detected on this machine — the reuse
|
|
17421
|
+
// signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
|
|
17422
|
+
occurrenceCount: external_exports.number().int().nonnegative(),
|
|
17423
|
+
firstSeen: external_exports.string(),
|
|
17424
|
+
lastSeen: external_exports.string()
|
|
17425
|
+
});
|
|
17426
|
+
var PointerDescriptor = external_exports.object({
|
|
17427
|
+
category: DetectionCategory,
|
|
17428
|
+
provider: external_exports.string().optional(),
|
|
17429
|
+
maskedMatch: external_exports.string(),
|
|
17430
|
+
occurrences: external_exports.number().int().nonnegative(),
|
|
17431
|
+
firstSeen: external_exports.string(),
|
|
17432
|
+
lastSeen: external_exports.string()
|
|
17433
|
+
});
|
|
17434
|
+
var PointerIdentity = external_exports.object({
|
|
17435
|
+
ruleId: external_exports.string(),
|
|
17436
|
+
valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
17437
|
+
fingerprintKeyVersion: external_exports.number().int().positive()
|
|
17438
|
+
});
|
|
17439
|
+
var DetokenizeTarget = external_exports.enum(["human", "model"]);
|
|
17440
|
+
var VaultDerefReason = external_exports.enum([
|
|
17441
|
+
"display",
|
|
17442
|
+
"explicit-reveal",
|
|
17443
|
+
"view-render",
|
|
17444
|
+
"model-input",
|
|
17445
|
+
"remediation",
|
|
17446
|
+
"purge"
|
|
17447
|
+
]);
|
|
17448
|
+
var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
|
|
17449
|
+
var BATCHED_DEREF_REASONS = ["display", "view-render"];
|
|
17450
|
+
function isBatchedDerefReason(reason) {
|
|
17451
|
+
return BATCHED_DEREF_REASONS.includes(reason);
|
|
17452
|
+
}
|
|
17453
|
+
var VaultDeref = external_exports.object({
|
|
17454
|
+
id: external_exports.guid(),
|
|
17455
|
+
pointerId: external_exports.string(),
|
|
17456
|
+
at: external_exports.string(),
|
|
17457
|
+
target: DetokenizeTarget,
|
|
17458
|
+
reason: VaultDerefReason,
|
|
17459
|
+
outcome: VaultDerefOutcome,
|
|
17460
|
+
// Present only on a model-target crossing that a reveal grant authorized.
|
|
17461
|
+
grantId: external_exports.string().optional(),
|
|
17462
|
+
// How many pointers ONE batched render resolved. 1 for unbatched rows. Named
|
|
17463
|
+
// apart from VaultEntry.occurrenceCount, which counts detections of a value.
|
|
17464
|
+
pointerCount: external_exports.number().int().positive().default(1)
|
|
17465
|
+
});
|
|
17466
|
+
var VaultSightingKind = external_exports.enum([
|
|
17467
|
+
"prompt",
|
|
17468
|
+
"tool-input",
|
|
17469
|
+
"tool-output",
|
|
17470
|
+
"file",
|
|
17471
|
+
"transcript"
|
|
17472
|
+
]);
|
|
17473
|
+
var VaultSighting = external_exports.object({
|
|
17474
|
+
location: external_exports.string(),
|
|
17475
|
+
kind: VaultSightingKind,
|
|
17476
|
+
firstSeen: external_exports.string(),
|
|
17477
|
+
lastSeen: external_exports.string()
|
|
17478
|
+
});
|
|
17479
|
+
var VaultInventoryEntry = external_exports.object({
|
|
17480
|
+
pointerId: external_exports.string(),
|
|
17481
|
+
category: DetectionCategory,
|
|
17482
|
+
provider: external_exports.string().optional(),
|
|
17483
|
+
maskedMatch: external_exports.string(),
|
|
17484
|
+
occurrences: external_exports.number().int().nonnegative(),
|
|
17485
|
+
firstSeen: external_exports.string(),
|
|
17486
|
+
lastSeen: external_exports.string(),
|
|
17487
|
+
// The active reveal-to-model grant covering this value, when one exists —
|
|
17488
|
+
// the inventory badges it, the row links to revocation.
|
|
17489
|
+
revealGrantId: external_exports.string().nullable(),
|
|
17490
|
+
sightings: external_exports.array(VaultSighting)
|
|
17491
|
+
});
|
|
17492
|
+
var VaultKeyCustody = external_exports.string();
|
|
17493
|
+
var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
|
|
17494
|
+
var VAULT_CONSENT_VERSION = 1;
|
|
17495
|
+
var VaultConsent = external_exports.object({
|
|
17496
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17497
|
+
version: external_exports.number().int().positive()
|
|
17498
|
+
});
|
|
17499
|
+
function isVaultConsentValid(consent) {
|
|
17500
|
+
return consent?.version === VAULT_CONSENT_VERSION;
|
|
17501
|
+
}
|
|
17502
|
+
|
|
17364
17503
|
// ../../packages/schema/src/zod/local.ts
|
|
17365
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17504
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
|
|
17366
17505
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17367
17506
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17368
17507
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
@@ -17384,6 +17523,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17384
17523
|
// In-place egress extraction on the scan paths; disable to stop all Data
|
|
17385
17524
|
// Shares writes.
|
|
17386
17525
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17526
|
+
// Consent to keep a RECOVERABLE encrypted copy of detected values in the local
|
|
17527
|
+
// vault, instead of destroying them. Absent by default: this is a custody
|
|
17528
|
+
// change from one-way redaction, so it is never an assumed grant on upgrade.
|
|
17529
|
+
// Revoking stops future vaulting; it does not erase what is already stored —
|
|
17530
|
+
// purging the vault is the eraser.
|
|
17531
|
+
vaultConsent: VaultConsent.optional(),
|
|
17532
|
+
// Where the vault master key lives.
|
|
17533
|
+
vaultKeyCustody: VaultKeyCustody.default("file"),
|
|
17534
|
+
// How a pointer renders in assistant prose on screen (see VaultInlineReveal).
|
|
17535
|
+
vaultInlineReveal: VaultInlineReveal.default("masked"),
|
|
17387
17536
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17388
17537
|
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17389
17538
|
// Records that the user consented to sending findings to the model API for
|
|
@@ -19798,6 +19947,9 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
19798
19947
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
19799
19948
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
19800
19949
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
19950
|
+
var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
|
|
19951
|
+
AND conditions IS NULL
|
|
19952
|
+
AND ${ACTIVE_PREDICATE}`;
|
|
19801
19953
|
var SqliteExceptionsRepository = class {
|
|
19802
19954
|
constructor(db) {
|
|
19803
19955
|
this.db = db;
|
|
@@ -19889,11 +20041,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19889
20041
|
this.db.prepare(
|
|
19890
20042
|
`INSERT INTO exceptions (
|
|
19891
20043
|
id, rule_id, category, value_fingerprint, key_version, masked_value,
|
|
19892
|
-
scope, expires_at, max_uses, use_count, last_used_at,
|
|
19893
|
-
conditions, created_by, created_via, created_at, updated_at
|
|
20044
|
+
capability, scope, expires_at, max_uses, use_count, last_used_at,
|
|
20045
|
+
justification, conditions, created_by, created_via, created_at, updated_at
|
|
19894
20046
|
) VALUES (
|
|
19895
20047
|
:id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
|
|
19896
|
-
:scope, :expiresAt, :maxUses, 0, NULL, :justification,
|
|
20048
|
+
:capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
|
|
19897
20049
|
:conditions, :createdBy, :createdVia, :now, :now
|
|
19898
20050
|
)`
|
|
19899
20051
|
).run({
|
|
@@ -19903,6 +20055,7 @@ var SqliteExceptionsRepository = class {
|
|
|
19903
20055
|
valueFingerprint: input.valueFingerprint,
|
|
19904
20056
|
keyVersion: input.keyVersion,
|
|
19905
20057
|
maskedValue: input.maskedValue,
|
|
20058
|
+
capability: input.capability ?? "suppress",
|
|
19906
20059
|
scope: input.scope,
|
|
19907
20060
|
expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
|
|
19908
20061
|
maxUses: input.maxUses,
|
|
@@ -19996,6 +20149,7 @@ var SqliteExceptionsRepository = class {
|
|
|
19996
20149
|
ruleId: row.rule_id,
|
|
19997
20150
|
valueFingerprint: row.value_fingerprint,
|
|
19998
20151
|
keyVersion: row.key_version,
|
|
20152
|
+
capability: row.capability,
|
|
19999
20153
|
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
20000
20154
|
maxUses: row.max_uses,
|
|
20001
20155
|
useCount: row.use_count,
|
|
@@ -20050,6 +20204,35 @@ var SqliteExceptionsRepository = class {
|
|
|
20050
20204
|
}))
|
|
20051
20205
|
);
|
|
20052
20206
|
}
|
|
20207
|
+
/**
|
|
20208
|
+
* The active reveal-to-model grant for a vaulted value's identity, or null.
|
|
20209
|
+
* Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
|
|
20210
|
+
* suppression uses — plus the capability: a suppression grant must never
|
|
20211
|
+
* authorize a reveal. Read-only: the caller does NOT consume here, because a
|
|
20212
|
+
* revealed value re-enters the detection scan immediately afterward and the
|
|
20213
|
+
* suppression match there claims the use — one crossing, one use.
|
|
20214
|
+
*
|
|
20215
|
+
* A grant with `conditions` NEVER matches here: the reveal path does not yet
|
|
20216
|
+
* evaluate conditions, and a narrowing clause that is ignored would WIDEN the
|
|
20217
|
+
* grant instead. Fail closed until reveal-side condition evaluation exists.
|
|
20218
|
+
*/
|
|
20219
|
+
activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
|
|
20220
|
+
try {
|
|
20221
|
+
const row = getRow(
|
|
20222
|
+
this.db.prepare(
|
|
20223
|
+
`SELECT id FROM exceptions
|
|
20224
|
+
WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
|
|
20225
|
+
AND key_version = :keyVersion
|
|
20226
|
+
AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
|
|
20227
|
+
LIMIT 1`
|
|
20228
|
+
),
|
|
20229
|
+
{ ruleId, valueFingerprint, keyVersion, now }
|
|
20230
|
+
);
|
|
20231
|
+
return Promise.resolve(row ?? null);
|
|
20232
|
+
} catch (err) {
|
|
20233
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
20234
|
+
}
|
|
20235
|
+
}
|
|
20053
20236
|
/**
|
|
20054
20237
|
* Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
|
|
20055
20238
|
* exhausted) whose last transition is older than the retention window.
|
|
@@ -20077,6 +20260,7 @@ function parseExceptionRow(row) {
|
|
|
20077
20260
|
valueFingerprint: row.value_fingerprint,
|
|
20078
20261
|
keyVersion: row.key_version,
|
|
20079
20262
|
maskedValue: row.masked_value,
|
|
20263
|
+
capability: row.capability,
|
|
20080
20264
|
scope: row.scope,
|
|
20081
20265
|
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
20082
20266
|
maxUses: row.max_uses,
|
|
@@ -22242,6 +22426,287 @@ var SqliteScanLedgerRepository = class {
|
|
|
22242
22426
|
}
|
|
22243
22427
|
};
|
|
22244
22428
|
|
|
22429
|
+
// ../../packages/persistence/src/repositories/secret-vault.ts
|
|
22430
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
22431
|
+
var SELECT_COLUMNS = `
|
|
22432
|
+
pointer_id AS pointerId,
|
|
22433
|
+
value_fingerprint AS valueFingerprint,
|
|
22434
|
+
fingerprint_key_version AS fingerprintKeyVersion,
|
|
22435
|
+
key_version AS keyVersion,
|
|
22436
|
+
format_version AS formatVersion,
|
|
22437
|
+
category,
|
|
22438
|
+
rule_id AS ruleId,
|
|
22439
|
+
masked_match AS maskedMatch,
|
|
22440
|
+
provider,
|
|
22441
|
+
ciphertext,
|
|
22442
|
+
nonce,
|
|
22443
|
+
auth_tag AS authTag,
|
|
22444
|
+
occurrence_count AS occurrenceCount,
|
|
22445
|
+
first_seen AS firstSeen,
|
|
22446
|
+
last_seen AS lastSeen`;
|
|
22447
|
+
function toRow(raw) {
|
|
22448
|
+
const { provider, ...rest } = raw;
|
|
22449
|
+
return provider === null ? rest : { ...rest, provider };
|
|
22450
|
+
}
|
|
22451
|
+
var SqliteSecretVaultRepository = class {
|
|
22452
|
+
constructor(db) {
|
|
22453
|
+
this.db = db;
|
|
22454
|
+
this.insertStmt = db.prepare(
|
|
22455
|
+
`INSERT INTO secret_vault (
|
|
22456
|
+
pointer_id, value_fingerprint, fingerprint_key_version, key_version,
|
|
22457
|
+
format_version, category, rule_id, masked_match, provider,
|
|
22458
|
+
ciphertext, nonce, auth_tag,
|
|
22459
|
+
occurrence_count, first_seen, last_seen
|
|
22460
|
+
) VALUES (
|
|
22461
|
+
:pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
|
|
22462
|
+
:formatVersion, :category, :ruleId, :maskedMatch, :provider,
|
|
22463
|
+
:ciphertext, :nonce, :authTag,
|
|
22464
|
+
1, :now, :now
|
|
22465
|
+
)`
|
|
22466
|
+
);
|
|
22467
|
+
this.bumpStmt = db.prepare(
|
|
22468
|
+
`UPDATE secret_vault
|
|
22469
|
+
SET occurrence_count = occurrence_count + 1, last_seen = :now
|
|
22470
|
+
WHERE value_fingerprint = :valueFingerprint`
|
|
22471
|
+
);
|
|
22472
|
+
this.byPointerStmt = db.prepare(
|
|
22473
|
+
`SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
|
|
22474
|
+
);
|
|
22475
|
+
this.byFingerprintStmt = db.prepare(
|
|
22476
|
+
`SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
|
|
22477
|
+
);
|
|
22478
|
+
this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
|
|
22479
|
+
this.replaceCiphertextStmt = db.prepare(
|
|
22480
|
+
`UPDATE secret_vault
|
|
22481
|
+
SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
|
|
22482
|
+
WHERE pointer_id = :pointerId`
|
|
22483
|
+
);
|
|
22484
|
+
this.refreshFingerprintStmt = db.prepare(
|
|
22485
|
+
`UPDATE secret_vault
|
|
22486
|
+
SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
|
|
22487
|
+
WHERE pointer_id = :pointerId`
|
|
22488
|
+
);
|
|
22489
|
+
this.derefStmt = db.prepare(
|
|
22490
|
+
`INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
|
|
22491
|
+
VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
|
|
22492
|
+
);
|
|
22493
|
+
}
|
|
22494
|
+
db;
|
|
22495
|
+
insertStmt;
|
|
22496
|
+
bumpStmt;
|
|
22497
|
+
byPointerStmt;
|
|
22498
|
+
byFingerprintStmt;
|
|
22499
|
+
listStmt;
|
|
22500
|
+
replaceCiphertextStmt;
|
|
22501
|
+
refreshFingerprintStmt;
|
|
22502
|
+
derefStmt;
|
|
22503
|
+
/**
|
|
22504
|
+
* Vault a value, or record another sighting of one already vaulted. Keyed on
|
|
22505
|
+
* `valueFingerprint`, never on the caller's pointer id: a value seen again
|
|
22506
|
+
* bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
|
|
22507
|
+
* pointer, category and ciphertext, so the same secret always resolves to one
|
|
22508
|
+
* wire token. `minted` is true only when this call created the row.
|
|
22509
|
+
*
|
|
22510
|
+
* The read-then-write runs in one IMMEDIATE transaction so two concurrent
|
|
22511
|
+
* writers cannot both decide they are minting.
|
|
22512
|
+
*/
|
|
22513
|
+
upsert(input, now) {
|
|
22514
|
+
let minted = false;
|
|
22515
|
+
withTransaction(
|
|
22516
|
+
this.db,
|
|
22517
|
+
() => {
|
|
22518
|
+
const existing = getRow(this.byFingerprintStmt, {
|
|
22519
|
+
valueFingerprint: input.valueFingerprint
|
|
22520
|
+
});
|
|
22521
|
+
if (existing === void 0) {
|
|
22522
|
+
this.insertStmt.run(
|
|
22523
|
+
bindParams({
|
|
22524
|
+
pointerId: input.pointerId,
|
|
22525
|
+
valueFingerprint: input.valueFingerprint,
|
|
22526
|
+
fingerprintKeyVersion: input.fingerprintKeyVersion,
|
|
22527
|
+
keyVersion: input.keyVersion,
|
|
22528
|
+
formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
|
|
22529
|
+
category: input.category,
|
|
22530
|
+
ruleId: input.ruleId,
|
|
22531
|
+
maskedMatch: input.maskedMatch,
|
|
22532
|
+
provider: input.provider,
|
|
22533
|
+
ciphertext: input.ciphertext,
|
|
22534
|
+
nonce: input.nonce,
|
|
22535
|
+
authTag: input.authTag,
|
|
22536
|
+
now
|
|
22537
|
+
})
|
|
22538
|
+
);
|
|
22539
|
+
minted = true;
|
|
22540
|
+
return;
|
|
22541
|
+
}
|
|
22542
|
+
this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
|
|
22543
|
+
},
|
|
22544
|
+
"IMMEDIATE"
|
|
22545
|
+
);
|
|
22546
|
+
const row = getRow(this.byFingerprintStmt, {
|
|
22547
|
+
valueFingerprint: input.valueFingerprint
|
|
22548
|
+
});
|
|
22549
|
+
if (row === void 0) throw new Error("vault: row vanished immediately after write");
|
|
22550
|
+
return { row: toRow(row), minted };
|
|
22551
|
+
}
|
|
22552
|
+
byPointerId(pointerId) {
|
|
22553
|
+
const raw = getRow(this.byPointerStmt, { pointerId });
|
|
22554
|
+
return raw === void 0 ? null : toRow(raw);
|
|
22555
|
+
}
|
|
22556
|
+
byValueFingerprint(fingerprint) {
|
|
22557
|
+
const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
|
|
22558
|
+
return raw === void 0 ? null : toRow(raw);
|
|
22559
|
+
}
|
|
22560
|
+
/** Append one audit row. Carries no raw value and no ciphertext, by shape. */
|
|
22561
|
+
recordDeref(entry) {
|
|
22562
|
+
this.derefStmt.run(
|
|
22563
|
+
bindParams({
|
|
22564
|
+
id: entry.id,
|
|
22565
|
+
pointerId: entry.pointerId,
|
|
22566
|
+
at: entry.at,
|
|
22567
|
+
target: entry.target,
|
|
22568
|
+
reason: entry.reason,
|
|
22569
|
+
outcome: entry.outcome,
|
|
22570
|
+
grantId: entry.grantId,
|
|
22571
|
+
pointerCount: entry.pointerCount ?? 1
|
|
22572
|
+
})
|
|
22573
|
+
);
|
|
22574
|
+
}
|
|
22575
|
+
listAll() {
|
|
22576
|
+
return allRows(this.listStmt).map(toRow);
|
|
22577
|
+
}
|
|
22578
|
+
/** Re-seal an entry under a new key epoch, leaving its identity untouched. */
|
|
22579
|
+
replaceCiphertext(pointerId, next) {
|
|
22580
|
+
this.replaceCiphertextStmt.run({ pointerId, ...next });
|
|
22581
|
+
}
|
|
22582
|
+
/** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
|
|
22583
|
+
refreshFingerprint(pointerId, next) {
|
|
22584
|
+
this.refreshFingerprintStmt.run({ pointerId, ...next });
|
|
22585
|
+
}
|
|
22586
|
+
/**
|
|
22587
|
+
* Destroy every vaulted value and report how many were destroyed. The deref
|
|
22588
|
+
* audit is left alone on purpose — see the table note above.
|
|
22589
|
+
*/
|
|
22590
|
+
purgeAll() {
|
|
22591
|
+
let destroyed = 0;
|
|
22592
|
+
withTransaction(
|
|
22593
|
+
this.db,
|
|
22594
|
+
() => {
|
|
22595
|
+
destroyed = this.countEntries();
|
|
22596
|
+
this.db.exec("DELETE FROM secret_vault");
|
|
22597
|
+
},
|
|
22598
|
+
"IMMEDIATE"
|
|
22599
|
+
);
|
|
22600
|
+
return destroyed;
|
|
22601
|
+
}
|
|
22602
|
+
/**
|
|
22603
|
+
* Record (or re-stamp) one place a pointer has been written. One row per
|
|
22604
|
+
* (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
|
|
22605
|
+
* on hook paths — a failure must never affect the rewrite that triggered it,
|
|
22606
|
+
* so callers wrap this, not the other way around.
|
|
22607
|
+
*/
|
|
22608
|
+
recordSighting(entry, now) {
|
|
22609
|
+
this.db.prepare(
|
|
22610
|
+
`INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
|
|
22611
|
+
VALUES (:id, :pointerId, :location, :kind, :now, :now)
|
|
22612
|
+
ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
|
|
22613
|
+
).run({
|
|
22614
|
+
id: randomUUID7(),
|
|
22615
|
+
pointerId: entry.pointerId,
|
|
22616
|
+
location: entry.location,
|
|
22617
|
+
kind: entry.kind,
|
|
22618
|
+
now
|
|
22619
|
+
});
|
|
22620
|
+
}
|
|
22621
|
+
listSightings(pointerId) {
|
|
22622
|
+
const rows = allRows(
|
|
22623
|
+
this.db.prepare(
|
|
22624
|
+
`SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
|
|
22625
|
+
WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
|
|
22626
|
+
),
|
|
22627
|
+
{ pointerId }
|
|
22628
|
+
);
|
|
22629
|
+
return rows.map((r) => ({
|
|
22630
|
+
location: r.location,
|
|
22631
|
+
kind: r.kind,
|
|
22632
|
+
firstSeen: new Date(r.first_seen).toISOString(),
|
|
22633
|
+
lastSeen: new Date(r.last_seen).toISOString()
|
|
22634
|
+
}));
|
|
22635
|
+
}
|
|
22636
|
+
/**
|
|
22637
|
+
* The dashboard inventory: every vaulted value's descriptor data joined with
|
|
22638
|
+
* its sightings and the active reveal-to-model grant when one exists.
|
|
22639
|
+
* Raw-free by construction — neither the fingerprint nor the ciphertext
|
|
22640
|
+
* columns are selected.
|
|
22641
|
+
*/
|
|
22642
|
+
listInventory(now = Date.now()) {
|
|
22643
|
+
const rows = allRows(
|
|
22644
|
+
this.db.prepare(
|
|
22645
|
+
`SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
|
|
22646
|
+
v.occurrence_count, v.first_seen, v.last_seen,
|
|
22647
|
+
(SELECT e.id FROM exceptions e
|
|
22648
|
+
WHERE e.rule_id = v.rule_id
|
|
22649
|
+
AND e.value_fingerprint = v.value_fingerprint
|
|
22650
|
+
AND e.key_version = v.fingerprint_key_version
|
|
22651
|
+
AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
|
|
22652
|
+
LIMIT 1) AS grant_id
|
|
22653
|
+
FROM secret_vault v
|
|
22654
|
+
ORDER BY v.last_seen DESC`
|
|
22655
|
+
),
|
|
22656
|
+
{ now }
|
|
22657
|
+
);
|
|
22658
|
+
return rows.map((r) => ({
|
|
22659
|
+
pointerId: r.pointer_id,
|
|
22660
|
+
category: r.category,
|
|
22661
|
+
...r.provider === null ? {} : { provider: r.provider },
|
|
22662
|
+
maskedMatch: r.masked_match,
|
|
22663
|
+
occurrences: r.occurrence_count,
|
|
22664
|
+
firstSeen: new Date(r.first_seen).toISOString(),
|
|
22665
|
+
lastSeen: new Date(r.last_seen).toISOString(),
|
|
22666
|
+
revealGrantId: r.grant_id,
|
|
22667
|
+
sightings: this.listSightings(r.pointer_id)
|
|
22668
|
+
}));
|
|
22669
|
+
}
|
|
22670
|
+
/**
|
|
22671
|
+
* The de-reference trail, newest first. By default the batched, high-volume
|
|
22672
|
+
* reasons (display, view-render) are hidden and counted instead — the rows
|
|
22673
|
+
* that matter as a signal are the model crossings, and burying them under
|
|
22674
|
+
* render noise would defeat the audit's purpose.
|
|
22675
|
+
*/
|
|
22676
|
+
listDerefs(opts) {
|
|
22677
|
+
const limit = opts?.limit ?? 200;
|
|
22678
|
+
const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
|
|
22679
|
+
const rows = allRows(
|
|
22680
|
+
this.db.prepare(
|
|
22681
|
+
`SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
|
|
22682
|
+
FROM secret_vault_deref ${where}
|
|
22683
|
+
ORDER BY at DESC, rowid DESC LIMIT :limit`
|
|
22684
|
+
),
|
|
22685
|
+
{ limit }
|
|
22686
|
+
);
|
|
22687
|
+
const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
|
|
22688
|
+
this.db,
|
|
22689
|
+
`SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
|
|
22690
|
+
);
|
|
22691
|
+
return {
|
|
22692
|
+
rows: rows.map((r) => ({
|
|
22693
|
+
id: r.id,
|
|
22694
|
+
pointerId: r.pointer_id,
|
|
22695
|
+
at: new Date(r.at).toISOString(),
|
|
22696
|
+
target: r.target,
|
|
22697
|
+
reason: r.reason,
|
|
22698
|
+
outcome: r.outcome,
|
|
22699
|
+
...r.grant_id === null ? {} : { grantId: r.grant_id },
|
|
22700
|
+
pointerCount: r.pointer_count
|
|
22701
|
+
})),
|
|
22702
|
+
hiddenBatched
|
|
22703
|
+
};
|
|
22704
|
+
}
|
|
22705
|
+
countEntries() {
|
|
22706
|
+
return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
|
|
22707
|
+
}
|
|
22708
|
+
};
|
|
22709
|
+
|
|
22245
22710
|
// ../../packages/persistence/src/repositories/security.ts
|
|
22246
22711
|
var DAY_MS4 = 864e5;
|
|
22247
22712
|
var SEVERITIES = ["critical", "high", "medium", "low"];
|
|
@@ -22587,7 +23052,7 @@ var SqliteSecurityRepository = class {
|
|
|
22587
23052
|
};
|
|
22588
23053
|
|
|
22589
23054
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
22590
|
-
import { randomUUID as
|
|
23055
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
22591
23056
|
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
22592
23057
|
var IN_CHUNK = 500;
|
|
22593
23058
|
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
@@ -22843,7 +23308,7 @@ var SqliteSharesRepository = class {
|
|
|
22843
23308
|
(id, destination_id, host, decision, created_at, updated_at)
|
|
22844
23309
|
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
22845
23310
|
).run({
|
|
22846
|
-
id:
|
|
23311
|
+
id: randomUUID8(),
|
|
22847
23312
|
destinationId,
|
|
22848
23313
|
host: dest.host,
|
|
22849
23314
|
decision,
|
|
@@ -22992,7 +23457,7 @@ var SqliteSharesRepository = class {
|
|
|
22992
23457
|
let destinationId = destIds.get(hit.host);
|
|
22993
23458
|
if (destinationId === void 0) {
|
|
22994
23459
|
destStmt.run({
|
|
22995
|
-
id:
|
|
23460
|
+
id: randomUUID8(),
|
|
22996
23461
|
kind: hit.kind,
|
|
22997
23462
|
name: hit.name,
|
|
22998
23463
|
host: hit.host,
|
|
@@ -23008,7 +23473,7 @@ var SqliteSharesRepository = class {
|
|
|
23008
23473
|
let endpointId = endpointIds.get(endpointKey);
|
|
23009
23474
|
if (endpointId === void 0) {
|
|
23010
23475
|
endpointStmt.run({
|
|
23011
|
-
id:
|
|
23476
|
+
id: randomUUID8(),
|
|
23012
23477
|
destinationId,
|
|
23013
23478
|
method: hit.method,
|
|
23014
23479
|
transport: hit.transport,
|
|
@@ -23021,7 +23486,7 @@ var SqliteSharesRepository = class {
|
|
|
23021
23486
|
endpointIds.set(endpointKey, endpointId);
|
|
23022
23487
|
}
|
|
23023
23488
|
siteStmt.run({
|
|
23024
|
-
id:
|
|
23489
|
+
id: randomUUID8(),
|
|
23025
23490
|
endpointId,
|
|
23026
23491
|
project: input.project,
|
|
23027
23492
|
projectKey: input.projectKey,
|
|
@@ -23437,6 +23902,7 @@ function openAndInitialize(file2) {
|
|
|
23437
23902
|
policies,
|
|
23438
23903
|
installedPacks,
|
|
23439
23904
|
scanLedger: new SqliteScanLedgerRepository(db),
|
|
23905
|
+
secretVault: new SqliteSecretVaultRepository(db),
|
|
23440
23906
|
exceptions: new SqliteExceptionsRepository(db),
|
|
23441
23907
|
resolutions: new SqliteResolutionsRepository(db),
|
|
23442
23908
|
ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
|
|
@@ -23472,6 +23938,7 @@ function openLocalDatabase(dir) {
|
|
|
23472
23938
|
policies,
|
|
23473
23939
|
installedPacks,
|
|
23474
23940
|
scanLedger,
|
|
23941
|
+
secretVault,
|
|
23475
23942
|
exceptions,
|
|
23476
23943
|
resolutions,
|
|
23477
23944
|
ruleProbeCache,
|
|
@@ -23580,7 +24047,7 @@ function openLocalDatabase(dir) {
|
|
|
23580
24047
|
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
23581
24048
|
if (!definitionId) continue;
|
|
23582
24049
|
inspectionFindings.insertFinding({
|
|
23583
|
-
id:
|
|
24050
|
+
id: randomUUID9(),
|
|
23584
24051
|
auditEventId: record2.scanEvent.id,
|
|
23585
24052
|
inspectionDefinitionId: definitionId,
|
|
23586
24053
|
span: finding.span,
|
|
@@ -23657,6 +24124,7 @@ function openLocalDatabase(dir) {
|
|
|
23657
24124
|
policies,
|
|
23658
24125
|
installedPacks,
|
|
23659
24126
|
scanLedger,
|
|
24127
|
+
secretVault,
|
|
23660
24128
|
exceptions,
|
|
23661
24129
|
resolutions,
|
|
23662
24130
|
ruleProbeCache,
|
|
@@ -23689,6 +24157,26 @@ function openLocalDatabase(dir) {
|
|
|
23689
24157
|
};
|
|
23690
24158
|
}
|
|
23691
24159
|
|
|
24160
|
+
// ../../packages/persistence/src/exception-policy.ts
|
|
24161
|
+
var UserGrantPolicyProvider = class {
|
|
24162
|
+
#exceptions;
|
|
24163
|
+
constructor(exceptions) {
|
|
24164
|
+
this.#exceptions = exceptions;
|
|
24165
|
+
}
|
|
24166
|
+
async decideReveal(identity) {
|
|
24167
|
+
try {
|
|
24168
|
+
const grant = await this.#exceptions.activeRevealGrant(
|
|
24169
|
+
identity.ruleId,
|
|
24170
|
+
identity.valueFingerprint,
|
|
24171
|
+
identity.fingerprintKeyVersion
|
|
24172
|
+
);
|
|
24173
|
+
return grant === null ? { allow: false } : { allow: true, grantId: grant.id };
|
|
24174
|
+
} catch {
|
|
24175
|
+
return { allow: false };
|
|
24176
|
+
}
|
|
24177
|
+
}
|
|
24178
|
+
};
|
|
24179
|
+
|
|
23692
24180
|
// ../../packages/persistence/src/finding-key.ts
|
|
23693
24181
|
import { createHash as createHash3 } from "crypto";
|
|
23694
24182
|
function normalizeFilePath(filePath) {
|
|
@@ -23727,7 +24215,11 @@ function parseKeyFile(raw) {
|
|
|
23727
24215
|
}
|
|
23728
24216
|
return { version: version2, material: bytes };
|
|
23729
24217
|
}
|
|
23730
|
-
var
|
|
24218
|
+
var KEY_VERSION_COLUMNS = {
|
|
24219
|
+
exceptions: "key_version",
|
|
24220
|
+
blocked_detections: "key_version",
|
|
24221
|
+
secret_vault: "fingerprint_key_version"
|
|
24222
|
+
};
|
|
23731
24223
|
var SQLITE_ERROR = 1;
|
|
23732
24224
|
var FLOOR_BUSY_TIMEOUT_MS = 250;
|
|
23733
24225
|
var FloorUnreadableError = class extends Error {
|
|
@@ -23748,10 +24240,10 @@ function storedKeyVersionFloor(dataDir2) {
|
|
|
23748
24240
|
db = new DatabaseSync2(file2, { readOnly: true });
|
|
23749
24241
|
db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
|
|
23750
24242
|
let floor = 0;
|
|
23751
|
-
for (const table2 of
|
|
24243
|
+
for (const [table2, column] of Object.entries(KEY_VERSION_COLUMNS)) {
|
|
23752
24244
|
try {
|
|
23753
24245
|
const row = getRow(
|
|
23754
|
-
db.prepare(`SELECT MAX(
|
|
24246
|
+
db.prepare(`SELECT MAX(${column}) AS v FROM ${table2}`)
|
|
23755
24247
|
);
|
|
23756
24248
|
floor = Math.max(floor, row?.v ?? 0);
|
|
23757
24249
|
} catch (err) {
|
|
@@ -23817,6 +24309,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
23817
24309
|
function dbPath(base = defaultDataDir()) {
|
|
23818
24310
|
return join3(dataDir(base), "aka.db");
|
|
23819
24311
|
}
|
|
24312
|
+
function keysDir(base = defaultDataDir()) {
|
|
24313
|
+
return join3(base, "keys");
|
|
24314
|
+
}
|
|
23820
24315
|
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23821
24316
|
ensureDataDirSync(dir);
|
|
23822
24317
|
}
|
|
@@ -23858,23 +24353,884 @@ function readJson(file2) {
|
|
|
23858
24353
|
return parseJsonObject(text) ?? null;
|
|
23859
24354
|
}
|
|
23860
24355
|
|
|
23861
|
-
// ../../packages/persistence/src/
|
|
23862
|
-
import {
|
|
24356
|
+
// ../../packages/persistence/src/vault/crypto.ts
|
|
24357
|
+
import {
|
|
24358
|
+
createCipheriv,
|
|
24359
|
+
createDecipheriv,
|
|
24360
|
+
createHmac as createHmac2,
|
|
24361
|
+
hkdfSync,
|
|
24362
|
+
timingSafeEqual
|
|
24363
|
+
} from "crypto";
|
|
24364
|
+
var POINTER_ID_BYTES = 16;
|
|
24365
|
+
var NONCE_BYTES = 12;
|
|
24366
|
+
var TAG_BYTES = 10;
|
|
24367
|
+
var SUBKEY_BYTES = 32;
|
|
24368
|
+
var HKDF_INFO_ENC = "aka:vault:enc:v1";
|
|
24369
|
+
var HKDF_INFO_SIGN = "aka:vault:sign:v1";
|
|
24370
|
+
var HKDF_SALT = "aka:vault:v1";
|
|
24371
|
+
var B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
24372
|
+
function base32Encode(bytes) {
|
|
24373
|
+
let out = "";
|
|
24374
|
+
let buffer = 0;
|
|
24375
|
+
let bits = 0;
|
|
24376
|
+
for (const byte of bytes) {
|
|
24377
|
+
buffer = buffer << 8 | byte;
|
|
24378
|
+
bits += 8;
|
|
24379
|
+
while (bits >= 5) {
|
|
24380
|
+
out += B32_ALPHABET.charAt(buffer >>> bits - 5 & 31);
|
|
24381
|
+
bits -= 5;
|
|
24382
|
+
}
|
|
24383
|
+
}
|
|
24384
|
+
if (bits > 0) out += B32_ALPHABET.charAt(buffer << 5 - bits & 31);
|
|
24385
|
+
return out;
|
|
24386
|
+
}
|
|
24387
|
+
function base32Decode(text) {
|
|
24388
|
+
const out = [];
|
|
24389
|
+
let buffer = 0;
|
|
24390
|
+
let bits = 0;
|
|
24391
|
+
for (const char of text) {
|
|
24392
|
+
const value = B32_ALPHABET.indexOf(char);
|
|
24393
|
+
if (value < 0) throw new Error("base32: character outside the alphabet");
|
|
24394
|
+
buffer = buffer << 5 | value;
|
|
24395
|
+
bits += 5;
|
|
24396
|
+
if (bits >= 8) {
|
|
24397
|
+
out.push(buffer >>> bits - 8 & 255);
|
|
24398
|
+
bits -= 8;
|
|
24399
|
+
}
|
|
24400
|
+
}
|
|
24401
|
+
return Buffer.from(out);
|
|
24402
|
+
}
|
|
24403
|
+
function encodeKeyVersion(version2) {
|
|
24404
|
+
if (!Number.isInteger(version2) || version2 < 1 || version2 > 4294967295) {
|
|
24405
|
+
throw new Error("vault: key version out of range");
|
|
24406
|
+
}
|
|
24407
|
+
const bytes = [];
|
|
24408
|
+
let remaining = version2;
|
|
24409
|
+
while (remaining > 0) {
|
|
24410
|
+
bytes.unshift(remaining & 255);
|
|
24411
|
+
remaining = Math.floor(remaining / 256);
|
|
24412
|
+
}
|
|
24413
|
+
return base32Encode(Uint8Array.from(bytes));
|
|
24414
|
+
}
|
|
24415
|
+
function decodeKeyVersion(encoded) {
|
|
24416
|
+
const bytes = base32Decode(encoded);
|
|
24417
|
+
if (bytes.length === 0 || bytes.length > 4) throw new Error("vault: bad key version encoding");
|
|
24418
|
+
let version2 = 0;
|
|
24419
|
+
for (const byte of bytes) version2 = version2 * 256 + byte;
|
|
24420
|
+
if (version2 < 1) throw new Error("vault: bad key version");
|
|
24421
|
+
return version2;
|
|
24422
|
+
}
|
|
24423
|
+
function deriveSubkeys(master) {
|
|
24424
|
+
const derive = (info) => Buffer.from(hkdfSync("sha256", master, HKDF_SALT, info, SUBKEY_BYTES));
|
|
24425
|
+
return { enc: derive(HKDF_INFO_ENC), sign: derive(HKDF_INFO_SIGN) };
|
|
24426
|
+
}
|
|
24427
|
+
function bindingInput(keyVersion, pointerId, category, formatVersion = POINTER_FORMAT_VERSION) {
|
|
24428
|
+
if (pointerId.length !== POINTER_ID_BYTES) {
|
|
24429
|
+
throw new Error("vault: pointer id must be 16 bytes");
|
|
24430
|
+
}
|
|
24431
|
+
const head = Buffer.alloc(6);
|
|
24432
|
+
head.writeUInt16BE(formatVersion, 0);
|
|
24433
|
+
head.writeUInt32BE(keyVersion, 2);
|
|
24434
|
+
return Buffer.concat([head, Buffer.from(pointerId), Buffer.from(category, "utf8")]);
|
|
24435
|
+
}
|
|
24436
|
+
function seal(encKey, plaintext, aad, nonce) {
|
|
24437
|
+
const cipher = createCipheriv("aes-256-gcm", encKey, nonce);
|
|
24438
|
+
cipher.setAAD(aad);
|
|
24439
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
24440
|
+
return { ciphertext, nonce, authTag: cipher.getAuthTag() };
|
|
24441
|
+
}
|
|
24442
|
+
function open(encKey, sealed, aad) {
|
|
24443
|
+
try {
|
|
24444
|
+
const decipher = createDecipheriv("aes-256-gcm", encKey, sealed.nonce);
|
|
24445
|
+
decipher.setAAD(aad);
|
|
24446
|
+
decipher.setAuthTag(sealed.authTag);
|
|
24447
|
+
return Buffer.concat([decipher.update(sealed.ciphertext), decipher.final()]).toString("utf8");
|
|
24448
|
+
} catch {
|
|
24449
|
+
return null;
|
|
24450
|
+
}
|
|
24451
|
+
}
|
|
24452
|
+
function signPointer(signKey, keyVersion, pointerId, category) {
|
|
24453
|
+
return createHmac2("sha256", signKey).update(bindingInput(keyVersion, pointerId, category, POINTER_FORMAT_VERSION)).digest().subarray(0, TAG_BYTES);
|
|
24454
|
+
}
|
|
24455
|
+
function verifyPointerTag(signKey, keyVersion, pointerId, category, tag) {
|
|
24456
|
+
if (tag.length !== TAG_BYTES) return false;
|
|
24457
|
+
const expected = signPointer(signKey, keyVersion, pointerId, category);
|
|
24458
|
+
return timingSafeEqual(expected, Buffer.from(tag));
|
|
24459
|
+
}
|
|
24460
|
+
function formatPointer(category, keyVersion, pointerId, tag) {
|
|
24461
|
+
return `[[aka:${category}:${encodeKeyVersion(keyVersion)}.${base32Encode(pointerId)}.${base32Encode(tag)}]]`;
|
|
24462
|
+
}
|
|
24463
|
+
|
|
24464
|
+
// ../../packages/persistence/src/vault/key-provider.ts
|
|
24465
|
+
import { execFileSync } from "child_process";
|
|
24466
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
24467
|
+
import {
|
|
24468
|
+
chmodSync as chmodSync2,
|
|
24469
|
+
mkdirSync as mkdirSync2,
|
|
24470
|
+
readFileSync as readFileSync3,
|
|
24471
|
+
renameSync as renameSync4,
|
|
24472
|
+
rmSync as rmSync3,
|
|
24473
|
+
statSync,
|
|
24474
|
+
writeFileSync as writeFileSync2
|
|
24475
|
+
} from "fs";
|
|
23863
24476
|
import { join as join5 } from "path";
|
|
24477
|
+
var VaultKeyEpochMissingError = class extends Error {
|
|
24478
|
+
version;
|
|
24479
|
+
constructor(version2) {
|
|
24480
|
+
super(`vault: key epoch ${String(version2)} is not present in the keyring`);
|
|
24481
|
+
this.name = "VaultKeyEpochMissingError";
|
|
24482
|
+
this.version = version2;
|
|
24483
|
+
}
|
|
24484
|
+
};
|
|
24485
|
+
var VAULT_KEY_FILENAME = "vault.key";
|
|
24486
|
+
var KEY_MATERIAL_BYTES2 = 32;
|
|
24487
|
+
var KEYCHAIN_SERVICE = "aka-vault";
|
|
24488
|
+
var KEYCHAIN_ACCOUNT = "keyring";
|
|
24489
|
+
function parseKeyring(raw) {
|
|
24490
|
+
const parsed = JSON.parse(raw);
|
|
24491
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
24492
|
+
throw new Error("vault key file is corrupt: not a JSON object");
|
|
24493
|
+
}
|
|
24494
|
+
const { current, keys } = parsed;
|
|
24495
|
+
if (typeof current !== "number" || !Number.isInteger(current) || current < 1) {
|
|
24496
|
+
throw new Error("vault key file is corrupt: bad current version");
|
|
24497
|
+
}
|
|
24498
|
+
if (typeof keys !== "object" || keys === null || Array.isArray(keys)) {
|
|
24499
|
+
throw new Error("vault key file is corrupt: bad keys map");
|
|
24500
|
+
}
|
|
24501
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
24502
|
+
for (const [rawVersion, rawMaterial] of Object.entries(keys)) {
|
|
24503
|
+
const version2 = Number(rawVersion);
|
|
24504
|
+
if (!Number.isInteger(version2) || version2 < 1) {
|
|
24505
|
+
throw new Error("vault key file is corrupt: bad key version");
|
|
24506
|
+
}
|
|
24507
|
+
if (typeof rawMaterial !== "string") {
|
|
24508
|
+
throw new Error("vault key file is corrupt: bad key material");
|
|
24509
|
+
}
|
|
24510
|
+
const bytes = Buffer.from(rawMaterial, "base64");
|
|
24511
|
+
if (bytes.length !== KEY_MATERIAL_BYTES2) {
|
|
24512
|
+
throw new Error("vault key file is corrupt: bad key material length");
|
|
24513
|
+
}
|
|
24514
|
+
map2.set(version2, bytes);
|
|
24515
|
+
}
|
|
24516
|
+
if (!map2.has(current)) {
|
|
24517
|
+
throw new Error("vault key file is corrupt: current version has no material");
|
|
24518
|
+
}
|
|
24519
|
+
return { current, keys: map2 };
|
|
24520
|
+
}
|
|
24521
|
+
function serializeKeyring(keyring) {
|
|
24522
|
+
const keys = {};
|
|
24523
|
+
for (const version2 of [...keyring.keys.keys()].sort((a, b) => a - b)) {
|
|
24524
|
+
const material = keyring.keys.get(version2);
|
|
24525
|
+
if (material) keys[String(version2)] = material.toString("base64");
|
|
24526
|
+
}
|
|
24527
|
+
return JSON.stringify({ current: keyring.current, keys });
|
|
24528
|
+
}
|
|
24529
|
+
function mintKeyring() {
|
|
24530
|
+
return { current: 1, keys: /* @__PURE__ */ new Map([[1, randomBytes2(KEY_MATERIAL_BYTES2)]]) };
|
|
24531
|
+
}
|
|
24532
|
+
function withNextEpoch(keyring) {
|
|
24533
|
+
const next = Math.max(...keyring.keys.keys()) + 1;
|
|
24534
|
+
const keys = new Map(keyring.keys);
|
|
24535
|
+
keys.set(next, randomBytes2(KEY_MATERIAL_BYTES2));
|
|
24536
|
+
return { current: next, keys };
|
|
24537
|
+
}
|
|
24538
|
+
function currentOf(keyring) {
|
|
24539
|
+
const material = keyring.keys.get(keyring.current);
|
|
24540
|
+
if (!material) throw new VaultKeyEpochMissingError(keyring.current);
|
|
24541
|
+
return { material, version: keyring.current };
|
|
24542
|
+
}
|
|
24543
|
+
function epochOf(keyring, version2) {
|
|
24544
|
+
const material = keyring.keys.get(version2);
|
|
24545
|
+
if (!material) throw new VaultKeyEpochMissingError(version2);
|
|
24546
|
+
return { material, version: version2 };
|
|
24547
|
+
}
|
|
24548
|
+
function asAsync(work) {
|
|
24549
|
+
try {
|
|
24550
|
+
return Promise.resolve(work());
|
|
24551
|
+
} catch (err) {
|
|
24552
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
24553
|
+
}
|
|
24554
|
+
}
|
|
24555
|
+
function asError(err) {
|
|
24556
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
24557
|
+
}
|
|
24558
|
+
var ROTATION_LOCK_STALE_MS = 6e4;
|
|
24559
|
+
var LOCK_OWNER_FILE = "owner";
|
|
24560
|
+
var ROTATION_IN_PROGRESS = "vault: a key rotation is already in progress";
|
|
24561
|
+
function claimRotationLock(lock, owner) {
|
|
24562
|
+
try {
|
|
24563
|
+
mkdirSync2(lock);
|
|
24564
|
+
} catch (err) {
|
|
24565
|
+
if (err.code === "EEXIST") return false;
|
|
24566
|
+
throw asError(err);
|
|
24567
|
+
}
|
|
24568
|
+
try {
|
|
24569
|
+
writeFileSync2(join5(lock, LOCK_OWNER_FILE), `${owner}
|
|
24570
|
+
`, { mode: DATA_FILE_MODE });
|
|
24571
|
+
return true;
|
|
24572
|
+
} catch (err) {
|
|
24573
|
+
rmSync3(lock, { recursive: true, force: true });
|
|
24574
|
+
throw asError(err);
|
|
24575
|
+
}
|
|
24576
|
+
}
|
|
24577
|
+
function acquireRotationLock(keysDir2) {
|
|
24578
|
+
const lock = join5(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
|
|
24579
|
+
const owner = randomBytes2(16).toString("hex");
|
|
24580
|
+
if (claimRotationLock(lock, owner)) return { lock, owner };
|
|
24581
|
+
let held;
|
|
24582
|
+
try {
|
|
24583
|
+
held = statSync(lock);
|
|
24584
|
+
} catch {
|
|
24585
|
+
throw new Error(ROTATION_IN_PROGRESS);
|
|
24586
|
+
}
|
|
24587
|
+
if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
|
|
24588
|
+
const aside = `${lock}.stale.${owner}`;
|
|
24589
|
+
try {
|
|
24590
|
+
const now = statSync(lock);
|
|
24591
|
+
if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
|
|
24592
|
+
throw new Error(ROTATION_IN_PROGRESS);
|
|
24593
|
+
}
|
|
24594
|
+
renameSync4(lock, aside);
|
|
24595
|
+
} catch (err) {
|
|
24596
|
+
if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
|
|
24597
|
+
throw new Error(ROTATION_IN_PROGRESS, { cause: err });
|
|
24598
|
+
}
|
|
24599
|
+
rmSync3(aside, { recursive: true, force: true });
|
|
24600
|
+
if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
|
|
24601
|
+
return { lock, owner };
|
|
24602
|
+
}
|
|
24603
|
+
function releaseRotationLock(lease) {
|
|
24604
|
+
try {
|
|
24605
|
+
if (readFileSync3(join5(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
|
|
24606
|
+
} catch {
|
|
24607
|
+
return;
|
|
24608
|
+
}
|
|
24609
|
+
rmSync3(lease.lock, { recursive: true, force: true });
|
|
24610
|
+
}
|
|
24611
|
+
function withRotationLock(keysDir2, work) {
|
|
24612
|
+
ensureDataDirSync(keysDir2);
|
|
24613
|
+
const lease = acquireRotationLock(keysDir2);
|
|
24614
|
+
try {
|
|
24615
|
+
return work();
|
|
24616
|
+
} finally {
|
|
24617
|
+
releaseRotationLock(lease);
|
|
24618
|
+
}
|
|
24619
|
+
}
|
|
24620
|
+
var FileKeyProvider = class {
|
|
24621
|
+
#keysDir;
|
|
24622
|
+
constructor(keysDir2) {
|
|
24623
|
+
this.#keysDir = keysDir2;
|
|
24624
|
+
}
|
|
24625
|
+
get filePath() {
|
|
24626
|
+
return join5(this.#keysDir, VAULT_KEY_FILENAME);
|
|
24627
|
+
}
|
|
24628
|
+
loadOrCreate() {
|
|
24629
|
+
return asAsync(() => {
|
|
24630
|
+
const existing = this.#read();
|
|
24631
|
+
if (!existing) return currentOf(this.#createExclusive());
|
|
24632
|
+
tightenFileMode(this.filePath);
|
|
24633
|
+
return currentOf(existing);
|
|
24634
|
+
});
|
|
24635
|
+
}
|
|
24636
|
+
rotate() {
|
|
24637
|
+
return asAsync(
|
|
24638
|
+
() => withRotationLock(this.#keysDir, () => {
|
|
24639
|
+
const existing = this.#read();
|
|
24640
|
+
if (!existing) return currentOf(this.#createExclusive());
|
|
24641
|
+
return currentOf(this.#write(withNextEpoch(existing)));
|
|
24642
|
+
})
|
|
24643
|
+
);
|
|
24644
|
+
}
|
|
24645
|
+
materialFor(version2) {
|
|
24646
|
+
return asAsync(() => {
|
|
24647
|
+
const existing = this.#read();
|
|
24648
|
+
if (!existing) throw new VaultKeyEpochMissingError(version2);
|
|
24649
|
+
return epochOf(existing, version2);
|
|
24650
|
+
});
|
|
24651
|
+
}
|
|
24652
|
+
/** The keyring, or null when the file is ABSENT. A corrupt file throws. */
|
|
24653
|
+
#read() {
|
|
24654
|
+
let raw;
|
|
24655
|
+
try {
|
|
24656
|
+
raw = readFileSync3(this.filePath, "utf8");
|
|
24657
|
+
} catch (err) {
|
|
24658
|
+
if (err.code === "ENOENT") return null;
|
|
24659
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
24660
|
+
}
|
|
24661
|
+
return parseKeyring(raw);
|
|
24662
|
+
}
|
|
24663
|
+
/**
|
|
24664
|
+
* First mint: the keyring is created at its FINAL path with a
|
|
24665
|
+
* creation-exclusive write, so two processes racing a fresh machine cannot
|
|
24666
|
+
* each mint a different epoch 1 — with tmp + rename the loser's replace
|
|
24667
|
+
* would orphan everything the winner had already sealed. On EEXIST the
|
|
24668
|
+
* loser re-reads and adopts the winner's keyring; it minted nothing.
|
|
24669
|
+
* Atomic replace is unnecessary here: nothing can be mid-read of a file
|
|
24670
|
+
* that did not exist, and a torn exclusive write parses as corrupt on the
|
|
24671
|
+
* next read and fails secure rather than being re-minted over.
|
|
24672
|
+
*/
|
|
24673
|
+
#createExclusive() {
|
|
24674
|
+
ensureDataDirSync(this.#keysDir);
|
|
24675
|
+
const keyring = mintKeyring();
|
|
24676
|
+
try {
|
|
24677
|
+
writeFileSync2(this.filePath, `${serializeKeyring(keyring)}
|
|
24678
|
+
`, {
|
|
24679
|
+
flag: "wx",
|
|
24680
|
+
mode: DATA_FILE_MODE
|
|
24681
|
+
});
|
|
24682
|
+
} catch (err) {
|
|
24683
|
+
if (err.code !== "EEXIST") throw asError(err);
|
|
24684
|
+
const winner = this.#read();
|
|
24685
|
+
if (!winner) {
|
|
24686
|
+
throw new Error("vault: key file vanished during first mint", { cause: err });
|
|
24687
|
+
}
|
|
24688
|
+
return winner;
|
|
24689
|
+
}
|
|
24690
|
+
tightenFileMode(this.filePath);
|
|
24691
|
+
return keyring;
|
|
24692
|
+
}
|
|
24693
|
+
/**
|
|
24694
|
+
* Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
|
|
24695
|
+
* Used only for rotation, under the rotation lock — first creation goes
|
|
24696
|
+
* through the creation-exclusive path instead.
|
|
24697
|
+
*/
|
|
24698
|
+
#write(keyring) {
|
|
24699
|
+
ensureDataDirSync(this.#keysDir);
|
|
24700
|
+
const file2 = this.filePath;
|
|
24701
|
+
const tmp = `${file2}.tmp`;
|
|
24702
|
+
writeFileSync2(tmp, `${serializeKeyring(keyring)}
|
|
24703
|
+
`, { mode: DATA_FILE_MODE });
|
|
24704
|
+
renameSync4(tmp, file2);
|
|
24705
|
+
tightenFileMode(file2);
|
|
24706
|
+
return keyring;
|
|
24707
|
+
}
|
|
24708
|
+
};
|
|
24709
|
+
function tightenFileMode(file2) {
|
|
24710
|
+
try {
|
|
24711
|
+
chmodSync2(file2, DATA_FILE_MODE);
|
|
24712
|
+
} catch {
|
|
24713
|
+
}
|
|
24714
|
+
}
|
|
24715
|
+
var runSecurity = (args) => execFileSync("/usr/bin/security", args, {
|
|
24716
|
+
encoding: "utf8",
|
|
24717
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
24718
|
+
});
|
|
24719
|
+
var SECURITY_ITEM_NOT_FOUND = 44;
|
|
24720
|
+
var KeychainKeyProvider = class {
|
|
24721
|
+
#keysDir;
|
|
24722
|
+
#exec;
|
|
24723
|
+
constructor(keysDir2, exec = runSecurity) {
|
|
24724
|
+
if (exec === runSecurity && process.platform !== "darwin") {
|
|
24725
|
+
throw new Error(
|
|
24726
|
+
`keychain custody is not available on this platform (${process.platform}); use file custody`
|
|
24727
|
+
);
|
|
24728
|
+
}
|
|
24729
|
+
this.#keysDir = keysDir2;
|
|
24730
|
+
this.#exec = exec;
|
|
24731
|
+
}
|
|
24732
|
+
/** Where a fallback file provider for the same vault would keep its keyring. */
|
|
24733
|
+
get keysDir() {
|
|
24734
|
+
return this.#keysDir;
|
|
24735
|
+
}
|
|
24736
|
+
loadOrCreate() {
|
|
24737
|
+
return asAsync(() => {
|
|
24738
|
+
const existing = this.#read();
|
|
24739
|
+
if (existing) return currentOf(existing);
|
|
24740
|
+
return currentOf(this.#create(mintKeyring()));
|
|
24741
|
+
});
|
|
24742
|
+
}
|
|
24743
|
+
rotate() {
|
|
24744
|
+
return asAsync(
|
|
24745
|
+
() => withRotationLock(this.#keysDir, () => {
|
|
24746
|
+
const existing = this.#read();
|
|
24747
|
+
if (!existing) return currentOf(this.#create(mintKeyring()));
|
|
24748
|
+
return currentOf(this.#replace(withNextEpoch(existing)));
|
|
24749
|
+
})
|
|
24750
|
+
);
|
|
24751
|
+
}
|
|
24752
|
+
materialFor(version2) {
|
|
24753
|
+
return asAsync(() => {
|
|
24754
|
+
const existing = this.#read();
|
|
24755
|
+
if (!existing) throw new VaultKeyEpochMissingError(version2);
|
|
24756
|
+
return epochOf(existing, version2);
|
|
24757
|
+
});
|
|
24758
|
+
}
|
|
24759
|
+
/** The keyring, or null when no item exists yet. A corrupt item throws. */
|
|
24760
|
+
#read() {
|
|
24761
|
+
let raw;
|
|
24762
|
+
try {
|
|
24763
|
+
raw = this.#exec([
|
|
24764
|
+
"find-generic-password",
|
|
24765
|
+
"-s",
|
|
24766
|
+
KEYCHAIN_SERVICE,
|
|
24767
|
+
"-a",
|
|
24768
|
+
KEYCHAIN_ACCOUNT,
|
|
24769
|
+
"-w"
|
|
24770
|
+
]);
|
|
24771
|
+
} catch (err) {
|
|
24772
|
+
if (err.status === SECURITY_ITEM_NOT_FOUND) return null;
|
|
24773
|
+
throw new Error(
|
|
24774
|
+
`vault: keychain read failed (${err instanceof Error ? err.message : String(err)}); refusing to treat the failure as an absent keyring`,
|
|
24775
|
+
{ cause: err }
|
|
24776
|
+
);
|
|
24777
|
+
}
|
|
24778
|
+
const body = raw.trim();
|
|
24779
|
+
if (body.length === 0) return null;
|
|
24780
|
+
return parseKeyring(body);
|
|
24781
|
+
}
|
|
24782
|
+
/**
|
|
24783
|
+
* First mint: a plain `add-generic-password` (no `-U`) fails when an item
|
|
24784
|
+
* already exists, so a concurrent first mint cannot overwrite the winner's
|
|
24785
|
+
* keyring — the loser re-reads and adopts it instead.
|
|
24786
|
+
*/
|
|
24787
|
+
#create(keyring) {
|
|
24788
|
+
const args = [
|
|
24789
|
+
"add-generic-password",
|
|
24790
|
+
"-s",
|
|
24791
|
+
KEYCHAIN_SERVICE,
|
|
24792
|
+
"-a",
|
|
24793
|
+
KEYCHAIN_ACCOUNT,
|
|
24794
|
+
"-w",
|
|
24795
|
+
serializeKeyring(keyring)
|
|
24796
|
+
];
|
|
24797
|
+
try {
|
|
24798
|
+
this.#exec(args);
|
|
24799
|
+
} catch (err) {
|
|
24800
|
+
const winner = this.#read();
|
|
24801
|
+
if (winner) return winner;
|
|
24802
|
+
throw asError(err);
|
|
24803
|
+
}
|
|
24804
|
+
return keyring;
|
|
24805
|
+
}
|
|
24806
|
+
// `-U` updates the item in place, deliberately replacing the stored map with
|
|
24807
|
+
// one that contains it — used only for rotation, under the rotation lock.
|
|
24808
|
+
#replace(keyring) {
|
|
24809
|
+
this.#exec([
|
|
24810
|
+
"add-generic-password",
|
|
24811
|
+
"-U",
|
|
24812
|
+
"-s",
|
|
24813
|
+
KEYCHAIN_SERVICE,
|
|
24814
|
+
"-a",
|
|
24815
|
+
KEYCHAIN_ACCOUNT,
|
|
24816
|
+
"-w",
|
|
24817
|
+
serializeKeyring(keyring)
|
|
24818
|
+
]);
|
|
24819
|
+
return keyring;
|
|
24820
|
+
}
|
|
24821
|
+
};
|
|
24822
|
+
function createKeyProvider(custody, keysDir2) {
|
|
24823
|
+
if (custody === "keychain") return new KeychainKeyProvider(keysDir2);
|
|
24824
|
+
return new FileKeyProvider(keysDir2);
|
|
24825
|
+
}
|
|
24826
|
+
|
|
24827
|
+
// ../../packages/persistence/src/vault/vault.ts
|
|
24828
|
+
import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
|
|
24829
|
+
var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
|
|
24830
|
+
var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
|
|
24831
|
+
var VAULT_PURGE_POINTER_ID = "*";
|
|
24832
|
+
function parsePointer(token) {
|
|
24833
|
+
if (!POINTER_TOKEN_ANCHORED.test(token)) return null;
|
|
24834
|
+
const body = token.slice("[[aka:".length, -"]]".length);
|
|
24835
|
+
const colon = body.indexOf(":");
|
|
24836
|
+
if (colon < 0) return null;
|
|
24837
|
+
const category = body.slice(0, colon);
|
|
24838
|
+
const [kv, id, tag] = body.slice(colon + 1).split(".");
|
|
24839
|
+
if (kv === void 0 || id === void 0 || tag === void 0) return null;
|
|
24840
|
+
try {
|
|
24841
|
+
const keyVersion = decodeKeyVersion(kv);
|
|
24842
|
+
const pointerId = base32Decode(id);
|
|
24843
|
+
const tagBytes = base32Decode(tag);
|
|
24844
|
+
if (encodeKeyVersion(keyVersion) !== kv || base32Encode(pointerId) !== id || base32Encode(tagBytes) !== tag) {
|
|
24845
|
+
return null;
|
|
24846
|
+
}
|
|
24847
|
+
return { category, keyVersion, pointerId, tag: tagBytes };
|
|
24848
|
+
} catch {
|
|
24849
|
+
return null;
|
|
24850
|
+
}
|
|
24851
|
+
}
|
|
24852
|
+
var SecretVault = class {
|
|
24853
|
+
#repo;
|
|
24854
|
+
#keys;
|
|
24855
|
+
#fingerprintKey;
|
|
24856
|
+
#isConsented;
|
|
24857
|
+
#verifyGrant;
|
|
24858
|
+
#now;
|
|
24859
|
+
constructor(deps) {
|
|
24860
|
+
this.#repo = deps.repo;
|
|
24861
|
+
this.#keys = deps.keys;
|
|
24862
|
+
this.#fingerprintKey = deps.fingerprintKey;
|
|
24863
|
+
this.#isConsented = deps.isConsented;
|
|
24864
|
+
this.#verifyGrant = deps.verifyGrant;
|
|
24865
|
+
this.#now = deps.now ?? (() => Date.now());
|
|
24866
|
+
}
|
|
24867
|
+
/**
|
|
24868
|
+
* Store a value and return the pointer that stands for it. The same value
|
|
24869
|
+
* always yields the same pointer on this machine — one row, one pointer id,
|
|
24870
|
+
* one category — which is what makes dedup and reuse counting work.
|
|
24871
|
+
*/
|
|
24872
|
+
async tokenize(raw, meta3) {
|
|
24873
|
+
if (!this.#isConsented()) return CONSENT_ABSENT;
|
|
24874
|
+
const valueFingerprint = fingerprintValue(this.#fingerprintKey, raw);
|
|
24875
|
+
const existing = this.#repo.byValueFingerprint(valueFingerprint);
|
|
24876
|
+
const now = this.#now();
|
|
24877
|
+
if (existing) {
|
|
24878
|
+
this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
|
|
24879
|
+
return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
|
|
24880
|
+
}
|
|
24881
|
+
const { material, version: version2 } = await this.#keys.loadOrCreate();
|
|
24882
|
+
const subkeys = deriveSubkeys(material);
|
|
24883
|
+
const pointerId = randomBytes3(POINTER_ID_BYTES);
|
|
24884
|
+
const aad = bindingInput(version2, pointerId, meta3.category, POINTER_FORMAT_VERSION);
|
|
24885
|
+
const sealed = seal(subkeys.enc, raw, aad, randomBytes3(NONCE_BYTES));
|
|
24886
|
+
const { row } = this.#repo.upsert(
|
|
24887
|
+
{
|
|
24888
|
+
pointerId: base32Encode(pointerId),
|
|
24889
|
+
valueFingerprint,
|
|
24890
|
+
fingerprintKeyVersion: this.#fingerprintKey.version,
|
|
24891
|
+
keyVersion: version2,
|
|
24892
|
+
// Recorded so the row stays OPENABLE if the wire-format constant ever
|
|
24893
|
+
// moves: it is part of this row's AEAD AAD. It is not a tag input —
|
|
24894
|
+
// tags are pinned to the constant on both sides.
|
|
24895
|
+
formatVersion: POINTER_FORMAT_VERSION,
|
|
24896
|
+
category: meta3.category,
|
|
24897
|
+
ruleId: meta3.ruleId,
|
|
24898
|
+
maskedMatch: meta3.maskedMatch,
|
|
24899
|
+
provider: meta3.provider,
|
|
24900
|
+
ciphertext: sealed.ciphertext.toString("base64"),
|
|
24901
|
+
nonce: sealed.nonce.toString("base64"),
|
|
24902
|
+
authTag: sealed.authTag.toString("base64")
|
|
24903
|
+
},
|
|
24904
|
+
now
|
|
24905
|
+
);
|
|
24906
|
+
return await this.#emitToken(row.keyVersion, row.pointerId, row.category);
|
|
24907
|
+
}
|
|
24908
|
+
/**
|
|
24909
|
+
* Resolve a pointer back to its value, for a human or (with a grant) for the
|
|
24910
|
+
* model. Every call that gets as far as an identified row writes an audit row.
|
|
24911
|
+
*/
|
|
24912
|
+
async detokenize(token, opts) {
|
|
24913
|
+
const parsed = parsePointer(token);
|
|
24914
|
+
if (!parsed) return UNAVAILABLE;
|
|
24915
|
+
let signKey;
|
|
24916
|
+
try {
|
|
24917
|
+
const epoch = await this.#keys.materialFor(parsed.keyVersion);
|
|
24918
|
+
signKey = deriveSubkeys(epoch.material).sign;
|
|
24919
|
+
} catch {
|
|
24920
|
+
return UNAVAILABLE;
|
|
24921
|
+
}
|
|
24922
|
+
if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
|
|
24923
|
+
return UNAVAILABLE;
|
|
24924
|
+
}
|
|
24925
|
+
const pointerId = base32Encode(parsed.pointerId);
|
|
24926
|
+
const row = this.#repo.byPointerId(pointerId);
|
|
24927
|
+
if (!row) {
|
|
24928
|
+
this.#audit(pointerId, opts, "unavailable");
|
|
24929
|
+
return UNAVAILABLE;
|
|
24930
|
+
}
|
|
24931
|
+
if (row.category !== parsed.category) return UNAVAILABLE;
|
|
24932
|
+
if (opts.target === "model") {
|
|
24933
|
+
const grantId = opts.grantId;
|
|
24934
|
+
const verify = this.#verifyGrant;
|
|
24935
|
+
if (verify === void 0 || grantId === void 0 || grantId === "") {
|
|
24936
|
+
this.#audit(pointerId, opts, "refused");
|
|
24937
|
+
return UNAVAILABLE;
|
|
24938
|
+
}
|
|
24939
|
+
let covered;
|
|
24940
|
+
try {
|
|
24941
|
+
covered = await verify(grantId, {
|
|
24942
|
+
ruleId: row.ruleId,
|
|
24943
|
+
valueFingerprint: row.valueFingerprint,
|
|
24944
|
+
fingerprintKeyVersion: row.fingerprintKeyVersion
|
|
24945
|
+
});
|
|
24946
|
+
} catch {
|
|
24947
|
+
covered = false;
|
|
24948
|
+
}
|
|
24949
|
+
if (!covered) {
|
|
24950
|
+
this.#audit(pointerId, opts, "refused");
|
|
24951
|
+
return UNAVAILABLE;
|
|
24952
|
+
}
|
|
24953
|
+
}
|
|
24954
|
+
let raw;
|
|
24955
|
+
try {
|
|
24956
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
24957
|
+
raw = open(
|
|
24958
|
+
deriveSubkeys(epoch.material).enc,
|
|
24959
|
+
{
|
|
24960
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
24961
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
24962
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
24963
|
+
},
|
|
24964
|
+
// Sealed under the ROW's epoch and format version. Rotation may have
|
|
24965
|
+
// moved the epoch past the one this token names, and a format bump may
|
|
24966
|
+
// have moved the constant past the generation this row was sealed
|
|
24967
|
+
// under — the AAD follows the row in both cases, never the token.
|
|
24968
|
+
bindingInput(row.keyVersion, parsed.pointerId, row.category, row.formatVersion)
|
|
24969
|
+
);
|
|
24970
|
+
} catch {
|
|
24971
|
+
raw = null;
|
|
24972
|
+
}
|
|
24973
|
+
if (raw === null) {
|
|
24974
|
+
this.#audit(pointerId, opts, "unavailable");
|
|
24975
|
+
return UNAVAILABLE;
|
|
24976
|
+
}
|
|
24977
|
+
this.#audit(pointerId, opts, "revealed");
|
|
24978
|
+
return raw;
|
|
24979
|
+
}
|
|
24980
|
+
/**
|
|
24981
|
+
* Owner-surface reveal by row id: the dashboard shows a row the owner can
|
|
24982
|
+
* already see and asks for its value. There is no wire token here to verify —
|
|
24983
|
+
* the tag exists to stop FORGED tokens arriving in untrusted text, and a row
|
|
24984
|
+
* id selected server-side from the owner's own store is not that — so this
|
|
24985
|
+
* loads the row directly, opens its ciphertext under the row's epoch, and
|
|
24986
|
+
* audits exactly like a human-target de-reference. Never callable with
|
|
24987
|
+
* target 'model': the wire-token path with its grant gate is the only road
|
|
24988
|
+
* raw travels toward the model.
|
|
24989
|
+
*/
|
|
24990
|
+
async revealEntry(pointerId, opts) {
|
|
24991
|
+
const row = this.#repo.byPointerId(pointerId);
|
|
24992
|
+
if (!row) {
|
|
24993
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
|
|
24994
|
+
return UNAVAILABLE;
|
|
24995
|
+
}
|
|
24996
|
+
const raw = await this.#openRow(row);
|
|
24997
|
+
if (raw === null) {
|
|
24998
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
|
|
24999
|
+
return UNAVAILABLE;
|
|
25000
|
+
}
|
|
25001
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "revealed");
|
|
25002
|
+
return raw;
|
|
25003
|
+
}
|
|
25004
|
+
/** Badge and listing data. No raw value, no fingerprint, and no audit row. */
|
|
25005
|
+
async describePointer(token) {
|
|
25006
|
+
const row = await this.#rowFor(token);
|
|
25007
|
+
if (!row) return null;
|
|
25008
|
+
return {
|
|
25009
|
+
category: row.category,
|
|
25010
|
+
...row.provider === void 0 ? {} : { provider: row.provider },
|
|
25011
|
+
maskedMatch: row.maskedMatch,
|
|
25012
|
+
occurrences: row.occurrenceCount,
|
|
25013
|
+
firstSeen: new Date(row.firstSeen).toISOString(),
|
|
25014
|
+
lastSeen: new Date(row.lastSeen).toISOString()
|
|
25015
|
+
};
|
|
25016
|
+
}
|
|
25017
|
+
/**
|
|
25018
|
+
* The raw-free row identity a reveal grant matches on. Deliberately not fed to
|
|
25019
|
+
* view surfaces: the keyed fingerprint is a correlation key and must not reach
|
|
25020
|
+
* a presentation layer.
|
|
25021
|
+
*/
|
|
25022
|
+
async resolvePointerIdentity(token) {
|
|
25023
|
+
const row = await this.#rowFor(token);
|
|
25024
|
+
if (!row) return null;
|
|
25025
|
+
return {
|
|
25026
|
+
ruleId: row.ruleId,
|
|
25027
|
+
valueFingerprint: row.valueFingerprint,
|
|
25028
|
+
fingerprintKeyVersion: row.fingerprintKeyVersion
|
|
25029
|
+
};
|
|
25030
|
+
}
|
|
25031
|
+
/**
|
|
25032
|
+
* Mint the next vault key epoch and re-encrypt every entry under it. Pointers
|
|
25033
|
+
* already emitted keep verifying: their tag is checked against the historical
|
|
25034
|
+
* epoch they name, which the key provider retains.
|
|
25035
|
+
*
|
|
25036
|
+
* Safe to interrupt — each row carries the epoch its ciphertext is sealed
|
|
25037
|
+
* under, so a half-finished pass leaves every row openable.
|
|
25038
|
+
*
|
|
25039
|
+
* The rotation lock covers only the keyring mint inside `rotate()`; the
|
|
25040
|
+
* re-seal pass below runs unlocked. Two concurrent rotations therefore
|
|
25041
|
+
* serialize on the keyring but interleave over the rows, so a slower pass can
|
|
25042
|
+
* re-seal a row back to an epoch a faster one already moved past, and
|
|
25043
|
+
* `reEncrypted` can double-count. No value is lost either way — every epoch is
|
|
25044
|
+
* retained and every row stays openable — but "after rotation every row sits
|
|
25045
|
+
* at the newest epoch" does not hold under concurrency. Holding the lock
|
|
25046
|
+
* across the whole pass requires an async-aware lock, since a callback that
|
|
25047
|
+
* awaits would release the lock at its first suspension.
|
|
25048
|
+
*/
|
|
25049
|
+
async rotateVaultKey() {
|
|
25050
|
+
const next = await this.#keys.rotate();
|
|
25051
|
+
const nextEnc = deriveSubkeys(next.material).enc;
|
|
25052
|
+
let reEncrypted = 0;
|
|
25053
|
+
for (const row of this.#repo.listAll()) {
|
|
25054
|
+
if (row.keyVersion === next.version) continue;
|
|
25055
|
+
const pointerId = base32Decode(row.pointerId);
|
|
25056
|
+
let raw;
|
|
25057
|
+
try {
|
|
25058
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
25059
|
+
raw = open(
|
|
25060
|
+
deriveSubkeys(epoch.material).enc,
|
|
25061
|
+
{
|
|
25062
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
25063
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
25064
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
25065
|
+
},
|
|
25066
|
+
bindingInput(row.keyVersion, pointerId, row.category, row.formatVersion)
|
|
25067
|
+
);
|
|
25068
|
+
} catch {
|
|
25069
|
+
raw = null;
|
|
25070
|
+
}
|
|
25071
|
+
if (raw === null) continue;
|
|
25072
|
+
const sealed = seal(
|
|
25073
|
+
nextEnc,
|
|
25074
|
+
raw,
|
|
25075
|
+
bindingInput(next.version, pointerId, row.category, row.formatVersion),
|
|
25076
|
+
randomBytes3(NONCE_BYTES)
|
|
25077
|
+
);
|
|
25078
|
+
this.#repo.replaceCiphertext(row.pointerId, {
|
|
25079
|
+
keyVersion: next.version,
|
|
25080
|
+
ciphertext: sealed.ciphertext.toString("base64"),
|
|
25081
|
+
nonce: sealed.nonce.toString("base64"),
|
|
25082
|
+
authTag: sealed.authTag.toString("base64")
|
|
25083
|
+
});
|
|
25084
|
+
reEncrypted += 1;
|
|
25085
|
+
}
|
|
25086
|
+
return { version: next.version, reEncrypted };
|
|
25087
|
+
}
|
|
25088
|
+
/**
|
|
25089
|
+
* Re-key every entry's value fingerprint after the exception key rotates,
|
|
25090
|
+
* PRESERVING each pointer id. Unlike grants — where rotation is invalidation,
|
|
25091
|
+
* because the raw values are gone — the vault still holds the values, so
|
|
25092
|
+
* determinism, dedup, and every outstanding pointer survive the rotation.
|
|
25093
|
+
*
|
|
25094
|
+
* Every fingerprint-key rotation must run this: a row left at the old epoch
|
|
25095
|
+
* still resolves, but the same value detected again fingerprints under the
|
|
25096
|
+
* NEW key, misses the dedup lookup, and mints a second row and a second
|
|
25097
|
+
* pointer — one value, two tokens in circulation.
|
|
25098
|
+
*
|
|
25099
|
+
* Per-row best-effort: a row that cannot open, or whose refreshed
|
|
25100
|
+
* fingerprint collides with a row already refreshed, is skipped rather than
|
|
25101
|
+
* aborting the pass — one damaged entry must not strand the re-key of every
|
|
25102
|
+
* other. A skipped row keeps resolving under its old fingerprint epoch.
|
|
25103
|
+
*/
|
|
25104
|
+
async refreshFingerprints(next) {
|
|
25105
|
+
let refreshed = 0;
|
|
25106
|
+
for (const row of this.#repo.listAll()) {
|
|
25107
|
+
try {
|
|
25108
|
+
const raw = await this.#openRow(row);
|
|
25109
|
+
if (raw === null) continue;
|
|
25110
|
+
this.#repo.refreshFingerprint(row.pointerId, {
|
|
25111
|
+
valueFingerprint: fingerprintValue(next, raw),
|
|
25112
|
+
fingerprintKeyVersion: next.version
|
|
25113
|
+
});
|
|
25114
|
+
refreshed += 1;
|
|
25115
|
+
} catch {
|
|
25116
|
+
continue;
|
|
25117
|
+
}
|
|
25118
|
+
}
|
|
25119
|
+
return refreshed;
|
|
25120
|
+
}
|
|
25121
|
+
/**
|
|
25122
|
+
* Destroy every entry, making all outstanding pointers permanently
|
|
25123
|
+
* unresolvable.
|
|
25124
|
+
*
|
|
25125
|
+
* The count comes from `purgeAll` rather than a separate `countEntries` —
|
|
25126
|
+
* `purgeAll` counts inside the same transaction that deletes, so the audit row
|
|
25127
|
+
* reports what was actually destroyed. Counting beforehand would let a
|
|
25128
|
+
* concurrent write land between the two statements and put a number in the
|
|
25129
|
+
* durable record that never matched reality.
|
|
25130
|
+
*/
|
|
25131
|
+
purgeVault() {
|
|
25132
|
+
const destroyed = this.#repo.purgeAll();
|
|
25133
|
+
this.#repo.recordDeref({
|
|
25134
|
+
id: randomUUID10(),
|
|
25135
|
+
pointerId: VAULT_PURGE_POINTER_ID,
|
|
25136
|
+
at: this.#now(),
|
|
25137
|
+
target: "human",
|
|
25138
|
+
reason: "purge",
|
|
25139
|
+
outcome: "unavailable",
|
|
25140
|
+
pointerCount: Math.max(destroyed, 1)
|
|
25141
|
+
});
|
|
25142
|
+
return destroyed;
|
|
25143
|
+
}
|
|
25144
|
+
// Sign under the epoch the token names — which for a re-detected value is the
|
|
25145
|
+
// epoch its row currently sits at rather than whatever is current.
|
|
25146
|
+
//
|
|
25147
|
+
// The row's format version is NOT a tag input. It binds the row's ciphertext
|
|
25148
|
+
// (it is part of the AEAD AAD, so an old row stays openable) but never the
|
|
25149
|
+
// wire tag, which verification checks against POINTER_FORMAT_VERSION without
|
|
25150
|
+
// knowing any row. Signing a token here under a row's own generation is what
|
|
25151
|
+
// would make the vault emit tokens it then refuses.
|
|
25152
|
+
async #emitToken(keyVersion, pointerIdB32, category) {
|
|
25153
|
+
const pointerId = base32Decode(pointerIdB32);
|
|
25154
|
+
const epoch = await this.#keys.materialFor(keyVersion);
|
|
25155
|
+
const signKey = deriveSubkeys(epoch.material).sign;
|
|
25156
|
+
return formatPointer(
|
|
25157
|
+
category,
|
|
25158
|
+
keyVersion,
|
|
25159
|
+
pointerId,
|
|
25160
|
+
signPointer(signKey, keyVersion, pointerId, category)
|
|
25161
|
+
);
|
|
25162
|
+
}
|
|
25163
|
+
async #openRow(row) {
|
|
25164
|
+
try {
|
|
25165
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
25166
|
+
return open(
|
|
25167
|
+
deriveSubkeys(epoch.material).enc,
|
|
25168
|
+
{
|
|
25169
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
25170
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
25171
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
25172
|
+
},
|
|
25173
|
+
bindingInput(row.keyVersion, base32Decode(row.pointerId), row.category, row.formatVersion)
|
|
25174
|
+
);
|
|
25175
|
+
} catch {
|
|
25176
|
+
return null;
|
|
25177
|
+
}
|
|
25178
|
+
}
|
|
25179
|
+
// Shared lookup for the read-only surfaces. It verifies the tag exactly as
|
|
25180
|
+
// detokenize does: a descriptor is not raw, but a token nobody can vouch for
|
|
25181
|
+
// should not resolve to anything at all — otherwise a fabricated pointer, or a
|
|
25182
|
+
// lookalike planted in a file, would still yield a category and a masked
|
|
25183
|
+
// preview. Verifying needs the historical epoch's key, which is why these
|
|
25184
|
+
// surfaces are async.
|
|
25185
|
+
async #rowFor(token) {
|
|
25186
|
+
const parsed = parsePointer(token);
|
|
25187
|
+
if (!parsed) return null;
|
|
25188
|
+
try {
|
|
25189
|
+
const epoch = await this.#keys.materialFor(parsed.keyVersion);
|
|
25190
|
+
const signKey = deriveSubkeys(epoch.material).sign;
|
|
25191
|
+
if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
|
|
25192
|
+
return null;
|
|
25193
|
+
}
|
|
25194
|
+
} catch {
|
|
25195
|
+
return null;
|
|
25196
|
+
}
|
|
25197
|
+
const row = this.#repo.byPointerId(base32Encode(parsed.pointerId));
|
|
25198
|
+
if (row?.category !== parsed.category) return null;
|
|
25199
|
+
return row;
|
|
25200
|
+
}
|
|
25201
|
+
#audit(pointerId, opts, outcome) {
|
|
25202
|
+
this.#repo.recordDeref({
|
|
25203
|
+
id: randomUUID10(),
|
|
25204
|
+
pointerId,
|
|
25205
|
+
at: this.#now(),
|
|
25206
|
+
target: opts.target,
|
|
25207
|
+
reason: opts.reason,
|
|
25208
|
+
outcome,
|
|
25209
|
+
...opts.grantId === void 0 ? {} : { grantId: opts.grantId },
|
|
25210
|
+
// Only the batched reasons carry a count above one; a model crossing is
|
|
25211
|
+
// always its own row.
|
|
25212
|
+
pointerCount: isBatchedDerefReason(opts.reason) ? opts.pointerCount ?? 1 : 1
|
|
25213
|
+
});
|
|
25214
|
+
}
|
|
25215
|
+
};
|
|
25216
|
+
|
|
25217
|
+
// ../../packages/persistence/src/warn-era-cap.ts
|
|
25218
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
25219
|
+
import { join as join6 } from "path";
|
|
23864
25220
|
var MARKER = "warn-era-capped";
|
|
23865
25221
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
23866
25222
|
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
23867
|
-
const marker =
|
|
25223
|
+
const marker = join6(dataDir2, MARKER);
|
|
23868
25224
|
if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
|
|
23869
25225
|
const capped = db.policies.capCategoryActions();
|
|
23870
|
-
|
|
25226
|
+
writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
|
|
23871
25227
|
`, { mode: DATA_FILE_MODE });
|
|
23872
25228
|
return { capped };
|
|
23873
25229
|
}
|
|
23874
25230
|
|
|
23875
25231
|
// ../../packages/plugin-sdk/src/config.ts
|
|
23876
25232
|
import { existsSync as existsSync4 } from "fs";
|
|
23877
|
-
import { join as
|
|
25233
|
+
import { join as join7 } from "path";
|
|
23878
25234
|
|
|
23879
25235
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
23880
25236
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
@@ -23928,7 +25284,7 @@ function resolveProvider() {
|
|
|
23928
25284
|
function loadConfig(base = defaultDataDir()) {
|
|
23929
25285
|
try {
|
|
23930
25286
|
ensureLayoutDirSync(base);
|
|
23931
|
-
const settingsFile =
|
|
25287
|
+
const settingsFile = join7(settingsDir(base), "settings.json");
|
|
23932
25288
|
if (existsSync4(settingsFile)) tightenFile(settingsFile);
|
|
23933
25289
|
} catch {
|
|
23934
25290
|
}
|
|
@@ -23952,9 +25308,9 @@ function resolveProviderSafe() {
|
|
|
23952
25308
|
}
|
|
23953
25309
|
|
|
23954
25310
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23955
|
-
import { readdirSync, readFileSync as
|
|
25311
|
+
import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
|
|
23956
25312
|
import { homedir as homedir2 } from "os";
|
|
23957
|
-
import { basename as basename2, join as
|
|
25313
|
+
import { basename as basename2, join as join9 } from "path";
|
|
23958
25314
|
|
|
23959
25315
|
// ../../packages/detections/src/egress/registry.ts
|
|
23960
25316
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -24742,12 +26098,12 @@ function redact(text, findings) {
|
|
|
24742
26098
|
const regions = [];
|
|
24743
26099
|
for (const f of sorted) {
|
|
24744
26100
|
const rank = SEVERITY_RANK2[f.severity];
|
|
24745
|
-
const
|
|
24746
|
-
if (
|
|
24747
|
-
|
|
24748
|
-
if (rank >
|
|
24749
|
-
|
|
24750
|
-
|
|
26101
|
+
const open2 = regions[regions.length - 1];
|
|
26102
|
+
if (open2 && f.span.start < open2.end) {
|
|
26103
|
+
open2.end = Math.max(open2.end, f.span.end);
|
|
26104
|
+
if (rank > open2.rank) {
|
|
26105
|
+
open2.rank = rank;
|
|
26106
|
+
open2.category = f.category;
|
|
24751
26107
|
}
|
|
24752
26108
|
} else {
|
|
24753
26109
|
regions.push({ start: f.span.start, end: f.span.end, category: f.category, rank });
|
|
@@ -24776,6 +26132,24 @@ function maskMatch(raw) {
|
|
|
24776
26132
|
return `${raw.charAt(0)}${"*".repeat(6)}${raw.charAt(raw.length - 1)}`;
|
|
24777
26133
|
}
|
|
24778
26134
|
|
|
26135
|
+
// ../../packages/detections/src/pointer-shield.ts
|
|
26136
|
+
function shieldPointers(text) {
|
|
26137
|
+
const spans = [];
|
|
26138
|
+
let out = null;
|
|
26139
|
+
for (const match of text.matchAll(pointerTokenScanner())) {
|
|
26140
|
+
spans.push({ start: match.index, end: match.index + match[0].length });
|
|
26141
|
+
out ??= text;
|
|
26142
|
+
out = out.slice(0, match.index) + " ".repeat(match[0].length) + out.slice(match.index + match[0].length);
|
|
26143
|
+
}
|
|
26144
|
+
return { text: out ?? text, spans };
|
|
26145
|
+
}
|
|
26146
|
+
function dropShieldedFindings(findings, spans) {
|
|
26147
|
+
if (spans.length === 0) return findings;
|
|
26148
|
+
return findings.filter(
|
|
26149
|
+
(finding) => !spans.some((s) => finding.span.start < s.end && finding.span.end > s.start)
|
|
26150
|
+
);
|
|
26151
|
+
}
|
|
26152
|
+
|
|
24779
26153
|
// ../../packages/detections/src/posture/config-posture.ts
|
|
24780
26154
|
var RULE_VERSION = "1";
|
|
24781
26155
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -26972,8 +28346,8 @@ function bundledDetections() {
|
|
|
26972
28346
|
}
|
|
26973
28347
|
|
|
26974
28348
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
26975
|
-
import { existsSync as existsSync5, readFileSync as
|
|
26976
|
-
import { basename, dirname, isAbsolute, join as
|
|
28349
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
28350
|
+
import { basename, dirname, isAbsolute, join as join8, sep as sep2 } from "path";
|
|
26977
28351
|
function resolveRepo(cwd) {
|
|
26978
28352
|
try {
|
|
26979
28353
|
const root = findGitRoot(cwd);
|
|
@@ -26995,36 +28369,36 @@ function resolveWorktreeRoot(cwd) {
|
|
|
26995
28369
|
function findGitRoot(start) {
|
|
26996
28370
|
let dir = start;
|
|
26997
28371
|
for (; ; ) {
|
|
26998
|
-
if (existsSync5(
|
|
28372
|
+
if (existsSync5(join8(dir, ".git"))) return dir;
|
|
26999
28373
|
const parent = dirname(dir);
|
|
27000
28374
|
if (parent === dir) return void 0;
|
|
27001
28375
|
dir = parent;
|
|
27002
28376
|
}
|
|
27003
28377
|
}
|
|
27004
28378
|
function resolveGitContext(root) {
|
|
27005
|
-
const dotGit =
|
|
28379
|
+
const dotGit = join8(root, ".git");
|
|
27006
28380
|
try {
|
|
27007
|
-
if (
|
|
27008
|
-
return { configPath:
|
|
28381
|
+
if (statSync2(dotGit).isDirectory()) {
|
|
28382
|
+
return { configPath: join8(dotGit, "config"), headRoot: root };
|
|
27009
28383
|
}
|
|
27010
28384
|
} catch {
|
|
27011
28385
|
return void 0;
|
|
27012
28386
|
}
|
|
27013
28387
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
27014
28388
|
if (!target) return void 0;
|
|
27015
|
-
const gitdir = isAbsolute(target) ? target :
|
|
27016
|
-
if (existsSync5(
|
|
27017
|
-
return { configPath:
|
|
28389
|
+
const gitdir = isAbsolute(target) ? target : join8(root, target);
|
|
28390
|
+
if (existsSync5(join8(gitdir, "config"))) {
|
|
28391
|
+
return { configPath: join8(gitdir, "config"), headRoot: root };
|
|
27018
28392
|
}
|
|
27019
|
-
const commonRaw = safeRead(
|
|
28393
|
+
const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
|
|
27020
28394
|
if (!commonRaw) return void 0;
|
|
27021
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
28395
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
|
|
27022
28396
|
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
27023
|
-
return { configPath:
|
|
28397
|
+
return { configPath: join8(commonGitDir, "config"), headRoot };
|
|
27024
28398
|
}
|
|
27025
28399
|
function safeRead(path) {
|
|
27026
28400
|
try {
|
|
27027
|
-
return
|
|
28401
|
+
return readFileSync4(path, "utf8");
|
|
27028
28402
|
} catch {
|
|
27029
28403
|
return void 0;
|
|
27030
28404
|
}
|
|
@@ -27062,13 +28436,13 @@ function slugFromUrl(url2) {
|
|
|
27062
28436
|
}
|
|
27063
28437
|
|
|
27064
28438
|
// ../../packages/plugin-sdk/src/events.ts
|
|
27065
|
-
import { createHash as createHash4, randomUUID as
|
|
28439
|
+
import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
|
|
27066
28440
|
function contentHashOf(text) {
|
|
27067
28441
|
return createHash4("sha256").update(text).digest("hex");
|
|
27068
28442
|
}
|
|
27069
28443
|
function buildIngestEvent(input) {
|
|
27070
28444
|
return {
|
|
27071
|
-
id:
|
|
28445
|
+
id: randomUUID11(),
|
|
27072
28446
|
sourceTool: input.sourceTool,
|
|
27073
28447
|
kind: input.kind,
|
|
27074
28448
|
occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -27079,7 +28453,7 @@ function buildIngestEvent(input) {
|
|
|
27079
28453
|
// SDK boot in the fail-open hook path). Preserve any id the caller already set.
|
|
27080
28454
|
metadata: {
|
|
27081
28455
|
...input.metadata,
|
|
27082
|
-
correlationId: input.metadata?.correlationId ??
|
|
28456
|
+
correlationId: input.metadata?.correlationId ?? randomUUID11()
|
|
27083
28457
|
}
|
|
27084
28458
|
};
|
|
27085
28459
|
}
|
|
@@ -27088,8 +28462,8 @@ function buildIngestEvent(input) {
|
|
|
27088
28462
|
import { arch, hostname as hostname3, platform, release } from "os";
|
|
27089
28463
|
|
|
27090
28464
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
27091
|
-
import { mkdirSync as
|
|
27092
|
-
import { join as
|
|
28465
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
28466
|
+
import { join as join10 } from "path";
|
|
27093
28467
|
|
|
27094
28468
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
27095
28469
|
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
@@ -27107,8 +28481,8 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
|
|
|
27107
28481
|
|
|
27108
28482
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
27109
28483
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
27110
|
-
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as
|
|
27111
|
-
import { basename as basename4, join as
|
|
28484
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
|
|
28485
|
+
import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
|
|
27112
28486
|
|
|
27113
28487
|
// ../../packages/plugin-sdk/src/raw-egress.ts
|
|
27114
28488
|
var MIN_RAW_LEN = 4;
|
|
@@ -27174,7 +28548,7 @@ async function filterUnsafeRules(rules, gateway, opts) {
|
|
|
27174
28548
|
}
|
|
27175
28549
|
|
|
27176
28550
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
27177
|
-
import { randomUUID as
|
|
28551
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
27178
28552
|
var ENFORCEMENT_CEILING_ENABLED = false;
|
|
27179
28553
|
var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
|
|
27180
28554
|
function entryIsActive(entry, now) {
|
|
@@ -27277,7 +28651,12 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27277
28651
|
if (worst === "block") return { action: "block", text: null, findings };
|
|
27278
28652
|
if (worst === "redact") {
|
|
27279
28653
|
const redactFindings = findings.filter((f) => actionFor(f) === "redact");
|
|
27280
|
-
return {
|
|
28654
|
+
return {
|
|
28655
|
+
action: "redact",
|
|
28656
|
+
text: redact(text, redactFindings),
|
|
28657
|
+
findings,
|
|
28658
|
+
enforcedFindings: redactFindings
|
|
28659
|
+
};
|
|
27281
28660
|
}
|
|
27282
28661
|
return { action: worst, text, findings };
|
|
27283
28662
|
}
|
|
@@ -27318,9 +28697,17 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27318
28697
|
else groups.set(pair, [finding]);
|
|
27319
28698
|
}
|
|
27320
28699
|
const now = Date.now();
|
|
28700
|
+
const preAuthorized = new Set(ctx.preAuthorizedGrantIds ?? []);
|
|
27321
28701
|
for (const [pair, group] of groups) {
|
|
27322
28702
|
const entry = entries.get(pair);
|
|
27323
|
-
if (!entry
|
|
28703
|
+
if (!entry) continue;
|
|
28704
|
+
if (preAuthorized.has(entry.id)) {
|
|
28705
|
+
if (!conditionsMatch(entry.conditions, ctx)) continue;
|
|
28706
|
+
for (const finding of group) excepted.add(finding);
|
|
28707
|
+
exceptionIds.push(entry.id);
|
|
28708
|
+
continue;
|
|
28709
|
+
}
|
|
28710
|
+
if (!entryIsActive(entry, now) || !conditionsMatch(entry.conditions, ctx)) {
|
|
27324
28711
|
continue;
|
|
27325
28712
|
}
|
|
27326
28713
|
let consumed = false;
|
|
@@ -27352,7 +28739,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27352
28739
|
const pair = `${finding.ruleId}:${fp}`;
|
|
27353
28740
|
if (seen.has(pair)) continue;
|
|
27354
28741
|
seen.add(pair);
|
|
27355
|
-
const reference =
|
|
28742
|
+
const reference = randomUUID12().replaceAll("-", "").slice(0, 6);
|
|
27356
28743
|
const maskedValue = maskMatch(finding.rawMatch);
|
|
27357
28744
|
try {
|
|
27358
28745
|
await gateway.recordBlockedDetection({
|
|
@@ -27376,7 +28763,8 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27376
28763
|
async function evaluate(text, context, ctx) {
|
|
27377
28764
|
try {
|
|
27378
28765
|
await ensureInitialized();
|
|
27379
|
-
const
|
|
28766
|
+
const shielded = shieldPointers(text);
|
|
28767
|
+
const findings = dropShieldedFindings(scan(shielded.text, rules, context), shielded.spans);
|
|
27380
28768
|
const fpCache = /* @__PURE__ */ new Map();
|
|
27381
28769
|
const { excepted, exceptionIds } = await applyExceptions(findings, ctx, fpCache);
|
|
27382
28770
|
const decision = decide(findings, text, excepted);
|
|
@@ -27399,7 +28787,11 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27399
28787
|
const { decision, excepted, exceptionIds } = await evaluate(
|
|
27400
28788
|
input.text,
|
|
27401
28789
|
filePath ? { filePath } : void 0,
|
|
27402
|
-
{
|
|
28790
|
+
{
|
|
28791
|
+
sourceTool: input.sourceTool,
|
|
28792
|
+
metadata: input.metadata,
|
|
28793
|
+
preAuthorizedGrantIds: opts2.preAuthorizedGrantIds
|
|
28794
|
+
}
|
|
27403
28795
|
);
|
|
27404
28796
|
if (opts2.persist === "with-findings" && decision.findings.length === 0) return decision;
|
|
27405
28797
|
try {
|
|
@@ -27429,7 +28821,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27429
28821
|
valueFingerprint: findingKeyFingerprintKey ? fingerprintOf(findingKeyFingerprintKey, match, findingKeyFpCache) : maskedMatch
|
|
27430
28822
|
}) : void 0;
|
|
27431
28823
|
return {
|
|
27432
|
-
id:
|
|
28824
|
+
id: randomUUID12(),
|
|
27433
28825
|
eventId: event.id,
|
|
27434
28826
|
ruleId: match.ruleId,
|
|
27435
28827
|
category: match.category,
|
|
@@ -27455,7 +28847,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27455
28847
|
const sorted = [...rules].sort((a, b) => a.id.localeCompare(b.id));
|
|
27456
28848
|
return contentHashOf(JSON.stringify(sorted));
|
|
27457
28849
|
} catch {
|
|
27458
|
-
return `unresolved-${
|
|
28850
|
+
return `unresolved-${randomUUID12()}`;
|
|
27459
28851
|
}
|
|
27460
28852
|
}
|
|
27461
28853
|
async function close() {
|
|
@@ -27468,8 +28860,303 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27468
28860
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
27469
28861
|
|
|
27470
28862
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
27471
|
-
import { mkdirSync as
|
|
27472
|
-
import { join as
|
|
28863
|
+
import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
28864
|
+
import { join as join12 } from "path";
|
|
28865
|
+
|
|
28866
|
+
// ../../packages/plugin-sdk/src/tokenize.ts
|
|
28867
|
+
function redactedPlaceholder(category) {
|
|
28868
|
+
return `[REDACTED:${category.toUpperCase()}]`;
|
|
28869
|
+
}
|
|
28870
|
+
var POINTER_UNAVAILABLE_TEXT = "[unavailable]";
|
|
28871
|
+
var SEVERITY_RANK3 = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
28872
|
+
function groupSpans(text, findings) {
|
|
28873
|
+
const sorted = [...findings].filter((f) => f.span.start >= 0 && f.span.end <= text.length && f.span.start < f.span.end).sort((a, b) => a.span.start - b.span.start || b.span.end - a.span.end);
|
|
28874
|
+
const groups = [];
|
|
28875
|
+
for (const finding of sorted) {
|
|
28876
|
+
const last = groups[groups.length - 1];
|
|
28877
|
+
if (last && finding.span.start < last.end) {
|
|
28878
|
+
last.end = Math.max(last.end, finding.span.end);
|
|
28879
|
+
if ((SEVERITY_RANK3[finding.severity] ?? 0) > (SEVERITY_RANK3[last.severity] ?? 0)) {
|
|
28880
|
+
last.category = finding.category;
|
|
28881
|
+
last.severity = finding.severity;
|
|
28882
|
+
}
|
|
28883
|
+
delete last.finding;
|
|
28884
|
+
continue;
|
|
28885
|
+
}
|
|
28886
|
+
groups.push({
|
|
28887
|
+
start: finding.span.start,
|
|
28888
|
+
end: finding.span.end,
|
|
28889
|
+
finding,
|
|
28890
|
+
category: finding.category,
|
|
28891
|
+
severity: finding.severity
|
|
28892
|
+
});
|
|
28893
|
+
}
|
|
28894
|
+
return groups;
|
|
28895
|
+
}
|
|
28896
|
+
var NULL_RESOLVER = () => Promise.resolve(null);
|
|
28897
|
+
var SecretVaultGlue = class {
|
|
28898
|
+
#vault;
|
|
28899
|
+
revealGrantResolver;
|
|
28900
|
+
// Set only when THIS glue opened the store, so a glue over an injected vault
|
|
28901
|
+
// never closes a handle it does not own.
|
|
28902
|
+
#release;
|
|
28903
|
+
constructor(vault, revealGrantResolver = NULL_RESOLVER, release2) {
|
|
28904
|
+
this.#vault = vault;
|
|
28905
|
+
this.revealGrantResolver = revealGrantResolver;
|
|
28906
|
+
this.#release = release2;
|
|
28907
|
+
}
|
|
28908
|
+
close() {
|
|
28909
|
+
const release2 = this.#release;
|
|
28910
|
+
this.#release = void 0;
|
|
28911
|
+
try {
|
|
28912
|
+
release2?.();
|
|
28913
|
+
} catch {
|
|
28914
|
+
}
|
|
28915
|
+
}
|
|
28916
|
+
async tokenizeValue(raw, meta3) {
|
|
28917
|
+
try {
|
|
28918
|
+
const result = await this.#vault.tokenize(raw, meta3);
|
|
28919
|
+
return typeof result === "string" ? result : redactedPlaceholder(meta3.category);
|
|
28920
|
+
} catch {
|
|
28921
|
+
return redactedPlaceholder(meta3.category);
|
|
28922
|
+
}
|
|
28923
|
+
}
|
|
28924
|
+
async tokenizeText(text, opts) {
|
|
28925
|
+
try {
|
|
28926
|
+
const findings = opts?.findings ?? this.#selfScan(text);
|
|
28927
|
+
if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
|
|
28928
|
+
if (findings.length === 0) return { text, pointers: [], degraded: [] };
|
|
28929
|
+
const groups = groupSpans(text, findings);
|
|
28930
|
+
const pointers = [];
|
|
28931
|
+
const degraded = [];
|
|
28932
|
+
let out = text;
|
|
28933
|
+
for (const group of [...groups].reverse()) {
|
|
28934
|
+
const original = text.slice(group.start, group.end);
|
|
28935
|
+
const finding = group.finding;
|
|
28936
|
+
let replacement;
|
|
28937
|
+
if (finding === void 0) {
|
|
28938
|
+
replacement = redactedPlaceholder(group.category);
|
|
28939
|
+
degraded.unshift({ category: group.category });
|
|
28940
|
+
} else if (original !== finding.rawMatch) {
|
|
28941
|
+
replacement = redactedPlaceholder(group.category);
|
|
28942
|
+
degraded.unshift({ category: group.category });
|
|
28943
|
+
} else {
|
|
28944
|
+
replacement = await this.tokenizeValue(finding.rawMatch, {
|
|
28945
|
+
ruleId: finding.ruleId,
|
|
28946
|
+
category: finding.category,
|
|
28947
|
+
maskedMatch: maskMatch(finding.rawMatch)
|
|
28948
|
+
});
|
|
28949
|
+
if (replacement.startsWith("[[aka:")) pointers.unshift(replacement);
|
|
28950
|
+
else degraded.unshift({ category: finding.category });
|
|
28951
|
+
}
|
|
28952
|
+
out = out.slice(0, group.start) + replacement + out.slice(group.end);
|
|
28953
|
+
}
|
|
28954
|
+
if (opts?.sighting && pointers.length > 0) {
|
|
28955
|
+
for (const pointer of pointers) {
|
|
28956
|
+
try {
|
|
28957
|
+
const id = pointer.split(".")[1];
|
|
28958
|
+
if (id !== void 0) this.#vault.recordSighting?.(id, opts.sighting);
|
|
28959
|
+
} catch {
|
|
28960
|
+
}
|
|
28961
|
+
}
|
|
28962
|
+
}
|
|
28963
|
+
return { text: out, pointers, degraded };
|
|
28964
|
+
} catch {
|
|
28965
|
+
return { text: "[REDACTED]", pointers: [], degraded: [] };
|
|
28966
|
+
}
|
|
28967
|
+
}
|
|
28968
|
+
async detokenizeText(text, opts) {
|
|
28969
|
+
try {
|
|
28970
|
+
const matches = [...text.matchAll(pointerTokenScanner())];
|
|
28971
|
+
if (matches.length === 0) return { text, revealed: 0 };
|
|
28972
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
28973
|
+
for (const match of matches) {
|
|
28974
|
+
occurrences.set(match[0], (occurrences.get(match[0]) ?? 0) + 1);
|
|
28975
|
+
}
|
|
28976
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
28977
|
+
for (const [pointer, count] of occurrences) {
|
|
28978
|
+
try {
|
|
28979
|
+
const value = await this.#vault.detokenize(pointer, {
|
|
28980
|
+
target: "human",
|
|
28981
|
+
reason: opts.reason,
|
|
28982
|
+
pointerCount: count
|
|
28983
|
+
});
|
|
28984
|
+
resolved.set(pointer, typeof value === "string" ? value : null);
|
|
28985
|
+
} catch {
|
|
28986
|
+
resolved.set(pointer, null);
|
|
28987
|
+
}
|
|
28988
|
+
}
|
|
28989
|
+
let out = text;
|
|
28990
|
+
let revealed = 0;
|
|
28991
|
+
for (const match of [...matches].reverse()) {
|
|
28992
|
+
const value = resolved.get(match[0]);
|
|
28993
|
+
const replacement = value ?? POINTER_UNAVAILABLE_TEXT;
|
|
28994
|
+
if (value !== null && value !== void 0) revealed += 1;
|
|
28995
|
+
out = out.slice(0, match.index) + replacement + out.slice(match.index + match[0].length);
|
|
28996
|
+
}
|
|
28997
|
+
return { text: out, revealed };
|
|
28998
|
+
} catch {
|
|
28999
|
+
return { text, revealed: 0 };
|
|
29000
|
+
}
|
|
29001
|
+
}
|
|
29002
|
+
// Scan with the bundled packs, as the mask path does. Pointers already in the
|
|
29003
|
+
// text are blanked first so a pointer is never re-tokenized. Returns null
|
|
29004
|
+
// when the registry or the scan itself failed — the caller must then treat
|
|
29005
|
+
// the whole text as unclassifiable.
|
|
29006
|
+
#selfScan(text) {
|
|
29007
|
+
try {
|
|
29008
|
+
registerBundledPacks();
|
|
29009
|
+
const shielded = shieldPointers(text);
|
|
29010
|
+
return dropShieldedFindings(scan(shielded.text, getLoadedRules()), shielded.spans);
|
|
29011
|
+
} catch {
|
|
29012
|
+
return null;
|
|
29013
|
+
}
|
|
29014
|
+
}
|
|
29015
|
+
async describePointerSafe(token) {
|
|
29016
|
+
try {
|
|
29017
|
+
return await this.#vault.describePointer(token);
|
|
29018
|
+
} catch {
|
|
29019
|
+
return null;
|
|
29020
|
+
}
|
|
29021
|
+
}
|
|
29022
|
+
async probeModelPointers(text, opts) {
|
|
29023
|
+
const granted = /* @__PURE__ */ new Map();
|
|
29024
|
+
const ungranted = [];
|
|
29025
|
+
try {
|
|
29026
|
+
for (const pointer of new Set([...text.matchAll(pointerTokenScanner())].map((m) => m[0]))) {
|
|
29027
|
+
try {
|
|
29028
|
+
const grantId = await opts.resolveGrant(pointer);
|
|
29029
|
+
if (grantId === null) ungranted.push(pointer);
|
|
29030
|
+
else granted.set(pointer, grantId);
|
|
29031
|
+
} catch {
|
|
29032
|
+
ungranted.push(pointer);
|
|
29033
|
+
}
|
|
29034
|
+
}
|
|
29035
|
+
return { granted, ungranted };
|
|
29036
|
+
} catch {
|
|
29037
|
+
return { granted: /* @__PURE__ */ new Map(), ungranted };
|
|
29038
|
+
}
|
|
29039
|
+
}
|
|
29040
|
+
async substituteModelPointers(text, opts) {
|
|
29041
|
+
try {
|
|
29042
|
+
const matches = [...text.matchAll(pointerTokenScanner())];
|
|
29043
|
+
if (matches.length === 0) return { text, revealed: [], unresolved: [], grantIds: [] };
|
|
29044
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
29045
|
+
for (const pointer of new Set(matches.map((m) => m[0]))) {
|
|
29046
|
+
try {
|
|
29047
|
+
const grantId = await opts.resolveGrant(pointer);
|
|
29048
|
+
if (grantId === null) {
|
|
29049
|
+
await this.#vault.detokenize(pointer, { target: "model", reason: "model-input" });
|
|
29050
|
+
resolved.set(pointer, null);
|
|
29051
|
+
continue;
|
|
29052
|
+
}
|
|
29053
|
+
const value = await this.#vault.detokenize(pointer, {
|
|
29054
|
+
target: "model",
|
|
29055
|
+
reason: "model-input",
|
|
29056
|
+
grantId
|
|
29057
|
+
});
|
|
29058
|
+
resolved.set(pointer, typeof value === "string" ? { value, grantId } : null);
|
|
29059
|
+
} catch {
|
|
29060
|
+
resolved.set(pointer, null);
|
|
29061
|
+
}
|
|
29062
|
+
}
|
|
29063
|
+
const spentGrants = /* @__PURE__ */ new Set();
|
|
29064
|
+
for (const entry of resolved.values()) {
|
|
29065
|
+
if (entry === null || spentGrants.has(entry.grantId)) continue;
|
|
29066
|
+
spentGrants.add(entry.grantId);
|
|
29067
|
+
try {
|
|
29068
|
+
await this.#vault.consumeGrant?.(entry.grantId);
|
|
29069
|
+
} catch {
|
|
29070
|
+
}
|
|
29071
|
+
}
|
|
29072
|
+
let out = text;
|
|
29073
|
+
const revealed = /* @__PURE__ */ new Set();
|
|
29074
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
29075
|
+
for (const match of [...matches].reverse()) {
|
|
29076
|
+
const entry = resolved.get(match[0]);
|
|
29077
|
+
if (entry === null || entry === void 0) {
|
|
29078
|
+
unresolved.add(match[0]);
|
|
29079
|
+
continue;
|
|
29080
|
+
}
|
|
29081
|
+
revealed.add(match[0]);
|
|
29082
|
+
out = out.slice(0, match.index) + entry.value + out.slice(match.index + match[0].length);
|
|
29083
|
+
}
|
|
29084
|
+
return {
|
|
29085
|
+
text: out,
|
|
29086
|
+
revealed: [...revealed],
|
|
29087
|
+
unresolved: [...unresolved],
|
|
29088
|
+
grantIds: [...spentGrants]
|
|
29089
|
+
};
|
|
29090
|
+
} catch {
|
|
29091
|
+
return { text, revealed: [], unresolved: [], grantIds: [] };
|
|
29092
|
+
}
|
|
29093
|
+
}
|
|
29094
|
+
};
|
|
29095
|
+
function createVaultGlue(options) {
|
|
29096
|
+
if (options?.vault) return new SecretVaultGlue(options.vault, options.revealResolver);
|
|
29097
|
+
const base = options?.base ?? defaultDataDir();
|
|
29098
|
+
try {
|
|
29099
|
+
const dir = dataDir(base);
|
|
29100
|
+
const db = openLocalDatabase(dir);
|
|
29101
|
+
const settings = readWorkspaceSettings(base);
|
|
29102
|
+
const provider = options?.policyProvider ?? new UserGrantPolicyProvider(db.exceptions);
|
|
29103
|
+
const vault = new SecretVault({
|
|
29104
|
+
repo: db.secretVault,
|
|
29105
|
+
keys: createKeyProvider(settings.vaultKeyCustody, keysDir(base)),
|
|
29106
|
+
fingerprintKey: loadOrCreateFingerprintKey(dir),
|
|
29107
|
+
// Read live so a revocation applies to the very next call, not the next
|
|
29108
|
+
// process.
|
|
29109
|
+
isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent),
|
|
29110
|
+
// This is the one construction site that reveals to the model, so it is
|
|
29111
|
+
// the one that supplies the last gate. The decision is re-taken from the
|
|
29112
|
+
// ROW's identity at the moment of crossing, which closes the window
|
|
29113
|
+
// between resolving a grant and spending it: a grant revoked in between
|
|
29114
|
+
// refuses here.
|
|
29115
|
+
//
|
|
29116
|
+
// The re-decision is on the identity alone, never on the grant id
|
|
29117
|
+
// matching the one the resolver returned. ExceptionPolicyProvider
|
|
29118
|
+
// promises no id stability across calls — a provider deciding from
|
|
29119
|
+
// external policy may well mint a fresh id each time — so comparing ids
|
|
29120
|
+
// would silently refuse every crossing for such a provider while looking
|
|
29121
|
+
// like a security check. `allow` for this row is the whole question.
|
|
29122
|
+
verifyGrant: async (_grantId, identity) => {
|
|
29123
|
+
const decision = await provider.decideReveal(identity);
|
|
29124
|
+
return decision.allow;
|
|
29125
|
+
}
|
|
29126
|
+
});
|
|
29127
|
+
const vaultWithSightings = {
|
|
29128
|
+
tokenize: (raw, meta3) => vault.tokenize(raw, meta3),
|
|
29129
|
+
detokenize: (token, opts) => vault.detokenize(token, opts),
|
|
29130
|
+
describePointer: (token) => vault.describePointer(token),
|
|
29131
|
+
resolvePointerIdentity: (token) => vault.resolvePointerIdentity(token),
|
|
29132
|
+
recordSighting: (pointerId, sighting) => {
|
|
29133
|
+
db.secretVault.recordSighting({ pointerId, ...sighting }, Date.now());
|
|
29134
|
+
},
|
|
29135
|
+
consumeGrant: (grantId) => db.exceptions.consume(grantId)
|
|
29136
|
+
};
|
|
29137
|
+
const revealGrantResolver = async (pointer) => {
|
|
29138
|
+
try {
|
|
29139
|
+
const identity = await vault.resolvePointerIdentity(pointer);
|
|
29140
|
+
if (identity === null) return null;
|
|
29141
|
+
const decision = await provider.decideReveal(identity);
|
|
29142
|
+
return decision.allow ? decision.grantId : null;
|
|
29143
|
+
} catch {
|
|
29144
|
+
return null;
|
|
29145
|
+
}
|
|
29146
|
+
};
|
|
29147
|
+
return new SecretVaultGlue(vaultWithSightings, revealGrantResolver, () => {
|
|
29148
|
+
db.close();
|
|
29149
|
+
});
|
|
29150
|
+
} catch {
|
|
29151
|
+
return new SecretVaultGlue(UNOPENABLE_VAULT);
|
|
29152
|
+
}
|
|
29153
|
+
}
|
|
29154
|
+
var UNOPENABLE_VAULT = {
|
|
29155
|
+
tokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
|
|
29156
|
+
detokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
|
|
29157
|
+
describePointer: () => Promise.resolve(null),
|
|
29158
|
+
resolvePointerIdentity: () => Promise.resolve(null)
|
|
29159
|
+
};
|
|
27473
29160
|
|
|
27474
29161
|
// src/command-registry.ts
|
|
27475
29162
|
import { readdirSync as readdirSync4 } from "fs";
|
|
@@ -27636,8 +29323,8 @@ function routeRemediationOption(option, handlers) {
|
|
|
27636
29323
|
}
|
|
27637
29324
|
|
|
27638
29325
|
// src/remediation/rotation-checklist.ts
|
|
27639
|
-
import { writeFileSync as
|
|
27640
|
-
import { join as
|
|
29326
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
29327
|
+
import { join as join13 } from "path";
|
|
27641
29328
|
var GENERIC_CONSOLE_PATH = "rotate via the provider's own console";
|
|
27642
29329
|
var CONSOLE_PATHS = {
|
|
27643
29330
|
anthropic: "console.anthropic.com \u2192 Settings \u2192 API keys",
|
|
@@ -27722,7 +29409,7 @@ function renderRotationChecklistResolvedLine(location) {
|
|
|
27722
29409
|
return `\u2713 I drafted a rotation checklist for you (${location}).`;
|
|
27723
29410
|
}
|
|
27724
29411
|
function writeRotationChecklist(entries, targetDirectory) {
|
|
27725
|
-
|
|
29412
|
+
writeFileSync6(
|
|
27726
29413
|
`${targetDirectory}/rotation-checklist.md`,
|
|
27727
29414
|
renderChecklistMarkdown(entries),
|
|
27728
29415
|
"utf8"
|
|
@@ -27737,7 +29424,7 @@ function generateRotationChecklist(input) {
|
|
|
27737
29424
|
try {
|
|
27738
29425
|
const target = resolveRotationChecklistTarget(input.cwd);
|
|
27739
29426
|
targetDirectory = target.directory;
|
|
27740
|
-
const filePath =
|
|
29427
|
+
const filePath = join13(target.directory, "rotation-checklist.md");
|
|
27741
29428
|
writeRotationChecklist(input.entries, target.directory);
|
|
27742
29429
|
return {
|
|
27743
29430
|
status: "written",
|
|
@@ -27776,6 +29463,10 @@ function renderRedactionConfirmation(redactedKeys) {
|
|
|
27776
29463
|
const noun = redactedKeys === 1 ? "key" : "keys";
|
|
27777
29464
|
return `\u2713 Redacted ${String(redactedKeys)} ${noun}`;
|
|
27778
29465
|
}
|
|
29466
|
+
function renderPointeredNote(pointeredKeys) {
|
|
29467
|
+
const subject = pointeredKeys === 1 ? "value was" : "values were";
|
|
29468
|
+
return `${String(pointeredKeys)} ${subject} replaced with recoverable vault pointers \u2014 view them in the dashboard or with \`aka vault show\`.`;
|
|
29469
|
+
}
|
|
27779
29470
|
function renderPartialRedactionLine(redactedKeys, totalKeys, unredactedFindings) {
|
|
27780
29471
|
const remainingCount = unredactedFindings.length;
|
|
27781
29472
|
const remainingFiles = [...new Set(unredactedFindings.map((finding) => finding.where.filePath))];
|
|
@@ -27785,21 +29476,30 @@ function renderPartialRedactionLine(redactedKeys, totalKeys, unredactedFindings)
|
|
|
27785
29476
|
return `Redacted ${String(redactedKeys)} of ${String(totalKeys)} ${totalNoun}; ${String(remainingCount)} ${remainingNoun} still ${remainingVerb} attention in ${remainingFiles.join(", ")}`;
|
|
27786
29477
|
}
|
|
27787
29478
|
function renderRedactionOutcome(input) {
|
|
29479
|
+
const pointeredKeys = input.pointeredKeys ?? 0;
|
|
27788
29480
|
const totalKeys = input.findings.length;
|
|
27789
29481
|
const isComplete = input.redactedKeys === totalKeys && input.unredactedFindings.length === 0;
|
|
27790
|
-
if (isComplete)
|
|
27791
|
-
|
|
29482
|
+
if (isComplete) {
|
|
29483
|
+
const confirmation = `${renderRedactionConfirmation(input.redactedKeys)}.`;
|
|
29484
|
+
return pointeredKeys === 0 ? confirmation : `${confirmation} ${renderPointeredNote(pointeredKeys)}`;
|
|
29485
|
+
}
|
|
29486
|
+
const partialLine = renderPartialRedactionLine(
|
|
29487
|
+
input.redactedKeys,
|
|
29488
|
+
totalKeys,
|
|
29489
|
+
input.unredactedFindings
|
|
29490
|
+
);
|
|
29491
|
+
return pointeredKeys === 0 ? partialLine : `${partialLine}. ${renderPointeredNote(pointeredKeys)}`;
|
|
27792
29492
|
}
|
|
27793
29493
|
function renderResolvedSummary(input) {
|
|
29494
|
+
const pointeredKeys = input.pointeredKeys ?? 0;
|
|
27794
29495
|
const totalKeys = input.findings.length;
|
|
27795
29496
|
const isComplete = input.redactedKeys === totalKeys && input.unredactedFindings.length === 0;
|
|
27796
29497
|
const preview = renderChecklistMarkdown(input.entries).trimEnd();
|
|
27797
29498
|
const checklistLine = input.degradedNote ?? renderRotationChecklistResolvedLine(input.location);
|
|
29499
|
+
const withPointeredNote = (line) => pointeredKeys === 0 ? line : `${line}. ${renderPointeredNote(pointeredKeys)}`;
|
|
27798
29500
|
if (!isComplete) {
|
|
27799
|
-
const redactionLine2 =
|
|
27800
|
-
input.redactedKeys,
|
|
27801
|
-
totalKeys,
|
|
27802
|
-
input.unredactedFindings
|
|
29501
|
+
const redactionLine2 = withPointeredNote(
|
|
29502
|
+
renderPartialRedactionLine(input.redactedKeys, totalKeys, input.unredactedFindings)
|
|
27803
29503
|
);
|
|
27804
29504
|
return ["Leaked secrets \u2014 partially redacted", redactionLine2, checklistLine, "", preview].join(
|
|
27805
29505
|
"\n"
|
|
@@ -27807,23 +29507,28 @@ function renderResolvedSummary(input) {
|
|
|
27807
29507
|
}
|
|
27808
29508
|
const transcriptCount = new Set(input.findings.map((finding) => finding.where.filePath)).size;
|
|
27809
29509
|
const transcriptNoun = transcriptCount === 1 ? "transcript" : "transcripts";
|
|
27810
|
-
const redactionLine =
|
|
29510
|
+
const redactionLine = withPointeredNote(
|
|
29511
|
+
`${renderRedactionConfirmation(input.redactedKeys)} across ${String(transcriptCount)} ${transcriptNoun}`
|
|
29512
|
+
);
|
|
27811
29513
|
return ["Leaked secrets \u2014 resolved", redactionLine, checklistLine, "", preview].join("\n");
|
|
27812
29514
|
}
|
|
27813
29515
|
|
|
27814
29516
|
// src/remediation/deliverable.ts
|
|
27815
29517
|
function resolveRemediationDeliverable(input) {
|
|
29518
|
+
const pointeredKeys = input.pointeredKeys ?? 0;
|
|
27816
29519
|
const unredactedFindings = input.unredactedFindings ?? [];
|
|
27817
29520
|
const entries = buildChecklistEntries(input.findings);
|
|
27818
29521
|
const writeResult = generateRotationChecklist({ entries, cwd: input.cwd });
|
|
27819
29522
|
const summary = writeResult.status === "written" ? renderResolvedSummary({
|
|
27820
29523
|
redactedKeys: input.redactedKeys,
|
|
29524
|
+
pointeredKeys,
|
|
27821
29525
|
findings: input.findings,
|
|
27822
29526
|
unredactedFindings,
|
|
27823
29527
|
location: writeResult.locationLabel,
|
|
27824
29528
|
entries
|
|
27825
29529
|
}) : renderResolvedSummary({
|
|
27826
29530
|
redactedKeys: input.redactedKeys,
|
|
29531
|
+
pointeredKeys,
|
|
27827
29532
|
findings: input.findings,
|
|
27828
29533
|
unredactedFindings,
|
|
27829
29534
|
degradedNote: writeResult.note,
|
|
@@ -27853,10 +29558,10 @@ function writeStandingSecretPosture(level, policies) {
|
|
|
27853
29558
|
}
|
|
27854
29559
|
|
|
27855
29560
|
// src/remediation/surfaced-redact.ts
|
|
27856
|
-
import { readFileSync as
|
|
29561
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
27857
29562
|
|
|
27858
29563
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
27859
|
-
import { randomUUID as
|
|
29564
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
27860
29565
|
|
|
27861
29566
|
// ../../packages/plugin-runtime/src/recorder.ts
|
|
27862
29567
|
var PLUGIN_RECORDER_BINARY = "plugin";
|
|
@@ -28018,7 +29723,7 @@ var StandaloneDataGateway = class {
|
|
|
28018
29723
|
const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
|
|
28019
29724
|
const installed = this.installedScanRules();
|
|
28020
29725
|
const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
|
|
28021
|
-
id:
|
|
29726
|
+
id: randomUUID13(),
|
|
28022
29727
|
scope: "global",
|
|
28023
29728
|
target: { ruleId },
|
|
28024
29729
|
action,
|
|
@@ -28171,15 +29876,15 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
|
|
|
28171
29876
|
}
|
|
28172
29877
|
|
|
28173
29878
|
// ../../packages/plugin-runtime/src/handle-session-start.ts
|
|
28174
|
-
import { randomUUID as
|
|
29879
|
+
import { randomUUID as randomUUID14 } from "crypto";
|
|
28175
29880
|
var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
28176
29881
|
|
|
28177
29882
|
// src/history/transcripts.ts
|
|
28178
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
29883
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync8 } from "fs";
|
|
28179
29884
|
import { homedir as homedir3 } from "os";
|
|
28180
|
-
import { join as
|
|
29885
|
+
import { join as join14 } from "path";
|
|
28181
29886
|
function transcriptsDir(home) {
|
|
28182
|
-
return
|
|
29887
|
+
return join14(home ?? homedir3(), ".claude", "projects");
|
|
28183
29888
|
}
|
|
28184
29889
|
var DAY_MS5 = 24 * 60 * 60 * 1e3;
|
|
28185
29890
|
|
|
@@ -28191,9 +29896,17 @@ function deriveProvider(ruleId) {
|
|
|
28191
29896
|
}
|
|
28192
29897
|
|
|
28193
29898
|
// src/remediation/redact.ts
|
|
28194
|
-
import { readFileSync as
|
|
29899
|
+
import { readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
28195
29900
|
import { isAbsolute as isAbsolute2, relative as relative2, resolve } from "path";
|
|
28196
29901
|
var REDACTED_PLACEHOLDER = "[REDACTED:SECRET]";
|
|
29902
|
+
var REPLACE_PATTERN_SEQUENCE = /\$[$&`'<0-9]/;
|
|
29903
|
+
function replacementFor(rawValue, replacements) {
|
|
29904
|
+
const candidate = replacements?.get(rawValue);
|
|
29905
|
+
if (candidate === void 0 || candidate.includes(rawValue) || REPLACE_PATTERN_SEQUENCE.test(candidate)) {
|
|
29906
|
+
return REDACTED_PLACEHOLDER;
|
|
29907
|
+
}
|
|
29908
|
+
return candidate;
|
|
29909
|
+
}
|
|
28197
29910
|
function platformRedactionScope(home) {
|
|
28198
29911
|
return { artifactRoots: [transcriptsDir(home)] };
|
|
28199
29912
|
}
|
|
@@ -28215,7 +29928,7 @@ function resolveRedactableArtifact(filePath, scope) {
|
|
|
28215
29928
|
if (realTarget === null) return null;
|
|
28216
29929
|
return scope.artifactRoots.some((root) => isWithinRoot(realTarget, root)) ? realTarget : null;
|
|
28217
29930
|
}
|
|
28218
|
-
function redactLeakedKeysDetailed(targets, scope = platformRedactionScope()) {
|
|
29931
|
+
function redactLeakedKeysDetailed(targets, scope = platformRedactionScope(), replacements) {
|
|
28219
29932
|
const byFile = /* @__PURE__ */ new Map();
|
|
28220
29933
|
for (const target of targets) {
|
|
28221
29934
|
if (target.rawValue === "") continue;
|
|
@@ -28226,42 +29939,55 @@ function redactLeakedKeysDetailed(targets, scope = platformRedactionScope()) {
|
|
|
28226
29939
|
else existing.push(target);
|
|
28227
29940
|
}
|
|
28228
29941
|
let redactedKeys = 0;
|
|
29942
|
+
let pointeredKeys = 0;
|
|
28229
29943
|
const struck = [];
|
|
28230
29944
|
for (const [filePath, fileTargets] of byFile) {
|
|
28231
29945
|
let content;
|
|
28232
29946
|
try {
|
|
28233
|
-
content =
|
|
29947
|
+
content = readFileSync9(filePath, "utf8");
|
|
28234
29948
|
} catch {
|
|
28235
29949
|
continue;
|
|
28236
29950
|
}
|
|
28237
29951
|
const struckHere = [];
|
|
28238
|
-
|
|
29952
|
+
let pointeredHere = 0;
|
|
29953
|
+
const applied = /* @__PURE__ */ new Map();
|
|
28239
29954
|
for (const target of fileTargets) {
|
|
28240
|
-
|
|
29955
|
+
const prior = applied.get(target.rawValue);
|
|
29956
|
+
if (prior !== void 0) {
|
|
28241
29957
|
struckHere.push(target);
|
|
29958
|
+
if (prior !== REDACTED_PLACEHOLDER) pointeredHere += 1;
|
|
28242
29959
|
continue;
|
|
28243
29960
|
}
|
|
28244
29961
|
if (!content.includes(target.rawValue)) continue;
|
|
28245
|
-
|
|
28246
|
-
|
|
29962
|
+
let replacement = replacementFor(target.rawValue, replacements);
|
|
29963
|
+
let next = content.split(target.rawValue).join(replacement);
|
|
29964
|
+
if (next.includes(target.rawValue)) {
|
|
29965
|
+
replacement = REDACTED_PLACEHOLDER;
|
|
29966
|
+
next = content.split(target.rawValue).join(replacement);
|
|
29967
|
+
if (next.includes(target.rawValue)) continue;
|
|
29968
|
+
}
|
|
29969
|
+
content = next;
|
|
29970
|
+
applied.set(target.rawValue, replacement);
|
|
28247
29971
|
struckHere.push(target);
|
|
29972
|
+
if (replacement !== REDACTED_PLACEHOLDER) pointeredHere += 1;
|
|
28248
29973
|
}
|
|
28249
29974
|
if (struckHere.length === 0) continue;
|
|
28250
29975
|
const tmpPath = `${filePath}.aka-redact.tmp`;
|
|
28251
29976
|
try {
|
|
28252
|
-
|
|
28253
|
-
|
|
29977
|
+
writeFileSync7(tmpPath, content);
|
|
29978
|
+
renameSync5(tmpPath, filePath);
|
|
28254
29979
|
} catch {
|
|
28255
29980
|
try {
|
|
28256
|
-
|
|
29981
|
+
rmSync4(tmpPath, { force: true, recursive: true });
|
|
28257
29982
|
} catch {
|
|
28258
29983
|
}
|
|
28259
29984
|
continue;
|
|
28260
29985
|
}
|
|
28261
29986
|
redactedKeys += struckHere.length;
|
|
29987
|
+
pointeredKeys += pointeredHere;
|
|
28262
29988
|
struck.push(...struckHere);
|
|
28263
29989
|
}
|
|
28264
|
-
return { redactedKeys, struck };
|
|
29990
|
+
return { redactedKeys, pointeredKeys, struck };
|
|
28265
29991
|
}
|
|
28266
29992
|
|
|
28267
29993
|
// src/remediation/surfaced-redact.ts
|
|
@@ -28279,10 +30005,44 @@ function recoverTarget(finding, matches) {
|
|
|
28279
30005
|
const hit = matches.find(
|
|
28280
30006
|
(m) => deriveProvider(m.ruleId) === finding.provider && safeMaskedMatch(m.rawMatch) === finding.maskedToken
|
|
28281
30007
|
);
|
|
28282
|
-
return hit === void 0 ? void 0 : {
|
|
30008
|
+
return hit === void 0 ? void 0 : {
|
|
30009
|
+
target: { where: finding.where, rawValue: hit.rawMatch },
|
|
30010
|
+
ruleId: hit.ruleId,
|
|
30011
|
+
category: hit.category
|
|
30012
|
+
};
|
|
30013
|
+
}
|
|
30014
|
+
function hasValidVaultConsent(base) {
|
|
30015
|
+
try {
|
|
30016
|
+
return isVaultConsentValid(loadConfig(base).settings.vaultConsent);
|
|
30017
|
+
} catch {
|
|
30018
|
+
return false;
|
|
30019
|
+
}
|
|
30020
|
+
}
|
|
30021
|
+
async function buildPointerReplacements(recovered, base) {
|
|
30022
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
30023
|
+
try {
|
|
30024
|
+
const glue = createVaultGlue(base === void 0 ? void 0 : { base });
|
|
30025
|
+
const distinct = /* @__PURE__ */ new Map();
|
|
30026
|
+
for (const entry of recovered) {
|
|
30027
|
+
if (!distinct.has(entry.target.rawValue)) distinct.set(entry.target.rawValue, entry);
|
|
30028
|
+
}
|
|
30029
|
+
for (const [rawValue, entry] of distinct) {
|
|
30030
|
+
const replacement = await glue.tokenizeValue(rawValue, {
|
|
30031
|
+
ruleId: entry.ruleId,
|
|
30032
|
+
category: entry.category,
|
|
30033
|
+
maskedMatch: maskMatch(rawValue)
|
|
30034
|
+
});
|
|
30035
|
+
if (replacement.startsWith("[[aka:") && replacement !== rawValue) {
|
|
30036
|
+
replacements.set(rawValue, replacement);
|
|
30037
|
+
}
|
|
30038
|
+
}
|
|
30039
|
+
} catch {
|
|
30040
|
+
return /* @__PURE__ */ new Map();
|
|
30041
|
+
}
|
|
30042
|
+
return replacements;
|
|
28283
30043
|
}
|
|
28284
30044
|
async function redactSurfacedSecrets(findings, overrides = {}) {
|
|
28285
|
-
if (findings.length === 0) return { redactedKeys: 0, unredacted: [] };
|
|
30045
|
+
if (findings.length === 0) return { redactedKeys: 0, pointeredKeys: 0, unredacted: [] };
|
|
28286
30046
|
const scope = enforcedScope(overrides);
|
|
28287
30047
|
const byFile = /* @__PURE__ */ new Map();
|
|
28288
30048
|
const outOfScope = [];
|
|
@@ -28295,7 +30055,8 @@ async function redactSurfacedSecrets(findings, overrides = {}) {
|
|
|
28295
30055
|
if (existing) existing.push(finding);
|
|
28296
30056
|
else byFile.set(finding.where.filePath, [finding]);
|
|
28297
30057
|
}
|
|
28298
|
-
if (byFile.size === 0) return { redactedKeys: 0, unredacted: findings };
|
|
30058
|
+
if (byFile.size === 0) return { redactedKeys: 0, pointeredKeys: 0, unredacted: findings };
|
|
30059
|
+
const vaultConsented = hasValidVaultConsent(overrides.dataDirBase);
|
|
28299
30060
|
const unrecovered = [...outOfScope];
|
|
28300
30061
|
const recovered = [];
|
|
28301
30062
|
try {
|
|
@@ -28306,7 +30067,7 @@ async function redactSurfacedSecrets(findings, overrides = {}) {
|
|
|
28306
30067
|
for (const [filePath, fileFindings] of byFile) {
|
|
28307
30068
|
let content;
|
|
28308
30069
|
try {
|
|
28309
|
-
content =
|
|
30070
|
+
content = readFileSync10(filePath, "utf8");
|
|
28310
30071
|
} catch {
|
|
28311
30072
|
unrecovered.push(...fileFindings);
|
|
28312
30073
|
continue;
|
|
@@ -28319,9 +30080,9 @@ async function redactSurfacedSecrets(findings, overrides = {}) {
|
|
|
28319
30080
|
continue;
|
|
28320
30081
|
}
|
|
28321
30082
|
for (const finding of fileFindings) {
|
|
28322
|
-
const
|
|
28323
|
-
if (
|
|
28324
|
-
else recovered.push({ finding,
|
|
30083
|
+
const recovery = recoverTarget(finding, matches);
|
|
30084
|
+
if (recovery === void 0) unrecovered.push(finding);
|
|
30085
|
+
else recovered.push({ finding, recovery });
|
|
28325
30086
|
}
|
|
28326
30087
|
}
|
|
28327
30088
|
} finally {
|
|
@@ -28331,18 +30092,23 @@ async function redactSurfacedSecrets(findings, overrides = {}) {
|
|
|
28331
30092
|
}
|
|
28332
30093
|
}
|
|
28333
30094
|
} catch {
|
|
28334
|
-
return { redactedKeys: 0, unredacted: findings };
|
|
28335
|
-
}
|
|
28336
|
-
const
|
|
28337
|
-
recovered.map((r) => r.
|
|
28338
|
-
|
|
30095
|
+
return { redactedKeys: 0, pointeredKeys: 0, unredacted: findings };
|
|
30096
|
+
}
|
|
30097
|
+
const replacements = vaultConsented ? await buildPointerReplacements(
|
|
30098
|
+
recovered.map((r) => r.recovery),
|
|
30099
|
+
overrides.dataDirBase
|
|
30100
|
+
) : void 0;
|
|
30101
|
+
const { redactedKeys, pointeredKeys, struck } = redactLeakedKeysDetailed(
|
|
30102
|
+
recovered.map((r) => r.recovery.target),
|
|
30103
|
+
scope,
|
|
30104
|
+
replacements
|
|
28339
30105
|
);
|
|
28340
30106
|
const struckTargets = new Set(struck);
|
|
28341
30107
|
const unredacted = [
|
|
28342
30108
|
...unrecovered,
|
|
28343
|
-
...recovered.filter((r) => !struckTargets.has(r.target)).map((r) => r.finding)
|
|
30109
|
+
...recovered.filter((r) => !struckTargets.has(r.recovery.target)).map((r) => r.finding)
|
|
28344
30110
|
];
|
|
28345
|
-
return { redactedKeys, unredacted };
|
|
30111
|
+
return { redactedKeys, pointeredKeys, unredacted };
|
|
28346
30112
|
}
|
|
28347
30113
|
|
|
28348
30114
|
// src/remediation/entry.ts
|
|
@@ -28420,7 +30186,7 @@ async function route(frameText, rawOption, rawPosture) {
|
|
|
28420
30186
|
process.stdout.write(show(FRAME_READ_NOTE));
|
|
28421
30187
|
return;
|
|
28422
30188
|
}
|
|
28423
|
-
const redaction = isRedactRoute ? await redactSurfacedSecrets(findings) : { redactedKeys: 0, unredacted: [] };
|
|
30189
|
+
const redaction = isRedactRoute ? await redactSurfacedSecrets(findings) : { redactedKeys: 0, pointeredKeys: 0, unredacted: [] };
|
|
28424
30190
|
const redactedKeys = redaction.redactedKeys;
|
|
28425
30191
|
const outcome = routeRemediationOption(option, {
|
|
28426
30192
|
redact: () => redactedKeys,
|
|
@@ -28433,6 +30199,7 @@ async function route(frameText, rawOption, rawPosture) {
|
|
|
28433
30199
|
show(
|
|
28434
30200
|
renderRedactionOutcome({
|
|
28435
30201
|
redactedKeys: outcome.redactedKeys,
|
|
30202
|
+
pointeredKeys: redaction.pointeredKeys,
|
|
28436
30203
|
findings,
|
|
28437
30204
|
unredactedFindings: redaction.unredacted
|
|
28438
30205
|
})
|
|
@@ -28446,6 +30213,7 @@ async function route(frameText, rawOption, rawPosture) {
|
|
|
28446
30213
|
const deliverable = resolveRemediationDeliverable({
|
|
28447
30214
|
findings,
|
|
28448
30215
|
redactedKeys: outcome.redactedKeys,
|
|
30216
|
+
pointeredKeys: redaction.pointeredKeys,
|
|
28449
30217
|
unredactedFindings: redaction.unredacted,
|
|
28450
30218
|
cwd: process.cwd()
|
|
28451
30219
|
});
|
|
@@ -28464,7 +30232,7 @@ if (process.argv[1] && fileURLToPath2(import.meta.url) === process.argv[1]) {
|
|
|
28464
30232
|
try {
|
|
28465
30233
|
const argv = process.argv.slice(2);
|
|
28466
30234
|
const optionIndex = argv.indexOf("--option");
|
|
28467
|
-
const frameText =
|
|
30235
|
+
const frameText = readFileSync11(0, "utf8");
|
|
28468
30236
|
if (optionIndex === -1) {
|
|
28469
30237
|
present(frameText);
|
|
28470
30238
|
} else {
|