@slicedearth/whoisleuth-cli 1.39.1 → 1.43.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 (66) hide show
  1. package/README.md +15 -0
  2. package/bin/whoisleuth.mjs +13 -2
  3. package/cli/archive-inspect.mjs +37 -2
  4. package/cli/arguments.mjs +361 -256
  5. package/cli/artifact-verify.mjs +35 -1
  6. package/cli/bulk-checkpoint.mjs +7 -11
  7. package/cli/bulk-command-runner.mjs +19 -2
  8. package/cli/case-pack.mjs +136 -0
  9. package/cli/ci-report.mjs +73 -0
  10. package/cli/collection-preflight.mjs +59 -0
  11. package/cli/command-reference.mjs +395 -0
  12. package/cli/completion.mjs +33 -10
  13. package/cli/config-profile.mjs +180 -0
  14. package/cli/ct-event-intake.mjs +159 -0
  15. package/cli/discovery-scan-command-runner.mjs +15 -1
  16. package/cli/domain-control-monitor.mjs +104 -0
  17. package/cli/domain-control-observations.mjs +235 -0
  18. package/cli/evidence-command-arguments.mjs +11 -0
  19. package/cli/evidence-command-runner.mjs +1 -0
  20. package/cli/external-observation-mapping.mjs +162 -0
  21. package/cli/fail-policy.mjs +64 -0
  22. package/cli/formatters/html.mjs +1 -1
  23. package/cli/investigation-manifest.mjs +113 -0
  24. package/cli/investigation-run.mjs +112 -0
  25. package/cli/lookup-brief.mjs +116 -0
  26. package/cli/lookup-command-runner.mjs +8 -1
  27. package/cli/lookup-diff.mjs +17 -19
  28. package/cli/network-command-runner.mjs +4 -1
  29. package/cli/offline-evidence-review.mjs +105 -8
  30. package/cli/open-asset-model-bridge.mjs +169 -0
  31. package/cli/registry-cohort.mjs +121 -0
  32. package/cli/registry-doctor.mjs +106 -11
  33. package/cli/registry-fixture-scaffold.mjs +73 -0
  34. package/cli/runner.mjs +368 -306
  35. package/docs/cli-reference.md +240 -14
  36. package/docs/cli.md +42 -7
  37. package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
  38. package/frontend/src/lib/analysis/brand-profile-model.js +46 -4
  39. package/frontend/src/lib/analysis/case-report.js +573 -0
  40. package/frontend/src/lib/analysis/case-response-model.js +30 -1
  41. package/frontend/src/lib/analysis/detection-rule-model.js +36 -0
  42. package/frontend/src/lib/analysis/external-findings-import.js +320 -0
  43. package/frontend/src/lib/analysis/lookup-decision-support.js +7 -0
  44. package/frontend/src/lib/analysis/lookup-presentation.js +10 -20
  45. package/frontend/src/lib/analysis/scoring.js +1 -1
  46. package/frontend/src/lib/analysis/visualization-models.js +2 -3
  47. package/frontend/src/lib/analysis/workspace-archive.js +2 -0
  48. package/lib/abusech-auth.mjs +1 -1
  49. package/lib/availability.mjs +1 -1
  50. package/lib/bounded-contract-normalizers.mjs +33 -0
  51. package/lib/dns-convergence-review.mjs +183 -0
  52. package/lib/dns-delegation-health.mjs +196 -3
  53. package/lib/domain-change-packet.mjs +133 -0
  54. package/lib/domain-change-review.mjs +307 -0
  55. package/lib/domain-control-flight-recorder.mjs +268 -0
  56. package/lib/domain-portfolio-review.mjs +151 -0
  57. package/lib/evidence-export.mjs +1 -1
  58. package/lib/html-signals.mjs +5 -11
  59. package/lib/nameserver-preflight-review.mjs +159 -0
  60. package/lib/page-language-signals.mjs +124 -0
  61. package/lib/risk-scoring.mjs +42 -1
  62. package/lib/static-html-analysis.mjs +29 -1
  63. package/lib/trust-store-comparison.mjs +146 -0
  64. package/lib/website-technology.mjs +24 -10
  65. package/lib/zone-intent-review.mjs +534 -0
  66. package/package.json +1 -1
@@ -69,6 +69,8 @@ node bin/whoisleuth.mts http example.com --json
69
69
  node bin/whoisleuth.mts tls example.com --json
70
70
  node bin/whoisleuth.mts registry-support example.uk --json
71
71
  node bin/whoisleuth.mts registry-doctor lookup.json --json
72
+ node bin/whoisleuth.mts registry-cohort saved-lookups.jsonl --json
73
+ node bin/whoisleuth.mts registry-scaffold --profile example-profile --suffix test --scenario registered
72
74
  node bin/whoisleuth.mts risk-calibrate calibration.json --json
73
75
  node bin/whoisleuth.mts lookalike-calibrate reviewed-candidates.json --json
74
76
  node bin/whoisleuth.mts verify-artifact workspace.json --json
@@ -78,16 +80,26 @@ node bin/whoisleuth.mts source-report lookup.json --json
78
80
  node bin/whoisleuth.mts inspect-archive workspace.json --json
79
81
  node bin/whoisleuth.mts inspect-archive workspace.json --search example.invalid --json
80
82
  node bin/whoisleuth.mts sign-artifact response-packet.json --private-key-file analyst-private.pem > response-packet.signed.json
