@akasecurity/ai-tc-claude-code 0.9.0 → 0.9.1

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.
@@ -545,6 +545,10 @@ var SQLITE_MIGRATIONS = [
545
545
  {
546
546
  tag: "0010_events_session_expression_index",
547
547
  sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
548
+ },
549
+ {
550
+ tag: "0011_egress_writer",
551
+ sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
548
552
  }
549
553
  ];
550
554
 
@@ -16203,6 +16207,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16203
16207
 
16204
16208
  // ../../packages/schema/src/zod/rule.ts
16205
16209
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16210
+ var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
16206
16211
  var KeywordMatcher = external_exports.object({
16207
16212
  type: external_exports.literal("keyword"),
16208
16213
  // An empty keyword matches at every position, yielding one zero-length span
@@ -16227,9 +16232,10 @@ function matchesEmptyString(pattern, flags) {
16227
16232
  return false;
16228
16233
  }
16229
16234
  }
16235
+ var MAX_PATTERN_LENGTH = 2e3;
16230
16236
  var RegexMatcher = external_exports.object({
16231
16237
  type: external_exports.literal("regex"),
16232
- pattern: external_exports.string(),
16238
+ pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
16233
16239
  flags: external_exports.string().default("gi"),
16234
16240
  captureGroup: external_exports.number().int().nonnegative().optional()
16235
16241
  }).refine((v) => isValidRegex(v.pattern, v.flags), {
@@ -16796,6 +16802,212 @@ function buildDetectionsList(summaries, query) {
16796
16802
  return { counts, items: filtered.map(summaryToDetectionListItem) };
16797
16803
  }
16798
16804
 
16805
+ // ../../packages/schema/src/zod/shares.ts
16806
+ var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
16807
+ var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
16808
+ var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
16809
+ var DATA_CLASS_ORDER = DataClass.options;
16810
+ var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
16811
+ var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
16812
+ var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
16813
+ var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
16814
+ var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
16815
+ var ReviewInfo = external_exports.object({
16816
+ needsReview: external_exports.boolean(),
16817
+ reasons: external_exports.array(ReviewReason)
16818
+ }).meta({ id: "ReviewInfo" });
16819
+ var DestinationNetwork = external_exports.object({
16820
+ port: external_exports.number().int().nullable(),
16821
+ geo: external_exports.string().nullable(),
16822
+ ptr: external_exports.string().nullable()
16823
+ }).meta({ id: "DestinationNetwork" });
16824
+ var EndpointSummary = external_exports.object({
16825
+ id: external_exports.string(),
16826
+ method: HttpMethod,
16827
+ transport: Transport,
16828
+ url: external_exports.string(),
16829
+ template: external_exports.boolean(),
16830
+ dataClass: DataClass,
16831
+ lastSeen: external_exports.iso.datetime(),
16832
+ callSiteCount: external_exports.number().int().nonnegative()
16833
+ }).meta({ id: "EndpointSummary" });
16834
+ var CallSite = external_exports.object({
16835
+ id: external_exports.string(),
16836
+ project: external_exports.string(),
16837
+ file: external_exports.string(),
16838
+ line: external_exports.number().int().nonnegative(),
16839
+ snippet: external_exports.string(),
16840
+ dynamic: external_exports.boolean(),
16841
+ vendored: external_exports.boolean(),
16842
+ /** Deep-link to the Inventory project, when the repo is governed there. */
16843
+ projectId: external_exports.string().nullable()
16844
+ }).meta({ id: "CallSite" });
16845
+ var EndpointWithSites = EndpointSummary.extend({
16846
+ sites: external_exports.array(CallSite)
16847
+ }).meta({ id: "EndpointWithSites" });
16848
+ var ShareDestinationSummary = external_exports.object({
16849
+ id: external_exports.string(),
16850
+ kind: DestinationKind,
16851
+ name: external_exports.string(),
16852
+ host: external_exports.string(),
16853
+ category: external_exports.string(),
16854
+ trust: ShareTrustLevel,
16855
+ /** Effective state (decision applied over the trust default). */
16856
+ status: EgressStatus,
16857
+ /** True when an egress decision override differs from the trust default. */
16858
+ isCustom: external_exports.boolean(),
16859
+ lastSeen: external_exports.iso.datetime(),
16860
+ endpointCount: external_exports.number().int().nonnegative(),
16861
+ callSiteCount: external_exports.number().int().nonnegative(),
16862
+ transports: external_exports.array(Transport),
16863
+ /** Most-sensitive first. */
16864
+ dataClasses: external_exports.array(DataClass),
16865
+ review: ReviewInfo,
16866
+ /** Non-provider hosts only; null for providers. */
16867
+ network: DestinationNetwork.nullable(),
16868
+ /** Embedded for inline expansion — no call sites here. */
16869
+ endpoints: external_exports.array(EndpointSummary)
16870
+ }).meta({ id: "ShareDestinationSummary" });
16871
+ var ShareDestinationDetail = ShareDestinationSummary.omit({
16872
+ endpointCount: true,
16873
+ callSiteCount: true,
16874
+ endpoints: true
16875
+ }).extend({
16876
+ /** Ownership/geo rationale; null for providers. */
16877
+ note: external_exports.string().nullable(),
16878
+ endpoints: external_exports.array(EndpointWithSites)
16879
+ }).meta({ id: "ShareDestinationDetail" });
16880
+ var ReviewDestination = external_exports.object({
16881
+ id: external_exports.string(),
16882
+ kind: DestinationKind,
16883
+ name: external_exports.string(),
16884
+ /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
16885
+ host: external_exports.string(),
16886
+ trust: ShareTrustLevel,
16887
+ status: EgressStatus,
16888
+ review: ReviewInfo,
16889
+ topDataClass: DataClass,
16890
+ callSiteCount: external_exports.number().int().nonnegative(),
16891
+ lastSeen: external_exports.iso.datetime()
16892
+ }).meta({ id: "ReviewDestination" });
16893
+ var ShareDestinationGroup = external_exports.object({
16894
+ kind: DestinationKind,
16895
+ total: external_exports.number().int().nonnegative(),
16896
+ items: external_exports.array(ShareDestinationSummary)
16897
+ }).meta({ id: "ShareDestinationGroup" });
16898
+ var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
16899
+ var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
16900
+ var SharesStats = external_exports.object({
16901
+ destinations: external_exports.number().int().nonnegative(),
16902
+ endpoints: external_exports.number().int().nonnegative(),
16903
+ callSites: external_exports.number().int().nonnegative(),
16904
+ needsReview: external_exports.number().int().nonnegative(),
16905
+ insecure: external_exports.number().int().nonnegative(),
16906
+ byKind: external_exports.object({
16907
+ provider: external_exports.number().int().nonnegative(),
16908
+ internal: external_exports.number().int().nonnegative(),
16909
+ external: external_exports.number().int().nonnegative(),
16910
+ ip: external_exports.number().int().nonnegative()
16911
+ }),
16912
+ byTrust: external_exports.object({
16913
+ recognized: external_exports.number().int().nonnegative(),
16914
+ internal: external_exports.number().int().nonnegative(),
16915
+ unverified: external_exports.number().int().nonnegative(),
16916
+ ip: external_exports.number().int().nonnegative()
16917
+ })
16918
+ }).meta({ id: "SharesStats" });
16919
+ var SetEgressDecisionBody = external_exports.object({
16920
+ /** `null` clears the override — reverts to the trust default, isCustom false. */
16921
+ decision: EgressDecision.nullable()
16922
+ }).meta({ id: "SetEgressDecisionBody" });
16923
+ var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
16924
+ var ListShareDestinationsQuery = external_exports.object({
16925
+ /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
16926
+ q: external_exports.string().optional(),
16927
+ /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
16928
+ kind: external_exports.array(DestinationKind).optional(),
16929
+ /** Reserved for future grouping modes; only 'destination' is supported today. */
16930
+ groupBy: external_exports.enum(["destination"]).default("destination"),
16931
+ /**
16932
+ * When true, return a flat severity-ordered `items[]` instead of `groups`.
16933
+ * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
16934
+ * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
16935
+ * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
16936
+ */
16937
+ review: external_exports.stringbool().default(false)
16938
+ });
16939
+ var ExportSharesQuery = external_exports.object({
16940
+ format: external_exports.enum(["csv", "json"]).default("csv"),
16941
+ q: external_exports.string().optional(),
16942
+ kind: external_exports.array(DestinationKind).optional()
16943
+ });
16944
+
16945
+ // ../../packages/schema/src/zod/egress-extraction.ts
16946
+ var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
16947
+ var ProviderRegistryEntry = external_exports.object({
16948
+ id: external_exports.string(),
16949
+ name: external_exports.string(),
16950
+ category: external_exports.string(),
16951
+ /** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
16952
+ hostSuffixes: external_exports.array(external_exports.string()).min(1),
16953
+ /** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
16954
+ apiBase: external_exports.string(),
16955
+ /** Most-sensitive first; index 0 becomes the endpoint dataClass. */
16956
+ defaultDataClasses: external_exports.array(DataClass).min(1),
16957
+ /** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
16958
+ sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
16959
+ }).meta({ id: "ProviderRegistryEntry" });
16960
+ var EgressCallSiteHit = external_exports.object({
16961
+ file: external_exports.string(),
16962
+ line: external_exports.number().int().positive(),
16963
+ snippet: external_exports.string(),
16964
+ dynamic: external_exports.boolean(),
16965
+ vendored: external_exports.boolean()
16966
+ }).meta({ id: "EgressCallSiteHit" });
16967
+ var ResolvedEgressHit = external_exports.object({
16968
+ host: external_exports.string(),
16969
+ kind: DestinationKind,
16970
+ name: external_exports.string(),
16971
+ category: external_exports.string(),
16972
+ trust: ShareTrustLevel,
16973
+ network: DestinationNetwork.nullable(),
16974
+ method: HttpMethod,
16975
+ transport: Transport,
16976
+ url: external_exports.string(),
16977
+ template: external_exports.boolean(),
16978
+ dataClass: DataClass,
16979
+ site: EgressCallSiteHit
16980
+ }).meta({ id: "ResolvedEgressHit" });
16981
+ var EgressReconcile = external_exports.discriminatedUnion("mode", [
16982
+ external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
16983
+ external_exports.object({
16984
+ mode: external_exports.literal("ledger"),
16985
+ scannedFiles: external_exports.array(external_exports.string()),
16986
+ deletedFiles: external_exports.array(external_exports.string())
16987
+ })
16988
+ ]).meta({ id: "EgressReconcile" });
16989
+ var RecordProjectEgressInput = external_exports.object({
16990
+ /** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
16991
+ projectKey: external_exports.string().min(1),
16992
+ /** Display name only — never keys reconciliation. */
16993
+ project: external_exports.string(),
16994
+ projectId: external_exports.string().nullable(),
16995
+ reconcile: EgressReconcile,
16996
+ hits: external_exports.array(ResolvedEgressHit)
16997
+ }).meta({ id: "RecordProjectEgressInput" });
16998
+ var EgressWriteSummary = external_exports.object({
16999
+ destinations: external_exports.number().int().nonnegative(),
17000
+ endpoints: external_exports.number().int().nonnegative(),
17001
+ callSites: external_exports.number().int().nonnegative(),
17002
+ truncated: external_exports.boolean(),
17003
+ /**
17004
+ * Files the cap dropped whole. Their stored rows were left untouched, so a
17005
+ * ledger-keeping caller must withhold their ledger entries and read them
17006
+ * again next scan.
17007
+ */
17008
+ droppedFiles: external_exports.array(external_exports.string()).default([])
17009
+ }).meta({ id: "EgressWriteSummary" });
17010
+
16799
17011
  // ../../packages/schema/src/zod/findings-group-build.ts
16800
17012
  function toApiAction(dbVal) {
16801
17013
  const map2 = {
@@ -17057,7 +17269,7 @@ var PatchInstalledPackRequest = external_exports.object({
17057
17269
  }).meta({ id: "PatchInstalledPackRequest" });
17058
17270
 
17059
17271
  // ../../packages/schema/src/zod/local.ts
17060
- var WORKSPACE_SETTINGS_SPEC_VERSION = 2;
17272
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 3;
17061
17273
  var RunMode = external_exports.enum(["standalone"]);
17062
17274
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17063
17275
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17072,6 +17284,9 @@ var WorkspaceSettings = external_exports.object({
17072
17284
  policy: SimpleDetectionPolicy.default("redact"),
17073
17285
  // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
17074
17286
  historicalAccess: HistoricalAccess.default("session-only"),
17287
+ // In-place egress extraction on the scan paths; disable to stop all Data
17288
+ // Shares writes.
17289
+ dataSharesInPlace: external_exports.boolean().default(true),
17075
17290
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17076
17291
  onboardedAt: external_exports.iso.datetime().optional()
17077
17292
  });
@@ -17555,145 +17770,6 @@ var SetupHandoffOffer = external_exports.object({
17555
17770
  path: ["liveKeys"]
17556
17771
  });
17557
17772
 
17558
- // ../../packages/schema/src/zod/shares.ts
17559
- var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17560
- var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
17561
- var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
17562
- var DATA_CLASS_ORDER = DataClass.options;
17563
- var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
17564
- var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
17565
- var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
17566
- var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
17567
- var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
17568
- var ReviewInfo = external_exports.object({
17569
- needsReview: external_exports.boolean(),
17570
- reasons: external_exports.array(ReviewReason)
17571
- }).meta({ id: "ReviewInfo" });
17572
- var DestinationNetwork = external_exports.object({
17573
- port: external_exports.number().int().nullable(),
17574
- geo: external_exports.string().nullable(),
17575
- ptr: external_exports.string().nullable()
17576
- }).meta({ id: "DestinationNetwork" });
17577
- var EndpointSummary = external_exports.object({
17578
- id: external_exports.string(),
17579
- method: HttpMethod,
17580
- transport: Transport,
17581
- url: external_exports.string(),
17582
- template: external_exports.boolean(),
17583
- dataClass: DataClass,
17584
- lastSeen: external_exports.iso.datetime(),
17585
- callSiteCount: external_exports.number().int().nonnegative()
17586
- }).meta({ id: "EndpointSummary" });
17587
- var CallSite = external_exports.object({
17588
- id: external_exports.string(),
17589
- project: external_exports.string(),
17590
- file: external_exports.string(),
17591
- line: external_exports.number().int().nonnegative(),
17592
- snippet: external_exports.string(),
17593
- dynamic: external_exports.boolean(),
17594
- vendored: external_exports.boolean(),
17595
- /** Deep-link to the Inventory project, when the repo is governed there. */
17596
- projectId: external_exports.string().nullable()
17597
- }).meta({ id: "CallSite" });
17598
- var EndpointWithSites = EndpointSummary.extend({
17599
- sites: external_exports.array(CallSite)
17600
- }).meta({ id: "EndpointWithSites" });
17601
- var ShareDestinationSummary = external_exports.object({
17602
- id: external_exports.string(),
17603
- kind: DestinationKind,
17604
- name: external_exports.string(),
17605
- host: external_exports.string(),
17606
- category: external_exports.string(),
17607
- trust: ShareTrustLevel,
17608
- /** Effective state (decision applied over the trust default). */
17609
- status: EgressStatus,
17610
- /** True when an egress decision override differs from the trust default. */
17611
- isCustom: external_exports.boolean(),
17612
- lastSeen: external_exports.iso.datetime(),
17613
- endpointCount: external_exports.number().int().nonnegative(),
17614
- callSiteCount: external_exports.number().int().nonnegative(),
17615
- transports: external_exports.array(Transport),
17616
- /** Most-sensitive first. */
17617
- dataClasses: external_exports.array(DataClass),
17618
- review: ReviewInfo,
17619
- /** Non-provider hosts only; null for providers. */
17620
- network: DestinationNetwork.nullable(),
17621
- /** Embedded for inline expansion — no call sites here. */
17622
- endpoints: external_exports.array(EndpointSummary)
17623
- }).meta({ id: "ShareDestinationSummary" });
17624
- var ShareDestinationDetail = ShareDestinationSummary.omit({
17625
- endpointCount: true,
17626
- callSiteCount: true,
17627
- endpoints: true
17628
- }).extend({
17629
- /** Ownership/geo rationale; null for providers. */
17630
- note: external_exports.string().nullable(),
17631
- endpoints: external_exports.array(EndpointWithSites)
17632
- }).meta({ id: "ShareDestinationDetail" });
17633
- var ReviewDestination = external_exports.object({
17634
- id: external_exports.string(),
17635
- kind: DestinationKind,
17636
- name: external_exports.string(),
17637
- /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
17638
- host: external_exports.string(),
17639
- trust: ShareTrustLevel,
17640
- status: EgressStatus,
17641
- review: ReviewInfo,
17642
- topDataClass: DataClass,
17643
- callSiteCount: external_exports.number().int().nonnegative(),
17644
- lastSeen: external_exports.iso.datetime()
17645
- }).meta({ id: "ReviewDestination" });
17646
- var ShareDestinationGroup = external_exports.object({
17647
- kind: DestinationKind,
17648
- total: external_exports.number().int().nonnegative(),
17649
- items: external_exports.array(ShareDestinationSummary)
17650
- }).meta({ id: "ShareDestinationGroup" });
17651
- var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
17652
- var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
17653
- var SharesStats = external_exports.object({
17654
- destinations: external_exports.number().int().nonnegative(),
17655
- endpoints: external_exports.number().int().nonnegative(),
17656
- callSites: external_exports.number().int().nonnegative(),
17657
- needsReview: external_exports.number().int().nonnegative(),
17658
- insecure: external_exports.number().int().nonnegative(),
17659
- byKind: external_exports.object({
17660
- provider: external_exports.number().int().nonnegative(),
17661
- internal: external_exports.number().int().nonnegative(),
17662
- ip: external_exports.number().int().nonnegative()
17663
- }),
17664
- byTrust: external_exports.object({
17665
- recognized: external_exports.number().int().nonnegative(),
17666
- internal: external_exports.number().int().nonnegative(),
17667
- unverified: external_exports.number().int().nonnegative(),
17668
- ip: external_exports.number().int().nonnegative()
17669
- })
17670
- }).meta({ id: "SharesStats" });
17671
- var SetEgressDecisionBody = external_exports.object({
17672
- /** `null` clears the override — reverts to the trust default, isCustom false. */
17673
- decision: EgressDecision.nullable()
17674
- }).meta({ id: "SetEgressDecisionBody" });
17675
- var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
17676
- var ListShareDestinationsQuery = external_exports.object({
17677
- /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
17678
- q: external_exports.string().optional(),
17679
- /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
17680
- kind: external_exports.array(DestinationKind).optional(),
17681
- /** Reserved for future grouping modes; only 'destination' is supported today. */
17682
- groupBy: external_exports.enum(["destination"]).default("destination"),
17683
- /**
17684
- * When true, return a flat severity-ordered `items[]` instead of `groups`.
17685
- * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
17686
- * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
17687
- * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17688
- */
17689
- review: external_exports.stringbool().default(false)
17690
- });
17691
- var ExportSharesQuery = external_exports.object({
17692
- format: external_exports.enum(["csv", "json"]).default("csv"),
17693
- q: external_exports.string().optional(),
17694
- kind: external_exports.array(DestinationKind).optional()
17695
- });
17696
-
17697
17773
  // ../../packages/schema/src/zod/shares-access.ts
17698
17774
  var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
17699
17775
  function trustDefaultStatus(trust) {
@@ -17713,7 +17789,7 @@ function deriveReviewReasons(trust, transports) {
17713
17789
  const reasons = [];
17714
17790
  if (trust === "ip") reasons.push("raw_ip");
17715
17791
  if (trust === "unverified") reasons.push("unverified_domain");
17716
- if (transports.includes("http")) reasons.push("plaintext_transport");
17792
+ if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
17717
17793
  return reasons;
17718
17794
  }
17719
17795
  function buildReviewInfo(trust, transports) {
@@ -17944,6 +18020,7 @@ function applyMigrations(db) {
17944
18020
  ensureSyncedAtColumn(db, "audit_events");
17945
18021
  ensureScanLedgerTable(db);
17946
18022
  ensureBlockedDetectionsTable(db);
18023
+ ensureRuleProbeCacheTable(db);
17947
18024
  ensureWriteGateTrigger(db);
17948
18025
  ensureTokenUsageColumns(db);
17949
18026
  reconcileSourceProjectIds(db);
@@ -18083,6 +18160,14 @@ function ensureBlockedDetectionsTable(db) {
18083
18160
  blocked_at INTEGER NOT NULL
18084
18161
  )`);
18085
18162
  }
18163
+ function ensureRuleProbeCacheTable(db) {
18164
+ db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
18165
+ rule_key TEXT PRIMARY KEY,
18166
+ verdict TEXT NOT NULL,
18167
+ worst_probe_ms REAL NOT NULL,
18168
+ checked_at INTEGER NOT NULL
18169
+ )`);
18170
+ }
18086
18171
 
18087
18172
  // ../../packages/persistence/src/paths.ts
18088
18173
  import { chmodSync, mkdirSync } from "fs";
@@ -21636,6 +21721,35 @@ var SqliteResolutionsRepository = class {
21636
21721
  }
21637
21722
  };
21638
21723
 
21724
+ // ../../packages/persistence/src/repositories/rule-probe-cache.ts
21725
+ var SqliteRuleProbeCacheRepository = class {
21726
+ constructor(db) {
21727
+ this.db = db;
21728
+ this.upsertStmt = db.prepare(
21729
+ `INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
21730
+ VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
21731
+ ON CONFLICT (rule_key) DO UPDATE SET
21732
+ verdict = excluded.verdict,
21733
+ worst_probe_ms = excluded.worst_probe_ms,
21734
+ checked_at = excluded.checked_at`
21735
+ );
21736
+ this.readStmt = db.prepare(
21737
+ `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
21738
+ );
21739
+ }
21740
+ db;
21741
+ upsertStmt;
21742
+ readStmt;
21743
+ getVerdict(ruleKey) {
21744
+ return getRow(this.readStmt, { ruleKey });
21745
+ }
21746
+ setVerdict(ruleKey, verdict, worstProbeMs2) {
21747
+ failOpenTransaction(this.db, () => {
21748
+ this.upsertStmt.run({ ruleKey, verdict, worstProbeMs: worstProbeMs2, checkedAt: Date.now() });
21749
+ });
21750
+ }
21751
+ };
21752
+
21639
21753
  // ../../packages/persistence/src/repositories/scan-ledger.ts
21640
21754
  var SqliteScanLedgerRepository = class {
21641
21755
  constructor(db) {
@@ -22047,11 +22161,50 @@ var SqliteSecurityRepository = class {
22047
22161
 
22048
22162
  // ../../packages/persistence/src/repositories/shares.ts
22049
22163
  import { randomUUID as randomUUID7 } from "crypto";
22050
- var KIND_ORDER = ["provider", "internal", "ip"];
22164
+ var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22165
+ var IN_CHUNK = 500;
22166
+ var KIND_ORDER = ["provider", "internal", "external", "ip"];
22167
+ var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
22168
+ var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
22169
+ LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
22051
22170
  var CALL_SITE_EMBED_CAP = 200;
22052
22171
  function parseNetwork(networkJson) {
22053
22172
  return safeJson(networkJson, null);
22054
22173
  }
22174
+ function capHits(all, mode) {
22175
+ if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
22176
+ return { hits: [...all], droppedFiles: [], truncated: false };
22177
+ }
22178
+ if (mode === "walk") {
22179
+ return {
22180
+ hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
22181
+ droppedFiles: [],
22182
+ truncated: true
22183
+ };
22184
+ }
22185
+ const byFile = /* @__PURE__ */ new Map();
22186
+ for (const hit of all) {
22187
+ const bucket = byFile.get(hit.site.file);
22188
+ if (bucket === void 0) byFile.set(hit.site.file, [hit]);
22189
+ else bucket.push(hit);
22190
+ }
22191
+ const hits = [];
22192
+ const droppedFiles = [];
22193
+ for (const [file2, bucket] of byFile) {
22194
+ if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
22195
+ else hits.push(...bucket);
22196
+ }
22197
+ return { hits, droppedFiles, truncated: true };
22198
+ }
22199
+ function withoutDroppedFiles(reconcile, droppedFiles) {
22200
+ if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
22201
+ const dropped = new Set(droppedFiles);
22202
+ return {
22203
+ mode: "ledger",
22204
+ scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
22205
+ deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
22206
+ };
22207
+ }
22055
22208
  function toEndpointSummary(row) {
22056
22209
  return {
22057
22210
  id: row.id,
@@ -22142,13 +22295,15 @@ var SqliteSharesRepository = class {
22142
22295
  const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22143
22296
  const insecure = countScalar(
22144
22297
  this.db,
22145
- "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
22298
+ `SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
22299
+ WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
22146
22300
  );
22147
22301
  const needsReview = countScalar(
22148
22302
  this.db,
22149
22303
  `SELECT count(DISTINCT d.id) AS n
22150
22304
  FROM share_destination d
22151
- LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
22305
+ LEFT JOIN share_endpoint e ON e.destination_id = d.id
22306
+ AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
22152
22307
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
22153
22308
  );
22154
22309
  const kindCounts = countBy(
@@ -22158,6 +22313,7 @@ var SqliteSharesRepository = class {
22158
22313
  const byKind = {
22159
22314
  provider: kindCounts.get("provider") ?? 0,
22160
22315
  internal: kindCounts.get("internal") ?? 0,
22316
+ external: kindCounts.get("external") ?? 0,
22161
22317
  ip: kindCounts.get("ip") ?? 0
22162
22318
  };
22163
22319
  const trustCounts = countBy(
@@ -22233,23 +22389,316 @@ var SqliteSharesRepository = class {
22233
22389
  // real edit from a no-such-destination.
22234
22390
  /**
22235
22391
  * Set (decision) or clear (null) the egress decision override for a destination.
22236
- * `null` deletes the override row → reverts to the trust default.
22392
+ * `null` deletes the override rows → reverts to the trust default.
22393
+ *
22394
+ * The written row carries both the destination id and its host, so the
22395
+ * decision re-attaches by host after the destination is pruned and
22396
+ * re-detected under a fresh id. Rows written before the host column existed
22397
+ * (host NULL, matched by destination id) are replaced rather than left to
22398
+ * shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
22399
+ * would otherwise race a concurrent prune.
22237
22400
  */
22238
22401
  setEgressDecision(destinationId, decision) {
22239
- const exists = this.db.prepare("SELECT 1 FROM share_destination WHERE id = ?").get(destinationId);
22240
- if (exists === void 0) return false;
22241
- if (decision === null) {
22242
- this.db.prepare("DELETE FROM egress_decision_override WHERE destination_id = ?").run(destinationId);
22243
- return true;
22402
+ let existed = false;
22403
+ withTransaction(
22404
+ this.db,
22405
+ () => {
22406
+ const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
22407
+ if (dest === void 0) return;
22408
+ existed = true;
22409
+ this.db.prepare(
22410
+ `DELETE FROM egress_decision_override
22411
+ WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
22412
+ ).run({ host: dest.host, destinationId });
22413
+ if (decision === null) return;
22414
+ this.db.prepare(
22415
+ `INSERT INTO egress_decision_override
22416
+ (id, destination_id, host, decision, created_at, updated_at)
22417
+ VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22418
+ ).run({
22419
+ id: randomUUID7(),
22420
+ destinationId,
22421
+ host: dest.host,
22422
+ decision,
22423
+ now: Date.now()
22424
+ });
22425
+ },
22426
+ "IMMEDIATE"
22427
+ );
22428
+ return existed;
22429
+ }
22430
+ /**
22431
+ * Record one project's statically-extracted egress: reconcile the previously
22432
+ * stored call sites against this scan, upsert destination → endpoint → call
22433
+ * site for every hit, confirm `last_seen` on everything the project still
22434
+ * references, and drop what no longer has evidence.
22435
+ *
22436
+ * Reconciliation keys on `projectKey` alone; `project` and `projectId` are
22437
+ * display payload and never scope a delete. The whole write is one
22438
+ * transaction: a failure leaves the project's previous inventory exactly as
22439
+ * it was, and THROWS rather than reporting a partial write — callers decide
22440
+ * their own fail-open behavior, and the scanner additionally withholds its
22441
+ * ledger commit so the next scan retries.
22442
+ *
22443
+ * Over-cap input is truncated at a FILE boundary, and the files that lost
22444
+ * their hits are both excluded from the reconcile delete and named in
22445
+ * `droppedFiles`. That pairing is what keeps truncation non-destructive on
22446
+ * the ledger path: a dropped file keeps whatever rows it already had, and its
22447
+ * caller withholds the ledger entry so the next scan reads it again.
22448
+ */
22449
+ recordProjectEgress(input) {
22450
+ const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
22451
+ const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
22452
+ const now = Date.now();
22453
+ let summary = {
22454
+ destinations: 0,
22455
+ endpoints: 0,
22456
+ callSites: 0,
22457
+ truncated,
22458
+ droppedFiles
22459
+ };
22460
+ withTransaction(
22461
+ this.db,
22462
+ () => {
22463
+ const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
22464
+ this.reconcileCallSites(input.projectKey, reconcile);
22465
+ this.upsertHits(input, hits, projectId, now);
22466
+ this.confirmLastSeen(input.projectKey, now);
22467
+ this.pruneOrphans();
22468
+ summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
22469
+ },
22470
+ "IMMEDIATE"
22471
+ );
22472
+ return summary;
22473
+ }
22474
+ // ─── Egress write internals ──────────────────────────────────────────────────
22475
+ /**
22476
+ * Clear the stored call sites this scan is responsible for re-creating.
22477
+ *
22478
+ * Each pipeline may only delete rows its own walker could have produced. The
22479
+ * fs walk behind 'walk' mode never descends into dot-directories, so its
22480
+ * delete excludes dot-path files — those rows are the plugin scanner's to
22481
+ * reconcile, and deleting them here would make the two pipelines erase each
22482
+ * other's rows on every alternating scan. 'ledger' mode names its files
22483
+ * outright and never mass-deletes, so rows the fs walk contributed for files
22484
+ * the scanner skips (vendored, oversize) survive it.
22485
+ */
22486
+ reconcileCallSites(projectKey, reconcile) {
22487
+ if (reconcile.mode === "walk") {
22488
+ const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
22489
+ this.db.prepare(
22490
+ `DELETE FROM share_call_site
22491
+ WHERE project_key = :key
22492
+ AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
22493
+ AND file NOT LIKE '.%'
22494
+ AND file NOT LIKE '%/.%'`
22495
+ ).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
22496
+ return;
22497
+ }
22498
+ const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
22499
+ for (let i = 0; i < files.length; i += IN_CHUNK) {
22500
+ const chunk = files.slice(i, i + IN_CHUNK);
22501
+ this.db.prepare(
22502
+ `DELETE FROM share_call_site
22503
+ WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
22504
+ ).run(projectKey, ...chunk);
22244
22505
  }
22506
+ }
22507
+ /**
22508
+ * Upsert every hit as destination → endpoint → call site. Destinations key on
22509
+ * `host` and endpoints on `(destination_id, method, url)`, both shared across
22510
+ * projects; only the call site carries `project_key`. A destination's `note`
22511
+ * is user-owned and never overwritten. The id caches keep one upsert per
22512
+ * distinct host and endpoint, so the first hit for a host supplies its
22513
+ * classification for this batch.
22514
+ */
22515
+ upsertHits(input, hits, projectId, now) {
22516
+ if (hits.length === 0) return;
22517
+ const destStmt = this.db.prepare(
22518
+ `INSERT INTO share_destination
22519
+ (id, kind, name, host, category, trust, network_json, last_seen, provenance,
22520
+ created_at, updated_at)
22521
+ VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
22522
+ ON CONFLICT (host) DO UPDATE SET
22523
+ kind = excluded.kind,
22524
+ name = excluded.name,
22525
+ category = excluded.category,
22526
+ trust = excluded.trust,
22527
+ network_json = excluded.network_json,
22528
+ last_seen = excluded.last_seen,
22529
+ updated_at = excluded.updated_at`
22530
+ );
22531
+ const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
22532
+ const endpointStmt = this.db.prepare(
22533
+ `INSERT INTO share_endpoint
22534
+ (id, destination_id, method, transport, url, template, data_class, last_seen,
22535
+ created_at, updated_at)
22536
+ VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
22537
+ :now, :now)
22538
+ ON CONFLICT (destination_id, method, url) DO UPDATE SET
22539
+ transport = excluded.transport,
22540
+ template = excluded.template,
22541
+ data_class = excluded.data_class,
22542
+ last_seen = excluded.last_seen,
22543
+ updated_at = excluded.updated_at`
22544
+ );
22545
+ const endpointIdStmt = this.db.prepare(
22546
+ "SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
22547
+ );
22548
+ const siteStmt = this.db.prepare(
22549
+ `INSERT INTO share_call_site
22550
+ (id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
22551
+ project_id, created_at, updated_at)
22552
+ VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
22553
+ :vendored, :projectId, :now, :now)
22554
+ ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
22555
+ snippet = excluded.snippet,
22556
+ dynamic = excluded.dynamic,
22557
+ vendored = excluded.vendored,
22558
+ project = excluded.project,
22559
+ project_id = COALESCE(excluded.project_id, share_call_site.project_id),
22560
+ updated_at = excluded.updated_at`
22561
+ );
22562
+ const destIds = /* @__PURE__ */ new Map();
22563
+ const endpointIds = /* @__PURE__ */ new Map();
22564
+ for (const hit of hits) {
22565
+ let destinationId = destIds.get(hit.host);
22566
+ if (destinationId === void 0) {
22567
+ destStmt.run({
22568
+ id: randomUUID7(),
22569
+ kind: hit.kind,
22570
+ name: hit.name,
22571
+ host: hit.host,
22572
+ category: hit.category,
22573
+ trust: hit.trust,
22574
+ networkJson: hit.network === null ? null : JSON.stringify(hit.network),
22575
+ now
22576
+ });
22577
+ destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
22578
+ destIds.set(hit.host, destinationId);
22579
+ }
22580
+ const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
22581
+ let endpointId = endpointIds.get(endpointKey);
22582
+ if (endpointId === void 0) {
22583
+ endpointStmt.run({
22584
+ id: randomUUID7(),
22585
+ destinationId,
22586
+ method: hit.method,
22587
+ transport: hit.transport,
22588
+ url: hit.url,
22589
+ template: boolToInt(hit.template),
22590
+ dataClass: hit.dataClass,
22591
+ now
22592
+ });
22593
+ endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
22594
+ endpointIds.set(endpointKey, endpointId);
22595
+ }
22596
+ siteStmt.run({
22597
+ id: randomUUID7(),
22598
+ endpointId,
22599
+ project: input.project,
22600
+ projectKey: input.projectKey,
22601
+ file: hit.site.file,
22602
+ line: hit.site.line,
22603
+ snippet: hit.site.snippet,
22604
+ dynamic: boolToInt(hit.site.dynamic),
22605
+ vendored: boolToInt(hit.site.vendored),
22606
+ projectId,
22607
+ now
22608
+ });
22609
+ }
22610
+ }
22611
+ /**
22612
+ * The source-project id this project's stored call sites already carry, if
22613
+ * any. Only the pipeline that resolves a source project supplies one; the
22614
+ * other passes null and inherits this, so the link stops flapping between a
22615
+ * real id and NULL depending on which pipeline ran last. The value is a
22616
+ * per-project attribute stored redundantly on each row, so any row's is
22617
+ * representative.
22618
+ */
22619
+ knownProjectId(projectKey) {
22620
+ return getRow(
22621
+ this.db.prepare(
22622
+ `SELECT project_id AS projectId FROM share_call_site
22623
+ WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
22624
+ ),
22625
+ [projectKey]
22626
+ )?.projectId ?? null;
22627
+ }
22628
+ /**
22629
+ * Stamp `last_seen` on every endpoint and destination this project still
22630
+ * references — including rows the scan preserved rather than re-wrote, so a
22631
+ * ledger-skipped file's references don't decay into "stale" on the page.
22632
+ */
22633
+ confirmLastSeen(projectKey, now) {
22245
22634
  this.db.prepare(
22246
- `INSERT INTO egress_decision_override (id, destination_id, decision, created_at, updated_at)
22247
- VALUES (:id, :destinationId, :decision, :now, :now)
22248
- ON CONFLICT (destination_id) DO UPDATE SET
22249
- decision = excluded.decision,
22250
- updated_at = excluded.updated_at`
22251
- ).run({ id: randomUUID7(), destinationId, decision, now: Date.now() });
22252
- return true;
22635
+ `UPDATE share_endpoint SET last_seen = :now, updated_at = :now
22636
+ WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
22637
+ ).run({ now, key: projectKey });
22638
+ this.db.prepare(
22639
+ `UPDATE share_destination SET last_seen = :now, updated_at = :now
22640
+ WHERE id IN (SELECT DISTINCT e.destination_id
22641
+ FROM share_endpoint e
22642
+ JOIN share_call_site c ON c.endpoint_id = e.id
22643
+ WHERE c.project_key = :key)`
22644
+ ).run({ now, key: projectKey });
22645
+ }
22646
+ /**
22647
+ * Drop rows left without evidence: endpoints with no call site, then
22648
+ * destinations with no endpoint. Call sites are the only evidence either one
22649
+ * has, so a row that lost its last one belongs to no project any more.
22650
+ *
22651
+ * Overrides are deleted between the two steps, and only the ones written
22652
+ * before the host column existed. Those match a destination by id alone;
22653
+ * because the id link is released on delete rather than cascading, leaving
22654
+ * them would accumulate rows that match neither join arm and that nothing can
22655
+ * reach again. Host-bearing rows deliberately survive — the host is what
22656
+ * re-attaches a user's decision when the destination comes back.
22657
+ */
22658
+ pruneOrphans() {
22659
+ this.db.exec(
22660
+ `DELETE FROM share_endpoint
22661
+ WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
22662
+ );
22663
+ this.db.exec(
22664
+ `DELETE FROM egress_decision_override
22665
+ WHERE host IS NULL
22666
+ AND destination_id IN (
22667
+ SELECT d.id FROM share_destination d
22668
+ WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
22669
+ );
22670
+ this.db.exec(
22671
+ `DELETE FROM share_destination
22672
+ WHERE NOT EXISTS (
22673
+ SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
22674
+ );
22675
+ }
22676
+ /**
22677
+ * Live totals for one project. Destinations and endpoints are shared across
22678
+ * projects and carry no project column, so both are counted through the call
22679
+ * sites that reference them.
22680
+ */
22681
+ projectTotals(projectKey) {
22682
+ return {
22683
+ destinations: countScalar(
22684
+ this.db,
22685
+ `SELECT count(DISTINCT e.destination_id) AS n
22686
+ FROM share_endpoint e
22687
+ JOIN share_call_site c ON c.endpoint_id = e.id
22688
+ WHERE c.project_key = ?`,
22689
+ [projectKey]
22690
+ ),
22691
+ endpoints: countScalar(
22692
+ this.db,
22693
+ "SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
22694
+ [projectKey]
22695
+ ),
22696
+ callSites: countScalar(
22697
+ this.db,
22698
+ "SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
22699
+ [projectKey]
22700
+ )
22701
+ };
22253
22702
  }
22254
22703
  // ─── Raw fetchers ────────────────────────────────────────────────────────────
22255
22704
  mapDestRow(r) {
@@ -22269,7 +22718,8 @@ var SqliteSharesRepository = class {
22269
22718
  fetchDestinations(q, kinds, reviewOnly = false) {
22270
22719
  const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22271
22720
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22272
- d.created_at AS createdAt, o.decision AS overrideDecision`;
22721
+ d.created_at AS createdAt,
22722
+ COALESCE(oh.decision, ol.decision) AS overrideDecision`;
22273
22723
  const conditions = [];
22274
22724
  const params = [];
22275
22725
  if (kinds && kinds.length > 0) {
@@ -22280,7 +22730,8 @@ var SqliteSharesRepository = class {
22280
22730
  conditions.push(
22281
22731
  `(d.trust IN ('unverified', 'ip')
22282
22732
  OR EXISTS (SELECT 1 FROM share_endpoint re
22283
- WHERE re.destination_id = d.id AND re.transport = 'http'))`
22733
+ WHERE re.destination_id = d.id
22734
+ AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
22284
22735
  );
22285
22736
  }
22286
22737
  let sql;
@@ -22293,7 +22744,7 @@ var SqliteSharesRepository = class {
22293
22744
  params.push(pattern, pattern, pattern, pattern, pattern);
22294
22745
  sql = `SELECT DISTINCT ${cols}
22295
22746
  FROM share_destination d
22296
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22747
+ ${OVERRIDE_JOIN}
22297
22748
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
22298
22749
  LEFT JOIN share_call_site c ON c.endpoint_id = e.id
22299
22750
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
@@ -22301,7 +22752,7 @@ var SqliteSharesRepository = class {
22301
22752
  } else {
22302
22753
  sql = `SELECT ${cols}
22303
22754
  FROM share_destination d
22304
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22755
+ ${OVERRIDE_JOIN}
22305
22756
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
22306
22757
  ORDER BY d.created_at ASC, d.id ASC`;
22307
22758
  }
@@ -22316,9 +22767,9 @@ var SqliteSharesRepository = class {
22316
22767
  this.db.prepare(
22317
22768
  `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22318
22769
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22319
- o.decision AS overrideDecision
22770
+ COALESCE(oh.decision, ol.decision) AS overrideDecision
22320
22771
  FROM share_destination d
22321
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22772
+ ${OVERRIDE_JOIN}
22322
22773
  WHERE d.id = ?`
22323
22774
  ),
22324
22775
  [destinationId]
@@ -22547,6 +22998,7 @@ function openLocalDatabase(dir) {
22547
22998
  const scanLedger = new SqliteScanLedgerRepository(db);
22548
22999
  const exceptions = new SqliteExceptionsRepository(db);
22549
23000
  const resolutions = new SqliteResolutionsRepository(db);
23001
+ const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
22550
23002
  const security = new SqliteSecurityRepository(db);
22551
23003
  const detections = new SqliteDetectionsRepository(db);
22552
23004
  const shares = new SqliteSharesRepository(db);
@@ -22684,6 +23136,7 @@ function openLocalDatabase(dir) {
22684
23136
  scanLedger,
22685
23137
  exceptions,
22686
23138
  resolutions,
23139
+ ruleProbeCache,
22687
23140
  security,
22688
23141
  detections,
22689
23142
  shares,
@@ -22934,13 +23387,581 @@ import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as s
22934
23387
  import { homedir as homedir2 } from "os";
22935
23388
  import { basename as basename2, join as join7 } from "path";
22936
23389
 
23390
+ // ../../packages/detections/src/egress/registry.ts
23391
+ var EXTRACTOR_VERSION = "1";
23392
+ var PROVIDER_REGISTRY = [
23393
+ {
23394
+ id: "stripe",
23395
+ name: "Stripe",
23396
+ category: "Payments",
23397
+ hostSuffixes: ["stripe.com"],
23398
+ apiBase: "https://api.stripe.com",
23399
+ defaultDataClasses: ["pii", "customer"],
23400
+ sdks: {
23401
+ npm: ["stripe"],
23402
+ pypi: ["stripe"],
23403
+ go: ["github.com/stripe/stripe-go"],
23404
+ maven: ["com.stripe"],
23405
+ rubygems: ["stripe"],
23406
+ composer: ["stripe/stripe-php"],
23407
+ nuget: ["Stripe.net"]
23408
+ }
23409
+ },
23410
+ {
23411
+ id: "datadog",
23412
+ name: "Datadog",
23413
+ category: "Observability",
23414
+ hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
23415
+ apiBase: "https://api.datadoghq.com",
23416
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23417
+ sdks: {
23418
+ npm: ["dd-trace", "@datadog/browser-logs"],
23419
+ pypi: ["datadog", "ddtrace"],
23420
+ go: ["github.com/DataDog/dd-trace-go"],
23421
+ maven: ["com.datadoghq"],
23422
+ rubygems: ["ddtrace", "dogapi"],
23423
+ nuget: ["Datadog.Trace"]
23424
+ }
23425
+ },
23426
+ {
23427
+ id: "newrelic",
23428
+ name: "New Relic",
23429
+ category: "Observability",
23430
+ hostSuffixes: ["newrelic.com", "nr-data.net"],
23431
+ apiBase: "https://api.newrelic.com",
23432
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23433
+ sdks: {
23434
+ npm: ["newrelic"],
23435
+ pypi: ["newrelic"],
23436
+ go: ["github.com/newrelic/go-agent"],
23437
+ maven: ["com.newrelic.agent.java"],
23438
+ rubygems: ["newrelic_rpm"],
23439
+ nuget: ["NewRelic.Agent"]
23440
+ }
23441
+ },
23442
+ {
23443
+ id: "sentry",
23444
+ name: "Sentry",
23445
+ category: "Error tracking",
23446
+ hostSuffixes: ["sentry.io"],
23447
+ apiBase: "https://sentry.io",
23448
+ defaultDataClasses: ["source", "telemetry"],
23449
+ sdks: {
23450
+ npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
23451
+ pypi: ["sentry-sdk"],
23452
+ go: ["github.com/getsentry/sentry-go"],
23453
+ maven: ["io.sentry"],
23454
+ rubygems: ["sentry-ruby"],
23455
+ cargo: ["sentry"],
23456
+ composer: ["sentry/sentry"],
23457
+ nuget: ["Sentry"]
23458
+ }
23459
+ },
23460
+ {
23461
+ id: "openai",
23462
+ name: "OpenAI",
23463
+ category: "LLM provider",
23464
+ hostSuffixes: ["openai.com"],
23465
+ apiBase: "https://api.openai.com",
23466
+ defaultDataClasses: ["pii", "source"],
23467
+ sdks: {
23468
+ npm: ["openai"],
23469
+ pypi: ["openai"],
23470
+ go: ["github.com/sashabaranov/go-openai"],
23471
+ maven: ["com.openai"],
23472
+ rubygems: ["ruby-openai"],
23473
+ cargo: ["async-openai"],
23474
+ composer: ["openai-php/client"],
23475
+ nuget: ["OpenAI"]
23476
+ }
23477
+ },
23478
+ {
23479
+ id: "anthropic",
23480
+ name: "Anthropic",
23481
+ category: "LLM provider",
23482
+ hostSuffixes: ["anthropic.com"],
23483
+ apiBase: "https://api.anthropic.com",
23484
+ defaultDataClasses: ["pii", "source"],
23485
+ sdks: {
23486
+ npm: ["@anthropic-ai/sdk"],
23487
+ pypi: ["anthropic"],
23488
+ go: ["github.com/anthropics/anthropic-sdk-go"],
23489
+ nuget: ["Anthropic.SDK"]
23490
+ }
23491
+ },
23492
+ {
23493
+ id: "aws",
23494
+ name: "Amazon Web Services",
23495
+ category: "Cloud platform",
23496
+ hostSuffixes: ["amazonaws.com"],
23497
+ apiBase: "https://s3.amazonaws.com",
23498
+ defaultDataClasses: ["secrets", "customer"],
23499
+ sdks: {
23500
+ npm: ["@aws-sdk/client-s3", "aws-sdk"],
23501
+ pypi: ["boto3"],
23502
+ go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
23503
+ maven: ["com.amazonaws", "software.amazon.awssdk"],
23504
+ rubygems: ["aws-sdk-s3"],
23505
+ cargo: ["aws-sdk-s3"],
23506
+ nuget: ["AWSSDK.S3"]
23507
+ }
23508
+ },
23509
+ {
23510
+ id: "gcp",
23511
+ name: "Google Cloud",
23512
+ category: "Cloud platform",
23513
+ hostSuffixes: ["googleapis.com"],
23514
+ apiBase: "https://storage.googleapis.com",
23515
+ defaultDataClasses: ["customer", "logs"],
23516
+ sdks: {
23517
+ npm: ["@google-cloud/storage"],
23518
+ pypi: ["google-cloud-storage"],
23519
+ go: ["cloud.google.com/go"],
23520
+ maven: ["com.google.cloud"],
23521
+ rubygems: ["google-cloud-storage"],
23522
+ nuget: ["Google.Cloud.Storage.V1"]
23523
+ }
23524
+ },
23525
+ {
23526
+ id: "azure",
23527
+ name: "Microsoft Azure",
23528
+ category: "Cloud platform",
23529
+ hostSuffixes: ["azure.com", "windows.net"],
23530
+ apiBase: "https://management.azure.com",
23531
+ defaultDataClasses: ["customer", "logs"],
23532
+ sdks: {
23533
+ npm: ["@azure/storage-blob"],
23534
+ pypi: ["azure-storage-blob"],
23535
+ go: ["github.com/Azure/azure-sdk-for-go"],
23536
+ maven: ["com.azure"],
23537
+ rubygems: ["azure-storage-blob"],
23538
+ nuget: ["Azure.Storage.Blobs"]
23539
+ }
23540
+ },
23541
+ {
23542
+ id: "slack",
23543
+ name: "Slack",
23544
+ category: "Notifications",
23545
+ hostSuffixes: ["slack.com"],
23546
+ apiBase: "https://slack.com/api",
23547
+ defaultDataClasses: ["logs"],
23548
+ sdks: {
23549
+ npm: ["@slack/web-api"],
23550
+ pypi: ["slack-sdk"],
23551
+ go: ["github.com/slack-go/slack"],
23552
+ maven: ["com.slack.api"],
23553
+ rubygems: ["slack-ruby-client"],
23554
+ composer: ["slack-php/slack-api"],
23555
+ nuget: ["SlackNet"]
23556
+ }
23557
+ },
23558
+ {
23559
+ id: "segment",
23560
+ name: "Segment",
23561
+ category: "Analytics",
23562
+ hostSuffixes: ["segment.io", "segment.com"],
23563
+ apiBase: "https://api.segment.io",
23564
+ defaultDataClasses: ["customer"],
23565
+ sdks: {
23566
+ npm: ["@segment/analytics-node", "analytics-node"],
23567
+ pypi: ["segment-analytics-python"],
23568
+ go: ["github.com/segmentio/analytics-go"],
23569
+ maven: ["com.segment.analytics.java"],
23570
+ rubygems: ["analytics-ruby"],
23571
+ nuget: ["Analytics"]
23572
+ }
23573
+ },
23574
+ {
23575
+ id: "twilio",
23576
+ name: "Twilio",
23577
+ category: "Communications",
23578
+ hostSuffixes: ["twilio.com"],
23579
+ apiBase: "https://api.twilio.com",
23580
+ defaultDataClasses: ["pii", "customer"],
23581
+ sdks: {
23582
+ npm: ["twilio"],
23583
+ pypi: ["twilio"],
23584
+ go: ["github.com/twilio/twilio-go"],
23585
+ maven: ["com.twilio.sdk"],
23586
+ rubygems: ["twilio-ruby"],
23587
+ composer: ["twilio/sdk"],
23588
+ nuget: ["Twilio"]
23589
+ }
23590
+ },
23591
+ {
23592
+ id: "sendgrid",
23593
+ name: "SendGrid",
23594
+ category: "Email",
23595
+ hostSuffixes: ["sendgrid.com"],
23596
+ apiBase: "https://api.sendgrid.com",
23597
+ defaultDataClasses: ["pii"],
23598
+ sdks: {
23599
+ npm: ["@sendgrid/mail"],
23600
+ pypi: ["sendgrid"],
23601
+ go: ["github.com/sendgrid/sendgrid-go"],
23602
+ maven: ["com.sendgrid"],
23603
+ rubygems: ["sendgrid-ruby"],
23604
+ composer: ["sendgrid/sendgrid"],
23605
+ nuget: ["SendGrid"]
23606
+ }
23607
+ },
23608
+ {
23609
+ id: "mailgun",
23610
+ name: "Mailgun",
23611
+ category: "Email",
23612
+ hostSuffixes: ["mailgun.net"],
23613
+ apiBase: "https://api.mailgun.net",
23614
+ defaultDataClasses: ["pii"],
23615
+ sdks: {
23616
+ npm: ["mailgun.js"],
23617
+ pypi: ["mailgun"],
23618
+ rubygems: ["mailgun-ruby"],
23619
+ composer: ["mailgun/mailgun-php"],
23620
+ nuget: ["Mailgun"]
23621
+ }
23622
+ },
23623
+ {
23624
+ id: "mixpanel",
23625
+ name: "Mixpanel",
23626
+ category: "Analytics",
23627
+ hostSuffixes: ["mixpanel.com"],
23628
+ apiBase: "https://api.mixpanel.com",
23629
+ defaultDataClasses: ["customer", "telemetry"],
23630
+ sdks: {
23631
+ npm: ["mixpanel"],
23632
+ pypi: ["mixpanel"],
23633
+ rubygems: ["mixpanel-ruby"],
23634
+ nuget: ["Mixpanel"]
23635
+ }
23636
+ },
23637
+ {
23638
+ id: "amplitude",
23639
+ name: "Amplitude",
23640
+ category: "Analytics",
23641
+ hostSuffixes: ["amplitude.com"],
23642
+ apiBase: "https://api2.amplitude.com",
23643
+ defaultDataClasses: ["customer", "telemetry"],
23644
+ sdks: {
23645
+ npm: ["@amplitude/analytics-node"],
23646
+ pypi: ["amplitude-analytics"],
23647
+ nuget: ["Amplitude"]
23648
+ }
23649
+ },
23650
+ {
23651
+ id: "posthog",
23652
+ name: "PostHog",
23653
+ category: "Analytics",
23654
+ hostSuffixes: ["posthog.com"],
23655
+ apiBase: "https://us.i.posthog.com",
23656
+ defaultDataClasses: ["customer", "telemetry"],
23657
+ sdks: {
23658
+ npm: ["posthog-node", "posthog-js"],
23659
+ pypi: ["posthog"],
23660
+ go: ["github.com/posthog/posthog-go"],
23661
+ rubygems: ["posthog-ruby"],
23662
+ composer: ["posthog/posthog-php"],
23663
+ nuget: ["PostHog"]
23664
+ }
23665
+ },
23666
+ {
23667
+ id: "honeycomb",
23668
+ name: "Honeycomb",
23669
+ category: "Observability",
23670
+ hostSuffixes: ["honeycomb.io"],
23671
+ apiBase: "https://api.honeycomb.io",
23672
+ defaultDataClasses: ["telemetry", "metrics"],
23673
+ sdks: {
23674
+ npm: ["libhoney"],
23675
+ pypi: ["libhoney"],
23676
+ go: ["github.com/honeycombio/libhoney-go"],
23677
+ rubygems: ["libhoney"]
23678
+ }
23679
+ },
23680
+ {
23681
+ id: "grafana",
23682
+ name: "Grafana Cloud",
23683
+ category: "Observability",
23684
+ hostSuffixes: ["grafana.net"],
23685
+ apiBase: "https://grafana.net",
23686
+ defaultDataClasses: ["logs", "metrics"],
23687
+ sdks: {
23688
+ npm: ["@grafana/faro-web-sdk"]
23689
+ }
23690
+ },
23691
+ {
23692
+ id: "splunk",
23693
+ name: "Splunk",
23694
+ category: "Observability",
23695
+ hostSuffixes: ["splunkcloud.com", "splunk.com"],
23696
+ apiBase: "https://http-inputs.splunkcloud.com",
23697
+ defaultDataClasses: ["logs"],
23698
+ sdks: {
23699
+ npm: ["splunk-logging"],
23700
+ pypi: ["splunk-sdk"],
23701
+ maven: ["com.splunk"],
23702
+ nuget: ["Splunk.Logging.Common"]
23703
+ }
23704
+ },
23705
+ {
23706
+ id: "pagerduty",
23707
+ name: "PagerDuty",
23708
+ category: "Incident response",
23709
+ hostSuffixes: ["pagerduty.com"],
23710
+ apiBase: "https://api.pagerduty.com",
23711
+ defaultDataClasses: ["logs"],
23712
+ sdks: {
23713
+ npm: ["@pagerduty/pdjs"],
23714
+ pypi: ["pdpyras"],
23715
+ go: ["github.com/PagerDuty/go-pagerduty"],
23716
+ rubygems: ["pagerduty"]
23717
+ }
23718
+ },
23719
+ {
23720
+ id: "github",
23721
+ name: "GitHub",
23722
+ category: "Developer platform",
23723
+ hostSuffixes: ["github.com", "githubusercontent.com"],
23724
+ apiBase: "https://api.github.com",
23725
+ defaultDataClasses: ["source"],
23726
+ sdks: {
23727
+ npm: ["@octokit/rest", "octokit"],
23728
+ pypi: ["pygithub"],
23729
+ go: ["github.com/google/go-github"],
23730
+ maven: ["org.kohsuke.github-api"],
23731
+ rubygems: ["octokit"],
23732
+ cargo: ["octocrab"],
23733
+ composer: ["knplabs/github-api"],
23734
+ nuget: ["Octokit"]
23735
+ }
23736
+ },
23737
+ {
23738
+ id: "gitlab",
23739
+ name: "GitLab",
23740
+ category: "Developer platform",
23741
+ hostSuffixes: ["gitlab.com"],
23742
+ apiBase: "https://gitlab.com/api",
23743
+ defaultDataClasses: ["source"],
23744
+ sdks: {
23745
+ npm: ["@gitbeaker/rest"],
23746
+ pypi: ["python-gitlab"],
23747
+ go: ["gitlab.com/gitlab-org/api/client-go"],
23748
+ rubygems: ["gitlab"],
23749
+ nuget: ["GitLabApiClient"]
23750
+ }
23751
+ },
23752
+ {
23753
+ id: "auth0",
23754
+ name: "Auth0",
23755
+ category: "Identity",
23756
+ hostSuffixes: ["auth0.com"],
23757
+ apiBase: "https://login.auth0.com",
23758
+ defaultDataClasses: ["pii"],
23759
+ sdks: {
23760
+ npm: ["auth0"],
23761
+ pypi: ["auth0-python"],
23762
+ go: ["github.com/auth0/go-auth0"],
23763
+ maven: ["com.auth0"],
23764
+ rubygems: ["auth0"],
23765
+ composer: ["auth0/auth0-php"],
23766
+ nuget: ["Auth0.ManagementApi"]
23767
+ }
23768
+ },
23769
+ {
23770
+ id: "okta",
23771
+ name: "Okta",
23772
+ category: "Identity",
23773
+ hostSuffixes: ["okta.com", "oktapreview.com"],
23774
+ apiBase: "https://login.okta.com",
23775
+ defaultDataClasses: ["pii"],
23776
+ sdks: {
23777
+ npm: ["@okta/okta-sdk-nodejs"],
23778
+ pypi: ["okta"],
23779
+ go: ["github.com/okta/okta-sdk-golang"],
23780
+ maven: ["com.okta.sdk"],
23781
+ nuget: ["Okta.Sdk"]
23782
+ }
23783
+ },
23784
+ {
23785
+ id: "clerk",
23786
+ name: "Clerk",
23787
+ category: "Identity",
23788
+ hostSuffixes: ["clerk.com", "clerk.dev"],
23789
+ apiBase: "https://api.clerk.com",
23790
+ defaultDataClasses: ["pii"],
23791
+ sdks: {
23792
+ npm: ["@clerk/backend", "@clerk/nextjs"],
23793
+ pypi: ["clerk-backend-api"],
23794
+ go: ["github.com/clerk/clerk-sdk-go"]
23795
+ }
23796
+ },
23797
+ {
23798
+ id: "supabase",
23799
+ name: "Supabase",
23800
+ category: "Backend platform",
23801
+ hostSuffixes: ["supabase.co", "supabase.com"],
23802
+ apiBase: "https://api.supabase.com",
23803
+ defaultDataClasses: ["pii", "customer"],
23804
+ sdks: {
23805
+ npm: ["@supabase/supabase-js"],
23806
+ pypi: ["supabase"],
23807
+ cargo: ["postgrest"]
23808
+ }
23809
+ },
23810
+ {
23811
+ id: "firebase",
23812
+ name: "Firebase",
23813
+ category: "Backend platform",
23814
+ hostSuffixes: ["firebaseio.com", "firebase.google.com"],
23815
+ apiBase: "https://firebaseio.com",
23816
+ defaultDataClasses: ["customer"],
23817
+ sdks: {
23818
+ npm: ["firebase", "firebase-admin"],
23819
+ pypi: ["firebase-admin"],
23820
+ go: ["firebase.google.com/go"],
23821
+ maven: ["com.google.firebase"]
23822
+ }
23823
+ },
23824
+ {
23825
+ id: "mongodb-atlas",
23826
+ name: "MongoDB Atlas",
23827
+ category: "Database SaaS",
23828
+ hostSuffixes: ["mongodb.net", "mongodb.com"],
23829
+ apiBase: "https://cloud.mongodb.com",
23830
+ defaultDataClasses: ["customer"],
23831
+ sdks: {
23832
+ npm: ["mongodb"],
23833
+ pypi: ["pymongo"],
23834
+ go: ["go.mongodb.org/mongo-driver"],
23835
+ maven: ["org.mongodb"],
23836
+ rubygems: ["mongo"],
23837
+ cargo: ["mongodb"],
23838
+ nuget: ["MongoDB.Driver"]
23839
+ }
23840
+ },
23841
+ {
23842
+ id: "planetscale",
23843
+ name: "PlanetScale",
23844
+ category: "Database SaaS",
23845
+ hostSuffixes: ["psdb.cloud", "planetscale.com"],
23846
+ apiBase: "https://api.planetscale.com",
23847
+ defaultDataClasses: ["customer"],
23848
+ sdks: {
23849
+ npm: ["@planetscale/database"],
23850
+ go: ["github.com/planetscale/planetscale-go"]
23851
+ }
23852
+ },
23853
+ {
23854
+ id: "algolia",
23855
+ name: "Algolia",
23856
+ category: "Search SaaS",
23857
+ hostSuffixes: ["algolia.net", "algolianet.com"],
23858
+ apiBase: "https://algolia.net",
23859
+ defaultDataClasses: ["customer"],
23860
+ sdks: {
23861
+ npm: ["algoliasearch"],
23862
+ pypi: ["algoliasearch"],
23863
+ go: ["github.com/algolia/algoliasearch-client-go"],
23864
+ maven: ["com.algolia"],
23865
+ rubygems: ["algolia"],
23866
+ composer: ["algolia/algoliasearch-client-php"],
23867
+ nuget: ["Algolia.Search"]
23868
+ }
23869
+ },
23870
+ {
23871
+ id: "cloudflare",
23872
+ name: "Cloudflare",
23873
+ category: "CDN / edge",
23874
+ hostSuffixes: ["cloudflare.com", "workers.dev"],
23875
+ apiBase: "https://api.cloudflare.com",
23876
+ defaultDataClasses: ["logs"],
23877
+ sdks: {
23878
+ npm: ["cloudflare"],
23879
+ pypi: ["cloudflare"],
23880
+ go: ["github.com/cloudflare/cloudflare-go"],
23881
+ nuget: ["CloudFlare.Client"]
23882
+ }
23883
+ },
23884
+ {
23885
+ id: "huggingface",
23886
+ name: "Hugging Face",
23887
+ category: "LLM provider",
23888
+ hostSuffixes: ["huggingface.co"],
23889
+ apiBase: "https://api-inference.huggingface.co",
23890
+ defaultDataClasses: ["source"],
23891
+ sdks: {
23892
+ npm: ["@huggingface/inference"],
23893
+ pypi: ["huggingface-hub", "transformers"],
23894
+ rubygems: ["hugging-face"]
23895
+ }
23896
+ },
23897
+ {
23898
+ id: "cohere",
23899
+ name: "Cohere",
23900
+ category: "LLM provider",
23901
+ hostSuffixes: ["cohere.com", "cohere.ai"],
23902
+ apiBase: "https://api.cohere.com",
23903
+ defaultDataClasses: ["pii", "source"],
23904
+ sdks: {
23905
+ npm: ["cohere-ai"],
23906
+ pypi: ["cohere"],
23907
+ go: ["github.com/cohere-ai/cohere-go"]
23908
+ }
23909
+ },
23910
+ {
23911
+ id: "mistral",
23912
+ name: "Mistral AI",
23913
+ category: "LLM provider",
23914
+ hostSuffixes: ["mistral.ai"],
23915
+ apiBase: "https://api.mistral.ai",
23916
+ defaultDataClasses: ["pii", "source"],
23917
+ sdks: {
23918
+ npm: ["@mistralai/mistralai"],
23919
+ pypi: ["mistralai"],
23920
+ go: ["github.com/gage-technologies/mistral-go"]
23921
+ }
23922
+ }
23923
+ ];
23924
+ var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
23925
+ ${JSON.stringify(PROVIDER_REGISTRY)}`;
23926
+
23927
+ // ../../packages/detections/src/egress/extract.ts
23928
+ var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
23929
+ var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
23930
+ var SECRET_VALUE = new RegExp(
23931
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
23932
+ "gi"
23933
+ );
23934
+ var AUTH_SCHEME_VALUE = new RegExp(
23935
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
23936
+ "gi"
23937
+ );
23938
+ var WEBHOOK_SECRET_PATHS = [
23939
+ { hosts: ["hooks.slack.com"], prefix: "/services/" },
23940
+ {
23941
+ hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
23942
+ prefix: "/api/webhooks/"
23943
+ },
23944
+ { hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
23945
+ { hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
23946
+ ];
23947
+ function escapeRegExp(literal2) {
23948
+ return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23949
+ }
23950
+ var WEBHOOK_URL = new RegExp(
23951
+ `(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
23952
+ (entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
23953
+ ).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
23954
+ "gi"
23955
+ );
23956
+
22937
23957
  // ../../packages/detections/src/escape-regexp.ts
22938
- function escapeRegExp(value) {
23958
+ function escapeRegExp2(value) {
22939
23959
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22940
23960
  }
22941
23961
 
22942
23962
  // ../../packages/detections/src/matchers/limits.ts
22943
23963
  var MAX_MATCHES_PER_RULE = 1e4;
23964
+ var MAX_REGEX_INPUT_LENGTH = 2e5;
22944
23965
 
22945
23966
  // ../../packages/detections/src/matchers/keyword.ts
22946
23967
  var KeywordMatcher2 = class {
@@ -22951,7 +23972,7 @@ var KeywordMatcher2 = class {
22951
23972
  for (const kw of keywords) {
22952
23973
  if (kw.length === 0) continue;
22953
23974
  if (spans.length >= MAX_MATCHES_PER_RULE) break;
22954
- const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
23975
+ const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
22955
23976
  let m;
22956
23977
  while ((m = re.exec(text)) !== null) {
22957
23978
  spans.push({ start: m.index, end: m.index + m[0].length });
@@ -22968,9 +23989,13 @@ var RegexMatcher2 = class {
22968
23989
  if (rule.matcher.type !== "regex") return [];
22969
23990
  const { pattern, flags, captureGroup } = rule.matcher;
22970
23991
  const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
23992
+ const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
22971
23993
  const spans = [];
22972
23994
  let m;
22973
- while ((m = re.exec(text)) !== null) {
23995
+ const maxIterations = scanText2.length + 1;
23996
+ let iterations = 0;
23997
+ while ((m = re.exec(scanText2)) !== null) {
23998
+ if (++iterations > maxIterations) break;
22974
23999
  const group = captureGroup != null ? m[captureGroup] : m[0];
22975
24000
  if (m[0].length === 0) re.lastIndex++;
22976
24001
  if (group && spans.length < MAX_MATCHES_PER_RULE) {
@@ -23074,7 +24099,7 @@ function isCorroborated(candidate, candidates, text) {
23074
24099
  for (const label of labels) {
23075
24100
  const trimmed = label.trim();
23076
24101
  if (trimmed.length === 0) continue;
23077
- const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(trimmed)}(?![A-Za-z0-9])`, "i");
24102
+ const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
23078
24103
  if (re.test(haystack)) return true;
23079
24104
  }
23080
24105
  }
@@ -23239,6 +24264,112 @@ var CONFIG_POSTURE_RULES = [
23239
24264
  }
23240
24265
  ];
23241
24266
 
24267
+ // ../../packages/detections/src/security/redos-probe.ts
24268
+ var BUDGET_MS = 100;
24269
+ var EXPONENTIAL_UNITS = [
24270
+ "a",
24271
+ "0",
24272
+ " ",
24273
+ "x",
24274
+ "ab",
24275
+ "a.",
24276
+ "a-",
24277
+ "a_",
24278
+ "a@",
24279
+ "a/",
24280
+ "a:",
24281
+ "a=",
24282
+ "a;",
24283
+ "aA0",
24284
+ " "
24285
+ ];
24286
+ var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
24287
+ (unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
24288
+ );
24289
+ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
24290
+ (unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
24291
+ );
24292
+ function literalPrefix(pattern) {
24293
+ let prefix = "";
24294
+ let i = 0;
24295
+ if (pattern[i] === "^") i++;
24296
+ while (i < pattern.length) {
24297
+ const c = pattern[i];
24298
+ if (c === void 0) break;
24299
+ if (c === "\\") {
24300
+ const next = pattern[i + 1];
24301
+ if (next === "b" || next === "B") {
24302
+ i += 2;
24303
+ continue;
24304
+ }
24305
+ if (next === void 0 || /[dDwWsSnrtfv.]/.test(next)) break;
24306
+ prefix += next;
24307
+ i += 2;
24308
+ continue;
24309
+ }
24310
+ if ("([{.*+?|)]}^$".includes(c)) break;
24311
+ prefix += c;
24312
+ i++;
24313
+ }
24314
+ return prefix;
24315
+ }
24316
+ function fuelChars(pattern) {
24317
+ const fuel = /* @__PURE__ */ new Set();
24318
+ for (const m of pattern.matchAll(/\[\^?([^\]]+)\]/g)) {
24319
+ const body = m[1];
24320
+ if (body === void 0) continue;
24321
+ const range = /([A-Za-z0-9])-[A-Za-z0-9]/.exec(body);
24322
+ const rangeStart = range?.[1];
24323
+ if (rangeStart !== void 0) fuel.add(rangeStart);
24324
+ else {
24325
+ const literal2 = body.replace(/\\/g, "")[0];
24326
+ if (literal2 !== void 0 && literal2 !== "^") fuel.add(literal2);
24327
+ }
24328
+ }
24329
+ if (pattern.includes("\\w")) fuel.add("a");
24330
+ if (pattern.includes("\\d")) fuel.add("0");
24331
+ if (pattern.includes("\\s")) fuel.add(" ");
24332
+ if (/(?<!\\)\./.test(pattern)) fuel.add("a");
24333
+ if (fuel.size === 0) fuel.add("a");
24334
+ return [...fuel];
24335
+ }
24336
+ function derivedProbes(pattern) {
24337
+ const prefix = literalPrefix(pattern);
24338
+ const fuel = fuelChars(pattern);
24339
+ const terminators = ["!", "#", "~", "\n"];
24340
+ const probes = [];
24341
+ for (const f of fuel) {
24342
+ for (const term of terminators) {
24343
+ if (term === f) continue;
24344
+ for (const len of [23, 25]) probes.push(prefix + f.repeat(len) + term);
24345
+ }
24346
+ }
24347
+ return probes;
24348
+ }
24349
+ function probesFor(rule) {
24350
+ const derived = rule.matcher.type === "regex" ? derivedProbes(rule.matcher.pattern) : [];
24351
+ return [...derived, ...EXPONENTIAL_PROBES, ...POLYNOMIAL_PROBES];
24352
+ }
24353
+ function worstProbeMs(rule) {
24354
+ let ms = 0;
24355
+ let probe = "";
24356
+ for (const text of probesFor(rule)) {
24357
+ const start = performance.now();
24358
+ scan(text, [rule]);
24359
+ const elapsed = performance.now() - start;
24360
+ if (elapsed > ms) {
24361
+ ms = elapsed;
24362
+ probe = text;
24363
+ }
24364
+ if (ms >= BUDGET_MS) break;
24365
+ }
24366
+ return { ms, probe };
24367
+ }
24368
+ function checkRuleTiming(rule) {
24369
+ const { ms, probe } = worstProbeMs(rule);
24370
+ return { safe: ms < BUDGET_MS, worstMs: ms, probe };
24371
+ }
24372
+
23242
24373
  // ../../rules/code-flaws/auth-jwt-no-verify.json
23243
24374
  var auth_jwt_no_verify_default = {
23244
24375
  specVersion: 1,
@@ -25492,10 +26623,14 @@ function resolveInventoryContext(input) {
25492
26623
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25493
26624
  import { join as join8 } from "path";
25494
26625
 
26626
+ // ../../packages/plugin-sdk/src/paths.ts
26627
+ import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
26628
+ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
26629
+
25495
26630
  // ../../packages/plugin-sdk/src/project-files.ts
25496
26631
  var import_ignore = __toESM(require_ignore(), 1);
25497
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25498
- import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
26632
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
26633
+ import { basename as basename4, join as join9, relative, sep as sep4 } from "path";
25499
26634
 
25500
26635
  // ../../packages/plugin-sdk/src/raw-egress.ts
25501
26636
  var RawEgressError = class extends Error {
@@ -25537,6 +26672,59 @@ function safeMaskedMatch(rawMatch) {
25537
26672
  return masked;
25538
26673
  }
25539
26674
 
26675
+ // ../../packages/plugin-sdk/src/rule-quarantine.ts
26676
+ var PASS_BUDGET_MS = 2e3;
26677
+ function ruleProbeKey(rule) {
26678
+ if (rule.matcher.type !== "regex") return void 0;
26679
+ return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
26680
+ }
26681
+ function warnQuarantined(rule, worstMs) {
26682
+ const timing = worstMs === void 0 ? "not verified in time" : `${worstMs.toFixed(1)}ms`;
26683
+ process.stderr.write(
26684
+ `[aka] quarantined rule "${rule.id}": regex matcher exceeded the ReDoS timing budget (${timing}); excluded from this scan.
26685
+ `
26686
+ );
26687
+ }
26688
+ async function filterUnsafeRules(rules, gateway, opts) {
26689
+ const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
26690
+ const passStart = performance.now();
26691
+ const safe = [];
26692
+ for (const rule of rules) {
26693
+ const key = ruleProbeKey(rule);
26694
+ if (key === void 0) {
26695
+ safe.push(rule);
26696
+ continue;
26697
+ }
26698
+ let cached2;
26699
+ try {
26700
+ cached2 = await gateway.getRuleProbeVerdict(key);
26701
+ } catch {
26702
+ cached2 = void 0;
26703
+ }
26704
+ if (cached2) {
26705
+ if (cached2.verdict === "safe") safe.push(rule);
26706
+ else warnQuarantined(rule, cached2.worstProbeMs);
26707
+ continue;
26708
+ }
26709
+ if (performance.now() - passStart >= passBudgetMs) {
26710
+ warnQuarantined(rule, void 0);
26711
+ continue;
26712
+ }
26713
+ let isSafe;
26714
+ let worstMs;
26715
+ try {
26716
+ ({ safe: isSafe, worstMs } = checkRuleTiming(rule));
26717
+ } catch {
26718
+ isSafe = false;
26719
+ worstMs = Number.POSITIVE_INFINITY;
26720
+ }
26721
+ await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
26722
+ if (isSafe) safe.push(rule);
26723
+ else warnQuarantined(rule, worstMs);
26724
+ }
26725
+ return safe;
26726
+ }
26727
+
25540
26728
  // ../../packages/plugin-sdk/src/runtime.ts
25541
26729
  import { randomUUID as randomUUID10 } from "crypto";
25542
26730
  var ENFORCEMENT_CEILING_ENABLED = false;
@@ -25579,7 +26767,17 @@ function createPluginRuntime(gateway, settings, opts) {
25579
26767
  categoryActionIndex.set(p.target.category, p.action);
25580
26768
  }
25581
26769
  }
25582
- rules = bundle.rulesComplete ? bundle.rules ?? [] : [...getLoadedRules(), ...bundle.rules ?? []];
26770
+ const bundledProbeKeys = new Set(
26771
+ getLoadedRules().map(ruleProbeKey).filter((key) => key !== void 0)
26772
+ );
26773
+ const incoming = bundle.rules ?? [];
26774
+ const ciVerified = incoming.filter((rule) => {
26775
+ const key = ruleProbeKey(rule);
26776
+ return key !== void 0 && bundledProbeKeys.has(key);
26777
+ });
26778
+ const needsGate = incoming.filter((rule) => !ciVerified.includes(rule));
26779
+ const safeBundleRules = [...ciVerified, ...await filterUnsafeRules(needsGate, gateway)];
26780
+ rules = bundle.rulesComplete ? safeBundleRules : [...getLoadedRules(), ...safeBundleRules];
25583
26781
  bundleExceptions = bundle.exceptions ?? [];
25584
26782
  initialized = true;
25585
26783
  }
@@ -26094,6 +27292,13 @@ var StandaloneDataGateway = class {
26094
27292
  this.db.scanLedger.upsertEntries(entries);
26095
27293
  return Promise.resolve();
26096
27294
  }
27295
+ getRuleProbeVerdict(ruleKey) {
27296
+ return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
27297
+ }
27298
+ setRuleProbeVerdict(ruleKey, verdict, worstProbeMs2) {
27299
+ this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs2);
27300
+ return Promise.resolve();
27301
+ }
26097
27302
  openAtRestKeysForPath(path) {
26098
27303
  return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
26099
27304
  }
@@ -26104,6 +27309,12 @@ var StandaloneDataGateway = class {
26104
27309
  this.db.resolutions.insertResolution(input);
26105
27310
  return Promise.resolve();
26106
27311
  }
27312
+ // Bare forward — no toggle read here. The plugin-path kill-switch is
27313
+ // enforced by the caller, which already holds the parsed workspace
27314
+ // settings; this class only ever sees `dataDir`, not the settings base.
27315
+ recordProjectEgress(input) {
27316
+ return Promise.resolve(this.db.shares.recordProjectEgress(input));
27317
+ }
26107
27318
  close() {
26108
27319
  this.db.close();
26109
27320
  return Promise.resolve();
@@ -26121,7 +27332,7 @@ import { randomUUID as randomUUID12 } from "crypto";
26121
27332
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
26122
27333
 
26123
27334
  // src/history/transcripts.ts
26124
- import { readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
27335
+ import { readdirSync as readdirSync4, readFileSync as readFileSync7 } from "fs";
26125
27336
  import { homedir as homedir3 } from "os";
26126
27337
  import { join as join11 } from "path";
26127
27338
  function transcriptsDir(home) {
@@ -26368,7 +27579,7 @@ var DAY_MS5 = 24 * 60 * 60 * 1e3;
26368
27579
  function* iterateFileContents(dir, excludeSessionId) {
26369
27580
  let projects;
26370
27581
  try {
26371
- projects = readdirSync3(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
27582
+ projects = readdirSync4(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
26372
27583
  } catch {
26373
27584
  return;
26374
27585
  }
@@ -26376,7 +27587,7 @@ function* iterateFileContents(dir, excludeSessionId) {
26376
27587
  const projectDir = join11(dir, project);
26377
27588
  let files;
26378
27589
  try {
26379
- files = readdirSync3(projectDir).filter((name) => name.endsWith(".jsonl"));
27590
+ files = readdirSync4(projectDir).filter((name) => name.endsWith(".jsonl"));
26380
27591
  } catch {
26381
27592
  continue;
26382
27593
  }