@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
@@ -1,6 +1,15 @@
1
1
  import { registryAccessProfileLabel } from "../registry-access.mjs";
2
2
  import { unicodeDomainFromAscii } from "../../lib/idn-confusables.mjs";
3
+ import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "../../lib/homepage-metadata-contract.mjs";
3
4
  const MAX_TERMINAL_VALUE_LENGTH = 240;
5
+ const MAX_LOOKUP_TERMINAL_RECORDS = 5;
6
+ const MAX_LOOKUP_TERMINAL_NAMES = 5;
7
+ const MAX_LOOKUP_TERMINAL_ALPN_IDS = 4;
8
+ const MAX_LOOKUP_TERMINAL_ALPN_ID_LENGTH = 32;
9
+ const MAX_LOOKUP_TERMINAL_LIMITATIONS = 3;
10
+ const MAX_LOOKUP_TERMINAL_CIDRS = 5;
11
+ const MAX_LOOKUP_TERMINAL_FINDINGS = 5;
12
+ const MAX_LOOKUP_TERMINAL_ABUSE_ROUTES = 6;
4
13
  const MAX_CT_TERMINAL_MATCHES = 100;
5
14
  const MAX_CT_TERMINAL_HOSTNAMES = 5;
6
15
  const MAX_DISCOVER_TERMINAL_CANDIDATES = 200;
@@ -8,15 +17,49 @@ const MAX_POSTURE_TERMINAL_RECORDS = 5;
8
17
  const MAX_TLS_TERMINAL_ALT_NAMES = 10;
9
18
  const MAX_TLS_TERMINAL_PURPOSES = 8;
10
19
  const MAX_RISK_CALIBRATION_TERMINAL_RECORDS = 100;
20
+ // Archive values are attacker-controlled evidence. Remove the complete Unicode
21
+ // default-ignorable class so visually identical identifiers cannot differ only
22
+ // through soft hyphens, combining grapheme joiners, variation selectors, tag
23
+ // characters, bidi controls, or other zero-width formatting code points.
24
+ const TERMINAL_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/gu;
11
25
  function safeTerminalValue(value, fallback = '—') {
12
26
  if (value === null || value === undefined || value === '')
13
27
  return fallback;
14
28
  const normalized = String(value)
15
- .replace(/[\x00-\x1f\x7f]+/g, ' ')
29
+ .replace(/[\x00-\x1f\x7f-\x9f]+/g, ' ')
30
+ .replace(TERMINAL_DEFAULT_IGNORABLE_RE, '')
16
31
  .replace(/\s+/g, ' ')
17
- .trim()
18
- .slice(0, MAX_TERMINAL_VALUE_LENGTH);
19
- return normalized || fallback;
32
+ .trim();
33
+ if (!normalized)
34
+ return fallback;
35
+ return normalized.length > MAX_TERMINAL_VALUE_LENGTH
36
+ ? `${normalized.slice(0, MAX_TERMINAL_VALUE_LENGTH - 1)}…`
37
+ : normalized;
38
+ }
39
+ function boundedTerminalList(values, omitted) {
40
+ const suffix = omitted > 0 ? ` · +${omitted} more` : '';
41
+ const maximumBodyLength = Math.max(1, MAX_TERMINAL_VALUE_LENGTH - suffix.length);
42
+ const joined = values.join(', ');
43
+ const body = joined.length > maximumBodyLength
44
+ ? `${joined.slice(0, Math.max(0, maximumBodyLength - 1))}…`
45
+ : joined;
46
+ return `${body}${suffix}`;
47
+ }
48
+ function boundedTerminalComponent(value, maximum) {
49
+ const normalized = safeTerminalValue(value);
50
+ return normalized.length > maximum ? `${normalized.slice(0, Math.max(0, maximum - 1))}…` : normalized;
51
+ }
52
+ function boundedTerminalWithSuffix(value, suffix) {
53
+ if (!suffix)
54
+ return safeTerminalValue(value);
55
+ const retainedSuffix = suffix.length >= MAX_TERMINAL_VALUE_LENGTH
56
+ ? `${suffix.slice(0, MAX_TERMINAL_VALUE_LENGTH - 1)}…`
57
+ : suffix;
58
+ const maximumValueLength = Math.max(1, MAX_TERMINAL_VALUE_LENGTH - retainedSuffix.length);
59
+ const retainedValue = value.length > maximumValueLength
60
+ ? `${value.slice(0, Math.max(0, maximumValueLength - 1))}…`
61
+ : value;
62
+ return `${retainedValue}${retainedSuffix}`;
20
63
  }