83
+ node bin/whoisleuth.mts ct-intake certificate-events.json --json > external-findings.json
84
+ node bin/whoisleuth.mts map-observations observation-mapping.json --json > external-findings.json
85
+ node bin/whoisleuth.mts oam-export external-findings.json --json > asset-bridge.json
86
+ node bin/whoisleuth.mts manifest lookup.json comparison.json --workflow 'domain review' --json > investigation-manifest.json
81
87
  node bin/whoisleuth.mts verify-signature response-packet.signed.json --public-key-file analyst-public.pem
82
88
  node bin/whoisleuth.mts lookup example.com --deep --json > lookup.json
83
89
  node bin/whoisleuth.mts compare lookup.json --json
84
90
  node bin/whoisleuth.mts page-compare official.json candidate.json --json
85
91
  node bin/whoisleuth.mts mail-review bulk.json --json
86
92
  node bin/whoisleuth.mts review-evidence dnssec-evidence.json --json
93
+ node bin/whoisleuth.mts review-evidence trust-store-comparison.json --json
87
94
  node bin/whoisleuth.mts domain-control domain-control-input.json --json
88
95
  node bin/whoisleuth.mts assurance domain-assurance-input.json --json
96
+ node bin/whoisleuth.mts change-packet domain-change-packet-input.json --json
89
97
  node bin/whoisleuth.mts sharing-review response-packet.json --marking amber --recipient-scope organization --purpose 'Reviewed incident handoff' --human-reviewed --personal-data-reviewed --redactions-confirmed --json
90
98
  node bin/whoisleuth.mts workflow-plan domain-triage example.test --json
99
+ node bin/whoisleuth.mts workflow-run domain-triage example.test --approve-network --json --output run.json
100
+ node bin/whoisleuth.mts brief lookup.json
101
+ node bin/whoisleuth.mts case-pack cases.json --audience trusted --reviewed --json
102
+ node bin/whoisleuth.mts monitor-once control-manifest.json --previous prior-control.json --junit
91
103
  node bin/whoisleuth.mts diff first-lookup.json second-lookup.json --json
92
104
  node bin/whoisleuth.mts reconcile office.json mobile.json external.json --json
93
105
  node bin/whoisleuth.mts timeline first-observation.json second-observation.json latest-observation.json --json
@@ -193,9 +205,11 @@ Commands that query RDAP, WHOIS, DNS, HTTP, TLS, or Certificate Transparency do
193
205
  so directly from the machine running the CLI. They do not use the hosted login,
194
206
  hosted session, or deployment usage controls; upstream providers can see and
195
207
  rate-limit the local machine's network address. Offline `discover`, `compare`,
196
- `page-compare`, `mail-review`, `review-evidence`, `domain-control`, `assurance`,
197
- `sharing-review`, `workflow-plan`, `diff`, `reconcile`, `timeline`,
198
- `risk-calibrate`, `lookalike-calibrate`, `registry-doctor`, `verify-artifact`, `source-report`, `export`,
208
+ `page-compare`, `mail-review`, `review-evidence`, `domain-control`, `assurance`, `change-packet`,
209
+ `sharing-review`, `workflow-plan`, `brief`, `case-pack`, `registry-cohort`,
210
+ `registry-scaffold`, `diff`, `reconcile`, `timeline`,
211
+ `risk-calibrate`, `lookalike-calibrate`, `registry-doctor`, `verify-artifact`, `source-report`,
212
+ `map-observations`, `oam-export`, `export`,
199
213
  `commands`, `completion`, and `manual` operations make no network requests. Commands write
200
214
  to stdout unless the analyst deliberately selects a local output file.
201
215
 
@@ -218,6 +232,23 @@ An existing destination is refused. `--force` is valid only with `--output` and
218
232
  allows an intentional atomic replacement. A failed or cancelled command does
219
233
  not publish a partial output file. Output is capped at 32 MiB.
220
234
 
235
+ `--config <file> --profile <name>` reads a strict version-1 local configuration
236
+ document of at most 64 KiB. Without `--config`, the path is
237
+ `$XDG_CONFIG_HOME/whoisleuth/config.json`. A profile may contain no more than
238
+ 16 safe defaults drawn from `--no-color`, `--summary`, `--verbose`, `--fast`,
239
+ `--concurrency`, `--observer`, and `--vantage`. It cannot supply a target,
240
+ Deep mode, output path, network approval, failure policy, credential, or
241
+ arbitrary argument. Explicit command options override profile defaults from
242
+ the same group.
243
+
244
+ Lookup, Bulk, and `monitor-once` can emit bounded JUnit XML. Owned-domain
245
+ Posture can emit SARIF only when `--owned-domain` confirms the passive target
246
+ boundary. The reports contain categorical source or posture findings rather
247
+ than raw evidence. Supported network commands accept an explicit comma-
248
+ separated `--fail-on` policy from `source-failure`, `inconclusive`, `danger`,
249
+ and `material-drift`; a match returns the partial-result exit code after the
250
+ normal output is emitted.
251
+
221
252
  `lookup --markdown` and `lookup --html` build the existing normalised evidence
222
253
  report directly after one completed domain lookup. They do not start a second
223
254
  collection. These report formats reject IP and ASN inputs before collection;
@@ -363,12 +394,12 @@ machine access is not evidence that a domain is unregistered or safe.
363
394
  | 70 | Unexpected CLI bootstrap failure. |
364
395
  | 130 | The analyst cancelled the command. No partial final result was emitted. |
365
396
 
