@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.
@@ -542,6 +542,10 @@ var SQLITE_MIGRATIONS = [
542
542
  {
543
543
  tag: "0010_events_session_expression_index",
544
544
  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"
545
+ },
546
+ {
547
+ tag: "0011_egress_writer",
548
+ 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'
545
549
  }
546
550
  ];
547
551
 
@@ -16197,6 +16201,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16197
16201
 
16198
16202
  // ../../packages/schema/src/zod/rule.ts
16199
16203
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16204
+ var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
16200
16205
  var KeywordMatcher = external_exports.object({
16201
16206
  type: external_exports.literal("keyword"),
16202
16207
  // An empty keyword matches at every position, yielding one zero-length span
@@ -16221,9 +16226,10 @@ function matchesEmptyString(pattern, flags) {
16221
16226
  return false;
16222
16227
  }
16223
16228
  }
16229
+ var MAX_PATTERN_LENGTH = 2e3;
16224
16230
  var RegexMatcher = external_exports.object({
16225
16231
  type: external_exports.literal("regex"),
16226
- pattern: external_exports.string(),
16232
+ pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
16227
16233
  flags: external_exports.string().default("gi"),
16228
16234
  captureGroup: external_exports.number().int().nonnegative().optional()
16229
16235
  }).refine((v) => isValidRegex(v.pattern, v.flags), {
@@ -16790,6 +16796,212 @@ function buildDetectionsList(summaries, query) {
16790
16796
  return { counts, items: filtered.map(summaryToDetectionListItem) };
16791
16797
  }
16792
16798
 
16799
+ // ../../packages/schema/src/zod/shares.ts
16800
+ var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
16801
+ var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
16802
+ var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
16803
+ var DATA_CLASS_ORDER = DataClass.options;
16804
+ var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
16805
+ var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
16806
+ var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
16807
+ var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
16808
+ var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
16809
+ var ReviewInfo = external_exports.object({
16810
+ needsReview: external_exports.boolean(),
16811
+ reasons: external_exports.array(ReviewReason)
16812
+ }).meta({ id: "ReviewInfo" });
16813
+ var DestinationNetwork = external_exports.object({
16814
+ port: external_exports.number().int().nullable(),
16815
+ geo: external_exports.string().nullable(),
16816
+ ptr: external_exports.string().nullable()
16817
+ }).meta({ id: "DestinationNetwork" });
16818
+ var EndpointSummary = external_exports.object({
16819
+ id: external_exports.string(),
16820
+ method: HttpMethod,
16821
+ transport: Transport,
16822
+ url: external_exports.string(),
16823
+ template: external_exports.boolean(),
16824
+ dataClass: DataClass,
16825
+ lastSeen: external_exports.iso.datetime(),
16826
+ callSiteCount: external_exports.number().int().nonnegative()
16827
+ }).meta({ id: "EndpointSummary" });
16828
+ var CallSite = external_exports.object({
16829
+ id: external_exports.string(),
16830
+ project: external_exports.string(),
16831
+ file: external_exports.string(),
16832
+ line: external_exports.number().int().nonnegative(),
16833
+ snippet: external_exports.string(),
16834
+ dynamic: external_exports.boolean(),
16835
+ vendored: external_exports.boolean(),
16836
+ /** Deep-link to the Inventory project, when the repo is governed there. */
16837
+ projectId: external_exports.string().nullable()
16838
+ }).meta({ id: "CallSite" });
16839
+ var EndpointWithSites = EndpointSummary.extend({
16840
+ sites: external_exports.array(CallSite)
16841
+ }).meta({ id: "EndpointWithSites" });
16842
+ var ShareDestinationSummary = external_exports.object({
16843
+ id: external_exports.string(),
16844
+ kind: DestinationKind,
16845
+ name: external_exports.string(),
16846
+ host: external_exports.string(),
16847
+ category: external_exports.string(),
16848
+ trust: ShareTrustLevel,
16849
+ /** Effective state (decision applied over the trust default). */
16850
+ status: EgressStatus,
16851
+ /** True when an egress decision override differs from the trust default. */
16852
+ isCustom: external_exports.boolean(),
16853
+ lastSeen: external_exports.iso.datetime(),
16854
+ endpointCount: external_exports.number().int().nonnegative(),
16855
+ callSiteCount: external_exports.number().int().nonnegative(),
16856
+ transports: external_exports.array(Transport),
16857
+ /** Most-sensitive first. */
16858
+ dataClasses: external_exports.array(DataClass),
16859
+ review: ReviewInfo,
16860
+ /** Non-provider hosts only; null for providers. */
16861
+ network: DestinationNetwork.nullable(),
16862
+ /** Embedded for inline expansion — no call sites here. */
16863
+ endpoints: external_exports.array(EndpointSummary)
16864
+ }).meta({ id: "ShareDestinationSummary" });
16865
+ var ShareDestinationDetail = ShareDestinationSummary.omit({
16866
+ endpointCount: true,
16867
+ callSiteCount: true,
16868
+ endpoints: true
16869
+ }).extend({
16870
+ /** Ownership/geo rationale; null for providers. */
16871
+ note: external_exports.string().nullable(),
16872
+ endpoints: external_exports.array(EndpointWithSites)
16873
+ }).meta({ id: "ShareDestinationDetail" });
16874
+ var ReviewDestination = external_exports.object({
16875
+ id: external_exports.string(),
16876
+ kind: DestinationKind,
16877
+ name: external_exports.string(),
16878
+ /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
16879
+ host: external_exports.string(),
16880
+ trust: ShareTrustLevel,
16881
+ status: EgressStatus,
16882
+ review: ReviewInfo,
16883
+ topDataClass: DataClass,
16884
+ callSiteCount: external_exports.number().int().nonnegative(),
16885
+ lastSeen: external_exports.iso.datetime()
16886
+ }).meta({ id: "ReviewDestination" });
16887
+ var ShareDestinationGroup = external_exports.object({
16888
+ kind: DestinationKind,
16889
+ total: external_exports.number().int().nonnegative(),
16890
+ items: external_exports.array(ShareDestinationSummary)
16891
+ }).meta({ id: "ShareDestinationGroup" });
16892
+ var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
16893
+ var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
16894
+ var SharesStats = external_exports.object({
16895
+ destinations: external_exports.number().int().nonnegative(),
16896
+ endpoints: external_exports.number().int().nonnegative(),
16897
+ callSites: external_exports.number().int().nonnegative(),
16898
+ needsReview: external_exports.number().int().nonnegative(),
16899
+ insecure: external_exports.number().int().nonnegative(),
16900
+ byKind: external_exports.object({
16901
+ provider: external_exports.number().int().nonnegative(),
16902
+ internal: external_exports.number().int().nonnegative(),
16903
+ external: external_exports.number().int().nonnegative(),
16904
+ ip: external_exports.number().int().nonnegative()
16905
+ }),
16906
+ byTrust: external_exports.object({
16907
+ recognized: external_exports.number().int().nonnegative(),
16908
+ internal: external_exports.number().int().nonnegative(),
16909
+ unverified: external_exports.number().int().nonnegative(),
16910
+ ip: external_exports.number().int().nonnegative()
16911
+ })
16912
+ }).meta({ id: "SharesStats" });
16913
+ var SetEgressDecisionBody = external_exports.object({
16914
+ /** `null` clears the override — reverts to the trust default, isCustom false. */
16915
+ decision: EgressDecision.nullable()
16916
+ }).meta({ id: "SetEgressDecisionBody" });
16917
+ var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
16918
+ var ListShareDestinationsQuery = external_exports.object({
16919
+ /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
16920
+ q: external_exports.string().optional(),
16921
+ /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
16922
+ kind: external_exports.array(DestinationKind).optional(),
16923
+ /** Reserved for future grouping modes; only 'destination' is supported today. */
16924
+ groupBy: external_exports.enum(["destination"]).default("destination"),
16925
+ /**
16926
+ * When true, return a flat severity-ordered `items[]` instead of `groups`.
16927
+ * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
16928
+ * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
16929
+ * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
16930
+ */
16931
+ review: external_exports.stringbool().default(false)
16932
+ });
16933
+ var ExportSharesQuery = external_exports.object({
16934
+ format: external_exports.enum(["csv", "json"]).default("csv"),
16935
+ q: external_exports.string().optional(),
16936
+ kind: external_exports.array(DestinationKind).optional()
16937
+ });
16938
+
16939
+ // ../../packages/schema/src/zod/egress-extraction.ts
16940
+ var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
16941
+ var ProviderRegistryEntry = external_exports.object({
16942
+ id: external_exports.string(),
16943
+ name: external_exports.string(),
16944
+ category: external_exports.string(),
16945
+ /** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
16946
+ hostSuffixes: external_exports.array(external_exports.string()).min(1),
16947
+ /** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
16948
+ apiBase: external_exports.string(),
16949
+ /** Most-sensitive first; index 0 becomes the endpoint dataClass. */
16950
+ defaultDataClasses: external_exports.array(DataClass).min(1),
16951
+ /** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
16952
+ sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
16953
+ }).meta({ id: "ProviderRegistryEntry" });
16954
+ var EgressCallSiteHit = external_exports.object({
16955
+ file: external_exports.string(),
16956
+ line: external_exports.number().int().positive(),
16957
+ snippet: external_exports.string(),
16958
+ dynamic: external_exports.boolean(),
16959
+ vendored: external_exports.boolean()
16960
+ }).meta({ id: "EgressCallSiteHit" });
16961
+ var ResolvedEgressHit = external_exports.object({
16962
+ host: external_exports.string(),
16963
+ kind: DestinationKind,
16964
+ name: external_exports.string(),
16965
+ category: external_exports.string(),
16966
+ trust: ShareTrustLevel,
16967
+ network: DestinationNetwork.nullable(),
16968
+ method: HttpMethod,
16969
+ transport: Transport,
16970
+ url: external_exports.string(),
16971
+ template: external_exports.boolean(),
16972
+ dataClass: DataClass,
16973
+ site: EgressCallSiteHit
16974
+ }).meta({ id: "ResolvedEgressHit" });
16975
+ var EgressReconcile = external_exports.discriminatedUnion("mode", [
16976
+ external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
16977
+ external_exports.object({
16978
+ mode: external_exports.literal("ledger"),
16979
+ scannedFiles: external_exports.array(external_exports.string()),
16980
+ deletedFiles: external_exports.array(external_exports.string())
16981
+ })
16982
+ ]).meta({ id: "EgressReconcile" });
16983
+ var RecordProjectEgressInput = external_exports.object({
16984
+ /** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
16985
+ projectKey: external_exports.string().min(1),
16986
+ /** Display name only — never keys reconciliation. */
16987
+ project: external_exports.string(),
16988
+ projectId: external_exports.string().nullable(),
16989
+ reconcile: EgressReconcile,
16990
+ hits: external_exports.array(ResolvedEgressHit)
16991
+ }).meta({ id: "RecordProjectEgressInput" });
16992
+ var EgressWriteSummary = external_exports.object({
16993
+ destinations: external_exports.number().int().nonnegative(),
16994
+ endpoints: external_exports.number().int().nonnegative(),
16995
+ callSites: external_exports.number().int().nonnegative(),
16996
+ truncated: external_exports.boolean(),
16997
+ /**
16998
+ * Files the cap dropped whole. Their stored rows were left untouched, so a
16999
+ * ledger-keeping caller must withhold their ledger entries and read them
17000
+ * again next scan.
17001
+ */
17002
+ droppedFiles: external_exports.array(external_exports.string()).default([])
17003
+ }).meta({ id: "EgressWriteSummary" });
17004
+
16793
17005
  // ../../packages/schema/src/zod/findings-group-build.ts
16794
17006
  function toApiAction(dbVal) {
16795
17007
  const map2 = {
@@ -17051,7 +17263,7 @@ var PatchInstalledPackRequest = external_exports.object({
17051
17263
  }).meta({ id: "PatchInstalledPackRequest" });
17052
17264
 
17053
17265
  // ../../packages/schema/src/zod/local.ts
17054
- var WORKSPACE_SETTINGS_SPEC_VERSION = 2;
17266
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 3;
17055
17267
  var RunMode = external_exports.enum(["standalone"]);
17056
17268
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17057
17269
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17066,6 +17278,9 @@ var WorkspaceSettings = external_exports.object({
17066
17278
  policy: SimpleDetectionPolicy.default("redact"),
17067
17279
  // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
17068
17280
  historicalAccess: HistoricalAccess.default("session-only"),
17281
+ // In-place egress extraction on the scan paths; disable to stop all Data
17282
+ // Shares writes.
17283
+ dataSharesInPlace: external_exports.boolean().default(true),
17069
17284
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17070
17285
  onboardedAt: external_exports.iso.datetime().optional()
17071
17286
  });
@@ -17549,145 +17764,6 @@ var SetupHandoffOffer = external_exports.object({
17549
17764
  path: ["liveKeys"]
17550
17765
  });
17551
17766
 
17552
- // ../../packages/schema/src/zod/shares.ts
17553
- var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17554
- var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
17555
- var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
17556
- var DATA_CLASS_ORDER = DataClass.options;
17557
- var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
17558
- var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
17559
- var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
17560
- var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
17561
- var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
17562
- var ReviewInfo = external_exports.object({
17563
- needsReview: external_exports.boolean(),
17564
- reasons: external_exports.array(ReviewReason)
17565
- }).meta({ id: "ReviewInfo" });
17566
- var DestinationNetwork = external_exports.object({
17567
- port: external_exports.number().int().nullable(),
17568
- geo: external_exports.string().nullable(),
17569
- ptr: external_exports.string().nullable()
17570
- }).meta({ id: "DestinationNetwork" });
17571
- var EndpointSummary = external_exports.object({
17572
- id: external_exports.string(),
17573
- method: HttpMethod,
17574
- transport: Transport,
17575
- url: external_exports.string(),
17576
- template: external_exports.boolean(),
17577
- dataClass: DataClass,
17578
- lastSeen: external_exports.iso.datetime(),
17579
- callSiteCount: external_exports.number().int().nonnegative()
17580
- }).meta({ id: "EndpointSummary" });
17581
- var CallSite = external_exports.object({
17582
- id: external_exports.string(),
17583
- project: external_exports.string(),
17584
- file: external_exports.string(),
17585
- line: external_exports.number().int().nonnegative(),
17586
- snippet: external_exports.string(),
17587
- dynamic: external_exports.boolean(),
17588
- vendored: external_exports.boolean(),
17589
- /** Deep-link to the Inventory project, when the repo is governed there. */
17590
- projectId: external_exports.string().nullable()
17591
- }).meta({ id: "CallSite" });
17592
- var EndpointWithSites = EndpointSummary.extend({
17593
- sites: external_exports.array(CallSite)
17594
- }).meta({ id: "EndpointWithSites" });
17595
- var ShareDestinationSummary = external_exports.object({
17596
- id: external_exports.string(),
17597
- kind: DestinationKind,
17598
- name: external_exports.string(),
17599
- host: external_exports.string(),
17600
- category: external_exports.string(),
17601
- trust: ShareTrustLevel,
17602
- /** Effective state (decision applied over the trust default). */
17603
- status: EgressStatus,
17604
- /** True when an egress decision override differs from the trust default. */
17605
- isCustom: external_exports.boolean(),
17606
- lastSeen: external_exports.iso.datetime(),
17607
- endpointCount: external_exports.number().int().nonnegative(),
17608
- callSiteCount: external_exports.number().int().nonnegative(),
17609
- transports: external_exports.array(Transport),
17610
- /** Most-sensitive first. */
17611
- dataClasses: external_exports.array(DataClass),
17612
- review: ReviewInfo,
17613
- /** Non-provider hosts only; null for providers. */
17614
- network: DestinationNetwork.nullable(),
17615
- /** Embedded for inline expansion — no call sites here. */
17616
- endpoints: external_exports.array(EndpointSummary)
17617
- }).meta({ id: "ShareDestinationSummary" });
17618
- var ShareDestinationDetail = ShareDestinationSummary.omit({
17619
- endpointCount: true,
17620
- callSiteCount: true,
17621
- endpoints: true
17622
- }).extend({
17623
- /** Ownership/geo rationale; null for providers. */
17624
- note: external_exports.string().nullable(),
17625
- endpoints: external_exports.array(EndpointWithSites)
17626
- }).meta({ id: "ShareDestinationDetail" });
17627
- var ReviewDestination = external_exports.object({
17628
- id: external_exports.string(),
17629
- kind: DestinationKind,
17630
- name: external_exports.string(),
17631
- /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
17632
- host: external_exports.string(),
17633
- trust: ShareTrustLevel,
17634
- status: EgressStatus,
17635
- review: ReviewInfo,
17636
- topDataClass: DataClass,
17637
- callSiteCount: external_exports.number().int().nonnegative(),
17638
- lastSeen: external_exports.iso.datetime()
17639
- }).meta({ id: "ReviewDestination" });
17640
- var ShareDestinationGroup = external_exports.object({
17641
- kind: DestinationKind,
17642
- total: external_exports.number().int().nonnegative(),
17643
- items: external_exports.array(ShareDestinationSummary)
17644
- }).meta({ id: "ShareDestinationGroup" });
17645
- var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
17646
- var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
17647
- var SharesStats = external_exports.object({
17648
- destinations: external_exports.number().int().nonnegative(),
17649
- endpoints: external_exports.number().int().nonnegative(),
17650
- callSites: external_exports.number().int().nonnegative(),
17651
- needsReview: external_exports.number().int().nonnegative(),
17652
- insecure: external_exports.number().int().nonnegative(),
17653
- byKind: external_exports.object({
17654
- provider: external_exports.number().int().nonnegative(),
17655
- internal: external_exports.number().int().nonnegative(),
17656
- ip: external_exports.number().int().nonnegative()
17657
- }),
17658
- byTrust: external_exports.object({
17659
- recognized: external_exports.number().int().nonnegative(),
17660
- internal: external_exports.number().int().nonnegative(),
17661
- unverified: external_exports.number().int().nonnegative(),
17662
- ip: external_exports.number().int().nonnegative()
17663
- })
17664
- }).meta({ id: "SharesStats" });
17665
- var SetEgressDecisionBody = external_exports.object({
17666
- /** `null` clears the override — reverts to the trust default, isCustom false. */
17667
- decision: EgressDecision.nullable()
17668
- }).meta({ id: "SetEgressDecisionBody" });
17669
- var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
17670
- var ListShareDestinationsQuery = external_exports.object({
17671
- /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
17672
- q: external_exports.string().optional(),
17673
- /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
17674
- kind: external_exports.array(DestinationKind).optional(),
17675
- /** Reserved for future grouping modes; only 'destination' is supported today. */
17676
- groupBy: external_exports.enum(["destination"]).default("destination"),
17677
- /**
17678
- * When true, return a flat severity-ordered `items[]` instead of `groups`.
17679
- * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
17680
- * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
17681
- * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17682
- */
17683
- review: external_exports.stringbool().default(false)
17684
- });
17685
- var ExportSharesQuery = external_exports.object({
17686
- format: external_exports.enum(["csv", "json"]).default("csv"),
17687
- q: external_exports.string().optional(),
17688
- kind: external_exports.array(DestinationKind).optional()
17689
- });
17690
-
17691
17767
  // ../../packages/schema/src/zod/shares-access.ts
17692
17768
  var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
17693
17769
  function trustDefaultStatus(trust) {
@@ -17707,7 +17783,7 @@ function deriveReviewReasons(trust, transports) {
17707
17783
  const reasons = [];
17708
17784
  if (trust === "ip") reasons.push("raw_ip");
17709
17785
  if (trust === "unverified") reasons.push("unverified_domain");
17710
- if (transports.includes("http")) reasons.push("plaintext_transport");
17786
+ if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
17711
17787
  return reasons;
17712
17788
  }
17713
17789
  function buildReviewInfo(trust, transports) {
@@ -17938,6 +18014,7 @@ function applyMigrations(db) {
17938
18014
  ensureSyncedAtColumn(db, "audit_events");
17939
18015
  ensureScanLedgerTable(db);
17940
18016
  ensureBlockedDetectionsTable(db);
18017
+ ensureRuleProbeCacheTable(db);
17941
18018
  ensureWriteGateTrigger(db);
17942
18019
  ensureTokenUsageColumns(db);
17943
18020
  reconcileSourceProjectIds(db);
@@ -18077,6 +18154,14 @@ function ensureBlockedDetectionsTable(db) {
18077
18154
  blocked_at INTEGER NOT NULL
18078
18155
  )`);
18079
18156
  }
18157
+ function ensureRuleProbeCacheTable(db) {
18158
+ db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
18159
+ rule_key TEXT PRIMARY KEY,
18160
+ verdict TEXT NOT NULL,
18161
+ worst_probe_ms REAL NOT NULL,
18162
+ checked_at INTEGER NOT NULL
18163
+ )`);
18164
+ }
18080
18165
 
18081
18166
  // ../../packages/persistence/src/paths.ts
18082
18167
  import { chmodSync, mkdirSync } from "fs";
@@ -21630,6 +21715,35 @@ var SqliteResolutionsRepository = class {
21630
21715
  }
21631
21716
  };
21632
21717
 
21718
+ // ../../packages/persistence/src/repositories/rule-probe-cache.ts
21719
+ var SqliteRuleProbeCacheRepository = class {
21720
+ constructor(db) {
21721
+ this.db = db;
21722
+ this.upsertStmt = db.prepare(
21723
+ `INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
21724
+ VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
21725
+ ON CONFLICT (rule_key) DO UPDATE SET
21726
+ verdict = excluded.verdict,
21727
+ worst_probe_ms = excluded.worst_probe_ms,
21728
+ checked_at = excluded.checked_at`
21729
+ );
21730
+ this.readStmt = db.prepare(
21731
+ `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
21732
+ );
21733
+ }
21734
+ db;
21735
+ upsertStmt;
21736
+ readStmt;
21737
+ getVerdict(ruleKey) {
21738
+ return getRow(this.readStmt, { ruleKey });
21739
+ }
21740
+ setVerdict(ruleKey, verdict, worstProbeMs2) {
21741
+ failOpenTransaction(this.db, () => {
21742
+ this.upsertStmt.run({ ruleKey, verdict, worstProbeMs: worstProbeMs2, checkedAt: Date.now() });
21743
+ });
21744
+ }
21745
+ };
21746
+
21633
21747
  // ../../packages/persistence/src/repositories/scan-ledger.ts
21634
21748
  var SqliteScanLedgerRepository = class {
21635
21749
  constructor(db) {
@@ -22041,11 +22155,50 @@ var SqliteSecurityRepository = class {
22041
22155
 
22042
22156
  // ../../packages/persistence/src/repositories/shares.ts
22043
22157
  import { randomUUID as randomUUID7 } from "crypto";
22044
- var KIND_ORDER = ["provider", "internal", "ip"];
22158
+ var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22159
+ var IN_CHUNK = 500;
22160
+ var KIND_ORDER = ["provider", "internal", "external", "ip"];
22161
+ var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
22162
+ var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
22163
+ LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
22045
22164
  var CALL_SITE_EMBED_CAP = 200;
22046
22165
  function parseNetwork(networkJson) {
22047
22166
  return safeJson(networkJson, null);
22048
22167
  }
22168
+ function capHits(all, mode) {
22169
+ if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
22170
+ return { hits: [...all], droppedFiles: [], truncated: false };
22171
+ }
22172
+ if (mode === "walk") {
22173
+ return {
22174
+ hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
22175
+ droppedFiles: [],
22176
+ truncated: true
22177
+ };
22178
+ }
22179
+ const byFile = /* @__PURE__ */ new Map();
22180
+ for (const hit of all) {
22181
+ const bucket = byFile.get(hit.site.file);
22182
+ if (bucket === void 0) byFile.set(hit.site.file, [hit]);
22183
+ else bucket.push(hit);
22184
+ }
22185
+ const hits = [];
22186
+ const droppedFiles = [];
22187
+ for (const [file2, bucket] of byFile) {
22188
+ if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
22189
+ else hits.push(...bucket);
22190
+ }
22191
+ return { hits, droppedFiles, truncated: true };
22192
+ }
22193
+ function withoutDroppedFiles(reconcile, droppedFiles) {
22194
+ if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
22195
+ const dropped = new Set(droppedFiles);
22196
+ return {
22197
+ mode: "ledger",
22198
+ scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
22199
+ deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
22200
+ };
22201
+ }
22049
22202
  function toEndpointSummary(row) {
22050
22203
  return {
22051
22204
  id: row.id,
@@ -22136,13 +22289,15 @@ var SqliteSharesRepository = class {
22136
22289
  const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22137
22290
  const insecure = countScalar(
22138
22291
  this.db,
22139
- "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
22292
+ `SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
22293
+ WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
22140
22294
  );
22141
22295
  const needsReview = countScalar(
22142
22296
  this.db,
22143
22297
  `SELECT count(DISTINCT d.id) AS n
22144
22298
  FROM share_destination d
22145
- LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
22299
+ LEFT JOIN share_endpoint e ON e.destination_id = d.id
22300
+ AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
22146
22301
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
22147
22302
  );
22148
22303
  const kindCounts = countBy(
@@ -22152,6 +22307,7 @@ var SqliteSharesRepository = class {
22152
22307
  const byKind = {
22153
22308
  provider: kindCounts.get("provider") ?? 0,
22154
22309
  internal: kindCounts.get("internal") ?? 0,
22310
+ external: kindCounts.get("external") ?? 0,
22155
22311
  ip: kindCounts.get("ip") ?? 0
22156
22312
  };
22157
22313
  const trustCounts = countBy(
@@ -22227,23 +22383,316 @@ var SqliteSharesRepository = class {
22227
22383
  // real edit from a no-such-destination.
22228
22384
  /**
22229
22385
  * Set (decision) or clear (null) the egress decision override for a destination.
22230
- * `null` deletes the override row → reverts to the trust default.
22386
+ * `null` deletes the override rows → reverts to the trust default.
22387
+ *
22388
+ * The written row carries both the destination id and its host, so the
22389
+ * decision re-attaches by host after the destination is pruned and
22390
+ * re-detected under a fresh id. Rows written before the host column existed
22391
+ * (host NULL, matched by destination id) are replaced rather than left to
22392
+ * shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
22393
+ * would otherwise race a concurrent prune.
22231
22394
  */
22232
22395
  setEgressDecision(destinationId, decision) {
22233
- const exists = this.db.prepare("SELECT 1 FROM share_destination WHERE id = ?").get(destinationId);
22234
- if (exists === void 0) return false;
22235
- if (decision === null) {
22236
- this.db.prepare("DELETE FROM egress_decision_override WHERE destination_id = ?").run(destinationId);
22237
- return true;
22396
+ let existed = false;
22397
+ withTransaction(
22398
+ this.db,
22399
+ () => {
22400
+ const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
22401
+ if (dest === void 0) return;
22402
+ existed = true;
22403
+ this.db.prepare(
22404
+ `DELETE FROM egress_decision_override
22405
+ WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
22406
+ ).run({ host: dest.host, destinationId });
22407
+ if (decision === null) return;
22408
+ this.db.prepare(
22409
+ `INSERT INTO egress_decision_override
22410
+ (id, destination_id, host, decision, created_at, updated_at)
22411
+ VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22412
+ ).run({
22413
+ id: randomUUID7(),
22414
+ destinationId,
22415
+ host: dest.host,
22416
+ decision,
22417
+ now: Date.now()
22418
+ });
22419
+ },
22420
+ "IMMEDIATE"
22421
+ );
22422
+ return existed;
22423
+ }
22424
+ /**
22425
+ * Record one project's statically-extracted egress: reconcile the previously
22426
+ * stored call sites against this scan, upsert destination → endpoint → call
22427
+ * site for every hit, confirm `last_seen` on everything the project still
22428
+ * references, and drop what no longer has evidence.
22429
+ *
22430
+ * Reconciliation keys on `projectKey` alone; `project` and `projectId` are
22431
+ * display payload and never scope a delete. The whole write is one
22432
+ * transaction: a failure leaves the project's previous inventory exactly as
22433
+ * it was, and THROWS rather than reporting a partial write — callers decide
22434
+ * their own fail-open behavior, and the scanner additionally withholds its
22435
+ * ledger commit so the next scan retries.
22436
+ *
22437
+ * Over-cap input is truncated at a FILE boundary, and the files that lost
22438
+ * their hits are both excluded from the reconcile delete and named in
22439
+ * `droppedFiles`. That pairing is what keeps truncation non-destructive on
22440
+ * the ledger path: a dropped file keeps whatever rows it already had, and its
22441
+ * caller withholds the ledger entry so the next scan reads it again.
22442
+ */
22443
+ recordProjectEgress(input) {
22444
+ const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
22445
+ const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
22446
+ const now = Date.now();
22447
+ let summary = {
22448
+ destinations: 0,
22449
+ endpoints: 0,
22450
+ callSites: 0,
22451
+ truncated,
22452
+ droppedFiles
22453
+ };
22454
+ withTransaction(
22455
+ this.db,
22456
+ () => {
22457
+ const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
22458
+ this.reconcileCallSites(input.projectKey, reconcile);
22459
+ this.upsertHits(input, hits, projectId, now);
22460
+ this.confirmLastSeen(input.projectKey, now);
22461
+ this.pruneOrphans();
22462
+ summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
22463
+ },
22464
+ "IMMEDIATE"
22465
+ );
22466
+ return summary;
22467
+ }
22468
+ // ─── Egress write internals ──────────────────────────────────────────────────
22469
+ /**
22470
+ * Clear the stored call sites this scan is responsible for re-creating.
22471
+ *
22472
+ * Each pipeline may only delete rows its own walker could have produced. The
22473
+ * fs walk behind 'walk' mode never descends into dot-directories, so its
22474
+ * delete excludes dot-path files — those rows are the plugin scanner's to
22475
+ * reconcile, and deleting them here would make the two pipelines erase each
22476
+ * other's rows on every alternating scan. 'ledger' mode names its files
22477
+ * outright and never mass-deletes, so rows the fs walk contributed for files
22478
+ * the scanner skips (vendored, oversize) survive it.
22479
+ */
22480
+ reconcileCallSites(projectKey, reconcile) {
22481
+ if (reconcile.mode === "walk") {
22482
+ const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
22483
+ this.db.prepare(
22484
+ `DELETE FROM share_call_site
22485
+ WHERE project_key = :key
22486
+ AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
22487
+ AND file NOT LIKE '.%'
22488
+ AND file NOT LIKE '%/.%'`
22489
+ ).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
22490
+ return;
22491
+ }
22492
+ const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
22493
+ for (let i = 0; i < files.length; i += IN_CHUNK) {
22494
+ const chunk = files.slice(i, i + IN_CHUNK);
22495
+ this.db.prepare(
22496
+ `DELETE FROM share_call_site
22497
+ WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
22498
+ ).run(projectKey, ...chunk);
22238
22499
  }
22500
+ }
22501
+ /**
22502
+ * Upsert every hit as destination → endpoint → call site. Destinations key on
22503
+ * `host` and endpoints on `(destination_id, method, url)`, both shared across
22504
+ * projects; only the call site carries `project_key`. A destination's `note`
22505
+ * is user-owned and never overwritten. The id caches keep one upsert per
22506
+ * distinct host and endpoint, so the first hit for a host supplies its
22507
+ * classification for this batch.
22508
+ */
22509
+ upsertHits(input, hits, projectId, now) {
22510
+ if (hits.length === 0) return;
22511
+ const destStmt = this.db.prepare(
22512
+ `INSERT INTO share_destination
22513
+ (id, kind, name, host, category, trust, network_json, last_seen, provenance,
22514
+ created_at, updated_at)
22515
+ VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
22516
+ ON CONFLICT (host) DO UPDATE SET
22517
+ kind = excluded.kind,
22518
+ name = excluded.name,
22519
+ category = excluded.category,
22520
+ trust = excluded.trust,
22521
+ network_json = excluded.network_json,
22522
+ last_seen = excluded.last_seen,
22523
+ updated_at = excluded.updated_at`
22524
+ );
22525
+ const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
22526
+ const endpointStmt = this.db.prepare(
22527
+ `INSERT INTO share_endpoint
22528
+ (id, destination_id, method, transport, url, template, data_class, last_seen,
22529
+ created_at, updated_at)
22530
+ VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
22531
+ :now, :now)
22532
+ ON CONFLICT (destination_id, method, url) DO UPDATE SET
22533
+ transport = excluded.transport,
22534
+ template = excluded.template,
22535
+ data_class = excluded.data_class,
22536
+ last_seen = excluded.last_seen,
22537
+ updated_at = excluded.updated_at`
22538
+ );
22539
+ const endpointIdStmt = this.db.prepare(
22540
+ "SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
22541
+ );
22542
+ const siteStmt = this.db.prepare(
22543
+ `INSERT INTO share_call_site
22544
+ (id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
22545
+ project_id, created_at, updated_at)
22546
+ VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
22547
+ :vendored, :projectId, :now, :now)
22548
+ ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
22549
+ snippet = excluded.snippet,
22550
+ dynamic = excluded.dynamic,
22551
+ vendored = excluded.vendored,
22552
+ project = excluded.project,
22553
+ project_id = COALESCE(excluded.project_id, share_call_site.project_id),
22554
+ updated_at = excluded.updated_at`
22555
+ );
22556
+ const destIds = /* @__PURE__ */ new Map();
22557
+ const endpointIds = /* @__PURE__ */ new Map();
22558
+ for (const hit of hits) {
22559
+ let destinationId = destIds.get(hit.host);
22560
+ if (destinationId === void 0) {
22561
+ destStmt.run({
22562
+ id: randomUUID7(),
22563
+ kind: hit.kind,
22564
+ name: hit.name,
22565
+ host: hit.host,
22566
+ category: hit.category,
22567
+ trust: hit.trust,
22568
+ networkJson: hit.network === null ? null : JSON.stringify(hit.network),
22569
+ now
22570
+ });
22571
+ destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
22572
+ destIds.set(hit.host, destinationId);
22573
+ }
22574
+ const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
22575
+ let endpointId = endpointIds.get(endpointKey);
22576
+ if (endpointId === void 0) {
22577
+ endpointStmt.run({
22578
+ id: randomUUID7(),
22579
+ destinationId,
22580
+ method: hit.method,
22581
+ transport: hit.transport,
22582
+ url: hit.url,
22583
+ template: boolToInt(hit.template),
22584
+ dataClass: hit.dataClass,
22585
+ now
22586
+ });
22587
+ endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
22588
+ endpointIds.set(endpointKey, endpointId);
22589
+ }
22590
+ siteStmt.run({
22591
+ id: randomUUID7(),
22592
+ endpointId,
22593
+ project: input.project,
22594
+ projectKey: input.projectKey,
22595
+ file: hit.site.file,
22596
+ line: hit.site.line,
22597
+ snippet: hit.site.snippet,
22598
+ dynamic: boolToInt(hit.site.dynamic),
22599
+ vendored: boolToInt(hit.site.vendored),
22600
+ projectId,
22601
+ now
22602
+ });
22603
+ }
22604
+ }
22605
+ /**
22606
+ * The source-project id this project's stored call sites already carry, if
22607
+ * any. Only the pipeline that resolves a source project supplies one; the
22608
+ * other passes null and inherits this, so the link stops flapping between a
22609
+ * real id and NULL depending on which pipeline ran last. The value is a
22610
+ * per-project attribute stored redundantly on each row, so any row's is
22611
+ * representative.
22612
+ */
22613
+ knownProjectId(projectKey) {
22614
+ return getRow(
22615
+ this.db.prepare(
22616
+ `SELECT project_id AS projectId FROM share_call_site
22617
+ WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
22618
+ ),
22619
+ [projectKey]
22620
+ )?.projectId ?? null;
22621
+ }
22622
+ /**
22623
+ * Stamp `last_seen` on every endpoint and destination this project still
22624
+ * references — including rows the scan preserved rather than re-wrote, so a
22625
+ * ledger-skipped file's references don't decay into "stale" on the page.
22626
+ */
22627
+ confirmLastSeen(projectKey, now) {
22239
22628
  this.db.prepare(
22240
- `INSERT INTO egress_decision_override (id, destination_id, decision, created_at, updated_at)
22241
- VALUES (:id, :destinationId, :decision, :now, :now)
22242
- ON CONFLICT (destination_id) DO UPDATE SET
22243
- decision = excluded.decision,
22244
- updated_at = excluded.updated_at`
22245
- ).run({ id: randomUUID7(), destinationId, decision, now: Date.now() });
22246
- return true;
22629
+ `UPDATE share_endpoint SET last_seen = :now, updated_at = :now
22630
+ WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
22631
+ ).run({ now, key: projectKey });
22632
+ this.db.prepare(
22633
+ `UPDATE share_destination SET last_seen = :now, updated_at = :now
22634
+ WHERE id IN (SELECT DISTINCT e.destination_id
22635
+ FROM share_endpoint e
22636
+ JOIN share_call_site c ON c.endpoint_id = e.id
22637
+ WHERE c.project_key = :key)`
22638
+ ).run({ now, key: projectKey });
22639
+ }
22640
+ /**
22641
+ * Drop rows left without evidence: endpoints with no call site, then
22642
+ * destinations with no endpoint. Call sites are the only evidence either one
22643
+ * has, so a row that lost its last one belongs to no project any more.
22644
+ *
22645
+ * Overrides are deleted between the two steps, and only the ones written
22646
+ * before the host column existed. Those match a destination by id alone;
22647
+ * because the id link is released on delete rather than cascading, leaving
22648
+ * them would accumulate rows that match neither join arm and that nothing can
22649
+ * reach again. Host-bearing rows deliberately survive — the host is what
22650
+ * re-attaches a user's decision when the destination comes back.
22651
+ */
22652
+ pruneOrphans() {
22653
+ this.db.exec(
22654
+ `DELETE FROM share_endpoint
22655
+ WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
22656
+ );
22657
+ this.db.exec(
22658
+ `DELETE FROM egress_decision_override
22659
+ WHERE host IS NULL
22660
+ AND destination_id IN (
22661
+ SELECT d.id FROM share_destination d
22662
+ WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
22663
+ );
22664
+ this.db.exec(
22665
+ `DELETE FROM share_destination
22666
+ WHERE NOT EXISTS (
22667
+ SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
22668
+ );
22669
+ }
22670
+ /**
22671
+ * Live totals for one project. Destinations and endpoints are shared across
22672
+ * projects and carry no project column, so both are counted through the call
22673
+ * sites that reference them.
22674
+ */
22675
+ projectTotals(projectKey) {
22676
+ return {
22677
+ destinations: countScalar(
22678
+ this.db,
22679
+ `SELECT count(DISTINCT e.destination_id) AS n
22680
+ FROM share_endpoint e
22681
+ JOIN share_call_site c ON c.endpoint_id = e.id
22682
+ WHERE c.project_key = ?`,
22683
+ [projectKey]
22684
+ ),
22685
+ endpoints: countScalar(
22686
+ this.db,
22687
+ "SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
22688
+ [projectKey]
22689
+ ),
22690
+ callSites: countScalar(
22691
+ this.db,
22692
+ "SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
22693
+ [projectKey]
22694
+ )
22695
+ };
22247
22696
  }
22248
22697
  // ─── Raw fetchers ────────────────────────────────────────────────────────────
22249
22698
  mapDestRow(r) {
@@ -22263,7 +22712,8 @@ var SqliteSharesRepository = class {
22263
22712
  fetchDestinations(q, kinds, reviewOnly = false) {
22264
22713
  const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22265
22714
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22266
- d.created_at AS createdAt, o.decision AS overrideDecision`;
22715
+ d.created_at AS createdAt,
22716
+ COALESCE(oh.decision, ol.decision) AS overrideDecision`;
22267
22717
  const conditions = [];
22268
22718
  const params = [];
22269
22719
  if (kinds && kinds.length > 0) {
@@ -22274,7 +22724,8 @@ var SqliteSharesRepository = class {
22274
22724
  conditions.push(
22275
22725
  `(d.trust IN ('unverified', 'ip')
22276
22726
  OR EXISTS (SELECT 1 FROM share_endpoint re
22277
- WHERE re.destination_id = d.id AND re.transport = 'http'))`
22727
+ WHERE re.destination_id = d.id
22728
+ AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
22278
22729
  );
22279
22730
  }
22280
22731
  let sql;
@@ -22287,7 +22738,7 @@ var SqliteSharesRepository = class {
22287
22738
  params.push(pattern, pattern, pattern, pattern, pattern);
22288
22739
  sql = `SELECT DISTINCT ${cols}
22289
22740
  FROM share_destination d
22290
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22741
+ ${OVERRIDE_JOIN}
22291
22742
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
22292
22743
  LEFT JOIN share_call_site c ON c.endpoint_id = e.id
22293
22744
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
@@ -22295,7 +22746,7 @@ var SqliteSharesRepository = class {
22295
22746
  } else {
22296
22747
  sql = `SELECT ${cols}
22297
22748
  FROM share_destination d
22298
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22749
+ ${OVERRIDE_JOIN}
22299
22750
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
22300
22751
  ORDER BY d.created_at ASC, d.id ASC`;
22301
22752
  }
@@ -22310,9 +22761,9 @@ var SqliteSharesRepository = class {
22310
22761
  this.db.prepare(
22311
22762
  `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22312
22763
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22313
- o.decision AS overrideDecision
22764
+ COALESCE(oh.decision, ol.decision) AS overrideDecision
22314
22765
  FROM share_destination d
22315
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22766
+ ${OVERRIDE_JOIN}
22316
22767
  WHERE d.id = ?`
22317
22768
  ),
22318
22769
  [destinationId]
@@ -22541,6 +22992,7 @@ function openLocalDatabase(dir) {
22541
22992
  const scanLedger = new SqliteScanLedgerRepository(db);
22542
22993
  const exceptions = new SqliteExceptionsRepository(db);
22543
22994
  const resolutions = new SqliteResolutionsRepository(db);
22995
+ const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
22544
22996
  const security = new SqliteSecurityRepository(db);
22545
22997
  const detections = new SqliteDetectionsRepository(db);
22546
22998
  const shares = new SqliteSharesRepository(db);
@@ -22678,6 +23130,7 @@ function openLocalDatabase(dir) {
22678
23130
  scanLedger,
22679
23131
  exceptions,
22680
23132
  resolutions,
23133
+ ruleProbeCache,
22681
23134
  security,
22682
23135
  detections,
22683
23136
  shares,
@@ -22918,13 +23371,581 @@ import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as s
22918
23371
  import { homedir as homedir2 } from "os";
22919
23372
  import { basename as basename2, join as join7 } from "path";
22920
23373
 
23374
+ // ../../packages/detections/src/egress/registry.ts
23375
+ var EXTRACTOR_VERSION = "1";
23376
+ var PROVIDER_REGISTRY = [
23377
+ {
23378
+ id: "stripe",
23379
+ name: "Stripe",
23380
+ category: "Payments",
23381
+ hostSuffixes: ["stripe.com"],
23382
+ apiBase: "https://api.stripe.com",
23383
+ defaultDataClasses: ["pii", "customer"],
23384
+ sdks: {
23385
+ npm: ["stripe"],
23386
+ pypi: ["stripe"],
23387
+ go: ["github.com/stripe/stripe-go"],
23388
+ maven: ["com.stripe"],
23389
+ rubygems: ["stripe"],
23390
+ composer: ["stripe/stripe-php"],
23391
+ nuget: ["Stripe.net"]
23392
+ }
23393
+ },
23394
+ {
23395
+ id: "datadog",
23396
+ name: "Datadog",
23397
+ category: "Observability",
23398
+ hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
23399
+ apiBase: "https://api.datadoghq.com",
23400
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23401
+ sdks: {
23402
+ npm: ["dd-trace", "@datadog/browser-logs"],
23403
+ pypi: ["datadog", "ddtrace"],
23404
+ go: ["github.com/DataDog/dd-trace-go"],
23405
+ maven: ["com.datadoghq"],
23406
+ rubygems: ["ddtrace", "dogapi"],
23407
+ nuget: ["Datadog.Trace"]
23408
+ }
23409
+ },
23410
+ {
23411
+ id: "newrelic",
23412
+ name: "New Relic",
23413
+ category: "Observability",
23414
+ hostSuffixes: ["newrelic.com", "nr-data.net"],
23415
+ apiBase: "https://api.newrelic.com",
23416
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23417
+ sdks: {
23418
+ npm: ["newrelic"],
23419
+ pypi: ["newrelic"],
23420
+ go: ["github.com/newrelic/go-agent"],
23421
+ maven: ["com.newrelic.agent.java"],
23422
+ rubygems: ["newrelic_rpm"],
23423
+ nuget: ["NewRelic.Agent"]
23424
+ }
23425
+ },
23426
+ {
23427
+ id: "sentry",
23428
+ name: "Sentry",
23429
+ category: "Error tracking",
23430
+ hostSuffixes: ["sentry.io"],
23431
+ apiBase: "https://sentry.io",
23432
+ defaultDataClasses: ["source", "telemetry"],
23433
+ sdks: {
23434
+ npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
23435
+ pypi: ["sentry-sdk"],
23436
+ go: ["github.com/getsentry/sentry-go"],
23437
+ maven: ["io.sentry"],
23438
+ rubygems: ["sentry-ruby"],
23439
+ cargo: ["sentry"],
23440
+ composer: ["sentry/sentry"],
23441
+ nuget: ["Sentry"]
23442
+ }
23443
+ },
23444
+ {
23445
+ id: "openai",
23446
+ name: "OpenAI",
23447
+ category: "LLM provider",
23448
+ hostSuffixes: ["openai.com"],
23449
+ apiBase: "https://api.openai.com",
23450
+ defaultDataClasses: ["pii", "source"],
23451
+ sdks: {
23452
+ npm: ["openai"],
23453
+ pypi: ["openai"],
23454
+ go: ["github.com/sashabaranov/go-openai"],
23455
+ maven: ["com.openai"],
23456
+ rubygems: ["ruby-openai"],
23457
+ cargo: ["async-openai"],
23458
+ composer: ["openai-php/client"],
23459
+ nuget: ["OpenAI"]
23460
+ }
23461
+ },
23462
+ {
23463
+ id: "anthropic",
23464
+ name: "Anthropic",
23465
+ category: "LLM provider",
23466
+ hostSuffixes: ["anthropic.com"],
23467
+ apiBase: "https://api.anthropic.com",
23468
+ defaultDataClasses: ["pii", "source"],
23469
+ sdks: {
23470
+ npm: ["@anthropic-ai/sdk"],
23471
+ pypi: ["anthropic"],
23472
+ go: ["github.com/anthropics/anthropic-sdk-go"],
23473
+ nuget: ["Anthropic.SDK"]
23474
+ }
23475
+ },
23476
+ {
23477
+ id: "aws",
23478
+ name: "Amazon Web Services",
23479
+ category: "Cloud platform",
23480
+ hostSuffixes: ["amazonaws.com"],
23481
+ apiBase: "https://s3.amazonaws.com",
23482
+ defaultDataClasses: ["secrets", "customer"],
23483
+ sdks: {
23484
+ npm: ["@aws-sdk/client-s3", "aws-sdk"],
23485
+ pypi: ["boto3"],
23486
+ go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
23487
+ maven: ["com.amazonaws", "software.amazon.awssdk"],
23488
+ rubygems: ["aws-sdk-s3"],
23489
+ cargo: ["aws-sdk-s3"],
23490
+ nuget: ["AWSSDK.S3"]
23491
+ }
23492
+ },
23493
+ {
23494
+ id: "gcp",
23495
+ name: "Google Cloud",
23496
+ category: "Cloud platform",
23497
+ hostSuffixes: ["googleapis.com"],
23498
+ apiBase: "https://storage.googleapis.com",
23499
+ defaultDataClasses: ["customer", "logs"],
23500
+ sdks: {
23501
+ npm: ["@google-cloud/storage"],
23502
+ pypi: ["google-cloud-storage"],
23503
+ go: ["cloud.google.com/go"],
23504
+ maven: ["com.google.cloud"],
23505
+ rubygems: ["google-cloud-storage"],
23506
+ nuget: ["Google.Cloud.Storage.V1"]
23507
+ }
23508
+ },
23509
+ {
23510
+ id: "azure",
23511
+ name: "Microsoft Azure",
23512
+ category: "Cloud platform",
23513
+ hostSuffixes: ["azure.com", "windows.net"],
23514
+ apiBase: "https://management.azure.com",
23515
+ defaultDataClasses: ["customer", "logs"],
23516
+ sdks: {
23517
+ npm: ["@azure/storage-blob"],
23518
+ pypi: ["azure-storage-blob"],
23519
+ go: ["github.com/Azure/azure-sdk-for-go"],
23520
+ maven: ["com.azure"],
23521
+ rubygems: ["azure-storage-blob"],
23522
+ nuget: ["Azure.Storage.Blobs"]
23523
+ }
23524
+ },
23525
+ {
23526
+ id: "slack",
23527
+ name: "Slack",
23528
+ category: "Notifications",
23529
+ hostSuffixes: ["slack.com"],
23530
+ apiBase: "https://slack.com/api",
23531
+ defaultDataClasses: ["logs"],
23532
+ sdks: {
23533
+ npm: ["@slack/web-api"],
23534
+ pypi: ["slack-sdk"],
23535
+ go: ["github.com/slack-go/slack"],
23536
+ maven: ["com.slack.api"],
23537
+ rubygems: ["slack-ruby-client"],
23538
+ composer: ["slack-php/slack-api"],
23539
+ nuget: ["SlackNet"]
23540
+ }
23541
+ },
23542
+ {
23543
+ id: "segment",
23544
+ name: "Segment",
23545
+ category: "Analytics",
23546
+ hostSuffixes: ["segment.io", "segment.com"],
23547
+ apiBase: "https://api.segment.io",
23548
+ defaultDataClasses: ["customer"],
23549
+ sdks: {
23550
+ npm: ["@segment/analytics-node", "analytics-node"],
23551
+ pypi: ["segment-analytics-python"],
23552
+ go: ["github.com/segmentio/analytics-go"],
23553
+ maven: ["com.segment.analytics.java"],
23554
+ rubygems: ["analytics-ruby"],
23555
+ nuget: ["Analytics"]
23556
+ }
23557
+ },
23558
+ {
23559
+ id: "twilio",
23560
+ name: "Twilio",
23561
+ category: "Communications",
23562
+ hostSuffixes: ["twilio.com"],
23563
+ apiBase: "https://api.twilio.com",
23564
+ defaultDataClasses: ["pii", "customer"],
23565
+ sdks: {
23566
+ npm: ["twilio"],
23567
+ pypi: ["twilio"],
23568
+ go: ["github.com/twilio/twilio-go"],
23569
+ maven: ["com.twilio.sdk"],
23570
+ rubygems: ["twilio-ruby"],
23571
+ composer: ["twilio/sdk"],
23572
+ nuget: ["Twilio"]
23573
+ }
23574
+ },
23575
+ {
23576
+ id: "sendgrid",
23577
+ name: "SendGrid",
23578
+ category: "Email",
23579
+ hostSuffixes: ["sendgrid.com"],
23580
+ apiBase: "https://api.sendgrid.com",
23581
+ defaultDataClasses: ["pii"],
23582
+ sdks: {
23583
+ npm: ["@sendgrid/mail"],
23584
+ pypi: ["sendgrid"],
23585
+ go: ["github.com/sendgrid/sendgrid-go"],
23586
+ maven: ["com.sendgrid"],
23587
+ rubygems: ["sendgrid-ruby"],
23588
+ composer: ["sendgrid/sendgrid"],
23589
+ nuget: ["SendGrid"]
23590
+ }
23591
+ },
23592
+ {
23593
+ id: "mailgun",
23594
+ name: "Mailgun",
23595
+ category: "Email",
23596
+ hostSuffixes: ["mailgun.net"],
23597
+ apiBase: "https://api.mailgun.net",
23598
+ defaultDataClasses: ["pii"],
23599
+ sdks: {
23600
+ npm: ["mailgun.js"],
23601
+ pypi: ["mailgun"],
23602
+ rubygems: ["mailgun-ruby"],
23603
+ composer: ["mailgun/mailgun-php"],
23604
+ nuget: ["Mailgun"]
23605
+ }
23606
+ },
23607
+ {
23608
+ id: "mixpanel",
23609
+ name: "Mixpanel",
23610
+ category: "Analytics",
23611
+ hostSuffixes: ["mixpanel.com"],
23612
+ apiBase: "https://api.mixpanel.com",
23613
+ defaultDataClasses: ["customer", "telemetry"],
23614
+ sdks: {
23615
+ npm: ["mixpanel"],
23616
+ pypi: ["mixpanel"],
23617
+ rubygems: ["mixpanel-ruby"],
23618
+ nuget: ["Mixpanel"]
23619
+ }
23620
+ },
23621
+ {
23622
+ id: "amplitude",
23623
+ name: "Amplitude",
23624
+ category: "Analytics",
23625
+ hostSuffixes: ["amplitude.com"],
23626
+ apiBase: "https://api2.amplitude.com",
23627
+ defaultDataClasses: ["customer", "telemetry"],
23628
+ sdks: {
23629
+ npm: ["@amplitude/analytics-node"],
23630
+ pypi: ["amplitude-analytics"],
23631
+ nuget: ["Amplitude"]
23632
+ }
23633
+ },
23634
+ {
23635
+ id: "posthog",
23636
+ name: "PostHog",
23637
+ category: "Analytics",
23638
+ hostSuffixes: ["posthog.com"],
23639
+ apiBase: "https://us.i.posthog.com",
23640
+ defaultDataClasses: ["customer", "telemetry"],
23641
+ sdks: {
23642
+ npm: ["posthog-node", "posthog-js"],
23643
+ pypi: ["posthog"],
23644
+ go: ["github.com/posthog/posthog-go"],
23645
+ rubygems: ["posthog-ruby"],
23646
+ composer: ["posthog/posthog-php"],
23647
+ nuget: ["PostHog"]
23648
+ }
23649
+ },
23650
+ {
23651
+ id: "honeycomb",
23652
+ name: "Honeycomb",
23653
+ category: "Observability",
23654
+ hostSuffixes: ["honeycomb.io"],
23655
+ apiBase: "https://api.honeycomb.io",
23656
+ defaultDataClasses: ["telemetry", "metrics"],
23657
+ sdks: {
23658
+ npm: ["libhoney"],
23659
+ pypi: ["libhoney"],
23660
+ go: ["github.com/honeycombio/libhoney-go"],
23661
+ rubygems: ["libhoney"]
23662
+ }
23663
+ },
23664
+ {
23665
+ id: "grafana",
23666
+ name: "Grafana Cloud",
23667
+ category: "Observability",
23668
+ hostSuffixes: ["grafana.net"],
23669
+ apiBase: "https://grafana.net",
23670
+ defaultDataClasses: ["logs", "metrics"],
23671
+ sdks: {
23672
+ npm: ["@grafana/faro-web-sdk"]
23673
+ }
23674
+ },
23675
+ {
23676
+ id: "splunk",
23677
+ name: "Splunk",
23678
+ category: "Observability",
23679
+ hostSuffixes: ["splunkcloud.com", "splunk.com"],
23680
+ apiBase: "https://http-inputs.splunkcloud.com",
23681
+ defaultDataClasses: ["logs"],
23682
+ sdks: {
23683
+ npm: ["splunk-logging"],
23684
+ pypi: ["splunk-sdk"],
23685
+ maven: ["com.splunk"],
23686
+ nuget: ["Splunk.Logging.Common"]
23687
+ }
23688
+ },
23689
+ {
23690
+ id: "pagerduty",
23691
+ name: "PagerDuty",
23692
+ category: "Incident response",
23693
+ hostSuffixes: ["pagerduty.com"],
23694
+ apiBase: "https://api.pagerduty.com",
23695
+ defaultDataClasses: ["logs"],
23696
+ sdks: {
23697
+ npm: ["@pagerduty/pdjs"],
23698
+ pypi: ["pdpyras"],
23699
+ go: ["github.com/PagerDuty/go-pagerduty"],
23700
+ rubygems: ["pagerduty"]
23701
+ }
23702
+ },
23703
+ {
23704
+ id: "github",
23705
+ name: "GitHub",
23706
+ category: "Developer platform",
23707
+ hostSuffixes: ["github.com", "githubusercontent.com"],
23708
+ apiBase: "https://api.github.com",
23709
+ defaultDataClasses: ["source"],
23710
+ sdks: {
23711
+ npm: ["@octokit/rest", "octokit"],
23712
+ pypi: ["pygithub"],
23713
+ go: ["github.com/google/go-github"],
23714
+ maven: ["org.kohsuke.github-api"],
23715
+ rubygems: ["octokit"],
23716
+ cargo: ["octocrab"],
23717
+ composer: ["knplabs/github-api"],
23718
+ nuget: ["Octokit"]
23719
+ }
23720
+ },
23721
+ {
23722
+ id: "gitlab",
23723
+ name: "GitLab",
23724
+ category: "Developer platform",
23725
+ hostSuffixes: ["gitlab.com"],
23726
+ apiBase: "https://gitlab.com/api",
23727
+ defaultDataClasses: ["source"],
23728
+ sdks: {
23729
+ npm: ["@gitbeaker/rest"],
23730
+ pypi: ["python-gitlab"],
23731
+ go: ["gitlab.com/gitlab-org/api/client-go"],
23732
+ rubygems: ["gitlab"],
23733
+ nuget: ["GitLabApiClient"]
23734
+ }
23735
+ },
23736
+ {
23737
+ id: "auth0",
23738
+ name: "Auth0",
23739
+ category: "Identity",
23740
+ hostSuffixes: ["auth0.com"],
23741
+ apiBase: "https://login.auth0.com",
23742
+ defaultDataClasses: ["pii"],
23743
+ sdks: {
23744
+ npm: ["auth0"],
23745
+ pypi: ["auth0-python"],
23746
+ go: ["github.com/auth0/go-auth0"],
23747
+ maven: ["com.auth0"],
23748
+ rubygems: ["auth0"],
23749
+ composer: ["auth0/auth0-php"],
23750
+ nuget: ["Auth0.ManagementApi"]
23751
+ }
23752
+ },
23753
+ {
23754
+ id: "okta",
23755
+ name: "Okta",
23756
+ category: "Identity",
23757
+ hostSuffixes: ["okta.com", "oktapreview.com"],
23758
+ apiBase: "https://login.okta.com",
23759
+ defaultDataClasses: ["pii"],
23760
+ sdks: {
23761
+ npm: ["@okta/okta-sdk-nodejs"],
23762
+ pypi: ["okta"],
23763
+ go: ["github.com/okta/okta-sdk-golang"],
23764
+ maven: ["com.okta.sdk"],
23765
+ nuget: ["Okta.Sdk"]
23766
+ }
23767
+ },
23768
+ {
23769
+ id: "clerk",
23770
+ name: "Clerk",
23771
+ category: "Identity",
23772
+ hostSuffixes: ["clerk.com", "clerk.dev"],
23773
+ apiBase: "https://api.clerk.com",
23774
+ defaultDataClasses: ["pii"],
23775
+ sdks: {
23776
+ npm: ["@clerk/backend", "@clerk/nextjs"],
23777
+ pypi: ["clerk-backend-api"],
23778
+ go: ["github.com/clerk/clerk-sdk-go"]
23779
+ }
23780
+ },
23781
+ {
23782
+ id: "supabase",
23783
+ name: "Supabase",
23784
+ category: "Backend platform",
23785
+ hostSuffixes: ["supabase.co", "supabase.com"],
23786
+ apiBase: "https://api.supabase.com",
23787
+ defaultDataClasses: ["pii", "customer"],
23788
+ sdks: {
23789
+ npm: ["@supabase/supabase-js"],
23790
+ pypi: ["supabase"],
23791
+ cargo: ["postgrest"]
23792
+ }
23793
+ },
23794
+ {
23795
+ id: "firebase",
23796
+ name: "Firebase",
23797
+ category: "Backend platform",
23798
+ hostSuffixes: ["firebaseio.com", "firebase.google.com"],
23799
+ apiBase: "https://firebaseio.com",
23800
+ defaultDataClasses: ["customer"],
23801
+ sdks: {
23802
+ npm: ["firebase", "firebase-admin"],
23803
+ pypi: ["firebase-admin"],
23804
+ go: ["firebase.google.com/go"],
23805
+ maven: ["com.google.firebase"]
23806
+ }
23807
+ },
23808
+ {
23809
+ id: "mongodb-atlas",
23810
+ name: "MongoDB Atlas",
23811
+ category: "Database SaaS",
23812
+ hostSuffixes: ["mongodb.net", "mongodb.com"],
23813
+ apiBase: "https://cloud.mongodb.com",
23814
+ defaultDataClasses: ["customer"],
23815
+ sdks: {
23816
+ npm: ["mongodb"],
23817
+ pypi: ["pymongo"],
23818
+ go: ["go.mongodb.org/mongo-driver"],
23819
+ maven: ["org.mongodb"],
23820
+ rubygems: ["mongo"],
23821
+ cargo: ["mongodb"],
23822
+ nuget: ["MongoDB.Driver"]
23823
+ }
23824
+ },
23825
+ {
23826
+ id: "planetscale",
23827
+ name: "PlanetScale",
23828
+ category: "Database SaaS",
23829
+ hostSuffixes: ["psdb.cloud", "planetscale.com"],
23830
+ apiBase: "https://api.planetscale.com",
23831
+ defaultDataClasses: ["customer"],
23832
+ sdks: {
23833
+ npm: ["@planetscale/database"],
23834
+ go: ["github.com/planetscale/planetscale-go"]
23835
+ }
23836
+ },
23837
+ {
23838
+ id: "algolia",
23839
+ name: "Algolia",
23840
+ category: "Search SaaS",
23841
+ hostSuffixes: ["algolia.net", "algolianet.com"],
23842
+ apiBase: "https://algolia.net",
23843
+ defaultDataClasses: ["customer"],
23844
+ sdks: {
23845
+ npm: ["algoliasearch"],
23846
+ pypi: ["algoliasearch"],
23847
+ go: ["github.com/algolia/algoliasearch-client-go"],
23848
+ maven: ["com.algolia"],
23849
+ rubygems: ["algolia"],
23850
+ composer: ["algolia/algoliasearch-client-php"],
23851
+ nuget: ["Algolia.Search"]
23852
+ }
23853
+ },
23854
+ {
23855
+ id: "cloudflare",
23856
+ name: "Cloudflare",
23857
+ category: "CDN / edge",
23858
+ hostSuffixes: ["cloudflare.com", "workers.dev"],
23859
+ apiBase: "https://api.cloudflare.com",
23860
+ defaultDataClasses: ["logs"],
23861
+ sdks: {
23862
+ npm: ["cloudflare"],
23863
+ pypi: ["cloudflare"],
23864
+ go: ["github.com/cloudflare/cloudflare-go"],
23865
+ nuget: ["CloudFlare.Client"]
23866
+ }
23867
+ },
23868
+ {
23869
+ id: "huggingface",
23870
+ name: "Hugging Face",
23871
+ category: "LLM provider",
23872
+ hostSuffixes: ["huggingface.co"],
23873
+ apiBase: "https://api-inference.huggingface.co",
23874
+ defaultDataClasses: ["source"],
23875
+ sdks: {
23876
+ npm: ["@huggingface/inference"],
23877
+ pypi: ["huggingface-hub", "transformers"],
23878
+ rubygems: ["hugging-face"]
23879
+ }
23880
+ },
23881
+ {
23882
+ id: "cohere",
23883
+ name: "Cohere",
23884
+ category: "LLM provider",
23885
+ hostSuffixes: ["cohere.com", "cohere.ai"],
23886
+ apiBase: "https://api.cohere.com",
23887
+ defaultDataClasses: ["pii", "source"],
23888
+ sdks: {
23889
+ npm: ["cohere-ai"],
23890
+ pypi: ["cohere"],
23891
+ go: ["github.com/cohere-ai/cohere-go"]
23892
+ }
23893
+ },
23894
+ {
23895
+ id: "mistral",
23896
+ name: "Mistral AI",
23897
+ category: "LLM provider",
23898
+ hostSuffixes: ["mistral.ai"],
23899
+ apiBase: "https://api.mistral.ai",
23900
+ defaultDataClasses: ["pii", "source"],
23901
+ sdks: {
23902
+ npm: ["@mistralai/mistralai"],
23903
+ pypi: ["mistralai"],
23904
+ go: ["github.com/gage-technologies/mistral-go"]
23905
+ }
23906
+ }
23907
+ ];
23908
+ var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
23909
+ ${JSON.stringify(PROVIDER_REGISTRY)}`;
23910
+
23911
+ // ../../packages/detections/src/egress/extract.ts
23912
+ var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
23913
+ var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
23914
+ var SECRET_VALUE = new RegExp(
23915
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
23916
+ "gi"
23917
+ );
23918
+ var AUTH_SCHEME_VALUE = new RegExp(
23919
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
23920
+ "gi"
23921
+ );
23922
+ var WEBHOOK_SECRET_PATHS = [
23923
+ { hosts: ["hooks.slack.com"], prefix: "/services/" },
23924
+ {
23925
+ hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
23926
+ prefix: "/api/webhooks/"
23927
+ },
23928
+ { hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
23929
+ { hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
23930
+ ];
23931
+ function escapeRegExp(literal2) {
23932
+ return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23933
+ }
23934
+ var WEBHOOK_URL = new RegExp(
23935
+ `(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
23936
+ (entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
23937
+ ).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
23938
+ "gi"
23939
+ );
23940
+
22921
23941
  // ../../packages/detections/src/escape-regexp.ts
22922
- function escapeRegExp(value) {
23942
+ function escapeRegExp2(value) {
22923
23943
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22924
23944
  }
22925
23945
 
22926
23946
  // ../../packages/detections/src/matchers/limits.ts
22927
23947
  var MAX_MATCHES_PER_RULE = 1e4;
23948
+ var MAX_REGEX_INPUT_LENGTH = 2e5;
22928
23949
 
22929
23950
  // ../../packages/detections/src/matchers/keyword.ts
22930
23951
  var KeywordMatcher2 = class {
@@ -22935,7 +23956,7 @@ var KeywordMatcher2 = class {
22935
23956
  for (const kw of keywords) {
22936
23957
  if (kw.length === 0) continue;
22937
23958
  if (spans.length >= MAX_MATCHES_PER_RULE) break;
22938
- const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
23959
+ const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
22939
23960
  let m;
22940
23961
  while ((m = re.exec(text)) !== null) {
22941
23962
  spans.push({ start: m.index, end: m.index + m[0].length });
@@ -22952,9 +23973,13 @@ var RegexMatcher2 = class {
22952
23973
  if (rule.matcher.type !== "regex") return [];
22953
23974
  const { pattern, flags, captureGroup } = rule.matcher;
22954
23975
  const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
23976
+ const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
22955
23977
  const spans = [];
22956
23978
  let m;
22957
- while ((m = re.exec(text)) !== null) {
23979
+ const maxIterations = scanText2.length + 1;
23980
+ let iterations = 0;
23981
+ while ((m = re.exec(scanText2)) !== null) {
23982
+ if (++iterations > maxIterations) break;
22958
23983
  const group = captureGroup != null ? m[captureGroup] : m[0];
22959
23984
  if (m[0].length === 0) re.lastIndex++;
22960
23985
  if (group && spans.length < MAX_MATCHES_PER_RULE) {
@@ -23058,7 +24083,7 @@ function isCorroborated(candidate, candidates, text) {
23058
24083
  for (const label of labels) {
23059
24084
  const trimmed = label.trim();
23060
24085
  if (trimmed.length === 0) continue;
23061
- const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(trimmed)}(?![A-Za-z0-9])`, "i");
24086
+ const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
23062
24087
  if (re.test(haystack)) return true;
23063
24088
  }
23064
24089
  }
@@ -23223,6 +24248,112 @@ var CONFIG_POSTURE_RULES = [
23223
24248
  }
23224
24249
  ];
23225
24250
 
24251
+ // ../../packages/detections/src/security/redos-probe.ts
24252
+ var BUDGET_MS = 100;
24253
+ var EXPONENTIAL_UNITS = [
24254
+ "a",
24255
+ "0",
24256
+ " ",
24257
+ "x",
24258
+ "ab",
24259
+ "a.",
24260
+ "a-",
24261
+ "a_",
24262
+ "a@",
24263
+ "a/",
24264
+ "a:",
24265
+ "a=",
24266
+ "a;",
24267
+ "aA0",
24268
+ " "
24269
+ ];
24270
+ var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
24271
+ (unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
24272
+ );
24273
+ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
24274
+ (unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
24275
+ );
24276
+ function literalPrefix(pattern) {
24277
+ let prefix = "";
24278
+ let i = 0;
24279
+ if (pattern[i] === "^") i++;
24280
+ while (i < pattern.length) {
24281
+ const c = pattern[i];
24282
+ if (c === void 0) break;
24283
+ if (c === "\\") {
24284
+ const next = pattern[i + 1];
24285
+ if (next === "b" || next === "B") {
24286
+ i += 2;
24287
+ continue;
24288
+ }
24289
+ if (next === void 0 || /[dDwWsSnrtfv.]/.test(next)) break;
24290
+ prefix += next;
24291
+ i += 2;
24292
+ continue;
24293
+ }
24294
+ if ("([{.*+?|)]}^$".includes(c)) break;
24295
+ prefix += c;
24296
+ i++;
24297
+ }
24298
+ return prefix;
24299
+ }
24300
+ function fuelChars(pattern) {
24301
+ const fuel = /* @__PURE__ */ new Set();
24302
+ for (const m of pattern.matchAll(/\[\^?([^\]]+)\]/g)) {
24303
+ const body = m[1];
24304
+ if (body === void 0) continue;
24305
+ const range = /([A-Za-z0-9])-[A-Za-z0-9]/.exec(body);
24306
+ const rangeStart = range?.[1];
24307
+ if (rangeStart !== void 0) fuel.add(rangeStart);
24308
+ else {
24309
+ const literal2 = body.replace(/\\/g, "")[0];
24310
+ if (literal2 !== void 0 && literal2 !== "^") fuel.add(literal2);
24311
+ }
24312
+ }
24313
+ if (pattern.includes("\\w")) fuel.add("a");
24314
+ if (pattern.includes("\\d")) fuel.add("0");
24315
+ if (pattern.includes("\\s")) fuel.add(" ");
24316
+ if (/(?<!\\)\./.test(pattern)) fuel.add("a");
24317
+ if (fuel.size === 0) fuel.add("a");
24318
+ return [...fuel];
24319
+ }
24320
+ function derivedProbes(pattern) {
24321
+ const prefix = literalPrefix(pattern);
24322
+ const fuel = fuelChars(pattern);
24323
+ const terminators = ["!", "#", "~", "\n"];
24324
+ const probes = [];
24325
+ for (const f of fuel) {
24326
+ for (const term of terminators) {
24327
+ if (term === f) continue;
24328
+ for (const len of [23, 25]) probes.push(prefix + f.repeat(len) + term);
24329
+ }
24330
+ }
24331
+ return probes;
24332
+ }
24333
+ function probesFor(rule) {
24334
+ const derived = rule.matcher.type === "regex" ? derivedProbes(rule.matcher.pattern) : [];
24335
+ return [...derived, ...EXPONENTIAL_PROBES, ...POLYNOMIAL_PROBES];
24336
+ }
24337
+ function worstProbeMs(rule) {
24338
+ let ms = 0;
24339
+ let probe = "";
24340
+ for (const text of probesFor(rule)) {
24341
+ const start = performance.now();
24342
+ scan(text, [rule]);
24343
+ const elapsed = performance.now() - start;
24344
+ if (elapsed > ms) {
24345
+ ms = elapsed;
24346
+ probe = text;
24347
+ }
24348
+ if (ms >= BUDGET_MS) break;
24349
+ }
24350
+ return { ms, probe };
24351
+ }
24352
+ function checkRuleTiming(rule) {
24353
+ const { ms, probe } = worstProbeMs(rule);
24354
+ return { safe: ms < BUDGET_MS, worstMs: ms, probe };
24355
+ }
24356
+
23226
24357
  // ../../rules/code-flaws/auth-jwt-no-verify.json
23227
24358
  var auth_jwt_no_verify_default = {
23228
24359
  specVersion: 1,
@@ -25399,10 +26530,67 @@ function claimOncePerSession(dataDir2, marker, sessionId) {
25399
26530
  return true;
25400
26531
  }
25401
26532
 
26533
+ // ../../packages/plugin-sdk/src/paths.ts
26534
+ import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
26535
+ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
26536
+
25402
26537
  // ../../packages/plugin-sdk/src/project-files.ts
25403
26538
  var import_ignore = __toESM(require_ignore(), 1);
25404
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25405
- import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
26539
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
26540
+ import { basename as basename4, join as join9, relative, sep as sep4 } from "path";
26541
+
26542
+ // ../../packages/plugin-sdk/src/rule-quarantine.ts
26543
+ var PASS_BUDGET_MS = 2e3;
26544
+ function ruleProbeKey(rule) {
26545
+ if (rule.matcher.type !== "regex") return void 0;
26546
+ return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
26547
+ }
26548
+ function warnQuarantined(rule, worstMs) {
26549
+ const timing = worstMs === void 0 ? "not verified in time" : `${worstMs.toFixed(1)}ms`;
26550
+ process.stderr.write(
26551
+ `[aka] quarantined rule "${rule.id}": regex matcher exceeded the ReDoS timing budget (${timing}); excluded from this scan.
26552
+ `
26553
+ );
26554
+ }
26555
+ async function filterUnsafeRules(rules, gateway, opts) {
26556
+ const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
26557
+ const passStart = performance.now();
26558
+ const safe = [];
26559
+ for (const rule of rules) {
26560
+ const key = ruleProbeKey(rule);
26561
+ if (key === void 0) {
26562
+ safe.push(rule);
26563
+ continue;
26564
+ }
26565
+ let cached2;
26566
+ try {
26567
+ cached2 = await gateway.getRuleProbeVerdict(key);
26568
+ } catch {
26569
+ cached2 = void 0;
26570
+ }
26571
+ if (cached2) {
26572
+ if (cached2.verdict === "safe") safe.push(rule);
26573
+ else warnQuarantined(rule, cached2.worstProbeMs);
26574
+ continue;
26575
+ }
26576
+ if (performance.now() - passStart >= passBudgetMs) {
26577
+ warnQuarantined(rule, void 0);
26578
+ continue;
26579
+ }
26580
+ let isSafe;
26581
+ let worstMs;
26582
+ try {
26583
+ ({ safe: isSafe, worstMs } = checkRuleTiming(rule));
26584
+ } catch {
26585
+ isSafe = false;
26586
+ worstMs = Number.POSITIVE_INFINITY;
26587
+ }
26588
+ await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
26589
+ if (isSafe) safe.push(rule);
26590
+ else warnQuarantined(rule, worstMs);
26591
+ }
26592
+ return safe;
26593
+ }
25406
26594
 
25407
26595
  // ../../packages/plugin-sdk/src/runtime.ts
25408
26596
  import { randomUUID as randomUUID10 } from "crypto";
@@ -25446,7 +26634,17 @@ function createPluginRuntime(gateway, settings, opts) {
25446
26634
  categoryActionIndex.set(p.target.category, p.action);
25447
26635
  }
25448
26636
  }
25449
- rules = bundle.rulesComplete ? bundle.rules ?? [] : [...getLoadedRules(), ...bundle.rules ?? []];
26637
+ const bundledProbeKeys = new Set(
26638
+ getLoadedRules().map(ruleProbeKey).filter((key) => key !== void 0)
26639
+ );
26640
+ const incoming = bundle.rules ?? [];
26641
+ const ciVerified = incoming.filter((rule) => {
26642
+ const key = ruleProbeKey(rule);
26643
+ return key !== void 0 && bundledProbeKeys.has(key);
26644
+ });
26645
+ const needsGate = incoming.filter((rule) => !ciVerified.includes(rule));
26646
+ const safeBundleRules = [...ciVerified, ...await filterUnsafeRules(needsGate, gateway)];
26647
+ rules = bundle.rulesComplete ? safeBundleRules : [...getLoadedRules(), ...safeBundleRules];
25450
26648
  bundleExceptions = bundle.exceptions ?? [];
25451
26649
  initialized = true;
25452
26650
  }
@@ -25746,13 +26944,23 @@ var ONBOARDING_NUDGE = "AKA Security is installed but not calibrated \u2014 run
25746
26944
  async function readStdin() {
25747
26945
  return new Promise((resolve) => {
25748
26946
  let data = "";
25749
- process.stdin.setEncoding("utf8");
25750
- process.stdin.on("data", (chunk) => {
25751
- data += chunk;
25752
- });
25753
- process.stdin.on("end", () => {
26947
+ let settled = false;
26948
+ const finish = () => {
26949
+ if (settled) return;
26950
+ settled = true;
26951
+ clearTimeout(timer);
26952
+ process.stdin.removeListener("data", onData);
26953
+ process.stdin.removeListener("end", finish);
25754
26954
  resolve(data);
25755
- });
26955
+ };
26956
+ const onData = (chunk) => {
26957
+ data += chunk;
26958
+ };
26959
+ const timer = setTimeout(finish, 5e3);
26960
+ process.stdin.setEncoding("utf8");
26961
+ process.stdin.on("data", onData);
26962
+ process.stdin.on("end", finish);
26963
+ process.stdin.on("error", finish);
25756
26964
  });
25757
26965
  }
25758
26966
  function parseJson(raw) {
@@ -25769,9 +26977,14 @@ function getString(record2, key) {
25769
26977
  }
25770
26978
  function emit(output) {
25771
26979
  return new Promise((resolve) => {
25772
- process.stdout.write(JSON.stringify(output), () => {
26980
+ let settled = false;
26981
+ const finish = () => {
26982
+ if (settled) return;
26983
+ settled = true;
25773
26984
  resolve();
25774
- });
26985
+ };
26986
+ process.stdout.on("error", finish);
26987
+ process.stdout.write(JSON.stringify(output), finish);
25775
26988
  });
25776
26989
  }
25777
26990
  function baseMetadata(input) {
@@ -26056,6 +27269,13 @@ var StandaloneDataGateway = class {
26056
27269
  this.db.scanLedger.upsertEntries(entries);
26057
27270
  return Promise.resolve();
26058
27271
  }
27272
+ getRuleProbeVerdict(ruleKey) {
27273
+ return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
27274
+ }
27275
+ setRuleProbeVerdict(ruleKey, verdict, worstProbeMs2) {
27276
+ this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs2);
27277
+ return Promise.resolve();
27278
+ }
26059
27279
  openAtRestKeysForPath(path) {
26060
27280
  return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
26061
27281
  }
@@ -26066,6 +27286,12 @@ var StandaloneDataGateway = class {
26066
27286
  this.db.resolutions.insertResolution(input);
26067
27287
  return Promise.resolve();
26068
27288
  }
27289
+ // Bare forward — no toggle read here. The plugin-path kill-switch is
27290
+ // enforced by the caller, which already holds the parsed workspace
27291
+ // settings; this class only ever sees `dataDir`, not the settings base.
27292
+ recordProjectEgress(input) {
27293
+ return Promise.resolve(this.db.shares.recordProjectEgress(input));
27294
+ }
26069
27295
  close() {
26070
27296
  this.db.close();
26071
27297
  return Promise.resolve();