@slicedearth/whoisleuth-cli 2.5.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/DISCLOSURE +1 -1
  2. package/cli/artifact-validation/case-response.mjs +51 -8
  3. package/cli/artifact-validation/lookup-evidence.mjs +15 -1
  4. package/cli/artifact-validation/signed-review.mjs +2 -51
  5. package/cli/artifact-verify.mjs +9 -2
  6. package/cli/bulk-command-runner.mjs +4 -6
  7. package/cli/case-command.mjs +79 -3
  8. package/cli/case-pack.mjs +7 -2
  9. package/cli/command-definition.mjs +2 -1
  10. package/cli/discovery-scan-command-runner.mjs +4 -6
  11. package/cli/domain-feed.mjs +61 -0
  12. package/cli/formatters/json.mjs +1 -13
  13. package/cli/indicator-set-command.mjs +1 -2
  14. package/cli/intake-command.mjs +43 -3
  15. package/cli/investigation-artifacts.mjs +2 -3
  16. package/cli/investigation-package-review.mjs +2 -0
  17. package/cli/local-mmdb-review.mjs +27 -4
  18. package/cli/lookup-command-runner.mjs +1 -0
  19. package/cli/offline-evidence-review.mjs +51 -5
  20. package/cli/review-arguments.mjs +3 -2
  21. package/cli/review-command-definitions.mjs +14 -8
  22. package/cli/review-command-runner.mjs +6 -3
  23. package/cli/risk-calibration.mjs +6 -4
  24. package/cli/selected-input-worker.mjs +12 -2
  25. package/cli/support-command-runner.mjs +3 -3
  26. package/cli/watchlist-review.mjs +43 -0
  27. package/cli/workflow-arguments.mjs +14 -0
  28. package/cli/workflow-command-definitions.mjs +26 -1
  29. package/cli/workflow-command-runner.mjs +32 -0
  30. package/docs/cli.md +99 -2
  31. package/docs/contextual-reviews.md +28 -5
  32. package/docs/infrastructure-observations.md +126 -0
  33. package/docs/offline-intake-context.md +213 -0
  34. package/lib/credential-form-attribution.mjs +136 -0
  35. package/lib/credential-surface-profile.mjs +6 -2
  36. package/lib/dns-delegation-health.mjs +23 -7
  37. package/lib/dns-intelligence.mjs +18 -4
  38. package/lib/domain-posture-analysis.mjs +53 -2
  39. package/lib/domain-posture-parsers.mjs +9 -0
  40. package/lib/domain-posture.mjs +29 -10
  41. package/lib/evidence-export-privacy.mjs +5 -2
  42. package/lib/evidence-report.mjs +1 -0
  43. package/lib/favicon.mjs +13 -4
  44. package/lib/html-signals.mjs +6 -0
  45. package/lib/lookup-child-profile-contract.mjs +28 -11
  46. package/lib/lookup-response-contract.mjs +1 -0
  47. package/lib/operation-budget.mjs +4 -0
  48. package/lib/service-binding-dns.mjs +19 -2
  49. package/lib/sslbl-certificates.generated.mjs +138 -137
  50. package/lib/static-html-analysis.mjs +32 -4
  51. package/lib/technology-evidence-role.mjs +1 -51
  52. package/lib/tls-intelligence.mjs +30 -5
  53. package/lib/website-enrichment.mjs +39 -23
  54. package/lib/whois-chain.mjs +12 -3
  55. package/lib/whois-common-formats.mjs +3 -1
  56. package/lib/whois-contacts.mjs +13 -2
  57. package/package.json +7 -5
  58. package/packages/analysis/risk-scoring.mjs +12 -9
  59. package/packages/cases/case-containment-handoff.mjs +195 -0
  60. package/packages/cases/case-evidence-links.mjs +16 -0
  61. package/packages/cases/case-evidence-model.mjs +14 -3
  62. package/packages/cases/case-object-coverage.mjs +33 -0
  63. package/packages/cases/case-packet-correction.mjs +106 -0
  64. package/packages/cases/case-recheck-model.mjs +35 -5
  65. package/packages/cases/case-record-operations.mjs +74 -9
  66. package/packages/cases/case-record-projection.mjs +15 -3
  67. package/packages/cases/case-record-version-input.mjs +23 -1
  68. package/packages/cases/case-report.mjs +30 -0
  69. package/packages/cases/case-requested-evidence.mjs +1 -1
  70. package/packages/cases/case-response-actions.mjs +57 -1
  71. package/packages/cases/case-response-model.mjs +7 -0
  72. package/packages/cases/case-response-object.mjs +119 -0
  73. package/packages/cases/case-response-outcomes.mjs +81 -14
  74. package/packages/cases/case-response-packet-render.mjs +22 -3
  75. package/packages/cases/case-response-packet.mjs +30 -2
  76. package/packages/cases/case-response-review-inputs.mjs +48 -7
  77. package/packages/cases/case-workflow-metadata.mjs +1 -9
  78. package/packages/contracts/analyst-interchange.mjs +7 -5
  79. package/packages/contracts/candidate-watch-review.mjs +54 -0
  80. package/packages/contracts/case-portability.mjs +260 -42
  81. package/packages/contracts/cli-command-semantics.mjs +3 -0
  82. package/packages/contracts/context-review.mjs +2 -1
  83. package/packages/contracts/external-observation-interchange.mjs +96 -28
  84. package/packages/contracts/lookup-evidence.mjs +3 -2
  85. package/packages/contracts/message-intake.mjs +7 -1
  86. package/packages/contracts/risk-calibration.mjs +44 -29
  87. package/packages/contracts/schema-lifecycle.mjs +3 -1
  88. package/packages/contracts/web-capture.mjs +1 -1
  89. package/packages/contracts/workspace-portability.mjs +86 -16
  90. package/packages/evidence/collection-quality.mjs +1 -1
  91. package/packages/evidence/credential-form-attribution.mjs +60 -0
  92. package/packages/evidence/image-regions.mjs +15 -0
  93. package/packages/evidence/record-identity.mjs +60 -16
  94. package/packages/evidence/technology-evidence-role.mjs +51 -0
  95. package/packages/interchange/external-findings-import.mjs +32 -17
  96. package/packages/interchange/json-output.mjs +14 -0
  97. package/packages/interchange/managed-indicator-export.mjs +3 -2
  98. package/packages/interchange/managed-indicator-set.mjs +13 -0
  99. package/packages/investigation/acquisition-decision-packet.mjs +78 -0
  100. package/packages/investigation/context-review.mjs +3 -3
  101. package/packages/investigation/document-intake.mjs +9 -3
  102. package/packages/investigation/domain-history-review.mjs +43 -11
  103. package/packages/investigation/domain-transition-review.mjs +2 -0
  104. package/packages/investigation/infrastructure-observation.mjs +234 -0
  105. package/packages/investigation/intake-context.mjs +284 -0
  106. package/packages/investigation/intake-indicators.mjs +116 -0
  107. package/packages/investigation/intake-phones.mjs +155 -0
  108. package/packages/investigation/intake-report.mjs +2 -1
  109. package/packages/investigation/investigation-manifest.mjs +33 -7
  110. package/packages/investigation/link-intake.mjs +38 -8
  111. package/packages/investigation/message-intake.mjs +55 -15
  112. package/packages/monitoring/domain-feed.mjs +321 -0
  113. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  114. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  115. package/packages/workspace/brand-profile-model.mjs +46 -2
  116. package/packages/workspace/bulk-session-model.mjs +4 -0
  117. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  118. package/packages/workspace/detection-rule-model.mjs +10 -2
  119. package/packages/workspace/shortlist-model.mjs +10 -2
  120. package/packages/workspace/watchlist-history.mjs +31 -4
  121. package/packages/workspace/watchlist-store.mjs +136 -7
  122. package/third-party-notices.txt +29 -29