366
- This release supports `lookup`, `bulk`, `ct-search`, `discover`, `discover-scan`, `posture`,
367
- `http`, `tls`, `registry-support`, `registry-doctor`, `risk-calibrate`,
397
+ This release supports `lookup`, `bulk`, `ct-search`, `ct-intake`, `map-observations`, `oam-export`, `discover`, `discover-scan`, `posture`,
398
+ `http`, `tls`, `registry-support`, `registry-doctor`, `registry-cohort`, `registry-scaffold`, `risk-calibrate`,
368
399
  `lookalike-calibrate`, `verify-artifact`,
369
- `inspect-archive`, `sign-artifact`, `verify-signature`, `source-report`,
370
- `compare`, `page-compare`, `mail-review`, `review-evidence`, `domain-control`,
371
- `assurance`, `sharing-review`, `workflow-plan`, `diff`, `reconcile`, `timeline`,
400
+ `inspect-archive`, `manifest`, `sign-artifact`, `verify-signature`, `source-report`,
401
+ `compare`, `page-compare`, `mail-review`, `review-evidence`, `brief`, `case-pack`, `domain-control`,
402
+ `monitor-once`, `assurance`, `change-packet`, `sharing-review`, `workflow-plan`, `workflow-run`, `diff`, `reconcile`, `timeline`,
372
403
  `export`, `doctor`, `commands`, `completion`, and `manual`. Additional command families
373
404
  are added as separate bounded increments rather than exposing incomplete
374
405
  aliases.
@@ -395,10 +426,24 @@ ownership, safety, or maliciousness.
395
426
  `registry-doctor [lookup.json]` compares one saved domain Lookup with the same
396
427
  reviewed local capability profile. It reports whether RDAP and WHOIS collection
397
428
  states align with allowed, permission-required, or unsupported access, counts
398
- bounded normalised publication fields, and reports whether a registry object
399
- identifier was observed. A missing identifier is a publication omission, not
400
- a failed lookup. The command is offline, makes no retry, and cannot establish
401
- current registry reachability.
429
+ bounded normalised publication fields, and reports publication-quality context
430
+ for object identity, base conformance, redaction metadata, required self links,
431
+ and event consistency. A missing identifier or metadata field is a publication
432
+ omission, not a failed lookup. The command is offline, makes no retry, and
433
+ cannot establish current registry reachability.
434
+
435
+ `registry-cohort [lookups.json|lookups.jsonl]` aggregates from 1 to 500 saved
436
+ Lookups into suffix and capability-profile cohorts. It retains only target-free
437
+ counts for source alignment and publication-quality states. A cohort with
438
+ fewer than five samples is labelled insufficient rather than treated as a
439
+ quality result, and repeated observations from one environment are not assumed
440
+ representative.
441
+
442
+ `registry-scaffold --profile <id> --suffix <suffix> --scenario <state>` emits
443
+ one bounded, sanitised synthetic fixture scaffold from the installed registry
444
+ catalogue. The three supported states are `registered`, `not_found`, and
445
+ `inconclusive`. It does not fetch a live response, include a real target, or
446
+ alter the embedded catalogue.
402
447
 
403
448
  The offline RDAP supplied-evidence review implements the request shape defined
404
449
  by RFC 9536 for `resources`, `domains`, `nameservers`, and `entities`. It also
@@ -513,7 +558,9 @@ establish that a current candidate is malicious or safe.
513
558
  evidence contents. Input is capped at 15 MiB. It currently recognises ordinary
514
559
  and encrypted workspace archives, case-response packets, acquisition-decision
515
560
  exports, domain-comparison exports, Bulk mail-exposure exports, and Bulk review
516
- manifests. It also verifies the internal digests of a complete investigation
561
+ manifests. Reviewed CLI case packs are checked against their canonical digest,
562
+ bounded browser-importable case collection, audience, and review marker. The
563
+ command also verifies the internal digests of a complete investigation
517
564
  capsule, including its evidence, brief, graph, and optional analyst-record
518
565
  projections, and that capsule can be passed to `sign-artifact`. It also validates the bounded versioned structure of saved CLI
519
566
  Lookup JSON. Because saved Lookup documents do not embed a checksum or
@@ -573,6 +620,17 @@ DNS-safe ASCII form before exact comparison. `--require-match` makes a
573
620
  no-match result fail for automation and is valid only with `--search`.
574
621
  Traversal, input size, matches, depth, arrays, and strings are bounded.
575
622
 
623
+ Version 2 of the inspection report adds a content digest over the ordered
624
+ section identifiers, schema versions, record counts, and normalised section
625
+ content. It deliberately excludes JSON formatting and each section envelope's
626
+ export time, so two separately exported archives with the same retained
627
+ workspace content have the same identity. `--expect-content-digest <sha256:digest>`
628
+ fails when that identity differs, which supports deliberate transfer checks
629
+ without printing records. Matching digests detect equivalent retained content;
630
+ they do not authenticate who created it or establish that its evidence is
631
+ accurate or current. The report separately identifies the source archive
632
+ version and the installed reader version.
633
+
576
634
  ## Optional evidence-package signing
577
635
 
578
636
  `sign-artifact` signs a locally verified case-response packet or supported
@@ -592,6 +650,23 @@ verification establishes only that the package is internally self-consistent.
592
650
  WHOISleuth does not generate, store, recover, rotate, publish, or establish
593
651
  trust in signing keys.
594
652
 
