@slicedearth/whoisleuth-cli 2.6.0 → 2.8.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 (96) hide show
  1. package/DISCLOSURE +8 -5
  2. package/NOTICE +3 -2
  3. package/README.md +60 -67
  4. package/cli/artifact-validation/case-response.mjs +6 -857
  5. package/cli/artifact-validation/lookup-evidence.mjs +15 -2
  6. package/cli/artifact-verify.mjs +32 -1
  7. package/cli/bulk-command-runner.mjs +5 -4
  8. package/cli/case-command.mjs +17 -17
  9. package/cli/case-pack.mjs +7 -1
  10. package/cli/ci-report.mjs +17 -5
  11. package/cli/collection-arguments.mjs +6 -0
  12. package/cli/collection-command-definitions.mjs +13 -3
  13. package/cli/command-definition.mjs +7 -5
  14. package/cli/discovery-scan-command-runner.mjs +13 -7
  15. package/cli/doctor.mjs +37 -3
  16. package/cli/domain-control-monitor.mjs +25 -2
  17. package/cli/domain-control-observations.mjs +52 -15
  18. package/cli/fail-policy.mjs +31 -22
  19. package/cli/formatters/html.mjs +3 -0
  20. package/cli/formatters/json.mjs +8 -1
  21. package/cli/formatters/terminal-lookup.mjs +24 -1
  22. package/cli/history-command-definitions.mjs +1 -1
  23. package/cli/investigation-plan.mjs +15 -0
  24. package/cli/lookup-brief.mjs +11 -5
  25. package/cli/lookup-command-runner.mjs +26 -18
  26. package/cli/lookup-diff.mjs +1 -0
  27. package/cli/review-command-definitions.mjs +11 -11
  28. package/cli/risk-policy.mjs +83 -0
  29. package/cli/saved-lookup.mjs +22 -7
  30. package/cli/source-reliability.mjs +12 -10
  31. package/cli/support-arguments.mjs +1 -1
  32. package/cli/support-command-definitions.mjs +5 -4
  33. package/cli/support-command-runner.mjs +6 -4
  34. package/cli/workflow-command-definitions.mjs +2 -1
  35. package/cli/workflow-command-runner.mjs +5 -1
  36. package/docs/cli-reference.md +56 -11
  37. package/docs/cli.md +162 -33
  38. package/docs/contextual-reviews.md +10 -0
  39. package/docs/infrastructure-observations.md +9 -0
  40. package/lib/dmarc-discovery.mjs +4 -2
  41. package/lib/dns-inheritance-review.mjs +14 -1
  42. package/lib/evidence-export.mjs +10 -4
  43. package/lib/evidence-report.mjs +2 -1
  44. package/lib/generated/cisa-kev-catalog.mjs +19 -4
  45. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  46. package/lib/generated/retire-browser-catalog.mjs +178 -5
  47. package/lib/idn-confusables.mjs +5 -1
  48. package/lib/lookup-collection-receipt.mjs +52 -0
  49. package/lib/lookup-diagnostics.mjs +2 -12
  50. package/lib/lookup-progress.mjs +29 -31
  51. package/lib/lookup-response-contract.mjs +9 -0
  52. package/lib/lookup-response.mjs +15 -14
  53. package/lib/lookup-source-progress.mjs +4 -2
  54. package/lib/lookup.mjs +33 -2
  55. package/lib/rdap-capabilities.mjs +43 -9
  56. package/lib/registry-insights.mjs +17 -6
  57. package/lib/sslbl-certificates.generated.mjs +143 -142
  58. package/lib/whois-authority.mjs +56 -1
  59. package/lib/whois-chain.mjs +2 -1
  60. package/lib/whois-contracts.mjs +2 -0
  61. package/lib/whois-normalization.mjs +4 -4
  62. package/package.json +6 -6
  63. package/packages/analysis/bounded-json.mjs +12 -2
  64. package/packages/analysis/brand-profile-signals.mjs +42 -0
  65. package/packages/cases/case-attachment-model.mjs +36 -1
  66. package/packages/cases/case-checkpoint-receipts.mjs +68 -0
  67. package/packages/cases/case-evidence-links.mjs +28 -0
  68. package/packages/cases/case-evidence-model.mjs +39 -6
  69. package/packages/cases/case-migration-model.mjs +9 -1
  70. package/packages/cases/case-object-coverage.mjs +106 -20
  71. package/packages/cases/case-record-operations.mjs +13 -0
  72. package/packages/cases/case-record-projection.mjs +28 -2
  73. package/packages/cases/case-report.mjs +5 -4
  74. package/packages/cases/case-response-packet-validation.mjs +880 -0
  75. package/packages/cases/case-response-packet-vocabulary.mjs +12 -1
  76. package/packages/cases/case-response-packet.mjs +2 -15
  77. package/packages/cases/case-response-review-inputs.mjs +18 -1
  78. package/packages/cases/case-type-presentation.mjs +10 -0
  79. package/packages/cases/case-workflow-metadata.mjs +0 -8
  80. package/packages/comparison/capture-context.mjs +3 -2
  81. package/packages/contracts/analyst-review-state-contract.mjs +1 -1
  82. package/packages/contracts/case-portability.mjs +59 -28
  83. package/packages/contracts/cli-doctor.mjs +4 -0
  84. package/packages/contracts/cli-lookup.mjs +57 -46
  85. package/packages/contracts/lookup-evidence.mjs +12 -2
  86. package/packages/contracts/registry-insights.mjs +6 -0
  87. package/packages/evidence/dmarc-policy-context.mjs +80 -0
  88. package/packages/evidence/lookup-collection-receipt.mjs +140 -0
  89. package/packages/evidence/lookup-collection-review.mjs +74 -0
  90. package/packages/interchange/external-findings-import.mjs +8 -4
  91. package/packages/investigation/bulk-domain-comparison.mjs +17 -8
  92. package/packages/investigation/bulk-mail-exposure.mjs +2 -2
  93. package/packages/investigation/domain-history-review.mjs +7 -1
  94. package/packages/workspace/brand-keyword-campaign.mjs +11 -1
  95. package/packages/workspace/bulk-session-model.mjs +8 -0
  96. package/third-party-notices.txt +30 -30
