@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -2,7 +2,50 @@
2
2
  // injected so every traversal can be fixture-tested and the caller retains the
3
3
  // existing DNS timeout and missing/error semantics.
4
4
  import { parse as parseDomain } from 'tldts';
5
+ import { isValidAsciiHostname } from "../packages/contracts/domain-name.mjs";
5
6
  import { parseDmarcRecords, parseDmarcReportingAuthorization, parseReportingDestination, parseSpfRecords } from "./domain-posture-parsers.mjs";
7
+ const SPF_PERMISSIVE_PATH_ISSUE = 'Observed permissive +all evidence on a potentially reachable positive SPF include or redirect path; review sender authorisation.';
8
+ // This is a conservative universal-result check for unconditional all/include
9
+ // chains, not a sender evaluator. Conditional mechanisms remain unknown.
10
+ function spfStaticResult(policy) {
11
+ const parsed = policy.parsed;
12
+ if (!parsed?.valid)
13
+ return { everySender: null, permissive: false };
14
+ let uncertain = false, permissive = false;
15
+ const finish = (result) => ({
16
+ everySender: !uncertain ? result : null,
17
+ permissive,
18
+ });
19
+ for (const mechanism of parsed.mechanisms) {
20
+ if (mechanism.kind === 'all') {
21
+ if (mechanism.qualifier === '+')
22
+ permissive = true;
23
+ return finish(mechanism.qualifier);
24
+ }
25
+ if (mechanism.kind === 'include') {
26
+ const child = policy.children.get(mechanism.includeIndex);
27
+ const result = child ? spfStaticResult(child) : { everySender: null, permissive: false };
28
+ if (mechanism.qualifier === '+' && result.permissive)
29
+ permissive = true;
30
+ if (result.everySender === '+')
31
+ return finish(mechanism.qualifier);
32
+ if (result.everySender !== null)
33
+ continue; // Only child pass matches include.
34
+ uncertain = true;
35
+ }
36
+ else {
37
+ // Without sender evaluation, even a literal mechanism remains conditional.
38
+ uncertain = true;
39
+ }
40
+ }
41
+ if (parsed.redirect) {
42
+ const child = policy.children.get(parsed.includes.length);
43
+ const result = child ? spfStaticResult(child) : { everySender: null, permissive: false };
44
+ permissive ||= result.permissive;
45
+ return result.everySender === null ? { everySender: null, permissive } : finish(result.everySender);
46
+ }
47
+ return { everySender: null, permissive };
48
+ }
6
49
  const SPF_LOOKUP_LIMIT = 10;
7
50
  const SPF_VOID_LOOKUP_LIMIT = 2;
8
51
  const SPF_MAX_DEPTH = 5;
@@ -40,6 +83,7 @@ function branchFor(item, state, terminalPolicy, dnsLookupTerms, issues) {
40
83
  };
41
84
  }
42
85
  async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
86
+ const rootPolicy = { parsed: null, children: new Map() };
43
87
  const queue = [{
44
88
  domain: rootDomain,
45
89
  parent: null,
@@ -48,6 +92,7 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
48
92
  path: [rootDomain],
49
93
  records: rootQuery.records,
50
94
  error: rootQuery.error,
95
+ policy: rootPolicy,
51
96
  }];
52
97
  const branches = [];
53
98
  const issues = [];
@@ -99,6 +144,7 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
99
144
  branches.push(branchFor(item, parsed.valid ? 'success' : 'invalid', parsed.terminalPolicy, parsed.dnsLookupTerms, parsed.issues));
100
145
  if (!parsed.valid)
101
146
  continue;
147
+ item.policy.parsed = parsed;
102
148
  if (dnsLookupTerms > SPF_LOOKUP_LIMIT) {
103
149
  boundReached = true;
104
150
  issues.push(`The expanded policy declares more than ${SPF_LOOKUP_LIMIT} DNS-querying terms.`);
@@ -110,7 +156,9 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
110
156
  ...parsed.includes.map((domain) => ({ relation: 'include', domain })),
111
157
  ...(parsed.redirect ? [{ relation: 'redirect', domain: parsed.redirect }] : []),
112
158
  ];
