@slicedearth/whoisleuth-cli 1.47.4 → 2.0.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 (235) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/provider-policy-admission.mjs +3 -1
  100. package/lib/rdap-bootstrap.mjs +76 -16
  101. package/lib/rdap-client.mjs +20 -12
  102. package/lib/rdap-normalization.mjs +306 -75
  103. package/lib/rdap-transport.mjs +2 -2
  104. package/lib/rdap-validation.mjs +65 -14
  105. package/lib/rdap.mjs +2 -2
  106. package/lib/registrable-domain.mjs +36 -0
  107. package/lib/registry-insights.mjs +7 -0
  108. package/lib/response-policy.mjs +27 -4
  109. package/lib/risk-calibration-summary.mjs +35 -33
  110. package/lib/risk-scoring.mjs +4 -3
  111. package/lib/safe-fetch.mjs +62 -40
  112. package/lib/security-txt.mjs +1 -1
  113. package/lib/smtp-transport-review.mjs +8 -5
  114. package/lib/sslbl-intelligence.mjs +6 -8
  115. package/lib/static-html-analysis.mjs +301 -4
  116. package/lib/structured-data-identity.mjs +51 -19
  117. package/lib/threat-intelligence-fixtures.mjs +2 -4
  118. package/lib/threat-intelligence-manifests.mjs +2 -7
  119. package/lib/threat-intelligence-runtime.mjs +3 -5
  120. package/lib/threat-intelligence-types.mjs +3 -0
  121. package/lib/threatfox-intelligence.mjs +7 -3
  122. package/lib/tls-intelligence.mjs +4 -4
  123. package/lib/urlhaus-intelligence.mjs +4 -2
  124. package/lib/urlscan-intelligence.mjs +6 -5
  125. package/lib/website-security-posture.mjs +1 -1
  126. package/lib/website-technology.mjs +1 -1
  127. package/lib/whois-authority.mjs +43 -8
  128. package/lib/whois-chain.mjs +25 -3
  129. package/lib/whois-normalization.mjs +3 -1
  130. package/lib/whois-transport.mjs +12 -3
  131. package/lib/zone-intent-review.mjs +1 -1
  132. package/package.json +12 -4
  133. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  134. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  135. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  136. package/packages/cases/case-migration-model.mjs +476 -0
  137. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  138. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  139. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  140. package/packages/cases/case-record-model.mjs +9 -0
  141. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  142. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  143. package/packages/cases/case-response-model.mjs +1604 -0
  144. package/packages/cases/case-response-packet.mjs +1312 -0
  145. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  146. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  147. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  148. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  149. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  150. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  151. package/packages/comparison/favicon-similarity.mjs +78 -0
  152. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  153. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  154. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  155. package/packages/contracts/analyst-review-state.mjs +50 -0
  156. package/packages/contracts/case-portability.mjs +1310 -0
  157. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  158. package/packages/contracts/cli-lookup.mjs +394 -0
  159. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  160. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  161. package/packages/contracts/domain-control-monitor.mjs +310 -0
  162. package/packages/contracts/domain-control-review.mjs +828 -0
  163. package/packages/contracts/domain-name.mjs +4 -0
  164. package/packages/contracts/external-observation-interchange.mjs +224 -0
  165. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  166. package/packages/contracts/investigation-portability.mjs +331 -0
  167. package/packages/contracts/offline-comparison.mjs +160 -0
  168. package/packages/contracts/risk-calibration.mjs +1288 -0
  169. package/packages/contracts/schema-compatibility.mjs +71 -0
  170. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  171. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  172. package/packages/contracts/workspace-portability.mjs +1347 -0
  173. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  174. package/packages/evidence/decision-fact.mjs +735 -0
  175. package/packages/evidence/domain-control-runtime.mjs +547 -0
  176. package/packages/evidence/domain-name.mjs +41 -0
  177. package/packages/evidence/observation.mjs +384 -0
  178. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  179. package/packages/interchange/retained-text.mjs +8 -0
  180. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  181. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  182. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  183. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  184. package/packages/investigation/bulk-review-policy.mjs +1 -0
  185. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  186. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  187. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  188. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  189. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  190. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  191. package/packages/monitoring/analyst-review-state.mjs +680 -0
  192. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
  193. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  194. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  195. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  196. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  197. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  198. package/packages/workspace/hostile-input.mjs +172 -0
  199. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  200. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  201. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  202. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  203. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  204. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  205. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  206. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  207. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  208. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  209. package/third-party-notices.txt +14 -14
  210. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  211. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  212. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  213. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  214. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  215. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  216. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  217. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  218. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  219. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  220. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  221. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  222. package/frontend/src/lib/analysis/scoring.js +0 -55
  223. package/frontend/src/lib/analysis/utils.js +0 -255
  224. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  225. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  226. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  227. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  228. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  229. package/lib/bounded-json-response.mjs +0 -170
  230. package/lib/lookup-response-contract.mjs +0 -1034
  231. package/lib/observation.mjs +0 -115
  232. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  233. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -32,10 +32,24 @@ function errorRecord(value) {
32
32
  ? value
33
33
  : {};
34
34
  }