21
64
  function titleCase(value) {
22
65
  const text = safeTerminalValue(value, 'unknown').replaceAll('_', ' ');
@@ -31,14 +74,76 @@ function terminalCount(value) {
31
74
  const count = Number(value);
32
75
  return Number.isSafeInteger(count) && count >= 0 ? Math.min(count, 999) : 0;
33
76
  }
77
+ function terminalDisplayCount(value) {
78
+ const count = Number(value);
79
+ return Number.isSafeInteger(count) && count >= 0 ? String(count) : '0';
80
+ }
34
81
  function terminalCountSummary(value, labels) {
35
82
  const source = terminalRecord(value);
36
83
  return labels
37
- .map(([key, label]) => [label, terminalCount(source[key])])
84
+ .map(([key, label]) => {
85
+ const count = Number(source[key]);
86
+ return [label, Number.isSafeInteger(count) && count >= 0 ? count : 0];
87
+ })
38
88
  .filter(([, count]) => count > 0)
39
- .map(([label, count]) => `${label} ${count}`)
89
+ .map(([label, count]) => `${label} ${terminalDisplayCount(count)}`)
40
90
  .join(', ');
41
91
  }
92
+ function appendPublicationMetadataLines(lines, value, detail) {
93
+ if (detail === 'summary' || !validPagePublicationMetadata(value))
94
+ return;
95
+ const metadata = terminalRecord(value);
96
+ const robots = terminalRecord(metadata.robots);
97
+ const twitter = terminalRecord(metadata.twitterCard);
98
+ const headings = terminalRecord(metadata.headings);
99
+ const images = terminalRecord(metadata.images);
100
+ const blocking = terminalRecord(metadata.renderBlockingCandidates);
101
+ lines.push(`Publication ${metadata.complete === true ? 'Complete' : 'Partial'} · robots ${titleCase(robots.status)} · card ${titleCase(twitter.status)}`);
102
+ const directives = Array.isArray(robots.directives) ? robots.directives.map((item) => safeTerminalValue(item)) : [];
103
+ if (directives.length)
104
+ lines.push(`Robots ${safeTerminalValue(directives.join(', '))}${robots.conflicting === true ? ' · conflicting' : ''}`);
105
+ else if (robots.status === 'not_observed')
106
+ lines.push('Robots No declaration observed in captured static HTML');
107
+ if (twitter.cardType)
108
+ lines.push(`Card type ${safeTerminalValue(twitter.cardType)}`);
109
+ lines.push(`Static page headings ${terminalDisplayCount(headings.total)} · images ${terminalDisplayCount(images.total)} · blocking candidates ${terminalDisplayCount(blocking.total)}`);
110
+ if (detail === 'verbose') {
111
+ lines.push(`Image alt missing ${terminalDisplayCount(images.altMissing)} · empty ${terminalDisplayCount(images.altEmpty)} · non-empty ${terminalDisplayCount(images.altNonEmpty)} · unclassified ${terminalDisplayCount(images.altUnclassified)}`);
112
+ lines.push(`Blocking scripts ${terminalDisplayCount(blocking.script)} · stylesheets ${terminalDisplayCount(blocking.stylesheet)} · static candidates only`);
113
+ }
114
+ }
115
+ function appendDeliveryMetadataLines(lines, value, verbose = false) {
116
+ if (!validHttpDeliveryMetadata(value))
117
+ return;
118
+ const metadata = terminalRecord(value);
119
+ const encoding = terminalRecord(metadata.contentEncoding);
120
+ const cache = terminalRecord(metadata.cachePolicy);
121
+ const codings = Array.isArray(encoding.codings) ? encoding.codings.map((item) => safeTerminalValue(item)) : [];
122
+ lines.push(`Delivery ${metadata.complete === true ? 'Complete' : 'Partial'} · encoding ${titleCase(encoding.status)} · cache ${titleCase(cache.status)}`);
123
+ if (codings.length)
124
+ lines.push(`Content coding ${safeTerminalValue(codings.join(', '))}`);
125
+ const cacheDirectives = [
126
+ ['no-store', cache.noStore], ['no-cache', cache.noCache], ['must-revalidate', cache.mustRevalidate],
127
+ ['public', cache.public], ['private', cache.private], ['immutable', cache.immutable],
128
+ ].filter(([, present]) => present === true).map(([label]) => label);
129
+ if (cacheDirectives.length)
130
+ lines.push(`Cache policy ${safeTerminalValue(cacheDirectives.join(', '))}`);
131
+ if (verbose) {
132
+ const seconds = [
133
+ cache.maxAgeSeconds === null ? null : `max-age ${terminalDisplayCount(cache.maxAgeSeconds)}s`,
134
+ cache.sMaxAgeSeconds === null ? null : `s-maxage ${terminalDisplayCount(cache.sMaxAgeSeconds)}s`,
135
+ cache.ageSeconds === null ? null : `Age ${terminalDisplayCount(cache.ageSeconds)}s`,
136
+ ].filter(Boolean);
137
+ if (seconds.length)
138
+ lines.push(`Cache timing ${safeTerminalValue(seconds.join(' · '))}`);
139
+ const declared = [
140
+ terminalRecord(cache.etag).present === true ? 'ETag' : null,
141
+ terminalRecord(cache.lastModified).present === true ? 'Last-Modified' : null,
142
+ terminalRecord(cache.expires).present === true ? 'Expires' : null,
143
+ ].filter(Boolean);
144
+ lines.push(`Validators ${declared.length ? safeTerminalValue(declared.join(', ')) : 'No validator declaration observed'}`);
145
+ }
146
+ }
42
147
  function appendSection(lines, label, values) {
43
148
  if (!values.length)
44
149
  return;
@@ -46,6 +151,197 @@ function appendSection(lines, label, values) {
46
151
  lines.push('');
47
152
  lines.push(`${label}:`, ...values);
48
153
  }
154
+ function formatLookupDnsRecord(value) {
155
+ if (typeof value === 'string' || typeof value === 'number')
156
+ return safeTerminalValue(value);
157
+ const record = terminalRecord(value);
158
+ if (!Object.keys(record).length)
159
+ return null;
160
+ if ('exchange' in record) {
161
+ return safeTerminalValue(`${safeTerminalValue(record.priority, '0')} ${record.exchange ? safeTerminalValue(record.exchange) : '.'}`);
162
+ }
163
+ if ('tag' in record && 'value' in record) {
164
+ return safeTerminalValue(`${record.critical ? 'critical ' : ''}${safeTerminalValue(record.tag)} ${safeTerminalValue(record.value)}`);
165
+ }
166
+ if ('nsname' in record) {
167
+ return safeTerminalValue(`${safeTerminalValue(record.nsname)} · serial ${safeTerminalValue(record.serial, 'unknown')}`);
168
+ }
169
+ if (record.type === 'HTTPS') {
170
+ const parameters = terminalRecord(record.parameters);
171
+ const alpnValues = Array.isArray(parameters.alpn) ? parameters.alpn : [];
172
+ const alpnVisible = alpnValues
173
+ .slice(0, MAX_LOOKUP_TERMINAL_ALPN_IDS)
174
+ .map((item) => boundedTerminalComponent(item, MAX_LOOKUP_TERMINAL_ALPN_ID_LENGTH));
175
+ const alpn = alpnVisible.length
176
+ ? boundedTerminalList(alpnVisible, Math.max(0, alpnValues.length - alpnVisible.length))
177
+ : '';
178
+ const suffix = [parameters.port ? `port ${safeTerminalValue(parameters.port)}` : '', alpn ? `ALPN ${alpn}` : ''].filter(Boolean).join(' · ');
179
+ const target = `${safeTerminalValue(record.priority, '0')} ${safeTerminalValue(record.target, '.')}`;
180
+ return boundedTerminalWithSuffix(target, suffix ? ` · ${suffix}` : '');
181
+ }
182
+ return null;
183
+ }
184
+ function appendLookupRecordLine(lines, label, value) {
185
+ if (!Array.isArray(value) || value.length === 0)
186
+ return;
187
+ const visible = value
188
+ .slice(0, MAX_LOOKUP_TERMINAL_RECORDS)
189
+ .map(formatLookupDnsRecord)
190
+ .filter((item) => item !== null);
191
+ if (!visible.length)
192
+ return;
193
+ const omitted = Math.max(0, value.length - visible.length);
194
+ lines.push(`${label.padEnd(14)}${boundedTerminalList(visible, omitted)}`);
195
+ }
196
+ function lookupObservationLabel(value, observed, notObserved) {
197
+ return value === true ? observed : value === false ? notObserved : 'Unavailable';
198
+ }
199
+ function boundedTerminalNames(value) {
200
+ if (typeof value === 'string')
201
+ return safeTerminalValue(value);
202
+ if (!Array.isArray(value))
203
+ return null;
204
+ const names = value
205
+ .filter((item) => typeof item === 'string' && Boolean(item.trim()))
206
+ .slice(0, MAX_LOOKUP_TERMINAL_NAMES)
207
+ .map((item) => safeTerminalValue(item));
208
+ if (!names.length)
209
+ return null;
210
+ return boundedTerminalList(names, Math.max(0, value.length - names.length));
211
+ }
212
+ function boundedTerminalStrings(value, maximum = MAX_LOOKUP_TERMINAL_NAMES) {
213
+ if (!Array.isArray(value))
214
+ return null;
215
+ const inspected = Math.min(value.length, maximum);
216
+ const retained = [];
217
+ let invalid = 0;
218
+ for (let index = 0; index < inspected; index += 1) {
219
+ const item = value[index];
220
+ if (typeof item === 'string' || typeof item === 'number')
221
+ retained.push(safeTerminalValue(item));
222
+ else
223
+ invalid += 1;
224
+ }
225
+ const uninspected = Math.max(0, value.length - inspected);
226
+ const disclosures = [
227
+ invalid > 0 ? `${invalid} malformed retained entr${invalid === 1 ? 'y' : 'ies'} omitted` : null,
228
+ uninspected > 0 ? `+${uninspected} more retained entr${uninspected === 1 ? 'y' : 'ies'}` : null,
229
+ ].filter(Boolean).join(' · ');
230
+ if (!retained.length)
231
+ return disclosures || null;
232
+ return boundedTerminalWithSuffix(retained.join(', '), disclosures ? ` · ${disclosures}` : '');
233
+ }
234
+ function sourceCompleteness(value) {
235
+ if (value.complete === true && value.truncated !== true)
236
+ return 'Complete';
237
+ if (value.complete === false || value.truncated === true)
238
+ return value.truncated === true ? 'Incomplete · truncated' : 'Incomplete';
239
+ return null;
240
+ }
241
+ function positiveSourceStatus(value) {
242
+ return value === 'success' || value === 'partial';
243
+ }
244
+ function lifecycleDate(value, field) {
245
+ const lifecycle = terminalRecord(value.lifecycle);
246
+ return lifecycle[`${field}Iso`] ?? lifecycle[field];
247
+ }
248
+ function addressSelectionLabel(value) {
249
+ const labels = {
250
+ tls_connection: 'TLS connection',
251
+ dns_a: 'DNS A',
252
+ dns_aaaa: 'DNS AAAA',
253
+ };
254
+ return labels[String(value)] || titleCase(value);
255
+ }
256
+ function retainedUrlRelationship(value, target) {
257
+ const urlValue = terminalRecord(value);
258
+ if (typeof urlValue.url !== 'string')
259
+ return null;
260
+ try {
261
+ const declared = new URL(urlValue.url);
262
+ const targetValue = String(target || '').trim().replace(/\.$/u, '');
263
+ const targetHost = new URL(`http://${targetValue}`).hostname.toLowerCase();
264
+ const relationship = declared.hostname.toLowerCase() === targetHost
265
+ ? 'Same target host'
266
+ : 'Different host declared';
267
+ return [
268
+ relationship,
269
+ urlValue.queryOmitted === true ? 'query omitted' : null,
270
+ urlValue.pathTruncated === true ? 'path shortened' : null,
271
+ ].filter(Boolean).join(' · ');
272
+ }
273
+ catch {
274
+ return null;
275
+ }
276
+ }
277
+ function boundedFindingLabels(value, formatter) {
278
+ if (!Array.isArray(value))
279
+ return null;
280
+ const inspected = Math.min(value.length, MAX_LOOKUP_TERMINAL_FINDINGS);
281
+ const retained = value
282
+ .slice(0, inspected)
283
+ .map(terminalRecord)
284
+ .map(formatter)
285
+ .filter(Boolean);
286
+ if (!retained.length)
287
+ return null;
288
+ return boundedTerminalList(retained, Math.max(0, value.length - inspected));
289
+ }
290
+ function appendNetworkRegistrationLines(lines, parsed, type, detail) {
291
+ if (!Object.keys(parsed).length || detail === 'summary')
292
+ return;
293
+ if (parsed.handle)
294
+ lines.push(`RDAP handle ${safeTerminalValue(parsed.handle)}`);
295
+ if (parsed.name)
296
+ lines.push(`RDAP name ${safeTerminalValue(parsed.name)}`);
297
+ if (type === 'asn') {
298
+ if (parsed.startAutnum !== undefined && parsed.endAutnum !== undefined) {
299
+ const range = parsed.startAutnum === parsed.endAutnum
300
+ ? safeTerminalValue(parsed.startAutnum)
301
+ : `${safeTerminalValue(parsed.startAutnum)} to ${safeTerminalValue(parsed.endAutnum)}`;
302
+ lines.push(`ASN range ${range}`);
303
+ }
304
+ else if (parsed.startAutnum !== undefined) {
305
+ lines.push(`ASN start ${safeTerminalValue(parsed.startAutnum)}`);
306
+ }
307
+ else if (parsed.endAutnum !== undefined) {
308
+ lines.push(`ASN end ${safeTerminalValue(parsed.endAutnum)}`);
309
+ }
310
+ if (parsed.autnumType)
311
+ lines.push(`Allocation ${safeTerminalValue(parsed.autnumType)}`);
312
+ }
313
+ else {
314
+ if (parsed.startAddress && parsed.endAddress) {
315
+ lines.push(`Address range ${safeTerminalValue(parsed.startAddress)} to ${safeTerminalValue(parsed.endAddress)}`);
316
+ }
317
+ else if (parsed.startAddress) {
318
+ lines.push(`Address start ${safeTerminalValue(parsed.startAddress)}`);
319
+ }
320
+ else if (parsed.endAddress) {
321
+ lines.push(`Address end ${safeTerminalValue(parsed.endAddress)}`);
322
+ }
323
+ const cidrs = boundedTerminalStrings(parsed.cidrs, MAX_LOOKUP_TERMINAL_CIDRS);
324
+ if (cidrs)
325
+ lines.push(`CIDR prefixes ${cidrs}`);
326
+ if (parsed.networkType)
327
+ lines.push(`Allocation ${safeTerminalValue(parsed.networkType)}`);
328
+ }
329
+ if (parsed.country)
330
+ lines.push(`Country ${safeTerminalValue(parsed.country)}`);
331
+ const statuses = boundedTerminalStrings(parsed.statuses);
332
+ if (statuses)
333
+ lines.push(`Statuses ${statuses}`);
334
+ if (detail === 'verbose') {
335
+ const created = lifecycleDate(parsed, 'createdDate');
336
+ const updated = lifecycleDate(parsed, 'updatedDate');
337
+ if (created)
338
+ lines.push(`Created ${safeTerminalValue(created)}`);
339
+ if (updated)
340
+ lines.push(`Updated ${safeTerminalValue(updated)}`);
341
+ if (parsed.serverTruncated === true || parsed.cidrsTruncated === true)
342
+ lines.push('RDAP detail Source declared or local list truncation');
343
+ }
344
+ }
49
345
  function formatTerminalLookup(document, { detail = 'standard' } = {}) {
50
346
  const availability = terminalRecord(document.availability);
51
347
  const diagnostics = terminalRecord(document.diagnostics);
@@ -78,6 +374,10 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
78
374
  registrationLines.push(`Registrar source ${safeTerminalValue(registrarRdap.endpoint)}`);
79
375
  }
80
376
  }
377
+ if (['ipv4', 'ipv6', 'asn'].includes(String(document.type)) && positiveSourceStatus(rdapDiagnostics.status)) {
378
+ const rdap = terminalRecord(document.rdap);
379
+ appendNetworkRegistrationLines(registrationLines, terminalRecord(rdap.parsed), document.type, detail);
380
+ }
81
381
  const registryInsights = terminalRecord(document.registryInsights);
82
382
  if (detail !== 'summary' && registryInsights.version === 1) {
83
383
  const lifecycle = terminalRecord(registryInsights.lifecycle);
@@ -98,24 +398,161 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
98
398
  registrationLines.push(`Reconciliation ${titleCase(reconciliation.state)}`);
99
399
  registrationLines.push(`Publications ${publicationCounts.complete} complete · ${publicationCounts.partial} partial · ${publicationCounts.unavailable} unavailable`);
100
400
  }
101
- const websiteLines = [];
401
+ const dnsLines = [];
402
+ const mailLines = [];
403
+ const tlsLines = [];
102
404
  if (document.mode === 'deep' && document.type === 'domain') {
103
405
  const dns = terminalRecord(availability.dns);
406
+ const dnsRecords = terminalRecord(dns.records);
407
+ const dnsDiagnostics = terminalRecord(dns.diagnostics);
408
+ if (Object.keys(dns).length) {
409
+ dnsLines.push(`Evidence ${titleCase(dns.status)}`);
410
+ dnsLines.push(`Completeness ${dns.complete === true ? 'Complete' : 'Incomplete'}`);
411
+ const recordTypes = [
412
+ ['a', 'A'], ['aaaa', 'AAAA'], ['cname', 'CNAME'], ['ns', 'NS'],
413
+ ['mx', 'MX'], ['spf', 'SPF'], ['dmarc', 'DMARC'], ['caa', 'CAA'],
414
+ ['soa', 'SOA'], ['https', 'HTTPS'],
415
+ ];
416
+ const retainedCounts = recordTypes
417
+ .map(([key, label]) => [label, Array.isArray(dnsRecords[key]) ? dnsRecords[key].length : 0])
418
+ .filter(([, count]) => count > 0)
419
+ .map(([label, count]) => `${label} ${count}`);
420
+ dnsLines.push(`Retained ${retainedCounts.length ? safeTerminalValue(retainedCounts.join(' · ')) : 'No records retained'}`);
421
+ if (detail === 'verbose') {
422
+ const unsettled = recordTypes.flatMap(([key, label]) => {
423
+ const diagnostic = terminalRecord(dnsDiagnostics[key]);
424
+ return diagnostic.status && !['success', 'not_found'].includes(String(diagnostic.status))
425
+ ? [`${label} ${titleCase(diagnostic.status)}`]
426
+ : [];
427
+ });
428
+ if (unsettled.length)
429
+ dnsLines.push(`Query states ${safeTerminalValue(unsettled.join(' · '))}`);
430
+ }
431
+ if (detail !== 'summary') {
432
+ for (const [key, label] of recordTypes)
433
+ appendLookupRecordLine(dnsLines, label, dnsRecords[key]);
434
+ }
435
+ mailLines.push(`Evidence ${titleCase(dns.status)}`);
436
+ mailLines.push(`Mail exchange ${lookupObservationLabel(dns.hasNullMx === true ? true : dns.hasMx, dns.hasNullMx === true ? 'Null MX observed' : 'MX observed', 'No MX observed')}`);
437
+ mailLines.push(`SPF ${lookupObservationLabel(dns.hasSpf, 'Record observed', 'No record observed')}`);
438
+ mailLines.push(`DMARC ${lookupObservationLabel(dns.hasDmarc, 'Record observed', 'No record observed')}`);
439
+ if (detail !== 'summary') {
440
+ appendLookupRecordLine(mailLines, 'MX hosts', Array.isArray(dns.mxHosts) ? dns.mxHosts : []);
441
+ appendLookupRecordLine(mailLines, 'SPF policy', dnsRecords.spf);
442
+ appendLookupRecordLine(mailLines, 'DMARC policy', dnsRecords.dmarc);
443
+ }
444
+ }
445
+ const tls = terminalRecord(availability.tls);
446
+ if (Object.keys(tls).length) {
447
+ const usableTls = positiveSourceStatus(tls.status);
448
+ const certificate = usableTls ? terminalRecord(tls.certificate) : {};
449
+ const subject = terminalRecord(certificate.subject);
450
+ const issuer = terminalRecord(certificate.issuer);
451
+ const authorization = usableTls ? terminalRecord(tls.authorization) : {};
452
+ const hostname = usableTls ? terminalRecord(tls.hostname) : {};
453
+ const validity = usableTls ? terminalRecord(tls.validity) : {};
454
+ const cipher = usableTls ? terminalRecord(tls.cipher) : {};
455
+ tlsLines.push(`Evidence ${titleCase(tls.status)}`);
456
+ const completeness = sourceCompleteness(tls);
457
+ if (completeness)
458
+ tlsLines.push(`Completeness ${completeness}`);
459
+ if (usableTls && tls.connectedAddress && detail !== 'summary')
460
+ tlsLines.push(`Address ${safeTerminalValue(tls.connectedAddress)}`);
461
+ if (usableTls && tls.protocol)
462
+ tlsLines.push(`Protocol ${safeTerminalValue(tls.protocol)}`);
463
+ if (usableTls && tls.alpnProtocol && detail !== 'summary')
464
+ tlsLines.push(`ALPN ${safeTerminalValue(tls.alpnProtocol)}`);
465
+ if (cipher.standardName || cipher.name)
466
+ tlsLines.push(`Cipher ${safeTerminalValue(cipher.standardName || cipher.name)}`);
467
+ tlsLines.push(`Chain trust ${authorization.authorized === true ? 'Authorised' : authorization.authorized === false ? 'Not authorised' : 'Unavailable'}`);
468
+ tlsLines.push(`Hostname ${hostname.matches === true ? 'Matched' : hostname.matches === false ? 'Not matched' : 'Unavailable'}`);
469
+ tlsLines.push(`Validity ${validity.status ? titleCase(validity.status) : 'Unavailable'}`);
470
+ const subjectName = boundedTerminalNames(subject.commonNames ?? subject.CN);
471
+ const issuerName = boundedTerminalNames(issuer.commonNames ?? issuer.CN);
472
+ if (detail !== 'summary' && subjectName)
473
+ tlsLines.push(`Subject ${subjectName}`);
474
+ if (detail !== 'summary' && issuerName)
475
+ tlsLines.push(`Issuer ${issuerName}`);
476
+ if (detail !== 'summary' && certificate.validFrom)
477
+ tlsLines.push(`Valid from ${safeTerminalValue(certificate.validFrom)}`);
478
+ if (detail !== 'summary' && certificate.validTo)
479
+ tlsLines.push(`Valid until ${safeTerminalValue(certificate.validTo)}`);
480
+ const publicKey = terminalRecord(certificate.publicKey);
481
+ if (detail !== 'summary' && (publicKey.type || publicKey.bits || publicKey.curve)) {
482
+ tlsLines.push(`Public key ${safeTerminalValue([publicKey.type, publicKey.bits ? `${publicKey.bits} bits` : null, publicKey.curve].filter(Boolean).join(' '))}`);
483
+ }
484
+ const signature = terminalRecord(certificate.signature);
485
+ if (detail !== 'summary' && (signature.algorithm || signature.oid)) {
486
+ tlsLines.push(`Signature ${safeTerminalValue(signature.algorithm || signature.oid)}`);
487
+ }
488
+ const altNames = terminalRecord(certificate.subjectAltNames);
489
+ const dnsAltNames = Array.isArray(altNames.dnsNames) ? altNames.dnsNames : [];
490
+ const ipAltNames = Array.isArray(altNames.ipAddresses) ? altNames.ipAddresses : [];
491
+ const sanClasses = terminalCountSummary(altNames.classes, [
492
+ ['dns', 'DNS'], ['ip', 'IP'], ['email', 'email'], ['uri', 'URI'],
493
+ ['directoryName', 'directory name'], ['registeredId', 'registered ID'],
494
+ ['otherName', 'other name'], ['unclassified', 'other'],
495
+ ]);
496
+ if (detail !== 'summary' && (dnsAltNames.length || ipAltNames.length || sanClasses)) {
497
+ tlsLines.push(`SAN summary ${sanClasses || `DNS ${dnsAltNames.length} · IP ${ipAltNames.length}`}${altNames.truncated ? ' · truncated' : ''}`);
498
+ }
499
+ const chainCount = Array.isArray(tls.chain) ? tls.chain.length : 0;
500
+ if (detail !== 'summary' && (chainCount || tls.chainTruncated === true)) {
501
+ tlsLines.push(`Chain ${chainCount} retained certificate${chainCount === 1 ? '' : 's'}${tls.chainTruncated ? ' · truncated' : ''}`);
502
+ }
503
+ const aia = terminalRecord(certificate.authorityInformationAccess);
504
+ if (detail !== 'summary' && Object.keys(aia).length) {
505
+ const ocsp = terminalRecord(aia.ocsp);
506
+ const caIssuers = terminalRecord(aia.caIssuers);
507
+ tlsLines.push(`AIA presence OCSP ${terminalCount(ocsp.total)} · CA issuers ${terminalCount(caIssuers.total)} · `
508
+ + `unknown methods ${terminalCount(aia.unknownMethods)}${aia.truncated ? ' · truncated' : ''}`);
509
+ }
510
+ if (detail === 'verbose') {
511
+ const visibleAltNames = [...dnsAltNames, ...ipAltNames]
512
+ .slice(0, MAX_TLS_TERMINAL_ALT_NAMES)
513
+ .map((value) => safeTerminalValue(value));
514
+ if (visibleAltNames.length)
515
+ tlsLines.push(`Alt names ${boundedTerminalList(visibleAltNames, Math.max(0, dnsAltNames.length + ipAltNames.length - visibleAltNames.length))}`);
516
+ const purposes = terminalRecord(certificate.extendedKeyUsage);
517
+ const purposeValues = Array.isArray(purposes.values) ? purposes.values : [];
518
+ const visiblePurposes = purposeValues.slice(0, MAX_TLS_TERMINAL_PURPOSES).map((value) => {
519
+ const purpose = terminalRecord(value);
520
+ return safeTerminalValue(purpose.name || purpose.oid || 'Unrecognized purpose');
521
+ });
522
+ if (visiblePurposes.length)
523
+ tlsLines.push(`Purposes ${boundedTerminalList(visiblePurposes, Math.max(0, purposeValues.length - visiblePurposes.length))}${purposes.truncated ? ' · truncated' : ''}`);
524
+ const findings = boundedFindingLabels(tls.findings, (finding) => safeTerminalValue(finding.label, 'Unlabelled finding'));
525
+ if (findings)
526
+ tlsLines.push(`Findings ${findings}`);
527
+ }
528
+ if (detail === 'verbose' && certificate.fingerprintSha256)
529
+ tlsLines.push(`SHA-256 ${safeTerminalValue(certificate.fingerprintSha256)}`);
530
+ if (detail === 'verbose' && Array.isArray(tls.limitations)) {
531
+ for (const limitation of tls.limitations.slice(0, MAX_LOOKUP_TERMINAL_LIMITATIONS))
532
+ tlsLines.push(`Limitation ${safeTerminalValue(limitation)}`);
533
+ const omittedLimitations = Math.max(0, tls.limitations.length - MAX_LOOKUP_TERMINAL_LIMITATIONS);
534
+ if (omittedLimitations)
535
+ tlsLines.push(`Limitation +${omittedLimitations} more retained limitation${omittedLimitations === 1 ? '' : 's'}`);
536
+ }
537
+ }
538
+ }
539
+ const websiteLines = [];
540
+ if (document.mode === 'deep' && document.type === 'domain') {
104
541
  const http = terminalRecord(availability.http);
105
542
  const httpResponse = terminalRecord(http.response);
106
- const tls = terminalRecord(availability.tls);
107
543
  const credentialSurface = terminalRecord(availability.credentialSurfaceProfile);
108
544
  const structuredIdentity = terminalRecord(availability.structuredDataIdentity);
109
545
  const technology = terminalRecord(availability.technologyProfile);
110
546
  const browserLibraries = terminalRecord(technology.browserLibraryProfile);
111
547
  const posture = terminalRecord(availability.securityPosture);
112
548
  const postureSummary = terminalRecord(posture.summary);
549
+ const pageIdentity = terminalRecord(availability.pageIdentity);
550
+ const pageRole = terminalRecord(availability.pageRoleProfile);
551
+ const clientBehavior = terminalRecord(availability.clientBehaviorProfile);
113
552
  if (availability.activityStatus)
114
553
  websiteLines.push(`Web activity ${titleCase(availability.activityStatus)}`);
115
554
  if (detail !== 'summary' && availability.pageTitle)
116
555
  websiteLines.push(`Page title ${safeTerminalValue(availability.pageTitle)}`);
117
- if (dns.status)
118
- websiteLines.push(`DNS evidence ${titleCase(dns.status)}`);
119
556
  if (http.status) {
120
557
  websiteLines.push(`HTTP evidence ${titleCase(http.status)}`);
121
558
  const responseDetail = [
@@ -124,11 +561,49 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
124
561
  ].filter(Boolean).join(' · ');
125
562
  if (detail !== 'summary' && responseDetail)
126
563
  websiteLines.push(`HTTP response ${responseDetail}`);
564
+ if (detail !== 'summary')
565
+ appendDeliveryMetadataLines(websiteLines, httpResponse.deliveryMetadata, detail === 'verbose');
127
566
  }
128
- if (tls.status) {
129
- websiteLines.push(`TLS evidence ${titleCase(tls.status)}`);
130
- if (detail !== 'summary' && tls.protocol)
131
- websiteLines.push(`TLS protocol ${safeTerminalValue(tls.protocol)}`);
567
+ if (pageIdentity.status || pageIdentity.source === 'html') {
568
+ websiteLines.push(`Page identity ${titleCase(pageIdentity.status)}`);
569
+ const completeness = sourceCompleteness(pageIdentity);
570
+ if (completeness)
571
+ websiteLines.push(`Page coverage ${completeness}`);
572
+ if (positiveSourceStatus(pageIdentity.status) && detail !== 'summary') {
573
+ if (pageIdentity.documentLanguage)
574
+ websiteLines.push(`Language ${safeTerminalValue(pageIdentity.documentLanguage)}`);
575
+ const canonical = retainedUrlRelationship(pageIdentity.canonical, document.registrableDomain || availability.domain || document.query);
576
+ if (canonical)
577
+ websiteLines.push(`Canonical ${canonical}`);
578
+ const openGraph = terminalRecord(pageIdentity.openGraph);
579
+ if (openGraph.title || openGraph.siteName) {
580
+ websiteLines.push(`Open Graph ${safeTerminalValue([openGraph.siteName, openGraph.title].filter(Boolean).join(' · '))}`);
581
+ }
582
+ if (pageIdentity.generator)
583
+ websiteLines.push(`Generator ${safeTerminalValue(pageIdentity.generator)}`);
584
+ const forms = terminalRecord(pageIdentity.forms);
585
+ if (Object.keys(forms).length)
586
+ websiteLines.push(`Page forms ${terminalDisplayCount(forms.count)} total · ${terminalDisplayCount(forms.postCount)} POST · ${terminalDisplayCount(forms.insecureActionCount)} insecure action`);
587
+ const resources = terminalRecord(pageIdentity.resources);
588
+ const resourceTypes = terminalCountSummary(resources.byType, [
589
+ ['image', 'image'], ['script', 'script'], ['stylesheet', 'stylesheet'], ['link', 'link'],
590
+ ['frame', 'frame'], ['media', 'media'], ['object', 'object'],
591
+ ]);
592
+ if (Object.keys(resources).length)
593
+ websiteLines.push(`Resources ${terminalDisplayCount(resources.count)} retained${resourceTypes ? ` · ${resourceTypes}` : ''}${resources.truncated ? ' · truncated' : ''}`);
594
+ const downloads = terminalRecord(pageIdentity.downloads);
595
+ if (Object.keys(downloads).length)
596
+ websiteLines.push(`Downloads ${terminalDisplayCount(downloads.count)} retained · ${terminalDisplayCount(downloads.riskyCount)} review file${Number(downloads.riskyCount) === 1 ? '' : 's'}${downloads.truncated ? ' · truncated' : ''}`);
597
+ if ([pageIdentity.embeddedOrigins, pageIdentity.contactDomains, pageIdentity.trackingIdentifiers].some(Array.isArray)) {
598
+ websiteLines.push(`Relationships ${Array.isArray(pageIdentity.embeddedOrigins) ? pageIdentity.embeddedOrigins.length : 0} embedded retained · `
599
+ + `${Array.isArray(pageIdentity.contactDomains) ? pageIdentity.contactDomains.length : 0} contact domain${Array.isArray(pageIdentity.contactDomains) && pageIdentity.contactDomains.length === 1 ? '' : 's'} retained · `
600
+ + `${Array.isArray(pageIdentity.trackingIdentifiers) ? pageIdentity.trackingIdentifiers.length : 0} tracking identifier${Array.isArray(pageIdentity.trackingIdentifiers) && pageIdentity.trackingIdentifiers.length === 1 ? '' : 's'} retained`);
601
+ }
602
+ appendPublicationMetadataLines(websiteLines, pageIdentity.publicationMetadata, detail);
603
+ }
604
+ }
605
+ if (availability.phishingLanguageMatch && detail !== 'summary') {
606
+ websiteLines.push(`Content cue ${safeTerminalValue(availability.phishingLanguageMatch)} · static review label`);
132
607
  }
133
608
  if (credentialSurface.status || credentialSurface.source === 'html') {
134
609
  const forms = terminalRecord(credentialSurface.forms);
@@ -183,35 +658,117 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
183
658
  }
184
659
  if (posture.status || posture.source === 'derived') {
185
660
  websiteLines.push(`Posture ${titleCase(posture.status)}`);
186
- if (detail !== 'summary')
661
+ if (detail !== 'summary' && Object.keys(postureSummary).length)
187
662
  websiteLines.push(`Posture counts ${terminalCount(postureSummary.observed)} observed · `
188
663
  + `${terminalCount(postureSummary.potentialExposure)} potential exposure · `
189
664
  + `${terminalCount(postureSummary.observedAbsence)} observed absence · `
190
665
  + `${terminalCount(postureSummary.unavailable)} unavailable`);
666
+ if (detail === 'verbose' && positiveSourceStatus(posture.status)) {
667
+ const findings = boundedFindingLabels(posture.findings, (finding) => {
668
+ const state = finding.state ? ` (${titleCase(finding.state)})` : '';
669
+ return `${safeTerminalValue(finding.label, 'Unlabelled finding')}${state}`;
670
+ });
671
+ if (findings)
672
+ websiteLines.push(`Posture labels ${findings}`);
673
+ }
674
+ }
675
+ if (pageRole.status || pageRole.source === 'derived') {
676
+ websiteLines.push(`Page role ${titleCase(pageRole.status)}`);
677
+ const roleFindings = Array.isArray(pageRole.findings) ? pageRole.findings : [];
678
+ const primary = roleFindings.map(terminalRecord).find((finding) => finding.role === pageRole.primaryRole)
679
+ || terminalRecord(roleFindings[0]);
680
+ if (positiveSourceStatus(pageRole.status) && detail !== 'summary' && Object.keys(primary).length) {
681
+ websiteLines.push(`Primary role ${safeTerminalValue(primary.label, 'Unclassified')} · ${titleCase(primary.confidence)}`);
682
+ }
683
+ if (positiveSourceStatus(pageRole.status) && detail === 'verbose') {
684
+ const findings = boundedFindingLabels(roleFindings, (finding) => `${safeTerminalValue(finding.label, 'Unclassified')} (${titleCase(finding.confidence)})`);
685
+ if (findings)
686
+ websiteLines.push(`Role labels ${findings}`);
687
+ }
688
+ }
689
+ if (clientBehavior.status || clientBehavior.source === 'derived') {
690
+ websiteLines.push(`Client signals ${titleCase(clientBehavior.status)}`);
691
+ const scriptSummary = terminalRecord(clientBehavior.scriptSummary);
692
+ if (positiveSourceStatus(clientBehavior.status) && detail !== 'summary' && Object.keys(scriptSummary).length)
693
+ websiteLines.push(`Scripts ${terminalCount(scriptSummary.elementsObserved)} elements · ${terminalCount(scriptSummary.referencedScripts)} referenced · `
694
+ + `${terminalCount(scriptSummary.inlineScripts)} inline · ${terminalCount(scriptSummary.moduleScripts)} modules`);
695
+ if (positiveSourceStatus(clientBehavior.status) && detail === 'verbose') {
696
+ const indicators = boundedFindingLabels(clientBehavior.indicators, (indicator) => {
697
+ const occurrences = terminalCount(indicator.occurrences);
698
+ return `${safeTerminalValue(indicator.label, 'Static indicator')} (${titleCase(indicator.evidenceClass)}, ${occurrences})`;
699
+ });
700
+ if (indicators)
701
+ websiteLines.push(`Client labels ${indicators}`);
702
+ }
191
703
  }
192
704
  }
193
705
  const networkLines = [];
194
706
  if (document.mode === 'deep' && (document.type === 'ipv4' || document.type === 'ipv6')) {
195
707
  const reverseDns = terminalRecord(document.reverseDns);
196
708
  const reverseDnsRecords = terminalRecord(reverseDns.records);
197
- const ptrNames = Array.isArray(reverseDnsRecords.ptr)
198
- ? reverseDnsRecords.ptr.slice(0, 5).map((value) => safeTerminalValue(value))
199
- : [];
709
+ const ptrNames = boundedTerminalStrings(reverseDnsRecords.ptr);
200
710
  if (reverseDns.status)
201
711
  networkLines.push(`Reverse DNS ${titleCase(reverseDns.status)}`);
202
- if (detail !== 'summary' && ptrNames.length)
203
- networkLines.push(`PTR names ${safeTerminalValue(ptrNames.join(', '))}`);
712
+ const completeness = sourceCompleteness(reverseDns);
713
+ if (completeness)
714
+ networkLines.push(`PTR coverage ${completeness}`);
715
+ if (detail !== 'summary' && ptrNames)
716
+ networkLines.push(`PTR names ${ptrNames}`);
717
+ if (detail !== 'summary' && !ptrNames && reverseDns.status)
718
+ networkLines.push('PTR names No names retained; source state remains inconclusive');
204
719
  }
205
720
  const network = terminalRecord(document.networkContext);
206
721
  if (network.contextVersion === 1) {
207
722
  const endpoint = terminalRecord(network.endpoint);
208
723
  const networkRecord = terminalRecord(network.network);
209
724
  networkLines.push(`Network RDAP ${titleCase(network.status)}`);
725
+ const completeness = sourceCompleteness(network);
726
+ if (completeness)
727
+ networkLines.push(`Completeness ${completeness}`);
210
728
  if (detail !== 'summary' && endpoint.address)
211
729
  networkLines.push(`Selected IP ${safeTerminalValue(endpoint.address)}`);
212
- if (networkRecord.name || networkRecord.holder) {
730
+ if (detail !== 'summary' && endpoint.selectedFrom)
731
+ networkLines.push(`Selected from ${addressSelectionLabel(endpoint.selectedFrom)}`);
732
+ if (positiveSourceStatus(network.status) && (networkRecord.name || networkRecord.holder)) {
213
733
  networkLines.push(`Network ${safeTerminalValue(networkRecord.name || networkRecord.holder)}`);
214
734
  }
735
+ if (positiveSourceStatus(network.status) && detail !== 'summary') {
736
+ if (networkRecord.handle)
737
+ networkLines.push(`Network handle ${safeTerminalValue(networkRecord.handle)}`);
738
+ if (networkRecord.holder && networkRecord.holder !== networkRecord.name)
739
+ networkLines.push(`Holder ${safeTerminalValue(networkRecord.holder)}`);
740
+ const cidrs = boundedTerminalStrings(networkRecord.cidrs, MAX_LOOKUP_TERMINAL_CIDRS);
741
+ if (cidrs)
742
+ networkLines.push(`CIDR prefixes ${cidrs}`);
743
+ if (networkRecord.startAddress || networkRecord.endAddress)
744
+ networkLines.push(`Address range ${safeTerminalValue(networkRecord.startAddress)} to ${safeTerminalValue(networkRecord.endAddress)}`);
745
+ if (networkRecord.country)
746
+ networkLines.push(`Country ${safeTerminalValue(networkRecord.country)}`);
747
+ if (networkRecord.networkType)
748
+ networkLines.push(`Allocation ${safeTerminalValue(networkRecord.networkType)}`);
749
+ const allAbuseRoutes = Array.isArray(network.abuseRouting) ? network.abuseRouting : [];
750
+ const inspectedRoutes = Math.min(allAbuseRoutes.length, MAX_LOOKUP_TERMINAL_ABUSE_ROUTES);
751
+ const abuseRoutes = allAbuseRoutes.slice(0, inspectedRoutes).map(terminalRecord);
752
+ if (abuseRoutes.length) {
753
+ const channelCounts = new Map();
754
+ for (const route of abuseRoutes) {
755
+ const channel = ['email', 'phone'].includes(String(route.channel)) ? String(route.channel) : 'other';
756
+ channelCounts.set(channel, (channelCounts.get(channel) || 0) + 1);
757
+ }
758
+ const summary = [...channelCounts].map(([channel, count]) => `${channel} ${count}`).join(' · ');
759
+ const omittedRoutes = Math.max(0, allAbuseRoutes.length - inspectedRoutes);
760
+ networkLines.push(`Published routes ${abuseRoutes.length} retained${omittedRoutes ? ` · +${omittedRoutes} more entries` : ''} (${summary}); values omitted`);
761
+ }
762
+ if (detail === 'verbose' && networkRecord.databaseUpdatedAt)
763
+ networkLines.push(`Registry date ${safeTerminalValue(networkRecord.databaseUpdatedAt)}`);
764
+ if (detail === 'verbose' && Array.isArray(network.limitations)) {
765
+ for (const limitation of network.limitations.slice(0, MAX_LOOKUP_TERMINAL_LIMITATIONS))
766
+ networkLines.push(`Limitation ${safeTerminalValue(limitation)}`);
767
+ const omitted = Math.max(0, network.limitations.length - MAX_LOOKUP_TERMINAL_LIMITATIONS);
768
+ if (omitted)
769
+ networkLines.push(`Limitation +${omitted} more retained limitation${omitted === 1 ? '' : 's'}`);
770
+ }
771
+ }
215
772
  }
216
773
  const sourceHealthLines = [];
217
774
  const registryAccess = terminalRecord(diagnostics.registryAccess);
@@ -236,7 +793,10 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
236
793
  const lines = [];
237
794
  appendSection(lines, 'Target', targetLines);
238
795
  appendSection(lines, 'Registration', registrationLines);
796
+ appendSection(lines, 'DNS', dnsLines);
797
+ appendSection(lines, 'Mail', mailLines);
239
798
  appendSection(lines, 'Website and security', websiteLines);
799
+ appendSection(lines, 'TLS and certificate', tlsLines);
240
800
  appendSection(lines, 'Network', networkLines);
241
801
  appendSection(lines, 'Source health', sourceHealthLines);
242
802
  appendSection(lines, 'Collection', collectionLines);
@@ -446,6 +1006,7 @@ function formatTerminalHttp(document) {
446
1006
  ];
447
1007
  if (document.detail)
448
1008
  lines.push(`Detail ${safeTerminalValue(document.detail)}`);
1009
+ appendDeliveryMetadataLines(lines, response.deliveryMetadata, true);
449
1010
  const bodyHash = terminalRecord(response.bodyHash);
450
1011
  if (bodyHash.value) {
451
1012
  lines.push(`Body hash ${safeTerminalValue(`${bodyHash.algorithm}:${bodyHash.value} (${bodyHash.scope})`)}`);
@@ -668,4 +1229,4 @@ function formatTerminalRiskCalibration(document) {
668
1229
  lines.push('', safeTerminalValue(terminalRecord(document.interpretation).statement));
669
1230
  return `${lines.join('\n')}\n`;
670
1231
  }
671
- export { MAX_CT_TERMINAL_HOSTNAMES, MAX_CT_TERMINAL_MATCHES, MAX_DISCOVER_TERMINAL_CANDIDATES, MAX_POSTURE_TERMINAL_RECORDS, MAX_TLS_TERMINAL_ALT_NAMES, MAX_TLS_TERMINAL_PURPOSES, MAX_RISK_CALIBRATION_TERMINAL_RECORDS, MAX_TERMINAL_VALUE_LENGTH, formatTerminalBulk, formatTerminalCompare, formatTerminalCtSearch, formatTerminalDiscover, formatTerminalHttp, formatTerminalLookup, formatTerminalPosture, formatTerminalRegistrySupport, formatTerminalRiskCalibration, formatTerminalTls, safeTerminalValue, };
1232
+ export { MAX_CT_TERMINAL_HOSTNAMES, MAX_CT_TERMINAL_MATCHES, MAX_DISCOVER_TERMINAL_CANDIDATES, MAX_LOOKUP_TERMINAL_ALPN_IDS, MAX_LOOKUP_TERMINAL_ALPN_ID_LENGTH, MAX_LOOKUP_TERMINAL_LIMITATIONS, MAX_LOOKUP_TERMINAL_NAMES, MAX_LOOKUP_TERMINAL_RECORDS, MAX_POSTURE_TERMINAL_RECORDS, MAX_TLS_TERMINAL_ALT_NAMES, MAX_TLS_TERMINAL_PURPOSES, MAX_RISK_CALIBRATION_TERMINAL_RECORDS, MAX_TERMINAL_VALUE_LENGTH, formatTerminalBulk, formatTerminalCompare, formatTerminalCtSearch, formatTerminalDiscover, formatTerminalHttp, formatTerminalLookup, formatTerminalPosture, formatTerminalRegistrySupport, formatTerminalRiskCalibration, formatTerminalTls, safeTerminalValue, };