@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0

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.
Files changed (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -62,6 +62,10 @@ export function requireRecord(value, label) {
62
62
  throw new TypeError(`${label} must be an object.`);
63
63
  return value;
64
64
  }
65
+ /** Stable ordering for bounded record selection, independent of host collation. */
66
+ export function compareCodeUnits(left, right) {
67
+ return left < right ? -1 : left > right ? 1 : 0;
68
+ }
65
69
  export function requireBoundedString(value, label, maxLength) {
66
70
  const normalised = strictBoundedString(value, maxLength);
67
71
  if (!normalised) {
@@ -1,330 +1,2 @@
1
- export const MAX_BOUNDED_JSON_DEPTH = 48;
2
- export const MAX_BOUNDED_JSON_KEYS = 50_000;
3
- export const MAX_BOUNDED_JSON_VALUES = 100_000;
4
- export const MAX_BOUNDED_JSON_CONTAINER_ITEMS = 10_000;
5
- export const UNSAFE_JSON_OBJECT_KEYS = Object.freeze(['__proto__']);
6
- /** Aggregate work allowed by a separately enforced UTF-8 byte budget. */
7
- export function boundedJsonLimitsForBytes(maximumBytes) {
8
- if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1) {
9
- throw new TypeError('The JSON byte budget must be a positive safe integer.');
10
- }
11
- // A member needs at least five bytes (comma, two quotes, colon, value).
12
- // Values need at least two bytes each, apart from a scalar root or the last
13
- // array item. These upper bounds do not depend on a model's current fields.
14
- // Nesting and individual-container limits remain independently bounded.
15
- return {
16
- maximumKeys: Math.floor(maximumBytes / 5),
17
- maximumValues: Math.ceil(maximumBytes / 2),
18
- // Decoded text is a lower bound on its UTF-8 JSON representation. Checking
19
- // it before stringify also prevents an oversized string allocation.
20
- maximumStringCodeUnits: maximumBytes,
21
- };
22
- }
23
- function syntaxError() {
24
- throw new TypeError('Artefact input is not valid JSON.');
25
- }
26
- export function isSafeJsonObjectKey(value) {
27
- return !UNSAFE_JSON_OBJECT_KEYS.includes(value);
28
- }
29
- export function assertBoundedJsonStructure(value, label = 'JSON value', limits = {}) {
30
- const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
31
- const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
32
- const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
33
- const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
34
- let keys = 0;
35
- let values = 0;
36
- let stringCodeUnits = 0;
37
- const ancestors = new Set();
38
- const text = (value) => {
39
- stringCodeUnits += value.length;
40
- if (limits.maximumStringCodeUnits !== undefined && stringCodeUnits > limits.maximumStringCodeUnits) {
41
- throw new TypeError(`${label} exceeds the aggregate text limit.`);
42
- }
43
- };
44
- const visit = (current, depth) => {
45
- values += 1;
46
- if (values > maximumValues) {
47
- throw new TypeError(`${label} exceeds the ${maximumValues}-value limit.`);
48
- }
49
- if (depth > maximumDepth) {
50
- throw new TypeError(`${label} exceeds the ${maximumDepth}-level nesting limit.`);
51
- }
52
- if (typeof current === 'string') {
53
- text(current);
54
- return;
55
- }
56
- if (current === null || typeof current === 'boolean')
57
- return;
58
- if (typeof current === 'number') {
59
- if (!Number.isFinite(current))
60
- throw new TypeError(`${label} contains a non-JSON number.`);
61
- return;
62
- }
63
- if (typeof current !== 'object')
64
- throw new TypeError(`${label} contains a non-JSON value.`);
65
- if (ancestors.has(current))
66
- throw new TypeError(`${label} contains a cyclic object reference.`);
67
- let prototype;
68
- let ownKeys;
69
- let descriptors;
70
- try {
71
- prototype = Object.getPrototypeOf(current);
72
- ownKeys = Reflect.ownKeys(current);
73
- descriptors = Object.getOwnPropertyDescriptors(current);
74
- }
75
- catch {
76
- throw new TypeError(`${label} could not be inspected safely.`);
77
- }
78
- if (ownKeys.some((key) => typeof key !== 'string')) {
79
- throw new TypeError(`${label} contains a symbol key.`);
80
- }
81
- if (ownKeys.some((key) => {
82
- const descriptor = descriptors[String(key)];
83
- return !descriptor || !Object.hasOwn(descriptor, 'value');
84
- })) {
85
- throw new TypeError(`${label} contains an accessor property.`);
86
- }
87
- ancestors.add(current);
88
- if (Array.isArray(current)) {
89
- if (prototype !== Array.prototype) {
90
- throw new TypeError(`${label} contains an array with a custom prototype.`);
91
- }
92
- const length = descriptors.length?.value;
93
- if (!Number.isSafeInteger(length) || length < 0 || length > maximumContainerItems) {
94
- throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
95
- }
96
- const indexes = ownKeys.filter((key) => key !== 'length');
97
- if (indexes.length !== length
98
- || indexes.some((key, index) => key !== String(index) || descriptors[key]?.enumerable !== true)) {
99
- throw new TypeError(`${label} contains a sparse, extended, or non-enumerable array.`);
100
- }
101
- for (const key of indexes)
102
- visit(descriptors[key].value, depth + 1);
103
- }
104
- else {
105
- if (prototype !== Object.prototype && prototype !== null) {
106
- throw new TypeError(`${label} contains an object with a custom prototype.`);
107
- }
108
- const objectKeys = ownKeys;
109
- if (objectKeys.length > maximumContainerItems) {
110
- throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
111
- }
112
- if (objectKeys.some((key) => descriptors[key]?.enumerable !== true)) {
113
- throw new TypeError(`${label} contains a non-enumerable object property.`);
114
- }
115
- keys += objectKeys.length;
116
- if (keys > maximumKeys) {
117
- throw new TypeError(`${label} exceeds the ${maximumKeys}-key limit.`);
118
- }
119
- for (const key of objectKeys) {
120
- if (!isSafeJsonObjectKey(key))
121
- throw new TypeError(`${label} contains an unsafe object key.`);
122
- text(key);
123
- visit(descriptors[key].value, depth + 1);
124
- }
125
- }
126
- ancestors.delete(current);
127
- };
128
- visit(value, 0);
129
- }
130
- export function scanBoundedJson(raw, limits = {}, observeString) {
131
- const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
132
- const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
133
- const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
134
- const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
135
- let index = 0;
136
- let keys = 0;
137
- let values = 0;
138
- let stringCodeUnits = 0;
139
- const whitespace = () => {
140
- while (index < raw.length) {
141
- const code = raw.charCodeAt(index);
142
- if (code !== 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d)
143
- break;
144
- index += 1;
145
- }
146
- };
147
- const stringToken = () => {
148
- const start = index;
149
- index += 1;
150
- while (index < raw.length) {
151
- const character = raw[index];
152
- if (character === '"') {
153
- index += 1;
154
- let value;
155
- try {
156
- value = JSON.parse(raw.slice(start, index));
157
- }
158
- catch {
159
- syntaxError();
160
- }
161
- stringCodeUnits += value.length;
162
- if (limits.maximumStringCodeUnits !== undefined && stringCodeUnits > limits.maximumStringCodeUnits) {
163
- throw new TypeError('Artefact JSON exceeds the aggregate text limit.');
164
- }
165
- return { value, offset: start };
166
- }
167
- if (character === '\\') {
168
- index += 1;
169
- if (index >= raw.length)
170
- syntaxError();
171
- if (raw[index] === 'u') {
172
- if (!/^[a-fA-F0-9]{4}$/u.test(raw.slice(index + 1, index + 5)))
173
- syntaxError();
174
- index += 5;
175
- }
176
- else {
177
- if (!/["\\/bfnrt]/u.test(raw[index]))
178
- syntaxError();
179
- index += 1;
180
- }
181
- continue;
182
- }
183
- if (character.charCodeAt(0) <= 0x1f)
184
- syntaxError();
185
- index += 1;
186
- }
187
- return syntaxError();
188
- };
189
- const literal = (token) => {
190
- if (raw.slice(index, index + token.length) !== token)
191
- syntaxError();
192
- index += token.length;
193
- };
194
- const number = () => {
195
- const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u.exec(raw.slice(index));
196
- if (!match)
197
- syntaxError();
198
- if (!Number.isFinite(Number(match[0]))) {
199
- throw new TypeError('Artefact JSON contains a non-finite number.');
200
- }
201
- index += match[0].length;
202
- };
203
- const value = (depth, propertyKey) => {
204
- values += 1;
205
- if (values > maximumValues) {
206
- throw new TypeError(`Artefact JSON exceeds the ${maximumValues}-value limit.`);
207
- }
208
- if (depth > maximumDepth) {
209
- throw new TypeError(`Artefact JSON exceeds the ${maximumDepth}-level nesting limit.`);
210
- }
211
- whitespace();
212
- const character = raw[index];
213
- if (character === '"') {
214
- const parsedString = stringToken();
215
- observeString?.({ ...parsedString, kind: 'value', ...(propertyKey === undefined ? {} : { propertyKey }) });
216
- return;
217
- }
218
- if (character === '{') {
219
- index += 1;
220
- whitespace();
221
- const seen = new Set();
222
- if (raw[index] === '}') {
223
- index += 1;
224
- return;
225
- }
226
- while (index < raw.length) {
227
- if (raw[index] !== '"')
228
- syntaxError();
229
- const parsedKey = stringToken();
230
- const key = parsedKey.value;
231
- if (seen.size >= maximumContainerItems) {
232
- throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
233
- }
234
- keys += 1;
235
- if (keys > maximumKeys) {
236
- throw new TypeError(`Artefact JSON exceeds the ${maximumKeys}-key limit.`);
237
- }
238
- if (seen.has(key))
239
- throw new TypeError('Artefact JSON contains a duplicate object key.');
240
- if (!isSafeJsonObjectKey(key))
241
- throw new TypeError('Artefact JSON contains an unsafe object key.');
242
- seen.add(key);
243
- observeString?.({ ...parsedKey, kind: 'key' });
244
- whitespace();
245
- if (raw[index] !== ':')
246
- syntaxError();
247
- index += 1;
248
- value(depth + 1, key);
249
- whitespace();
250
- if (raw[index] === '}') {
251
- index += 1;
252
- return;
253
- }
254
- if (raw[index] !== ',')
255
- syntaxError();
256
- index += 1;
257
- whitespace();
258
- }
259
- syntaxError();
260
- }
261
- if (character === '[') {
262
- index += 1;
263
- whitespace();
264
- if (raw[index] === ']') {
265
- index += 1;
266
- return;
267
- }
268
- let items = 0;
269
- while (index < raw.length) {
270
- items += 1;
271
- if (items > maximumContainerItems) {
272
- throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
273
- }
274
- value(depth + 1);
275
- whitespace();
276
- if (raw[index] === ']') {
277
- index += 1;
278
- return;
279
- }
280
- if (raw[index] !== ',')
281
- syntaxError();
282
- index += 1;
283
- whitespace();
284
- }
285
- syntaxError();
286
- }
287
- if (character === 't') {
288
- literal('true');
289
- return;
290
- }
291
- if (character === 'f') {
292
- literal('false');
293
- return;
294
- }
295
- if (character === 'n') {
296
- literal('null');
297
- return;
298
- }
299
- number();
300
- };
301
- whitespace();
302
- value(0);
303
- whitespace();
304
- if (index !== raw.length)
305
- syntaxError();
306
- }
307
- export function parseBoundedJson(raw, options) {
308
- const label = options.label ?? 'Artefact input';
309
- if (typeof raw !== 'string')
310
- throw new TypeError(`${label} must be UTF-8 JSON text.`);
311
- const bytes = new TextEncoder().encode(raw).byteLength;
312
- if (bytes < 1 || bytes > options.maximumBytes) {
313
- throw new TypeError(`${label} must be between 1 byte and ${options.maximumBytes} bytes.`);
314
- }
315
- scanBoundedJson(raw, options.limits);
316
- try {
317
- return JSON.parse(raw);
318
- }
319
- catch {
320
- return syntaxError();
321
- }
322
- }
323
- export function parseBoundedJsonObject(raw, options) {
324
- const label = options.label ?? 'Artefact input';
325
- const parsed = parseBoundedJson(raw, options);
326
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
327
- throw new TypeError(`${label} must contain one JSON object.`);
328
- }
329
- return parsed;
330
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/bounded-json.mjs";
@@ -11,9 +11,9 @@ import { CISA_KEV_CATALOG } from "./generated/cisa-kev-catalog.mjs";
11
11
  import { createObservation, normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
12
12
  import { isCveIdentifier } from "../packages/contracts/vulnerability-identifiers.mjs";
13
13
  import { BROWSER_LIBRARY_PROFILE_VERSION, MAX_LIBRARY_FINDINGS, } from "./lookup-child-profile-contract.mjs";
14
- import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
14
+ import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_REFERENCE_LENGTH, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
15
15
  // Catalogue expressions are reviewed third-party inputs. Keep every individual
16
- // regular-expression evaluation and the aggregate inline-signature work small
16
+ // regular-expression evaluation and aggregate reference/inline-signature work small
17
17
  // enough that a target-controlled script cannot monopolise the Node event loop.
18
18
  // Full-content hashes still use the complete already-capped script body.
19
19
  const MAX_INLINE_LIBRARY_SCAN_CHARS = 1_024;
@@ -23,7 +23,6 @@ const MAX_INLINE_LIBRARY_WORKER_BYTES = 64 * 1024;
23
23
  const MAX_LIBRARY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
24
24
  const MAX_ADVISORY_IDENTIFIERS = 16;
25
25
  const MAX_WEAKNESS_CLASSES = 12;
26
- const MAX_MATCHES_PER_PATTERN = 4;
27
26
  const MAX_VERSION_LENGTH = 64;
28
27
  const COMPONENT_RE = /^[a-z0-9._-]{1,80}$/i;
29
28
  const VERSION_RE = /^[0-9][0-9.a-z_-]{0,63}$/i;
@@ -41,38 +40,61 @@ const KNOWN_EXPLOITED_IDENTIFIERS = new Set(CISA_KEV_CATALOG.identifiers);
41
40
  const KNOWN_EXPLOITED_RELEASED_AT = normalizeExplicitIsoTimestamp(CISA_KEV_CATALOG.releasedAt);
42
41
  if (KNOWN_EXPLOITED_RELEASED_AT === null)
43
42
  throw new Error('Pinned known-exploited catalogue has an invalid release timestamp.');
44
- const INLINE_EXTRACTOR_CATALOGUE = Object.freeze(Object.entries(CATALOG_COMPONENTS).map(([component, value]) => {
43
+ const PATTERN_EXTRACTOR_CATALOGUE = Object.freeze(Object.entries(CATALOG_COMPONENTS).map(([component, value]) => {
45
44
  const extractors = record(record(value).extractors);
45
+ const patterns = (name) => Object.freeze((Array.isArray(extractors[name]) ? extractors[name] : [])
46
+ .filter((pattern) => typeof pattern === 'string' && pattern.length > 0 && pattern.length <= 2_048)
47
+ .slice(0, 64));
46
48
  return Object.freeze({
47
49
  component,
48
- patterns: Object.freeze((Array.isArray(extractors.filecontent) ? extractors.filecontent : [])
49
- .filter((pattern) => typeof pattern === 'string' && pattern.length > 0 && pattern.length <= 2_048)
50
- .slice(0, 64)),
51
- replacements: Object.freeze((Array.isArray(extractors.filecontentreplace) ? extractors.filecontentreplace : [])
52
- .filter((pattern) => typeof pattern === 'string' && pattern.length > 0 && pattern.length <= 2_048)
53
- .slice(0, 64)),
50
+ uri: patterns('uri'), filename: patterns('filename'),
51
+ patterns: patterns('filecontent'), replacements: patterns('filecontentreplace'),
54
52
  });
55
53
  }));
56
- const INLINE_REGEX_WORKER_SOURCE = String.raw `
54
+ const LIBRARY_REGEX_WORKER_SOURCE = String.raw `
57
55
  const { parentPort, workerData } = require('node:worker_threads');
58
56
  const versions = /^[0-9][0-9.a-z_-]{0,63}$/i;
59
57
  parentPort.on('message', (job) => {
60
58
  const control = new Int32Array(job.control);
61
59
  const output = new Uint8Array(job.output);
62
- const matches = [];
63
- const add = (component, version) => {
60
+ const matches = new Map();
61
+ const add = (component, version, method) => {
64
62
  const normalized = String(version).replace(/(?:\.|-)?min$/i, '').slice(0, 64);
65
- if (versions.test(normalized) && matches.length < 64) matches.push([component, normalized]);
63
+ const key = component + '\u0000' + normalized;
64
+ if (!versions.test(normalized)) return;
65
+ if (!matches.has(key) && matches.size < 64) matches.set(key, [component, normalized, new Set()]);
66
+ matches.get(key)?.[2].add(method);
66
67
  };
67
68
  try {
69
+ const input = JSON.parse(job.value);
70
+ for (const reference of input.references) {
71
+ const filename = (reference.replace(/\\/g, '/').split('/').pop() || '').split(/[?#]/, 1)[0];
72
+ for (const entry of workerData.catalogue) {
73
+ for (const [patterns, value, method, anchored] of [
74
+ [entry.uri, reference, 'script URL', false],
75
+ [entry.filename, filename, 'script filename', true],
76
+ ]) {
77
+ if (!value) continue;
78
+ for (const pattern of patterns) {
79
+ let expression;
80
+ try { expression = new RegExp(anchored ? '^(?:' + pattern + ')$' : pattern, 'g'); } catch { continue; }
81
+ let match, count = 0;
82
+ while ((match = expression.exec(value)) && count < 4) {
83
+ if (typeof match[1] === 'string' && versions.test(match[1])) { add(entry.component, match[1], method); count += 1; }
84
+ if (match[0] === '') expression.lastIndex += 1;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
68
90
  for (const entry of workerData.catalogue) {
69
91
  for (const pattern of entry.patterns) {
70
92
  let expression;
71
93
  try { expression = new RegExp(pattern, 'g'); } catch { continue; }
72
94
  let match;
73
95
  let count = 0;
74
- while ((match = expression.exec(job.value)) && count < 4) {
75
- if (typeof match[1] === 'string') add(entry.component, match[1]);
96
+ while ((match = expression.exec(input.inline)) && count < 4) {
97
+ if (typeof match[1] === 'string') add(entry.component, match[1], 'inline signature');
76
98
  if (match[0] === '') expression.lastIndex += 1;
77
99
  count += 1;
78
100
  }
@@ -84,16 +106,16 @@ parentPort.on('message', (job) => {
84
106
  try { expression = new RegExp(descriptor[1], 'g'); } catch { continue; }
85
107
  let match;
86
108
  let count = 0;
87
- while ((match = expression.exec(job.value)) && count < 4) {
109
+ while ((match = expression.exec(input.inline)) && count < 4) {
88
110
  let replaced = match[0];
89
111
  try { replaced = match[0].replace(new RegExp(descriptor[1]), descriptor[2]); } catch { break; }
90
- add(entry.component, replaced);
112
+ add(entry.component, replaced, 'inline signature');
91
113
  if (match[0] === '') expression.lastIndex += 1;
92
114
  count += 1;
93
115
  }
94
116
  }
95
117
  }
96
- const encoded = Buffer.from(JSON.stringify(matches));
118
+ const encoded = Buffer.from(JSON.stringify([...matches.values()].flatMap(([component, version, methods]) => [...methods].map(method => [component, version, method]))));
97
119
  if (encoded.length > output.length) Atomics.store(control, 0, -2);
98
120
  else { output.set(encoded); Atomics.store(control, 0, encoded.length); }
99
121
  } catch {
@@ -102,8 +124,9 @@ parentPort.on('message', (job) => {
102
124
  parentPort.postMessage({ id: job.id });
103
125
  });
104
126
  `;
105
- const scanInlineLibrary = createInlineLibraryScanner(() => new Worker(INLINE_REGEX_WORKER_SOURCE, { eval: true, workerData: { catalogue: INLINE_EXTRACTOR_CATALOGUE } }), {
106
- maximumCharacters: MAX_INLINE_LIBRARY_SCAN_TOTAL_CHARS + MAX_SCRIPT_ELEMENTS,
127
+ const scanLibraryPatterns = createInlineLibraryScanner(() => new Worker(LIBRARY_REGEX_WORKER_SOURCE, { eval: true, workerData: { catalogue: PATTERN_EXTRACTOR_CATALOGUE } }), {
128
+ // JSON can escape each admitted character to six characters; include separators and keys.
129
+ maximumCharacters: 6 * (MAX_INLINE_LIBRARY_SCAN_TOTAL_CHARS + MAX_SCRIPT_ELEMENTS * MAX_SCRIPT_REFERENCE_LENGTH) + MAX_SCRIPT_ELEMENTS * 10 + 128,
107
130
  outputBytes: MAX_INLINE_LIBRARY_WORKER_BYTES,
108
131
  deadlineMs: MAX_INLINE_LIBRARY_SCAN_MS,
109
132
  // One worker/output buffer, with at most sixteen bounded pending inputs.
@@ -166,26 +189,6 @@ function matchingVulnerabilities(component, version) {
166
189
  }
167
190
  return matched;
168
191
  }
169
- function matchPattern(pattern, value) {
170
- if (typeof pattern !== 'string' || pattern.length === 0 || pattern.length > 2_048)
171
- return [];
172
- let expression;
173
- try {
174
- expression = new RegExp(pattern, 'g');
175
- }
176
- catch {
177
- return [];
178
- }
179
- const matches = [];
180
- let match;
181
- while ((match = expression.exec(value)) && matches.length < MAX_MATCHES_PER_PATTERN) {
182
- if (typeof match[1] === 'string' && VERSION_RE.test(match[1]))
183
- matches.push(match[1]);
184
- if (match[0] === '')
185
- expression.lastIndex += 1;
186
- }
187
- return matches;
188
- }
189
192
  function addDetected(detected, component, version, method) {
190
193
  const normalizedVersion = version.replace(/(?:\.|-)?min$/i, '').slice(0, MAX_VERSION_LENGTH);
191
194
  if (!COMPONENT_RE.test(component) || !VERSION_RE.test(normalizedVersion))
@@ -200,38 +203,10 @@ function addDetected(detected, component, version, method) {
200
203
  return;
201
204
  detected.set(key, { component, version: normalizedVersion, detections: new Set([method]) });
202
205
  }
203
- function scanExtractor(detected, extractorName, value, method) {
204
- for (const [componentName, rawComponent] of Object.entries(CATALOG_COMPONENTS)) {
205
- const component = record(rawComponent);
206
- const extractors = record(component.extractors);
207
- const patterns = Array.isArray(extractors[extractorName]) ? extractors[extractorName] : [];
208
- for (const pattern of patterns.slice(0, 64)) {
209
- for (const version of matchPattern(pattern, value))
210
- addDetected(detected, componentName, version, method);
211
- }
212
- }
213
- }
214
- function scanFilename(detected, reference) {
215
- const filename = (reference.replace(/\\/g, '/').split('/').pop() || '').split(/[?#]/, 1)[0];
216
- if (!filename)
217
- return;
218
- for (const [componentName, rawComponent] of Object.entries(CATALOG_COMPONENTS)) {
219
- const component = record(rawComponent);
220
- const extractors = record(component.extractors);
221
- const patterns = Array.isArray(extractors.filename) ? extractors.filename : [];
222
- for (const pattern of patterns.slice(0, 64)) {
223
- if (typeof pattern !== 'string' || pattern.length > 2_048)
224
- continue;
225
- for (const version of matchPattern(`^(?:${pattern})$`, filename)) {
226
- addDetected(detected, componentName, version, 'script filename');
227
- }
228
- }
229
- }
230
- }
231
- async function scanInlineSignatures(detected, value, signal) {
232
- if (!value)
206
+ async function scanSignatures(detected, value, references, signal) {
207
+ if (!value && !references.length)
233
208
  return { timedOut: false, unavailable: false };
234
- const result = await scanInlineLibrary(value, signal);
209
+ const result = await scanLibraryPatterns(JSON.stringify({ inline: value, references }), signal);
235
210
  signal?.throwIfAborted();
236
211
  if (result.output === null)
237
212
  return result;
@@ -239,12 +214,13 @@ async function scanInlineSignatures(detected, value, signal) {
239
214
  const parsed = JSON.parse(result.output);
240
215
  if (!Array.isArray(parsed))
241
216
  return { timedOut: false, unavailable: true };
242
- for (const item of parsed.slice(0, MAX_LIBRARY_FINDINGS * 4)) {
243
- if (!Array.isArray(item) || item.length !== 2)
217
+ for (const item of parsed.slice(0, MAX_LIBRARY_FINDINGS * 4 * 3)) {
218
+ if (!Array.isArray(item) || item.length !== 3)
244
219
  continue;
245
- const [component, version] = item;
246
- if (typeof component === 'string' && typeof version === 'string') {
247
- addDetected(detected, component, version, 'inline signature');
220
+ const [component, version, method] = item;
221
+ if (typeof component === 'string' && typeof version === 'string'
222
+ && (method === 'inline signature' || method === 'script URL' || method === 'script filename')) {
223
+ addDetected(detected, component, version, method);
248
224
  }
249
225
  }
250
226
  return { timedOut: false, unavailable: false };
@@ -339,11 +315,11 @@ async function analyzeBrowserLibraries(input = {}) {
339
315
  let inlineSignatureCharactersExamined = 0;
340
316
  let inlineSignatureLimitReached = false;
341
317
  const inlineSignatureSamples = [];
318
+ const references = [];
342
319
  for (const script of htmlAnalysis.scripts) {
343
320
  if (script.reference) {
344
321
  referencesExamined += 1;
345
- scanExtractor(detected, 'uri', script.reference, 'script URL');
346
- scanFilename(detected, script.reference);
322
+ references.push(script.reference);
347
323
  }
348
324
  else if (script.inlineContent && script.mediaType !== 'application/ld+json') {
349
325
  inlineScriptsExamined += 1;
@@ -355,7 +331,7 @@ async function analyzeBrowserLibraries(input = {}) {
355
331
  inlineSignatureSamples.push(signatureScan.signatureContent);
356
332
  }
357
333
  }
358
- const inlineSignatureResult = await scanInlineSignatures(detected, inlineSignatureSamples.join('\u0000'), input.signal);
334
+ const inlineSignatureResult = await scanSignatures(detected, inlineSignatureSamples.join('\u0000'), references, input.signal);
359
335
  input.signal?.throwIfAborted();
360
336
  const inlineSignatureUnavailable = inlineSignatureResult.timedOut || inlineSignatureResult.unavailable;
361
337
  const projected = [...detected.values()].map(findingFromDetection);
@@ -395,9 +371,9 @@ async function analyzeBrowserLibraries(input = {}) {
395
371
  if (inlineSignatureLimitReached)
396
372
  limitations.push(`Passive library signature matching evaluated a deterministic ${MAX_INLINE_LIBRARY_SCAN_TOTAL_CHARS}-character cumulative sample across inline scripts; full-content hashes still used the complete bounded script content.`);
397
373
  if (inlineSignatureResult.timedOut)
398
- limitations.push(`Passive library signature matching exceeded its ${MAX_INLINE_LIBRARY_SCAN_MS} ms isolated-worker deadline; hash evidence was still evaluated.`);
374
+ limitations.push(`Passive library URL, filename and inline signature matching exceeded its ${MAX_INLINE_LIBRARY_SCAN_MS} ms isolated-worker deadline; hash evidence was still evaluated.`);
399
375
  if (inlineSignatureResult.unavailable)
400
- limitations.push('Passive library signature matching was unavailable in its isolated worker; hash evidence was still evaluated.');
376
+ limitations.push('Passive library URL, filename and inline signature matching was unavailable in its isolated worker; hash evidence was still evaluated.');
401
377
  if (findingLimitReached)
402
378
  limitations.push(`Only the first ${MAX_LIBRARY_FINDINGS} library findings were retained.`);
403
379
  if (omittedCveIdentifiers)
@@ -428,8 +404,10 @@ async function analyzeBrowserLibraries(input = {}) {
428
404
  inlineScriptsExamined,
429
405
  inlineCharactersExamined: htmlAnalysis.inlineCharactersExamined,
430
406
  inlineSignatureCharactersExamined,
431
- inlineSignatureTimedOut: inlineSignatureResult.timedOut,
432
- inlineSignatureUnavailable: inlineSignatureResult.unavailable,
407
+ inlineSignatureTimedOut: inlineSignatureSamples.length > 0 && inlineSignatureResult.timedOut,
408
+ inlineSignatureUnavailable: inlineSignatureSamples.length > 0 && inlineSignatureResult.unavailable,
409
+ referenceSignatureTimedOut: references.length > 0 && inlineSignatureResult.timedOut,
410
+ referenceSignatureUnavailable: references.length > 0 && inlineSignatureResult.unavailable,
433
411
  catalogComponents: Object.keys(CATALOG_COMPONENTS).length,
434
412
  findings: allFindings.length,
435
413
  advisoryMatches: allFindings.reduce((sum, finding) => sum + finding.advisoryCount, 0),
@@ -0,0 +1,2 @@
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/candidate-provenance-bounds.mjs";
package/lib/classify.mjs CHANGED
@@ -16,7 +16,7 @@
16
16
  import { isIP } from 'node:net';
17
17
  import { domainToASCII } from 'node:url';
18
18
  import { parse } from 'tldts';
19
- import { canonicalRegistrableDomain } from "./registrable-domain.mjs";
19
+ import { canonicalRegistrableDomain } from "../packages/analysis/registrable-domain.mjs";
20
20
  // A CR/LF here would let one query become multiple lines once it reaches
21
21
  // lib/whois.mts's raw `socket.write(query + '\r\n')` - a normal URL-encoded
22
22
  // query string (?q=example.com%0D%0AHELP) decodes back into literal control