35
+ function registryStatusToken(value) {
36
+ return value.toLowerCase().replace(/[^a-z0-9]+/gu, '');
37
+ }
35
38
  function rdapEventDate(events, action) {
36
39
  const date = events.find((event) => event.action === action)?.date;
37
40
  return typeof date === 'string' ? date : null;
38
41
  }
42
+ function withoutHttpDeliveryMetadata(value) {
43
+ if (!value || typeof value !== 'object' || Array.isArray(value))
44
+ return value;
45
+ const output = { ...value };
46
+ if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
47
+ const response = { ...output.response };
48
+ delete response.deliveryMetadata;
49
+ output.response = response;
50
+ }
51
+ return output;
52
+ }
39
53
  // No marketplace (Afternic/Sedo/Dan.com/GoDaddy Auctions/etc.) offers a
40
54
  // free, no-auth API to check "is this specific domain listed for sale" -
41
55
  // what's checkable without credentials is the parking/landing-page
@@ -448,40 +462,39 @@ async function checkDomainAvailability(domain, options = {}) {
448
462
  source: 'whois',
449
463
  };
450
464
  }
451
- if (parsed.nameservers.length)
452
- nameservers = parsed.nameservers;
453
- if (parsed.statuses.length)
454
- statuses = parsed.statuses.map((status) => status.toLowerCase());
455
- if (parsed.registrar) {
456
- registrar = {
457
- handle: null,
458
- ianaId: parsed.registrarIanaId || null,
459
- name: parsed.registrar,
460
- org: null,
461
- email: parsed.abuseEmail || null,
462
- phone: parsed.abusePhone || null,
463
- };
464
- }
465
- if (parsed.registrantName || parsed.registrantOrg || parsed.registrantEmail || parsed.registrantPhone) {
466
- registrant = {
467
- handle: null,
468
- name: parsed.registrantName || null,
469
- org: parsed.registrantOrg || null,
470
- email: parsed.registrantEmail || null,
471
- phone: parsed.registrantPhone || null,
472
- };
473
- }
474
- if (parsed.abuseEmail || parsed.abusePhone) {
475
- abuse = { handle: null, name: null, org: parsed.registrar || null, email: parsed.abuseEmail || null, phone: parsed.abusePhone || null };
476
- }
477
- createdDate = parsed.createdDate || null;
478
- expiryDate = parsed.expiryDate || null;
479
- createdDateIso = parsed.createdDateIso || parsed.lifecycle?.createdDateIso || registryDateIso(createdDate);
480
- expiryDateIso = parsed.expiryDateIso || parsed.lifecycle?.expiryDateIso || registryDateIso(expiryDate);
481
- if (!dnssec)
482
- dnssec = parsed.dnssec || null;
483
- if (parsed.registrationStatus === 'registered'
484
- || registrar || createdDate || expiryDate || nameservers.length > 0 || statuses.length > 0) {
465
+ if (parsed.registrationStatus === 'registered') {
466
+ if (parsed.nameservers.length)
467
+ nameservers = parsed.nameservers;
468
+ if (parsed.statuses.length)
469
+ statuses = parsed.statuses.map((status) => status.toLowerCase());
470
+ if (parsed.registrar) {
471
+ registrar = {
472
+ handle: null,
473
+ ianaId: parsed.registrarIanaId || null,
474
+ name: parsed.registrar,
475
+ org: null,
476
+ email: parsed.abuseEmail || null,
477
+ phone: parsed.abusePhone || null,
478
+ };
479
+ }
480
+ if (parsed.registrantName || parsed.registrantOrg || parsed.registrantEmail || parsed.registrantPhone) {
481
+ registrant = {
482
+ handle: null,
483
+ name: parsed.registrantName || null,
484
+ org: parsed.registrantOrg || null,
485
+ email: parsed.registrantEmail || null,
486
+ phone: parsed.registrantPhone || null,
487
+ };
488
+ }
489
+ if (parsed.abuseEmail || parsed.abusePhone) {
490
+ abuse = { handle: null, name: null, org: parsed.registrar || null, email: parsed.abuseEmail || null, phone: parsed.abusePhone || null };
491
+ }
492
+ createdDate = parsed.createdDate || null;
493
+ expiryDate = parsed.expiryDate || null;
494
+ createdDateIso = parsed.createdDateIso || parsed.lifecycle?.createdDateIso || registryDateIso(createdDate);
495
+ expiryDateIso = parsed.expiryDateIso || parsed.lifecycle?.expiryDateIso || registryDateIso(expiryDate);
496
+ if (!dnssec)
497
+ dnssec = parsed.dnssec || null;
485
498
  registrationSource = 'whois';
486
499
  }
487
500
  }
