@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +1 -1
- package/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +67 -9
- package/cli/artifact-validation/lookup-evidence.mjs +18 -4
- package/cli/artifact-validation/signed-review.mjs +61 -64
- package/cli/artifact-verify.mjs +11 -2
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +6 -8
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +99 -6
- package/cli/case-pack.mjs +19 -4
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -210
- package/cli/command-definition.mjs +330 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +7 -9
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/domain-feed.mjs +61 -0
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/json.mjs +1 -13
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +32 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +147 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +3 -4
- package/cli/investigation-package-review.mjs +2 -0
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +220 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +19 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +85 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +132 -0
- package/cli/review-command-definitions.mjs +279 -0
- package/cli/review-command-runner.mjs +72 -159
- package/cli/risk-calibration.mjs +14 -12
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +73 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +4 -4
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +100 -0
- package/cli/workflow-command-definitions.mjs +103 -0
- package/cli/workflow-command-runner.mjs +40 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +322 -4
- package/docs/contextual-reviews.md +178 -0
- package/docs/infrastructure-observations.md +126 -0
- package/docs/offline-intake-context.md +213 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +35 -15
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +27 -11
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +75 -7
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +45 -25
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +27 -17
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +5 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +13 -4
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +13 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-child-profile-contract.mjs +28 -11
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +70 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/operation-budget.mjs +4 -0
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +46 -2
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +142 -139
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/technology-evidence-role.mjs +1 -51
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/tls-intelligence.mjs +30 -5
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +404 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-chain.mjs +12 -3
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +15 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +348 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +151 -0
- package/packages/cases/case-evidence-model.mjs +89 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-object-coverage.mjs +33 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +124 -36
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +111 -118
- package/packages/cases/case-record-projection.mjs +23 -4
- package/packages/cases/case-record-version-input.mjs +41 -0
- package/packages/cases/case-report.mjs +93 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +159 -1
- package/packages/cases/case-response-model.mjs +16 -1
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +108 -20
- package/packages/cases/case-response-packet-render.mjs +35 -12
- package/packages/cases/case-response-packet.mjs +55 -13
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +60 -8
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +217 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +35 -15
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +360 -49
- package/packages/contracts/cli-command-semantics.mjs +5 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +25 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/external-observation-interchange.mjs +96 -28
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/lookup-evidence.mjs +3 -2
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +37 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +144 -28
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +15 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +36 -29
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +36 -0
- package/packages/interchange/managed-indicator-set.mjs +223 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +79 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +87 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +132 -0
- package/packages/investigation/domain-transition-review.mjs +49 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +20 -0
- package/packages/investigation/investigation-manifest.mjs +35 -9
- package/packages/investigation/link-intake.mjs +209 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +235 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +231 -0
- package/packages/workspace/brand-profile-model.mjs +48 -4
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +27 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +11 -3
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +24 -7
- package/packages/workspace/watchlist-history.mjs +59 -16
- package/packages/workspace/watchlist-store.mjs +152 -11
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- package/lib/hostname.mjs +0 -2
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { constants as fsConstants } from 'node:fs';
|
|
2
2
|
import { open, realpath } from 'node:fs/promises';
|
|
3
3
|
import { isIP } from 'node:net';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
|
|
4
6
|
import { Reader } from 'maxmind';
|
|
5
7
|
import { formatIpPrefix, parseIpPrefix } from "../lib/ip-prefix.mjs";
|
|
6
8
|
import { CliUsageError } from "./errors.mjs";
|
|
9
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
10
|
+
import { canonicalPublicIpAddress } from "../packages/evidence/public-address-policy.mjs";
|
|
7
11
|
export const LOCAL_MMDB_QUERY_SCHEMA = 'whoisleuth.local-mmdb-query';
|
|
8
|
-
export const LOCAL_MMDB_QUERY_VERSION =
|
|
12
|
+
export const LOCAL_MMDB_QUERY_VERSION = 2;
|
|
13
|
+
export const LOCAL_MMDB_REVIEW_SCHEMA = 'whoisleuth.local-mmdb-review';
|
|
14
|
+
export const LOCAL_MMDB_REVIEW_VERSION = 1;
|
|
9
15
|
export const MAX_LOCAL_MMDB_BYTES = 512 * 1024 * 1024;
|
|
16
|
+
export const LOCAL_MMDB_REVIEW_DEADLINE_MS = 10_000;
|
|
17
|
+
export const MAX_LOCAL_MMDB_REVIEW_BYTES = 8 * 1024;
|
|
18
|
+
const DAY_MS = 86_400_000;
|
|
19
|
+
const LOCAL_MMDB_WORKER_KIND = 'local-mmdb-review';
|
|
20
|
+
const SUPPORTED_DATABASE_TYPES = new Set(['GeoIP2-City', 'GeoLite2-City', 'GeoIP2-Country', 'GeoLite2-Country',
|
|
21
|
+
'GeoIP2-ASN', 'GeoLite2-ASN', 'GeoIP2-ISP', 'GeoIP2-Enterprise']);
|
|
10
22
|
function record(value) {
|
|
11
23
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
12
24
|
}
|
|
@@ -19,24 +31,33 @@ function names(value) {
|
|
|
19
31
|
const source = record(value);
|
|
20
32
|
return boundedText(source.en, 120);
|
|
21
33
|
}
|
|
22
|
-
async function readStableMmdb(databasePath) {
|
|
34
|
+
async function readStableMmdb(databasePath, openFile = open, signal) {
|
|
23
35
|
let handle = null;
|
|
36
|
+
let database = null;
|
|
24
37
|
try {
|
|
38
|
+
signal?.throwIfAborted();
|
|
25
39
|
const resolvedPath = await realpath(databasePath);
|
|
26
|
-
|
|
40
|
+
signal?.throwIfAborted();
|
|
41
|
+
handle = await openFile(resolvedPath, fsConstants.O_RDONLY | fsConstants.O_NONBLOCK | fsConstants.O_NOFOLLOW);
|
|
42
|
+
signal?.throwIfAborted();
|
|
27
43
|
const before = await handle.stat();
|
|
44
|
+
signal?.throwIfAborted();
|
|
28
45
|
if (!before.isFile() || before.size <= 0 || before.size > MAX_LOCAL_MMDB_BYTES) {
|
|
29
46
|
throw new CliUsageError(`The supplied MMDB must be a file no larger than ${MAX_LOCAL_MMDB_BYTES} bytes.`);
|
|
30
47
|
}
|
|
31
|
-
|
|
48
|
+
// Dedicated backing storage can be transferred without copying a pooled
|
|
49
|
+
// buffer or exposing bytes outside the stable file's exact length.
|
|
50
|
+
database = Buffer.allocUnsafeSlow(before.size);
|
|
32
51
|
let offset = 0;
|
|
33
52
|
while (offset < database.length) {
|
|
34
53
|
const { bytesRead } = await handle.read(database, offset, database.length - offset, offset);
|
|
54
|
+
signal?.throwIfAborted();
|
|
35
55
|
if (bytesRead === 0)
|
|
36
56
|
break;
|
|
37
57
|
offset += bytesRead;
|
|
38
58
|
}
|
|
39
59
|
const after = await handle.stat();
|
|
60
|
+
signal?.throwIfAborted();
|
|
40
61
|
if (offset !== database.length
|
|
41
62
|
|| before.dev !== after.dev
|
|
42
63
|
|| before.ino !== after.ino
|
|
@@ -48,6 +69,8 @@ async function readStableMmdb(databasePath) {
|
|
|
48
69
|
return database;
|
|
49
70
|
}
|
|
50
71
|
catch (cause) {
|
|
72
|
+
database?.fill(0);
|
|
73
|
+
signal?.throwIfAborted();
|
|
51
74
|
if (cause instanceof CliUsageError)
|
|
52
75
|
throw cause;
|
|
53
76
|
throw new CliUsageError('The supplied MMDB file could not be read.');
|
|
@@ -56,7 +79,7 @@ async function readStableMmdb(databasePath) {
|
|
|
56
79
|
await handle?.close().catch(() => { });
|
|
57
80
|
}
|
|
58
81
|
}
|
|
59
|
-
|
|
82
|
+
function prepareQuery(input, checkedAt) {
|
|
60
83
|
const address = boundedText(input.address, 64)?.toLowerCase() ?? '';
|
|
61
84
|
const sourceLabel = boundedText(input.sourceLabel, 120);
|
|
62
85
|
const databaseVersion = boundedText(input.databaseVersion, 80);
|
|
@@ -66,15 +89,79 @@ export async function reviewLocalMmdb(input, databasePath) {
|
|
|
66
89
|
if (!sourceLabel || !databaseVersion || !license) {
|
|
67
90
|
throw new CliUsageError('Local MMDB review requires bounded sourceLabel, databaseVersion, and licence metadata.');
|
|
68
91
|
}
|
|
92
|
+
if (input.version !== undefined && input.version !== 1 && input.version !== LOCAL_MMDB_QUERY_VERSION) {
|
|
93
|
+
throw new CliUsageError('Local MMDB review requires a supported query version.');
|
|
94
|
+
}
|
|
95
|
+
if (input.version !== LOCAL_MMDB_QUERY_VERSION)
|
|
96
|
+
return { version: 1, address, sourceLabel, databaseVersion, license, checkedAt: null, freshnessPolicy: null };
|
|
97
|
+
const policy = record(input.freshnessPolicy);
|
|
98
|
+
const rationale = boundedText(policy.rationale, 240);
|
|
99
|
+
const reviewedAt = normalizeExplicitIsoTimestamp(checkedAt);
|
|
100
|
+
if (!reviewedAt || !rationale || /[\u0080-\u009f]/u.test(String(policy.rationale)) || !Number.isSafeInteger(policy.maxAgeDays) || Number(policy.maxAgeDays) <= 0
|
|
101
|
+
|| Number(policy.maxAgeDays) > Math.floor(Number.MAX_SAFE_INTEGER / DAY_MS)) {
|
|
102
|
+
throw new CliUsageError('Current MMDB review requires an explicit review time and freshnessPolicy with positive whole maxAgeDays and a bounded plain-text rationale.');
|
|
103
|
+
}
|
|
104
|
+
return { version: 2, address, sourceLabel, databaseVersion, license, checkedAt: reviewedAt,
|
|
105
|
+
freshnessPolicy: { maxAgeDays: Number(policy.maxAgeDays), rationale } };
|
|
106
|
+
}
|
|
107
|
+
/** Intrinsic metadata is separate from the analyst's declared version/licence.
|
|
108
|
+
* The policy admits age for this review, not accuracy or an update guarantee. */
|
|
109
|
+
export function inspectLocalMmdbMetadata(value, byteLength, checkedAt, policy) {
|
|
110
|
+
const metadata = record(value);
|
|
111
|
+
const databaseType = boundedText(metadata.databaseType, 120);
|
|
112
|
+
const buildMs = metadata.buildEpoch instanceof Date ? metadata.buildEpoch.getTime() : NaN;
|
|
113
|
+
const reviewMs = Date.parse(normalizeExplicitIsoTimestamp(checkedAt) ?? '');
|
|
114
|
+
const integer = (candidate) => typeof candidate === 'number' && Number.isSafeInteger(candidate) && candidate >= 0;
|
|
115
|
+
if (!databaseType || metadata.databaseType !== databaseType || !Number.isSafeInteger(buildMs) || buildMs < 0 || buildMs % 1000 !== 0 || !Number.isFinite(reviewMs)
|
|
116
|
+
|| !integer(metadata.binaryFormatMajorVersion) || !integer(metadata.binaryFormatMinorVersion)
|
|
117
|
+
|| ![4, 6].includes(Number(metadata.ipVersion)) || typeof metadata.ipVersion !== 'number'
|
|
118
|
+
|| !integer(metadata.nodeCount) || metadata.nodeCount < 1 || ![24, 28, 32].includes(Number(metadata.recordSize))
|
|
119
|
+
|| typeof metadata.recordSize !== 'number' || !integer(metadata.searchTreeSize)
|
|
120
|
+
|| metadata.searchTreeSize !== metadata.nodeCount * metadata.recordSize / 4 || metadata.searchTreeSize + 16 >= byteLength) {
|
|
121
|
+
return { metadata: null, ageDays: null, reason: 'invalid_metadata' };
|
|
122
|
+
}
|
|
123
|
+
const intrinsic = { databaseType, builtAt: new Date(buildMs).toISOString(),
|
|
124
|
+
binaryFormat: { major: metadata.binaryFormatMajorVersion, minor: metadata.binaryFormatMinorVersion }, ipVersion: metadata.ipVersion };
|
|
125
|
+
const ageDays = (reviewMs - buildMs) / DAY_MS;
|
|
126
|
+
const reason = metadata.binaryFormatMajorVersion !== 2 || metadata.binaryFormatMinorVersion !== 0 ? 'unsupported_format'
|
|
127
|
+
: !SUPPORTED_DATABASE_TYPES.has(databaseType) ? 'unsupported_database'
|
|
128
|
+
: ageDays < 0 ? 'future_database' : ageDays > policy.maxAgeDays ? 'stale_database' : 'admitted';
|
|
129
|
+
return { metadata: intrinsic, ageDays, reason };
|
|
130
|
+
}
|
|
131
|
+
const LIMITATIONS = Object.freeze([
|
|
132
|
+
'This CLI-only result comes from an analyst-supplied local MMDB file. WHOISleuth does not bundle, download, update, license, or transmit the database.',
|
|
133
|
+
'Geolocation and network attribution are provider estimates and may be stale, coarse, shared, or incorrect.',
|
|
134
|
+
'A location or ASN association does not establish the operator, owner, user, intent, safety, or maliciousness of an address.',
|
|
135
|
+
]);
|
|
136
|
+
function currentResult(query, identity, admission, reason, match = null) {
|
|
137
|
+
const state = match ? reason === 'matched' ? 'matched' : 'partial' : 'unavailable';
|
|
138
|
+
return { schema: LOCAL_MMDB_REVIEW_SCHEMA, version: LOCAL_MMDB_REVIEW_VERSION, state, reason,
|
|
139
|
+
completeness: state === 'matched' ? 'complete' : state === 'partial' ? 'partial' : 'unavailable',
|
|
140
|
+
address: query.address, match, source: { label: query.sourceLabel, version: query.databaseVersion, license: query.license },
|
|
141
|
+
database: { ...identity, metadata: admission.metadata },
|
|
142
|
+
freshness: { state: admission.reason === 'stale_database' ? 'stale' : admission.reason === 'admitted' ? 'current' : 'unknown',
|
|
143
|
+
checkedAt: query.checkedAt, ageDays: admission.ageDays, policy: query.freshnessPolicy },
|
|
144
|
+
limitations: [...LIMITATIONS, 'Declared source, version and licence labels are analyst claims; intrinsic metadata and digest do not authenticate the database or verify licensing.',
|
|
145
|
+
'Freshness is checked against the explicitly justified age policy. A miss, rejected or stale database is unavailable context, not evidence of location or absence.'] };
|
|
146
|
+
}
|
|
147
|
+
function reviewBytes(query, database, identity) {
|
|
69
148
|
let reader;
|
|
70
149
|
try {
|
|
71
|
-
reader = new Reader(
|
|
150
|
+
reader = new Reader(database);
|
|
72
151
|
}
|
|
73
|
-
catch
|
|
74
|
-
if (
|
|
75
|
-
|
|
152
|
+
catch {
|
|
153
|
+
if (query.version === 2)
|
|
154
|
+
return currentResult(query, identity, { metadata: null, ageDays: null, reason: 'invalid_metadata' }, 'invalid_database');
|
|
76
155
|
throw new CliUsageError('The supplied file is not a readable MaxMind DB database.');
|
|
77
156
|
}
|
|
157
|
+
const admission = query.version === 2 ? inspectLocalMmdbMetadata(reader.metadata, database.length, query.checkedAt, query.freshnessPolicy) : null;
|
|
158
|
+
if (admission && admission.reason !== 'admitted')
|
|
159
|
+
return currentResult(query, identity, admission, admission.reason);
|
|
160
|
+
if (admission && !canonicalPublicIpAddress(query.address))
|
|
161
|
+
return currentResult(query, identity, admission, 'unsupported_address');
|
|
162
|
+
if (admission && isIP(query.address) === 6 && admission.metadata?.ipVersion === 4)
|
|
163
|
+
return currentResult(query, identity, admission, 'unsupported_address_family');
|
|
164
|
+
const address = query.address;
|
|
78
165
|
const [rawMatch, prefixLength] = reader.getWithPrefixLength(address);
|
|
79
166
|
const match = record(rawMatch);
|
|
80
167
|
const traits = record(match.traits);
|
|
@@ -84,9 +171,10 @@ export async function reviewLocalMmdb(input, databasePath) {
|
|
|
84
171
|
const subdivision = record(subdivisions[0]);
|
|
85
172
|
const city = record(match.city);
|
|
86
173
|
const asnValue = match.autonomous_system_number ?? traits.autonomous_system_number;
|
|
87
|
-
const asn = typeof asnValue === 'number' && Number.isSafeInteger(asnValue) && asnValue >= 0
|
|
174
|
+
const asn = typeof asnValue === 'number' && Number.isSafeInteger(asnValue) && asnValue >= 0
|
|
175
|
+
&& (query.version === 1 || asnValue <= 4_294_967_295) ? asnValue : null;
|
|
88
176
|
const prefix = parseIpPrefix(`${address}/${prefixLength}`);
|
|
89
|
-
|
|
177
|
+
const result = Object.freeze({
|
|
90
178
|
state: rawMatch ? 'matched' : 'not_found',
|
|
91
179
|
address,
|
|
92
180
|
match: rawMatch ? Object.freeze({
|
|
@@ -97,11 +185,126 @@ export async function reviewLocalMmdb(input, databasePath) {
|
|
|
97
185
|
asn,
|
|
98
186
|
asName: boundedText(match.autonomous_system_organization ?? traits.autonomous_system_organization, 240),
|
|
99
187
|
}) : null,
|
|
100
|
-
source: Object.freeze({ label: sourceLabel, version: databaseVersion, license }),
|
|
101
|
-
limitations:
|
|
102
|
-
'This CLI-only result comes from an analyst-supplied local MMDB file. WHOISleuth does not bundle, download, update, license, or transmit the database.',
|
|
103
|
-
'Geolocation and network attribution are provider estimates and may be stale, coarse, shared, or incorrect.',
|
|
104
|
-
'A location or ASN association does not establish the operator, owner, user, intent, safety, or maliciousness of an address.',
|
|
105
|
-
]),
|
|
188
|
+
source: Object.freeze({ label: query.sourceLabel, version: query.databaseVersion, license: query.license }),
|
|
189
|
+
limitations: LIMITATIONS,
|
|
106
190
|
});
|
|
191
|
+
if (!admission)
|
|
192
|
+
return result;
|
|
193
|
+
if (!rawMatch)
|
|
194
|
+
return currentResult(query, identity, admission, 'no_match');
|
|
195
|
+
if (!result.match || !prefix || !rawMatch || typeof rawMatch !== 'object' || Array.isArray(rawMatch))
|
|
196
|
+
return currentResult(query, identity, admission, 'invalid_record');
|
|
197
|
+
const rawCountry = country.iso_code;
|
|
198
|
+
const countryCode = boundedText(rawCountry, 2)?.toUpperCase() ?? null;
|
|
199
|
+
const projected = { ...result.match, countryCode: countryCode && /^[A-Z]{2}$/u.test(countryCode) ? countryCode : null };
|
|
200
|
+
if (![projected.countryCode, projected.region, projected.city, projected.asn, projected.asName].some(value => value !== null)) {
|
|
201
|
+
return currentResult(query, identity, admission, 'no_useful_record');
|
|
202
|
+
}
|
|
203
|
+
const invalid = rawCountry !== undefined && projected.countryCode === null
|
|
204
|
+
|| asnValue !== undefined && asn === null
|
|
205
|
+
|| match.country !== undefined && (!match.country || typeof match.country !== 'object' || Array.isArray(match.country))
|
|
206
|
+
|| match.city !== undefined && (!match.city || typeof match.city !== 'object' || Array.isArray(match.city))
|
|
207
|
+
|| match.subdivisions !== undefined && !Array.isArray(match.subdivisions)
|
|
208
|
+
|| subdivision.names !== undefined && projected.region === null
|
|
209
|
+
|| subdivision.iso_code !== undefined && projected.region === null
|
|
210
|
+
|| city.names !== undefined && projected.city === null
|
|
211
|
+
|| (match.autonomous_system_organization ?? traits.autonomous_system_organization) !== undefined && projected.asName === null;
|
|
212
|
+
return currentResult(query, identity, admission, invalid ? 'incomplete_record' : 'matched', projected);
|
|
213
|
+
}
|
|
214
|
+
function freezeResult(result) {
|
|
215
|
+
if (result.match)
|
|
216
|
+
Object.freeze(result.match);
|
|
217
|
+
Object.freeze(result.source);
|
|
218
|
+
Object.freeze(result.limitations);
|
|
219
|
+
if ('database' in result) {
|
|
220
|
+
if (result.database.metadata) {
|
|
221
|
+
Object.freeze(result.database.metadata.binaryFormat);
|
|
222
|
+
Object.freeze(result.database.metadata);
|
|
223
|
+
}
|
|
224
|
+
Object.freeze(result.database);
|
|
225
|
+
Object.freeze(result.freshness.policy);
|
|
226
|
+
Object.freeze(result.freshness);
|
|
227
|
+
}
|
|
228
|
+
return Object.freeze(result);
|
|
229
|
+
}
|
|
230
|
+
if (!isMainThread && parentPort && record(workerData).kind === LOCAL_MMDB_WORKER_KIND) {
|
|
231
|
+
const port = parentPort, request = workerData;
|
|
232
|
+
try {
|
|
233
|
+
const bytes = Buffer.from(request.bytes.buffer, request.bytes.byteOffset, request.bytes.byteLength);
|
|
234
|
+
const result = reviewBytes(request.query, bytes, request.identity);
|
|
235
|
+
if (Buffer.byteLength(JSON.stringify(result), 'utf8') > MAX_LOCAL_MMDB_REVIEW_BYTES)
|
|
236
|
+
throw new TypeError('Local result exceeds its output bound.');
|
|
237
|
+
port.postMessage({ result });
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
port.postMessage({ error: true });
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
request.bytes.fill(0);
|
|
244
|
+
port.close();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** One disposable worker pre-empts the existing synchronous reader. It is not
|
|
248
|
+
* a sandbox and never receives a database path or inherited environment. */
|
|
249
|
+
export async function reviewLocalMmdb(input, databasePath, checkedAt = new Date().toISOString(), dependencies = {}) {
|
|
250
|
+
const { signal } = dependencies;
|
|
251
|
+
signal?.throwIfAborted();
|
|
252
|
+
const query = prepareQuery(input, checkedAt);
|
|
253
|
+
const deadlineMs = dependencies.deadlineMs ?? LOCAL_MMDB_REVIEW_DEADLINE_MS;
|
|
254
|
+
if (!Number.isSafeInteger(deadlineMs) || deadlineMs <= 0 || deadlineMs > LOCAL_MMDB_REVIEW_DEADLINE_MS)
|
|
255
|
+
throw new CliUsageError('Invalid local database processing deadline.');
|
|
256
|
+
const database = await readStableMmdb(databasePath, dependencies.openFile, signal);
|
|
257
|
+
if (signal?.aborted) {
|
|
258
|
+
database.fill(0);
|
|
259
|
+
signal.throwIfAborted();
|
|
260
|
+
}
|
|
261
|
+
const identity = { sha256: createHash('sha256').update(database).digest('hex'), byteLength: database.length };
|
|
262
|
+
const options = { workerData: { kind: LOCAL_MMDB_WORKER_KIND, query, bytes: database, identity },
|
|
263
|
+
transferList: [database.buffer], env: {}, execArgv: [], stdout: true, stderr: true,
|
|
264
|
+
resourceLimits: { maxOldGenerationSizeMb: 64, maxYoungGenerationSizeMb: 16, stackSizeMb: 4 } };
|
|
265
|
+
let worker;
|
|
266
|
+
try {
|
|
267
|
+
worker = dependencies.createWorker ? dependencies.createWorker(options) : new Worker(new URL(import.meta.url), options);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
if (database.byteLength)
|
|
271
|
+
database.fill(0);
|
|
272
|
+
signal?.throwIfAborted();
|
|
273
|
+
if (query.version === 2)
|
|
274
|
+
return freezeResult(currentResult(query, identity, { metadata: null, ageDays: null, reason: 'invalid_metadata' }, 'processing_unavailable'));
|
|
275
|
+
throw new CliUsageError('The supplied database could not be reviewed within its format and processing bounds.');
|
|
276
|
+
}
|
|
277
|
+
worker.stdout?.resume();
|
|
278
|
+
worker.stderr?.resume();
|
|
279
|
+
try {
|
|
280
|
+
return await new Promise((resolve, reject) => {
|
|
281
|
+
let settled = false;
|
|
282
|
+
const finish = (result, reason = 'processing_unavailable') => {
|
|
283
|
+
if (settled)
|
|
284
|
+
return;
|
|
285
|
+
settled = true;
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
signal?.removeEventListener('abort', onAbort);
|
|
288
|
+
if (signal?.aborted)
|
|
289
|
+
reject(signal.reason);
|
|
290
|
+
else if (result)
|
|
291
|
+
resolve(freezeResult(result));
|
|
292
|
+
else if (query.version === 2)
|
|
293
|
+
resolve(freezeResult(currentResult(query, identity, { metadata: null, ageDays: null, reason: 'invalid_metadata' }, reason)));
|
|
294
|
+
else
|
|
295
|
+
reject(new CliUsageError('The supplied database could not be reviewed within its format and processing bounds.'));
|
|
296
|
+
};
|
|
297
|
+
const onAbort = () => finish();
|
|
298
|
+
const timer = setTimeout(() => finish(undefined, 'processing_deadline'), deadlineMs);
|
|
299
|
+
worker.once('message', (reply) => reply && 'result' in reply ? finish(reply.result) : finish());
|
|
300
|
+
worker.once('error', () => finish());
|
|
301
|
+
worker.once('exit', () => finish());
|
|
302
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
303
|
+
if (signal?.aborted)
|
|
304
|
+
onAbort();
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
await worker.terminate();
|
|
309
|
+
}
|
|
107
310
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { scanBoundedJson } from "../
|
|
2
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
3
3
|
const LOOKALIKE_CALIBRATION_INPUT_SCHEMA = 'whoisleuth.lookalike-calibration-input';
|
|
4
4
|
const LOOKALIKE_CALIBRATION_SCHEMA = 'whoisleuth.lookalike-calibration';
|
|
5
5
|
const LOOKALIKE_CALIBRATION_VERSION = 1;
|
package/cli/lookup-brief.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
2
2
|
import { safeTerminalValue } from "./formatters/terminal.mjs";
|
|
3
3
|
import { compareRdapPublications, compareRegistrySources } from "../lib/registry-comparison.mjs";
|
|
4
|
-
|
|
5
|
-
export
|
|
4
|
+
import { CLI_LOOKUP_BRIEF_SCHEMA, CLI_LOOKUP_BRIEF_VERSION } from "../packages/contracts/cli-lookup-brief.mjs";
|
|
5
|
+
export { CLI_LOOKUP_BRIEF_SCHEMA, CLI_LOOKUP_BRIEF_VERSION };
|
|
6
6
|
function record(value) {
|
|
7
7
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
8
8
|
}
|
|
@@ -38,7 +38,7 @@ async function runLookupCommand(args, dependencies, context) {
|
|
|
38
38
|
eventProgress.emit({ event: 'started' });
|
|
39
39
|
const input = args.query || await context.readSingleInput();
|
|
40
40
|
if (!input)
|
|
41
|
-
throw new CliUsageError('lookup requires one domain, IP address, or ASN as an argument or on stdin.');
|
|
41
|
+
throw new CliUsageError('lookup requires one domain, IP address, or ASN as an argument or on stdin.', 'missing_input');
|
|
42
42
|
const classify = dependencies.classifyQuery || classifyQuery;
|
|
43
43
|
const executeLookup = dependencies.runUnifiedLookup || runUnifiedLookup;
|
|
44
44
|
let classified;
|
|
@@ -74,6 +74,18 @@ async function runLookupCommand(args, dependencies, context) {
|
|
|
74
74
|
...(args.vantageLabel ? { vantageLabel: args.vantageLabel } : {}),
|
|
75
75
|
});
|
|
76
76
|
};
|
|
77
|
+
const collect = (signal, onSourceSettled) => abortable(() => executeLookup(classified, {
|
|
78
|
+
fast: !args.deep,
|
|
79
|
+
compact: false,
|
|
80
|
+
...(signal ? { signal } : {}),
|
|
81
|
+
...(args.deep ? {
|
|
82
|
+
...(selectedUrl ? { selectedUrl } : {}),
|
|
83
|
+
onSourceSettled: settlement => {
|
|
84
|
+
onSourceSettled(settlement);
|
|
85
|
+
eventProgress.emit({ event: 'source_settled', source: settlement.source, state: settlement.state });
|
|
86
|
+
},
|
|
87
|
+
} : {}),
|
|
88
|
+
}), signal);
|
|
77
89
|
let document;
|
|
78
90
|
if (args.browse === true) {
|
|
79
91
|
const browse = dependencies.browseLookupOperation || browseLookupOperation;
|
|
@@ -88,23 +100,7 @@ async function runLookupCommand(args, dependencies, context) {
|
|
|
88
100
|
plannedSources: args.deep ? plannedLookupProgressSources(classified) : [],
|
|
89
101
|
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
90
102
|
collect: async ({ signal, onSourceSettled }) => {
|
|
91
|
-
|
|
92
|
-
? {
|
|
93
|
-
fast: false,
|
|
94
|
-
compact: false,
|
|
95
|
-
...(selectedUrl ? { selectedUrl } : {}),
|
|
96
|
-
signal,
|
|
97
|
-
onSourceSettled: (settlement) => {
|
|
98
|
-
onSourceSettled(settlement);
|
|
99
|
-
eventProgress.emit({ event: 'source_settled', source: settlement.source, state: settlement.state });
|
|
100
|
-
},
|
|
101
|
-
}
|
|
102
|
-
: {
|
|
103
|
-
fast: true,
|
|
104
|
-
compact: false,
|
|
105
|
-
signal,
|
|
106
|
-
}), signal);
|
|
107
|
-
return buildDocument(result);
|
|
103
|
+
return buildDocument(await collect(signal, onSourceSettled));
|
|
108
104
|
},
|
|
109
105
|
});
|
|
110
106
|
if (args.saveLookup) {
|
|
@@ -114,6 +110,7 @@ async function runLookupCommand(args, dependencies, context) {
|
|
|
114
110
|
const save = dependencies.writePrivateFile || writePrivateFile;
|
|
115
111
|
await save(args.saveLookup, serializeCliLookupDocument(document), {
|
|
116
112
|
existingFileMessage: 'Saved Lookup file already exists. Choose a new --save-lookup path or remove the existing file explicitly.',
|
|
113
|
+
beforePublish: async () => { dependencies.signal?.throwIfAborted(); },
|
|
117
114
|
});
|
|
118
115
|
context.writeStderr('Saved the completed private Lookup JSON. It can contain raw public registry and WHOIS responses plus target and collection context; review it before sharing.\n');
|
|
119
116
|
}
|
|
@@ -125,23 +122,10 @@ async function runLookupCommand(args, dependencies, context) {
|
|
|
125
122
|
let settledSources = 0;
|
|
126
123
|
let result;
|
|
127
124
|
try {
|
|
128
|
-
result = await
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
...(selectedUrl ? { selectedUrl } : {}),
|
|
133
|
-
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
134
|
-
onSourceSettled: (settlement) => {
|
|
135
|
-
settledSources += 1;
|
|
136
|
-
indicator.update(`Collected ${settledSources} source${settledSources === 1 ? '' : 's'} · ${settlement.source.replaceAll('_', ' ')} ${settlement.state}`);
|
|
137
|
-
eventProgress.emit({ event: 'source_settled', source: settlement.source, state: settlement.state });
|
|
138
|
-
},
|
|
139
|
-
}
|
|
140
|
-
: {
|
|
141
|
-
fast: true,
|
|
142
|
-
compact: false,
|
|
143
|
-
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
144
|
-
}), dependencies.signal);
|
|
125
|
+
result = await collect(dependencies.signal, settlement => {
|
|
126
|
+
settledSources += 1;
|
|
127
|
+
indicator.update(`Collected ${settledSources} source${settledSources === 1 ? '' : 's'} · ${settlement.source.replaceAll('_', ' ')} ${settlement.state}`);
|
|
128
|
+
});
|
|
145
129
|
}
|
|
146
130
|
finally {
|
|
147
131
|
context.endProgress();
|
package/cli/lookup-diff.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { relationshipObservation } from "../packages/comparison/relationship-evi
|
|
|
5
5
|
import { CliUsageError } from "./errors.mjs";
|
|
6
6
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
7
7
|
import { lookupObservationHostname } from "../packages/evidence/lookup-target.mjs";
|
|
8
|
+
import { safeTerminalValue } from "./formatters/terminal-shared.mjs";
|
|
8
9
|
export const CLI_LOOKUP_DIFF_SCHEMA = 'whoisleuth.cli.lookup-diff';
|
|
9
10
|
export const CLI_LOOKUP_DIFF_VERSION = 1;
|
|
10
11
|
function text(value, maximum = 300) {
|
|
@@ -198,10 +199,10 @@ function formatCliLookupDiff(document) {
|
|
|
198
199
|
const rows = document.comparison.rows.filter((row) => row.state !== 'equal');
|
|
199
200
|
const output = [
|
|
200
201
|
'Domain evidence diff',
|
|
201
|
-
`Left ${document.left.domain}`,
|
|
202
|
-
`Left observed ${document.left.generatedAt}`,
|
|
203
|
-
`Right ${document.right.domain}`,
|
|
204
|
-
`Right observed ${document.right.generatedAt}`,
|
|
202
|
+
`Left ${safeTerminalValue(document.left.domain)}`,
|
|
203
|
+
`Left observed ${safeTerminalValue(document.left.generatedAt)}`,
|
|
204
|
+
`Right ${safeTerminalValue(document.right.domain)}`,
|
|
205
|
+
`Right observed ${safeTerminalValue(document.right.generatedAt)}`,
|
|
205
206
|
`Changed ${document.comparison.counts.different + document.comparison.counts.conflicting}`,
|
|
206
207
|
`One-sided ${document.comparison.counts.missing}`,
|
|
207
208
|
`Unavailable ${document.comparison.counts.unavailable + document.comparison.counts.not_recorded}`,
|
|
@@ -210,14 +211,14 @@ function formatCliLookupDiff(document) {
|
|
|
210
211
|
if (!rows.length)
|
|
211
212
|
output.push('No bounded differences were observed in the comparable fields.');
|
|
212
213
|
for (const row of rows) {
|
|
213
|
-
output.push(`${row.label} [${row.state.replaceAll('_', ' ')}]`);
|
|
214
|
-
output.push(` Left: ${row.left}`);
|
|
215
|
-
output.push(` Right: ${row.right}`);
|
|
216
|
-
output.push(` Source: ${row.source}`);
|
|
214
|
+
output.push(`${safeTerminalValue(row.label)} [${safeTerminalValue(row.state).replaceAll('_', ' ')}]`);
|
|
215
|
+
output.push(` Left: ${safeTerminalValue(row.left)}`);
|
|
216
|
+
output.push(` Right: ${safeTerminalValue(row.right)}`);
|
|
217
|
+
output.push(` Source: ${safeTerminalValue(row.source)}`);
|
|
217
218
|
}
|
|
218
219
|
output.push('', 'Limitations:');
|
|
219
220
|
for (const limitation of document.limitations)
|
|
220
|
-
output.push(` - ${limitation}`);
|
|
221
|
+
output.push(` - ${safeTerminalValue(limitation)}`);
|
|
221
222
|
return `${output.join('\n')}\n`;
|
|
222
223
|
}
|
|
223
224
|
export { buildCliLookupDiff, formatCliLookupDiff };
|
package/cli/lookup-plan.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { plannedLookupSources } from "../lib/lookup-progress.mjs";
|
|
1
2
|
export const CLI_LOOKUP_PLAN_SCHEMA = 'whoisleuth.cli.lookup-plan';
|
|
2
3
|
export const CLI_LOOKUP_PLAN_VERSION = 1;
|
|
3
4
|
const RDAP = Object.freeze({
|
|
@@ -43,20 +44,15 @@ const REVERSE_DNS = Object.freeze({
|
|
|
43
44
|
conditional: false,
|
|
44
45
|
});
|
|
45
46
|
function plannedSources(classified, deep, selectedUrl) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
if (classified.type === 'ipv4' || classified.type === 'ipv6') {
|
|
57
|
-
return Object.freeze([RDAP, WHOIS, REVERSE_DNS]);
|
|
58
|
-
}
|
|
59
|
-
return Object.freeze([RDAP, WHOIS]);
|
|
47
|
+
const domainEvidence = selectedUrl && deep ? Object.freeze({
|
|
48
|
+
...DEEP_DOMAIN_EVIDENCE,
|
|
49
|
+
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.',
|
|
50
|
+
}) : deep ? DEEP_DOMAIN_EVIDENCE : FAST_DOMAIN_EVIDENCE;
|
|
51
|
+
const sources = {
|
|
52
|
+
rdap: RDAP, whois: WHOIS, domain_evidence: domainEvidence,
|
|
53
|
+
registrar_rdap: REGISTRAR_RDAP, network_context: NETWORK_CONTEXT, reverse_dns: REVERSE_DNS,
|
|
54
|
+
};
|
|
55
|
+
return Object.freeze(plannedLookupSources(classified.type, deep ? 'deep' : 'fast').map(id => sources[id]));
|
|
60
56
|
}
|
|
61
57
|
function buildCliLookupPlan(query, classified, deep, selectedUrl = false) {
|
|
62
58
|
return Object.freeze({
|
package/cli/lookup-reconcile.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { CliUsageError } from "./errors.mjs";
|
|
|
2
2
|
import { buildCliLookupDiff } from "./lookup-diff.mjs";
|
|
3
3
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
4
4
|
import { compareObservationContexts } from "../packages/comparison/capture-context.mjs";
|
|
5
|
+
import { safeTerminalValue } from "./formatters/terminal-shared.mjs";
|
|
5
6
|
const CLI_LOOKUP_RECONCILIATION_SCHEMA = 'whoisleuth.cli.lookup-reconciliation';
|
|
6
7
|
const CLI_LOOKUP_RECONCILIATION_VERSION = 1;
|
|
7
8
|
const MAX_LOOKUP_RECONCILIATION_INPUT_BYTES = 32 * 1024 * 1024;
|
|
@@ -117,7 +118,7 @@ function formatCliLookupReconciliation(document) {
|
|
|
117
118
|
const context = compareObservationContexts(document.observations.map(observation => ({ ...observation, observedAt: observation.generatedAt })));
|
|
118
119
|
const output = [
|
|
119
120
|
'Lookup observation reconciliation',
|
|
120
|
-
`Domain ${document.domain}`,
|
|
121
|
+
`Domain ${safeTerminalValue(document.domain)}`,
|
|
121
122
|
`Observations ${document.summary.observationCount}`,
|
|
122
123
|
`Declared labels ${context.labels}; collection independence not verified`,
|
|
123
124
|
`Capture time span ${context.time.spanMilliseconds === null ? 'unknown' : `${context.time.spanMilliseconds / 1000} seconds`}`,
|
|
@@ -125,18 +126,18 @@ function formatCliLookupReconciliation(document) {
|
|
|
125
126
|
`Disagreement ${document.summary.disagreement}`,
|
|
126
127
|
`Non-comparable ${document.summary.nonComparable}`,
|
|
127
128
|
'',
|
|
128
|
-
...document.observations.map((observation, index) => `Observation ${index + 1}: ${observation.generatedAt} · ${observation.mode} · observer ${observation.observerLabel
|
|
129
|
+
...document.observations.map((observation, index) => `Observation ${index + 1}: ${safeTerminalValue(observation.generatedAt)} · ${safeTerminalValue(observation.mode)} · observer ${safeTerminalValue(observation.observerLabel, 'not declared')} · vantage ${safeTerminalValue(observation.vantageLabel, 'not declared')}`),
|
|
129
130
|
'',
|
|
130
131
|
];
|
|
131
132
|
for (const field of document.fields.filter((candidate) => candidate.state !== 'agreement')) {
|
|
132
|
-
output.push(`${field.label} [${field.state.replaceAll('_', ' ')}]`);
|
|
133
|
+
output.push(`${safeTerminalValue(field.label)} [${safeTerminalValue(field.state).replaceAll('_', ' ')}]`);
|
|
133
134
|
for (const value of field.values) {
|
|
134
|
-
output.push(` Observation ${value.observation + 1}: ${value.value} (${value.sourceState.replaceAll('_', ' ')})`);
|
|
135
|
+
output.push(` Observation ${value.observation + 1}: ${safeTerminalValue(value.value)} (${safeTerminalValue(value.sourceState).replaceAll('_', ' ')})`);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
output.push('', 'Limitations:');
|
|
138
139
|
for (const limitation of document.limitations)
|
|
139
|
-
output.push(` - ${limitation}`);
|
|
140
|
+
output.push(` - ${safeTerminalValue(limitation)}`);
|
|
140
141
|
output.push(' - These selected observations do not establish worldwide availability, removal or takedown.');
|
|
141
142
|
return `${output.join('\n')}\n`;
|
|
142
143
|
}
|
package/cli/lookup-timeline.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer';
|
|
|
2
2
|
import { CliUsageError } from "./errors.mjs";
|
|
3
3
|
import { buildCliLookupDiff } from "./lookup-diff.mjs";
|
|
4
4
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
5
|
+
import { safeTerminalValue } from "./formatters/terminal-shared.mjs";
|
|
5
6
|
export const CLI_LOOKUP_TIMELINE_SCHEMA = 'whoisleuth.cli.lookup-timeline';
|
|
6
7
|
export const CLI_LOOKUP_TIMELINE_VERSION = 1;
|
|
7
8
|
export const MAX_LOOKUP_TIMELINE_OBSERVATIONS = 20;
|
|
@@ -95,10 +96,10 @@ function buildCliLookupTimeline(inputs, generatedAt = new Date().toISOString())
|
|
|
95
96
|
function formatCliLookupTimeline(document) {
|
|
96
97
|
const output = [
|
|
97
98
|
'Same-domain observation timeline',
|
|
98
|
-
`Domain ${document.domain}`,
|
|
99
|
+
`Domain ${safeTerminalValue(document.domain)}`,
|
|
99
100
|
`Observations ${document.summary.observationCount}`,
|
|
100
|
-
`First observed ${document.observations[0]?.generatedAt
|
|
101
|
-
`Last observed ${document.observations.at(-1)?.generatedAt
|
|
101
|
+
`First observed ${safeTerminalValue(document.observations[0]?.generatedAt, 'Unavailable')}`,
|
|
102
|
+
`Last observed ${safeTerminalValue(document.observations.at(-1)?.generatedAt, 'Unavailable')}`,
|
|
102
103
|
`Changed transitions ${document.summary.transitionsWithObservedChanges}`,
|
|
103
104
|
];
|
|
104
105
|
for (const transition of document.transitions) {
|
|
@@ -106,18 +107,18 @@ function formatCliLookupTimeline(document) {
|
|
|
106
107
|
const to = document.observations[transition.toObservation];
|
|
107
108
|
const counts = transition.comparison.counts;
|
|
108
109
|
const changedRows = transition.comparison.rows.filter((row) => (row.state === 'conflicting' || row.state === 'different' || row.state === 'missing'));
|
|
109
|
-
output.push('', `${from?.generatedAt
|
|
110
|
+
output.push('', `${safeTerminalValue(from?.generatedAt, 'Unavailable')} to ${safeTerminalValue(to?.generatedAt, 'Unavailable')}`, ` Observed changes ${counts.conflicting + counts.different}`, ` One-sided fields ${counts.missing}`, ` Unavailable ${counts.unavailable + counts.not_recorded}`);
|
|
110
111
|
if (!changedRows.length)
|
|
111
112
|
output.push(' No observed field changes in comparable evidence.');
|
|
112
113
|
for (const row of changedRows.slice(0, 10)) {
|
|
113
|
-
output.push(` ${row.label} [${row.state.replaceAll('_', ' ')}]: ${row.left} -> ${row.right}`);
|
|
114
|
+
output.push(` ${safeTerminalValue(row.label)} [${safeTerminalValue(row.state).replaceAll('_', ' ')}]: ${safeTerminalValue(row.left)} -> ${safeTerminalValue(row.right)}`);
|
|
114
115
|
}
|
|
115
116
|
if (changedRows.length > 10)
|
|
116
117
|
output.push(` ${changedRows.length - 10} additional changed fields omitted; use --json for the complete bounded comparison.`);
|
|
117
118
|
}
|
|
118
119
|
output.push('', 'Limitations:');
|
|
119
120
|
for (const limitation of document.limitations)
|
|
120
|
-
output.push(` - ${limitation}`);
|
|
121
|
+
output.push(` - ${safeTerminalValue(limitation)}`);
|
|
121
122
|
return `${output.join('\n')}\n`;
|
|
122
123
|
}
|
|
123
124
|
export { buildCliLookupTimeline, formatCliLookupTimeline };
|