@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
 
@@ -16200,6 +16204,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16200
16204
 
16201
16205
  // ../../packages/schema/src/zod/rule.ts
16202
16206
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16207
+ var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
16203
16208
  var KeywordMatcher = external_exports.object({
16204
16209
  type: external_exports.literal("keyword"),
16205
16210
  // An empty keyword matches at every position, yielding one zero-length span
@@ -16224,9 +16229,10 @@ function matchesEmptyString(pattern, flags) {
16224
16229
  return false;
16225
16230
  }
16226
16231
  }
16232
+ var MAX_PATTERN_LENGTH = 2e3;
16227
16233
  var RegexMatcher = external_exports.object({
16228
16234
  type: external_exports.literal("regex"),
16229
- pattern: external_exports.string(),
16235
+ pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
16230
16236
  flags: external_exports.string().default("gi"),
16231
16237
  captureGroup: external_exports.number().int().nonnegative().optional()
16232
16238
  }).refine((v) => isValidRegex(v.pattern, v.flags), {
@@ -16793,6 +16799,212 @@ function buildDetectionsList(summaries, query) {
16793
16799
  return { counts, items: filtered.map(summaryToDetectionListItem) };
16794
16800
  }
16795
16801
 
16802
+ // ../../packages/schema/src/zod/shares.ts
16803
+ var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
16804
+ var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
16805
+ var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
16806
+ var DATA_CLASS_ORDER = DataClass.options;
16807
+ var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
16808
+ var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
16809
+ var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
16810
+ var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
16811
+ var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
16812
+ var ReviewInfo = external_exports.object({
16813
+ needsReview: external_exports.boolean(),
16814
+ reasons: external_exports.array(ReviewReason)
16815
+ }).meta({ id: "ReviewInfo" });
16816
+ var DestinationNetwork = external_exports.object({
16817
+ port: external_exports.number().int().nullable(),
16818
+ geo: external_exports.string().nullable(),
16819
+ ptr: external_exports.string().nullable()
16820
+ }).meta({ id: "DestinationNetwork" });
16821
+ var EndpointSummary = external_exports.object({
16822
+ id: external_exports.string(),
16823
+ method: HttpMethod,
16824
+ transport: Transport,
16825
+ url: external_exports.string(),
16826
+ template: external_exports.boolean(),
16827
+ dataClass: DataClass,
16828
+ lastSeen: external_exports.iso.datetime(),
16829
+ callSiteCount: external_exports.number().int().nonnegative()
16830
+ }).meta({ id: "EndpointSummary" });
16831
+ var CallSite = external_exports.object({
16832
+ id: external_exports.string(),
16833
+ project: external_exports.string(),
16834
+ file: external_exports.string(),
16835
+ line: external_exports.number().int().nonnegative(),
16836
+ snippet: external_exports.string(),
16837
+ dynamic: external_exports.boolean(),
16838
+ vendored: external_exports.boolean(),
16839
+ /** Deep-link to the Inventory project, when the repo is governed there. */
16840
+ projectId: external_exports.string().nullable()
16841
+ }).meta({ id: "CallSite" });
16842
+ var EndpointWithSites = EndpointSummary.extend({
16843
+ sites: external_exports.array(CallSite)
16844
+ }).meta({ id: "EndpointWithSites" });
16845
+ var ShareDestinationSummary = external_exports.object({
16846
+ id: external_exports.string(),
16847
+ kind: DestinationKind,
16848
+ name: external_exports.string(),
16849
+ host: external_exports.string(),
16850
+ category: external_exports.string(),
16851
+ trust: ShareTrustLevel,
16852
+ /** Effective state (decision applied over the trust default). */
16853
+ status: EgressStatus,
16854
+ /** True when an egress decision override differs from the trust default. */
16855
+ isCustom: external_exports.boolean(),
16856
+ lastSeen: external_exports.iso.datetime(),
16857
+ endpointCount: external_exports.number().int().nonnegative(),
16858
+ callSiteCount: external_exports.number().int().nonnegative(),
16859
+ transports: external_exports.array(Transport),
16860
+ /** Most-sensitive first. */
16861
+ dataClasses: external_exports.array(DataClass),
16862
+ review: ReviewInfo,
16863
+ /** Non-provider hosts only; null for providers. */
16864
+ network: DestinationNetwork.nullable(),
16865
+ /** Embedded for inline expansion — no call sites here. */
16866
+ endpoints: external_exports.array(EndpointSummary)
16867
+ }).meta({ id: "ShareDestinationSummary" });
16868
+ var ShareDestinationDetail = ShareDestinationSummary.omit({
16869
+ endpointCount: true,
16870
+ callSiteCount: true,
16871
+ endpoints: true
16872
+ }).extend({
16873
+ /** Ownership/geo rationale; null for providers. */
16874
+ note: external_exports.string().nullable(),
16875
+ endpoints: external_exports.array(EndpointWithSites)
16876
+ }).meta({ id: "ShareDestinationDetail" });
16877
+ var ReviewDestination = external_exports.object({
16878
+ id: external_exports.string(),
16879
+ kind: DestinationKind,
16880
+ name: external_exports.string(),
16881
+ /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
16882
+ host: external_exports.string(),
16883
+ trust: ShareTrustLevel,
16884
+ status: EgressStatus,
16885
+ review: ReviewInfo,
16886
+ topDataClass: DataClass,
16887
+ callSiteCount: external_exports.number().int().nonnegative(),
16888
+ lastSeen: external_exports.iso.datetime()
16889
+ }).meta({ id: "ReviewDestination" });
16890
+ var ShareDestinationGroup = external_exports.object({
16891
+ kind: DestinationKind,
16892
+ total: external_exports.number().int().nonnegative(),
16893
+ items: external_exports.array(ShareDestinationSummary)
16894
+ }).meta({ id: "ShareDestinationGroup" });
16895
+ var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
16896
+ var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
16897
+ var SharesStats = external_exports.object({
16898
+ destinations: external_exports.number().int().nonnegative(),
16899
+ endpoints: external_exports.number().int().nonnegative(),
16900
+ callSites: external_exports.number().int().nonnegative(),
16901
+ needsReview: external_exports.number().int().nonnegative(),
16902
+ insecure: external_exports.number().int().nonnegative(),
16903
+ byKind: external_exports.object({
16904
+ provider: external_exports.number().int().nonnegative(),
16905
+ internal: external_exports.number().int().nonnegative(),
16906
+ external: external_exports.number().int().nonnegative(),
16907
+ ip: external_exports.number().int().nonnegative()
16908
+ }),
16909
+ byTrust: external_exports.object({
16910
+ recognized: external_exports.number().int().nonnegative(),
16911
+ internal: external_exports.number().int().nonnegative(),
16912
+ unverified: external_exports.number().int().nonnegative(),
16913
+ ip: external_exports.number().int().nonnegative()
16914
+ })
16915
+ }).meta({ id: "SharesStats" });
16916
+ var SetEgressDecisionBody = external_exports.object({
16917
+ /** `null` clears the override — reverts to the trust default, isCustom false. */
16918
+ decision: EgressDecision.nullable()
16919
+ }).meta({ id: "SetEgressDecisionBody" });
16920
+ var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
16921
+ var ListShareDestinationsQuery = external_exports.object({
16922
+ /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
16923
+ q: external_exports.string().optional(),
16924
+ /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
16925
+ kind: external_exports.array(DestinationKind).optional(),
16926
+ /** Reserved for future grouping modes; only 'destination' is supported today. */
16927
+ groupBy: external_exports.enum(["destination"]).default("destination"),
16928
+ /**
16929
+ * When true, return a flat severity-ordered `items[]` instead of `groups`.
16930
+ * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
16931
+ * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
16932
+ * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
16933
+ */
16934
+ review: external_exports.stringbool().default(false)
16935
+ });
16936
+ var ExportSharesQuery = external_exports.object({
16937
+ format: external_exports.enum(["csv", "json"]).default("csv"),
16938
+ q: external_exports.string().optional(),
16939
+ kind: external_exports.array(DestinationKind).optional()
16940
+ });
16941
+
16942
+ // ../../packages/schema/src/zod/egress-extraction.ts
16943
+ var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
16944
+ var ProviderRegistryEntry = external_exports.object({
16945
+ id: external_exports.string(),
16946
+ name: external_exports.string(),
16947
+ category: external_exports.string(),
16948
+ /** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
16949
+ hostSuffixes: external_exports.array(external_exports.string()).min(1),
16950
+ /** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
16951
+ apiBase: external_exports.string(),
16952
+ /** Most-sensitive first; index 0 becomes the endpoint dataClass. */
16953
+ defaultDataClasses: external_exports.array(DataClass).min(1),
16954
+ /** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
16955
+ sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
16956
+ }).meta({ id: "ProviderRegistryEntry" });
16957
+ var EgressCallSiteHit = external_exports.object({
16958
+ file: external_exports.string(),
16959
+ line: external_exports.number().int().positive(),
16960
+ snippet: external_exports.string(),
16961
+ dynamic: external_exports.boolean(),
16962
+ vendored: external_exports.boolean()
16963
+ }).meta({ id: "EgressCallSiteHit" });
16964
+ var ResolvedEgressHit = external_exports.object({
16965
+ host: external_exports.string(),
16966
+ kind: DestinationKind,
16967
+ name: external_exports.string(),
16968
+ category: external_exports.string(),
16969
+ trust: ShareTrustLevel,
16970
+ network: DestinationNetwork.nullable(),
16971
+ method: HttpMethod,
16972
+ transport: Transport,
16973
+ url: external_exports.string(),
16974
+ template: external_exports.boolean(),
16975
+ dataClass: DataClass,
16976
+ site: EgressCallSiteHit
16977
+ }).meta({ id: "ResolvedEgressHit" });
16978
+ var EgressReconcile = external_exports.discriminatedUnion("mode", [
16979
+ external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
16980
+ external_exports.object({
16981
+ mode: external_exports.literal("ledger"),
16982
+ scannedFiles: external_exports.array(external_exports.string()),
16983
+ deletedFiles: external_exports.array(external_exports.string())
16984
+ })
16985
+ ]).meta({ id: "EgressReconcile" });
16986
+ var RecordProjectEgressInput = external_exports.object({
16987
+ /** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
16988
+ projectKey: external_exports.string().min(1),
16989
+ /** Display name only — never keys reconciliation. */
16990
+ project: external_exports.string(),
16991
+ projectId: external_exports.string().nullable(),
16992
+ reconcile: EgressReconcile,
16993
+ hits: external_exports.array(ResolvedEgressHit)
16994
+ }).meta({ id: "RecordProjectEgressInput" });
16995
+ var EgressWriteSummary = external_exports.object({
16996
+ destinations: external_exports.number().int().nonnegative(),
16997
+ endpoints: external_exports.number().int().nonnegative(),
16998
+ callSites: external_exports.number().int().nonnegative(),
16999
+ truncated: external_exports.boolean(),
17000
+ /**
17001
+ * Files the cap dropped whole. Their stored rows were left untouched, so a
17002
+ * ledger-keeping caller must withhold their ledger entries and read them
17003
+ * again next scan.
17004
+ */
17005
+ droppedFiles: external_exports.array(external_exports.string()).default([])
17006
+ }).meta({ id: "EgressWriteSummary" });
17007
+
16796
17008
  // ../../packages/schema/src/zod/findings-group-build.ts
16797
17009
  function toApiAction(dbVal) {
16798
17010
  const map2 = {
@@ -17054,7 +17266,7 @@ var PatchInstalledPackRequest = external_exports.object({
17054
17266
  }).meta({ id: "PatchInstalledPackRequest" });
17055
17267
 
17056
17268
  // ../../packages/schema/src/zod/local.ts
17057
- var WORKSPACE_SETTINGS_SPEC_VERSION = 2;
17269
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 3;
17058
17270
  var RunMode = external_exports.enum(["standalone"]);
17059
17271
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17060
17272
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17069,6 +17281,9 @@ var WorkspaceSettings = external_exports.object({
17069
17281
  policy: SimpleDetectionPolicy.default("redact"),
17070
17282
  // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
17071
17283
  historicalAccess: HistoricalAccess.default("session-only"),
17284
+ // In-place egress extraction on the scan paths; disable to stop all Data
17285
+ // Shares writes.
17286
+ dataSharesInPlace: external_exports.boolean().default(true),
17072
17287
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17073
17288
  onboardedAt: external_exports.iso.datetime().optional()
17074
17289
  });
@@ -17552,145 +17767,6 @@ var SetupHandoffOffer = external_exports.object({
17552
17767
  path: ["liveKeys"]
17553
17768
  });
17554
17769
 
17555
- // ../../packages/schema/src/zod/shares.ts
17556
- var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17557
- var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
17558
- var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
17559
- var DATA_CLASS_ORDER = DataClass.options;
17560
- var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
17561
- var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
17562
- var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
17563
- var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
17564
- var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
17565
- var ReviewInfo = external_exports.object({
17566
- needsReview: external_exports.boolean(),
17567
- reasons: external_exports.array(ReviewReason)
17568
- }).meta({ id: "ReviewInfo" });
17569
- var DestinationNetwork = external_exports.object({
17570
- port: external_exports.number().int().nullable(),
17571
- geo: external_exports.string().nullable(),
17572
- ptr: external_exports.string().nullable()
17573
- }).meta({ id: "DestinationNetwork" });
17574
- var EndpointSummary = external_exports.object({
17575
- id: external_exports.string(),
17576
- method: HttpMethod,
17577
- transport: Transport,
17578
- url: external_exports.string(),
17579
- template: external_exports.boolean(),
17580
- dataClass: DataClass,
17581
- lastSeen: external_exports.iso.datetime(),
17582
- callSiteCount: external_exports.number().int().nonnegative()
17583
- }).meta({ id: "EndpointSummary" });
17584
- var CallSite = external_exports.object({
17585
- id: external_exports.string(),
17586
- project: external_exports.string(),
17587
- file: external_exports.string(),
17588
- line: external_exports.number().int().nonnegative(),
17589
- snippet: external_exports.string(),
17590
- dynamic: external_exports.boolean(),
17591
- vendored: external_exports.boolean(),
17592
- /** Deep-link to the Inventory project, when the repo is governed there. */
17593
- projectId: external_exports.string().nullable()
17594
- }).meta({ id: "CallSite" });
17595
- var EndpointWithSites = EndpointSummary.extend({
17596
- sites: external_exports.array(CallSite)
17597
- }).meta({ id: "EndpointWithSites" });
17598
- var ShareDestinationSummary = external_exports.object({
17599
- id: external_exports.string(),
17600
- kind: DestinationKind,
17601
- name: external_exports.string(),
17602
- host: external_exports.string(),
17603
- category: external_exports.string(),
17604
- trust: ShareTrustLevel,
17605
- /** Effective state (decision applied over the trust default). */
17606
- status: EgressStatus,
17607
- /** True when an egress decision override differs from the trust default. */
17608
- isCustom: external_exports.boolean(),
17609
- lastSeen: external_exports.iso.datetime(),
17610
- endpointCount: external_exports.number().int().nonnegative(),
17611
- callSiteCount: external_exports.number().int().nonnegative(),
17612
- transports: external_exports.array(Transport),
17613
- /** Most-sensitive first. */
17614
- dataClasses: external_exports.array(DataClass),
17615
- review: ReviewInfo,
17616
- /** Non-provider hosts only; null for providers. */
17617
- network: DestinationNetwork.nullable(),
17618
- /** Embedded for inline expansion — no call sites here. */
17619
- endpoints: external_exports.array(EndpointSummary)
17620
- }).meta({ id: "ShareDestinationSummary" });
17621
- var ShareDestinationDetail = ShareDestinationSummary.omit({
17622
- endpointCount: true,
17623
- callSiteCount: true,
17624
- endpoints: true
17625
- }).extend({
17626
- /** Ownership/geo rationale; null for providers. */
17627
- note: external_exports.string().nullable(),
17628
- endpoints: external_exports.array(EndpointWithSites)
17629
- }).meta({ id: "ShareDestinationDetail" });
17630
- var ReviewDestination = external_exports.object({
17631
- id: external_exports.string(),
17632
- kind: DestinationKind,
17633
- name: external_exports.string(),
17634
- /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
17635
- host: external_exports.string(),
17636
- trust: ShareTrustLevel,
17637
- status: EgressStatus,
17638
- review: ReviewInfo,
17639
- topDataClass: DataClass,
17640
- callSiteCount: external_exports.number().int().nonnegative(),
17641
- lastSeen: external_exports.iso.datetime()
17642
- }).meta({ id: "ReviewDestination" });
17643
- var ShareDestinationGroup = external_exports.object({
17644
- kind: DestinationKind,
17645
- total: external_exports.number().int().nonnegative(),
17646
- items: external_exports.array(ShareDestinationSummary)
17647
- }).meta({ id: "ShareDestinationGroup" });
17648
- var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
17649
- var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
17650
- var SharesStats = external_exports.object({
17651
- destinations: external_exports.number().int().nonnegative(),
17652
- endpoints: external_exports.number().int().nonnegative(),
17653
- callSites: external_exports.number().int().nonnegative(),
17654
- needsReview: external_exports.number().int().nonnegative(),
17655
- insecure: external_exports.number().int().nonnegative(),
17656
- byKind: external_exports.object({
17657
- provider: external_exports.number().int().nonnegative(),
17658
- internal: external_exports.number().int().nonnegative(),
17659
- ip: external_exports.number().int().nonnegative()
17660
- }),
17661
- byTrust: external_exports.object({
17662
- recognized: external_exports.number().int().nonnegative(),
17663
- internal: external_exports.number().int().nonnegative(),
17664
- unverified: external_exports.number().int().nonnegative(),
17665
- ip: external_exports.number().int().nonnegative()
17666
- })
17667
- }).meta({ id: "SharesStats" });
17668
- var SetEgressDecisionBody = external_exports.object({
17669
- /** `null` clears the override — reverts to the trust default, isCustom false. */
17670
- decision: EgressDecision.nullable()
17671
- }).meta({ id: "SetEgressDecisionBody" });
17672
- var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
17673
- var ListShareDestinationsQuery = external_exports.object({
17674
- /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
17675
- q: external_exports.string().optional(),
17676
- /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
17677
- kind: external_exports.array(DestinationKind).optional(),
17678
- /** Reserved for future grouping modes; only 'destination' is supported today. */
17679
- groupBy: external_exports.enum(["destination"]).default("destination"),
17680
- /**
17681
- * When true, return a flat severity-ordered `items[]` instead of `groups`.
17682
- * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
17683
- * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
17684
- * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17685
- */
17686
- review: external_exports.stringbool().default(false)
17687
- });
17688
- var ExportSharesQuery = external_exports.object({
17689
- format: external_exports.enum(["csv", "json"]).default("csv"),
17690
- q: external_exports.string().optional(),
17691
- kind: external_exports.array(DestinationKind).optional()
17692
- });
17693
-
17694
17770
  // ../../packages/schema/src/zod/shares-access.ts
17695
17771
  var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
17696
17772
  function trustDefaultStatus(trust) {
@@ -17710,7 +17786,7 @@ function deriveReviewReasons(trust, transports) {
17710
17786
  const reasons = [];
17711
17787
  if (trust === "ip") reasons.push("raw_ip");
17712
17788
  if (trust === "unverified") reasons.push("unverified_domain");
17713
- if (transports.includes("http")) reasons.push("plaintext_transport");
17789
+ if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
17714
17790
  return reasons;
17715
17791
  }
17716
17792
  function buildReviewInfo(trust, transports) {
@@ -17941,6 +18017,7 @@ function applyMigrations(db) {
17941
18017
  ensureSyncedAtColumn(db, "audit_events");
17942
18018
  ensureScanLedgerTable(db);
17943
18019
  ensureBlockedDetectionsTable(db);
18020
+ ensureRuleProbeCacheTable(db);
17944
18021
  ensureWriteGateTrigger(db);
17945
18022
  ensureTokenUsageColumns(db);
17946
18023
  reconcileSourceProjectIds(db);
@@ -18080,6 +18157,14 @@ function ensureBlockedDetectionsTable(db) {
18080
18157
  blocked_at INTEGER NOT NULL
18081
18158
  )`);
18082
18159
  }
18160
+ function ensureRuleProbeCacheTable(db) {
18161
+ db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
18162
+ rule_key TEXT PRIMARY KEY,
18163
+ verdict TEXT NOT NULL,
18164
+ worst_probe_ms REAL NOT NULL,
18165
+ checked_at INTEGER NOT NULL
18166
+ )`);
18167
+ }
18083
18168
 
18084
18169
  // ../../packages/persistence/src/paths.ts
18085
18170
  import { chmodSync, mkdirSync } from "fs";
@@ -21633,6 +21718,35 @@ var SqliteResolutionsRepository = class {
21633
21718
  }
21634
21719
  };
21635
21720
 
21721
+ // ../../packages/persistence/src/repositories/rule-probe-cache.ts
21722
+ var SqliteRuleProbeCacheRepository = class {
21723
+ constructor(db) {
21724
+ this.db = db;
21725
+ this.upsertStmt = db.prepare(
21726
+ `INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
21727
+ VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
21728
+ ON CONFLICT (rule_key) DO UPDATE SET
21729
+ verdict = excluded.verdict,
21730
+ worst_probe_ms = excluded.worst_probe_ms,
21731
+ checked_at = excluded.checked_at`
21732
+ );
21733
+ this.readStmt = db.prepare(
21734
+ `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
21735
+ );
21736
+ }
21737
+ db;
21738
+ upsertStmt;
21739
+ readStmt;
21740
+ getVerdict(ruleKey) {
21741
+ return getRow(this.readStmt, { ruleKey });
21742
+ }
21743
+ setVerdict(ruleKey, verdict, worstProbeMs) {
21744
+ failOpenTransaction(this.db, () => {
21745
+ this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
21746
+ });
21747
+ }
21748
+ };
21749
+
21636
21750
  // ../../packages/persistence/src/repositories/scan-ledger.ts
21637
21751
  var SqliteScanLedgerRepository = class {
21638
21752
  constructor(db) {
@@ -22044,11 +22158,50 @@ var SqliteSecurityRepository = class {
22044
22158
 
22045
22159
  // ../../packages/persistence/src/repositories/shares.ts
22046
22160
  import { randomUUID as randomUUID7 } from "crypto";
22047
- var KIND_ORDER = ["provider", "internal", "ip"];
22161
+ var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22162
+ var IN_CHUNK = 500;
22163
+ var KIND_ORDER = ["provider", "internal", "external", "ip"];
22164
+ var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
22165
+ var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
22166
+ LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
22048
22167
  var CALL_SITE_EMBED_CAP = 200;
22049
22168
  function parseNetwork(networkJson) {
22050
22169
  return safeJson(networkJson, null);
22051
22170
  }
22171
+ function capHits(all, mode) {
22172
+ if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
22173
+ return { hits: [...all], droppedFiles: [], truncated: false };
22174
+ }
22175
+ if (mode === "walk") {
22176
+ return {
22177
+ hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
22178
+ droppedFiles: [],
22179
+ truncated: true
22180
+ };
22181
+ }
22182
+ const byFile = /* @__PURE__ */ new Map();
22183
+ for (const hit of all) {
22184
+ const bucket = byFile.get(hit.site.file);
22185
+ if (bucket === void 0) byFile.set(hit.site.file, [hit]);
22186
+ else bucket.push(hit);
22187
+ }
22188
+ const hits = [];
22189
+ const droppedFiles = [];
22190
+ for (const [file2, bucket] of byFile) {
22191
+ if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
22192
+ else hits.push(...bucket);
22193
+ }
22194
+ return { hits, droppedFiles, truncated: true };
22195
+ }
22196
+ function withoutDroppedFiles(reconcile, droppedFiles) {
22197
+ if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
22198
+ const dropped = new Set(droppedFiles);
22199
+ return {
22200
+ mode: "ledger",
22201
+ scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
22202
+ deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
22203
+ };
22204
+ }
22052
22205
  function toEndpointSummary(row) {
22053
22206
  return {
22054
22207
  id: row.id,
@@ -22139,13 +22292,15 @@ var SqliteSharesRepository = class {
22139
22292
  const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22140
22293
  const insecure = countScalar(
22141
22294
  this.db,
22142
- "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
22295
+ `SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
22296
+ WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
22143
22297
  );
22144
22298
  const needsReview = countScalar(
22145
22299
  this.db,
22146
22300
  `SELECT count(DISTINCT d.id) AS n
22147
22301
  FROM share_destination d
22148
- LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
22302
+ LEFT JOIN share_endpoint e ON e.destination_id = d.id
22303
+ AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
22149
22304
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
22150
22305
  );
22151
22306
  const kindCounts = countBy(
@@ -22155,6 +22310,7 @@ var SqliteSharesRepository = class {
22155
22310
  const byKind = {
22156
22311
  provider: kindCounts.get("provider") ?? 0,
22157
22312
  internal: kindCounts.get("internal") ?? 0,
22313
+ external: kindCounts.get("external") ?? 0,
22158
22314
  ip: kindCounts.get("ip") ?? 0
22159
22315
  };
22160
22316
  const trustCounts = countBy(
@@ -22230,23 +22386,316 @@ var SqliteSharesRepository = class {
22230
22386
  // real edit from a no-such-destination.
22231
22387
  /**
22232
22388
  * Set (decision) or clear (null) the egress decision override for a destination.
22233
- * `null` deletes the override row → reverts to the trust default.
22389
+ * `null` deletes the override rows → reverts to the trust default.
22390
+ *
22391
+ * The written row carries both the destination id and its host, so the
22392
+ * decision re-attaches by host after the destination is pruned and
22393
+ * re-detected under a fresh id. Rows written before the host column existed
22394
+ * (host NULL, matched by destination id) are replaced rather than left to
22395
+ * shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
22396
+ * would otherwise race a concurrent prune.
22234
22397
  */
22235
22398
  setEgressDecision(destinationId, decision) {
22236
- const exists = this.db.prepare("SELECT 1 FROM share_destination WHERE id = ?").get(destinationId);
22237
- if (exists === void 0) return false;
22238
- if (decision === null) {
22239
- this.db.prepare("DELETE FROM egress_decision_override WHERE destination_id = ?").run(destinationId);
22240
- return true;
22399
+ let existed = false;
22400
+ withTransaction(
22401
+ this.db,
22402
+ () => {
22403
+ const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
22404
+ if (dest === void 0) return;
22405
+ existed = true;
22406
+ this.db.prepare(
22407
+ `DELETE FROM egress_decision_override
22408
+ WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
22409
+ ).run({ host: dest.host, destinationId });
22410
+ if (decision === null) return;
22411
+ this.db.prepare(
22412
+ `INSERT INTO egress_decision_override
22413
+ (id, destination_id, host, decision, created_at, updated_at)
22414
+ VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22415
+ ).run({
22416
+ id: randomUUID7(),
22417
+ destinationId,
22418
+ host: dest.host,
22419
+ decision,
22420
+ now: Date.now()
22421
+ });
22422
+ },
22423
+ "IMMEDIATE"
22424
+ );
22425
+ return existed;
22426
+ }
22427
+ /**
22428
+ * Record one project's statically-extracted egress: reconcile the previously
22429
+ * stored call sites against this scan, upsert destination → endpoint → call
22430
+ * site for every hit, confirm `last_seen` on everything the project still
22431
+ * references, and drop what no longer has evidence.
22432
+ *
22433
+ * Reconciliation keys on `projectKey` alone; `project` and `projectId` are
22434
+ * display payload and never scope a delete. The whole write is one
22435
+ * transaction: a failure leaves the project's previous inventory exactly as
22436
+ * it was, and THROWS rather than reporting a partial write — callers decide
22437
+ * their own fail-open behavior, and the scanner additionally withholds its
22438
+ * ledger commit so the next scan retries.
22439
+ *
22440
+ * Over-cap input is truncated at a FILE boundary, and the files that lost
22441
+ * their hits are both excluded from the reconcile delete and named in
22442
+ * `droppedFiles`. That pairing is what keeps truncation non-destructive on
22443
+ * the ledger path: a dropped file keeps whatever rows it already had, and its
22444
+ * caller withholds the ledger entry so the next scan reads it again.
22445
+ */
22446
+ recordProjectEgress(input) {
22447
+ const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
22448
+ const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
22449
+ const now = Date.now();
22450
+ let summary = {
22451
+ destinations: 0,
22452
+ endpoints: 0,
22453
+ callSites: 0,
22454
+ truncated,
22455
+ droppedFiles
22456
+ };
22457
+ withTransaction(
22458
+ this.db,
22459
+ () => {
22460
+ const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
22461
+ this.reconcileCallSites(input.projectKey, reconcile);
22462
+ this.upsertHits(input, hits, projectId, now);
22463
+ this.confirmLastSeen(input.projectKey, now);
22464
+ this.pruneOrphans();
22465
+ summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
22466
+ },
22467
+ "IMMEDIATE"
22468
+ );
22469
+ return summary;
22470
+ }
22471
+ // ─── Egress write internals ──────────────────────────────────────────────────
22472
+ /**
22473
+ * Clear the stored call sites this scan is responsible for re-creating.
22474
+ *
22475
+ * Each pipeline may only delete rows its own walker could have produced. The
22476
+ * fs walk behind 'walk' mode never descends into dot-directories, so its
22477
+ * delete excludes dot-path files — those rows are the plugin scanner's to
22478
+ * reconcile, and deleting them here would make the two pipelines erase each
22479
+ * other's rows on every alternating scan. 'ledger' mode names its files
22480
+ * outright and never mass-deletes, so rows the fs walk contributed for files
22481
+ * the scanner skips (vendored, oversize) survive it.
22482
+ */
22483
+ reconcileCallSites(projectKey, reconcile) {
22484
+ if (reconcile.mode === "walk") {
22485
+ const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
22486
+ this.db.prepare(
22487
+ `DELETE FROM share_call_site
22488
+ WHERE project_key = :key
22489
+ AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
22490
+ AND file NOT LIKE '.%'
22491
+ AND file NOT LIKE '%/.%'`
22492
+ ).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
22493
+ return;
22241
22494
  }
22495
+ const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
22496
+ for (let i = 0; i < files.length; i += IN_CHUNK) {
22497
+ const chunk = files.slice(i, i + IN_CHUNK);
22498
+ this.db.prepare(
22499
+ `DELETE FROM share_call_site
22500
+ WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
22501
+ ).run(projectKey, ...chunk);
22502
+ }
22503
+ }
22504
+ /**
22505
+ * Upsert every hit as destination → endpoint → call site. Destinations key on
22506
+ * `host` and endpoints on `(destination_id, method, url)`, both shared across
22507
+ * projects; only the call site carries `project_key`. A destination's `note`
22508
+ * is user-owned and never overwritten. The id caches keep one upsert per
22509
+ * distinct host and endpoint, so the first hit for a host supplies its
22510
+ * classification for this batch.
22511
+ */
22512
+ upsertHits(input, hits, projectId, now) {
22513
+ if (hits.length === 0) return;
22514
+ const destStmt = this.db.prepare(
22515
+ `INSERT INTO share_destination
22516
+ (id, kind, name, host, category, trust, network_json, last_seen, provenance,
22517
+ created_at, updated_at)
22518
+ VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
22519
+ ON CONFLICT (host) DO UPDATE SET
22520
+ kind = excluded.kind,
22521
+ name = excluded.name,
22522
+ category = excluded.category,
22523
+ trust = excluded.trust,
22524
+ network_json = excluded.network_json,
22525
+ last_seen = excluded.last_seen,
22526
+ updated_at = excluded.updated_at`
22527
+ );
22528
+ const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
22529
+ const endpointStmt = this.db.prepare(
22530
+ `INSERT INTO share_endpoint
22531
+ (id, destination_id, method, transport, url, template, data_class, last_seen,
22532
+ created_at, updated_at)
22533
+ VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
22534
+ :now, :now)
22535
+ ON CONFLICT (destination_id, method, url) DO UPDATE SET
22536
+ transport = excluded.transport,
22537
+ template = excluded.template,
22538
+ data_class = excluded.data_class,
22539
+ last_seen = excluded.last_seen,
22540
+ updated_at = excluded.updated_at`
22541
+ );
22542
+ const endpointIdStmt = this.db.prepare(
22543
+ "SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
22544
+ );
22545
+ const siteStmt = this.db.prepare(
22546
+ `INSERT INTO share_call_site
22547
+ (id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
22548
+ project_id, created_at, updated_at)
22549
+ VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
22550
+ :vendored, :projectId, :now, :now)
22551
+ ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
22552
+ snippet = excluded.snippet,
22553
+ dynamic = excluded.dynamic,
22554
+ vendored = excluded.vendored,
22555
+ project = excluded.project,
22556
+ project_id = COALESCE(excluded.project_id, share_call_site.project_id),
22557
+ updated_at = excluded.updated_at`
22558
+ );
22559
+ const destIds = /* @__PURE__ */ new Map();
22560
+ const endpointIds = /* @__PURE__ */ new Map();
22561
+ for (const hit of hits) {
22562
+ let destinationId = destIds.get(hit.host);
22563
+ if (destinationId === void 0) {
22564
+ destStmt.run({
22565
+ id: randomUUID7(),
22566
+ kind: hit.kind,
22567
+ name: hit.name,
22568
+ host: hit.host,
22569
+ category: hit.category,
22570
+ trust: hit.trust,
22571
+ networkJson: hit.network === null ? null : JSON.stringify(hit.network),
22572
+ now
22573
+ });
22574
+ destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
22575
+ destIds.set(hit.host, destinationId);
22576
+ }
22577
+ const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
22578
+ let endpointId = endpointIds.get(endpointKey);
22579
+ if (endpointId === void 0) {
22580
+ endpointStmt.run({
22581
+ id: randomUUID7(),
22582
+ destinationId,
22583
+ method: hit.method,
22584
+ transport: hit.transport,
22585
+ url: hit.url,
22586
+ template: boolToInt(hit.template),
22587
+ dataClass: hit.dataClass,
22588
+ now
22589
+ });
22590
+ endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
22591
+ endpointIds.set(endpointKey, endpointId);
22592
+ }
22593
+ siteStmt.run({
22594
+ id: randomUUID7(),
22595
+ endpointId,
22596
+ project: input.project,
22597
+ projectKey: input.projectKey,
22598
+ file: hit.site.file,
22599
+ line: hit.site.line,
22600
+ snippet: hit.site.snippet,
22601
+ dynamic: boolToInt(hit.site.dynamic),
22602
+ vendored: boolToInt(hit.site.vendored),
22603
+ projectId,
22604
+ now
22605
+ });
22606
+ }
22607
+ }
22608
+ /**
22609
+ * The source-project id this project's stored call sites already carry, if
22610
+ * any. Only the pipeline that resolves a source project supplies one; the
22611
+ * other passes null and inherits this, so the link stops flapping between a
22612
+ * real id and NULL depending on which pipeline ran last. The value is a
22613
+ * per-project attribute stored redundantly on each row, so any row's is
22614
+ * representative.
22615
+ */
22616
+ knownProjectId(projectKey) {
22617
+ return getRow(
22618
+ this.db.prepare(
22619
+ `SELECT project_id AS projectId FROM share_call_site
22620
+ WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
22621
+ ),
22622
+ [projectKey]
22623
+ )?.projectId ?? null;
22624
+ }
22625
+ /**
22626
+ * Stamp `last_seen` on every endpoint and destination this project still
22627
+ * references — including rows the scan preserved rather than re-wrote, so a
22628
+ * ledger-skipped file's references don't decay into "stale" on the page.
22629
+ */
22630
+ confirmLastSeen(projectKey, now) {
22242
22631
  this.db.prepare(
22243
- `INSERT INTO egress_decision_override (id, destination_id, decision, created_at, updated_at)
22244
- VALUES (:id, :destinationId, :decision, :now, :now)
22245
- ON CONFLICT (destination_id) DO UPDATE SET
22246
- decision = excluded.decision,
22247
- updated_at = excluded.updated_at`
22248
- ).run({ id: randomUUID7(), destinationId, decision, now: Date.now() });
22249
- return true;
22632
+ `UPDATE share_endpoint SET last_seen = :now, updated_at = :now
22633
+ WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
22634
+ ).run({ now, key: projectKey });
22635
+ this.db.prepare(
22636
+ `UPDATE share_destination SET last_seen = :now, updated_at = :now
22637
+ WHERE id IN (SELECT DISTINCT e.destination_id
22638
+ FROM share_endpoint e
22639
+ JOIN share_call_site c ON c.endpoint_id = e.id
22640
+ WHERE c.project_key = :key)`
22641
+ ).run({ now, key: projectKey });
22642
+ }
22643
+ /**
22644
+ * Drop rows left without evidence: endpoints with no call site, then
22645
+ * destinations with no endpoint. Call sites are the only evidence either one
22646
+ * has, so a row that lost its last one belongs to no project any more.
22647
+ *
22648
+ * Overrides are deleted between the two steps, and only the ones written
22649
+ * before the host column existed. Those match a destination by id alone;
22650
+ * because the id link is released on delete rather than cascading, leaving
22651
+ * them would accumulate rows that match neither join arm and that nothing can
22652
+ * reach again. Host-bearing rows deliberately survive — the host is what
22653
+ * re-attaches a user's decision when the destination comes back.
22654
+ */
22655
+ pruneOrphans() {
22656
+ this.db.exec(
22657
+ `DELETE FROM share_endpoint
22658
+ WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
22659
+ );
22660
+ this.db.exec(
22661
+ `DELETE FROM egress_decision_override
22662
+ WHERE host IS NULL
22663
+ AND destination_id IN (
22664
+ SELECT d.id FROM share_destination d
22665
+ WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
22666
+ );
22667
+ this.db.exec(
22668
+ `DELETE FROM share_destination
22669
+ WHERE NOT EXISTS (
22670
+ SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
22671
+ );
22672
+ }
22673
+ /**
22674
+ * Live totals for one project. Destinations and endpoints are shared across
22675
+ * projects and carry no project column, so both are counted through the call
22676
+ * sites that reference them.
22677
+ */
22678
+ projectTotals(projectKey) {
22679
+ return {
22680
+ destinations: countScalar(
22681
+ this.db,
22682
+ `SELECT count(DISTINCT e.destination_id) AS n
22683
+ FROM share_endpoint e
22684
+ JOIN share_call_site c ON c.endpoint_id = e.id
22685
+ WHERE c.project_key = ?`,
22686
+ [projectKey]
22687
+ ),
22688
+ endpoints: countScalar(
22689
+ this.db,
22690
+ "SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
22691
+ [projectKey]
22692
+ ),
22693
+ callSites: countScalar(
22694
+ this.db,
22695
+ "SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
22696
+ [projectKey]
22697
+ )
22698
+ };
22250
22699
  }
22251
22700
  // ─── Raw fetchers ────────────────────────────────────────────────────────────
22252
22701
  mapDestRow(r) {
@@ -22266,7 +22715,8 @@ var SqliteSharesRepository = class {
22266
22715
  fetchDestinations(q, kinds, reviewOnly = false) {
22267
22716
  const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22268
22717
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22269
- d.created_at AS createdAt, o.decision AS overrideDecision`;
22718
+ d.created_at AS createdAt,
22719
+ COALESCE(oh.decision, ol.decision) AS overrideDecision`;
22270
22720
  const conditions = [];
22271
22721
  const params = [];
22272
22722
  if (kinds && kinds.length > 0) {
@@ -22277,7 +22727,8 @@ var SqliteSharesRepository = class {
22277
22727
  conditions.push(
22278
22728
  `(d.trust IN ('unverified', 'ip')
22279
22729
  OR EXISTS (SELECT 1 FROM share_endpoint re
22280
- WHERE re.destination_id = d.id AND re.transport = 'http'))`
22730
+ WHERE re.destination_id = d.id
22731
+ AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
22281
22732
  );
22282
22733
  }
22283
22734
  let sql;
@@ -22290,7 +22741,7 @@ var SqliteSharesRepository = class {
22290
22741
  params.push(pattern, pattern, pattern, pattern, pattern);
22291
22742
  sql = `SELECT DISTINCT ${cols}
22292
22743
  FROM share_destination d
22293
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22744
+ ${OVERRIDE_JOIN}
22294
22745
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
22295
22746
  LEFT JOIN share_call_site c ON c.endpoint_id = e.id
22296
22747
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
@@ -22298,7 +22749,7 @@ var SqliteSharesRepository = class {
22298
22749
  } else {
22299
22750
  sql = `SELECT ${cols}
22300
22751
  FROM share_destination d
22301
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22752
+ ${OVERRIDE_JOIN}
22302
22753
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
22303
22754
  ORDER BY d.created_at ASC, d.id ASC`;
22304
22755
  }
@@ -22313,9 +22764,9 @@ var SqliteSharesRepository = class {
22313
22764
  this.db.prepare(
22314
22765
  `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22315
22766
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22316
- o.decision AS overrideDecision
22767
+ COALESCE(oh.decision, ol.decision) AS overrideDecision
22317
22768
  FROM share_destination d
22318
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22769
+ ${OVERRIDE_JOIN}
22319
22770
  WHERE d.id = ?`
22320
22771
  ),
22321
22772
  [destinationId]
@@ -22544,6 +22995,7 @@ function openLocalDatabase(dir) {
22544
22995
  const scanLedger = new SqliteScanLedgerRepository(db);
22545
22996
  const exceptions = new SqliteExceptionsRepository(db);
22546
22997
  const resolutions = new SqliteResolutionsRepository(db);
22998
+ const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
22547
22999
  const security = new SqliteSecurityRepository(db);
22548
23000
  const detections = new SqliteDetectionsRepository(db);
22549
23001
  const shares = new SqliteSharesRepository(db);
@@ -22681,6 +23133,7 @@ function openLocalDatabase(dir) {
22681
23133
  scanLedger,
22682
23134
  exceptions,
22683
23135
  resolutions,
23136
+ ruleProbeCache,
22684
23137
  security,
22685
23138
  detections,
22686
23139
  shares,
@@ -22903,13 +23356,581 @@ import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as s
22903
23356
  import { homedir as homedir2 } from "os";
22904
23357
  import { basename as basename2, join as join7 } from "path";
22905
23358
 
23359
+ // ../../packages/detections/src/egress/registry.ts
23360
+ var EXTRACTOR_VERSION = "1";
23361
+ var PROVIDER_REGISTRY = [
23362
+ {
23363
+ id: "stripe",
23364
+ name: "Stripe",
23365
+ category: "Payments",
23366
+ hostSuffixes: ["stripe.com"],
23367
+ apiBase: "https://api.stripe.com",
23368
+ defaultDataClasses: ["pii", "customer"],
23369
+ sdks: {
23370
+ npm: ["stripe"],
23371
+ pypi: ["stripe"],
23372
+ go: ["github.com/stripe/stripe-go"],
23373
+ maven: ["com.stripe"],
23374
+ rubygems: ["stripe"],
23375
+ composer: ["stripe/stripe-php"],
23376
+ nuget: ["Stripe.net"]
23377
+ }
23378
+ },
23379
+ {
23380
+ id: "datadog",
23381
+ name: "Datadog",
23382
+ category: "Observability",
23383
+ hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
23384
+ apiBase: "https://api.datadoghq.com",
23385
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23386
+ sdks: {
23387
+ npm: ["dd-trace", "@datadog/browser-logs"],
23388
+ pypi: ["datadog", "ddtrace"],
23389
+ go: ["github.com/DataDog/dd-trace-go"],
23390
+ maven: ["com.datadoghq"],
23391
+ rubygems: ["ddtrace", "dogapi"],
23392
+ nuget: ["Datadog.Trace"]
23393
+ }
23394
+ },
23395
+ {
23396
+ id: "newrelic",
23397
+ name: "New Relic",
23398
+ category: "Observability",
23399
+ hostSuffixes: ["newrelic.com", "nr-data.net"],
23400
+ apiBase: "https://api.newrelic.com",
23401
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23402
+ sdks: {
23403
+ npm: ["newrelic"],
23404
+ pypi: ["newrelic"],
23405
+ go: ["github.com/newrelic/go-agent"],
23406
+ maven: ["com.newrelic.agent.java"],
23407
+ rubygems: ["newrelic_rpm"],
23408
+ nuget: ["NewRelic.Agent"]
23409
+ }
23410
+ },
23411
+ {
23412
+ id: "sentry",
23413
+ name: "Sentry",
23414
+ category: "Error tracking",
23415
+ hostSuffixes: ["sentry.io"],
23416
+ apiBase: "https://sentry.io",
23417
+ defaultDataClasses: ["source", "telemetry"],
23418
+ sdks: {
23419
+ npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
23420
+ pypi: ["sentry-sdk"],
23421
+ go: ["github.com/getsentry/sentry-go"],
23422
+ maven: ["io.sentry"],
23423
+ rubygems: ["sentry-ruby"],
23424
+ cargo: ["sentry"],
23425
+ composer: ["sentry/sentry"],
23426
+ nuget: ["Sentry"]
23427
+ }
23428
+ },
23429
+ {
23430
+ id: "openai",
23431
+ name: "OpenAI",
23432
+ category: "LLM provider",
23433
+ hostSuffixes: ["openai.com"],
23434
+ apiBase: "https://api.openai.com",
23435
+ defaultDataClasses: ["pii", "source"],
23436
+ sdks: {
23437
+ npm: ["openai"],
23438
+ pypi: ["openai"],
23439
+ go: ["github.com/sashabaranov/go-openai"],
23440
+ maven: ["com.openai"],
23441
+ rubygems: ["ruby-openai"],
23442
+ cargo: ["async-openai"],
23443
+ composer: ["openai-php/client"],
23444
+ nuget: ["OpenAI"]
23445
+ }
23446
+ },
23447
+ {
23448
+ id: "anthropic",
23449
+ name: "Anthropic",
23450
+ category: "LLM provider",
23451
+ hostSuffixes: ["anthropic.com"],
23452
+ apiBase: "https://api.anthropic.com",
23453
+ defaultDataClasses: ["pii", "source"],
23454
+ sdks: {
23455
+ npm: ["@anthropic-ai/sdk"],
23456
+ pypi: ["anthropic"],
23457
+ go: ["github.com/anthropics/anthropic-sdk-go"],
23458
+ nuget: ["Anthropic.SDK"]
23459
+ }
23460
+ },
23461
+ {
23462
+ id: "aws",
23463
+ name: "Amazon Web Services",
23464
+ category: "Cloud platform",
23465
+ hostSuffixes: ["amazonaws.com"],
23466
+ apiBase: "https://s3.amazonaws.com",
23467
+ defaultDataClasses: ["secrets", "customer"],
23468
+ sdks: {
23469
+ npm: ["@aws-sdk/client-s3", "aws-sdk"],
23470
+ pypi: ["boto3"],
23471
+ go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
23472
+ maven: ["com.amazonaws", "software.amazon.awssdk"],
23473
+ rubygems: ["aws-sdk-s3"],
23474
+ cargo: ["aws-sdk-s3"],
23475
+ nuget: ["AWSSDK.S3"]
23476
+ }
23477
+ },
23478
+ {
23479
+ id: "gcp",
23480
+ name: "Google Cloud",
23481
+ category: "Cloud platform",
23482
+ hostSuffixes: ["googleapis.com"],
23483
+ apiBase: "https://storage.googleapis.com",
23484
+ defaultDataClasses: ["customer", "logs"],
23485
+ sdks: {
23486
+ npm: ["@google-cloud/storage"],
23487
+ pypi: ["google-cloud-storage"],
23488
+ go: ["cloud.google.com/go"],
23489
+ maven: ["com.google.cloud"],
23490
+ rubygems: ["google-cloud-storage"],
23491
+ nuget: ["Google.Cloud.Storage.V1"]
23492
+ }
23493
+ },
23494
+ {
23495
+ id: "azure",
23496
+ name: "Microsoft Azure",
23497
+ category: "Cloud platform",
23498
+ hostSuffixes: ["azure.com", "windows.net"],
23499
+ apiBase: "https://management.azure.com",
23500
+ defaultDataClasses: ["customer", "logs"],
23501
+ sdks: {
23502
+ npm: ["@azure/storage-blob"],
23503
+ pypi: ["azure-storage-blob"],
23504
+ go: ["github.com/Azure/azure-sdk-for-go"],
23505
+ maven: ["com.azure"],
23506
+ rubygems: ["azure-storage-blob"],
23507
+ nuget: ["Azure.Storage.Blobs"]
23508
+ }
23509
+ },
23510
+ {
23511
+ id: "slack",
23512
+ name: "Slack",
23513
+ category: "Notifications",
23514
+ hostSuffixes: ["slack.com"],
23515
+ apiBase: "https://slack.com/api",
23516
+ defaultDataClasses: ["logs"],
23517
+ sdks: {
23518
+ npm: ["@slack/web-api"],
23519
+ pypi: ["slack-sdk"],
23520
+ go: ["github.com/slack-go/slack"],
23521
+ maven: ["com.slack.api"],
23522
+ rubygems: ["slack-ruby-client"],
23523
+ composer: ["slack-php/slack-api"],
23524
+ nuget: ["SlackNet"]
23525
+ }
23526
+ },
23527
+ {
23528
+ id: "segment",
23529
+ name: "Segment",
23530
+ category: "Analytics",
23531
+ hostSuffixes: ["segment.io", "segment.com"],
23532
+ apiBase: "https://api.segment.io",
23533
+ defaultDataClasses: ["customer"],
23534
+ sdks: {
23535
+ npm: ["@segment/analytics-node", "analytics-node"],
23536
+ pypi: ["segment-analytics-python"],
23537
+ go: ["github.com/segmentio/analytics-go"],
23538
+ maven: ["com.segment.analytics.java"],
23539
+ rubygems: ["analytics-ruby"],
23540
+ nuget: ["Analytics"]
23541
+ }
23542
+ },
23543
+ {
23544
+ id: "twilio",
23545
+ name: "Twilio",
23546
+ category: "Communications",
23547
+ hostSuffixes: ["twilio.com"],
23548
+ apiBase: "https://api.twilio.com",
23549
+ defaultDataClasses: ["pii", "customer"],
23550
+ sdks: {
23551
+ npm: ["twilio"],
23552
+ pypi: ["twilio"],
23553
+ go: ["github.com/twilio/twilio-go"],
23554
+ maven: ["com.twilio.sdk"],
23555
+ rubygems: ["twilio-ruby"],
23556
+ composer: ["twilio/sdk"],
23557
+ nuget: ["Twilio"]
23558
+ }
23559
+ },
23560
+ {
23561
+ id: "sendgrid",
23562
+ name: "SendGrid",
23563
+ category: "Email",
23564
+ hostSuffixes: ["sendgrid.com"],
23565
+ apiBase: "https://api.sendgrid.com",
23566
+ defaultDataClasses: ["pii"],
23567
+ sdks: {
23568
+ npm: ["@sendgrid/mail"],
23569
+ pypi: ["sendgrid"],
23570
+ go: ["github.com/sendgrid/sendgrid-go"],
23571
+ maven: ["com.sendgrid"],
23572
+ rubygems: ["sendgrid-ruby"],
23573
+ composer: ["sendgrid/sendgrid"],
23574
+ nuget: ["SendGrid"]
23575
+ }
23576
+ },
23577
+ {
23578
+ id: "mailgun",
23579
+ name: "Mailgun",
23580
+ category: "Email",
23581
+ hostSuffixes: ["mailgun.net"],
23582
+ apiBase: "https://api.mailgun.net",
23583
+ defaultDataClasses: ["pii"],
23584
+ sdks: {
23585
+ npm: ["mailgun.js"],
23586
+ pypi: ["mailgun"],
23587
+ rubygems: ["mailgun-ruby"],
23588
+ composer: ["mailgun/mailgun-php"],
23589
+ nuget: ["Mailgun"]
23590
+ }
23591
+ },
23592
+ {
23593
+ id: "mixpanel",
23594
+ name: "Mixpanel",
23595
+ category: "Analytics",
23596
+ hostSuffixes: ["mixpanel.com"],
23597
+ apiBase: "https://api.mixpanel.com",
23598
+ defaultDataClasses: ["customer", "telemetry"],
23599
+ sdks: {
23600
+ npm: ["mixpanel"],
23601
+ pypi: ["mixpanel"],
23602
+ rubygems: ["mixpanel-ruby"],
23603
+ nuget: ["Mixpanel"]
23604
+ }
23605
+ },
23606
+ {
23607
+ id: "amplitude",
23608
+ name: "Amplitude",
23609
+ category: "Analytics",
23610
+ hostSuffixes: ["amplitude.com"],
23611
+ apiBase: "https://api2.amplitude.com",
23612
+ defaultDataClasses: ["customer", "telemetry"],
23613
+ sdks: {
23614
+ npm: ["@amplitude/analytics-node"],
23615
+ pypi: ["amplitude-analytics"],
23616
+ nuget: ["Amplitude"]
23617
+ }
23618
+ },
23619
+ {
23620
+ id: "posthog",
23621
+ name: "PostHog",
23622
+ category: "Analytics",
23623
+ hostSuffixes: ["posthog.com"],
23624
+ apiBase: "https://us.i.posthog.com",
23625
+ defaultDataClasses: ["customer", "telemetry"],
23626
+ sdks: {
23627
+ npm: ["posthog-node", "posthog-js"],
23628
+ pypi: ["posthog"],
23629
+ go: ["github.com/posthog/posthog-go"],
23630
+ rubygems: ["posthog-ruby"],
23631
+ composer: ["posthog/posthog-php"],
23632
+ nuget: ["PostHog"]
23633
+ }
23634
+ },
23635
+ {
23636
+ id: "honeycomb",
23637
+ name: "Honeycomb",
23638
+ category: "Observability",
23639
+ hostSuffixes: ["honeycomb.io"],
23640
+ apiBase: "https://api.honeycomb.io",
23641
+ defaultDataClasses: ["telemetry", "metrics"],
23642
+ sdks: {
23643
+ npm: ["libhoney"],
23644
+ pypi: ["libhoney"],
23645
+ go: ["github.com/honeycombio/libhoney-go"],
23646
+ rubygems: ["libhoney"]
23647
+ }
23648
+ },
23649
+ {
23650
+ id: "grafana",
23651
+ name: "Grafana Cloud",
23652
+ category: "Observability",
23653
+ hostSuffixes: ["grafana.net"],
23654
+ apiBase: "https://grafana.net",
23655
+ defaultDataClasses: ["logs", "metrics"],
23656
+ sdks: {
23657
+ npm: ["@grafana/faro-web-sdk"]
23658
+ }
23659
+ },
23660
+ {
23661
+ id: "splunk",
23662
+ name: "Splunk",
23663
+ category: "Observability",
23664
+ hostSuffixes: ["splunkcloud.com", "splunk.com"],
23665
+ apiBase: "https://http-inputs.splunkcloud.com",
23666
+ defaultDataClasses: ["logs"],
23667
+ sdks: {
23668
+ npm: ["splunk-logging"],
23669
+ pypi: ["splunk-sdk"],
23670
+ maven: ["com.splunk"],
23671
+ nuget: ["Splunk.Logging.Common"]
23672
+ }
23673
+ },
23674
+ {
23675
+ id: "pagerduty",
23676
+ name: "PagerDuty",
23677
+ category: "Incident response",
23678
+ hostSuffixes: ["pagerduty.com"],
23679
+ apiBase: "https://api.pagerduty.com",
23680
+ defaultDataClasses: ["logs"],
23681
+ sdks: {
23682
+ npm: ["@pagerduty/pdjs"],
23683
+ pypi: ["pdpyras"],
23684
+ go: ["github.com/PagerDuty/go-pagerduty"],
23685
+ rubygems: ["pagerduty"]
23686
+ }
23687
+ },
23688
+ {
23689
+ id: "github",
23690
+ name: "GitHub",
23691
+ category: "Developer platform",
23692
+ hostSuffixes: ["github.com", "githubusercontent.com"],
23693
+ apiBase: "https://api.github.com",
23694
+ defaultDataClasses: ["source"],
23695
+ sdks: {
23696
+ npm: ["@octokit/rest", "octokit"],
23697
+ pypi: ["pygithub"],
23698
+ go: ["github.com/google/go-github"],
23699
+ maven: ["org.kohsuke.github-api"],
23700
+ rubygems: ["octokit"],
23701
+ cargo: ["octocrab"],
23702
+ composer: ["knplabs/github-api"],
23703
+ nuget: ["Octokit"]
23704
+ }
23705
+ },
23706
+ {
23707
+ id: "gitlab",
23708
+ name: "GitLab",
23709
+ category: "Developer platform",
23710
+ hostSuffixes: ["gitlab.com"],
23711
+ apiBase: "https://gitlab.com/api",
23712
+ defaultDataClasses: ["source"],
23713
+ sdks: {
23714
+ npm: ["@gitbeaker/rest"],
23715
+ pypi: ["python-gitlab"],
23716
+ go: ["gitlab.com/gitlab-org/api/client-go"],
23717
+ rubygems: ["gitlab"],
23718
+ nuget: ["GitLabApiClient"]
23719
+ }
23720
+ },
23721
+ {
23722
+ id: "auth0",
23723
+ name: "Auth0",
23724
+ category: "Identity",
23725
+ hostSuffixes: ["auth0.com"],
23726
+ apiBase: "https://login.auth0.com",
23727
+ defaultDataClasses: ["pii"],
23728
+ sdks: {
23729
+ npm: ["auth0"],
23730
+ pypi: ["auth0-python"],
23731
+ go: ["github.com/auth0/go-auth0"],
23732
+ maven: ["com.auth0"],
23733
+ rubygems: ["auth0"],
23734
+ composer: ["auth0/auth0-php"],
23735
+ nuget: ["Auth0.ManagementApi"]
23736
+ }
23737
+ },
23738
+ {
23739
+ id: "okta",
23740
+ name: "Okta",
23741
+ category: "Identity",
23742
+ hostSuffixes: ["okta.com", "oktapreview.com"],
23743
+ apiBase: "https://login.okta.com",
23744
+ defaultDataClasses: ["pii"],
23745
+ sdks: {
23746
+ npm: ["@okta/okta-sdk-nodejs"],
23747
+ pypi: ["okta"],
23748
+ go: ["github.com/okta/okta-sdk-golang"],
23749
+ maven: ["com.okta.sdk"],
23750
+ nuget: ["Okta.Sdk"]
23751
+ }
23752
+ },
23753
+ {
23754
+ id: "clerk",
23755
+ name: "Clerk",
23756
+ category: "Identity",
23757
+ hostSuffixes: ["clerk.com", "clerk.dev"],
23758
+ apiBase: "https://api.clerk.com",
23759
+ defaultDataClasses: ["pii"],
23760
+ sdks: {
23761
+ npm: ["@clerk/backend", "@clerk/nextjs"],
23762
+ pypi: ["clerk-backend-api"],
23763
+ go: ["github.com/clerk/clerk-sdk-go"]
23764
+ }
23765
+ },
23766
+ {
23767
+ id: "supabase",
23768
+ name: "Supabase",
23769
+ category: "Backend platform",
23770
+ hostSuffixes: ["supabase.co", "supabase.com"],
23771
+ apiBase: "https://api.supabase.com",
23772
+ defaultDataClasses: ["pii", "customer"],
23773
+ sdks: {
23774
+ npm: ["@supabase/supabase-js"],
23775
+ pypi: ["supabase"],
23776
+ cargo: ["postgrest"]
23777
+ }
23778
+ },
23779
+ {
23780
+ id: "firebase",
23781
+ name: "Firebase",
23782
+ category: "Backend platform",
23783
+ hostSuffixes: ["firebaseio.com", "firebase.google.com"],
23784
+ apiBase: "https://firebaseio.com",
23785
+ defaultDataClasses: ["customer"],
23786
+ sdks: {
23787
+ npm: ["firebase", "firebase-admin"],
23788
+ pypi: ["firebase-admin"],
23789
+ go: ["firebase.google.com/go"],
23790
+ maven: ["com.google.firebase"]
23791
+ }
23792
+ },
23793
+ {
23794
+ id: "mongodb-atlas",
23795
+ name: "MongoDB Atlas",
23796
+ category: "Database SaaS",
23797
+ hostSuffixes: ["mongodb.net", "mongodb.com"],
23798
+ apiBase: "https://cloud.mongodb.com",
23799
+ defaultDataClasses: ["customer"],
23800
+ sdks: {
23801
+ npm: ["mongodb"],
23802
+ pypi: ["pymongo"],
23803
+ go: ["go.mongodb.org/mongo-driver"],
23804
+ maven: ["org.mongodb"],
23805
+ rubygems: ["mongo"],
23806
+ cargo: ["mongodb"],
23807
+ nuget: ["MongoDB.Driver"]
23808
+ }
23809
+ },
23810
+ {
23811
+ id: "planetscale",
23812
+ name: "PlanetScale",
23813
+ category: "Database SaaS",
23814
+ hostSuffixes: ["psdb.cloud", "planetscale.com"],
23815
+ apiBase: "https://api.planetscale.com",
23816
+ defaultDataClasses: ["customer"],
23817
+ sdks: {
23818
+ npm: ["@planetscale/database"],
23819
+ go: ["github.com/planetscale/planetscale-go"]
23820
+ }
23821
+ },
23822
+ {
23823
+ id: "algolia",
23824
+ name: "Algolia",
23825
+ category: "Search SaaS",
23826
+ hostSuffixes: ["algolia.net", "algolianet.com"],
23827
+ apiBase: "https://algolia.net",
23828
+ defaultDataClasses: ["customer"],
23829
+ sdks: {
23830
+ npm: ["algoliasearch"],
23831
+ pypi: ["algoliasearch"],
23832
+ go: ["github.com/algolia/algoliasearch-client-go"],
23833
+ maven: ["com.algolia"],
23834
+ rubygems: ["algolia"],
23835
+ composer: ["algolia/algoliasearch-client-php"],
23836
+ nuget: ["Algolia.Search"]
23837
+ }
23838
+ },
23839
+ {
23840
+ id: "cloudflare",
23841
+ name: "Cloudflare",
23842
+ category: "CDN / edge",
23843
+ hostSuffixes: ["cloudflare.com", "workers.dev"],
23844
+ apiBase: "https://api.cloudflare.com",
23845
+ defaultDataClasses: ["logs"],
23846
+ sdks: {
23847
+ npm: ["cloudflare"],
23848
+ pypi: ["cloudflare"],
23849
+ go: ["github.com/cloudflare/cloudflare-go"],
23850
+ nuget: ["CloudFlare.Client"]
23851
+ }
23852
+ },
23853
+ {
23854
+ id: "huggingface",
23855
+ name: "Hugging Face",
23856
+ category: "LLM provider",
23857
+ hostSuffixes: ["huggingface.co"],
23858
+ apiBase: "https://api-inference.huggingface.co",
23859
+ defaultDataClasses: ["source"],
23860
+ sdks: {
23861
+ npm: ["@huggingface/inference"],
23862
+ pypi: ["huggingface-hub", "transformers"],
23863
+ rubygems: ["hugging-face"]
23864
+ }
23865
+ },
23866
+ {
23867
+ id: "cohere",
23868
+ name: "Cohere",
23869
+ category: "LLM provider",
23870
+ hostSuffixes: ["cohere.com", "cohere.ai"],
23871
+ apiBase: "https://api.cohere.com",
23872
+ defaultDataClasses: ["pii", "source"],
23873
+ sdks: {
23874
+ npm: ["cohere-ai"],
23875
+ pypi: ["cohere"],
23876
+ go: ["github.com/cohere-ai/cohere-go"]
23877
+ }
23878
+ },
23879
+ {
23880
+ id: "mistral",
23881
+ name: "Mistral AI",
23882
+ category: "LLM provider",
23883
+ hostSuffixes: ["mistral.ai"],
23884
+ apiBase: "https://api.mistral.ai",
23885
+ defaultDataClasses: ["pii", "source"],
23886
+ sdks: {
23887
+ npm: ["@mistralai/mistralai"],
23888
+ pypi: ["mistralai"],
23889
+ go: ["github.com/gage-technologies/mistral-go"]
23890
+ }
23891
+ }
23892
+ ];
23893
+ var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
23894
+ ${JSON.stringify(PROVIDER_REGISTRY)}`;
23895
+
23896
+ // ../../packages/detections/src/egress/extract.ts
23897
+ var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
23898
+ var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
23899
+ var SECRET_VALUE = new RegExp(
23900
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
23901
+ "gi"
23902
+ );
23903
+ var AUTH_SCHEME_VALUE = new RegExp(
23904
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
23905
+ "gi"
23906
+ );
23907
+ var WEBHOOK_SECRET_PATHS = [
23908
+ { hosts: ["hooks.slack.com"], prefix: "/services/" },
23909
+ {
23910
+ hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
23911
+ prefix: "/api/webhooks/"
23912
+ },
23913
+ { hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
23914
+ { hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
23915
+ ];
23916
+ function escapeRegExp(literal2) {
23917
+ return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23918
+ }
23919
+ var WEBHOOK_URL = new RegExp(
23920
+ `(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
23921
+ (entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
23922
+ ).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
23923
+ "gi"
23924
+ );
23925
+
22906
23926
  // ../../packages/detections/src/escape-regexp.ts
22907
- function escapeRegExp(value) {
23927
+ function escapeRegExp2(value) {
22908
23928
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22909
23929
  }
22910
23930
 
22911
23931
  // ../../packages/detections/src/matchers/limits.ts
22912
23932
  var MAX_MATCHES_PER_RULE = 1e4;
23933
+ var MAX_REGEX_INPUT_LENGTH = 2e5;
22913
23934
 
22914
23935
  // ../../packages/detections/src/matchers/keyword.ts
22915
23936
  var KeywordMatcher2 = class {
@@ -22920,7 +23941,7 @@ var KeywordMatcher2 = class {
22920
23941
  for (const kw of keywords) {
22921
23942
  if (kw.length === 0) continue;
22922
23943
  if (spans.length >= MAX_MATCHES_PER_RULE) break;
22923
- const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
23944
+ const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
22924
23945
  let m;
22925
23946
  while ((m = re.exec(text)) !== null) {
22926
23947
  spans.push({ start: m.index, end: m.index + m[0].length });
@@ -22937,9 +23958,13 @@ var RegexMatcher2 = class {
22937
23958
  if (rule.matcher.type !== "regex") return [];
22938
23959
  const { pattern, flags, captureGroup } = rule.matcher;
22939
23960
  const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
23961
+ const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
22940
23962
  const spans = [];
22941
23963
  let m;
22942
- while ((m = re.exec(text)) !== null) {
23964
+ const maxIterations = scanText2.length + 1;
23965
+ let iterations = 0;
23966
+ while ((m = re.exec(scanText2)) !== null) {
23967
+ if (++iterations > maxIterations) break;
22943
23968
  const group = captureGroup != null ? m[captureGroup] : m[0];
22944
23969
  if (m[0].length === 0) re.lastIndex++;
22945
23970
  if (group && spans.length < MAX_MATCHES_PER_RULE) {
@@ -23043,7 +24068,7 @@ function isCorroborated(candidate, candidates, text) {
23043
24068
  for (const label of labels) {
23044
24069
  const trimmed = label.trim();
23045
24070
  if (trimmed.length === 0) continue;
23046
- const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(trimmed)}(?![A-Za-z0-9])`, "i");
24071
+ const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
23047
24072
  if (re.test(haystack)) return true;
23048
24073
  }
23049
24074
  }
@@ -23208,6 +24233,31 @@ var CONFIG_POSTURE_RULES = [
23208
24233
  }
23209
24234
  ];
23210
24235
 
24236
+ // ../../packages/detections/src/security/redos-probe.ts
24237
+ var EXPONENTIAL_UNITS = [
24238
+ "a",
24239
+ "0",
24240
+ " ",
24241
+ "x",
24242
+ "ab",
24243
+ "a.",
24244
+ "a-",
24245
+ "a_",
24246
+ "a@",
24247
+ "a/",
24248
+ "a:",
24249
+ "a=",
24250
+ "a;",
24251
+ "aA0",
24252
+ " "
24253
+ ];
24254
+ var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
24255
+ (unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
24256
+ );
24257
+ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
24258
+ (unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
24259
+ );
24260
+
23211
24261
  // ../../rules/code-flaws/auth-jwt-no-verify.json
23212
24262
  var auth_jwt_no_verify_default = {
23213
24263
  specVersion: 1,
@@ -25434,10 +26484,14 @@ function resolveInventoryContext(input) {
25434
26484
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25435
26485
  import { join as join8 } from "path";
25436
26486
 
26487
+ // ../../packages/plugin-sdk/src/paths.ts
26488
+ import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
26489
+ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
26490
+
25437
26491
  // ../../packages/plugin-sdk/src/project-files.ts
25438
26492
  var import_ignore = __toESM(require_ignore(), 1);
25439
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25440
- import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
26493
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
26494
+ import { basename as basename4, join as join9, relative, sep as sep4 } from "path";
25441
26495
 
25442
26496
  // ../../packages/plugin-sdk/src/runtime.ts
25443
26497
  import { randomUUID as randomUUID10 } from "crypto";
@@ -25718,6 +26772,13 @@ var StandaloneDataGateway = class {
25718
26772
  this.db.scanLedger.upsertEntries(entries);
25719
26773
  return Promise.resolve();
25720
26774
  }
26775
+ getRuleProbeVerdict(ruleKey) {
26776
+ return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
26777
+ }
26778
+ setRuleProbeVerdict(ruleKey, verdict, worstProbeMs) {
26779
+ this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs);
26780
+ return Promise.resolve();
26781
+ }
25721
26782
  openAtRestKeysForPath(path) {
25722
26783
  return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
25723
26784
  }
@@ -25728,6 +26789,12 @@ var StandaloneDataGateway = class {
25728
26789
  this.db.resolutions.insertResolution(input);
25729
26790
  return Promise.resolve();
25730
26791
  }
26792
+ // Bare forward — no toggle read here. The plugin-path kill-switch is
26793
+ // enforced by the caller, which already holds the parsed workspace
26794
+ // settings; this class only ever sees `dataDir`, not the settings base.
26795
+ recordProjectEgress(input) {
26796
+ return Promise.resolve(this.db.shares.recordProjectEgress(input));
26797
+ }
25731
26798
  close() {
25732
26799
  this.db.close();
25733
26800
  return Promise.resolve();
@@ -25829,7 +26896,7 @@ function readTail(transcriptPath, startOffset) {
25829
26896
  }
25830
26897
 
25831
26898
  // src/history/transcripts.ts
25832
- import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
26899
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
25833
26900
  import { homedir as homedir3 } from "os";
25834
26901
  import { join as join12 } from "path";
25835
26902
  function isRecord(value) {