@akasecurity/ai-tc-claude-code 0.9.9 → 0.9.11
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 +2 -2
- package/hooks/hooks.json +1 -1
- package/package.json +4 -4
- package/scripts/apply-suppressions.js +1703 -982
- package/scripts/backfill.js +2669 -1576
- package/scripts/filescan.js +2463 -1429
- package/scripts/firstrun.js +2479 -1450
- package/scripts/history-sync.js +2544 -1498
- package/scripts/intro.js +577 -302
- package/scripts/message-display.js +1716 -974
- package/scripts/onboard.js +2136 -1411
- package/scripts/post-model-switch.js +663 -379
- package/scripts/post-tool-use.js +2655 -1441
- package/scripts/pre-model-switch.js +2371 -1372
- package/scripts/pre-tool-use.js +2698 -1457
- package/scripts/query.js +2557 -1465
- package/scripts/reconcile.js +2469 -1407
- package/scripts/remediate.js +2548 -1445
- package/scripts/scan-worker.js +425 -228
- package/scripts/session-start.js +2460 -1462
- package/scripts/start-light.js +580 -305
- package/scripts/statusline.js +2404 -1409
- package/scripts/stop.js +893 -391
- package/scripts/sync.js +17709 -2077
- package/scripts/user-prompt-submit.js +2506 -1445
|
@@ -492,12 +492,13 @@ var require_ignore = __commonJS({
|
|
|
492
492
|
});
|
|
493
493
|
|
|
494
494
|
// ../../packages/plugin-sdk/src/config.ts
|
|
495
|
-
import { existsSync as
|
|
496
|
-
import { join as
|
|
495
|
+
import { existsSync as existsSync8 } from "fs";
|
|
496
|
+
import { join as join14 } from "path";
|
|
497
497
|
|
|
498
498
|
// ../../packages/persistence/src/attached-derived.ts
|
|
499
499
|
import { rmSync } from "fs";
|
|
500
500
|
import { join } from "path";
|
|
501
|
+
var POLICY_CACHE_FILENAME = "policy-cache.json";
|
|
501
502
|
|
|
502
503
|
// ../../packages/persistence/src/control-plane-credential.ts
|
|
503
504
|
import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
|
|
@@ -596,6 +597,30 @@ var SQLITE_MIGRATIONS = [
|
|
|
596
597
|
{
|
|
597
598
|
tag: "0022_audit_inspection_ms",
|
|
598
599
|
sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
tag: "0023_secret_vault_user_authorized",
|
|
603
|
+
sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
tag: "0024_finding_resolution_key_created_index",
|
|
607
|
+
sql: "DROP INDEX IF EXISTS `idx_finding_resolution_key`;--> statement-breakpoint\nCREATE INDEX `idx_finding_resolution_key_created` ON `finding_resolution` (`finding_key`,`created_at`);"
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
tag: "0025_audit_capture_attribute_columns",
|
|
611
|
+
sql: "ALTER TABLE `audit_events` ADD `source_tool` text GENERATED ALWAYS AS (json_extract(attributes, '$.source_tool')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `repo` text GENERATED ALWAYS AS (json_extract(attributes, '$.repo')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `file_path` text GENERATED ALWAYS AS (json_extract(attributes, '$.file_path')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `tool_name` text GENERATED ALWAYS AS (json_extract(attributes, '$.tool_name')) VIRTUAL;"
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
tag: "0026_audit_llm_call_usage_columns",
|
|
615
|
+
sql: "ALTER TABLE `audit_events` ADD `service_tier` text GENERATED ALWAYS AS (json_extract(attributes, '$.service_tier')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_1h_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_1h_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_5m_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_5m_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `web_search_requests` integer GENERATED ALWAYS AS (json_extract(attributes, '$.web_search_requests')) VIRTUAL;"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
tag: "0027_audit_llm_usage_index",
|
|
619
|
+
sql: "CREATE INDEX `idx_audit_llm_usage` ON `audit_events` (`started_at`,`root_session_id`,`provider`,`model`,`service_tier`,`input_tokens`,`output_tokens`,`cache_creation_input_tokens`,`cache_read_input_tokens`,`ephemeral_1h_input_tokens`,`ephemeral_5m_input_tokens`,`web_search_requests`) WHERE event_type = 'llm_call' AND attributes IS NOT NULL;"
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
tag: "0028_activity_session_probe_indexes",
|
|
623
|
+
sql: "CREATE INDEX `idx_audit_session_prompt` ON `audit_events` (`root_session_id`) WHERE event_type = 'prompt';--> statement-breakpoint\nCREATE INDEX `idx_audit_session_share` ON `audit_events` (`root_session_id`) WHERE event_type = 'share';--> statement-breakpoint\nCREATE INDEX `idx_audit_ended_at` ON `audit_events` (`ended_at`,`root_session_id`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\nCREATE INDEX `idx_audit_session_ended` ON `audit_events` (`root_session_id`,`ended_at`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\n-- Expression index for the activity list's turns rollup: a live-captured\n-- session's turns are the DISTINCT `run_key` across its `llm_call` leaves, and\n-- carrying the extracted key in the index answers that count from the index\n-- alone instead of parsing every leaf's attribute bag. Written by hand, as\n-- 0013's `idx_audit_code_change_path` was: drizzle-kit cannot emit an\n-- expression containing a comma, so this index is not declared in sqlite.ts.\nCREATE INDEX `idx_audit_session_run_key` ON `audit_events` (`root_session_id`, json_extract(`attributes`, '$.run_key')) WHERE `event_type` = 'llm_call';\n"
|
|
599
624
|
}
|
|
600
625
|
];
|
|
601
626
|
|
|
@@ -20716,13 +20741,11 @@ var FindingGroup = external_exports.object({
|
|
|
20716
20741
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
20717
20742
|
instances: external_exports.array(FindingInstance),
|
|
20718
20743
|
// Derived from instances' statuses with open-dominates precedence (see
|
|
20719
|
-
//
|
|
20744
|
+
// foldGroupStatus). Undefined only when no instance carries a status.
|
|
20720
20745
|
status: FindingStatus.optional(),
|
|
20721
|
-
// The distinct people across the WHOLE group, not just the
|
|
20722
|
-
//
|
|
20723
|
-
//
|
|
20724
|
-
// instance carries a user, or when the store supplied whole-group folds
|
|
20725
|
-
// without one.
|
|
20746
|
+
// The distinct people across the WHOLE group, not just the instances
|
|
20747
|
+
// carried here. Undefined when no instance carries a user, or when the
|
|
20748
|
+
// store supplied whole-group folds without one.
|
|
20726
20749
|
users: external_exports.array(FindingUser).optional()
|
|
20727
20750
|
}).meta({ id: "FindingGroup" });
|
|
20728
20751
|
var FindingStats = external_exports.object({
|
|
@@ -20751,21 +20774,31 @@ var FindingFacets = external_exports.object({
|
|
|
20751
20774
|
// counted under no value.
|
|
20752
20775
|
status: external_exports.array(FindingFacetItem),
|
|
20753
20776
|
// Host tool (attributes.tool_name). Present only on the instance-level
|
|
20754
|
-
// reads, which can filter by it; the
|
|
20777
|
+
// reads, which can filter by it; the type-level read omits the dimension
|
|
20755
20778
|
// because a group spans tools.
|
|
20756
20779
|
tool: external_exports.array(FindingFacetItem).optional()
|
|
20757
20780
|
}).meta({ id: "FindingFacets" });
|
|
20758
|
-
var
|
|
20759
|
-
|
|
20781
|
+
var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
|
|
20782
|
+
id: "FindingTypeSummary"
|
|
20783
|
+
});
|
|
20784
|
+
var DEFAULT_FINDING_TYPES_LIMIT = 50;
|
|
20785
|
+
var MAX_FINDING_TYPES_LIMIT = 100;
|
|
20786
|
+
var ListFindingTypesQuery = external_exports.object({
|
|
20760
20787
|
// NOTE: severity filters by Severity (critical/high/medium/low), not by
|
|
20761
|
-
// FindingAction.
|
|
20788
|
+
// FindingAction. It narrows TYPES: a type's severity is the one its newest
|
|
20789
|
+
// firing version carries, and this list pages types.
|
|
20790
|
+
//
|
|
20791
|
+
// That is NOT a claim the findings of a type share it. A rule can hold several
|
|
20792
|
+
// definition versions at different severities, so a type kept by this filter
|
|
20793
|
+
// can hold findings that individually do not match — see totals.findings on
|
|
20794
|
+
// ListFindingTypesResponse, which counts them all.
|
|
20762
20795
|
severity: external_exports.array(Severity).optional(),
|
|
20763
20796
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20764
20797
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20765
20798
|
action: external_exports.array(FindingAction).optional(),
|
|
20766
|
-
// Matches a
|
|
20767
|
-
// individual
|
|
20768
|
-
//
|
|
20799
|
+
// Matches a type's DERIVED status (see FindingGroup.status), not its
|
|
20800
|
+
// individual findings' — so a filtered row's status always reads one of the
|
|
20801
|
+
// requested values.
|
|
20769
20802
|
status: external_exports.array(FindingStatus).optional(),
|
|
20770
20803
|
q: external_exports.string().optional(),
|
|
20771
20804
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
@@ -20775,23 +20808,37 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
20775
20808
|
// from a time-scoped page (Activity's range) can carry that scope. Absent
|
|
20776
20809
|
// means all time — this list has no default window.
|
|
20777
20810
|
from: external_exports.iso.datetime().optional(),
|
|
20778
|
-
// A
|
|
20779
|
-
//
|
|
20780
|
-
//
|
|
20781
|
-
//
|
|
20782
|
-
//
|
|
20811
|
+
// A RULE id that must appear in the page even when the cursor has already
|
|
20812
|
+
// advanced past its sort position. This is what keeps the selected type
|
|
20813
|
+
// visible in the list once it paginates: the target is appended out of sort
|
|
20814
|
+
// order rather than scanned forward for. Never affects totals, facets or the
|
|
20815
|
+
// cursor. Unlike the grouped read this replaces, it names a rule only — an
|
|
20816
|
+
// instance id is resolved by `findingInstance`, which is a primary-key seek
|
|
20817
|
+
// and so is not bounded by what any page happens to hold.
|
|
20783
20818
|
includeId: external_exports.string().optional(),
|
|
20784
|
-
|
|
20785
|
-
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
20819
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
|
|
20786
20820
|
cursor: external_exports.string().optional()
|
|
20787
20821
|
});
|
|
20788
|
-
var
|
|
20822
|
+
var ListFindingTypesResponse = external_exports.object({
|
|
20789
20823
|
totals: external_exports.object({
|
|
20824
|
+
// Findings belonging to the matching TYPES — not findings that each match
|
|
20825
|
+
// the filters. The filters here select types, so a type that survives
|
|
20826
|
+
// contributes its whole instanceCount.
|
|
20827
|
+
//
|
|
20828
|
+
// `status` is the one exception, narrowed per finding via
|
|
20829
|
+
// countInstancesByStatus. `severity`, `provider` and `action` are not, so
|
|
20830
|
+
// this can exceed what the instance read reports for the same filters: a
|
|
20831
|
+
// rule whose severity moved between versions is kept on its newest and
|
|
20832
|
+
// still counts its older findings. Narrowing the other three needs
|
|
20833
|
+
// per-dimension counts the aggregate does not carry today.
|
|
20790
20834
|
findings: external_exports.number().int().nonnegative(),
|
|
20791
|
-
|
|
20835
|
+
// Counts TYPES, which is the unit this read pages. The instance read's
|
|
20836
|
+
// own totals count findings; the two deliberately answer different
|
|
20837
|
+
// questions and are never summed.
|
|
20838
|
+
types: external_exports.number().int().nonnegative()
|
|
20792
20839
|
}),
|
|
20793
20840
|
facets: FindingFacets,
|
|
20794
|
-
items: external_exports.array(
|
|
20841
|
+
items: external_exports.array(FindingTypeSummary),
|
|
20795
20842
|
nextCursor: external_exports.string().nullable(),
|
|
20796
20843
|
// Present only on session-scoped queries (`sessionId` set): per ruleId, how
|
|
20797
20844
|
// many times that rule fired in the session's persisted transcript. Findings
|
|
@@ -20799,7 +20846,7 @@ var ListGroupedFindingsResponse = external_exports.object({
|
|
|
20799
20846
|
// every firing, so the two numbers legitimately differ — this map lets a
|
|
20800
20847
|
// session-scoped view show both.
|
|
20801
20848
|
sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
|
|
20802
|
-
}).meta({ id: "
|
|
20849
|
+
}).meta({ id: "ListFindingTypesResponse" });
|
|
20803
20850
|
var ApplyFindingActionRequest = external_exports.object({
|
|
20804
20851
|
// 'quarantined' is system-assigned (see FindingAction) — clients may not set
|
|
20805
20852
|
// it, so it is excluded from the request contract. The mapping helper
|
|
@@ -20829,12 +20876,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
|
|
|
20829
20876
|
var MAX_FLAT_FINDINGS_LIMIT = 200;
|
|
20830
20877
|
var ListFindingInstancesQuery = external_exports.object({
|
|
20831
20878
|
severity: external_exports.array(Severity).optional(),
|
|
20832
|
-
// Rule ids, the same vocabulary the
|
|
20879
|
+
// Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
|
|
20880
|
+
// ONE of them is how the master/detail view scopes its right-hand panel.
|
|
20833
20881
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20834
20882
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20835
20883
|
action: external_exports.array(FindingAction).optional(),
|
|
20836
20884
|
// Matches each instance's OWN derived status (deriveFindingStatus), unlike
|
|
20837
|
-
// the
|
|
20885
|
+
// the types query's type-level fold.
|
|
20838
20886
|
status: external_exports.array(FindingStatus).optional(),
|
|
20839
20887
|
// Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
|
|
20840
20888
|
// where the free-text `q` can only match the rendered "via Bash" label.
|
|
@@ -20851,37 +20899,47 @@ var ListFindingInstancesQuery = external_exports.object({
|
|
|
20851
20899
|
});
|
|
20852
20900
|
var ListFindingInstancesResponse = external_exports.object({
|
|
20853
20901
|
// Instances matching the filters across the whole scope, not just this
|
|
20854
|
-
// page — cursor-independent, like the
|
|
20902
|
+
// page — cursor-independent, like the types list's totals.
|
|
20855
20903
|
totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
|
|
20856
|
-
// Counts in INSTANCES here, where the
|
|
20904
|
+
// Counts in INSTANCES here, where the types response counts types. Each
|
|
20857
20905
|
// dimension still excludes its own filter.
|
|
20858
20906
|
facets: FindingFacets,
|
|
20859
20907
|
items: external_exports.array(FindingInstanceDetail),
|
|
20860
20908
|
nextCursor: external_exports.string().nullable()
|
|
20861
20909
|
}).meta({ id: "ListFindingInstancesResponse" });
|
|
20862
|
-
var
|
|
20863
|
-
//
|
|
20864
|
-
//
|
|
20865
|
-
|
|
20866
|
-
|
|
20867
|
-
|
|
20868
|
-
|
|
20869
|
-
// Folded from the instances' derived statuses with the same
|
|
20870
|
-
// open-dominates precedence a group uses.
|
|
20871
|
-
status: FindingStatus.optional(),
|
|
20872
|
-
// Distinct rules seen at this location, capped — the row shows them as
|
|
20873
|
-
// chips, and the count is what conveys scale.
|
|
20874
|
-
ruleIds: external_exports.array(external_exports.string())
|
|
20875
|
-
}).meta({ id: "FindingLocationFile" });
|
|
20876
|
-
var FindingLocationRepo = external_exports.object({
|
|
20910
|
+
var FindingLocationSummary = external_exports.object({
|
|
20911
|
+
// Opaque, stable, minted from the pair by encodeLocationId. It exists
|
|
20912
|
+
// because a location's identity is two values and a URL param carries one:
|
|
20913
|
+
// `?loc=` names a location the way `?rule=` names a type. Only ever compared
|
|
20914
|
+
// for EQUALITY — the page's selection check, this read's `includeId`, the
|
|
20915
|
+
// client's page dedupe — never decoded, and never a sort key.
|
|
20916
|
+
id: external_exports.string(),
|
|
20877
20917
|
/** Empty when the instances carried no repo attribute. */
|
|
20878
20918
|
repo: external_exports.string(),
|
|
20919
|
+
// Empty when the instances carried no file path (a prompt, or a tool call
|
|
20920
|
+
// with no file attribution). Both halves empty is a real location — usually
|
|
20921
|
+
// the largest one in a store — and is selectable like any other.
|
|
20922
|
+
file: external_exports.string(),
|
|
20879
20923
|
instanceCount: external_exports.number().int().nonnegative(),
|
|
20924
|
+
// The WORST severity present, not the first row's. It is this list's primary
|
|
20925
|
+
// sort key, so it is also what explains why a row is where it is, and it is
|
|
20926
|
+
// how a reader decides what to open without opening everything.
|
|
20880
20927
|
maxSeverity: Severity,
|
|
20881
20928
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
20929
|
+
// Folded from the instances' derived statuses with the same open-dominates
|
|
20930
|
+
// precedence a group uses, so it answers "is anything left to do here" and
|
|
20931
|
+
// not much more: a location holding 1 open among 40 resolved reads like one
|
|
20932
|
+
// holding 40 open. That loss is accepted — the panel beside this list
|
|
20933
|
+
// carries each finding's own status, and instanceCount sits next to the
|
|
20934
|
+
// badge.
|
|
20882
20935
|
status: FindingStatus.optional(),
|
|
20883
|
-
|
|
20884
|
-
|
|
20936
|
+
// Every distinct rule seen at this location, UNCAPPED — so the length is a
|
|
20937
|
+
// tally rather than a sample and a row can say how many there are. Bounded
|
|
20938
|
+
// by the ruleset, not by the store. The view bounds what it DISPLAYS.
|
|
20939
|
+
ruleIds: external_exports.array(external_exports.string())
|
|
20940
|
+
}).meta({ id: "FindingLocationSummary" });
|
|
20941
|
+
var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
|
|
20942
|
+
var MAX_FINDING_LOCATIONS_LIMIT = 100;
|
|
20885
20943
|
var ListFindingLocationsQuery = external_exports.object({
|
|
20886
20944
|
severity: external_exports.array(Severity).optional(),
|
|
20887
20945
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
@@ -20894,18 +20952,42 @@ var ListFindingLocationsQuery = external_exports.object({
|
|
|
20894
20952
|
q: external_exports.string().optional(),
|
|
20895
20953
|
sessionId: external_exports.string().optional(),
|
|
20896
20954
|
from: external_exports.iso.datetime().optional(),
|
|
20897
|
-
|
|
20955
|
+
// A LOCATION id (see FindingLocationSummary.id) that must appear in the page
|
|
20956
|
+
// even when the cursor has already advanced past its sort position — the
|
|
20957
|
+
// counterpart of ListFindingTypesQuery.includeId, and needed far more often
|
|
20958
|
+
// here. Selecting a row pushes the URL, which re-renders the server and resets
|
|
20959
|
+
// the client's page cache to page 0; with distinct (repo, file) pairs running
|
|
20960
|
+
// into the thousands, a selection sitting off page 0 is the ordinary case
|
|
20961
|
+
// rather than a deep-link corner. Never affects totals, facets or the cursor.
|
|
20962
|
+
includeId: external_exports.string().optional(),
|
|
20963
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
|
|
20964
|
+
cursor: external_exports.string().optional()
|
|
20898
20965
|
});
|
|
20899
20966
|
var ListFindingLocationsResponse = external_exports.object({
|
|
20900
20967
|
totals: external_exports.object({
|
|
20968
|
+
// Findings matching the filters across the whole scope. Unlike the types
|
|
20969
|
+
// read's same-named field this needs no caveat: the filters here narrow
|
|
20970
|
+
// per finding, so this is the sum of every row's instanceCount.
|
|
20901
20971
|
findings: external_exports.number().int().nonnegative(),
|
|
20902
|
-
|
|
20903
|
-
|
|
20972
|
+
// Counts LOCATIONS, the unit this read pages — the number the paginator
|
|
20973
|
+
// states. The facets beside it count FINDINGS (see below); a surface
|
|
20974
|
+
// showing both says which is which.
|
|
20975
|
+
locations: external_exports.number().int().nonnegative()
|
|
20904
20976
|
}),
|
|
20905
|
-
|
|
20906
|
-
|
|
20907
|
-
|
|
20908
|
-
|
|
20977
|
+
// Counts in FINDINGS, where the types response counts types, each dimension
|
|
20978
|
+
// still excluding its own filter. Deliberately not locations: counting those
|
|
20979
|
+
// needs a set of location keys per dimension per value — memory tracking the
|
|
20980
|
+
// store times the vocabulary, in a read whose scan promises flat memory —
|
|
20981
|
+
// and the cheap per-location version is not an approximation but WRONG. A
|
|
20982
|
+
// location holding {claudecode, block} and {codex, warn} would survive
|
|
20983
|
+
// provider=claudecode AND action=warn, under which no single finding
|
|
20984
|
+
// matches, so the facet would contradict the instanceCount this whole view
|
|
20985
|
+
// rests on. Findings also keep the toolbar in the same unit as the page
|
|
20986
|
+
// tally and the panel it sits above.
|
|
20987
|
+
facets: FindingFacets,
|
|
20988
|
+
/** Sorted by max severity, then most recent, then (repo, file). */
|
|
20989
|
+
items: external_exports.array(FindingLocationSummary),
|
|
20990
|
+
nextCursor: external_exports.string().nullable()
|
|
20909
20991
|
}).meta({ id: "ListFindingLocationsResponse" });
|
|
20910
20992
|
|
|
20911
20993
|
// ../../packages/schema/src/zod/meta.ts
|
|
@@ -22073,6 +22155,14 @@ var ControlPlaneErrorBody = external_exports.object({
|
|
|
22073
22155
|
message: external_exports.string().optional()
|
|
22074
22156
|
}).optional()
|
|
22075
22157
|
});
|
|
22158
|
+
var RemoteFailureKind = external_exports.enum([
|
|
22159
|
+
"unauthorized",
|
|
22160
|
+
"forbidden",
|
|
22161
|
+
"route-absent",
|
|
22162
|
+
"invalid-request",
|
|
22163
|
+
"rejected",
|
|
22164
|
+
"unreachable"
|
|
22165
|
+
]);
|
|
22076
22166
|
var AttachDeviceRequest = external_exports.object({
|
|
22077
22167
|
// This machine's own continuity id, so re-attaching ROTATES the credential
|
|
22078
22168
|
// on one machine record instead of producing a second one. Client-minted
|
|
@@ -22134,6 +22224,26 @@ var AttachTokenResponse = external_exports.union([
|
|
|
22134
22224
|
AttachTokenExpired,
|
|
22135
22225
|
external_exports.object({ status: printable(64) })
|
|
22136
22226
|
]);
|
|
22227
|
+
var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
|
|
22228
|
+
var DeviceCommand = external_exports.object({
|
|
22229
|
+
id: printable(128).min(1),
|
|
22230
|
+
kind: DeviceCommandKind,
|
|
22231
|
+
issuedAt: printable(64).min(1),
|
|
22232
|
+
expiresAt: printable(64).min(1)
|
|
22233
|
+
}).strict();
|
|
22234
|
+
var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
|
|
22235
|
+
var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
|
|
22236
|
+
var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
|
|
22237
|
+
external_exports.object({
|
|
22238
|
+
outcome: external_exports.literal("reported"),
|
|
22239
|
+
projectsScanned: external_exports.number().int().nonnegative()
|
|
22240
|
+
}).strict(),
|
|
22241
|
+
external_exports.object({
|
|
22242
|
+
outcome: external_exports.literal("failed"),
|
|
22243
|
+
reason: DeviceCommandFailureReason,
|
|
22244
|
+
projectsScanned: external_exports.number().int().nonnegative()
|
|
22245
|
+
}).strict()
|
|
22246
|
+
]);
|
|
22137
22247
|
|
|
22138
22248
|
// ../../packages/schema/src/zod/registry.ts
|
|
22139
22249
|
var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
|
|
@@ -22300,7 +22410,7 @@ var PackManifest = external_exports.object({
|
|
|
22300
22410
|
}).meta({ id: "PackManifest" });
|
|
22301
22411
|
|
|
22302
22412
|
// ../../packages/schema/src/zod/detection.ts
|
|
22303
|
-
var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
|
|
22413
|
+
var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
|
|
22304
22414
|
var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
|
|
22305
22415
|
var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
|
|
22306
22416
|
var DetectionCounts = external_exports.object({
|
|
@@ -22437,14 +22547,17 @@ function optional2(key, parsed, raw) {
|
|
|
22437
22547
|
function isStringArray(value) {
|
|
22438
22548
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
22439
22549
|
}
|
|
22550
|
+
var ORIGIN_VALUES = { library: true, custom: true };
|
|
22551
|
+
function resolveOrigin(origin) {
|
|
22552
|
+
return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
|
|
22553
|
+
}
|
|
22440
22554
|
function summaryToDetectionListItem(s) {
|
|
22441
22555
|
return {
|
|
22442
22556
|
id: `${s.namespace}/${s.packId}`,
|
|
22443
22557
|
name: s.name,
|
|
22444
22558
|
version: s.version,
|
|
22445
22559
|
enabled: s.enabled,
|
|
22446
|
-
origin:
|
|
22447
|
-
// v1: every installed pack is library origin
|
|
22560
|
+
origin: resolveOrigin(s.origin),
|
|
22448
22561
|
namespace: s.namespace,
|
|
22449
22562
|
packId: s.packId,
|
|
22450
22563
|
ruleCount: s.ruleCount,
|
|
@@ -22496,7 +22609,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
|
|
|
22496
22609
|
name: row.name,
|
|
22497
22610
|
version: row.version,
|
|
22498
22611
|
enabled: row.enabled,
|
|
22499
|
-
origin:
|
|
22612
|
+
origin: resolveOrigin(row.origin),
|
|
22500
22613
|
namespace: row.namespace,
|
|
22501
22614
|
packId: row.packId,
|
|
22502
22615
|
ruleCount: row.rules.length,
|
|
@@ -22516,16 +22629,20 @@ function splitDetectionId(id) {
|
|
|
22516
22629
|
}
|
|
22517
22630
|
function buildDetectionsList(summaries, query) {
|
|
22518
22631
|
const withUpdate = summaries.filter((s) => s.latestVersion != null);
|
|
22632
|
+
const originOf = (s) => resolveOrigin(s.origin);
|
|
22519
22633
|
const counts = {
|
|
22520
22634
|
all: summaries.length,
|
|
22521
|
-
library: summaries.length,
|
|
22522
|
-
|
|
22523
|
-
|
|
22635
|
+
library: summaries.filter((s) => originOf(s) === "library").length,
|
|
22636
|
+
custom: summaries.filter((s) => originOf(s) === "custom").length,
|
|
22637
|
+
// No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
|
|
22638
|
+
// omission: `customized` would mean a LIBRARY pack whose rules were edited in
|
|
22639
|
+
// place, and that state does not exist — editing a library pack forks it. See
|
|
22640
|
+
// OriginEnum.
|
|
22524
22641
|
customized: 0,
|
|
22525
22642
|
updates: withUpdate.length
|
|
22526
22643
|
};
|
|
22527
22644
|
const filter = query.filter;
|
|
22528
|
-
let filtered = filter === "custom"
|
|
22645
|
+
let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
|
|
22529
22646
|
if (query.q) {
|
|
22530
22647
|
const q = query.q.toLowerCase();
|
|
22531
22648
|
filtered = filtered.filter(
|
|
@@ -22605,8 +22722,9 @@ var Event = external_exports.object({
|
|
|
22605
22722
|
metadata: EventMetadata.optional()
|
|
22606
22723
|
}).meta({ id: "Event" });
|
|
22607
22724
|
var IngestEvent = Event.meta({ id: "IngestEvent" });
|
|
22725
|
+
var INGEST_BATCH_MAX = 100;
|
|
22608
22726
|
var IngestBatch = external_exports.object({
|
|
22609
|
-
events: external_exports.array(IngestEvent).min(1).max(
|
|
22727
|
+
events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
|
|
22610
22728
|
// Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
|
|
22611
22729
|
// additionally rejects any event whose contentHash the store has already
|
|
22612
22730
|
// recorded — for re-runnable bulk ingest (worktree scan, transcript
|
|
@@ -22738,139 +22856,62 @@ function deriveFindingStatus(row) {
|
|
|
22738
22856
|
if (row.latestResolutionStatus === "dismissed") return "dismissed";
|
|
22739
22857
|
return "open";
|
|
22740
22858
|
}
|
|
22741
|
-
function distinctUsers(instances) {
|
|
22742
|
-
const seen = /* @__PURE__ */ new Set();
|
|
22743
|
-
const users = [];
|
|
22744
|
-
for (const i of instances) {
|
|
22745
|
-
if (i.user === void 0 || seen.has(i.user.id)) continue;
|
|
22746
|
-
seen.add(i.user.id);
|
|
22747
|
-
users.push(i.user);
|
|
22748
|
-
}
|
|
22749
|
-
return users;
|
|
22750
|
-
}
|
|
22751
22859
|
function sortUsers(users) {
|
|
22752
22860
|
return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
22753
22861
|
}
|
|
22754
|
-
function
|
|
22755
|
-
const overrides = opts.overrides;
|
|
22862
|
+
function buildFindingTypes(aggregates, opts = {}) {
|
|
22756
22863
|
const packNames = opts.packNames;
|
|
22757
|
-
const
|
|
22758
|
-
const
|
|
22759
|
-
|
|
22760
|
-
const
|
|
22761
|
-
|
|
22762
|
-
else byRuleId.set(row.ruleId, [row]);
|
|
22763
|
-
}
|
|
22764
|
-
const groups = [];
|
|
22765
|
-
for (const [ruleId, ruleRows] of byRuleId) {
|
|
22766
|
-
const instances = ruleRows.map((r) => {
|
|
22767
|
-
const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
|
|
22768
|
-
return {
|
|
22769
|
-
id: r.id,
|
|
22770
|
-
provider: toApiProvider(r.sourceTool),
|
|
22771
|
-
repo: r.repo,
|
|
22772
|
-
file: r.file,
|
|
22773
|
-
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
22774
|
-
...r.eventId === void 0 ? {} : { eventId: r.eventId },
|
|
22775
|
-
...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
|
|
22776
|
-
...r.user === void 0 ? {} : { user: r.user },
|
|
22777
|
-
action: toApiAction(effectiveDbAction),
|
|
22778
|
-
detectedAt: r.occurredAt,
|
|
22779
|
-
confidence: r.confidence,
|
|
22780
|
-
status: r.status
|
|
22781
|
-
};
|
|
22782
|
-
});
|
|
22783
|
-
const agg = aggregates?.get(ruleId);
|
|
22784
|
-
const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
|
|
22785
|
-
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
22786
|
-
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
22787
|
-
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
22788
|
-
);
|
|
22789
|
-
const seenProviders = /* @__PURE__ */ new Set();
|
|
22790
|
-
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
22791
|
-
if (seenProviders.has(p)) return false;
|
|
22792
|
-
seenProviders.add(p);
|
|
22793
|
-
return true;
|
|
22794
|
-
});
|
|
22795
|
-
const actionSet = new Set(
|
|
22796
|
-
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
22797
|
-
);
|
|
22864
|
+
const types = [];
|
|
22865
|
+
for (const [ruleId, agg] of aggregates) {
|
|
22866
|
+
const users = sortUsers(agg.users ?? []);
|
|
22867
|
+
const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
|
|
22868
|
+
const actionSet = new Set(agg.actionsTaken.map(toApiAction));
|
|
22798
22869
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
22799
|
-
const
|
|
22800
|
-
const
|
|
22801
|
-
id: ruleId,
|
|
22802
|
-
name: packNames?.get(ruleId) ?? null
|
|
22803
|
-
};
|
|
22804
|
-
const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
|
|
22805
|
-
const policy = { id: `category:${apiCategory}`, name: apiCategory };
|
|
22806
|
-
const match = {
|
|
22807
|
-
maskedValue: ruleRows[0]?.maskedMatch ?? "",
|
|
22808
|
-
contextPrefix: ""
|
|
22809
|
-
// empty (pending privacy review)
|
|
22810
|
-
};
|
|
22811
|
-
const status = foldGroupStatus(
|
|
22812
|
-
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
22813
|
-
);
|
|
22814
|
-
const group = {
|
|
22870
|
+
const apiCategory = toApiCategory(agg.category ?? "custom");
|
|
22871
|
+
const type = {
|
|
22815
22872
|
id: ruleId,
|
|
22816
22873
|
category: apiCategory,
|
|
22817
22874
|
subtype: ruleId,
|
|
22818
22875
|
// human label comes with pack metadata later
|
|
22819
|
-
severity,
|
|
22820
|
-
|
|
22821
|
-
|
|
22822
|
-
|
|
22823
|
-
instanceCount: agg?.instanceCount ?? instances.length,
|
|
22876
|
+
severity: agg.severity ?? "low",
|
|
22877
|
+
detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
|
|
22878
|
+
policy: { id: `category:${apiCategory}`, name: apiCategory },
|
|
22879
|
+
instanceCount: agg.instanceCount,
|
|
22824
22880
|
providers,
|
|
22825
22881
|
aggregateAction,
|
|
22826
|
-
latestDetectedAt,
|
|
22827
|
-
|
|
22828
|
-
status,
|
|
22882
|
+
latestDetectedAt: agg.latestDetectedAt,
|
|
22883
|
+
status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
|
|
22829
22884
|
...users.length > 0 ? { users } : {}
|
|
22830
22885
|
};
|
|
22831
|
-
|
|
22832
|
-
|
|
22833
|
-
|
|
22834
|
-
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
22835
|
-
}
|
|
22886
|
+
actionsCache.set(type, [...actionSet]);
|
|
22887
|
+
if (agg.searchText !== void 0) {
|
|
22888
|
+
haystackCache.set(type, buildHaystack(type, agg.searchText));
|
|
22836
22889
|
}
|
|
22837
|
-
|
|
22890
|
+
types.push(type);
|
|
22838
22891
|
}
|
|
22839
|
-
return
|
|
22892
|
+
return types;
|
|
22840
22893
|
}
|
|
22841
22894
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
22842
|
-
function buildHaystack(
|
|
22895
|
+
function buildHaystack(t, extra) {
|
|
22843
22896
|
return [
|
|
22844
|
-
|
|
22845
|
-
|
|
22846
|
-
|
|
22847
|
-
|
|
22848
|
-
|
|
22849
|
-
...g.instances.map((i) => i.repo),
|
|
22850
|
-
...g.instances.map((i) => i.file),
|
|
22851
|
-
...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
|
|
22852
|
-
...g.instances.map((i) => i.id),
|
|
22853
|
-
// The people: the whole group's list when the store folded one, plus the
|
|
22854
|
-
// preview's own — the two overlap, and a haystack does not mind.
|
|
22855
|
-
...(g.users ?? []).map((u) => u.name),
|
|
22856
|
-
...g.instances.map((i) => i.user?.name ?? ""),
|
|
22897
|
+
t.subtype,
|
|
22898
|
+
t.category,
|
|
22899
|
+
t.policy.name,
|
|
22900
|
+
t.id,
|
|
22901
|
+
...(t.users ?? []).map((u) => u.name),
|
|
22857
22902
|
...extra === void 0 ? [] : [extra]
|
|
22858
22903
|
].join(" ").toLowerCase();
|
|
22859
22904
|
}
|
|
22860
|
-
function
|
|
22861
|
-
const cached2 = haystackCache.get(
|
|
22905
|
+
function typeHaystack(t) {
|
|
22906
|
+
const cached2 = haystackCache.get(t);
|
|
22862
22907
|
if (cached2 !== void 0) return cached2;
|
|
22863
|
-
const haystack = buildHaystack(
|
|
22864
|
-
haystackCache.set(
|
|
22908
|
+
const haystack = buildHaystack(t);
|
|
22909
|
+
haystackCache.set(t, haystack);
|
|
22865
22910
|
return haystack;
|
|
22866
22911
|
}
|
|
22867
22912
|
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
22868
|
-
function
|
|
22869
|
-
|
|
22870
|
-
if (cached2 !== void 0) return cached2;
|
|
22871
|
-
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
22872
|
-
actionsCache.set(g, actions);
|
|
22873
|
-
return actions;
|
|
22913
|
+
function typeActions(t) {
|
|
22914
|
+
return actionsCache.get(t) ?? [];
|
|
22874
22915
|
}
|
|
22875
22916
|
function countInstancesByStatus(statusInputs, statuses) {
|
|
22876
22917
|
const statusSet = new Set(statuses);
|
|
@@ -22881,8 +22922,8 @@ function countInstancesByStatus(statusInputs, statuses) {
|
|
|
22881
22922
|
}
|
|
22882
22923
|
return sum;
|
|
22883
22924
|
}
|
|
22884
|
-
function applyFindingFilters(
|
|
22885
|
-
let filtered =
|
|
22925
|
+
function applyFindingFilters(types, opts) {
|
|
22926
|
+
let filtered = types;
|
|
22886
22927
|
if (opts.severity && opts.severity.length > 0) {
|
|
22887
22928
|
const sevSet = new Set(opts.severity);
|
|
22888
22929
|
filtered = filtered.filter((g) => sevSet.has(g.severity));
|
|
@@ -22893,7 +22934,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
22893
22934
|
}
|
|
22894
22935
|
if (opts.actions && opts.actions.length > 0) {
|
|
22895
22936
|
const actionSet = new Set(opts.actions);
|
|
22896
|
-
filtered = filtered.filter((
|
|
22937
|
+
filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
|
|
22897
22938
|
}
|
|
22898
22939
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
22899
22940
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -22905,7 +22946,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
22905
22946
|
}
|
|
22906
22947
|
if (opts.q) {
|
|
22907
22948
|
const q = opts.q.toLowerCase();
|
|
22908
|
-
filtered = filtered.filter((
|
|
22949
|
+
filtered = filtered.filter((t) => typeHaystack(t).includes(q));
|
|
22909
22950
|
}
|
|
22910
22951
|
return filtered;
|
|
22911
22952
|
}
|
|
@@ -22920,11 +22961,11 @@ function compareFindingGroupOrder(a, b) {
|
|
|
22920
22961
|
if (recencyDiff !== 0) return recencyDiff;
|
|
22921
22962
|
return a.id.localeCompare(b.id);
|
|
22922
22963
|
}
|
|
22923
|
-
function
|
|
22924
|
-
return [...
|
|
22964
|
+
function sortFindingTypes(types) {
|
|
22965
|
+
return [...types].sort(compareFindingGroupOrder);
|
|
22925
22966
|
}
|
|
22926
|
-
function computeFindingFacets(
|
|
22927
|
-
const forSeverity = applyFindingFilters(
|
|
22967
|
+
function computeFindingFacets(allTypes, opts) {
|
|
22968
|
+
const forSeverity = applyFindingFilters(allTypes, {
|
|
22928
22969
|
providers: opts.providers,
|
|
22929
22970
|
actions: opts.actions,
|
|
22930
22971
|
statuses: opts.statuses,
|
|
@@ -22935,7 +22976,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22935
22976
|
for (const g of forSeverity) {
|
|
22936
22977
|
severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
|
|
22937
22978
|
}
|
|
22938
|
-
const forProvider = applyFindingFilters(
|
|
22979
|
+
const forProvider = applyFindingFilters(allTypes, {
|
|
22939
22980
|
actions: opts.actions,
|
|
22940
22981
|
statuses: opts.statuses,
|
|
22941
22982
|
q: opts.q,
|
|
@@ -22946,7 +22987,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22946
22987
|
for (const g of forProvider) {
|
|
22947
22988
|
for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
|
|
22948
22989
|
}
|
|
22949
|
-
const forAction = applyFindingFilters(
|
|
22990
|
+
const forAction = applyFindingFilters(allTypes, {
|
|
22950
22991
|
providers: opts.providers,
|
|
22951
22992
|
statuses: opts.statuses,
|
|
22952
22993
|
q: opts.q,
|
|
@@ -22955,9 +22996,9 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22955
22996
|
});
|
|
22956
22997
|
const actionMap = /* @__PURE__ */ new Map();
|
|
22957
22998
|
for (const g of forAction) {
|
|
22958
|
-
for (const a of
|
|
22999
|
+
for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
22959
23000
|
}
|
|
22960
|
-
const forSubtype = applyFindingFilters(
|
|
23001
|
+
const forSubtype = applyFindingFilters(allTypes, {
|
|
22961
23002
|
providers: opts.providers,
|
|
22962
23003
|
actions: opts.actions,
|
|
22963
23004
|
statuses: opts.statuses,
|
|
@@ -22966,7 +23007,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22966
23007
|
});
|
|
22967
23008
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
22968
23009
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
22969
|
-
const forStatus = applyFindingFilters(
|
|
23010
|
+
const forStatus = applyFindingFilters(allTypes, {
|
|
22970
23011
|
providers: opts.providers,
|
|
22971
23012
|
actions: opts.actions,
|
|
22972
23013
|
q: opts.q,
|
|
@@ -23014,10 +23055,20 @@ function matchesDimension(row, opts, dimension) {
|
|
|
23014
23055
|
return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
|
|
23015
23056
|
case "tools":
|
|
23016
23057
|
return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
|
|
23058
|
+
// An EMPTY value is a real filter here, not an absent one. The location
|
|
23059
|
+
// list buckets a finding whose event recorded no repo — or no file — under
|
|
23060
|
+
// the empty string, and selecting that bucket has to narrow the panel to
|
|
23061
|
+
// exactly it. Only `undefined` means "no filter"; a caller that wants every
|
|
23062
|
+
// row omits the key, which every call site already does.
|
|
23063
|
+
//
|
|
23064
|
+
// Reading '' as unset is what this replaced, and it failed in the one place
|
|
23065
|
+
// it mattered: the no-repo/no-file bucket is often the largest in a real
|
|
23066
|
+
// store, and its panel dropped both predicates and returned the WHOLE scope
|
|
23067
|
+
// — a row reading 3 findings beside a panel listing every finding there is.
|
|
23017
23068
|
case "repo":
|
|
23018
|
-
return opts.repo === void 0 ||
|
|
23069
|
+
return opts.repo === void 0 || row.repo === opts.repo;
|
|
23019
23070
|
case "file":
|
|
23020
|
-
return opts.file === void 0 ||
|
|
23071
|
+
return opts.file === void 0 || row.file === opts.file;
|
|
23021
23072
|
case "q":
|
|
23022
23073
|
return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
|
|
23023
23074
|
}
|
|
@@ -23131,6 +23182,23 @@ function addToLocation(acc, row) {
|
|
|
23131
23182
|
acc.statuses.push(row.status);
|
|
23132
23183
|
acc.ruleIds.add(row.ruleId);
|
|
23133
23184
|
}
|
|
23185
|
+
function compareLocationOrder(a, b) {
|
|
23186
|
+
const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
|
|
23187
|
+
const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
|
|
23188
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
23189
|
+
if (a.latestDetectedAt !== b.latestDetectedAt) {
|
|
23190
|
+
return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
|
|
23191
|
+
}
|
|
23192
|
+
if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
|
|
23193
|
+
if (a.file !== b.file) return a.file < b.file ? -1 : 1;
|
|
23194
|
+
return 0;
|
|
23195
|
+
}
|
|
23196
|
+
function encodeLocationId(repo, file2) {
|
|
23197
|
+
return `${encodePart(repo)}/${encodePart(file2)}`;
|
|
23198
|
+
}
|
|
23199
|
+
function encodePart(value) {
|
|
23200
|
+
return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
|
|
23201
|
+
}
|
|
23134
23202
|
|
|
23135
23203
|
// ../../packages/schema/src/zod/installed-pack.ts
|
|
23136
23204
|
var InstalledPack = external_exports.object({
|
|
@@ -23162,6 +23230,252 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
23162
23230
|
message: "At least one field must be provided"
|
|
23163
23231
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
23164
23232
|
|
|
23233
|
+
// ../../packages/schema/src/zod/policy.ts
|
|
23234
|
+
var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
|
|
23235
|
+
var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
|
|
23236
|
+
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
23237
|
+
var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
|
|
23238
|
+
var Policy = external_exports.object({
|
|
23239
|
+
id: external_exports.guid(),
|
|
23240
|
+
scope: PolicyScope,
|
|
23241
|
+
target: PolicyTarget,
|
|
23242
|
+
action: ActionTaken,
|
|
23243
|
+
enabled: external_exports.boolean().default(true),
|
|
23244
|
+
customKeywords: external_exports.array(external_exports.string()).optional(),
|
|
23245
|
+
// Display name — optional so older policy rows without name still parse.
|
|
23246
|
+
// Added for the findings API (policy.name column migration).
|
|
23247
|
+
name: external_exports.string().optional(),
|
|
23248
|
+
// Whether an AUTHORED policy governs this row's target — not a claim about
|
|
23249
|
+
// which row this is. A producer that collapses several rows onto one target
|
|
23250
|
+
// must carry the marker onto whichever row survives, or the collapse decides
|
|
23251
|
+
// the answer; a survivor may therefore be a built-in expansion still marked
|
|
23252
|
+
// 'authored' because an authored sibling targeted the same thing.
|
|
23253
|
+
// Optional so an older producer — and an older on-disk cache — still parses;
|
|
23254
|
+
// absent reads as 'builtin', which is the behaviour that predates the field.
|
|
23255
|
+
//
|
|
23256
|
+
// Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
|
|
23257
|
+
// built-in archetype catalog entry a policy is, which every catalog surface
|
|
23258
|
+
// reads and which a caller may state. This one is a statement the PRODUCER
|
|
23259
|
+
// of a bundle makes about a row, and only the bundle builder ever stamps it
|
|
23260
|
+
// — the CRUD routes neither accept nor set it.
|
|
23261
|
+
//
|
|
23262
|
+
// A device consumes this in exactly one direction: an 'authored' policy
|
|
23263
|
+
// arriving from a control plane marks the rules it targets as not
|
|
23264
|
+
// locally re-assignable. That can only ever ADD a refusal, never relax one,
|
|
23265
|
+
// which is what makes it safe to honour from an unsigned cache — the same
|
|
23266
|
+
// test `prohibitedModels` passes and `reversibleRuleIds` fails.
|
|
23267
|
+
provenance: PolicyProvenance.optional()
|
|
23268
|
+
}).meta({ id: "Policy" });
|
|
23269
|
+
var PolicyBundle = external_exports.object({
|
|
23270
|
+
version: external_exports.string(),
|
|
23271
|
+
policies: external_exports.array(Policy),
|
|
23272
|
+
// Rules from the installed marketplace packs (snapshotted by the
|
|
23273
|
+
// control plane). The plugin registers these in addition to its bundled
|
|
23274
|
+
// packs. Optional so older backends — and older on-disk caches — that omit
|
|
23275
|
+
// the field still parse; consumers read `bundle.rules ?? []`.
|
|
23276
|
+
rules: external_exports.array(Rule).optional(),
|
|
23277
|
+
// When true, `rules` IS the complete effective ruleset and the runtime must
|
|
23278
|
+
// NOT merge its compiled-in bundled packs — the standalone gateway sets this
|
|
23279
|
+
// after reading the user's installed snapshot (installed_packs, enabled
|
|
23280
|
+
// packs only), which is how detection updates stay manual: new bundled
|
|
23281
|
+
// rules run only after the user applies the pack update. Absent/false keeps
|
|
23282
|
+
// the historical composition (bundled packs + rules) — older caches.
|
|
23283
|
+
rulesComplete: external_exports.boolean().optional(),
|
|
23284
|
+
// Active detection exceptions, evaluation subset only (see
|
|
23285
|
+
// ExceptionBundleEntry). Optional so older bundle producers — and older
|
|
23286
|
+
// on-disk caches — that omit the field still parse; consumers read
|
|
23287
|
+
// `bundle.exceptions ?? []`.
|
|
23288
|
+
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
23289
|
+
// Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
|
|
23290
|
+
// A second axis over the same `redact` action, carried beside the policies
|
|
23291
|
+
// rather than on them: nothing writes ruleId-targeted policies to disk, so
|
|
23292
|
+
// widening Policy itself would change a persisted shape to express something
|
|
23293
|
+
// only the in-memory bundle needs. Optional so an older producer — or an
|
|
23294
|
+
// older on-disk cache — still parses; consumers read `?? []` and get the
|
|
23295
|
+
// pre-existing one-way behaviour, which is the safe direction to default.
|
|
23296
|
+
reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
|
|
23297
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
23298
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
23299
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
23300
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
23301
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
23302
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
23303
|
+
// Model ids (the raw `model` string a harness reports, e.g.
|
|
23304
|
+
// `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
|
|
23305
|
+
// a session onto one (PreModelSwitch) and refuses a turn that would run on
|
|
23306
|
+
// one (UserPromptSubmit). Optional so an older backend — and an older
|
|
23307
|
+
// on-disk cache — still parses; consumers read `?? []`, which is the
|
|
23308
|
+
// unenforced behaviour that predates this field and the safe direction to
|
|
23309
|
+
// default.
|
|
23310
|
+
//
|
|
23311
|
+
// Ids, not display names: the governance decision is keyed on the exact
|
|
23312
|
+
// string the harness reports (`model_status_override.versionId` in the
|
|
23313
|
+
// control plane), so no name resolution stands between the decision and the
|
|
23314
|
+
// comparison.
|
|
23315
|
+
prohibitedModels: external_exports.array(external_exports.string()).optional(),
|
|
23316
|
+
customKeywords: external_exports.array(external_exports.string()),
|
|
23317
|
+
fetchedAt: external_exports.iso.datetime()
|
|
23318
|
+
}).meta({ id: "PolicyBundle" });
|
|
23319
|
+
var POLICY_BUNDLE_SHAPE_ID = [
|
|
23320
|
+
...Object.keys(PolicyBundle.shape),
|
|
23321
|
+
...Object.keys(Policy.shape).map((key) => `policies.${key}`),
|
|
23322
|
+
...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
|
|
23323
|
+
...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
|
|
23324
|
+
].sort().join(",");
|
|
23325
|
+
var OBSERVE_ONLY_CATEGORIES = ["config"];
|
|
23326
|
+
var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
|
|
23327
|
+
var CATEGORY_PEAK_SEVERITY = {
|
|
23328
|
+
secret: "critical",
|
|
23329
|
+
financial: "critical",
|
|
23330
|
+
// core-financial/credit-card
|
|
23331
|
+
code_flaw: "critical",
|
|
23332
|
+
pii: "high",
|
|
23333
|
+
phi: "high",
|
|
23334
|
+
custom: "high",
|
|
23335
|
+
// user-defined; conservative
|
|
23336
|
+
code_context: "low",
|
|
23337
|
+
config: "low"
|
|
23338
|
+
// observe-only; floors to monitor regardless
|
|
23339
|
+
};
|
|
23340
|
+
function severityFloorPolicy(category) {
|
|
23341
|
+
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
23342
|
+
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
23343
|
+
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
23344
|
+
}
|
|
23345
|
+
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
|
|
23346
|
+
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
23347
|
+
var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
|
|
23348
|
+
id: "RedactFallback"
|
|
23349
|
+
});
|
|
23350
|
+
var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
|
|
23351
|
+
var BUILTIN_POLICY_SPECS = {
|
|
23352
|
+
monitor: {
|
|
23353
|
+
name: "Monitor",
|
|
23354
|
+
action: "log",
|
|
23355
|
+
reversible: false,
|
|
23356
|
+
description: "Log every match for audit. The request is allowed through untouched."
|
|
23357
|
+
},
|
|
23358
|
+
warn: {
|
|
23359
|
+
name: "Warn",
|
|
23360
|
+
action: "warn",
|
|
23361
|
+
reversible: false,
|
|
23362
|
+
description: "Allow the request, but warn the user inline before it is sent."
|
|
23363
|
+
},
|
|
23364
|
+
redact: {
|
|
23365
|
+
name: "Redact",
|
|
23366
|
+
action: "redact",
|
|
23367
|
+
reversible: false,
|
|
23368
|
+
description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
|
|
23369
|
+
},
|
|
23370
|
+
vault: {
|
|
23371
|
+
name: "Redact & Vault",
|
|
23372
|
+
action: "redact",
|
|
23373
|
+
reversible: true,
|
|
23374
|
+
description: "Strip the matched value from the request and keep an encrypted, recoverable copy in the local vault, leaving a pointer in its place. Needs the vault consent granted under Settings; without it this behaves as Redact."
|
|
23375
|
+
},
|
|
23376
|
+
block: {
|
|
23377
|
+
name: "Block",
|
|
23378
|
+
action: "block",
|
|
23379
|
+
reversible: false,
|
|
23380
|
+
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
23381
|
+
}
|
|
23382
|
+
};
|
|
23383
|
+
function builtinPolicyToAction(id) {
|
|
23384
|
+
return BUILTIN_POLICY_SPECS[id].action;
|
|
23385
|
+
}
|
|
23386
|
+
var PALETTE_WEAKEST_FIRST = [
|
|
23387
|
+
...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
|
|
23388
|
+
];
|
|
23389
|
+
var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
|
|
23390
|
+
(action) => !PALETTE_WEAKEST_FIRST.includes(action)
|
|
23391
|
+
);
|
|
23392
|
+
var ACTION_STRENGTH_ORDER = [
|
|
23393
|
+
...BELOW_PALETTE,
|
|
23394
|
+
...PALETTE_WEAKEST_FIRST
|
|
23395
|
+
];
|
|
23396
|
+
function actionRank(action) {
|
|
23397
|
+
return ACTION_STRENGTH_ORDER.indexOf(action);
|
|
23398
|
+
}
|
|
23399
|
+
function isActionAtLeast(action, floor) {
|
|
23400
|
+
return actionRank(action) >= actionRank(floor);
|
|
23401
|
+
}
|
|
23402
|
+
function strongerAction(a, b) {
|
|
23403
|
+
return actionRank(a) >= actionRank(b) ? a : b;
|
|
23404
|
+
}
|
|
23405
|
+
function weakestBuiltinAtLeast(floor) {
|
|
23406
|
+
return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
|
|
23407
|
+
}
|
|
23408
|
+
var PackPolicyFloor = external_exports.object({
|
|
23409
|
+
/**
|
|
23410
|
+
* The weakest archetype the device may assign. Stated as a BuiltinPolicyId
|
|
23411
|
+
* rather than a raw ActionTaken because that is the vocabulary the user
|
|
23412
|
+
* picks from — a floor a UI cannot name is one it cannot explain.
|
|
23413
|
+
*/
|
|
23414
|
+
floor: BuiltinPolicyId,
|
|
23415
|
+
/**
|
|
23416
|
+
* True when the organization AUTHORED a policy governing this pack rather
|
|
23417
|
+
* than stating a minimum: it gave the answer, so the pack is not
|
|
23418
|
+
* re-assignable locally in either direction.
|
|
23419
|
+
*/
|
|
23420
|
+
locked: external_exports.boolean()
|
|
23421
|
+
}).describe("PackPolicyFloor");
|
|
23422
|
+
var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
|
|
23423
|
+
(id) => !BUILTIN_POLICY_SPECS[id].reversible
|
|
23424
|
+
);
|
|
23425
|
+
var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
|
|
23426
|
+
(id) => BUILTIN_POLICY_SPECS[id].reversible
|
|
23427
|
+
);
|
|
23428
|
+
var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
|
|
23429
|
+
function builtinPolicyIsReversible(id) {
|
|
23430
|
+
return BUILTIN_POLICY_SPECS[id].reversible;
|
|
23431
|
+
}
|
|
23432
|
+
function policyIdIsReversible(policyId) {
|
|
23433
|
+
const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
|
|
23434
|
+
const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
|
|
23435
|
+
return builtinPolicyIsReversible(id);
|
|
23436
|
+
}
|
|
23437
|
+
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
23438
|
+
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
23439
|
+
);
|
|
23440
|
+
var BUILTIN_POLICIES = Object.fromEntries(
|
|
23441
|
+
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
23442
|
+
);
|
|
23443
|
+
var DEFAULT_PACK_POLICY_ID = "monitor";
|
|
23444
|
+
function policyIdToAction(policyId) {
|
|
23445
|
+
const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
|
|
23446
|
+
const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
|
|
23447
|
+
return BUILTIN_POLICIES[id].action;
|
|
23448
|
+
}
|
|
23449
|
+
var UsedByItem = external_exports.object({
|
|
23450
|
+
id: external_exports.string(),
|
|
23451
|
+
name: external_exports.string(),
|
|
23452
|
+
ruleCount: external_exports.number().int().nonnegative(),
|
|
23453
|
+
enabled: external_exports.boolean()
|
|
23454
|
+
}).meta({ id: "UsedByItem" });
|
|
23455
|
+
var PolicyListItem = external_exports.object({
|
|
23456
|
+
id: external_exports.string(),
|
|
23457
|
+
kind: PolicyKind,
|
|
23458
|
+
name: external_exports.string(),
|
|
23459
|
+
enabled: external_exports.boolean(),
|
|
23460
|
+
usedByCount: external_exports.number().int().nonnegative()
|
|
23461
|
+
}).meta({ id: "PolicyListItem" });
|
|
23462
|
+
var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
|
|
23463
|
+
var PolicyDetail = external_exports.object({
|
|
23464
|
+
specVersion: external_exports.literal(1),
|
|
23465
|
+
id: external_exports.string(),
|
|
23466
|
+
kind: PolicyKind,
|
|
23467
|
+
name: external_exports.string(),
|
|
23468
|
+
enabled: external_exports.boolean(),
|
|
23469
|
+
description: external_exports.string(),
|
|
23470
|
+
usedBy: external_exports.array(UsedByItem)
|
|
23471
|
+
}).meta({ id: "PolicyDetail" });
|
|
23472
|
+
var PolicyStatsResponse = external_exports.object({
|
|
23473
|
+
policies: external_exports.number().int().nonnegative(),
|
|
23474
|
+
builtin: external_exports.number().int().nonnegative(),
|
|
23475
|
+
custom: external_exports.number().int().nonnegative(),
|
|
23476
|
+
detectionsGoverned: external_exports.number().int().nonnegative()
|
|
23477
|
+
}).meta({ id: "PolicyStatsResponse" });
|
|
23478
|
+
|
|
23165
23479
|
// ../../packages/schema/src/zod/vault.ts
|
|
23166
23480
|
var POINTER_FORMAT_VERSION = 2;
|
|
23167
23481
|
var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
|
|
@@ -23202,6 +23516,14 @@ var VaultEntry = external_exports.object({
|
|
|
23202
23516
|
// How many times this value has been detected on this machine — the reuse
|
|
23203
23517
|
// signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
|
|
23204
23518
|
occurrenceCount: external_exports.number().int().nonnegative(),
|
|
23519
|
+
// True when a PERSON asked for this value to be replaced — the surfaced-
|
|
23520
|
+
// secrets strike — rather than a pack enforcing its assignment. One value is
|
|
23521
|
+
// one row however many paths vault it, so this is what tells a policy sweep
|
|
23522
|
+
// that the row carries somebody's own instruction and not just an assignment
|
|
23523
|
+
// that has since been lowered. STICKY and MONOTONIC: a later automatic
|
|
23524
|
+
// vaulting of the same value must never clear it — what the user said about
|
|
23525
|
+
// the value does not expire.
|
|
23526
|
+
userAuthorized: external_exports.boolean(),
|
|
23205
23527
|
firstSeen: external_exports.string(),
|
|
23206
23528
|
lastSeen: external_exports.string()
|
|
23207
23529
|
});
|
|
@@ -23328,7 +23650,7 @@ function isVaultConsentValid(consent) {
|
|
|
23328
23650
|
}
|
|
23329
23651
|
|
|
23330
23652
|
// ../../packages/schema/src/zod/local.ts
|
|
23331
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
23653
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
|
|
23332
23654
|
var MODEL_JUDGE_PAYLOAD_VERSION = 1;
|
|
23333
23655
|
var RunMode = external_exports.enum(["standalone", "attached"]);
|
|
23334
23656
|
var ControlPlaneConnection = external_exports.object({
|
|
@@ -23370,6 +23692,19 @@ var WorkspaceSettings = external_exports.object({
|
|
|
23370
23692
|
vaultKeyCustody: VaultKeyCustody.default("file"),
|
|
23371
23693
|
// How a pointer renders in assistant prose on screen (see VaultInlineReveal).
|
|
23372
23694
|
vaultInlineReveal: VaultInlineReveal.default("masked"),
|
|
23695
|
+
// What a `redact` policy degrades to on a FIELD the host cannot rewrite in
|
|
23696
|
+
// place. Not a handling policy: the policy has already resolved to redact,
|
|
23697
|
+
// and this only says what happens when the host offers no channel to carry it
|
|
23698
|
+
// out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
|
|
23699
|
+
// Claude Code decline to mask a field that EXECUTES because masking would
|
|
23700
|
+
// change what runs. Per FIELD rather than per host, so a host that can
|
|
23701
|
+
// rewrite some inputs keeps true redaction on those.
|
|
23702
|
+
//
|
|
23703
|
+
// Spelled in the built-in policy vocabulary rather than as a fresh enum, so
|
|
23704
|
+
// an attached machine's merge is `strongerAction` over the one action ladder
|
|
23705
|
+
// and no second rank order exists to drift from it. 'deny' is a host wire
|
|
23706
|
+
// word and stays out of the stored value.
|
|
23707
|
+
redactFallback: RedactFallback.default("warn"),
|
|
23373
23708
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
23374
23709
|
onboardedAt: external_exports.iso.datetime().optional(),
|
|
23375
23710
|
// Records that the user consented to sending findings to the model API for
|
|
@@ -23377,15 +23712,20 @@ var WorkspaceSettings = external_exports.object({
|
|
|
23377
23712
|
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
23378
23713
|
// covers the current payload and must be re-granted.
|
|
23379
23714
|
modelJudgeConsent: ModelJudgeConsent.optional(),
|
|
23380
|
-
// Records that the user consented to
|
|
23381
|
-
//
|
|
23382
|
-
//
|
|
23383
|
-
//
|
|
23715
|
+
// Records that the user consented to the DEFERRED send — the outbox — along
|
|
23716
|
+
// with the payload shape and the endpoint they agreed to. Since payload v3
|
|
23717
|
+
// that covers the pre-attach backlog AND undelivered captures alike, and both
|
|
23718
|
+
// carry prompt/reply/tool-output text in `content`; the key name predates
|
|
23719
|
+
// both widenings. Absent until granted, and a grant for a different endpoint
|
|
23720
|
+
// or an older payload no longer counts.
|
|
23384
23721
|
historySyncConsent: HistorySyncConsent.optional()
|
|
23385
23722
|
});
|
|
23386
23723
|
function defaultWorkspaceSettings() {
|
|
23387
23724
|
return WorkspaceSettings.parse({});
|
|
23388
23725
|
}
|
|
23726
|
+
function isAttached(settings) {
|
|
23727
|
+
return settings.runMode === "attached" && settings.controlPlane !== void 0;
|
|
23728
|
+
}
|
|
23389
23729
|
function toInventoryRow(input2, id, now) {
|
|
23390
23730
|
return {
|
|
23391
23731
|
id,
|
|
@@ -23505,8 +23845,12 @@ var ManagedSettingKey = external_exports.enum([
|
|
|
23505
23845
|
"vaultKeyCustody",
|
|
23506
23846
|
"vaultInlineReveal",
|
|
23507
23847
|
"modelJudgeConsent",
|
|
23508
|
-
"dataSharesInPlace"
|
|
23848
|
+
"dataSharesInPlace",
|
|
23849
|
+
"redactFallback"
|
|
23509
23850
|
]).meta({ id: "ManagedSettingKey" });
|
|
23851
|
+
function isManagedSettingKey(value) {
|
|
23852
|
+
return ManagedSettingKey.safeParse(value).success;
|
|
23853
|
+
}
|
|
23510
23854
|
var ManagedSettingsValues = external_exports.object({
|
|
23511
23855
|
runMode: external_exports.enum(["standalone", "attached"]).optional(),
|
|
23512
23856
|
controlPlane: external_exports.object({
|
|
@@ -23518,7 +23862,8 @@ var ManagedSettingsValues = external_exports.object({
|
|
|
23518
23862
|
vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
|
|
23519
23863
|
vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
|
|
23520
23864
|
modelJudgeConsent: external_exports.boolean().optional(),
|
|
23521
|
-
dataSharesInPlace: external_exports.boolean().optional()
|
|
23865
|
+
dataSharesInPlace: external_exports.boolean().optional(),
|
|
23866
|
+
redactFallback: RedactFallback.optional()
|
|
23522
23867
|
}).meta({ id: "ManagedSettingsValues" });
|
|
23523
23868
|
var ManagedSettings = external_exports.object({
|
|
23524
23869
|
specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
|
|
@@ -23530,188 +23875,28 @@ var ManagedSettings = external_exports.object({
|
|
|
23530
23875
|
// Which of those the user may not change. A key here with no matching value
|
|
23531
23876
|
// freezes whatever the user last chose; a value with no lock is a DEFAULT
|
|
23532
23877
|
// the user may still override. The two are separable on purpose.
|
|
23533
|
-
lockedFields: external_exports.array(ManagedSettingKey).default([])
|
|
23534
|
-
}).meta({ id: "ManagedSettings" });
|
|
23535
|
-
|
|
23536
|
-
// ../../packages/schema/src/zod/policy.ts
|
|
23537
|
-
var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
|
|
23538
|
-
var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
|
|
23539
|
-
var Policy = external_exports.object({
|
|
23540
|
-
id: external_exports.guid(),
|
|
23541
|
-
scope: PolicyScope,
|
|
23542
|
-
target: PolicyTarget,
|
|
23543
|
-
action: ActionTaken,
|
|
23544
|
-
enabled: external_exports.boolean().default(true),
|
|
23545
|
-
customKeywords: external_exports.array(external_exports.string()).optional(),
|
|
23546
|
-
// Display name — optional so older policy rows without name still parse.
|
|
23547
|
-
// Added for the findings API (policy.name column migration).
|
|
23548
|
-
name: external_exports.string().optional()
|
|
23549
|
-
}).meta({ id: "Policy" });
|
|
23550
|
-
var PolicyBundle = external_exports.object({
|
|
23551
|
-
version: external_exports.string(),
|
|
23552
|
-
policies: external_exports.array(Policy),
|
|
23553
|
-
// Rules from the installed marketplace packs (snapshotted by the
|
|
23554
|
-
// control plane). The plugin registers these in addition to its bundled
|
|
23555
|
-
// packs. Optional so older backends — and older on-disk caches — that omit
|
|
23556
|
-
// the field still parse; consumers read `bundle.rules ?? []`.
|
|
23557
|
-
rules: external_exports.array(Rule).optional(),
|
|
23558
|
-
// When true, `rules` IS the complete effective ruleset and the runtime must
|
|
23559
|
-
// NOT merge its compiled-in bundled packs — the standalone gateway sets this
|
|
23560
|
-
// after reading the user's installed snapshot (installed_packs, enabled
|
|
23561
|
-
// packs only), which is how detection updates stay manual: new bundled
|
|
23562
|
-
// rules run only after the user applies the pack update. Absent/false keeps
|
|
23563
|
-
// the historical composition (bundled packs + rules) — older caches.
|
|
23564
|
-
rulesComplete: external_exports.boolean().optional(),
|
|
23565
|
-
// Active detection exceptions, evaluation subset only (see
|
|
23566
|
-
// ExceptionBundleEntry). Optional so older bundle producers — and older
|
|
23567
|
-
// on-disk caches — that omit the field still parse; consumers read
|
|
23568
|
-
// `bundle.exceptions ?? []`.
|
|
23569
|
-
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
23570
|
-
// Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
|
|
23571
|
-
// A second axis over the same `redact` action, carried beside the policies
|
|
23572
|
-
// rather than on them: nothing writes ruleId-targeted policies to disk, so
|
|
23573
|
-
// widening Policy itself would change a persisted shape to express something
|
|
23574
|
-
// only the in-memory bundle needs. Optional so an older producer — or an
|
|
23575
|
-
// older on-disk cache — still parses; consumers read `?? []` and get the
|
|
23576
|
-
// pre-existing one-way behaviour, which is the safe direction to default.
|
|
23577
|
-
reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
|
|
23578
|
-
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
23579
|
-
// from a versioned installed pack. Optional so older backends — and older
|
|
23580
|
-
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
23581
|
-
// the rule's own spec version. NOT the bundle version above — see
|
|
23582
|
-
// installedRuleset's ruleVersions for the source of truth.
|
|
23583
|
-
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
23584
|
-
// Model ids (the raw `model` string a harness reports, e.g.
|
|
23585
|
-
// `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
|
|
23586
|
-
// a session onto one (PreModelSwitch) and refuses a turn that would run on
|
|
23587
|
-
// one (UserPromptSubmit). Optional so an older backend — and an older
|
|
23588
|
-
// on-disk cache — still parses; consumers read `?? []`, which is the
|
|
23589
|
-
// unenforced behaviour that predates this field and the safe direction to
|
|
23590
|
-
// default.
|
|
23591
23878
|
//
|
|
23592
|
-
//
|
|
23593
|
-
//
|
|
23594
|
-
//
|
|
23595
|
-
//
|
|
23596
|
-
|
|
23597
|
-
|
|
23598
|
-
|
|
23599
|
-
|
|
23600
|
-
|
|
23601
|
-
|
|
23602
|
-
|
|
23603
|
-
|
|
23604
|
-
|
|
23605
|
-
|
|
23606
|
-
code_flaw: "critical",
|
|
23607
|
-
pii: "high",
|
|
23608
|
-
phi: "high",
|
|
23609
|
-
custom: "high",
|
|
23610
|
-
// user-defined; conservative
|
|
23611
|
-
code_context: "low",
|
|
23612
|
-
config: "low"
|
|
23613
|
-
// observe-only; floors to monitor regardless
|
|
23614
|
-
};
|
|
23615
|
-
function severityFloorPolicy(category) {
|
|
23616
|
-
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
23617
|
-
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
23618
|
-
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
23619
|
-
}
|
|
23620
|
-
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
23621
|
-
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
|
|
23622
|
-
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
23623
|
-
var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
|
|
23624
|
-
var BUILTIN_POLICY_SPECS = {
|
|
23625
|
-
monitor: {
|
|
23626
|
-
name: "Monitor",
|
|
23627
|
-
action: "log",
|
|
23628
|
-
reversible: false,
|
|
23629
|
-
description: "Log every match for audit. The request is allowed through untouched."
|
|
23630
|
-
},
|
|
23631
|
-
warn: {
|
|
23632
|
-
name: "Warn",
|
|
23633
|
-
action: "warn",
|
|
23634
|
-
reversible: false,
|
|
23635
|
-
description: "Allow the request, but warn the user inline before it is sent."
|
|
23636
|
-
},
|
|
23637
|
-
redact: {
|
|
23638
|
-
name: "Redact",
|
|
23639
|
-
action: "redact",
|
|
23640
|
-
reversible: false,
|
|
23641
|
-
description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
|
|
23642
|
-
},
|
|
23643
|
-
vault: {
|
|
23644
|
-
name: "Redact & Vault",
|
|
23645
|
-
action: "redact",
|
|
23646
|
-
reversible: true,
|
|
23647
|
-
description: "Strip the matched value from the request and keep an encrypted, recoverable copy in the local vault, leaving a pointer in its place. Needs the vault consent granted under Settings; without it this behaves as Redact."
|
|
23648
|
-
},
|
|
23649
|
-
block: {
|
|
23650
|
-
name: "Block",
|
|
23651
|
-
action: "block",
|
|
23652
|
-
reversible: false,
|
|
23653
|
-
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
23879
|
+
// Parsed as NAMES rather than as the enum, and split below: a name this
|
|
23880
|
+
// build does not know is dropped from the locked set and reported, never a
|
|
23881
|
+
// reason to refuse the file. The same shape reaches an older build whenever
|
|
23882
|
+
// an administrator locks a key a newer build added, and refusing it there
|
|
23883
|
+
// ran that build entirely unmanaged — every pin and lock gone — on exactly
|
|
23884
|
+
// the fleets most likely to carry a version skew. A name outside the enum
|
|
23885
|
+
// is still never HONOURED: the lockable set stays explicit above.
|
|
23886
|
+
lockedFields: external_exports.array(external_exports.string()).default([])
|
|
23887
|
+
}).transform(({ lockedFields, ...rest }) => {
|
|
23888
|
+
const known = [];
|
|
23889
|
+
const unknown2 = [];
|
|
23890
|
+
for (const name of lockedFields) {
|
|
23891
|
+
if (isManagedSettingKey(name)) known.push(name);
|
|
23892
|
+
else unknown2.push(name);
|
|
23654
23893
|
}
|
|
23655
|
-
|
|
23656
|
-
|
|
23657
|
-
|
|
23658
|
-
}
|
|
23659
|
-
|
|
23660
|
-
|
|
23661
|
-
);
|
|
23662
|
-
var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
|
|
23663
|
-
(id) => BUILTIN_POLICY_SPECS[id].reversible
|
|
23664
|
-
);
|
|
23665
|
-
var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
|
|
23666
|
-
function builtinPolicyIsReversible(id) {
|
|
23667
|
-
return BUILTIN_POLICY_SPECS[id].reversible;
|
|
23668
|
-
}
|
|
23669
|
-
function policyIdIsReversible(policyId) {
|
|
23670
|
-
const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
|
|
23671
|
-
const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
|
|
23672
|
-
return builtinPolicyIsReversible(id);
|
|
23673
|
-
}
|
|
23674
|
-
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
23675
|
-
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
23676
|
-
);
|
|
23677
|
-
var BUILTIN_POLICIES = Object.fromEntries(
|
|
23678
|
-
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
23679
|
-
);
|
|
23680
|
-
var DEFAULT_PACK_POLICY_ID = "monitor";
|
|
23681
|
-
function policyIdToAction(policyId) {
|
|
23682
|
-
const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
|
|
23683
|
-
const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
|
|
23684
|
-
return BUILTIN_POLICIES[id].action;
|
|
23685
|
-
}
|
|
23686
|
-
var UsedByItem = external_exports.object({
|
|
23687
|
-
id: external_exports.string(),
|
|
23688
|
-
name: external_exports.string(),
|
|
23689
|
-
ruleCount: external_exports.number().int().nonnegative(),
|
|
23690
|
-
enabled: external_exports.boolean()
|
|
23691
|
-
}).meta({ id: "UsedByItem" });
|
|
23692
|
-
var PolicyListItem = external_exports.object({
|
|
23693
|
-
id: external_exports.string(),
|
|
23694
|
-
kind: PolicyKind,
|
|
23695
|
-
name: external_exports.string(),
|
|
23696
|
-
enabled: external_exports.boolean(),
|
|
23697
|
-
usedByCount: external_exports.number().int().nonnegative()
|
|
23698
|
-
}).meta({ id: "PolicyListItem" });
|
|
23699
|
-
var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
|
|
23700
|
-
var PolicyDetail = external_exports.object({
|
|
23701
|
-
specVersion: external_exports.literal(1),
|
|
23702
|
-
id: external_exports.string(),
|
|
23703
|
-
kind: PolicyKind,
|
|
23704
|
-
name: external_exports.string(),
|
|
23705
|
-
enabled: external_exports.boolean(),
|
|
23706
|
-
description: external_exports.string(),
|
|
23707
|
-
usedBy: external_exports.array(UsedByItem)
|
|
23708
|
-
}).meta({ id: "PolicyDetail" });
|
|
23709
|
-
var PolicyStatsResponse = external_exports.object({
|
|
23710
|
-
policies: external_exports.number().int().nonnegative(),
|
|
23711
|
-
builtin: external_exports.number().int().nonnegative(),
|
|
23712
|
-
custom: external_exports.number().int().nonnegative(),
|
|
23713
|
-
detectionsGoverned: external_exports.number().int().nonnegative()
|
|
23714
|
-
}).meta({ id: "PolicyStatsResponse" });
|
|
23894
|
+
return {
|
|
23895
|
+
...rest,
|
|
23896
|
+
lockedFields: known,
|
|
23897
|
+
...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
|
|
23898
|
+
};
|
|
23899
|
+
}).meta({ id: "ManagedSettings" });
|
|
23715
23900
|
|
|
23716
23901
|
// ../../packages/schema/src/zod/project-files.ts
|
|
23717
23902
|
var ProjectFileInput = external_exports.object({
|
|
@@ -23834,7 +24019,11 @@ var FindingsTimeseriesPoint = external_exports.object({
|
|
|
23834
24019
|
timestamp: external_exports.iso.date(),
|
|
23835
24020
|
critical: external_exports.number().int().nonnegative(),
|
|
23836
24021
|
high: external_exports.number().int().nonnegative(),
|
|
23837
|
-
medium: external_exports.number().int().nonnegative()
|
|
24022
|
+
medium: external_exports.number().int().nonnegative(),
|
|
24023
|
+
// Optional and additive, so a producer written against the earlier
|
|
24024
|
+
// three-series contract keeps validating. A consumer plotting it resolves the
|
|
24025
|
+
// absent case itself — the chart point requires a number.
|
|
24026
|
+
low: external_exports.number().int().nonnegative().optional()
|
|
23838
24027
|
}).meta({ id: "FindingsTimeseriesPoint" });
|
|
23839
24028
|
var FindingsTimeseriesResponse = external_exports.object({
|
|
23840
24029
|
range: TimeRange,
|
|
@@ -23860,6 +24049,10 @@ var ResolvedFeedItem = external_exports.object({
|
|
|
23860
24049
|
findingKey: external_exports.string(),
|
|
23861
24050
|
ruleId: external_exports.string(),
|
|
23862
24051
|
severity: Severity,
|
|
24052
|
+
// Repository slug, and the file path RELATIVE to it. The pair is what
|
|
24053
|
+
// identifies the file: a bare path matches the same name in every repo.
|
|
24054
|
+
// Optional and additive; empty when the event carried no repo.
|
|
24055
|
+
repo: external_exports.string().optional(),
|
|
23863
24056
|
path: external_exports.string(),
|
|
23864
24057
|
// ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
|
|
23865
24058
|
// findings domain). The reader `.toISOString()`s the DB epoch-ms values.
|
|
@@ -23958,10 +24151,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
|
|
|
23958
24151
|
var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
|
|
23959
24152
|
|
|
23960
24153
|
// ../../packages/schema/src/zod/settings-action.ts
|
|
24154
|
+
var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
|
|
24155
|
+
var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
|
|
23961
24156
|
var SaveSettingsInput = external_exports.object({
|
|
23962
24157
|
historicalAccess: external_exports.string(),
|
|
23963
|
-
modelJudgeConsent:
|
|
23964
|
-
historySyncConsent:
|
|
24158
|
+
modelJudgeConsent: ModelJudgeConsentChoice,
|
|
24159
|
+
historySyncConsent: HistorySyncConsentChoice,
|
|
23965
24160
|
vaultConsent: external_exports.string(),
|
|
23966
24161
|
vaultInlineReveal: external_exports.string()
|
|
23967
24162
|
});
|
|
@@ -24111,9 +24306,9 @@ function deriveReviewReasons(trust, transports) {
|
|
|
24111
24306
|
if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
|
|
24112
24307
|
return reasons;
|
|
24113
24308
|
}
|
|
24114
|
-
function buildReviewInfo(trust, transports) {
|
|
24309
|
+
function buildReviewInfo(trust, transports, decided) {
|
|
24115
24310
|
const reasons = deriveReviewReasons(trust, transports);
|
|
24116
|
-
return { needsReview: reasons.length > 0, reasons };
|
|
24311
|
+
return { needsReview: reasons.length > 0 && !decided, reasons };
|
|
24117
24312
|
}
|
|
24118
24313
|
function distinctTransports(transports) {
|
|
24119
24314
|
return Array.from(new Set(transports));
|
|
@@ -24261,8 +24456,8 @@ function publishByLink(tmp, file2, data) {
|
|
|
24261
24456
|
}
|
|
24262
24457
|
|
|
24263
24458
|
// ../../packages/persistence/src/database.ts
|
|
24264
|
-
import { randomUUID as
|
|
24265
|
-
import { join as
|
|
24459
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
24460
|
+
import { dirname as dirname2, join as join7, sep } from "path";
|
|
24266
24461
|
import { DatabaseSync } from "node:sqlite";
|
|
24267
24462
|
|
|
24268
24463
|
// ../../packages/persistence/src/ids.ts
|
|
@@ -24506,6 +24701,10 @@ function allRows(stmt, params) {
|
|
|
24506
24701
|
if (Array.isArray(params)) return stmt.all(...params);
|
|
24507
24702
|
return stmt.all(params);
|
|
24508
24703
|
}
|
|
24704
|
+
function* iterateRows(stmt, params) {
|
|
24705
|
+
const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
|
|
24706
|
+
for (const row of rows) yield row;
|
|
24707
|
+
}
|
|
24509
24708
|
function getRow(stmt, params) {
|
|
24510
24709
|
if (params === void 0) return stmt.get();
|
|
24511
24710
|
if (Array.isArray(params)) return stmt.get(...params);
|
|
@@ -24974,10 +25173,17 @@ function ensureSyncedAtColumn(db, table) {
|
|
|
24974
25173
|
if (!columns.includes("sync_claimed_at")) {
|
|
24975
25174
|
db.exec(`ALTER TABLE ${table} ADD COLUMN sync_claimed_at integer`);
|
|
24976
25175
|
}
|
|
25176
|
+
if (!columns.includes("outbox_owed")) {
|
|
25177
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN outbox_owed integer`);
|
|
25178
|
+
}
|
|
24977
25179
|
db.exec(
|
|
24978
25180
|
`CREATE INDEX IF NOT EXISTS idx_audit_events_sync
|
|
24979
25181
|
ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
|
|
24980
25182
|
);
|
|
25183
|
+
db.exec(
|
|
25184
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
|
|
25185
|
+
ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
|
|
25186
|
+
);
|
|
24981
25187
|
db.exec(
|
|
24982
25188
|
`CREATE INDEX IF NOT EXISTS idx_audit_claimed
|
|
24983
25189
|
ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
|
|
@@ -25082,7 +25288,6 @@ function decodeKeysetCursor(cursor) {
|
|
|
25082
25288
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
25083
25289
|
var DAY_MS = 864e5;
|
|
25084
25290
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
25085
|
-
var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
|
|
25086
25291
|
function defaultTimeZone() {
|
|
25087
25292
|
try {
|
|
25088
25293
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -25137,6 +25342,7 @@ var DB_EVENT_TYPE_TO_KIND = {
|
|
|
25137
25342
|
error: "error",
|
|
25138
25343
|
active: "active"
|
|
25139
25344
|
};
|
|
25345
|
+
var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
|
|
25140
25346
|
function safeParseStringArray(raw) {
|
|
25141
25347
|
if (!raw) return [];
|
|
25142
25348
|
const parsed = safeJson(raw, null);
|
|
@@ -25210,6 +25416,37 @@ var TIMELINE_COLUMNS = `
|
|
|
25210
25416
|
json_extract(attributes, '$.targetId') AS target_id,
|
|
25211
25417
|
json_extract(attributes, '$.internal') AS internal,
|
|
25212
25418
|
json_extract(attributes, '$.flagged') AS flagged`;
|
|
25419
|
+
var LLM_USAGE_SELECT = `
|
|
25420
|
+
SELECT root_session_id AS sessionId,
|
|
25421
|
+
provider,
|
|
25422
|
+
model,
|
|
25423
|
+
service_tier AS serviceTier,
|
|
25424
|
+
coalesce(sum(input_tokens), 0) AS inputTokens,
|
|
25425
|
+
coalesce(sum(output_tokens), 0) AS outputTokens,
|
|
25426
|
+
coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
|
|
25427
|
+
coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
|
|
25428
|
+
coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
|
|
25429
|
+
coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
|
|
25430
|
+
coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
|
|
25431
|
+
var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
|
|
25432
|
+
var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
|
|
25433
|
+
function usageLeaves(rows) {
|
|
25434
|
+
return rows.map((row) => {
|
|
25435
|
+
const attributes = {
|
|
25436
|
+
input_tokens: row.inputTokens,
|
|
25437
|
+
output_tokens: row.outputTokens,
|
|
25438
|
+
cache_creation_input_tokens: row.cacheCreationTokens,
|
|
25439
|
+
cache_read_input_tokens: row.cacheReadTokens,
|
|
25440
|
+
ephemeral_1h_input_tokens: row.ephemeral1hTokens,
|
|
25441
|
+
ephemeral_5m_input_tokens: row.ephemeral5mTokens,
|
|
25442
|
+
web_search_requests: row.webSearchRequests
|
|
25443
|
+
};
|
|
25444
|
+
if (row.provider !== null) attributes.provider = row.provider;
|
|
25445
|
+
if (row.model !== null) attributes.model = row.model;
|
|
25446
|
+
if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
|
|
25447
|
+
return { sessionId: row.sessionId, attributes };
|
|
25448
|
+
});
|
|
25449
|
+
}
|
|
25213
25450
|
var SESSION_ROOT = `event_type = 'session'`;
|
|
25214
25451
|
var HAS_ACTIVITY = `EXISTS (
|
|
25215
25452
|
SELECT 1 FROM audit_events c
|
|
@@ -25235,16 +25472,17 @@ var SqliteActivityRepository = class {
|
|
|
25235
25472
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
25236
25473
|
const liveNow = countScalar(
|
|
25237
25474
|
this.db,
|
|
25238
|
-
`SELECT count(*) AS n FROM audit_events s
|
|
25475
|
+
`SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
|
|
25239
25476
|
WHERE s.event_type = 'session' AND s.ended_at IS NULL
|
|
25240
|
-
AND
|
|
25241
|
-
|
|
25242
|
-
|
|
25243
|
-
|
|
25244
|
-
|
|
25245
|
-
|
|
25246
|
-
|
|
25247
|
-
|
|
25477
|
+
AND s.id IN (
|
|
25478
|
+
SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
|
|
25479
|
+
UNION
|
|
25480
|
+
SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
|
|
25481
|
+
WHERE started_at >= ?
|
|
25482
|
+
UNION
|
|
25483
|
+
SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
|
|
25484
|
+
WHERE ended_at >= ?)`,
|
|
25485
|
+
[liveThreshold, liveThreshold, liveThreshold]
|
|
25248
25486
|
);
|
|
25249
25487
|
const toolCallsToday = countScalar(
|
|
25250
25488
|
this.db,
|
|
@@ -25297,7 +25535,8 @@ var SqliteActivityRepository = class {
|
|
|
25297
25535
|
SELECT 1 FROM audit_events d
|
|
25298
25536
|
WHERE d.root_session_id = audit_events.id
|
|
25299
25537
|
AND (d.content LIKE ? ESCAPE '\\'
|
|
25300
|
-
OR json_extract(d.attributes, '$.detail')
|
|
25538
|
+
OR coalesce(json_extract(d.attributes, '$.detail'),
|
|
25539
|
+
json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
|
|
25301
25540
|
);
|
|
25302
25541
|
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
25303
25542
|
}
|
|
@@ -25374,7 +25613,7 @@ var SqliteActivityRepository = class {
|
|
|
25374
25613
|
this.db.prepare(
|
|
25375
25614
|
`SELECT ${TIMELINE_COLUMNS}
|
|
25376
25615
|
FROM audit_events
|
|
25377
|
-
WHERE id = ? OR root_session_id = ?
|
|
25616
|
+
WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
|
|
25378
25617
|
ORDER BY started_at ASC, id ASC`
|
|
25379
25618
|
),
|
|
25380
25619
|
[sessionId, sessionId]
|
|
@@ -25387,14 +25626,14 @@ var SqliteActivityRepository = class {
|
|
|
25387
25626
|
coalesce(sum(output_tokens), 0) AS output,
|
|
25388
25627
|
coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
|
|
25389
25628
|
coalesce(sum(cache_read_input_tokens), 0) AS cache_read
|
|
25390
|
-
FROM audit_events
|
|
25629
|
+
FROM audit_events INDEXED BY idx_audit_session_type
|
|
25391
25630
|
WHERE root_session_id = ? AND event_type = 'llm_call'`
|
|
25392
25631
|
),
|
|
25393
25632
|
[sessionId]
|
|
25394
25633
|
) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
|
|
25395
25634
|
const primaryModel = getRow(
|
|
25396
25635
|
this.db.prepare(
|
|
25397
|
-
`SELECT model, provider FROM audit_events
|
|
25636
|
+
`SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
|
|
25398
25637
|
WHERE root_session_id = ? AND event_type = 'llm_call'
|
|
25399
25638
|
ORDER BY started_at ASC, id ASC
|
|
25400
25639
|
LIMIT 1`
|
|
@@ -25405,7 +25644,7 @@ var SqliteActivityRepository = class {
|
|
|
25405
25644
|
this.db.prepare(
|
|
25406
25645
|
`SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
25407
25646
|
count(*) AS n
|
|
25408
|
-
FROM audit_events
|
|
25647
|
+
FROM audit_events INDEXED BY idx_audit_session
|
|
25409
25648
|
WHERE root_session_id = ? AND event_type = 'tool_call'
|
|
25410
25649
|
GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
|
|
25411
25650
|
),
|
|
@@ -25413,7 +25652,7 @@ var SqliteActivityRepository = class {
|
|
|
25413
25652
|
);
|
|
25414
25653
|
const modelRows = allRows(
|
|
25415
25654
|
this.db.prepare(
|
|
25416
|
-
`SELECT DISTINCT model FROM audit_events
|
|
25655
|
+
`SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
|
|
25417
25656
|
WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
|
|
25418
25657
|
ORDER BY model`
|
|
25419
25658
|
),
|
|
@@ -25422,7 +25661,7 @@ var SqliteActivityRepository = class {
|
|
|
25422
25661
|
const derivedModels = modelRows.map((r) => r.model);
|
|
25423
25662
|
const commits = countScalar(
|
|
25424
25663
|
this.db,
|
|
25425
|
-
`SELECT count(*) AS n FROM audit_events
|
|
25664
|
+
`SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
|
|
25426
25665
|
WHERE root_session_id = ? AND event_type = 'commit'`,
|
|
25427
25666
|
[sessionId]
|
|
25428
25667
|
);
|
|
@@ -25458,25 +25697,57 @@ var SqliteActivityRepository = class {
|
|
|
25458
25697
|
return Promise.resolve(session);
|
|
25459
25698
|
}
|
|
25460
25699
|
/**
|
|
25461
|
-
* Cross-session token report — every `llm_call`
|
|
25462
|
-
* `started_at >= fromMs`
|
|
25463
|
-
* USD cost DERIVED at read time via the shared
|
|
25464
|
-
*
|
|
25465
|
-
*
|
|
25466
|
-
*
|
|
25700
|
+
* Cross-session token report — every `llm_call` in the store (or in a
|
|
25701
|
+
* `started_at >= fromMs` window, the Activity page's range) grouped per
|
|
25702
|
+
* session, with USD cost DERIVED at read time via the shared
|
|
25703
|
+
* `defaultCostModel` (never stored). The caller collapses these onto
|
|
25704
|
+
* per-model rows with `aggregateTokenUsage`.
|
|
25705
|
+
*
|
|
25706
|
+
* Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
|
|
25707
|
+
* the members the rollup sums — and priced once per group, which is exact
|
|
25708
|
+
* (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
|
|
25709
|
+
* for a seven-day window at 50k calls, and naming the VIRTUAL columns
|
|
25710
|
+
* against the table 40 ms, since each is a json_extract recomputed per row;
|
|
25711
|
+
* the index stores the values once, at write, and answers the same window in
|
|
25712
|
+
* 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
|
|
25713
|
+
* planner prefers the general event-type index and fetches every row to
|
|
25714
|
+
* recompute the columns it could have read. The index is one every open
|
|
25715
|
+
* store carries, since opening runs the migrations, so the hard requirement
|
|
25716
|
+
* `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
|
|
25717
|
+
* the plan. All-time is a scan of the whole index — still one narrow entry
|
|
25718
|
+
* per call, no bag parsed.
|
|
25467
25719
|
*/
|
|
25468
25720
|
tokenReports(fromMs) {
|
|
25469
|
-
const
|
|
25470
|
-
|
|
25721
|
+
const rows = allRows(
|
|
25722
|
+
this.db.prepare(
|
|
25723
|
+
`${LLM_USAGE_SELECT}
|
|
25724
|
+
FROM audit_events INDEXED BY idx_audit_llm_usage
|
|
25725
|
+
WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
|
|
25726
|
+
${LLM_USAGE_GROUP}`
|
|
25727
|
+
),
|
|
25728
|
+
fromMs === void 0 ? void 0 : [fromMs]
|
|
25729
|
+
);
|
|
25730
|
+
return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
|
|
25471
25731
|
}
|
|
25472
25732
|
/**
|
|
25473
|
-
* One session's token report — its `llm_call`
|
|
25474
|
-
* model) with derived cost, or `null` when the session made no
|
|
25475
|
-
* (an empty/tool-only session). Feeds the session-detail pane's
|
|
25476
|
-
* breakdown + estimated cost.
|
|
25733
|
+
* One session's token report — its `llm_call`s grouped per (provider,
|
|
25734
|
+
* model, tier) with derived cost, or `null` when the session made no
|
|
25735
|
+
* `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
|
|
25736
|
+
* per-model breakdown + estimated cost. The same rollup as `tokenReports`,
|
|
25737
|
+
* seeking one root through a root-led `llm_call` index; the bag-reading fold
|
|
25738
|
+
* it replaces walked every `llm_call` in the store to find one session's.
|
|
25477
25739
|
*/
|
|
25478
25740
|
tokenReportForSession(sessionId) {
|
|
25479
|
-
const
|
|
25741
|
+
const rows = allRows(
|
|
25742
|
+
this.db.prepare(
|
|
25743
|
+
`${LLM_USAGE_SELECT}
|
|
25744
|
+
FROM audit_events
|
|
25745
|
+
WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
|
|
25746
|
+
${LLM_USAGE_GROUP}`
|
|
25747
|
+
),
|
|
25748
|
+
[sessionId]
|
|
25749
|
+
);
|
|
25750
|
+
const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
|
|
25480
25751
|
return Promise.resolve(reports[0] ?? null);
|
|
25481
25752
|
}
|
|
25482
25753
|
/**
|
|
@@ -25500,42 +25771,6 @@ var SqliteActivityRepository = class {
|
|
|
25500
25771
|
for (const row of rows) seen.add(toHarness(row.harness));
|
|
25501
25772
|
return Promise.resolve([...seen]);
|
|
25502
25773
|
}
|
|
25503
|
-
/**
|
|
25504
|
-
* The raw `llm_call` leaves (session id + parsed attribute bag) for the token
|
|
25505
|
-
* rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
|
|
25506
|
-
* window. A leaf whose attributes blob is NULL or unparseable is skipped
|
|
25507
|
-
* (best-effort read — a corrupt bag never breaks the report). `root_session_id`
|
|
25508
|
-
* is the leaf's session (the reconciler sets parent_id = root_session_id).
|
|
25509
|
-
*/
|
|
25510
|
-
readLlmCallLeaves(opts = {}) {
|
|
25511
|
-
const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
|
|
25512
|
-
const params = [];
|
|
25513
|
-
if (opts.sessionId !== void 0) {
|
|
25514
|
-
conditions.push("root_session_id = ?");
|
|
25515
|
-
params.push(opts.sessionId);
|
|
25516
|
-
}
|
|
25517
|
-
if (opts.fromMs !== void 0) {
|
|
25518
|
-
conditions.push("started_at >= ?");
|
|
25519
|
-
params.push(opts.fromMs);
|
|
25520
|
-
}
|
|
25521
|
-
const rows = allRows(
|
|
25522
|
-
this.db.prepare(
|
|
25523
|
-
`SELECT root_session_id AS sessionId, attributes
|
|
25524
|
-
FROM audit_events
|
|
25525
|
-
WHERE ${conditions.join(" AND ")}`
|
|
25526
|
-
),
|
|
25527
|
-
params
|
|
25528
|
-
);
|
|
25529
|
-
return mapRowsTolerant(
|
|
25530
|
-
rows.filter(
|
|
25531
|
-
(row) => row.sessionId !== null
|
|
25532
|
-
),
|
|
25533
|
-
(row) => ({
|
|
25534
|
-
sessionId: row.sessionId,
|
|
25535
|
-
attributes: JSON.parse(row.attributes)
|
|
25536
|
-
})
|
|
25537
|
-
);
|
|
25538
|
-
}
|
|
25539
25774
|
/**
|
|
25540
25775
|
* Per-session turns/findings/shares + last-activity for a page of session ids,
|
|
25541
25776
|
* in grouped queries (not one per row). An id with no matching rows still
|
|
@@ -25550,20 +25785,23 @@ var SqliteActivityRepository = class {
|
|
|
25550
25785
|
const inClause = placeholders(sessionIds.length);
|
|
25551
25786
|
const lastActivityRows = allRows(
|
|
25552
25787
|
this.db.prepare(
|
|
25553
|
-
`SELECT
|
|
25554
|
-
|
|
25555
|
-
|
|
25788
|
+
`SELECT ids.value AS id,
|
|
25789
|
+
(SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
|
|
25790
|
+
(SELECT max(ended_at) FROM audit_events e
|
|
25791
|
+
WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
|
|
25792
|
+
FROM json_each(?) AS ids`
|
|
25556
25793
|
),
|
|
25557
|
-
sessionIds
|
|
25794
|
+
[JSON.stringify(sessionIds)]
|
|
25558
25795
|
);
|
|
25559
25796
|
for (const row of lastActivityRows) {
|
|
25560
|
-
if (row.id === null) continue;
|
|
25561
25797
|
const entry = result.get(row.id);
|
|
25562
|
-
|
|
25798
|
+
const last = Math.max(row.ms ?? 0, row.me ?? 0);
|
|
25799
|
+
if (entry && last > 0) entry.lastActivityMs = last;
|
|
25563
25800
|
}
|
|
25564
25801
|
const turnsRows = allRows(
|
|
25565
25802
|
this.db.prepare(
|
|
25566
|
-
`SELECT root_session_id AS id, count(*) AS n
|
|
25803
|
+
`SELECT root_session_id AS id, count(*) AS n
|
|
25804
|
+
FROM audit_events INDEXED BY idx_audit_session_prompt
|
|
25567
25805
|
WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
|
|
25568
25806
|
GROUP BY root_session_id`
|
|
25569
25807
|
),
|
|
@@ -25578,7 +25816,7 @@ var SqliteActivityRepository = class {
|
|
|
25578
25816
|
this.db.prepare(
|
|
25579
25817
|
`SELECT root_session_id AS id,
|
|
25580
25818
|
count(DISTINCT json_extract(attributes, '$.run_key')) AS n
|
|
25581
|
-
FROM audit_events
|
|
25819
|
+
FROM audit_events INDEXED BY idx_audit_session_run_key
|
|
25582
25820
|
WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
|
|
25583
25821
|
AND json_extract(attributes, '$.run_key') IS NOT NULL
|
|
25584
25822
|
GROUP BY root_session_id`
|
|
@@ -25608,7 +25846,7 @@ var SqliteActivityRepository = class {
|
|
|
25608
25846
|
this.db.prepare(
|
|
25609
25847
|
`SELECT root_session_id AS id,
|
|
25610
25848
|
count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
25611
|
-
FROM audit_events
|
|
25849
|
+
FROM audit_events INDEXED BY idx_audit_session_share
|
|
25612
25850
|
WHERE root_session_id IN (${inClause}) AND event_type = 'share'
|
|
25613
25851
|
GROUP BY root_session_id`
|
|
25614
25852
|
),
|
|
@@ -26636,24 +26874,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
26636
26874
|
)`;
|
|
26637
26875
|
|
|
26638
26876
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
26639
|
-
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
26640
|
-
var SCAN_BATCH_ROWS = 1e3;
|
|
26641
|
-
var DEFAULT_LOCATIONS_LIMIT = 100;
|
|
26642
|
-
var LOCATION_RULE_IDS_CAP = 20;
|
|
26643
|
-
function compareLocationOrder(a, b) {
|
|
26644
|
-
return compareFindingGroupOrder(
|
|
26645
|
-
{
|
|
26646
|
-
severity: a.maxSeverity,
|
|
26647
|
-
latestDetectedAt: a.latestDetectedAt,
|
|
26648
|
-
id: ""
|
|
26649
|
-
},
|
|
26650
|
-
{
|
|
26651
|
-
severity: b.maxSeverity,
|
|
26652
|
-
latestDetectedAt: b.latestDetectedAt,
|
|
26653
|
-
id: ""
|
|
26654
|
-
}
|
|
26655
|
-
);
|
|
26656
|
-
}
|
|
26657
26877
|
var CONCAT_SEP = ",";
|
|
26658
26878
|
var TUPLE_SEP = "|";
|
|
26659
26879
|
function splitConcat(value) {
|
|
@@ -26666,6 +26886,25 @@ function deriveInstanceStatus(row) {
|
|
|
26666
26886
|
latestResolutionStatus: row.latest_status
|
|
26667
26887
|
});
|
|
26668
26888
|
}
|
|
26889
|
+
function toFlatFindingRow(r) {
|
|
26890
|
+
return {
|
|
26891
|
+
id: r.id,
|
|
26892
|
+
ruleId: r.rule_id,
|
|
26893
|
+
category: r.category,
|
|
26894
|
+
severity: r.severity,
|
|
26895
|
+
maskedMatch: r.masked_match,
|
|
26896
|
+
actionTaken: r.action_taken,
|
|
26897
|
+
confidence: r.confidence,
|
|
26898
|
+
occurredAt: epochMillisToIso(r.occurred_at),
|
|
26899
|
+
sourceTool: r.source_tool,
|
|
26900
|
+
repo: r.repo ?? "",
|
|
26901
|
+
file: r.file ?? "",
|
|
26902
|
+
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
26903
|
+
eventId: r.event_id,
|
|
26904
|
+
...r.session_id === null ? {} : { sessionId: r.session_id },
|
|
26905
|
+
status: deriveInstanceStatus(r)
|
|
26906
|
+
};
|
|
26907
|
+
}
|
|
26669
26908
|
function encodeGroupCursor(group) {
|
|
26670
26909
|
const payload = {
|
|
26671
26910
|
sev: group.severity,
|
|
@@ -26686,13 +26925,48 @@ function decodeGroupCursor(cursor) {
|
|
|
26686
26925
|
return null;
|
|
26687
26926
|
}
|
|
26688
26927
|
function firstAfter(sorted, cursor) {
|
|
26689
|
-
const index = sorted.findIndex((
|
|
26928
|
+
const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
|
|
26690
26929
|
return index === -1 ? sorted.length : index;
|
|
26691
26930
|
}
|
|
26692
26931
|
function findDeepLinked(sorted, page, id) {
|
|
26693
|
-
if (page.some((
|
|
26694
|
-
return sorted.find((
|
|
26932
|
+
if (page.some((t) => t.id === id)) return void 0;
|
|
26933
|
+
return sorted.find((t) => t.id === id);
|
|
26695
26934
|
}
|
|
26935
|
+
function encodeLocationCursor(location) {
|
|
26936
|
+
const payload = {
|
|
26937
|
+
sev: location.maxSeverity,
|
|
26938
|
+
t: location.latestDetectedAt,
|
|
26939
|
+
r: location.repo,
|
|
26940
|
+
f: location.file
|
|
26941
|
+
};
|
|
26942
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
26943
|
+
}
|
|
26944
|
+
function decodeLocationCursor(cursor) {
|
|
26945
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
26946
|
+
if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.r === "string" && typeof parsed.f === "string") {
|
|
26947
|
+
return { maxSeverity: parsed.sev, latestDetectedAt: parsed.t, repo: parsed.r, file: parsed.f };
|
|
26948
|
+
}
|
|
26949
|
+
return null;
|
|
26950
|
+
}
|
|
26951
|
+
function firstLocationAfter(sorted, cursor) {
|
|
26952
|
+
const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
|
|
26953
|
+
return index === -1 ? sorted.length : index;
|
|
26954
|
+
}
|
|
26955
|
+
function findDeepLinkedLocation(sorted, page, id) {
|
|
26956
|
+
if (page.some((l) => l.id === id)) return void 0;
|
|
26957
|
+
return sorted.find((l) => l.id === id);
|
|
26958
|
+
}
|
|
26959
|
+
var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
26960
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
26961
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
26962
|
+
e.started_at AS occurred_at,
|
|
26963
|
+
e.source_tool AS source_tool,
|
|
26964
|
+
e.repo AS repo,
|
|
26965
|
+
e.file_path AS file,
|
|
26966
|
+
e.tool_name AS tool_name,
|
|
26967
|
+
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
26968
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
26969
|
+
${latestResolutionStatusSql("f")} AS latest_status`;
|
|
26696
26970
|
var DAY_MS3 = 864e5;
|
|
26697
26971
|
var SqliteFindingsRepository = class {
|
|
26698
26972
|
constructor(db) {
|
|
@@ -26741,7 +27015,7 @@ var SqliteFindingsRepository = class {
|
|
|
26741
27015
|
this.db.prepare(
|
|
26742
27016
|
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
26743
27017
|
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
26744
|
-
|
|
27018
|
+
e.source_tool AS source_tool,
|
|
26745
27019
|
e.event_type AS kind
|
|
26746
27020
|
FROM audit_events e
|
|
26747
27021
|
CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
|
|
@@ -26813,30 +27087,26 @@ var SqliteFindingsRepository = class {
|
|
|
26813
27087
|
);
|
|
26814
27088
|
}
|
|
26815
27089
|
/**
|
|
26816
|
-
*
|
|
26817
|
-
*
|
|
26818
|
-
*
|
|
26819
|
-
*
|
|
26820
|
-
*
|
|
26821
|
-
*
|
|
26822
|
-
*
|
|
26823
|
-
*
|
|
26824
|
-
*
|
|
26825
|
-
*
|
|
26826
|
-
*
|
|
26827
|
-
*
|
|
27090
|
+
* Finding TYPES for the dashboard — one row per rule, scoped to the four
|
|
27091
|
+
* capture kinds (audit_events also holds structural/reconciler/scan rows this
|
|
27092
|
+
* list must never surface), with per-filter-excluded facets, the requested
|
|
27093
|
+
* filters applied, and sorted by severity then recency. Filtering and faceting
|
|
27094
|
+
* run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
|
|
27095
|
+
* full filtered set; `items` is the requested page (default 50), keyset-paged.
|
|
27096
|
+
* Under a `status` filter, `totals.findings` counts only findings whose
|
|
27097
|
+
* derived status was requested.
|
|
27098
|
+
*
|
|
27099
|
+
* ONE read, which materializes no findings: a single aggregate per rule_id,
|
|
27100
|
+
* folding EVERY finding into the numbers a type row and the filters need
|
|
27101
|
+
* (count, severity, category, providers, actions, statuses, latest, search
|
|
27102
|
+
* text). The findings OF a type come from listFindingInstances scoped to
|
|
27103
|
+
* `subtype`, so neither list bounds the other and no per-type cap exists.
|
|
26828
27104
|
*
|
|
26829
|
-
* Two reads, neither of which materializes a row per finding:
|
|
26830
|
-
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
26831
|
-
* the group and the filters need (count, providers, actions, statuses,
|
|
26832
|
-
* latest, search text);
|
|
26833
|
-
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
26834
|
-
* populate `instances` for the table's expanded rows.
|
|
26835
27105
|
* The aggregates carry raw DB values and are translated by the same
|
|
26836
|
-
* @akasecurity/schema mappers
|
|
26837
|
-
* rule is ever restated in SQL.
|
|
27106
|
+
* @akasecurity/schema mappers every other path uses, so no enum mapping or
|
|
27107
|
+
* status rule is ever restated in SQL.
|
|
26838
27108
|
*/
|
|
26839
|
-
|
|
27109
|
+
listFindingTypes(query) {
|
|
26840
27110
|
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
26841
27111
|
const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
|
|
26842
27112
|
const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
|
|
@@ -26849,57 +27119,7 @@ var SqliteFindingsRepository = class {
|
|
|
26849
27119
|
predicate,
|
|
26850
27120
|
params: sessionParams
|
|
26851
27121
|
});
|
|
26852
|
-
const
|
|
26853
|
-
this.db.prepare(
|
|
26854
|
-
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
26855
|
-
occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
|
|
26856
|
-
kind, finding_key, latest_status
|
|
26857
|
-
FROM (
|
|
26858
|
-
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
26859
|
-
d.severity AS severity, f.masked_match AS masked_match,
|
|
26860
|
-
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
26861
|
-
e.started_at AS occurred_at,
|
|
26862
|
-
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
26863
|
-
json_extract(e.attributes, '$.repo') AS repo,
|
|
26864
|
-
json_extract(e.attributes, '$.file_path') AS file,
|
|
26865
|
-
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
26866
|
-
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
26867
|
-
e.event_type AS kind, f.finding_key AS finding_key,
|
|
26868
|
-
latest.status AS latest_status,
|
|
26869
|
-
ROW_NUMBER() OVER (
|
|
26870
|
-
PARTITION BY d.rule_id
|
|
26871
|
-
ORDER BY e.started_at DESC, f.id DESC
|
|
26872
|
-
) AS rn
|
|
26873
|
-
FROM inspection_findings f
|
|
26874
|
-
JOIN audit_events e ON e.id = f.audit_event_id
|
|
26875
|
-
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
26876
|
-
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
26877
|
-
ON latest.finding_key = f.finding_key
|
|
26878
|
-
${predicate}
|
|
26879
|
-
)
|
|
26880
|
-
WHERE rn <= :cap
|
|
26881
|
-
ORDER BY occurred_at DESC, id DESC`
|
|
26882
|
-
),
|
|
26883
|
-
{ cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
|
|
26884
|
-
);
|
|
26885
|
-
const groupable = rows.map((r) => ({
|
|
26886
|
-
id: r.id,
|
|
26887
|
-
ruleId: r.rule_id,
|
|
26888
|
-
category: r.category,
|
|
26889
|
-
severity: r.severity,
|
|
26890
|
-
maskedMatch: r.masked_match,
|
|
26891
|
-
actionTaken: r.action_taken,
|
|
26892
|
-
confidence: r.confidence,
|
|
26893
|
-
occurredAt: epochMillisToIso(r.occurred_at),
|
|
26894
|
-
sourceTool: r.source_tool,
|
|
26895
|
-
repo: r.repo ?? "",
|
|
26896
|
-
file: r.file ?? "",
|
|
26897
|
-
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
26898
|
-
eventId: r.event_id,
|
|
26899
|
-
...r.session_id === null ? {} : { sessionId: r.session_id },
|
|
26900
|
-
status: deriveInstanceStatus(r)
|
|
26901
|
-
}));
|
|
26902
|
-
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
27122
|
+
const allTypes = buildFindingTypes(aggregates);
|
|
26903
27123
|
const filterOpts = {
|
|
26904
27124
|
severity: query.severity,
|
|
26905
27125
|
providers: query.provider,
|
|
@@ -26908,30 +27128,25 @@ var SqliteFindingsRepository = class {
|
|
|
26908
27128
|
subtype: query.subtype,
|
|
26909
27129
|
q: query.q
|
|
26910
27130
|
};
|
|
26911
|
-
const facets = computeFindingFacets(
|
|
26912
|
-
const sorted =
|
|
27131
|
+
const facets = computeFindingFacets(allTypes, filterOpts);
|
|
27132
|
+
const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
|
|
26913
27133
|
const statusFilter = query.status ?? [];
|
|
26914
27134
|
const totals = {
|
|
26915
|
-
findings: sorted.reduce((acc,
|
|
26916
|
-
if (statusFilter.length === 0) return acc +
|
|
26917
|
-
const agg = aggregates.get(
|
|
26918
|
-
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ??
|
|
27135
|
+
findings: sorted.reduce((acc, t) => {
|
|
27136
|
+
if (statusFilter.length === 0) return acc + t.instanceCount;
|
|
27137
|
+
const agg = aggregates.get(t.id);
|
|
27138
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
|
|
26919
27139
|
}, 0),
|
|
26920
|
-
|
|
27140
|
+
types: sorted.length
|
|
26921
27141
|
};
|
|
26922
|
-
const limit = query.limit ??
|
|
27142
|
+
const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
|
|
26923
27143
|
const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
|
|
26924
27144
|
const start = cursor === null ? 0 : firstAfter(sorted, cursor);
|
|
26925
27145
|
const page = sorted.slice(start, start + limit);
|
|
26926
27146
|
const lastOnPage = page.at(-1);
|
|
26927
27147
|
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
|
|
26928
27148
|
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
|
|
26929
|
-
const
|
|
26930
|
-
const narrow = (g) => statusSet ? {
|
|
26931
|
-
...g,
|
|
26932
|
-
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
26933
|
-
} : g;
|
|
26934
|
-
const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
|
|
27149
|
+
const items = [...page, ...deepLinked ? [deepLinked] : []];
|
|
26935
27150
|
return Promise.resolve({
|
|
26936
27151
|
totals,
|
|
26937
27152
|
facets,
|
|
@@ -26942,7 +27157,7 @@ var SqliteFindingsRepository = class {
|
|
|
26942
27157
|
}
|
|
26943
27158
|
/**
|
|
26944
27159
|
* One row per rule_id, folding EVERY instance of the group into the values
|
|
26945
|
-
*
|
|
27160
|
+
* buildFindingTypes cannot recover from an aggregate. Bounded by the number of
|
|
26946
27161
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
26947
27162
|
*
|
|
26948
27163
|
* A single scan, folded in two levels: the inner SELECT groups by
|
|
@@ -26984,8 +27199,10 @@ var SqliteFindingsRepository = class {
|
|
|
26984
27199
|
*
|
|
26985
27200
|
* The scan runs from the top of the scope on every request, not from the
|
|
26986
27201
|
* cursor: `totals` and `facets` describe the whole filtered scope and must not
|
|
26987
|
-
* move as the caller pages. Rows
|
|
26988
|
-
*
|
|
27202
|
+
* move as the caller pages. Rows come off ONE statement, iterated rather
|
|
27203
|
+
* than materialized (`scanFindingRows`), so memory stays flat while the
|
|
27204
|
+
* counting runs — a generator streaming the index order, not a sequence of
|
|
27205
|
+
* fetched batches; only the page itself is retained.
|
|
26989
27206
|
*/
|
|
26990
27207
|
listFindingInstances(query) {
|
|
26991
27208
|
const opts = {
|
|
@@ -27001,6 +27218,10 @@ var SqliteFindingsRepository = class {
|
|
|
27001
27218
|
};
|
|
27002
27219
|
const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
|
|
27003
27220
|
const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
|
|
27221
|
+
const isPastCursor = cursor === null ? () => true : (row) => {
|
|
27222
|
+
const rowMs = isoToEpochMillis(row.occurredAt);
|
|
27223
|
+
return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
|
|
27224
|
+
};
|
|
27004
27225
|
const accumulator = createInstanceFacetAccumulator(opts);
|
|
27005
27226
|
const items = [];
|
|
27006
27227
|
let total = 0;
|
|
@@ -27013,6 +27234,7 @@ var SqliteFindingsRepository = class {
|
|
|
27013
27234
|
accumulator.add(row);
|
|
27014
27235
|
if (!matchesInstanceFilters(row, opts)) continue;
|
|
27015
27236
|
total += 1;
|
|
27237
|
+
if (!isPastCursor(row)) continue;
|
|
27016
27238
|
if (items.length < limit) {
|
|
27017
27239
|
items.push(toInstanceDetail(row));
|
|
27018
27240
|
last = row;
|
|
@@ -27021,15 +27243,6 @@ var SqliteFindingsRepository = class {
|
|
|
27021
27243
|
}
|
|
27022
27244
|
}
|
|
27023
27245
|
const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
|
|
27024
|
-
if (cursor !== null) {
|
|
27025
|
-
const resumed = this.pageAfter(cursor, opts, limit, query);
|
|
27026
|
-
return Promise.resolve({
|
|
27027
|
-
totals: { findings: total },
|
|
27028
|
-
facets: accumulator.facets(),
|
|
27029
|
-
items: resumed.items,
|
|
27030
|
-
nextCursor: resumed.nextCursor
|
|
27031
|
-
});
|
|
27032
|
-
}
|
|
27033
27246
|
return Promise.resolve({
|
|
27034
27247
|
totals: { findings: total },
|
|
27035
27248
|
facets: accumulator.facets(),
|
|
@@ -27038,42 +27251,25 @@ var SqliteFindingsRepository = class {
|
|
|
27038
27251
|
});
|
|
27039
27252
|
}
|
|
27040
27253
|
/**
|
|
27041
|
-
* The
|
|
27042
|
-
* counting pass because that one starts at the top of the scope by design;
|
|
27043
|
-
* this one narrows the scan with the same keyset predicate the activity list
|
|
27044
|
-
* uses, so a later page costs less than the first rather than more.
|
|
27045
|
-
*/
|
|
27046
|
-
pageAfter(cursor, opts, limit, query) {
|
|
27047
|
-
const items = [];
|
|
27048
|
-
let last;
|
|
27049
|
-
let hasMore = false;
|
|
27050
|
-
for (const row of this.scanFindingRows({
|
|
27051
|
-
sessionId: query.sessionId,
|
|
27052
|
-
from: query.from,
|
|
27053
|
-
after: cursor
|
|
27054
|
-
})) {
|
|
27055
|
-
if (!matchesInstanceFilters(row, opts)) continue;
|
|
27056
|
-
if (items.length < limit) {
|
|
27057
|
-
items.push(toInstanceDetail(row));
|
|
27058
|
-
last = row;
|
|
27059
|
-
} else {
|
|
27060
|
-
hasMore = true;
|
|
27061
|
-
break;
|
|
27062
|
-
}
|
|
27063
|
-
}
|
|
27064
|
-
return {
|
|
27065
|
-
items,
|
|
27066
|
-
nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
|
|
27067
|
-
};
|
|
27068
|
-
}
|
|
27069
|
-
/**
|
|
27070
|
-
* The same findings folded by location: repository, then file within it.
|
|
27254
|
+
* The same findings folded by WHERE they live — one row per (repo, file) pair.
|
|
27071
27255
|
*
|
|
27072
27256
|
* The grouping keys come from the capturing event's attributes, which is what
|
|
27073
|
-
* the local store relates a finding to
|
|
27074
|
-
*
|
|
27075
|
-
* empty-string bucket, which
|
|
27076
|
-
*
|
|
27257
|
+
* the local store relates a finding to; there is no finding↔asset row to group
|
|
27258
|
+
* by instead. A repo or file the event did not record folds into the
|
|
27259
|
+
* empty-string bucket, which is a real location like any other: it is listed,
|
|
27260
|
+
* it is selectable, and its `?loc=` token is as good as any other row's.
|
|
27261
|
+
*
|
|
27262
|
+
* ONE flat list rather than repos nesting files. A rollup can only be paged by
|
|
27263
|
+
* repo, which leaves the file list inside it unbounded — the shape the by-type
|
|
27264
|
+
* list was rebuilt to remove — and two-level pagination inside an
|
|
27265
|
+
* expand/collapse table is what pushed that view to master/detail in the first
|
|
27266
|
+
* place.
|
|
27267
|
+
*
|
|
27268
|
+
* Every filter narrows the FINDINGS and the locations fall out of what
|
|
27269
|
+
* survives, so each row's `instanceCount` is exactly what listFindingInstances
|
|
27270
|
+
* reports for the same filters scoped to that pair. The view depends on it:
|
|
27271
|
+
* one toolbar sits over both panels precisely because a location owns none of
|
|
27272
|
+
* its fields.
|
|
27077
27273
|
*/
|
|
27078
27274
|
listFindingLocations(query) {
|
|
27079
27275
|
const opts = {
|
|
@@ -27085,13 +27281,16 @@ var SqliteFindingsRepository = class {
|
|
|
27085
27281
|
tools: query.tool,
|
|
27086
27282
|
q: query.q
|
|
27087
27283
|
};
|
|
27088
|
-
const limit = query.limit ??
|
|
27284
|
+
const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
|
|
27285
|
+
const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
|
|
27089
27286
|
const byRepo = /* @__PURE__ */ new Map();
|
|
27287
|
+
const accumulator = createInstanceFacetAccumulator(opts);
|
|
27090
27288
|
let total = 0;
|
|
27091
27289
|
for (const row of this.scanFindingRows({
|
|
27092
27290
|
sessionId: query.sessionId,
|
|
27093
27291
|
from: query.from
|
|
27094
27292
|
})) {
|
|
27293
|
+
accumulator.add(row);
|
|
27095
27294
|
if (!matchesInstanceFilters(row, opts)) continue;
|
|
27096
27295
|
total += 1;
|
|
27097
27296
|
let files = byRepo.get(row.repo);
|
|
@@ -27106,67 +27305,112 @@ var SqliteFindingsRepository = class {
|
|
|
27106
27305
|
}
|
|
27107
27306
|
addToLocation(acc, row);
|
|
27108
27307
|
}
|
|
27109
|
-
|
|
27110
|
-
const
|
|
27111
|
-
|
|
27112
|
-
|
|
27113
|
-
|
|
27114
|
-
|
|
27115
|
-
|
|
27116
|
-
|
|
27117
|
-
|
|
27118
|
-
|
|
27119
|
-
|
|
27120
|
-
|
|
27121
|
-
|
|
27122
|
-
|
|
27123
|
-
|
|
27124
|
-
|
|
27125
|
-
|
|
27126
|
-
|
|
27127
|
-
|
|
27128
|
-
|
|
27129
|
-
|
|
27130
|
-
|
|
27131
|
-
);
|
|
27132
|
-
const statuses = fileRows.map((f) => f.status);
|
|
27133
|
-
const folded = foldGroupStatus(statuses);
|
|
27134
|
-
return {
|
|
27135
|
-
repo,
|
|
27136
|
-
instanceCount: rollup.instanceCount,
|
|
27137
|
-
maxSeverity: rollup.maxSeverity,
|
|
27138
|
-
latestDetectedAt: rollup.latestDetectedAt,
|
|
27139
|
-
...folded === void 0 ? {} : { status: folded },
|
|
27140
|
-
files: fileRows
|
|
27141
|
-
};
|
|
27142
|
-
});
|
|
27143
|
-
repos.sort(compareLocationOrder);
|
|
27308
|
+
const sorted = [];
|
|
27309
|
+
for (const [repo, files] of byRepo) {
|
|
27310
|
+
for (const [file2, acc] of files) {
|
|
27311
|
+
const status = foldGroupStatus(acc.statuses);
|
|
27312
|
+
sorted.push({
|
|
27313
|
+
id: encodeLocationId(repo, file2),
|
|
27314
|
+
repo,
|
|
27315
|
+
file: file2,
|
|
27316
|
+
instanceCount: acc.instanceCount,
|
|
27317
|
+
maxSeverity: acc.maxSeverity,
|
|
27318
|
+
latestDetectedAt: acc.latestDetectedAt,
|
|
27319
|
+
...status === void 0 ? {} : { status },
|
|
27320
|
+
ruleIds: [...acc.ruleIds]
|
|
27321
|
+
});
|
|
27322
|
+
}
|
|
27323
|
+
}
|
|
27324
|
+
sorted.sort(compareLocationOrder);
|
|
27325
|
+
const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
|
|
27326
|
+
const page = sorted.slice(start, start + limit);
|
|
27327
|
+
const lastOnPage = page.at(-1);
|
|
27328
|
+
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
|
|
27329
|
+
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
|
|
27144
27330
|
return Promise.resolve({
|
|
27145
|
-
totals: { findings: total,
|
|
27146
|
-
|
|
27147
|
-
|
|
27331
|
+
totals: { findings: total, locations: sorted.length },
|
|
27332
|
+
facets: accumulator.facets(),
|
|
27333
|
+
items: [...page, ...deepLinked ? [deepLinked] : []],
|
|
27334
|
+
nextCursor
|
|
27148
27335
|
});
|
|
27149
27336
|
}
|
|
27150
27337
|
/**
|
|
27151
|
-
* Every finding in scope as a FlatFindingRow, newest first,
|
|
27338
|
+
* Every finding in scope as a FlatFindingRow, newest first, streamed.
|
|
27152
27339
|
*
|
|
27153
27340
|
* A generator so a caller streams the scope without it ever being an array:
|
|
27154
27341
|
* the flat list counts and facets the whole filtered scope, which on a large
|
|
27155
|
-
* store is far more rows than any page.
|
|
27156
|
-
*
|
|
27157
|
-
*
|
|
27342
|
+
* store is far more rows than any page. The rows come off ONE statement,
|
|
27343
|
+
* iterated rather than materialized, in the index order `findingScanSql`
|
|
27344
|
+
* arranges — so the scan is a single pass with a block sort of the id
|
|
27345
|
+
* tie-break only, never a sort of the scope, where a sequence of
|
|
27346
|
+
* keyset-bounded batches re-sorted everything below the cursor on every
|
|
27347
|
+
* batch and cost the square of the scope.
|
|
27158
27348
|
*
|
|
27159
|
-
*
|
|
27160
|
-
*
|
|
27161
|
-
*
|
|
27162
|
-
*
|
|
27163
|
-
*
|
|
27164
|
-
*
|
|
27165
|
-
*
|
|
27166
|
-
* dimension — see listFindingInstances.
|
|
27349
|
+
* `sessionId` and `from` carry ONLY what no facet counts — a filter
|
|
27350
|
+
* dimension narrowed here would be missing from its own facet, which is
|
|
27351
|
+
* computed by excluding that dimension (see listFindingInstances). There is
|
|
27352
|
+
* no `after`/cursor parameter: a keyset page is collected inline from this
|
|
27353
|
+
* same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
|
|
27354
|
+
* narrower statement, since the counting pass already visits every row a
|
|
27355
|
+
* page-2+ request would otherwise re-seek for.
|
|
27167
27356
|
*/
|
|
27168
27357
|
*scanFindingRows(scope) {
|
|
27169
|
-
const
|
|
27358
|
+
const { sql, params } = this.findingScanSql(scope);
|
|
27359
|
+
for (const r of iterateRows(this.db.prepare(sql), params)) {
|
|
27360
|
+
yield toFlatFindingRow(r);
|
|
27361
|
+
}
|
|
27362
|
+
}
|
|
27363
|
+
/**
|
|
27364
|
+
* One finding by its own id, or null when no such row exists.
|
|
27365
|
+
*
|
|
27366
|
+
* A primary-key seek on `inspection_findings`, so its cost does not grow with
|
|
27367
|
+
* the store — and, unlike anything derived from a list page, it resolves a
|
|
27368
|
+
* finding of ANY age. That is what the Findings page's one-shot `?finding=`
|
|
27369
|
+
* deep link needs: the id it carries may name a finding thousands of rows
|
|
27370
|
+
* older than anything a first page holds.
|
|
27371
|
+
*
|
|
27372
|
+
* Deliberately UNFILTERED — no capture-kind, session or time predicate. It
|
|
27373
|
+
* RESOLVES an id; whether that row would survive the list's current filters is
|
|
27374
|
+
* a different question, and hiding the target because a filter excludes it is
|
|
27375
|
+
* worse than showing it.
|
|
27376
|
+
*
|
|
27377
|
+
* `groupId` on the result IS the rule id, so this one read answers both "which
|
|
27378
|
+
* type should the list select?" and "what does the drawer show?".
|
|
27379
|
+
*/
|
|
27380
|
+
findingInstance(id) {
|
|
27381
|
+
const row = this.db.prepare(
|
|
27382
|
+
`SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
27383
|
+
FROM inspection_findings f
|
|
27384
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
27385
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
27386
|
+
WHERE f.id = ?`
|
|
27387
|
+
).get(id);
|
|
27388
|
+
return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
|
|
27389
|
+
}
|
|
27390
|
+
/**
|
|
27391
|
+
* The one statement both instance-level scans run: every finding in scope,
|
|
27392
|
+
* joined to its event and definition, newest first.
|
|
27393
|
+
*
|
|
27394
|
+
* THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
|
|
27395
|
+
* the same two `recentFindings` documents at length, for the same reason:
|
|
27396
|
+
*
|
|
27397
|
+
* - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
|
|
27398
|
+
* the planner cannot pick `idx_audit_type_t` and then sort. That index
|
|
27399
|
+
* yields `started_at` order per event type, not across the four, so
|
|
27400
|
+
* satisfying the ORDER BY from it would need a merge SQLite does not do.
|
|
27401
|
+
* Freed of it, the planner walks `idx_audit_started_at` backwards — or
|
|
27402
|
+
* `idx_audit_session` for a session scope, which is also `started_at`
|
|
27403
|
+
* ordered within the session — and the order falls out of the index.
|
|
27404
|
+
* - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
|
|
27405
|
+
* JOINs the planner drives from the findings and sorts everything.
|
|
27406
|
+
*
|
|
27407
|
+
* The latest-resolution lookup is the CORRELATED form: only `status` is
|
|
27408
|
+
* needed, `idx_finding_resolution_key_created` answers it with one backward
|
|
27409
|
+
* index probe per keyed row, and a derived table over the whole resolution
|
|
27410
|
+
* table would be materialized before the first row streamed.
|
|
27411
|
+
*/
|
|
27412
|
+
findingScanSql(scope) {
|
|
27413
|
+
const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
|
|
27170
27414
|
const params = [];
|
|
27171
27415
|
if (scope.sessionId !== void 0 && scope.sessionId !== "") {
|
|
27172
27416
|
conditions.push("e.root_session_id = ?");
|
|
@@ -27176,64 +27420,40 @@ var SqliteFindingsRepository = class {
|
|
|
27176
27420
|
conditions.push("e.started_at >= ?");
|
|
27177
27421
|
params.push(isoToEpochMillis(scope.from));
|
|
27178
27422
|
}
|
|
27179
|
-
const sql = `SELECT
|
|
27180
|
-
|
|
27181
|
-
|
|
27182
|
-
|
|
27183
|
-
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
27184
|
-
json_extract(e.attributes, '$.repo') AS repo,
|
|
27185
|
-
json_extract(e.attributes, '$.file_path') AS file,
|
|
27186
|
-
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
27187
|
-
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
27188
|
-
e.event_type AS kind, f.finding_key AS finding_key,
|
|
27189
|
-
${latestResolutionStatusSql("f")} AS latest_status
|
|
27190
|
-
FROM inspection_findings f
|
|
27191
|
-
JOIN audit_events e ON e.id = f.audit_event_id
|
|
27192
|
-
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
27423
|
+
const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
27424
|
+
FROM audit_events e
|
|
27425
|
+
CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
|
|
27426
|
+
CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
27193
27427
|
WHERE ${conditions.join(" AND ")}
|
|
27194
|
-
|
|
27195
|
-
|
|
27196
|
-
LIMIT ?`;
|
|
27197
|
-
let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
|
|
27198
|
-
for (; ; ) {
|
|
27199
|
-
const rows = allRows(this.db.prepare(sql), [
|
|
27200
|
-
...params,
|
|
27201
|
-
after.startedAtMs,
|
|
27202
|
-
after.startedAtMs,
|
|
27203
|
-
after.id,
|
|
27204
|
-
SCAN_BATCH_ROWS
|
|
27205
|
-
]);
|
|
27206
|
-
for (const r of rows) {
|
|
27207
|
-
yield {
|
|
27208
|
-
id: r.id,
|
|
27209
|
-
ruleId: r.rule_id,
|
|
27210
|
-
category: r.category,
|
|
27211
|
-
severity: r.severity,
|
|
27212
|
-
maskedMatch: r.masked_match,
|
|
27213
|
-
actionTaken: r.action_taken,
|
|
27214
|
-
confidence: r.confidence,
|
|
27215
|
-
occurredAt: epochMillisToIso(r.occurred_at),
|
|
27216
|
-
sourceTool: r.source_tool,
|
|
27217
|
-
repo: r.repo ?? "",
|
|
27218
|
-
file: r.file ?? "",
|
|
27219
|
-
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
27220
|
-
eventId: r.event_id,
|
|
27221
|
-
...r.session_id === null ? {} : { sessionId: r.session_id },
|
|
27222
|
-
status: deriveInstanceStatus(r)
|
|
27223
|
-
};
|
|
27224
|
-
}
|
|
27225
|
-
if (rows.length < SCAN_BATCH_ROWS) return;
|
|
27226
|
-
const lastRow = rows[rows.length - 1];
|
|
27227
|
-
if (lastRow === void 0) return;
|
|
27228
|
-
after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
|
|
27229
|
-
}
|
|
27428
|
+
ORDER BY e.started_at DESC, f.id DESC`;
|
|
27429
|
+
return { sql, params };
|
|
27230
27430
|
}
|
|
27231
27431
|
groupAggregates(withSearchText, scope) {
|
|
27232
|
-
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT
|
|
27233
|
-
group_concat(DISTINCT
|
|
27234
|
-
group_concat(DISTINCT 'via ' ||
|
|
27432
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
|
|
27433
|
+
group_concat(DISTINCT e.file_path) AS files,
|
|
27434
|
+
group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
27235
27435
|
const rows = this.db.prepare(
|
|
27236
27436
|
`SELECT rule_id,
|
|
27437
|
+
-- BARE columns beside max(latest_at), which is deliberate and
|
|
27438
|
+
-- is SQLite's documented behaviour: with a single min()/max()
|
|
27439
|
+
-- in an aggregate query, every bare column takes its value from
|
|
27440
|
+
-- the row that produced the extremum. So these are the severity
|
|
27441
|
+
-- and category of the definition whose finding is NEWEST, which
|
|
27442
|
+
-- is what the row-based build they replaced read off its first
|
|
27443
|
+
-- (newest-first) row.
|
|
27444
|
+
--
|
|
27445
|
+
-- min() is WRONG here and was the defect: inspection_definitions
|
|
27446
|
+
-- holds one row per rule VERSION (see its writer \u2014 a version bump
|
|
27447
|
+
-- mints a new row), so a rule whose severity moved between
|
|
27448
|
+
-- versions has several, and min() picks the ALPHABETICALLY
|
|
27449
|
+
-- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
|
|
27450
|
+
-- That is arbitrary in direction, and it feeds the badge, the
|
|
27451
|
+
-- filter, the facet counts and the primary sort key.
|
|
27452
|
+
--
|
|
27453
|
+
-- Adding a second min()/max() aggregate here would make these
|
|
27454
|
+
-- bare columns ambiguous again; keep max(latest_at) the only one.
|
|
27455
|
+
severity,
|
|
27456
|
+
category,
|
|
27237
27457
|
sum(tuple_count) AS instance_count,
|
|
27238
27458
|
max(latest_at) AS latest_at,
|
|
27239
27459
|
group_concat(source_tools) AS source_tools,
|
|
@@ -27244,12 +27464,20 @@ var SqliteFindingsRepository = class {
|
|
|
27244
27464
|
group_concat(tool_names) AS tool_names
|
|
27245
27465
|
FROM (
|
|
27246
27466
|
SELECT d.rule_id AS rule_id,
|
|
27467
|
+
-- Severity and category are columns of the DEFINITION, and
|
|
27468
|
+
-- a rule can have SEVERAL definitions (one per version), so
|
|
27469
|
+
-- these are grouped on below and resolved to the newest
|
|
27470
|
+
-- firing version by the outer query's bare-column select.
|
|
27471
|
+
-- They ride the aggregate because the type build has no rows
|
|
27472
|
+
-- to read them off \u2014 see buildFindingTypes.
|
|
27473
|
+
d.severity AS severity,
|
|
27474
|
+
d.category AS category,
|
|
27247
27475
|
e.event_type || '${TUPLE_SEP}' ||
|
|
27248
27476
|
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
27249
27477
|
coalesce(latest.status, '') AS status_tuple,
|
|
27250
27478
|
count(*) AS tuple_count,
|
|
27251
27479
|
max(e.started_at) AS latest_at,
|
|
27252
|
-
group_concat(DISTINCT
|
|
27480
|
+
group_concat(DISTINCT e.source_tool) AS source_tools,
|
|
27253
27481
|
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
27254
27482
|
${innerSearchColumns}
|
|
27255
27483
|
FROM inspection_findings f
|
|
@@ -27258,7 +27486,7 @@ var SqliteFindingsRepository = class {
|
|
|
27258
27486
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
27259
27487
|
ON latest.finding_key = f.finding_key
|
|
27260
27488
|
${scope.predicate}
|
|
27261
|
-
GROUP BY d.rule_id, status_tuple
|
|
27489
|
+
GROUP BY d.rule_id, d.severity, d.category, status_tuple
|
|
27262
27490
|
)
|
|
27263
27491
|
GROUP BY rule_id`
|
|
27264
27492
|
).all(scope.params);
|
|
@@ -27267,6 +27495,8 @@ var SqliteFindingsRepository = class {
|
|
|
27267
27495
|
r.rule_id,
|
|
27268
27496
|
{
|
|
27269
27497
|
instanceCount: r.instance_count,
|
|
27498
|
+
severity: r.severity,
|
|
27499
|
+
category: r.category,
|
|
27270
27500
|
sourceTools: splitConcat(r.source_tools),
|
|
27271
27501
|
actionsTaken: splitConcat(r.actions_taken),
|
|
27272
27502
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
@@ -27283,7 +27513,7 @@ var SqliteFindingsRepository = class {
|
|
|
27283
27513
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
27284
27514
|
// Free text only — joined and substring-matched, so group_concat's
|
|
27285
27515
|
// commas need no unpicking (a repo/path containing one still matches).
|
|
27286
|
-
// Left undefined (not '') when unfetched, so
|
|
27516
|
+
// Left undefined (not '') when unfetched, so buildFindingTypes can
|
|
27287
27517
|
// tell "no q this request" from "a group with no repo/file at all"
|
|
27288
27518
|
// and skip priming a haystack nothing will read.
|
|
27289
27519
|
...withSearchText ? {
|
|
@@ -27380,6 +27610,8 @@ function isoDay(ms) {
|
|
|
27380
27610
|
// ../../packages/persistence/src/repositories/history-sync.ts
|
|
27381
27611
|
var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
|
|
27382
27612
|
var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
27613
|
+
var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
|
|
27614
|
+
var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
27383
27615
|
var SKIPPED = -1;
|
|
27384
27616
|
var ROW_COLUMNS = `id,
|
|
27385
27617
|
parent_id AS parentId,
|
|
@@ -27419,6 +27651,26 @@ var SqliteHistorySyncRepository = class {
|
|
|
27419
27651
|
ORDER BY (event_type = 'session') DESC, started_at
|
|
27420
27652
|
LIMIT :limit`
|
|
27421
27653
|
);
|
|
27654
|
+
this.captureRowsStmt = db.prepare(
|
|
27655
|
+
`SELECT ${ROW_COLUMNS}
|
|
27656
|
+
FROM audit_events
|
|
27657
|
+
WHERE synced_at IS NULL
|
|
27658
|
+
AND sync_claimed_at IS NULL
|
|
27659
|
+
AND outbox_owed = 1
|
|
27660
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27661
|
+
AND started_at < :before
|
|
27662
|
+
ORDER BY started_at
|
|
27663
|
+
LIMIT :limit`
|
|
27664
|
+
);
|
|
27665
|
+
this.markOwedStmt = db.prepare(
|
|
27666
|
+
`UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
|
|
27667
|
+
);
|
|
27668
|
+
this.markCaptureBacklogOwedStmt = db.prepare(
|
|
27669
|
+
`UPDATE audit_events SET outbox_owed = 1
|
|
27670
|
+
WHERE synced_at IS NULL
|
|
27671
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27672
|
+
AND started_at < :before`
|
|
27673
|
+
);
|
|
27422
27674
|
this.stampStmt = db.prepare(
|
|
27423
27675
|
`UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
|
|
27424
27676
|
);
|
|
@@ -27450,6 +27702,12 @@ var SqliteHistorySyncRepository = class {
|
|
|
27450
27702
|
FROM audit_events
|
|
27451
27703
|
WHERE event_type IN (${TYPE_LIST})`
|
|
27452
27704
|
);
|
|
27705
|
+
this.captureSkipCountStmt = db.prepare(
|
|
27706
|
+
`SELECT COUNT(*) AS skipped
|
|
27707
|
+
FROM audit_events
|
|
27708
|
+
WHERE synced_at = ${String(SKIPPED)}
|
|
27709
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})`
|
|
27710
|
+
);
|
|
27453
27711
|
this.fingerprintStmt = db.prepare(
|
|
27454
27712
|
`SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
|
|
27455
27713
|
FROM history_sync WHERE id = 1`
|
|
@@ -27459,6 +27717,12 @@ var SqliteHistorySyncRepository = class {
|
|
|
27459
27717
|
SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
|
|
27460
27718
|
WHERE id = 1`
|
|
27461
27719
|
);
|
|
27720
|
+
this.disownCapturesStmt = db.prepare(
|
|
27721
|
+
`UPDATE audit_events SET outbox_owed = NULL
|
|
27722
|
+
WHERE outbox_owed IS NOT NULL
|
|
27723
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27724
|
+
AND started_at < :attachedAt`
|
|
27725
|
+
);
|
|
27462
27726
|
this.rearmStmt = db.prepare(
|
|
27463
27727
|
`UPDATE audit_events SET synced_at = NULL
|
|
27464
27728
|
WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
|
|
@@ -27531,6 +27795,11 @@ var SqliteHistorySyncRepository = class {
|
|
|
27531
27795
|
closeWindowStmt;
|
|
27532
27796
|
releaseBoundaryStmt;
|
|
27533
27797
|
freezeBoundaryStmt;
|
|
27798
|
+
captureRowsStmt;
|
|
27799
|
+
markOwedStmt;
|
|
27800
|
+
markCaptureBacklogOwedStmt;
|
|
27801
|
+
captureSkipCountStmt;
|
|
27802
|
+
disownCapturesStmt;
|
|
27534
27803
|
partitionStmt;
|
|
27535
27804
|
claimRowStmt;
|
|
27536
27805
|
releaseRowStmt;
|
|
@@ -27564,6 +27833,51 @@ var SqliteHistorySyncRepository = class {
|
|
|
27564
27833
|
pendingRows(sessionId, limit, before) {
|
|
27565
27834
|
return allRows(this.rowsStmt, { sessionId, limit, before });
|
|
27566
27835
|
}
|
|
27836
|
+
/**
|
|
27837
|
+
* Captures this machine still owes the deployment, oldest first.
|
|
27838
|
+
*
|
|
27839
|
+
* Selected by the `outbox_owed` marker the attached forward path writes, not
|
|
27840
|
+
* by a time window — see captureRowsStmt for why a window could not express
|
|
27841
|
+
* this. `before` is the grace window that leaves a just-recorded capture to
|
|
27842
|
+
* the live path.
|
|
27843
|
+
*/
|
|
27844
|
+
pendingCaptureRows(limit, before) {
|
|
27845
|
+
return allRows(this.captureRowsStmt, { limit, before });
|
|
27846
|
+
}
|
|
27847
|
+
/**
|
|
27848
|
+
* Record that a capture is OWED to the deployment.
|
|
27849
|
+
*
|
|
27850
|
+
* Written by the attached forward path when a live send did not confirm
|
|
27851
|
+
* delivery, and read by the drain as the whole of its eligibility test. It is
|
|
27852
|
+
* a fact rather than an inference: the machine was attached, the send did not
|
|
27853
|
+
* land, so the row is owed — which no time window can state, because the same
|
|
27854
|
+
* window that holds the rows a past attachment left owed also holds every
|
|
27855
|
+
* capture recorded while the machine was DETACHED, and those were never
|
|
27856
|
+
* offered to anyone.
|
|
27857
|
+
*
|
|
27858
|
+
* Idempotent, and never un-set: `markSynced` settling the row is what takes it
|
|
27859
|
+
* out of the drain's read.
|
|
27860
|
+
*/
|
|
27861
|
+
markCaptureOwed(id) {
|
|
27862
|
+
this.markOwedStmt.run({ id });
|
|
27863
|
+
}
|
|
27864
|
+
/**
|
|
27865
|
+
* Mark every capture already on disk as owed, as of `before`.
|
|
27866
|
+
*
|
|
27867
|
+
* The consent-time backfill, called once from `aka attach` when a human
|
|
27868
|
+
* grants existing-history consent — never from an ongoing drain pass, and
|
|
27869
|
+
* never inferred from a boundary that could later move. `before` is the
|
|
27870
|
+
* caller's own "now" at the moment consent was granted, so what this marks
|
|
27871
|
+
* is exactly the backlog the consent prompt already counted, not whatever a
|
|
27872
|
+
* later re-attach or key rotation might widen it to.
|
|
27873
|
+
*
|
|
27874
|
+
* Returns how many rows matched, for the caller to log or test against. Not a
|
|
27875
|
+
* count of NEWLY marked rows — a row still unsynced from an earlier call
|
|
27876
|
+
* matches again and is counted again, the same as `UPDATE`'s own `changes`.
|
|
27877
|
+
*/
|
|
27878
|
+
markCaptureBacklogOwed(before) {
|
|
27879
|
+
return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
|
|
27880
|
+
}
|
|
27567
27881
|
/** Record delivery. Called only AFTER the far side has accepted the rows. */
|
|
27568
27882
|
markSynced(ids, atMs) {
|
|
27569
27883
|
this.stampAll(ids, atMs);
|
|
@@ -27647,10 +27961,12 @@ var SqliteHistorySyncRepository = class {
|
|
|
27647
27961
|
this.countsStmt,
|
|
27648
27962
|
{ before }
|
|
27649
27963
|
);
|
|
27964
|
+
const captures = getRow(this.captureSkipCountStmt);
|
|
27650
27965
|
return {
|
|
27651
27966
|
pending: row?.pending ?? 0,
|
|
27652
27967
|
sent: row?.sent ?? 0,
|
|
27653
|
-
skipped: row?.skipped ?? 0
|
|
27968
|
+
skipped: row?.skipped ?? 0,
|
|
27969
|
+
capturesSkipped: captures?.skipped ?? 0
|
|
27654
27970
|
};
|
|
27655
27971
|
}
|
|
27656
27972
|
/**
|
|
@@ -27678,20 +27994,54 @@ var SqliteHistorySyncRepository = class {
|
|
|
27678
27994
|
*
|
|
27679
27995
|
* Delivery is a fact about ONE recipient: rows sent to the deployment a
|
|
27680
27996
|
* machine has just left are undelivered as far as the new one is concerned.
|
|
27681
|
-
* All
|
|
27682
|
-
* attributed to the wrong deployment,
|
|
27997
|
+
* All four in one transaction, so a crash between them cannot leave stamps
|
|
27998
|
+
* attributed to the wrong deployment, a boundary that belongs to another, or
|
|
27999
|
+
* a disown with no re-mark to follow it.
|
|
27683
28000
|
*
|
|
27684
28001
|
* The boundary is written HERE and only here, which is what freezes it: a
|
|
27685
28002
|
* re-attach to the SAME deployment (a key rotation) leaves the fingerprint
|
|
27686
28003
|
* unchanged, so this never runs and the backlog does not widen back over rows
|
|
27687
28004
|
* the live path has since delivered.
|
|
28005
|
+
*
|
|
28006
|
+
* `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
|
|
28007
|
+
* granted existing-history consent for the deployment this call is arming —
|
|
28008
|
+
* a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
|
|
28009
|
+
* instant, `backlogBefore` is the ATTACH instant, and the two can be far
|
|
28010
|
+
* apart. Passed only when that grant is valid, since this method has no way
|
|
28011
|
+
* to check consent itself and must not mark a row owed for a machine that
|
|
28012
|
+
* never agreed to it. Applied AFTER the disown above, in the SAME
|
|
28013
|
+
* transaction: what the disown clears is every marker below `backlogBefore`,
|
|
28014
|
+
* which includes this deployment's OWN pre-attach rows — `aka attach` calls
|
|
28015
|
+
* `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
|
|
28016
|
+
* on the cleared side of that bound — and the re-mark in the same
|
|
28017
|
+
* transaction is what puts those rows back. A crash between the two cannot
|
|
28018
|
+
* strand the ledger disowned with nothing re-marked — the transaction either
|
|
28019
|
+
* lands whole or not at all, and a fingerprint mismatch that has not yet
|
|
28020
|
+
* committed re-enters this method on the very next pass. Omit it (the
|
|
28021
|
+
* structural-only tests do) to exercise the disown in isolation.
|
|
28022
|
+
*
|
|
28023
|
+
* The disown is bounded by `backlogBefore`, which is what keeps it from
|
|
28024
|
+
* touching a marker the NEW deployment's OWN live path has already set: B's
|
|
28025
|
+
* live path can mark a capture owed from the moment `aka attach` writes the
|
|
28026
|
+
* descriptor, before the drain's first pass ever reaches this method, and
|
|
28027
|
+
* such a row sits at or after the bound rather than below it. What keeps the
|
|
28028
|
+
* disown from eating THIS SAME CALL's own re-mark is the order, not the
|
|
28029
|
+
* bound — disown runs first, re-mark second, both inside the one
|
|
28030
|
+
* transaction above.
|
|
27688
28031
|
*/
|
|
27689
|
-
rearmFor(fingerprint, backlogBefore) {
|
|
28032
|
+
rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
|
|
27690
28033
|
this.ensureRowStmt.run();
|
|
27691
28034
|
withTransaction(
|
|
27692
28035
|
this.db,
|
|
27693
28036
|
() => {
|
|
28037
|
+
const previous = getRow(this.fingerprintStmt)?.fingerprint;
|
|
27694
28038
|
this.rearmStmt.run();
|
|
28039
|
+
if (previous !== null && previous !== void 0 && previous !== fingerprint) {
|
|
28040
|
+
this.disownCapturesStmt.run({ attachedAt: backlogBefore });
|
|
28041
|
+
}
|
|
28042
|
+
if (backfillCapturesBefore !== void 0) {
|
|
28043
|
+
this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
|
|
28044
|
+
}
|
|
27695
28045
|
this.setFingerprintStmt.run({ fingerprint, backlogBefore });
|
|
27696
28046
|
},
|
|
27697
28047
|
"IMMEDIATE"
|
|
@@ -27888,7 +28238,256 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
27888
28238
|
};
|
|
27889
28239
|
|
|
27890
28240
|
// ../../packages/persistence/src/repositories/installed-packs.ts
|
|
27891
|
-
import { createHash as createHash2, randomUUID as
|
|
28241
|
+
import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
|
|
28242
|
+
|
|
28243
|
+
// ../../packages/persistence/src/policy-floor.ts
|
|
28244
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
28245
|
+
import { join as join6 } from "path";
|
|
28246
|
+
|
|
28247
|
+
// ../../packages/persistence/src/local-layout.ts
|
|
28248
|
+
import { renameSync as renameSync3 } from "fs";
|
|
28249
|
+
import { mkdir } from "fs/promises";
|
|
28250
|
+
import { homedir } from "os";
|
|
28251
|
+
import { join as join4 } from "path";
|
|
28252
|
+
function defaultDataDir() {
|
|
28253
|
+
return join4(homedir(), ".aka");
|
|
28254
|
+
}
|
|
28255
|
+
function settingsDir(base = defaultDataDir()) {
|
|
28256
|
+
return join4(base, "settings");
|
|
28257
|
+
}
|
|
28258
|
+
function dataDir(base = defaultDataDir()) {
|
|
28259
|
+
return join4(base, "data");
|
|
28260
|
+
}
|
|
28261
|
+
function dbPath(base = defaultDataDir()) {
|
|
28262
|
+
return join4(dataDir(base), "aka.db");
|
|
28263
|
+
}
|
|
28264
|
+
function keysDir(base = defaultDataDir()) {
|
|
28265
|
+
return join4(base, "keys");
|
|
28266
|
+
}
|
|
28267
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
28268
|
+
ensureDataDirSync(dir);
|
|
28269
|
+
}
|
|
28270
|
+
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
28271
|
+
const moves = [
|
|
28272
|
+
{ name: "config.json", dest: settingsDir(base) },
|
|
28273
|
+
{ name: "policy-cache.json", dest: dataDir(base) }
|
|
28274
|
+
];
|
|
28275
|
+
for (const { name, dest } of moves) {
|
|
28276
|
+
try {
|
|
28277
|
+
ensureDataDirSync(dest);
|
|
28278
|
+
const moved = join4(dest, name);
|
|
28279
|
+
renameSync3(join4(base, name), moved);
|
|
28280
|
+
tightenFile(moved);
|
|
28281
|
+
} catch {
|
|
28282
|
+
}
|
|
28283
|
+
}
|
|
28284
|
+
}
|
|
28285
|
+
|
|
28286
|
+
// ../../packages/persistence/src/settings.ts
|
|
28287
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
28288
|
+
import { join as join5 } from "path";
|
|
28289
|
+
|
|
28290
|
+
// ../../packages/persistence/src/file-lock.ts
|
|
28291
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
28292
|
+
import {
|
|
28293
|
+
closeSync,
|
|
28294
|
+
existsSync as existsSync2,
|
|
28295
|
+
openSync,
|
|
28296
|
+
readFileSync as readFileSync2,
|
|
28297
|
+
rmSync as rmSync5,
|
|
28298
|
+
statSync as statSync3,
|
|
28299
|
+
writeFileSync as writeFileSync2
|
|
28300
|
+
} from "fs";
|
|
28301
|
+
import { hostname as hostname3 } from "os";
|
|
28302
|
+
var PARK = new Int32Array(new SharedArrayBuffer(4));
|
|
28303
|
+
|
|
28304
|
+
// ../../packages/persistence/src/managed-settings.ts
|
|
28305
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
28306
|
+
import { posix, win32 } from "path";
|
|
28307
|
+
function managedSettingsPaths(platform2 = process.platform) {
|
|
28308
|
+
if (platform2 === "darwin") {
|
|
28309
|
+
return [
|
|
28310
|
+
posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
|
|
28311
|
+
posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
|
|
28312
|
+
];
|
|
28313
|
+
}
|
|
28314
|
+
if (platform2 === "win32") {
|
|
28315
|
+
return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
|
|
28316
|
+
}
|
|
28317
|
+
return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
|
|
28318
|
+
}
|
|
28319
|
+
var testOnlyManagedPaths = null;
|
|
28320
|
+
function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
|
|
28321
|
+
for (const path of paths) {
|
|
28322
|
+
let text;
|
|
28323
|
+
try {
|
|
28324
|
+
text = readFileSync3(path, "utf8");
|
|
28325
|
+
} catch {
|
|
28326
|
+
continue;
|
|
28327
|
+
}
|
|
28328
|
+
const record2 = parseJsonObject(text);
|
|
28329
|
+
if (!record2) continue;
|
|
28330
|
+
const parsed = ManagedSettings.safeParse(record2);
|
|
28331
|
+
if (parsed.success) return parsed.data;
|
|
28332
|
+
}
|
|
28333
|
+
return null;
|
|
28334
|
+
}
|
|
28335
|
+
function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
|
|
28336
|
+
if (!managed) return settings;
|
|
28337
|
+
const { values } = managed;
|
|
28338
|
+
const merged = { ...settings };
|
|
28339
|
+
if (values.runMode !== void 0) merged.runMode = values.runMode;
|
|
28340
|
+
if (values.controlPlane !== void 0) {
|
|
28341
|
+
merged.controlPlane = {
|
|
28342
|
+
...values.controlPlane,
|
|
28343
|
+
// The administrator pinned WHICH deployment, not WHEN this machine
|
|
28344
|
+
// joined it. Keep the user's own attach time when the endpoint is
|
|
28345
|
+
// unchanged, so a managed machine does not appear to re-attach on every
|
|
28346
|
+
// read; stamp a fresh one when the administrator moved it.
|
|
28347
|
+
attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
|
|
28348
|
+
};
|
|
28349
|
+
}
|
|
28350
|
+
if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
|
|
28351
|
+
if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
|
|
28352
|
+
if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
|
|
28353
|
+
if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
|
|
28354
|
+
if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
|
|
28355
|
+
if (values.vaultConsent !== void 0) {
|
|
28356
|
+
merged.vaultConsent = values.vaultConsent ? (
|
|
28357
|
+
// Keep an existing valid grant so its acknowledgedAt survives; mint one
|
|
28358
|
+
// at the current version otherwise.
|
|
28359
|
+
settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
|
|
28360
|
+
) : void 0;
|
|
28361
|
+
}
|
|
28362
|
+
if (values.modelJudgeConsent !== void 0) {
|
|
28363
|
+
merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
|
|
28364
|
+
acknowledgedAt: now().toISOString(),
|
|
28365
|
+
payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
|
|
28366
|
+
} : void 0;
|
|
28367
|
+
}
|
|
28368
|
+
return merged;
|
|
28369
|
+
}
|
|
28370
|
+
|
|
28371
|
+
// ../../packages/persistence/src/settings.ts
|
|
28372
|
+
var SETTINGS_FILENAME = "settings.json";
|
|
28373
|
+
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
28374
|
+
return overlayManagedSettings(readUserSettings(base), readManagedSettings());
|
|
28375
|
+
}
|
|
28376
|
+
function readUserSettings(base) {
|
|
28377
|
+
const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
|
|
28378
|
+
if (!record2) return defaultWorkspaceSettings();
|
|
28379
|
+
try {
|
|
28380
|
+
return WorkspaceSettings.parse(record2);
|
|
28381
|
+
} catch {
|
|
28382
|
+
return defaultWorkspaceSettings();
|
|
28383
|
+
}
|
|
28384
|
+
}
|
|
28385
|
+
function readJson(file2) {
|
|
28386
|
+
let text;
|
|
28387
|
+
try {
|
|
28388
|
+
text = readFileSync4(file2, "utf8");
|
|
28389
|
+
} catch {
|
|
28390
|
+
return null;
|
|
28391
|
+
}
|
|
28392
|
+
return parseJsonObject(text) ?? null;
|
|
28393
|
+
}
|
|
28394
|
+
|
|
28395
|
+
// ../../packages/persistence/src/policy-floor.ts
|
|
28396
|
+
function refusalMessage(pack, attempted, floor, refusal) {
|
|
28397
|
+
switch (refusal) {
|
|
28398
|
+
case "lock":
|
|
28399
|
+
return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
|
|
28400
|
+
case "disable":
|
|
28401
|
+
return `refusing to disable '${pack}': it is governed by the connected control plane`;
|
|
28402
|
+
case "floor":
|
|
28403
|
+
return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
|
|
28404
|
+
}
|
|
28405
|
+
}
|
|
28406
|
+
var PolicyFloorError = class extends Error {
|
|
28407
|
+
/** `namespace/packId` of the detection whose write was refused. */
|
|
28408
|
+
pack;
|
|
28409
|
+
/**
|
|
28410
|
+
* The archetype the caller asked for, or null when the write named none —
|
|
28411
|
+
* clearing the assignment, or switching the detection off.
|
|
28412
|
+
*/
|
|
28413
|
+
attempted;
|
|
28414
|
+
/** The weakest archetype the control plane permits for this pack. */
|
|
28415
|
+
floor;
|
|
28416
|
+
refusal;
|
|
28417
|
+
constructor(pack, attempted, floor, refusal) {
|
|
28418
|
+
super(refusalMessage(pack, attempted, floor, refusal));
|
|
28419
|
+
this.name = "PolicyFloorError";
|
|
28420
|
+
this.pack = pack;
|
|
28421
|
+
this.attempted = attempted;
|
|
28422
|
+
this.floor = floor;
|
|
28423
|
+
this.refusal = refusal;
|
|
28424
|
+
}
|
|
28425
|
+
};
|
|
28426
|
+
function readCachedPolicyBundle(base = defaultDataDir()) {
|
|
28427
|
+
try {
|
|
28428
|
+
const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
|
|
28429
|
+
const parsed = JSON.parse(raw);
|
|
28430
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
28431
|
+
return PolicyBundle.parse(parsed.bundle);
|
|
28432
|
+
} catch {
|
|
28433
|
+
return null;
|
|
28434
|
+
}
|
|
28435
|
+
}
|
|
28436
|
+
function indexEnabled(policies) {
|
|
28437
|
+
const byRuleId = /* @__PURE__ */ new Map();
|
|
28438
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
28439
|
+
for (const policy of policies) {
|
|
28440
|
+
if (!policy.enabled) continue;
|
|
28441
|
+
if ("ruleId" in policy.target) {
|
|
28442
|
+
if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
|
|
28443
|
+
} else if (!byCategory.has(policy.target.category)) {
|
|
28444
|
+
byCategory.set(policy.target.category, policy.action);
|
|
28445
|
+
}
|
|
28446
|
+
}
|
|
28447
|
+
return { byRuleId, byCategory };
|
|
28448
|
+
}
|
|
28449
|
+
function hasAuthoredPolicy(policies, rules, byRuleId) {
|
|
28450
|
+
const ruleIds = new Set(rules.map((rule) => rule.id));
|
|
28451
|
+
const categories = new Set(rules.map((rule) => rule.category));
|
|
28452
|
+
const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
|
|
28453
|
+
return policies.some((policy) => {
|
|
28454
|
+
if (!policy.enabled || policy.provenance !== "authored") return false;
|
|
28455
|
+
return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
|
|
28456
|
+
});
|
|
28457
|
+
}
|
|
28458
|
+
function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
|
|
28459
|
+
const floors = openControlPlaneFloors(base);
|
|
28460
|
+
return floors === null ? null : floors.floorFor(rules);
|
|
28461
|
+
}
|
|
28462
|
+
function openControlPlaneFloors(base = defaultDataDir()) {
|
|
28463
|
+
if (!isAttached(readWorkspaceSettings(base))) return null;
|
|
28464
|
+
const bundle = readCachedPolicyBundle(base);
|
|
28465
|
+
if (bundle === null) return null;
|
|
28466
|
+
const indexes = indexEnabled(bundle.policies);
|
|
28467
|
+
return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
|
|
28468
|
+
}
|
|
28469
|
+
function resolveFloor(rules, policies, { byRuleId, byCategory }) {
|
|
28470
|
+
let action = null;
|
|
28471
|
+
for (const rule of rules) {
|
|
28472
|
+
const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
|
|
28473
|
+
if (resolved === void 0) continue;
|
|
28474
|
+
action = action === null ? resolved : strongerAction(action, resolved);
|
|
28475
|
+
}
|
|
28476
|
+
if (action === null) return null;
|
|
28477
|
+
return {
|
|
28478
|
+
floor: weakestBuiltinAtLeast(action),
|
|
28479
|
+
locked: hasAuthoredPolicy(policies, rules, byRuleId)
|
|
28480
|
+
};
|
|
28481
|
+
}
|
|
28482
|
+
function policyAssignmentRefusal(policyId, floor) {
|
|
28483
|
+
if (floor.locked) return "lock";
|
|
28484
|
+
const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
|
|
28485
|
+
return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
|
|
28486
|
+
}
|
|
28487
|
+
function packEnablementRefusal(enabled, floor) {
|
|
28488
|
+
if (floor === null || enabled) return null;
|
|
28489
|
+
return "disable";
|
|
28490
|
+
}
|
|
27892
28491
|
|
|
27893
28492
|
// ../../packages/persistence/src/semver.ts
|
|
27894
28493
|
function parse3(version2) {
|
|
@@ -27982,8 +28581,19 @@ function ruleIdsOf(rulesJson) {
|
|
|
27982
28581
|
return ids;
|
|
27983
28582
|
}
|
|
27984
28583
|
var SqliteInstalledPacksRepository = class {
|
|
27985
|
-
|
|
28584
|
+
/**
|
|
28585
|
+
* `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
|
|
28586
|
+
* floor needs both halves of it (settings/ says whether this machine is
|
|
28587
|
+
* attached, data/ holds the cached bundle). It is optional because a caller
|
|
28588
|
+
* holding only a DatabaseSync — every test construction site, and any embedder
|
|
28589
|
+
* that opens the store itself — has no layout to point at, and such a caller
|
|
28590
|
+
* gets the pre-existing behaviour: no floor, no lock. Production threads it in
|
|
28591
|
+
* from `openLocalDatabase`, which is the single construction site that owns a
|
|
28592
|
+
* real `~/.aka`.
|
|
28593
|
+
*/
|
|
28594
|
+
constructor(db, baseDir) {
|
|
27986
28595
|
this.db = db;
|
|
28596
|
+
this.baseDir = baseDir;
|
|
27987
28597
|
this.insertMissingStmt = db.prepare(
|
|
27988
28598
|
`INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
|
|
27989
28599
|
VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
|
|
@@ -28005,11 +28615,17 @@ var SqliteInstalledPacksRepository = class {
|
|
|
28005
28615
|
this.signatureStmt = db.prepare(
|
|
28006
28616
|
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
|
|
28007
28617
|
);
|
|
28618
|
+
this.packRulesStmt = db.prepare(
|
|
28619
|
+
`SELECT rules_json AS rulesJson FROM installed_packs
|
|
28620
|
+
WHERE namespace = ? AND pack_id = ?`
|
|
28621
|
+
);
|
|
28008
28622
|
}
|
|
28009
28623
|
db;
|
|
28624
|
+
baseDir;
|
|
28010
28625
|
insertMissingStmt;
|
|
28011
28626
|
upsertAvailableStmt;
|
|
28012
28627
|
signatureStmt;
|
|
28628
|
+
packRulesStmt;
|
|
28013
28629
|
/**
|
|
28014
28630
|
* Record the running binary's detection inventory. Refreshes the
|
|
28015
28631
|
* available_packs mirror (pruning packs the binary no longer ships) and
|
|
@@ -28051,7 +28667,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
28051
28667
|
let behind = false;
|
|
28052
28668
|
for (const row of rows) {
|
|
28053
28669
|
const params = {
|
|
28054
|
-
id:
|
|
28670
|
+
id: randomUUID4(),
|
|
28055
28671
|
namespace: row.namespace,
|
|
28056
28672
|
packId: row.packId,
|
|
28057
28673
|
version: row.version,
|
|
@@ -28063,7 +28679,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
28063
28679
|
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
28064
28680
|
this.upsertAvailableStmt.run({
|
|
28065
28681
|
...params,
|
|
28066
|
-
id:
|
|
28682
|
+
id: randomUUID4(),
|
|
28067
28683
|
recordedBy: meta4?.recordedBy ?? null
|
|
28068
28684
|
});
|
|
28069
28685
|
} else {
|
|
@@ -28309,9 +28925,65 @@ var SqliteInstalledPacksRepository = class {
|
|
|
28309
28925
|
// NOT on the hook path — so, unlike recordInventory, these surface errors to the
|
|
28310
28926
|
// caller rather than swallowing them. Each returns whether a row matched, so the
|
|
28311
28927
|
// caller can tell an edit from a no-such-detection.
|
|
28928
|
+
/**
|
|
28929
|
+
* The rules one installed pack owns, reduced to what a floor computation
|
|
28930
|
+
* reads. Display-tolerant parsing on purpose: a pack whose snapshot is
|
|
28931
|
+
* unreadable contributes no rules to a scan either, so it is not a detection
|
|
28932
|
+
* the control plane can be governing, and an empty list correctly imposes no
|
|
28933
|
+
* floor. Enabled state is deliberately not filtered — a disabled pack is one
|
|
28934
|
+
* the user can re-enable, and its assignment stays governed meanwhile.
|
|
28935
|
+
*/
|
|
28936
|
+
packFloorRules(namespace, packId) {
|
|
28937
|
+
const row = getRow(this.packRulesStmt, [namespace, packId]);
|
|
28938
|
+
if (!row) return [];
|
|
28939
|
+
return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
|
|
28940
|
+
}
|
|
28941
|
+
/**
|
|
28942
|
+
* What the connected control plane imposes on one installed pack, or null on a
|
|
28943
|
+
* machine that is its own authority (standalone, no cached bundle, or a
|
|
28944
|
+
* repository constructed without a layout base).
|
|
28945
|
+
*
|
|
28946
|
+
* Exposed as a READ so a surface can render the constraint — grey out the
|
|
28947
|
+
* choices below the floor, mark a locked detection as locked — rather than
|
|
28948
|
+
* offer the user a picker whose selections it will then be told it may not
|
|
28949
|
+
* make. The refusal in `setPolicy` does not depend on any surface calling this.
|
|
28950
|
+
*/
|
|
28951
|
+
policyFloor(namespace, packId) {
|
|
28952
|
+
if (this.baseDir === void 0) return null;
|
|
28953
|
+
return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
|
|
28954
|
+
}
|
|
28955
|
+
/**
|
|
28956
|
+
* The same answer for several packs, keyed `namespace/packId` and carrying an
|
|
28957
|
+
* entry only for a pack the control plane actually governs.
|
|
28958
|
+
*
|
|
28959
|
+
* A surface listing every detection asks per pack, and asking through
|
|
28960
|
+
* `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
|
|
28961
|
+
* bundle and rebuilds its indexes once per pack — the entire cost of one
|
|
28962
|
+
* answer, repeated for each row, on every render. This reads all of that once.
|
|
28963
|
+
* Packs whose rules the snapshot cannot produce simply contribute no entry,
|
|
28964
|
+
* exactly as the single-pack read returns null for them.
|
|
28965
|
+
*/
|
|
28966
|
+
policyFloors(packs2) {
|
|
28967
|
+
const floors = /* @__PURE__ */ new Map();
|
|
28968
|
+
if (this.baseDir === void 0) return floors;
|
|
28969
|
+
const source = openControlPlaneFloors(this.baseDir);
|
|
28970
|
+
if (source === null) return floors;
|
|
28971
|
+
for (const pack of packs2) {
|
|
28972
|
+
const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
|
|
28973
|
+
if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
|
|
28974
|
+
}
|
|
28975
|
+
return floors;
|
|
28976
|
+
}
|
|
28312
28977
|
/**
|
|
28313
28978
|
* Assign (or clear, with null) the enforcement policy for one installed pack.
|
|
28314
|
-
* `policyId` must be a known built-in id (monitor/warn/redact/block).
|
|
28979
|
+
* `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
|
|
28980
|
+
*
|
|
28981
|
+
* On an ATTACHED machine the organization's bundle is a floor this refuses to
|
|
28982
|
+
* write below, and a detection the organization has authored a policy for is
|
|
28983
|
+
* refused outright — see policy-floor.ts for both, and for why the refusal is
|
|
28984
|
+
* a throw rather than a silently substituted value. This is the one device-local
|
|
28985
|
+
* write path for the assignment, so the check belongs here rather than on any
|
|
28986
|
+
* surface that offers the choice.
|
|
28315
28987
|
*/
|
|
28316
28988
|
setPolicy(namespace, packId, policyId) {
|
|
28317
28989
|
if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
|
|
@@ -28319,14 +28991,38 @@ var SqliteInstalledPacksRepository = class {
|
|
|
28319
28991
|
`Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
|
|
28320
28992
|
);
|
|
28321
28993
|
}
|
|
28994
|
+
const requested = policyId;
|
|
28995
|
+
const floor = this.policyFloor(namespace, packId);
|
|
28996
|
+
if (floor !== null) {
|
|
28997
|
+
const refusal = policyAssignmentRefusal(requested, floor);
|
|
28998
|
+
if (refusal !== null) {
|
|
28999
|
+
throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
|
|
29000
|
+
}
|
|
29001
|
+
}
|
|
28322
29002
|
const res = this.db.prepare(
|
|
28323
29003
|
`UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
|
|
28324
29004
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
28325
29005
|
).run({ policyId, now: Date.now(), namespace, packId });
|
|
28326
29006
|
return Number(res.changes) > 0;
|
|
28327
29007
|
}
|
|
28328
|
-
/**
|
|
29008
|
+
/**
|
|
29009
|
+
* Enable or disable one installed pack.
|
|
29010
|
+
*
|
|
29011
|
+
* On an ATTACHED machine a detection the organization's bundle governs at all
|
|
29012
|
+
* may not be switched OFF here — see packEnablementRefusal for why that is not
|
|
29013
|
+
* merely another point below the floor, and why re-enabling stays open. Like
|
|
29014
|
+
* the assignment above, the check belongs at this write path rather than on a
|
|
29015
|
+
* surface: this is the one device-local writer of the column, and a refusal
|
|
29016
|
+
* that lived in a page would leave the CLI free.
|
|
29017
|
+
*/
|
|
28329
29018
|
setEnabled(namespace, packId, enabled) {
|
|
29019
|
+
const floor = this.policyFloor(namespace, packId);
|
|
29020
|
+
if (floor !== null) {
|
|
29021
|
+
const refusal = packEnablementRefusal(enabled, floor);
|
|
29022
|
+
if (refusal !== null) {
|
|
29023
|
+
throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
|
|
29024
|
+
}
|
|
29025
|
+
}
|
|
28330
29026
|
const res = this.db.prepare(
|
|
28331
29027
|
`UPDATE installed_packs SET enabled = :enabled, updated_at = :now
|
|
28332
29028
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
@@ -28412,7 +29108,7 @@ var SqliteInventoryRepository = class {
|
|
|
28412
29108
|
};
|
|
28413
29109
|
|
|
28414
29110
|
// ../../packages/persistence/src/repositories/inventory-assets.ts
|
|
28415
|
-
import { randomUUID as
|
|
29111
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
28416
29112
|
var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
|
|
28417
29113
|
var VALID_HARNESS_IDS = new Set(HarnessId.options);
|
|
28418
29114
|
var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
|
|
@@ -28901,7 +29597,7 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
28901
29597
|
`INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
|
|
28902
29598
|
VALUES (:id, :projectId, :path, :access, :now, :now)
|
|
28903
29599
|
ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
|
|
28904
|
-
).run({ id:
|
|
29600
|
+
).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
|
|
28905
29601
|
}
|
|
28906
29602
|
return true;
|
|
28907
29603
|
}
|
|
@@ -28922,7 +29618,7 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
28922
29618
|
`INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
|
|
28923
29619
|
VALUES (:id, :assetId, :trust, :now, :now)
|
|
28924
29620
|
ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
|
|
28925
|
-
).run({ id:
|
|
29621
|
+
).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
|
|
28926
29622
|
}
|
|
28927
29623
|
this.configRowsCache = void 0;
|
|
28928
29624
|
return "ok";
|
|
@@ -29219,7 +29915,7 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
29219
29915
|
};
|
|
29220
29916
|
|
|
29221
29917
|
// ../../packages/persistence/src/repositories/policies.ts
|
|
29222
|
-
import { randomUUID as
|
|
29918
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
29223
29919
|
var SqlitePoliciesRepository = class {
|
|
29224
29920
|
constructor(db) {
|
|
29225
29921
|
this.db = db;
|
|
@@ -29254,7 +29950,7 @@ var SqlitePoliciesRepository = class {
|
|
|
29254
29950
|
failOpenTransaction(this.db, () => {
|
|
29255
29951
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
29256
29952
|
stmt.run({
|
|
29257
|
-
id:
|
|
29953
|
+
id: randomUUID6(),
|
|
29258
29954
|
target: JSON.stringify({ category }),
|
|
29259
29955
|
action,
|
|
29260
29956
|
now: Date.now()
|
|
@@ -29274,7 +29970,7 @@ var SqlitePoliciesRepository = class {
|
|
|
29274
29970
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
29275
29971
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
29276
29972
|
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
29277
|
-
).run({ id:
|
|
29973
|
+
).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
|
|
29278
29974
|
}
|
|
29279
29975
|
// Caps every global per-category policy currently set to block/redact down
|
|
29280
29976
|
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
@@ -29342,7 +30038,7 @@ var SqlitePolicyCatalogRepository = class {
|
|
|
29342
30038
|
};
|
|
29343
30039
|
|
|
29344
30040
|
// ../../packages/persistence/src/repositories/project-files.ts
|
|
29345
|
-
import { randomUUID as
|
|
30041
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
29346
30042
|
var SqliteProjectFilesRepository = class {
|
|
29347
30043
|
constructor(db) {
|
|
29348
30044
|
this.db = db;
|
|
@@ -29374,7 +30070,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
29374
30070
|
const stamp = Math.max(now, maxStamp + 1);
|
|
29375
30071
|
for (const file2 of scan2.files) {
|
|
29376
30072
|
this.upsertStmt.run({
|
|
29377
|
-
id:
|
|
30073
|
+
id: randomUUID7(),
|
|
29378
30074
|
projectId,
|
|
29379
30075
|
path: file2.path,
|
|
29380
30076
|
name: file2.name,
|
|
@@ -29388,9 +30084,9 @@ var SqliteProjectFilesRepository = class {
|
|
|
29388
30084
|
};
|
|
29389
30085
|
|
|
29390
30086
|
// ../../packages/persistence/src/repositories/resolutions.ts
|
|
29391
|
-
import { randomUUID as
|
|
30087
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
29392
30088
|
var SqliteResolutionsRepository = class {
|
|
29393
|
-
constructor(db, now = () => Date.now(), newId = () =>
|
|
30089
|
+
constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
|
|
29394
30090
|
this.db = db;
|
|
29395
30091
|
this.now = now;
|
|
29396
30092
|
this.newId = newId;
|
|
@@ -29603,7 +30299,7 @@ var SqliteScanLedgerRepository = class {
|
|
|
29603
30299
|
};
|
|
29604
30300
|
|
|
29605
30301
|
// ../../packages/persistence/src/repositories/secret-vault.ts
|
|
29606
|
-
import { randomUUID as
|
|
30302
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
29607
30303
|
function pageLimit(requested, fallback) {
|
|
29608
30304
|
if (requested === void 0) return fallback;
|
|
29609
30305
|
return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
|
|
@@ -29649,12 +30345,14 @@ var SELECT_COLUMNS = `
|
|
|
29649
30345
|
ciphertext,
|
|
29650
30346
|
nonce,
|
|
29651
30347
|
auth_tag AS authTag,
|
|
30348
|
+
user_authorized AS userAuthorized,
|
|
29652
30349
|
occurrence_count AS occurrenceCount,
|
|
29653
30350
|
first_seen AS firstSeen,
|
|
29654
30351
|
last_seen AS lastSeen`;
|
|
29655
30352
|
function toRow(raw) {
|
|
29656
|
-
const { provider, ...rest } = raw;
|
|
29657
|
-
|
|
30353
|
+
const { provider, userAuthorized, ...rest } = raw;
|
|
30354
|
+
const row = { ...rest, userAuthorized: userAuthorized !== 0 };
|
|
30355
|
+
return provider === null ? row : { ...row, provider };
|
|
29658
30356
|
}
|
|
29659
30357
|
var SqliteSecretVaultRepository = class {
|
|
29660
30358
|
constructor(db) {
|
|
@@ -29664,17 +30362,18 @@ var SqliteSecretVaultRepository = class {
|
|
|
29664
30362
|
pointer_id, value_fingerprint, fingerprint_key_version, key_version,
|
|
29665
30363
|
format_version, category, rule_id, masked_match, provider,
|
|
29666
30364
|
ciphertext, nonce, auth_tag,
|
|
29667
|
-
occurrence_count, first_seen, last_seen
|
|
30365
|
+
user_authorized, occurrence_count, first_seen, last_seen
|
|
29668
30366
|
) VALUES (
|
|
29669
30367
|
:pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
|
|
29670
30368
|
:formatVersion, :category, :ruleId, :maskedMatch, :provider,
|
|
29671
30369
|
:ciphertext, :nonce, :authTag,
|
|
29672
|
-
1, :now, :now
|
|
30370
|
+
:userAuthorized, 1, :now, :now
|
|
29673
30371
|
)`
|
|
29674
30372
|
);
|
|
29675
30373
|
this.bumpStmt = db.prepare(
|
|
29676
30374
|
`UPDATE secret_vault
|
|
29677
|
-
SET occurrence_count = occurrence_count + 1, last_seen = :now
|
|
30375
|
+
SET occurrence_count = occurrence_count + 1, last_seen = :now,
|
|
30376
|
+
user_authorized = max(user_authorized, :userAuthorized)
|
|
29678
30377
|
WHERE value_fingerprint = :valueFingerprint`
|
|
29679
30378
|
);
|
|
29680
30379
|
this.byPointerStmt = db.prepare(
|
|
@@ -29694,6 +30393,7 @@ var SqliteSecretVaultRepository = class {
|
|
|
29694
30393
|
SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
|
|
29695
30394
|
WHERE pointer_id = :pointerId`
|
|
29696
30395
|
);
|
|
30396
|
+
this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
|
|
29697
30397
|
this.derefStmt = db.prepare(
|
|
29698
30398
|
`INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
|
|
29699
30399
|
VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
|
|
@@ -29707,6 +30407,7 @@ var SqliteSecretVaultRepository = class {
|
|
|
29707
30407
|
listStmt;
|
|
29708
30408
|
replaceCiphertextStmt;
|
|
29709
30409
|
refreshFingerprintStmt;
|
|
30410
|
+
deleteByPointerStmt;
|
|
29710
30411
|
derefStmt;
|
|
29711
30412
|
/**
|
|
29712
30413
|
* Vault a value, or record another sighting of one already vaulted. Keyed on
|
|
@@ -29715,6 +30416,11 @@ var SqliteSecretVaultRepository = class {
|
|
|
29715
30416
|
* pointer, category and ciphertext, so the same secret always resolves to one
|
|
29716
30417
|
* wire token. `minted` is true only when this call created the row.
|
|
29717
30418
|
*
|
|
30419
|
+
* `userAuthorized` is the one field a repeat call may still change, and only
|
|
30420
|
+
* upwards: it records that a PERSON asked for this value to be replaced, and
|
|
30421
|
+
* the row is shared with every automatic path that vaults the same value. See
|
|
30422
|
+
* `bumpStmt` for why clearing it is the defect this shape exists to refuse.
|
|
30423
|
+
*
|
|
29718
30424
|
* The read-then-write runs in one IMMEDIATE transaction so two concurrent
|
|
29719
30425
|
* writers cannot both decide they are minting.
|
|
29720
30426
|
*/
|
|
@@ -29741,13 +30447,18 @@ var SqliteSecretVaultRepository = class {
|
|
|
29741
30447
|
ciphertext: input2.ciphertext,
|
|
29742
30448
|
nonce: input2.nonce,
|
|
29743
30449
|
authTag: input2.authTag,
|
|
30450
|
+
userAuthorized: input2.userAuthorized === true ? 1 : 0,
|
|
29744
30451
|
now
|
|
29745
30452
|
})
|
|
29746
30453
|
);
|
|
29747
30454
|
minted = true;
|
|
29748
30455
|
return;
|
|
29749
30456
|
}
|
|
29750
|
-
this.bumpStmt.run({
|
|
30457
|
+
this.bumpStmt.run({
|
|
30458
|
+
valueFingerprint: input2.valueFingerprint,
|
|
30459
|
+
userAuthorized: input2.userAuthorized === true ? 1 : 0,
|
|
30460
|
+
now
|
|
30461
|
+
});
|
|
29751
30462
|
},
|
|
29752
30463
|
"IMMEDIATE"
|
|
29753
30464
|
);
|
|
@@ -29807,6 +30518,42 @@ var SqliteSecretVaultRepository = class {
|
|
|
29807
30518
|
);
|
|
29808
30519
|
return destroyed;
|
|
29809
30520
|
}
|
|
30521
|
+
/**
|
|
30522
|
+
* Destroy the named entries and report WHICH ones went — the scoped
|
|
30523
|
+
* counterpart to `purgeAll`, for a caller that has already put those specific
|
|
30524
|
+
* values back where they came from. Ids the store does not hold are absent
|
|
30525
|
+
* from the answer rather than an error, so a set assembled from a stale read
|
|
30526
|
+
* is not a fault. The deref audit is left alone, exactly as the purge leaves
|
|
30527
|
+
* it.
|
|
30528
|
+
*
|
|
30529
|
+
* The ids come back rather than a count because the caller's next act is to
|
|
30530
|
+
* write a purge row per destroyed entry, and a record of destruction has to
|
|
30531
|
+
* be a record of what was really destroyed: a selection is a claim about a
|
|
30532
|
+
* read that has since gone stale, and auditing from it invents a purge for an
|
|
30533
|
+
* entry still sitting in the vault.
|
|
30534
|
+
*
|
|
30535
|
+
* One transaction over the whole set rather than a statement per id: the
|
|
30536
|
+
* caller hands this the result of a restore pass it has completed, and a
|
|
30537
|
+
* fault partway through must leave the vault as it was found rather than
|
|
30538
|
+
* destroying a prefix of it. The vault holds the only copy of what a pointer
|
|
30539
|
+
* stands for, so half a delete is not a state anything can recover from.
|
|
30540
|
+
*/
|
|
30541
|
+
deleteByPointerIds(pointerIds) {
|
|
30542
|
+
if (pointerIds.length === 0) return [];
|
|
30543
|
+
const deleted = [];
|
|
30544
|
+
withTransaction(
|
|
30545
|
+
this.db,
|
|
30546
|
+
() => {
|
|
30547
|
+
for (const pointerId of pointerIds) {
|
|
30548
|
+
if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
|
|
30549
|
+
deleted.push(pointerId);
|
|
30550
|
+
}
|
|
30551
|
+
}
|
|
30552
|
+
},
|
|
30553
|
+
"IMMEDIATE"
|
|
30554
|
+
);
|
|
30555
|
+
return deleted;
|
|
30556
|
+
}
|
|
29810
30557
|
/**
|
|
29811
30558
|
* Record (or re-stamp) one place a pointer has been written. One row per
|
|
29812
30559
|
* (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
|
|
@@ -29819,7 +30566,7 @@ var SqliteSecretVaultRepository = class {
|
|
|
29819
30566
|
VALUES (:id, :pointerId, :location, :kind, :now, :now)
|
|
29820
30567
|
ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
|
|
29821
30568
|
).run({
|
|
29822
|
-
id:
|
|
30569
|
+
id: randomUUID9(),
|
|
29823
30570
|
pointerId: entry.pointerId,
|
|
29824
30571
|
location: entry.location,
|
|
29825
30572
|
kind: entry.kind,
|
|
@@ -30050,7 +30797,7 @@ function toUtcDateString(ms) {
|
|
|
30050
30797
|
return new Date(ms).toISOString().slice(0, 10);
|
|
30051
30798
|
}
|
|
30052
30799
|
function isTimeseriesSeverity(s) {
|
|
30053
|
-
return s === "critical" || s === "high" || s === "medium";
|
|
30800
|
+
return s === "critical" || s === "high" || s === "medium" || s === "low";
|
|
30054
30801
|
}
|
|
30055
30802
|
var SqliteSecurityRepository = class {
|
|
30056
30803
|
constructor(db, now = () => Date.now()) {
|
|
@@ -30179,12 +30926,16 @@ var SqliteSecurityRepository = class {
|
|
|
30179
30926
|
const now = this.now();
|
|
30180
30927
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
30181
30928
|
const rows = this.findingsInRange(windowStart, now);
|
|
30182
|
-
const points = Array.from(
|
|
30183
|
-
|
|
30184
|
-
|
|
30185
|
-
|
|
30186
|
-
|
|
30187
|
-
|
|
30929
|
+
const points = Array.from(
|
|
30930
|
+
{ length: numBuckets },
|
|
30931
|
+
(_, i) => ({
|
|
30932
|
+
timestamp: toUtcDateString(windowStart + i * bucketMs),
|
|
30933
|
+
critical: 0,
|
|
30934
|
+
high: 0,
|
|
30935
|
+
medium: 0,
|
|
30936
|
+
low: 0
|
|
30937
|
+
})
|
|
30938
|
+
);
|
|
30188
30939
|
for (const r of rows) {
|
|
30189
30940
|
const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
|
|
30190
30941
|
const bucket = points[idx];
|
|
@@ -30332,15 +31083,15 @@ var SqliteSecurityRepository = class {
|
|
|
30332
31083
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
30333
31084
|
const rows = allRows(
|
|
30334
31085
|
this.db.prepare(
|
|
30335
|
-
`SELECT
|
|
31086
|
+
`SELECT e.repo AS repo, count(*) AS c
|
|
30336
31087
|
FROM inspection_findings f
|
|
30337
31088
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
30338
31089
|
WHERE e.started_at >= :from AND e.started_at < :to
|
|
30339
31090
|
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
30340
|
-
AND
|
|
30341
|
-
AND
|
|
30342
|
-
GROUP BY repo
|
|
30343
|
-
ORDER BY c DESC, repo
|
|
31091
|
+
AND e.repo IS NOT NULL
|
|
31092
|
+
AND e.repo != ''
|
|
31093
|
+
GROUP BY e.repo
|
|
31094
|
+
ORDER BY c DESC, e.repo
|
|
30344
31095
|
LIMIT :limit`
|
|
30345
31096
|
),
|
|
30346
31097
|
{ from, to: now, limit }
|
|
@@ -30402,7 +31153,8 @@ var SqliteSecurityRepository = class {
|
|
|
30402
31153
|
`SELECT f.finding_key AS finding_key,
|
|
30403
31154
|
d.rule_id AS rule_id,
|
|
30404
31155
|
d.severity AS severity,
|
|
30405
|
-
|
|
31156
|
+
e.repo AS repo,
|
|
31157
|
+
e.file_path AS path,
|
|
30406
31158
|
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
30407
31159
|
latest.resolved_at AS latest_resolved_at
|
|
30408
31160
|
FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
@@ -30421,6 +31173,7 @@ var SqliteSecurityRepository = class {
|
|
|
30421
31173
|
const items = rows.map((r) => ({
|
|
30422
31174
|
findingKey: r.finding_key,
|
|
30423
31175
|
ruleId: r.rule_id,
|
|
31176
|
+
repo: r.repo ?? "",
|
|
30424
31177
|
severity: r.severity,
|
|
30425
31178
|
path: r.path ?? "",
|
|
30426
31179
|
resolvedAt: new Date(r.latest_resolved_at).toISOString(),
|
|
@@ -30430,13 +31183,66 @@ var SqliteSecurityRepository = class {
|
|
|
30430
31183
|
}));
|
|
30431
31184
|
return Promise.resolve({ items });
|
|
30432
31185
|
}
|
|
31186
|
+
/**
|
|
31187
|
+
* Per-rule tallies of the findings that are still OPEN, whole-store.
|
|
31188
|
+
*
|
|
31189
|
+
* Scoped by status rather than by time, because the card this feeds is a to-do
|
|
31190
|
+
* list: a secret committed three weeks ago and never rotated is still the most
|
|
31191
|
+
* important thing to fix, and any window hides it. It carried a "newest N
|
|
31192
|
+
* findings" cap and then a range; the first meant a different span on every
|
|
31193
|
+
* machine, and the second reported "no recommendations" over live exposure.
|
|
31194
|
+
*
|
|
31195
|
+
* `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
|
|
31196
|
+
* so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
|
|
31197
|
+
* is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
|
|
31198
|
+
* dismissal is a judgement, not a remediation) and drops untracked legacy rows.
|
|
31199
|
+
* The two answer different questions and only this one has to match a link.
|
|
31200
|
+
*
|
|
31201
|
+
* Aggregated in SQL: the result is O(distinct rule × category × severity), so a
|
|
31202
|
+
* whole-store scope costs a grouped scan rather than a row per finding.
|
|
31203
|
+
*/
|
|
31204
|
+
recommendationInputs() {
|
|
31205
|
+
const rows = allRows(
|
|
31206
|
+
this.db.prepare(
|
|
31207
|
+
`SELECT d.rule_id AS rule_id,
|
|
31208
|
+
d.category AS category,
|
|
31209
|
+
d.severity AS severity,
|
|
31210
|
+
COUNT(*) AS count
|
|
31211
|
+
FROM inspection_findings f
|
|
31212
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
31213
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
31214
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
31215
|
+
ON latest.finding_key = f.finding_key
|
|
31216
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
31217
|
+
AND e.event_type = 'code_change'
|
|
31218
|
+
AND (
|
|
31219
|
+
f.finding_key IS NULL
|
|
31220
|
+
OR latest.status IS NULL
|
|
31221
|
+
OR latest.status NOT IN ('resolved', 'dismissed')
|
|
31222
|
+
)
|
|
31223
|
+
GROUP BY d.rule_id, d.category, d.severity`
|
|
31224
|
+
)
|
|
31225
|
+
);
|
|
31226
|
+
return Promise.resolve(
|
|
31227
|
+
rows.map((r) => ({
|
|
31228
|
+
ruleId: r.rule_id,
|
|
31229
|
+
category: r.category,
|
|
31230
|
+
severity: r.severity,
|
|
31231
|
+
count: r.count
|
|
31232
|
+
}))
|
|
31233
|
+
);
|
|
31234
|
+
}
|
|
30433
31235
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
30434
31236
|
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
30435
31237
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
30436
31238
|
findingsInRange(fromMs, toMs) {
|
|
30437
31239
|
const rows = allRows(
|
|
30438
31240
|
this.db.prepare(
|
|
30439
|
-
`
|
|
31241
|
+
// `rule_id`/`category` cost nothing extra: inspection_definitions is already
|
|
31242
|
+
// joined for `severity`, so they are two more columns off a row this read
|
|
31243
|
+
// already fetches. They feed the recommended-actions rollup.
|
|
31244
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
|
|
31245
|
+
d.rule_id AS rule_id, d.category AS category
|
|
30440
31246
|
FROM inspection_findings f
|
|
30441
31247
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
30442
31248
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
@@ -30449,13 +31255,15 @@ var SqliteSecurityRepository = class {
|
|
|
30449
31255
|
return rows.map((r) => ({
|
|
30450
31256
|
occurredAt: r.occurred_at,
|
|
30451
31257
|
severity: r.severity,
|
|
30452
|
-
actionTaken: r.action_taken
|
|
31258
|
+
actionTaken: r.action_taken,
|
|
31259
|
+
ruleId: r.rule_id,
|
|
31260
|
+
category: r.category
|
|
30453
31261
|
}));
|
|
30454
31262
|
}
|
|
30455
31263
|
};
|
|
30456
31264
|
|
|
30457
31265
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
30458
|
-
import { randomUUID as
|
|
31266
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
30459
31267
|
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
30460
31268
|
var IN_CHUNK = 500;
|
|
30461
31269
|
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
@@ -30543,7 +31351,7 @@ function buildSummary(dest, endpoints) {
|
|
|
30543
31351
|
callSiteCount,
|
|
30544
31352
|
transports: distinctTransports(transports),
|
|
30545
31353
|
dataClasses: distinctDataClasses(dataClasses),
|
|
30546
|
-
review: buildReviewInfo(dest.trust, transports),
|
|
31354
|
+
review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
|
|
30547
31355
|
network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
|
|
30548
31356
|
endpoints: endpoints.map(toEndpointSummary)
|
|
30549
31357
|
};
|
|
@@ -30570,7 +31378,7 @@ function buildDetail(dest, endpoints, callSites) {
|
|
|
30570
31378
|
lastSeen: new Date(lastSeenMs).toISOString(),
|
|
30571
31379
|
transports: distinctTransports(transports),
|
|
30572
31380
|
dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
|
|
30573
|
-
review: buildReviewInfo(dest.trust, transports),
|
|
31381
|
+
review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
|
|
30574
31382
|
network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
|
|
30575
31383
|
note: dest.note,
|
|
30576
31384
|
endpoints: endpoints.map((ep) => ({
|
|
@@ -30599,7 +31407,11 @@ var SqliteSharesRepository = class {
|
|
|
30599
31407
|
FROM share_destination d
|
|
30600
31408
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
30601
31409
|
AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
|
|
30602
|
-
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL
|
|
31410
|
+
WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
|
|
31411
|
+
AND NOT EXISTS (
|
|
31412
|
+
SELECT 1 FROM egress_decision_override o
|
|
31413
|
+
WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
|
|
31414
|
+
)`
|
|
30603
31415
|
);
|
|
30604
31416
|
const kindCounts = countBy(
|
|
30605
31417
|
this.db,
|
|
@@ -30711,7 +31523,7 @@ var SqliteSharesRepository = class {
|
|
|
30711
31523
|
(id, destination_id, host, decision, created_at, updated_at)
|
|
30712
31524
|
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
30713
31525
|
).run({
|
|
30714
|
-
id:
|
|
31526
|
+
id: randomUUID10(),
|
|
30715
31527
|
destinationId,
|
|
30716
31528
|
host: dest.host,
|
|
30717
31529
|
decision,
|
|
@@ -30860,7 +31672,7 @@ var SqliteSharesRepository = class {
|
|
|
30860
31672
|
let destinationId = destIds.get(hit.host);
|
|
30861
31673
|
if (destinationId === void 0) {
|
|
30862
31674
|
destStmt.run({
|
|
30863
|
-
id:
|
|
31675
|
+
id: randomUUID10(),
|
|
30864
31676
|
kind: hit.kind,
|
|
30865
31677
|
name: hit.name,
|
|
30866
31678
|
host: hit.host,
|
|
@@ -30876,7 +31688,7 @@ var SqliteSharesRepository = class {
|
|
|
30876
31688
|
let endpointId = endpointIds.get(endpointKey);
|
|
30877
31689
|
if (endpointId === void 0) {
|
|
30878
31690
|
endpointStmt.run({
|
|
30879
|
-
id:
|
|
31691
|
+
id: randomUUID10(),
|
|
30880
31692
|
destinationId,
|
|
30881
31693
|
method: hit.method,
|
|
30882
31694
|
transport: hit.transport,
|
|
@@ -30889,7 +31701,7 @@ var SqliteSharesRepository = class {
|
|
|
30889
31701
|
endpointIds.set(endpointKey, endpointId);
|
|
30890
31702
|
}
|
|
30891
31703
|
siteStmt.run({
|
|
30892
|
-
id:
|
|
31704
|
+
id: randomUUID10(),
|
|
30893
31705
|
endpointId,
|
|
30894
31706
|
project: input2.project,
|
|
30895
31707
|
projectKey: input2.projectKey,
|
|
@@ -31254,6 +32066,7 @@ function purgeSampleData(db) {
|
|
|
31254
32066
|
}
|
|
31255
32067
|
|
|
31256
32068
|
// ../../packages/persistence/src/database.ts
|
|
32069
|
+
var CAPTURE_GRAIN = new Set(EventKind.options);
|
|
31257
32070
|
var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
|
|
31258
32071
|
"aka.persistence.unsafeTestOnlyRawHandle"
|
|
31259
32072
|
);
|
|
@@ -31301,7 +32114,7 @@ function backupLegacyStore(db, file2) {
|
|
|
31301
32114
|
discardStore(file2, backup);
|
|
31302
32115
|
return backup;
|
|
31303
32116
|
}
|
|
31304
|
-
function openAndInitialize(file2) {
|
|
32117
|
+
function openAndInitialize(file2, base) {
|
|
31305
32118
|
let db = openWithPragmas(file2);
|
|
31306
32119
|
try {
|
|
31307
32120
|
if (isForeignSqliteLineage(db)) {
|
|
@@ -31314,7 +32127,7 @@ function openAndInitialize(file2) {
|
|
|
31314
32127
|
applyMigrations(db, file2);
|
|
31315
32128
|
tightenPerms(file2);
|
|
31316
32129
|
const policies = new SqlitePoliciesRepository(db);
|
|
31317
|
-
const installedPacks = new SqliteInstalledPacksRepository(db);
|
|
32130
|
+
const installedPacks = new SqliteInstalledPacksRepository(db, base);
|
|
31318
32131
|
const repositories = {
|
|
31319
32132
|
events: new SqliteEventsRepository(db),
|
|
31320
32133
|
findings: new SqliteFindingsRepository(db),
|
|
@@ -31350,7 +32163,7 @@ function openAndInitialize(file2) {
|
|
|
31350
32163
|
}
|
|
31351
32164
|
function openLocalDatabase(dir) {
|
|
31352
32165
|
ensureDataDirSync(dir);
|
|
31353
|
-
const file2 =
|
|
32166
|
+
const file2 = join7(dir, DB_FILENAME);
|
|
31354
32167
|
reapStalePartials(file2);
|
|
31355
32168
|
const {
|
|
31356
32169
|
db,
|
|
@@ -31378,7 +32191,13 @@ function openLocalDatabase(dir) {
|
|
|
31378
32191
|
inspectionDefinitions,
|
|
31379
32192
|
inspectionFindings,
|
|
31380
32193
|
configInventory
|
|
31381
|
-
} = openAndInitialize(
|
|
32194
|
+
} = openAndInitialize(
|
|
32195
|
+
file2,
|
|
32196
|
+
// `dir` is always `<base>/data` — every caller resolves it through
|
|
32197
|
+
// `dataDir()` — so its parent is the `~/.aka` base the layout splits into
|
|
32198
|
+
// settings/ and data/, and the pack-policy floor needs both halves.
|
|
32199
|
+
dirname2(dir)
|
|
32200
|
+
);
|
|
31382
32201
|
function captureRowId(event) {
|
|
31383
32202
|
return captureId(
|
|
31384
32203
|
event.metadata?.sessionId ?? null,
|
|
@@ -31391,6 +32210,21 @@ function openLocalDatabase(dir) {
|
|
|
31391
32210
|
historySync.markSynced([captureRowId(event)], atMs);
|
|
31392
32211
|
});
|
|
31393
32212
|
}
|
|
32213
|
+
function markCaptureOwed(event) {
|
|
32214
|
+
failOpenTransaction(db, () => {
|
|
32215
|
+
historySync.markCaptureOwed(captureRowId(event));
|
|
32216
|
+
});
|
|
32217
|
+
}
|
|
32218
|
+
function markAuditEventsDelivered(events2, atMs) {
|
|
32219
|
+
const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
|
|
32220
|
+
if (stampable.length === 0) return;
|
|
32221
|
+
failOpenTransaction(db, () => {
|
|
32222
|
+
historySync.markSynced(
|
|
32223
|
+
stampable.map((event) => event.id),
|
|
32224
|
+
atMs
|
|
32225
|
+
);
|
|
32226
|
+
});
|
|
32227
|
+
}
|
|
31394
32228
|
function recordCapture(event, detected) {
|
|
31395
32229
|
failOpenTransaction(db, () => {
|
|
31396
32230
|
const sessionId = event.metadata?.sessionId;
|
|
@@ -31477,7 +32311,7 @@ function openLocalDatabase(dir) {
|
|
|
31477
32311
|
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
31478
32312
|
if (!definitionId) continue;
|
|
31479
32313
|
inspectionFindings.insertFinding({
|
|
31480
|
-
id:
|
|
32314
|
+
id: randomUUID11(),
|
|
31481
32315
|
auditEventId: record2.scanEvent.id,
|
|
31482
32316
|
inspectionDefinitionId: definitionId,
|
|
31483
32317
|
span: finding.span,
|
|
@@ -31573,6 +32407,8 @@ function openLocalDatabase(dir) {
|
|
|
31573
32407
|
inspectionFindings,
|
|
31574
32408
|
recordCapture,
|
|
31575
32409
|
markCaptureDelivered,
|
|
32410
|
+
markCaptureOwed,
|
|
32411
|
+
markAuditEventsDelivered,
|
|
31576
32412
|
ensureInventory,
|
|
31577
32413
|
recordConfigScan,
|
|
31578
32414
|
recordProjectFiles,
|
|
@@ -31591,6 +32427,9 @@ function openLocalDatabase(dir) {
|
|
|
31591
32427
|
};
|
|
31592
32428
|
}
|
|
31593
32429
|
|
|
32430
|
+
// ../../packages/persistence/src/egress-wire.ts
|
|
32431
|
+
import { createHash as createHash3 } from "crypto";
|
|
32432
|
+
|
|
31594
32433
|
// ../../packages/persistence/src/exception-policy.ts
|
|
31595
32434
|
var UserGrantPolicyProvider = class {
|
|
31596
32435
|
#exceptions;
|
|
@@ -31611,32 +32450,18 @@ var UserGrantPolicyProvider = class {
|
|
|
31611
32450
|
}
|
|
31612
32451
|
};
|
|
31613
32452
|
|
|
31614
|
-
// ../../packages/persistence/src/file-lock.ts
|
|
31615
|
-
import { randomUUID as randomUUID11 } from "crypto";
|
|
31616
|
-
import {
|
|
31617
|
-
closeSync,
|
|
31618
|
-
existsSync as existsSync2,
|
|
31619
|
-
openSync,
|
|
31620
|
-
readFileSync as readFileSync2,
|
|
31621
|
-
rmSync as rmSync5,
|
|
31622
|
-
statSync as statSync3,
|
|
31623
|
-
writeFileSync as writeFileSync2
|
|
31624
|
-
} from "fs";
|
|
31625
|
-
import { hostname as hostname3 } from "os";
|
|
31626
|
-
var PARK = new Int32Array(new SharedArrayBuffer(4));
|
|
31627
|
-
|
|
31628
32453
|
// ../../packages/persistence/src/finding-key.ts
|
|
31629
|
-
import { createHash as
|
|
32454
|
+
import { createHash as createHash4 } from "crypto";
|
|
31630
32455
|
|
|
31631
32456
|
// ../../packages/persistence/src/fingerprint.ts
|
|
31632
32457
|
import { createHmac, randomBytes } from "crypto";
|
|
31633
|
-
import { existsSync as existsSync3, readFileSync as
|
|
31634
|
-
import { join as
|
|
32458
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
|
|
32459
|
+
import { join as join8 } from "path";
|
|
31635
32460
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
31636
32461
|
var EXCEPTION_KEY_FILENAME = "exception.key";
|
|
31637
32462
|
var KEY_MATERIAL_BYTES = 32;
|
|
31638
32463
|
function keyFilePath(dataDir2) {
|
|
31639
|
-
return
|
|
32464
|
+
return join8(dataDir2, EXCEPTION_KEY_FILENAME);
|
|
31640
32465
|
}
|
|
31641
32466
|
function parseKeyFile(raw) {
|
|
31642
32467
|
const parsed = JSON.parse(raw);
|
|
@@ -31674,7 +32499,7 @@ var FloorUnreadableError = class extends Error {
|
|
|
31674
32499
|
}
|
|
31675
32500
|
};
|
|
31676
32501
|
function storedKeyVersionFloor(dataDir2) {
|
|
31677
|
-
const file2 =
|
|
32502
|
+
const file2 = join8(dataDir2, DB_FILENAME);
|
|
31678
32503
|
if (!existsSync3(file2)) return 0;
|
|
31679
32504
|
let db;
|
|
31680
32505
|
try {
|
|
@@ -31729,7 +32554,7 @@ function occupantMessage(file2, kind) {
|
|
|
31729
32554
|
function readFingerprintKey(dataDir2) {
|
|
31730
32555
|
let raw;
|
|
31731
32556
|
try {
|
|
31732
|
-
raw =
|
|
32557
|
+
raw = readFileSync6(keyFilePath(dataDir2), "utf8");
|
|
31733
32558
|
} catch (err) {
|
|
31734
32559
|
if (err.code === "ENOENT") return null;
|
|
31735
32560
|
throw err instanceof Error ? err : new Error(String(err));
|
|
@@ -31751,144 +32576,18 @@ function fingerprintValue(key, raw) {
|
|
|
31751
32576
|
return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
|
|
31752
32577
|
}
|
|
31753
32578
|
|
|
31754
|
-
// ../../packages/persistence/src/history-
|
|
32579
|
+
// ../../packages/persistence/src/history-backfill.ts
|
|
31755
32580
|
import { existsSync as existsSync4 } from "fs";
|
|
31756
|
-
import { join as
|
|
31757
|
-
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
32581
|
+
import { join as join9 } from "path";
|
|
31758
32582
|
|
|
31759
|
-
// ../../packages/persistence/src/
|
|
31760
|
-
import {
|
|
31761
|
-
import {
|
|
31762
|
-
import {
|
|
31763
|
-
import { join as join7 } from "path";
|
|
31764
|
-
function defaultDataDir() {
|
|
31765
|
-
return join7(homedir(), ".aka");
|
|
31766
|
-
}
|
|
31767
|
-
function settingsDir(base = defaultDataDir()) {
|
|
31768
|
-
return join7(base, "settings");
|
|
31769
|
-
}
|
|
31770
|
-
function dataDir(base = defaultDataDir()) {
|
|
31771
|
-
return join7(base, "data");
|
|
31772
|
-
}
|
|
31773
|
-
function dbPath(base = defaultDataDir()) {
|
|
31774
|
-
return join7(dataDir(base), "aka.db");
|
|
31775
|
-
}
|
|
31776
|
-
function keysDir(base = defaultDataDir()) {
|
|
31777
|
-
return join7(base, "keys");
|
|
31778
|
-
}
|
|
31779
|
-
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
31780
|
-
ensureDataDirSync(dir);
|
|
31781
|
-
}
|
|
31782
|
-
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
31783
|
-
const moves = [
|
|
31784
|
-
{ name: "config.json", dest: settingsDir(base) },
|
|
31785
|
-
{ name: "policy-cache.json", dest: dataDir(base) }
|
|
31786
|
-
];
|
|
31787
|
-
for (const { name, dest } of moves) {
|
|
31788
|
-
try {
|
|
31789
|
-
ensureDataDirSync(dest);
|
|
31790
|
-
const moved = join7(dest, name);
|
|
31791
|
-
renameSync3(join7(base, name), moved);
|
|
31792
|
-
tightenFile(moved);
|
|
31793
|
-
} catch {
|
|
31794
|
-
}
|
|
31795
|
-
}
|
|
31796
|
-
}
|
|
31797
|
-
|
|
31798
|
-
// ../../packages/persistence/src/managed-settings.ts
|
|
31799
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
31800
|
-
import { posix, win32 } from "path";
|
|
31801
|
-
function managedSettingsPaths(platform2 = process.platform) {
|
|
31802
|
-
if (platform2 === "darwin") {
|
|
31803
|
-
return [
|
|
31804
|
-
posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
|
|
31805
|
-
posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
|
|
31806
|
-
];
|
|
31807
|
-
}
|
|
31808
|
-
if (platform2 === "win32") {
|
|
31809
|
-
return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
|
|
31810
|
-
}
|
|
31811
|
-
return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
|
|
31812
|
-
}
|
|
31813
|
-
function readManagedSettings(paths = managedSettingsPaths()) {
|
|
31814
|
-
for (const path of paths) {
|
|
31815
|
-
let text;
|
|
31816
|
-
try {
|
|
31817
|
-
text = readFileSync4(path, "utf8");
|
|
31818
|
-
} catch {
|
|
31819
|
-
continue;
|
|
31820
|
-
}
|
|
31821
|
-
const record2 = parseJsonObject(text);
|
|
31822
|
-
if (!record2) continue;
|
|
31823
|
-
const parsed = ManagedSettings.safeParse(record2);
|
|
31824
|
-
if (parsed.success) return parsed.data;
|
|
31825
|
-
}
|
|
31826
|
-
return null;
|
|
31827
|
-
}
|
|
31828
|
-
function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
|
|
31829
|
-
if (!managed) return settings;
|
|
31830
|
-
const { values } = managed;
|
|
31831
|
-
const merged = { ...settings };
|
|
31832
|
-
if (values.runMode !== void 0) merged.runMode = values.runMode;
|
|
31833
|
-
if (values.controlPlane !== void 0) {
|
|
31834
|
-
merged.controlPlane = {
|
|
31835
|
-
...values.controlPlane,
|
|
31836
|
-
// The administrator pinned WHICH deployment, not WHEN this machine
|
|
31837
|
-
// joined it. Keep the user's own attach time when the endpoint is
|
|
31838
|
-
// unchanged, so a managed machine does not appear to re-attach on every
|
|
31839
|
-
// read; stamp a fresh one when the administrator moved it.
|
|
31840
|
-
attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
|
|
31841
|
-
};
|
|
31842
|
-
}
|
|
31843
|
-
if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
|
|
31844
|
-
if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
|
|
31845
|
-
if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
|
|
31846
|
-
if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
|
|
31847
|
-
if (values.vaultConsent !== void 0) {
|
|
31848
|
-
merged.vaultConsent = values.vaultConsent ? (
|
|
31849
|
-
// Keep an existing valid grant so its acknowledgedAt survives; mint one
|
|
31850
|
-
// at the current version otherwise.
|
|
31851
|
-
settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
|
|
31852
|
-
) : void 0;
|
|
31853
|
-
}
|
|
31854
|
-
if (values.modelJudgeConsent !== void 0) {
|
|
31855
|
-
merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
|
|
31856
|
-
acknowledgedAt: now().toISOString(),
|
|
31857
|
-
payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
|
|
31858
|
-
} : void 0;
|
|
31859
|
-
}
|
|
31860
|
-
return merged;
|
|
31861
|
-
}
|
|
31862
|
-
|
|
31863
|
-
// ../../packages/persistence/src/settings.ts
|
|
31864
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
31865
|
-
import { join as join8 } from "path";
|
|
31866
|
-
var SETTINGS_FILENAME = "settings.json";
|
|
31867
|
-
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
31868
|
-
return overlayManagedSettings(readUserSettings(base), readManagedSettings());
|
|
31869
|
-
}
|
|
31870
|
-
function readUserSettings(base) {
|
|
31871
|
-
const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
|
|
31872
|
-
if (!record2) return defaultWorkspaceSettings();
|
|
31873
|
-
try {
|
|
31874
|
-
return WorkspaceSettings.parse(record2);
|
|
31875
|
-
} catch {
|
|
31876
|
-
return defaultWorkspaceSettings();
|
|
31877
|
-
}
|
|
31878
|
-
}
|
|
31879
|
-
function readJson(file2) {
|
|
31880
|
-
let text;
|
|
31881
|
-
try {
|
|
31882
|
-
text = readFileSync5(file2, "utf8");
|
|
31883
|
-
} catch {
|
|
31884
|
-
return null;
|
|
31885
|
-
}
|
|
31886
|
-
return parseJsonObject(text) ?? null;
|
|
31887
|
-
}
|
|
32583
|
+
// ../../packages/persistence/src/history-preview.ts
|
|
32584
|
+
import { existsSync as existsSync5 } from "fs";
|
|
32585
|
+
import { join as join10 } from "path";
|
|
32586
|
+
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
31888
32587
|
|
|
31889
32588
|
// ../../packages/persistence/src/store-symlinks.ts
|
|
31890
|
-
import { existsSync as
|
|
31891
|
-
import { dirname as
|
|
32589
|
+
import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
|
|
32590
|
+
import { dirname as dirname3, join as join11, resolve } from "path";
|
|
31892
32591
|
|
|
31893
32592
|
// ../../packages/persistence/src/vault/crypto.ts
|
|
31894
32593
|
import {
|
|
@@ -32001,8 +32700,8 @@ function formatPointer(category, keyVersion, pointerId, tag) {
|
|
|
32001
32700
|
// ../../packages/persistence/src/vault/key-provider.ts
|
|
32002
32701
|
import { execFileSync } from "child_process";
|
|
32003
32702
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
32004
|
-
import { chmodSync as chmodSync3, readFileSync as
|
|
32005
|
-
import { join as
|
|
32703
|
+
import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
32704
|
+
import { join as join12 } from "path";
|
|
32006
32705
|
var VAULT_OCCUPANT_REASON = {
|
|
32007
32706
|
symlink: "the path is a symlink; remove it so a keyring can be created",
|
|
32008
32707
|
gone: "the path was occupied but holds no keyring (removed while it was being created)",
|
|
@@ -32101,7 +32800,7 @@ function claimRotationLock(lock, owner) {
|
|
|
32101
32800
|
throw asError(err);
|
|
32102
32801
|
}
|
|
32103
32802
|
try {
|
|
32104
|
-
writeFileSync3(
|
|
32803
|
+
writeFileSync3(join12(lock, LOCK_OWNER_FILE), `${owner}
|
|
32105
32804
|
`, { mode: DATA_FILE_MODE });
|
|
32106
32805
|
return true;
|
|
32107
32806
|
} catch (err) {
|
|
@@ -32110,7 +32809,7 @@ function claimRotationLock(lock, owner) {
|
|
|
32110
32809
|
}
|
|
32111
32810
|
}
|
|
32112
32811
|
function acquireRotationLock(keysDir2) {
|
|
32113
|
-
const lock =
|
|
32812
|
+
const lock = join12(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
|
|
32114
32813
|
const owner = randomBytes2(16).toString("hex");
|
|
32115
32814
|
if (claimRotationLock(lock, owner)) return { lock, owner };
|
|
32116
32815
|
let held;
|
|
@@ -32137,7 +32836,7 @@ function acquireRotationLock(keysDir2) {
|
|
|
32137
32836
|
}
|
|
32138
32837
|
function releaseRotationLock(lease) {
|
|
32139
32838
|
try {
|
|
32140
|
-
if (
|
|
32839
|
+
if (readFileSync7(join12(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
|
|
32141
32840
|
} catch {
|
|
32142
32841
|
return;
|
|
32143
32842
|
}
|
|
@@ -32158,7 +32857,7 @@ var FileKeyProvider = class {
|
|
|
32158
32857
|
this.#keysDir = keysDir2;
|
|
32159
32858
|
}
|
|
32160
32859
|
get filePath() {
|
|
32161
|
-
return
|
|
32860
|
+
return join12(this.#keysDir, VAULT_KEY_FILENAME);
|
|
32162
32861
|
}
|
|
32163
32862
|
loadOrCreate() {
|
|
32164
32863
|
return asAsync(() => {
|
|
@@ -32188,7 +32887,7 @@ var FileKeyProvider = class {
|
|
|
32188
32887
|
#read() {
|
|
32189
32888
|
let raw;
|
|
32190
32889
|
try {
|
|
32191
|
-
raw =
|
|
32890
|
+
raw = readFileSync7(this.filePath, "utf8");
|
|
32192
32891
|
} catch (err) {
|
|
32193
32892
|
if (err.code === "ENOENT") return null;
|
|
32194
32893
|
throw err instanceof Error ? err : new Error(String(err));
|
|
@@ -32475,7 +33174,14 @@ var SecretVault = class {
|
|
|
32475
33174
|
const existing = this.#repo.byValueFingerprint(valueFingerprint);
|
|
32476
33175
|
const now = this.#now();
|
|
32477
33176
|
if (existing) {
|
|
32478
|
-
this.#repo.upsert(
|
|
33177
|
+
this.#repo.upsert(
|
|
33178
|
+
{
|
|
33179
|
+
...existing,
|
|
33180
|
+
provider: existing.provider ?? void 0,
|
|
33181
|
+
userAuthorized: meta4.userAuthorized === true
|
|
33182
|
+
},
|
|
33183
|
+
now
|
|
33184
|
+
);
|
|
32479
33185
|
return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
|
|
32480
33186
|
}
|
|
32481
33187
|
const { material, version: version2 } = await this.#keys.loadOrCreate();
|
|
@@ -32497,6 +33203,7 @@ var SecretVault = class {
|
|
|
32497
33203
|
ruleId: meta4.ruleId,
|
|
32498
33204
|
maskedMatch: meta4.maskedMatch,
|
|
32499
33205
|
provider: meta4.provider,
|
|
33206
|
+
userAuthorized: meta4.userAuthorized === true,
|
|
32500
33207
|
ciphertext: sealed.ciphertext.toString("base64"),
|
|
32501
33208
|
nonce: sealed.nonce.toString("base64"),
|
|
32502
33209
|
authTag: sealed.authTag.toString("base64")
|
|
@@ -32815,8 +33522,8 @@ var SecretVault = class {
|
|
|
32815
33522
|
};
|
|
32816
33523
|
|
|
32817
33524
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
32818
|
-
import { existsSync as
|
|
32819
|
-
import { join as
|
|
33525
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
33526
|
+
import { join as join13 } from "path";
|
|
32820
33527
|
|
|
32821
33528
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
32822
33529
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
@@ -32870,8 +33577,8 @@ function resolveProvider() {
|
|
|
32870
33577
|
function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
|
|
32871
33578
|
try {
|
|
32872
33579
|
ensureLayoutDirSync(base);
|
|
32873
|
-
const settingsFile =
|
|
32874
|
-
if (
|
|
33580
|
+
const settingsFile = join14(settingsDir(base), "settings.json");
|
|
33581
|
+
if (existsSync8(settingsFile)) tightenFile(settingsFile);
|
|
32875
33582
|
} catch {
|
|
32876
33583
|
}
|
|
32877
33584
|
migrateLegacyLayout(base);
|
|
@@ -32894,9 +33601,9 @@ function resolveProviderSafe(resolveProviderFn) {
|
|
|
32894
33601
|
}
|
|
32895
33602
|
|
|
32896
33603
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
32897
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
33604
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
|
|
32898
33605
|
import { homedir as homedir2 } from "os";
|
|
32899
|
-
import { basename as basename3, join as
|
|
33606
|
+
import { basename as basename3, join as join16 } from "path";
|
|
32900
33607
|
|
|
32901
33608
|
// ../../packages/detections/src/egress/registry.ts
|
|
32902
33609
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -35854,24 +36561,20 @@ function registerBundledPacks() {
|
|
|
35854
36561
|
}
|
|
35855
36562
|
|
|
35856
36563
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
35857
|
-
import { existsSync as
|
|
35858
|
-
import { basename as basename2, dirname as
|
|
36564
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
|
|
36565
|
+
import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
|
|
35859
36566
|
|
|
35860
36567
|
// ../../packages/plugin-sdk/src/events.ts
|
|
35861
|
-
import { createHash as
|
|
36568
|
+
import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
|
|
35862
36569
|
|
|
35863
36570
|
// ../../packages/plugin-sdk/src/isolated-scan.ts
|
|
35864
|
-
import { existsSync as
|
|
36571
|
+
import { existsSync as existsSync10 } from "fs";
|
|
35865
36572
|
import { fileURLToPath } from "url";
|
|
35866
36573
|
import { Worker } from "worker_threads";
|
|
35867
36574
|
|
|
35868
|
-
// ../../packages/plugin-sdk/src/
|
|
35869
|
-
|
|
35870
|
-
import {
|
|
35871
|
-
import { join as join15 } from "path";
|
|
35872
|
-
|
|
35873
|
-
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
35874
|
-
import { arch, hostname as hostname4, platform, release } from "os";
|
|
36575
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
36576
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
36577
|
+
import { join as join18 } from "path";
|
|
35875
36578
|
|
|
35876
36579
|
// ../../packages/plugin-sdk/src/model-governance.ts
|
|
35877
36580
|
import {
|
|
@@ -35883,20 +36586,46 @@ import {
|
|
|
35883
36586
|
readSync,
|
|
35884
36587
|
writeFileSync as writeFileSync5
|
|
35885
36588
|
} from "fs";
|
|
35886
|
-
import { join as
|
|
36589
|
+
import { join as join17 } from "path";
|
|
35887
36590
|
var TAIL_BYTES = 256 * 1024;
|
|
35888
36591
|
|
|
36592
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
36593
|
+
var HOST_FEATURE = {
|
|
36594
|
+
ModelSwitch: "model-switch",
|
|
36595
|
+
VaultPointerDisplay: "vault-pointer-display"
|
|
36596
|
+
};
|
|
36597
|
+
var HOST_FLOORS = {
|
|
36598
|
+
[HOST_FEATURE.ModelSwitch]: {
|
|
36599
|
+
label: "model-switch protection",
|
|
36600
|
+
hookEvents: ["PreModelSwitch", "PostModelSwitch"],
|
|
36601
|
+
since: "2.1.251"
|
|
36602
|
+
},
|
|
36603
|
+
[HOST_FEATURE.VaultPointerDisplay]: {
|
|
36604
|
+
label: "vault pointer display",
|
|
36605
|
+
hookEvents: ["MessageDisplay"],
|
|
36606
|
+
since: "2.1.152"
|
|
36607
|
+
}
|
|
36608
|
+
};
|
|
36609
|
+
|
|
36610
|
+
// ../../packages/plugin-sdk/src/ignore-layers.ts
|
|
36611
|
+
var import_ignore = __toESM(require_ignore(), 1);
|
|
36612
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
36613
|
+
import { join as join19 } from "path";
|
|
36614
|
+
|
|
36615
|
+
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
36616
|
+
import { arch, hostname as hostname4, platform, release } from "os";
|
|
36617
|
+
|
|
35889
36618
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
35890
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
35891
|
-
import { join as
|
|
36619
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
|
|
36620
|
+
import { join as join20 } from "path";
|
|
35892
36621
|
|
|
35893
36622
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
35894
36623
|
import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
|
|
35895
|
-
import { basename as basename4, dirname as
|
|
36624
|
+
import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
|
|
35896
36625
|
|
|
35897
36626
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
35898
|
-
import { existsSync as
|
|
35899
|
-
import { basename as basename5, join as
|
|
36627
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
|
|
36628
|
+
import { basename as basename5, join as join21 } from "path";
|
|
35900
36629
|
|
|
35901
36630
|
// ../../packages/plugin-sdk/src/provider-env-antigravity.ts
|
|
35902
36631
|
var optionalBaseUrl2 = external_exports.preprocess((v) => {
|
|
@@ -35932,7 +36661,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
|
35932
36661
|
|
|
35933
36662
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
35934
36663
|
import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
35935
|
-
import { join as
|
|
36664
|
+
import { join as join22 } from "path";
|
|
35936
36665
|
|
|
35937
36666
|
// ../../packages/plugin-sdk/src/tokenize.ts
|
|
35938
36667
|
function redactedPlaceholder(category) {
|
|
@@ -35994,14 +36723,26 @@ var SecretVaultGlue = class {
|
|
|
35994
36723
|
}
|
|
35995
36724
|
async tokenizeText(text, opts) {
|
|
35996
36725
|
try {
|
|
35997
|
-
const
|
|
35998
|
-
const
|
|
35999
|
-
const
|
|
36000
|
-
if (
|
|
36001
|
-
|
|
36726
|
+
const supplied = opts?.findings;
|
|
36727
|
+
const resolver = opts?.resolver;
|
|
36728
|
+
const scanned = supplied ?? this.#selfScan(text);
|
|
36729
|
+
if (scanned === null) {
|
|
36730
|
+
return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
|
|
36731
|
+
}
|
|
36732
|
+
const findings = supplied === void 0 && resolver !== void 0 ? scanned.filter(
|
|
36733
|
+
(f) => isActionAtLeast(resolver.actionFor(f.ruleId, f.category), "redact")
|
|
36734
|
+
) : scanned;
|
|
36735
|
+
let reversible = opts?.reversible;
|
|
36736
|
+
if (resolver !== void 0 && reversible === void 0) {
|
|
36737
|
+
reversible = new Set(findings.filter((f) => resolver.isReversible(f.ruleId)));
|
|
36738
|
+
}
|
|
36739
|
+
const reversibleSet = reversible;
|
|
36740
|
+
const keeps = (finding) => reversibleSet === void 0 || reversibleSet.has(finding);
|
|
36741
|
+
if (findings.length === 0) return { text, pointers: [], degraded: [], redacted: [] };
|
|
36002
36742
|
const groups = groupSpans(text, findings);
|
|
36003
36743
|
const pointers = [];
|
|
36004
36744
|
const degraded = [];
|
|
36745
|
+
const redacted = [];
|
|
36005
36746
|
let out = text;
|
|
36006
36747
|
for (const group of [...groups].reverse()) {
|
|
36007
36748
|
const original = text.slice(group.start, group.end);
|
|
@@ -36015,6 +36756,7 @@ var SecretVaultGlue = class {
|
|
|
36015
36756
|
degraded.unshift({ category: group.category });
|
|
36016
36757
|
} else if (!keeps(finding)) {
|
|
36017
36758
|
replacement = redactedPlaceholder(finding.category);
|
|
36759
|
+
redacted.unshift({ category: finding.category });
|
|
36018
36760
|
} else {
|
|
36019
36761
|
replacement = await this.tokenizeValue(finding.rawMatch, {
|
|
36020
36762
|
ruleId: finding.ruleId,
|
|
@@ -36035,9 +36777,9 @@ var SecretVaultGlue = class {
|
|
|
36035
36777
|
}
|
|
36036
36778
|
}
|
|
36037
36779
|
}
|
|
36038
|
-
return { text: out, pointers, degraded };
|
|
36780
|
+
return { text: out, pointers, degraded, redacted };
|
|
36039
36781
|
} catch {
|
|
36040
|
-
return { text: "[REDACTED]", pointers: [], degraded: [] };
|
|
36782
|
+
return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
|
|
36041
36783
|
}
|
|
36042
36784
|
}
|
|
36043
36785
|
async detokenizeText(text, opts) {
|
|
@@ -36249,13 +36991,13 @@ function describePointerSafe(token) {
|
|
|
36249
36991
|
import {
|
|
36250
36992
|
mkdirSync as mkdirSync5,
|
|
36251
36993
|
readdirSync as readdirSync5,
|
|
36252
|
-
readFileSync as
|
|
36994
|
+
readFileSync as readFileSync14,
|
|
36253
36995
|
renameSync as renameSync5,
|
|
36254
36996
|
rmSync as rmSync7,
|
|
36255
36997
|
statSync as statSync9,
|
|
36256
36998
|
writeFileSync as writeFileSync8
|
|
36257
36999
|
} from "fs";
|
|
36258
|
-
import { dirname as
|
|
37000
|
+
import { dirname as dirname6, join as join23 } from "path";
|
|
36259
37001
|
var EMPTY_CARRY = Object.freeze({
|
|
36260
37002
|
tail: "",
|
|
36261
37003
|
fence: null,
|
|
@@ -36452,7 +37194,7 @@ var CARRY_FILE_PREFIX = "display-carry";
|
|
|
36452
37194
|
var STALE_CARRY_MS = 15 * 60 * 1e3;
|
|
36453
37195
|
function carryFilePath(dataDir2, sessionId) {
|
|
36454
37196
|
const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
|
|
36455
|
-
return
|
|
37197
|
+
return join23(dataDir2, `${CARRY_FILE_PREFIX}-${safe === "" ? "session" : safe}.json`);
|
|
36456
37198
|
}
|
|
36457
37199
|
function parseFence(value) {
|
|
36458
37200
|
if (typeof value !== "object" || value === null) return null;
|
|
@@ -36463,7 +37205,7 @@ function parseFence(value) {
|
|
|
36463
37205
|
}
|
|
36464
37206
|
function loadCarry(file2, keys) {
|
|
36465
37207
|
try {
|
|
36466
|
-
const parsed = JSON.parse(
|
|
37208
|
+
const parsed = JSON.parse(readFileSync14(file2, "utf8"));
|
|
36467
37209
|
if (typeof parsed !== "object" || parsed === null) return EMPTY_CARRY;
|
|
36468
37210
|
const record2 = parsed;
|
|
36469
37211
|
const revealedCount = record2.messageKey === keys.messageKey && typeof record2.revealedCount === "number" && Number.isFinite(record2.revealedCount) ? record2.revealedCount : 0;
|
|
@@ -36516,7 +37258,7 @@ function removeStaleCarryFiles(dir, keep) {
|
|
|
36516
37258
|
const cutoff = Date.now() - STALE_CARRY_MS;
|
|
36517
37259
|
for (const name of readdirSync5(dir)) {
|
|
36518
37260
|
if (!name.startsWith(CARRY_FILE_PREFIX) || !name.endsWith(".json")) continue;
|
|
36519
|
-
const path =
|
|
37261
|
+
const path = join23(dir, name);
|
|
36520
37262
|
if (path === keep) continue;
|
|
36521
37263
|
try {
|
|
36522
37264
|
if (statSync9(path).mtimeMs < cutoff) rmSync7(path, { force: true });
|
|
@@ -36528,7 +37270,7 @@ function removeStaleCarryFiles(dir, keep) {
|
|
|
36528
37270
|
}
|
|
36529
37271
|
function saveCarry(file2, keys, carry) {
|
|
36530
37272
|
try {
|
|
36531
|
-
const dir =
|
|
37273
|
+
const dir = dirname6(file2);
|
|
36532
37274
|
mkdirSync5(dir, { recursive: true });
|
|
36533
37275
|
removeStaleCarryFiles(dir, file2);
|
|
36534
37276
|
writeCarryRecord(file2, keys.blockKey, keys.messageKey, carry);
|
|
@@ -36538,7 +37280,7 @@ function saveCarry(file2, keys, carry) {
|
|
|
36538
37280
|
function finalizeCarry(file2, keys, carry) {
|
|
36539
37281
|
try {
|
|
36540
37282
|
if (carry.revealedCount > 0) {
|
|
36541
|
-
mkdirSync5(
|
|
37283
|
+
mkdirSync5(dirname6(file2), { recursive: true });
|
|
36542
37284
|
writeCarryRecord(file2, null, keys.messageKey, {
|
|
36543
37285
|
...EMPTY_CARRY,
|
|
36544
37286
|
revealedCount: carry.revealedCount
|
|
@@ -36547,7 +37289,7 @@ function finalizeCarry(file2, keys, carry) {
|
|
|
36547
37289
|
}
|
|
36548
37290
|
let owned = true;
|
|
36549
37291
|
try {
|
|
36550
|
-
const parsed = JSON.parse(
|
|
37292
|
+
const parsed = JSON.parse(readFileSync14(file2, "utf8"));
|
|
36551
37293
|
if (typeof parsed === "object" && parsed !== null) {
|
|
36552
37294
|
const record2 = parsed;
|
|
36553
37295
|
owned = record2.blockKey === keys.blockKey || record2.messageKey === keys.messageKey;
|