@@ -3,7 +3,19 @@ import { CliUsageError } from "./errors.mjs";
3
3
  import {} from "./fail-policy.mjs";
4
4
  import { commandDefaultNumber } from "./command-reference.mjs";
5
5
  import { WORKFLOW_COMMAND_DEFINITIONS } from "./workflow-command-definitions.mjs";
6
+ import { parseDomainFeedSelectors } from "./domain-feed.mjs";
6
7
  import { terminalOptions, jsonOutput, failPolicies, parseOutput, } from "./argument-values.mjs";
8
+ function parseDomainFeedArguments(parsed) {
9
+ const operation = parsed.positionalValue('operation');
10
+ const contextSource = parsed.positionalValue('context');
11
+ if ((operation === 'watch-input') !== Boolean(contextSource))
12
+ throw new CliUsageError('watch-input requires one existing candidate-watch-input context file; review does not accept a context file.');
13
+ return { action: 'domain-feed', operation, feedId: parsed.positionalValue('feed'), source: parsed.positionalValue('source'), contextSource,
14
+ selection: parseDomainFeedSelectors(parsed.optionValues('--select')), output: jsonOutput(parsed), ...terminalOptions(parsed) };
15
+ }
16
+ function parseWatchlistReviewArguments(parsed) {
17
+ return { action: 'watchlist-review', operation: parsed.positionalValue('operation'), source: parsed.positionalValue('source'), output: jsonOutput(parsed), ...terminalOptions(parsed) };
18
+ }
7
19
  function parseMonitorOnceArguments(parsed) {
8
20
  const selectedFailPolicies = failPolicies(parsed, 'monitor-once');
9
21
  return {
@@ -80,6 +92,8 @@ function parseWorkflowRunArguments(parsed) {
80
92
  };
81
93
  }
82
94
  export const WORKFLOW_ARGUMENT_PARSERS = Object.freeze({
95
+ 'domain-feed': parseDomainFeedArguments,
96
+ 'watchlist-review': parseWatchlistReviewArguments,
83
97
  'monitor-once': parseMonitorOnceArguments,
84
98
  'workflow-plan': parseWorkflowPlanArguments,
85
99
  'workflow-run': parseWorkflowRunArguments,
@@ -1,6 +1,31 @@
1
1
  import { INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES } from "./investigation-recipes.mjs";
2
- import { positional, OPTIONAL_FILE_POSITIONAL, constraint, commandSeed, integer, BASE_INTEGER_RANGE, file } from "./command-definition.mjs";
2
+ import { positional, OPTIONAL_FILE_POSITIONAL, constraint, commandSeed, integer, BASE_INTEGER_RANGE, file, optionDefinition } from "./command-definition.mjs";
3
+ import { DOMAIN_FEED_CATALOGUE } from "../packages/monitoring/domain-feed.mjs";
3
4
  export const WORKFLOW_COMMAND_DEFINITIONS = Object.freeze({
5
+ 'domain-feed': commandSeed({
6
+ reference: { description: 'Review exact hosts or literal terms in a selected local plain-domain feed.',
7
+ example: 'whoisleuth domain-feed review tif-mini feed.txt --select host:candidate.example --json',
8
+ boundary: 'Always available offline. Reads one explicit local regular file; never downloads a feed, performs a Lookup, changes scores, retains candidates or enables a schedule. Streams at most 256 MiB and 10 million physical rows with 1,024-byte lines and a ten-minute deadline; retains at most 200 matched hosts. Repeat --select host:<hostname>, term:<literal> or exclude:<literal> for at most 200 exact hosts, 20 positive and 20 negative literal terms of 3–80 characters. A negative match vetoes even an exact host before the match bound; at least one positive term or exact host is required. Matching uses canonical ASCII/punycode substrings, never token boundaries, parent equivalence, typo/confusable expansion or regular expressions. Review emits the full raw-byte digest, separate declared file/local clocks, unknown per-host clocks and omissions. watch-input also requires a candidate-watch-input v1 context file with an empty candidates array, and emits that existing format for a separate watchlist-review plan/export. Inclusion is not a maliciousness, activity, ownership or availability verdict.' },
9
+ collection: { scope: 'Local raw file: 256 MiB, 10 million physical rows, 1,024 bytes per line, ten minutes; 200 retained hosts, 20 positive and 20 negative literal terms, no requests.' },
10
+ summary: 'Review a selected local plain-domain feed', options: ['--select', '--json', '--quiet', '--no-color'],
11
+ optionOverrides: { '--select': optionDefinition('text', 'Select host:<exact-hostname>, term:<literal-text> or exclude:<literal-text>; repeat for separate explicit selectors.', { occurrence: 'repeatable', acceptsOptionLikeValue: true }) },
12
+ positionals: Object.freeze([positional('operation', 'enum', 1, 1, ['review', 'watch-input']),
13
+ positional('feed', 'enum', 1, 1, DOMAIN_FEED_CATALOGUE.map(feed => feed.id)), positional('source', 'file', 1, 1), positional('context', 'file', 0, 1)]),
14
+ constraints: Object.freeze([constraint({ kind: 'required', options: ['--select'] })]),
15
+ handlerOwner: 'workflow', networkEffect: 'offline', common: false,
16
+ schemaIdentifiers: Object.freeze(['whoisleuth.candidate-watch-input']), primaryArtefacts: Object.freeze(['Bounded feed inclusion review', 'Explicit candidate watch selection']),
17
+ planSupport: true, additionalOutputFormats: Object.freeze([]), bootstrapProfile: 'allowed',
18
+ }),
19
+ 'watchlist-review': commandSeed({
20
+ reference: { description: 'Preview or export an explicit candidate selection to a local watchlist.', example: 'whoisleuth watchlist-review plan selection.json --json',
21
+ boundary: 'Reads bounded local JSON only. Plan shows exact domains, duplicates, per-domain outcomes, destination and review priority with zero additional requests. Export emits Watchlist schema 5 without creating a scan or baseline. Neither mode enables collection or scheduling.' },
22
+ collection: { scope: 'At most 2,000 selected candidate domains; no target requests.' },
23
+ summary: 'Review a local candidate watchlist handoff', options: ['--json', '--quiet', '--no-color'],
24
+ positionals: Object.freeze([positional('operation', 'enum', 1, 1, ['plan', 'export']), positional('source', 'file', 0, 1, [], 'argv_or_stdin')]),
25
+ constraints: Object.freeze([]), handlerOwner: 'workflow', networkEffect: 'offline', common: false,
26
+ schemaIdentifiers: Object.freeze(['whoisleuth.candidate-watch-input', 'whoisleuth.watchlists']), primaryArtefacts: Object.freeze(['Exact candidate handoff plan', 'Local watchlist export']),
27
+ planSupport: true, additionalOutputFormats: Object.freeze([]), bootstrapProfile: 'allowed',
28
+ }),
4
29
  "monitor-once": commandSeed({
5
30
  reference: {
6
31
  description: 'Collect one bounded owned-domain review and compare it with an optional prior checkpoint.',
@@ -16,7 +16,37 @@ import { MAX_SHARING_REVIEW_BYTES } from "./sharing-review.mjs";
16
16
  import { createBufferedOutput } from "./output-file.mjs";
17
17
  import { boundedInteractiveAnswer, canReadInteractiveLine, readBoundedInteractiveLine } from "./terminal-input.mjs";
18
18
  import { WORKFLOW_INLINE_COMMANDS } from "./inline-command-families.mjs";
19
+ import { reviewCandidateWatchInput, parseCandidateWatchInput } from "./watchlist-review.mjs";
20
+ import { readDomainFeedFile } from "./domain-feed.mjs";
21
+ import { scanDomainFeed, buildDomainFeedWatchInput, normalizeDomainFeedSelection } from "../packages/monitoring/domain-feed.mjs";
22
+ import { MAX_WATCHLIST_IMPORT_BYTES } from "../packages/contracts/workspace-portability.mjs";
19
23
  import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
24
+ async function runWatchlistReviewCommand(args, dependencies, context) {
25
+ const raw = dependencies.readArtifactInput ? await dependencies.readArtifactInput(args.source) : await context.readInput(args.source, MAX_WATCHLIST_IMPORT_BYTES, 'Candidate watch selection');
26
+ const document = reviewCandidateWatchInput(raw, args.operation, context.now());
27
+ if (!args.quiet)
28
+ context.writeStdout(formatJsonDocument(document));
29
+ return EXIT_CODES.SUCCESS;
30
+ }
31
+ async function runDomainFeedCommand(args, dependencies, context) {
32
+ context.setFailureLabel('Offline domain-feed review');
33
+ const handoff = args.contextSource ? parseCandidateWatchInput(dependencies.readArtifactInput
34
+ ? await dependencies.readArtifactInput(args.contextSource)
35
+ : await context.readInput(args.contextSource, MAX_WATCHLIST_IMPORT_BYTES, 'Candidate watch context')) : null;
36
+ const watchSelection = handoff?.selection;
37
+ if (watchSelection && watchSelection.candidates.length)
38
+ throw new CliUsageError('The watch-input context must have an empty candidates array; only reviewed feed matches may be nominated.');
39
+ const selection = normalizeDomainFeedSelection({ ...args.selection, brandProfileId: watchSelection?.brandProfileId ?? null });
40
+ const review = await scanDomainFeed((dependencies.readDomainFeedInput ?? readDomainFeedFile)(args.source, dependencies.signal), {
41
+ feedId: args.feedId, selection, importedAt: context.now(), ...(dependencies.signal ? { signal: dependencies.signal } : {}),
42
+ });
43
+ const document = watchSelection && handoff
44
+ ? buildDomainFeedWatchInput(review, review.matches.map(match => match.domain), watchSelection, handoff.watchlists)
45
+ : review;
46
+ if (!args.quiet)
47
+ context.writeStdout(formatJsonDocument(document));
48
+ return EXIT_CODES.SUCCESS;
49
+ }
20
50
  function boundWorkflowInputs(command, inputs, dependencies, context) {
21
51
  if (!inputs.size)
22
52
  return {};
@@ -181,6 +211,8 @@ async function runWorkflowRecipeCommand(args, dependencies, context) {
181
211
  return investigationRunExitCode(document);
182
212
  }
183
213
  const WORKFLOW_COMMAND_HANDLERS = Object.freeze({
214
+ 'domain-feed': runDomainFeedCommand,
215
+ 'watchlist-review': runWatchlistReviewCommand,
184
216
  'monitor-once': runMonitorOnceCommand,
185
217
  'workflow-plan': runWorkflowPlanCommand,
186
218
  'workflow-run': runWorkflowRecipeCommand,
package/docs/cli.md CHANGED
@@ -1,5 +1,7 @@
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
+
3
5
  The first-party CLI runs on the operator's machine and does not call the hosted
4
6
  WHOISleuth deployment. Use this guide for installation, common commands,
5
7
  collection boundaries and output. Installed `whoisleuth --help`, focused
@@ -106,6 +108,18 @@ One strict domain, IP address or ASN can occupy command position as Lookup
106
108
  shorthand. URL-like or ambiguous input requires the explicit `lookup` command.
107
109
  Only `bulk` accepts multiple targets.
108
110
 
111
+ For a downloaded plain-domain feed, use
112
+ `whoisleuth domain-feed review nrd7 feed.txt --select term:example --json`.
113
+ Repeat `--select exclude:literal` to veto a literal substring before the result
114
+ bound, including for exact-host selections. At most 20 positive and 20 negative
115
+ terms of 3–80 characters are supported; at least one positive term or exact host
116
+ is required. Matching uses canonical ASCII/punycode text without token, typo,
117
+ confusable or regular-expression expansion. These offline selectors do not infer
118
+ saved browser campaign identity or default priority.
119
+ This is an offline literal match, not a feed download or target Lookup. The
120
+ [candidate-review guide](https://github.com/slicedearth/whoisleuth/blob/main/docs/brand-candidate-review.md#review-a-domain-feed) covers
121
+ source attribution and the separate Watchlist handoff.
122
+
109
123
  `discover` accepts a brand label or registrable domain and supports multi-part
110
124
  public suffixes in `--tlds` (for example, `co.uk,com.au`). It preserves the full
111
125
  suffix and does not collect candidate evidence; use `discover-scan` only after
@@ -179,6 +193,30 @@ Excluded links and unsupported QR payloads make the review partial, with
179
193
  category counts rather than private payload text. Bare QR hostnames are not
180
194
  automatically treated as URLs.
181
195
 
196
+ Current message-intake reports also retain bounded literal IPv4/IPv6 and
197
+ explicitly labelled MD5, SHA-1 and SHA-256 observations from supported body and
198
+ document text. Each observation links to its selected source part and digest;
199
+ it is not a Lookup target, file identification or threat verdict. Complete URLs,
200
+ message headers, QR payloads, HAR and identity-event fields are excluded from
201
+ this text scan. Historical version-1 reports do not acquire new observations.
202
+
203
+ Use `intake text selected.txt --intake-context context.json --json` to add an
204
+ explicit analyst-supplied distribution declaration. The separate context file
205
+ is limited to 8 KiB and cannot use stdin. Its channel, non-sensitive source
206
+ label and optional declared time, reference, observer and vantage labels are
207
+ included in the output, not verified as delivery or capture conditions. See
208
+ [selected-input indicators and declarations](offline-intake-context.md) for the
209
+ exact schema, privacy exclusions and extraction bounds.
210
+
211
+ Plain-text phone discovery reports only candidate counts, the source digest and
212
+ UTF-16 selection ranges to standard error; unselected numbers do not enter JSON
213
+ output. Use a version-2 `--intake-context` file to select exact ranges and declare
214
+ their source, basis, role and known time. The same optional file can supply a
215
+ displayed/destination pair for local hostname comparison. See the
216
+ [selection example](offline-intake-context.md#select-phone-observations-in-the-cli).
217
+ `--quiet` suppresses discovery guidance, and `--strict-exit` reports partial
218
+ phone coverage as well as other incomplete intake work.
219
+
182
220
  ### Local IP-location database review
183
221
 
184
222
  `review-evidence query.json --mmdb selected.mmdb --json --strict-exit` reads
@@ -233,6 +271,21 @@ comparison and connector provenance. These workflows are offline; partial
233
271
  context reviews return 4 when `--strict-exit` is selected. Browser-generated
234
272
  reusable inputs use the same validators.
235
273
 
274
+ Domain-history input version 2 adds analyst-declared registration, deletion,
275
+ re-registration, transfer or review boundaries linked to retained snapshot or
276
+ pin IDs. Version 1 remains supported with its original output. Boundaries prompt
277
+ reassessment; they do not establish ownership or discard prior evidence.
278
+
279
+ `review-evidence containment.json --json --strict-exit` accepts a
280
+ `whoisleuth.internal-containment.input` version-1 document containing a supported
281
+ Case export, selected next-step assertion and linked pin IDs, internal or trusted
282
+ audience, recipient role and an explicit disclosure-review acknowledgement.
283
+ It exports only that selected handoff, never writes the Case or performs a
284
+ 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
286
+ internal follow-up. See [internal containment](offline-intake-context.md#selected-internal-containment-handoff)
287
+ for the complete input and audience limitations.
288
+
236
289
  ### Message-header review
237
290
 
238
291
  `mail-headers` parses only the bounded header block from a selected message file
@@ -284,6 +337,45 @@ selected file, including an envelope when supplied. No provider API is contacted
284
337
 
285
338
  ### Local Case files
286
339
 
340
+ Manual response scope is explicit and offline. `case incident-link` takes
341
+ `{"url":"https://example.test/reported-page"}`; use its retained ID to bind
342
+ `responseObjects` on `case action`. Each object contains `kind`, `identifier`
343
+ and `incidentTargetId` (null for the Case domain or an associated hostname).
344
+ New URL objects must match that exact retained link. Exact historical identities
345
+ already retained in response records remain selectable after a link edit or
346
+ removal; they are not rebound to its replacement. An action may bind up to 20
347
+ objects; missing historical scope stays unknown.
348
+
349
+ `case action-event` takes `{"id":"action-id","transition":{...}}`, using
350
+ the same reviewed, authorised and manually submitted workflow as Console.
351
+ After submission, a typed `objectOutcome` such as `removed`, `restricted`,
352
+ `suspended`, `delisted`, `restored` or `disputed` requires explicit event
353
+ `responseObjects`: a partial receipt never applies to all objects by default.
354
+ Acknowledgement is a workflow event, not independently observed remediation.
355
+
356
+ A self-initiated correction uses a new `case action`, with `originActionId`
357
+ identifying the original action and `responseObjects` selecting a subset of that
358
+ delivery's exact objects. Its `correction` block identifies one delivery event,
359
+ packet digest/version and profile, plus the purpose (`correction` or
360
+ `retraction_request`), reason, previous and corrected statements, and retained
361
+ evidence-pin IDs. This is separate from `amendment`, which still requires an
362
+ actual provider-request event. The correction must match a retained typed packet
363
+ receipt on the selected delivery; a historical free-text digest alone is
364
+ insufficient. The usual recipient, authority and disclosure review then applies
365
+ to the new draft. The original packet and delivery remain unchanged.
366
+
367
+ `case recheck-question` saves `statement` and `recheck` comparison conditions.
368
+ Pins, questions and recheck input can retain one `responseObject`; exact-object
369
+ non-reproduction requires matching complete baseline/current evidence, a later
370
+ observation and comparable conditions. Hostname similarity is insufficient.
371
+ Independent removal, restriction, suspension, delisting, transfer and restoration
372
+ use those same comparison requirements. Disputes and warnings retain procedural
373
+ source attribution separately; provider claims are not independent observations.
374
+ `case close-object` requires one `responseObject` and the usual typed closure
375
+ reason/evidence links. It leaves the Case and other objects unchanged.
376
+ All these JSON operations require `--input`, `--output`, deliberate file
377
+ replacement and the existing digest/lease checks. No report or request is sent.
378
+
287
379
  Create a working file, inspect its Case IDs, then append a note:
288
380
 
289
381
  ```sh
@@ -395,12 +487,17 @@ An ordinary Case JSON file alone provides structural validity, not a checksum.
395
487
  They use generated entry names, not original paths. Ordinary ZIPs and folders
396
488
  are private and unencrypted; packaging does not redact selected files. The report distinguishes
397
489
  file identity, supported source formats, opaque content, exact capsule/source
398
- links and capture-manifest attachment matches. Include the capture manifest and
490
+ links, declared image derivation and capture-manifest attachment matches.
491
+ Image derivation reports an immediate-parent digest and byte length, editing
492
+ 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
494
+ attachment reference does not count. Parent pixels are optional; absent declarations
495
+ leave editing history unknown. Include the capture manifest and
399
496
  its screenshot/DOM-digest files together to check their declared bytes; original
400
497
  filenames are not needed to establish a match. It does not import files or establish source truth, signature trust or a
401
498
  trusted timestamp. Unsupported or rejected entries produce a partial report;
402
499
  `--strict-exit` returns 4. Without `--package` or `--folder`, `manifest` produces a
403
- standalone JSON manifest; exact public version-2 manifests remain readable.
500
+ standalone JSON manifest; exact version-2 and version-3 manifests remain readable.
404
501
 
405
502
  For [BagIt 1.0](https://www.rfc-editor.org/rfc/rfc8493.html) interchange, use
406
503
  `--bagit` with `--package` or `--folder` on both commands:
@@ -17,8 +17,9 @@ The CLI uses the same models:
17
17
  whoisleuth review-evidence selected-input.json --json --strict-exit
18
18
  ```
19
19
 
20
- Each input has `schema`, `version: 1` and `evidence`. Unknown versions and fields
21
- are rejected. Inputs are bounded to 16 MiB; each supplied context list permits
20
+ Each input has `schema`, a supported `version` and `evidence`. Most inputs use
21
+ version 1; domain history also supports version 2 as described below. Unknown
22
+ versions and fields are rejected. Inputs are bounded to 16 MiB; each supplied context list permits
22
23
  200 records. `--strict-exit` returns 4 for a partial contextual review. Neither
23
24
  adapter contacts a destination or executes selected software.
24
25
 
@@ -57,8 +58,12 @@ later. These stages never execute a command, submit a form or open a destination
57
58
 
58
59
  The Case view compares retained snapshots using the existing source, hostname,
59
60
  scan-depth and model-version rules. Registration, DNS, mail and web changes are
60
- shown alongside source-qualified certificate pins. Equal-time observations are
61
- not ordered into a change sequence. Retired dependencies must involve the Case
61
+ shown alongside source-qualified certificate pins.
62
+ Registration and nameserver changes target the Case registration domain; mail
63
+ and web changes target the explicit observation hostname. Certificate pins keep
64
+ their own hostname, or an unknown target when none was retained. These targets
65
+ are preserved in review downloads, retained reports and print previews.
66
+ Equal-time observations are not ordered into a change sequence. Retired dependencies must involve the Case
62
67
  domain or one of its hostnames. An expected window qualifies the review; it does
63
68
  not approve a change automatically or establish transfer or takeover.
64
69
  Review prompts connect comparable page, mail, delegation and registration changes
@@ -71,7 +76,8 @@ Schema: `whoisleuth.domain-history.input`. Its `evidence` contains:
71
76
  - `caseExport`: an ordinary supported editable Case export, with `version` and
72
77
  `cases`; the exact existing Case reader rejects data loss or silent repair.
73
78
  - `caseId`: the Case selected from that export.
74
- - `declarations`: `expectedChanges` and `retiredDependencies` arrays.
79
+ - `declarations`: `expectedChanges` and `retiredDependencies` arrays; version 2
80
+ also requires `registrationBoundaries`. Version 1 remains readable unchanged.
75
81
 
76
82
  An expected change is `{family, start, end, reason}`. A retired dependency is
77
83
  `{asset, dependency, family, retiredAt, source}`. Families are `registration`,
@@ -79,6 +85,23 @@ An expected change is `{family, start, end, reason}`. A retired dependency is
79
85
  reusable input download includes the selected Case and is not duplicated in
80
86
  workspace storage when saving the report.
81
87
 
88
+ Version-2 registration boundaries are analyst declarations, not confirmed
89
+ registration epochs. Each has `{kind, occurredAt, source, rationale,
90
+ snapshotIds, evidencePinIds}`. Kinds are `registration_reported`,
91
+ `deletion_reported`, `reregistration_reported`, `transfer_reported` and
92
+ `review_boundary`. Supply an explicit ISO time and at least one exact reference
93
+ to a snapshot or evidence pin retained in the selected Case. References must be
94
+ unique; unavailable references fail rather than selecting a replacement.
95
+ Up to 40 boundaries and 20 references of each type per boundary are accepted.
96
+
97
+ The Case history form exposes the same declarations. Reports count retained
98
+ observations before and at/after a declared time without assigning ownership or
99
+ asserting that deletion, re-registration or transfer occurred. Earlier evidence,
100
+ decisions, exceptions and open follow-ups are not rewritten. Reports retain the
101
+ declared source and exact evidence references, and can be deliberately saved as
102
+ ordinary Case files. The reusable input remains a separate download containing
103
+ the selected Case, not an additional workspace copy.
104
+
82
105
  ## Platform objects
83
106
 
84
107
  Schema: `whoisleuth.platform-continuity.input`. `evidence` is an array:
@@ -0,0 +1,126 @@
1
+ # Source-qualified infrastructure snapshots
2
+
3
+ The retained infrastructure inventory combines admitted browser-local evidence,
4
+ not all infrastructure belonging to an organisation. It makes no requests on
5
+ import, opening a view, filtering, selecting a host or comparing history.
6
+
7
+ Import a `whoisleuth.infrastructure-observation` version 1 document through the
8
+ existing external-findings import. The exact bounded snapshot is retained as one
9
+ Case evidence pin. Review the preview before confirming the mutation. The source
10
+ descriptions are supplied provenance, not authenticated source identities.
11
+ Legacy DNS and certificate imports remain readable: an old Case-domain DNS pin
12
+ does not acquire a precise queried hostname or complete collection scope.
13
+
14
+ The document records an explicit target, selected full hostnames and DNS types,
15
+ collection mode, observation time and coverage. Each DNS response retains its
16
+ queried name, response owner, type, outcome and source. An alias link uses its
17
+ record owner; an address returned after an alias belongs to the returned address
18
+ owner, not automatically to the initial query. Only retained alias links are
19
+ drawn. Links from different dates do not prove one contemporaneous alias chain.
20
+ No-data, NXDOMAIN, failed and not-checked outcomes remain different observations
21
+ and do not decide registration availability.
22
+
23
+ Certificate-log and TLS observations keep distinct sources and exact SHA-256
24
+ identities. Full names remain visible; `*.example.test` is a wildcard pattern,
25
+ not an enumerated hostname, and cannot be selected for Lookup or Bulk.
26
+ Certificate names alone do not establish current resolution or activity.
27
+
28
+ DNS operator, observed edge/CDN/proxy, application platform, runtime, embedded
29
+ dependency, address registration, routing origin and independently observed
30
+ origin are distinct source-qualified roles. Routing requires a routing source;
31
+ IP-registration data cannot substitute for it. Edge indicators cannot identify
32
+ a hidden origin. Multiple providers and conflicting observations remain separate.
33
+ The snapshot reader does not derive physical location, responsibility, ownership,
34
+ safety or maliciousness from these roles.
35
+
36
+ ## Review and deliberately act
37
+
38
+ Open the inventory from saved-work search. Type, source and date filters apply to
39
+ the admitted set before pagination. Inspect one snapshot to review every retained
40
+ DNS outcome, certificate name and provider role, or select two snapshots in
41
+ chronological order to compare. Export preserves the exact snapshot format.
42
+ Topology search applies to the complete admitted one-hop relationship set, not
43
+ the current source page. Its diagram has a 50-relationship budget; the paginated
44
+ source list and exact snapshot review retain access to evidence beyond the diagram.
45
+
46
+ Lookup links fill the selected target for review without submitting it. Explicitly
47
+ selected hostnames can be prepared in existing Bulk. That handoff does not scan,
48
+ recursively expand targets, change a watchlist or enrol a host in monitoring.
49
+ Use the existing reviewed Lookup/Bulk/Monitor collection plans and explicit
50
+ save actions to collect or retain another observation.
51
+
52
+ ## Historical comparison and CLI
53
+
54
+ Source, mode and exact selection scope must match, and the later snapshot must
55
+ have a strictly later observation time. Newly observed means newly present in
56
+ retained evidence, not newly created. A value not returned by a complete,
57
+ comparable selected collection is labelled **not returned**, never confirmed
58
+ absence or disappearance. Failed, partial, limited, changed-source and
59
+ incomparable snapshots cannot generate removal findings. Equal-time observations
60
+ have no temporal order. Provider-reported history stays labelled separately from
61
+ supplied local observations.
62
+
63
+ Comparison rows retain the exact DNS query, response owner and record type, or
64
+ the certificate SHA-256, alongside their source and observation clocks. Labels
65
+ are presentation only: relationship groups and graph exports use canonical
66
+ entity identities, including when long nameserver-set labels look identical.
67
+
68
+ Derived relationship-group, graph-node and cluster IDs can change when their
69
+ identity calculation changes, including for short labels. They are not durable
70
+ cross-export keys; use the canonical `entityId` where provided. Historical groups
71
+ without it use their complete retained value. Graph fields named `canonical`
72
+ and `value` are bounded display text, not a replacement for that identity. Inspect
73
+ the contributing Case evidence for full nameserver members. Component selections
74
+ and cluster adjustments are transient and are not persisted review history.
75
+
76
+ In GraphML and GEXF, the display label for `node_canonical` is now
77
+ `Bounded display value` (previously `Canonical value`). The field key, JSON
78
+ property, entity identities and values are unchanged; XML consumers should use
79
+ the field key rather than its display label.
80
+
81
+ The existing offline command shares the website reader and comparison owner:
82
+
83
+ ```sh
84
+ whoisleuth review-evidence infrastructure-observation.json --json
85
+ whoisleuth review-evidence infrastructure-comparison.json --json
86
+ ```
87
+
88
+ The comparison input is exactly:
89
+
90
+ ```json
91
+ {
92
+ "schema": "whoisleuth.infrastructure-comparison.input",
93
+ "version": 1,
94
+ "earlier": { "...": "complete version-1 snapshot" },
95
+ "later": { "...": "complete version-1 snapshot" }
96
+ }
97
+ ```
98
+
99
+ The placeholders above are illustrative, not valid evidence. See the reserved
100
+ fixture in `test/fixtures/infrastructure-observations/` for an exact snapshot.
101
+ The CLI performs no DNS, registry, BGP, TLS, HTTP or provider request.
102
+
103
+ Changed or incomplete latest retained pairs surface as existing local Review
104
+ Items with supporting Case navigation. Complete earlier snapshots remain
105
+ available as stronger baselines; later failures do not rewrite them. There is no
106
+ new history store or scheduling engine.
107
+
108
+ ## Resource and compatibility boundary
109
+
110
+ One snapshot is limited to 128 KiB, 128 selected hostnames, six DNS types, 16
111
+ source identities, 512 DNS response rows with at most 32 values each, 32
112
+ certificate observations with at most 128 exact names each, 128 role observations
113
+ and 12 bounded limitations. Bounds reject oversized documents rather than
114
+ silently prune accepted snapshot evidence. Existing Case/workspace quotas still
115
+ apply. Projection/diagram budgets may admit a smaller disposable view, with
116
+ incomplete coverage disclosed; exact snapshot review/export remains available.
117
+
118
+ Version 1 has exact keys and refuses future versions. Existing external-findings
119
+ version 4 and typed version-1 row imports preserve their historical meaning;
120
+ version 5 adds the optional exact infrastructure snapshot. No external discovery
121
+ API, new provider, dependency, collector or automatic request is required.
122
+ Current snapshot and version-5 findings exports use compact UTF-8 JSON with a
123
+ terminal line feed so the emitted representation stays within its parser ceiling.
124
+ An offline comparison input is bounded to two snapshot budgets plus 1 KiB of
125
+ framing. Historical version-4 fixture bytes and reader meaning remain unchanged;
126
+ new typed row conversions emit version 5.