@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, flags2) {
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), {
@@ -16795,6 +16801,212 @@ function buildDetectionsList(summaries, query) {
16795
16801
  return { counts, items: filtered.map(summaryToDetectionListItem) };
16796
16802
  }
16797
16803
 
16804
+ // ../../packages/schema/src/zod/shares.ts
16805
+ var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
16806
+ var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
16807
+ var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
16808
+ var DATA_CLASS_ORDER = DataClass.options;
16809
+ var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
16810
+ var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
16811
+ var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
16812
+ var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
16813
+ var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
16814
+ var ReviewInfo = external_exports.object({
16815
+ needsReview: external_exports.boolean(),
16816
+ reasons: external_exports.array(ReviewReason)
16817
+ }).meta({ id: "ReviewInfo" });
16818
+ var DestinationNetwork = external_exports.object({
16819
+ port: external_exports.number().int().nullable(),
16820
+ geo: external_exports.string().nullable(),
16821
+ ptr: external_exports.string().nullable()
16822
+ }).meta({ id: "DestinationNetwork" });
16823
+ var EndpointSummary = external_exports.object({
16824
+ id: external_exports.string(),
16825
+ method: HttpMethod,
16826
+ transport: Transport,
16827
+ url: external_exports.string(),
16828
+ template: external_exports.boolean(),
16829
+ dataClass: DataClass,
16830
+ lastSeen: external_exports.iso.datetime(),
16831
+ callSiteCount: external_exports.number().int().nonnegative()
16832
+ }).meta({ id: "EndpointSummary" });
16833
+ var CallSite = external_exports.object({
16834
+ id: external_exports.string(),
16835
+ project: external_exports.string(),
16836
+ file: external_exports.string(),
16837
+ line: external_exports.number().int().nonnegative(),
16838
+ snippet: external_exports.string(),
16839
+ dynamic: external_exports.boolean(),
16840
+ vendored: external_exports.boolean(),
16841
+ /** Deep-link to the Inventory project, when the repo is governed there. */
16842
+ projectId: external_exports.string().nullable()
16843
+ }).meta({ id: "CallSite" });
16844
+ var EndpointWithSites = EndpointSummary.extend({
16845
+ sites: external_exports.array(CallSite)
16846
+ }).meta({ id: "EndpointWithSites" });
16847
+ var ShareDestinationSummary = external_exports.object({
16848
+ id: external_exports.string(),
16849
+ kind: DestinationKind,
16850
+ name: external_exports.string(),
16851
+ host: external_exports.string(),
16852
+ category: external_exports.string(),
16853
+ trust: ShareTrustLevel,
16854
+ /** Effective state (decision applied over the trust default). */
16855
+ status: EgressStatus,
16856
+ /** True when an egress decision override differs from the trust default. */
16857
+ isCustom: external_exports.boolean(),
16858
+ lastSeen: external_exports.iso.datetime(),
16859
+ endpointCount: external_exports.number().int().nonnegative(),
16860
+ callSiteCount: external_exports.number().int().nonnegative(),
16861
+ transports: external_exports.array(Transport),
16862
+ /** Most-sensitive first. */
16863
+ dataClasses: external_exports.array(DataClass),
16864
+ review: ReviewInfo,
16865
+ /** Non-provider hosts only; null for providers. */
16866
+ network: DestinationNetwork.nullable(),
16867
+ /** Embedded for inline expansion — no call sites here. */
16868
+ endpoints: external_exports.array(EndpointSummary)
16869
+ }).meta({ id: "ShareDestinationSummary" });
16870
+ var ShareDestinationDetail = ShareDestinationSummary.omit({
16871
+ endpointCount: true,
16872
+ callSiteCount: true,
16873
+ endpoints: true
16874
+ }).extend({
16875
+ /** Ownership/geo rationale; null for providers. */
16876
+ note: external_exports.string().nullable(),
16877
+ endpoints: external_exports.array(EndpointWithSites)
16878
+ }).meta({ id: "ShareDestinationDetail" });
16879
+ var ReviewDestination = external_exports.object({
16880
+ id: external_exports.string(),
16881
+ kind: DestinationKind,
16882
+ name: external_exports.string(),
16883
+ /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
16884
+ host: external_exports.string(),
16885
+ trust: ShareTrustLevel,
16886
+ status: EgressStatus,
16887
+ review: ReviewInfo,
16888
+ topDataClass: DataClass,
16889
+ callSiteCount: external_exports.number().int().nonnegative(),
16890
+ lastSeen: external_exports.iso.datetime()
16891
+ }).meta({ id: "ReviewDestination" });
16892
+ var ShareDestinationGroup = external_exports.object({
16893
+ kind: DestinationKind,
16894
+ total: external_exports.number().int().nonnegative(),
16895
+ items: external_exports.array(ShareDestinationSummary)
16896
+ }).meta({ id: "ShareDestinationGroup" });
16897
+ var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
16898
+ var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
16899
+ var SharesStats = external_exports.object({
16900
+ destinations: external_exports.number().int().nonnegative(),
16901
+ endpoints: external_exports.number().int().nonnegative(),
16902
+ callSites: external_exports.number().int().nonnegative(),
16903
+ needsReview: external_exports.number().int().nonnegative(),
16904
+ insecure: external_exports.number().int().nonnegative(),
16905
+ byKind: external_exports.object({
16906
+ provider: external_exports.number().int().nonnegative(),
16907
+ internal: external_exports.number().int().nonnegative(),
16908
+ external: external_exports.number().int().nonnegative(),
16909
+ ip: external_exports.number().int().nonnegative()
16910
+ }),
16911
+ byTrust: external_exports.object({
16912
+ recognized: external_exports.number().int().nonnegative(),
16913
+ internal: external_exports.number().int().nonnegative(),
16914
+ unverified: external_exports.number().int().nonnegative(),
16915
+ ip: external_exports.number().int().nonnegative()
16916
+ })
16917
+ }).meta({ id: "SharesStats" });
16918
+ var SetEgressDecisionBody = external_exports.object({
16919
+ /** `null` clears the override — reverts to the trust default, isCustom false. */
16920
+ decision: EgressDecision.nullable()
16921
+ }).meta({ id: "SetEgressDecisionBody" });
16922
+ var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
16923
+ var ListShareDestinationsQuery = external_exports.object({
16924
+ /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
16925
+ q: external_exports.string().optional(),
16926
+ /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
16927
+ kind: external_exports.array(DestinationKind).optional(),
16928
+ /** Reserved for future grouping modes; only 'destination' is supported today. */
16929
+ groupBy: external_exports.enum(["destination"]).default("destination"),
16930
+ /**
16931
+ * When true, return a flat severity-ordered `items[]` instead of `groups`.
16932
+ * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
16933
+ * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
16934
+ * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
16935
+ */
16936
+ review: external_exports.stringbool().default(false)
16937
+ });
16938
+ var ExportSharesQuery = external_exports.object({
16939
+ format: external_exports.enum(["csv", "json"]).default("csv"),
16940
+ q: external_exports.string().optional(),
16941
+ kind: external_exports.array(DestinationKind).optional()
16942
+ });
16943
+
16944
+ // ../../packages/schema/src/zod/egress-extraction.ts
16945
+ var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
16946
+ var ProviderRegistryEntry = external_exports.object({
16947
+ id: external_exports.string(),
16948
+ name: external_exports.string(),
16949
+ category: external_exports.string(),
16950
+ /** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
16951
+ hostSuffixes: external_exports.array(external_exports.string()).min(1),
16952
+ /** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
16953
+ apiBase: external_exports.string(),
16954
+ /** Most-sensitive first; index 0 becomes the endpoint dataClass. */
16955
+ defaultDataClasses: external_exports.array(DataClass).min(1),
16956
+ /** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
16957
+ sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
16958
+ }).meta({ id: "ProviderRegistryEntry" });
16959
+ var EgressCallSiteHit = external_exports.object({
16960
+ file: external_exports.string(),
16961
+ line: external_exports.number().int().positive(),
16962
+ snippet: external_exports.string(),
16963
+ dynamic: external_exports.boolean(),
16964
+ vendored: external_exports.boolean()
16965
+ }).meta({ id: "EgressCallSiteHit" });
16966
+ var ResolvedEgressHit = external_exports.object({
16967
+ host: external_exports.string(),
16968
+ kind: DestinationKind,
16969
+ name: external_exports.string(),
16970
+ category: external_exports.string(),
16971
+ trust: ShareTrustLevel,
16972
+ network: DestinationNetwork.nullable(),
16973
+ method: HttpMethod,
16974
+ transport: Transport,
16975
+ url: external_exports.string(),
16976
+ template: external_exports.boolean(),
16977
+ dataClass: DataClass,
16978
+ site: EgressCallSiteHit
16979
+ }).meta({ id: "ResolvedEgressHit" });
16980
+ var EgressReconcile = external_exports.discriminatedUnion("mode", [
16981
+ external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
16982
+ external_exports.object({
16983
+ mode: external_exports.literal("ledger"),
16984
+ scannedFiles: external_exports.array(external_exports.string()),
16985
+ deletedFiles: external_exports.array(external_exports.string())
16986
+ })
16987
+ ]).meta({ id: "EgressReconcile" });
16988
+ var RecordProjectEgressInput = external_exports.object({
16989
+ /** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
16990
+ projectKey: external_exports.string().min(1),
16991
+ /** Display name only — never keys reconciliation. */
16992
+ project: external_exports.string(),
16993
+ projectId: external_exports.string().nullable(),
16994
+ reconcile: EgressReconcile,
16995
+ hits: external_exports.array(ResolvedEgressHit)
16996
+ }).meta({ id: "RecordProjectEgressInput" });
16997
+ var EgressWriteSummary = external_exports.object({
16998
+ destinations: external_exports.number().int().nonnegative(),
16999
+ endpoints: external_exports.number().int().nonnegative(),
17000
+ callSites: external_exports.number().int().nonnegative(),
17001
+ truncated: external_exports.boolean(),
17002
+ /**
17003
+ * Files the cap dropped whole. Their stored rows were left untouched, so a
17004
+ * ledger-keeping caller must withhold their ledger entries and read them
17005
+ * again next scan.
17006
+ */
17007
+ droppedFiles: external_exports.array(external_exports.string()).default([])
17008
+ }).meta({ id: "EgressWriteSummary" });
17009
+
16798
17010
  // ../../packages/schema/src/zod/findings-group-build.ts
16799
17011
  function toApiAction(dbVal) {
16800
17012
  const map2 = {
@@ -17056,7 +17268,7 @@ var PatchInstalledPackRequest = external_exports.object({
17056
17268
  }).meta({ id: "PatchInstalledPackRequest" });
17057
17269
 
17058
17270
  // ../../packages/schema/src/zod/local.ts
17059
- var WORKSPACE_SETTINGS_SPEC_VERSION = 2;
17271
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 3;
17060
17272
  var RunMode = external_exports.enum(["standalone"]);
17061
17273
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17062
17274
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17071,6 +17283,9 @@ var WorkspaceSettings = external_exports.object({
17071
17283
  policy: SimpleDetectionPolicy.default("redact"),
17072
17284
  // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
17073
17285
  historicalAccess: HistoricalAccess.default("session-only"),
17286
+ // In-place egress extraction on the scan paths; disable to stop all Data
17287
+ // Shares writes.
17288
+ dataSharesInPlace: external_exports.boolean().default(true),
17074
17289
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17075
17290
  onboardedAt: external_exports.iso.datetime().optional()
17076
17291
  });
@@ -17554,145 +17769,6 @@ var SetupHandoffOffer = external_exports.object({
17554
17769
  path: ["liveKeys"]
17555
17770
  });
17556
17771
 
17557
- // ../../packages/schema/src/zod/shares.ts
17558
- var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17559
- var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
17560
- var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
17561
- var DATA_CLASS_ORDER = DataClass.options;
17562
- var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
17563
- var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
17564
- var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
17565
- var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
17566
- var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
17567
- var ReviewInfo = external_exports.object({
17568
- needsReview: external_exports.boolean(),
17569
- reasons: external_exports.array(ReviewReason)
17570
- }).meta({ id: "ReviewInfo" });
17571
- var DestinationNetwork = external_exports.object({
17572
- port: external_exports.number().int().nullable(),
17573
- geo: external_exports.string().nullable(),
17574
- ptr: external_exports.string().nullable()
17575
- }).meta({ id: "DestinationNetwork" });
17576
- var EndpointSummary = external_exports.object({
17577
- id: external_exports.string(),
17578
- method: HttpMethod,
17579
- transport: Transport,
17580
- url: external_exports.string(),
17581
- template: external_exports.boolean(),
17582
- dataClass: DataClass,
17583
- lastSeen: external_exports.iso.datetime(),
17584
- callSiteCount: external_exports.number().int().nonnegative()
17585
- }).meta({ id: "EndpointSummary" });
17586
- var CallSite = external_exports.object({
17587
- id: external_exports.string(),
17588
- project: external_exports.string(),
17589
- file: external_exports.string(),
17590
- line: external_exports.number().int().nonnegative(),
17591
- snippet: external_exports.string(),
17592
- dynamic: external_exports.boolean(),
17593
- vendored: external_exports.boolean(),
17594
- /** Deep-link to the Inventory project, when the repo is governed there. */
17595
- projectId: external_exports.string().nullable()
17596
- }).meta({ id: "CallSite" });
17597
- var EndpointWithSites = EndpointSummary.extend({
17598
- sites: external_exports.array(CallSite)
17599
- }).meta({ id: "EndpointWithSites" });
17600
- var ShareDestinationSummary = external_exports.object({
17601
- id: external_exports.string(),
17602
- kind: DestinationKind,
17603
- name: external_exports.string(),
17604
- host: external_exports.string(),
17605
- category: external_exports.string(),
17606
- trust: ShareTrustLevel,
17607
- /** Effective state (decision applied over the trust default). */
17608
- status: EgressStatus,
17609
- /** True when an egress decision override differs from the trust default. */
17610
- isCustom: external_exports.boolean(),
17611
- lastSeen: external_exports.iso.datetime(),
17612
- endpointCount: external_exports.number().int().nonnegative(),
17613
- callSiteCount: external_exports.number().int().nonnegative(),
17614
- transports: external_exports.array(Transport),
17615
- /** Most-sensitive first. */
17616
- dataClasses: external_exports.array(DataClass),
17617
- review: ReviewInfo,
17618
- /** Non-provider hosts only; null for providers. */
17619
- network: DestinationNetwork.nullable(),
17620
- /** Embedded for inline expansion — no call sites here. */
17621
- endpoints: external_exports.array(EndpointSummary)
17622
- }).meta({ id: "ShareDestinationSummary" });
17623
- var ShareDestinationDetail = ShareDestinationSummary.omit({
17624
- endpointCount: true,
17625
- callSiteCount: true,
17626
- endpoints: true
17627
- }).extend({
17628
- /** Ownership/geo rationale; null for providers. */
17629
- note: external_exports.string().nullable(),
17630
- endpoints: external_exports.array(EndpointWithSites)
17631
- }).meta({ id: "ShareDestinationDetail" });
17632
- var ReviewDestination = external_exports.object({
17633
- id: external_exports.string(),
17634
- kind: DestinationKind,
17635
- name: external_exports.string(),
17636
- /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
17637
- host: external_exports.string(),
17638
- trust: ShareTrustLevel,
17639
- status: EgressStatus,
17640
- review: ReviewInfo,
17641
- topDataClass: DataClass,
17642
- callSiteCount: external_exports.number().int().nonnegative(),
17643
- lastSeen: external_exports.iso.datetime()
17644
- }).meta({ id: "ReviewDestination" });
17645
- var ShareDestinationGroup = external_exports.object({
17646
- kind: DestinationKind,
17647
- total: external_exports.number().int().nonnegative(),
17648
- items: external_exports.array(ShareDestinationSummary)
17649
- }).meta({ id: "ShareDestinationGroup" });
17650
- var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
17651
- var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
17652
- var SharesStats = external_exports.object({
17653
- destinations: external_exports.number().int().nonnegative(),
17654
- endpoints: external_exports.number().int().nonnegative(),
17655
- callSites: external_exports.number().int().nonnegative(),
17656
- needsReview: external_exports.number().int().nonnegative(),
17657
- insecure: external_exports.number().int().nonnegative(),
17658
- byKind: external_exports.object({
17659
- provider: external_exports.number().int().nonnegative(),
17660
- internal: external_exports.number().int().nonnegative(),
17661
- ip: external_exports.number().int().nonnegative()
17662
- }),
17663
- byTrust: external_exports.object({
17664
- recognized: external_exports.number().int().nonnegative(),
17665
- internal: external_exports.number().int().nonnegative(),
17666
- unverified: external_exports.number().int().nonnegative(),
17667
- ip: external_exports.number().int().nonnegative()
17668
- })
17669
- }).meta({ id: "SharesStats" });
17670
- var SetEgressDecisionBody = external_exports.object({
17671
- /** `null` clears the override — reverts to the trust default, isCustom false. */
17672
- decision: EgressDecision.nullable()
17673
- }).meta({ id: "SetEgressDecisionBody" });
17674
- var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
17675
- var ListShareDestinationsQuery = external_exports.object({
17676
- /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
17677
- q: external_exports.string().optional(),
17678
- /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
17679
- kind: external_exports.array(DestinationKind).optional(),
17680
- /** Reserved for future grouping modes; only 'destination' is supported today. */
17681
- groupBy: external_exports.enum(["destination"]).default("destination"),
17682
- /**
17683
- * When true, return a flat severity-ordered `items[]` instead of `groups`.
17684
- * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
17685
- * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
17686
- * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17687
- */
17688
- review: external_exports.stringbool().default(false)
17689
- });
17690
- var ExportSharesQuery = external_exports.object({
17691
- format: external_exports.enum(["csv", "json"]).default("csv"),
17692
- q: external_exports.string().optional(),
17693
- kind: external_exports.array(DestinationKind).optional()
17694
- });
17695
-
17696
17772
  // ../../packages/schema/src/zod/shares-access.ts
17697
17773
  var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
17698
17774
  function trustDefaultStatus(trust) {
@@ -17712,7 +17788,7 @@ function deriveReviewReasons(trust, transports) {
17712
17788
  const reasons = [];
17713
17789
  if (trust === "ip") reasons.push("raw_ip");
17714
17790
  if (trust === "unverified") reasons.push("unverified_domain");
17715
- if (transports.includes("http")) reasons.push("plaintext_transport");
17791
+ if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
17716
17792
  return reasons;
17717
17793
  }
17718
17794
  function buildReviewInfo(trust, transports) {
@@ -17932,6 +18008,7 @@ function applyMigrations(db) {
17932
18008
  ensureSyncedAtColumn(db, "audit_events");
17933
18009
  ensureScanLedgerTable(db);
17934
18010
  ensureBlockedDetectionsTable(db);
18011
+ ensureRuleProbeCacheTable(db);
17935
18012
  ensureWriteGateTrigger(db);
17936
18013
  ensureTokenUsageColumns(db);
17937
18014
  reconcileSourceProjectIds(db);
@@ -18071,6 +18148,14 @@ function ensureBlockedDetectionsTable(db) {
18071
18148
  blocked_at INTEGER NOT NULL
18072
18149
  )`);
18073
18150
  }
18151
+ function ensureRuleProbeCacheTable(db) {
18152
+ db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
18153
+ rule_key TEXT PRIMARY KEY,
18154
+ verdict TEXT NOT NULL,
18155
+ worst_probe_ms REAL NOT NULL,
18156
+ checked_at INTEGER NOT NULL
18157
+ )`);
18158
+ }
18074
18159
 
18075
18160
  // ../../packages/persistence/src/paths.ts
18076
18161
  import { chmodSync, mkdirSync } from "fs";
@@ -21624,6 +21709,35 @@ var SqliteResolutionsRepository = class {
21624
21709
  }
21625
21710
  };
21626
21711
 
21712
+ // ../../packages/persistence/src/repositories/rule-probe-cache.ts
21713
+ var SqliteRuleProbeCacheRepository = class {
21714
+ constructor(db) {
21715
+ this.db = db;
21716
+ this.upsertStmt = db.prepare(
21717
+ `INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
21718
+ VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
21719
+ ON CONFLICT (rule_key) DO UPDATE SET
21720
+ verdict = excluded.verdict,
21721
+ worst_probe_ms = excluded.worst_probe_ms,
21722
+ checked_at = excluded.checked_at`
21723
+ );
21724
+ this.readStmt = db.prepare(
21725
+ `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
21726
+ );
21727
+ }
21728
+ db;
21729
+ upsertStmt;
21730
+ readStmt;
21731
+ getVerdict(ruleKey) {
21732
+ return getRow(this.readStmt, { ruleKey });
21733
+ }
21734
+ setVerdict(ruleKey, verdict, worstProbeMs) {
21735
+ failOpenTransaction(this.db, () => {
21736
+ this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
21737
+ });
21738
+ }
21739
+ };
21740
+
21627
21741
  // ../../packages/persistence/src/repositories/scan-ledger.ts
21628
21742
  var SqliteScanLedgerRepository = class {
21629
21743
  constructor(db) {
@@ -22035,11 +22149,50 @@ var SqliteSecurityRepository = class {
22035
22149
 
22036
22150
  // ../../packages/persistence/src/repositories/shares.ts
22037
22151
  import { randomUUID as randomUUID7 } from "crypto";
22038
- var KIND_ORDER = ["provider", "internal", "ip"];
22152
+ var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22153
+ var IN_CHUNK = 500;
22154
+ var KIND_ORDER = ["provider", "internal", "external", "ip"];
22155
+ var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
22156
+ var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
22157
+ LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
22039
22158
  var CALL_SITE_EMBED_CAP = 200;
22040
22159
  function parseNetwork(networkJson) {
22041
22160
  return safeJson(networkJson, null);
22042
22161
  }
22162
+ function capHits(all, mode) {
22163
+ if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
22164
+ return { hits: [...all], droppedFiles: [], truncated: false };
22165
+ }
22166
+ if (mode === "walk") {
22167
+ return {
22168
+ hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
22169
+ droppedFiles: [],
22170
+ truncated: true
22171
+ };
22172
+ }
22173
+ const byFile = /* @__PURE__ */ new Map();
22174
+ for (const hit of all) {
22175
+ const bucket = byFile.get(hit.site.file);
22176
+ if (bucket === void 0) byFile.set(hit.site.file, [hit]);
22177
+ else bucket.push(hit);
22178
+ }
22179
+ const hits = [];
22180
+ const droppedFiles = [];
22181
+ for (const [file2, bucket] of byFile) {
22182
+ if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
22183
+ else hits.push(...bucket);
22184
+ }
22185
+ return { hits, droppedFiles, truncated: true };
22186
+ }
22187
+ function withoutDroppedFiles(reconcile, droppedFiles) {
22188
+ if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
22189
+ const dropped = new Set(droppedFiles);
22190
+ return {
22191
+ mode: "ledger",
22192
+ scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
22193
+ deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
22194
+ };
22195
+ }
22043
22196
  function toEndpointSummary(row) {
22044
22197
  return {
22045
22198
  id: row.id,
@@ -22130,13 +22283,15 @@ var SqliteSharesRepository = class {
22130
22283
  const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22131
22284
  const insecure = countScalar(
22132
22285
  this.db,
22133
- "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
22286
+ `SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
22287
+ WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
22134
22288
  );
22135
22289
  const needsReview = countScalar(
22136
22290
  this.db,
22137
22291
  `SELECT count(DISTINCT d.id) AS n
22138
22292
  FROM share_destination d
22139
- LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
22293
+ LEFT JOIN share_endpoint e ON e.destination_id = d.id
22294
+ AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
22140
22295
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
22141
22296
  );
22142
22297
  const kindCounts = countBy(
@@ -22146,6 +22301,7 @@ var SqliteSharesRepository = class {
22146
22301
  const byKind = {
22147
22302
  provider: kindCounts.get("provider") ?? 0,
22148
22303
  internal: kindCounts.get("internal") ?? 0,
22304
+ external: kindCounts.get("external") ?? 0,
22149
22305
  ip: kindCounts.get("ip") ?? 0
22150
22306
  };
22151
22307
  const trustCounts = countBy(
@@ -22221,23 +22377,316 @@ var SqliteSharesRepository = class {
22221
22377
  // real edit from a no-such-destination.
22222
22378
  /**
22223
22379
  * Set (decision) or clear (null) the egress decision override for a destination.
22224
- * `null` deletes the override row → reverts to the trust default.
22380
+ * `null` deletes the override rows → reverts to the trust default.
22381
+ *
22382
+ * The written row carries both the destination id and its host, so the
22383
+ * decision re-attaches by host after the destination is pruned and
22384
+ * re-detected under a fresh id. Rows written before the host column existed
22385
+ * (host NULL, matched by destination id) are replaced rather than left to
22386
+ * shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
22387
+ * would otherwise race a concurrent prune.
22225
22388
  */
22226
22389
  setEgressDecision(destinationId, decision) {
22227
- const exists = this.db.prepare("SELECT 1 FROM share_destination WHERE id = ?").get(destinationId);
22228
- if (exists === void 0) return false;
22229
- if (decision === null) {
22230
- this.db.prepare("DELETE FROM egress_decision_override WHERE destination_id = ?").run(destinationId);
22231
- return true;
22390
+ let existed = false;
22391
+ withTransaction(
22392
+ this.db,
22393
+ () => {
22394
+ const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
22395
+ if (dest === void 0) return;
22396
+ existed = true;
22397
+ this.db.prepare(
22398
+ `DELETE FROM egress_decision_override
22399
+ WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
22400
+ ).run({ host: dest.host, destinationId });
22401
+ if (decision === null) return;
22402
+ this.db.prepare(
22403
+ `INSERT INTO egress_decision_override
22404
+ (id, destination_id, host, decision, created_at, updated_at)
22405
+ VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22406
+ ).run({
22407
+ id: randomUUID7(),
22408
+ destinationId,
22409
+ host: dest.host,
22410
+ decision,
22411
+ now: Date.now()
22412
+ });
22413
+ },
22414
+ "IMMEDIATE"
22415
+ );
22416
+ return existed;
22417
+ }
22418
+ /**
22419
+ * Record one project's statically-extracted egress: reconcile the previously
22420
+ * stored call sites against this scan, upsert destination → endpoint → call
22421
+ * site for every hit, confirm `last_seen` on everything the project still
22422
+ * references, and drop what no longer has evidence.
22423
+ *
22424
+ * Reconciliation keys on `projectKey` alone; `project` and `projectId` are
22425
+ * display payload and never scope a delete. The whole write is one
22426
+ * transaction: a failure leaves the project's previous inventory exactly as
22427
+ * it was, and THROWS rather than reporting a partial write — callers decide
22428
+ * their own fail-open behavior, and the scanner additionally withholds its
22429
+ * ledger commit so the next scan retries.
22430
+ *
22431
+ * Over-cap input is truncated at a FILE boundary, and the files that lost
22432
+ * their hits are both excluded from the reconcile delete and named in
22433
+ * `droppedFiles`. That pairing is what keeps truncation non-destructive on
22434
+ * the ledger path: a dropped file keeps whatever rows it already had, and its
22435
+ * caller withholds the ledger entry so the next scan reads it again.
22436
+ */
22437
+ recordProjectEgress(input) {
22438
+ const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
22439
+ const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
22440
+ const now = Date.now();
22441
+ let summary = {
22442
+ destinations: 0,
22443
+ endpoints: 0,
22444
+ callSites: 0,
22445
+ truncated,
22446
+ droppedFiles
22447
+ };
22448
+ withTransaction(
22449
+ this.db,
22450
+ () => {
22451
+ const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
22452
+ this.reconcileCallSites(input.projectKey, reconcile);
22453
+ this.upsertHits(input, hits, projectId, now);
22454
+ this.confirmLastSeen(input.projectKey, now);
22455
+ this.pruneOrphans();
22456
+ summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
22457
+ },
22458
+ "IMMEDIATE"
22459
+ );
22460
+ return summary;
22461
+ }
22462
+ // ─── Egress write internals ──────────────────────────────────────────────────
22463
+ /**
22464
+ * Clear the stored call sites this scan is responsible for re-creating.
22465
+ *
22466
+ * Each pipeline may only delete rows its own walker could have produced. The
22467
+ * fs walk behind 'walk' mode never descends into dot-directories, so its
22468
+ * delete excludes dot-path files — those rows are the plugin scanner's to
22469
+ * reconcile, and deleting them here would make the two pipelines erase each
22470
+ * other's rows on every alternating scan. 'ledger' mode names its files
22471
+ * outright and never mass-deletes, so rows the fs walk contributed for files
22472
+ * the scanner skips (vendored, oversize) survive it.
22473
+ */
22474
+ reconcileCallSites(projectKey, reconcile) {
22475
+ if (reconcile.mode === "walk") {
22476
+ const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
22477
+ this.db.prepare(
22478
+ `DELETE FROM share_call_site
22479
+ WHERE project_key = :key
22480
+ AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
22481
+ AND file NOT LIKE '.%'
22482
+ AND file NOT LIKE '%/.%'`
22483
+ ).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
22484
+ return;
22485
+ }
22486
+ const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
22487
+ for (let i = 0; i < files.length; i += IN_CHUNK) {
22488
+ const chunk = files.slice(i, i + IN_CHUNK);
22489
+ this.db.prepare(
22490
+ `DELETE FROM share_call_site
22491
+ WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
22492
+ ).run(projectKey, ...chunk);
22493
+ }
22494
+ }
22495
+ /**
22496
+ * Upsert every hit as destination → endpoint → call site. Destinations key on
22497
+ * `host` and endpoints on `(destination_id, method, url)`, both shared across
22498
+ * projects; only the call site carries `project_key`. A destination's `note`
22499
+ * is user-owned and never overwritten. The id caches keep one upsert per
22500
+ * distinct host and endpoint, so the first hit for a host supplies its
22501
+ * classification for this batch.
22502
+ */
22503
+ upsertHits(input, hits, projectId, now) {
22504
+ if (hits.length === 0) return;
22505
+ const destStmt = this.db.prepare(
22506
+ `INSERT INTO share_destination
22507
+ (id, kind, name, host, category, trust, network_json, last_seen, provenance,
22508
+ created_at, updated_at)
22509
+ VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
22510
+ ON CONFLICT (host) DO UPDATE SET
22511
+ kind = excluded.kind,
22512
+ name = excluded.name,
22513
+ category = excluded.category,
22514
+ trust = excluded.trust,
22515
+ network_json = excluded.network_json,
22516
+ last_seen = excluded.last_seen,
22517
+ updated_at = excluded.updated_at`
22518
+ );
22519
+ const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
22520
+ const endpointStmt = this.db.prepare(
22521
+ `INSERT INTO share_endpoint
22522
+ (id, destination_id, method, transport, url, template, data_class, last_seen,
22523
+ created_at, updated_at)
22524
+ VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
22525
+ :now, :now)
22526
+ ON CONFLICT (destination_id, method, url) DO UPDATE SET
22527
+ transport = excluded.transport,
22528
+ template = excluded.template,
22529
+ data_class = excluded.data_class,
22530
+ last_seen = excluded.last_seen,
22531
+ updated_at = excluded.updated_at`
22532
+ );
22533
+ const endpointIdStmt = this.db.prepare(
22534
+ "SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
22535
+ );
22536
+ const siteStmt = this.db.prepare(
22537
+ `INSERT INTO share_call_site
22538
+ (id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
22539
+ project_id, created_at, updated_at)
22540
+ VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
22541
+ :vendored, :projectId, :now, :now)
22542
+ ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
22543
+ snippet = excluded.snippet,
22544
+ dynamic = excluded.dynamic,
22545
+ vendored = excluded.vendored,
22546
+ project = excluded.project,
22547
+ project_id = COALESCE(excluded.project_id, share_call_site.project_id),
22548
+ updated_at = excluded.updated_at`
22549
+ );
22550
+ const destIds = /* @__PURE__ */ new Map();
22551
+ const endpointIds = /* @__PURE__ */ new Map();
22552
+ for (const hit of hits) {
22553
+ let destinationId = destIds.get(hit.host);
22554
+ if (destinationId === void 0) {
22555
+ destStmt.run({
22556
+ id: randomUUID7(),
22557
+ kind: hit.kind,
22558
+ name: hit.name,
22559
+ host: hit.host,
22560
+ category: hit.category,
22561
+ trust: hit.trust,
22562
+ networkJson: hit.network === null ? null : JSON.stringify(hit.network),
22563
+ now
22564
+ });
22565
+ destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
22566
+ destIds.set(hit.host, destinationId);
22567
+ }
22568
+ const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
22569
+ let endpointId = endpointIds.get(endpointKey);
22570
+ if (endpointId === void 0) {
22571
+ endpointStmt.run({
22572
+ id: randomUUID7(),
22573
+ destinationId,
22574
+ method: hit.method,
22575
+ transport: hit.transport,
22576
+ url: hit.url,
22577
+ template: boolToInt(hit.template),
22578
+ dataClass: hit.dataClass,
22579
+ now
22580
+ });
22581
+ endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
22582
+ endpointIds.set(endpointKey, endpointId);
22583
+ }
22584
+ siteStmt.run({
22585
+ id: randomUUID7(),
22586
+ endpointId,
22587
+ project: input.project,
22588
+ projectKey: input.projectKey,
22589
+ file: hit.site.file,
22590
+ line: hit.site.line,
22591
+ snippet: hit.site.snippet,
22592
+ dynamic: boolToInt(hit.site.dynamic),
22593
+ vendored: boolToInt(hit.site.vendored),
22594
+ projectId,
22595
+ now
22596
+ });
22232
22597
  }
22598
+ }
22599
+ /**
22600
+ * The source-project id this project's stored call sites already carry, if
22601
+ * any. Only the pipeline that resolves a source project supplies one; the
22602
+ * other passes null and inherits this, so the link stops flapping between a
22603
+ * real id and NULL depending on which pipeline ran last. The value is a
22604
+ * per-project attribute stored redundantly on each row, so any row's is
22605
+ * representative.
22606
+ */
22607
+ knownProjectId(projectKey) {
22608
+ return getRow(
22609
+ this.db.prepare(
22610
+ `SELECT project_id AS projectId FROM share_call_site
22611
+ WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
22612
+ ),
22613
+ [projectKey]
22614
+ )?.projectId ?? null;
22615
+ }
22616
+ /**
22617
+ * Stamp `last_seen` on every endpoint and destination this project still
22618
+ * references — including rows the scan preserved rather than re-wrote, so a
22619
+ * ledger-skipped file's references don't decay into "stale" on the page.
22620
+ */
22621
+ confirmLastSeen(projectKey, now) {
22233
22622
  this.db.prepare(
22234
- `INSERT INTO egress_decision_override (id, destination_id, decision, created_at, updated_at)
22235
- VALUES (:id, :destinationId, :decision, :now, :now)
22236
- ON CONFLICT (destination_id) DO UPDATE SET
22237
- decision = excluded.decision,
22238
- updated_at = excluded.updated_at`
22239
- ).run({ id: randomUUID7(), destinationId, decision, now: Date.now() });
22240
- return true;
22623
+ `UPDATE share_endpoint SET last_seen = :now, updated_at = :now
22624
+ WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
22625
+ ).run({ now, key: projectKey });
22626
+ this.db.prepare(
22627
+ `UPDATE share_destination SET last_seen = :now, updated_at = :now
22628
+ WHERE id IN (SELECT DISTINCT e.destination_id
22629
+ FROM share_endpoint e
22630
+ JOIN share_call_site c ON c.endpoint_id = e.id
22631
+ WHERE c.project_key = :key)`
22632
+ ).run({ now, key: projectKey });
22633
+ }
22634
+ /**
22635
+ * Drop rows left without evidence: endpoints with no call site, then
22636
+ * destinations with no endpoint. Call sites are the only evidence either one
22637
+ * has, so a row that lost its last one belongs to no project any more.
22638
+ *
22639
+ * Overrides are deleted between the two steps, and only the ones written
22640
+ * before the host column existed. Those match a destination by id alone;
22641
+ * because the id link is released on delete rather than cascading, leaving
22642
+ * them would accumulate rows that match neither join arm and that nothing can
22643
+ * reach again. Host-bearing rows deliberately survive — the host is what
22644
+ * re-attaches a user's decision when the destination comes back.
22645
+ */
22646
+ pruneOrphans() {
22647
+ this.db.exec(
22648
+ `DELETE FROM share_endpoint
22649
+ WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
22650
+ );
22651
+ this.db.exec(
22652
+ `DELETE FROM egress_decision_override
22653
+ WHERE host IS NULL
22654
+ AND destination_id IN (
22655
+ SELECT d.id FROM share_destination d
22656
+ WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
22657
+ );
22658
+ this.db.exec(
22659
+ `DELETE FROM share_destination
22660
+ WHERE NOT EXISTS (
22661
+ SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
22662
+ );
22663
+ }
22664
+ /**
22665
+ * Live totals for one project. Destinations and endpoints are shared across
22666
+ * projects and carry no project column, so both are counted through the call
22667
+ * sites that reference them.
22668
+ */
22669
+ projectTotals(projectKey) {
22670
+ return {
22671
+ destinations: countScalar(
22672
+ this.db,
22673
+ `SELECT count(DISTINCT e.destination_id) AS n
22674
+ FROM share_endpoint e
22675
+ JOIN share_call_site c ON c.endpoint_id = e.id
22676
+ WHERE c.project_key = ?`,
22677
+ [projectKey]
22678
+ ),
22679
+ endpoints: countScalar(
22680
+ this.db,
22681
+ "SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
22682
+ [projectKey]
22683
+ ),
22684
+ callSites: countScalar(
22685
+ this.db,
22686
+ "SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
22687
+ [projectKey]
22688
+ )
22689
+ };
22241
22690
  }
22242
22691
  // ─── Raw fetchers ────────────────────────────────────────────────────────────
22243
22692
  mapDestRow(r) {
@@ -22257,7 +22706,8 @@ var SqliteSharesRepository = class {
22257
22706
  fetchDestinations(q, kinds, reviewOnly = false) {
22258
22707
  const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22259
22708
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22260
- d.created_at AS createdAt, o.decision AS overrideDecision`;
22709
+ d.created_at AS createdAt,
22710
+ COALESCE(oh.decision, ol.decision) AS overrideDecision`;
22261
22711
  const conditions = [];
22262
22712
  const params = [];
22263
22713
  if (kinds && kinds.length > 0) {
@@ -22268,7 +22718,8 @@ var SqliteSharesRepository = class {
22268
22718
  conditions.push(
22269
22719
  `(d.trust IN ('unverified', 'ip')
22270
22720
  OR EXISTS (SELECT 1 FROM share_endpoint re
22271
- WHERE re.destination_id = d.id AND re.transport = 'http'))`
22721
+ WHERE re.destination_id = d.id
22722
+ AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
22272
22723
  );
22273
22724
  }
22274
22725
  let sql;
@@ -22281,7 +22732,7 @@ var SqliteSharesRepository = class {
22281
22732
  params.push(pattern, pattern, pattern, pattern, pattern);
22282
22733
  sql = `SELECT DISTINCT ${cols}
22283
22734
  FROM share_destination d
22284
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22735
+ ${OVERRIDE_JOIN}
22285
22736
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
22286
22737
  LEFT JOIN share_call_site c ON c.endpoint_id = e.id
22287
22738
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
@@ -22289,7 +22740,7 @@ var SqliteSharesRepository = class {
22289
22740
  } else {
22290
22741
  sql = `SELECT ${cols}
22291
22742
  FROM share_destination d
22292
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22743
+ ${OVERRIDE_JOIN}
22293
22744
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
22294
22745
  ORDER BY d.created_at ASC, d.id ASC`;
22295
22746
  }
@@ -22304,9 +22755,9 @@ var SqliteSharesRepository = class {
22304
22755
  this.db.prepare(
22305
22756
  `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22306
22757
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22307
- o.decision AS overrideDecision
22758
+ COALESCE(oh.decision, ol.decision) AS overrideDecision
22308
22759
  FROM share_destination d
22309
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22760
+ ${OVERRIDE_JOIN}
22310
22761
  WHERE d.id = ?`
22311
22762
  ),
22312
22763
  [destinationId]
@@ -22535,6 +22986,7 @@ function openLocalDatabase(dir) {
22535
22986
  const scanLedger = new SqliteScanLedgerRepository(db);
22536
22987
  const exceptions = new SqliteExceptionsRepository(db);
22537
22988
  const resolutions = new SqliteResolutionsRepository(db);
22989
+ const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
22538
22990
  const security = new SqliteSecurityRepository(db);
22539
22991
  const detections = new SqliteDetectionsRepository(db);
22540
22992
  const shares = new SqliteSharesRepository(db);
@@ -22672,6 +23124,7 @@ function openLocalDatabase(dir) {
22672
23124
  scanLedger,
22673
23125
  exceptions,
22674
23126
  resolutions,
23127
+ ruleProbeCache,
22675
23128
  security,
22676
23129
  detections,
22677
23130
  shares,
@@ -22868,13 +23321,581 @@ import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as s
22868
23321
  import { homedir as homedir2 } from "os";
22869
23322
  import { basename as basename2, join as join7 } from "path";
22870
23323
 
23324
+ // ../../packages/detections/src/egress/registry.ts
23325
+ var EXTRACTOR_VERSION = "1";
23326
+ var PROVIDER_REGISTRY = [
23327
+ {
23328
+ id: "stripe",
23329
+ name: "Stripe",
23330
+ category: "Payments",
23331
+ hostSuffixes: ["stripe.com"],
23332
+ apiBase: "https://api.stripe.com",
23333
+ defaultDataClasses: ["pii", "customer"],
23334
+ sdks: {
23335
+ npm: ["stripe"],
23336
+ pypi: ["stripe"],
23337
+ go: ["github.com/stripe/stripe-go"],
23338
+ maven: ["com.stripe"],
23339
+ rubygems: ["stripe"],
23340
+ composer: ["stripe/stripe-php"],
23341
+ nuget: ["Stripe.net"]
23342
+ }
23343
+ },
23344
+ {
23345
+ id: "datadog",
23346
+ name: "Datadog",
23347
+ category: "Observability",
23348
+ hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
23349
+ apiBase: "https://api.datadoghq.com",
23350
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23351
+ sdks: {
23352
+ npm: ["dd-trace", "@datadog/browser-logs"],
23353
+ pypi: ["datadog", "ddtrace"],
23354
+ go: ["github.com/DataDog/dd-trace-go"],
23355
+ maven: ["com.datadoghq"],
23356
+ rubygems: ["ddtrace", "dogapi"],
23357
+ nuget: ["Datadog.Trace"]
23358
+ }
23359
+ },
23360
+ {
23361
+ id: "newrelic",
23362
+ name: "New Relic",
23363
+ category: "Observability",
23364
+ hostSuffixes: ["newrelic.com", "nr-data.net"],
23365
+ apiBase: "https://api.newrelic.com",
23366
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23367
+ sdks: {
23368
+ npm: ["newrelic"],
23369
+ pypi: ["newrelic"],
23370
+ go: ["github.com/newrelic/go-agent"],
23371
+ maven: ["com.newrelic.agent.java"],
23372
+ rubygems: ["newrelic_rpm"],
23373
+ nuget: ["NewRelic.Agent"]
23374
+ }
23375
+ },
23376
+ {
23377
+ id: "sentry",
23378
+ name: "Sentry",
23379
+ category: "Error tracking",
23380
+ hostSuffixes: ["sentry.io"],
23381
+ apiBase: "https://sentry.io",
23382
+ defaultDataClasses: ["source", "telemetry"],
23383
+ sdks: {
23384
+ npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
23385
+ pypi: ["sentry-sdk"],
23386
+ go: ["github.com/getsentry/sentry-go"],
23387
+ maven: ["io.sentry"],
23388
+ rubygems: ["sentry-ruby"],
23389
+ cargo: ["sentry"],
23390
+ composer: ["sentry/sentry"],
23391
+ nuget: ["Sentry"]
23392
+ }
23393
+ },
23394
+ {
23395
+ id: "openai",
23396
+ name: "OpenAI",
23397
+ category: "LLM provider",
23398
+ hostSuffixes: ["openai.com"],
23399
+ apiBase: "https://api.openai.com",
23400
+ defaultDataClasses: ["pii", "source"],
23401
+ sdks: {
23402
+ npm: ["openai"],
23403
+ pypi: ["openai"],
23404
+ go: ["github.com/sashabaranov/go-openai"],
23405
+ maven: ["com.openai"],
23406
+ rubygems: ["ruby-openai"],
23407
+ cargo: ["async-openai"],
23408
+ composer: ["openai-php/client"],
23409
+ nuget: ["OpenAI"]
23410
+ }
23411
+ },
23412
+ {
23413
+ id: "anthropic",
23414
+ name: "Anthropic",
23415
+ category: "LLM provider",
23416
+ hostSuffixes: ["anthropic.com"],
23417
+ apiBase: "https://api.anthropic.com",
23418
+ defaultDataClasses: ["pii", "source"],
23419
+ sdks: {
23420
+ npm: ["@anthropic-ai/sdk"],
23421
+ pypi: ["anthropic"],
23422
+ go: ["github.com/anthropics/anthropic-sdk-go"],
23423
+ nuget: ["Anthropic.SDK"]
23424
+ }
23425
+ },
23426
+ {
23427
+ id: "aws",
23428
+ name: "Amazon Web Services",
23429
+ category: "Cloud platform",
23430
+ hostSuffixes: ["amazonaws.com"],
23431
+ apiBase: "https://s3.amazonaws.com",
23432
+ defaultDataClasses: ["secrets", "customer"],
23433
+ sdks: {
23434
+ npm: ["@aws-sdk/client-s3", "aws-sdk"],
23435
+ pypi: ["boto3"],
23436
+ go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
23437
+ maven: ["com.amazonaws", "software.amazon.awssdk"],
23438
+ rubygems: ["aws-sdk-s3"],
23439
+ cargo: ["aws-sdk-s3"],
23440
+ nuget: ["AWSSDK.S3"]
23441
+ }
23442
+ },
23443
+ {
23444
+ id: "gcp",
23445
+ name: "Google Cloud",
23446
+ category: "Cloud platform",
23447
+ hostSuffixes: ["googleapis.com"],
23448
+ apiBase: "https://storage.googleapis.com",
23449
+ defaultDataClasses: ["customer", "logs"],
23450
+ sdks: {
23451
+ npm: ["@google-cloud/storage"],
23452
+ pypi: ["google-cloud-storage"],
23453
+ go: ["cloud.google.com/go"],
23454
+ maven: ["com.google.cloud"],
23455
+ rubygems: ["google-cloud-storage"],
23456
+ nuget: ["Google.Cloud.Storage.V1"]
23457
+ }
23458
+ },
23459
+ {
23460
+ id: "azure",
23461
+ name: "Microsoft Azure",
23462
+ category: "Cloud platform",
23463
+ hostSuffixes: ["azure.com", "windows.net"],
23464
+ apiBase: "https://management.azure.com",
23465
+ defaultDataClasses: ["customer", "logs"],
23466
+ sdks: {
23467
+ npm: ["@azure/storage-blob"],
23468
+ pypi: ["azure-storage-blob"],
23469
+ go: ["github.com/Azure/azure-sdk-for-go"],
23470
+ maven: ["com.azure"],
23471
+ rubygems: ["azure-storage-blob"],
23472
+ nuget: ["Azure.Storage.Blobs"]
23473
+ }
23474
+ },
23475
+ {
23476
+ id: "slack",
23477
+ name: "Slack",
23478
+ category: "Notifications",
23479
+ hostSuffixes: ["slack.com"],
23480
+ apiBase: "https://slack.com/api",
23481
+ defaultDataClasses: ["logs"],
23482
+ sdks: {
23483
+ npm: ["@slack/web-api"],
23484
+ pypi: ["slack-sdk"],
23485
+ go: ["github.com/slack-go/slack"],
23486
+ maven: ["com.slack.api"],
23487
+ rubygems: ["slack-ruby-client"],
23488
+ composer: ["slack-php/slack-api"],
23489
+ nuget: ["SlackNet"]
23490
+ }
23491
+ },
23492
+ {
23493
+ id: "segment",
23494
+ name: "Segment",
23495
+ category: "Analytics",
23496
+ hostSuffixes: ["segment.io", "segment.com"],
23497
+ apiBase: "https://api.segment.io",
23498
+ defaultDataClasses: ["customer"],
23499
+ sdks: {
23500
+ npm: ["@segment/analytics-node", "analytics-node"],
23501
+ pypi: ["segment-analytics-python"],
23502
+ go: ["github.com/segmentio/analytics-go"],
23503
+ maven: ["com.segment.analytics.java"],
23504
+ rubygems: ["analytics-ruby"],
23505
+ nuget: ["Analytics"]
23506
+ }
23507
+ },
23508
+ {
23509
+ id: "twilio",
23510
+ name: "Twilio",
23511
+ category: "Communications",
23512
+ hostSuffixes: ["twilio.com"],
23513
+ apiBase: "https://api.twilio.com",
23514
+ defaultDataClasses: ["pii", "customer"],
23515
+ sdks: {
23516
+ npm: ["twilio"],
23517
+ pypi: ["twilio"],
23518
+ go: ["github.com/twilio/twilio-go"],
23519
+ maven: ["com.twilio.sdk"],
23520
+ rubygems: ["twilio-ruby"],
23521
+ composer: ["twilio/sdk"],
23522
+ nuget: ["Twilio"]
23523
+ }
23524
+ },
23525
+ {
23526
+ id: "sendgrid",
23527
+ name: "SendGrid",
23528
+ category: "Email",
23529
+ hostSuffixes: ["sendgrid.com"],
23530
+ apiBase: "https://api.sendgrid.com",
23531
+ defaultDataClasses: ["pii"],
23532
+ sdks: {
23533
+ npm: ["@sendgrid/mail"],
23534
+ pypi: ["sendgrid"],
23535
+ go: ["github.com/sendgrid/sendgrid-go"],
23536
+ maven: ["com.sendgrid"],
23537
+ rubygems: ["sendgrid-ruby"],
23538
+ composer: ["sendgrid/sendgrid"],
23539
+ nuget: ["SendGrid"]
23540
+ }
23541
+ },
23542
+ {
23543
+ id: "mailgun",
23544
+ name: "Mailgun",
23545
+ category: "Email",
23546
+ hostSuffixes: ["mailgun.net"],
23547
+ apiBase: "https://api.mailgun.net",
23548
+ defaultDataClasses: ["pii"],
23549
+ sdks: {
23550
+ npm: ["mailgun.js"],
23551
+ pypi: ["mailgun"],
23552
+ rubygems: ["mailgun-ruby"],
23553
+ composer: ["mailgun/mailgun-php"],
23554
+ nuget: ["Mailgun"]
23555
+ }
23556
+ },
23557
+ {
23558
+ id: "mixpanel",
23559
+ name: "Mixpanel",
23560
+ category: "Analytics",
23561
+ hostSuffixes: ["mixpanel.com"],
23562
+ apiBase: "https://api.mixpanel.com",
23563
+ defaultDataClasses: ["customer", "telemetry"],
23564
+ sdks: {
23565
+ npm: ["mixpanel"],
23566
+ pypi: ["mixpanel"],
23567
+ rubygems: ["mixpanel-ruby"],
23568
+ nuget: ["Mixpanel"]
23569
+ }
23570
+ },
23571
+ {
23572
+ id: "amplitude",
23573
+ name: "Amplitude",
23574
+ category: "Analytics",
23575
+ hostSuffixes: ["amplitude.com"],
23576
+ apiBase: "https://api2.amplitude.com",
23577
+ defaultDataClasses: ["customer", "telemetry"],
23578
+ sdks: {
23579
+ npm: ["@amplitude/analytics-node"],
23580
+ pypi: ["amplitude-analytics"],
23581
+ nuget: ["Amplitude"]
23582
+ }
23583
+ },
23584
+ {
23585
+ id: "posthog",
23586
+ name: "PostHog",
23587
+ category: "Analytics",
23588
+ hostSuffixes: ["posthog.com"],
23589
+ apiBase: "https://us.i.posthog.com",
23590
+ defaultDataClasses: ["customer", "telemetry"],
23591
+ sdks: {
23592
+ npm: ["posthog-node", "posthog-js"],
23593
+ pypi: ["posthog"],
23594
+ go: ["github.com/posthog/posthog-go"],
23595
+ rubygems: ["posthog-ruby"],
23596
+ composer: ["posthog/posthog-php"],
23597
+ nuget: ["PostHog"]
23598
+ }
23599
+ },
23600
+ {
23601
+ id: "honeycomb",
23602
+ name: "Honeycomb",
23603
+ category: "Observability",
23604
+ hostSuffixes: ["honeycomb.io"],
23605
+ apiBase: "https://api.honeycomb.io",
23606
+ defaultDataClasses: ["telemetry", "metrics"],
23607
+ sdks: {
23608
+ npm: ["libhoney"],
23609
+ pypi: ["libhoney"],
23610
+ go: ["github.com/honeycombio/libhoney-go"],
23611
+ rubygems: ["libhoney"]
23612
+ }
23613
+ },
23614
+ {
23615
+ id: "grafana",
23616
+ name: "Grafana Cloud",
23617
+ category: "Observability",
23618
+ hostSuffixes: ["grafana.net"],
23619
+ apiBase: "https://grafana.net",
23620
+ defaultDataClasses: ["logs", "metrics"],
23621
+ sdks: {
23622
+ npm: ["@grafana/faro-web-sdk"]
23623
+ }
23624
+ },
23625
+ {
23626
+ id: "splunk",
23627
+ name: "Splunk",
23628
+ category: "Observability",
23629
+ hostSuffixes: ["splunkcloud.com", "splunk.com"],
23630
+ apiBase: "https://http-inputs.splunkcloud.com",
23631
+ defaultDataClasses: ["logs"],
23632
+ sdks: {
23633
+ npm: ["splunk-logging"],
23634
+ pypi: ["splunk-sdk"],
23635
+ maven: ["com.splunk"],
23636
+ nuget: ["Splunk.Logging.Common"]
23637
+ }
23638
+ },
23639
+ {
23640
+ id: "pagerduty",
23641
+ name: "PagerDuty",
23642
+ category: "Incident response",
23643
+ hostSuffixes: ["pagerduty.com"],
23644
+ apiBase: "https://api.pagerduty.com",
23645
+ defaultDataClasses: ["logs"],
23646
+ sdks: {
23647
+ npm: ["@pagerduty/pdjs"],
23648
+ pypi: ["pdpyras"],
23649
+ go: ["github.com/PagerDuty/go-pagerduty"],
23650
+ rubygems: ["pagerduty"]
23651
+ }
23652
+ },
23653
+ {
23654
+ id: "github",
23655
+ name: "GitHub",
23656
+ category: "Developer platform",
23657
+ hostSuffixes: ["github.com", "githubusercontent.com"],
23658
+ apiBase: "https://api.github.com",
23659
+ defaultDataClasses: ["source"],
23660
+ sdks: {
23661
+ npm: ["@octokit/rest", "octokit"],
23662
+ pypi: ["pygithub"],
23663
+ go: ["github.com/google/go-github"],
23664
+ maven: ["org.kohsuke.github-api"],
23665
+ rubygems: ["octokit"],
23666
+ cargo: ["octocrab"],
23667
+ composer: ["knplabs/github-api"],
23668
+ nuget: ["Octokit"]
23669
+ }
23670
+ },
23671
+ {
23672
+ id: "gitlab",
23673
+ name: "GitLab",
23674
+ category: "Developer platform",
23675
+ hostSuffixes: ["gitlab.com"],
23676
+ apiBase: "https://gitlab.com/api",
23677
+ defaultDataClasses: ["source"],
23678
+ sdks: {
23679
+ npm: ["@gitbeaker/rest"],
23680
+ pypi: ["python-gitlab"],
23681
+ go: ["gitlab.com/gitlab-org/api/client-go"],
23682
+ rubygems: ["gitlab"],
23683
+ nuget: ["GitLabApiClient"]
23684
+ }
23685
+ },
23686
+ {
23687
+ id: "auth0",
23688
+ name: "Auth0",
23689
+ category: "Identity",
23690
+ hostSuffixes: ["auth0.com"],
23691
+ apiBase: "https://login.auth0.com",
23692
+ defaultDataClasses: ["pii"],
23693
+ sdks: {
23694
+ npm: ["auth0"],
23695
+ pypi: ["auth0-python"],
23696
+ go: ["github.com/auth0/go-auth0"],
23697
+ maven: ["com.auth0"],
23698
+ rubygems: ["auth0"],
23699
+ composer: ["auth0/auth0-php"],
23700
+ nuget: ["Auth0.ManagementApi"]
23701
+ }
23702
+ },
23703
+ {
23704
+ id: "okta",
23705
+ name: "Okta",
23706
+ category: "Identity",
23707
+ hostSuffixes: ["okta.com", "oktapreview.com"],
23708
+ apiBase: "https://login.okta.com",
23709
+ defaultDataClasses: ["pii"],
23710
+ sdks: {
23711
+ npm: ["@okta/okta-sdk-nodejs"],
23712
+ pypi: ["okta"],
23713
+ go: ["github.com/okta/okta-sdk-golang"],
23714
+ maven: ["com.okta.sdk"],
23715
+ nuget: ["Okta.Sdk"]
23716
+ }
23717
+ },
23718
+ {
23719
+ id: "clerk",
23720
+ name: "Clerk",
23721
+ category: "Identity",
23722
+ hostSuffixes: ["clerk.com", "clerk.dev"],
23723
+ apiBase: "https://api.clerk.com",
23724
+ defaultDataClasses: ["pii"],
23725
+ sdks: {
23726
+ npm: ["@clerk/backend", "@clerk/nextjs"],
23727
+ pypi: ["clerk-backend-api"],
23728
+ go: ["github.com/clerk/clerk-sdk-go"]
23729
+ }
23730
+ },
23731
+ {
23732
+ id: "supabase",
23733
+ name: "Supabase",
23734
+ category: "Backend platform",
23735
+ hostSuffixes: ["supabase.co", "supabase.com"],
23736
+ apiBase: "https://api.supabase.com",
23737
+ defaultDataClasses: ["pii", "customer"],
23738
+ sdks: {
23739
+ npm: ["@supabase/supabase-js"],
23740
+ pypi: ["supabase"],
23741
+ cargo: ["postgrest"]
23742
+ }
23743
+ },
23744
+ {
23745
+ id: "firebase",
23746
+ name: "Firebase",
23747
+ category: "Backend platform",
23748
+ hostSuffixes: ["firebaseio.com", "firebase.google.com"],
23749
+ apiBase: "https://firebaseio.com",
23750
+ defaultDataClasses: ["customer"],
23751
+ sdks: {
23752
+ npm: ["firebase", "firebase-admin"],
23753
+ pypi: ["firebase-admin"],
23754
+ go: ["firebase.google.com/go"],
23755
+ maven: ["com.google.firebase"]
23756
+ }
23757
+ },
23758
+ {
23759
+ id: "mongodb-atlas",
23760
+ name: "MongoDB Atlas",
23761
+ category: "Database SaaS",
23762
+ hostSuffixes: ["mongodb.net", "mongodb.com"],
23763
+ apiBase: "https://cloud.mongodb.com",
23764
+ defaultDataClasses: ["customer"],
23765
+ sdks: {
23766
+ npm: ["mongodb"],
23767
+ pypi: ["pymongo"],
23768
+ go: ["go.mongodb.org/mongo-driver"],
23769
+ maven: ["org.mongodb"],
23770
+ rubygems: ["mongo"],
23771
+ cargo: ["mongodb"],
23772
+ nuget: ["MongoDB.Driver"]
23773
+ }
23774
+ },
23775
+ {
23776
+ id: "planetscale",
23777
+ name: "PlanetScale",
23778
+ category: "Database SaaS",
23779
+ hostSuffixes: ["psdb.cloud", "planetscale.com"],
23780
+ apiBase: "https://api.planetscale.com",
23781
+ defaultDataClasses: ["customer"],
23782
+ sdks: {
23783
+ npm: ["@planetscale/database"],
23784
+ go: ["github.com/planetscale/planetscale-go"]
23785
+ }
23786
+ },
23787
+ {
23788
+ id: "algolia",
23789
+ name: "Algolia",
23790
+ category: "Search SaaS",
23791
+ hostSuffixes: ["algolia.net", "algolianet.com"],
23792
+ apiBase: "https://algolia.net",
23793
+ defaultDataClasses: ["customer"],
23794
+ sdks: {
23795
+ npm: ["algoliasearch"],
23796
+ pypi: ["algoliasearch"],
23797
+ go: ["github.com/algolia/algoliasearch-client-go"],
23798
+ maven: ["com.algolia"],
23799
+ rubygems: ["algolia"],
23800
+ composer: ["algolia/algoliasearch-client-php"],
23801
+ nuget: ["Algolia.Search"]
23802
+ }
23803
+ },
23804
+ {
23805
+ id: "cloudflare",
23806
+ name: "Cloudflare",
23807
+ category: "CDN / edge",
23808
+ hostSuffixes: ["cloudflare.com", "workers.dev"],
23809
+ apiBase: "https://api.cloudflare.com",
23810
+ defaultDataClasses: ["logs"],
23811
+ sdks: {
23812
+ npm: ["cloudflare"],
23813
+ pypi: ["cloudflare"],
23814
+ go: ["github.com/cloudflare/cloudflare-go"],
23815
+ nuget: ["CloudFlare.Client"]
23816
+ }
23817
+ },
23818
+ {
23819
+ id: "huggingface",
23820
+ name: "Hugging Face",
23821
+ category: "LLM provider",
23822
+ hostSuffixes: ["huggingface.co"],
23823
+ apiBase: "https://api-inference.huggingface.co",
23824
+ defaultDataClasses: ["source"],
23825
+ sdks: {
23826
+ npm: ["@huggingface/inference"],
23827
+ pypi: ["huggingface-hub", "transformers"],
23828
+ rubygems: ["hugging-face"]
23829
+ }
23830
+ },
23831
+ {
23832
+ id: "cohere",
23833
+ name: "Cohere",
23834
+ category: "LLM provider",
23835
+ hostSuffixes: ["cohere.com", "cohere.ai"],
23836
+ apiBase: "https://api.cohere.com",
23837
+ defaultDataClasses: ["pii", "source"],
23838
+ sdks: {
23839
+ npm: ["cohere-ai"],
23840
+ pypi: ["cohere"],
23841
+ go: ["github.com/cohere-ai/cohere-go"]
23842
+ }
23843
+ },
23844
+ {
23845
+ id: "mistral",
23846
+ name: "Mistral AI",
23847
+ category: "LLM provider",
23848
+ hostSuffixes: ["mistral.ai"],
23849
+ apiBase: "https://api.mistral.ai",
23850
+ defaultDataClasses: ["pii", "source"],
23851
+ sdks: {
23852
+ npm: ["@mistralai/mistralai"],
23853
+ pypi: ["mistralai"],
23854
+ go: ["github.com/gage-technologies/mistral-go"]
23855
+ }
23856
+ }
23857
+ ];
23858
+ var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
23859
+ ${JSON.stringify(PROVIDER_REGISTRY)}`;
23860
+
23861
+ // ../../packages/detections/src/egress/extract.ts
23862
+ var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
23863
+ var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
23864
+ var SECRET_VALUE = new RegExp(
23865
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
23866
+ "gi"
23867
+ );
23868
+ var AUTH_SCHEME_VALUE = new RegExp(
23869
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
23870
+ "gi"
23871
+ );
23872
+ var WEBHOOK_SECRET_PATHS = [
23873
+ { hosts: ["hooks.slack.com"], prefix: "/services/" },
23874
+ {
23875
+ hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
23876
+ prefix: "/api/webhooks/"
23877
+ },
23878
+ { hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
23879
+ { hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
23880
+ ];
23881
+ function escapeRegExp(literal2) {
23882
+ return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23883
+ }
23884
+ var WEBHOOK_URL = new RegExp(
23885
+ `(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
23886
+ (entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
23887
+ ).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
23888
+ "gi"
23889
+ );
23890
+
22871
23891
  // ../../packages/detections/src/escape-regexp.ts
22872
- function escapeRegExp(value) {
23892
+ function escapeRegExp2(value) {
22873
23893
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22874
23894
  }
22875
23895
 
22876
23896
  // ../../packages/detections/src/matchers/limits.ts
22877
23897
  var MAX_MATCHES_PER_RULE = 1e4;
23898
+ var MAX_REGEX_INPUT_LENGTH = 2e5;
22878
23899
 
22879
23900
  // ../../packages/detections/src/matchers/keyword.ts
22880
23901
  var KeywordMatcher2 = class {
@@ -22885,7 +23906,7 @@ var KeywordMatcher2 = class {
22885
23906
  for (const kw of keywords) {
22886
23907
  if (kw.length === 0) continue;
22887
23908
  if (spans.length >= MAX_MATCHES_PER_RULE) break;
22888
- const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
23909
+ const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
22889
23910
  let m;
22890
23911
  while ((m = re.exec(text)) !== null) {
22891
23912
  spans.push({ start: m.index, end: m.index + m[0].length });
@@ -22902,9 +23923,13 @@ var RegexMatcher2 = class {
22902
23923
  if (rule.matcher.type !== "regex") return [];
22903
23924
  const { pattern, flags: flags2, captureGroup } = rule.matcher;
22904
23925
  const re = new RegExp(pattern, flags2.includes("d") ? flags2 : `${flags2}d`);
23926
+ const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
22905
23927
  const spans = [];
22906
23928
  let m;
22907
- while ((m = re.exec(text)) !== null) {
23929
+ const maxIterations = scanText2.length + 1;
23930
+ let iterations = 0;
23931
+ while ((m = re.exec(scanText2)) !== null) {
23932
+ if (++iterations > maxIterations) break;
22908
23933
  const group = captureGroup != null ? m[captureGroup] : m[0];
22909
23934
  if (m[0].length === 0) re.lastIndex++;
22910
23935
  if (group && spans.length < MAX_MATCHES_PER_RULE) {
@@ -22979,6 +24004,31 @@ var CONFIG_POSTURE_RULES = [
22979
24004
  }
22980
24005
  ];
22981
24006
 
24007
+ // ../../packages/detections/src/security/redos-probe.ts
24008
+ var EXPONENTIAL_UNITS = [
24009
+ "a",
24010
+ "0",
24011
+ " ",
24012
+ "x",
24013
+ "ab",
24014
+ "a.",
24015
+ "a-",
24016
+ "a_",
24017
+ "a@",
24018
+ "a/",
24019
+ "a:",
24020
+ "a=",
24021
+ "a;",
24022
+ "aA0",
24023
+ " "
24024
+ ];
24025
+ var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
24026
+ (unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
24027
+ );
24028
+ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
24029
+ (unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
24030
+ );
24031
+
22982
24032
  // ../../packages/plugin-sdk/src/repo.ts
22983
24033
  import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
22984
24034
  import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
@@ -22996,6 +24046,10 @@ import { arch, hostname as hostname3, platform, release } from "os";
22996
24046
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
22997
24047
  import { join as join8 } from "path";
22998
24048
 
24049
+ // ../../packages/plugin-sdk/src/paths.ts
24050
+ import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
24051
+ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
24052
+
22999
24053
  // ../../packages/plugin-sdk/src/posture.ts
23000
24054
  function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
23001
24055
  for (const category of Object.keys(posture)) {
@@ -23008,8 +24062,8 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
23008
24062
 
23009
24063
  // ../../packages/plugin-sdk/src/project-files.ts
23010
24064
  var import_ignore = __toESM(require_ignore(), 1);
23011
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
23012
- import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
24065
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
24066
+ import { basename as basename4, join as join9, relative, sep as sep4 } from "path";
23013
24067
 
23014
24068
  // ../../packages/plugin-sdk/src/runtime.ts
23015
24069
  import { randomUUID as randomUUID10 } from "crypto";
@@ -23086,7 +24140,7 @@ function show(body) {
23086
24140
  }
23087
24141
 
23088
24142
  // src/command-registry.ts
23089
- import { readdirSync as readdirSync3 } from "fs";
24143
+ import { readdirSync as readdirSync4 } from "fs";
23090
24144
  import { fileURLToPath } from "url";
23091
24145
  var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
23092
24146