@@ -493,9 +506,7 @@ async function checkDomainAvailability(domain, options = {}) {
493
506
  // registration evidence either - e.g. the registry answered inconclusively
494
507
  // or every referral hop failed/rate-limited. Report "unknown" rather than
495
508
  // fabricating "registered" from an empty record.
496
- const hasWhoisRegistrationData = whoisParsed?.registrationStatus === 'registered'
497
- || Boolean(registrar) || Boolean(createdDate) || Boolean(expiryDate)
498
- || nameservers.length > 0 || statuses.length > 0;
509
+ const hasWhoisRegistrationData = whoisParsed?.registrationStatus === 'registered';
499
510
  let dnsDelegated = false;
500
511
  if (!rdapFound && !hasWhoisRegistrationData && dnsDelegationPromise) {
501
512
  const delegation = await dnsDelegationPromise;
@@ -549,7 +560,10 @@ async function checkDomainAvailability(domain, options = {}) {
549
560
  dnssec,
550
561
  source: registrationSource,
551
562
  };
552
- if (statuses.some((s) => s.includes('pendingdelete') || s.includes('redemptionperiod'))) {
563
+ if (statuses.some((status) => {
564
+ const normalizedStatus = registryStatusToken(status);
565
+ return normalizedStatus.includes('pendingdelete') || normalizedStatus.includes('redemptionperiod');
566
+ })) {
553
567
  return {
554
568
  state: 'expiring',
555
569
  confidence: 'medium',
@@ -648,6 +662,7 @@ async function checkDomainAvailability(domain, options = {}) {
648
662
  responseHeaders: homepage.technologyHeaders,
649
663
  activityStatus,
650
664
  includePageIdentity: pageIdentityEligible,
665
+ includePublicationMetadata: options.includePublicationMetadata !== false,
651
666
  ...(options.includeCredentialSurfaceProfile !== undefined
652
667
  ? { includeCredentialSurfaceProfile: options.includeCredentialSurfaceProfile }
653
668
  : {}),
@@ -661,6 +676,9 @@ async function checkDomainAvailability(domain, options = {}) {
661
676
  }
662
677
  }
663
678
  const responsePolicy = qualifyResponsePolicyWithCspMeta(homepage.responsePolicy, htmlSignals.cspMetaPolicy);
679
+ const retainedHttp = options.includeDeliveryMetadata === false
680
+ ? withoutHttpDeliveryMetadata(homepage.http)
681
+ : homepage.http;
664
682
  const { cspMetaPolicy: _cspMetaPolicy, ...retainedHtmlSignals } = htmlSignals;
665
683
  const securityPosture = options.includeSecurityPosture === false ? null : analyzeWebsiteSecurityPosture({
666
684
  http: homepage.http,
@@ -694,7 +712,7 @@ async function checkDomainAvailability(domain, options = {}) {
694
712
  activityStatus,
695
713
  websiteProbeStatus,
696
714
  websiteProbeDetail,
697
- http: homepage.http,
715
+ http: retainedHttp,
698
716
  deepScanComplete,
699
717
  faviconHash,
700
718
  faviconPHash,
@@ -718,7 +736,7 @@ async function checkDomainAvailability(domain, options = {}) {
718
736
  activityStatus,
719
737
  websiteProbeStatus,
720
738
  websiteProbeDetail,
721
- http: homepage.http,
739
+ http: retainedHttp,
722
740
  deepScanComplete,
723
741
  faviconHash,
724
742
  faviconPHash,
@@ -2,6 +2,7 @@
2
2
  // not infer missing values and keep every string, object, enum, and counter
3
3
  // bounded before a provider or connector result reaches its public model.
4
4
  import { domainToASCII } from 'node:url';
5
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
5
6
  export function isRecord(value) {
6
7
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
7
8
  }
@@ -26,10 +27,7 @@ export function strictBoundedString(value, maxLength) {
26
27
  return normalized && normalized.length <= maxLength ? normalized : null;
27
28
  }
28
29
  export function isoTimestamp(value) {
29
- if (typeof value !== 'string' || value.length > 64 || /[\u0000-\u001f\u007f]/u.test(value))
30
- return null;
31
- const timestamp = Date.parse(value);
32
- return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
30
+ return normalizeExplicitIsoTimestamp(value);
33
31
  }
34
32
  export function boundedHttpsUrl(value, maxLength) {
35
33
  const raw = strictBoundedString(value, maxLength);
@@ -1,5 +1,6 @@
1
1
  import { constants } from 'node:fs';
2
- import { open } from 'node:fs/promises';
2
+ import { lstat, open, realpath } from 'node:fs/promises';
3
+ import path from 'node:path';
3
4
  const MAX_BOUNDED_FILE_BYTES = 64 * 1024 * 1024;
4
5
  function boundedSize(value, fallback) {
5
6
  return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= MAX_BOUNDED_FILE_BYTES
@@ -13,7 +14,7 @@ function throwIfAborted(signal) {
13
14
  ? signal.reason
14
15
  : new DOMException('The operation was aborted.', 'AbortError');
15
16
  }
16
- export async function readBoundedRegularFile(filePath, options, dependencies = {}) {
17
+ async function readBoundedRegularFileIdentity(filePath, options, dependencies = {}) {
17
18
  const label = options.label?.trim().slice(0, 120) || 'Input file';
18
19
  const maximumBytes = boundedSize(options.maximumBytes, 0);
19
20
  const minimumBytes = boundedSize(options.minimumBytes, 0);
@@ -70,13 +71,86 @@ export async function readBoundedRegularFile(filePath, options, dependencies = {
70
71
  || after.ctimeMs !== before.ctimeMs) {
71
72
  throw new TypeError(`${label} changed while it was being read.`);
72
73
  }
73
- return Buffer.from(storage.subarray(0, offset));
74
+ return {
75
+ bytes: Buffer.from(storage.subarray(0, offset)),
76
+ device: before.dev,
77
+ inode: before.ino,
78
+ };
74
79
  }
75
80
  finally {
76
81
  await handle.close();
77
82
  }
78
83
  }
84
+ export async function readBoundedRegularFile(filePath, options, dependencies = {}) {
85
+ return (await readBoundedRegularFileIdentity(filePath, options, dependencies)).bytes;
86
+ }
87
+ /**
88
+ * Decode admitted bytes without replacement-character normalisation. The BOM
89
+ * is deliberately retained so re-encoding the returned string preserves the
90
+ * exact bytes used by artefact identity and manifest checks.
91
+ */
92
+ export function decodeBoundedUtf8(bytes, label = 'Input') {
93
+ try {
94
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
95
+ }
96
+ catch {
97
+ throw new TypeError(`${label.trim().slice(0, 120) || 'Input'} must contain valid UTF-8 text.`);
98
+ }
99
+ }
79
100
  export async function readBoundedRegularTextFile(filePath, options, dependencies = {}) {
80
- return (await readBoundedRegularFile(filePath, options, dependencies)).toString('utf8');
101
+ return decodeBoundedUtf8(await readBoundedRegularFile(filePath, options, dependencies), options.label);
102
+ }
103
+ function safeRelativeFilePath(value, label) {
104
+ if (!value || path.isAbsolute(value) || value.includes('\\')
105
+ || value.split('/').some((part) => !part || part === '.' || part === '..')) {
106
+ throw new TypeError(`${label} must be a safe relative file path.`);
107
+ }
108
+ return value;
109
+ }
110
+ /**
111
+ * Reads one regular file beneath a canonical root while rejecting final and
112
+ * intermediate symbolic links. The ordinary descriptor-level size/change
113
+ * checks still apply after path confinement is established.
114
+ */
115
+ export async function readBoundedRegularFileWithin(rootDirectory, relativeFilePath, options, dependencies = {}) {
116
+ const label = options.label?.trim().slice(0, 120) || 'Input file';
117
+ const relativePath = safeRelativeFilePath(relativeFilePath, label);
118
+ const canonicalRoot = await realpath(rootDirectory);
119
+ const targetPath = path.resolve(canonicalRoot, relativePath);
120
+ const confined = path.relative(canonicalRoot, targetPath);
121
+ if (!confined || confined.startsWith(`..${path.sep}`) || confined === '..' || path.isAbsolute(confined)) {
122
+ throw new TypeError(`${label} escapes its configured root.`);
123
+ }
124
+ let cursor = canonicalRoot;
125
+ const components = relativePath.split('/');
126
+ let admittedDevice = -1;
127
+ let admittedInode = -1;
128
+ for (const [index, component] of components.entries()) {
129
+ cursor = path.join(cursor, component);
130
+ const metadata = await lstat(cursor);
131
+ if (metadata.isSymbolicLink())
132
+ throw new TypeError(`${label} must not traverse a symbolic link.`);
133
+ if (index === components.length - 1) {
134
+ admittedDevice = metadata.dev;
135
+ admittedInode = metadata.ino;
136
+ }
137
+ }
138
+ const canonicalTarget = await realpath(targetPath);
139
+ if (canonicalTarget !== targetPath)
140
+ throw new TypeError(`${label} must not traverse a symbolic link.`);
141
+ const identity = await readBoundedRegularFileIdentity(targetPath, {
142
+ ...options,
143
+ allowSymbolicLink: false,
144
+ }, dependencies);
145
+ const finalMetadata = await lstat(targetPath);
146
+ if (finalMetadata.isSymbolicLink()
147
+ || identity.device !== admittedDevice
148
+ || identity.inode !== admittedInode
149
+ || identity.device !== finalMetadata.dev
150
+ || identity.inode !== finalMetadata.ino
151
+ || await realpath(targetPath) !== canonicalTarget) {
152
+ throw new TypeError(`${label} changed while it was being read.`);
153
+ }
154
+ return identity.bytes;
81
155
  }
82
156
  export { MAX_BOUNDED_FILE_BYTES };
@@ -36,27 +36,62 @@ export function assertBoundedJsonStructure(value, label = 'JSON value', limits =
36
36
  throw new TypeError(`${label} contains a non-JSON value.`);
37
37
  if (ancestors.has(current))
38
38
  throw new TypeError(`${label} contains a cyclic object reference.`);
39
+ let prototype;
40
+ let ownKeys;
41
+ let descriptors;
42
+ try {
43
+ prototype = Object.getPrototypeOf(current);
44
+ ownKeys = Reflect.ownKeys(current);
45
+ descriptors = Object.getOwnPropertyDescriptors(current);
46
+ }
47
+ catch {
48
+ throw new TypeError(`${label} could not be inspected safely.`);
49
+ }
50
+ if (ownKeys.some((key) => typeof key !== 'string')) {
51
+ throw new TypeError(`${label} contains a symbol key.`);
52
+ }
53
+ if (ownKeys.some((key) => {
54
+ const descriptor = descriptors[String(key)];
55
+ return !descriptor || !Object.hasOwn(descriptor, 'value');
56
+ })) {
57
+ throw new TypeError(`${label} contains an accessor property.`);
58
+ }
39
59
  ancestors.add(current);
40
60
  if (Array.isArray(current)) {
41
- if (current.length > maximumContainerItems) {
61
+ if (prototype !== Array.prototype) {
62
+ throw new TypeError(`${label} contains an array with a custom prototype.`);
63
+ }
64
+ const length = descriptors.length?.value;
65
+ if (!Number.isSafeInteger(length) || length < 0 || length > maximumContainerItems) {
42
66
  throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
43
67
  }
44
- for (const item of current)
45
- visit(item, depth + 1);
68
+ const indexes = ownKeys.filter((key) => key !== 'length');
69
+ if (indexes.length !== length
70
+ || indexes.some((key, index) => key !== String(index) || descriptors[key]?.enumerable !== true)) {
71
+ throw new TypeError(`${label} contains a sparse, extended, or non-enumerable array.`);
72
+ }
73
+ for (const key of indexes)
74
+ visit(descriptors[key].value, depth + 1);
46
75
  }
47
76
  else {
48
- const entries = Object.entries(current);
49
- if (entries.length > maximumContainerItems) {
77
+ if (prototype !== Object.prototype && prototype !== null) {
78
+ throw new TypeError(`${label} contains an object with a custom prototype.`);
79
+ }
80
+ const objectKeys = ownKeys;
81
+ if (objectKeys.length > maximumContainerItems) {
50
82
  throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
51
83
  }
52
- keys += entries.length;
84
+ if (objectKeys.some((key) => descriptors[key]?.enumerable !== true)) {
85
+ throw new TypeError(`${label} contains a non-enumerable object property.`);
86
+ }
87
+ keys += objectKeys.length;
53
88
  if (keys > maximumKeys) {
54
89
  throw new TypeError(`${label} exceeds the ${maximumKeys}-key limit.`);
55
90
  }
56
- for (const [key, item] of entries) {
91
+ for (const key of objectKeys) {
57
92
  if (!isSafeJsonObjectKey(key))
58
93
  throw new TypeError(`${label} contains an unsafe object key.`);
59
- visit(item, depth + 1);
94
+ visit(descriptors[key].value, depth + 1);
60
95
  }
61
96
  }
62
97
  ancestors.delete(current);
@@ -226,7 +261,7 @@ export function scanBoundedJson(raw, limits = {}) {
226
261
  if (index !== raw.length)
227
262
  syntaxError();
228
263
  }
229
- export function parseBoundedJsonObject(raw, options) {
264
+ export function parseBoundedJson(raw, options) {
230
265
  const label = options.label ?? 'Artefact input';
231
266
  if (typeof raw !== 'string')
232
267
  throw new TypeError(`${label} must be UTF-8 JSON text.`);
@@ -234,14 +269,17 @@ export function parseBoundedJsonObject(raw, options) {
234
269
  if (bytes < 1 || bytes > options.maximumBytes) {
235
270
  throw new TypeError(`${label} must be between 1 byte and ${options.maximumBytes} bytes.`);
236
271
  }
237
- scanBoundedJson(raw);
238
- let parsed;
272
+ scanBoundedJson(raw, options.limits);
239
273
  try {
240
- parsed = JSON.parse(raw);
274
+ return JSON.parse(raw);
241
275
  }
242
276
  catch {
243
- syntaxError();
277
+ return syntaxError();
244
278
  }
279
+ }
280
+ export function parseBoundedJsonObject(raw, options) {
281
+ const label = options.label ?? 'Artefact input';
282
+ const parsed = parseBoundedJson(raw, options);
245
283
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
246
284
  throw new TypeError(`${label} must contain one JSON object.`);
247
285
  }
@@ -7,7 +7,7 @@ import { createHash } from 'node:crypto';
7
7
  import { Worker } from 'node:worker_threads';
8
8
  import { RETIRE_BROWSER_CATALOG } from "./generated/retire-browser-catalog.mjs";
9
9
  import { CISA_KEV_CATALOG } from "./generated/cisa-kev-catalog.mjs";
10
- import { createObservation } from "./observation.mjs";
10
+ import { createObservation } from "../packages/evidence/observation.mjs";
11
11
  import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
12
12
  // Catalogue expressions are reviewed third-party inputs. Keep every individual
13
13
  // regular-expression evaluation and the aggregate inline-signature work small
package/lib/classify.mjs CHANGED
@@ -14,7 +14,9 @@
14
14
  // original input hostname and the resolved registrable domain are returned so
15
15
  // callers can look up the latter while still showing the user what they typed.
16
16
  import { isIP } from 'node:net';
17
+ import { domainToASCII } from 'node:url';
17
18
  import { parse } from 'tldts';
19
+ import { canonicalRegistrableDomain } from "./registrable-domain.mjs";
18
20
  // A CR/LF here would let one query become multiple lines once it reaches
19
21
  // lib/whois.mts's raw `socket.write(query + '\r\n')` - a normal URL-encoded
20
22
  // query string (?q=example.com%0D%0AHELP) decodes back into literal control
@@ -24,6 +26,9 @@ const CONTROL_CHAR_RE = /[\x00-\x1f\x7f]/;
24
26
  // 32-bit ASN ceiling (RFC 6793 four-octet ASNs). AS0 and AS4294967295 are
25
27
  // reserved but syntactically valid; anything above is not an ASN at all.
26
28
  const MAX_ASN = 4294967295;
29
+ const MAX_DIRECT_LOOKUP_TARGET_LENGTH = 1024;
30
+ const DIRECT_LOOKUP_RESERVED_SUFFIXES = new Set(['example', 'test']);
31
+ const DIRECT_LOOKUP_EXCLUDED_PUBLIC_SUFFIXES = new Set(['onion']);
27
32
  // A registrable-domain label: LDH (letter/digit/hyphen), 1-63 characters, no
28
33
  // leading or trailing hyphen. tldts already rejects empty labels, over-long
29
34
  // labels, leading/trailing hyphens, and public-suffix-only inputs (all yield
@@ -33,6 +38,52 @@ const MAX_ASN = 4294967295;
33
38
  function isValidRegistrableLabel(label) {
34
39
  return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label);
35
40
  }
41
+ // Direct CLI shorthand is deliberately narrower than normal query
42
+ // classification. classifyQuery() accepts pasted URLs and other convenient
43
+ // input forms after normalisation, while a value occupying command position
44
+ // must be unambiguously a target before it may select the networked Lookup
45
+ // command. This predicate therefore accepts only a complete hostname, literal
46
+ // IP address, or bounded ASN and never attempts to repair URL-like input.
47
+ function isDirectLookupTarget(raw) {
48
+ if (typeof raw !== 'string'
49
+ || !raw
50
+ || raw.length > MAX_DIRECT_LOOKUP_TARGET_LENGTH
51
+ || raw.startsWith('-')
52
+ || raw.trim() !== raw
53
+ || /\s/u.test(raw)
54
+ || CONTROL_CHAR_RE.test(raw)) {
55
+ return false;
56
+ }
57
+ if (isIP(raw) !== 0)
58
+ return !raw.includes('%') && !raw.startsWith('[');
59
+ const asnMatch = raw.match(/^(?:AS)?(\d+)$/i);
60
+ if (asnMatch) {
61
+ const number = Number(asnMatch[1]);
62
+ return Number.isInteger(number) && number >= 0 && number <= MAX_ASN;
63
+ }
64
+ if (/[\\/@:?#%\[\]]/u.test(raw) || raw.endsWith('..'))
65
+ return false;
66
+ const hostname = raw.endsWith('.') ? raw.slice(0, -1) : raw;
67
+ if (!hostname.includes('.'))
68
+ return false;
69
+ const ascii = domainToASCII(hostname).toLowerCase();
70
+ if (!ascii || ascii.length > 253 || !ascii.split('.').every(isValidRegistrableLabel))
71
+ return false;
72
+ const parsed = parse(ascii);
73
+ const publicSuffix = parsed.publicSuffix || '';
74
+ if (DIRECT_LOOKUP_EXCLUDED_PUBLIC_SUFFIXES.has(publicSuffix)
75
+ || publicSuffix === 'arpa'
76
+ || publicSuffix.endsWith('.arpa'))
77
+ return false;
78
+ if (parsed.isIcann !== true && !DIRECT_LOOKUP_RESERVED_SUFFIXES.has(publicSuffix))
79
+ return false;
80
+ try {
81
+ return classifyQuery(raw).type === 'domain';
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
36
87
  function classifyQuery(raw) {
37
88
  const trimmed = raw.trim();
38
89
  if (CONTROL_CHAR_RE.test(trimmed)) {
@@ -75,13 +126,13 @@ function classifyQuery(raw) {
75
126
  if (!inputHostname || !inputHostname.includes('.') || inputHostname.length > 253) {
76
127
  throw new Error(`"${trimmed}" is not a valid domain, IP, or ASN.`);
77
128
  }
78
- const registrableDomain = parse(inputHostname).domain;
129
+ if (!inputHostname.split('.').every(isValidRegistrableLabel)) {
130
+ throw new Error(`"${trimmed}" contains an invalid domain label (underscores, empty, or malformed labels are not registrable).`);
131
+ }
132
+ const registrableDomain = canonicalRegistrableDomain(inputHostname);
79
133
  if (!registrableDomain) {
80
134
  throw new Error(`"${trimmed}" is not a registrable domain (no public-suffix match).`);
81
135
  }
82
- if (!registrableDomain.split('.').every(isValidRegistrableLabel)) {
83
- throw new Error(`"${trimmed}" contains an invalid domain label (underscores, empty, or malformed labels are not registrable).`);
84
- }
85
136
  return {
86
137
  type: 'domain',
87
138
  value: registrableDomain,
@@ -90,4 +141,4 @@ function classifyQuery(raw) {
90
141
  isSubdomain: inputHostname !== registrableDomain,
91
142
  };
92
143
  }
93
- export { classifyQuery, MAX_ASN };
144
+ export { classifyQuery, isDirectLookupTarget, MAX_ASN };
@@ -2,7 +2,7 @@
2
2
  // HTML tokenizer pass. Scripts are never fetched or executed. Only fixed
3
3
  // indicator identifiers, descriptions, evidence classes, and bounded counts
4
4
  // leave this module; script references and contents are discarded.
5
- import { createObservation } from "./observation.mjs";
5
+ import { createObservation } from "../packages/evidence/observation.mjs";
6
6
  import { analyzeStaticHtml, } from "./static-html-analysis.mjs";
7
7
  const CLIENT_BEHAVIOR_PROFILE_VERSION = 1;
8
8
  const MAX_CLIENT_BEHAVIOR_INDICATORS = 12;
@@ -3,7 +3,7 @@
3
3
  // shared tokenizer projects only fixed categories and bounded counts; field
4
4
  // names, values, labels, placeholders, arbitrary attributes, and action URLs
5
5
  // never enter this profile.
6
- import { createObservation } from "./observation.mjs";
6
+ import { createObservation } from "../packages/evidence/observation.mjs";
7
7
  import { MAX_STATIC_FORMS, MAX_STATIC_INPUTS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
8
8
  const CREDENTIAL_SURFACE_PROFILE_VERSION = 1;
9
9
  function analyzeCredentialSurfaceProfile(input = {}) {
@@ -1,9 +1,11 @@
1
1
  import { validateDnssecEvidence } from "./dnssec-evidence-validation.mjs";
2
2
  import { reviewRpkiRoute } from "./rpki-evidence.mjs";
3
3
  import { analyzeTlsaEvidence } from "./tlsa-evidence.mjs";
4
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
4
5
  const CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA = 'whoisleuth.cryptographic-assurance.input';
5
6
  const CRYPTOGRAPHIC_ASSURANCE_SCHEMA = 'whoisleuth.cryptographic-assurance.review';
6
- const CRYPTOGRAPHIC_ASSURANCE_VERSION = 1;
7
+ const CRYPTOGRAPHIC_ASSURANCE_INPUT_VERSION = 1;
8
+ const CRYPTOGRAPHIC_ASSURANCE_REVIEW_VERSION = 2;
7
9
  function exactRecord(value, keys, label) {
8
10
  if (!value || typeof value !== 'object' || Array.isArray(value))
9
11
  throw new TypeError(`${label} must be one object.`);
@@ -22,10 +24,11 @@ function sourceLabel(value, label) {
22
24
  return normalized;
23
25
  }
24
26
  function observationTime(value, label) {
25
- if (typeof value !== 'string' || value.length > 64 || !Number.isFinite(Date.parse(value))) {
27
+ const normalized = normalizeExplicitIsoTimestamp(value);
28
+ if (!normalized) {
26
29
  throw new TypeError(`${label}.observedAt must be one ISO-compatible timestamp.`);
27
30
  }
28
- return new Date(value).toISOString();
31
+ return normalized;
29
32
  }
30
33
  function suppliedEvidence(value, label) {
31
34
  if (value === null || value === undefined)
@@ -59,7 +62,16 @@ function dnssecCompleteness(report) {
59
62
  : report.state === 'invalid' ? 'unavailable' : 'partial';
60
63
  }
61
64
  function rpkiCompleteness(report) {
62
- return report.state === 'partial' ? 'partial' : report.state === 'invalid_input' ? 'unavailable' : 'complete';
65
+ return report.state === 'invalid_input' ? 'unavailable' : 'partial';
66
+ }
67
+ function rpkiCardState(report) {
68
+ if (report.state === 'valid')
69
+ return 'matches_supplied_rows';
70
+ if (report.state === 'invalid')
71
+ return 'differs_from_supplied_rows';
72
+ if (report.state === 'not_found')
73
+ return 'not_found_in_supplied_rows';
74
+ return report.state;
63
75
  }
64
76
  function tlsaCompleteness(report) {
65
77
  const comparisonMaterialComplete = !report.truncated
@@ -74,8 +86,8 @@ function tlsaCompleteness(report) {
74
86
  }
75
87
  function buildCryptographicAssuranceReview(inputValue, generatedAt = new Date().toISOString()) {
76
88
  const input = exactRecord(inputValue, new Set(['schema', 'version', 'dnssec', 'routeOrigin', 'tlsa']), 'Cryptographic assurance input');
77
- if (input.schema !== CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA || input.version !== CRYPTOGRAPHIC_ASSURANCE_VERSION) {
78
- throw new TypeError(`Cryptographic assurance input must use ${CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA} version ${CRYPTOGRAPHIC_ASSURANCE_VERSION}.`);
89
+ if (input.schema !== CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA || input.version !== CRYPTOGRAPHIC_ASSURANCE_INPUT_VERSION) {
90
+ throw new TypeError(`Cryptographic assurance input must use ${CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA} version ${CRYPTOGRAPHIC_ASSURANCE_INPUT_VERSION}.`);
79
91
  }
80
92
  const dnssecInput = suppliedEvidence(input.dnssec, 'dnssec');
81
93
  const routeInput = suppliedEvidence(input.routeOrigin, 'routeOrigin');
@@ -97,8 +109,8 @@ function buildCryptographicAssuranceReview(inputValue, generatedAt = new Date().
97
109
  }) : null;
98
110
  const tlsa = tlsaInput ? analyzeTlsaEvidence({
99
111
  serviceName: tlsaInput.evidence.serviceName,
100
- dnssecState: tlsaInput.evidence.dnssecState,
101
- pkixValidationState: tlsaInput.evidence.pkixValidationState,
112
+ dnssecState: 'unavailable',
113
+ pkixValidationState: 'unavailable',
102
114
  records: tlsaInput.evidence.records,
103
115
  certificateDerBase64: tlsaInput.evidence.certificateDerBase64,
104
116
  spkiDerBase64: tlsaInput.evidence.spkiDerBase64,
@@ -119,29 +131,35 @@ function buildCryptographicAssuranceReview(inputValue, generatedAt = new Date().
119
131
  routeOrigin && routeInput ? Object.freeze({
120
132
  family: 'route_origin_authorisation',
121
133
  label: 'Route-origin authorisation',
122
- authority: 'Analyst-supplied validated route-origin snapshot',
134
+ authority: 'Analyst-supplied route-origin rows',
123
135
  source: routeInput.source,
124
136
  observedAt: routeInput.observedAt,
125
- state: routeOrigin.state,
137
+ state: rpkiCardState(routeOrigin),
126
138
  completeness: rpkiCompleteness(routeOrigin),
127
139
  result: routeOrigin,
128
- limitations: routeOrigin.limitations,
129
- }) : unavailableCard('route_origin_authorisation', 'Route-origin authorisation', 'Analyst-supplied validated route-origin snapshot'),
140
+ limitations: Object.freeze([
141
+ ...routeOrigin.limitations,
142
+ 'The supplied rows do not include bounded validator provenance, implementation identity, validation time, or a complete authenticated snapshot, so this card reports only their local comparison and remains partial.',
143
+ ]),
144
+ }) : unavailableCard('route_origin_authorisation', 'Route-origin authorisation', 'Analyst-supplied route-origin rows'),
130
145
  tlsa && tlsaInput ? Object.freeze({
131
146
  family: 'dane_tlsa',
132
147
  label: 'DANE and TLSA evidence review',
133
- authority: 'Supplied TLSA, DNSSEC, PKIX, and certificate material',
148
+ authority: 'Supplied TLSA and certificate association material',
134
149
  source: tlsaInput.source,
135
150
  observedAt: tlsaInput.observedAt,
136
151
  state: tlsa.state,
137
152
  completeness: tlsaCompleteness(tlsa),
138
153
  result: tlsa,
139
- limitations: tlsa.limitations,
140
- }) : unavailableCard('dane_tlsa', 'DANE and TLSA evidence review', 'Supplied TLSA, DNSSEC, PKIX, and certificate material'),
154
+ limitations: Object.freeze([
155
+ ...tlsa.limitations,
156
+ 'Caller-provided DNSSEC and PKIX state labels are not accepted as validator evidence. Positive DANE state requires separately validated prerequisite artefacts from an explicit collector.',
157
+ ]),
158
+ }) : unavailableCard('dane_tlsa', 'DANE and TLSA evidence review', 'Supplied TLSA and certificate association material'),
141
159
  ];
142
160
  return Object.freeze({
143
161
  schema: CRYPTOGRAPHIC_ASSURANCE_SCHEMA,
144
- version: CRYPTOGRAPHIC_ASSURANCE_VERSION,
162
+ version: CRYPTOGRAPHIC_ASSURANCE_REVIEW_VERSION,
145
163
  generatedAt: observationTime(generatedAt, 'generatedAt'),
146
164
  cards: Object.freeze(cards),
147
165
  combinedState: null,
@@ -152,4 +170,4 @@ function buildCryptographicAssuranceReview(inputValue, generatedAt = new Date().
152
170
  ]),
153
171
  });
154
172
  }
155
- export { CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA, CRYPTOGRAPHIC_ASSURANCE_SCHEMA, CRYPTOGRAPHIC_ASSURANCE_VERSION, buildCryptographicAssuranceReview, };
173
+ export { CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA, CRYPTOGRAPHIC_ASSURANCE_INPUT_VERSION, CRYPTOGRAPHIC_ASSURANCE_REVIEW_VERSION, CRYPTOGRAPHIC_ASSURANCE_SCHEMA, buildCryptographicAssuranceReview, };
package/lib/ct-query.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  const MAX_CT_QUERY_LENGTH = 200;
2
2
  const CT_QUERY_ERROR_CODE = 'INVALID_CT_QUERY';
3
- const CT_QUERY_ERROR_MESSAGE = `Certificate Transparency query must be at most ${MAX_CT_QUERY_LENGTH} characters and contain no control characters.`;
4
- const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]/u;
3
+ const CT_QUERY_ERROR_MESSAGE = `Certificate Transparency query must be at most ${MAX_CT_QUERY_LENGTH} characters and contain no control or invisible formatting characters.`;
4
+ const UNSAFE_QUERY_CHARACTER_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
5
5
  class CtQueryError extends Error {
6
6
  code = CT_QUERY_ERROR_CODE;
7
7
  constructor() {
@@ -14,7 +14,7 @@ function normalizeCtQuery(value) {
14
14
  return '';
15
15
  if (typeof value !== 'string'
16
16
  || value.length > MAX_CT_QUERY_LENGTH
17
- || CONTROL_CHARACTER_RE.test(value)) {
17
+ || UNSAFE_QUERY_CHARACTER_RE.test(value)) {
18
18
  throw new CtQueryError();
19
19
  }
20
20
  return value.trim();