113
- for (const dependency of dependencies) {
159
+ for (const [dependencyIndex, dependency] of dependencies.entries()) {
160
+ const policy = { parsed: null, children: new Map() };
161
+ item.policy.children.set(dependencyIndex, policy);
114
162
  const normalized = strictHostname(dependency.domain);
115
163
  if (!normalized) {
116
164
  branches.push(branchFor({
@@ -119,6 +167,7 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
119
167
  relation: dependency.relation,
120
168
  depth: item.depth + 1,
121
169
  path: item.path,
170
+ policy,
122
171
  }, 'invalid', null, 0, ['The SPF dependency is not a literal public hostname; macros are not expanded by this audit.']));
123
172
  continue;
124
173
  }
@@ -128,6 +177,7 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
128
177
  relation: dependency.relation,
129
178
  depth: item.depth + 1,
130
179
  path: [...item.path, normalized],
180
+ policy,
131
181
  });
132
182
  }
133
183
  }
@@ -144,6 +194,8 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
144
194
  : incomplete
145
195
  ? 'partial'
146
196
  : 'complete';
197
+ if (rootPolicy.parsed?.terminalPolicy !== 'pass' && spfStaticResult(rootPolicy).permissive)
198
+ issues.unshift(SPF_PERMISSIVE_PATH_ISSUE);
147
199
  return {
148
200
  version: 1,
149
201
  state,
@@ -166,9 +218,10 @@ function reportDestinationDomain(value) {
166
218
  if (/[!,;]/u.test(uri))
167
219
  return null;
168
220
  const destination = parseReportingDestination(uri);
169
- return destination?.scheme === 'mailto' ? strictHostname(destination.host) : null;
221
+ const domain = destination?.scheme === 'mailto' ? strictHostname(destination.host) : null;
222
+ return domain && isValidAsciiHostname(domain) ? domain : null;
170
223
  }
171
- async function validateDmarcExternalReporting(ownerDomain, dmarcQuery, resolveTxt) {
224
+ async function validateDmarcExternalReporting(ownerDomain, dmarcQuery, resolveTxt, resolveOrganisationalDomain) {
172
225
  if (dmarcQuery.error)
173
226
  return [];
174
227
  const parsed = parseDmarcRecords(dmarcQuery.records);
@@ -192,7 +245,7 @@ async function validateDmarcExternalReporting(ownerDomain, dmarcQuery, resolveTx
192
245
  });
193
246
  continue;
194
247
  }
195
- if (destinationDomain === ownerDomain || destinationDomain.endsWith(`.${ownerDomain}`)) {
248
+ if (destinationDomain === ownerDomain) {
196
249
  results.push({
197
250
  destination: destinationDomain,
198
251
  reportType: item.reportType,
@@ -202,6 +255,20 @@ async function validateDmarcExternalReporting(ownerDomain, dmarcQuery, resolveTx
202
255
  });
203
256
  continue;
204
257
  }
258
+ const ownerOrganisation = resolveOrganisationalDomain ? await resolveOrganisationalDomain(ownerDomain) : null;
259
+ const recipientOrganisation = ownerOrganisation && resolveOrganisationalDomain ? await resolveOrganisationalDomain(destinationDomain) : null;
260
+ if (ownerOrganisation && ownerOrganisation === recipientOrganisation) {
261
+ results.push({ destination: destinationDomain, reportType: item.reportType, recordName: null, state: 'self', error: null });
262
+ continue;
263
+ }
264
+ const externalConfirmed = Boolean(ownerOrganisation && recipientOrganisation);
265
+ // Descendant reporting previously made no authorisation request. Do not
266
+ // broaden default collection to resolve its unknown organisational scope.
267
+ if (!resolveOrganisationalDomain && destinationDomain.endsWith(`.${ownerDomain}`)) {
268
+ results.push({ destination: destinationDomain, reportType: item.reportType, recordName: null, state: 'unavailable',
269
+ error: 'The organisational boundary was not checked. Select the additional DNS review to determine whether external reporting authorisation is required.' });
270
+ continue;
271
+ }
205
272
  const recordName = `${ownerDomain}._report._dmarc.${destinationDomain}`;
206
273
  if (recordName.length > 253) {
207
274
  results.push({ destination: destinationDomain, reportType: item.reportType, recordName: null, state: 'unavailable', error: 'The reporting authorisation query exceeds the DNS name limit.' });
@@ -223,8 +290,9 @@ async function validateDmarcExternalReporting(ownerDomain, dmarcQuery, resolveTx
223
290
  destination: destinationDomain,
224
291
  reportType: item.reportType,
225
292
  recordName,
226
- state: query.error ? 'unavailable' : authorized ? 'authorized' : 'not_found',
227
- error: query.error,
293
+ state: query.error ? 'unavailable' : authorized ? 'authorized' : externalConfirmed ? 'not_found' : 'unavailable',
294
+ error: query.error ?? (!authorized && !externalConfirmed
295
+ ? 'The organisational boundary is unknown; no authorisation requirement can be inferred from the missing record.' : null),
228
296
  });
229
297
  }
230
298
  return results;
@@ -276,4 +344,4 @@ function buildExternalDependencies({ domain, nameservers, mx, spfExpansion, dmar
276
344
  || left.target.localeCompare(right.target)
277
345
  || left.source.localeCompare(right.source)));
278
346
  }
279
- export { SPF_LOOKUP_LIMIT, SPF_MAX_DEPTH, SPF_VOID_LOOKUP_LIMIT, buildExternalDependencies, expandSpfPolicy, reportDestinationDomain, validateDmarcExternalReporting, };
347
+ export { SPF_PERMISSIVE_PATH_ISSUE, SPF_LOOKUP_LIMIT, SPF_MAX_DEPTH, SPF_VOID_LOOKUP_LIMIT, buildExternalDependencies, expandSpfPolicy, reportDestinationDomain, validateDmarcExternalReporting, };
@@ -48,6 +48,7 @@ function parseSpfRecords(records) {
48
48
  terminalPolicy: null,
49
49
  redirect: null,
50
50
  includes: [],
51
+ mechanisms: [],
51
52
  authorizingTerms: [],
52
53
  dnsLookupTerms: 0,
53
54
  issues: [],
@@ -175,6 +176,14 @@ function parseSpfRecords(records) {
175
176
  result.includes = terms
176
177
  .map((term) => term.match(/^[+?~-]?include:(.+)$/i)?.[1] || '')
177
178
  .filter(Boolean);
179
+ let includeIndex = 0;
180
+ result.mechanisms = terms.filter(term => !/^[+?~-]?[a-z][a-z0-9_.-]*=/iu.test(term)).map(term => {
181
+ const qualifier = (/^[+?~-]/u.test(term) ? term[0] : '+');
182
+ const body = /^[+?~-]/u.test(term) ? term.slice(1) : term;
183
+ const include = body.match(/^include:(.+)$/iu);
184
+ return { kind: /^all$/iu.test(body) ? 'all' : include ? 'include' : 'other', qualifier,
185
+ includeIndex: include ? includeIndex++ : null };
186
+ });
178
187
  if (terms.some((term) => /^[+?~-]?ptr(?::|$)/i.test(term))) {
179
188
  result.issues.push('The deprecated ptr mechanism should not be used.');
180
189
  }
@@ -7,12 +7,12 @@ import { nonEmptyErrorMessage } from "./error-detail.mjs";
7
7
  import { safeFetch, readTextCapped } from "./safe-fetch.mjs";
8
8
  import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
9
9
  import { classifyMxRecords } from "./dns-mx.mjs";
10
- import { collectDnsInheritanceChecks } from "./dns-inheritance-review.mjs";
10
+ import { collectDnsInheritanceReview } from "./dns-inheritance-review.mjs";
11
11
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
12
12
  import { DOMAIN_POSTURE_COMPARISON_VERSION, MAX_POSTURE_CHECK_RECORDS, POSTURE_CHECK_SOURCES } from "../packages/evidence/domain-posture-context.mjs";
13
13
  import { canonicalPostureRecords } from "../packages/evidence/domain-control-runtime.mjs";
14
14
  import { parseSpfRecords, parseDmarcRecords, parseMtaStsDnsRecords, parseMtaStsPolicy, parseTlsRptRecords, parseBimiRecords, parseDkimRecords, } from "./domain-posture-parsers.mjs";
15
- import { buildExternalDependencies, expandSpfPolicy, validateDmarcExternalReporting, } from "./domain-posture-analysis.mjs";
15
+ import { SPF_PERMISSIVE_PATH_ISSUE, buildExternalDependencies, expandSpfPolicy, validateDmarcExternalReporting, } from "./domain-posture-analysis.mjs";
16
16
  const DNS_TIMEOUT_MS = 6000;
17
17
  const POLICY_TIMEOUT_MS = 7000;
18
18
  const MAX_POLICY_BYTES = 64 * 1024;
@@ -156,7 +156,8 @@ function spfCheck(query, expansion) {
156
156
  remediation: 'Replace +all with an explicit sender allowlist and a restrictive terminal policy.',
157
157
  });
158
158
  }
159
- if (parsed.terminalPolicy === 'fail' && parsed.issues.length === 0 && (!expansion || expansion.state === 'complete')) {
159
+ const permissiveExpansion = expansion?.issues.includes(SPF_PERMISSIVE_PATH_ISSUE) ?? false;
160
+ if (parsed.terminalPolicy === 'fail' && parsed.issues.length === 0 && !permissiveExpansion && (!expansion || expansion.state === 'complete')) {
160
161
  return check('spf', 'SPF', 'pass', 'Restrictive fail-all policy', { detail: details.join(' '), records: parsed.records });
161
162
  }
162
163
  if (expansion && expansion.state !== 'complete') {
@@ -166,6 +167,12 @@ function spfCheck(query, expansion) {
166
167
  remediation: 'Review unresolved, invalid, cyclic, or budget-limited include and redirect branches before treating the policy as complete.',
167
168
  });
168
169
  }
170
+ if (permissiveExpansion) {
171
+ return check('spf', 'SPF', 'warning', 'SPF authorisation needs review', {
172
+ detail: details.join(' '), records: parsed.records,
173
+ remediation: 'Review the positive include or redirect path containing +all; complete collection does not establish restrictive sender authorisation.',
174
+ });
175
+ }
169
176
  if (parsed.terminalPolicy === 'redirect') {
170
177
  return check('spf', 'SPF', 'info', 'Policy delegates evaluation with redirect', {
171
178
  detail: details.join(' '), records: parsed.records,
@@ -202,16 +209,18 @@ function dmarcCheck(query, authorizations = []) {
202
209
  parsed.failureReporting ? 'Failure reporting is configured.' : 'No failure reporting destination is configured.',
203
210
  ...parsed.issues,
204
211
  ];
205
- const external = authorizations.filter((authorization) => authorization.state !== 'self');
206
- const unresolvedExternal = external.filter((authorization) => authorization.state !== 'authorized');
212
+ const unresolvedExternal = authorizations.filter((authorization) => !['self', 'authorized'].includes(authorization.state));
207
213
  const intendedDestinations = parsed.aggregateDestinations.length + parsed.failureDestinations.length;
208
214
  const omittedDestinations = Math.max(0, intendedDestinations - authorizations.length);
209
215
  if (omittedDestinations) {
210
216
  details.push(`Reporting authorisation coverage: ${authorizations.length} of ${intendedDestinations} destinations reviewed; ${omittedDestinations} not checked within the bounded collection. Omitted destinations are not assumed authorised.`);
211
217
  }
212
- if (external.length > 0) {
213
- details.push(`${external.length} external reporting destination${external.length === 1 ? '' : 's'} checked; `
214
- + `${unresolvedExternal.length} could not be authorized.`);
218
+ if (authorizations.length > 0) {
219
+ details.push(`${authorizations.length} reporting destination${authorizations.length === 1 ? '' : 's'} reviewed; `
220
+ + `${unresolvedExternal.length} need further evidence.`);
221
+ }
222
+ if (unresolvedExternal.some(item => item.state === 'unavailable')) {
223
+ details.push('An unknown organisational boundary or unavailable query does not establish a missing authorisation requirement.');
215
224
  }
216
225
  if (parsed.testMode) {
217
226
  return check('dmarc', 'DMARC', 'warning', 'Policy is in test mode (t=y)', {
@@ -235,9 +244,7 @@ function dmarcCheck(query, authorizations = []) {
235
244
  return check('dmarc', 'DMARC', 'warning', `Enforced at p=${parsed.policy}; reporting authorisation is incomplete`, {
236
245
  detail: `${details.join(' ')} ${unresolvedExternal.map((authorization) => `${authorization.destination}: ${authorization.state}.`).join(' ')}`,
237
246
  records: parsed.records,
238
- remediation: omittedDestinations > 0
239
- ? 'Review the unchecked destinations and confirm any required external reporting authorisation before relying on delivery.'
240
- : 'Publish the required external reporting authorisation record or remove the unavailable destination.',
247
+ remediation: 'Review unresolved destinations and their organisational boundaries, then confirm any required external reporting authorisation before relying on delivery.',
241
248
  });
242
249
  }
243
250
  if (!parsed.aggregateReporting) {
@@ -301,6 +308,11 @@ function dnssecDelegationConsistencyCheck(input, registry) {
301
308
  }
302
309
  const value = String(input.value || '').toLowerCase();
303
310
  const hasDsRecords = registry.dsRecordCount > 0;
311
+ if (registry.dsDataTruncated) {
312
+ return check('dnssec_delegation_consistency', 'DNSSEC delegation consistency', 'info', 'Retained DS evidence is incomplete', {
313
+ detail: 'The normalised registry response reported truncated DS data, so delegation consistency is inconclusive.',
314
+ });
315
+ }
304
316
  if (value === 'signed' && hasDsRecords) {
305
317
  return check('dnssec_delegation_consistency', 'DNSSEC delegation consistency', 'pass', 'Signed state and retained DS records agree', {
306
318
  detail: `${registry.dsRecordCount} registry DS record${registry.dsRecordCount === 1 ? '' : 's'} retained.`,
@@ -311,11 +323,6 @@ function dnssecDelegationConsistencyCheck(input, registry) {
311
323
  detail: 'This is a consistency result, not a recommendation to leave DNSSEC disabled.',
312
324
  });
313
325
  }
314
- if (registry.dsDataTruncated) {
315
- return check('dnssec_delegation_consistency', 'DNSSEC delegation consistency', 'info', 'Retained DS evidence is incomplete', {
316
- detail: 'The normalised registry response reported truncated DS data, so apparent disagreement is inconclusive.',
317
- });
318
- }
319
326
  if (value === 'signed' && !hasDsRecords) {
320
327
  return check('dnssec_delegation_consistency', 'DNSSEC delegation consistency', 'warning', 'Signed state reported without retained DS records', {
321
328
  detail: 'This can reflect registry publication differences or incomplete upstream data and should be reviewed before changing delegation.',
@@ -360,13 +367,25 @@ function matchesMtaPattern(host, pattern) {
360
367
  }
361
368
  return normalizedHost === normalizedPattern;
362
369
  }
370
+ function mailPolicyApplicability(mxQuery) {
371
+ if (mxQuery.error)
372
+ return { hasMx: false, detail: 'Receiving-mail applicability is unknown because the MX lookup failed.' };
373
+ const records = completeMxRecords(mxQuery.records);
374
+ if (!records)
375
+ return { hasMx: false, detail: 'Receiving-mail applicability is unknown because the MX evidence is incomplete.' };
376
+ const { hasMx, hasNullMx } = classifyMxRecords(records);
377
+ return { hasMx, detail: hasMx ? '' : hasNullMx
378
+ ? 'Null MX explicitly declares that this domain does not accept inbound mail.'
379
+ : 'No MX records were observed. This alone does not establish that the domain cannot receive mail.' };
380
+ }
363
381
  function mtaStsCheck(dnsQuery, policyFetch, mxQuery) {
364
382
  if (dnsQuery.error)
365
383
  return queryFailureCheck('mta_sts', 'MTA-STS', dnsQuery.error);
366
384
  const dnsPolicy = parseMtaStsDnsRecords(dnsQuery.records);
367
- const hasMx = !mxQuery.error && classifyMxRecords(asMxRecords(mxQuery.records)).hasMx;
368
385
  if (dnsPolicy.records.length === 0) {
386
+ const { hasMx, detail } = mailPolicyApplicability(mxQuery);
369
387
  return check('mta_sts', 'MTA-STS', hasMx ? 'warning' : 'info', 'No MTA-STS policy advertised', {
388
+ detail,
370
389
  remediation: hasMx ? 'Publish _mta-sts and serve a valid policy over HTTPS to require authenticated TLS for inbound mail.' : '',
371
390
  });
372
391
  }
@@ -436,9 +455,9 @@ function tlsRptCheck(query, mxQuery) {
436
455
  return queryFailureCheck('tls_rpt', 'TLS-RPT', query.error);
437
456
  const parsed = parseTlsRptRecords(query.records);
438
457
  if (parsed.records.length === 0) {
439
- const hasMx = !mxQuery.error && classifyMxRecords(asMxRecords(mxQuery.records)).hasMx;
458
+ const { hasMx, detail } = mailPolicyApplicability(mxQuery);
440
459
  return check('tls_rpt', 'TLS-RPT', hasMx ? 'warning' : 'info', 'No SMTP TLS reporting policy', {
441
- detail: hasMx ? '' : 'The domain has no receiving mail exchanger, so SMTP TLS reporting is not currently actionable.',
460
+ detail,
442
461
  remediation: hasMx ? 'Publish a v=TLSRPTv1 record at _smtp._tls with a monitored rua destination.' : '',
443
462
  });
444
463
  }
@@ -785,17 +804,18 @@ async function collectDomainPosture(domain, { dkimSelectors = [], retiredDkimSel
785
804
  const enrichment = [
786
805
  parsedMtaDns?.valid ? dependencies.fetchMtaStsPolicy(domain, signal) : Promise.resolve(null),
787
806
  expandSpfPolicy(domain, spf, resolveEnrichmentTxt),
788
- validateDmarcExternalReporting(domain, dmarc, resolveEnrichmentTxt),
789
807
  includeInheritedDns === true
790
- ? (dependencies.collectDnsInheritanceChecks
791
- ? dependencies.collectDnsInheritanceChecks(domain, dmarc, nameservers, signal ? { signal } : {})
808
+ ? (dependencies.collectDnsInheritanceReview
809
+ ? dependencies.collectDnsInheritanceReview(domain, dmarc, nameservers, signal ? { signal } : {})
792
810
  : Promise.reject(new TypeError('The explicit DNS review collector is unavailable.')))
793
- : Promise.resolve([]),
811
+ : validateDmarcExternalReporting(domain, dmarc, resolveEnrichmentTxt)
812
+ .then(dmarcAuthorizations => ({ checks: [], dmarcAuthorizations })),
794
813
  ];
795
814
  // A rejected policy request must not release the operation's capacity while
796
815
  // other started enrichment collectors are still settling.
797
- const [mtaStsPolicy, spfExpansion, dmarcAuthorizations, inheritanceChecks] = await Promise.all(enrichment)
816
+ const [mtaStsPolicy, spfExpansion, dnsReview] = await Promise.all(enrichment)
798
817
  .finally(() => Promise.allSettled(enrichment));
818
+ const { dmarcAuthorizations, checks: inheritanceChecks } = dnsReview;
799
819
  signal?.throwIfAborted();
800
820
  const dnssec = !rdap
801
821
  ? { value: null, error: 'RDAP did not return a domain record.' }
@@ -884,7 +904,7 @@ async function checkDomainPosture(domain, options = {}, dependencies) {
884
904
  now: () => new Date(),
885
905
  setTimer: (callback, milliseconds) => setTimeout(callback, milliseconds),
886
906
  clearTimer: handle => clearTimeout(handle),
887
- collectDnsInheritanceChecks,
907
+ collectDnsInheritanceReview,
888
908
  });
889
909
  }
890
910
  finally {