@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.
- package/README.md +15 -0
- package/bin/whoisleuth.mjs +13 -2
- package/cli/archive-inspect.mjs +37 -2
- package/cli/arguments.mjs +361 -256
- package/cli/artifact-verify.mjs +35 -1
- package/cli/bulk-checkpoint.mjs +7 -11
- package/cli/bulk-command-runner.mjs +19 -2
- package/cli/case-pack.mjs +136 -0
- package/cli/ci-report.mjs +73 -0
- package/cli/collection-preflight.mjs +59 -0
- package/cli/command-reference.mjs +395 -0
- package/cli/completion.mjs +33 -10
- package/cli/config-profile.mjs +180 -0
- package/cli/ct-event-intake.mjs +159 -0
- package/cli/discovery-scan-command-runner.mjs +15 -1
- package/cli/domain-control-monitor.mjs +104 -0
- package/cli/domain-control-observations.mjs +235 -0
- package/cli/evidence-command-arguments.mjs +11 -0
- package/cli/evidence-command-runner.mjs +1 -0
- package/cli/external-observation-mapping.mjs +162 -0
- package/cli/fail-policy.mjs +64 -0
- package/cli/formatters/html.mjs +1 -1
- package/cli/investigation-manifest.mjs +113 -0
- package/cli/investigation-run.mjs +112 -0
- package/cli/lookup-brief.mjs +116 -0
- package/cli/lookup-command-runner.mjs +8 -1
- package/cli/lookup-diff.mjs +17 -19
- package/cli/network-command-runner.mjs +4 -1
- package/cli/offline-evidence-review.mjs +105 -8
- package/cli/open-asset-model-bridge.mjs +169 -0
- package/cli/registry-cohort.mjs +121 -0
- package/cli/registry-doctor.mjs +106 -11
- package/cli/registry-fixture-scaffold.mjs +73 -0
- package/cli/runner.mjs +368 -306
- package/docs/cli-reference.md +240 -14
- package/docs/cli.md +42 -7
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
- package/frontend/src/lib/analysis/brand-profile-model.js +46 -4
- package/frontend/src/lib/analysis/case-report.js +573 -0
- package/frontend/src/lib/analysis/case-response-model.js +30 -1
- package/frontend/src/lib/analysis/detection-rule-model.js +36 -0
- package/frontend/src/lib/analysis/external-findings-import.js +320 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +7 -0
- package/frontend/src/lib/analysis/lookup-presentation.js +10 -20
- package/frontend/src/lib/analysis/scoring.js +1 -1
- package/frontend/src/lib/analysis/visualization-models.js +2 -3
- package/frontend/src/lib/analysis/workspace-archive.js +2 -0
- package/lib/abusech-auth.mjs +1 -1
- package/lib/availability.mjs +1 -1
- package/lib/bounded-contract-normalizers.mjs +33 -0
- package/lib/dns-convergence-review.mjs +183 -0
- package/lib/dns-delegation-health.mjs +196 -3
- package/lib/domain-change-packet.mjs +133 -0
- package/lib/domain-change-review.mjs +307 -0
- package/lib/domain-control-flight-recorder.mjs +268 -0
- package/lib/domain-portfolio-review.mjs +151 -0
- package/lib/evidence-export.mjs +1 -1
- package/lib/html-signals.mjs +5 -11
- package/lib/nameserver-preflight-review.mjs +159 -0
- package/lib/page-language-signals.mjs +124 -0
- package/lib/risk-scoring.mjs +42 -1
- package/lib/static-html-analysis.mjs +29 -1
- package/lib/trust-store-comparison.mjs +146 -0
- package/lib/website-technology.mjs +24 -10
- package/lib/zone-intent-review.mjs +534 -0
- package/package.json +1 -1
package/docs/cli-reference.md
CHANGED
|
@@ -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`, `
|
|
198
|
-
`
|
|
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
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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]));
|