@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/.claude-plugin/plugin.json +1 -1
- package/commands/setup.md +11 -10
- package/package.json +4 -4
- package/scripts/apply-suppressions.js +1235 -181
- package/scripts/backfill.js +1385 -174
- package/scripts/filescan.js +2641 -333
- package/scripts/firstrun.js +1241 -174
- package/scripts/intro.js +820 -147
- package/scripts/onboard.js +1224 -170
- package/scripts/post-tool-use.js +1405 -179
- package/scripts/pre-tool-use.js +1405 -179
- package/scripts/query.js +1242 -175
- package/scripts/reconcile.js +1238 -171
- package/scripts/remediate.js +1390 -179
- package/scripts/session-start.js +1263 -186
- package/scripts/start-light.js +823 -150
- package/scripts/statusline.js +1255 -178
- package/scripts/stop.js +837 -154
- package/scripts/user-prompt-submit.js +1405 -179
package/scripts/filescan.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
|
|
|
@@ -16197,6 +16201,7 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16197
16201
|
|
|
16198
16202
|
// ../../packages/schema/src/zod/rule.ts
|
|
16199
16203
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
16204
|
+
var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
|
|
16200
16205
|
var KeywordMatcher = external_exports.object({
|
|
16201
16206
|
type: external_exports.literal("keyword"),
|
|
16202
16207
|
// An empty keyword matches at every position, yielding one zero-length span
|
|
@@ -16221,9 +16226,10 @@ function matchesEmptyString(pattern, flags) {
|
|
|
16221
16226
|
return false;
|
|
16222
16227
|
}
|
|
16223
16228
|
}
|
|
16229
|
+
var MAX_PATTERN_LENGTH = 2e3;
|
|
16224
16230
|
var RegexMatcher = external_exports.object({
|
|
16225
16231
|
type: external_exports.literal("regex"),
|
|
16226
|
-
pattern: external_exports.string(),
|
|
16232
|
+
pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
|
|
16227
16233
|
flags: external_exports.string().default("gi"),
|
|
16228
16234
|
captureGroup: external_exports.number().int().nonnegative().optional()
|
|
16229
16235
|
}).refine((v) => isValidRegex(v.pattern, v.flags), {
|
|
@@ -16790,6 +16796,212 @@ function buildDetectionsList(summaries, query) {
|
|
|
16790
16796
|
return { counts, items: filtered.map(summaryToDetectionListItem) };
|
|
16791
16797
|
}
|
|
16792
16798
|
|
|
16799
|
+
// ../../packages/schema/src/zod/shares.ts
|
|
16800
|
+
var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
|
|
16801
|
+
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
|
|
16802
|
+
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
16803
|
+
var DATA_CLASS_ORDER = DataClass.options;
|
|
16804
|
+
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
16805
|
+
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
16806
|
+
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
16807
|
+
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
16808
|
+
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
|
|
16809
|
+
var ReviewInfo = external_exports.object({
|
|
16810
|
+
needsReview: external_exports.boolean(),
|
|
16811
|
+
reasons: external_exports.array(ReviewReason)
|
|
16812
|
+
}).meta({ id: "ReviewInfo" });
|
|
16813
|
+
var DestinationNetwork = external_exports.object({
|
|
16814
|
+
port: external_exports.number().int().nullable(),
|
|
16815
|
+
geo: external_exports.string().nullable(),
|
|
16816
|
+
ptr: external_exports.string().nullable()
|
|
16817
|
+
}).meta({ id: "DestinationNetwork" });
|
|
16818
|
+
var EndpointSummary = external_exports.object({
|
|
16819
|
+
id: external_exports.string(),
|
|
16820
|
+
method: HttpMethod,
|
|
16821
|
+
transport: Transport,
|
|
16822
|
+
url: external_exports.string(),
|
|
16823
|
+
template: external_exports.boolean(),
|
|
16824
|
+
dataClass: DataClass,
|
|
16825
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16826
|
+
callSiteCount: external_exports.number().int().nonnegative()
|
|
16827
|
+
}).meta({ id: "EndpointSummary" });
|
|
16828
|
+
var CallSite = external_exports.object({
|
|
16829
|
+
id: external_exports.string(),
|
|
16830
|
+
project: external_exports.string(),
|
|
16831
|
+
file: external_exports.string(),
|
|
16832
|
+
line: external_exports.number().int().nonnegative(),
|
|
16833
|
+
snippet: external_exports.string(),
|
|
16834
|
+
dynamic: external_exports.boolean(),
|
|
16835
|
+
vendored: external_exports.boolean(),
|
|
16836
|
+
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
16837
|
+
projectId: external_exports.string().nullable()
|
|
16838
|
+
}).meta({ id: "CallSite" });
|
|
16839
|
+
var EndpointWithSites = EndpointSummary.extend({
|
|
16840
|
+
sites: external_exports.array(CallSite)
|
|
16841
|
+
}).meta({ id: "EndpointWithSites" });
|
|
16842
|
+
var ShareDestinationSummary = external_exports.object({
|
|
16843
|
+
id: external_exports.string(),
|
|
16844
|
+
kind: DestinationKind,
|
|
16845
|
+
name: external_exports.string(),
|
|
16846
|
+
host: external_exports.string(),
|
|
16847
|
+
category: external_exports.string(),
|
|
16848
|
+
trust: ShareTrustLevel,
|
|
16849
|
+
/** Effective state (decision applied over the trust default). */
|
|
16850
|
+
status: EgressStatus,
|
|
16851
|
+
/** True when an egress decision override differs from the trust default. */
|
|
16852
|
+
isCustom: external_exports.boolean(),
|
|
16853
|
+
lastSeen: external_exports.iso.datetime(),
|
|
16854
|
+
endpointCount: external_exports.number().int().nonnegative(),
|
|
16855
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
16856
|
+
transports: external_exports.array(Transport),
|
|
16857
|
+
/** Most-sensitive first. */
|
|
16858
|
+
dataClasses: external_exports.array(DataClass),
|
|
16859
|
+
review: ReviewInfo,
|
|
16860
|
+
/** Non-provider hosts only; null for providers. */
|
|
16861
|
+
network: DestinationNetwork.nullable(),
|
|
16862
|
+
/** Embedded for inline expansion — no call sites here. */
|
|
16863
|
+
endpoints: external_exports.array(EndpointSummary)
|
|
16864
|
+
}).meta({ id: "ShareDestinationSummary" });
|
|
16865
|
+
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
16866
|
+
endpointCount: true,
|
|
16867
|
+
callSiteCount: true,
|
|
16868
|
+
endpoints: true
|
|
16869
|
+
}).extend({
|
|
16870
|
+
/** Ownership/geo rationale; null for providers. */
|
|
16871
|
+
note: external_exports.string().nullable(),
|
|
16872
|
+
endpoints: external_exports.array(EndpointWithSites)
|
|
16873
|
+
}).meta({ id: "ShareDestinationDetail" });
|
|
16874
|
+
var ReviewDestination = external_exports.object({
|
|
16875
|
+
id: external_exports.string(),
|
|
16876
|
+
kind: DestinationKind,
|
|
16877
|
+
name: external_exports.string(),
|
|
16878
|
+
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
16879
|
+
host: external_exports.string(),
|
|
16880
|
+
trust: ShareTrustLevel,
|
|
16881
|
+
status: EgressStatus,
|
|
16882
|
+
review: ReviewInfo,
|
|
16883
|
+
topDataClass: DataClass,
|
|
16884
|
+
callSiteCount: external_exports.number().int().nonnegative(),
|
|
16885
|
+
lastSeen: external_exports.iso.datetime()
|
|
16886
|
+
}).meta({ id: "ReviewDestination" });
|
|
16887
|
+
var ShareDestinationGroup = external_exports.object({
|
|
16888
|
+
kind: DestinationKind,
|
|
16889
|
+
total: external_exports.number().int().nonnegative(),
|
|
16890
|
+
items: external_exports.array(ShareDestinationSummary)
|
|
16891
|
+
}).meta({ id: "ShareDestinationGroup" });
|
|
16892
|
+
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
16893
|
+
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
16894
|
+
var SharesStats = external_exports.object({
|
|
16895
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
16896
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
16897
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
16898
|
+
needsReview: external_exports.number().int().nonnegative(),
|
|
16899
|
+
insecure: external_exports.number().int().nonnegative(),
|
|
16900
|
+
byKind: external_exports.object({
|
|
16901
|
+
provider: external_exports.number().int().nonnegative(),
|
|
16902
|
+
internal: external_exports.number().int().nonnegative(),
|
|
16903
|
+
external: external_exports.number().int().nonnegative(),
|
|
16904
|
+
ip: external_exports.number().int().nonnegative()
|
|
16905
|
+
}),
|
|
16906
|
+
byTrust: external_exports.object({
|
|
16907
|
+
recognized: external_exports.number().int().nonnegative(),
|
|
16908
|
+
internal: external_exports.number().int().nonnegative(),
|
|
16909
|
+
unverified: external_exports.number().int().nonnegative(),
|
|
16910
|
+
ip: external_exports.number().int().nonnegative()
|
|
16911
|
+
})
|
|
16912
|
+
}).meta({ id: "SharesStats" });
|
|
16913
|
+
var SetEgressDecisionBody = external_exports.object({
|
|
16914
|
+
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
16915
|
+
decision: EgressDecision.nullable()
|
|
16916
|
+
}).meta({ id: "SetEgressDecisionBody" });
|
|
16917
|
+
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
16918
|
+
var ListShareDestinationsQuery = external_exports.object({
|
|
16919
|
+
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
16920
|
+
q: external_exports.string().optional(),
|
|
16921
|
+
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
16922
|
+
kind: external_exports.array(DestinationKind).optional(),
|
|
16923
|
+
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
16924
|
+
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
16925
|
+
/**
|
|
16926
|
+
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
16927
|
+
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
16928
|
+
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
16929
|
+
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
16930
|
+
*/
|
|
16931
|
+
review: external_exports.stringbool().default(false)
|
|
16932
|
+
});
|
|
16933
|
+
var ExportSharesQuery = external_exports.object({
|
|
16934
|
+
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
16935
|
+
q: external_exports.string().optional(),
|
|
16936
|
+
kind: external_exports.array(DestinationKind).optional()
|
|
16937
|
+
});
|
|
16938
|
+
|
|
16939
|
+
// ../../packages/schema/src/zod/egress-extraction.ts
|
|
16940
|
+
var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
|
|
16941
|
+
var ProviderRegistryEntry = external_exports.object({
|
|
16942
|
+
id: external_exports.string(),
|
|
16943
|
+
name: external_exports.string(),
|
|
16944
|
+
category: external_exports.string(),
|
|
16945
|
+
/** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
|
|
16946
|
+
hostSuffixes: external_exports.array(external_exports.string()).min(1),
|
|
16947
|
+
/** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
|
|
16948
|
+
apiBase: external_exports.string(),
|
|
16949
|
+
/** Most-sensitive first; index 0 becomes the endpoint dataClass. */
|
|
16950
|
+
defaultDataClasses: external_exports.array(DataClass).min(1),
|
|
16951
|
+
/** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
|
|
16952
|
+
sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
|
|
16953
|
+
}).meta({ id: "ProviderRegistryEntry" });
|
|
16954
|
+
var EgressCallSiteHit = external_exports.object({
|
|
16955
|
+
file: external_exports.string(),
|
|
16956
|
+
line: external_exports.number().int().positive(),
|
|
16957
|
+
snippet: external_exports.string(),
|
|
16958
|
+
dynamic: external_exports.boolean(),
|
|
16959
|
+
vendored: external_exports.boolean()
|
|
16960
|
+
}).meta({ id: "EgressCallSiteHit" });
|
|
16961
|
+
var ResolvedEgressHit = external_exports.object({
|
|
16962
|
+
host: external_exports.string(),
|
|
16963
|
+
kind: DestinationKind,
|
|
16964
|
+
name: external_exports.string(),
|
|
16965
|
+
category: external_exports.string(),
|
|
16966
|
+
trust: ShareTrustLevel,
|
|
16967
|
+
network: DestinationNetwork.nullable(),
|
|
16968
|
+
method: HttpMethod,
|
|
16969
|
+
transport: Transport,
|
|
16970
|
+
url: external_exports.string(),
|
|
16971
|
+
template: external_exports.boolean(),
|
|
16972
|
+
dataClass: DataClass,
|
|
16973
|
+
site: EgressCallSiteHit
|
|
16974
|
+
}).meta({ id: "ResolvedEgressHit" });
|
|
16975
|
+
var EgressReconcile = external_exports.discriminatedUnion("mode", [
|
|
16976
|
+
external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
|
|
16977
|
+
external_exports.object({
|
|
16978
|
+
mode: external_exports.literal("ledger"),
|
|
16979
|
+
scannedFiles: external_exports.array(external_exports.string()),
|
|
16980
|
+
deletedFiles: external_exports.array(external_exports.string())
|
|
16981
|
+
})
|
|
16982
|
+
]).meta({ id: "EgressReconcile" });
|
|
16983
|
+
var RecordProjectEgressInput = external_exports.object({
|
|
16984
|
+
/** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
|
|
16985
|
+
projectKey: external_exports.string().min(1),
|
|
16986
|
+
/** Display name only — never keys reconciliation. */
|
|
16987
|
+
project: external_exports.string(),
|
|
16988
|
+
projectId: external_exports.string().nullable(),
|
|
16989
|
+
reconcile: EgressReconcile,
|
|
16990
|
+
hits: external_exports.array(ResolvedEgressHit)
|
|
16991
|
+
}).meta({ id: "RecordProjectEgressInput" });
|
|
16992
|
+
var EgressWriteSummary = external_exports.object({
|
|
16993
|
+
destinations: external_exports.number().int().nonnegative(),
|
|
16994
|
+
endpoints: external_exports.number().int().nonnegative(),
|
|
16995
|
+
callSites: external_exports.number().int().nonnegative(),
|
|
16996
|
+
truncated: external_exports.boolean(),
|
|
16997
|
+
/**
|
|
16998
|
+
* Files the cap dropped whole. Their stored rows were left untouched, so a
|
|
16999
|
+
* ledger-keeping caller must withhold their ledger entries and read them
|
|
17000
|
+
* again next scan.
|
|
17001
|
+
*/
|
|
17002
|
+
droppedFiles: external_exports.array(external_exports.string()).default([])
|
|
17003
|
+
}).meta({ id: "EgressWriteSummary" });
|
|
17004
|
+
|
|
16793
17005
|
// ../../packages/schema/src/zod/findings-group-build.ts
|
|
16794
17006
|
function toApiAction(dbVal) {
|
|
16795
17007
|
const map2 = {
|
|
@@ -17051,7 +17263,7 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17051
17263
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17052
17264
|
|
|
17053
17265
|
// ../../packages/schema/src/zod/local.ts
|
|
17054
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17266
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 3;
|
|
17055
17267
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17056
17268
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17057
17269
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
@@ -17066,6 +17278,9 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17066
17278
|
policy: SimpleDetectionPolicy.default("redact"),
|
|
17067
17279
|
// Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
|
|
17068
17280
|
historicalAccess: HistoricalAccess.default("session-only"),
|
|
17281
|
+
// In-place egress extraction on the scan paths; disable to stop all Data
|
|
17282
|
+
// Shares writes.
|
|
17283
|
+
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17069
17284
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17070
17285
|
onboardedAt: external_exports.iso.datetime().optional()
|
|
17071
17286
|
});
|
|
@@ -17549,145 +17764,6 @@ var SetupHandoffOffer = external_exports.object({
|
|
|
17549
17764
|
path: ["liveKeys"]
|
|
17550
17765
|
});
|
|
17551
17766
|
|
|
17552
|
-
// ../../packages/schema/src/zod/shares.ts
|
|
17553
|
-
var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
|
|
17554
|
-
var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
|
|
17555
|
-
var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
|
|
17556
|
-
var DATA_CLASS_ORDER = DataClass.options;
|
|
17557
|
-
var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
|
|
17558
|
-
var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
|
|
17559
|
-
var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
|
|
17560
|
-
var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
|
|
17561
|
-
var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
|
|
17562
|
-
var ReviewInfo = external_exports.object({
|
|
17563
|
-
needsReview: external_exports.boolean(),
|
|
17564
|
-
reasons: external_exports.array(ReviewReason)
|
|
17565
|
-
}).meta({ id: "ReviewInfo" });
|
|
17566
|
-
var DestinationNetwork = external_exports.object({
|
|
17567
|
-
port: external_exports.number().int().nullable(),
|
|
17568
|
-
geo: external_exports.string().nullable(),
|
|
17569
|
-
ptr: external_exports.string().nullable()
|
|
17570
|
-
}).meta({ id: "DestinationNetwork" });
|
|
17571
|
-
var EndpointSummary = external_exports.object({
|
|
17572
|
-
id: external_exports.string(),
|
|
17573
|
-
method: HttpMethod,
|
|
17574
|
-
transport: Transport,
|
|
17575
|
-
url: external_exports.string(),
|
|
17576
|
-
template: external_exports.boolean(),
|
|
17577
|
-
dataClass: DataClass,
|
|
17578
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17579
|
-
callSiteCount: external_exports.number().int().nonnegative()
|
|
17580
|
-
}).meta({ id: "EndpointSummary" });
|
|
17581
|
-
var CallSite = external_exports.object({
|
|
17582
|
-
id: external_exports.string(),
|
|
17583
|
-
project: external_exports.string(),
|
|
17584
|
-
file: external_exports.string(),
|
|
17585
|
-
line: external_exports.number().int().nonnegative(),
|
|
17586
|
-
snippet: external_exports.string(),
|
|
17587
|
-
dynamic: external_exports.boolean(),
|
|
17588
|
-
vendored: external_exports.boolean(),
|
|
17589
|
-
/** Deep-link to the Inventory project, when the repo is governed there. */
|
|
17590
|
-
projectId: external_exports.string().nullable()
|
|
17591
|
-
}).meta({ id: "CallSite" });
|
|
17592
|
-
var EndpointWithSites = EndpointSummary.extend({
|
|
17593
|
-
sites: external_exports.array(CallSite)
|
|
17594
|
-
}).meta({ id: "EndpointWithSites" });
|
|
17595
|
-
var ShareDestinationSummary = external_exports.object({
|
|
17596
|
-
id: external_exports.string(),
|
|
17597
|
-
kind: DestinationKind,
|
|
17598
|
-
name: external_exports.string(),
|
|
17599
|
-
host: external_exports.string(),
|
|
17600
|
-
category: external_exports.string(),
|
|
17601
|
-
trust: ShareTrustLevel,
|
|
17602
|
-
/** Effective state (decision applied over the trust default). */
|
|
17603
|
-
status: EgressStatus,
|
|
17604
|
-
/** True when an egress decision override differs from the trust default. */
|
|
17605
|
-
isCustom: external_exports.boolean(),
|
|
17606
|
-
lastSeen: external_exports.iso.datetime(),
|
|
17607
|
-
endpointCount: external_exports.number().int().nonnegative(),
|
|
17608
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17609
|
-
transports: external_exports.array(Transport),
|
|
17610
|
-
/** Most-sensitive first. */
|
|
17611
|
-
dataClasses: external_exports.array(DataClass),
|
|
17612
|
-
review: ReviewInfo,
|
|
17613
|
-
/** Non-provider hosts only; null for providers. */
|
|
17614
|
-
network: DestinationNetwork.nullable(),
|
|
17615
|
-
/** Embedded for inline expansion — no call sites here. */
|
|
17616
|
-
endpoints: external_exports.array(EndpointSummary)
|
|
17617
|
-
}).meta({ id: "ShareDestinationSummary" });
|
|
17618
|
-
var ShareDestinationDetail = ShareDestinationSummary.omit({
|
|
17619
|
-
endpointCount: true,
|
|
17620
|
-
callSiteCount: true,
|
|
17621
|
-
endpoints: true
|
|
17622
|
-
}).extend({
|
|
17623
|
-
/** Ownership/geo rationale; null for providers. */
|
|
17624
|
-
note: external_exports.string().nullable(),
|
|
17625
|
-
endpoints: external_exports.array(EndpointWithSites)
|
|
17626
|
-
}).meta({ id: "ShareDestinationDetail" });
|
|
17627
|
-
var ReviewDestination = external_exports.object({
|
|
17628
|
-
id: external_exports.string(),
|
|
17629
|
-
kind: DestinationKind,
|
|
17630
|
-
name: external_exports.string(),
|
|
17631
|
-
/** Registrable host — lets the strip derive the provider lettermark, as the register does. */
|
|
17632
|
-
host: external_exports.string(),
|
|
17633
|
-
trust: ShareTrustLevel,
|
|
17634
|
-
status: EgressStatus,
|
|
17635
|
-
review: ReviewInfo,
|
|
17636
|
-
topDataClass: DataClass,
|
|
17637
|
-
callSiteCount: external_exports.number().int().nonnegative(),
|
|
17638
|
-
lastSeen: external_exports.iso.datetime()
|
|
17639
|
-
}).meta({ id: "ReviewDestination" });
|
|
17640
|
-
var ShareDestinationGroup = external_exports.object({
|
|
17641
|
-
kind: DestinationKind,
|
|
17642
|
-
total: external_exports.number().int().nonnegative(),
|
|
17643
|
-
items: external_exports.array(ShareDestinationSummary)
|
|
17644
|
-
}).meta({ id: "ShareDestinationGroup" });
|
|
17645
|
-
var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
|
|
17646
|
-
var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
|
|
17647
|
-
var SharesStats = external_exports.object({
|
|
17648
|
-
destinations: external_exports.number().int().nonnegative(),
|
|
17649
|
-
endpoints: external_exports.number().int().nonnegative(),
|
|
17650
|
-
callSites: external_exports.number().int().nonnegative(),
|
|
17651
|
-
needsReview: external_exports.number().int().nonnegative(),
|
|
17652
|
-
insecure: external_exports.number().int().nonnegative(),
|
|
17653
|
-
byKind: external_exports.object({
|
|
17654
|
-
provider: external_exports.number().int().nonnegative(),
|
|
17655
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17656
|
-
ip: external_exports.number().int().nonnegative()
|
|
17657
|
-
}),
|
|
17658
|
-
byTrust: external_exports.object({
|
|
17659
|
-
recognized: external_exports.number().int().nonnegative(),
|
|
17660
|
-
internal: external_exports.number().int().nonnegative(),
|
|
17661
|
-
unverified: external_exports.number().int().nonnegative(),
|
|
17662
|
-
ip: external_exports.number().int().nonnegative()
|
|
17663
|
-
})
|
|
17664
|
-
}).meta({ id: "SharesStats" });
|
|
17665
|
-
var SetEgressDecisionBody = external_exports.object({
|
|
17666
|
-
/** `null` clears the override — reverts to the trust default, isCustom false. */
|
|
17667
|
-
decision: EgressDecision.nullable()
|
|
17668
|
-
}).meta({ id: "SetEgressDecisionBody" });
|
|
17669
|
-
var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
|
|
17670
|
-
var ListShareDestinationsQuery = external_exports.object({
|
|
17671
|
-
/** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
|
|
17672
|
-
q: external_exports.string().optional(),
|
|
17673
|
-
/** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
|
|
17674
|
-
kind: external_exports.array(DestinationKind).optional(),
|
|
17675
|
-
/** Reserved for future grouping modes; only 'destination' is supported today. */
|
|
17676
|
-
groupBy: external_exports.enum(["destination"]).default("destination"),
|
|
17677
|
-
/**
|
|
17678
|
-
* When true, return a flat severity-ordered `items[]` instead of `groups`.
|
|
17679
|
-
* Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
|
|
17680
|
-
* any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
|
|
17681
|
-
* to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
|
|
17682
|
-
*/
|
|
17683
|
-
review: external_exports.stringbool().default(false)
|
|
17684
|
-
});
|
|
17685
|
-
var ExportSharesQuery = external_exports.object({
|
|
17686
|
-
format: external_exports.enum(["csv", "json"]).default("csv"),
|
|
17687
|
-
q: external_exports.string().optional(),
|
|
17688
|
-
kind: external_exports.array(DestinationKind).optional()
|
|
17689
|
-
});
|
|
17690
|
-
|
|
17691
17767
|
// ../../packages/schema/src/zod/shares-access.ts
|
|
17692
17768
|
var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
|
|
17693
17769
|
function trustDefaultStatus(trust) {
|
|
@@ -17707,7 +17783,7 @@ function deriveReviewReasons(trust, transports) {
|
|
|
17707
17783
|
const reasons = [];
|
|
17708
17784
|
if (trust === "ip") reasons.push("raw_ip");
|
|
17709
17785
|
if (trust === "unverified") reasons.push("unverified_domain");
|
|
17710
|
-
if (transports.includes("http")) reasons.push("plaintext_transport");
|
|
17786
|
+
if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
|
|
17711
17787
|
return reasons;
|
|
17712
17788
|
}
|
|
17713
17789
|
function buildReviewInfo(trust, transports) {
|
|
@@ -17938,6 +18014,7 @@ function applyMigrations(db) {
|
|
|
17938
18014
|
ensureSyncedAtColumn(db, "audit_events");
|
|
17939
18015
|
ensureScanLedgerTable(db);
|
|
17940
18016
|
ensureBlockedDetectionsTable(db);
|
|
18017
|
+
ensureRuleProbeCacheTable(db);
|
|
17941
18018
|
ensureWriteGateTrigger(db);
|
|
17942
18019
|
ensureTokenUsageColumns(db);
|
|
17943
18020
|
reconcileSourceProjectIds(db);
|
|
@@ -18077,6 +18154,14 @@ function ensureBlockedDetectionsTable(db) {
|
|
|
18077
18154
|
blocked_at INTEGER NOT NULL
|
|
18078
18155
|
)`);
|
|
18079
18156
|
}
|
|
18157
|
+
function ensureRuleProbeCacheTable(db) {
|
|
18158
|
+
db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
|
|
18159
|
+
rule_key TEXT PRIMARY KEY,
|
|
18160
|
+
verdict TEXT NOT NULL,
|
|
18161
|
+
worst_probe_ms REAL NOT NULL,
|
|
18162
|
+
checked_at INTEGER NOT NULL
|
|
18163
|
+
)`);
|
|
18164
|
+
}
|
|
18080
18165
|
|
|
18081
18166
|
// ../../packages/persistence/src/paths.ts
|
|
18082
18167
|
import { chmodSync, mkdirSync } from "fs";
|
|
@@ -21630,6 +21715,35 @@ var SqliteResolutionsRepository = class {
|
|
|
21630
21715
|
}
|
|
21631
21716
|
};
|
|
21632
21717
|
|
|
21718
|
+
// ../../packages/persistence/src/repositories/rule-probe-cache.ts
|
|
21719
|
+
var SqliteRuleProbeCacheRepository = class {
|
|
21720
|
+
constructor(db) {
|
|
21721
|
+
this.db = db;
|
|
21722
|
+
this.upsertStmt = db.prepare(
|
|
21723
|
+
`INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
|
|
21724
|
+
VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
|
|
21725
|
+
ON CONFLICT (rule_key) DO UPDATE SET
|
|
21726
|
+
verdict = excluded.verdict,
|
|
21727
|
+
worst_probe_ms = excluded.worst_probe_ms,
|
|
21728
|
+
checked_at = excluded.checked_at`
|
|
21729
|
+
);
|
|
21730
|
+
this.readStmt = db.prepare(
|
|
21731
|
+
`SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
|
|
21732
|
+
);
|
|
21733
|
+
}
|
|
21734
|
+
db;
|
|
21735
|
+
upsertStmt;
|
|
21736
|
+
readStmt;
|
|
21737
|
+
getVerdict(ruleKey) {
|
|
21738
|
+
return getRow(this.readStmt, { ruleKey });
|
|
21739
|
+
}
|
|
21740
|
+
setVerdict(ruleKey, verdict, worstProbeMs2) {
|
|
21741
|
+
failOpenTransaction(this.db, () => {
|
|
21742
|
+
this.upsertStmt.run({ ruleKey, verdict, worstProbeMs: worstProbeMs2, checkedAt: Date.now() });
|
|
21743
|
+
});
|
|
21744
|
+
}
|
|
21745
|
+
};
|
|
21746
|
+
|
|
21633
21747
|
// ../../packages/persistence/src/repositories/scan-ledger.ts
|
|
21634
21748
|
var SqliteScanLedgerRepository = class {
|
|
21635
21749
|
constructor(db) {
|
|
@@ -22041,11 +22155,50 @@ var SqliteSecurityRepository = class {
|
|
|
22041
22155
|
|
|
22042
22156
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
22043
22157
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
22044
|
-
var
|
|
22158
|
+
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
22159
|
+
var IN_CHUNK = 500;
|
|
22160
|
+
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
22161
|
+
var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
|
|
22162
|
+
var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
|
|
22163
|
+
LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
|
|
22045
22164
|
var CALL_SITE_EMBED_CAP = 200;
|
|
22046
22165
|
function parseNetwork(networkJson) {
|
|
22047
22166
|
return safeJson(networkJson, null);
|
|
22048
22167
|
}
|
|
22168
|
+
function capHits(all, mode) {
|
|
22169
|
+
if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
|
|
22170
|
+
return { hits: [...all], droppedFiles: [], truncated: false };
|
|
22171
|
+
}
|
|
22172
|
+
if (mode === "walk") {
|
|
22173
|
+
return {
|
|
22174
|
+
hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
|
|
22175
|
+
droppedFiles: [],
|
|
22176
|
+
truncated: true
|
|
22177
|
+
};
|
|
22178
|
+
}
|
|
22179
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
22180
|
+
for (const hit of all) {
|
|
22181
|
+
const bucket = byFile.get(hit.site.file);
|
|
22182
|
+
if (bucket === void 0) byFile.set(hit.site.file, [hit]);
|
|
22183
|
+
else bucket.push(hit);
|
|
22184
|
+
}
|
|
22185
|
+
const hits = [];
|
|
22186
|
+
const droppedFiles = [];
|
|
22187
|
+
for (const [file2, bucket] of byFile) {
|
|
22188
|
+
if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
|
|
22189
|
+
else hits.push(...bucket);
|
|
22190
|
+
}
|
|
22191
|
+
return { hits, droppedFiles, truncated: true };
|
|
22192
|
+
}
|
|
22193
|
+
function withoutDroppedFiles(reconcile, droppedFiles) {
|
|
22194
|
+
if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
|
|
22195
|
+
const dropped = new Set(droppedFiles);
|
|
22196
|
+
return {
|
|
22197
|
+
mode: "ledger",
|
|
22198
|
+
scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
|
|
22199
|
+
deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
|
|
22200
|
+
};
|
|
22201
|
+
}
|
|
22049
22202
|
function toEndpointSummary(row) {
|
|
22050
22203
|
return {
|
|
22051
22204
|
id: row.id,
|
|
@@ -22136,13 +22289,15 @@ var SqliteSharesRepository = class {
|
|
|
22136
22289
|
const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
|
|
22137
22290
|
const insecure = countScalar(
|
|
22138
22291
|
this.db,
|
|
22139
|
-
|
|
22292
|
+
`SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
|
|
22293
|
+
WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
|
|
22140
22294
|
);
|
|
22141
22295
|
const needsReview = countScalar(
|
|
22142
22296
|
this.db,
|
|
22143
22297
|
`SELECT count(DISTINCT d.id) AS n
|
|
22144
22298
|
FROM share_destination d
|
|
22145
|
-
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22299
|
+
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22300
|
+
AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
|
|
22146
22301
|
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
|
|
22147
22302
|
);
|
|
22148
22303
|
const kindCounts = countBy(
|
|
@@ -22152,6 +22307,7 @@ var SqliteSharesRepository = class {
|
|
|
22152
22307
|
const byKind = {
|
|
22153
22308
|
provider: kindCounts.get("provider") ?? 0,
|
|
22154
22309
|
internal: kindCounts.get("internal") ?? 0,
|
|
22310
|
+
external: kindCounts.get("external") ?? 0,
|
|
22155
22311
|
ip: kindCounts.get("ip") ?? 0
|
|
22156
22312
|
};
|
|
22157
22313
|
const trustCounts = countBy(
|
|
@@ -22227,23 +22383,316 @@ var SqliteSharesRepository = class {
|
|
|
22227
22383
|
// real edit from a no-such-destination.
|
|
22228
22384
|
/**
|
|
22229
22385
|
* Set (decision) or clear (null) the egress decision override for a destination.
|
|
22230
|
-
* `null` deletes the override
|
|
22386
|
+
* `null` deletes the override rows → reverts to the trust default.
|
|
22387
|
+
*
|
|
22388
|
+
* The written row carries both the destination id and its host, so the
|
|
22389
|
+
* decision re-attaches by host after the destination is pruned and
|
|
22390
|
+
* re-detected under a fresh id. Rows written before the host column existed
|
|
22391
|
+
* (host NULL, matched by destination id) are replaced rather than left to
|
|
22392
|
+
* shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
|
|
22393
|
+
* would otherwise race a concurrent prune.
|
|
22231
22394
|
*/
|
|
22232
22395
|
setEgressDecision(destinationId, decision) {
|
|
22233
|
-
|
|
22234
|
-
|
|
22235
|
-
|
|
22236
|
-
|
|
22237
|
-
|
|
22396
|
+
let existed = false;
|
|
22397
|
+
withTransaction(
|
|
22398
|
+
this.db,
|
|
22399
|
+
() => {
|
|
22400
|
+
const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
|
|
22401
|
+
if (dest === void 0) return;
|
|
22402
|
+
existed = true;
|
|
22403
|
+
this.db.prepare(
|
|
22404
|
+
`DELETE FROM egress_decision_override
|
|
22405
|
+
WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
|
|
22406
|
+
).run({ host: dest.host, destinationId });
|
|
22407
|
+
if (decision === null) return;
|
|
22408
|
+
this.db.prepare(
|
|
22409
|
+
`INSERT INTO egress_decision_override
|
|
22410
|
+
(id, destination_id, host, decision, created_at, updated_at)
|
|
22411
|
+
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
22412
|
+
).run({
|
|
22413
|
+
id: randomUUID7(),
|
|
22414
|
+
destinationId,
|
|
22415
|
+
host: dest.host,
|
|
22416
|
+
decision,
|
|
22417
|
+
now: Date.now()
|
|
22418
|
+
});
|
|
22419
|
+
},
|
|
22420
|
+
"IMMEDIATE"
|
|
22421
|
+
);
|
|
22422
|
+
return existed;
|
|
22423
|
+
}
|
|
22424
|
+
/**
|
|
22425
|
+
* Record one project's statically-extracted egress: reconcile the previously
|
|
22426
|
+
* stored call sites against this scan, upsert destination → endpoint → call
|
|
22427
|
+
* site for every hit, confirm `last_seen` on everything the project still
|
|
22428
|
+
* references, and drop what no longer has evidence.
|
|
22429
|
+
*
|
|
22430
|
+
* Reconciliation keys on `projectKey` alone; `project` and `projectId` are
|
|
22431
|
+
* display payload and never scope a delete. The whole write is one
|
|
22432
|
+
* transaction: a failure leaves the project's previous inventory exactly as
|
|
22433
|
+
* it was, and THROWS rather than reporting a partial write — callers decide
|
|
22434
|
+
* their own fail-open behavior, and the scanner additionally withholds its
|
|
22435
|
+
* ledger commit so the next scan retries.
|
|
22436
|
+
*
|
|
22437
|
+
* Over-cap input is truncated at a FILE boundary, and the files that lost
|
|
22438
|
+
* their hits are both excluded from the reconcile delete and named in
|
|
22439
|
+
* `droppedFiles`. That pairing is what keeps truncation non-destructive on
|
|
22440
|
+
* the ledger path: a dropped file keeps whatever rows it already had, and its
|
|
22441
|
+
* caller withholds the ledger entry so the next scan reads it again.
|
|
22442
|
+
*/
|
|
22443
|
+
recordProjectEgress(input) {
|
|
22444
|
+
const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
|
|
22445
|
+
const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
|
|
22446
|
+
const now = Date.now();
|
|
22447
|
+
let summary = {
|
|
22448
|
+
destinations: 0,
|
|
22449
|
+
endpoints: 0,
|
|
22450
|
+
callSites: 0,
|
|
22451
|
+
truncated,
|
|
22452
|
+
droppedFiles
|
|
22453
|
+
};
|
|
22454
|
+
withTransaction(
|
|
22455
|
+
this.db,
|
|
22456
|
+
() => {
|
|
22457
|
+
const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
|
|
22458
|
+
this.reconcileCallSites(input.projectKey, reconcile);
|
|
22459
|
+
this.upsertHits(input, hits, projectId, now);
|
|
22460
|
+
this.confirmLastSeen(input.projectKey, now);
|
|
22461
|
+
this.pruneOrphans();
|
|
22462
|
+
summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
|
|
22463
|
+
},
|
|
22464
|
+
"IMMEDIATE"
|
|
22465
|
+
);
|
|
22466
|
+
return summary;
|
|
22467
|
+
}
|
|
22468
|
+
// ─── Egress write internals ──────────────────────────────────────────────────
|
|
22469
|
+
/**
|
|
22470
|
+
* Clear the stored call sites this scan is responsible for re-creating.
|
|
22471
|
+
*
|
|
22472
|
+
* Each pipeline may only delete rows its own walker could have produced. The
|
|
22473
|
+
* fs walk behind 'walk' mode never descends into dot-directories, so its
|
|
22474
|
+
* delete excludes dot-path files — those rows are the plugin scanner's to
|
|
22475
|
+
* reconcile, and deleting them here would make the two pipelines erase each
|
|
22476
|
+
* other's rows on every alternating scan. 'ledger' mode names its files
|
|
22477
|
+
* outright and never mass-deletes, so rows the fs walk contributed for files
|
|
22478
|
+
* the scanner skips (vendored, oversize) survive it.
|
|
22479
|
+
*/
|
|
22480
|
+
reconcileCallSites(projectKey, reconcile) {
|
|
22481
|
+
if (reconcile.mode === "walk") {
|
|
22482
|
+
const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
|
|
22483
|
+
this.db.prepare(
|
|
22484
|
+
`DELETE FROM share_call_site
|
|
22485
|
+
WHERE project_key = :key
|
|
22486
|
+
AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
|
|
22487
|
+
AND file NOT LIKE '.%'
|
|
22488
|
+
AND file NOT LIKE '%/.%'`
|
|
22489
|
+
).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
|
|
22490
|
+
return;
|
|
22491
|
+
}
|
|
22492
|
+
const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
|
|
22493
|
+
for (let i = 0; i < files.length; i += IN_CHUNK) {
|
|
22494
|
+
const chunk = files.slice(i, i + IN_CHUNK);
|
|
22495
|
+
this.db.prepare(
|
|
22496
|
+
`DELETE FROM share_call_site
|
|
22497
|
+
WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
|
|
22498
|
+
).run(projectKey, ...chunk);
|
|
22499
|
+
}
|
|
22500
|
+
}
|
|
22501
|
+
/**
|
|
22502
|
+
* Upsert every hit as destination → endpoint → call site. Destinations key on
|
|
22503
|
+
* `host` and endpoints on `(destination_id, method, url)`, both shared across
|
|
22504
|
+
* projects; only the call site carries `project_key`. A destination's `note`
|
|
22505
|
+
* is user-owned and never overwritten. The id caches keep one upsert per
|
|
22506
|
+
* distinct host and endpoint, so the first hit for a host supplies its
|
|
22507
|
+
* classification for this batch.
|
|
22508
|
+
*/
|
|
22509
|
+
upsertHits(input, hits, projectId, now) {
|
|
22510
|
+
if (hits.length === 0) return;
|
|
22511
|
+
const destStmt = this.db.prepare(
|
|
22512
|
+
`INSERT INTO share_destination
|
|
22513
|
+
(id, kind, name, host, category, trust, network_json, last_seen, provenance,
|
|
22514
|
+
created_at, updated_at)
|
|
22515
|
+
VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
|
|
22516
|
+
ON CONFLICT (host) DO UPDATE SET
|
|
22517
|
+
kind = excluded.kind,
|
|
22518
|
+
name = excluded.name,
|
|
22519
|
+
category = excluded.category,
|
|
22520
|
+
trust = excluded.trust,
|
|
22521
|
+
network_json = excluded.network_json,
|
|
22522
|
+
last_seen = excluded.last_seen,
|
|
22523
|
+
updated_at = excluded.updated_at`
|
|
22524
|
+
);
|
|
22525
|
+
const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
|
|
22526
|
+
const endpointStmt = this.db.prepare(
|
|
22527
|
+
`INSERT INTO share_endpoint
|
|
22528
|
+
(id, destination_id, method, transport, url, template, data_class, last_seen,
|
|
22529
|
+
created_at, updated_at)
|
|
22530
|
+
VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
|
|
22531
|
+
:now, :now)
|
|
22532
|
+
ON CONFLICT (destination_id, method, url) DO UPDATE SET
|
|
22533
|
+
transport = excluded.transport,
|
|
22534
|
+
template = excluded.template,
|
|
22535
|
+
data_class = excluded.data_class,
|
|
22536
|
+
last_seen = excluded.last_seen,
|
|
22537
|
+
updated_at = excluded.updated_at`
|
|
22538
|
+
);
|
|
22539
|
+
const endpointIdStmt = this.db.prepare(
|
|
22540
|
+
"SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
|
|
22541
|
+
);
|
|
22542
|
+
const siteStmt = this.db.prepare(
|
|
22543
|
+
`INSERT INTO share_call_site
|
|
22544
|
+
(id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
|
|
22545
|
+
project_id, created_at, updated_at)
|
|
22546
|
+
VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
|
|
22547
|
+
:vendored, :projectId, :now, :now)
|
|
22548
|
+
ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
|
|
22549
|
+
snippet = excluded.snippet,
|
|
22550
|
+
dynamic = excluded.dynamic,
|
|
22551
|
+
vendored = excluded.vendored,
|
|
22552
|
+
project = excluded.project,
|
|
22553
|
+
project_id = COALESCE(excluded.project_id, share_call_site.project_id),
|
|
22554
|
+
updated_at = excluded.updated_at`
|
|
22555
|
+
);
|
|
22556
|
+
const destIds = /* @__PURE__ */ new Map();
|
|
22557
|
+
const endpointIds = /* @__PURE__ */ new Map();
|
|
22558
|
+
for (const hit of hits) {
|
|
22559
|
+
let destinationId = destIds.get(hit.host);
|
|
22560
|
+
if (destinationId === void 0) {
|
|
22561
|
+
destStmt.run({
|
|
22562
|
+
id: randomUUID7(),
|
|
22563
|
+
kind: hit.kind,
|
|
22564
|
+
name: hit.name,
|
|
22565
|
+
host: hit.host,
|
|
22566
|
+
category: hit.category,
|
|
22567
|
+
trust: hit.trust,
|
|
22568
|
+
networkJson: hit.network === null ? null : JSON.stringify(hit.network),
|
|
22569
|
+
now
|
|
22570
|
+
});
|
|
22571
|
+
destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
|
|
22572
|
+
destIds.set(hit.host, destinationId);
|
|
22573
|
+
}
|
|
22574
|
+
const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
|
|
22575
|
+
let endpointId = endpointIds.get(endpointKey);
|
|
22576
|
+
if (endpointId === void 0) {
|
|
22577
|
+
endpointStmt.run({
|
|
22578
|
+
id: randomUUID7(),
|
|
22579
|
+
destinationId,
|
|
22580
|
+
method: hit.method,
|
|
22581
|
+
transport: hit.transport,
|
|
22582
|
+
url: hit.url,
|
|
22583
|
+
template: boolToInt(hit.template),
|
|
22584
|
+
dataClass: hit.dataClass,
|
|
22585
|
+
now
|
|
22586
|
+
});
|
|
22587
|
+
endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
|
|
22588
|
+
endpointIds.set(endpointKey, endpointId);
|
|
22589
|
+
}
|
|
22590
|
+
siteStmt.run({
|
|
22591
|
+
id: randomUUID7(),
|
|
22592
|
+
endpointId,
|
|
22593
|
+
project: input.project,
|
|
22594
|
+
projectKey: input.projectKey,
|
|
22595
|
+
file: hit.site.file,
|
|
22596
|
+
line: hit.site.line,
|
|
22597
|
+
snippet: hit.site.snippet,
|
|
22598
|
+
dynamic: boolToInt(hit.site.dynamic),
|
|
22599
|
+
vendored: boolToInt(hit.site.vendored),
|
|
22600
|
+
projectId,
|
|
22601
|
+
now
|
|
22602
|
+
});
|
|
22238
22603
|
}
|
|
22604
|
+
}
|
|
22605
|
+
/**
|
|
22606
|
+
* The source-project id this project's stored call sites already carry, if
|
|
22607
|
+
* any. Only the pipeline that resolves a source project supplies one; the
|
|
22608
|
+
* other passes null and inherits this, so the link stops flapping between a
|
|
22609
|
+
* real id and NULL depending on which pipeline ran last. The value is a
|
|
22610
|
+
* per-project attribute stored redundantly on each row, so any row's is
|
|
22611
|
+
* representative.
|
|
22612
|
+
*/
|
|
22613
|
+
knownProjectId(projectKey) {
|
|
22614
|
+
return getRow(
|
|
22615
|
+
this.db.prepare(
|
|
22616
|
+
`SELECT project_id AS projectId FROM share_call_site
|
|
22617
|
+
WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
|
|
22618
|
+
),
|
|
22619
|
+
[projectKey]
|
|
22620
|
+
)?.projectId ?? null;
|
|
22621
|
+
}
|
|
22622
|
+
/**
|
|
22623
|
+
* Stamp `last_seen` on every endpoint and destination this project still
|
|
22624
|
+
* references — including rows the scan preserved rather than re-wrote, so a
|
|
22625
|
+
* ledger-skipped file's references don't decay into "stale" on the page.
|
|
22626
|
+
*/
|
|
22627
|
+
confirmLastSeen(projectKey, now) {
|
|
22239
22628
|
this.db.prepare(
|
|
22240
|
-
`
|
|
22241
|
-
|
|
22242
|
-
|
|
22243
|
-
|
|
22244
|
-
|
|
22245
|
-
|
|
22246
|
-
|
|
22629
|
+
`UPDATE share_endpoint SET last_seen = :now, updated_at = :now
|
|
22630
|
+
WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
|
|
22631
|
+
).run({ now, key: projectKey });
|
|
22632
|
+
this.db.prepare(
|
|
22633
|
+
`UPDATE share_destination SET last_seen = :now, updated_at = :now
|
|
22634
|
+
WHERE id IN (SELECT DISTINCT e.destination_id
|
|
22635
|
+
FROM share_endpoint e
|
|
22636
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
22637
|
+
WHERE c.project_key = :key)`
|
|
22638
|
+
).run({ now, key: projectKey });
|
|
22639
|
+
}
|
|
22640
|
+
/**
|
|
22641
|
+
* Drop rows left without evidence: endpoints with no call site, then
|
|
22642
|
+
* destinations with no endpoint. Call sites are the only evidence either one
|
|
22643
|
+
* has, so a row that lost its last one belongs to no project any more.
|
|
22644
|
+
*
|
|
22645
|
+
* Overrides are deleted between the two steps, and only the ones written
|
|
22646
|
+
* before the host column existed. Those match a destination by id alone;
|
|
22647
|
+
* because the id link is released on delete rather than cascading, leaving
|
|
22648
|
+
* them would accumulate rows that match neither join arm and that nothing can
|
|
22649
|
+
* reach again. Host-bearing rows deliberately survive — the host is what
|
|
22650
|
+
* re-attaches a user's decision when the destination comes back.
|
|
22651
|
+
*/
|
|
22652
|
+
pruneOrphans() {
|
|
22653
|
+
this.db.exec(
|
|
22654
|
+
`DELETE FROM share_endpoint
|
|
22655
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
|
|
22656
|
+
);
|
|
22657
|
+
this.db.exec(
|
|
22658
|
+
`DELETE FROM egress_decision_override
|
|
22659
|
+
WHERE host IS NULL
|
|
22660
|
+
AND destination_id IN (
|
|
22661
|
+
SELECT d.id FROM share_destination d
|
|
22662
|
+
WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
|
|
22663
|
+
);
|
|
22664
|
+
this.db.exec(
|
|
22665
|
+
`DELETE FROM share_destination
|
|
22666
|
+
WHERE NOT EXISTS (
|
|
22667
|
+
SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
|
|
22668
|
+
);
|
|
22669
|
+
}
|
|
22670
|
+
/**
|
|
22671
|
+
* Live totals for one project. Destinations and endpoints are shared across
|
|
22672
|
+
* projects and carry no project column, so both are counted through the call
|
|
22673
|
+
* sites that reference them.
|
|
22674
|
+
*/
|
|
22675
|
+
projectTotals(projectKey) {
|
|
22676
|
+
return {
|
|
22677
|
+
destinations: countScalar(
|
|
22678
|
+
this.db,
|
|
22679
|
+
`SELECT count(DISTINCT e.destination_id) AS n
|
|
22680
|
+
FROM share_endpoint e
|
|
22681
|
+
JOIN share_call_site c ON c.endpoint_id = e.id
|
|
22682
|
+
WHERE c.project_key = ?`,
|
|
22683
|
+
[projectKey]
|
|
22684
|
+
),
|
|
22685
|
+
endpoints: countScalar(
|
|
22686
|
+
this.db,
|
|
22687
|
+
"SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
|
|
22688
|
+
[projectKey]
|
|
22689
|
+
),
|
|
22690
|
+
callSites: countScalar(
|
|
22691
|
+
this.db,
|
|
22692
|
+
"SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
|
|
22693
|
+
[projectKey]
|
|
22694
|
+
)
|
|
22695
|
+
};
|
|
22247
22696
|
}
|
|
22248
22697
|
// ─── Raw fetchers ────────────────────────────────────────────────────────────
|
|
22249
22698
|
mapDestRow(r) {
|
|
@@ -22263,7 +22712,8 @@ var SqliteSharesRepository = class {
|
|
|
22263
22712
|
fetchDestinations(q, kinds, reviewOnly = false) {
|
|
22264
22713
|
const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22265
22714
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22266
|
-
d.created_at AS createdAt,
|
|
22715
|
+
d.created_at AS createdAt,
|
|
22716
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision`;
|
|
22267
22717
|
const conditions = [];
|
|
22268
22718
|
const params = [];
|
|
22269
22719
|
if (kinds && kinds.length > 0) {
|
|
@@ -22274,7 +22724,8 @@ var SqliteSharesRepository = class {
|
|
|
22274
22724
|
conditions.push(
|
|
22275
22725
|
`(d.trust IN ('unverified', 'ip')
|
|
22276
22726
|
OR EXISTS (SELECT 1 FROM share_endpoint re
|
|
22277
|
-
WHERE re.destination_id = d.id
|
|
22727
|
+
WHERE re.destination_id = d.id
|
|
22728
|
+
AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
|
|
22278
22729
|
);
|
|
22279
22730
|
}
|
|
22280
22731
|
let sql;
|
|
@@ -22287,7 +22738,7 @@ var SqliteSharesRepository = class {
|
|
|
22287
22738
|
params.push(pattern, pattern, pattern, pattern, pattern);
|
|
22288
22739
|
sql = `SELECT DISTINCT ${cols}
|
|
22289
22740
|
FROM share_destination d
|
|
22290
|
-
|
|
22741
|
+
${OVERRIDE_JOIN}
|
|
22291
22742
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
22292
22743
|
LEFT JOIN share_call_site c ON c.endpoint_id = e.id
|
|
22293
22744
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
@@ -22295,7 +22746,7 @@ var SqliteSharesRepository = class {
|
|
|
22295
22746
|
} else {
|
|
22296
22747
|
sql = `SELECT ${cols}
|
|
22297
22748
|
FROM share_destination d
|
|
22298
|
-
|
|
22749
|
+
${OVERRIDE_JOIN}
|
|
22299
22750
|
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
22300
22751
|
ORDER BY d.created_at ASC, d.id ASC`;
|
|
22301
22752
|
}
|
|
@@ -22310,9 +22761,9 @@ var SqliteSharesRepository = class {
|
|
|
22310
22761
|
this.db.prepare(
|
|
22311
22762
|
`SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
|
|
22312
22763
|
d.network_json AS networkJson, d.last_seen AS lastSeenMs,
|
|
22313
|
-
|
|
22764
|
+
COALESCE(oh.decision, ol.decision) AS overrideDecision
|
|
22314
22765
|
FROM share_destination d
|
|
22315
|
-
|
|
22766
|
+
${OVERRIDE_JOIN}
|
|
22316
22767
|
WHERE d.id = ?`
|
|
22317
22768
|
),
|
|
22318
22769
|
[destinationId]
|
|
@@ -22541,6 +22992,7 @@ function openLocalDatabase(dir) {
|
|
|
22541
22992
|
const scanLedger = new SqliteScanLedgerRepository(db);
|
|
22542
22993
|
const exceptions = new SqliteExceptionsRepository(db);
|
|
22543
22994
|
const resolutions = new SqliteResolutionsRepository(db);
|
|
22995
|
+
const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
|
|
22544
22996
|
const security = new SqliteSecurityRepository(db);
|
|
22545
22997
|
const detections = new SqliteDetectionsRepository(db);
|
|
22546
22998
|
const shares = new SqliteSharesRepository(db);
|
|
@@ -22678,6 +23130,7 @@ function openLocalDatabase(dir) {
|
|
|
22678
23130
|
scanLedger,
|
|
22679
23131
|
exceptions,
|
|
22680
23132
|
resolutions,
|
|
23133
|
+
ruleProbeCache,
|
|
22681
23134
|
security,
|
|
22682
23135
|
detections,
|
|
22683
23136
|
shares,
|
|
@@ -22918,108 +23371,1515 @@ import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as s
|
|
|
22918
23371
|
import { homedir as homedir2 } from "os";
|
|
22919
23372
|
import { basename as basename2, join as join7 } from "path";
|
|
22920
23373
|
|
|
22921
|
-
// ../../packages/detections/src/
|
|
22922
|
-
|
|
22923
|
-
|
|
22924
|
-
|
|
22925
|
-
|
|
22926
|
-
|
|
22927
|
-
|
|
22928
|
-
|
|
22929
|
-
|
|
22930
|
-
|
|
22931
|
-
|
|
22932
|
-
|
|
22933
|
-
|
|
22934
|
-
|
|
22935
|
-
|
|
22936
|
-
|
|
22937
|
-
|
|
22938
|
-
|
|
22939
|
-
let m;
|
|
22940
|
-
while ((m = re.exec(text)) !== null) {
|
|
22941
|
-
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
22942
|
-
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
22943
|
-
}
|
|
23374
|
+
// ../../packages/detections/src/egress/registry.ts
|
|
23375
|
+
var EXTRACTOR_VERSION = "1";
|
|
23376
|
+
var PROVIDER_REGISTRY = [
|
|
23377
|
+
{
|
|
23378
|
+
id: "stripe",
|
|
23379
|
+
name: "Stripe",
|
|
23380
|
+
category: "Payments",
|
|
23381
|
+
hostSuffixes: ["stripe.com"],
|
|
23382
|
+
apiBase: "https://api.stripe.com",
|
|
23383
|
+
defaultDataClasses: ["pii", "customer"],
|
|
23384
|
+
sdks: {
|
|
23385
|
+
npm: ["stripe"],
|
|
23386
|
+
pypi: ["stripe"],
|
|
23387
|
+
go: ["github.com/stripe/stripe-go"],
|
|
23388
|
+
maven: ["com.stripe"],
|
|
23389
|
+
rubygems: ["stripe"],
|
|
23390
|
+
composer: ["stripe/stripe-php"],
|
|
23391
|
+
nuget: ["Stripe.net"]
|
|
22944
23392
|
}
|
|
22945
|
-
|
|
22946
|
-
|
|
22947
|
-
|
|
22948
|
-
|
|
22949
|
-
|
|
22950
|
-
|
|
22951
|
-
|
|
22952
|
-
|
|
22953
|
-
|
|
22954
|
-
|
|
22955
|
-
|
|
22956
|
-
|
|
22957
|
-
|
|
22958
|
-
|
|
22959
|
-
|
|
22960
|
-
if (group && spans.length < MAX_MATCHES_PER_RULE) {
|
|
22961
|
-
const groupIndices = captureGroup != null ? m.indices?.[captureGroup] : void 0;
|
|
22962
|
-
const start = groupIndices ? groupIndices[0] : m.index;
|
|
22963
|
-
spans.push({ start, end: start + group.length });
|
|
22964
|
-
}
|
|
22965
|
-
if (!flags.includes("g") || spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
23393
|
+
},
|
|
23394
|
+
{
|
|
23395
|
+
id: "datadog",
|
|
23396
|
+
name: "Datadog",
|
|
23397
|
+
category: "Observability",
|
|
23398
|
+
hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
|
|
23399
|
+
apiBase: "https://api.datadoghq.com",
|
|
23400
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23401
|
+
sdks: {
|
|
23402
|
+
npm: ["dd-trace", "@datadog/browser-logs"],
|
|
23403
|
+
pypi: ["datadog", "ddtrace"],
|
|
23404
|
+
go: ["github.com/DataDog/dd-trace-go"],
|
|
23405
|
+
maven: ["com.datadoghq"],
|
|
23406
|
+
rubygems: ["ddtrace", "dogapi"],
|
|
23407
|
+
nuget: ["Datadog.Trace"]
|
|
22966
23408
|
}
|
|
22967
|
-
|
|
22968
|
-
|
|
22969
|
-
|
|
22970
|
-
|
|
22971
|
-
|
|
22972
|
-
|
|
22973
|
-
|
|
22974
|
-
|
|
22975
|
-
|
|
22976
|
-
|
|
22977
|
-
|
|
22978
|
-
|
|
22979
|
-
|
|
22980
|
-
|
|
22981
|
-
|
|
22982
|
-
return entropy;
|
|
22983
|
-
}
|
|
22984
|
-
function isHighEntropy(value, threshold = 3.5, minLength = 20) {
|
|
22985
|
-
return value.length >= minLength && shannonEntropy(value) >= threshold;
|
|
22986
|
-
}
|
|
22987
|
-
|
|
22988
|
-
// ../../packages/detections/src/validators/luhn.ts
|
|
22989
|
-
function luhnCheck(digits) {
|
|
22990
|
-
const nums = digits.replace(/\D/g, "");
|
|
22991
|
-
if (nums.length < 13) return false;
|
|
22992
|
-
let sum = 0;
|
|
22993
|
-
let isOdd = true;
|
|
22994
|
-
for (let i = nums.length - 1; i >= 0; i--) {
|
|
22995
|
-
let digit = parseInt(nums[i] ?? "0", 10);
|
|
22996
|
-
if (!isOdd) {
|
|
22997
|
-
digit *= 2;
|
|
22998
|
-
if (digit > 9) digit -= 9;
|
|
23409
|
+
},
|
|
23410
|
+
{
|
|
23411
|
+
id: "newrelic",
|
|
23412
|
+
name: "New Relic",
|
|
23413
|
+
category: "Observability",
|
|
23414
|
+
hostSuffixes: ["newrelic.com", "nr-data.net"],
|
|
23415
|
+
apiBase: "https://api.newrelic.com",
|
|
23416
|
+
defaultDataClasses: ["telemetry", "logs", "metrics"],
|
|
23417
|
+
sdks: {
|
|
23418
|
+
npm: ["newrelic"],
|
|
23419
|
+
pypi: ["newrelic"],
|
|
23420
|
+
go: ["github.com/newrelic/go-agent"],
|
|
23421
|
+
maven: ["com.newrelic.agent.java"],
|
|
23422
|
+
rubygems: ["newrelic_rpm"],
|
|
23423
|
+
nuget: ["NewRelic.Agent"]
|
|
22999
23424
|
}
|
|
23000
|
-
|
|
23001
|
-
|
|
23002
|
-
|
|
23003
|
-
|
|
23004
|
-
|
|
23005
|
-
|
|
23006
|
-
|
|
23007
|
-
|
|
23008
|
-
|
|
23009
|
-
|
|
23010
|
-
|
|
23011
|
-
|
|
23012
|
-
|
|
23013
|
-
|
|
23014
|
-
|
|
23015
|
-
|
|
23016
|
-
|
|
23017
|
-
}
|
|
23018
|
-
|
|
23019
|
-
|
|
23020
|
-
|
|
23021
|
-
|
|
23022
|
-
|
|
23425
|
+
},
|
|
23426
|
+
{
|
|
23427
|
+
id: "sentry",
|
|
23428
|
+
name: "Sentry",
|
|
23429
|
+
category: "Error tracking",
|
|
23430
|
+
hostSuffixes: ["sentry.io"],
|
|
23431
|
+
apiBase: "https://sentry.io",
|
|
23432
|
+
defaultDataClasses: ["source", "telemetry"],
|
|
23433
|
+
sdks: {
|
|
23434
|
+
npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
|
|
23435
|
+
pypi: ["sentry-sdk"],
|
|
23436
|
+
go: ["github.com/getsentry/sentry-go"],
|
|
23437
|
+
maven: ["io.sentry"],
|
|
23438
|
+
rubygems: ["sentry-ruby"],
|
|
23439
|
+
cargo: ["sentry"],
|
|
23440
|
+
composer: ["sentry/sentry"],
|
|
23441
|
+
nuget: ["Sentry"]
|
|
23442
|
+
}
|
|
23443
|
+
},
|
|
23444
|
+
{
|
|
23445
|
+
id: "openai",
|
|
23446
|
+
name: "OpenAI",
|
|
23447
|
+
category: "LLM provider",
|
|
23448
|
+
hostSuffixes: ["openai.com"],
|
|
23449
|
+
apiBase: "https://api.openai.com",
|
|
23450
|
+
defaultDataClasses: ["pii", "source"],
|
|
23451
|
+
sdks: {
|
|
23452
|
+
npm: ["openai"],
|
|
23453
|
+
pypi: ["openai"],
|
|
23454
|
+
go: ["github.com/sashabaranov/go-openai"],
|
|
23455
|
+
maven: ["com.openai"],
|
|
23456
|
+
rubygems: ["ruby-openai"],
|
|
23457
|
+
cargo: ["async-openai"],
|
|
23458
|
+
composer: ["openai-php/client"],
|
|
23459
|
+
nuget: ["OpenAI"]
|
|
23460
|
+
}
|
|
23461
|
+
},
|
|
23462
|
+
{
|
|
23463
|
+
id: "anthropic",
|
|
23464
|
+
name: "Anthropic",
|
|
23465
|
+
category: "LLM provider",
|
|
23466
|
+
hostSuffixes: ["anthropic.com"],
|
|
23467
|
+
apiBase: "https://api.anthropic.com",
|
|
23468
|
+
defaultDataClasses: ["pii", "source"],
|
|
23469
|
+
sdks: {
|
|
23470
|
+
npm: ["@anthropic-ai/sdk"],
|
|
23471
|
+
pypi: ["anthropic"],
|
|
23472
|
+
go: ["github.com/anthropics/anthropic-sdk-go"],
|
|
23473
|
+
nuget: ["Anthropic.SDK"]
|
|
23474
|
+
}
|
|
23475
|
+
},
|
|
23476
|
+
{
|
|
23477
|
+
id: "aws",
|
|
23478
|
+
name: "Amazon Web Services",
|
|
23479
|
+
category: "Cloud platform",
|
|
23480
|
+
hostSuffixes: ["amazonaws.com"],
|
|
23481
|
+
apiBase: "https://s3.amazonaws.com",
|
|
23482
|
+
defaultDataClasses: ["secrets", "customer"],
|
|
23483
|
+
sdks: {
|
|
23484
|
+
npm: ["@aws-sdk/client-s3", "aws-sdk"],
|
|
23485
|
+
pypi: ["boto3"],
|
|
23486
|
+
go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
|
|
23487
|
+
maven: ["com.amazonaws", "software.amazon.awssdk"],
|
|
23488
|
+
rubygems: ["aws-sdk-s3"],
|
|
23489
|
+
cargo: ["aws-sdk-s3"],
|
|
23490
|
+
nuget: ["AWSSDK.S3"]
|
|
23491
|
+
}
|
|
23492
|
+
},
|
|
23493
|
+
{
|
|
23494
|
+
id: "gcp",
|
|
23495
|
+
name: "Google Cloud",
|
|
23496
|
+
category: "Cloud platform",
|
|
23497
|
+
hostSuffixes: ["googleapis.com"],
|
|
23498
|
+
apiBase: "https://storage.googleapis.com",
|
|
23499
|
+
defaultDataClasses: ["customer", "logs"],
|
|
23500
|
+
sdks: {
|
|
23501
|
+
npm: ["@google-cloud/storage"],
|
|
23502
|
+
pypi: ["google-cloud-storage"],
|
|
23503
|
+
go: ["cloud.google.com/go"],
|
|
23504
|
+
maven: ["com.google.cloud"],
|
|
23505
|
+
rubygems: ["google-cloud-storage"],
|
|
23506
|
+
nuget: ["Google.Cloud.Storage.V1"]
|
|
23507
|
+
}
|
|
23508
|
+
},
|
|
23509
|
+
{
|
|
23510
|
+
id: "azure",
|
|
23511
|
+
name: "Microsoft Azure",
|
|
23512
|
+
category: "Cloud platform",
|
|
23513
|
+
hostSuffixes: ["azure.com", "windows.net"],
|
|
23514
|
+
apiBase: "https://management.azure.com",
|
|
23515
|
+
defaultDataClasses: ["customer", "logs"],
|
|
23516
|
+
sdks: {
|
|
23517
|
+
npm: ["@azure/storage-blob"],
|
|
23518
|
+
pypi: ["azure-storage-blob"],
|
|
23519
|
+
go: ["github.com/Azure/azure-sdk-for-go"],
|
|
23520
|
+
maven: ["com.azure"],
|
|
23521
|
+
rubygems: ["azure-storage-blob"],
|
|
23522
|
+
nuget: ["Azure.Storage.Blobs"]
|
|
23523
|
+
}
|
|
23524
|
+
},
|
|
23525
|
+
{
|
|
23526
|
+
id: "slack",
|
|
23527
|
+
name: "Slack",
|
|
23528
|
+
category: "Notifications",
|
|
23529
|
+
hostSuffixes: ["slack.com"],
|
|
23530
|
+
apiBase: "https://slack.com/api",
|
|
23531
|
+
defaultDataClasses: ["logs"],
|
|
23532
|
+
sdks: {
|
|
23533
|
+
npm: ["@slack/web-api"],
|
|
23534
|
+
pypi: ["slack-sdk"],
|
|
23535
|
+
go: ["github.com/slack-go/slack"],
|
|
23536
|
+
maven: ["com.slack.api"],
|
|
23537
|
+
rubygems: ["slack-ruby-client"],
|
|
23538
|
+
composer: ["slack-php/slack-api"],
|
|
23539
|
+
nuget: ["SlackNet"]
|
|
23540
|
+
}
|
|
23541
|
+
},
|
|
23542
|
+
{
|
|
23543
|
+
id: "segment",
|
|
23544
|
+
name: "Segment",
|
|
23545
|
+
category: "Analytics",
|
|
23546
|
+
hostSuffixes: ["segment.io", "segment.com"],
|
|
23547
|
+
apiBase: "https://api.segment.io",
|
|
23548
|
+
defaultDataClasses: ["customer"],
|
|
23549
|
+
sdks: {
|
|
23550
|
+
npm: ["@segment/analytics-node", "analytics-node"],
|
|
23551
|
+
pypi: ["segment-analytics-python"],
|
|
23552
|
+
go: ["github.com/segmentio/analytics-go"],
|
|
23553
|
+
maven: ["com.segment.analytics.java"],
|
|
23554
|
+
rubygems: ["analytics-ruby"],
|
|
23555
|
+
nuget: ["Analytics"]
|
|
23556
|
+
}
|
|
23557
|
+
},
|
|
23558
|
+
{
|
|
23559
|
+
id: "twilio",
|
|
23560
|
+
name: "Twilio",
|
|
23561
|
+
category: "Communications",
|
|
23562
|
+
hostSuffixes: ["twilio.com"],
|
|
23563
|
+
apiBase: "https://api.twilio.com",
|
|
23564
|
+
defaultDataClasses: ["pii", "customer"],
|
|
23565
|
+
sdks: {
|
|
23566
|
+
npm: ["twilio"],
|
|
23567
|
+
pypi: ["twilio"],
|
|
23568
|
+
go: ["github.com/twilio/twilio-go"],
|
|
23569
|
+
maven: ["com.twilio.sdk"],
|
|
23570
|
+
rubygems: ["twilio-ruby"],
|
|
23571
|
+
composer: ["twilio/sdk"],
|
|
23572
|
+
nuget: ["Twilio"]
|
|
23573
|
+
}
|
|
23574
|
+
},
|
|
23575
|
+
{
|
|
23576
|
+
id: "sendgrid",
|
|
23577
|
+
name: "SendGrid",
|
|
23578
|
+
category: "Email",
|
|
23579
|
+
hostSuffixes: ["sendgrid.com"],
|
|
23580
|
+
apiBase: "https://api.sendgrid.com",
|
|
23581
|
+
defaultDataClasses: ["pii"],
|
|
23582
|
+
sdks: {
|
|
23583
|
+
npm: ["@sendgrid/mail"],
|
|
23584
|
+
pypi: ["sendgrid"],
|
|
23585
|
+
go: ["github.com/sendgrid/sendgrid-go"],
|
|
23586
|
+
maven: ["com.sendgrid"],
|
|
23587
|
+
rubygems: ["sendgrid-ruby"],
|
|
23588
|
+
composer: ["sendgrid/sendgrid"],
|
|
23589
|
+
nuget: ["SendGrid"]
|
|
23590
|
+
}
|
|
23591
|
+
},
|
|
23592
|
+
{
|
|
23593
|
+
id: "mailgun",
|
|
23594
|
+
name: "Mailgun",
|
|
23595
|
+
category: "Email",
|
|
23596
|
+
hostSuffixes: ["mailgun.net"],
|
|
23597
|
+
apiBase: "https://api.mailgun.net",
|
|
23598
|
+
defaultDataClasses: ["pii"],
|
|
23599
|
+
sdks: {
|
|
23600
|
+
npm: ["mailgun.js"],
|
|
23601
|
+
pypi: ["mailgun"],
|
|
23602
|
+
rubygems: ["mailgun-ruby"],
|
|
23603
|
+
composer: ["mailgun/mailgun-php"],
|
|
23604
|
+
nuget: ["Mailgun"]
|
|
23605
|
+
}
|
|
23606
|
+
},
|
|
23607
|
+
{
|
|
23608
|
+
id: "mixpanel",
|
|
23609
|
+
name: "Mixpanel",
|
|
23610
|
+
category: "Analytics",
|
|
23611
|
+
hostSuffixes: ["mixpanel.com"],
|
|
23612
|
+
apiBase: "https://api.mixpanel.com",
|
|
23613
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
23614
|
+
sdks: {
|
|
23615
|
+
npm: ["mixpanel"],
|
|
23616
|
+
pypi: ["mixpanel"],
|
|
23617
|
+
rubygems: ["mixpanel-ruby"],
|
|
23618
|
+
nuget: ["Mixpanel"]
|
|
23619
|
+
}
|
|
23620
|
+
},
|
|
23621
|
+
{
|
|
23622
|
+
id: "amplitude",
|
|
23623
|
+
name: "Amplitude",
|
|
23624
|
+
category: "Analytics",
|
|
23625
|
+
hostSuffixes: ["amplitude.com"],
|
|
23626
|
+
apiBase: "https://api2.amplitude.com",
|
|
23627
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
23628
|
+
sdks: {
|
|
23629
|
+
npm: ["@amplitude/analytics-node"],
|
|
23630
|
+
pypi: ["amplitude-analytics"],
|
|
23631
|
+
nuget: ["Amplitude"]
|
|
23632
|
+
}
|
|
23633
|
+
},
|
|
23634
|
+
{
|
|
23635
|
+
id: "posthog",
|
|
23636
|
+
name: "PostHog",
|
|
23637
|
+
category: "Analytics",
|
|
23638
|
+
hostSuffixes: ["posthog.com"],
|
|
23639
|
+
apiBase: "https://us.i.posthog.com",
|
|
23640
|
+
defaultDataClasses: ["customer", "telemetry"],
|
|
23641
|
+
sdks: {
|
|
23642
|
+
npm: ["posthog-node", "posthog-js"],
|
|
23643
|
+
pypi: ["posthog"],
|
|
23644
|
+
go: ["github.com/posthog/posthog-go"],
|
|
23645
|
+
rubygems: ["posthog-ruby"],
|
|
23646
|
+
composer: ["posthog/posthog-php"],
|
|
23647
|
+
nuget: ["PostHog"]
|
|
23648
|
+
}
|
|
23649
|
+
},
|
|
23650
|
+
{
|
|
23651
|
+
id: "honeycomb",
|
|
23652
|
+
name: "Honeycomb",
|
|
23653
|
+
category: "Observability",
|
|
23654
|
+
hostSuffixes: ["honeycomb.io"],
|
|
23655
|
+
apiBase: "https://api.honeycomb.io",
|
|
23656
|
+
defaultDataClasses: ["telemetry", "metrics"],
|
|
23657
|
+
sdks: {
|
|
23658
|
+
npm: ["libhoney"],
|
|
23659
|
+
pypi: ["libhoney"],
|
|
23660
|
+
go: ["github.com/honeycombio/libhoney-go"],
|
|
23661
|
+
rubygems: ["libhoney"]
|
|
23662
|
+
}
|
|
23663
|
+
},
|
|
23664
|
+
{
|
|
23665
|
+
id: "grafana",
|
|
23666
|
+
name: "Grafana Cloud",
|
|
23667
|
+
category: "Observability",
|
|
23668
|
+
hostSuffixes: ["grafana.net"],
|
|
23669
|
+
apiBase: "https://grafana.net",
|
|
23670
|
+
defaultDataClasses: ["logs", "metrics"],
|
|
23671
|
+
sdks: {
|
|
23672
|
+
npm: ["@grafana/faro-web-sdk"]
|
|
23673
|
+
}
|
|
23674
|
+
},
|
|
23675
|
+
{
|
|
23676
|
+
id: "splunk",
|
|
23677
|
+
name: "Splunk",
|
|
23678
|
+
category: "Observability",
|
|
23679
|
+
hostSuffixes: ["splunkcloud.com", "splunk.com"],
|
|
23680
|
+
apiBase: "https://http-inputs.splunkcloud.com",
|
|
23681
|
+
defaultDataClasses: ["logs"],
|
|
23682
|
+
sdks: {
|
|
23683
|
+
npm: ["splunk-logging"],
|
|
23684
|
+
pypi: ["splunk-sdk"],
|
|
23685
|
+
maven: ["com.splunk"],
|
|
23686
|
+
nuget: ["Splunk.Logging.Common"]
|
|
23687
|
+
}
|
|
23688
|
+
},
|
|
23689
|
+
{
|
|
23690
|
+
id: "pagerduty",
|
|
23691
|
+
name: "PagerDuty",
|
|
23692
|
+
category: "Incident response",
|
|
23693
|
+
hostSuffixes: ["pagerduty.com"],
|
|
23694
|
+
apiBase: "https://api.pagerduty.com",
|
|
23695
|
+
defaultDataClasses: ["logs"],
|
|
23696
|
+
sdks: {
|
|
23697
|
+
npm: ["@pagerduty/pdjs"],
|
|
23698
|
+
pypi: ["pdpyras"],
|
|
23699
|
+
go: ["github.com/PagerDuty/go-pagerduty"],
|
|
23700
|
+
rubygems: ["pagerduty"]
|
|
23701
|
+
}
|
|
23702
|
+
},
|
|
23703
|
+
{
|
|
23704
|
+
id: "github",
|
|
23705
|
+
name: "GitHub",
|
|
23706
|
+
category: "Developer platform",
|
|
23707
|
+
hostSuffixes: ["github.com", "githubusercontent.com"],
|
|
23708
|
+
apiBase: "https://api.github.com",
|
|
23709
|
+
defaultDataClasses: ["source"],
|
|
23710
|
+
sdks: {
|
|
23711
|
+
npm: ["@octokit/rest", "octokit"],
|
|
23712
|
+
pypi: ["pygithub"],
|
|
23713
|
+
go: ["github.com/google/go-github"],
|
|
23714
|
+
maven: ["org.kohsuke.github-api"],
|
|
23715
|
+
rubygems: ["octokit"],
|
|
23716
|
+
cargo: ["octocrab"],
|
|
23717
|
+
composer: ["knplabs/github-api"],
|
|
23718
|
+
nuget: ["Octokit"]
|
|
23719
|
+
}
|
|
23720
|
+
},
|
|
23721
|
+
{
|
|
23722
|
+
id: "gitlab",
|
|
23723
|
+
name: "GitLab",
|
|
23724
|
+
category: "Developer platform",
|
|
23725
|
+
hostSuffixes: ["gitlab.com"],
|
|
23726
|
+
apiBase: "https://gitlab.com/api",
|
|
23727
|
+
defaultDataClasses: ["source"],
|
|
23728
|
+
sdks: {
|
|
23729
|
+
npm: ["@gitbeaker/rest"],
|
|
23730
|
+
pypi: ["python-gitlab"],
|
|
23731
|
+
go: ["gitlab.com/gitlab-org/api/client-go"],
|
|
23732
|
+
rubygems: ["gitlab"],
|
|
23733
|
+
nuget: ["GitLabApiClient"]
|
|
23734
|
+
}
|
|
23735
|
+
},
|
|
23736
|
+
{
|
|
23737
|
+
id: "auth0",
|
|
23738
|
+
name: "Auth0",
|
|
23739
|
+
category: "Identity",
|
|
23740
|
+
hostSuffixes: ["auth0.com"],
|
|
23741
|
+
apiBase: "https://login.auth0.com",
|
|
23742
|
+
defaultDataClasses: ["pii"],
|
|
23743
|
+
sdks: {
|
|
23744
|
+
npm: ["auth0"],
|
|
23745
|
+
pypi: ["auth0-python"],
|
|
23746
|
+
go: ["github.com/auth0/go-auth0"],
|
|
23747
|
+
maven: ["com.auth0"],
|
|
23748
|
+
rubygems: ["auth0"],
|
|
23749
|
+
composer: ["auth0/auth0-php"],
|
|
23750
|
+
nuget: ["Auth0.ManagementApi"]
|
|
23751
|
+
}
|
|
23752
|
+
},
|
|
23753
|
+
{
|
|
23754
|
+
id: "okta",
|
|
23755
|
+
name: "Okta",
|
|
23756
|
+
category: "Identity",
|
|
23757
|
+
hostSuffixes: ["okta.com", "oktapreview.com"],
|
|
23758
|
+
apiBase: "https://login.okta.com",
|
|
23759
|
+
defaultDataClasses: ["pii"],
|
|
23760
|
+
sdks: {
|
|
23761
|
+
npm: ["@okta/okta-sdk-nodejs"],
|
|
23762
|
+
pypi: ["okta"],
|
|
23763
|
+
go: ["github.com/okta/okta-sdk-golang"],
|
|
23764
|
+
maven: ["com.okta.sdk"],
|
|
23765
|
+
nuget: ["Okta.Sdk"]
|
|
23766
|
+
}
|
|
23767
|
+
},
|
|
23768
|
+
{
|
|
23769
|
+
id: "clerk",
|
|
23770
|
+
name: "Clerk",
|
|
23771
|
+
category: "Identity",
|
|
23772
|
+
hostSuffixes: ["clerk.com", "clerk.dev"],
|
|
23773
|
+
apiBase: "https://api.clerk.com",
|
|
23774
|
+
defaultDataClasses: ["pii"],
|
|
23775
|
+
sdks: {
|
|
23776
|
+
npm: ["@clerk/backend", "@clerk/nextjs"],
|
|
23777
|
+
pypi: ["clerk-backend-api"],
|
|
23778
|
+
go: ["github.com/clerk/clerk-sdk-go"]
|
|
23779
|
+
}
|
|
23780
|
+
},
|
|
23781
|
+
{
|
|
23782
|
+
id: "supabase",
|
|
23783
|
+
name: "Supabase",
|
|
23784
|
+
category: "Backend platform",
|
|
23785
|
+
hostSuffixes: ["supabase.co", "supabase.com"],
|
|
23786
|
+
apiBase: "https://api.supabase.com",
|
|
23787
|
+
defaultDataClasses: ["pii", "customer"],
|
|
23788
|
+
sdks: {
|
|
23789
|
+
npm: ["@supabase/supabase-js"],
|
|
23790
|
+
pypi: ["supabase"],
|
|
23791
|
+
cargo: ["postgrest"]
|
|
23792
|
+
}
|
|
23793
|
+
},
|
|
23794
|
+
{
|
|
23795
|
+
id: "firebase",
|
|
23796
|
+
name: "Firebase",
|
|
23797
|
+
category: "Backend platform",
|
|
23798
|
+
hostSuffixes: ["firebaseio.com", "firebase.google.com"],
|
|
23799
|
+
apiBase: "https://firebaseio.com",
|
|
23800
|
+
defaultDataClasses: ["customer"],
|
|
23801
|
+
sdks: {
|
|
23802
|
+
npm: ["firebase", "firebase-admin"],
|
|
23803
|
+
pypi: ["firebase-admin"],
|
|
23804
|
+
go: ["firebase.google.com/go"],
|
|
23805
|
+
maven: ["com.google.firebase"]
|
|
23806
|
+
}
|
|
23807
|
+
},
|
|
23808
|
+
{
|
|
23809
|
+
id: "mongodb-atlas",
|
|
23810
|
+
name: "MongoDB Atlas",
|
|
23811
|
+
category: "Database SaaS",
|
|
23812
|
+
hostSuffixes: ["mongodb.net", "mongodb.com"],
|
|
23813
|
+
apiBase: "https://cloud.mongodb.com",
|
|
23814
|
+
defaultDataClasses: ["customer"],
|
|
23815
|
+
sdks: {
|
|
23816
|
+
npm: ["mongodb"],
|
|
23817
|
+
pypi: ["pymongo"],
|
|
23818
|
+
go: ["go.mongodb.org/mongo-driver"],
|
|
23819
|
+
maven: ["org.mongodb"],
|
|
23820
|
+
rubygems: ["mongo"],
|
|
23821
|
+
cargo: ["mongodb"],
|
|
23822
|
+
nuget: ["MongoDB.Driver"]
|
|
23823
|
+
}
|
|
23824
|
+
},
|
|
23825
|
+
{
|
|
23826
|
+
id: "planetscale",
|
|
23827
|
+
name: "PlanetScale",
|
|
23828
|
+
category: "Database SaaS",
|
|
23829
|
+
hostSuffixes: ["psdb.cloud", "planetscale.com"],
|
|
23830
|
+
apiBase: "https://api.planetscale.com",
|
|
23831
|
+
defaultDataClasses: ["customer"],
|
|
23832
|
+
sdks: {
|
|
23833
|
+
npm: ["@planetscale/database"],
|
|
23834
|
+
go: ["github.com/planetscale/planetscale-go"]
|
|
23835
|
+
}
|
|
23836
|
+
},
|
|
23837
|
+
{
|
|
23838
|
+
id: "algolia",
|
|
23839
|
+
name: "Algolia",
|
|
23840
|
+
category: "Search SaaS",
|
|
23841
|
+
hostSuffixes: ["algolia.net", "algolianet.com"],
|
|
23842
|
+
apiBase: "https://algolia.net",
|
|
23843
|
+
defaultDataClasses: ["customer"],
|
|
23844
|
+
sdks: {
|
|
23845
|
+
npm: ["algoliasearch"],
|
|
23846
|
+
pypi: ["algoliasearch"],
|
|
23847
|
+
go: ["github.com/algolia/algoliasearch-client-go"],
|
|
23848
|
+
maven: ["com.algolia"],
|
|
23849
|
+
rubygems: ["algolia"],
|
|
23850
|
+
composer: ["algolia/algoliasearch-client-php"],
|
|
23851
|
+
nuget: ["Algolia.Search"]
|
|
23852
|
+
}
|
|
23853
|
+
},
|
|
23854
|
+
{
|
|
23855
|
+
id: "cloudflare",
|
|
23856
|
+
name: "Cloudflare",
|
|
23857
|
+
category: "CDN / edge",
|
|
23858
|
+
hostSuffixes: ["cloudflare.com", "workers.dev"],
|
|
23859
|
+
apiBase: "https://api.cloudflare.com",
|
|
23860
|
+
defaultDataClasses: ["logs"],
|
|
23861
|
+
sdks: {
|
|
23862
|
+
npm: ["cloudflare"],
|
|
23863
|
+
pypi: ["cloudflare"],
|
|
23864
|
+
go: ["github.com/cloudflare/cloudflare-go"],
|
|
23865
|
+
nuget: ["CloudFlare.Client"]
|
|
23866
|
+
}
|
|
23867
|
+
},
|
|
23868
|
+
{
|
|
23869
|
+
id: "huggingface",
|
|
23870
|
+
name: "Hugging Face",
|
|
23871
|
+
category: "LLM provider",
|
|
23872
|
+
hostSuffixes: ["huggingface.co"],
|
|
23873
|
+
apiBase: "https://api-inference.huggingface.co",
|
|
23874
|
+
defaultDataClasses: ["source"],
|
|
23875
|
+
sdks: {
|
|
23876
|
+
npm: ["@huggingface/inference"],
|
|
23877
|
+
pypi: ["huggingface-hub", "transformers"],
|
|
23878
|
+
rubygems: ["hugging-face"]
|
|
23879
|
+
}
|
|
23880
|
+
},
|
|
23881
|
+
{
|
|
23882
|
+
id: "cohere",
|
|
23883
|
+
name: "Cohere",
|
|
23884
|
+
category: "LLM provider",
|
|
23885
|
+
hostSuffixes: ["cohere.com", "cohere.ai"],
|
|
23886
|
+
apiBase: "https://api.cohere.com",
|
|
23887
|
+
defaultDataClasses: ["pii", "source"],
|
|
23888
|
+
sdks: {
|
|
23889
|
+
npm: ["cohere-ai"],
|
|
23890
|
+
pypi: ["cohere"],
|
|
23891
|
+
go: ["github.com/cohere-ai/cohere-go"]
|
|
23892
|
+
}
|
|
23893
|
+
},
|
|
23894
|
+
{
|
|
23895
|
+
id: "mistral",
|
|
23896
|
+
name: "Mistral AI",
|
|
23897
|
+
category: "LLM provider",
|
|
23898
|
+
hostSuffixes: ["mistral.ai"],
|
|
23899
|
+
apiBase: "https://api.mistral.ai",
|
|
23900
|
+
defaultDataClasses: ["pii", "source"],
|
|
23901
|
+
sdks: {
|
|
23902
|
+
npm: ["@mistralai/mistralai"],
|
|
23903
|
+
pypi: ["mistralai"],
|
|
23904
|
+
go: ["github.com/gage-technologies/mistral-go"]
|
|
23905
|
+
}
|
|
23906
|
+
}
|
|
23907
|
+
];
|
|
23908
|
+
var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
|
|
23909
|
+
${JSON.stringify(PROVIDER_REGISTRY)}`;
|
|
23910
|
+
var INTERNAL_TLDS = ["internal", "local", "corp", "lan", "intranet", "home.arpa"];
|
|
23911
|
+
var EXCLUDED_HOST_SUFFIXES = [
|
|
23912
|
+
"localhost",
|
|
23913
|
+
"test",
|
|
23914
|
+
"example",
|
|
23915
|
+
"invalid",
|
|
23916
|
+
"example.com",
|
|
23917
|
+
"example.org",
|
|
23918
|
+
"example.net",
|
|
23919
|
+
"w3.org",
|
|
23920
|
+
"schemas.openxmlformats.org",
|
|
23921
|
+
"schemas.microsoft.com",
|
|
23922
|
+
"schemas.android.com",
|
|
23923
|
+
"maven.apache.org"
|
|
23924
|
+
];
|
|
23925
|
+
function hostMatchesSuffix(host, suffix) {
|
|
23926
|
+
return host === suffix || host.endsWith(`.${suffix}`);
|
|
23927
|
+
}
|
|
23928
|
+
function isValidIPv4(host) {
|
|
23929
|
+
const parts = host.split(".");
|
|
23930
|
+
if (parts.length !== 4) return false;
|
|
23931
|
+
return parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
23932
|
+
}
|
|
23933
|
+
function isPrivateOrReservedIPv4(host) {
|
|
23934
|
+
const octets = host.split(".").map(Number);
|
|
23935
|
+
const a = octets[0];
|
|
23936
|
+
const b = octets[1];
|
|
23937
|
+
if (a === void 0 || b === void 0) return false;
|
|
23938
|
+
if (a === 0 || a === 10 || a === 127 || a >= 224) return true;
|
|
23939
|
+
if (a === 169 && b === 254) return true;
|
|
23940
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
23941
|
+
if (a === 192 && b === 168) return true;
|
|
23942
|
+
return false;
|
|
23943
|
+
}
|
|
23944
|
+
function normalizeIPv6Literal(host) {
|
|
23945
|
+
const unbracketed = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
23946
|
+
const zoneIndex = unbracketed.indexOf("%");
|
|
23947
|
+
const unzoned = zoneIndex === -1 ? unbracketed : unbracketed.slice(0, zoneIndex);
|
|
23948
|
+
return unzoned.toLowerCase();
|
|
23949
|
+
}
|
|
23950
|
+
var IPV6_HEX_GROUP = /^[0-9a-f]{1,4}$/;
|
|
23951
|
+
function isValidIPv6(host) {
|
|
23952
|
+
const h = normalizeIPv6Literal(host);
|
|
23953
|
+
if (!h.includes(":")) return false;
|
|
23954
|
+
const collapsedHalves = h.split("::");
|
|
23955
|
+
if (collapsedHalves.length > 2) return false;
|
|
23956
|
+
const sides = collapsedHalves.length === 2 ? collapsedHalves : [h];
|
|
23957
|
+
const groups = sides.flatMap((side) => side === "" ? [] : side.split(":"));
|
|
23958
|
+
if (!groups.every((g) => IPV6_HEX_GROUP.test(g))) return false;
|
|
23959
|
+
return collapsedHalves.length === 2 ? groups.length <= 7 : groups.length === 8;
|
|
23960
|
+
}
|
|
23961
|
+
function isPrivateOrReservedIPv6(host) {
|
|
23962
|
+
const h = normalizeIPv6Literal(host);
|
|
23963
|
+
if (h === "::" || h === "::1") return true;
|
|
23964
|
+
const firstGroup = (h.split(":")[0] ?? "").padStart(4, "0");
|
|
23965
|
+
if (firstGroup.startsWith("fc") || firstGroup.startsWith("fd")) return true;
|
|
23966
|
+
if (firstGroup.startsWith("fe") && "89ab".includes(firstGroup[2] ?? "")) return true;
|
|
23967
|
+
if (firstGroup.startsWith("ff")) return true;
|
|
23968
|
+
return false;
|
|
23969
|
+
}
|
|
23970
|
+
var IPV4_MAPPED_IPV6 = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/;
|
|
23971
|
+
function ipv4MappedAddress(host) {
|
|
23972
|
+
const match = IPV4_MAPPED_IPV6.exec(normalizeIPv6Literal(host));
|
|
23973
|
+
return match?.[1] ?? null;
|
|
23974
|
+
}
|
|
23975
|
+
function resolveHost(host, opts) {
|
|
23976
|
+
const h = host.toLowerCase();
|
|
23977
|
+
if (isValidIPv4(h)) {
|
|
23978
|
+
if (isPrivateOrReservedIPv4(h)) return null;
|
|
23979
|
+
return { kind: "ip", trust: "ip", name: h, category: "Unresolved host", entry: null };
|
|
23980
|
+
}
|
|
23981
|
+
if (isValidIPv6(h)) {
|
|
23982
|
+
if (isPrivateOrReservedIPv6(h)) return null;
|
|
23983
|
+
return { kind: "ip", trust: "ip", name: h, category: "Unresolved host", entry: null };
|
|
23984
|
+
}
|
|
23985
|
+
const mappedIPv4 = ipv4MappedAddress(h);
|
|
23986
|
+
if (mappedIPv4 !== null && isValidIPv4(mappedIPv4)) {
|
|
23987
|
+
if (isPrivateOrReservedIPv4(mappedIPv4)) return null;
|
|
23988
|
+
return { kind: "ip", trust: "ip", name: h, category: "Unresolved host", entry: null };
|
|
23989
|
+
}
|
|
23990
|
+
if (EXCLUDED_HOST_SUFFIXES.some((suffix) => hostMatchesSuffix(h, suffix))) return null;
|
|
23991
|
+
const entry = PROVIDER_REGISTRY.find(
|
|
23992
|
+
(p) => p.hostSuffixes.some((suffix) => hostMatchesSuffix(h, suffix))
|
|
23993
|
+
);
|
|
23994
|
+
if (entry) {
|
|
23995
|
+
return {
|
|
23996
|
+
kind: "provider",
|
|
23997
|
+
trust: "recognized",
|
|
23998
|
+
name: entry.name,
|
|
23999
|
+
category: entry.category,
|
|
24000
|
+
entry
|
|
24001
|
+
};
|
|
24002
|
+
}
|
|
24003
|
+
const internalDomains = opts?.internalDomains ?? [];
|
|
24004
|
+
const isInternal = !h.includes(".") && !h.includes(":") || INTERNAL_TLDS.some((tld) => hostMatchesSuffix(h, tld)) || internalDomains.some((domain2) => hostMatchesSuffix(h, domain2.toLowerCase()));
|
|
24005
|
+
if (isInternal) {
|
|
24006
|
+
return {
|
|
24007
|
+
kind: "internal",
|
|
24008
|
+
trust: "internal",
|
|
24009
|
+
name: h,
|
|
24010
|
+
category: "Internal services",
|
|
24011
|
+
entry: null
|
|
24012
|
+
};
|
|
24013
|
+
}
|
|
24014
|
+
return {
|
|
24015
|
+
kind: "external",
|
|
24016
|
+
trust: "unverified",
|
|
24017
|
+
name: h,
|
|
24018
|
+
category: "External domain",
|
|
24019
|
+
entry: null
|
|
24020
|
+
};
|
|
24021
|
+
}
|
|
24022
|
+
function resolveSdk(ecosystem, pkg) {
|
|
24023
|
+
for (const entry of PROVIDER_REGISTRY) {
|
|
24024
|
+
const idents = entry.sdks[ecosystem];
|
|
24025
|
+
if (idents === void 0) continue;
|
|
24026
|
+
if (idents.some((ident) => sdkMatches(ecosystem, pkg, ident))) return entry;
|
|
24027
|
+
}
|
|
24028
|
+
return null;
|
|
24029
|
+
}
|
|
24030
|
+
function sdkMatches(ecosystem, pkg, ident) {
|
|
24031
|
+
switch (ecosystem) {
|
|
24032
|
+
case "nuget":
|
|
24033
|
+
return pkg.toLowerCase() === ident.toLowerCase();
|
|
24034
|
+
case "pypi":
|
|
24035
|
+
return normalizePypi(pkg) === normalizePypi(ident);
|
|
24036
|
+
case "go":
|
|
24037
|
+
return pkg === ident || pkg.startsWith(`${ident}/`);
|
|
24038
|
+
case "maven":
|
|
24039
|
+
return pkg === ident || pkg.startsWith(`${ident}.`);
|
|
24040
|
+
default:
|
|
24041
|
+
return pkg === ident;
|
|
24042
|
+
}
|
|
24043
|
+
}
|
|
24044
|
+
function normalizePypi(name) {
|
|
24045
|
+
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
24046
|
+
}
|
|
24047
|
+
|
|
24048
|
+
// ../../packages/detections/src/egress/extract.ts
|
|
24049
|
+
var EGRESS_CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
24050
|
+
".ts",
|
|
24051
|
+
".tsx",
|
|
24052
|
+
".js",
|
|
24053
|
+
".jsx",
|
|
24054
|
+
".mjs",
|
|
24055
|
+
".cjs",
|
|
24056
|
+
".py",
|
|
24057
|
+
".java",
|
|
24058
|
+
".rb",
|
|
24059
|
+
".cs",
|
|
24060
|
+
".php",
|
|
24061
|
+
".go",
|
|
24062
|
+
".rs"
|
|
24063
|
+
]);
|
|
24064
|
+
var SNIPPET_MAX = 200;
|
|
24065
|
+
var MASK = "\u2022\u2022\u2022\u2022";
|
|
24066
|
+
var URL_CANDIDATE = /(https?|wss?|sftp|grpcs?|smtp):\/\/(?:[^\s'"`<>()[\]{},;]|\$\{[^}\s]{1,64}\}|\{[A-Za-z_]\w{0,63}\}|%[sd])+/gi;
|
|
24067
|
+
var PLACEHOLDER = /\$\{[^}\s]{1,64}\}|\{[A-Za-z_]\w{0,63}\}|%[sd]/g;
|
|
24068
|
+
var VAR_TOKEN = "${var}";
|
|
24069
|
+
var VAR_SENTINEL = "akaegressvar0";
|
|
24070
|
+
var TRAILING_PUNCTUATION = /[.,;:'"]+$/;
|
|
24071
|
+
var TRANSPORT_BY_SCHEME = {
|
|
24072
|
+
http: "http",
|
|
24073
|
+
https: "https",
|
|
24074
|
+
ws: "ws",
|
|
24075
|
+
wss: "wss",
|
|
24076
|
+
sftp: "sftp",
|
|
24077
|
+
grpc: "grpc",
|
|
24078
|
+
grpcs: "grpc",
|
|
24079
|
+
smtp: "smtp"
|
|
24080
|
+
};
|
|
24081
|
+
var BEFORE_WINDOW = 200;
|
|
24082
|
+
var AFTER_WINDOW = 300;
|
|
24083
|
+
var VERB_METHOD_OPENER = /\.\s*(get|post|put|delete)\s*\(\s*['"`]*$/i;
|
|
24084
|
+
var VERB_FIRST_ARGUMENT = /["'](GET|POST|PUT|DELETE)["']\s*,([^)\]};]{0,150})$/i;
|
|
24085
|
+
var OPTIONS_METHOD = /method\s*[:=]\s*['"](GET|POST|PUT|DELETE)/i;
|
|
24086
|
+
var CLIENT_OPENER = /\b(fetch|urlopen|got|ky)\s*\(\s*['"`]*$/i;
|
|
24087
|
+
var ANY_METHOD_KEY = /\bmethod\s*[:=]/i;
|
|
24088
|
+
var STATEMENT_BREAK = /;|\n\s*(?:(?:const|let|var|val|function|func|fn|def|class|return|export|import|if|for|while|switch|try|public|private)\b|\w+\s*=[^=])/;
|
|
24089
|
+
var IPV4_CANDIDATE = /\b(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?\b/g;
|
|
24090
|
+
var IP_HOST_CONTEXT = /\b(host|hostname|server|address|endpoint|ip|url|uri)\b/i;
|
|
24091
|
+
var IP_SFTP_CONTEXT = /\bs(ftp|cp|sh)\b/i;
|
|
24092
|
+
var IP_SMTP_CONTEXT = /\b(smtp|mail)\b/i;
|
|
24093
|
+
var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
|
|
24094
|
+
var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
|
|
24095
|
+
var USERINFO = /:\/\/[^@/\s]+@/g;
|
|
24096
|
+
var SECRET_VALUE = new RegExp(
|
|
24097
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
|
|
24098
|
+
"gi"
|
|
24099
|
+
);
|
|
24100
|
+
var AUTH_SCHEME_VALUE = new RegExp(
|
|
24101
|
+
`((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
|
|
24102
|
+
"gi"
|
|
24103
|
+
);
|
|
24104
|
+
var BEARER_TOKEN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
|
|
24105
|
+
var WEBHOOK_SECRET_PATHS = [
|
|
24106
|
+
{ hosts: ["hooks.slack.com"], prefix: "/services/" },
|
|
24107
|
+
{
|
|
24108
|
+
hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
|
|
24109
|
+
prefix: "/api/webhooks/"
|
|
24110
|
+
},
|
|
24111
|
+
{ hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
|
|
24112
|
+
{ hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
|
|
24113
|
+
];
|
|
24114
|
+
function escapeRegExp(literal2) {
|
|
24115
|
+
return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24116
|
+
}
|
|
24117
|
+
var WEBHOOK_URL = new RegExp(
|
|
24118
|
+
`(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
|
|
24119
|
+
(entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
|
|
24120
|
+
).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
|
|
24121
|
+
"gi"
|
|
24122
|
+
);
|
|
24123
|
+
function maskWebhookPath(host, pathname) {
|
|
24124
|
+
for (const entry of WEBHOOK_SECRET_PATHS) {
|
|
24125
|
+
if (entry.hosts.includes(host) && pathname.startsWith(entry.prefix)) {
|
|
24126
|
+
return `${entry.prefix}${MASK}`;
|
|
24127
|
+
}
|
|
24128
|
+
}
|
|
24129
|
+
return pathname;
|
|
24130
|
+
}
|
|
24131
|
+
var VENDORED_PATH = /(^|\/)(vendor|third_party|external)\//;
|
|
24132
|
+
function isVendoredPath(file2) {
|
|
24133
|
+
return VENDORED_PATH.test(file2);
|
|
24134
|
+
}
|
|
24135
|
+
function redactLine(line) {
|
|
24136
|
+
return line.trim().replace(USERINFO, "://").replace(WEBHOOK_URL, `$1${MASK}`).replace(SECRET_VALUE, `$1${MASK}`).replace(AUTH_SCHEME_VALUE, `$1$2 ${MASK}`).replace(BEARER_TOKEN, `Bearer ${MASK}`);
|
|
24137
|
+
}
|
|
24138
|
+
function redactSnippet(line, anchor = 0) {
|
|
24139
|
+
const redacted = redactLine(line);
|
|
24140
|
+
if (redacted.length <= SNIPPET_MAX) return redacted;
|
|
24141
|
+
const lead = line.length - line.trimStart().length;
|
|
24142
|
+
const trimmed = line.trim();
|
|
24143
|
+
const mapped = redacted.length === trimmed.length ? anchor - lead : redactLine(trimmed.slice(0, Math.max(0, anchor - lead))).length;
|
|
24144
|
+
const start = Math.min(
|
|
24145
|
+
Math.max(0, mapped - Math.floor(SNIPPET_MAX / 2)),
|
|
24146
|
+
redacted.length - SNIPPET_MAX
|
|
24147
|
+
);
|
|
24148
|
+
return redacted.slice(start, start + SNIPPET_MAX);
|
|
24149
|
+
}
|
|
24150
|
+
function extractEgress(text) {
|
|
24151
|
+
const lineStarts = lineStartOffsets(text);
|
|
24152
|
+
const urlSpans = [];
|
|
24153
|
+
const hits = [];
|
|
24154
|
+
const lineTextOf = memoizeByLine((index) => lineTextAt(text, lineStarts, index));
|
|
24155
|
+
const ipContextOf = memoizeByLine((index) => ipLineContext(lineTextOf(index)));
|
|
24156
|
+
const snippetAt = (index, offset) => redactSnippet(lineTextOf(index), offset - (lineStarts[index] ?? 0));
|
|
24157
|
+
for (const match of text.matchAll(URL_CANDIDATE)) {
|
|
24158
|
+
const start = match.index;
|
|
24159
|
+
const matched = match[0];
|
|
24160
|
+
urlSpans.push([start, start + matched.length]);
|
|
24161
|
+
const scheme = match[1];
|
|
24162
|
+
if (scheme === void 0) continue;
|
|
24163
|
+
const candidate = matched.replace(TRAILING_PUNCTUATION, "");
|
|
24164
|
+
if (candidate === "") continue;
|
|
24165
|
+
const parsed = parseCandidate(candidate, scheme);
|
|
24166
|
+
if (parsed === null) continue;
|
|
24167
|
+
const index = lineIndexAt(lineStarts, start);
|
|
24168
|
+
hits.push({
|
|
24169
|
+
...parsed,
|
|
24170
|
+
method: inferMethod(text, start, start + matched.length),
|
|
24171
|
+
line: index + 1,
|
|
24172
|
+
snippet: snippetAt(index, start)
|
|
24173
|
+
});
|
|
24174
|
+
}
|
|
24175
|
+
for (const match of text.matchAll(IPV4_CANDIDATE)) {
|
|
24176
|
+
const start = match.index;
|
|
24177
|
+
if (isInsideSpan(urlSpans, start)) continue;
|
|
24178
|
+
const index = lineIndexAt(lineStarts, start);
|
|
24179
|
+
const parsed = parseBareIp(match[0], ipContextOf(index));
|
|
24180
|
+
if (parsed === null) continue;
|
|
24181
|
+
hits.push({ ...parsed, method: "REF", line: index + 1, snippet: snippetAt(index, start) });
|
|
24182
|
+
}
|
|
24183
|
+
return hits.sort(compareHits);
|
|
24184
|
+
}
|
|
24185
|
+
function parseCandidate(candidate, scheme) {
|
|
24186
|
+
const transport = TRANSPORT_BY_SCHEME[scheme.toLowerCase()];
|
|
24187
|
+
if (transport === void 0) return null;
|
|
24188
|
+
let placeholders2 = 0;
|
|
24189
|
+
const normalized = candidate.replace(PLACEHOLDER, () => {
|
|
24190
|
+
placeholders2 += 1;
|
|
24191
|
+
return VAR_TOKEN;
|
|
24192
|
+
});
|
|
24193
|
+
let parsed;
|
|
24194
|
+
try {
|
|
24195
|
+
parsed = new URL(normalized.split(VAR_TOKEN).join(VAR_SENTINEL));
|
|
24196
|
+
} catch {
|
|
24197
|
+
return null;
|
|
24198
|
+
}
|
|
24199
|
+
const host = parsed.hostname.toLowerCase();
|
|
24200
|
+
if (host === "" || host.includes(VAR_SENTINEL)) return null;
|
|
24201
|
+
const authority = parsed.host.toLowerCase();
|
|
24202
|
+
const path = maskWebhookPath(host, parsed.pathname);
|
|
24203
|
+
const url2 = `${transport}://${authority}${path}`.split(VAR_SENTINEL).join(VAR_TOKEN);
|
|
24204
|
+
return {
|
|
24205
|
+
url: url2,
|
|
24206
|
+
host,
|
|
24207
|
+
port: parsed.port === "" ? null : Number(parsed.port),
|
|
24208
|
+
transport,
|
|
24209
|
+
template: placeholders2 > 0
|
|
24210
|
+
};
|
|
24211
|
+
}
|
|
24212
|
+
function parseBareIp(candidate, context) {
|
|
24213
|
+
const [address, port] = splitPort(candidate);
|
|
24214
|
+
if (!isValidIPv4(address) || isPrivateOrReservedIPv4(address)) return null;
|
|
24215
|
+
if (!context.named) return null;
|
|
24216
|
+
const { transport } = context;
|
|
24217
|
+
return {
|
|
24218
|
+
url: `${transport}://${address}${port === null ? "" : `:${String(port)}`}`,
|
|
24219
|
+
host: address,
|
|
24220
|
+
port,
|
|
24221
|
+
transport,
|
|
24222
|
+
template: false
|
|
24223
|
+
};
|
|
24224
|
+
}
|
|
24225
|
+
function ipLineContext(line) {
|
|
24226
|
+
const context = identifierWords(line);
|
|
24227
|
+
let transport = "http";
|
|
24228
|
+
if (IP_SFTP_CONTEXT.test(context)) transport = "sftp";
|
|
24229
|
+
else if (IP_SMTP_CONTEXT.test(context)) transport = "smtp";
|
|
24230
|
+
return { named: IP_HOST_CONTEXT.test(context), transport };
|
|
24231
|
+
}
|
|
24232
|
+
function isInsideSpan(spans, offset) {
|
|
24233
|
+
let low = 0;
|
|
24234
|
+
let high = spans.length - 1;
|
|
24235
|
+
let found = -1;
|
|
24236
|
+
while (low <= high) {
|
|
24237
|
+
const mid = Math.floor((low + high) / 2);
|
|
24238
|
+
const span2 = spans[mid];
|
|
24239
|
+
if (span2 === void 0) break;
|
|
24240
|
+
if (span2[0] <= offset) {
|
|
24241
|
+
found = mid;
|
|
24242
|
+
low = mid + 1;
|
|
24243
|
+
} else {
|
|
24244
|
+
high = mid - 1;
|
|
24245
|
+
}
|
|
24246
|
+
}
|
|
24247
|
+
const span = found === -1 ? void 0 : spans[found];
|
|
24248
|
+
return span !== void 0 && offset < span[1];
|
|
24249
|
+
}
|
|
24250
|
+
function splitPort(candidate) {
|
|
24251
|
+
const colon = candidate.indexOf(":");
|
|
24252
|
+
if (colon === -1) return [candidate, null];
|
|
24253
|
+
return [candidate.slice(0, colon), Number(candidate.slice(colon + 1))];
|
|
24254
|
+
}
|
|
24255
|
+
function identifierWords(line) {
|
|
24256
|
+
return line.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
24257
|
+
}
|
|
24258
|
+
function inferMethod(text, start, end) {
|
|
24259
|
+
const before = text.slice(Math.max(0, start - BEFORE_WINDOW), start);
|
|
24260
|
+
const after = text.slice(end, end + AFTER_WINDOW);
|
|
24261
|
+
const opener = VERB_METHOD_OPENER.exec(before);
|
|
24262
|
+
if (opener?.[1] !== void 0) return verbOf(opener[1]);
|
|
24263
|
+
const firstArgument = VERB_FIRST_ARGUMENT.exec(before);
|
|
24264
|
+
if (firstArgument?.[1] !== void 0 && !STATEMENT_BREAK.test(firstArgument[2] ?? "")) {
|
|
24265
|
+
return verbOf(firstArgument[1]);
|
|
24266
|
+
}
|
|
24267
|
+
const options = OPTIONS_METHOD.exec(after);
|
|
24268
|
+
if (options?.[1] !== void 0 && !STATEMENT_BREAK.test(after.slice(0, options.index))) {
|
|
24269
|
+
return verbOf(options[1]);
|
|
24270
|
+
}
|
|
24271
|
+
if (CLIENT_OPENER.test(before)) {
|
|
24272
|
+
const close = callCloseIndex(after);
|
|
24273
|
+
if (close !== -1 && isSoleArgument(after, close) && !ANY_METHOD_KEY.test(after.slice(0, close))) {
|
|
24274
|
+
return "GET";
|
|
24275
|
+
}
|
|
24276
|
+
}
|
|
24277
|
+
return "REF";
|
|
24278
|
+
}
|
|
24279
|
+
function verbOf(raw) {
|
|
24280
|
+
return raw.toUpperCase();
|
|
24281
|
+
}
|
|
24282
|
+
function callCloseIndex(span) {
|
|
24283
|
+
let depth = 1;
|
|
24284
|
+
for (let i = 0; i < span.length; i += 1) {
|
|
24285
|
+
const char = span[i];
|
|
24286
|
+
if (char === "(") depth += 1;
|
|
24287
|
+
else if (char === ")") {
|
|
24288
|
+
depth -= 1;
|
|
24289
|
+
if (depth === 0) return i + 1;
|
|
24290
|
+
}
|
|
24291
|
+
}
|
|
24292
|
+
return -1;
|
|
24293
|
+
}
|
|
24294
|
+
function isSoleArgument(span, close) {
|
|
24295
|
+
const between = span.slice(0, close - 1).trimEnd();
|
|
24296
|
+
const withoutTrailingComma = between.endsWith(",") ? between.slice(0, -1) : between;
|
|
24297
|
+
return !withoutTrailingComma.includes(",");
|
|
24298
|
+
}
|
|
24299
|
+
function memoizeByLine(compute) {
|
|
24300
|
+
const cache = /* @__PURE__ */ new Map();
|
|
24301
|
+
return (index) => {
|
|
24302
|
+
const cached2 = cache.get(index);
|
|
24303
|
+
if (cached2 !== void 0) return cached2;
|
|
24304
|
+
const value = compute(index);
|
|
24305
|
+
cache.set(index, value);
|
|
24306
|
+
return value;
|
|
24307
|
+
};
|
|
24308
|
+
}
|
|
24309
|
+
function lineStartOffsets(text) {
|
|
24310
|
+
const starts = [0];
|
|
24311
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
24312
|
+
if (text[i] === "\n") starts.push(i + 1);
|
|
24313
|
+
}
|
|
24314
|
+
return starts;
|
|
24315
|
+
}
|
|
24316
|
+
function lineIndexAt(starts, offset) {
|
|
24317
|
+
let low = 0;
|
|
24318
|
+
let high = starts.length - 1;
|
|
24319
|
+
while (low < high) {
|
|
24320
|
+
const mid = Math.ceil((low + high) / 2);
|
|
24321
|
+
if ((starts[mid] ?? 0) <= offset) low = mid;
|
|
24322
|
+
else high = mid - 1;
|
|
24323
|
+
}
|
|
24324
|
+
return low;
|
|
24325
|
+
}
|
|
24326
|
+
function lineTextAt(text, starts, index) {
|
|
24327
|
+
const start = starts[index] ?? 0;
|
|
24328
|
+
const next = starts[index + 1];
|
|
24329
|
+
return next === void 0 ? text.slice(start) : text.slice(start, next - 1);
|
|
24330
|
+
}
|
|
24331
|
+
function compareHits(a, b) {
|
|
24332
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
24333
|
+
if (a.url !== b.url) return a.url < b.url ? -1 : 1;
|
|
24334
|
+
return 0;
|
|
24335
|
+
}
|
|
24336
|
+
|
|
24337
|
+
// ../../packages/detections/src/egress/manifests.ts
|
|
24338
|
+
var LOCKFILE_BASENAMES = /* @__PURE__ */ new Set([
|
|
24339
|
+
"package-lock.json",
|
|
24340
|
+
"yarn.lock",
|
|
24341
|
+
"pnpm-lock.yaml",
|
|
24342
|
+
"Cargo.lock",
|
|
24343
|
+
"composer.lock",
|
|
24344
|
+
"Gemfile.lock",
|
|
24345
|
+
"poetry.lock",
|
|
24346
|
+
"go.sum",
|
|
24347
|
+
"packages.lock.json"
|
|
24348
|
+
]);
|
|
24349
|
+
var EXACT_KIND_BY_BASENAME = {
|
|
24350
|
+
"package.json": "package.json",
|
|
24351
|
+
"requirements.txt": "requirements.txt",
|
|
24352
|
+
"pyproject.toml": "pyproject.toml",
|
|
24353
|
+
"go.mod": "go.mod",
|
|
24354
|
+
"pom.xml": "pom.xml",
|
|
24355
|
+
"build.gradle": "build.gradle",
|
|
24356
|
+
"build.gradle.kts": "build.gradle",
|
|
24357
|
+
Gemfile: "Gemfile",
|
|
24358
|
+
"Cargo.toml": "Cargo.toml",
|
|
24359
|
+
"composer.json": "composer.json",
|
|
24360
|
+
"packages.config": "packages.config"
|
|
24361
|
+
};
|
|
24362
|
+
function manifestKindOf(basename6) {
|
|
24363
|
+
if (LOCKFILE_BASENAMES.has(basename6)) return null;
|
|
24364
|
+
const exact = Object.hasOwn(EXACT_KIND_BY_BASENAME, basename6) ? EXACT_KIND_BY_BASENAME[basename6] : void 0;
|
|
24365
|
+
if (exact !== void 0) return exact;
|
|
24366
|
+
if (basename6.endsWith(".csproj")) return "csproj";
|
|
24367
|
+
return null;
|
|
24368
|
+
}
|
|
24369
|
+
function extractManifestSdks(text, kind) {
|
|
24370
|
+
switch (kind) {
|
|
24371
|
+
case "package.json":
|
|
24372
|
+
return extractPackageJson(text);
|
|
24373
|
+
case "requirements.txt":
|
|
24374
|
+
return extractRequirementsTxt(text);
|
|
24375
|
+
case "pyproject.toml":
|
|
24376
|
+
return extractPyprojectToml(text);
|
|
24377
|
+
case "go.mod":
|
|
24378
|
+
return extractGoMod(text);
|
|
24379
|
+
case "pom.xml":
|
|
24380
|
+
return extractPomXml(text);
|
|
24381
|
+
case "build.gradle":
|
|
24382
|
+
return extractBuildGradle(text);
|
|
24383
|
+
case "Gemfile":
|
|
24384
|
+
return extractGemfile(text);
|
|
24385
|
+
case "Cargo.toml":
|
|
24386
|
+
return extractCargoToml(text);
|
|
24387
|
+
case "composer.json":
|
|
24388
|
+
return extractComposerJson(text);
|
|
24389
|
+
case "csproj":
|
|
24390
|
+
return extractCsproj(text);
|
|
24391
|
+
case "packages.config":
|
|
24392
|
+
return extractPackagesConfig(text);
|
|
24393
|
+
default:
|
|
24394
|
+
return [];
|
|
24395
|
+
}
|
|
24396
|
+
}
|
|
24397
|
+
function makeHit(ecosystem, pkg, line, rawLine) {
|
|
24398
|
+
return { ecosystem, pkg, line, snippet: redactSnippet(rawLine) };
|
|
24399
|
+
}
|
|
24400
|
+
function extractPackageJson(text) {
|
|
24401
|
+
const parsed = parseJson(text);
|
|
24402
|
+
if (parsed === null) return [];
|
|
24403
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24404
|
+
const hits = [];
|
|
24405
|
+
for (const pkg of objectKeys(parsed.dependencies)) {
|
|
24406
|
+
seen.add(pkg);
|
|
24407
|
+
hits.push(hitAtQuotedKey("npm", pkg, text, "dependencies"));
|
|
24408
|
+
}
|
|
24409
|
+
for (const pkg of objectKeys(parsed.optionalDependencies)) {
|
|
24410
|
+
if (seen.has(pkg)) continue;
|
|
24411
|
+
seen.add(pkg);
|
|
24412
|
+
hits.push(hitAtQuotedKey("npm", pkg, text, "optionalDependencies"));
|
|
24413
|
+
}
|
|
24414
|
+
return hits;
|
|
24415
|
+
}
|
|
24416
|
+
var REQUIREMENTS_NAME = /^\s*([A-Za-z0-9][\w.-]*)/;
|
|
24417
|
+
function extractRequirementsTxt(text) {
|
|
24418
|
+
const hits = [];
|
|
24419
|
+
eachLine(text, (rawLine, lineNumber) => {
|
|
24420
|
+
const match = REQUIREMENTS_NAME.exec(rawLine);
|
|
24421
|
+
const name = match?.[1];
|
|
24422
|
+
if (name === void 0) return;
|
|
24423
|
+
hits.push(makeHit("pypi", normalizePypi(name), lineNumber, rawLine));
|
|
24424
|
+
});
|
|
24425
|
+
return hits;
|
|
24426
|
+
}
|
|
24427
|
+
var TOML_SECTION = /^\s*\[([^\]]+)\]\s*$/;
|
|
24428
|
+
var TOML_QUOTED = /"([^"]*)"|'([^']*)'/g;
|
|
24429
|
+
var PEP508_NAME = /^([A-Za-z0-9][\w.-]*)/;
|
|
24430
|
+
var POETRY_KEY = /^\s*([A-Za-z0-9][\w.-]*)\s*=/;
|
|
24431
|
+
function extractPyprojectToml(text) {
|
|
24432
|
+
const hits = [];
|
|
24433
|
+
let section = "";
|
|
24434
|
+
let inDependenciesArray = false;
|
|
24435
|
+
eachLine(text, (rawLine, lineNumber) => {
|
|
24436
|
+
const sectionMatch = TOML_SECTION.exec(rawLine);
|
|
24437
|
+
if (sectionMatch) {
|
|
24438
|
+
section = sectionMatch[1]?.trim() ?? "";
|
|
24439
|
+
inDependenciesArray = false;
|
|
24440
|
+
return;
|
|
24441
|
+
}
|
|
24442
|
+
if (!inDependenciesArray && /^\s*dependencies\s*=\s*\[/.test(rawLine)) {
|
|
24443
|
+
inDependenciesArray = true;
|
|
24444
|
+
}
|
|
24445
|
+
if (inDependenciesArray) {
|
|
24446
|
+
for (const spec of quotedStrings(rawLine)) {
|
|
24447
|
+
const name = PEP508_NAME.exec(spec)?.[1];
|
|
24448
|
+
if (name !== void 0)
|
|
24449
|
+
hits.push(makeHit("pypi", normalizePypi(name), lineNumber, rawLine));
|
|
24450
|
+
}
|
|
24451
|
+
if (rawLine.includes("]")) inDependenciesArray = false;
|
|
24452
|
+
return;
|
|
24453
|
+
}
|
|
24454
|
+
if (section === "tool.poetry.dependencies") {
|
|
24455
|
+
const key = POETRY_KEY.exec(rawLine)?.[1];
|
|
24456
|
+
if (key !== void 0 && key !== "python") {
|
|
24457
|
+
hits.push(makeHit("pypi", normalizePypi(key), lineNumber, rawLine));
|
|
24458
|
+
}
|
|
24459
|
+
}
|
|
24460
|
+
});
|
|
24461
|
+
return hits;
|
|
24462
|
+
}
|
|
24463
|
+
function quotedStrings(line) {
|
|
24464
|
+
return [...line.matchAll(TOML_QUOTED)].map((m) => m[1] ?? m[2] ?? "");
|
|
24465
|
+
}
|
|
24466
|
+
var GO_BLOCK_OPEN = /^\s*(require|exclude|replace|retract)\s*\(/;
|
|
24467
|
+
var GO_BLOCK_CLOSE = /^\s*\)/;
|
|
24468
|
+
var GO_REQUIRE_SINGLE_LINE = /^\s*require\s+([\w./-]+)\s+v\d/;
|
|
24469
|
+
var GO_MODULE_VERSION_LINE = /^\s*([\w./-]+)\s+v\d/;
|
|
24470
|
+
function extractGoMod(text) {
|
|
24471
|
+
const hits = [];
|
|
24472
|
+
let blockKeyword = null;
|
|
24473
|
+
eachLine(text, (rawLine, lineNumber) => {
|
|
24474
|
+
if (blockKeyword === null) {
|
|
24475
|
+
const open = GO_BLOCK_OPEN.exec(rawLine)?.[1];
|
|
24476
|
+
if (open !== void 0) {
|
|
24477
|
+
blockKeyword = open;
|
|
24478
|
+
return;
|
|
24479
|
+
}
|
|
24480
|
+
const path = GO_REQUIRE_SINGLE_LINE.exec(rawLine)?.[1];
|
|
24481
|
+
if (path !== void 0) hits.push(makeHit("go", path, lineNumber, rawLine));
|
|
24482
|
+
return;
|
|
24483
|
+
}
|
|
24484
|
+
if (GO_BLOCK_CLOSE.test(rawLine)) {
|
|
24485
|
+
blockKeyword = null;
|
|
24486
|
+
return;
|
|
24487
|
+
}
|
|
24488
|
+
if (blockKeyword === "require") {
|
|
24489
|
+
const path = GO_MODULE_VERSION_LINE.exec(rawLine)?.[1];
|
|
24490
|
+
if (path !== void 0) hits.push(makeHit("go", path, lineNumber, rawLine));
|
|
24491
|
+
}
|
|
24492
|
+
});
|
|
24493
|
+
return hits;
|
|
24494
|
+
}
|
|
24495
|
+
var POM_CONTEXT_TAGS = /* @__PURE__ */ new Set([
|
|
24496
|
+
"project",
|
|
24497
|
+
"parent",
|
|
24498
|
+
"dependencyManagement",
|
|
24499
|
+
"dependencies",
|
|
24500
|
+
"dependency",
|
|
24501
|
+
"build",
|
|
24502
|
+
"plugins",
|
|
24503
|
+
"plugin",
|
|
24504
|
+
"exclusions",
|
|
24505
|
+
"exclusion"
|
|
24506
|
+
]);
|
|
24507
|
+
var XML_TAG = /<(\/?)([A-Za-z][\w.-]*)([^<>]*)>/g;
|
|
24508
|
+
var LEADING_TEXT = /^([^<]*)/;
|
|
24509
|
+
function extractPomXml(text) {
|
|
24510
|
+
const hits = [];
|
|
24511
|
+
const stack = [];
|
|
24512
|
+
let inComment = false;
|
|
24513
|
+
eachLine(text, (rawLine, lineNumber) => {
|
|
24514
|
+
const stripped = stripXmlComments(rawLine, inComment);
|
|
24515
|
+
inComment = stripped.inComment;
|
|
24516
|
+
const visible = stripped.visible;
|
|
24517
|
+
for (const match of visible.matchAll(XML_TAG)) {
|
|
24518
|
+
const name = match[2];
|
|
24519
|
+
if (name === void 0) continue;
|
|
24520
|
+
const closing = match[1] === "/";
|
|
24521
|
+
const rest = match[3] ?? "";
|
|
24522
|
+
const selfClosing = rest.trimEnd().endsWith("/");
|
|
24523
|
+
if (closing) {
|
|
24524
|
+
if (POM_CONTEXT_TAGS.has(name) && stack[stack.length - 1] === name) stack.pop();
|
|
24525
|
+
continue;
|
|
24526
|
+
}
|
|
24527
|
+
if (selfClosing) continue;
|
|
24528
|
+
if (name === "groupId") {
|
|
24529
|
+
const after = visible.slice(match.index + match[0].length);
|
|
24530
|
+
const value = LEADING_TEXT.exec(after)?.[1]?.trim() ?? "";
|
|
24531
|
+
if (value !== "" && isProjectDependencyGroupId(stack)) {
|
|
24532
|
+
hits.push(makeHit("maven", value, lineNumber, rawLine));
|
|
24533
|
+
}
|
|
24534
|
+
continue;
|
|
24535
|
+
}
|
|
24536
|
+
if (POM_CONTEXT_TAGS.has(name)) stack.push(name);
|
|
24537
|
+
}
|
|
24538
|
+
});
|
|
24539
|
+
return hits;
|
|
24540
|
+
}
|
|
24541
|
+
function isProjectDependencyGroupId(stack) {
|
|
24542
|
+
return stack[stack.length - 1] === "dependency" && stack[stack.length - 2] === "dependencies" && !stack.includes("dependencyManagement") && !stack.includes("parent") && !stack.includes("build");
|
|
24543
|
+
}
|
|
24544
|
+
var GRADLE_DEPENDENCY = /\b(?:implementation|api|compile)\b\s*[('"]*(?:platform\s*\(\s*['"])?([\w.-]+):[\w.-]+/g;
|
|
24545
|
+
var LINE_COMMENT = /^\s*\/\//;
|
|
24546
|
+
function extractBuildGradle(text) {
|
|
24547
|
+
const hits = [];
|
|
24548
|
+
eachLine(text, (rawLine, lineNumber) => {
|
|
24549
|
+
if (LINE_COMMENT.test(rawLine)) return;
|
|
24550
|
+
for (const match of rawLine.matchAll(GRADLE_DEPENDENCY)) {
|
|
24551
|
+
const groupId = match[1];
|
|
24552
|
+
if (groupId !== void 0) hits.push(makeHit("maven", groupId, lineNumber, rawLine));
|
|
24553
|
+
}
|
|
24554
|
+
});
|
|
24555
|
+
return hits;
|
|
24556
|
+
}
|
|
24557
|
+
var GEMFILE_DEPENDENCY = /^\s*gem\s+['"]([\w-]+)['"]/;
|
|
24558
|
+
function extractGemfile(text) {
|
|
24559
|
+
const hits = [];
|
|
24560
|
+
eachLine(text, (rawLine, lineNumber) => {
|
|
24561
|
+
const name = GEMFILE_DEPENDENCY.exec(rawLine)?.[1];
|
|
24562
|
+
if (name !== void 0) hits.push(makeHit("rubygems", name, lineNumber, rawLine));
|
|
24563
|
+
});
|
|
24564
|
+
return hits;
|
|
24565
|
+
}
|
|
24566
|
+
var CARGO_KEY = /^([A-Za-z0-9_-]+)\s*=/;
|
|
24567
|
+
function extractCargoToml(text) {
|
|
24568
|
+
const hits = [];
|
|
24569
|
+
let mode = "none";
|
|
24570
|
+
eachLine(text, (rawLine, lineNumber) => {
|
|
24571
|
+
const sectionMatch = TOML_SECTION.exec(rawLine);
|
|
24572
|
+
if (sectionMatch) {
|
|
24573
|
+
const name = sectionMatch[1]?.trim() ?? "";
|
|
24574
|
+
if (name === "dependencies") {
|
|
24575
|
+
mode = "plain";
|
|
24576
|
+
} else if (name.startsWith("dependencies.")) {
|
|
24577
|
+
mode = "dotted";
|
|
24578
|
+
const crate = name.slice("dependencies.".length);
|
|
24579
|
+
if (crate !== "") hits.push(makeHit("cargo", crate, lineNumber, rawLine));
|
|
24580
|
+
} else {
|
|
24581
|
+
mode = "none";
|
|
24582
|
+
}
|
|
24583
|
+
return;
|
|
24584
|
+
}
|
|
24585
|
+
if (mode === "plain") {
|
|
24586
|
+
const crate = CARGO_KEY.exec(rawLine)?.[1];
|
|
24587
|
+
if (crate !== void 0) hits.push(makeHit("cargo", crate, lineNumber, rawLine));
|
|
24588
|
+
}
|
|
24589
|
+
});
|
|
24590
|
+
return hits;
|
|
24591
|
+
}
|
|
24592
|
+
function extractComposerJson(text) {
|
|
24593
|
+
const parsed = parseJson(text);
|
|
24594
|
+
if (parsed === null) return [];
|
|
24595
|
+
const pkgs = objectKeys(parsed.require).filter((pkg) => pkg !== "php" && !pkg.startsWith("ext-"));
|
|
24596
|
+
return pkgs.map((pkg) => hitAtQuotedKey("composer", pkg, text, "require"));
|
|
24597
|
+
}
|
|
24598
|
+
var CSPROJ_PACKAGE_REFERENCE = /<PackageReference\s+Include="([^"]+)"/;
|
|
24599
|
+
function extractCsproj(text) {
|
|
24600
|
+
const hits = [];
|
|
24601
|
+
let inComment = false;
|
|
24602
|
+
eachLine(text, (rawLine, lineNumber) => {
|
|
24603
|
+
const stripped = stripXmlComments(rawLine, inComment);
|
|
24604
|
+
inComment = stripped.inComment;
|
|
24605
|
+
const name = CSPROJ_PACKAGE_REFERENCE.exec(stripped.visible)?.[1];
|
|
24606
|
+
if (name !== void 0) hits.push(makeHit("nuget", name, lineNumber, rawLine));
|
|
24607
|
+
});
|
|
24608
|
+
return hits;
|
|
24609
|
+
}
|
|
24610
|
+
var PACKAGES_CONFIG_PACKAGE = /<package\s+id="([^"]+)"/;
|
|
24611
|
+
function extractPackagesConfig(text) {
|
|
24612
|
+
const hits = [];
|
|
24613
|
+
let inComment = false;
|
|
24614
|
+
eachLine(text, (rawLine, lineNumber) => {
|
|
24615
|
+
const stripped = stripXmlComments(rawLine, inComment);
|
|
24616
|
+
inComment = stripped.inComment;
|
|
24617
|
+
const name = PACKAGES_CONFIG_PACKAGE.exec(stripped.visible)?.[1];
|
|
24618
|
+
if (name !== void 0) hits.push(makeHit("nuget", name, lineNumber, rawLine));
|
|
24619
|
+
});
|
|
24620
|
+
return hits;
|
|
24621
|
+
}
|
|
24622
|
+
function stripXmlComments(line, inComment) {
|
|
24623
|
+
let visible = "";
|
|
24624
|
+
let rest = line;
|
|
24625
|
+
let comment = inComment;
|
|
24626
|
+
for (; ; ) {
|
|
24627
|
+
if (comment) {
|
|
24628
|
+
const end = rest.indexOf("-->");
|
|
24629
|
+
if (end === -1) return { visible, inComment: true };
|
|
24630
|
+
rest = rest.slice(end + 3);
|
|
24631
|
+
comment = false;
|
|
24632
|
+
continue;
|
|
24633
|
+
}
|
|
24634
|
+
const start = rest.indexOf("<!--");
|
|
24635
|
+
if (start === -1) return { visible: visible + rest, inComment: false };
|
|
24636
|
+
visible += rest.slice(0, start);
|
|
24637
|
+
rest = rest.slice(start + 4);
|
|
24638
|
+
comment = true;
|
|
24639
|
+
}
|
|
24640
|
+
}
|
|
24641
|
+
function eachLine(text, fn) {
|
|
24642
|
+
const lines = text.split("\n");
|
|
24643
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
24644
|
+
fn(lines[i] ?? "", i + 1);
|
|
24645
|
+
}
|
|
24646
|
+
}
|
|
24647
|
+
function parseJson(text) {
|
|
24648
|
+
let parsed;
|
|
24649
|
+
try {
|
|
24650
|
+
parsed = JSON.parse(text);
|
|
24651
|
+
} catch {
|
|
24652
|
+
return null;
|
|
24653
|
+
}
|
|
24654
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
24655
|
+
const record2 = parsed;
|
|
24656
|
+
return {
|
|
24657
|
+
dependencies: record2.dependencies,
|
|
24658
|
+
optionalDependencies: record2.optionalDependencies,
|
|
24659
|
+
require: record2.require
|
|
24660
|
+
};
|
|
24661
|
+
}
|
|
24662
|
+
function objectKeys(value) {
|
|
24663
|
+
if (typeof value !== "object" || value === null) return [];
|
|
24664
|
+
return Object.keys(value);
|
|
24665
|
+
}
|
|
24666
|
+
function hitAtQuotedKey(ecosystem, pkg, text, sectionKey) {
|
|
24667
|
+
const sectionStart = text.indexOf(`"${sectionKey}"`);
|
|
24668
|
+
const searchFrom = sectionStart === -1 ? 0 : sectionStart;
|
|
24669
|
+
const index = text.indexOf(`"${pkg}"`, searchFrom);
|
|
24670
|
+
if (index === -1) return makeHit(ecosystem, pkg, 1, pkg);
|
|
24671
|
+
return makeHit(ecosystem, pkg, lineNumberAt(text, index), lineContaining(text, index));
|
|
24672
|
+
}
|
|
24673
|
+
function lineNumberAt(text, index) {
|
|
24674
|
+
let line = 1;
|
|
24675
|
+
for (let i = 0; i < index; i += 1) {
|
|
24676
|
+
if (text[i] === "\n") line += 1;
|
|
24677
|
+
}
|
|
24678
|
+
return line;
|
|
24679
|
+
}
|
|
24680
|
+
function lineContaining(text, index) {
|
|
24681
|
+
const start = text.lastIndexOf("\n", index) + 1;
|
|
24682
|
+
const end = text.indexOf("\n", index);
|
|
24683
|
+
return text.slice(start, end === -1 ? text.length : end);
|
|
24684
|
+
}
|
|
24685
|
+
|
|
24686
|
+
// ../../packages/detections/src/egress/resolve.ts
|
|
24687
|
+
var VERB_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE"]);
|
|
24688
|
+
function resolveEgress(files, opts) {
|
|
24689
|
+
const resolved = [];
|
|
24690
|
+
for (const file2 of files) {
|
|
24691
|
+
for (const endpoint of file2.endpoints) {
|
|
24692
|
+
const hit = resolveEndpointHit(file2, endpoint, opts);
|
|
24693
|
+
if (hit !== null) resolved.push(hit);
|
|
24694
|
+
}
|
|
24695
|
+
for (const sdkHit of file2.sdkHits) {
|
|
24696
|
+
const hit = resolveSdkHit(file2, sdkHit);
|
|
24697
|
+
if (hit !== null) resolved.push(hit);
|
|
24698
|
+
}
|
|
24699
|
+
}
|
|
24700
|
+
return dropRefsWithVerbSibling(dedupeExact(resolved));
|
|
24701
|
+
}
|
|
24702
|
+
function resolveEndpointHit(file2, endpoint, opts) {
|
|
24703
|
+
const resolution = resolveHost(endpoint.host, opts);
|
|
24704
|
+
if (resolution === null) return null;
|
|
24705
|
+
const isProvider = resolution.kind === "provider";
|
|
24706
|
+
return {
|
|
24707
|
+
host: endpoint.host,
|
|
24708
|
+
kind: resolution.kind,
|
|
24709
|
+
name: resolution.name,
|
|
24710
|
+
category: resolution.category,
|
|
24711
|
+
trust: resolution.trust,
|
|
24712
|
+
network: isProvider ? null : { port: endpoint.port, geo: null, ptr: null },
|
|
24713
|
+
method: endpoint.method,
|
|
24714
|
+
transport: endpoint.transport,
|
|
24715
|
+
url: endpoint.url,
|
|
24716
|
+
template: endpoint.template,
|
|
24717
|
+
dataClass: isProvider ? resolution.entry?.defaultDataClasses[0] ?? "none" : "none",
|
|
24718
|
+
site: {
|
|
24719
|
+
file: file2.file,
|
|
24720
|
+
line: endpoint.line,
|
|
24721
|
+
snippet: endpoint.snippet,
|
|
24722
|
+
dynamic: endpoint.template,
|
|
24723
|
+
vendored: file2.vendored
|
|
24724
|
+
}
|
|
24725
|
+
};
|
|
24726
|
+
}
|
|
24727
|
+
function resolveSdkHit(file2, sdkHit) {
|
|
24728
|
+
const entry = resolveSdk(sdkHit.ecosystem, sdkHit.pkg);
|
|
24729
|
+
if (entry === null) return null;
|
|
24730
|
+
return {
|
|
24731
|
+
host: new URL(entry.apiBase).hostname,
|
|
24732
|
+
kind: "provider",
|
|
24733
|
+
name: entry.name,
|
|
24734
|
+
category: entry.category,
|
|
24735
|
+
trust: "recognized",
|
|
24736
|
+
network: null,
|
|
24737
|
+
method: "SDK",
|
|
24738
|
+
transport: "https",
|
|
24739
|
+
url: entry.apiBase,
|
|
24740
|
+
template: false,
|
|
24741
|
+
dataClass: entry.defaultDataClasses[0] ?? "none",
|
|
24742
|
+
site: {
|
|
24743
|
+
file: file2.file,
|
|
24744
|
+
line: sdkHit.line,
|
|
24745
|
+
snippet: sdkHit.snippet,
|
|
24746
|
+
dynamic: false,
|
|
24747
|
+
vendored: file2.vendored
|
|
24748
|
+
}
|
|
24749
|
+
};
|
|
24750
|
+
}
|
|
24751
|
+
function dedupeExact(hits) {
|
|
24752
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24753
|
+
const deduped = [];
|
|
24754
|
+
for (const hit of hits) {
|
|
24755
|
+
const key = exactKey(hit);
|
|
24756
|
+
if (seen.has(key)) continue;
|
|
24757
|
+
seen.add(key);
|
|
24758
|
+
deduped.push(hit);
|
|
24759
|
+
}
|
|
24760
|
+
return deduped;
|
|
24761
|
+
}
|
|
24762
|
+
function exactKey(hit) {
|
|
24763
|
+
return JSON.stringify([hit.host, hit.method, hit.url, hit.site.file, hit.site.line]);
|
|
24764
|
+
}
|
|
24765
|
+
function dropRefsWithVerbSibling(hits) {
|
|
24766
|
+
const verbPairs = /* @__PURE__ */ new Set();
|
|
24767
|
+
for (const hit of hits) {
|
|
24768
|
+
if (VERB_METHODS.has(hit.method)) verbPairs.add(pairKey(hit));
|
|
24769
|
+
}
|
|
24770
|
+
return hits.filter((hit) => hit.method !== "REF" || !verbPairs.has(pairKey(hit)));
|
|
24771
|
+
}
|
|
24772
|
+
function pairKey(hit) {
|
|
24773
|
+
return JSON.stringify([hit.host, hit.url]);
|
|
24774
|
+
}
|
|
24775
|
+
|
|
24776
|
+
// ../../packages/detections/src/escape-regexp.ts
|
|
24777
|
+
function escapeRegExp2(value) {
|
|
24778
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24779
|
+
}
|
|
24780
|
+
|
|
24781
|
+
// ../../packages/detections/src/matchers/limits.ts
|
|
24782
|
+
var MAX_MATCHES_PER_RULE = 1e4;
|
|
24783
|
+
var MAX_REGEX_INPUT_LENGTH = 2e5;
|
|
24784
|
+
|
|
24785
|
+
// ../../packages/detections/src/matchers/keyword.ts
|
|
24786
|
+
var KeywordMatcher2 = class {
|
|
24787
|
+
match(text, rule) {
|
|
24788
|
+
if (rule.matcher.type !== "keyword") return [];
|
|
24789
|
+
const { keywords, caseSensitive } = rule.matcher;
|
|
24790
|
+
const spans = [];
|
|
24791
|
+
for (const kw of keywords) {
|
|
24792
|
+
if (kw.length === 0) continue;
|
|
24793
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
24794
|
+
const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
|
|
24795
|
+
let m;
|
|
24796
|
+
while ((m = re.exec(text)) !== null) {
|
|
24797
|
+
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
24798
|
+
if (spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
24799
|
+
}
|
|
24800
|
+
}
|
|
24801
|
+
return spans;
|
|
24802
|
+
}
|
|
24803
|
+
};
|
|
24804
|
+
|
|
24805
|
+
// ../../packages/detections/src/matchers/regex.ts
|
|
24806
|
+
var RegexMatcher2 = class {
|
|
24807
|
+
match(text, rule) {
|
|
24808
|
+
if (rule.matcher.type !== "regex") return [];
|
|
24809
|
+
const { pattern, flags, captureGroup } = rule.matcher;
|
|
24810
|
+
const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
|
|
24811
|
+
const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
|
|
24812
|
+
const spans = [];
|
|
24813
|
+
let m;
|
|
24814
|
+
const maxIterations = scanText2.length + 1;
|
|
24815
|
+
let iterations = 0;
|
|
24816
|
+
while ((m = re.exec(scanText2)) !== null) {
|
|
24817
|
+
if (++iterations > maxIterations) break;
|
|
24818
|
+
const group = captureGroup != null ? m[captureGroup] : m[0];
|
|
24819
|
+
if (m[0].length === 0) re.lastIndex++;
|
|
24820
|
+
if (group && spans.length < MAX_MATCHES_PER_RULE) {
|
|
24821
|
+
const groupIndices = captureGroup != null ? m.indices?.[captureGroup] : void 0;
|
|
24822
|
+
const start = groupIndices ? groupIndices[0] : m.index;
|
|
24823
|
+
spans.push({ start, end: start + group.length });
|
|
24824
|
+
}
|
|
24825
|
+
if (!flags.includes("g") || spans.length >= MAX_MATCHES_PER_RULE) break;
|
|
24826
|
+
}
|
|
24827
|
+
return spans;
|
|
24828
|
+
}
|
|
24829
|
+
};
|
|
24830
|
+
|
|
24831
|
+
// ../../packages/detections/src/validators/entropy.ts
|
|
24832
|
+
function shannonEntropy(str2) {
|
|
24833
|
+
const freq = {};
|
|
24834
|
+
for (const ch of str2) {
|
|
24835
|
+
freq[ch] = (freq[ch] ?? 0) + 1;
|
|
24836
|
+
}
|
|
24837
|
+
let entropy = 0;
|
|
24838
|
+
for (const count of Object.values(freq)) {
|
|
24839
|
+
const p = count / str2.length;
|
|
24840
|
+
entropy -= p * Math.log2(p);
|
|
24841
|
+
}
|
|
24842
|
+
return entropy;
|
|
24843
|
+
}
|
|
24844
|
+
function isHighEntropy(value, threshold = 3.5, minLength = 20) {
|
|
24845
|
+
return value.length >= minLength && shannonEntropy(value) >= threshold;
|
|
24846
|
+
}
|
|
24847
|
+
|
|
24848
|
+
// ../../packages/detections/src/validators/luhn.ts
|
|
24849
|
+
function luhnCheck(digits) {
|
|
24850
|
+
const nums = digits.replace(/\D/g, "");
|
|
24851
|
+
if (nums.length < 13) return false;
|
|
24852
|
+
let sum = 0;
|
|
24853
|
+
let isOdd = true;
|
|
24854
|
+
for (let i = nums.length - 1; i >= 0; i--) {
|
|
24855
|
+
let digit = parseInt(nums[i] ?? "0", 10);
|
|
24856
|
+
if (!isOdd) {
|
|
24857
|
+
digit *= 2;
|
|
24858
|
+
if (digit > 9) digit -= 9;
|
|
24859
|
+
}
|
|
24860
|
+
sum += digit;
|
|
24861
|
+
isOdd = !isOdd;
|
|
24862
|
+
}
|
|
24863
|
+
return sum % 10 === 0;
|
|
24864
|
+
}
|
|
24865
|
+
|
|
24866
|
+
// ../../packages/detections/src/engine.ts
|
|
24867
|
+
var keywordMatcher = new KeywordMatcher2();
|
|
24868
|
+
var regexMatcher = new RegexMatcher2();
|
|
24869
|
+
var packs = /* @__PURE__ */ new Map();
|
|
24870
|
+
var POST_VALIDATORS = {
|
|
24871
|
+
entropy: (value, config2) => isHighEntropy(value, numberOption(config2, "threshold"), numberOption(config2, "minLength")),
|
|
24872
|
+
luhn: (value) => luhnCheck(value)
|
|
24873
|
+
};
|
|
24874
|
+
function numberOption(config2, key) {
|
|
24875
|
+
const value = config2?.[key];
|
|
24876
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
24877
|
+
}
|
|
24878
|
+
function passesPostValidators(rule, value) {
|
|
24879
|
+
const validators = rule.postValidators;
|
|
24880
|
+
if (!validators || validators.length === 0) return true;
|
|
24881
|
+
for (const ref of validators) {
|
|
24882
|
+
const name = typeof ref === "string" ? ref : ref.name;
|
|
23023
24883
|
const config2 = typeof ref === "string" ? void 0 : ref.config;
|
|
23024
24884
|
const validate = POST_VALIDATORS[name];
|
|
23025
24885
|
if (validate && !validate(value, config2)) return false;
|
|
@@ -23058,7 +24918,7 @@ function isCorroborated(candidate, candidates, text) {
|
|
|
23058
24918
|
for (const label of labels) {
|
|
23059
24919
|
const trimmed = label.trim();
|
|
23060
24920
|
if (trimmed.length === 0) continue;
|
|
23061
|
-
const re = new RegExp(`(?<![A-Za-z0-9])${
|
|
24921
|
+
const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
|
|
23062
24922
|
if (re.test(haystack)) return true;
|
|
23063
24923
|
}
|
|
23064
24924
|
}
|
|
@@ -23223,6 +25083,112 @@ var CONFIG_POSTURE_RULES = [
|
|
|
23223
25083
|
}
|
|
23224
25084
|
];
|
|
23225
25085
|
|
|
25086
|
+
// ../../packages/detections/src/security/redos-probe.ts
|
|
25087
|
+
var BUDGET_MS = 100;
|
|
25088
|
+
var EXPONENTIAL_UNITS = [
|
|
25089
|
+
"a",
|
|
25090
|
+
"0",
|
|
25091
|
+
" ",
|
|
25092
|
+
"x",
|
|
25093
|
+
"ab",
|
|
25094
|
+
"a.",
|
|
25095
|
+
"a-",
|
|
25096
|
+
"a_",
|
|
25097
|
+
"a@",
|
|
25098
|
+
"a/",
|
|
25099
|
+
"a:",
|
|
25100
|
+
"a=",
|
|
25101
|
+
"a;",
|
|
25102
|
+
"aA0",
|
|
25103
|
+
" "
|
|
25104
|
+
];
|
|
25105
|
+
var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
|
|
25106
|
+
(unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
|
|
25107
|
+
);
|
|
25108
|
+
var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
|
|
25109
|
+
(unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
|
|
25110
|
+
);
|
|
25111
|
+
function literalPrefix(pattern) {
|
|
25112
|
+
let prefix = "";
|
|
25113
|
+
let i = 0;
|
|
25114
|
+
if (pattern[i] === "^") i++;
|
|
25115
|
+
while (i < pattern.length) {
|
|
25116
|
+
const c = pattern[i];
|
|
25117
|
+
if (c === void 0) break;
|
|
25118
|
+
if (c === "\\") {
|
|
25119
|
+
const next = pattern[i + 1];
|
|
25120
|
+
if (next === "b" || next === "B") {
|
|
25121
|
+
i += 2;
|
|
25122
|
+
continue;
|
|
25123
|
+
}
|
|
25124
|
+
if (next === void 0 || /[dDwWsSnrtfv.]/.test(next)) break;
|
|
25125
|
+
prefix += next;
|
|
25126
|
+
i += 2;
|
|
25127
|
+
continue;
|
|
25128
|
+
}
|
|
25129
|
+
if ("([{.*+?|)]}^$".includes(c)) break;
|
|
25130
|
+
prefix += c;
|
|
25131
|
+
i++;
|
|
25132
|
+
}
|
|
25133
|
+
return prefix;
|
|
25134
|
+
}
|
|
25135
|
+
function fuelChars(pattern) {
|
|
25136
|
+
const fuel = /* @__PURE__ */ new Set();
|
|
25137
|
+
for (const m of pattern.matchAll(/\[\^?([^\]]+)\]/g)) {
|
|
25138
|
+
const body = m[1];
|
|
25139
|
+
if (body === void 0) continue;
|
|
25140
|
+
const range = /([A-Za-z0-9])-[A-Za-z0-9]/.exec(body);
|
|
25141
|
+
const rangeStart = range?.[1];
|
|
25142
|
+
if (rangeStart !== void 0) fuel.add(rangeStart);
|
|
25143
|
+
else {
|
|
25144
|
+
const literal2 = body.replace(/\\/g, "")[0];
|
|
25145
|
+
if (literal2 !== void 0 && literal2 !== "^") fuel.add(literal2);
|
|
25146
|
+
}
|
|
25147
|
+
}
|
|
25148
|
+
if (pattern.includes("\\w")) fuel.add("a");
|
|
25149
|
+
if (pattern.includes("\\d")) fuel.add("0");
|
|
25150
|
+
if (pattern.includes("\\s")) fuel.add(" ");
|
|
25151
|
+
if (/(?<!\\)\./.test(pattern)) fuel.add("a");
|
|
25152
|
+
if (fuel.size === 0) fuel.add("a");
|
|
25153
|
+
return [...fuel];
|
|
25154
|
+
}
|
|
25155
|
+
function derivedProbes(pattern) {
|
|
25156
|
+
const prefix = literalPrefix(pattern);
|
|
25157
|
+
const fuel = fuelChars(pattern);
|
|
25158
|
+
const terminators = ["!", "#", "~", "\n"];
|
|
25159
|
+
const probes = [];
|
|
25160
|
+
for (const f of fuel) {
|
|
25161
|
+
for (const term of terminators) {
|
|
25162
|
+
if (term === f) continue;
|
|
25163
|
+
for (const len of [23, 25]) probes.push(prefix + f.repeat(len) + term);
|
|
25164
|
+
}
|
|
25165
|
+
}
|
|
25166
|
+
return probes;
|
|
25167
|
+
}
|
|
25168
|
+
function probesFor(rule) {
|
|
25169
|
+
const derived = rule.matcher.type === "regex" ? derivedProbes(rule.matcher.pattern) : [];
|
|
25170
|
+
return [...derived, ...EXPONENTIAL_PROBES, ...POLYNOMIAL_PROBES];
|
|
25171
|
+
}
|
|
25172
|
+
function worstProbeMs(rule) {
|
|
25173
|
+
let ms = 0;
|
|
25174
|
+
let probe = "";
|
|
25175
|
+
for (const text of probesFor(rule)) {
|
|
25176
|
+
const start = performance.now();
|
|
25177
|
+
scan(text, [rule]);
|
|
25178
|
+
const elapsed = performance.now() - start;
|
|
25179
|
+
if (elapsed > ms) {
|
|
25180
|
+
ms = elapsed;
|
|
25181
|
+
probe = text;
|
|
25182
|
+
}
|
|
25183
|
+
if (ms >= BUDGET_MS) break;
|
|
25184
|
+
}
|
|
25185
|
+
return { ms, probe };
|
|
25186
|
+
}
|
|
25187
|
+
function checkRuleTiming(rule) {
|
|
25188
|
+
const { ms, probe } = worstProbeMs(rule);
|
|
25189
|
+
return { safe: ms < BUDGET_MS, worstMs: ms, probe };
|
|
25190
|
+
}
|
|
25191
|
+
|
|
23226
25192
|
// ../../rules/code-flaws/auth-jwt-no-verify.json
|
|
23227
25193
|
var auth_jwt_no_verify_default = {
|
|
23228
25194
|
specVersion: 1,
|
|
@@ -25258,6 +27224,99 @@ function bundledDetections() {
|
|
|
25258
27224
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
25259
27225
|
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
|
|
25260
27226
|
import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
|
|
27227
|
+
function resolveRepoIdentity(cwd) {
|
|
27228
|
+
try {
|
|
27229
|
+
const root = findGitRoot(cwd);
|
|
27230
|
+
if (!root) return void 0;
|
|
27231
|
+
const ctx = resolveGitContext(root);
|
|
27232
|
+
const headRoot = ctx?.headRoot ?? root;
|
|
27233
|
+
const url2 = ctx ? remoteUrl(ctx) : void 0;
|
|
27234
|
+
return {
|
|
27235
|
+
// The path fallback is normalized to posix separators (a no-op outside
|
|
27236
|
+
// win32) so the persistence layer's `/`-separated checkout-path patterns
|
|
27237
|
+
// (the ghost sweep + the read-side worktree filter) match it as written.
|
|
27238
|
+
url: url2 ?? headRoot.split(sep2).join("/"),
|
|
27239
|
+
name: (url2 ? slugFromUrl(url2) : void 0) ?? basename(headRoot)
|
|
27240
|
+
};
|
|
27241
|
+
} catch {
|
|
27242
|
+
return void 0;
|
|
27243
|
+
}
|
|
27244
|
+
}
|
|
27245
|
+
function resolveWorktreeRoot(cwd) {
|
|
27246
|
+
try {
|
|
27247
|
+
return findGitRoot(cwd);
|
|
27248
|
+
} catch {
|
|
27249
|
+
return void 0;
|
|
27250
|
+
}
|
|
27251
|
+
}
|
|
27252
|
+
function findGitRoot(start) {
|
|
27253
|
+
let dir = start;
|
|
27254
|
+
for (; ; ) {
|
|
27255
|
+
if (existsSync3(join6(dir, ".git"))) return dir;
|
|
27256
|
+
const parent = dirname(dir);
|
|
27257
|
+
if (parent === dir) return void 0;
|
|
27258
|
+
dir = parent;
|
|
27259
|
+
}
|
|
27260
|
+
}
|
|
27261
|
+
function resolveGitContext(root) {
|
|
27262
|
+
const dotGit = join6(root, ".git");
|
|
27263
|
+
try {
|
|
27264
|
+
if (statSync(dotGit).isDirectory()) {
|
|
27265
|
+
return { configPath: join6(dotGit, "config"), headRoot: root };
|
|
27266
|
+
}
|
|
27267
|
+
} catch {
|
|
27268
|
+
return void 0;
|
|
27269
|
+
}
|
|
27270
|
+
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
27271
|
+
if (!target) return void 0;
|
|
27272
|
+
const gitdir = isAbsolute(target) ? target : join6(root, target);
|
|
27273
|
+
if (existsSync3(join6(gitdir, "config"))) {
|
|
27274
|
+
return { configPath: join6(gitdir, "config"), headRoot: root };
|
|
27275
|
+
}
|
|
27276
|
+
const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
|
|
27277
|
+
if (!commonRaw) return void 0;
|
|
27278
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
|
|
27279
|
+
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
27280
|
+
return { configPath: join6(commonGitDir, "config"), headRoot };
|
|
27281
|
+
}
|
|
27282
|
+
function safeRead(path) {
|
|
27283
|
+
try {
|
|
27284
|
+
return readFileSync3(path, "utf8");
|
|
27285
|
+
} catch {
|
|
27286
|
+
return void 0;
|
|
27287
|
+
}
|
|
27288
|
+
}
|
|
27289
|
+
function remoteUrl(ctx) {
|
|
27290
|
+
const config2 = safeRead(ctx.configPath);
|
|
27291
|
+
if (config2 === void 0) return void 0;
|
|
27292
|
+
const remotes = parseRemoteUrls(config2);
|
|
27293
|
+
return remotes.origin ?? Object.values(remotes)[0];
|
|
27294
|
+
}
|
|
27295
|
+
function parseRemoteUrls(config2) {
|
|
27296
|
+
const remotes = {};
|
|
27297
|
+
let current;
|
|
27298
|
+
for (const raw of config2.split("\n")) {
|
|
27299
|
+
const line = raw.trim();
|
|
27300
|
+
const header = /^\[remote "([^"]+)"\]$/.exec(line);
|
|
27301
|
+
if (header) {
|
|
27302
|
+
current = header[1];
|
|
27303
|
+
continue;
|
|
27304
|
+
}
|
|
27305
|
+
if (line.startsWith("[")) {
|
|
27306
|
+
current = void 0;
|
|
27307
|
+
continue;
|
|
27308
|
+
}
|
|
27309
|
+
if (current && !(current in remotes)) {
|
|
27310
|
+
const url2 = /^url\s*=\s*(.+)$/.exec(line)?.[1];
|
|
27311
|
+
if (url2) remotes[current] = url2.trim();
|
|
27312
|
+
}
|
|
27313
|
+
}
|
|
27314
|
+
return remotes;
|
|
27315
|
+
}
|
|
27316
|
+
function slugFromUrl(url2) {
|
|
27317
|
+
const segment = url2.replace(/\.git$/, "").split(/[/:]/).filter(Boolean).pop();
|
|
27318
|
+
return segment && segment.length > 0 ? segment : void 0;
|
|
27319
|
+
}
|
|
25261
27320
|
|
|
25262
27321
|
// ../../packages/plugin-sdk/src/events.ts
|
|
25263
27322
|
import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
|
|
@@ -25299,10 +27358,97 @@ import { arch, hostname as hostname3, platform, release } from "os";
|
|
|
25299
27358
|
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
25300
27359
|
import { join as join8 } from "path";
|
|
25301
27360
|
|
|
27361
|
+
// ../../packages/plugin-sdk/src/paths.ts
|
|
27362
|
+
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
27363
|
+
import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
27364
|
+
function toPosix(path) {
|
|
27365
|
+
return path.split(sep3).join("/");
|
|
27366
|
+
}
|
|
27367
|
+
var MAX_PROJECT_ROOT_LEVELS = 40;
|
|
27368
|
+
function findProjectRoot(startDir, recognizeMarker) {
|
|
27369
|
+
let dir = startDir;
|
|
27370
|
+
let root = null;
|
|
27371
|
+
for (let level = 0; level < MAX_PROJECT_ROOT_LEVELS; level += 1) {
|
|
27372
|
+
if (directoryHasMarker(dir, recognizeMarker)) root = dir;
|
|
27373
|
+
const parent = dirname2(dir);
|
|
27374
|
+
if (parent === dir) break;
|
|
27375
|
+
dir = parent;
|
|
27376
|
+
}
|
|
27377
|
+
return root ?? startDir;
|
|
27378
|
+
}
|
|
27379
|
+
function directoryHasMarker(dir, recognizeMarker) {
|
|
27380
|
+
try {
|
|
27381
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
27382
|
+
if (entry.isFile() && recognizeMarker(entry.name) != null) return true;
|
|
27383
|
+
}
|
|
27384
|
+
} catch {
|
|
27385
|
+
return false;
|
|
27386
|
+
}
|
|
27387
|
+
return false;
|
|
27388
|
+
}
|
|
27389
|
+
function resolveNonGitProject(startDir, recognizeMarker) {
|
|
27390
|
+
const projectRoot = findProjectRoot(startDir, recognizeMarker);
|
|
27391
|
+
const realRoot = realpathSync2(projectRoot);
|
|
27392
|
+
return { root: projectRoot, projectKey: `path:${realRoot}`, project: basename3(realRoot) };
|
|
27393
|
+
}
|
|
27394
|
+
|
|
25302
27395
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
25303
27396
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
25304
|
-
import { existsSync as existsSync4, readdirSync as
|
|
25305
|
-
import { basename as
|
|
27397
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
27398
|
+
import { basename as basename4, join as join9, relative, sep as sep4 } from "path";
|
|
27399
|
+
|
|
27400
|
+
// ../../packages/plugin-sdk/src/rule-quarantine.ts
|
|
27401
|
+
var PASS_BUDGET_MS = 2e3;
|
|
27402
|
+
function ruleProbeKey(rule) {
|
|
27403
|
+
if (rule.matcher.type !== "regex") return void 0;
|
|
27404
|
+
return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
|
|
27405
|
+
}
|
|
27406
|
+
function warnQuarantined(rule, worstMs) {
|
|
27407
|
+
const timing = worstMs === void 0 ? "not verified in time" : `${worstMs.toFixed(1)}ms`;
|
|
27408
|
+
process.stderr.write(
|
|
27409
|
+
`[aka] quarantined rule "${rule.id}": regex matcher exceeded the ReDoS timing budget (${timing}); excluded from this scan.
|
|
27410
|
+
`
|
|
27411
|
+
);
|
|
27412
|
+
}
|
|
27413
|
+
async function filterUnsafeRules(rules, gateway, opts) {
|
|
27414
|
+
const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
|
|
27415
|
+
const passStart = performance.now();
|
|
27416
|
+
const safe = [];
|
|
27417
|
+
for (const rule of rules) {
|
|
27418
|
+
const key = ruleProbeKey(rule);
|
|
27419
|
+
if (key === void 0) {
|
|
27420
|
+
safe.push(rule);
|
|
27421
|
+
continue;
|
|
27422
|
+
}
|
|
27423
|
+
let cached2;
|
|
27424
|
+
try {
|
|
27425
|
+
cached2 = await gateway.getRuleProbeVerdict(key);
|
|
27426
|
+
} catch {
|
|
27427
|
+
cached2 = void 0;
|
|
27428
|
+
}
|
|
27429
|
+
if (cached2) {
|
|
27430
|
+
if (cached2.verdict === "safe") safe.push(rule);
|
|
27431
|
+
else warnQuarantined(rule, cached2.worstProbeMs);
|
|
27432
|
+
continue;
|
|
27433
|
+
}
|
|
27434
|
+
if (performance.now() - passStart >= passBudgetMs) {
|
|
27435
|
+
warnQuarantined(rule, void 0);
|
|
27436
|
+
continue;
|
|
27437
|
+
}
|
|
27438
|
+
let isSafe;
|
|
27439
|
+
let worstMs;
|
|
27440
|
+
try {
|
|
27441
|
+
({ safe: isSafe, worstMs } = checkRuleTiming(rule));
|
|
27442
|
+
} catch {
|
|
27443
|
+
isSafe = false;
|
|
27444
|
+
worstMs = Number.POSITIVE_INFINITY;
|
|
27445
|
+
}
|
|
27446
|
+
await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
|
|
27447
|
+
if (isSafe) safe.push(rule);
|
|
27448
|
+
else warnQuarantined(rule, worstMs);
|
|
27449
|
+
}
|
|
27450
|
+
return safe;
|
|
27451
|
+
}
|
|
25306
27452
|
|
|
25307
27453
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
25308
27454
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -25346,7 +27492,17 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
25346
27492
|
categoryActionIndex.set(p.target.category, p.action);
|
|
25347
27493
|
}
|
|
25348
27494
|
}
|
|
25349
|
-
|
|
27495
|
+
const bundledProbeKeys = new Set(
|
|
27496
|
+
getLoadedRules().map(ruleProbeKey).filter((key) => key !== void 0)
|
|
27497
|
+
);
|
|
27498
|
+
const incoming = bundle.rules ?? [];
|
|
27499
|
+
const ciVerified = incoming.filter((rule) => {
|
|
27500
|
+
const key = ruleProbeKey(rule);
|
|
27501
|
+
return key !== void 0 && bundledProbeKeys.has(key);
|
|
27502
|
+
});
|
|
27503
|
+
const needsGate = incoming.filter((rule) => !ciVerified.includes(rule));
|
|
27504
|
+
const safeBundleRules = [...ciVerified, ...await filterUnsafeRules(needsGate, gateway)];
|
|
27505
|
+
rules = bundle.rulesComplete ? safeBundleRules : [...getLoadedRules(), ...safeBundleRules];
|
|
25350
27506
|
bundleExceptions = bundle.exceptions ?? [];
|
|
25351
27507
|
initialized = true;
|
|
25352
27508
|
}
|
|
@@ -25593,7 +27749,7 @@ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeF
|
|
|
25593
27749
|
import { join as join10 } from "path";
|
|
25594
27750
|
|
|
25595
27751
|
// ../../packages/scanner/src/discover.ts
|
|
25596
|
-
import { readdirSync as
|
|
27752
|
+
import { readdirSync as readdirSync4 } from "fs";
|
|
25597
27753
|
import { join as join11 } from "path";
|
|
25598
27754
|
|
|
25599
27755
|
// ../../packages/scanner/src/constants.ts
|
|
@@ -25623,7 +27779,7 @@ function discoverGitRepos(opts) {
|
|
|
25623
27779
|
if (depth > maxDepth || excludePaths.has(dir)) return;
|
|
25624
27780
|
let entries;
|
|
25625
27781
|
try {
|
|
25626
|
-
entries =
|
|
27782
|
+
entries = readdirSync4(dir, { withFileTypes: true, encoding: "utf8" });
|
|
25627
27783
|
} catch {
|
|
25628
27784
|
return;
|
|
25629
27785
|
}
|
|
@@ -25649,7 +27805,7 @@ function discoverGitRepos(opts) {
|
|
|
25649
27805
|
}
|
|
25650
27806
|
|
|
25651
27807
|
// ../../packages/scanner/src/render.ts
|
|
25652
|
-
import { basename as
|
|
27808
|
+
import { basename as basename5, relative as relative2 } from "path";
|
|
25653
27809
|
var SEVERITY_ORDER2 = ["critical", "high", "medium", "low"];
|
|
25654
27810
|
var SEVERITY_GLYPH = {
|
|
25655
27811
|
critical: "\u2588",
|
|
@@ -25667,9 +27823,9 @@ function indent(text, spaces = 2) {
|
|
|
25667
27823
|
}
|
|
25668
27824
|
function table(headers, rows, gap = 3) {
|
|
25669
27825
|
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
25670
|
-
const
|
|
25671
|
-
const fmt = (cells) => cells.map((cell, i) => padEnd(cell, widths[i] ?? 0)).join(
|
|
25672
|
-
const ruleLine = widths.map((w) => "\u2500".repeat(w)).join(
|
|
27826
|
+
const sep6 = " ".repeat(gap);
|
|
27827
|
+
const fmt = (cells) => cells.map((cell, i) => padEnd(cell, widths[i] ?? 0)).join(sep6);
|
|
27828
|
+
const ruleLine = widths.map((w) => "\u2500".repeat(w)).join(sep6);
|
|
25673
27829
|
return [fmt(headers.map((h) => h.toUpperCase())), ruleLine, ...rows.map(fmt)].join("\n");
|
|
25674
27830
|
}
|
|
25675
27831
|
function metaBlock(rows) {
|
|
@@ -25729,7 +27885,7 @@ function renderMultiRepoSummary(summary, opts = {}) {
|
|
|
25729
27885
|
"\n"
|
|
25730
27886
|
);
|
|
25731
27887
|
}
|
|
25732
|
-
const repoRows = summary.repos.filter((r) => r.summary.scanned > 0 || r.summary.findings > 0).map((r) => [
|
|
27888
|
+
const repoRows = summary.repos.filter((r) => r.summary.scanned > 0 || r.summary.findings > 0).map((r) => [basename5(r.rootDir), String(r.summary.scanned), String(r.summary.findings)]);
|
|
25733
27889
|
const repoSection = repoRows.length > 0 ? ["", indent(table(["REPO", "SCANNED", "FINDINGS"], repoRows))].join("\n") : "";
|
|
25734
27890
|
return [
|
|
25735
27891
|
"\u2713 Multi-repo scan complete",
|
|
@@ -25742,8 +27898,8 @@ function renderMultiRepoSummary(summary, opts = {}) {
|
|
|
25742
27898
|
}
|
|
25743
27899
|
|
|
25744
27900
|
// ../../packages/scanner/src/scan.ts
|
|
25745
|
-
import { existsSync as existsSync5 } from "fs";
|
|
25746
|
-
import { isAbsolute as isAbsolute2, relative as relative4 } from "path";
|
|
27901
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
|
|
27902
|
+
import { extname as extname2, isAbsolute as isAbsolute2, relative as relative4 } from "path";
|
|
25747
27903
|
|
|
25748
27904
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
25749
27905
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -26014,6 +28170,13 @@ var StandaloneDataGateway = class {
|
|
|
26014
28170
|
this.db.scanLedger.upsertEntries(entries);
|
|
26015
28171
|
return Promise.resolve();
|
|
26016
28172
|
}
|
|
28173
|
+
getRuleProbeVerdict(ruleKey) {
|
|
28174
|
+
return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
|
|
28175
|
+
}
|
|
28176
|
+
setRuleProbeVerdict(ruleKey, verdict, worstProbeMs2) {
|
|
28177
|
+
this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs2);
|
|
28178
|
+
return Promise.resolve();
|
|
28179
|
+
}
|
|
26017
28180
|
openAtRestKeysForPath(path) {
|
|
26018
28181
|
return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
|
|
26019
28182
|
}
|
|
@@ -26024,6 +28187,12 @@ var StandaloneDataGateway = class {
|
|
|
26024
28187
|
this.db.resolutions.insertResolution(input);
|
|
26025
28188
|
return Promise.resolve();
|
|
26026
28189
|
}
|
|
28190
|
+
// Bare forward — no toggle read here. The plugin-path kill-switch is
|
|
28191
|
+
// enforced by the caller, which already holds the parsed workspace
|
|
28192
|
+
// settings; this class only ever sees `dataDir`, not the settings base.
|
|
28193
|
+
recordProjectEgress(input) {
|
|
28194
|
+
return Promise.resolve(this.db.shares.recordProjectEgress(input));
|
|
28195
|
+
}
|
|
26027
28196
|
close() {
|
|
26028
28197
|
this.db.close();
|
|
26029
28198
|
return Promise.resolve();
|
|
@@ -26040,16 +28209,13 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
|
|
|
26040
28209
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
26041
28210
|
var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
26042
28211
|
|
|
26043
|
-
// ../../packages/scanner/src/
|
|
26044
|
-
|
|
26045
|
-
const cur = new Set(current);
|
|
26046
|
-
return [...new Set(prior)].filter((k) => !cur.has(k));
|
|
26047
|
-
}
|
|
28212
|
+
// ../../packages/scanner/src/manifests.ts
|
|
28213
|
+
import { statSync as statSync5 } from "fs";
|
|
26048
28214
|
|
|
26049
28215
|
// ../../packages/scanner/src/walk.ts
|
|
26050
28216
|
var import_ignore2 = __toESM(require_ignore(), 1);
|
|
26051
|
-
import { readdirSync as
|
|
26052
|
-
import { extname, join as join12, relative as relative3, sep as
|
|
28217
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
|
|
28218
|
+
import { extname, join as join12, relative as relative3, sep as sep5 } from "path";
|
|
26053
28219
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
26054
28220
|
".ts",
|
|
26055
28221
|
".tsx",
|
|
@@ -26090,26 +28256,24 @@ function readIgnoreLayer(dir, filename) {
|
|
|
26090
28256
|
function evaluate(layers, absPath, isDir) {
|
|
26091
28257
|
let state = "unmatched";
|
|
26092
28258
|
for (const layer of layers) {
|
|
26093
|
-
const rel = relative3(layer.base, absPath).split(
|
|
28259
|
+
const rel = relative3(layer.base, absPath).split(sep5).join("/") + (isDir ? "/" : "");
|
|
26094
28260
|
const verdict = layer.matcher.test(rel);
|
|
26095
28261
|
if (verdict.ignored) state = "ignored";
|
|
26096
28262
|
else if (verdict.unignored) state = "unignored";
|
|
26097
28263
|
}
|
|
26098
28264
|
return state;
|
|
26099
28265
|
}
|
|
26100
|
-
function*
|
|
26101
|
-
const
|
|
26102
|
-
const extensions = opts.extensions ?? SOURCE_EXTENSIONS;
|
|
26103
|
-
const maxBytes = opts.maxFileSizeBytes ?? DEFAULT_MAX_BYTES;
|
|
28266
|
+
function* walkTree(rootDir, opts = {}) {
|
|
28267
|
+
const trackGitignore = opts.trackGitignore ?? false;
|
|
26104
28268
|
const rootSkipLayers = opts.excludePatterns && opts.excludePatterns.length > 0 ? [{ base: rootDir, matcher: (0, import_ignore2.default)().add(opts.excludePatterns) }] : [];
|
|
26105
28269
|
function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
|
|
26106
28270
|
let dirents;
|
|
26107
28271
|
try {
|
|
26108
|
-
dirents =
|
|
28272
|
+
dirents = readdirSync5(dir, { withFileTypes: true, encoding: "utf8" });
|
|
26109
28273
|
} catch {
|
|
26110
28274
|
return;
|
|
26111
28275
|
}
|
|
26112
|
-
const markLayer = inIgnoredDir ?
|
|
28276
|
+
const markLayer = trackGitignore && !inIgnoredDir ? readIgnoreLayer(dir, ".gitignore") : void 0;
|
|
26113
28277
|
const dirMarkLayers = markLayer ? [...markLayers, markLayer] : markLayers;
|
|
26114
28278
|
const skipLayer = readIgnoreLayer(dir, AKAIGNORE_FILENAME);
|
|
26115
28279
|
const dirSkipLayers = skipLayer ? [...skipLayers, skipLayer] : skipLayers;
|
|
@@ -26121,57 +28285,137 @@ function* walkSourceFiles(opts = {}) {
|
|
|
26121
28285
|
if (skipState !== "unignored" && (SKIP_DIRS.has(name) || skipState === "ignored")) {
|
|
26122
28286
|
continue;
|
|
26123
28287
|
}
|
|
26124
|
-
const dirIgnored = inIgnoredDir || evaluate(dirMarkLayers, fullPath, true) === "ignored";
|
|
28288
|
+
const dirIgnored = trackGitignore && (inIgnoredDir || evaluate(dirMarkLayers, fullPath, true) === "ignored");
|
|
26125
28289
|
yield* visit(fullPath, dirMarkLayers, dirSkipLayers, dirIgnored);
|
|
26126
28290
|
continue;
|
|
26127
28291
|
}
|
|
26128
28292
|
if (!entry.isFile()) continue;
|
|
26129
|
-
const ext = extname(name);
|
|
26130
|
-
if (!extensions.has(ext)) continue;
|
|
26131
28293
|
if (evaluate(dirSkipLayers, fullPath, false) === "ignored") continue;
|
|
26132
|
-
let size;
|
|
26133
|
-
let mtime;
|
|
26134
|
-
try {
|
|
26135
|
-
const st = statSync4(fullPath);
|
|
26136
|
-
size = st.size;
|
|
26137
|
-
mtime = st.mtime;
|
|
26138
|
-
} catch {
|
|
26139
|
-
continue;
|
|
26140
|
-
}
|
|
26141
|
-
if (size > maxBytes) continue;
|
|
26142
|
-
const meta3 = {
|
|
26143
|
-
path: fullPath,
|
|
26144
|
-
// Posix-separated like every stored relative path (and the ignore
|
|
26145
|
-
// matching above) — native separators must not leak into the contract.
|
|
26146
|
-
relativePath: relative3(rootDir, fullPath).split(sep4).join("/"),
|
|
26147
|
-
mtime: mtime.toISOString(),
|
|
26148
|
-
size,
|
|
26149
|
-
gitignored: inIgnoredDir || evaluate(dirMarkLayers, fullPath, false) === "ignored"
|
|
26150
|
-
};
|
|
26151
|
-
if (opts.shouldRead && !opts.shouldRead(meta3)) continue;
|
|
26152
|
-
let content;
|
|
26153
|
-
try {
|
|
26154
|
-
content = readFileSync7(fullPath, "utf8");
|
|
26155
|
-
} catch {
|
|
26156
|
-
continue;
|
|
26157
|
-
}
|
|
26158
28294
|
yield {
|
|
26159
|
-
path:
|
|
26160
|
-
|
|
26161
|
-
|
|
26162
|
-
mtime: meta3.mtime,
|
|
26163
|
-
gitignored: meta3.gitignored
|
|
28295
|
+
path: fullPath,
|
|
28296
|
+
name,
|
|
28297
|
+
gitignored: trackGitignore && (inIgnoredDir || evaluate(dirMarkLayers, fullPath, false) === "ignored")
|
|
26164
28298
|
};
|
|
26165
28299
|
}
|
|
26166
28300
|
}
|
|
26167
28301
|
yield* visit(rootDir, [], rootSkipLayers, false);
|
|
26168
28302
|
}
|
|
28303
|
+
function* walkSourceFiles(opts = {}) {
|
|
28304
|
+
const rootDir = opts.rootDir ?? process.cwd();
|
|
28305
|
+
const extensions = opts.extensions ?? SOURCE_EXTENSIONS;
|
|
28306
|
+
const maxBytes = opts.maxFileSizeBytes ?? DEFAULT_MAX_BYTES;
|
|
28307
|
+
for (const file2 of walkTree(rootDir, {
|
|
28308
|
+
excludePatterns: opts.excludePatterns,
|
|
28309
|
+
trackGitignore: true
|
|
28310
|
+
})) {
|
|
28311
|
+
const ext = extname(file2.name);
|
|
28312
|
+
if (!extensions.has(ext)) continue;
|
|
28313
|
+
let size;
|
|
28314
|
+
let mtime;
|
|
28315
|
+
try {
|
|
28316
|
+
const st = statSync4(file2.path);
|
|
28317
|
+
size = st.size;
|
|
28318
|
+
mtime = st.mtime;
|
|
28319
|
+
} catch {
|
|
28320
|
+
continue;
|
|
28321
|
+
}
|
|
28322
|
+
if (size > maxBytes) continue;
|
|
28323
|
+
const meta3 = {
|
|
28324
|
+
path: file2.path,
|
|
28325
|
+
// Posix-separated like every stored relative path (and the ignore
|
|
28326
|
+
// matching inside walkTree) — native separators must not leak into the
|
|
28327
|
+
// contract.
|
|
28328
|
+
relativePath: relative3(rootDir, file2.path).split(sep5).join("/"),
|
|
28329
|
+
mtime: mtime.toISOString(),
|
|
28330
|
+
size,
|
|
28331
|
+
gitignored: file2.gitignored
|
|
28332
|
+
};
|
|
28333
|
+
if (opts.shouldRead && !opts.shouldRead(meta3)) continue;
|
|
28334
|
+
let content;
|
|
28335
|
+
try {
|
|
28336
|
+
content = readFileSync7(file2.path, "utf8");
|
|
28337
|
+
} catch {
|
|
28338
|
+
continue;
|
|
28339
|
+
}
|
|
28340
|
+
yield {
|
|
28341
|
+
path: meta3.path,
|
|
28342
|
+
relativePath: meta3.relativePath,
|
|
28343
|
+
content,
|
|
28344
|
+
mtime: meta3.mtime,
|
|
28345
|
+
gitignored: meta3.gitignored
|
|
28346
|
+
};
|
|
28347
|
+
}
|
|
28348
|
+
}
|
|
28349
|
+
|
|
28350
|
+
// ../../packages/scanner/src/manifests.ts
|
|
28351
|
+
var MAX_MANIFEST_BYTES = 512 * 1024;
|
|
28352
|
+
function collectManifests(rootDir, maxFileSizeBytes = MAX_MANIFEST_BYTES) {
|
|
28353
|
+
const found = [];
|
|
28354
|
+
for (const file2 of walkTree(rootDir)) {
|
|
28355
|
+
const kind = manifestKindOf(file2.name);
|
|
28356
|
+
if (kind === null) continue;
|
|
28357
|
+
try {
|
|
28358
|
+
const st = statSync5(file2.path);
|
|
28359
|
+
if (st.size > maxFileSizeBytes) continue;
|
|
28360
|
+
found.push({ path: file2.path, kind, mtime: st.mtime.toISOString(), size: st.size });
|
|
28361
|
+
} catch {
|
|
28362
|
+
continue;
|
|
28363
|
+
}
|
|
28364
|
+
}
|
|
28365
|
+
return found;
|
|
28366
|
+
}
|
|
28367
|
+
|
|
28368
|
+
// ../../packages/scanner/src/resolve.ts
|
|
28369
|
+
function computeResolutions(prior, current) {
|
|
28370
|
+
const cur = new Set(current);
|
|
28371
|
+
return [...new Set(prior)].filter((k) => !cur.has(k));
|
|
28372
|
+
}
|
|
26169
28373
|
|
|
26170
28374
|
// ../../packages/scanner/src/scan.ts
|
|
26171
|
-
async function loadLedger(gateway, runtime) {
|
|
26172
|
-
const
|
|
28375
|
+
async function loadLedger(gateway, runtime, dataSharesInPlace) {
|
|
28376
|
+
const egressMaterial = `${dataSharesInPlace ? "egress:on" : "egress:off"}
|
|
28377
|
+
${EGRESS_VERSION_MATERIAL}`;
|
|
28378
|
+
const rulesetHash = contentHashOf(
|
|
28379
|
+
`${await runtime.rulesetFingerprint()}:${contentHashOf(egressMaterial)}`
|
|
28380
|
+
);
|
|
26173
28381
|
return { previous: await gateway.scanLedger(rulesetHash), rulesetHash };
|
|
26174
28382
|
}
|
|
28383
|
+
function resolveEgressProject(rootDir) {
|
|
28384
|
+
try {
|
|
28385
|
+
const identity = resolveRepoIdentity(rootDir);
|
|
28386
|
+
const worktreeRoot = resolveWorktreeRoot(rootDir);
|
|
28387
|
+
if (identity && worktreeRoot) {
|
|
28388
|
+
return { root: worktreeRoot, projectKey: `git:${identity.url}`, project: identity.name };
|
|
28389
|
+
}
|
|
28390
|
+
return resolveNonGitProject(rootDir, manifestKindOf);
|
|
28391
|
+
} catch {
|
|
28392
|
+
return null;
|
|
28393
|
+
}
|
|
28394
|
+
}
|
|
28395
|
+
function egressKey(root, absPath) {
|
|
28396
|
+
const rel = toPosix(relative4(root, absPath));
|
|
28397
|
+
return rel === "" || rel.startsWith("../") ? null : rel;
|
|
28398
|
+
}
|
|
28399
|
+
function startEgress(rootDir) {
|
|
28400
|
+
const project = resolveEgressProject(rootDir);
|
|
28401
|
+
if (project === null) return null;
|
|
28402
|
+
return { project, files: [], scannedFiles: [], deletedFiles: [] };
|
|
28403
|
+
}
|
|
28404
|
+
function collectFileEgress(acc, absPath, content, manifestKind) {
|
|
28405
|
+
const file2 = egressKey(acc.project.root, absPath);
|
|
28406
|
+
if (file2 === null) return;
|
|
28407
|
+
acc.scannedFiles.push(file2);
|
|
28408
|
+
if (content.includes("\0")) return;
|
|
28409
|
+
const vendored = isVendoredPath(file2);
|
|
28410
|
+
if (manifestKind !== null) {
|
|
28411
|
+
const sdkHits = extractManifestSdks(content, manifestKind);
|
|
28412
|
+
if (sdkHits.length > 0) acc.files.push({ file: file2, vendored, endpoints: [], sdkHits });
|
|
28413
|
+
return;
|
|
28414
|
+
}
|
|
28415
|
+
if (!EGRESS_CODE_EXTENSIONS.has(extname2(absPath))) return;
|
|
28416
|
+
const endpoints = extractEgress(content);
|
|
28417
|
+
if (endpoints.length > 0) acc.files.push({ file: file2, vendored, endpoints, sdkHits: [] });
|
|
28418
|
+
}
|
|
26175
28419
|
async function resolveRemovedFindings(gateway, path, currentKeys, evidence) {
|
|
26176
28420
|
const prior = await gateway.openAtRestKeysForPath(path);
|
|
26177
28421
|
const toResolve = computeResolutions(prior, currentKeys);
|
|
@@ -26207,12 +28451,15 @@ function isUnderRoot(path, rootDir) {
|
|
|
26207
28451
|
return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
26208
28452
|
}
|
|
26209
28453
|
async function sweepDeletedFiles(gateway, rootDir, previous) {
|
|
28454
|
+
const deleted = [];
|
|
26210
28455
|
for (const path of previous.keys()) {
|
|
26211
28456
|
if (!isUnderRoot(path, rootDir) || existsSync5(path)) continue;
|
|
28457
|
+
deleted.push(path);
|
|
26212
28458
|
await resolveRemovedFindings(gateway, path, [], { deleted: true });
|
|
26213
28459
|
}
|
|
28460
|
+
return deleted;
|
|
26214
28461
|
}
|
|
26215
|
-
async function scanDir(runtime, gateway, seen, ledger, rootDir, opts) {
|
|
28462
|
+
async function scanDir(runtime, gateway, config2, seen, ledger, rootDir, opts) {
|
|
26216
28463
|
const byRule = {};
|
|
26217
28464
|
const bySeverity = {};
|
|
26218
28465
|
const updates = [];
|
|
@@ -26220,6 +28467,7 @@ async function scanDir(runtime, gateway, seen, ledger, rootDir, opts) {
|
|
|
26220
28467
|
let skipped = 0;
|
|
26221
28468
|
let findings = 0;
|
|
26222
28469
|
let gitignoredFindings = 0;
|
|
28470
|
+
const egress = config2.settings.dataSharesInPlace ? startEgress(rootDir) : null;
|
|
26223
28471
|
const shouldRead = (meta3) => {
|
|
26224
28472
|
const prev = ledger.previous.get(meta3.path);
|
|
26225
28473
|
if (prev?.mtime === meta3.mtime) {
|
|
@@ -26246,6 +28494,7 @@ async function scanDir(runtime, gateway, seen, ledger, rootDir, opts) {
|
|
|
26246
28494
|
updates.push(ledgerEntry);
|
|
26247
28495
|
continue;
|
|
26248
28496
|
}
|
|
28497
|
+
if (egress) collectFileEgress(egress, file2.path, file2.content, null);
|
|
26249
28498
|
if (seen.has(hash2) && (await gateway.openAtRestKeysForPath(file2.path)).length === 0) {
|
|
26250
28499
|
skipped++;
|
|
26251
28500
|
updates.push(ledgerEntry);
|
|
@@ -26287,18 +28536,77 @@ async function scanDir(runtime, gateway, seen, ledger, rootDir, opts) {
|
|
|
26287
28536
|
contentHash: hash2
|
|
26288
28537
|
});
|
|
26289
28538
|
}
|
|
26290
|
-
|
|
26291
|
-
await sweepDeletedFiles(gateway, rootDir, ledger.previous);
|
|
28539
|
+
if (egress) scanManifests(egress, ledger, updates, rootDir);
|
|
28540
|
+
const deleted = await sweepDeletedFiles(gateway, rootDir, ledger.previous);
|
|
28541
|
+
if (egress) {
|
|
28542
|
+
for (const path of deleted) {
|
|
28543
|
+
const key = egressKey(egress.project.root, path);
|
|
28544
|
+
if (key !== null) egress.deletedFiles.push(key);
|
|
28545
|
+
}
|
|
28546
|
+
}
|
|
28547
|
+
const committed = await commitEgress(gateway, egress);
|
|
28548
|
+
if (committed === null) {
|
|
28549
|
+
return { rootDir, scanned, skipped, findings, gitignoredFindings, byRule, bySeverity };
|
|
28550
|
+
}
|
|
28551
|
+
await gateway.recordScanned(ledgerable(updates, egress, committed));
|
|
26292
28552
|
return { rootDir, scanned, skipped, findings, gitignoredFindings, byRule, bySeverity };
|
|
26293
28553
|
}
|
|
28554
|
+
function scanManifests(egress, ledger, updates, rootDir) {
|
|
28555
|
+
for (const manifest of collectManifests(rootDir)) {
|
|
28556
|
+
const prev = ledger.previous.get(manifest.path);
|
|
28557
|
+
if (prev?.mtime === manifest.mtime) continue;
|
|
28558
|
+
let content;
|
|
28559
|
+
try {
|
|
28560
|
+
content = readFileSync8(manifest.path, "utf8");
|
|
28561
|
+
} catch {
|
|
28562
|
+
continue;
|
|
28563
|
+
}
|
|
28564
|
+
const hash2 = contentHashOf(content);
|
|
28565
|
+
updates.push({
|
|
28566
|
+
path: manifest.path,
|
|
28567
|
+
mtime: manifest.mtime,
|
|
28568
|
+
contentHash: hash2,
|
|
28569
|
+
rulesetHash: ledger.rulesetHash
|
|
28570
|
+
});
|
|
28571
|
+
if (prev?.contentHash === hash2) continue;
|
|
28572
|
+
collectFileEgress(egress, manifest.path, content, manifest.kind);
|
|
28573
|
+
}
|
|
28574
|
+
}
|
|
28575
|
+
async function commitEgress(gateway, egress) {
|
|
28576
|
+
if (!egress) return EMPTY_DROPPED;
|
|
28577
|
+
const { project, files, scannedFiles, deletedFiles } = egress;
|
|
28578
|
+
if (scannedFiles.length === 0 && deletedFiles.length === 0 && files.length === 0) {
|
|
28579
|
+
return EMPTY_DROPPED;
|
|
28580
|
+
}
|
|
28581
|
+
try {
|
|
28582
|
+
const summary = await gateway.recordProjectEgress({
|
|
28583
|
+
projectKey: project.projectKey,
|
|
28584
|
+
project: project.project,
|
|
28585
|
+
projectId: null,
|
|
28586
|
+
reconcile: { mode: "ledger", scannedFiles, deletedFiles },
|
|
28587
|
+
hits: resolveEgress(files)
|
|
28588
|
+
});
|
|
28589
|
+
return new Set(summary.droppedFiles);
|
|
28590
|
+
} catch {
|
|
28591
|
+
return null;
|
|
28592
|
+
}
|
|
28593
|
+
}
|
|
28594
|
+
var EMPTY_DROPPED = /* @__PURE__ */ new Set();
|
|
28595
|
+
function ledgerable(updates, egress, dropped) {
|
|
28596
|
+
if (!egress || dropped.size === 0) return [...updates];
|
|
28597
|
+
return updates.filter((entry) => {
|
|
28598
|
+
const key = egressKey(egress.project.root, entry.path);
|
|
28599
|
+
return key === null || !dropped.has(key);
|
|
28600
|
+
});
|
|
28601
|
+
}
|
|
26294
28602
|
async function scanWorktree(config2, opts) {
|
|
26295
28603
|
const rootDir = opts.rootDir ?? process.cwd();
|
|
26296
28604
|
const gateway = resolveDataGateway(config2);
|
|
26297
28605
|
const runtime = createPluginRuntime(gateway, config2.settings, { dataDir: config2.dataDir });
|
|
26298
28606
|
try {
|
|
26299
28607
|
const seen = await gateway.knownContentHashes();
|
|
26300
|
-
const ledger = await loadLedger(gateway, runtime);
|
|
26301
|
-
return await scanDir(runtime, gateway, seen, ledger, rootDir, opts);
|
|
28608
|
+
const ledger = await loadLedger(gateway, runtime, config2.settings.dataSharesInPlace);
|
|
28609
|
+
return await scanDir(runtime, gateway, config2, seen, ledger, rootDir, opts);
|
|
26302
28610
|
} finally {
|
|
26303
28611
|
await runtime.close();
|
|
26304
28612
|
}
|
|
@@ -26318,9 +28626,9 @@ async function scanAllRepos(config2, opts) {
|
|
|
26318
28626
|
};
|
|
26319
28627
|
try {
|
|
26320
28628
|
const seen = await gateway.knownContentHashes();
|
|
26321
|
-
const ledger = await loadLedger(gateway, runtime);
|
|
28629
|
+
const ledger = await loadLedger(gateway, runtime, config2.settings.dataSharesInPlace);
|
|
26322
28630
|
for (const rootDir of repoDirs) {
|
|
26323
|
-
const repoSummary = await scanDir(runtime, gateway, seen, ledger, rootDir, opts);
|
|
28631
|
+
const repoSummary = await scanDir(runtime, gateway, config2, seen, ledger, rootDir, opts);
|
|
26324
28632
|
summary.repos.push({ rootDir, summary: repoSummary });
|
|
26325
28633
|
summary.totalScanned += repoSummary.scanned;
|
|
26326
28634
|
summary.totalSkipped += repoSummary.skipped;
|