653
+ ## Reproducible investigation manifest
654
+
655
+ `manifest <artefact.json> [...] --workflow <label>` reads from 1 to 16 ordered
656
+ JSON artefacts and emits `whoisleuth.investigation-manifest` version 1. Each
657
+ entry records its sequence, bounded schema and version metadata, exact UTF-8
658
+ content digest, canonical JSON digest, and byte count. The manifest also records
659
+ the installed CLI version and can include an externally calculated
660
+ `--configuration-digest sha256:...` without copying configuration values.
661
+
662
+ Source paths, filenames, command-line arguments, and artefact contents are not
663
+ retained. Individual artefacts are capped at 15 MiB and the ordered set at
664
+ 32 MiB. The manifest carries its own canonical integrity digest, works with
665
+ `verify-artifact`, and can be passed to `sign-artifact`. Reproduction still
666
+ requires the exact digested artefacts, compatible source services, and any
667
+ separately held configuration; matching digests do not establish observation
668
+ accuracy or freshness.
669
+
595
670
  ## Bulk lookup
596
671
 
597
672
  `bulk` accepts a newline-delimited file or stdin. It preserves input order,
@@ -642,6 +717,56 @@ the complete bounded structured result in the versioned
642
717
  separately from the registrable-domain result cap. CT observations do not prove
643
718
  that a website is active or malicious.
644
719
 
