@slicedearth/whoisleuth-cli 1.36.1 → 1.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -2
- package/bin/whoisleuth.mjs +41 -24
- package/cli/arguments.mjs +236 -5
- package/cli/artifact-verify.mjs +56 -14
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +18 -3
- package/cli/doctor.mjs +11 -5
- package/cli/evidence-command-arguments.mjs +1 -1
- package/cli/evidence-command-runner.mjs +2 -2
- package/cli/evidence-signing.mjs +4 -4
- package/cli/formatters/html.mjs +1 -1
- package/cli/formatters/json.mjs +13 -1
- package/cli/formatters/terminal.mjs +7 -3
- package/cli/investigation-plan.mjs +2 -2
- package/cli/local-mmdb-review.mjs +76 -0
- package/cli/lookalike-calibration.mjs +121 -0
- package/cli/lookup-command-runner.mjs +4 -1
- package/cli/lookup-plan.mjs +3 -3
- package/cli/lookup-reconcile.mjs +141 -0
- package/cli/lookup-timeline.mjs +1 -1
- package/cli/mail-review.mjs +1 -1
- package/cli/offline-evidence-review.mjs +20 -2
- package/cli/registry-doctor.mjs +125 -0
- package/cli/risk-calibration.mjs +117 -9
- package/cli/runner.mjs +236 -17
- package/cli/saved-lookup.mjs +2 -2
- package/cli/sharing-review.mjs +202 -0
- package/cli/source-reliability.mjs +1 -1
- package/docs/cli-reference.md +1110 -0
- package/docs/cli.md +224 -940
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +6 -6
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -12
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +1 -1
- package/frontend/src/lib/analysis/bulk-session-model.js +7 -2
- package/frontend/src/lib/analysis/case-evidence-model.js +26 -4
- package/frontend/src/lib/analysis/case-migration-model.js +6 -1
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-core.js +5 -1
- package/frontend/src/lib/analysis/case-record-operations.js +6 -1
- package/frontend/src/lib/analysis/case-response-packet.js +7 -7
- package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
- package/frontend/src/lib/analysis/investigation-capsule.js +105 -0
- package/frontend/src/lib/analysis/lookup-asset-graph.js +806 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +480 -0
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
- package/frontend/src/lib/analysis/lookup-presentation.js +74 -0
- package/frontend/src/lib/analysis/lookup-response.js +2 -0
- package/frontend/src/lib/analysis/lookup-source-refresh.js +304 -0
- package/frontend/src/lib/analysis/lookup-summary-model.js +398 -0
- package/frontend/src/lib/analysis/page-similarity.js +1 -1
- package/frontend/src/lib/analysis/relationship-evidence.js +3 -3
- package/frontend/src/lib/analysis/relationship-observation-model.js +4 -4
- package/frontend/src/lib/analysis/scoring.js +8 -88
- package/frontend/src/lib/analysis/shortlist-model.js +8 -4
- package/frontend/src/lib/analysis/visualization-bounds.js +26 -0
- package/frontend/src/lib/analysis/visualization-force-graph.js +352 -0
- package/frontend/src/lib/analysis/visualization-matrix.js +88 -0
- package/frontend/src/lib/analysis/visualization-models.js +370 -0
- package/frontend/src/lib/analysis/visualization-time-series.js +144 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -1
- package/frontend/src/lib/analysis/workspace-archive.js +2 -2
- package/lib/analyst-taxonomy.mjs +40 -0
- package/lib/availability.mjs +1 -1
- package/lib/browser-library-profile.mjs +15 -2
- package/lib/certificate-extension-profile.mjs +71 -0
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/domain-assurance.mjs +368 -0
- package/lib/domain-control-manifest.mjs +50 -7
- package/lib/domain-posture.mjs +9 -9
- package/lib/encrypted-dns-contract.mjs +1 -1
- package/lib/evidence-report-markdown.mjs +1 -1
- package/lib/evidence-report.mjs +1 -1
- package/lib/generated/cisa-kev-catalog.mjs +1668 -0
- package/lib/ip-prefix.mjs +34 -1
- package/lib/local-geoip-evidence.mjs +2 -2
- package/lib/lookup-response-contract.mjs +346 -0
- package/lib/observed-network-context.mjs +1 -1
- package/lib/opportunity-scoring.mjs +107 -0
- package/lib/page-fingerprints.mjs +1 -1
- package/lib/rdap-capabilities.mjs +6 -6
- package/lib/rdap-search-workbench.mjs +96 -6
- package/lib/registry-access.mjs +1 -1
- package/lib/registry-capability-catalogue.mjs +4 -4
- package/lib/registry-insights.mjs +6 -6
- package/lib/risk-scoring.mjs +206 -83
- package/lib/scoring-evidence-quality.mjs +107 -0
- package/lib/security-txt.mjs +3 -3
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threatfox-intelligence.mjs +1 -1
- package/lib/tls-intelligence.mjs +13 -3
- package/lib/urlhaus-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +5 -5
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-chain.mjs +1 -1
- package/package.json +5 -1
- package/third-party-notices.txt +911 -19
package/cli/runner.mjs
CHANGED
|
@@ -2,18 +2,13 @@ import { Buffer } from 'node:buffer';
|
|
|
2
2
|
import { createReadStream } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { abortable } from "../lib/abort.mjs";
|
|
5
|
-
import { resolvePublicAddresses, safeFetch } from "../lib/safe-fetch.mjs";
|
|
6
|
-
import { whoisQuery } from "../lib/whois-transport.mjs";
|
|
7
5
|
import { REGISTRY_CAPABILITIES_VERSION, registryCapabilityFor } from "../lib/registry-capabilities.mjs";
|
|
8
|
-
import { explainRiskScore, RISK_MODEL_VERSION, RISK_REVIEW_THRESHOLD } from "../lib/risk-scoring.mjs";
|
|
6
|
+
import { explainRiskScore, explainRiskScoreV6, RISK_MODEL_VERSION, RISK_REVIEW_THRESHOLD } from "../lib/risk-scoring.mjs";
|
|
9
7
|
import { CLI_COMMANDS, parseCliArguments } from "./arguments.mjs";
|
|
10
|
-
import { runBulkCommand } from "./bulk-command-runner.mjs";
|
|
11
8
|
import { buildCliCommandCatalogue, formatCliCommandCatalogue } from "./command-catalogue.mjs";
|
|
12
9
|
import { buildShellCompletion } from "./completion.mjs";
|
|
13
10
|
import { buildDoctorReport, formatDoctorReport } from "./doctor.mjs";
|
|
14
11
|
import { MAX_COMPARE_INPUT_BYTES, compareLookupDocument, parseCliLookupDocument, readCompareInputBounded, } from "./compare.mjs";
|
|
15
|
-
import { runDiscoveryCommand } from "./discovery-command-runner.mjs";
|
|
16
|
-
import { runDiscoveryScanCommand } from "./discovery-scan-command-runner.mjs";
|
|
17
12
|
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
18
13
|
import { evidenceCommandFailureLabel, isEvidenceCommand, runEvidenceCommand, } from "./evidence-command-runner.mjs";
|
|
19
14
|
import { buildCliEvidenceExport, formatCliEvidenceExport } from "./export-evidence.mjs";
|
|
@@ -23,24 +18,27 @@ import { buildCliCompareDocument, formatJsonDocument, } from "./formatters/json.
|
|
|
23
18
|
import { formatLookupEvidenceMarkdown } from "./formatters/markdown.mjs";
|
|
24
19
|
import { formatTerminalCompare, formatTerminalRegistrySupport, formatTerminalRiskCalibration, } from "./formatters/terminal.mjs";
|
|
25
20
|
import { buildCliLookupDiff, formatCliLookupDiff } from "./lookup-diff.mjs";
|
|
21
|
+
import { MAX_LOOKUP_RECONCILIATION_INPUT_BYTES, buildCliLookupReconciliation, formatCliLookupReconciliation, } from "./lookup-reconcile.mjs";
|
|
22
|
+
import { MAX_SHARING_REVIEW_BYTES, buildSharingReview, formatSharingReview, } from "./sharing-review.mjs";
|
|
26
23
|
import { MAX_LOOKUP_TIMELINE_INPUT_BYTES, buildCliLookupTimeline, formatCliLookupTimeline, } from "./lookup-timeline.mjs";
|
|
27
24
|
import { buildCliPageComparison, formatCliPageComparison } from "./page-compare.mjs";
|
|
28
25
|
import { MAX_MAIL_REVIEW_INPUT_BYTES, buildCliMailReview, formatCliMailReview, } from "./mail-review.mjs";
|
|
29
|
-
import { MAX_OFFLINE_EVIDENCE_INPUT_BYTES, buildOfflineEvidenceReview, formatOfflineEvidenceReview, } from "./offline-evidence-review.mjs";
|
|
26
|
+
import { MAX_OFFLINE_EVIDENCE_INPUT_BYTES, buildOfflineEvidenceReview, buildOfflineEvidenceReviewWithLocalResources, formatOfflineEvidenceReview, } from "./offline-evidence-review.mjs";
|
|
30
27
|
import { DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA, DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, buildDomainControlManifest, formatDomainControlResult, reviewDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
31
|
-
import {
|
|
28
|
+
import { MAX_ASSURANCE_INPUT_BYTES, buildDomainAssurance, formatDomainAssurance, } from "../lib/domain-assurance.mjs";
|
|
32
29
|
import { buildCliManual } from "./manual.mjs";
|
|
33
30
|
import { buildInvestigationPlan, formatInvestigationPlan } from "./investigation-plan.mjs";
|
|
34
31
|
import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../lib/project-metadata.mjs";
|
|
35
32
|
import { createBufferedOutput, writePrivateFile } from "./output-file.mjs";
|
|
36
33
|
import { createTerminalProgress } from "./progress.mjs";
|
|
37
34
|
import { buildRegistrySupportDocument } from "./registry-support.mjs";
|
|
35
|
+
import { buildRegistryDoctorReport, formatRegistryDoctorReport } from "./registry-doctor.mjs";
|
|
38
36
|
import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, formatOfflineArtifactVerification, verifyOfflineArtifact, } from "./artifact-verify.mjs";
|
|
39
37
|
import { MAX_SOURCE_RELIABILITY_INPUT_BYTES, buildSourceReliabilityReport, formatSourceReliabilityReport, } from "./source-reliability.mjs";
|
|
40
38
|
import { MAX_RISK_CALIBRATION_INPUT_BYTES, buildRiskCalibrationReport, parseRiskCalibrationDataset, readRiskCalibrationInputBounded, } from "./risk-calibration.mjs";
|
|
39
|
+
import { MAX_LOOKALIKE_CALIBRATION_BYTES, buildLookalikeCalibration, formatLookalikeCalibration, } from "./lookalike-calibration.mjs";
|
|
41
40
|
import { MAX_SAVED_LOOKUP_INPUT_BYTES, readSavedLookupInputBounded } from "./saved-lookup.mjs";
|
|
42
41
|
import { presentTerminalOutput, terminalPresentation, } from "./terminal-presentation.mjs";
|
|
43
|
-
import { runNetworkCommand } from "./network-command-runner.mjs";
|
|
44
42
|
const require = createRequire(import.meta.url);
|
|
45
43
|
const { version: VERSION } = require('../package.json');
|
|
46
44
|
const MAX_STDIN_BYTES = 4096;
|
|
@@ -64,6 +62,7 @@ Discover:
|
|
|
64
62
|
discover Generate offline lookalike candidates.
|
|
65
63
|
discover-scan Collect a bounded candidate review queue.
|
|
66
64
|
registry-support Explain local registry coverage without a request.
|
|
65
|
+
registry-doctor Diagnose a saved registry collection against local policy.
|
|
67
66
|
|
|
68
67
|
Review saved evidence:
|
|
69
68
|
source-report Summarise source reliability without retaining targets.
|
|
@@ -72,8 +71,11 @@ Review saved evidence:
|
|
|
72
71
|
mail-review Review saved passive mail exposure evidence.
|
|
73
72
|
review-evidence Review supplied DNS, routing, GeoIP, or RDAP evidence offline.
|
|
74
73
|
domain-control Build or review an integrity-protected desired-state manifest.
|
|
74
|
+
assurance Review change, recovery, concentration, or retirement plans.
|
|
75
|
+
sharing-review Lint a reviewed artifact before deliberate sharing.
|
|
75
76
|
workflow-plan Plan a fixed investigation recipe without executing it.
|
|
76
77
|
diff Compare two saved domain observations.
|
|
78
|
+
reconcile Reconcile independently labelled observations.
|
|
77
79
|
timeline Compare a sequence of observations for one domain.
|
|
78
80
|
export Convert a saved lookup into an evidence report.
|
|
79
81
|
inspect-archive Inspect a workspace archive, redacted by default.
|
|
@@ -83,6 +85,7 @@ Integrity and calibration:
|
|
|
83
85
|
sign-artifact Sign one reviewed packet or manifest locally.
|
|
84
86
|
verify-signature Verify one signed evidence package locally.
|
|
85
87
|
risk-calibrate Replay reviewed labels without changing the model.
|
|
88
|
+
lookalike-calibrate Summarise reviewed mutation-family yield without tuning generation.
|
|
86
89
|
|
|
87
90
|
Terminal:
|
|
88
91
|
doctor Check the local runtime; network tests require --network.
|
|
@@ -104,7 +107,7 @@ const COMMAND_USAGE = Object.freeze({
|
|
|
104
107
|
doctor: 'whoisleuth doctor [--network] [--json] [--quiet] [--no-color]',
|
|
105
108
|
commands: 'whoisleuth commands [--json] [--quiet] [--no-color]',
|
|
106
109
|
manual: 'whoisleuth manual',
|
|
107
|
-
lookup: 'whoisleuth lookup <domain|IP|ASN> [--json|--markdown|--html] [--fast|--deep] [--plan] [--summary|--verbose] [--strict-exit] [--events] [--quiet] [--no-color]',
|
|
110
|
+
lookup: 'whoisleuth lookup <domain|IP|ASN> [--json|--markdown|--html] [--fast|--deep] [--observer <label>] [--vantage <label>] [--plan] [--summary|--verbose] [--strict-exit] [--events] [--quiet] [--no-color]',
|
|
108
111
|
bulk: 'whoisleuth bulk [file] [--json|--jsonl|--csv|--domains|--queries] [--registered-only|--inconclusive-only|--errors-only] [--fast|--deep] [--concurrency <1-8>] [--checkpoint <file> [--resume]] [--events]',
|
|
109
112
|
'ct-search': 'whoisleuth ct-search <keyword> [--json] [--quiet] [--no-color]',
|
|
110
113
|
discover: 'whoisleuth discover <brand|domain> [--tlds <list>] [--preset <name>|--families <ids>] [--keyboard <layout>] [--dictionary <file>] [--snapshot <file>] [--json|--jsonl|--domains]',
|
|
@@ -113,7 +116,9 @@ const COMMAND_USAGE = Object.freeze({
|
|
|
113
116
|
http: 'whoisleuth http <domain> [--json] [--quiet] [--no-color]',
|
|
114
117
|
tls: 'whoisleuth tls <hostname> [--json] [--quiet] [--no-color]',
|
|
115
118
|
'registry-support': 'whoisleuth registry-support <domain|suffix> [--json] [--quiet] [--no-color]',
|
|
119
|
+
'registry-doctor': 'whoisleuth registry-doctor [lookup.json] [--json] [--quiet] [--no-color]',
|
|
116
120
|
'risk-calibrate': 'whoisleuth risk-calibrate [dataset.json] [--json] [--quiet] [--no-color]',
|
|
121
|
+
'lookalike-calibrate': 'whoisleuth lookalike-calibrate [dataset.json] [--json] [--quiet] [--no-color]',
|
|
117
122
|
'verify-artifact': 'whoisleuth verify-artifact [artifact.json] [--passphrase-file <file>] [--json] [--quiet] [--no-color]',
|
|
118
123
|
'inspect-archive': 'whoisleuth inspect-archive [archive.json] [--passphrase-file <file>] [--search <value>] [--require-match] [--reveal] [--json]',
|
|
119
124
|
'sign-artifact': 'whoisleuth sign-artifact [artifact.json] --private-key-file <file>',
|
|
@@ -122,10 +127,13 @@ const COMMAND_USAGE = Object.freeze({
|
|
|
122
127
|
compare: 'whoisleuth compare [lookup.json] [--json] [--quiet] [--no-color]',
|
|
123
128
|
'page-compare': 'whoisleuth page-compare <left.json> <right.json> [--json] [--quiet] [--no-color]',
|
|
124
129
|
'mail-review': 'whoisleuth mail-review [bulk.json|bulk.jsonl] [--json] [--quiet] [--no-color]',
|
|
125
|
-
'review-evidence': 'whoisleuth review-evidence [evidence.json] [--json] [--quiet] [--no-color]',
|
|
130
|
+
'review-evidence': 'whoisleuth review-evidence [evidence.json] [--mmdb <database-file>] [--json] [--quiet] [--no-color]',
|
|
126
131
|
'domain-control': 'whoisleuth domain-control [manifest-input.json|review-input.json] [--json] [--quiet] [--no-color]',
|
|
132
|
+
assurance: 'whoisleuth assurance [assurance-input.json] [--json] [--quiet] [--no-color]',
|
|
133
|
+
'sharing-review': 'whoisleuth sharing-review [artifact.json] --marking <level> --recipient-scope <scope> --purpose <text> [--human-reviewed] [--personal-data-reviewed] [--redactions-confirmed] [--json]',
|
|
127
134
|
'workflow-plan': 'whoisleuth workflow-plan <recipe> <domain|brand> [--json] [--quiet] [--no-color]',
|
|
128
135
|
diff: 'whoisleuth diff <left.json> <right.json> [--json] [--quiet] [--no-color]',
|
|
136
|
+
reconcile: 'whoisleuth reconcile <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
|
|
129
137
|
timeline: 'whoisleuth timeline <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
|
|
130
138
|
export: 'whoisleuth export [lookup.json] [--markdown|--html|--compact]',
|
|
131
139
|
});
|
|
@@ -195,11 +203,21 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
195
203
|
example: 'whoisleuth registry-support example.test --json',
|
|
196
204
|
boundary: 'This command is offline. Catalogue coverage does not test live reachability or decide registration or availability.',
|
|
197
205
|
},
|
|
206
|
+
'registry-doctor': {
|
|
207
|
+
description: 'Compare a saved Lookup registry result with the reviewed local capability profile.',
|
|
208
|
+
example: 'whoisleuth registry-doctor lookup.json --json',
|
|
209
|
+
boundary: 'The command is offline. It distinguishes expected access constraints from collection results and does not contact a live registry.',
|
|
210
|
+
},
|
|
198
211
|
'risk-calibrate': {
|
|
199
212
|
description: 'Replay reviewed labels against the current explainable Risk model.',
|
|
200
213
|
example: 'whoisleuth risk-calibrate calibration.json --json',
|
|
201
214
|
boundary: 'Calibration is offline and diagnostic. It never trains, tunes, or changes the scoring model automatically.',
|
|
202
215
|
},
|
|
216
|
+
'lookalike-calibrate': {
|
|
217
|
+
description: 'Summarise reviewed candidate dispositions by mutation family without retaining domains.',
|
|
218
|
+
example: 'whoisleuth lookalike-calibrate reviewed-candidates.json --json',
|
|
219
|
+
boundary: 'Calibration is offline and diagnostic. It omits candidate identifiers, domains, notes, and evidence and never tunes generation or filtering automatically.',
|
|
220
|
+
},
|
|
203
221
|
'verify-artifact': {
|
|
204
222
|
description: 'Validate a supported archive, packet, manifest, or saved Lookup without printing evidence contents.',
|
|
205
223
|
example: 'whoisleuth verify-artifact workspace.json --json',
|
|
@@ -250,6 +268,16 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
250
268
|
example: 'whoisleuth domain-control domain-control-input.json --json',
|
|
251
269
|
boundary: 'The command is offline and changes no registrar, DNS, mail, or certificate configuration. Only complete supplied observations can produce drift.',
|
|
252
270
|
},
|
|
271
|
+
assurance: {
|
|
272
|
+
description: 'Review a versioned domain change, recovery-dependency, or retirement plan.',
|
|
273
|
+
example: 'whoisleuth assurance domain-assurance.json --json',
|
|
274
|
+
boundary: 'The command is offline and treats every provider label, readiness state, and evidence reference as analyst-authored input. It changes no configuration.',
|
|
275
|
+
},
|
|
276
|
+
'sharing-review': {
|
|
277
|
+
description: 'Lint one reviewed artefact against local integrity, marking, recipient, personal-data, and redaction controls.',
|
|
278
|
+
example: 'whoisleuth sharing-review packet.json --marking amber --recipient-scope organization --purpose "Reviewed incident handoff" --human-reviewed --personal-data-reviewed --redactions-confirmed --json',
|
|
279
|
+
boundary: 'The command is offline and emits only bounded schema/version metadata, no content values, and no raw evidence. Its result is a review aid, not legal advice or recipient authorisation.',
|
|
280
|
+
},
|
|
253
281
|
'workflow-plan': {
|
|
254
282
|
description: 'Build a fixed domain-investigation plan from existing bounded CLI commands.',
|
|
255
283
|
example: 'whoisleuth workflow-plan domain-triage example.test --json',
|
|
@@ -260,6 +288,11 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
260
288
|
example: 'whoisleuth diff first.json second.json --json',
|
|
261
289
|
boundary: 'Comparison is offline. Missing, unavailable, equal, and different evidence remain separate states.',
|
|
262
290
|
},
|
|
291
|
+
reconcile: {
|
|
292
|
+
description: 'Reconcile bounded values across independently labelled observations of one domain.',
|
|
293
|
+
example: 'whoisleuth reconcile office.json mobile.json external.json --json',
|
|
294
|
+
boundary: 'The command is offline, accepts 2 to 5 saved observations for one domain, and never treats labels as proof of network independence or majority agreement as truth.',
|
|
295
|
+
},
|
|
263
296
|
timeline: {
|
|
264
297
|
description: 'Build an ordered same-domain history from saved Lookup observations.',
|
|
265
298
|
example: 'whoisleuth timeline first.json second.json latest.json --json',
|
|
@@ -285,10 +318,12 @@ const COMMAND_COLLECTION = Object.freeze({
|
|
|
285
318
|
http: { mode: 'network', scope: 'Accepts one domain and follows only the bounded SSRF-guarded homepage redirect workflow.' },
|
|
286
319
|
tls: { mode: 'network', scope: 'Accepts one public hostname and opens one bounded certificate connection.' },
|
|
287
320
|
'registry-support': { mode: 'offline', scope: 'Reads the embedded registry capability catalogue for one domain or suffix.' },
|
|
321
|
+
'registry-doctor': { mode: 'offline', scope: 'Reads one saved Lookup and the embedded registry capability catalogue.' },
|
|
288
322
|
'risk-calibrate': { mode: 'offline', scope: 'Reads one bounded reviewed-label dataset and changes no model or evidence.' },
|
|
323
|
+
'lookalike-calibrate': { mode: 'offline', scope: 'Reads at most 5,000 reviewed candidate labels from one dataset capped at 2 MiB.' },
|
|
289
324
|
'verify-artifact': { mode: 'offline', scope: 'Reads one selected bounded archive, packet, manifest, or saved Lookup document.' },
|
|
290
325
|
'inspect-archive': { mode: 'offline', scope: 'Reads one selected bounded workspace archive with redacted output by default.' },
|
|
291
|
-
'sign-artifact': { mode: 'offline', scope: 'Reads one selected
|
|
326
|
+
'sign-artifact': { mode: 'offline', scope: 'Reads one selected artefact and one local private key without transmitting either.' },
|
|
292
327
|
'verify-signature': { mode: 'offline', scope: 'Reads one selected signed package and optional local public key.' },
|
|
293
328
|
'source-report': { mode: 'offline', scope: 'Reads bounded saved evidence and emits target-free source reliability data.' },
|
|
294
329
|
compare: { mode: 'offline', scope: 'Reads one saved Lookup and compares its separately attributed registry publications.' },
|
|
@@ -296,8 +331,11 @@ const COMMAND_COLLECTION = Object.freeze({
|
|
|
296
331
|
'mail-review': { mode: 'offline', scope: 'Reads one saved Bulk result and sends no DNS or SMTP traffic.' },
|
|
297
332
|
'review-evidence': { mode: 'offline', scope: 'Reads one bounded versioned evidence or request-planning document and performs no collection.' },
|
|
298
333
|
'domain-control': { mode: 'offline', scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
|
|
334
|
+
assurance: { mode: 'offline', scope: 'Reads one versioned plan capped at 2 MiB and makes no request or configuration change.' },
|
|
335
|
+
'sharing-review': { mode: 'offline', scope: 'Reads one artefact capped at 15 MiB, emits only bounded schema/version metadata and no content values, and performs no transmission.' },
|
|
299
336
|
'workflow-plan': { mode: 'offline', scope: 'Builds a fixed typed recipe and executes none of its network or file steps.' },
|
|
300
337
|
diff: { mode: 'offline', scope: 'Reads two saved Lookup documents for different domains.' },
|
|
338
|
+
reconcile: { mode: 'offline', scope: 'Reads 2 to 5 saved observations for one domain, capped at 32 MiB in total.' },
|
|
301
339
|
timeline: { mode: 'offline', scope: 'Reads 2 to 20 saved observations for one domain, capped at 32 MiB in total.' },
|
|
302
340
|
export: { mode: 'offline', scope: 'Reads one saved Lookup and writes one bounded report.' },
|
|
303
341
|
});
|
|
@@ -445,7 +483,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
445
483
|
network: args.network,
|
|
446
484
|
presentation: terminalPresentation(stdout, args.color, environment),
|
|
447
485
|
...(dependencies.resolvePublicAddresses ? { resolveAddresses: dependencies.resolvePublicAddresses } : {}),
|
|
448
|
-
...(dependencies.safeFetch ? { fetchHttps: dependencies.safeFetch } : {
|
|
486
|
+
...(dependencies.safeFetch ? { fetchHttps: dependencies.safeFetch } : {}),
|
|
449
487
|
...(dependencies.whoisQuery ? { queryWhois: dependencies.whoisQuery } : {}),
|
|
450
488
|
});
|
|
451
489
|
const report = args.network
|
|
@@ -475,6 +513,33 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
475
513
|
write(stdout, args.output === 'json' ? formatJsonDocument(document) : terminal(formatTerminalRegistrySupport(document), args.color));
|
|
476
514
|
return EXIT_CODES.SUCCESS;
|
|
477
515
|
}
|
|
516
|
+
if (args.action === 'registry-doctor') {
|
|
517
|
+
failureLabel = 'Registry compatibility diagnostic';
|
|
518
|
+
let input;
|
|
519
|
+
try {
|
|
520
|
+
input = dependencies.readCompareInput
|
|
521
|
+
? await dependencies.readCompareInput(args.source)
|
|
522
|
+
: await readSavedLookupInputBounded(args.source
|
|
523
|
+
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
524
|
+
: dependencies.stdin || process.stdin, {
|
|
525
|
+
limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
|
|
526
|
+
label: 'Registry doctor input',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
if (error instanceof CliUsageError)
|
|
531
|
+
throw error;
|
|
532
|
+
throw new CliUsageError(`Could not read registry doctor input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
533
|
+
}
|
|
534
|
+
if (!input.trim())
|
|
535
|
+
throw new CliUsageError('registry-doctor requires one saved Lookup JSON file or a document on stdin.');
|
|
536
|
+
const report = buildRegistryDoctorReport(input, commandContext.now());
|
|
537
|
+
if (!args.quiet)
|
|
538
|
+
write(stdout, args.output === 'json'
|
|
539
|
+
? formatJsonDocument(report)
|
|
540
|
+
: terminal(formatRegistryDoctorReport(report), args.color));
|
|
541
|
+
return report.summary.investigate ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
542
|
+
}
|
|
478
543
|
if (args.action === 'risk-calibrate') {
|
|
479
544
|
failureLabel = 'Risk calibration';
|
|
480
545
|
let input;
|
|
@@ -497,13 +562,50 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
497
562
|
generatedAt: dependencies.now ? dependencies.now() : new Date().toISOString(),
|
|
498
563
|
modelVersion: dependencies.riskModelVersion || RISK_MODEL_VERSION,
|
|
499
564
|
reviewThreshold: dependencies.riskReviewThreshold || RISK_REVIEW_THRESHOLD,
|
|
565
|
+
...(!dependencies.explainRiskScore ? {
|
|
566
|
+
previousModelVersion: 6,
|
|
567
|
+
explainPreviousRiskScore: explainRiskScoreV6,
|
|
568
|
+
} : {}),
|
|
500
569
|
});
|
|
501
570
|
if (!args.quiet)
|
|
502
571
|
write(stdout, args.output === 'json' ? formatJsonDocument(report) : terminal(formatTerminalRiskCalibration(report), args.color));
|
|
503
572
|
return EXIT_CODES.SUCCESS;
|
|
504
573
|
}
|
|
574
|
+
if (args.action === 'lookalike-calibrate') {
|
|
575
|
+
failureLabel = 'Lookalike review-yield calibration';
|
|
576
|
+
let input;
|
|
577
|
+
try {
|
|
578
|
+
input = dependencies.readRiskCalibrationInput
|
|
579
|
+
? await dependencies.readRiskCalibrationInput(args.source)
|
|
580
|
+
: await readSavedLookupInputBounded(args.source
|
|
581
|
+
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
582
|
+
: dependencies.stdin || process.stdin, {
|
|
583
|
+
limit: MAX_LOOKALIKE_CALIBRATION_BYTES,
|
|
584
|
+
label: 'Lookalike calibration input',
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (error instanceof CliUsageError)
|
|
589
|
+
throw error;
|
|
590
|
+
throw new CliUsageError(`Could not read lookalike calibration input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
591
|
+
}
|
|
592
|
+
if (!input.trim())
|
|
593
|
+
throw new CliUsageError('lookalike-calibrate requires one dataset JSON file or a document on stdin.');
|
|
594
|
+
let report;
|
|
595
|
+
try {
|
|
596
|
+
report = buildLookalikeCalibration(input, commandContext.now());
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Lookalike calibration input is invalid'));
|
|
600
|
+
}
|
|
601
|
+
if (!args.quiet)
|
|
602
|
+
write(stdout, args.output === 'json'
|
|
603
|
+
? formatJsonDocument(report)
|
|
604
|
+
: terminal(formatLookalikeCalibration(report), args.color));
|
|
605
|
+
return EXIT_CODES.SUCCESS;
|
|
606
|
+
}
|
|
505
607
|
if (args.action === 'verify-artifact') {
|
|
506
|
-
failureLabel = '
|
|
608
|
+
failureLabel = 'Artefact verification';
|
|
507
609
|
let input;
|
|
508
610
|
try {
|
|
509
611
|
input = dependencies.readArtifactInput
|
|
@@ -512,7 +614,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
512
614
|
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
513
615
|
: dependencies.stdin || process.stdin, {
|
|
514
616
|
limit: MAX_OFFLINE_ARTIFACT_BYTES,
|
|
515
|
-
label: '
|
|
617
|
+
label: 'Artefact input',
|
|
516
618
|
});
|
|
517
619
|
}
|
|
518
620
|
catch (error) {
|
|
@@ -521,7 +623,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
521
623
|
throw new CliUsageError(`Could not read artifact input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
522
624
|
}
|
|
523
625
|
if (!input.trim())
|
|
524
|
-
throw new CliUsageError('verify-artifact requires one JSON file or an
|
|
626
|
+
throw new CliUsageError('verify-artifact requires one JSON file or an artefact on stdin.');
|
|
525
627
|
let passphrase = null;
|
|
526
628
|
if (args.passphraseSource) {
|
|
527
629
|
let passphraseText;
|
|
@@ -692,7 +794,9 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
692
794
|
}
|
|
693
795
|
if (!input.trim())
|
|
694
796
|
throw new CliUsageError('review-evidence requires one JSON file or a document on stdin.');
|
|
695
|
-
const document =
|
|
797
|
+
const document = args.mmdbSource
|
|
798
|
+
? await buildOfflineEvidenceReviewWithLocalResources(input, commandContext.now(), { mmdbPath: args.mmdbSource })
|
|
799
|
+
: buildOfflineEvidenceReview(input, commandContext.now());
|
|
696
800
|
if (!args.quiet)
|
|
697
801
|
write(stdout, args.output === 'json'
|
|
698
802
|
? formatJsonDocument(document)
|
|
@@ -743,6 +847,86 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
743
847
|
: terminal(formatDomainControlResult(document), args.color));
|
|
744
848
|
return EXIT_CODES.SUCCESS;
|
|
745
849
|
}
|
|
850
|
+
if (args.action === 'assurance') {
|
|
851
|
+
failureLabel = 'Domain assurance review';
|
|
852
|
+
let input;
|
|
853
|
+
try {
|
|
854
|
+
input = dependencies.readArtifactInput
|
|
855
|
+
? await dependencies.readArtifactInput(args.source)
|
|
856
|
+
: await readSavedLookupInputBounded(args.source
|
|
857
|
+
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
858
|
+
: dependencies.stdin || process.stdin, {
|
|
859
|
+
limit: MAX_ASSURANCE_INPUT_BYTES,
|
|
860
|
+
label: 'Domain assurance input',
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
if (error instanceof CliUsageError)
|
|
865
|
+
throw error;
|
|
866
|
+
throw new CliUsageError(`Could not read domain assurance input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
867
|
+
}
|
|
868
|
+
if (!input.trim())
|
|
869
|
+
throw new CliUsageError('assurance requires one versioned JSON file or a document on stdin.');
|
|
870
|
+
let parsed;
|
|
871
|
+
try {
|
|
872
|
+
parsed = JSON.parse(input);
|
|
873
|
+
}
|
|
874
|
+
catch {
|
|
875
|
+
throw new CliUsageError('Domain assurance input is not valid JSON.');
|
|
876
|
+
}
|
|
877
|
+
let document;
|
|
878
|
+
try {
|
|
879
|
+
document = buildDomainAssurance(parsed, commandContext.now());
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Domain assurance input is invalid'));
|
|
883
|
+
}
|
|
884
|
+
if (!args.quiet)
|
|
885
|
+
write(stdout, args.output === 'json'
|
|
886
|
+
? formatJsonDocument(document)
|
|
887
|
+
: terminal(formatDomainAssurance(document), args.color));
|
|
888
|
+
return EXIT_CODES.SUCCESS;
|
|
889
|
+
}
|
|
890
|
+
if (args.action === 'sharing-review') {
|
|
891
|
+
failureLabel = 'Evidence sharing review';
|
|
892
|
+
let input;
|
|
893
|
+
try {
|
|
894
|
+
input = dependencies.readArtifactInput
|
|
895
|
+
? await dependencies.readArtifactInput(args.source)
|
|
896
|
+
: await readSavedLookupInputBounded(args.source
|
|
897
|
+
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
898
|
+
: dependencies.stdin || process.stdin, {
|
|
899
|
+
limit: MAX_SHARING_REVIEW_BYTES,
|
|
900
|
+
label: 'Sharing review input',
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
if (error instanceof CliUsageError)
|
|
905
|
+
throw error;
|
|
906
|
+
throw new CliUsageError(`Could not read sharing review input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
907
|
+
}
|
|
908
|
+
if (!input.trim())
|
|
909
|
+
throw new CliUsageError('sharing-review requires one artefact JSON file or a document on stdin.');
|
|
910
|
+
let document;
|
|
911
|
+
try {
|
|
912
|
+
document = await buildSharingReview(input, {
|
|
913
|
+
marking: args.marking,
|
|
914
|
+
recipientScope: args.recipientScope,
|
|
915
|
+
purpose: args.purpose,
|
|
916
|
+
humanReviewed: args.humanReviewed,
|
|
917
|
+
personalDataReviewed: args.personalDataReviewed,
|
|
918
|
+
redactionsConfirmed: args.redactionsConfirmed,
|
|
919
|
+
}, commandContext.now());
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Sharing review input is invalid'));
|
|
923
|
+
}
|
|
924
|
+
if (!args.quiet)
|
|
925
|
+
write(stdout, args.output === 'json'
|
|
926
|
+
? formatJsonDocument(document)
|
|
927
|
+
: terminal(formatSharingReview(document), args.color));
|
|
928
|
+
return document.summary.status === 'blocked' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
929
|
+
}
|
|
746
930
|
if (args.action === 'workflow-plan') {
|
|
747
931
|
failureLabel = 'Investigation plan';
|
|
748
932
|
const document = buildInvestigationPlan(args.recipe, args.subject, commandContext.now());
|
|
@@ -778,6 +962,36 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
778
962
|
: terminal(formatCliLookupDiff(document), args.color));
|
|
779
963
|
return EXIT_CODES.SUCCESS;
|
|
780
964
|
}
|
|
965
|
+
if (args.action === 'reconcile') {
|
|
966
|
+
failureLabel = 'Lookup observation reconciliation';
|
|
967
|
+
const readReconciliationInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
|
|
968
|
+
limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
|
|
969
|
+
label: 'Lookup reconciliation input',
|
|
970
|
+
})));
|
|
971
|
+
const inputs = [];
|
|
972
|
+
let totalBytes = 0;
|
|
973
|
+
try {
|
|
974
|
+
for (const source of args.sources) {
|
|
975
|
+
const input = await readReconciliationInput(source);
|
|
976
|
+
totalBytes += Buffer.byteLength(input, 'utf8');
|
|
977
|
+
if (totalBytes > MAX_LOOKUP_RECONCILIATION_INPUT_BYTES) {
|
|
978
|
+
throw new CliUsageError(`Lookup reconciliation input is limited to ${MAX_LOOKUP_RECONCILIATION_INPUT_BYTES} bytes in total.`);
|
|
979
|
+
}
|
|
980
|
+
inputs.push(input);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
catch (error) {
|
|
984
|
+
if (error instanceof CliUsageError)
|
|
985
|
+
throw error;
|
|
986
|
+
throw new CliUsageError(`Could not read Lookup reconciliation input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
987
|
+
}
|
|
988
|
+
const document = buildCliLookupReconciliation(inputs, commandContext.now());
|
|
989
|
+
if (!args.quiet)
|
|
990
|
+
write(stdout, args.output === 'json'
|
|
991
|
+
? formatJsonDocument(document)
|
|
992
|
+
: terminal(formatCliLookupReconciliation(document), args.color));
|
|
993
|
+
return EXIT_CODES.SUCCESS;
|
|
994
|
+
}
|
|
781
995
|
if (args.action === 'timeline') {
|
|
782
996
|
failureLabel = 'Lookup observation timeline';
|
|
783
997
|
const readTimelineInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
|
|
@@ -842,14 +1056,17 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
842
1056
|
}
|
|
843
1057
|
if (args.action === 'bulk') {
|
|
844
1058
|
failureLabel = 'Bulk lookup';
|
|
1059
|
+
const { runBulkCommand } = await import("./bulk-command-runner.mjs");
|
|
845
1060
|
return await runBulkCommand(args, dependencies, commandContext);
|
|
846
1061
|
}
|
|
847
1062
|
if (args.action === 'discover') {
|
|
848
1063
|
failureLabel = 'Candidate generation';
|
|
1064
|
+
const { runDiscoveryCommand } = await import("./discovery-command-runner.mjs");
|
|
849
1065
|
return await runDiscoveryCommand(args, dependencies, commandContext);
|
|
850
1066
|
}
|
|
851
1067
|
if (args.action === 'discover-scan') {
|
|
852
1068
|
failureLabel = 'Candidate scan';
|
|
1069
|
+
const { runDiscoveryScanCommand } = await import("./discovery-scan-command-runner.mjs");
|
|
853
1070
|
return await runDiscoveryScanCommand(args, dependencies, commandContext);
|
|
854
1071
|
}
|
|
855
1072
|
if (args.action === 'ct-search'
|
|
@@ -863,8 +1080,10 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
863
1080
|
: args.action === 'http'
|
|
864
1081
|
? 'HTTP probe'
|
|
865
1082
|
: 'TLS intelligence';
|
|
1083
|
+
const { runNetworkCommand } = await import("./network-command-runner.mjs");
|
|
866
1084
|
return await runNetworkCommand(args, dependencies, commandContext);
|
|
867
1085
|
}
|
|
1086
|
+
const { runLookupCommand } = await import("./lookup-command-runner.mjs");
|
|
868
1087
|
return await runLookupCommand(args, dependencies, commandContext);
|
|
869
1088
|
}
|
|
870
1089
|
catch (error) {
|
package/cli/saved-lookup.mjs
CHANGED
|
@@ -77,10 +77,10 @@ function parseSavedLookupDocument(text, options = {}) {
|
|
|
77
77
|
const rdapParsed = objectOrNull(rdap?.parsed);
|
|
78
78
|
const whoisParsed = objectOrNull(whois?.parsed);
|
|
79
79
|
if (rdapStatus === 'success' && !rdapParsed) {
|
|
80
|
-
throw new CliUsageError('Successful RDAP input is missing
|
|
80
|
+
throw new CliUsageError('Successful RDAP input is missing normalised parsed data.');
|
|
81
81
|
}
|
|
82
82
|
if ((whoisStatus === 'complete' || whoisStatus === 'partial') && !whoisParsed) {
|
|
83
|
-
throw new CliUsageError('Successful WHOIS input is missing
|
|
83
|
+
throw new CliUsageError('Successful WHOIS input is missing normalised parsed data.');
|
|
84
84
|
}
|
|
85
85
|
return document;
|
|
86
86
|
}
|