@slicedearth/whoisleuth-cli 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +8 -5
- package/NOTICE +3 -2
- package/README.md +60 -67
- package/cli/artifact-validation/case-response.mjs +6 -857
- package/cli/artifact-validation/lookup-evidence.mjs +15 -2
- package/cli/artifact-verify.mjs +32 -1
- package/cli/bulk-command-runner.mjs +5 -4
- package/cli/case-command.mjs +17 -17
- package/cli/case-pack.mjs +7 -1
- package/cli/ci-report.mjs +17 -5
- package/cli/collection-arguments.mjs +6 -0
- package/cli/collection-command-definitions.mjs +13 -3
- package/cli/command-definition.mjs +7 -5
- package/cli/discovery-scan-command-runner.mjs +13 -7
- package/cli/doctor.mjs +37 -3
- package/cli/domain-control-monitor.mjs +25 -2
- package/cli/domain-control-observations.mjs +52 -15
- package/cli/fail-policy.mjs +31 -22
- package/cli/formatters/html.mjs +3 -0
- package/cli/formatters/json.mjs +8 -1
- package/cli/formatters/terminal-lookup.mjs +24 -1
- package/cli/history-command-definitions.mjs +1 -1
- package/cli/investigation-plan.mjs +15 -0
- package/cli/lookup-brief.mjs +11 -5
- package/cli/lookup-command-runner.mjs +26 -18
- package/cli/lookup-diff.mjs +1 -0
- package/cli/review-command-definitions.mjs +11 -11
- package/cli/risk-policy.mjs +83 -0
- package/cli/saved-lookup.mjs +22 -7
- package/cli/source-reliability.mjs +12 -10
- package/cli/support-arguments.mjs +1 -1
- package/cli/support-command-definitions.mjs +5 -4
- package/cli/support-command-runner.mjs +6 -4
- package/cli/workflow-command-definitions.mjs +2 -1
- package/cli/workflow-command-runner.mjs +5 -1
- package/docs/cli-reference.md +56 -11
- package/docs/cli.md +162 -33
- package/docs/contextual-reviews.md +10 -0
- package/docs/infrastructure-observations.md +9 -0
- package/lib/dmarc-discovery.mjs +4 -2
- package/lib/dns-inheritance-review.mjs +14 -1
- package/lib/evidence-export.mjs +10 -4
- package/lib/evidence-report.mjs +2 -1
- package/lib/generated/cisa-kev-catalog.mjs +19 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +178 -5
- package/lib/idn-confusables.mjs +5 -1
- package/lib/lookup-collection-receipt.mjs +52 -0
- package/lib/lookup-diagnostics.mjs +2 -12
- package/lib/lookup-progress.mjs +29 -31
- package/lib/lookup-response-contract.mjs +9 -0
- package/lib/lookup-response.mjs +15 -14
- package/lib/lookup-source-progress.mjs +4 -2
- package/lib/lookup.mjs +33 -2
- package/lib/rdap-capabilities.mjs +43 -9
- package/lib/registry-insights.mjs +17 -6
- package/lib/sslbl-certificates.generated.mjs +143 -142
- package/lib/whois-authority.mjs +56 -1
- package/lib/whois-chain.mjs +2 -1
- package/lib/whois-contracts.mjs +2 -0
- package/lib/whois-normalization.mjs +4 -4
- package/package.json +6 -6
- package/packages/analysis/bounded-json.mjs +12 -2
- package/packages/analysis/brand-profile-signals.mjs +42 -0
- package/packages/cases/case-attachment-model.mjs +36 -1
- package/packages/cases/case-checkpoint-receipts.mjs +68 -0
- package/packages/cases/case-evidence-links.mjs +28 -0
- package/packages/cases/case-evidence-model.mjs +39 -6
- package/packages/cases/case-migration-model.mjs +9 -1
- package/packages/cases/case-object-coverage.mjs +106 -20
- package/packages/cases/case-record-operations.mjs +13 -0
- package/packages/cases/case-record-projection.mjs +28 -2
- package/packages/cases/case-report.mjs +5 -4
- package/packages/cases/case-response-packet-validation.mjs +880 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +12 -1
- package/packages/cases/case-response-packet.mjs +2 -15
- package/packages/cases/case-response-review-inputs.mjs +18 -1
- package/packages/cases/case-type-presentation.mjs +10 -0
- package/packages/cases/case-workflow-metadata.mjs +0 -8
- package/packages/comparison/capture-context.mjs +3 -2
- package/packages/contracts/analyst-review-state-contract.mjs +1 -1
- package/packages/contracts/case-portability.mjs +59 -28
- package/packages/contracts/cli-doctor.mjs +4 -0
- package/packages/contracts/cli-lookup.mjs +57 -46
- package/packages/contracts/lookup-evidence.mjs +12 -2
- package/packages/contracts/registry-insights.mjs +6 -0
- package/packages/evidence/dmarc-policy-context.mjs +80 -0
- package/packages/evidence/lookup-collection-receipt.mjs +140 -0
- package/packages/evidence/lookup-collection-review.mjs +74 -0
- package/packages/interchange/external-findings-import.mjs +8 -4
- package/packages/investigation/bulk-domain-comparison.mjs +17 -8
- package/packages/investigation/bulk-mail-exposure.mjs +2 -2
- package/packages/investigation/domain-history-review.mjs +7 -1
- package/packages/workspace/brand-keyword-campaign.mjs +11 -1
- package/packages/workspace/bulk-session-model.mjs +8 -0
- package/third-party-notices.txt +30 -30
|
@@ -9,6 +9,8 @@ import { requireDomainName, requireIsoTimestamp } from "../lib/bounded-contract-
|
|
|
9
9
|
import { buildCliLookupDocument } from "./formatters/json.mjs";
|
|
10
10
|
import { buildCliDomainControlReview, DOMAIN_CONTROL_REVIEW_SOURCE_FIELDS, } from "./domain-control-observations.mjs";
|
|
11
11
|
import { CliUsageError } from "./errors.mjs";
|
|
12
|
+
import { lookupStrictExitFindings } from "./strict-exit.mjs";
|
|
13
|
+
import { lookupObservationHostname } from "../packages/evidence/lookup-target.mjs";
|
|
12
14
|
export { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION };
|
|
13
15
|
export const MAX_DOMAIN_CONTROL_MONITOR_DOMAINS = CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS;
|
|
14
16
|
function parseMonitorJson(input, label) {
|
|
@@ -232,6 +234,19 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
|
|
|
232
234
|
}
|
|
233
235
|
const previous = previousSnapshot(previousInput, startedAt);
|
|
234
236
|
const entries = manifest.entries.slice(0, Math.min(options.limit, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS));
|
|
237
|
+
const targets = entries.map(entry => {
|
|
238
|
+
let classified;
|
|
239
|
+
try {
|
|
240
|
+
classified = classifyQuery(entry.domain);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
throw new CliUsageError('monitor-once requires registrable-domain targets.');
|
|
244
|
+
}
|
|
245
|
+
if (classified.type !== 'domain' || classified.isSubdomain) {
|
|
246
|
+
throw new CliUsageError('monitor-once supports registrable-domain targets only; exact-host monitoring is not supported.');
|
|
247
|
+
}
|
|
248
|
+
return classified;
|
|
249
|
+
});
|
|
235
250
|
const lookups = new Array(entries.length);
|
|
236
251
|
const failuresByIndex = new Array(entries.length);
|
|
237
252
|
let cursor = 0;
|
|
@@ -244,13 +259,18 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
|
|
|
244
259
|
if (!entry)
|
|
245
260
|
break;
|
|
246
261
|
try {
|
|
247
|
-
const classified =
|
|
262
|
+
const classified = targets[index];
|
|
248
263
|
const result = await options.executeLookup(classified, { fast: false, compact: false, ...(options.signal ? { signal: options.signal } : {}) });
|
|
249
264
|
options.signal?.throwIfAborted();
|
|
250
265
|
const capturedAt = requireIsoTimestamp(options.now(), 'Domain-control capture time');
|
|
251
266
|
if (Date.parse(capturedAt) < checkedAt)
|
|
252
267
|
throw new CliUsageError('Domain-control capture time must not precede the run.');
|
|
253
|
-
|
|
268
|
+
const lookup = buildCliLookupDocument(entry.domain, classified, result, capturedAt, 'deep');
|
|
269
|
+
const observedHostname = lookupObservationHostname({ domain: entry.domain, ...lookup.availability });
|
|
270
|
+
if (observedHostname !== null && observedHostname !== entry.domain) {
|
|
271
|
+
throw new TypeError('Lookup observation does not match the monitor target.');
|
|
272
|
+
}
|
|
273
|
+
lookups[index] = lookup;
|
|
254
274
|
}
|
|
255
275
|
catch {
|
|
256
276
|
if (options.signal?.aborted) {
|
|
@@ -270,6 +290,9 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
|
|
|
270
290
|
const successful = lookups.filter(Boolean);
|
|
271
291
|
if (!successful.length)
|
|
272
292
|
throw new Error('No domain-control monitor lookup completed successfully.');
|
|
293
|
+
// Only current Lookup diagnostics contribute; checkpoints retain observations,
|
|
294
|
+
// not policy metadata or raw diagnostics.
|
|
295
|
+
options.onSourceFailures?.(successful.reduce((count, lookup) => count + lookupStrictExitFindings(lookup).length, 0));
|
|
273
296
|
const generatedAt = requireIsoTimestamp(options.now(), 'Domain-control completion time');
|
|
274
297
|
if (Date.parse(generatedAt) < checkedAt || successful.some((lookup) => Date.parse(String(lookup.generatedAt)) > Date.parse(generatedAt))) {
|
|
275
298
|
throw new CliUsageError('Domain-control completion time must not precede its captures.');
|
|
@@ -3,10 +3,11 @@ import { latestObservationCohort } from "../packages/evidence/latest-observation
|
|
|
3
3
|
import { canonicalCaaRecord, canonicalDsRecord, canonicalMxRecord, canonicalDomainControlRecords, canonicalPostureRecords } from "../packages/evidence/domain-control-runtime.mjs";
|
|
4
4
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
5
|
import { postureTransferRestriction } from "../packages/evidence/domain-posture-context.mjs";
|
|
6
|
+
import { lookupObservationHostname } from "../packages/evidence/lookup-target.mjs";
|
|
6
7
|
import { MAX_DOMAIN_CONTROL_INPUT_RECORDS, PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION } from "../packages/contracts/domain-control-manifest.mjs";
|
|
7
8
|
import { scanBoundedJson, } from "../packages/analysis/bounded-json.mjs";
|
|
8
9
|
import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION, SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS, DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS, } from "../packages/contracts/domain-control-review.mjs";
|
|
9
|
-
import { MAX_FLIGHT_RECORDER_VALUES, mergeConcurrentDomainControlFields, } from "../lib/domain-control-flight-recorder.mjs";
|
|
10
|
+
import { MAX_FLIGHT_RECORDER_VALUES, DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS, mergeConcurrentDomainControlFields, } from "../lib/domain-control-flight-recorder.mjs";
|
|
10
11
|
import { DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, reviewDomainControlManifest, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
11
12
|
import { CliUsageError } from "./errors.mjs";
|
|
12
13
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
@@ -113,12 +114,21 @@ function pageIdentityValue(availability) {
|
|
|
113
114
|
].filter((item) => Boolean(item));
|
|
114
115
|
return components.length ? [components.join(' | ')] : [];
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
+
function serviceHostname(document) {
|
|
118
|
+
// Historical saved Lookups collected supporting evidence at the registration
|
|
119
|
+
// domain unless availability retained a more specific scope. Never use query.
|
|
120
|
+
return lookupObservationHostname({ domain: document.registrableDomain, ...record(document.availability) });
|
|
121
|
+
}
|
|
122
|
+
export function domainControlObservationFromSavedLookup(document, domain = document.registrableDomain) {
|
|
117
123
|
const availability = record(document.availability);
|
|
124
|
+
const observedHostname = serviceHostname(document);
|
|
118
125
|
const dns = record(availability.dns);
|
|
119
126
|
const dnsRecords = record(dns.records);
|
|
120
127
|
const dnsAt = sourceClock(dns.observedAt, dns.checkedAt);
|
|
121
128
|
const delegation = record(dns.delegation);
|
|
129
|
+
const delegationHostname = delegation.domain === undefined
|
|
130
|
+
? observedHostname === document.registrableDomain ? observedHostname : null
|
|
131
|
+
: lookupObservationHostname({ domain: delegation.domain });
|
|
122
132
|
const delegationRecords = record(delegation.records);
|
|
123
133
|
const tls = record(availability.tls);
|
|
124
134
|
const tlsState = sourceState(tls);
|
|
@@ -134,14 +144,15 @@ export function domainControlObservationFromSavedLookup(document) {
|
|
|
134
144
|
const hostname = (value) => canonicalDomainControlRecords([value], 'nameservers')[0] ?? '';
|
|
135
145
|
const identity = record(availability.pageIdentity);
|
|
136
146
|
const identityValues = pageIdentityValue(availability);
|
|
137
|
-
const
|
|
147
|
+
const registrationFields = [
|
|
138
148
|
scalar('registrar', registry.source, registry.sourceState, record(registry.parsed.registrar).name ?? registry.parsed.registrar, registry.observedAt, 300),
|
|
139
149
|
scalar('registrar_lock', registry.source, registry.sourceState, lockValue(registry.parsed), registry.observedAt, 20),
|
|
140
150
|
scalar('registry_dnssec', 'Registry RDAP', rdapState, rdapParsed.dnssec, rdapAt, 80),
|
|
141
151
|
recordField('registry_nameservers', 'Registry RDAP', rdapState, rdapParsed.nameservers, hostname, rdapAt),
|
|
142
152
|
recordField('whois_nameservers', 'WHOIS', diagnostic(document, 'whois'), record(record(document.whois).parsed).nameservers, hostname, registryClock(document, 'whois')),
|
|
153
|
+
];
|
|
154
|
+
const serviceFields = [
|
|
143
155
|
recordField('delegated_nameservers', 'DNS NS', dnsRecordState(dns, 'ns'), dnsRecords.ns, hostname, dnsAt),
|
|
144
|
-
recordField('delegation_ds', 'DNS delegation', Object.hasOwn(delegationRecords, 'ds') ? sourceState(delegation) : 'unsupported', delegationRecords.ds, canonicalDsRecord, sourceClock(delegation.observedAt)),
|
|
145
156
|
recordField('mail_exchangers', 'DNS MX', dnsRecordState(dns, 'mx'), dnsRecords.mx, canonicalMxRecord, dnsAt),
|
|
146
157
|
recordField('caa_policy', 'DNS CAA', dnsRecordState(dns, 'caa'), dnsRecords.caa, canonicalCaaRecord, dnsAt),
|
|
147
158
|
scalar('tls_certificate', 'TLS', tlsState, certificate.fingerprintSha256 ?? tls.fingerprintSha256, tlsAt, 128),
|
|
@@ -149,8 +160,13 @@ export function domainControlObservationFromSavedLookup(document) {
|
|
|
149
160
|
scalar('http_origin', 'HTTP', sourceState(http), http.finalOrigin, sourceClock(http.observedAt, http.checkedAt)),
|
|
150
161
|
field('page_identity', 'Static page identity', sourceState(identity) === 'observed' && !identityValues.length ? 'partial' : sourceState(identity), identityValues, sourceClock(identity.observedAt)),
|
|
151
162
|
];
|
|
163
|
+
const fields = mergeConcurrentDomainControlFields([
|
|
164
|
+
domain === document.registrableDomain ? registrationFields : [],
|
|
165
|
+
domain === observedHostname ? serviceFields : [],
|
|
166
|
+
domain === delegationHostname ? [recordField('delegation_ds', 'DNS delegation', Object.hasOwn(delegationRecords, 'ds') ? sourceState(delegation) : 'unsupported', delegationRecords.ds, canonicalDsRecord, sourceClock(delegation.observedAt))] : [],
|
|
167
|
+
]);
|
|
152
168
|
return Object.freeze({
|
|
153
|
-
domain
|
|
169
|
+
domain, capturedAt: document.generatedAt, collectionDepth: document.mode,
|
|
154
170
|
fields: Object.freeze(fields.map((item) => item.state === 'observed' && item.observedAt !== null
|
|
155
171
|
&& Date.parse(item.observedAt) > Date.parse(document.generatedAt) ? Object.freeze({ ...item, state: 'partial' }) : item)),
|
|
156
172
|
});
|
|
@@ -219,19 +235,40 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
219
235
|
const lookups = input.lookups.map((item, index) => parseSavedLookupDocument(JSON.stringify(item), { label: `Lookup ${index + 1}` }));
|
|
220
236
|
const byDomain = new Map();
|
|
221
237
|
for (const lookup of lookups) {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
const domains = new Set([lookup.registrableDomain, serviceHostname(lookup)]);
|
|
239
|
+
for (const domain of domains) {
|
|
240
|
+
if (domain === null)
|
|
241
|
+
continue;
|
|
242
|
+
const group = byDomain.get(domain) ?? [];
|
|
243
|
+
group.push(lookup);
|
|
244
|
+
byDomain.set(domain, group);
|
|
245
|
+
}
|
|
225
246
|
}
|
|
226
|
-
|
|
247
|
+
const retainedLookups = new Set();
|
|
227
248
|
const selected = [...byDomain.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([domain, documents]) => {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
249
|
+
const candidates = documents.map((document) => {
|
|
250
|
+
const observation = domainControlObservationFromSavedLookup(document, domain);
|
|
251
|
+
return { document, observation, reviewFields: reviewFields(document, observation) };
|
|
252
|
+
});
|
|
253
|
+
const currentCaptures = new Set();
|
|
254
|
+
const currentFields = [];
|
|
255
|
+
const currentReviewFields = [];
|
|
256
|
+
for (const id of DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS) {
|
|
257
|
+
const eligible = candidates.filter(candidate => candidate.observation.fields.some(item => item.id === id));
|
|
258
|
+
const latest = latestObservationCohort(eligible, candidate => candidate.document.generatedAt);
|
|
259
|
+
for (const capture of latest.latest) {
|
|
260
|
+
currentCaptures.add(capture);
|
|
261
|
+
retainedLookups.add(capture.document);
|
|
262
|
+
currentFields.push(capture.observation.fields.filter(item => item.id === id));
|
|
263
|
+
currentReviewFields.push(capture.reviewFields.filter(item => item.id === id));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const captures = [...currentCaptures];
|
|
267
|
+
const cohort = latestObservationCohort(captures, capture => capture.document.generatedAt);
|
|
231
268
|
const first = captures[0];
|
|
232
269
|
if (!first || !cohort.observedAt || cohort.undated.length)
|
|
233
270
|
throw new CliUsageError('Saved Lookup observation times could not be ordered.');
|
|
234
|
-
const mergedFields = mergeConcurrentDomainControlFields(
|
|
271
|
+
const mergedFields = mergeConcurrentDomainControlFields(currentFields);
|
|
235
272
|
const futureCapture = Date.parse(cohort.observedAt) > Date.parse(generatedAt);
|
|
236
273
|
const observation = Object.freeze({
|
|
237
274
|
domain,
|
|
@@ -240,7 +277,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
240
277
|
? first.observation.collectionDepth : 'unknown',
|
|
241
278
|
fields: futureCapture ? mergedFields.map((field) => ({ ...field, state: 'partial' })) : mergedFields,
|
|
242
279
|
});
|
|
243
|
-
const fields = mergeConcurrentDomainControlFields(
|
|
280
|
+
const fields = mergeConcurrentDomainControlFields(currentReviewFields);
|
|
244
281
|
return { observation, reviewFields: futureCapture || observation.collectionDepth === 'unknown'
|
|
245
282
|
? fields.map((field) => ({ ...field, state: 'partial' })) : fields };
|
|
246
283
|
});
|
|
@@ -268,7 +305,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
268
305
|
input: Object.freeze({
|
|
269
306
|
lookupsReceived: lookups.length,
|
|
270
307
|
latestDomainObservations: observations.length,
|
|
271
|
-
ignoredHistoricalLookups,
|
|
308
|
+
ignoredHistoricalLookups: lookups.length - retainedLookups.size,
|
|
272
309
|
}),
|
|
273
310
|
limitations: CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS,
|
|
274
311
|
});
|
package/cli/fail-policy.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { lookupStrictExitFindings } from "./strict-exit.mjs";
|
|
2
2
|
import { CliUsageError } from "./errors.mjs";
|
|
3
|
+
import { RISK_REVIEW_THRESHOLD } from "../packages/analysis/risk-scoring.mjs";
|
|
4
|
+
import { explainCliLookupRisk } from "./risk-policy.mjs";
|
|
5
|
+
import { DOMAIN_CONTROL_REVIEW_SCHEMA } from "../packages/contracts/domain-control-review.mjs";
|
|
3
6
|
export const CLI_FAIL_POLICIES = ['source-failure', 'inconclusive', 'danger', 'material-drift'];
|
|
4
7
|
export const CLI_FAIL_POLICIES_BY_COMMAND = Object.freeze({
|
|
5
8
|
lookup: Object.freeze(['source-failure', 'inconclusive', 'danger']),
|
|
@@ -31,14 +34,7 @@ function availabilityStates(document) {
|
|
|
31
34
|
});
|
|
32
35
|
return [direct, review, ...resultStates].filter(Boolean);
|
|
33
36
|
}
|
|
34
|
-
function
|
|
35
|
-
const raw = record(value.risk).score ?? value.riskScore;
|
|
36
|
-
if (raw === null || raw === undefined || raw === '')
|
|
37
|
-
return null;
|
|
38
|
-
const parsed = Number(raw);
|
|
39
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
40
|
-
}
|
|
41
|
-
export function evaluateCliFailPolicies(document, policies) {
|
|
37
|
+
export function evaluateCliFailPolicies(document, policies, profile = null, currentSourceFailureCount = 0) {
|
|
42
38
|
const root = record(document);
|
|
43
39
|
const findings = [];
|
|
44
40
|
for (const policy of policies) {
|
|
@@ -50,28 +46,31 @@ export function evaluateCliFailPolicies(document, policies) {
|
|
|
50
46
|
const collection = record(root.collection);
|
|
51
47
|
const summary = record(root.summary);
|
|
52
48
|
const failed = Number(collection.failed ?? summary.failed ?? 0);
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
const failures = sourceFindings.length + failed + currentSourceFailureCount;
|
|
50
|
+
if (failures > 0)
|
|
51
|
+
findings.push({ policy, reason: `${failures} failed or incomplete collection state(s)` });
|
|
55
52
|
}
|
|
56
53
|
else if (policy === 'inconclusive') {
|
|
57
|
-
const
|
|
54
|
+
const review = record(root.review);
|
|
55
|
+
const comparisons = review.schema === DOMAIN_CONTROL_REVIEW_SCHEMA;
|
|
56
|
+
const counts = record(review.counts);
|
|
57
|
+
const count = comparisons
|
|
58
|
+
? Number(counts.partial ?? 0) + Number(counts.unavailable ?? 0) + Number(counts.unsupported ?? 0)
|
|
59
|
+
: availabilityStates(root).filter((state) => ['unknown', 'inconclusive', 'partial', 'unavailable'].includes(state)).length;
|
|
58
60
|
if (count)
|
|
59
|
-
findings.push({ policy, reason: `${count} authority or availability state(s) remained inconclusive` });
|
|
61
|
+
findings.push({ policy, reason: `${count} ${comparisons ? 'requested comparison(s)' : 'authority or availability state(s)'} remained inconclusive` });
|
|
60
62
|
}
|
|
61
63
|
else if (policy === 'danger') {
|
|
62
64
|
const documents = [root, ...(Array.isArray(root.results) ? root.results.map(record) : [])];
|
|
63
|
-
const risks = documents.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if ((highestRisk !== null && highestRisk >= 70) || severities.length) {
|
|
65
|
+
const risks = documents.flatMap(item => {
|
|
66
|
+
const risk = explainCliLookupRisk(item, profile, item.mode ?? root.mode);
|
|
67
|
+
return risk ? [risk] : [];
|
|
68
|
+
});
|
|
69
|
+
const highestRisk = risks.reduce((highest, risk) => (!highest || risk.score > highest.score ? risk : highest), null);
|
|
70
|
+
if (highestRisk && highestRisk.score >= RISK_REVIEW_THRESHOLD) {
|
|
70
71
|
findings.push({
|
|
71
72
|
policy,
|
|
72
|
-
reason: highestRisk
|
|
73
|
-
? `risk score ${highestRisk} met the 70 review threshold`
|
|
74
|
-
: `explicit ${severities[0]} severity was present`,
|
|
73
|
+
reason: `risk score ${highestRisk.score} met the ${RISK_REVIEW_THRESHOLD} review threshold; model v${highestRisk.modelVersion}; evidence ${highestRisk.evidenceQuality.state} (heuristic, not a verdict)`,
|
|
75
74
|
});
|
|
76
75
|
}
|
|
77
76
|
}
|
|
@@ -87,3 +86,13 @@ export function evaluateCliFailPolicies(document, policies) {
|
|
|
87
86
|
export function formatFailPolicyNotice(findings) {
|
|
88
87
|
return `Failure policy matched: ${findings.map((item) => `${item.policy} (${item.reason})`).join('; ')}.\n`;
|
|
89
88
|
}
|
|
89
|
+
export function reportCliFailPolicies(findings, writeStderr, progress) {
|
|
90
|
+
if (!findings.length)
|
|
91
|
+
return;
|
|
92
|
+
if (!progress.enabled)
|
|
93
|
+
writeStderr(formatFailPolicyNotice(findings));
|
|
94
|
+
else
|
|
95
|
+
for (const finding of findings) {
|
|
96
|
+
progress.emit({ event: 'warning', state: 'failure_policy_matched', source: finding.policy, reason: finding.reason });
|
|
97
|
+
}
|
|
98
|
+
}
|
package/cli/formatters/html.mjs
CHANGED
|
@@ -40,6 +40,7 @@ function formatLookupEvidenceHtml(document, options = {}) {
|
|
|
40
40
|
<style>
|
|
41
41
|
:root{color-scheme:dark;--bg:#07110d;--panel:#0d1b15;--panel2:#10241b;--line:#254837;--text:#d8f5e4;--muted:#91b7a0;--accent:#6dff9d;--shadow:rgba(0,0,0,.28)}
|
|
42
42
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:15px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}main{width:min(1120px,calc(100% - 32px));margin:0 auto;padding:42px 0 64px}header{border:1px solid var(--line);background:linear-gradient(145deg,var(--panel2),var(--panel));padding:28px;box-shadow:0 18px 50px var(--shadow)}h1,h2,h3{line-height:1.2;margin:0;color:var(--accent)}h1{font-size:clamp(1.5rem,4vw,2.35rem);overflow-wrap:anywhere}h2{font-size:1.22rem;margin-bottom:16px}h3{font-size:1rem;margin-bottom:12px}.eyebrow{margin:0 0 10px;color:var(--muted);letter-spacing:.12em;text-transform:uppercase}.notice{margin:18px 0 0;color:var(--muted);max-width:85ch}.block{margin-top:24px;border:1px solid var(--line);background:var(--panel);padding:22px}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 24px;margin:0}.fields div{display:grid;grid-template-columns:minmax(130px,.8fr) minmax(0,1.6fr);gap:12px;border-top:1px solid var(--line);padding:9px 0}.fields div:first-child,.fields div:nth-child(2){border-top:0}.fields dt{color:var(--muted)}.fields dd{margin:0;overflow-wrap:anywhere}.card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr));gap:16px}.card{border:1px solid var(--line);background:var(--panel2);padding:18px}.card .fields{display:block}.card .fields div{grid-template-columns:minmax(120px,.8fr) minmax(0,1.4fr)}.card .fields div:nth-child(2){border-top:1px solid var(--line)}.table-scroll{overflow-x:auto}table{width:100%;border-collapse:collapse;min-width:720px}caption{text-align:left;color:var(--muted);padding:0 0 10px}th,td{border:1px solid var(--line);padding:9px 10px;text-align:left;vertical-align:top;overflow-wrap:anywhere}thead th{background:var(--panel2);color:var(--accent)}tbody th{color:var(--text)}.status{white-space:nowrap;color:var(--accent)}.empty,.omission{color:var(--muted)}ul{margin:0;padding-left:22px}li+li{margin-top:8px}footer{margin-top:24px;color:var(--muted);font-size:.88rem;text-align:center}
|
|
43
|
+
footer{overflow-wrap:anywhere}
|
|
43
44
|
@media(max-width:700px){main{width:min(100% - 20px,1120px);padding-top:16px}header,.block{padding:17px}.fields{display:block}.fields div,.card .fields div{grid-template-columns:1fr;gap:2px}.fields div:nth-child(2){border-top:1px solid var(--line)}}
|
|
44
45
|
@media print{:root{color-scheme:light;--bg:#fff;--panel:#fff;--panel2:#f5f7f5;--line:#b9c5bd;--text:#17231b;--muted:#46564b;--accent:#155c2f;--shadow:transparent}body{font:10pt/1.42 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}main{width:100%;padding:0}header,.block,.card{box-shadow:none;break-inside:avoid}.block{margin-top:12px}}
|
|
45
46
|
</style>
|
|
@@ -55,6 +56,8 @@ function formatLookupEvidenceHtml(document, options = {}) {
|
|
|
55
56
|
<section class="block"><h2>Query</h2>${renderFields(report.query)}</section>
|
|
56
57
|
<section class="block"><h2>Assessment</h2>${renderFields(report.assessment)}</section>
|
|
57
58
|
<section class="block"><h2>Registry sources</h2>${renderGroups(report.registryGroups)}</section>
|
|
59
|
+
${report.registrarStanding.length ? `<section class="block"><h2>Registrar standing</h2>${renderFields(report.registrarStanding)}</section>` : ''}
|
|
60
|
+
${report.registryInterpretation.length ? `<section class="block"><h2>Registry interpretation</h2>${renderFields(report.registryInterpretation)}</section>` : ''}
|
|
58
61
|
<section class="block"><h2>Registry-source comparison</h2>${renderFields(report.comparison.health)}${renderComparison(report.comparison)}</section>
|
|
59
62
|
${report.registrarComparison.fields.length ? `<section class="block"><h2>Registry / registrar RDAP comparison</h2>${renderFields(report.registrarComparison.health)}${renderRegistrarComparison(report.registrarComparison)}</section>` : ''}
|
|
60
63
|
<section class="block"><h2>Network evidence</h2>${renderGroups(report.networkGroups)}</section>
|
package/cli/formatters/json.mjs
CHANGED
|
@@ -69,7 +69,14 @@ function versionedResult(result, schema, version, generatedAt, requestedField, r
|
|
|
69
69
|
return { ...result, schema, version, generatedAt, [requestedField]: requestedValue };
|
|
70
70
|
}
|
|
71
71
|
function buildCliPostureDocument(requestedDomain, report, generatedAt = new Date().toISOString()) {
|
|
72
|
-
|
|
72
|
+
const checks = report.checks;
|
|
73
|
+
const retained = Array.isArray(checks) ? { ...report, checks: checks.map(check => {
|
|
74
|
+
if (!check || typeof check !== 'object' || Array.isArray(check))
|
|
75
|
+
return check;
|
|
76
|
+
const { dmarcPolicyContext: _transient, ...fields } = check;
|
|
77
|
+
return fields;
|
|
78
|
+
}) } : report;
|
|
79
|
+
return versionedResult(retained, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, generatedAt, 'requestedDomain', requestedDomain);
|
|
73
80
|
}
|
|
74
81
|
function buildCliHttpDocument(requestedDomain, result, generatedAt = new Date().toISOString()) {
|
|
75
82
|
return versionedResult(result, CLI_HTTP_SCHEMA, CLI_HTTP_SCHEMA_VERSION, generatedAt, 'requestedDomain', requestedDomain);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { registryAccessProfileLabel } from "../registry-access.mjs";
|
|
2
|
+
import { isSupportedRegistryInsightsVersion } from "../../packages/contracts/registry-insights.mjs";
|
|
2
3
|
import { dnsAddressFamilyEvidence } from "../../packages/evidence/dns-query-outcome.mjs";
|
|
4
|
+
import { lookupCollectionReceiptReview } from "../../packages/evidence/lookup-collection-review.mjs";
|
|
3
5
|
import { appendTechnologyLines } from "./terminal-technology.mjs";
|
|
4
6
|
import { appendDeliveryMetadataLines, appendPublicationMetadataLines } from "./terminal-metadata.mjs";
|
|
5
7
|
import { securityPostureReview, summarizeSecurityPostureReview } from "../../lib/website-security-posture.mjs";
|
|
@@ -252,6 +254,14 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
|
|
|
252
254
|
`RDAP ${titleCase(rdapDiagnostics.status)}`,
|
|
253
255
|
`WHOIS ${titleCase(whoisDiagnostics.status)}`,
|
|
254
256
|
];
|
|
257
|
+
if (detail !== 'summary') {
|
|
258
|
+
for (const [source, status] of [['rdap', rdapDiagnostics.status], ['whois', whoisDiagnostics.status]]) {
|
|
259
|
+
const sourceDetail = terminalRecord(document[source]).detail;
|
|
260
|
+
if (['unsupported', 'skipped', 'disabled'].includes(String(status)) && typeof sourceDetail === 'string' && sourceDetail) {
|
|
261
|
+
registrationLines.push(`${source.toUpperCase()} detail ${boundedTerminalComponent(sourceDetail, 300)}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
255
265
|
if (detail !== 'summary' && rdapDiagnostics.endpoint) {
|
|
256
266
|
registrationLines.splice(1, 0, `RDAP source ${safeTerminalValue(rdapDiagnostics.endpoint)}`);
|
|
257
267
|
}
|
|
@@ -267,7 +277,7 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
|
|
|
267
277
|
appendNetworkRegistrationLines(registrationLines, terminalRecord(rdap.parsed), document.type, detail);
|
|
268
278
|
}
|
|
269
279
|
const registryInsights = terminalRecord(document.registryInsights);
|
|
270
|
-
if (detail !== 'summary' && registryInsights.version
|
|
280
|
+
if (detail !== 'summary' && isSupportedRegistryInsightsVersion(registryInsights.version)) {
|
|
271
281
|
const lifecycle = terminalRecord(registryInsights.lifecycle);
|
|
272
282
|
const disclosure = terminalRecord(registryInsights.contactDisclosure);
|
|
273
283
|
const registryDisclosure = terminalRecord(disclosure.registryRdap);
|
|
@@ -712,6 +722,19 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
|
|
|
712
722
|
const collectionLines = [];
|
|
713
723
|
if (detail === 'verbose') {
|
|
714
724
|
collectionLines.push(`Generated ${safeTerminalValue(document.generatedAt, 'unknown')}`);
|
|
725
|
+
const receipt = lookupCollectionReceiptReview(document.collectionReceipt);
|
|
726
|
+
if (receipt) {
|
|
727
|
+
collectionLines.push(`Original plan ${receipt.selection}`);
|
|
728
|
+
for (const choice of receipt.choices)
|
|
729
|
+
collectionLines.push(`${choice.label}: ${choice.selected ? 'selected' : 'not selected'}`);
|
|
730
|
+
for (const source of receipt.sources) {
|
|
731
|
+
collectionLines.push(`${source.label}: ${source.summary}; ${source.outcome}`);
|
|
732
|
+
collectionLines.push(` ${source.explanation}`);
|
|
733
|
+
}
|
|
734
|
+
collectionLines.push('Collector start is local execution provenance, not proof of network delivery or complete evidence.');
|
|
735
|
+
}
|
|
736
|
+
else
|
|
737
|
+
collectionLines.push('Original plan Not retained; original optional choices and collector starts are unknown.');
|
|
715
738
|
const timing = terminalRecord(document.timing);
|
|
716
739
|
if (Number(timing.totalMs) >= 0)
|
|
717
740
|
collectionLines.push(`Total time ${safeTerminalValue(timing.totalMs)} ms`);
|
|
@@ -65,7 +65,7 @@ export const HISTORY_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
65
65
|
reference: {
|
|
66
66
|
description: 'Convert one saved lookup into a versioned evidence report.',
|
|
67
67
|
example: 'whoisleuth export lookup.json --markdown',
|
|
68
|
-
boundary: `
|
|
68
|
+
boundary: `Supported saved Lookups are capped at 8 MiB and scanned for duplicate keys, the prototype-sensitive __proto__ key, and bounded nesting, key, value, and per-container counts before parsing. Current schema-${LOOKUP_EVIDENCE_SCHEMA_VERSION} exports preserve evidence-source attribution and limitations; published v2 schemas ${PUBLISHED_V2_LOOKUP_EVIDENCE_VERSIONS} and exact v1 schema ${V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION} remain readable, while other historical and unreleased shapes are unsupported. Markdown and HTML include a presentation-only generator footer unless --no-attribution is selected; JSON retains bounded generator provenance. Compact output intentionally omits raw registry payloads.`,
|
|
69
69
|
},
|
|
70
70
|
collection: { scope: 'Reads one saved Lookup and writes one bounded report.' },
|
|
71
71
|
summary: 'Convert a lookup to an evidence report',
|
|
@@ -75,6 +75,7 @@ export function formatWorkflowRecipeCatalogue(catalogue) {
|
|
|
75
75
|
`${recipe.id} — ${recipe.label}`,
|
|
76
76
|
` Subject: ${recipe.subjectRequirement.replaceAll('_', ' ')}`,
|
|
77
77
|
` Workflow run: ${recipe.runnableByWorkflowRun ? 'supported' : 'plan and explanation only'}`,
|
|
78
|
+
...formatWorkflowEffort(recipe.id, recipe.steps).map((line) => ` ${line}`),
|
|
78
79
|
` ${recipe.objective}`,
|
|
79
80
|
...recipe.steps.map((item, index) => ` ${index + 1}. ${item.label} [${item.mode}; ${item.approval.replaceAll('_', ' ')}]`),
|
|
80
81
|
...recipe.limitations.map((item) => ` Limitation: ${item}`),
|
|
@@ -84,6 +85,19 @@ export function formatWorkflowRecipeCatalogue(catalogue) {
|
|
|
84
85
|
'',
|
|
85
86
|
].join('\n');
|
|
86
87
|
}
|
|
88
|
+
function formatWorkflowEffort(recipeId, steps) {
|
|
89
|
+
const counts = { network: 0, offline: 0 };
|
|
90
|
+
for (const step of steps)
|
|
91
|
+
counts[step.mode] += 1;
|
|
92
|
+
const reuse = workflowStandardInputs(recipeId).length;
|
|
93
|
+
return [
|
|
94
|
+
`Work planned: ${counts.network} networked step${counts.network === 1 ? '' : 's'}; ${counts.offline} offline step${counts.offline === 1 ? '' : 's'}.`,
|
|
95
|
+
reuse
|
|
96
|
+
? `Evidence reuse: ${reuse} link${reuse === 1 ? '' : 's'} from earlier outputs to later steps.`
|
|
97
|
+
: 'Evidence reuse: no earlier-output links are declared.',
|
|
98
|
+
'Counts describe steps and reuse links, not requests. Saved files and cache state are not checked.',
|
|
99
|
+
];
|
|
100
|
+
}
|
|
87
101
|
function validatedSteps(steps) {
|
|
88
102
|
for (const step of steps) {
|
|
89
103
|
const invocationEffect = cliInvocationNetworkEffect(step.command, step.arguments);
|
|
@@ -147,6 +161,7 @@ export function formatInvestigationPlan(plan) {
|
|
|
147
161
|
`Investigation plan: ${plan.recipe.label}`,
|
|
148
162
|
`Subject ${plan.subject}`,
|
|
149
163
|
`Mode ${plan.execution}`,
|
|
164
|
+
...formatWorkflowEffort(plan.recipe.id, plan.steps),
|
|
150
165
|
'',
|
|
151
166
|
...plan.steps.flatMap((item, index) => [
|
|
152
167
|
`${index + 1}. ${item.label}`,
|
package/cli/lookup-brief.mjs
CHANGED
|
@@ -214,23 +214,29 @@ export function buildCliLookupBrief(input, generatedAt = new Date().toISOString(
|
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
export function formatCliLookupBrief(document) {
|
|
217
|
+
const sourceFacts = document.sourceHealth.flatMap((source) => {
|
|
218
|
+
const facts = document.facts.filter((item) => item.source.id === source.id);
|
|
219
|
+
return [
|
|
220
|
+
` ${safeTerminalValue(source.label)} [${safeTerminalValue(source.state)}; ${source.observedAt ? `observed ${safeTerminalValue(source.observedAt)}` : 'source time not recorded'}]`,
|
|
221
|
+
...(facts.length
|
|
222
|
+
? facts.map((item) => ` ${safeTerminalValue(item.label)}: ${safeTerminalValue(item.value)}`)
|
|
223
|
+
: [' No fact selected for this brief.']),
|
|
224
|
+
];
|
|
225
|
+
});
|
|
217
226
|
return [
|
|
218
227
|
`Lookup brief: ${safeTerminalValue(document.target)}`,
|
|
219
228
|
`Observed ${safeTerminalValue(document.observedAt)}`,
|
|
220
229
|
`Mode ${safeTerminalValue(document.mode)}`,
|
|
221
230
|
'',
|
|
222
231
|
'Source-attributed facts',
|
|
223
|
-
...
|
|
232
|
+
...sourceFacts,
|
|
224
233
|
'',
|
|
225
234
|
'Contradictions',
|
|
226
235
|
...(document.contradictions.length ? document.contradictions.flatMap((item) => [
|
|
227
236
|
` ${safeTerminalValue(item.field)}: ${safeTerminalValue(item.detail)}`,
|
|
228
|
-
...item.observations.map((observation) => ` ${safeTerminalValue(observation.source.label)}: ${safeTerminalValue(observation.value)} (${safeTerminalValue(observation.state)}; ${observation.source.observedAt ? `observed ${safeTerminalValue(observation.source.observedAt)}` : 'source time not recorded'})`),
|
|
237
|
+
...item.observations.map((observation) => ` ${safeTerminalValue(observation.source.label)}: ${safeTerminalValue(observation.value)} (value state: ${safeTerminalValue(observation.state)}; source state: ${safeTerminalValue(observation.source.state)}; ${observation.source.observedAt ? `observed ${safeTerminalValue(observation.source.observedAt)}` : 'source time not recorded'})`),
|
|
229
238
|
]) : [' No conflicting publication field was retained.']),
|
|
230
239
|
'',
|
|
231
|
-
'Unknown or incomplete',
|
|
232
|
-
...(document.unknowns.length ? document.unknowns.map((item) => ` ${safeTerminalValue(item)}`) : [' No incomplete source state was identified.']),
|
|
233
|
-
'',
|
|
234
240
|
'Recommended manual actions',
|
|
235
241
|
...document.actionPlan.flatMap((item, index) => [
|
|
236
242
|
` ${index + 1}. ${safeTerminalValue(item.action)}`,
|
|
@@ -13,7 +13,8 @@ import { buildCliLookupPlan, formatCliLookupPlan } from "./lookup-plan.mjs";
|
|
|
13
13
|
import { createCliProgressEvents } from "./progress-events.mjs";
|
|
14
14
|
import { serializeCliLookupDocument } from "./saved-lookup.mjs";
|
|
15
15
|
import { lookupStrictExitFindings } from "./strict-exit.mjs";
|
|
16
|
-
import { evaluateCliFailPolicies,
|
|
16
|
+
import { evaluateCliFailPolicies, reportCliFailPolicies } from "./fail-policy.mjs";
|
|
17
|
+
import { readCliRiskProfile } from "./risk-policy.mjs";
|
|
17
18
|
import { formatCliJunit } from "./ci-report.mjs";
|
|
18
19
|
import { browseLookupOperation, canBrowseLookup } from "./lookup-browser.mjs";
|
|
19
20
|
import { writePrivateFile } from "./output-file.mjs";
|
|
@@ -66,26 +67,34 @@ async function runLookupCommand(args, dependencies, context) {
|
|
|
66
67
|
: context.terminal(formatCliLookupPlan(plan), args.color));
|
|
67
68
|
return EXIT_CODES.SUCCESS;
|
|
68
69
|
}
|
|
70
|
+
const riskProfile = await readCliRiskProfile(args.brandProfileSource, context.readInput, args.brandProfileId);
|
|
69
71
|
let documentGeneratedAt = '';
|
|
70
|
-
const buildDocument = (result) => {
|
|
72
|
+
const buildDocument = ({ result, collectionReceipt }) => {
|
|
71
73
|
documentGeneratedAt = context.now();
|
|
72
74
|
return buildCliLookupDocument(query, classified, result, documentGeneratedAt, args.deep ? 'deep' : 'fast', {
|
|
75
|
+
collectionReceipt,
|
|
73
76
|
...(args.observerLabel ? { observerLabel: args.observerLabel } : {}),
|
|
74
77
|
...(args.vantageLabel ? { vantageLabel: args.vantageLabel } : {}),
|
|
75
78
|
});
|
|
76
79
|
};
|
|
77
|
-
const collect = (signal, onSourceSettled) => abortable(() =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
const collect = (signal, onSourceSettled) => abortable(async () => {
|
|
81
|
+
let collectionReceipt;
|
|
82
|
+
const result = await executeLookup(classified, {
|
|
83
|
+
fast: !args.deep,
|
|
84
|
+
compact: false,
|
|
85
|
+
onCollectionReceipt: value => { collectionReceipt = value; },
|
|
86
|
+
...(signal ? { signal } : {}),
|
|
87
|
+
...(args.deep ? {
|
|
88
|
+
...(selectedUrl ? { selectedUrl } : {}),
|
|
89
|
+
onSourceSettled: settlement => {
|
|
90
|
+
onSourceSettled(settlement);
|
|
91
|
+
eventProgress.emit({ event: 'source_settled', source: settlement.source, state: settlement.state });
|
|
92
|
+
},
|
|
93
|
+
} : {}),
|
|
94
|
+
});
|
|
95
|
+
signal?.throwIfAborted();
|
|
96
|
+
return { result, collectionReceipt };
|
|
97
|
+
}, signal);
|
|
89
98
|
let document;
|
|
90
99
|
if (args.browse === true) {
|
|
91
100
|
const browse = dependencies.browseLookupOperation || browseLookupOperation;
|
|
@@ -132,6 +141,8 @@ async function runLookupCommand(args, dependencies, context) {
|
|
|
132
141
|
}
|
|
133
142
|
document = buildDocument(result);
|
|
134
143
|
}
|
|
144
|
+
const strictFindings = args.strictExit ? lookupStrictExitFindings(document) : [];
|
|
145
|
+
const policyFindings = evaluateCliFailPolicies(document, args.failOn || [], riskProfile);
|
|
135
146
|
if (!args.quiet) {
|
|
136
147
|
if (args.output === 'json')
|
|
137
148
|
context.writeStdout(serializeCliLookupDocument(document));
|
|
@@ -149,14 +160,11 @@ async function runLookupCommand(args, dependencies, context) {
|
|
|
149
160
|
context.writeStdout(context.terminal(formatTerminalLookup(document, { detail: args.detail }), args.color));
|
|
150
161
|
}
|
|
151
162
|
}
|
|
152
|
-
const strictFindings = args.strictExit ? lookupStrictExitFindings(document) : [];
|
|
153
|
-
const policyFindings = evaluateCliFailPolicies(document, args.failOn || []);
|
|
154
163
|
const exitCode = strictFindings.length || policyFindings.length ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
155
164
|
if (strictFindings.length && !args.events) {
|
|
156
165
|
context.writeStderr(`Strict exit: ${strictFindings.length} requested source state${strictFindings.length === 1 ? '' : 's'} were incomplete.\n`);
|
|
157
166
|
}
|
|
158
|
-
|
|
159
|
-
context.writeStderr(formatFailPolicyNotice(policyFindings));
|
|
167
|
+
reportCliFailPolicies(policyFindings, context.writeStderr, eventProgress);
|
|
160
168
|
eventProgress.emit({ event: 'completed', exitCode });
|
|
161
169
|
return exitCode;
|
|
162
170
|
}
|
package/cli/lookup-diff.mjs
CHANGED
|
@@ -83,6 +83,7 @@ function lookupComparisonInput(document) {
|
|
|
83
83
|
trusted: null,
|
|
84
84
|
error: '',
|
|
85
85
|
scanDepth: document.mode,
|
|
86
|
+
webCollectionQuality: availability.webCollectionQuality,
|
|
86
87
|
createdDate: lifecycleValue(preferredRegistry, 'createdDate'),
|
|
87
88
|
expiryDate: lifecycleValue(preferredRegistry, 'expiryDate'),
|
|
88
89
|
nameservers: relationship.nameservers.length ? relationship.nameservers : stringList(preferredRegistry.nameservers, 20),
|