@slicedearth/whoisleuth-cli 2.3.0 → 2.4.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/NOTICE +6 -0
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +68 -6
- package/cli/artifact-validation/case-response.mjs +47 -41
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +83 -61
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +39 -31
- package/cli/command-argument-grammar.mjs +4 -0
- package/cli/command-reference.mjs +145 -75
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +44 -16
- package/cli/discovery-scan-command-runner.mjs +2 -0
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +1 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +4 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +45 -0
- package/cli/runner.mjs +55 -6
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +76 -30
- package/docs/cli.md +289 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +43 -3
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +36 -23
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +7 -2
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +219 -118
- package/lib/domain-control-manifest.mjs +117 -57
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +113 -26
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -592
- package/lib/evidence-report.mjs +3 -1
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +82 -85
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +7 -10
- package/lib/lookup-source-progress.mjs +17 -14
- package/lib/lookup.mjs +48 -27
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +17 -3
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +10 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +59 -25
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-core.mjs +3 -2
- package/packages/cases/case-record-operations.mjs +89 -27
- package/packages/cases/case-record-projection.mjs +23 -20
- package/packages/cases/case-report.mjs +63 -52
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +41 -1129
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +38 -587
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +25 -0
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/page-similarity.mjs +2 -1
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +279 -116
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +1 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +160 -225
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +89 -49
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +158 -20
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +11 -5
- package/packages/workspace/bulk-session-model.mjs +95 -20
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -26
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +70 -30
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +85 -21
- package/third-party-notices.txt +29 -2
- package/packages/investigation/bulk-review-export.mjs +0 -63
package/cli/lookup-diff.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { buildBulkDomainComparison, } from "../packages/investigation/bulk-domai
|
|
|
4
4
|
import { relationshipObservation } from "../packages/comparison/relationship-evidence.mjs";
|
|
5
5
|
import { CliUsageError } from "./errors.mjs";
|
|
6
6
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
7
|
+
import { lookupObservationHostname } from "../packages/evidence/lookup-target.mjs";
|
|
7
8
|
export const CLI_LOOKUP_DIFF_SCHEMA = 'whoisleuth.cli.lookup-diff';
|
|
8
9
|
export const CLI_LOOKUP_DIFF_VERSION = 1;
|
|
9
10
|
function text(value, maximum = 300) {
|
|
@@ -129,19 +130,22 @@ function lookupComparisonInput(document) {
|
|
|
129
130
|
function buildCliLookupDiff(leftText, rightText, generatedAt = new Date().toISOString(), options = {}) {
|
|
130
131
|
const left = parseSavedLookupDocument(leftText, { label: 'Left lookup input' });
|
|
131
132
|
const right = parseSavedLookupDocument(rightText, { label: 'Right lookup input' });
|
|
132
|
-
const domainMode = options.domainMode
|
|
133
|
+
const domainMode = options.domainMode === 'auto'
|
|
134
|
+
? left.registrableDomain === right.registrableDomain ? 'same' : 'different'
|
|
135
|
+
: options.domainMode ?? 'different';
|
|
133
136
|
if (domainMode === 'different' && left.registrableDomain === right.registrableDomain) {
|
|
134
137
|
throw new CliUsageError('Lookup diff requires documents for two different domains.');
|
|
135
138
|
}
|
|
136
139
|
if (domainMode === 'same' && left.registrableDomain !== right.registrableDomain) {
|
|
137
140
|
throw new CliUsageError('Lookup history requires observations for the same domain.');
|
|
138
141
|
}
|
|
139
|
-
const
|
|
142
|
+
const measuredComparison = buildBulkDomainComparison(lookupComparisonInput(left), lookupComparisonInput(right), domainMode === 'same' ? right.generatedAt : null, {
|
|
140
143
|
allowSameDomain: domainMode === 'same',
|
|
141
144
|
...(domainMode === 'same' ? { now: Date.parse(right.generatedAt) } : {}),
|
|
142
145
|
});
|
|
143
|
-
if (!
|
|
146
|
+
if (!measuredComparison)
|
|
144
147
|
throw new CliUsageError('Lookup documents could not be compared.');
|
|
148
|
+
const comparison = qualifyLookupComparison(measuredComparison, left, right, domainMode === 'same');
|
|
145
149
|
return {
|
|
146
150
|
schema: CLI_LOOKUP_DIFF_SCHEMA,
|
|
147
151
|
version: CLI_LOOKUP_DIFF_VERSION,
|
|
@@ -160,6 +164,36 @@ function buildCliLookupDiff(leftText, rightText, generatedAt = new Date().toISOS
|
|
|
160
164
|
],
|
|
161
165
|
};
|
|
162
166
|
}
|
|
167
|
+
function qualifyLookupComparison(comparison, left, right, temporal) {
|
|
168
|
+
const leftScope = recordOrEmpty(left.availability);
|
|
169
|
+
const rightScope = recordOrEmpty(right.availability);
|
|
170
|
+
const hostnameChanged = temporal && lookupObservationHostname(leftScope) !== lookupObservationHostname(rightScope);
|
|
171
|
+
const selectedPage = leftScope.webObservationMode === 'selected_url' || rightScope.webObservationMode === 'selected_url';
|
|
172
|
+
if (!hostnameChanged && !selectedPage)
|
|
173
|
+
return comparison;
|
|
174
|
+
const limitation = 'The retained collection hostname differs or a selected URL has no retained complete path/query identity. Values remain visible, but those fields cannot establish change at the same target.';
|
|
175
|
+
const rows = comparison.rows.map((row) => {
|
|
176
|
+
const web = ['web', 'technology', 'identity'].includes(row.category);
|
|
177
|
+
const incomparable = hostnameChanged && !['registration', 'lifecycle', 'source'].includes(row.category)
|
|
178
|
+
|| temporal && selectedPage && web;
|
|
179
|
+
return {
|
|
180
|
+
...row,
|
|
181
|
+
...(selectedPage && web ? { method: 'Bounded website observation; one or both captures concern a selected URL rather than the homepage' } : {}),
|
|
182
|
+
...(incomparable ? {
|
|
183
|
+
state: 'not_recorded',
|
|
184
|
+
source: `${row.source}; comparable target identity not recorded`,
|
|
185
|
+
leftSourceState: 'not_recorded',
|
|
186
|
+
rightSourceState: 'not_recorded',
|
|
187
|
+
limitations: [...row.limitations, limitation],
|
|
188
|
+
} : {}),
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
return {
|
|
192
|
+
...comparison, rows,
|
|
193
|
+
counts: Object.fromEntries(Object.keys(comparison.counts).map((state) => [state, rows.filter((row) => row.state === state).length])),
|
|
194
|
+
limitations: [...comparison.limitations, limitation],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
163
197
|
function formatCliLookupDiff(document) {
|
|
164
198
|
const rows = document.comparison.rows.filter((row) => row.state !== 'equal');
|
|
165
199
|
const output = [
|
package/cli/lookup-plan.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const FAST_DOMAIN_EVIDENCE = Object.freeze({
|
|
|
21
21
|
const DEEP_DOMAIN_EVIDENCE = Object.freeze({
|
|
22
22
|
source: 'domain_evidence',
|
|
23
23
|
purpose: 'Collect bounded DNS, HTTP, TLS, page-identity, technology, and security-posture evidence.',
|
|
24
|
-
disclosure: 'DNS
|
|
24
|
+
disclosure: 'DNS, TLS and website probes use the submitted hostname. Registration-delegation queries use the registrable domain.',
|
|
25
25
|
conditional: false,
|
|
26
26
|
});
|
|
27
27
|
const REGISTRAR_RDAP = Object.freeze({
|
|
@@ -42,19 +42,23 @@ const REVERSE_DNS = Object.freeze({
|
|
|
42
42
|
disclosure: 'A DNS resolver receives the reverse lookup for the normalised IP address.',
|
|
43
43
|
conditional: false,
|
|
44
44
|
});
|
|
45
|
-
function plannedSources(classified, deep) {
|
|
45
|
+
function plannedSources(classified, deep, selectedUrl) {
|
|
46
46
|
if (!deep) {
|
|
47
47
|
return Object.freeze(classified.type === 'domain' ? [RDAP, FAST_DOMAIN_EVIDENCE] : [RDAP]);
|
|
48
48
|
}
|
|
49
49
|
if (classified.type === 'domain') {
|
|
50
|
-
|
|
50
|
+
const domainEvidence = selectedUrl ? Object.freeze({
|
|
51
|
+
...DEEP_DOMAIN_EVIDENCE,
|
|
52
|
+
disclosure: 'The selected URL path and query are sent to the website and followed redirects. The fragment is not sent. DNS and TLS use its hostname; registration uses the registrable domain. Saved HTTP provenance omits queries, but page-derived text may contain sensitive information.',
|
|
53
|
+
}) : DEEP_DOMAIN_EVIDENCE;
|
|
54
|
+
return Object.freeze([RDAP, WHOIS, domainEvidence, REGISTRAR_RDAP, NETWORK_CONTEXT]);
|
|
51
55
|
}
|
|
52
56
|
if (classified.type === 'ipv4' || classified.type === 'ipv6') {
|
|
53
57
|
return Object.freeze([RDAP, WHOIS, REVERSE_DNS]);
|
|
54
58
|
}
|
|
55
59
|
return Object.freeze([RDAP, WHOIS]);
|
|
56
60
|
}
|
|
57
|
-
function buildCliLookupPlan(query, classified, deep) {
|
|
61
|
+
function buildCliLookupPlan(query, classified, deep, selectedUrl = false) {
|
|
58
62
|
return Object.freeze({
|
|
59
63
|
schema: CLI_LOOKUP_PLAN_SCHEMA,
|
|
60
64
|
version: CLI_LOOKUP_PLAN_VERSION,
|
|
@@ -69,7 +73,7 @@ function buildCliLookupPlan(query, classified, deep) {
|
|
|
69
73
|
planning: Object.freeze({
|
|
70
74
|
networkRequestsMade: false,
|
|
71
75
|
collectionRequiresNetwork: true,
|
|
72
|
-
sources: plannedSources(classified, deep),
|
|
76
|
+
sources: plannedSources(classified, deep, selectedUrl),
|
|
73
77
|
}),
|
|
74
78
|
limitations: Object.freeze([
|
|
75
79
|
'This is a local preflight. It does not test source availability, feature configuration, cache state, redirects, referrals, or the exact number of requests a completed lookup may require.',
|
|
@@ -81,6 +85,8 @@ function formatCliLookupPlan(plan) {
|
|
|
81
85
|
const lines = [
|
|
82
86
|
'WHOISleuth lookup preflight',
|
|
83
87
|
`Target: ${plan.target.normalized}`,
|
|
88
|
+
...(plan.target.inputHostname && plan.target.inputHostname !== plan.target.normalized
|
|
89
|
+
? [`Submitted hostname: ${plan.target.inputHostname}`] : []),
|
|
84
90
|
`Type: ${plan.target.type}`,
|
|
85
91
|
`Mode: ${plan.mode}`,
|
|
86
92
|
'Network requests made: no',
|
package/cli/lookup-reconcile.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliUsageError } from "./errors.mjs";
|
|
2
2
|
import { buildCliLookupDiff } from "./lookup-diff.mjs";
|
|
3
3
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
4
|
+
import { compareObservationContexts } from "../packages/comparison/capture-context.mjs";
|
|
4
5
|
const CLI_LOOKUP_RECONCILIATION_SCHEMA = 'whoisleuth.cli.lookup-reconciliation';
|
|
5
6
|
const CLI_LOOKUP_RECONCILIATION_VERSION = 1;
|
|
6
7
|
const MAX_LOOKUP_RECONCILIATION_INPUT_BYTES = 32 * 1024 * 1024;
|
|
@@ -79,15 +80,11 @@ function buildCliLookupReconciliation(inputs, generatedAt = new Date().toISOStri
|
|
|
79
80
|
};
|
|
80
81
|
});
|
|
81
82
|
const observations = documents.map(observationContext);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
: null));
|
|
85
|
-
const labelsComplete = identities.every((identity) => identity !== null);
|
|
86
|
-
const labelsDistinct = labelsComplete && new Set(identities).size === identities.length;
|
|
87
|
-
const independence = labelsDistinct
|
|
83
|
+
const context = compareObservationContexts(observations.map(observation => ({ ...observation, observedAt: observation.generatedAt })));
|
|
84
|
+
const independence = context.labels === 'distinct'
|
|
88
85
|
? {
|
|
89
86
|
state: 'verified_distinct_labels',
|
|
90
|
-
reason: 'Every observation carries a
|
|
87
|
+
reason: 'Every observation carries a distinct analyst-supplied observer/vantage label pair. Actual collection independence is not verified.',
|
|
91
88
|
}
|
|
92
89
|
: {
|
|
93
90
|
state: 'unverified',
|
|
@@ -117,15 +114,19 @@ function buildCliLookupReconciliation(inputs, generatedAt = new Date().toISOStri
|
|
|
117
114
|
};
|
|
118
115
|
}
|
|
119
116
|
function formatCliLookupReconciliation(document) {
|
|
117
|
+
const context = compareObservationContexts(document.observations.map(observation => ({ ...observation, observedAt: observation.generatedAt })));
|
|
120
118
|
const output = [
|
|
121
119
|
'Lookup observation reconciliation',
|
|
122
120
|
`Domain ${document.domain}`,
|
|
123
121
|
`Observations ${document.summary.observationCount}`,
|
|
124
|
-
`
|
|
122
|
+
`Declared labels ${context.labels}; collection independence not verified`,
|
|
123
|
+
`Capture time span ${context.time.spanMilliseconds === null ? 'unknown' : `${context.time.spanMilliseconds / 1000} seconds`}`,
|
|
125
124
|
`Agreement ${document.summary.agreement}`,
|
|
126
125
|
`Disagreement ${document.summary.disagreement}`,
|
|
127
126
|
`Non-comparable ${document.summary.nonComparable}`,
|
|
128
127
|
'',
|
|
128
|
+
...document.observations.map((observation, index) => `Observation ${index + 1}: ${observation.generatedAt} · ${observation.mode} · observer ${observation.observerLabel ?? 'not declared'} · vantage ${observation.vantageLabel ?? 'not declared'}`),
|
|
129
|
+
'',
|
|
129
130
|
];
|
|
130
131
|
for (const field of document.fields.filter((candidate) => candidate.state !== 'agreement')) {
|
|
131
132
|
output.push(`${field.label} [${field.state.replaceAll('_', ' ')}]`);
|
|
@@ -136,6 +137,7 @@ function formatCliLookupReconciliation(document) {
|
|
|
136
137
|
output.push('', 'Limitations:');
|
|
137
138
|
for (const limitation of document.limitations)
|
|
138
139
|
output.push(` - ${limitation}`);
|
|
140
|
+
output.push(' - These selected observations do not establish worldwide availability, removal or takedown.');
|
|
139
141
|
return `${output.join('\n')}\n`;
|
|
140
142
|
}
|
|
141
143
|
export { CLI_LOOKUP_RECONCILIATION_SCHEMA, CLI_LOOKUP_RECONCILIATION_VERSION, MAX_LOOKUP_RECONCILIATION_INPUT_BYTES, buildCliLookupReconciliation, formatCliLookupReconciliation, };
|
package/cli/mail-review.mjs
CHANGED
|
@@ -3,9 +3,11 @@ import { getDomain } from 'tldts';
|
|
|
3
3
|
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
4
4
|
import { isValidAsciiDomainName } from "../lib/hostname.mjs";
|
|
5
5
|
import { isRecord, recordOrEmpty } from "../lib/json-record.mjs";
|
|
6
|
+
import { classifyQuery } from "../lib/classify.mjs";
|
|
7
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
6
8
|
import { CliUsageError } from "./errors.mjs";
|
|
7
9
|
export const CLI_MAIL_REVIEW_SCHEMA = 'whoisleuth.cli.mail-review';
|
|
8
|
-
export const CLI_MAIL_REVIEW_VERSION =
|
|
10
|
+
export const CLI_MAIL_REVIEW_VERSION = 4;
|
|
9
11
|
export const MAX_MAIL_REVIEW_INPUT_BYTES = 16 * 1024 * 1024;
|
|
10
12
|
export const MAX_MAIL_REVIEW_ROWS = 500;
|
|
11
13
|
const UNSAFE_TEXT_RE = /[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f]/u;
|
|
@@ -20,10 +22,10 @@ function boolean(value) {
|
|
|
20
22
|
return typeof value === 'boolean' ? value : null;
|
|
21
23
|
}
|
|
22
24
|
function timestamp(value, label) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return
|
|
25
|
+
const result = normalizeExplicitIsoTimestamp(value);
|
|
26
|
+
if (!result)
|
|
27
|
+
throw new CliUsageError(`${label} must be a valid bounded timestamp with an explicit timezone.`);
|
|
28
|
+
return result;
|
|
27
29
|
}
|
|
28
30
|
function booleanOrNull(value, label) {
|
|
29
31
|
if (value === null || typeof value === 'boolean')
|
|
@@ -87,8 +89,8 @@ function validateMxEvidence(availability, summary, label, hasMx, hasNullMx) {
|
|
|
87
89
|
function validateMailRow(value, index, expectedVersion) {
|
|
88
90
|
const item = record(value);
|
|
89
91
|
const label = `Mail review row ${index + 1}`;
|
|
90
|
-
const version =
|
|
91
|
-
if (item.schema !== 'whoisleuth.cli.bulk.item' || ![2, 3].includes(version)
|
|
92
|
+
const version = item.version;
|
|
93
|
+
if (item.schema !== 'whoisleuth.cli.bulk.item' || typeof version !== 'number' || ![2, 3].includes(version)
|
|
92
94
|
|| (expectedVersion !== null && version !== expectedVersion)) {
|
|
93
95
|
throw new CliUsageError(`${label} must use the matching WHOISleuth Bulk item schema version 2 or 3.`);
|
|
94
96
|
}
|
|
@@ -193,6 +195,7 @@ function normalizeMailRow(value) {
|
|
|
193
195
|
const providers = providerDomains(hosts);
|
|
194
196
|
return {
|
|
195
197
|
domain,
|
|
198
|
+
provenance: mailRowProvenance(item),
|
|
196
199
|
state,
|
|
197
200
|
dnsStatus,
|
|
198
201
|
hasMx,
|
|
@@ -210,6 +213,29 @@ function normalizeMailRow(value) {
|
|
|
210
213
|
],
|
|
211
214
|
};
|
|
212
215
|
}
|
|
216
|
+
function mailRowProvenance(item) {
|
|
217
|
+
return Object.freeze({
|
|
218
|
+
sourceGeneratedAt: timestamp(item.generatedAt, 'Bulk row generation time'),
|
|
219
|
+
observedAt: item.version === 3 ? normalizeExplicitIsoTimestamp(item.observedAt) : null,
|
|
220
|
+
dnsObservedAt: item.ok === true ? normalizeExplicitIsoTimestamp(record(record(item.availability).dns).observedAt) : null,
|
|
221
|
+
collectionOrigin: item.version === 3
|
|
222
|
+
? item.collectionOrigin
|
|
223
|
+
: 'unknown',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function failedMailRow(item, index) {
|
|
227
|
+
let target = null;
|
|
228
|
+
try {
|
|
229
|
+
target = classifyQuery(String(item.query)).value;
|
|
230
|
+
}
|
|
231
|
+
catch { /* Invalid targets remain identifiable by input position. */ }
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
rowNumber: index + 1,
|
|
234
|
+
target,
|
|
235
|
+
state: 'collection_failed',
|
|
236
|
+
provenance: mailRowProvenance(item),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
213
239
|
function parseInput(textValue) {
|
|
214
240
|
if (typeof textValue !== 'string' || Buffer.byteLength(textValue, 'utf8') > MAX_MAIL_REVIEW_INPUT_BYTES) {
|
|
215
241
|
throw new CliUsageError(`Mail review input is limited to ${MAX_MAIL_REVIEW_INPUT_BYTES} bytes.`);
|
|
@@ -223,12 +249,12 @@ function parseInput(textValue) {
|
|
|
223
249
|
scanBoundedJson(textInput);
|
|
224
250
|
const parsed = JSON.parse(textInput);
|
|
225
251
|
const document = record(parsed);
|
|
226
|
-
if (document.schema === 'whoisleuth.cli.bulk' && [2, 3].includes(
|
|
252
|
+
if (document.schema === 'whoisleuth.cli.bulk' && typeof document.version === 'number' && [2, 3].includes(document.version) && Array.isArray(document.results)) {
|
|
227
253
|
values = document.results;
|
|
228
254
|
containerVersion = Number(document.version);
|
|
229
255
|
timestamp(document.generatedAt, 'Mail review Bulk generation time');
|
|
230
256
|
}
|
|
231
|
-
else if (document.schema === 'whoisleuth.cli.bulk.item' && [2, 3].includes(
|
|
257
|
+
else if (document.schema === 'whoisleuth.cli.bulk.item' && typeof document.version === 'number' && [2, 3].includes(document.version)) {
|
|
232
258
|
values = [document];
|
|
233
259
|
}
|
|
234
260
|
else {
|
|
@@ -258,6 +284,13 @@ function buildCliMailReview(textValue, generatedAt = new Date().toISOString()) {
|
|
|
258
284
|
const rows = inputRows.map(normalizeMailRow).filter((row) => Boolean(row));
|
|
259
285
|
if (!rows.length)
|
|
260
286
|
throw new CliUsageError('Mail review input contains no completed domain results.');
|
|
287
|
+
const failedRows = inputRows.flatMap((row, index) => row.ok === false ? [failedMailRow(row, index)] : []);
|
|
288
|
+
const inputCoverage = Object.freeze({
|
|
289
|
+
inputRows: inputRows.length,
|
|
290
|
+
reviewedRows: rows.length,
|
|
291
|
+
failedRows: failedRows.length,
|
|
292
|
+
unmeasuredObservationRows: [...rows, ...failedRows].filter((row) => row.provenance.observedAt === null).length,
|
|
293
|
+
});
|
|
261
294
|
const counts = {
|
|
262
295
|
authenticated_mail: 0,
|
|
263
296
|
evidence_incomplete: 0,
|
|
@@ -295,10 +328,14 @@ function buildCliMailReview(textValue, generatedAt = new Date().toISOString()) {
|
|
|
295
328
|
})
|
|
296
329
|
.sort((left, right) => left.providerDomain.localeCompare(right.providerDomain));
|
|
297
330
|
const providerRelationships = allProviderRelationships.slice(0, 100);
|
|
331
|
+
const rowsWithIncompleteDns = rows.filter((row) => row.dnsStatus !== 'success' || row.hasMx === null || row.hasNullMx === null).length;
|
|
298
332
|
const providerCoverage = {
|
|
299
|
-
complete:
|
|
333
|
+
complete: failedRows.length === 0 && rowsWithIncompleteDns === 0
|
|
334
|
+
&& rows.every((row) => !row.providerDomainsTruncated)
|
|
300
335
|
&& allProviderRelationships.length <= 100
|
|
301
336
|
&& allProviderRelationships.every((relationship) => !relationship.domainsTruncated),
|
|
337
|
+
failedRows: failedRows.length,
|
|
338
|
+
rowsWithIncompleteDns,
|
|
302
339
|
rowsWithOmittedProviders: rows.filter((row) => row.providerDomainsTruncated).length,
|
|
303
340
|
omittedProviderDomains: rows.reduce((sum, row) => sum + row.providerDomainsOmitted, 0),
|
|
304
341
|
relationshipCount: allProviderRelationships.length,
|
|
@@ -309,16 +346,20 @@ function buildCliMailReview(textValue, generatedAt = new Date().toISOString()) {
|
|
|
309
346
|
return {
|
|
310
347
|
schema: CLI_MAIL_REVIEW_SCHEMA,
|
|
311
348
|
version: CLI_MAIL_REVIEW_VERSION,
|
|
312
|
-
generatedAt,
|
|
349
|
+
generatedAt: timestamp(generatedAt, 'Mail review generation time'),
|
|
313
350
|
counts,
|
|
314
351
|
rows,
|
|
352
|
+
failedRows,
|
|
353
|
+
inputCoverage,
|
|
315
354
|
providerRelationships,
|
|
316
355
|
providerCoverage,
|
|
317
356
|
limitations: [
|
|
318
357
|
'This review is passive and uses DNS evidence already retained in a WHOISleuth Bulk result; it makes no network request.',
|
|
319
358
|
'Null MX, no explicit MX, receiving mail, authentication gaps, and incomplete evidence remain separate states.',
|
|
320
359
|
'SMTP delivery, mailbox existence, catch-all behaviour, banner collection, and message acceptance were not tested.',
|
|
321
|
-
...(
|
|
360
|
+
...(failedRows.length ? [`${failedRows.length} input target${failedRows.length === 1 ? '' : 's'} failed collection and cannot contribute mail evidence. Input positions and normalised targets are retained; consult the original input for failure diagnostics.`] : []),
|
|
361
|
+
...(rowsWithIncompleteDns ? [`${rowsWithIncompleteDns} reviewed domain${rowsWithIncompleteDns === 1 ? ' has' : 's have'} incomplete DNS evidence; unobserved provider relationships remain unknown.`] : []),
|
|
362
|
+
...(providerCoverage.rowsWithOmittedProviders || providerCoverage.omittedRelationships || providerCoverage.omittedRelationshipDomains
|
|
322
363
|
? ['Mail-provider relationship coverage reached a configured row, relationship, or domain bound; omitted relationships remain unknown.']
|
|
323
364
|
: []),
|
|
324
365
|
],
|
|
@@ -327,19 +368,30 @@ function buildCliMailReview(textValue, generatedAt = new Date().toISOString()) {
|
|
|
327
368
|
function formatCliMailReview(document) {
|
|
328
369
|
const lines = [
|
|
329
370
|
'Passive mail exposure review',
|
|
371
|
+
`Input targets ${document.inputCoverage.inputRows}`,
|
|
330
372
|
`Domains ${document.rows.length}`,
|
|
373
|
+
`Failed targets ${document.inputCoverage.failedRows}`,
|
|
331
374
|
`Authenticated ${document.counts.authenticated_mail}`,
|
|
332
375
|
`Auth gaps ${document.counts.mail_auth_gap}`,
|
|
333
376
|
`Null MX ${document.counts.null_mx}`,
|
|
334
377
|
`No explicit MX ${document.counts.no_explicit_mx}`,
|
|
335
|
-
`Incomplete ${document.counts.evidence_incomplete + document.counts.mail_auth_incomplete}`,
|
|
378
|
+
`Incomplete ${document.counts.evidence_incomplete + document.counts.mail_auth_incomplete + document.inputCoverage.failedRows}`,
|
|
336
379
|
'',
|
|
337
380
|
];
|
|
338
381
|
for (const row of document.rows) {
|
|
339
382
|
lines.push(`${row.domain} ${row.state.replaceAll('_', ' ')}`);
|
|
383
|
+
lines.push(` Observed ${row.provenance.observedAt ?? 'unknown'} · ${row.provenance.collectionOrigin.replaceAll('_', ' ')}`);
|
|
384
|
+
lines.push(` DNS observed ${row.provenance.dnsObservedAt ?? 'unknown'} · Source output ${row.provenance.sourceGeneratedAt}`);
|
|
340
385
|
lines.push(` MX providers ${row.providerDomains.join(', ') || 'None observed'}${row.providerDomainsOmitted ? ` · +${row.providerDomainsOmitted} omitted` : ''}`);
|
|
341
386
|
lines.push(` SPF / DMARC ${row.hasSpf === null ? 'unknown' : row.hasSpf ? 'observed' : 'not observed'} / ${row.hasDmarc === null ? 'unknown' : row.hasDmarc ? 'observed' : 'not observed'}`);
|
|
342
387
|
}
|
|
388
|
+
if (document.failedRows.length) {
|
|
389
|
+
lines.push('', 'Failed collection inputs');
|
|
390
|
+
for (const row of document.failedRows) {
|
|
391
|
+
lines.push(`Input ${row.rowNumber} ${row.target ?? 'unrecognised target'} · collection failed`);
|
|
392
|
+
lines.push(` Observed ${row.provenance.observedAt ?? 'unknown'} · ${row.provenance.collectionOrigin.replaceAll('_', ' ')} · Source output ${row.provenance.sourceGeneratedAt}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
343
395
|
if (document.providerRelationships.length) {
|
|
344
396
|
lines.push('', 'Shared mail-provider relationships');
|
|
345
397
|
for (const relationship of document.providerRelationships) {
|
|
@@ -347,7 +399,7 @@ function formatCliMailReview(document) {
|
|
|
347
399
|
}
|
|
348
400
|
}
|
|
349
401
|
if (!document.providerCoverage.complete) {
|
|
350
|
-
lines.push('', `Provider coverage partial ${document.providerCoverage.omittedProviderDomains} row providers · ${document.providerCoverage.omittedRelationships} relationships · ${document.providerCoverage.omittedRelationshipDomains} relationship domains omitted`);
|
|
402
|
+
lines.push('', `Provider coverage partial ${document.providerCoverage.failedRows} failed targets · ${document.providerCoverage.rowsWithIncompleteDns} incomplete DNS rows · ${document.providerCoverage.omittedProviderDomains} row providers · ${document.providerCoverage.omittedRelationships} relationships · ${document.providerCoverage.omittedRelationshipDomains} relationship domains omitted`);
|
|
351
403
|
}
|
|
352
404
|
lines.push('', 'Limitations:');
|
|
353
405
|
for (const limitation of document.limitations)
|
package/cli/manual.mjs
CHANGED
|
@@ -24,6 +24,7 @@ ${commands}
|
|
|
24
24
|
Human-readable output is the default. Versioned JSON and JSONL are available where documented. Diagnostics and optional progress events use standard error. Use --output and optional --force for atomic private file output. Use --palette auto, light, or dark for a fixed terminal palette. Lookup --browse provides an interactive terminal view.
|
|
25
25
|
.SH EXIT STATUS
|
|
26
26
|
0 indicates command completion, 2 invalid usage, 3 a collection or comparison failure, 4 an explicitly detected partial result, 70 an internal bootstrap failure, 130 analyst cancellation, and 143 service termination.
|
|
27
|
+
For verify-artifact, use --strict-exit in scripts: incomplete verification then returns 4. Without it, producing a verification report can return 0 even when its checks fail. Read the report state and checks.
|
|
27
28
|
.SH PRIVACY
|
|
28
29
|
Network commands disclose the target to the sources named in focused help. Offline commands read local input only. Output files use private permissions and remain on the operator's machine.
|
|
29
30
|
.SH LICENSE
|
|
@@ -44,6 +44,8 @@ async function runNetworkCommand(args, dependencies, context) {
|
|
|
44
44
|
dkimSelectors,
|
|
45
45
|
retiredDkimSelectors,
|
|
46
46
|
mailProtectionProfile: args.mailProfile,
|
|
47
|
+
...(args.includeInheritedDns ? { includeInheritedDns: true } : {}),
|
|
48
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
47
49
|
}));
|
|
48
50
|
const document = buildCliPostureDocument(requestedDomain, report, context.now());
|
|
49
51
|
if (!args.quiet) {
|
package/cli/output-file.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { unlinkSync } from 'node:fs';
|
|
|
3
3
|
import { link, open, rename, unlink } from 'node:fs/promises';
|
|
4
4
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { CliUsageError } from "./errors.mjs";
|
|
6
|
+
import { MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES } from "../packages/contracts/investigation-package-limits.mjs";
|
|
6
7
|
export const MAX_CLI_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
7
8
|
const MAX_CLI_OUTPUT_PATH_LENGTH = 4096;
|
|
8
9
|
const pendingOutputFiles = new Map();
|
|
@@ -22,14 +23,26 @@ function safeOutputPath(value) {
|
|
|
22
23
|
}
|
|
23
24
|
return resolve(value);
|
|
24
25
|
}
|
|
25
|
-
function createBufferedOutput() {
|
|
26
|
+
function createBufferedOutput(options = { binary: false }) {
|
|
26
27
|
const chunks = [];
|
|
28
|
+
const binaryChunks = [];
|
|
27
29
|
let bytes = 0;
|
|
30
|
+
const writeBinary = (chunk) => {
|
|
31
|
+
if (!options.binary || !(chunk instanceof Uint8Array))
|
|
32
|
+
throw new CliUsageError('Binary output requires an explicit package destination.');
|
|
33
|
+
if (chunk.byteLength > MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES - bytes)
|
|
34
|
+
throw new CliUsageError('Binary package output exceeds its byte limit.');
|
|
35
|
+
bytes += chunk.byteLength;
|
|
36
|
+
binaryChunks.push(new Uint8Array(chunk));
|
|
37
|
+
};
|
|
28
38
|
return Object.freeze({
|
|
39
|
+
writeBinary,
|
|
29
40
|
stream: {
|
|
30
41
|
isTTY: false,
|
|
31
42
|
columns: 80,
|
|
32
43
|
write(chunk) {
|
|
44
|
+
if (options.binary)
|
|
45
|
+
throw new CliUsageError('Binary package output cannot contain terminal text.');
|
|
33
46
|
const value = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
|
|
34
47
|
bytes += Buffer.byteLength(value, 'utf8');
|
|
35
48
|
if (bytes > MAX_CLI_OUTPUT_BYTES) {
|
|
@@ -39,14 +52,26 @@ function createBufferedOutput() {
|
|
|
39
52
|
return true;
|
|
40
53
|
},
|
|
41
54
|
},
|
|
42
|
-
value: () =>
|
|
55
|
+
value: () => {
|
|
56
|
+
if (!options.binary)
|
|
57
|
+
return chunks.join('');
|
|
58
|
+
const result = new Uint8Array(bytes);
|
|
59
|
+
let offset = 0;
|
|
60
|
+
for (const chunk of binaryChunks) {
|
|
61
|
+
result.set(chunk, offset);
|
|
62
|
+
offset += chunk.byteLength;
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
},
|
|
43
66
|
});
|
|
44
67
|
}
|
|
45
68
|
async function writePrivateFile(pathValue, content, options = {}, operations = OUTPUT_FILE_OPERATIONS) {
|
|
46
69
|
const target = safeOutputPath(pathValue);
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
const maximumBytes = typeof content === 'string' ? MAX_CLI_OUTPUT_BYTES : MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES;
|
|
71
|
+
if ((typeof content === 'string' ? Buffer.byteLength(content, 'utf8') : content.byteLength) > maximumBytes) {
|
|
72
|
+
throw new CliUsageError(`Generated output is limited to ${maximumBytes} bytes.`);
|
|
49
73
|
}
|
|
74
|
+
const captured = typeof content === 'string' ? content : new Uint8Array(content);
|
|
50
75
|
const directory = dirname(target);
|
|
51
76
|
const temporary = join(directory, `.${basename(target)}.${process.pid}.${operations.randomUUID()}.tmp`);
|
|
52
77
|
let temporaryCreated = false;
|
|
@@ -59,12 +84,13 @@ async function writePrivateFile(pathValue, content, options = {}, operations = O
|
|
|
59
84
|
settled: false,
|
|
60
85
|
});
|
|
61
86
|
try {
|
|
62
|
-
await handle.writeFile(
|
|
87
|
+
await handle.writeFile(captured, { encoding: 'utf8' });
|
|
63
88
|
await handle.sync();
|
|
64
89
|
}
|
|
65
90
|
finally {
|
|
66
91
|
await handle.close();
|
|
67
92
|
}
|
|
93
|
+
await options.beforePublish?.();
|
|
68
94
|
if (options.force) {
|
|
69
95
|
await operations.rename(temporary, target);
|
|
70
96
|
temporaryCreated = false;
|
package/cli/registry-doctor.mjs
CHANGED
|
@@ -189,7 +189,9 @@ function buildRegistryDoctorReport(raw, generatedAt = new Date().toISOString())
|
|
|
189
189
|
...sources.filter((source) => source.alignment === 'investigate').map((source) => (source.expected === 'allowed'
|
|
190
190
|
? `Review ${source.source.toUpperCase()} source health, transport attempts, and fixture coverage; the catalogue expected collection to be available.`
|
|
191
191
|
: `Review the ${source.source.toUpperCase()} result and capability profile; the observed state did not match the declared access constraint.`)),
|
|
192
|
-
...sources.filter((source) => source.alignment === 'unexpected_observation').map((source) => (
|
|
192
|
+
...sources.filter((source) => source.alignment === 'unexpected_observation').map((source) => (source.source === 'rdap' && source.expected === 'unsupported'
|
|
193
|
+
? 'Review whether the retained no-service RDAP profile needs refreshing; current bootstrap discovery may identify a newly published service.'
|
|
194
|
+
: `Review why ${source.source.toUpperCase()} produced normalized evidence despite the declared ${source.expected.replaceAll('_', ' ')} access profile.`)),
|
|
193
195
|
...sources.filter((source) => source.alignment === 'observed' && source.objectIdentifier === 'not_observed').map((source) => (`${source.source.toUpperCase()} succeeded without a retained registry object identifier; this can be a valid publication omission and should remain unknown rather than an error.`)),
|
|
194
196
|
].slice(0, 12);
|
|
195
197
|
return {
|
package/cli/registry-support.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { registryStandardsCoverageSnapshot, } from "../lib/registry-capabilities.mjs";
|
|
2
|
+
import { REGISTRY_STANDARDS_COVERAGE_SCHEMA } from "../lib/registry-capability-catalogue.mjs";
|
|
2
3
|
const REGISTRY_SUPPORT_SCHEMA = 'whoisleuth.cli.registry-support';
|
|
3
4
|
const REGISTRY_SUPPORT_SCHEMA_VERSION = 4;
|
|
4
|
-
const REGISTRY_STANDARDS_COVERAGE_SCHEMA = 'whoisleuth.registry-standards-coverage';
|
|
5
5
|
const MAX_REGISTRY_SUPPORT_TEXT_LENGTH = 2048;
|
|
6
6
|
const MAX_REGISTRY_SUPPORT_REFERENCES = 20;
|
|
7
7
|
const MAX_REGISTRY_SUPPORT_REFERENCE_LENGTH = 2048;
|
|
@@ -62,7 +62,7 @@ function parseBulkExport(root) {
|
|
|
62
62
|
const rawSession = record(candidate);
|
|
63
63
|
const rawResults = Array.isArray(rawSession?.results) ? rawSession.results : null;
|
|
64
64
|
const rawDomains = Array.isArray(rawSession?.domains) ? rawSession.domains : null;
|
|
65
|
-
const session = normalizeBulkSession(candidate);
|
|
65
|
+
const session = normalizeBulkSession(candidate, version);
|
|
66
66
|
if (!session || !rawResults || !rawDomains
|
|
67
67
|
|| rawResults.length > MAX_BULK_SESSION_ROWS
|
|
68
68
|
|| rawResults.length !== session.results.length
|
|
@@ -338,7 +338,7 @@ export function buildCliRetainedArtifactDiff(leftRaw, rightRaw, options = {}, ge
|
|
|
338
338
|
if (left.schema === SAVED_LOOKUP_SCHEMA) {
|
|
339
339
|
if (options.leftSessionId || options.rightSessionId)
|
|
340
340
|
throw new CliUsageError('Saved-session selectors apply only to Bulk session exports.');
|
|
341
|
-
return buildCliLookupDiff(leftRaw, rightRaw, generatedAt);
|
|
341
|
+
return buildCliLookupDiff(leftRaw, rightRaw, generatedAt, { domainMode: 'auto' });
|
|
342
342
|
}
|
|
343
343
|
if (left.schema === BULK_SESSION_SCHEMA)
|
|
344
344
|
return buildBulkLedger(leftRaw, rightRaw, left, right, options, generatedAt);
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { readBoundedRegularFile } from "../lib/bounded-file.mjs";
|
|
2
|
+
import { MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES } from "../packages/contracts/investigation-package-limits.mjs";
|
|
3
|
+
import { verifyOfflineInvestigationFolder, verifyOfflineInvestigationPackage } from "./investigation-package-review.mjs";
|
|
4
|
+
import { readInvestigationFolder } from "./investigation-folder.mjs";
|
|
5
|
+
import { readBagItFolder } from "./bagit-folder.mjs";
|
|
6
|
+
import { verifyOfflineBagIt } from "./bagit-review.mjs";
|
|
7
|
+
import { MAX_BAGIT_ZIP_BYTES } from "../packages/interchange/bagit.mjs";
|
|
1
8
|
import { MAX_OFFLINE_ARTIFACT_BYTES, formatOfflineArtifactVerification, isCompleteOfflineArtifactVerification, verifyOfflineArtifact, } from "./artifact-verify.mjs";
|
|
2
9
|
import { MAX_CASE_PACK_INPUT_BYTES, buildCliCasePack, formatCliCasePack } from "./case-pack.mjs";
|
|
3
10
|
import { MAX_COMPARE_INPUT_BYTES, compareLookupDocument, parseCliLookupDocument, } from "./compare.mjs";
|
|
@@ -5,6 +12,7 @@ import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
|
5
12
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
6
13
|
import { MAX_OFFLINE_EVIDENCE_INPUT_BYTES, buildOfflineEvidenceReview, buildOfflineEvidenceReviewWithLocalResources, formatOfflineEvidenceReview, } from "./offline-evidence-review.mjs";
|
|
7
14
|
import { buildCliLookupBrief, formatCliLookupBrief } from "./lookup-brief.mjs";
|
|
15
|
+
import { runCaseCommand } from "./case-command.mjs";
|
|
8
16
|
import { MAX_MAIL_REVIEW_INPUT_BYTES, buildCliMailReview, formatCliMailReview, } from "./mail-review.mjs";
|
|
9
17
|
import { MAX_MAIL_HEADER_INPUT_BYTES, buildCliMailHeaderReview, formatCliMailHeaderReview, } from "./mail-header-review.mjs";
|
|
10
18
|
import { buildCliPageComparison, formatCliPageComparison } from "./page-compare.mjs";
|
|
@@ -17,6 +25,42 @@ import { REVIEW_INLINE_COMMANDS } from "./inline-command-families.mjs";
|
|
|
17
25
|
import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
|
|
18
26
|
async function runVerifyArtifactCommand(args, dependencies, context) {
|
|
19
27
|
context.setFailureLabel('Artefact verification');
|
|
28
|
+
if (args.folder) {
|
|
29
|
+
let files;
|
|
30
|
+
try {
|
|
31
|
+
files = await (args.bagit ? readBagItFolder : readInvestigationFolder)(args.folder, dependencies.signal);
|
|
32
|
+
}
|
|
33
|
+
catch (cause) {
|
|
34
|
+
dependencies.signal?.throwIfAborted();
|
|
35
|
+
const reason = cause instanceof TypeError ? boundedCliErrorMessage(cause) : 'The selected folder is unavailable or could not be read.';
|
|
36
|
+
throw new CliUsageError(`Could not read evidence folder: ${reason}`);
|
|
37
|
+
}
|
|
38
|
+
const report = await (args.bagit ? verifyOfflineBagIt : verifyOfflineInvestigationFolder)(files);
|
|
39
|
+
dependencies.signal?.throwIfAborted();
|
|
40
|
+
if (!args.quiet)
|
|
41
|
+
context.writeStdout(args.output === 'json' ? formatJsonDocument(report) : context.terminal(formatOfflineArtifactVerification(report), args.color));
|
|
42
|
+
return args.strictExit && !isCompleteOfflineArtifactVerification(report) ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
43
|
+
}
|
|
44
|
+
if (args.package) {
|
|
45
|
+
if (!args.source || args.source === '-')
|
|
46
|
+
throw new CliUsageError('--package requires a selected ZIP file; binary stdin is not accepted.');
|
|
47
|
+
let bytes;
|
|
48
|
+
try {
|
|
49
|
+
bytes = dependencies.readBinaryArtifactInput ? await dependencies.readBinaryArtifactInput(args.source)
|
|
50
|
+
: await readBoundedRegularFile(args.source, { maximumBytes: args.bagit ? MAX_BAGIT_ZIP_BYTES : MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES, minimumBytes: 22, label: 'Investigation package', ...(dependencies.signal ? { signal: dependencies.signal } : {}) });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof CliUsageError)
|
|
54
|
+
throw error;
|
|
55
|
+
throw new CliUsageError(`Could not read package input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
56
|
+
}
|
|
57
|
+
const passphrase = args.passphraseSource ? await context.readPassphraseSource(args.passphraseSource) : undefined;
|
|
58
|
+
const report = args.bagit ? await verifyOfflineBagIt(bytes) : await verifyOfflineInvestigationPackage(bytes, passphrase);
|
|
59
|
+
dependencies.signal?.throwIfAborted();
|
|
60
|
+
if (!args.quiet)
|
|
61
|
+
context.writeStdout(args.output === 'json' ? formatJsonDocument(report) : context.terminal(formatOfflineArtifactVerification(report), args.color));
|
|
62
|
+
return args.strictExit && !isCompleteOfflineArtifactVerification(report) ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
63
|
+
}
|
|
20
64
|
let input;
|
|
21
65
|
try {
|
|
22
66
|
input = dependencies.readArtifactInput
|
|
@@ -286,6 +330,7 @@ const REVIEW_COMMAND_HANDLERS = Object.freeze({
|
|
|
286
330
|
'mail-headers': runMailHeadersCommand,
|
|
287
331
|
'review-evidence': runOfflineEvidenceReviewCommand,
|
|
288
332
|
'brief': runBriefOrCasePackCommand,
|
|
333
|
+
'case': runCaseCommand,
|
|
289
334
|
'case-pack': runBriefOrCasePackCommand,
|
|
290
335
|
});
|
|
291
336
|
function runReviewCommand(args, dependencies, context) {
|