@slicedearth/whoisleuth-cli 2.1.0 → 2.3.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/README.md +2 -1
- package/cli/arguments.mjs +389 -1745
- package/cli/artifact-validation/case-response.mjs +55 -30
- package/cli/artifact-validation/signed-review.mjs +30 -12
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/case-pack.mjs +39 -92
- package/cli/command-argument-grammar.mjs +211 -0
- package/cli/command-reference.mjs +200 -213
- package/cli/completion.mjs +5 -1
- package/cli/discovery-observation-snapshot.mjs +16 -3
- package/cli/discovery-scan-command-runner.mjs +19 -0
- package/cli/discovery-scan.mjs +6 -0
- package/cli/domain-control-observations.mjs +1 -1
- package/cli/inline-command-families.mjs +1 -0
- package/cli/input.mjs +116 -0
- package/cli/interchange-report.mjs +14 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/mail-header-review.mjs +467 -0
- package/cli/review-command-runner.mjs +25 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +7 -1
- package/docs/cli-reference.md +8 -1
- package/docs/cli.md +24 -1
- package/lib/bounded-json.mjs +9 -6
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-intelligence.mjs +37 -3
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/domain-change-packet.mjs +42 -14
- package/lib/domain-control-flight-recorder.mjs +4 -1
- package/lib/domain-control-manifest.mjs +16 -16
- package/lib/domain-posture-parsers.mjs +114 -3
- package/lib/domain-posture.mjs +34 -3
- package/lib/evidence-export.mjs +63 -13
- package/lib/evidence-report.mjs +2 -2
- package/lib/html-signals.mjs +39 -33
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-response.mjs +5 -4
- package/lib/lookup-source-progress.mjs +27 -4
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/registry-insights.mjs +35 -22
- package/lib/risk-calibration-summary.mjs +1 -3
- package/lib/security-txt.mjs +33 -1
- package/lib/static-html-analysis.mjs +69 -13
- package/lib/website-technology.mjs +79 -11
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +1 -1
- package/packages/cases/case-migration-model.mjs +71 -22
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +10 -21
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +240 -3
- package/packages/cases/case-record-projection.mjs +218 -0
- package/packages/cases/case-report.mjs +11 -6
- package/packages/cases/case-response-model.mjs +43 -6
- package/packages/cases/case-response-packet.mjs +78 -27
- package/packages/cases/case-storage-model.mjs +6 -2
- package/packages/comparison/comparison-ledger-bulk.mjs +9 -10
- package/packages/comparison/page-similarity.mjs +3 -1
- package/packages/contracts/case-portability.mjs +183 -53
- package/packages/contracts/cli-command-semantics.mjs +93 -0
- package/packages/contracts/risk-calibration.mjs +15 -10
- package/packages/contracts/workspace-portability.mjs +44 -7
- package/packages/evidence/domain-control-runtime.mjs +46 -6
- package/packages/interchange/external-findings-import.mjs +115 -46
- package/packages/investigation/lookup-asset-graph.mjs +35 -27
- package/packages/workspace/brand-profile-model.mjs +118 -2
- package/packages/workspace/bulk-review-model.mjs +13 -7
- package/packages/workspace/bulk-session-model.mjs +5 -4
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/relationship-observation-model.mjs +4 -1
- package/packages/workspace/website-snapshot-model.mjs +106 -8
- package/packages/workspace/workspace-archive.mjs +2 -0
- package/cli/evidence-command-arguments.mjs +0 -156
package/cli/completion.mjs
CHANGED
|
@@ -266,7 +266,7 @@ fi
|
|
|
266
266
|
`;
|
|
267
267
|
}
|
|
268
268
|
function fishCompletion() {
|
|
269
|
-
const commandLines = CLI_COMMANDS.map((command) => (`
|
|
269
|
+
const commandLines = CLI_COMMANDS.map((command) => (`__whoisleuth_add_command '${command}' '${COMMAND_DESCRIPTIONS[command]}'`));
|
|
270
270
|
const optionGroups = new Map();
|
|
271
271
|
for (const definition of CLI_COMMAND_REGISTRY) {
|
|
272
272
|
for (const option of definition.grammar.options) {
|
|
@@ -373,6 +373,9 @@ function __whoisleuth_command_is
|
|
|
373
373
|
test (count \$words) -ge 2; or return 1
|
|
374
374
|
contains -- \$words[2] $argv
|
|
375
375
|
end
|
|
376
|
+
function __whoisleuth_add_command
|
|
377
|
+
complete -c whoisleuth -n '__fish_use_subcommand' -a \$argv[1] -d \$argv[2]
|
|
378
|
+
end
|
|
376
379
|
function __whoisleuth_integer_values
|
|
377
380
|
set -l value $argv[1]
|
|
378
381
|
while test $value -le $argv[2]
|
|
@@ -434,6 +437,7 @@ end
|
|
|
434
437
|
complete -c whoisleuth -f
|
|
435
438
|
${[HELP_META_ACTION, VERSION_META_ACTION].map((action) => (`complete -c whoisleuth -n '__fish_use_subcommand' -s ${shortMetaAlias(action).slice(1)} -l ${longMetaAlias(action).slice(2)}`)).join('\n')}
|
|
436
439
|
${commandLines.join('\n')}
|
|
440
|
+
functions -e __whoisleuth_add_command
|
|
437
441
|
${positionalEnumLines.join('\n')}
|
|
438
442
|
complete -c whoisleuth -n '__whoisleuth_file_position' -F
|
|
439
443
|
${optionLines.join('\n')}
|
|
@@ -84,7 +84,11 @@ function normalizeObservation(value) {
|
|
|
84
84
|
return null;
|
|
85
85
|
const latestRegistrationState = item.latestRegistrationState;
|
|
86
86
|
const latestDnsState = item.latestDnsState;
|
|
87
|
+
const collectionOrigin = item.collectionOrigin === undefined
|
|
88
|
+
? 'current_run'
|
|
89
|
+
: item.collectionOrigin;
|
|
87
90
|
if (!['observed', 'unavailable'].includes(String(latestRegistrationState))
|
|
91
|
+
|| !['current_run', 'resumed_checkpoint'].includes(String(collectionOrigin))
|
|
88
92
|
|| typeof latestDnsState !== 'string' || !latestDnsState || latestDnsState.length > 40
|
|
89
93
|
|| /[\u0000-\u001f\u007f]/u.test(latestDnsState))
|
|
90
94
|
return null;
|
|
@@ -96,6 +100,7 @@ function normalizeObservation(value) {
|
|
|
96
100
|
registrationObservedAt,
|
|
97
101
|
dnsObservedAt,
|
|
98
102
|
latestAttemptAt: normalizedTimestamp(item.latestAttemptAt, 'Discovery latest-attempt time') ?? '',
|
|
103
|
+
collectionOrigin: collectionOrigin,
|
|
99
104
|
latestAttemptState: item.latestAttemptState,
|
|
100
105
|
latestRegistrationState: latestRegistrationState,
|
|
101
106
|
latestDnsState,
|
|
@@ -167,6 +172,13 @@ async function readExisting(path) {
|
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
function currentAttempt(domain, item, previous, generatedAt, deep) {
|
|
175
|
+
const suppliedObservedAt = item.observedAt === null || item.observedAt === undefined
|
|
176
|
+
? null
|
|
177
|
+
: normalizedTimestamp(item.observedAt, 'Discovery checkpoint observation time');
|
|
178
|
+
if (item.collectionOrigin === 'resumed_checkpoint' && !suppliedObservedAt) {
|
|
179
|
+
throw new CliUsageError('Discovery resume requires a valid observation time for retained checkpoint evidence.');
|
|
180
|
+
}
|
|
181
|
+
const attemptAt = suppliedObservedAt ?? generatedAt;
|
|
170
182
|
const availability = item.ok ? record(record(item.result).availability) : {};
|
|
171
183
|
const currentAvailabilityState = availabilityState(item) || 'unknown';
|
|
172
184
|
const registrationObserved = item.ok && currentAvailabilityState !== 'unknown';
|
|
@@ -177,9 +189,9 @@ function currentAttempt(domain, item, previous, generatedAt, deep) {
|
|
|
177
189
|
const dns = dnsComplete || (dnsRetainable && !previousDnsRetainable)
|
|
178
190
|
? currentDns
|
|
179
191
|
: previousDnsRetainable ? previous.dns : currentDns;
|
|
180
|
-
const registrationObservedAt = registrationObserved ?
|
|
192
|
+
const registrationObservedAt = registrationObserved ? attemptAt : previous?.registrationObservedAt ?? null;
|
|
181
193
|
const dnsObservedAt = dnsComplete || (dnsRetainable && !previousDnsRetainable)
|
|
182
|
-
?
|
|
194
|
+
? attemptAt
|
|
183
195
|
: previous?.dnsObservedAt ?? null;
|
|
184
196
|
const unavailableComponents = [];
|
|
185
197
|
if (!registrationObserved)
|
|
@@ -204,7 +216,8 @@ function currentAttempt(domain, item, previous, generatedAt, deep) {
|
|
|
204
216
|
observedAt: latestObservedAt(registrationObservedAt, dnsObservedAt),
|
|
205
217
|
registrationObservedAt,
|
|
206
218
|
dnsObservedAt,
|
|
207
|
-
latestAttemptAt:
|
|
219
|
+
latestAttemptAt: attemptAt,
|
|
220
|
+
collectionOrigin: item.collectionOrigin ?? 'current_run',
|
|
208
221
|
latestAttemptState: attemptState,
|
|
209
222
|
latestRegistrationState: registrationObserved ? 'observed' : 'unavailable',
|
|
210
223
|
latestDnsState: deep ? currentDns.status : 'not_requested',
|
|
@@ -62,6 +62,9 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
62
62
|
const classify = dependencies.classifyQuery || classifyQuery;
|
|
63
63
|
const allowlist = await readAllowlist(args.allowlistSource, dependencies, context, classify);
|
|
64
64
|
const checkpointWriter = dependencies.createBulkCheckpointWriter || createBulkCheckpointWriter;
|
|
65
|
+
const collectionContext = resolverServers.length
|
|
66
|
+
? { dnsResolver: 'analyst_selected', resolverServers: [...resolverServers] }
|
|
67
|
+
: { dnsResolver: 'system_default', resolverServers: [] };
|
|
65
68
|
const checkpoint = args.checkpoint
|
|
66
69
|
? await checkpointWriter({
|
|
67
70
|
path: args.checkpoint,
|
|
@@ -72,6 +75,20 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
72
75
|
...(dependencies.now ? { now: dependencies.now } : {}),
|
|
73
76
|
})
|
|
74
77
|
: null;
|
|
78
|
+
for (const item of checkpoint?.initialResults ?? []) {
|
|
79
|
+
const retainedContext = item.collectionContext;
|
|
80
|
+
const contextMatches = retainedContext
|
|
81
|
+
? retainedContext.dnsResolver === collectionContext.dnsResolver
|
|
82
|
+
&& retainedContext.resolverServers.length === collectionContext.resolverServers.length
|
|
83
|
+
&& retainedContext.resolverServers.every((server, index) => server === collectionContext.resolverServers[index])
|
|
84
|
+
: collectionContext.dnsResolver === 'system_default';
|
|
85
|
+
if (!contextMatches) {
|
|
86
|
+
throw new CliUsageError('Bulk checkpoint collection context does not match the current DNS resolver selection.');
|
|
87
|
+
}
|
|
88
|
+
if (item.observedAt === null || item.observedAt === undefined) {
|
|
89
|
+
throw new CliUsageError('Discovery resume requires an observation time for every retained checkpoint result.');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
75
92
|
const indicator = context.beginProgress(`Collecting 0 of ${queries.length} generated candidates`);
|
|
76
93
|
let completed = checkpoint?.initialResults.length || 0;
|
|
77
94
|
const resumedItems = new Map((checkpoint?.initialResults ?? []).map((item) => [item.index, item]));
|
|
@@ -95,6 +112,7 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
95
112
|
...item,
|
|
96
113
|
observedAt: context.now(),
|
|
97
114
|
collectionOrigin: 'current_run',
|
|
115
|
+
collectionContext,
|
|
98
116
|
};
|
|
99
117
|
settledItems.set(item.index, observed);
|
|
100
118
|
completed += 1;
|
|
@@ -107,6 +125,7 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
107
125
|
...item,
|
|
108
126
|
observedAt: null,
|
|
109
127
|
collectionOrigin: 'current_run',
|
|
128
|
+
collectionContext,
|
|
110
129
|
});
|
|
111
130
|
}
|
|
112
131
|
finally {
|
package/cli/discovery-scan.mjs
CHANGED
|
@@ -125,6 +125,12 @@ function buildScanItem(candidate, item, allowlist, relationshipIds = []) {
|
|
|
125
125
|
dnsSummary: bulkDnsSummary(item),
|
|
126
126
|
review: { lane: review.lane, reasons: review.reasons },
|
|
127
127
|
relationshipIds: [...relationshipIds],
|
|
128
|
+
observedAt: item.observedAt ?? null,
|
|
129
|
+
collectionOrigin: item.collectionOrigin ?? 'current_run',
|
|
130
|
+
collectionContext: item.collectionContext ?? {
|
|
131
|
+
dnsResolver: 'system_default',
|
|
132
|
+
resolverServers: [],
|
|
133
|
+
},
|
|
128
134
|
...(item.ok ? {
|
|
129
135
|
availability: result.availability,
|
|
130
136
|
diagnostics: result.diagnostics,
|
|
@@ -115,7 +115,7 @@ export function domainControlObservationFromSavedLookup(document) {
|
|
|
115
115
|
field('registry_nameservers', 'Registry RDAP', rdapState, list(rdapParsed.nameservers, hostname)),
|
|
116
116
|
field('whois_nameservers', 'WHOIS', diagnostic(document, 'whois'), list(record(record(document.whois).parsed).nameservers, hostname)),
|
|
117
117
|
field('delegated_nameservers', 'DNS', dnsState, list(dnsRecords.ns, hostname)),
|
|
118
|
-
field('delegation_ds', 'DNS delegation', state(delegation.status
|
|
118
|
+
field('delegation_ds', 'DNS delegation', state(delegation.status), list(delegationRecords.ds, dsValue)),
|
|
119
119
|
field('mail_exchangers', 'DNS', dnsState, list(dnsRecords.mx, mxValue)),
|
|
120
120
|
field('caa_policy', 'DNS', dnsState, list(record(dns.caaPolicy).records ?? dnsRecords.caa, caaValue)),
|
|
121
121
|
field('tls_certificate', 'TLS', tlsState, list([certificate.fingerprintSha256 ?? tls.fingerprintSha256], (item) => text(item, 128))),
|
package/cli/input.mjs
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { open } from 'node:fs/promises';
|
|
2
4
|
import { decodeBoundedUtf8, readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
3
5
|
import { CliUsageError } from "./errors.mjs";
|
|
6
|
+
function headerEnd(bytes) {
|
|
7
|
+
for (let index = 0; index < bytes.length - 1; index += 1) {
|
|
8
|
+
if (bytes[index] === 0x0a && bytes[index + 1] === 0x0a)
|
|
9
|
+
return index;
|
|
10
|
+
if (index < bytes.length - 3
|
|
11
|
+
&& bytes[index] === 0x0d && bytes[index + 1] === 0x0a
|
|
12
|
+
&& bytes[index + 2] === 0x0d && bytes[index + 3] === 0x0a)
|
|
13
|
+
return index;
|
|
14
|
+
}
|
|
15
|
+
return -1;
|
|
16
|
+
}
|
|
17
|
+
function decodeHeaderStorage(storage, length, maximumBytes, label) {
|
|
18
|
+
const end = headerEnd(storage.subarray(0, length));
|
|
19
|
+
if ((end < 0 && length > maximumBytes) || end > maximumBytes) {
|
|
20
|
+
throw new CliUsageError(`${label} is limited to ${maximumBytes} bytes before the message body.`);
|
|
21
|
+
}
|
|
22
|
+
return decodeBoundedUtf8(storage.subarray(0, end < 0 ? length : end), label);
|
|
23
|
+
}
|
|
24
|
+
function appendUntilHeaderEnd(storage, offset, input) {
|
|
25
|
+
let nextOffset = offset;
|
|
26
|
+
for (const byte of input) {
|
|
27
|
+
if (nextOffset >= storage.length)
|
|
28
|
+
return Object.freeze({ offset: nextOffset, complete: false });
|
|
29
|
+
storage[nextOffset] = byte;
|
|
30
|
+
nextOffset += 1;
|
|
31
|
+
const lfBoundary = nextOffset >= 2
|
|
32
|
+
&& storage[nextOffset - 2] === 0x0a
|
|
33
|
+
&& storage[nextOffset - 1] === 0x0a;
|
|
34
|
+
const crlfBoundary = nextOffset >= 4
|
|
35
|
+
&& storage[nextOffset - 4] === 0x0d
|
|
36
|
+
&& storage[nextOffset - 3] === 0x0a
|
|
37
|
+
&& storage[nextOffset - 2] === 0x0d
|
|
38
|
+
&& storage[nextOffset - 1] === 0x0a;
|
|
39
|
+
if (lfBoundary || crlfBoundary)
|
|
40
|
+
return Object.freeze({ offset: nextOffset, complete: true });
|
|
41
|
+
}
|
|
42
|
+
return Object.freeze({ offset: nextOffset, complete: false });
|
|
43
|
+
}
|
|
4
44
|
function abortError(signal) {
|
|
5
45
|
return signal.reason instanceof Error
|
|
6
46
|
? signal.reason
|
|
@@ -60,3 +100,79 @@ export async function readCliTextInput(source, stdin, options) {
|
|
|
60
100
|
options.signal?.removeEventListener('abort', onAbort);
|
|
61
101
|
}
|
|
62
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Read only the RFC-style header block from a selected regular file or stdin.
|
|
105
|
+
* The fixed retained allocation includes delimiter look-ahead but accumulates
|
|
106
|
+
* only bytes through the first header/body boundary. A regular file may be
|
|
107
|
+
* larger than the header limit while parsing stays tied to the admitted prefix.
|
|
108
|
+
*/
|
|
109
|
+
export async function readCliHeaderInput(source, stdin, options) {
|
|
110
|
+
if (!Number.isSafeInteger(options.maximumBytes) || options.maximumBytes < 1 || options.maximumBytes > 1024 * 1024) {
|
|
111
|
+
throw new TypeError('Header input has an invalid byte boundary.');
|
|
112
|
+
}
|
|
113
|
+
if (options.signal?.aborted)
|
|
114
|
+
throw abortError(options.signal);
|
|
115
|
+
const storage = Buffer.allocUnsafe(options.maximumBytes + 4);
|
|
116
|
+
if (source) {
|
|
117
|
+
let handle;
|
|
118
|
+
try {
|
|
119
|
+
handle = await open(source, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
120
|
+
const before = await handle.stat();
|
|
121
|
+
if (!before.isFile())
|
|
122
|
+
throw new CliUsageError(`${options.label} must be a regular file.`);
|
|
123
|
+
let offset = 0;
|
|
124
|
+
let complete = false;
|
|
125
|
+
const chunk = Buffer.allocUnsafe(Math.min(4 * 1024, storage.length));
|
|
126
|
+
while (offset < storage.length && !complete) {
|
|
127
|
+
if (options.signal?.aborted)
|
|
128
|
+
throw abortError(options.signal);
|
|
129
|
+
const result = await handle.read(chunk, 0, Math.min(chunk.length, storage.length - offset), null);
|
|
130
|
+
if (result.bytesRead === 0)
|
|
131
|
+
break;
|
|
132
|
+
({ offset, complete } = appendUntilHeaderEnd(storage, offset, chunk.subarray(0, result.bytesRead)));
|
|
133
|
+
}
|
|
134
|
+
chunk.fill(0);
|
|
135
|
+
const after = await handle.stat();
|
|
136
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
|
|
137
|
+
throw new CliUsageError(`${options.label} changed while it was being read.`);
|
|
138
|
+
}
|
|
139
|
+
return decodeHeaderStorage(storage, offset, options.maximumBytes, options.label);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof CliUsageError || options.signal?.aborted)
|
|
143
|
+
throw error;
|
|
144
|
+
const message = error instanceof Error ? error.message : `${options.label} could not be read.`;
|
|
145
|
+
throw new CliUsageError(message);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await handle?.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!stdin || stdin.isTTY)
|
|
152
|
+
return '';
|
|
153
|
+
let offset = 0;
|
|
154
|
+
const onAbort = () => {
|
|
155
|
+
if ('destroy' in stdin && typeof stdin.destroy === 'function')
|
|
156
|
+
stdin.destroy(abortError(options.signal));
|
|
157
|
+
};
|
|
158
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
159
|
+
try {
|
|
160
|
+
for await (const chunk of stdin) {
|
|
161
|
+
if (options.signal?.aborted)
|
|
162
|
+
throw abortError(options.signal);
|
|
163
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
164
|
+
const retained = appendUntilHeaderEnd(storage, offset, bytes);
|
|
165
|
+
offset = retained.offset;
|
|
166
|
+
if (retained.complete)
|
|
167
|
+
break;
|
|
168
|
+
if (offset >= storage.length)
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
if (options.signal?.aborted)
|
|
172
|
+
throw abortError(options.signal);
|
|
173
|
+
return decodeHeaderStorage(storage, offset, options.maximumBytes, options.label);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -198,6 +198,19 @@ export async function buildInterchangeFidelityReport(raw, options = {}) {
|
|
|
198
198
|
export function formatInterchangeFidelityReport(report) {
|
|
199
199
|
const browser = report.compatibility.browser;
|
|
200
200
|
const cli = report.compatibility.cli;
|
|
201
|
+
const nextAction = !report.recognised
|
|
202
|
+
? 'Choose a supported WHOISleuth export and run verify-artifact before trying to import it.'
|
|
203
|
+
: !report.artifact.versionSupported
|
|
204
|
+
? 'Use a WHOISleuth release that supports this exact version, or re-export it with its last supported release.'
|
|
205
|
+
: !report.verification.assuranceSatisfied
|
|
206
|
+
? 'Do not import or share this artefact yet; run verify-artifact and resolve the reported structure or integrity failure.'
|
|
207
|
+
: report.compatibility.fullyImportable === false
|
|
208
|
+
? 'Review unsupported, blocked, skipped or pruned records before selecting any import.'
|
|
209
|
+
: browser?.import === 'supported'
|
|
210
|
+
? 'Open the browser import preview and review the proposed additions and updates before applying them.'
|
|
211
|
+
: cli?.read === 'supported'
|
|
212
|
+
? 'Use the declared offline CLI reader; browser import is not supported for this artefact.'
|
|
213
|
+
: 'Retain the verification result and use only the operations declared above.';
|
|
201
214
|
return [
|
|
202
215
|
'Interchange fidelity report',
|
|
203
216
|
`Recognised ${report.recognised ? 'yes' : 'no'}`,
|
|
@@ -219,6 +232,7 @@ export function formatInterchangeFidelityReport(report) {
|
|
|
219
232
|
'',
|
|
220
233
|
`Preserved ${report.compatibility.preservedFieldGroups.join(', ') || 'none declared'}`,
|
|
221
234
|
`Excluded ${report.compatibility.excludedFieldGroups.join(', ') || 'none declared'}`,
|
|
235
|
+
`Next action ${nextAction}`,
|
|
222
236
|
'',
|
|
223
237
|
...report.limitations.map((item) => `Limitation: ${item}`),
|
|
224
238
|
'',
|
|
@@ -6,7 +6,9 @@ const LOOKALIKE_CALIBRATION_VERSION = 1;
|
|
|
6
6
|
const MAX_LOOKALIKE_CALIBRATION_BYTES = 2 * 1024 * 1024;
|
|
7
7
|
const MAX_LOOKALIKE_CALIBRATION_RECORDS = 5_000;
|
|
8
8
|
const MIN_LOOKALIKE_CALIBRATION_SAMPLE = 20;
|
|
9
|
-
|
|
9
|
+
// Version 1 intentionally admits reviewed Case dispositions only. This local
|
|
10
|
+
// schema remains an independent compatibility decision from the live Case model.
|
|
11
|
+
const DISPOSITIONS = Object.freeze(['suspicious', 'confirmed_abuse', 'false_positive', 'expected', 'closed_no_action']);
|
|
10
12
|
function record(value) {
|
|
11
13
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
12
14
|
}
|
|
@@ -22,7 +24,10 @@ function ratio(value, total) {
|
|
|
22
24
|
return total ? Number((value / total).toFixed(4)) : 0;
|
|
23
25
|
}
|
|
24
26
|
function emptyCounts() {
|
|
25
|
-
|
|
27
|
+
const counts = {};
|
|
28
|
+
for (const disposition of DISPOSITIONS)
|
|
29
|
+
counts[disposition] = 0;
|
|
30
|
+
return counts;
|
|
26
31
|
}
|
|
27
32
|
function buildLookalikeCalibration(raw, generatedAt = new Date().toISOString()) {
|
|
28
33
|
if (Buffer.byteLength(raw, 'utf8') > MAX_LOOKALIKE_CALIBRATION_BYTES) {
|
package/cli/lookup-brief.mjs
CHANGED
|
@@ -111,8 +111,10 @@ export function buildCliLookupBrief(input, generatedAt = new Date().toISOString(
|
|
|
111
111
|
const facts = [
|
|
112
112
|
['registration-state', 'Registration state', availability.state, availabilitySource.id],
|
|
113
113
|
['registrar', 'Registrar', record(preferred.registrar).name ?? preferred.registrar, preferredRegistrationSource.id],
|
|
114
|
-
['created', 'Created', record(preferred.lifecycle).
|
|
115
|
-
|
|
114
|
+
['created', 'Created', record(preferred.lifecycle).createdDateIso
|
|
115
|
+
?? record(preferred.lifecycle).createdIso ?? preferred.createdDateIso, preferredRegistrationSource.id],
|
|
116
|
+
['expires', 'Expires', record(preferred.lifecycle).expiryDateIso
|
|
117
|
+
?? record(preferred.lifecycle).expiryIso ?? preferred.expiryDateIso, preferredRegistrationSource.id],
|
|
116
118
|
['name-servers', 'Name servers', joinedText(preferred.nameservers), preferredRegistrationSource.id],
|
|
117
119
|
['website-activity', 'Website activity', availability.activityStatus, httpSource.id],
|
|
118
120
|
['page-title', 'Page title', availability.pageTitle, httpSource.id],
|
|
@@ -2,7 +2,7 @@ import { formatTerminalLookup, safeTerminalValue } from "./formatters/terminal.m
|
|
|
2
2
|
import { presentTerminalOutput, wrapTerminalOutput, } from "./terminal-presentation.mjs";
|
|
3
3
|
const MIN_LOOKUP_BROWSER_HEIGHT = 12;
|
|
4
4
|
const MAX_LOOKUP_BROWSER_HEIGHT = 80;
|
|
5
|
-
const MAX_LOOKUP_BROWSER_PANELS =
|
|
5
|
+
const MAX_LOOKUP_BROWSER_PANELS = 16;
|
|
6
6
|
const MAX_LOOKUP_BROWSER_SEARCH_BYTES = 256;
|
|
7
7
|
const MAX_LOOKUP_BROWSER_SEARCH_SCALARS = 80;
|
|
8
8
|
const MAX_LOOKUP_BROWSER_SEARCH_MATCHES = 256;
|