720
+ ## Local certificate event intake
721
+
722
+ `ct-intake [events.json]` reads `whoisleuth.ct-event-batch` version 1 entirely
723
+ offline. Each source-qualified event supplies a log identifier, observation
724
+ time, certificate SHA-256 digest, one or more DNS names, completeness, and
725
+ optional issuer, expiry, and limitations. Unknown fields, malformed names,
726
+ unsupported completeness values, and oversized batches are rejected.
727
+
728
+ JSON output uses the existing `whoisleuth.external-findings` version 3 contract
729
+ and can be deliberately imported into the browser Console. Wildcard names are
730
+ normalised to their base DNS name, exact event/domain duplicates are removed,
731
+ and deterministic output is capped at 100 findings, 25 domains, and 20 findings
732
+ per domain to match the browser import boundary. Omission is stated in each
733
+ retained finding when the supplied batch exceeds those limits. The command
734
+ makes no request and treats every certificate event as a review lead, not proof
735
+ that the certificate was served, requested, or controlled by the named domain.
736
+
737
+ ## Declarative observation interchange
738
+
739
+ `map-observations [mapping.json]` reads
740
+ `whoisleuth.external-observation-mapping` version 1 and applies one strict,
741
+ declarative profile to at most 500 local records. The profile selects bounded
742
+ dotted field paths for the domain, summary, observation time, optional
743
+ reference, and optional completeness. It fixes one source, evidence class, and
744
+ finding category for the batch. Profiles cannot contain scripts, templates,
745
+ computed expressions, network endpoints, or executable callbacks. Prototype
746
+ paths and unknown fields are rejected.
747
+
748
+ The resulting JSON is the existing browser-compatible
749
+ `whoisleuth.external-findings` version 3 contract. It retains source identity,
750
+ observation time, completeness, and limitations, while stating that field
751
+ semantics were not independently verified. Deterministic output is capped at
752
+ 100 findings, 25 domains, and 20 findings per domain. The command makes no
753
+ request and never interprets an omitted field as a negative observation.
754
+
755
+ `oam-export [external-findings.json]` projects that strict findings document
756
+ into `whoisleuth.open-asset-model-bridge` version 1. It uses the documented
757
+ [OWASP Open Asset Model](https://github.com/owasp-amass/open-asset-model)
758
+ vocabulary for bounded `FQDN`, `IPAddress`, and `TLSCertificate` assets plus
759
+ `dns_record` and `san_dns_name` relations. Only typed A, AAAA, and SHA-256
760
+ certificate observations create infrastructure assets or relations. Other
761
+ findings remain represented by their FQDN asset.
762
+
763
+ This bridge wrapper is not a claim that every receiving implementation accepts
764
+ one universal native serialisation. Receivers must validate the projection
765
+ against their supported model version. WHOISleuth preserves source and
766
+ completeness as discovery metadata, does not invent a confidence score, and
767
+ does not treat an edge as proof of ownership, control, coordination, intent,
768
+ safety, or maliciousness.
769
+
645
770
  ## Lookalike discovery
646
771
 
647
772
  `discover` runs the same pure, bounded lookalike generator as the Discover
@@ -907,7 +1032,10 @@ Supported input schemas are:
907
1032
  The command does not retrieve the certificate or negotiate STARTTLS.
908
1033
  - `whoisleuth.rdap-search-input`: normalises a supplied RDAP search-help
909
1034
  response and prepares an exact supported reverse-search request without
910
- sending it.
1035
+ sending it. If the document also contains a previously obtained `response`,
1036
+ the command checks its bounded property mappings against the reviewed
1037
+ registration and reports omitted or unrecognised mappings without treating
1038
+ the response as authoritative or complete.
911
1039
  - `whoisleuth.rpki-route-input`: compares a route prefix and origin ASN with a
912
1040
  bounded analyst-supplied VRP set.
913
1041
  - `whoisleuth.local-geoip-query`: queries an analyst-supplied bounded prefix
@@ -924,12 +1052,57 @@ Supported input schemas are:
924
1052
  - `whoisleuth.encrypted-dns-plan-input`: validates an explicitly reviewed
925
1053
  encrypted-DNS provider contract and prepares a bounded query plan. It does
926
1054
  not execute the plan.
1055
+ - `whoisleuth.zone-intent.input`: parses either a deliberately supported,
1056
+ bounded BIND master-file subset or normalised DNS records and compares it
1057
+ with one separately attributed observation. TXT values are reduced to
1058
+ SHA-256 digests, unsupported directives and records remain rejected, and an
1059
+ incomplete observation never becomes a missing-record conclusion. The
1060
+ review does not contact a resolver or apply a DNS change.
1061
+ - `whoisleuth.domain-portfolio.input`: reviews up to 500 analyst-supplied
1062
+ domain inventory rows for exact registrar, DNS, mail, certificate and
1063
+ recovery concentration, renewal-review gaps and internal recovery-domain
1064
+ dependencies. Account labels and provider assignments remain analyst
1065
+ assertions, and the review makes no provider or account request.
1066
+ - `whoisleuth.domain-change.input`: compares bounded, separately labelled
1067
+ authority and resolver observations; reviews CDS, CDNSKEY and CSYNC
1068
+ publication consistency; inventories supplied SRV services and ACME
1069
+ dependencies; and records TLS key continuity, stapling, embedded timestamp,
1070
+ and HSTS preload context. TXT values are retained only as SHA-256 digests.
1071
+ The result is an offline review gate, not a DNSSEC validation, preload-list
1072
+ lookup, certificate-authority request, or permission to apply a change.
1073
+ - `whoisleuth.dns-convergence.input`: compares the latest supplied observation
1074
+ from at least two labelled resolvers or network vantages. It preserves
1075
+ converged, divergent, unexpected, incomplete, and insufficient record-set
1076
+ states, can compare against optional expected values, and projects a simple
1077
+ observation-time plus TTL cache horizon. It does not query DNS or decide
1078
+ which divergent value is authoritative, and supplied TTLs do not reveal
1079
+ actual cache age or resolver refresh policy.
1080
+ - `whoisleuth.nameserver-preflight.input`: reviews an intended nameserver set
1081
+ against separately attributed direct-service observations before a
1082
+ delegation change. It checks authoritative service, the served NS set, SOA
1083
+ evidence, public addresses and in-bailiwick glue readiness without querying
1084
+ DNS, opening a socket or changing registry configuration.
1085
+ - `whoisleuth.trust-store-comparison.input`: compares an already-observed,
1086
+ bounded certificate-chain SHA-256 fingerprint set with up to 16
1087
+ analyst-supplied trust-store snapshots. Each snapshot carries its own name,
1088
+ version, source, review time, completeness, and no more than 2,048 anchor
1089
+ fingerprints. Exact intersections, complete non-observations, and
1090
+ inconclusive inputs remain separate. A match does not build or validate a
1091
+ certificate path, and non-observation does not mean a client would reject
1092
+ the chain because served chains commonly omit their root.
927
1093
 
928
1094
  The common output is `whoisleuth.cli.offline-evidence-review` version 1. It
929
1095
  retains the nested result's explicit state and limitations. A locally
930
1096
  consistent relationship is not converted into a claim about current
931
1097
  publication, ownership, safety, or maliciousness.
932
1098
 
1099
+ `review-evidence --strict-exit` is an opt-in local automation boundary. It
1100
+ returns the partial-failure exit code when an input family exposes an explicit
1101
+ failed `gate`, or when a zone-intent comparison is incomplete or contains a
1102
+ difference, missing value or unexpected value. Other evidence families retain
1103
+ their ordinary exit behaviour; the flag does not invent a pass/fail policy for
1104
+ them. JSON is still emitted before the exit status is selected.
1105
+
933
1106
  ## Domain control manifests
934
1107
 
935
1108
  `domain-control [input.json]` builds or reviews a bounded desired-state
@@ -981,6 +1154,22 @@ The versioned input rejects unknown fields and contradictory evidence attached
981
1154
  to an unfinished check. It stores no credentials, makes no request, and changes
982
1155
  no registrar, DNS, mail, certificate, or recovery configuration.
983
1156
 
1157
+ ## Domain change assurance packet
1158
+
1159
+ `change-packet [input.json]` combines one pre-change domain review, one
1160
+ post-change domain review, and one planned-change assurance input for the same
1161
+ domain. The input uses `whoisleuth.domain-change-packet.input` version 1. The
1162
+ command revalidates each nested contract, summarises changed authoritative
1163
+ record sets, and emits `whoisleuth.domain-change-packet` version 1 with a
1164
+ canonical SHA-256 integrity digest.
1165
+
1166
+ The packet is ready only when both supplied observation reviews and the change
1167
+ plan pass their own bounded gates. Partial or unavailable evidence remains a
1168
+ review reason. Packet assembly performs no collection or configuration change,
1169
+ and digest validity does not authenticate the analyst. A reviewed packet can be
1170
+ passed to `sign-artifact` when the operator has an independently managed
1171
+ Ed25519 signing-key lifecycle.
1172
+
984
1173
  ## Pre-sharing review
985
1174
 
986
1175
  `sharing-review [artifact.json]` performs a redacted local preflight before an
@@ -1012,6 +1201,43 @@ change a case, or submit evidence. Analysts deliberately run selected commands
1012
1201
  after reviewing their collection boundaries. This provides repeatable
1013
1202
  domain-specific workflows without an arbitrary automation or plugin surface.
1014
1203
 
1204
+ ## Local handoffs and one-shot monitoring
1205
+
1206
+ `brief [lookup.json]` reads one saved Lookup and emits version 2 of the bounded
1207
+ `whoisleuth.cli.lookup-brief` contract. It separates retained facts,
1208
+ contradictions, incomplete source states, and manual actions. Each action
1209
+ states its evidence basis and expected outcome; it does not run the action or
1210
+ create a case.
1211
+
1212
+ `case-pack [cases.json] --audience <internal|trusted|public> --reviewed` reads a
1213
+ browser case export containing at most 25 normalised cases, applies the selected
1214
+ redaction boundary, and emits a browser-importable case collection with a
1215
+ canonical SHA-256 digest and redaction manifest. It refuses an invalid,
1216
+ duplicate, larger, or generated package above the browser's 2 MiB import limit
1217
+ rather than silently omitting records or evidence. Trusted output removes notes,
1218
+ recipient values, and manual-pivot targets. Public output additionally removes
1219
+ actions and analyst assertions. The review flag records an explicit choice; it
1220
+ does not prove recipient authorisation or factual correctness.
1221
+
1222
+ `verify-artifact` recognises the complete case-pack envelope before browser
1223
+ import. It checks the canonical digest, supported case-export schema, bounded
1224
+ case count, report count, audience and explicit review marker without printing
1225
+ case contents. This detects a changed hand-off file but does not authenticate
1226
+ the analyst, recipient, source observations, or review decision.
1227
+
1228
+ `monitor-once [manifest.json]` performs one bounded Deep control review for at
1229
+ most 20 manifest domains with concurrency capped at three. An optional
1230
+ `--previous` checkpoint adds local first/last-observed comparison. It is not a
1231
+ daemon or scheduler, retains compact normalised observations rather than raw
1232
+ payloads, and keeps failed collection incomplete. JSON, JUnit, and explicit
1233
+ failure policies are supported.
1234
+
1235
+ `workflow-run <recipe> <subject>` executes only concrete steps from the four
1236
+ installed fixed recipes. Network steps require `--approve-network` for that
1237
+ invocation. Analyst-selection placeholders always pause. A bounded `--resume`
1238
+ state must match the exact recipe and subject, cannot add commands or shell
1239
+ syntax, and is not evidence that earlier results remain current.
1240
+
1015
1241
  ## Optional local rendered capture
1016
1242
 
1017
1243
  `packages/web-capture` is a private repo-local Playwright package, not part of
package/docs/cli.md CHANGED
@@ -67,6 +67,8 @@ whoisleuth bulk domains.txt --csv
67
67
  whoisleuth discover example.com --preset common --jsonl
68
68
  whoisleuth verify-artifact lookup.json --json
69
69
  whoisleuth compare lookup.json --json
70
+ whoisleuth brief lookup.json
71
+ whoisleuth registry-doctor lookup.json --json
70
72
  ```
71
73
 
72
74
  Only one query is accepted by `lookup`; multi-input processing belongs to
@@ -105,13 +107,13 @@ HTTP status, and a bounded explanation.
105
107
  | Goal | Commands |
106
108
  | --- | --- |
107
109
  | Investigate a target | `lookup`, `http`, `tls`, `posture`, `registry-support` |
108
- | Review many targets | `bulk`, `discover`, `discover-scan`, `ct-search` |
110
+ | Review many targets | `bulk`, `discover`, `discover-scan`, `ct-search`, `ct-intake` |
109
111
  | Compare saved evidence | `compare`, `page-compare`, `diff`, `reconcile`, `timeline`, `mail-review` |
110
- | Review supplied evidence | `review-evidence`, `registry-doctor`, `source-report`, `sharing-review` |
111
- | Plan and assure changes | `domain-control`, `assurance`, `workflow-plan` |
112
- | Verify and package evidence | `verify-artifact`, `inspect-archive`, `sign-artifact`, `verify-signature`, `export` |
112
+ | Review supplied evidence | `brief`, `review-evidence`, `registry-doctor`, `registry-cohort`, `source-report`, `sharing-review`, `map-observations` |
113
+ | Plan and assure changes | `domain-control`, `monitor-once`, `assurance`, `change-packet`, `workflow-plan`, `workflow-run` |
114
+ | Verify and package evidence | `case-pack`, `manifest`, `oam-export`, `verify-artifact`, `inspect-archive`, `sign-artifact`, `verify-signature`, `export` |
113
115
  | Calibrate offline | `risk-calibrate`, `lookalike-calibrate` |
114
- | Operate the CLI | `doctor`, `commands`, `completion`, `manual` |
116
+ | Operate the CLI | `doctor`, `registry-scaffold`, `commands`, `completion`, `manual` |
115
117
 
116
118
  Use focused help for the exact arguments and collection ceiling:
117
119
 
@@ -137,6 +139,18 @@ explicit in text, so colour is never the only distinction.
137
139
  lifecycle events to stderr.
138
140
  - `--strict-exit` treats failures in explicitly requested source families as a
139
141
  partial failure while keeping skipped or unsupported sources neutral.
142
+ - `--fail-on` applies explicit `source-failure`, `inconclusive`, `danger`, or
143
+ `material-drift` automation policy where the selected command supports it.
144
+ - `--junit` emits bounded CI test cases for Lookup, Bulk, and one-shot control
145
+ reviews; owned-domain Posture can emit `--sarif` only with
146
+ `--owned-domain`.
147
+
148
+ `--config <file> --profile <name>` applies a strict version-1 local profile.
149
+ The default path is `$XDG_CONFIG_HOME/whoisleuth/config.json`. Profiles can set
150
+ only presentation mode, Fast collection, bounded concurrency, and observer or
151
+ vantage labels. They cannot add targets, enable Deep collection, select output
152
+ paths, approve network work, or set failure policy. Explicit command options
153
+ override defaults from the same option group.
140
154
 
141
155
  Every command accepts `--output <file>`. WHOISleuth buffers bounded output,
142
156
  creates a private temporary file beside the destination, syncs it, and publishes
@@ -182,12 +196,17 @@ full contracts into the packaged reference.
182
196
 
183
197
  ### Workspace archive inspection
184
198
 
185
- [Read the redacted archive summary and search contract.](cli-reference.md#workspace-archive-inspection)
199
+ [Read the redacted archive summary, stable content-digest, transfer-check, and
200
+ search contract.](cli-reference.md#workspace-archive-inspection)
186
201
 
187
202
  ### Optional evidence-package signing
188
203
 
189
204
  [Read the local signing and verification contract.](cli-reference.md#optional-evidence-package-signing)
190
205
 
206
+ ### Reproducible investigation manifest
207
+
208
+ [Read the ordered, path-free artefact digest contract.](cli-reference.md#reproducible-investigation-manifest)
209
+
191
210
  ### Bulk lookup
192
211
 
193
212
  [Read Bulk bounds, filters, formats, checkpoints, and cancellation behaviour.](cli-reference.md#bulk-lookup)
@@ -196,6 +215,14 @@ full contracts into the packaged reference.
196
215
 
197
216
  [Read the CT query and source-limit contract.](cli-reference.md#certificate-transparency-search)
198
217
 
218
+ ### Local certificate event intake
219
+
220
+ [Read the offline, source-qualified event import contract.](cli-reference.md#local-certificate-event-intake)
221
+
222
+ ### Declarative observation interchange
223
+
224
+ [Read the bounded field-mapping and Open Asset Model bridge contracts.](cli-reference.md#declarative-observation-interchange)
225
+
199
226
  ### Lookalike discovery
200
227
 
201
228
  [Read candidate generation, dictionaries, snapshots, and output behaviour.](cli-reference.md#lookalike-discovery)
@@ -230,7 +257,7 @@ full contracts into the packaged reference.
230
257
 
231
258
  ### Offline supplied-evidence review
232
259
 
233
- [Read the DNSSEC, DANE, and supplied-observation review contract.](cli-reference.md#offline-supplied-evidence-review)
260
+ [Read the DNSSEC, DANE, zone-intent, portfolio, domain-change, and supplied-observation review contract.](cli-reference.md#offline-supplied-evidence-review)
234
261
 
235
262
  ### Domain control manifests
236
263
 
@@ -240,6 +267,10 @@ full contracts into the packaged reference.
240
267
 
241
268
  [Read planned-change, recovery-dependency, and retirement contracts.](cli-reference.md#domain-assurance-review)
242
269
 
270
+ ### Domain change assurance packet
271
+
272
+ [Read the bounded pre-change, post-change, planning, and integrity contract.](cli-reference.md#domain-change-assurance-packet)
273
+
243
274
  ### Pre-sharing review
244
275
 
245
276
  [Read the local marking, scope, redaction, and integrity preflight.](cli-reference.md#pre-sharing-review)
@@ -248,6 +279,10 @@ full contracts into the packaged reference.
248
279
 
249
280
  [Read the plan-only recipe contract and approval gates.](cli-reference.md#fixed-investigation-plans)
250
281
 
282
+ ### Local handoffs and one-shot monitoring
283
+
284
+ [Read brief, case-package, fixed-recipe execution, and one-shot control-review contracts.](cli-reference.md#local-handoffs-and-one-shot-monitoring)
285
+
251
286
  ### Optional local rendered capture
252
287
 
253
288
  [Read the separate Playwright capture package boundary.](cli-reference.md#optional-local-rendered-capture)
@@ -383,7 +383,7 @@ export function buildAcquisitionDueDiligence(input) {
383
383
  policyChecks,
384
384
  nextSteps,
385
385
  limitations: [
386
- 'This workspace organizes observed evidence; it does not value a domain or establish that it can be acquired.',
386
+ 'This workspace organises observed evidence; it does not value a domain or establish that it can be acquired.',
387
387
  'Expiry dates, EPP statuses, sale signals, and contact routes can change after collection.',
388
388
  'WHOISleuth does not assess legal rights, registry eligibility, registrar terms, escrow, tax, or payment risk.',
389
389
  ],
@@ -5,7 +5,7 @@ import { normalizeDomain } from "./case-model.js";
5
5
  import { normalizePageBaseline } from "./page-baseline.js";
6
6
  import { isInformativeFaviconHash } from "./utils.js";
7
7
  export const BRAND_PROFILE_SCHEMA = 'whoisleuth.brand-profiles';
8
- export const BRAND_PROFILE_SCHEMA_VERSION = 5;
8
+ export const BRAND_PROFILE_SCHEMA_VERSION = 6;
9
9
  export const MAX_PROFILES = 100;
10
10
  export const MAX_PROFILE_VALUES = 200;
11
11
  export const MAX_PROFILE_VALUE_INPUTS = MAX_PROFILE_VALUES * 4;
@@ -22,6 +22,8 @@ export const MAX_PROTECTION_ATTESTATIONS = 6;
22
22
  export const MAX_DESIRED_POSTURE_BASELINES = 20;
23
23
  export const MAX_DESIRED_POSTURE_RECORDS = 32;
24
24
  export const MAX_DESIRED_POSTURE_SUPPRESSIONS = 12;
25
+ export const MAX_DESIRED_POSTURE_OBSERVATIONS = 12;
26
+ export const MAX_DESIRED_POSTURE_CHANGE_WINDOWS = 8;
25
27
  const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
26
28
  const SHA256_RE = /^[a-f0-9]{64}$/i;
27
29
  const CONTROL_RE = /[\x00-\x1f\x7f]/;
@@ -203,6 +205,35 @@ function normalizeDesiredPostureObservation(value) {
203
205
  }
204
206
  return checks.length ? { observedAt, checks } : null;
205
207
  }
208
+ function normalizeDesiredPostureObservationHistory(value, previous) {
209
+ const candidates = Array.isArray(value) ? value : previous ? [previous] : [];
210
+ const byTime = new Map();
211
+ for (const item of candidates.slice(0, MAX_DESIRED_POSTURE_OBSERVATIONS * 4)) {
212
+ const normalized = normalizeDesiredPostureObservation(item);
213
+ if (normalized)
214
+ byTime.set(normalized.observedAt, normalized);
215
+ }
216
+ return [...byTime.values()]
217
+ .sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt))
218
+ .slice(-MAX_DESIRED_POSTURE_OBSERVATIONS);
219
+ }
220
+ function normalizeDesiredPostureChangeWindows(value) {
221
+ if (!Array.isArray(value))
222
+ return [];
223
+ const output = [];
224
+ for (const item of value.slice(0, MAX_DESIRED_POSTURE_CHANGE_WINDOWS * 4)) {
225
+ const candidate = record(item);
226
+ const startsAt = timestamp(candidate.startsAt, null);
227
+ const endsAt = timestamp(candidate.endsAt, null);
228
+ const summary = boundedText(candidate.summary, 300);
229
+ if (!startsAt || !endsAt || Date.parse(endsAt) <= Date.parse(startsAt) || !summary)
230
+ continue;
231
+ output.push({ startsAt, endsAt, summary });
232
+ if (output.length >= MAX_DESIRED_POSTURE_CHANGE_WINDOWS)
233
+ break;
234
+ }
235
+ return output.sort((left, right) => Date.parse(left.startsAt) - Date.parse(right.startsAt));
236
+ }
206
237
  function normalizeTlsSanPatterns(value) {
207
238
  if (!Array.isArray(value))
208
239
  return [];
@@ -250,6 +281,8 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
250
281
  break;
251
282
  }
252
283
  seen.add(domain);
284
+ const previousObservation = normalizeDesiredPostureObservation(candidate.previousObservation);
285
+ const observationHistory = normalizeDesiredPostureObservationHistory(candidate.observationHistory, previousObservation);
253
286
  output.push({
254
287
  version: 1,
255
288
  domain,
@@ -266,9 +299,18 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
266
299
  ? candidate.registrarLock
267
300
  : 'unconfigured',
268
301
  renewalReviewAt: timestamp(candidate.renewalReviewAt, null),
302
+ zoneIntent: ['active_service', 'defensive_registration', 'no_service', 'parked', 'redirect_only'].includes(String(candidate.zoneIntent))
303
+ ? candidate.zoneIntent
304
+ : 'unconfigured',
305
+ lifecycle: ['active', 'change_planned', 'retiring', 'retired'].includes(String(candidate.lifecycle))
306
+ ? candidate.lifecycle
307
+ : 'active',
308
+ recoveryDependency: boundedText(candidate.recoveryDependency, 200),
309
+ approvedChangeWindows: normalizeDesiredPostureChangeWindows(candidate.approvedChangeWindows),
269
310
  suppressions,
270
311
  note: boundedText(candidate.note, MAX_PROFILE_TEXT_LENGTH),
271
- previousObservation: normalizeDesiredPostureObservation(candidate.previousObservation),
312
+ previousObservation: observationHistory.at(-1) ?? previousObservation,
313
+ observationHistory,
272
314
  updatedAt: timestamp(candidate.updatedAt, fallback),
273
315
  });
274
316
  if (output.length >= MAX_DESIRED_POSTURE_BASELINES)
@@ -376,8 +418,8 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
376
418
  if (importedVersion !== null && importedVersion > BRAND_PROFILE_SCHEMA_VERSION) {
377
419
  throw new Error(`This Brand Profile file uses newer schema ${importedVersion}. Update the app before importing it.`);
378
420
  }
379
- if (![2, 3, 4, BRAND_PROFILE_SCHEMA_VERSION].includes(importedVersion ?? 0)) {
380
- throw new Error(`Expected a WHOISleuth Brand Profile export using schema 2, 3, 4, or ${BRAND_PROFILE_SCHEMA_VERSION}.`);
421
+ if (![2, 3, 4, 5, BRAND_PROFILE_SCHEMA_VERSION].includes(importedVersion ?? 0)) {
422
+ throw new Error(`Expected a WHOISleuth Brand Profile export using schema 2, 3, 4, 5, or ${BRAND_PROFILE_SCHEMA_VERSION}.`);
381
423
  }
382
424
  const local = normalizeBrandProfileStore(localRaw).profiles;
383
425
  const byName = new Map(local.map((profile) => [profile.name.toLowerCase(), profile]));