@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.
package/scripts/query.js CHANGED
@@ -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
 
@@ -16257,6 +16261,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16257
16261
 
16258
16262
  // ../../packages/schema/src/zod/rule.ts
16259
16263
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16264
+ var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
16260
16265
  var KeywordMatcher = external_exports.object({
16261
16266
  type: external_exports.literal("keyword"),
16262
16267
  // An empty keyword matches at every position, yielding one zero-length span
@@ -16281,9 +16286,10 @@ function matchesEmptyString(pattern, flags) {
16281
16286
  return false;
16282
16287
  }
16283
16288
  }
16289
+ var MAX_PATTERN_LENGTH = 2e3;
16284
16290
  var RegexMatcher = external_exports.object({
16285
16291
  type: external_exports.literal("regex"),
16286
- pattern: external_exports.string(),
16292
+ pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
16287
16293
  flags: external_exports.string().default("gi"),
16288
16294
  captureGroup: external_exports.number().int().nonnegative().optional()
16289
16295
  }).refine((v) => isValidRegex(v.pattern, v.flags), {
@@ -16850,6 +16856,212 @@ function buildDetectionsList(summaries, query) {
16850
16856
  return { counts, items: filtered.map(summaryToDetectionListItem) };
16851
16857
  }
16852
16858
 
16859
+ // ../../packages/schema/src/zod/shares.ts
16860
+ var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
16861
+ var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
16862
+ var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
16863
+ var DATA_CLASS_ORDER = DataClass.options;
16864
+ var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
16865
+ var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
16866
+ var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
16867
+ var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
16868
+ var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
16869
+ var ReviewInfo = external_exports.object({
16870
+ needsReview: external_exports.boolean(),
16871
+ reasons: external_exports.array(ReviewReason)
16872
+ }).meta({ id: "ReviewInfo" });
16873
+ var DestinationNetwork = external_exports.object({
16874
+ port: external_exports.number().int().nullable(),
16875
+ geo: external_exports.string().nullable(),
16876
+ ptr: external_exports.string().nullable()
16877
+ }).meta({ id: "DestinationNetwork" });
16878
+ var EndpointSummary = external_exports.object({
16879
+ id: external_exports.string(),
16880
+ method: HttpMethod,
16881
+ transport: Transport,
16882
+ url: external_exports.string(),
16883
+ template: external_exports.boolean(),
16884
+ dataClass: DataClass,
16885
+ lastSeen: external_exports.iso.datetime(),
16886
+ callSiteCount: external_exports.number().int().nonnegative()
16887
+ }).meta({ id: "EndpointSummary" });
16888
+ var CallSite = external_exports.object({
16889
+ id: external_exports.string(),
16890
+ project: external_exports.string(),
16891
+ file: external_exports.string(),
16892
+ line: external_exports.number().int().nonnegative(),
16893
+ snippet: external_exports.string(),
16894
+ dynamic: external_exports.boolean(),
16895
+ vendored: external_exports.boolean(),
16896
+ /** Deep-link to the Inventory project, when the repo is governed there. */
16897
+ projectId: external_exports.string().nullable()
16898
+ }).meta({ id: "CallSite" });
16899
+ var EndpointWithSites = EndpointSummary.extend({
16900
+ sites: external_exports.array(CallSite)
16901
+ }).meta({ id: "EndpointWithSites" });
16902
+ var ShareDestinationSummary = external_exports.object({
16903
+ id: external_exports.string(),
16904
+ kind: DestinationKind,
16905
+ name: external_exports.string(),
16906
+ host: external_exports.string(),
16907
+ category: external_exports.string(),
16908
+ trust: ShareTrustLevel,
16909
+ /** Effective state (decision applied over the trust default). */
16910
+ status: EgressStatus,
16911
+ /** True when an egress decision override differs from the trust default. */
16912
+ isCustom: external_exports.boolean(),
16913
+ lastSeen: external_exports.iso.datetime(),
16914
+ endpointCount: external_exports.number().int().nonnegative(),
16915
+ callSiteCount: external_exports.number().int().nonnegative(),
16916
+ transports: external_exports.array(Transport),
16917
+ /** Most-sensitive first. */
16918
+ dataClasses: external_exports.array(DataClass),
16919
+ review: ReviewInfo,
16920
+ /** Non-provider hosts only; null for providers. */
16921
+ network: DestinationNetwork.nullable(),
16922
+ /** Embedded for inline expansion — no call sites here. */
16923
+ endpoints: external_exports.array(EndpointSummary)
16924
+ }).meta({ id: "ShareDestinationSummary" });
16925
+ var ShareDestinationDetail = ShareDestinationSummary.omit({
16926
+ endpointCount: true,
16927
+ callSiteCount: true,
16928
+ endpoints: true
16929
+ }).extend({
16930
+ /** Ownership/geo rationale; null for providers. */
16931
+ note: external_exports.string().nullable(),
16932
+ endpoints: external_exports.array(EndpointWithSites)
16933
+ }).meta({ id: "ShareDestinationDetail" });
16934
+ var ReviewDestination = external_exports.object({
16935
+ id: external_exports.string(),
16936
+ kind: DestinationKind,
16937
+ name: external_exports.string(),
16938
+ /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
16939
+ host: external_exports.string(),
16940
+ trust: ShareTrustLevel,
16941
+ status: EgressStatus,
16942
+ review: ReviewInfo,
16943
+ topDataClass: DataClass,
16944
+ callSiteCount: external_exports.number().int().nonnegative(),
16945
+ lastSeen: external_exports.iso.datetime()
16946
+ }).meta({ id: "ReviewDestination" });
16947
+ var ShareDestinationGroup = external_exports.object({
16948
+ kind: DestinationKind,
16949
+ total: external_exports.number().int().nonnegative(),
16950
+ items: external_exports.array(ShareDestinationSummary)
16951
+ }).meta({ id: "ShareDestinationGroup" });
16952
+ var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
16953
+ var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
16954
+ var SharesStats = external_exports.object({
16955
+ destinations: external_exports.number().int().nonnegative(),
16956
+ endpoints: external_exports.number().int().nonnegative(),
16957
+ callSites: external_exports.number().int().nonnegative(),
16958
+ needsReview: external_exports.number().int().nonnegative(),
16959
+ insecure: external_exports.number().int().nonnegative(),
16960
+ byKind: external_exports.object({
16961
+ provider: external_exports.number().int().nonnegative(),
16962
+ internal: external_exports.number().int().nonnegative(),
16963
+ external: external_exports.number().int().nonnegative(),
16964
+ ip: external_exports.number().int().nonnegative()
16965
+ }),
16966
+ byTrust: external_exports.object({
16967
+ recognized: external_exports.number().int().nonnegative(),
16968
+ internal: external_exports.number().int().nonnegative(),
16969
+ unverified: external_exports.number().int().nonnegative(),
16970
+ ip: external_exports.number().int().nonnegative()
16971
+ })
16972
+ }).meta({ id: "SharesStats" });
16973
+ var SetEgressDecisionBody = external_exports.object({
16974
+ /** `null` clears the override — reverts to the trust default, isCustom false. */
16975
+ decision: EgressDecision.nullable()
16976
+ }).meta({ id: "SetEgressDecisionBody" });
16977
+ var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
16978
+ var ListShareDestinationsQuery = external_exports.object({
16979
+ /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
16980
+ q: external_exports.string().optional(),
16981
+ /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
16982
+ kind: external_exports.array(DestinationKind).optional(),
16983
+ /** Reserved for future grouping modes; only 'destination' is supported today. */
16984
+ groupBy: external_exports.enum(["destination"]).default("destination"),
16985
+ /**
16986
+ * When true, return a flat severity-ordered `items[]` instead of `groups`.
16987
+ * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
16988
+ * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
16989
+ * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
16990
+ */
16991
+ review: external_exports.stringbool().default(false)
16992
+ });
16993
+ var ExportSharesQuery = external_exports.object({
16994
+ format: external_exports.enum(["csv", "json"]).default("csv"),
16995
+ q: external_exports.string().optional(),
16996
+ kind: external_exports.array(DestinationKind).optional()
16997
+ });
16998
+
16999
+ // ../../packages/schema/src/zod/egress-extraction.ts
17000
+ var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
17001
+ var ProviderRegistryEntry = external_exports.object({
17002
+ id: external_exports.string(),
17003
+ name: external_exports.string(),
17004
+ category: external_exports.string(),
17005
+ /** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
17006
+ hostSuffixes: external_exports.array(external_exports.string()).min(1),
17007
+ /** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
17008
+ apiBase: external_exports.string(),
17009
+ /** Most-sensitive first; index 0 becomes the endpoint dataClass. */
17010
+ defaultDataClasses: external_exports.array(DataClass).min(1),
17011
+ /** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
17012
+ sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
17013
+ }).meta({ id: "ProviderRegistryEntry" });
17014
+ var EgressCallSiteHit = external_exports.object({
17015
+ file: external_exports.string(),
17016
+ line: external_exports.number().int().positive(),
17017
+ snippet: external_exports.string(),
17018
+ dynamic: external_exports.boolean(),
17019
+ vendored: external_exports.boolean()
17020
+ }).meta({ id: "EgressCallSiteHit" });
17021
+ var ResolvedEgressHit = external_exports.object({
17022
+ host: external_exports.string(),
17023
+ kind: DestinationKind,
17024
+ name: external_exports.string(),
17025
+ category: external_exports.string(),
17026
+ trust: ShareTrustLevel,
17027
+ network: DestinationNetwork.nullable(),
17028
+ method: HttpMethod,
17029
+ transport: Transport,
17030
+ url: external_exports.string(),
17031
+ template: external_exports.boolean(),
17032
+ dataClass: DataClass,
17033
+ site: EgressCallSiteHit
17034
+ }).meta({ id: "ResolvedEgressHit" });
17035
+ var EgressReconcile = external_exports.discriminatedUnion("mode", [
17036
+ external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
17037
+ external_exports.object({
17038
+ mode: external_exports.literal("ledger"),
17039
+ scannedFiles: external_exports.array(external_exports.string()),
17040
+ deletedFiles: external_exports.array(external_exports.string())
17041
+ })
17042
+ ]).meta({ id: "EgressReconcile" });
17043
+ var RecordProjectEgressInput = external_exports.object({
17044
+ /** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
17045
+ projectKey: external_exports.string().min(1),
17046
+ /** Display name only — never keys reconciliation. */
17047
+ project: external_exports.string(),
17048
+ projectId: external_exports.string().nullable(),
17049
+ reconcile: EgressReconcile,
17050
+ hits: external_exports.array(ResolvedEgressHit)
17051
+ }).meta({ id: "RecordProjectEgressInput" });
17052
+ var EgressWriteSummary = external_exports.object({
17053
+ destinations: external_exports.number().int().nonnegative(),
17054
+ endpoints: external_exports.number().int().nonnegative(),
17055
+ callSites: external_exports.number().int().nonnegative(),
17056
+ truncated: external_exports.boolean(),
17057
+ /**
17058
+ * Files the cap dropped whole. Their stored rows were left untouched, so a
17059
+ * ledger-keeping caller must withhold their ledger entries and read them
17060
+ * again next scan.
17061
+ */
17062
+ droppedFiles: external_exports.array(external_exports.string()).default([])
17063
+ }).meta({ id: "EgressWriteSummary" });
17064
+
16853
17065
  // ../../packages/schema/src/zod/findings-group-build.ts
16854
17066
  function toApiAction(dbVal) {
16855
17067
  const map2 = {
@@ -17111,7 +17323,7 @@ var PatchInstalledPackRequest = external_exports.object({
17111
17323
  }).meta({ id: "PatchInstalledPackRequest" });
17112
17324
 
17113
17325
  // ../../packages/schema/src/zod/local.ts
17114
- var WORKSPACE_SETTINGS_SPEC_VERSION = 2;
17326
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 3;
17115
17327
  var RunMode = external_exports.enum(["standalone"]);
17116
17328
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17117
17329
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17126,6 +17338,9 @@ var WorkspaceSettings = external_exports.object({
17126
17338
  policy: SimpleDetectionPolicy.default("redact"),
17127
17339
  // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
17128
17340
  historicalAccess: HistoricalAccess.default("session-only"),
17341
+ // In-place egress extraction on the scan paths; disable to stop all Data
17342
+ // Shares writes.
17343
+ dataSharesInPlace: external_exports.boolean().default(true),
17129
17344
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17130
17345
  onboardedAt: external_exports.iso.datetime().optional()
17131
17346
  });
@@ -17609,145 +17824,6 @@ var SetupHandoffOffer = external_exports.object({
17609
17824
  path: ["liveKeys"]
17610
17825
  });
17611
17826
 
17612
- // ../../packages/schema/src/zod/shares.ts
17613
- var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17614
- var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
17615
- var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
17616
- var DATA_CLASS_ORDER = DataClass.options;
17617
- var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
17618
- var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
17619
- var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
17620
- var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
17621
- var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
17622
- var ReviewInfo = external_exports.object({
17623
- needsReview: external_exports.boolean(),
17624
- reasons: external_exports.array(ReviewReason)
17625
- }).meta({ id: "ReviewInfo" });
17626
- var DestinationNetwork = external_exports.object({
17627
- port: external_exports.number().int().nullable(),
17628
- geo: external_exports.string().nullable(),
17629
- ptr: external_exports.string().nullable()
17630
- }).meta({ id: "DestinationNetwork" });
17631
- var EndpointSummary = external_exports.object({
17632
- id: external_exports.string(),
17633
- method: HttpMethod,
17634
- transport: Transport,
17635
- url: external_exports.string(),
17636
- template: external_exports.boolean(),
17637
- dataClass: DataClass,
17638
- lastSeen: external_exports.iso.datetime(),
17639
- callSiteCount: external_exports.number().int().nonnegative()
17640
- }).meta({ id: "EndpointSummary" });
17641
- var CallSite = external_exports.object({
17642
- id: external_exports.string(),
17643
- project: external_exports.string(),
17644
- file: external_exports.string(),
17645
- line: external_exports.number().int().nonnegative(),
17646
- snippet: external_exports.string(),
17647
- dynamic: external_exports.boolean(),
17648
- vendored: external_exports.boolean(),
17649
- /** Deep-link to the Inventory project, when the repo is governed there. */
17650
- projectId: external_exports.string().nullable()
17651
- }).meta({ id: "CallSite" });
17652
- var EndpointWithSites = EndpointSummary.extend({
17653
- sites: external_exports.array(CallSite)
17654
- }).meta({ id: "EndpointWithSites" });
17655
- var ShareDestinationSummary = external_exports.object({
17656
- id: external_exports.string(),
17657
- kind: DestinationKind,
17658
- name: external_exports.string(),
17659
- host: external_exports.string(),
17660
- category: external_exports.string(),
17661
- trust: ShareTrustLevel,
17662
- /** Effective state (decision applied over the trust default). */
17663
- status: EgressStatus,
17664
- /** True when an egress decision override differs from the trust default. */
17665
- isCustom: external_exports.boolean(),
17666
- lastSeen: external_exports.iso.datetime(),
17667
- endpointCount: external_exports.number().int().nonnegative(),
17668
- callSiteCount: external_exports.number().int().nonnegative(),
17669
- transports: external_exports.array(Transport),
17670
- /** Most-sensitive first. */
17671
- dataClasses: external_exports.array(DataClass),
17672
- review: ReviewInfo,
17673
- /** Non-provider hosts only; null for providers. */
17674
- network: DestinationNetwork.nullable(),
17675
- /** Embedded for inline expansion — no call sites here. */
17676
- endpoints: external_exports.array(EndpointSummary)
17677
- }).meta({ id: "ShareDestinationSummary" });
17678
- var ShareDestinationDetail = ShareDestinationSummary.omit({
17679
- endpointCount: true,
17680
- callSiteCount: true,
17681
- endpoints: true
17682
- }).extend({
17683
- /** Ownership/geo rationale; null for providers. */
17684
- note: external_exports.string().nullable(),
17685
- endpoints: external_exports.array(EndpointWithSites)
17686
- }).meta({ id: "ShareDestinationDetail" });
17687
- var ReviewDestination = external_exports.object({
17688
- id: external_exports.string(),
17689
- kind: DestinationKind,
17690
- name: external_exports.string(),
17691
- /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
17692
- host: external_exports.string(),
17693
- trust: ShareTrustLevel,
17694
- status: EgressStatus,
17695
- review: ReviewInfo,
17696
- topDataClass: DataClass,
17697
- callSiteCount: external_exports.number().int().nonnegative(),
17698
- lastSeen: external_exports.iso.datetime()
17699
- }).meta({ id: "ReviewDestination" });
17700
- var ShareDestinationGroup = external_exports.object({
17701
- kind: DestinationKind,
17702
- total: external_exports.number().int().nonnegative(),
17703
- items: external_exports.array(ShareDestinationSummary)
17704
- }).meta({ id: "ShareDestinationGroup" });
17705
- var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
17706
- var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
17707
- var SharesStats = external_exports.object({
17708
- destinations: external_exports.number().int().nonnegative(),
17709
- endpoints: external_exports.number().int().nonnegative(),
17710
- callSites: external_exports.number().int().nonnegative(),
17711
- needsReview: external_exports.number().int().nonnegative(),
17712
- insecure: external_exports.number().int().nonnegative(),
17713
- byKind: external_exports.object({
17714
- provider: external_exports.number().int().nonnegative(),
17715
- internal: external_exports.number().int().nonnegative(),
17716
- ip: external_exports.number().int().nonnegative()
17717
- }),
17718
- byTrust: external_exports.object({
17719
- recognized: external_exports.number().int().nonnegative(),
17720
- internal: external_exports.number().int().nonnegative(),
17721
- unverified: external_exports.number().int().nonnegative(),
17722
- ip: external_exports.number().int().nonnegative()
17723
- })
17724
- }).meta({ id: "SharesStats" });
17725
- var SetEgressDecisionBody = external_exports.object({
17726
- /** `null` clears the override — reverts to the trust default, isCustom false. */
17727
- decision: EgressDecision.nullable()
17728
- }).meta({ id: "SetEgressDecisionBody" });
17729
- var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
17730
- var ListShareDestinationsQuery = external_exports.object({
17731
- /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
17732
- q: external_exports.string().optional(),
17733
- /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
17734
- kind: external_exports.array(DestinationKind).optional(),
17735
- /** Reserved for future grouping modes; only 'destination' is supported today. */
17736
- groupBy: external_exports.enum(["destination"]).default("destination"),
17737
- /**
17738
- * When true, return a flat severity-ordered `items[]` instead of `groups`.
17739
- * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
17740
- * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
17741
- * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17742
- */
17743
- review: external_exports.stringbool().default(false)
17744
- });
17745
- var ExportSharesQuery = external_exports.object({
17746
- format: external_exports.enum(["csv", "json"]).default("csv"),
17747
- q: external_exports.string().optional(),
17748
- kind: external_exports.array(DestinationKind).optional()
17749
- });
17750
-
17751
17827
  // ../../packages/schema/src/zod/shares-access.ts
17752
17828
  var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
17753
17829
  function trustDefaultStatus(trust) {
@@ -17767,7 +17843,7 @@ function deriveReviewReasons(trust, transports) {
17767
17843
  const reasons = [];
17768
17844
  if (trust === "ip") reasons.push("raw_ip");
17769
17845
  if (trust === "unverified") reasons.push("unverified_domain");
17770
- if (transports.includes("http")) reasons.push("plaintext_transport");
17846
+ if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
17771
17847
  return reasons;
17772
17848
  }
17773
17849
  function buildReviewInfo(trust, transports) {
@@ -17998,6 +18074,7 @@ function applyMigrations(db) {
17998
18074
  ensureSyncedAtColumn(db, "audit_events");
17999
18075
  ensureScanLedgerTable(db);
18000
18076
  ensureBlockedDetectionsTable(db);
18077
+ ensureRuleProbeCacheTable(db);
18001
18078
  ensureWriteGateTrigger(db);
18002
18079
  ensureTokenUsageColumns(db);
18003
18080
  reconcileSourceProjectIds(db);
@@ -18137,6 +18214,14 @@ function ensureBlockedDetectionsTable(db) {
18137
18214
  blocked_at INTEGER NOT NULL
18138
18215
  )`);
18139
18216
  }
18217
+ function ensureRuleProbeCacheTable(db) {
18218
+ db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
18219
+ rule_key TEXT PRIMARY KEY,
18220
+ verdict TEXT NOT NULL,
18221
+ worst_probe_ms REAL NOT NULL,
18222
+ checked_at INTEGER NOT NULL
18223
+ )`);
18224
+ }
18140
18225
 
18141
18226
  // ../../packages/persistence/src/paths.ts
18142
18227
  import { chmodSync, mkdirSync } from "fs";
@@ -21690,6 +21775,35 @@ var SqliteResolutionsRepository = class {
21690
21775
  }
21691
21776
  };
21692
21777
 
21778
+ // ../../packages/persistence/src/repositories/rule-probe-cache.ts
21779
+ var SqliteRuleProbeCacheRepository = class {
21780
+ constructor(db) {
21781
+ this.db = db;
21782
+ this.upsertStmt = db.prepare(
21783
+ `INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
21784
+ VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
21785
+ ON CONFLICT (rule_key) DO UPDATE SET
21786
+ verdict = excluded.verdict,
21787
+ worst_probe_ms = excluded.worst_probe_ms,
21788
+ checked_at = excluded.checked_at`
21789
+ );
21790
+ this.readStmt = db.prepare(
21791
+ `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
21792
+ );
21793
+ }
21794
+ db;
21795
+ upsertStmt;
21796
+ readStmt;
21797
+ getVerdict(ruleKey) {
21798
+ return getRow(this.readStmt, { ruleKey });
21799
+ }
21800
+ setVerdict(ruleKey, verdict, worstProbeMs) {
21801
+ failOpenTransaction(this.db, () => {
21802
+ this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
21803
+ });
21804
+ }
21805
+ };
21806
+
21693
21807
  // ../../packages/persistence/src/repositories/scan-ledger.ts
21694
21808
  var SqliteScanLedgerRepository = class {
21695
21809
  constructor(db) {
@@ -22101,11 +22215,50 @@ var SqliteSecurityRepository = class {
22101
22215
 
22102
22216
  // ../../packages/persistence/src/repositories/shares.ts
22103
22217
  import { randomUUID as randomUUID7 } from "crypto";
22104
- var KIND_ORDER = ["provider", "internal", "ip"];
22218
+ var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22219
+ var IN_CHUNK = 500;
22220
+ var KIND_ORDER = ["provider", "internal", "external", "ip"];
22221
+ var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
22222
+ var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
22223
+ LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
22105
22224
  var CALL_SITE_EMBED_CAP = 200;
22106
22225
  function parseNetwork(networkJson) {
22107
22226
  return safeJson(networkJson, null);
22108
22227
  }
22228
+ function capHits(all, mode) {
22229
+ if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
22230
+ return { hits: [...all], droppedFiles: [], truncated: false };
22231
+ }
22232
+ if (mode === "walk") {
22233
+ return {
22234
+ hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
22235
+ droppedFiles: [],
22236
+ truncated: true
22237
+ };
22238
+ }
22239
+ const byFile = /* @__PURE__ */ new Map();
22240
+ for (const hit of all) {
22241
+ const bucket = byFile.get(hit.site.file);
22242
+ if (bucket === void 0) byFile.set(hit.site.file, [hit]);
22243
+ else bucket.push(hit);
22244
+ }
22245
+ const hits = [];
22246
+ const droppedFiles = [];
22247
+ for (const [file2, bucket] of byFile) {
22248
+ if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
22249
+ else hits.push(...bucket);
22250
+ }
22251
+ return { hits, droppedFiles, truncated: true };
22252
+ }
22253
+ function withoutDroppedFiles(reconcile, droppedFiles) {
22254
+ if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
22255
+ const dropped = new Set(droppedFiles);
22256
+ return {
22257
+ mode: "ledger",
22258
+ scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
22259
+ deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
22260
+ };
22261
+ }
22109
22262
  function toEndpointSummary(row) {
22110
22263
  return {
22111
22264
  id: row.id,
@@ -22196,13 +22349,15 @@ var SqliteSharesRepository = class {
22196
22349
  const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22197
22350
  const insecure = countScalar(
22198
22351
  this.db,
22199
- "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
22352
+ `SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
22353
+ WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
22200
22354
  );
22201
22355
  const needsReview = countScalar(
22202
22356
  this.db,
22203
22357
  `SELECT count(DISTINCT d.id) AS n
22204
22358
  FROM share_destination d
22205
- LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
22359
+ LEFT JOIN share_endpoint e ON e.destination_id = d.id
22360
+ AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
22206
22361
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
22207
22362
  );
22208
22363
  const kindCounts = countBy(
@@ -22212,6 +22367,7 @@ var SqliteSharesRepository = class {
22212
22367
  const byKind = {
22213
22368
  provider: kindCounts.get("provider") ?? 0,
22214
22369
  internal: kindCounts.get("internal") ?? 0,
22370
+ external: kindCounts.get("external") ?? 0,
22215
22371
  ip: kindCounts.get("ip") ?? 0
22216
22372
  };
22217
22373
  const trustCounts = countBy(
@@ -22287,23 +22443,316 @@ var SqliteSharesRepository = class {
22287
22443
  // real edit from a no-such-destination.
22288
22444
  /**
22289
22445
  * Set (decision) or clear (null) the egress decision override for a destination.
22290
- * `null` deletes the override row → reverts to the trust default.
22446
+ * `null` deletes the override rows → reverts to the trust default.
22447
+ *
22448
+ * The written row carries both the destination id and its host, so the
22449
+ * decision re-attaches by host after the destination is pruned and
22450
+ * re-detected under a fresh id. Rows written before the host column existed
22451
+ * (host NULL, matched by destination id) are replaced rather than left to
22452
+ * shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
22453
+ * would otherwise race a concurrent prune.
22291
22454
  */
22292
22455
  setEgressDecision(destinationId, decision) {
22293
- const exists = this.db.prepare("SELECT 1 FROM share_destination WHERE id = ?").get(destinationId);
22294
- if (exists === void 0) return false;
22295
- if (decision === null) {
22296
- this.db.prepare("DELETE FROM egress_decision_override WHERE destination_id = ?").run(destinationId);
22297
- return true;
22456
+ let existed = false;
22457
+ withTransaction(
22458
+ this.db,
22459
+ () => {
22460
+ const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
22461
+ if (dest === void 0) return;
22462
+ existed = true;
22463
+ this.db.prepare(
22464
+ `DELETE FROM egress_decision_override
22465
+ WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
22466
+ ).run({ host: dest.host, destinationId });
22467
+ if (decision === null) return;
22468
+ this.db.prepare(
22469
+ `INSERT INTO egress_decision_override
22470
+ (id, destination_id, host, decision, created_at, updated_at)
22471
+ VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22472
+ ).run({
22473
+ id: randomUUID7(),
22474
+ destinationId,
22475
+ host: dest.host,
22476
+ decision,
22477
+ now: Date.now()
22478
+ });
22479
+ },
22480
+ "IMMEDIATE"
22481
+ );
22482
+ return existed;
22483
+ }
22484
+ /**
22485
+ * Record one project's statically-extracted egress: reconcile the previously
22486
+ * stored call sites against this scan, upsert destination → endpoint → call
22487
+ * site for every hit, confirm `last_seen` on everything the project still
22488
+ * references, and drop what no longer has evidence.
22489
+ *
22490
+ * Reconciliation keys on `projectKey` alone; `project` and `projectId` are
22491
+ * display payload and never scope a delete. The whole write is one
22492
+ * transaction: a failure leaves the project's previous inventory exactly as
22493
+ * it was, and THROWS rather than reporting a partial write — callers decide
22494
+ * their own fail-open behavior, and the scanner additionally withholds its
22495
+ * ledger commit so the next scan retries.
22496
+ *
22497
+ * Over-cap input is truncated at a FILE boundary, and the files that lost
22498
+ * their hits are both excluded from the reconcile delete and named in
22499
+ * `droppedFiles`. That pairing is what keeps truncation non-destructive on
22500
+ * the ledger path: a dropped file keeps whatever rows it already had, and its
22501
+ * caller withholds the ledger entry so the next scan reads it again.
22502
+ */
22503
+ recordProjectEgress(input) {
22504
+ const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
22505
+ const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
22506
+ const now = Date.now();
22507
+ let summary = {
22508
+ destinations: 0,
22509
+ endpoints: 0,
22510
+ callSites: 0,
22511
+ truncated,
22512
+ droppedFiles
22513
+ };
22514
+ withTransaction(
22515
+ this.db,
22516
+ () => {
22517
+ const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
22518
+ this.reconcileCallSites(input.projectKey, reconcile);
22519
+ this.upsertHits(input, hits, projectId, now);
22520
+ this.confirmLastSeen(input.projectKey, now);
22521
+ this.pruneOrphans();
22522
+ summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
22523
+ },
22524
+ "IMMEDIATE"
22525
+ );
22526
+ return summary;
22527
+ }
22528
+ // ─── Egress write internals ──────────────────────────────────────────────────
22529
+ /**
22530
+ * Clear the stored call sites this scan is responsible for re-creating.
22531
+ *
22532
+ * Each pipeline may only delete rows its own walker could have produced. The
22533
+ * fs walk behind 'walk' mode never descends into dot-directories, so its
22534
+ * delete excludes dot-path files — those rows are the plugin scanner's to
22535
+ * reconcile, and deleting them here would make the two pipelines erase each
22536
+ * other's rows on every alternating scan. 'ledger' mode names its files
22537
+ * outright and never mass-deletes, so rows the fs walk contributed for files
22538
+ * the scanner skips (vendored, oversize) survive it.
22539
+ */
22540
+ reconcileCallSites(projectKey, reconcile) {
22541
+ if (reconcile.mode === "walk") {
22542
+ const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
22543
+ this.db.prepare(
22544
+ `DELETE FROM share_call_site
22545
+ WHERE project_key = :key
22546
+ AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
22547
+ AND file NOT LIKE '.%'
22548
+ AND file NOT LIKE '%/.%'`
22549
+ ).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
22550
+ return;
22298
22551
  }
22552
+ const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
22553
+ for (let i = 0; i < files.length; i += IN_CHUNK) {
22554
+ const chunk = files.slice(i, i + IN_CHUNK);
22555
+ this.db.prepare(
22556
+ `DELETE FROM share_call_site
22557
+ WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
22558
+ ).run(projectKey, ...chunk);
22559
+ }
22560
+ }
22561
+ /**
22562
+ * Upsert every hit as destination → endpoint → call site. Destinations key on
22563
+ * `host` and endpoints on `(destination_id, method, url)`, both shared across
22564
+ * projects; only the call site carries `project_key`. A destination's `note`
22565
+ * is user-owned and never overwritten. The id caches keep one upsert per
22566
+ * distinct host and endpoint, so the first hit for a host supplies its
22567
+ * classification for this batch.
22568
+ */
22569
+ upsertHits(input, hits, projectId, now) {
22570
+ if (hits.length === 0) return;
22571
+ const destStmt = this.db.prepare(
22572
+ `INSERT INTO share_destination
22573
+ (id, kind, name, host, category, trust, network_json, last_seen, provenance,
22574
+ created_at, updated_at)
22575
+ VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
22576
+ ON CONFLICT (host) DO UPDATE SET
22577
+ kind = excluded.kind,
22578
+ name = excluded.name,
22579
+ category = excluded.category,
22580
+ trust = excluded.trust,
22581
+ network_json = excluded.network_json,
22582
+ last_seen = excluded.last_seen,
22583
+ updated_at = excluded.updated_at`
22584
+ );
22585
+ const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
22586
+ const endpointStmt = this.db.prepare(
22587
+ `INSERT INTO share_endpoint
22588
+ (id, destination_id, method, transport, url, template, data_class, last_seen,
22589
+ created_at, updated_at)
22590
+ VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
22591
+ :now, :now)
22592
+ ON CONFLICT (destination_id, method, url) DO UPDATE SET
22593
+ transport = excluded.transport,
22594
+ template = excluded.template,
22595
+ data_class = excluded.data_class,
22596
+ last_seen = excluded.last_seen,
22597
+ updated_at = excluded.updated_at`
22598
+ );
22599
+ const endpointIdStmt = this.db.prepare(
22600
+ "SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
22601
+ );
22602
+ const siteStmt = this.db.prepare(
22603
+ `INSERT INTO share_call_site
22604
+ (id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
22605
+ project_id, created_at, updated_at)
22606
+ VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
22607
+ :vendored, :projectId, :now, :now)
22608
+ ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
22609
+ snippet = excluded.snippet,
22610
+ dynamic = excluded.dynamic,
22611
+ vendored = excluded.vendored,
22612
+ project = excluded.project,
22613
+ project_id = COALESCE(excluded.project_id, share_call_site.project_id),
22614
+ updated_at = excluded.updated_at`
22615
+ );
22616
+ const destIds = /* @__PURE__ */ new Map();
22617
+ const endpointIds = /* @__PURE__ */ new Map();
22618
+ for (const hit of hits) {
22619
+ let destinationId = destIds.get(hit.host);
22620
+ if (destinationId === void 0) {
22621
+ destStmt.run({
22622
+ id: randomUUID7(),
22623
+ kind: hit.kind,
22624
+ name: hit.name,
22625
+ host: hit.host,
22626
+ category: hit.category,
22627
+ trust: hit.trust,
22628
+ networkJson: hit.network === null ? null : JSON.stringify(hit.network),
22629
+ now
22630
+ });
22631
+ destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
22632
+ destIds.set(hit.host, destinationId);
22633
+ }
22634
+ const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
22635
+ let endpointId = endpointIds.get(endpointKey);
22636
+ if (endpointId === void 0) {
22637
+ endpointStmt.run({
22638
+ id: randomUUID7(),
22639
+ destinationId,
22640
+ method: hit.method,
22641
+ transport: hit.transport,
22642
+ url: hit.url,
22643
+ template: boolToInt(hit.template),
22644
+ dataClass: hit.dataClass,
22645
+ now
22646
+ });
22647
+ endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
22648
+ endpointIds.set(endpointKey, endpointId);
22649
+ }
22650
+ siteStmt.run({
22651
+ id: randomUUID7(),
22652
+ endpointId,
22653
+ project: input.project,
22654
+ projectKey: input.projectKey,
22655
+ file: hit.site.file,
22656
+ line: hit.site.line,
22657
+ snippet: hit.site.snippet,
22658
+ dynamic: boolToInt(hit.site.dynamic),
22659
+ vendored: boolToInt(hit.site.vendored),
22660
+ projectId,
22661
+ now
22662
+ });
22663
+ }
22664
+ }
22665
+ /**
22666
+ * The source-project id this project's stored call sites already carry, if
22667
+ * any. Only the pipeline that resolves a source project supplies one; the
22668
+ * other passes null and inherits this, so the link stops flapping between a
22669
+ * real id and NULL depending on which pipeline ran last. The value is a
22670
+ * per-project attribute stored redundantly on each row, so any row's is
22671
+ * representative.
22672
+ */
22673
+ knownProjectId(projectKey) {
22674
+ return getRow(
22675
+ this.db.prepare(
22676
+ `SELECT project_id AS projectId FROM share_call_site
22677
+ WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
22678
+ ),
22679
+ [projectKey]
22680
+ )?.projectId ?? null;
22681
+ }
22682
+ /**
22683
+ * Stamp `last_seen` on every endpoint and destination this project still
22684
+ * references — including rows the scan preserved rather than re-wrote, so a
22685
+ * ledger-skipped file's references don't decay into "stale" on the page.
22686
+ */
22687
+ confirmLastSeen(projectKey, now) {
22299
22688
  this.db.prepare(
22300
- `INSERT INTO egress_decision_override (id, destination_id, decision, created_at, updated_at)
22301
- VALUES (:id, :destinationId, :decision, :now, :now)
22302
- ON CONFLICT (destination_id) DO UPDATE SET
22303
- decision = excluded.decision,
22304
- updated_at = excluded.updated_at`
22305
- ).run({ id: randomUUID7(), destinationId, decision, now: Date.now() });
22306
- return true;
22689
+ `UPDATE share_endpoint SET last_seen = :now, updated_at = :now
22690
+ WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
22691
+ ).run({ now, key: projectKey });
22692
+ this.db.prepare(
22693
+ `UPDATE share_destination SET last_seen = :now, updated_at = :now
22694
+ WHERE id IN (SELECT DISTINCT e.destination_id
22695
+ FROM share_endpoint e
22696
+ JOIN share_call_site c ON c.endpoint_id = e.id
22697
+ WHERE c.project_key = :key)`
22698
+ ).run({ now, key: projectKey });
22699
+ }
22700
+ /**
22701
+ * Drop rows left without evidence: endpoints with no call site, then
22702
+ * destinations with no endpoint. Call sites are the only evidence either one
22703
+ * has, so a row that lost its last one belongs to no project any more.
22704
+ *
22705
+ * Overrides are deleted between the two steps, and only the ones written
22706
+ * before the host column existed. Those match a destination by id alone;
22707
+ * because the id link is released on delete rather than cascading, leaving
22708
+ * them would accumulate rows that match neither join arm and that nothing can
22709
+ * reach again. Host-bearing rows deliberately survive — the host is what
22710
+ * re-attaches a user's decision when the destination comes back.
22711
+ */
22712
+ pruneOrphans() {
22713
+ this.db.exec(
22714
+ `DELETE FROM share_endpoint
22715
+ WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
22716
+ );
22717
+ this.db.exec(
22718
+ `DELETE FROM egress_decision_override
22719
+ WHERE host IS NULL
22720
+ AND destination_id IN (
22721
+ SELECT d.id FROM share_destination d
22722
+ WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
22723
+ );
22724
+ this.db.exec(
22725
+ `DELETE FROM share_destination
22726
+ WHERE NOT EXISTS (
22727
+ SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
22728
+ );
22729
+ }
22730
+ /**
22731
+ * Live totals for one project. Destinations and endpoints are shared across
22732
+ * projects and carry no project column, so both are counted through the call
22733
+ * sites that reference them.
22734
+ */
22735
+ projectTotals(projectKey) {
22736
+ return {
22737
+ destinations: countScalar(
22738
+ this.db,
22739
+ `SELECT count(DISTINCT e.destination_id) AS n
22740
+ FROM share_endpoint e
22741
+ JOIN share_call_site c ON c.endpoint_id = e.id
22742
+ WHERE c.project_key = ?`,
22743
+ [projectKey]
22744
+ ),
22745
+ endpoints: countScalar(
22746
+ this.db,
22747
+ "SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
22748
+ [projectKey]
22749
+ ),
22750
+ callSites: countScalar(
22751
+ this.db,
22752
+ "SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
22753
+ [projectKey]
22754
+ )
22755
+ };
22307
22756
  }
22308
22757
  // ─── Raw fetchers ────────────────────────────────────────────────────────────
22309
22758
  mapDestRow(r) {
@@ -22323,7 +22772,8 @@ var SqliteSharesRepository = class {
22323
22772
  fetchDestinations(q, kinds, reviewOnly = false) {
22324
22773
  const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22325
22774
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22326
- d.created_at AS createdAt, o.decision AS overrideDecision`;
22775
+ d.created_at AS createdAt,
22776
+ COALESCE(oh.decision, ol.decision) AS overrideDecision`;
22327
22777
  const conditions = [];
22328
22778
  const params = [];
22329
22779
  if (kinds && kinds.length > 0) {
@@ -22334,7 +22784,8 @@ var SqliteSharesRepository = class {
22334
22784
  conditions.push(
22335
22785
  `(d.trust IN ('unverified', 'ip')
22336
22786
  OR EXISTS (SELECT 1 FROM share_endpoint re
22337
- WHERE re.destination_id = d.id AND re.transport = 'http'))`
22787
+ WHERE re.destination_id = d.id
22788
+ AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
22338
22789
  );
22339
22790
  }
22340
22791
  let sql;
@@ -22347,7 +22798,7 @@ var SqliteSharesRepository = class {
22347
22798
  params.push(pattern, pattern, pattern, pattern, pattern);
22348
22799
  sql = `SELECT DISTINCT ${cols}
22349
22800
  FROM share_destination d
22350
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22801
+ ${OVERRIDE_JOIN}
22351
22802
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
22352
22803
  LEFT JOIN share_call_site c ON c.endpoint_id = e.id
22353
22804
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
@@ -22355,7 +22806,7 @@ var SqliteSharesRepository = class {
22355
22806
  } else {
22356
22807
  sql = `SELECT ${cols}
22357
22808
  FROM share_destination d
22358
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22809
+ ${OVERRIDE_JOIN}
22359
22810
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
22360
22811
  ORDER BY d.created_at ASC, d.id ASC`;
22361
22812
  }
@@ -22370,9 +22821,9 @@ var SqliteSharesRepository = class {
22370
22821
  this.db.prepare(
22371
22822
  `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22372
22823
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22373
- o.decision AS overrideDecision
22824
+ COALESCE(oh.decision, ol.decision) AS overrideDecision
22374
22825
  FROM share_destination d
22375
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22826
+ ${OVERRIDE_JOIN}
22376
22827
  WHERE d.id = ?`
22377
22828
  ),
22378
22829
  [destinationId]
@@ -22601,6 +23052,7 @@ function openLocalDatabase(dir) {
22601
23052
  const scanLedger = new SqliteScanLedgerRepository(db);
22602
23053
  const exceptions = new SqliteExceptionsRepository(db);
22603
23054
  const resolutions = new SqliteResolutionsRepository(db);
23055
+ const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
22604
23056
  const security = new SqliteSecurityRepository(db);
22605
23057
  const detections = new SqliteDetectionsRepository(db);
22606
23058
  const shares = new SqliteSharesRepository(db);
@@ -22738,6 +23190,7 @@ function openLocalDatabase(dir) {
22738
23190
  scanLedger,
22739
23191
  exceptions,
22740
23192
  resolutions,
23193
+ ruleProbeCache,
22741
23194
  security,
22742
23195
  detections,
22743
23196
  shares,
@@ -22950,13 +23403,581 @@ import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as s
22950
23403
  import { homedir as homedir2 } from "os";
22951
23404
  import { basename as basename2, join as join7 } from "path";
22952
23405
 
23406
+ // ../../packages/detections/src/egress/registry.ts
23407
+ var EXTRACTOR_VERSION = "1";
23408
+ var PROVIDER_REGISTRY = [
23409
+ {
23410
+ id: "stripe",
23411
+ name: "Stripe",
23412
+ category: "Payments",
23413
+ hostSuffixes: ["stripe.com"],
23414
+ apiBase: "https://api.stripe.com",
23415
+ defaultDataClasses: ["pii", "customer"],
23416
+ sdks: {
23417
+ npm: ["stripe"],
23418
+ pypi: ["stripe"],
23419
+ go: ["github.com/stripe/stripe-go"],
23420
+ maven: ["com.stripe"],
23421
+ rubygems: ["stripe"],
23422
+ composer: ["stripe/stripe-php"],
23423
+ nuget: ["Stripe.net"]
23424
+ }
23425
+ },
23426
+ {
23427
+ id: "datadog",
23428
+ name: "Datadog",
23429
+ category: "Observability",
23430
+ hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
23431
+ apiBase: "https://api.datadoghq.com",
23432
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23433
+ sdks: {
23434
+ npm: ["dd-trace", "@datadog/browser-logs"],
23435
+ pypi: ["datadog", "ddtrace"],
23436
+ go: ["github.com/DataDog/dd-trace-go"],
23437
+ maven: ["com.datadoghq"],
23438
+ rubygems: ["ddtrace", "dogapi"],
23439
+ nuget: ["Datadog.Trace"]
23440
+ }
23441
+ },
23442
+ {
23443
+ id: "newrelic",
23444
+ name: "New Relic",
23445
+ category: "Observability",
23446
+ hostSuffixes: ["newrelic.com", "nr-data.net"],
23447
+ apiBase: "https://api.newrelic.com",
23448
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23449
+ sdks: {
23450
+ npm: ["newrelic"],
23451
+ pypi: ["newrelic"],
23452
+ go: ["github.com/newrelic/go-agent"],
23453
+ maven: ["com.newrelic.agent.java"],
23454
+ rubygems: ["newrelic_rpm"],
23455
+ nuget: ["NewRelic.Agent"]
23456
+ }
23457
+ },
23458
+ {
23459
+ id: "sentry",
23460
+ name: "Sentry",
23461
+ category: "Error tracking",
23462
+ hostSuffixes: ["sentry.io"],
23463
+ apiBase: "https://sentry.io",
23464
+ defaultDataClasses: ["source", "telemetry"],
23465
+ sdks: {
23466
+ npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
23467
+ pypi: ["sentry-sdk"],
23468
+ go: ["github.com/getsentry/sentry-go"],
23469
+ maven: ["io.sentry"],
23470
+ rubygems: ["sentry-ruby"],
23471
+ cargo: ["sentry"],
23472
+ composer: ["sentry/sentry"],
23473
+ nuget: ["Sentry"]
23474
+ }
23475
+ },
23476
+ {
23477
+ id: "openai",
23478
+ name: "OpenAI",
23479
+ category: "LLM provider",
23480
+ hostSuffixes: ["openai.com"],
23481
+ apiBase: "https://api.openai.com",
23482
+ defaultDataClasses: ["pii", "source"],
23483
+ sdks: {
23484
+ npm: ["openai"],
23485
+ pypi: ["openai"],
23486
+ go: ["github.com/sashabaranov/go-openai"],
23487
+ maven: ["com.openai"],
23488
+ rubygems: ["ruby-openai"],
23489
+ cargo: ["async-openai"],
23490
+ composer: ["openai-php/client"],
23491
+ nuget: ["OpenAI"]
23492
+ }
23493
+ },
23494
+ {
23495
+ id: "anthropic",
23496
+ name: "Anthropic",
23497
+ category: "LLM provider",
23498
+ hostSuffixes: ["anthropic.com"],
23499
+ apiBase: "https://api.anthropic.com",
23500
+ defaultDataClasses: ["pii", "source"],
23501
+ sdks: {
23502
+ npm: ["@anthropic-ai/sdk"],
23503
+ pypi: ["anthropic"],
23504
+ go: ["github.com/anthropics/anthropic-sdk-go"],
23505
+ nuget: ["Anthropic.SDK"]
23506
+ }
23507
+ },
23508
+ {
23509
+ id: "aws",
23510
+ name: "Amazon Web Services",
23511
+ category: "Cloud platform",
23512
+ hostSuffixes: ["amazonaws.com"],
23513
+ apiBase: "https://s3.amazonaws.com",
23514
+ defaultDataClasses: ["secrets", "customer"],
23515
+ sdks: {
23516
+ npm: ["@aws-sdk/client-s3", "aws-sdk"],
23517
+ pypi: ["boto3"],
23518
+ go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
23519
+ maven: ["com.amazonaws", "software.amazon.awssdk"],
23520
+ rubygems: ["aws-sdk-s3"],
23521
+ cargo: ["aws-sdk-s3"],
23522
+ nuget: ["AWSSDK.S3"]
23523
+ }
23524
+ },
23525
+ {
23526
+ id: "gcp",
23527
+ name: "Google Cloud",
23528
+ category: "Cloud platform",
23529
+ hostSuffixes: ["googleapis.com"],
23530
+ apiBase: "https://storage.googleapis.com",
23531
+ defaultDataClasses: ["customer", "logs"],
23532
+ sdks: {
23533
+ npm: ["@google-cloud/storage"],
23534
+ pypi: ["google-cloud-storage"],
23535
+ go: ["cloud.google.com/go"],
23536
+ maven: ["com.google.cloud"],
23537
+ rubygems: ["google-cloud-storage"],
23538
+ nuget: ["Google.Cloud.Storage.V1"]
23539
+ }
23540
+ },
23541
+ {
23542
+ id: "azure",
23543
+ name: "Microsoft Azure",
23544
+ category: "Cloud platform",
23545
+ hostSuffixes: ["azure.com", "windows.net"],
23546
+ apiBase: "https://management.azure.com",
23547
+ defaultDataClasses: ["customer", "logs"],
23548
+ sdks: {
23549
+ npm: ["@azure/storage-blob"],
23550
+ pypi: ["azure-storage-blob"],
23551
+ go: ["github.com/Azure/azure-sdk-for-go"],
23552
+ maven: ["com.azure"],
23553
+ rubygems: ["azure-storage-blob"],
23554
+ nuget: ["Azure.Storage.Blobs"]
23555
+ }
23556
+ },
23557
+ {
23558
+ id: "slack",
23559
+ name: "Slack",
23560
+ category: "Notifications",
23561
+ hostSuffixes: ["slack.com"],
23562
+ apiBase: "https://slack.com/api",
23563
+ defaultDataClasses: ["logs"],
23564
+ sdks: {
23565
+ npm: ["@slack/web-api"],
23566
+ pypi: ["slack-sdk"],
23567
+ go: ["github.com/slack-go/slack"],
23568
+ maven: ["com.slack.api"],
23569
+ rubygems: ["slack-ruby-client"],
23570
+ composer: ["slack-php/slack-api"],
23571
+ nuget: ["SlackNet"]
23572
+ }
23573
+ },
23574
+ {
23575
+ id: "segment",
23576
+ name: "Segment",
23577
+ category: "Analytics",
23578
+ hostSuffixes: ["segment.io", "segment.com"],
23579
+ apiBase: "https://api.segment.io",
23580
+ defaultDataClasses: ["customer"],
23581
+ sdks: {
23582
+ npm: ["@segment/analytics-node", "analytics-node"],
23583
+ pypi: ["segment-analytics-python"],
23584
+ go: ["github.com/segmentio/analytics-go"],
23585
+ maven: ["com.segment.analytics.java"],
23586
+ rubygems: ["analytics-ruby"],
23587
+ nuget: ["Analytics"]
23588
+ }
23589
+ },
23590
+ {
23591
+ id: "twilio",
23592
+ name: "Twilio",
23593
+ category: "Communications",
23594
+ hostSuffixes: ["twilio.com"],
23595
+ apiBase: "https://api.twilio.com",
23596
+ defaultDataClasses: ["pii", "customer"],
23597
+ sdks: {
23598
+ npm: ["twilio"],
23599
+ pypi: ["twilio"],
23600
+ go: ["github.com/twilio/twilio-go"],
23601
+ maven: ["com.twilio.sdk"],
23602
+ rubygems: ["twilio-ruby"],
23603
+ composer: ["twilio/sdk"],
23604
+ nuget: ["Twilio"]
23605
+ }
23606
+ },
23607
+ {
23608
+ id: "sendgrid",
23609
+ name: "SendGrid",
23610
+ category: "Email",
23611
+ hostSuffixes: ["sendgrid.com"],
23612
+ apiBase: "https://api.sendgrid.com",
23613
+ defaultDataClasses: ["pii"],
23614
+ sdks: {
23615
+ npm: ["@sendgrid/mail"],
23616
+ pypi: ["sendgrid"],
23617
+ go: ["github.com/sendgrid/sendgrid-go"],
23618
+ maven: ["com.sendgrid"],
23619
+ rubygems: ["sendgrid-ruby"],
23620
+ composer: ["sendgrid/sendgrid"],
23621
+ nuget: ["SendGrid"]
23622
+ }
23623
+ },
23624
+ {
23625
+ id: "mailgun",
23626
+ name: "Mailgun",
23627
+ category: "Email",
23628
+ hostSuffixes: ["mailgun.net"],
23629
+ apiBase: "https://api.mailgun.net",
23630
+ defaultDataClasses: ["pii"],
23631
+ sdks: {
23632
+ npm: ["mailgun.js"],
23633
+ pypi: ["mailgun"],
23634
+ rubygems: ["mailgun-ruby"],
23635
+ composer: ["mailgun/mailgun-php"],
23636
+ nuget: ["Mailgun"]
23637
+ }
23638
+ },
23639
+ {
23640
+ id: "mixpanel",
23641
+ name: "Mixpanel",
23642
+ category: "Analytics",
23643
+ hostSuffixes: ["mixpanel.com"],
23644
+ apiBase: "https://api.mixpanel.com",
23645
+ defaultDataClasses: ["customer", "telemetry"],
23646
+ sdks: {
23647
+ npm: ["mixpanel"],
23648
+ pypi: ["mixpanel"],
23649
+ rubygems: ["mixpanel-ruby"],
23650
+ nuget: ["Mixpanel"]
23651
+ }
23652
+ },
23653
+ {
23654
+ id: "amplitude",
23655
+ name: "Amplitude",
23656
+ category: "Analytics",
23657
+ hostSuffixes: ["amplitude.com"],
23658
+ apiBase: "https://api2.amplitude.com",
23659
+ defaultDataClasses: ["customer", "telemetry"],
23660
+ sdks: {
23661
+ npm: ["@amplitude/analytics-node"],
23662
+ pypi: ["amplitude-analytics"],
23663
+ nuget: ["Amplitude"]
23664
+ }
23665
+ },
23666
+ {
23667
+ id: "posthog",
23668
+ name: "PostHog",
23669
+ category: "Analytics",
23670
+ hostSuffixes: ["posthog.com"],
23671
+ apiBase: "https://us.i.posthog.com",
23672
+ defaultDataClasses: ["customer", "telemetry"],
23673
+ sdks: {
23674
+ npm: ["posthog-node", "posthog-js"],
23675
+ pypi: ["posthog"],
23676
+ go: ["github.com/posthog/posthog-go"],
23677
+ rubygems: ["posthog-ruby"],
23678
+ composer: ["posthog/posthog-php"],
23679
+ nuget: ["PostHog"]
23680
+ }
23681
+ },
23682
+ {
23683
+ id: "honeycomb",
23684
+ name: "Honeycomb",
23685
+ category: "Observability",
23686
+ hostSuffixes: ["honeycomb.io"],
23687
+ apiBase: "https://api.honeycomb.io",
23688
+ defaultDataClasses: ["telemetry", "metrics"],
23689
+ sdks: {
23690
+ npm: ["libhoney"],
23691
+ pypi: ["libhoney"],
23692
+ go: ["github.com/honeycombio/libhoney-go"],
23693
+ rubygems: ["libhoney"]
23694
+ }
23695
+ },
23696
+ {
23697
+ id: "grafana",
23698
+ name: "Grafana Cloud",
23699
+ category: "Observability",
23700
+ hostSuffixes: ["grafana.net"],
23701
+ apiBase: "https://grafana.net",
23702
+ defaultDataClasses: ["logs", "metrics"],
23703
+ sdks: {
23704
+ npm: ["@grafana/faro-web-sdk"]
23705
+ }
23706
+ },
23707
+ {
23708
+ id: "splunk",
23709
+ name: "Splunk",
23710
+ category: "Observability",
23711
+ hostSuffixes: ["splunkcloud.com", "splunk.com"],
23712
+ apiBase: "https://http-inputs.splunkcloud.com",
23713
+ defaultDataClasses: ["logs"],
23714
+ sdks: {
23715
+ npm: ["splunk-logging"],
23716
+ pypi: ["splunk-sdk"],
23717
+ maven: ["com.splunk"],
23718
+ nuget: ["Splunk.Logging.Common"]
23719
+ }
23720
+ },
23721
+ {
23722
+ id: "pagerduty",
23723
+ name: "PagerDuty",
23724
+ category: "Incident response",
23725
+ hostSuffixes: ["pagerduty.com"],
23726
+ apiBase: "https://api.pagerduty.com",
23727
+ defaultDataClasses: ["logs"],
23728
+ sdks: {
23729
+ npm: ["@pagerduty/pdjs"],
23730
+ pypi: ["pdpyras"],
23731
+ go: ["github.com/PagerDuty/go-pagerduty"],
23732
+ rubygems: ["pagerduty"]
23733
+ }
23734
+ },
23735
+ {
23736
+ id: "github",
23737
+ name: "GitHub",
23738
+ category: "Developer platform",
23739
+ hostSuffixes: ["github.com", "githubusercontent.com"],
23740
+ apiBase: "https://api.github.com",
23741
+ defaultDataClasses: ["source"],
23742
+ sdks: {
23743
+ npm: ["@octokit/rest", "octokit"],
23744
+ pypi: ["pygithub"],
23745
+ go: ["github.com/google/go-github"],
23746
+ maven: ["org.kohsuke.github-api"],
23747
+ rubygems: ["octokit"],
23748
+ cargo: ["octocrab"],
23749
+ composer: ["knplabs/github-api"],
23750
+ nuget: ["Octokit"]
23751
+ }
23752
+ },
23753
+ {
23754
+ id: "gitlab",
23755
+ name: "GitLab",
23756
+ category: "Developer platform",
23757
+ hostSuffixes: ["gitlab.com"],
23758
+ apiBase: "https://gitlab.com/api",
23759
+ defaultDataClasses: ["source"],
23760
+ sdks: {
23761
+ npm: ["@gitbeaker/rest"],
23762
+ pypi: ["python-gitlab"],
23763
+ go: ["gitlab.com/gitlab-org/api/client-go"],
23764
+ rubygems: ["gitlab"],
23765
+ nuget: ["GitLabApiClient"]
23766
+ }
23767
+ },
23768
+ {
23769
+ id: "auth0",
23770
+ name: "Auth0",
23771
+ category: "Identity",
23772
+ hostSuffixes: ["auth0.com"],
23773
+ apiBase: "https://login.auth0.com",
23774
+ defaultDataClasses: ["pii"],
23775
+ sdks: {
23776
+ npm: ["auth0"],
23777
+ pypi: ["auth0-python"],
23778
+ go: ["github.com/auth0/go-auth0"],
23779
+ maven: ["com.auth0"],
23780
+ rubygems: ["auth0"],
23781
+ composer: ["auth0/auth0-php"],
23782
+ nuget: ["Auth0.ManagementApi"]
23783
+ }
23784
+ },
23785
+ {
23786
+ id: "okta",
23787
+ name: "Okta",
23788
+ category: "Identity",
23789
+ hostSuffixes: ["okta.com", "oktapreview.com"],
23790
+ apiBase: "https://login.okta.com",
23791
+ defaultDataClasses: ["pii"],
23792
+ sdks: {
23793
+ npm: ["@okta/okta-sdk-nodejs"],
23794
+ pypi: ["okta"],
23795
+ go: ["github.com/okta/okta-sdk-golang"],
23796
+ maven: ["com.okta.sdk"],
23797
+ nuget: ["Okta.Sdk"]
23798
+ }
23799
+ },
23800
+ {
23801
+ id: "clerk",
23802
+ name: "Clerk",
23803
+ category: "Identity",
23804
+ hostSuffixes: ["clerk.com", "clerk.dev"],
23805
+ apiBase: "https://api.clerk.com",
23806
+ defaultDataClasses: ["pii"],
23807
+ sdks: {
23808
+ npm: ["@clerk/backend", "@clerk/nextjs"],
23809
+ pypi: ["clerk-backend-api"],
23810
+ go: ["github.com/clerk/clerk-sdk-go"]
23811
+ }
23812
+ },
23813
+ {
23814
+ id: "supabase",
23815
+ name: "Supabase",
23816
+ category: "Backend platform",
23817
+ hostSuffixes: ["supabase.co", "supabase.com"],
23818
+ apiBase: "https://api.supabase.com",
23819
+ defaultDataClasses: ["pii", "customer"],
23820
+ sdks: {
23821
+ npm: ["@supabase/supabase-js"],
23822
+ pypi: ["supabase"],
23823
+ cargo: ["postgrest"]
23824
+ }
23825
+ },
23826
+ {
23827
+ id: "firebase",
23828
+ name: "Firebase",
23829
+ category: "Backend platform",
23830
+ hostSuffixes: ["firebaseio.com", "firebase.google.com"],
23831
+ apiBase: "https://firebaseio.com",
23832
+ defaultDataClasses: ["customer"],
23833
+ sdks: {
23834
+ npm: ["firebase", "firebase-admin"],
23835
+ pypi: ["firebase-admin"],
23836
+ go: ["firebase.google.com/go"],
23837
+ maven: ["com.google.firebase"]
23838
+ }
23839
+ },
23840
+ {
23841
+ id: "mongodb-atlas",
23842
+ name: "MongoDB Atlas",
23843
+ category: "Database SaaS",
23844
+ hostSuffixes: ["mongodb.net", "mongodb.com"],
23845
+ apiBase: "https://cloud.mongodb.com",
23846
+ defaultDataClasses: ["customer"],
23847
+ sdks: {
23848
+ npm: ["mongodb"],
23849
+ pypi: ["pymongo"],
23850
+ go: ["go.mongodb.org/mongo-driver"],
23851
+ maven: ["org.mongodb"],
23852
+ rubygems: ["mongo"],
23853
+ cargo: ["mongodb"],
23854
+ nuget: ["MongoDB.Driver"]
23855
+ }
23856
+ },
23857
+ {
23858
+ id: "planetscale",
23859
+ name: "PlanetScale",
23860
+ category: "Database SaaS",
23861
+ hostSuffixes: ["psdb.cloud", "planetscale.com"],
23862
+ apiBase: "https://api.planetscale.com",
23863
+ defaultDataClasses: ["customer"],
23864
+ sdks: {
23865
+ npm: ["@planetscale/database"],
23866
+ go: ["github.com/planetscale/planetscale-go"]
23867
+ }
23868
+ },
23869
+ {
23870
+ id: "algolia",
23871
+ name: "Algolia",
23872
+ category: "Search SaaS",
23873
+ hostSuffixes: ["algolia.net", "algolianet.com"],
23874
+ apiBase: "https://algolia.net",
23875
+ defaultDataClasses: ["customer"],
23876
+ sdks: {
23877
+ npm: ["algoliasearch"],
23878
+ pypi: ["algoliasearch"],
23879
+ go: ["github.com/algolia/algoliasearch-client-go"],
23880
+ maven: ["com.algolia"],
23881
+ rubygems: ["algolia"],
23882
+ composer: ["algolia/algoliasearch-client-php"],
23883
+ nuget: ["Algolia.Search"]
23884
+ }
23885
+ },
23886
+ {
23887
+ id: "cloudflare",
23888
+ name: "Cloudflare",
23889
+ category: "CDN / edge",
23890
+ hostSuffixes: ["cloudflare.com", "workers.dev"],
23891
+ apiBase: "https://api.cloudflare.com",
23892
+ defaultDataClasses: ["logs"],
23893
+ sdks: {
23894
+ npm: ["cloudflare"],
23895
+ pypi: ["cloudflare"],
23896
+ go: ["github.com/cloudflare/cloudflare-go"],
23897
+ nuget: ["CloudFlare.Client"]
23898
+ }
23899
+ },
23900
+ {
23901
+ id: "huggingface",
23902
+ name: "Hugging Face",
23903
+ category: "LLM provider",
23904
+ hostSuffixes: ["huggingface.co"],
23905
+ apiBase: "https://api-inference.huggingface.co",
23906
+ defaultDataClasses: ["source"],
23907
+ sdks: {
23908
+ npm: ["@huggingface/inference"],
23909
+ pypi: ["huggingface-hub", "transformers"],
23910
+ rubygems: ["hugging-face"]
23911
+ }
23912
+ },
23913
+ {
23914
+ id: "cohere",
23915
+ name: "Cohere",
23916
+ category: "LLM provider",
23917
+ hostSuffixes: ["cohere.com", "cohere.ai"],
23918
+ apiBase: "https://api.cohere.com",
23919
+ defaultDataClasses: ["pii", "source"],
23920
+ sdks: {
23921
+ npm: ["cohere-ai"],
23922
+ pypi: ["cohere"],
23923
+ go: ["github.com/cohere-ai/cohere-go"]
23924
+ }
23925
+ },
23926
+ {
23927
+ id: "mistral",
23928
+ name: "Mistral AI",
23929
+ category: "LLM provider",
23930
+ hostSuffixes: ["mistral.ai"],
23931
+ apiBase: "https://api.mistral.ai",
23932
+ defaultDataClasses: ["pii", "source"],
23933
+ sdks: {
23934
+ npm: ["@mistralai/mistralai"],
23935
+ pypi: ["mistralai"],
23936
+ go: ["github.com/gage-technologies/mistral-go"]
23937
+ }
23938
+ }
23939
+ ];
23940
+ var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
23941
+ ${JSON.stringify(PROVIDER_REGISTRY)}`;
23942
+
23943
+ // ../../packages/detections/src/egress/extract.ts
23944
+ var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
23945
+ var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
23946
+ var SECRET_VALUE = new RegExp(
23947
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
23948
+ "gi"
23949
+ );
23950
+ var AUTH_SCHEME_VALUE = new RegExp(
23951
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
23952
+ "gi"
23953
+ );
23954
+ var WEBHOOK_SECRET_PATHS = [
23955
+ { hosts: ["hooks.slack.com"], prefix: "/services/" },
23956
+ {
23957
+ hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
23958
+ prefix: "/api/webhooks/"
23959
+ },
23960
+ { hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
23961
+ { hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
23962
+ ];
23963
+ function escapeRegExp(literal2) {
23964
+ return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23965
+ }
23966
+ var WEBHOOK_URL = new RegExp(
23967
+ `(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
23968
+ (entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
23969
+ ).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
23970
+ "gi"
23971
+ );
23972
+
22953
23973
  // ../../packages/detections/src/escape-regexp.ts
22954
- function escapeRegExp(value) {
23974
+ function escapeRegExp2(value) {
22955
23975
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22956
23976
  }
22957
23977
 
22958
23978
  // ../../packages/detections/src/matchers/limits.ts
22959
23979
  var MAX_MATCHES_PER_RULE = 1e4;
23980
+ var MAX_REGEX_INPUT_LENGTH = 2e5;
22960
23981
 
22961
23982
  // ../../packages/detections/src/matchers/keyword.ts
22962
23983
  var KeywordMatcher2 = class {
@@ -22967,7 +23988,7 @@ var KeywordMatcher2 = class {
22967
23988
  for (const kw of keywords) {
22968
23989
  if (kw.length === 0) continue;
22969
23990
  if (spans.length >= MAX_MATCHES_PER_RULE) break;
22970
- const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
23991
+ const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
22971
23992
  let m;
22972
23993
  while ((m = re.exec(text)) !== null) {
22973
23994
  spans.push({ start: m.index, end: m.index + m[0].length });
@@ -22984,9 +24005,13 @@ var RegexMatcher2 = class {
22984
24005
  if (rule.matcher.type !== "regex") return [];
22985
24006
  const { pattern, flags, captureGroup } = rule.matcher;
22986
24007
  const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
24008
+ const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
22987
24009
  const spans = [];
22988
24010
  let m;
22989
- while ((m = re.exec(text)) !== null) {
24011
+ const maxIterations = scanText2.length + 1;
24012
+ let iterations = 0;
24013
+ while ((m = re.exec(scanText2)) !== null) {
24014
+ if (++iterations > maxIterations) break;
22990
24015
  const group = captureGroup != null ? m[captureGroup] : m[0];
22991
24016
  if (m[0].length === 0) re.lastIndex++;
22992
24017
  if (group && spans.length < MAX_MATCHES_PER_RULE) {
@@ -23061,6 +24086,31 @@ var CONFIG_POSTURE_RULES = [
23061
24086
  }
23062
24087
  ];
23063
24088
 
24089
+ // ../../packages/detections/src/security/redos-probe.ts
24090
+ var EXPONENTIAL_UNITS = [
24091
+ "a",
24092
+ "0",
24093
+ " ",
24094
+ "x",
24095
+ "ab",
24096
+ "a.",
24097
+ "a-",
24098
+ "a_",
24099
+ "a@",
24100
+ "a/",
24101
+ "a:",
24102
+ "a=",
24103
+ "a;",
24104
+ "aA0",
24105
+ " "
24106
+ ];
24107
+ var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
24108
+ (unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
24109
+ );
24110
+ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
24111
+ (unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
24112
+ );
24113
+
23064
24114
  // ../../rules/code-flaws/auth-jwt-no-verify.json
23065
24115
  var auth_jwt_no_verify_default = {
23066
24116
  specVersion: 1,
@@ -25103,10 +26153,14 @@ import { arch, hostname as hostname3, platform, release } from "os";
25103
26153
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25104
26154
  import { join as join8 } from "path";
25105
26155
 
26156
+ // ../../packages/plugin-sdk/src/paths.ts
26157
+ import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
26158
+ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
26159
+
25106
26160
  // ../../packages/plugin-sdk/src/project-files.ts
25107
26161
  var import_ignore = __toESM(require_ignore(), 1);
25108
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25109
- import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
26162
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
26163
+ import { basename as basename4, join as join9, relative, sep as sep4 } from "path";
25110
26164
 
25111
26165
  // ../../packages/plugin-sdk/src/runtime.ts
25112
26166
  import { randomUUID as randomUUID10 } from "crypto";
@@ -25387,6 +26441,13 @@ var StandaloneDataGateway = class {
25387
26441
  this.db.scanLedger.upsertEntries(entries);
25388
26442
  return Promise.resolve();
25389
26443
  }
26444
+ getRuleProbeVerdict(ruleKey) {
26445
+ return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
26446
+ }
26447
+ setRuleProbeVerdict(ruleKey, verdict, worstProbeMs) {
26448
+ this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs);
26449
+ return Promise.resolve();
26450
+ }
25390
26451
  openAtRestKeysForPath(path) {
25391
26452
  return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
25392
26453
  }
@@ -25397,6 +26458,12 @@ var StandaloneDataGateway = class {
25397
26458
  this.db.resolutions.insertResolution(input);
25398
26459
  return Promise.resolve();
25399
26460
  }
26461
+ // Bare forward — no toggle read here. The plugin-path kill-switch is
26462
+ // enforced by the caller, which already holds the parsed workspace
26463
+ // settings; this class only ever sees `dataDir`, not the settings base.
26464
+ recordProjectEgress(input) {
26465
+ return Promise.resolve(this.db.shares.recordProjectEgress(input));
26466
+ }
25400
26467
  close() {
25401
26468
  this.db.close();
25402
26469
  return Promise.resolve();
@@ -25507,8 +26574,8 @@ function table(headers, rows, opts = {}) {
25507
26574
  const widths = headers.map(
25508
26575
  (h, i) => Math.max(visibleLength(h), ...rows.map((r) => visibleLength(r[i] ?? "")))
25509
26576
  );
25510
- const sep4 = " ".repeat(gap);
25511
- const fmt = (cells) => cells.map((cell, i) => padEnd(cell, widths[i] ?? 0)).join(sep4);
26577
+ const sep5 = " ".repeat(gap);
26578
+ const fmt = (cells) => cells.map((cell, i) => padEnd(cell, widths[i] ?? 0)).join(sep5);
25512
26579
  const headerLine = fmt(headers.map((h) => h.toUpperCase()));
25513
26580
  if (opts.rowSep === true) {
25514
26581
  const fullWidth = widths.reduce((n, w) => n + w, 0) + gap * Math.max(0, widths.length - 1);
@@ -25520,7 +26587,7 @@ function table(headers, rows, opts = {}) {
25520
26587
  });
25521
26588
  return [headerLine, rule, ...body].join("\n");
25522
26589
  }
25523
- const ruleLine = widths.map((w) => "\u2500".repeat(w)).join(sep4);
26590
+ const ruleLine = widths.map((w) => "\u2500".repeat(w)).join(sep5);
25524
26591
  return [headerLine, ruleLine, ...rows.map(fmt)].join("\n");
25525
26592
  }
25526
26593
  function fenced(body) {
@@ -25530,7 +26597,7 @@ function fenced(body) {
25530
26597
  }
25531
26598
 
25532
26599
  // src/command-registry.ts
25533
- import { readdirSync as readdirSync3 } from "fs";
26600
+ import { readdirSync as readdirSync4 } from "fs";
25534
26601
  import { fileURLToPath } from "url";
25535
26602
  var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
25536
26603
 
@@ -25613,14 +26680,14 @@ function renderStatusBar(s, opts = {}) {
25613
26680
  const unreviewed = `unreviewed ${SHADE.full}${String(u.critical)} ${SHADE.dark}${String(u.high)} ${SHADE.medium}${String(u.medium)} ${SHADE.light}${String(u.low)}`;
25614
26681
  return `\u25B8\u25B8 AKA health ${String(s.score)}/100 ${unreviewed} \u2691 ${String(s.openFindings)} open findings`;
25615
26682
  }
25616
- const sep4 = ` ${paint.dim("\u2502")} `;
26683
+ const sep5 = ` ${paint.dim("\u2502")} `;
25617
26684
  const sq = "\u25A0";
25618
26685
  const dot = s.score >= 80 ? paint.ok("\u25CF") : s.score >= 50 ? paint.high("\u25CF") : paint.critical("\u25CF");
25619
26686
  const score = `${dot} health ${paint.bold(String(s.score))}${paint.dim("/100")}`;
25620
26687
  const tally = `${paint.dim("unreviewed")} ${paint.critical(sq)}${String(u.critical)} ${paint.high(sq)}${String(u.high)} ${paint.medium(sq)}${String(u.medium)} ${paint.low(sq)}${String(u.low)}`;
25621
26688
  const flag = s.openFindings > 0 ? paint.critical("\u2691") : paint.dim("\u2691");
25622
26689
  const open = `${flag} ${String(s.openFindings)} open findings`;
25623
- return `${paint.brand("\u25B8\u25B8 AKA")}${sep4}${score}${sep4}${tally}${sep4}${open}`;
26690
+ return `${paint.brand("\u25B8\u25B8 AKA")}${sep5}${score}${sep5}${tally}${sep5}${open}`;
25624
26691
  }
25625
26692
  function findingStatus(summary) {
25626
26693
  return {