package/docs/cli.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # WHOISleuth CLI guide
2
2
 
3
- `review-evidence` also reviews bounded source-qualified infrastructure snapshots and explicit earlier/later comparison inputs entirely offline. See [infrastructure observations](infrastructure-observations.md) for the exact schemas, outcome semantics and retained-evidence limitations.
4
-
5
3
  The first-party CLI runs on the operator's machine and does not call the hosted
6
4
  WHOISleuth deployment. Use this guide for installation, common commands,
7
5
  collection boundaries and output. Installed `whoisleuth --help`, focused
@@ -14,6 +12,11 @@ tasks, then offers searchable commands with examples, option explanations and
14
12
  defaults. Command-section links can be bookmarked. Its option guidance and
15
13
  installed help derive from the same command definitions.
16
14
 
15
+ Not sure which interface to use? The [installation-mode comparison](https://www.whoisleuth.com/cli#choose-mode)
16
+ covers the hosted console, filesystem-local application, CLI and capture companion,
17
+ including request origin, storage, offline use and distribution. The optional
18
+ application and capture archives are separate from the public CLI package.
19
+
17
20
  The generated [privacy and data-flow catalogue](https://github.com/slicedearth/whoisleuth/blob/main/docs/privacy-data-flow-catalogue.md)
18
21
  lists the network, recipient, retention and export boundary for every command.
19
22
 
@@ -77,22 +80,35 @@ private document from the interactive view:
77
80
 
78
81
  ```bash
79
82
  whoisleuth lookup example.test --deep --browse --save-lookup lookup.json
83
+ whoisleuth verify-artifact lookup.json --json
84
+ whoisleuth export lookup.json --output evidence.json
85
+ whoisleuth verify-artifact evidence.json --json
80
86
  ```
81
87
 
82
- In Console Lookup, open **Replay exported evidence**, select the file, verify
83
- its digest and source states, then create or update a browser-local Case. The
84
- file is not uploaded. Case classification, exact incident links and response
85
- actions remain deliberate analyst steps. Ordinary Case files also support the
88
+ Keep `lookup.json` as the private CLI original. In Lookup in the console, open **Replay
89
+ exported evidence**, select `evidence.json`, check its digest and source states,
90
+ then create or update a browser-local case. The private CLI document is not a
91
+ browser replay format; the separate evidence export excludes raw registration
92
+ payloads and expanded contacts. Neither file is uploaded. Case classification, exact incident links and response
93
+ actions remain deliberate analyst steps. Ordinary case files also support the
86
94
  offline CLI workflow below.
87
- When several incident Cases share a domain, select the intended Case before
88
- retaining replay evidence. Case packs preserve each current Case ID separately;
95
+ When several incident cases share a domain, select the intended case before
96
+ retaining replay evidence. Case packs preserve each current case ID separately;
89
97
  trusted and public packs exclude analyst-entered incident titles.
90
98
  Every audience retains tags, decision summaries and rationale as well as domain
91
- evidence and Case, pin and decision IDs. Public does not mean anonymous.
99
+ evidence and case, pin and decision IDs. Public does not mean anonymous.
92
100
  Browser Lookup evidence can be verified or packaged in the CLI. `brief`,
93
101
  `export`, `compare`, `registry-doctor` and `source-report` instead require a
94
102
  saved CLI Lookup; they do not reconstruct it from a browser export.
95
103
 
104
+ Registration source states are independent: `unsupported` means no RDAP or
105
+ WHOIS service is published for the target, while `skipped` means no query was
106
+ issued in that mode or under the registry access policy. Neither is a transport
107
+ failure or a domain-absence finding. Standard terminal output explains these
108
+ non-query outcomes; JSON and evidence exports retain their separate states.
109
+ `--strict-exit` does not fail solely because a protocol is unsupported or skipped,
110
+ but continues to report genuine failed or partial sources.
111
+
96
112
  Process selected local input:
97
113
 
98
114
  ```bash
@@ -146,6 +162,8 @@ For an explicit inherited-policy and direct parent-delegation review, use
146
162
  option, posture collection keeps its existing exact-name scope. The extra
147
163
  results distinguish recursive policy publication from sampled parent referrals;
148
164
  they do not establish name existence, message authentication or DNSSEC validity.
165
+ The machine-readable posture document keeps its existing versioned fields;
166
+ the website's transient structured policy scope is not included in that export.
149
167
 
150
168
  | Mode | Behaviour |
151
169
  | --- | --- |
@@ -160,6 +178,16 @@ their focused help. They do not use the hosted login or hosted usage controls.
160
178
  `lookup --plan` lists planned source families and disclosure targets before
161
179
  collection. `doctor` is offline unless `--network` is selected.
162
180
 
181
+ For troubleshooting that you intend to share, use
182
+ `whoisleuth doctor --summary-json > diagnostics.json`. This summary keeps numeric
183
+ CLI/Node versions, fixed check codes and public format identifiers from the command
184
+ catalogue. Those identifiers describe declared formats, not a compatibility test.
185
+ Targets, URLs, filenames, paths, user names, credentials, headers, timestamps and
186
+ free-form diagnostic details are omitted. Version prerelease/build metadata is
187
+ also omitted. Normal terminal and `--json` diagnostics can include error details;
188
+ inspect them before sharing. The summary does not enable network checks or upload
189
+ anything, and it has the same success/partial exit behaviour as the detailed report.
190
+
163
191
  Deep Lookup reports IPv4 and IPv6 results separately, including empty answers,
164
192
  NODATA, resolver-reported missing names and query failures. These outcomes remain
165
193
  distinct in evidence exports; none alone establishes domain removal.
@@ -265,6 +293,8 @@ metadata or freshness admission; terminal output identifies that limitation.
265
293
 
266
294
  ### Contextual reviews
267
295
 
296
+ `review-evidence` reviews bounded source-qualified infrastructure snapshots and explicit earlier/later comparisons offline. See [infrastructure observations](infrastructure-observations.md) for supported inputs and how to interpret their outcomes.
297
+
268
298
  `review-evidence` also accepts [contextual review inputs](contextual-reviews.md)
269
299
  for source-qualified incident sequences, retained domain history, platform-object continuity, authorised storefront
270
300
  comparison and connector provenance. These workflows are offline; partial
@@ -278,11 +308,11 @@ reassessment; they do not establish ownership or discard prior evidence.
278
308
 
279
309
  `review-evidence containment.json --json --strict-exit` accepts a
280
310
  `whoisleuth.internal-containment.input` version-1 document containing a supported
281
- Case export, selected next-step assertion and linked pin IDs, internal or trusted
311
+ case export, selected next-step assertion and linked pin IDs, internal or trusted
282
312
  audience, recipient role and an explicit disclosure-review acknowledgement.
283
- It exports only that selected handoff, never writes the Case or performs a
313
+ It exports only that selected handoff, never writes the case or performs a
284
314
  control. Missing or unselected supporting context stays partial (exit 4 under
285
- `--strict-exit`), and a resolved Case or provider outcome does not close an open
315
+ `--strict-exit`), and a resolved case or provider outcome does not close an open
286
316
  internal follow-up. See [internal containment](offline-intake-context.md#selected-internal-containment-handoff)
287
317
  for the complete input and audience limitations.
288
318
 
@@ -335,19 +365,19 @@ selected file, including an envelope when supplied. No provider API is contacted
335
365
 
336
366
  ## Output and automation
337
367
 
338
- ### Local Case files
368
+ ### Local case files
339
369
 
340
370
  Manual response scope is explicit and offline. `case incident-link` takes
341
371
  `{"url":"https://example.test/reported-page"}`; use its retained ID to bind
342
372
  `responseObjects` on `case action`. Each object contains `kind`, `identifier`
343
- and `incidentTargetId` (null for the Case domain or an associated hostname).
373
+ and `incidentTargetId` (null for the case domain or an associated hostname).
344
374
  New URL objects must match that exact retained link. Exact historical identities
345
375
  already retained in response records remain selectable after a link edit or
346
376
  removal; they are not rebound to its replacement. An action may bind up to 20
347
377
  objects; missing historical scope stays unknown.
348
378
 
349
379
  `case action-event` takes `{"id":"action-id","transition":{...}}`, using
350
- the same reviewed, authorised and manually submitted workflow as Console.
380
+ the same reviewed, authorised and manually submitted workflow as the console.
351
381
  After submission, a typed `objectOutcome` such as `removed`, `restricted`,
352
382
  `suspended`, `delisted`, `restored` or `disputed` requires explicit event
353
383
  `responseObjects`: a partial receipt never applies to all objects by default.
@@ -372,11 +402,11 @@ Independent removal, restriction, suspension, delisting, transfer and restoratio
372
402
  use those same comparison requirements. Disputes and warnings retain procedural
373
403
  source attribution separately; provider claims are not independent observations.
374
404
  `case close-object` requires one `responseObject` and the usual typed closure
375
- reason/evidence links. It leaves the Case and other objects unchanged.
405
+ reason/evidence links. It leaves the case and other objects unchanged.
376
406
  All these JSON operations require `--input`, `--output`, deliberate file
377
407
  replacement and the existing digest/lease checks. No report or request is sent.
378
408
 
379
- Create a working file, inspect its Case IDs, then append a note:
409
+ Create a working file, inspect its case IDs, then append a note:
380
410
 
381
411
  ```sh
382
412
  whoisleuth case open --domain example.test --title "Review the selected form" --output cases.json
@@ -384,8 +414,8 @@ whoisleuth case show cases.json
384
414
  whoisleuth case note cases.json --note-file note.txt --output cases.json --force
385
415
  ```
386
416
 
387
- Use `--case-id` when the file contains several Cases. `open` reuses the selected
388
- Case; `--new-incident --title "Another incident"` creates a distinct ID for the
417
+ Use `--case-id` when the file contains several cases. `open` reuses the selected
418
+ case; `--new-incident --title "Another incident"` creates a distinct ID for the
389
419
  same domain. It does not open a browser or collect anything.
390
420
 
391
421
  `pin`, `link`, `withdraw-link`, `assess` and `recheck` read a selected JSON file with `--input`:
@@ -442,23 +472,23 @@ A recheck input contains `state`, `observedAt`, `completeness`, `source` and
442
472
  `comparisonSummary`, with optional limitations and follow-up time. For example,
443
473
  use `state: "unavailable"` and `completeness: "partial"` when a supplied capture
444
474
  did not complete. `not_reproduced` requires a saved recheck question from the
445
- Case's console workflow, its exact `recheck` context, the target
475
+ case's console workflow, its exact `recheck` context, the target
446
476
  `observationHostname`, a complete later observation where a baseline exists,
447
477
  and `conditionsMatch: "comparable"`. It never means global removal or takedown.
448
478
 
449
- Mutations require an explicit output path and write the complete current Case
479
+ Mutations require an explicit output path and write the complete current case
450
480
  export. They never prune old evidence to fit. Input formatting may use up to
451
- 16 MiB; the canonical store remains limited to 4 MiB and 500 Cases. `show --json`
452
- can export a selected subset to a **different** path. Ordinary Case imports also
481
+ 16 MiB; the canonical store remains limited to 4 MiB and 500 cases. `show --json`
482
+ can export a selected subset to a **different** path. Ordinary case imports also
453
483
  allow 16 MiB for formatting and export metadata; the stored-data limit remains
454
- 4 MiB. Case packs retain their separate 25-Case selection and 4-MiB source-file
484
+ 4 MiB. Case packs retain their separate 25-case selection and 4-MiB source-file
455
485
  limits, and include generated reports within the bounded import allowance.
456
486
 
457
487
  `show` reports the exact file digest. Add `--expect-file-digest sha256:<digest>`
458
488
  to reject a file changed since that review. Source/output locks and atomic
459
489
  replacement also detect changes during execution. After an interrupted process,
460
490
  inspect its adjacent `.workflow.lock` before deliberately removing it. Validation
461
- or conflict failures leave the Case file unchanged and return 2. Filesystem
491
+ or conflict failures leave the case file unchanged and return 2. Filesystem
462
492
  publication failures return 3.
463
493
 
464
494
  Working files contain private analyst content and file references, not attachment
@@ -478,19 +508,19 @@ whoisleuth manifest evidence.json screenshot.png --workflow "Evidence review" \
478
508
  whoisleuth verify-artifact --folder ./evidence-project --json --strict-exit
479
509
  ```
480
510
 
481
- Packages admit the 128-file, 64-MiB original selection plus one bounded Case
511
+ Packages admit the 128-file, 64-MiB original selection plus one bounded case
482
512
  export, with a separate manifest allowance. Original-file limits remain unchanged.
483
- `verify-artifact` uses the same exact ordinary Case reader as `case show`,
513
+ `verify-artifact` uses the same exact ordinary case reader as `case show`,
484
514
  without repairing content. Package verification also reports original-reference
485
- completeness for each Case file; `--strict-exit` returns 4 when a referenced original is missing.
486
- An ordinary Case JSON file alone provides structural validity, not a checksum.
515
+ completeness for each case file; `--strict-exit` returns 4 when a referenced original is missing.
516
+ An ordinary case JSON file alone provides structural validity, not a checksum.
487
517
  They use generated entry names, not original paths. Ordinary ZIPs and folders
488
518
  are private and unencrypted; packaging does not redact selected files. The report distinguishes
489
519
  file identity, supported source formats, opaque content, exact capsule/source
490
520
  links, declared image derivation and capture-manifest attachment matches.
491
521
  Image derivation reports an immediate-parent digest and byte length, editing
492
522
  method and operation kinds, not proof of transformation. Parent inclusion matches
493
- another selected manifest entry by exact byte digest and size; an unselected Case
523
+ another selected manifest entry by exact byte digest and size; an unselected case
494
524
  attachment reference does not count. Parent pixels are optional; absent declarations
495
525
  leave editing history unknown. Include the capture manifest and
496
526
  its screenshot/DOM-digest files together to check their declared bytes; original
@@ -551,6 +581,35 @@ trees and unlisted files. Its package digest and byte count describe the
551
581
  canonical stored-ZIP representation, not filesystem metadata. These are
552
582
  selected evidence exports, not complete workspace backups.
553
583
 
584
+ ### Exporting a fixed workflow for review
585
+
586
+ Terminal plans and recipe explanations distinguish networked steps, offline
587
+ steps and declared reuse of earlier outputs. These are step and handoff counts,
588
+ not request totals or confirmation that a saved file is available. The planner
589
+ does not inspect files or caches; collection and evidence selection remain
590
+ deliberate actions. JSON exports keep their existing versioned fields.
591
+
592
+ Select one installed recipe and save its declared steps for review:
593
+
594
+ ```sh
595
+ whoisleuth workflow-plan --explain domain-triage --json --output recipe.json
596
+ ```
597
+
598
+ The document includes the subject requirement, command and argument examples,
599
+ network modes, required approvals, output artefact families and completion guidance.
600
+ To prepare the same recipe for a specific subject:
601
+
602
+ ```sh
603
+ whoisleuth workflow-plan domain-triage example.test --json --output plan.json
604
+ ```
605
+
606
+ Both commands plan offline. The files contain declarative steps rather than
607
+ shell history or executable scripts. Argument examples and approval fields
608
+ describe requirements; they do not record a completed review or grant approval.
609
+ These are review documents, not `workflow-run` checkpoints or custom recipe
610
+ imports. Run an installed recipe deliberately with its required inputs and
611
+ fresh approvals.
612
+
554
613
  ### Resuming a fixed workflow
555
614
 
556
615
  Inspect a paused run without executing any step or rewriting its checkpoint:
@@ -583,14 +642,17 @@ whoisleuth workflow-run domain-triage example.test --approve-network \
583
642
  ```
584
643
 
585
644
  Bindings accept Lookup outputs for export, diff, timeline, comparison, source
586
- reports and briefs; evidence exports for verification; and Case-pack output
645
+ reports and briefs; evidence exports for verification; and case-pack output
587
646
  for sharing review. Remaining placeholders use `--select` in order;
588
647
  for example, `--select diff=previous.json` keeps the current observation as the
589
648
  second input. Supply every input for an uncompleted step to use files instead,
590
649
  or override a connection with `--use-artifact <step-id>:<input-number>=<earlier-step-id>`.
591
650
  Input numbers start at 1 and refer to the fixed recipe's placeholders.
592
651
  `diff` compares saved observations of the same or different domains; `timeline`
593
- orders observations of one domain. Candidate and domain-control intent inputs
652
+ orders observations of one domain. Web-derived fields require complete collection
653
+ for the relevant page or favicon; failed, partial and historical unknown collection
654
+ keep their values visible without establishing equality or change. Source-health
655
+ changes remain comparable separately. Candidate and domain-control intent inputs
594
656
  remain analyst selections. No external file is inferred or extracted automatically.
595
657
  `reconcile` lists each observation time and declared observer/vantage, keeping
596
658
  disagreement and unavailable evidence separate. Distinct labels do not verify
@@ -698,9 +760,61 @@ and machine output contains no ANSI or transient progress text.
698
760
  - `lookup --browse` provides an interactive terminal view; press `?` for keys.
699
761
  - `--events` writes versioned lifecycle events to stderr.
700
762
  - `--strict-exit` and `--fail-on` expose selected evidence states to automation.
763
+ - `lookup --junit` reports retained diagnostic source states, not version or
764
+ timing metadata. Nested diagnostic failures remain failures; target-bearing
765
+ diagnostic paths are not copied into the XML. This is not a truth verdict.
701
766
  - `--output` writes a private local file atomically and refuses an existing path
702
767
  unless `--force` is also selected.
703
768
 
769
+ Lookup, Bulk and Discover scan evaluate `--fail-on` against every collected or
770
+ restored result, before output filters. A fulfilled lookup can still match
771
+ `source-failure` through its diagnostics. Quiet and event output do not change
772
+ the policy exit: matches return **4**, with diagnostics on stderr. Ordinary
773
+ collection/checkpoint failures and Lookup's separate `--strict-exit` still apply.
774
+
775
+ For `monitor-once`, `source-failure` also checks the current run's constituent
776
+ source diagnostics, even when the Lookup fulfils. `inconclusive` checks incomplete
777
+ requested comparisons independently of the headline: MX drift does not hide
778
+ unavailable nameserver evidence. Skipped or unsupported sources are not failed
779
+ requests; an unsupported requested comparison can still be inconclusive.
780
+ Unconfigured expectations and historical source failures do not match these
781
+ policies. `material-drift` continues to mean unexpected complete observation
782
+ changes in the flight recorder, not a desired-state mismatch or missing evidence.
783
+ Policy matches return **4** in JSON and quiet modes too, without adding diagnostic
784
+ fields to monitor reports or checkpoints. Without a policy, rejected Lookups
785
+ retain their existing failure exit; constituent-source states alone do not.
786
+
787
+ Domain-control review keeps registration evidence at the registrable domain and
788
+ DNS, TLS and web evidence at the observed hostname. It selects the newest eligible
789
+ evidence within each scope; a newer child-host capture cannot replace an apex or
790
+ sibling's service evidence. Separately declared delegation scope is retained.
791
+ Historical Lookups use their retained availability domain, or their original
792
+ registration scope when no hostname was retained, never the submitted query.
793
+ `monitor-once` accepts only registrable-domain targets and rejects an admitted
794
+ child target before any collection starts. A wrong-host collector response is a
795
+ failed Lookup, not an extra successful observation or a retargeted check.
796
+
797
+ For `danger`, export your Brand Profiles from Brands and explicitly select Deep
798
+ collection. The same Risk model and Brand comparisons used by the console
799
+ evaluate the collected evidence without extra requests:
800
+
801
+ ```bash
802
+ whoisleuth lookup candidate.example --deep --brand-profile brand.json --fail-on danger --json
803
+ whoisleuth bulk domains.txt --deep --brand-profile brand.json --fail-on danger --errors-only --json
804
+ ```
805
+
806
+ The selected file uses the existing versioned Brand Profile export format,
807
+ must provide one unambiguous valid profile with an official domain, and is bounded
808
+ by the existing Brand Profile import byte limit. If the export contains multiple
809
+ profiles, add `--brand-id <exact-profile-id>` from the selected profile's `id`
810
+ field; no first-profile default is chosen. Its private fields are not copied into
811
+ results or checkpoints or sent to sources. Restore it explicitly when resuming.
812
+ Danger matches report the scoring-model version and evidence quality. The
813
+ threshold prioritises review, not confirmed maliciousness. Fast collection,
814
+ missing Brand context or ineligible registration evidence cannot establish a
815
+ match; exit **0** does not establish low risk, complete coverage or safety.
816
+ `inconclusive` and `source-failure` remain independently selected policies.
817
+
704
818
  Local configuration profiles can set presentation, Fast collection, bounded
705
819
  concurrency and observer labels. They cannot add targets, enable Deep
706
820
  collection, choose output paths or approve network work. Explicit command
@@ -708,7 +822,22 @@ options override profile defaults.
708
822
 
709
823
  `source-report` summarises collector-owned health envelopes in saved Lookup
710
824
  and Bulk output. Raw publications and extension fields are not interpreted as
711
- diagnostics, and missing health metadata remains unmeasured.
825
+ diagnostics, and missing health metadata remains unmeasured. Its text view groups
826
+ collection states, sample counts and durations under each source. Observation
827
+ durations and Lookup timings stay separate; merged-report distributions describe
828
+ retained report statistics, not a new measurement of individual requests.
829
+
830
+ After verifying a domain-control or Lookup-claim passport, `verify-artifact`
831
+ suggests the compatible offline `interchange-report` command. Suggestions use
832
+ example filenames and never run automatically. Other artefact families receive
833
+ guidance only when the suggested reader supports that format.
834
+
835
+ The text output of `brief` groups selected facts in an indented source tree.
836
+ Each source keeps its collection state and observation time, including sources
837
+ with no fact selected for the brief; that does not mean the source was empty.
838
+ Conflicting values remain separate and include both the value state and source
839
+ state. The brief uses retained evidence only. Use `--json` for the
840
+ structured report, including its facts, source health, unknowns and action plan.
712
841
 
713
842
  `mail-review` keeps failed input targets, incomplete DNS coverage and original
714
843
  collection times visible. Source-output and review-generation times do not
@@ -71,6 +71,16 @@ to the retained concern. Incomplete comparisons remain visible. A changed creati
71
71
  date asks for reassessment of earlier decisions without asserting a new owner,
72
72
  resetting a baseline or discarding history; expiry alone never establishes deletion.
73
73
 
74
+ Open **Retirement review checklist** for DNS-alias, recovery-email and retained
75
+ integration-reference prompts. Review the configuration you control, then use
76
+ the existing declaration form to record the affected hostnames, evidence family,
77
+ retirement time and source. The draft shows these details before you prepare a
78
+ review. Prefer a configuration or change-reference label; do not enter recovery
79
+ addresses, credentials or complete URLs. Reports with declared retired dependencies
80
+ include the same suggested next reviews in the browser and CLI. Suggestions do not
81
+ record checks as completed or establish takeover availability. Saving preserves
82
+ the source-qualified declaration, not confirmation that configuration was changed.
83
+
74
84
  Schema: `whoisleuth.domain-history.input`. Its `evidence` contains:
75
85
 
76
86
  - `caseExport`: an ordinary supported editable Case export, with `version` and
@@ -43,6 +43,15 @@ Topology search applies to the complete admitted one-hop relationship set, not
43
43
  the current source page. Its diagram has a 50-relationship budget; the paginated
44
44
  source list and exact snapshot review retain access to evidence beyond the diagram.
45
45
 
46
+ Follow a retained domain to an address or certificate, then open its supporting
47
+ Case. **Return to retained infrastructure** restores the exact query, filters,
48
+ page positions and selected identity. **Tab references → Navigation trail** also
49
+ lets you return to earlier successful pivots. This trail lives only in the
50
+ current tab and workspace; it creates no observed relationships. Sources are
51
+ read again rather than copied into navigation state. A removed identity stays
52
+ unavailable, and clearing references or locking the workspace expires the links.
53
+ Certificate identities here are leaf-certificate SHA-256, not SPKI digests.
54
+
46
55
  Lookup links fill the selected target for review without submitting it. Explicitly
47
56
  selected hostnames can be prepared in existing Bulk. That handoff does not scan,
48
57
  recursively expand targets, change a watchlist or enrol a host in monitoring.
@@ -1,6 +1,7 @@
1
1
  import { isValidAsciiHostname } from "../packages/contracts/domain-name.mjs";
2
2
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
3
3
  import { parseDmarcRecords } from "./domain-posture-parsers.mjs";
4
+ import { MAX_DMARC_DISCOVERY_OWNERS } from "../packages/evidence/dmarc-policy-context.mjs";
4
5
  export function requireDmarcDomain(domain) {
5
6
  if (typeof domain !== 'string' || domain.length > 253 || !isValidAsciiHostname(domain) || domain !== domain.toLowerCase()) {
6
7
  throw new TypeError('The additional DNS review requires a normalised domain.');
@@ -29,7 +30,8 @@ export function dmarcPolicyObservation(owner, observation) {
29
30
  : state;
30
31
  const time = typeof observation.observedAt === 'string' && observation.observedAt.length <= 64
31
32
  ? normalizeExplicitIsoTimestamp(observation.observedAt) : null;
32
- return { owner, parsed, psd, valid, unavailable, incomplete,
33
+ return { owner, parsed, psd, valid, unavailable, incomplete, observedAt: time,
34
+ state: unavailable ? 'unavailable' : valid ? 'observed' : parsed.records.length ? 'invalid' : 'not_found',
33
35
  record: `Recursive DNS TXT _dmarc.${owner} · ${time ?? 'time unavailable'} · ${description}` };
34
36
  }
35
37
  /** RFC 9989 sections 4.10–4.10.2: at most eight owners, never PSL inference. */
@@ -38,7 +40,7 @@ export async function walkDmarcTree(domain, exact, resolve, purpose) {
38
40
  const rows = [dmarcPolicyObservation(domain, exact)];
39
41
  const first = rows[0];
40
42
  const labels = domain.split('.');
41
- let next = labels.length >= 8 ? labels.slice(-7) : labels.slice(1);
43
+ let next = labels.length >= MAX_DMARC_DISCOVERY_OWNERS ? labels.slice(-(MAX_DMARC_DISCOVERY_OWNERS - 1)) : labels.slice(1);
42
44
  const policyOnly = purpose === 'policy' && first.valid;
43
45
  if (!policyOnly && !first.unavailable && !(first.valid && ['n', 'y'].includes(first.psd))) {
44
46
  while (next.length) {
@@ -5,6 +5,7 @@ import { requireDmarcDomain, walkDmarcTree } from "./dmarc-discovery.mjs";
5
5
  import { buildDnssecQuery, DNS_TYPE_NS, normalizeResolverEndpoint, parseDnssecResponse } from "./dnssec-chain-validation.mjs";
6
6
  import { defaultTcpExchange } from "./service-binding-dns.mjs";
7
7
  import { isValidAsciiHostname } from "../packages/contracts/domain-name.mjs";
8
+ import { normalizeDmarcPolicyContext } from "../packages/evidence/dmarc-policy-context.mjs";
8
9
  const MAX_PARENT_SERVERS = 2;
9
10
  const MAX_NS = 16;
10
11
  const QUERY_TIMEOUT_MS = 2200;
@@ -35,7 +36,19 @@ export async function reviewInheritedDmarc(domain, exact, resolve) {
35
36
  ? `Published policy: ${selected.parsed.policy}. Test mode: ${selected.parsed.testMode ? 'yes' : 'no'}.`
36
37
  : `For an existing name: ${selected.parsed.subdomainPolicy}. For a nonexistent name: ${selected.parsed.nonexistentSubdomainPolicy}. Test mode: ${selected.parsed.testMode ? 'yes' : 'no'}. Name existence is not inferred from this review.`
37
38
  : 'Invalid or unavailable records do not establish absence or a usable inherited policy.';
38
- return check('dmarc_inheritance', 'Inherited DMARC policy', incomplete || !selected ? 'warning' : 'info', summary, `${detail} Discovery follows RFC 9989 with at most eight owners. This is publication evidence, not message authentication or a guarantee of receiver behaviour.`, rows.map(row => row.record));
39
+ const effective = !incomplete ? selected : null;
40
+ return {
41
+ ...check('dmarc_inheritance', 'Inherited DMARC policy', incomplete || !selected ? 'warning' : 'info', summary, `${detail} Discovery follows RFC 9989 with at most eight owners. This is publication evidence, not message authentication or a guarantee of receiver behaviour.`, rows.map(row => row.record)),
42
+ dmarcPolicyContext: normalizeDmarcPolicyContext({
43
+ version: 1, source: 'recursive_dns_txt', complete: !incomplete,
44
+ relation: effective ? effective.owner === domain ? 'exact' : 'inherited' : 'undetermined',
45
+ owner: effective?.owner ?? null, observedAt: effective?.observedAt ?? null,
46
+ policy: effective?.parsed.policy ?? null, subdomainPolicy: effective?.parsed.subdomainPolicy ?? null,
47
+ nonexistentPolicy: effective?.parsed.nonexistentSubdomainPolicy ?? null,
48
+ psd: effective?.psd ?? null, testMode: effective?.parsed.testMode ?? null,
49
+ discovery: rows.map(row => ({ owner: row.owner, state: row.state, observedAt: row.observedAt })),
50
+ }, domain),
51
+ };
39
52
  }
40
53
  function nameservers(value) {
41
54
  if (!Array.isArray(value) || value.length > MAX_NS)
@@ -6,7 +6,7 @@ import { buildRegistryInsights } from "./registry-insights.mjs";
6
6
  import { buildPortableGeneratorMetadata } from "../packages/analysis/portable-generator.mjs";
7
7
  import { assertBoundedJsonStructure } from "../packages/analysis/bounded-json.mjs";
8
8
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
9
- import { LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, } from "../packages/contracts/lookup-evidence.mjs";
9
+ import { LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, AUTHORITY_AWARE_LOOKUP_EVIDENCE_SCHEMA_VERSION, } from "../packages/contracts/lookup-evidence.mjs";
10
10
  export * from "../packages/contracts/lookup-evidence.mjs";
11
11
  import { isValidAsciiHostname } from "../packages/contracts/domain-name.mjs";
12
12
  import { validLookupObservationScope } from "../packages/evidence/lookup-target.mjs";
@@ -705,6 +705,7 @@ const LOOKUP_WHOIS_PORTABLE_SCALARS = Object.freeze({
705
705
  failedHop: nullableText(253),
706
706
  conflictingHop: nullableText(253),
707
707
  });
708
+ const WHOIS_REGISTRATION_STATUS = enumeration(new Set(['registered', 'not_found', 'inconclusive']));
708
709
  const LOOKUP_REGISTRY_INSIGHT_PORTABLE_KEYS = new Set([
709
710
  'version', 'contactDisclosure', 'lifecycle', 'reconciliation', 'publications',
710
711
  'rdapCapabilities',
@@ -815,11 +816,16 @@ export function projectLookupEvidenceRdapPublication(value) {
815
816
  return output;
816
817
  }
817
818
  /** Positive portable projection for normalized WHOIS publication fields. */
818
- export function projectLookupEvidenceWhoisPublication(value) {
819
+ export function projectLookupEvidenceWhoisPublication(value, schemaVersion = LOOKUP_EVIDENCE_SCHEMA_VERSION) {
819
820
  const input = recordOrNull(value);
820
821
  if (!input)
821
822
  return null;
822
823
  const output = projectedKnownScalarRecord(input, LOOKUP_WHOIS_PORTABLE_SCALARS) || {};
824
+ if (schemaVersion >= AUTHORITY_AWARE_LOOKUP_EVIDENCE_SCHEMA_VERSION && Object.hasOwn(input, 'registrationStatus')) {
825
+ const status = WHOIS_REGISTRATION_STATUS(input.registrationStatus);
826
+ if (status !== undefined)
827
+ output.registrationStatus = status;
828
+ }
823
829
  if (Object.hasOwn(input, 'lifecycle')) {
824
830
  output.lifecycle = projectedKnownScalarRecord(input.lifecycle, LOOKUP_RDAP_LIFECYCLE_SCALARS);
825
831
  }
@@ -884,7 +890,7 @@ function projectedWhoisChainHop(value) {
884
890
  };
885
891
  }
886
892
  /** Exact positive projection for one current-schema WHOIS source wrapper. */
887
- export function projectLookupEvidenceWhoisSourcePublication(value) {
893
+ export function projectLookupEvidenceWhoisSourcePublication(value, schemaVersion = LOOKUP_EVIDENCE_SCHEMA_VERSION) {
888
894
  const source = recordOrNull(value);
889
895
  if (!source)
890
896
  return null;
@@ -895,7 +901,7 @@ export function projectLookupEvidenceWhoisSourcePublication(value) {
895
901
  error: boundedSourceError(source.error, 'WHOIS source reported an error.'),
896
902
  };
897
903
  const hasPublication = status === 'complete' || status === 'partial';
898
- const parsed = hasPublication ? projectLookupEvidenceWhoisPublication(source.parsed) : null;
904
+ const parsed = hasPublication ? projectLookupEvidenceWhoisPublication(source.parsed, schemaVersion) : null;
899
905
  if (status === 'complete' && parsed === null)
900
906
  return null;
901
907
  return {
@@ -1,4 +1,5 @@
1
1
  import { registryAccessProfileLabel } from "./registry-access.mjs";
2
+ import { isSupportedRegistryInsightsVersion } from "../packages/contracts/registry-insights.mjs";
2
3
  import { dnsAddressFamilyEvidence } from "../packages/evidence/dns-query-outcome.mjs";
3
4
  import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../packages/analysis/portable-generator.mjs";
4
5
  import { technologyEvidenceRoles, } from "./technology-evidence-role.mjs";
@@ -386,7 +387,7 @@ function buildLookupEvidenceReport(document, options = {}) {
386
387
  reportField('Standing limitations', listText(registrarStanding.limitations)),
387
388
  reportField('Review steps', listText(registrarStanding.nextActions)),
388
389
  ] : [],
389
- registryInterpretation: registryInsights.version === 1 ? [
390
+ registryInterpretation: isSupportedRegistryInsightsVersion(registryInsights.version) ? [
390
391
  reportField('Lifecycle', displayLabel(registryLifecycle.label)),
391
392
  reportField('Raw lifecycle statuses', listText(registryLifecycle.rawStatuses)),
392
393
  reportField('Client registration lock observed', yesNoUnknown(registryLocks.client)),
@@ -1,9 +1,9 @@
1
1
  // Generated by tools/cisa-kev-catalog.mts from the pinned CISA KEV feed.
2
2
  // United States Government work; do not edit by hand.
3
3
  const CISA_KEV_CATALOG = Object.freeze({
4
- catalogVersion: "2026.10.02",
5
- releasedAt: "2026-10-02T15:19:38.2945Z",
6
- sourceSha256: "d2c8c6cb23291b46ff2b086197641a6b1fa35e6b746bba714cc280a1778f9e48",
4
+ catalogVersion: "2026.10.16",
5
+ releasedAt: "2026-10-16T17:15:48.8752Z",
6
+ sourceSha256: "b6e27975f6635ecfc546fbd4fd6e444ba63a07ab2abc8876229d77fe083d357d",
7
7
  sourceUrl: "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
8
8
  identifiers: Object.freeze([
9
9
  "CVE-2002-0367",
@@ -194,6 +194,7 @@ const CISA_KEV_CATALOG = Object.freeze({
194
194
  "CVE-2015-3043",
195
195
  "CVE-2015-3113",
196
196
  "CVE-2015-3246",
197
+ "CVE-2015-3306",
197
198
  "CVE-2015-4068",
198
199
  "CVE-2015-4495",
199
200
  "CVE-2015-4852",
@@ -203,6 +204,7 @@ const CISA_KEV_CATALOG = Object.freeze({
203
204
  "CVE-2015-5123",
204
205
  "CVE-2015-5287",
205
206
  "CVE-2015-5317",
207
+ "CVE-2015-5477",
206
208
  "CVE-2015-6175",
207
209
  "CVE-2015-7450",
208
210
  "CVE-2015-7645",
@@ -229,6 +231,7 @@ const CISA_KEV_CATALOG = Object.freeze({
229
231
  "CVE-2016-20017",
230
232
  "CVE-2016-2386",
231
233
  "CVE-2016-2388",
234
+ "CVE-2016-3081",
232
235
  "CVE-2016-3088",
233
236
  "CVE-2016-3235",
234
237
  "CVE-2016-3298",
@@ -842,6 +845,7 @@ const CISA_KEV_CATALOG = Object.freeze({
842
845
  "CVE-2021-31955",
843
846
  "CVE-2021-31956",
844
847
  "CVE-2021-31979",
848
+ "CVE-2021-3199",
845
849
  "CVE-2021-32030",
846
850
  "CVE-2021-32648",
847
851
  "CVE-2021-33044",
@@ -1080,6 +1084,7 @@ const CISA_KEV_CATALOG = Object.freeze({
1080
1084
  "CVE-2023-22515",
1081
1085
  "CVE-2023-22518",
1082
1086
  "CVE-2023-22527",
1087
+ "CVE-2023-22894",
1083
1088
  "CVE-2023-22952",
1084
1089
  "CVE-2023-23376",
1085
1090
  "CVE-2023-23397",
@@ -1542,6 +1547,7 @@ const CISA_KEV_CATALOG = Object.freeze({
1542
1547
  "CVE-2025-59689",
1543
1548
  "CVE-2025-59718",
1544
1549
  "CVE-2025-60710",
1550
+ "CVE-2025-61624",
1545
1551
  "CVE-2025-61757",
1546
1552
  "CVE-2025-61882",
1547
1553
  "CVE-2025-61884",
@@ -1573,10 +1579,15 @@ const CISA_KEV_CATALOG = Object.freeze({
1573
1579
  "CVE-2025-9377",
1574
1580
  "CVE-2026-0257",
1575
1581
  "CVE-2026-0300",
1582
+ "CVE-2026-0322",
1576
1583
  "CVE-2026-0770",
1577
1584
  "CVE-2026-102489",
1578
1585
  "CVE-2026-102490",
1586
+ "CVE-2026-102764",
1579
1587
  "CVE-2026-104286",
1588
+ "CVE-2026-104935",
1589
+ "CVE-2026-105133",
1590
+ "CVE-2026-105134",
1580
1591
  "CVE-2026-10520",
1581
1592
  "CVE-2026-11645",
1582
1593
  "CVE-2026-12569",
@@ -1616,6 +1627,7 @@ const CISA_KEV_CATALOG = Object.freeze({
1616
1627
  "CVE-2026-21519",
1617
1628
  "CVE-2026-21525",
1618
1629
  "CVE-2026-21533",
1630
+ "CVE-2026-21589",
1619
1631
  "CVE-2026-21643",
1620
1632
  "CVE-2026-21962",
1621
1633
  "CVE-2026-22719",
@@ -1711,6 +1723,7 @@ const CISA_KEV_CATALOG = Object.freeze({
1711
1723
  "CVE-2026-76460",
1712
1724
  "CVE-2026-76461",
1713
1725
  "CVE-2026-76504",
1726
+ "CVE-2026-76519",
1714
1727
  "CVE-2026-8037",
1715
1728
  "CVE-2026-81578",
1716
1729
  "CVE-2026-81963",
@@ -1733,12 +1746,14 @@ const CISA_KEV_CATALOG = Object.freeze({
1733
1746
  "CVE-2026-87902",
1734
1747
  "CVE-2026-88771",
1735
1748
  "CVE-2026-88772",
1749
+ "CVE-2026-88779",
1736
1750
  "CVE-2026-9082",
1737
1751
  "CVE-2026-9198",
1738
1752
  "CVE-2026-93616",
1739
1753
  "CVE-2026-93952",
1740
1754
  "CVE-2026-94127",
1741
- "CVE-2026-9586"
1755
+ "CVE-2026-9586",
1756
+ "CVE-2026-96951"
1742
1757
  ]),
1743
1758
  });
1744
1759
  export { CISA_KEV_CATALOG };