@slicedearth/whoisleuth-cli 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +8 -5
- package/NOTICE +3 -2
- package/README.md +60 -67
- package/cli/artifact-validation/case-response.mjs +6 -857
- package/cli/artifact-validation/lookup-evidence.mjs +15 -2
- package/cli/artifact-verify.mjs +32 -1
- package/cli/bulk-command-runner.mjs +5 -4
- package/cli/case-command.mjs +17 -17
- package/cli/case-pack.mjs +7 -1
- package/cli/ci-report.mjs +17 -5
- package/cli/collection-arguments.mjs +6 -0
- package/cli/collection-command-definitions.mjs +13 -3
- package/cli/command-definition.mjs +7 -5
- package/cli/discovery-scan-command-runner.mjs +13 -7
- package/cli/doctor.mjs +37 -3
- package/cli/domain-control-monitor.mjs +25 -2
- package/cli/domain-control-observations.mjs +52 -15
- package/cli/fail-policy.mjs +31 -22
- package/cli/formatters/html.mjs +3 -0
- package/cli/formatters/json.mjs +8 -1
- package/cli/formatters/terminal-lookup.mjs +24 -1
- package/cli/history-command-definitions.mjs +1 -1
- package/cli/investigation-plan.mjs +15 -0
- package/cli/lookup-brief.mjs +11 -5
- package/cli/lookup-command-runner.mjs +26 -18
- package/cli/lookup-diff.mjs +1 -0
- package/cli/review-command-definitions.mjs +11 -11
- package/cli/risk-policy.mjs +83 -0
- package/cli/saved-lookup.mjs +22 -7
- package/cli/source-reliability.mjs +12 -10
- package/cli/support-arguments.mjs +1 -1
- package/cli/support-command-definitions.mjs +5 -4
- package/cli/support-command-runner.mjs +6 -4
- package/cli/workflow-command-definitions.mjs +2 -1
- package/cli/workflow-command-runner.mjs +5 -1
- package/docs/cli-reference.md +56 -11
- package/docs/cli.md +162 -33
- package/docs/contextual-reviews.md +10 -0
- package/docs/infrastructure-observations.md +9 -0
- package/lib/dmarc-discovery.mjs +4 -2
- package/lib/dns-inheritance-review.mjs +14 -1
- package/lib/evidence-export.mjs +10 -4
- package/lib/evidence-report.mjs +2 -1
- package/lib/generated/cisa-kev-catalog.mjs +19 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +178 -5
- package/lib/idn-confusables.mjs +5 -1
- package/lib/lookup-collection-receipt.mjs +52 -0
- package/lib/lookup-diagnostics.mjs +2 -12
- package/lib/lookup-progress.mjs +29 -31
- package/lib/lookup-response-contract.mjs +9 -0
- package/lib/lookup-response.mjs +15 -14
- package/lib/lookup-source-progress.mjs +4 -2
- package/lib/lookup.mjs +33 -2
- package/lib/rdap-capabilities.mjs +43 -9
- package/lib/registry-insights.mjs +17 -6
- package/lib/sslbl-certificates.generated.mjs +143 -142
- package/lib/whois-authority.mjs +56 -1
- package/lib/whois-chain.mjs +2 -1
- package/lib/whois-contracts.mjs +2 -0
- package/lib/whois-normalization.mjs +4 -4
- package/package.json +6 -6
- package/packages/analysis/bounded-json.mjs +12 -2
- package/packages/analysis/brand-profile-signals.mjs +42 -0
- package/packages/cases/case-attachment-model.mjs +36 -1
- package/packages/cases/case-checkpoint-receipts.mjs +68 -0
- package/packages/cases/case-evidence-links.mjs +28 -0
- package/packages/cases/case-evidence-model.mjs +39 -6
- package/packages/cases/case-migration-model.mjs +9 -1
- package/packages/cases/case-object-coverage.mjs +106 -20
- package/packages/cases/case-record-operations.mjs +13 -0
- package/packages/cases/case-record-projection.mjs +28 -2
- package/packages/cases/case-report.mjs +5 -4
- package/packages/cases/case-response-packet-validation.mjs +880 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +12 -1
- package/packages/cases/case-response-packet.mjs +2 -15
- package/packages/cases/case-response-review-inputs.mjs +18 -1
- package/packages/cases/case-type-presentation.mjs +10 -0
- package/packages/cases/case-workflow-metadata.mjs +0 -8
- package/packages/comparison/capture-context.mjs +3 -2
- package/packages/contracts/analyst-review-state-contract.mjs +1 -1
- package/packages/contracts/case-portability.mjs +59 -28
- package/packages/contracts/cli-doctor.mjs +4 -0
- package/packages/contracts/cli-lookup.mjs +57 -46
- package/packages/contracts/lookup-evidence.mjs +12 -2
- package/packages/contracts/registry-insights.mjs +6 -0
- package/packages/evidence/dmarc-policy-context.mjs +80 -0
- package/packages/evidence/lookup-collection-receipt.mjs +140 -0
- package/packages/evidence/lookup-collection-review.mjs +74 -0
- package/packages/interchange/external-findings-import.mjs +8 -4
- package/packages/investigation/bulk-domain-comparison.mjs +17 -8
- package/packages/investigation/bulk-mail-exposure.mjs +2 -2
- package/packages/investigation/domain-history-review.mjs +7 -1
- package/packages/workspace/brand-keyword-campaign.mjs +11 -1
- package/packages/workspace/bulk-session-model.mjs +8 -0
- package/third-party-notices.txt +30 -30
|
@@ -9,7 +9,7 @@ import { CLI_CASE_OPERATIONS, CLI_INDICATOR_OPERATIONS, positional, OPTIONAL_FIL
|
|
|
9
9
|
export const REVIEW_COMMAND_DEFINITIONS = Object.freeze({
|
|
10
10
|
"verify-artifact": commandSeed({
|
|
11
11
|
reference: {
|
|
12
|
-
description: 'Validate a supported archive, ordinary
|
|
12
|
+
description: 'Validate a supported archive, ordinary case export, claim passport, packet, manifest, saved Lookup or Lookup-evidence export without printing evidence contents. Use --package for an evidence ZIP or encrypted package, with --passphrase-file to unlock it; use --folder ./evidence for an unencrypted evidence folder. Add --bagit with --package or --folder to verify BagIt 1.0.',
|
|
13
13
|
example: 'whoisleuth verify-artifact report.json --manifest manifest.json --manifest-entry artifact-2 --json --strict-exit',
|
|
14
14
|
boundary: 'Verification is offline and redacted. ZIP and folder entries are reported separately without importing them. Case exports are checked without repairing content; ordinary package review also counts original references with matching bytes. Ordinary folders allow only the declared layout; BagIt allows bounded nested payloads and checks SHA-256/SHA-512 manifests without interpreting payloads. Symbolic links are refused. BagIt fetch.txt is never fetched; missing files, mismatches and unsupported algorithms remain explicit. Package digests describe bytes, not filesystem metadata or authenticity. In scripts, use --strict-exit: incomplete verification returns 4. Unencrypted workspace archives always return 4 under --strict-exit because section digests do not cover mutable root metadata; structure-only formats can pass their narrower contract. Default exit 0 means the report was produced, not that its checks passed.',
|
|
15
15
|
},
|
|
@@ -56,9 +56,9 @@ export const REVIEW_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
56
56
|
}),
|
|
57
57
|
"source-report": commandSeed({
|
|
58
58
|
reference: {
|
|
59
|
-
description: '
|
|
59
|
+
description: 'Summarise source reliability from saved Lookup or Bulk evidence, or merge source-reliability reports.',
|
|
60
60
|
example: 'whoisleuth source-report lookup.json --json',
|
|
61
|
-
boundary: '
|
|
61
|
+
boundary: 'Accepts supported Lookup, Bulk or Bulk-item documents, or an unmixed set of source-reliability reports. Retains source states and timings but excludes targets, queries, endpoints and raw evidence.',
|
|
62
62
|
},
|
|
63
63
|
collection: { scope: 'Reads bounded saved evidence and emits target-free source reliability data.' },
|
|
64
64
|
summary: 'Build a target-free source report',
|
|
@@ -160,7 +160,7 @@ export const REVIEW_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
160
160
|
example: 'whoisleuth intake email message.eml --json',
|
|
161
161
|
boundary: 'Offline only: no destination, embedded command, phone number or QR payload is opened, contacted or executed. Output omits original bodies, subjects, address local parts, URL paths, queries and fragments. Literal IPs and labelled hashes are bounded text observations, not lookup targets or verdicts; headers, URL contents, QR, HAR and identity-event fields are not scanned for them. Plain-text phone discovery writes only counts, source digest, ranges and status to stderr; --quiet suppresses it. Optional --intake-context reads one separate file up to 8 KiB, not stdin. Version 1 carries distribution declarations; version 2 can also bind explicit phone selections and a supplied destination pair to the exact input digest. Selected phone strings and source declarations enter output; unselected candidates and exact private URLs do not. Authentication headers remain attributed claims. Selected PDF and DOCX review retains part identities and extraction coverage; embedded raster QR support does not imply complete rendered-page review. HAR retains minimised request order and timings, never replay authority, headers, cookies or bodies. Use --strict-exit to return 4 for partial coverage.',
|
|
162
162
|
},
|
|
163
|
-
collection: { scope: 'Reads one selected text, MIME email, calendar, PNG, PDF, DOCX, HAR or identity-event JSON file and, only with --intake-context, one bounded declaration/selection file. No collection or automatic
|
|
163
|
+
collection: { scope: 'Reads one selected text, MIME email, calendar, PNG, PDF, DOCX, HAR or identity-event JSON file and, only with --intake-context, one bounded declaration/selection file. No collection or automatic case write.' },
|
|
164
164
|
summary: 'Review selected files and identity events offline',
|
|
165
165
|
options: ['--json', '--reported-action', '--trusted-auth-header', '--intake-context', '--strict-exit', '--quiet', '--no-color'],
|
|
166
166
|
positionals: Object.freeze([positional('kind', 'enum', 1, 1, MESSAGE_INTAKE_KINDS), positional('source', 'file', 0, 1, [], 'argv_or_stdin')]),
|
|
@@ -226,12 +226,12 @@ export const REVIEW_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
226
226
|
}),
|
|
227
227
|
case: commandSeed({
|
|
228
228
|
reference: {
|
|
229
|
-
description: 'Show or open a local
|
|
229
|
+
description: 'Show or open a local case, append a note or evidence pin, link evidence, record an assessment or offline recheck, retain an incident-link, create an action, append an action-event, save a recheck-question, or close-object. JSON mutations use --input; notes use --text or --note-file. Link input contains fromPinId, toPinId, kind and basis. Action-event input contains id and transition. Explicit responseObjects bind an action; a typed objectOutcome requires an explicit affected subset on its event. Object closure never closes other objects or the case. Mutations require --output and write the complete current case export.',
|
|
230
230
|
example: 'whoisleuth case open --domain example.test --output cases.json\n whoisleuth case show cases.json\n whoisleuth case note cases.json --text "Review the retained observation" --output cases.json --force',
|
|
231
|
-
boundary: 'No database, browser launch, request or external report is created. Select --case-id when a file contains multiple
|
|
231
|
+
boundary: 'No database, browser launch, request or external report is created. Select --case-id when a file contains multiple cases. Existing files require --force; --expect-file-digest sha256:<digest> additionally checks the exact file reviewed earlier. Source and output leases reject concurrent changes. Interrupted .workflow.lock files require deliberate inspection. Recheck records supplied observations; it does not collect them. Not reproduced requires an existing saved question, a complete observation and comparable conditions. Working exports include private analyst content and file references, not attached file bytes.',
|
|
232
232
|
},
|
|
233
|
-
collection: { scope: `Reads exact
|
|
234
|
-
summary: 'Review and update ordinary local
|
|
233
|
+
collection: { scope: `Reads exact case schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')}. Input is bounded to ${MAX_EDITABLE_CASE_INPUT_BYTES / 1024 / 1024} MiB including formatting; the complete canonical case store must fit ${MAX_CASE_STORE_BYTES / 1024 / 1024} MiB without pruning. Writes current schema ${CASE_SCHEMA_VERSION}.` },
|
|
234
|
+
summary: 'Review and update ordinary local case files',
|
|
235
235
|
options: ['--case-id', '--domain', '--title', '--new-incident', '--text', '--note-file', '--input', '--expect-file-digest', '--json', '--no-color'],
|
|
236
236
|
positionals: Object.freeze([positional('operation', 'enum', 1, 1, CLI_CASE_OPERATIONS), positional('source', 'file', 0, 1)]),
|
|
237
237
|
constraints: Object.freeze([constraint({ kind: 'mutually_exclusive', options: ['--text', '--note-file'] })]),
|
|
@@ -256,11 +256,11 @@ export const REVIEW_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
256
256
|
}),
|
|
257
257
|
"case-pack": commandSeed({
|
|
258
258
|
reference: {
|
|
259
|
-
description: `Package browser-created
|
|
259
|
+
description: `Package browser-created case records from schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')} as a reviewed, audience-specific Case-pack v2 with current schema ${CASE_SCHEMA_VERSION}.`,
|
|
260
260
|
example: 'whoisleuth case-pack cases.json --audience trusted --reviewed --json',
|
|
261
|
-
boundary: 'The command creates a separate offline package without changing the source or durable
|
|
261
|
+
boundary: 'The command creates a separate offline package without changing the source or durable cases and requires an explicit review acknowledgement. Every audience, including public, retains domain evidence, case, pin and decision identifiers, tags, decision summaries and rationale. Review that analyst-authored content as well as the exclusions before sharing.',
|
|
262
262
|
},
|
|
263
|
-
collection: { scope: `Reads one bounded
|
|
263
|
+
collection: { scope: `Reads one bounded case export from schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')} and writes a separate audience-specific Case-pack v2.` },
|
|
264
264
|
summary: 'Build a reviewed case package',
|
|
265
265
|
options: ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
|
|
266
266
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { analyzeDomainIdn } from "../lib/idn-confusables.mjs";
|
|
2
|
+
import { profileSignals } from "../packages/analysis/brand-profile-signals.mjs";
|
|
3
|
+
import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "../packages/analysis/bounded-json.mjs";
|
|
4
|
+
import { canonicalRegistrableDomain } from "../packages/analysis/registrable-domain.mjs";
|
|
5
|
+
import { explainRiskScore } from "../packages/analysis/risk-scoring.mjs";
|
|
6
|
+
import { BRAND_PROFILE_SCHEMA, MAX_PROFILES, MAX_PROFILE_IMPORT_BYTES } from "../packages/contracts/workspace-portability.mjs";
|
|
7
|
+
import { CliUsageError } from "./errors.mjs";
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
10
|
+
}
|
|
11
|
+
/** Read one explicitly selected export, not browser storage or configuration. */
|
|
12
|
+
export async function readCliRiskProfile(source, readInput, selectedId) {
|
|
13
|
+
if (!source)
|
|
14
|
+
return null;
|
|
15
|
+
const text = await readInput(source, MAX_PROFILE_IMPORT_BYTES, 'Brand Profile export');
|
|
16
|
+
try {
|
|
17
|
+
const raw = parseBoundedJsonObject(text, {
|
|
18
|
+
maximumBytes: MAX_PROFILE_IMPORT_BYTES, limits: boundedJsonLimitsForBytes(MAX_PROFILE_IMPORT_BYTES), label: 'Brand Profile export',
|
|
19
|
+
});
|
|
20
|
+
if (raw.schema !== BRAND_PROFILE_SCHEMA || typeof raw.version !== 'number'
|
|
21
|
+
|| !Array.isArray(raw.profiles) || !raw.profiles.length || raw.profiles.length > MAX_PROFILES)
|
|
22
|
+
throw new TypeError();
|
|
23
|
+
const { normalizeBrandProfileId, normalizeBrandProfileStore } = await import("../packages/workspace/brand-profile-model.mjs");
|
|
24
|
+
if (selectedId !== undefined && normalizeBrandProfileId(selectedId) !== selectedId)
|
|
25
|
+
throw new TypeError();
|
|
26
|
+
const selected = selectedId === undefined ? raw.profiles : raw.profiles.filter(value => record(value).id === selectedId);
|
|
27
|
+
if (selected.length !== 1)
|
|
28
|
+
throw new TypeError();
|
|
29
|
+
const profile = normalizeBrandProfileStore({ ...raw, profiles: selected }).profiles[0];
|
|
30
|
+
if (!profile || !profile.officialDomains.length)
|
|
31
|
+
throw new TypeError();
|
|
32
|
+
// Private notes, contacts and other profile fields never enter output or
|
|
33
|
+
// checkpoints. Keep only the existing comparison inputs for this run.
|
|
34
|
+
return {
|
|
35
|
+
officialDomains: profile.officialDomains,
|
|
36
|
+
approvedPartnerDomains: profile.approvedPartnerDomains,
|
|
37
|
+
allowlistedDomains: profile.allowlistedDomains,
|
|
38
|
+
officialFaviconHash: profile.officialFaviconHash,
|
|
39
|
+
officialFaviconPHash: profile.officialFaviconPHash,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new CliUsageError('Use a supported Brand Profile export with a valid official domain; select one unambiguous profile with --brand-id if the export contains multiple profiles.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Adapt collected evidence, never an asserted score, lane or severity label. */
|
|
47
|
+
export function explainCliLookupRisk(document, profile, mode = document.mode) {
|
|
48
|
+
if (document.type !== undefined && document.type !== 'domain')
|
|
49
|
+
return null;
|
|
50
|
+
const availability = record(document.availability);
|
|
51
|
+
const domain = canonicalRegistrableDomain(document.registrableDomain ?? document.domain ?? availability.domain);
|
|
52
|
+
if (!domain || canonicalRegistrableDomain(availability.domain) !== domain)
|
|
53
|
+
return null;
|
|
54
|
+
const diagnostics = record(document.diagnostics);
|
|
55
|
+
const sourceCoverage = [
|
|
56
|
+
...['rdap', 'whois', 'availability'].map(source => ({ source, state: record(diagnostics[source]).status })),
|
|
57
|
+
...['dns', 'http', 'tls', 'pageIdentity'].map(source => ({
|
|
58
|
+
source: source === 'pageIdentity' ? 'page-identity' : source,
|
|
59
|
+
state: record(availability[source]).status,
|
|
60
|
+
})),
|
|
61
|
+
].map(item => ({ ...item, state: item.state === 'disabled' ? 'skipped' : item.state }));
|
|
62
|
+
return explainRiskScore({
|
|
63
|
+
domain,
|
|
64
|
+
availability: availability.state,
|
|
65
|
+
...profileSignals(domain, availability, profile),
|
|
66
|
+
hasActiveBrandProfile: Boolean(profile),
|
|
67
|
+
idnReferenceMatch: profile ? Boolean(analyzeDomainIdn(domain, profile.officialDomains)?.referenceMatches.length) : null,
|
|
68
|
+
mutationTypes: document.mutationTypes,
|
|
69
|
+
hasPasswordField: availability.hasPasswordField,
|
|
70
|
+
hasExternalFormAction: availability.hasExternalFormAction,
|
|
71
|
+
hasExternalPasswordForm: availability.hasExternalPasswordForm,
|
|
72
|
+
phishingLanguageMatch: availability.phishingLanguageMatch,
|
|
73
|
+
activityStatus: availability.activityStatus,
|
|
74
|
+
hasMx: availability.hasMx,
|
|
75
|
+
hasSpf: availability.hasSpf,
|
|
76
|
+
hasDmarc: availability.hasDmarc,
|
|
77
|
+
domainAgeDays: availability.domainAgeDays,
|
|
78
|
+
privacyProtected: availability.privacyProtected,
|
|
79
|
+
scanDepth: mode,
|
|
80
|
+
observedAt: document.observedAt ?? record(availability.http).observedAt,
|
|
81
|
+
sourceCoverage,
|
|
82
|
+
});
|
|
83
|
+
}
|
package/cli/saved-lookup.mjs
CHANGED
|
@@ -7,7 +7,8 @@ import { CliUsageError } from "./errors.mjs";
|
|
|
7
7
|
import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "../lib/homepage-metadata-contract.mjs";
|
|
8
8
|
import { registrarStandingObservedBy, resolveRegistrarIanaId, validRegistrarStanding, } from "../lib/registrar-standing-contract.mjs";
|
|
9
9
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { parseLookupCollectionReceipt } from "../packages/evidence/lookup-collection-receipt.mjs";
|
|
11
|
+
import { CLI_LOOKUP_SCHEMA as SAVED_LOOKUP_SCHEMA, CLI_LOOKUP_VERSION as SAVED_LOOKUP_SCHEMA_VERSION, PUBLIC_CLI_LOOKUP_VERSION as PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION, HOMEPAGE_CLI_LOOKUP_VERSION, COLLECTION_RECEIPT_CLI_LOOKUP_VERSION, MAX_CLI_LOOKUP_BYTES as MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_CLI_LOOKUP_JSON_CONTAINER_ITEMS as MAX_BOUNDED_JSON_CONTAINER_ITEMS, MAX_CLI_LOOKUP_JSON_DEPTH as MAX_BOUNDED_JSON_DEPTH, MAX_CLI_LOOKUP_JSON_KEYS as MAX_BOUNDED_JSON_KEYS, MAX_CLI_LOOKUP_JSON_VALUES as MAX_BOUNDED_JSON_VALUES, MAX_CLI_LOOKUP_STRING_LENGTH as MAX_SAVED_LOOKUP_STRING_LENGTH, SUPPORTED_CLI_LOOKUP_VERSIONS as SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS, } from "../packages/contracts/cli-lookup.mjs";
|
|
11
12
|
const RDAP_STATUSES = new Set([
|
|
12
13
|
'success', 'complete', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled',
|
|
13
14
|
'unavailable', 'rate_limited', 'not_applicable', 'stale',
|
|
@@ -317,23 +318,31 @@ function assertLookupEnvelope(document, label) {
|
|
|
317
318
|
const publicationMetadata = pageIdentity?.publicationMetadata;
|
|
318
319
|
const deliveryMetadata = httpResponse?.deliveryMetadata;
|
|
319
320
|
const registrarStanding = document.registrarStanding;
|
|
321
|
+
if (document.collectionReceipt !== undefined) {
|
|
322
|
+
if (Number(document.version) < COLLECTION_RECEIPT_CLI_LOOKUP_VERSION)
|
|
323
|
+
throw new TypeError(`${label} collection receipt requires saved Lookup version ${COLLECTION_RECEIPT_CLI_LOOKUP_VERSION}.`);
|
|
324
|
+
const receipt = parseLookupCollectionReceipt(document.collectionReceipt);
|
|
325
|
+
if (receipt.targetClass !== document.type || receipt.requestedDepth !== document.mode) {
|
|
326
|
+
throw new TypeError(`${label} collection receipt does not match the Lookup.`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
320
329
|
if (document.version === PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION
|
|
321
330
|
&& (publicationMetadata !== undefined || deliveryMetadata !== undefined || registrarStanding !== undefined)) {
|
|
322
|
-
throw new TypeError(`${label} version ${PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION} cannot contain version ${
|
|
331
|
+
throw new TypeError(`${label} version ${PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION} cannot contain version ${HOMEPAGE_CLI_LOOKUP_VERSION} evidence fields.`);
|
|
323
332
|
}
|
|
324
|
-
if (document.version
|
|
333
|
+
if (document.version >= HOMEPAGE_CLI_LOOKUP_VERSION
|
|
325
334
|
&& (publicationMetadata !== undefined && (!['success', 'partial'].includes(String(pageIdentity?.status))
|
|
326
335
|
|| !validPagePublicationMetadata(publicationMetadata))
|
|
327
336
|
|| deliveryMetadata !== undefined && (!['success', 'partial'].includes(String(http?.status))
|
|
328
337
|
|| !validHttpDeliveryMetadata(deliveryMetadata)))) {
|
|
329
338
|
throw new TypeError(`${label} contains invalid homepage metadata.`);
|
|
330
339
|
}
|
|
331
|
-
if (document.version
|
|
340
|
+
if (document.version >= HOMEPAGE_CLI_LOOKUP_VERSION
|
|
332
341
|
&& registrarStanding !== undefined
|
|
333
342
|
&& !validRegistrarStanding(registrarStanding)) {
|
|
334
343
|
throw new TypeError(`${label} contains invalid registrar standing.`);
|
|
335
344
|
}
|
|
336
|
-
if (document.version
|
|
345
|
+
if (document.version >= HOMEPAGE_CLI_LOOKUP_VERSION
|
|
337
346
|
&& registrarStanding !== undefined
|
|
338
347
|
&& !registrarStandingObservedBy(registrarStanding, generatedAt)) {
|
|
339
348
|
throw new TypeError(`${label} registrar standing was observed after the saved Lookup time.`);
|
|
@@ -346,7 +355,7 @@ function assertLookupEnvelope(document, label) {
|
|
|
346
355
|
if ((whoisStatus === 'complete' || whoisStatus === 'partial') && !whoisParsed) {
|
|
347
356
|
throw new TypeError('Successful WHOIS input is missing normalised parsed data.');
|
|
348
357
|
}
|
|
349
|
-
if (document.version
|
|
358
|
+
if (document.version >= HOMEPAGE_CLI_LOOKUP_VERSION
|
|
350
359
|
&& validRegistrarStanding(registrarStanding)
|
|
351
360
|
&& registrarStanding.ianaId !== resolveRegistrarIanaId(rdapParsed, whoisParsed)) {
|
|
352
361
|
throw new TypeError(`${label} registrar standing does not match its retained registration sources.`);
|
|
@@ -430,7 +439,7 @@ function buildCliLookupDocument(query, classified, result, generatedAt = new Dat
|
|
|
430
439
|
throw new TypeError('Lookup document generation time must be valid and include an explicit timezone.');
|
|
431
440
|
}
|
|
432
441
|
const classifiedSnapshot = snapshotKnownDataFields(classified, 'Lookup classification', new Set(['type', 'value', 'inputHostname', 'registrableDomain', 'isSubdomain']));
|
|
433
|
-
const contextSnapshot = snapshotKnownDataFields(collectionContext, 'Lookup collection context', new Set(['observerLabel', 'vantageLabel']));
|
|
442
|
+
const contextSnapshot = snapshotKnownDataFields(collectionContext, 'Lookup collection context', new Set(['observerLabel', 'vantageLabel', 'collectionReceipt']));
|
|
434
443
|
const observerLabel = typeof contextSnapshot.observerLabel === 'string'
|
|
435
444
|
? contextSnapshot.observerLabel
|
|
436
445
|
: null;
|
|
@@ -439,6 +448,12 @@ function buildCliLookupDocument(query, classified, result, generatedAt = new Dat
|
|
|
439
448
|
: null;
|
|
440
449
|
const resultSnapshot = snapshotLookupResultRoot(result);
|
|
441
450
|
delete resultSnapshot.collectionContext;
|
|
451
|
+
if (Object.hasOwn(contextSnapshot, 'collectionReceipt')) {
|
|
452
|
+
delete resultSnapshot.collectionReceipt;
|
|
453
|
+
const receipt = parseLookupCollectionReceipt(contextSnapshot.collectionReceipt);
|
|
454
|
+
if (receipt)
|
|
455
|
+
resultSnapshot.collectionReceipt = receipt;
|
|
456
|
+
}
|
|
442
457
|
const classifiedType = classifiedSnapshot.type;
|
|
443
458
|
if (typeof classifiedType !== 'string' || !QUERY_TYPES.has(classifiedType)) {
|
|
444
459
|
throw new TypeError('Lookup classification has an unsupported query type.');
|
|
@@ -730,6 +730,8 @@ export function buildSourceReliabilityReport(raw, generatedAt = new Date().toISO
|
|
|
730
730
|
});
|
|
731
731
|
}
|
|
732
732
|
export function formatSourceReliabilityReport(report) {
|
|
733
|
+
const duration = (value) => value
|
|
734
|
+
? `median ${value.median} ms / p95 ${value.p95} ms (range ${value.minimum}-${value.maximum} ms)` : 'unmeasured';
|
|
733
735
|
const lines = [
|
|
734
736
|
'WHOISleuth source reliability report',
|
|
735
737
|
`Documents: ${report.documentsReviewed}`,
|
|
@@ -741,16 +743,16 @@ export function formatSourceReliabilityReport(report) {
|
|
|
741
743
|
`Exceptions: ${report.totals.truncations} truncated · ${report.totals.rateLimits} rate-limited`,
|
|
742
744
|
];
|
|
743
745
|
for (const source of report.sources) {
|
|
744
|
-
const states = Object.entries(source.states).map(([state, count]) => `${state} ${count}`).join(', ') || '
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
}
|
|
753
|
-
lines.push('Privacy: targets 0 · queries 0 · raw evidence 0');
|
|
746
|
+
const states = Object.entries(source.states).map(([state, count]) => `${state} ${count}`).join(', ') || 'no states retained';
|
|
747
|
+
lines.push('', source.source, ` Collection states: ${states}`, ` Samples: states ${source.samples.states} · observations ${source.samples.observations} · Lookup timings ${source.samples.timing}`, ` Exceptions: ${source.truncationCount} truncated · ${source.rateLimitedCount} rate-limited`, ` Rates (0–1): failure ${source.rates.failure ?? 'unmeasured'} · partial ${source.rates.partial ?? 'unmeasured'}`, ` Observation duration: ${duration(source.durationMs.observation)}`, ` Lookup timing: ${duration(source.durationMs.lookupTiming)}`);
|
|
748
|
+
if (source.durationTrend.reportObservationMedian)
|
|
749
|
+
lines.push(` Merged observation-median distribution: ${duration(source.durationTrend.reportObservationMedian)}`);
|
|
750
|
+
if (source.durationTrend.reportTimingMedian)
|
|
751
|
+
lines.push(` Merged Lookup-median distribution: ${duration(source.durationTrend.reportTimingMedian)}`);
|
|
752
|
+
if (source.durationTimeline.length)
|
|
753
|
+
lines.push(` Retained report timeline: ${source.durationTimeline.length} points`);
|
|
754
|
+
}
|
|
755
|
+
lines.push('', 'Privacy: targets 0 · queries 0 · raw evidence 0');
|
|
754
756
|
for (const limitation of report.limitations)
|
|
755
757
|
lines.push(`Limitation: ${limitation}`);
|
|
756
758
|
return `${lines.join('\n')}\n`;
|
|
@@ -9,7 +9,7 @@ export const SUPPORT_ARGUMENT_PARSERS = Object.freeze({
|
|
|
9
9
|
doctor: (parsed) => ({
|
|
10
10
|
action: 'doctor',
|
|
11
11
|
network: parsed.hasOption('--network'),
|
|
12
|
-
output: jsonOutput(parsed),
|
|
12
|
+
output: parsed.hasOption('--summary-json') ? 'summary_json' : jsonOutput(parsed),
|
|
13
13
|
...terminalOptions(parsed),
|
|
14
14
|
}),
|
|
15
15
|
commands: (parsed) => ({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RISK_CALIBRATION_DATASET_SCHEMA, RISK_CALIBRATION_REPORT_SCHEMA } from "../packages/contracts/risk-calibration.mjs";
|
|
2
|
+
import { DOCTOR_SCHEMA, DOCTOR_SUMMARY_SCHEMA } from "../packages/contracts/cli-doctor.mjs";
|
|
2
3
|
import { positional, NO_POSITIONALS, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed, text } from "./command-definition.mjs";
|
|
3
4
|
export const SUPPORT_COMMAND_DEFINITIONS = Object.freeze({
|
|
4
5
|
completion: commandSeed({
|
|
@@ -25,17 +26,17 @@ export const SUPPORT_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
25
26
|
reference: {
|
|
26
27
|
description: 'Check the supported runtime and local terminal capabilities.',
|
|
27
28
|
example: 'whoisleuth doctor --json',
|
|
28
|
-
boundary: 'The default check is offline. Public DNS and port 43 checks run only
|
|
29
|
+
boundary: 'The default check is offline. Public DNS, HTTPS and port 43 checks run only with --network. --summary-json omits private context and free-form diagnostic details.',
|
|
29
30
|
},
|
|
30
31
|
collection: { scope: 'Network access is opt-in with --network and is limited to fixed public DNS, HTTPS, and WHOIS diagnostics.' },
|
|
31
32
|
summary: 'Check the local CLI runtime',
|
|
32
|
-
options: ['--network', '--json', '--quiet', '--no-color'],
|
|
33
|
+
options: ['--network', '--json', '--summary-json', '--quiet', '--no-color'],
|
|
33
34
|
positionals: NO_POSITIONALS,
|
|
34
|
-
constraints:
|
|
35
|
+
constraints: Object.freeze([constraint({ kind: 'mutually_exclusive', options: ['--json', '--summary-json'] })]),
|
|
35
36
|
handlerOwner: 'support',
|
|
36
37
|
networkEffect: 'conditional_network',
|
|
37
38
|
common: true,
|
|
38
|
-
schemaIdentifiers: Object.freeze([
|
|
39
|
+
schemaIdentifiers: Object.freeze([DOCTOR_SCHEMA, DOCTOR_SUMMARY_SCHEMA]),
|
|
39
40
|
primaryArtefacts: Object.freeze([]),
|
|
40
41
|
planSupport: false,
|
|
41
42
|
additionalOutputFormats: Object.freeze([]),
|
|
@@ -4,7 +4,7 @@ import { explainRiskScore, explainRiskScoreV8, RISK_MODEL_VERSION, RISK_REVIEW_T
|
|
|
4
4
|
import { buildCliCommandCatalogue, formatCliCommandCatalogue, selectCliCommands } from "./command-catalogue.mjs";
|
|
5
5
|
import { CLI_COMMAND_REGISTRY, CLI_COMMANDS, COMMAND_COLLECTION, COMMAND_DETAILS, COMMAND_USAGE, } from "./command-reference.mjs";
|
|
6
6
|
import { buildShellCompletion } from "./completion.mjs";
|
|
7
|
-
import { buildDoctorReport, formatDoctorReport } from "./doctor.mjs";
|
|
7
|
+
import { buildDoctorReport, buildDoctorSummary, formatDoctorReport } from "./doctor.mjs";
|
|
8
8
|
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
9
9
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
10
10
|
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
@@ -68,9 +68,11 @@ async function runDoctorCommand(args, dependencies, context) {
|
|
|
68
68
|
? await context.withProgress('Checking public DNS, HTTPS, and WHOIS connectivity', buildReport)
|
|
69
69
|
: await buildReport();
|
|
70
70
|
if (!args.quiet) {
|
|
71
|
-
context.writeStdout(args.output === '
|
|
72
|
-
? formatJsonDocument(report)
|
|
73
|
-
:
|
|
71
|
+
context.writeStdout(args.output === 'summary_json'
|
|
72
|
+
? formatJsonDocument(buildDoctorSummary(report))
|
|
73
|
+
: args.output === 'json'
|
|
74
|
+
? formatJsonDocument(report)
|
|
75
|
+
: context.terminal(formatDoctorReport(report), args.color));
|
|
74
76
|
}
|
|
75
77
|
return report.state === 'partial' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
76
78
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES } from "./investigation-recipes.mjs";
|
|
2
2
|
import { positional, OPTIONAL_FILE_POSITIONAL, constraint, commandSeed, integer, BASE_INTEGER_RANGE, file, optionDefinition } from "./command-definition.mjs";
|
|
3
3
|
import { DOMAIN_FEED_CATALOGUE } from "../packages/monitoring/domain-feed.mjs";
|
|
4
|
+
import { WATCHLIST_SCHEMA_VERSION } from "../packages/contracts/workspace-portability.mjs";
|
|
4
5
|
export const WORKFLOW_COMMAND_DEFINITIONS = Object.freeze({
|
|
5
6
|
'domain-feed': commandSeed({
|
|
6
7
|
reference: { description: 'Review exact hosts or literal terms in a selected local plain-domain feed.',
|
|
@@ -18,7 +19,7 @@ export const WORKFLOW_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
18
19
|
}),
|
|
19
20
|
'watchlist-review': commandSeed({
|
|
20
21
|
reference: { description: 'Preview or export an explicit candidate selection to a local watchlist.', example: 'whoisleuth watchlist-review plan selection.json --json',
|
|
21
|
-
boundary:
|
|
22
|
+
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 ${WATCHLIST_SCHEMA_VERSION} without creating a scan or baseline. Neither mode enables collection or scheduling.` },
|
|
22
23
|
collection: { scope: 'At most 2,000 selected candidate domains; no target requests.' },
|
|
23
24
|
summary: 'Review a local candidate watchlist handoff', options: ['--json', '--quiet', '--no-color'],
|
|
24
25
|
positionals: Object.freeze([positional('operation', 'enum', 1, 1, ['plan', 'export']), positional('source', 'file', 0, 1, [], 'argv_or_stdin')]),
|
|
@@ -88,6 +88,7 @@ async function runMonitorOnceCommand(args, dependencies, context) {
|
|
|
88
88
|
const executeLookup = dependencies.runUnifiedLookup || (await import("../lib/lookup.mjs")).runUnifiedLookup;
|
|
89
89
|
const progress = context.beginProgress('Collecting bounded domain-control evidence');
|
|
90
90
|
let document;
|
|
91
|
+
let currentSourceFailureCount = 0;
|
|
91
92
|
try {
|
|
92
93
|
document = await runDomainControlMonitor(manifestInput, previousInput, {
|
|
93
94
|
executeLookup,
|
|
@@ -95,6 +96,9 @@ async function runMonitorOnceCommand(args, dependencies, context) {
|
|
|
95
96
|
limit: args.limit,
|
|
96
97
|
concurrency: args.concurrency,
|
|
97
98
|
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
99
|
+
...(args.failOn?.includes('source-failure') ? {
|
|
100
|
+
onSourceFailures: (count) => { currentSourceFailureCount = count; },
|
|
101
|
+
} : {}),
|
|
98
102
|
onSettled: (completed, total) => progress.update(`Collected ${completed} of ${total} owned domains`),
|
|
99
103
|
});
|
|
100
104
|
}
|
|
@@ -108,7 +112,7 @@ async function runMonitorOnceCommand(args, dependencies, context) {
|
|
|
108
112
|
? formatCliJunit(document)
|
|
109
113
|
: context.terminal(formatDomainControlMonitor(document), args.color));
|
|
110
114
|
}
|
|
111
|
-
const policyFindings = evaluateCliFailPolicies(document, args.failOn || []);
|
|
115
|
+
const policyFindings = evaluateCliFailPolicies(document, args.failOn || [], null, currentSourceFailureCount);
|
|
112
116
|
if (policyFindings.length)
|
|
113
117
|
context.writeStderr(formatFailPolicyNotice(policyFindings));
|
|
114
118
|
return document.collection.failed || policyFindings.length
|
package/docs/cli-reference.md
CHANGED
|
@@ -71,11 +71,11 @@ candidate, form destination and deliberately observed navigation separately
|
|
|
71
71
|
attributed. A similar page without a logo or brand name can still merit manual
|
|
72
72
|
evidence review; correspondence does not establish authorship, rights authority
|
|
73
73
|
or maliciousness. Missing optional capture or comparison evidence does not
|
|
74
|
-
prevent ordinary
|
|
74
|
+
prevent ordinary case pins and conclusions. Installed command help remains the
|
|
75
75
|
authority for offline file inputs and explicit collection plans.
|
|
76
76
|
|
|
77
77
|
The [reporting guide](https://www.whoisleuth.com/resources/reporting-and-takedown-guidance)
|
|
78
|
-
uses the same reviewed official-route catalogue as the
|
|
78
|
+
uses the same reviewed official-route catalogue as the console for social
|
|
79
79
|
content, storefronts, app listings, hosted files/forms and advertising. Verify
|
|
80
80
|
the exact object, current route, reporter authority and disclosure before any
|
|
81
81
|
manual submission. Fraud, order complaints and rights notices are different
|
|
@@ -96,8 +96,10 @@ PKIX, STARTTLS and SMTP transport remain separate evidence states.
|
|
|
96
96
|
STARTTLS detection uses the full bounded reply. If the retained capability
|
|
97
97
|
inventory is shortened, the report remains partial and names the omission.
|
|
98
98
|
|
|
99
|
-
The
|
|
100
|
-
|
|
99
|
+
The separate optional rendered-capture companion is distributed as a locally
|
|
100
|
+
assembled archive, not as part of the hosted application or main CLI, and is
|
|
101
|
+
not published to a package registry. Its sandboxed, bounded browser executes
|
|
102
|
+
remote page JavaScript only for an explicitly
|
|
101
103
|
authorised route set, and each admitted resource operator receives the exact
|
|
102
104
|
requested URL. Captures and screenshots remain local until the operator deletes
|
|
103
105
|
them.
|
|
@@ -126,6 +128,13 @@ source state is unmeasured, not a negative result. `--csv` keeps the compact col
|
|
|
126
128
|
`--fail-on` and `--strict-exit` expose selected evidence states to automation
|
|
127
129
|
without changing the result document. Review focused help for the policies a
|
|
128
130
|
command supports.
|
|
131
|
+
Lookup, Bulk and Discover scan evaluate all collected and restored evidence,
|
|
132
|
+
not only the rows retained by a filter. `danger` uses the canonical Risk model;
|
|
133
|
+
select `--deep --brand-profile brand.json` for local reference comparisons, adding
|
|
134
|
+
`--brand-id <exact-profile-id>` for a multi-profile export. A match returns 4 and identifies model and evidence
|
|
135
|
+
quality on stderr. Missing/ineligible evidence is not zero risk and does not
|
|
136
|
+
implicitly select another failure policy. See the
|
|
137
|
+
[automation examples and limits](cli.md#formats-and-exit-behaviour).
|
|
129
138
|
|
|
130
139
|
| Code | Meaning |
|
|
131
140
|
| ---: | --- |
|
|
@@ -143,18 +152,46 @@ Readers accept only their declared public and current versions. Unknown,
|
|
|
143
152
|
unreleased historical and future schemas fail before partial interpretation;
|
|
144
153
|
an invalid import is not treated as an empty document.
|
|
145
154
|
|
|
146
|
-
`inspect-archive` reads current workspace archive
|
|
155
|
+
`inspect-archive` reads current workspace archive v10 and exact versions 5–9. It
|
|
147
156
|
reports section metadata and digest-only search results unless `--reveal` is
|
|
148
157
|
explicitly selected. It never searches notes, contacts or arbitrary raw fields.
|
|
149
158
|
|
|
150
|
-
`export` reads supported saved Lookup
|
|
151
|
-
evidence schema
|
|
152
|
-
readable. Versions 27–
|
|
159
|
+
`export` reads supported saved Lookup versions 1–3 and writes current Lookup
|
|
160
|
+
evidence schema 31. Supported schemas 27–30 and exact v1 schema 26 remain
|
|
161
|
+
readable. Versions 27–31 exclude raw registration payloads, expanded
|
|
153
162
|
contacts, credentials, complete query-bearing URLs and provider payloads;
|
|
154
|
-
|
|
163
|
+
Schemas 28 and later also retain the bounded registrar-standing projection.
|
|
155
164
|
Schema 29 identifies the hostname used for DNS, TLS and web observations,
|
|
156
165
|
separately from the registrable domain used by registration sources. Older
|
|
157
166
|
evidence retains its original collection scope.
|
|
167
|
+
Schema 31 retains WHOIS registration classification separately from collection
|
|
168
|
+
completion. Lifecycle interpretation requires positive registration evidence;
|
|
169
|
+
a completed negative or inconclusive WHOIS reply does not establish registration
|
|
170
|
+
or absence of locks. Earlier exports retain their exact original interpretation
|
|
171
|
+
for historical verification and are not silently relabelled.
|
|
172
|
+
|
|
173
|
+
New WHOIS collection distinguishes confirmed no-service discovery from an
|
|
174
|
+
incomplete discovery response. Empty, refused or unrecognised bootstrap
|
|
175
|
+
responses remain partial; they do not establish that WHOIS is unsupported.
|
|
176
|
+
`--strict-exit` treats that incomplete attempt as a source failure. Explicit
|
|
177
|
+
no-request access-policy restrictions remain distinct from transport failures.
|
|
178
|
+
|
|
179
|
+
Saved Lookup version 3 can retain a bounded collection receipt: original
|
|
180
|
+
optional selections, admitted source families, collector starts, outcomes and
|
|
181
|
+
explicitly known completeness. `lookup --verbose` and `lookup --browse`
|
|
182
|
+
show this account with source-specific explanations and qualified retry guidance.
|
|
183
|
+
Earlier files have no inferred receipt. The receipt contains
|
|
184
|
+
no target values, URLs, contacts or raw errors and does not prove network
|
|
185
|
+
delivery. It stays in full private Lookup JSON; the minimised `export` report
|
|
186
|
+
does not include it. `--plan` remains offline and produces no completed receipt.
|
|
187
|
+
|
|
188
|
+
Case schema 20 also retains this context when an analyst saves a completed Lookup
|
|
189
|
+
assessment or selected checkpoint facts. A selected checkpoint saves only its
|
|
190
|
+
selected pins and one payload-free receipt, not every Lookup result. Ordinary
|
|
191
|
+
backups and internal case packs preserve it. Trusted/public case packs, case
|
|
192
|
+
reports and response packets omit receipts; shared assessment identities are
|
|
193
|
+
recomputed without them. Historical case formats remain readable without
|
|
194
|
+
inventing missing collection choices.
|
|
158
195
|
|
|
159
196
|
`verify-artifact` checks a recognised structure and its applicable integrity
|
|
160
197
|
contract. `interchange-report` describes retained and omitted fields.
|
|
@@ -162,6 +199,14 @@ contract. `interchange-report` describes retained and omitted fields.
|
|
|
162
199
|
validity and signer trust separate. None of these checks establishes that
|
|
163
200
|
evidence is accurate, current, safe to share or attributable to a person.
|
|
164
201
|
|
|
202
|
+
The text output of `verify-artifact` suggests compatible offline follow-up
|
|
203
|
+
commands for recognised saved Lookups, case exports, workspace archives,
|
|
204
|
+
Lookup-evidence reports, Case-packs and managed indicator revisions. These are
|
|
205
|
+
examples with placeholder filenames, not executed actions or a sharing approval.
|
|
206
|
+
Replace the filenames deliberately; a case or indicator inspection can display
|
|
207
|
+
private contents. Encrypted workspace guidance uses a separate passphrase file,
|
|
208
|
+
never a passphrase in command history. JSON output and `--quiet` are unchanged.
|
|
209
|
+
|
|
165
210
|
### Signer trust
|
|
166
211
|
|
|
167
212
|
Use `verify-signature package.json --trust-store-file trust.json --json` to
|
|
@@ -211,9 +256,9 @@ own retention policy. No automatic key discovery, key storage or trust refresh o
|
|
|
211
256
|
control, activity, intent or maliciousness.
|
|
212
257
|
- Bulk applies one declared collection contract per target. Fast accepts up to
|
|
213
258
|
500 targets and Deep up to 50; each target remains a separate request.
|
|
214
|
-
- Respond commands package browser-created
|
|
259
|
+
- Respond commands package browser-created cases and prepare local packets,
|
|
215
260
|
reports and sharing reviews. The offline [`case` workflow](cli.md#local-case-files)
|
|
216
|
-
also creates and updates ordinary local
|
|
261
|
+
also creates and updates ordinary local case files through explicit, conflict-checked
|
|
217
262
|
output. Neither workflow submits, publishes, notifies or grants recipient authorisation.
|
|
218
263
|
- Assurance, comparison and calibration commands describe supplied or retained
|
|
219
264
|
evidence. They do not tune the running model, change infrastructure or turn an
|