@slicedearth/whoisleuth-cli 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +6 -0
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +68 -6
- package/cli/artifact-validation/case-response.mjs +47 -41
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +83 -61
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +39 -31
- package/cli/command-argument-grammar.mjs +4 -0
- package/cli/command-reference.mjs +145 -75
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +44 -16
- package/cli/discovery-scan-command-runner.mjs +2 -0
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +1 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +4 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +45 -0
- package/cli/runner.mjs +55 -6
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +76 -30
- package/docs/cli.md +289 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +43 -3
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +36 -23
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +7 -2
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +219 -118
- package/lib/domain-control-manifest.mjs +117 -57
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +113 -26
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -592
- package/lib/evidence-report.mjs +3 -1
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +82 -85
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +7 -10
- package/lib/lookup-source-progress.mjs +17 -14
- package/lib/lookup.mjs +48 -27
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +17 -3
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +10 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +59 -25
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-core.mjs +3 -2
- package/packages/cases/case-record-operations.mjs +89 -27
- package/packages/cases/case-record-projection.mjs +23 -20
- package/packages/cases/case-report.mjs +63 -52
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +41 -1129
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +38 -587
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +25 -0
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/page-similarity.mjs +2 -1
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +279 -116
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +1 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +160 -225
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +89 -49
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +158 -20
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +11 -5
- package/packages/workspace/bulk-session-model.mjs +95 -20
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -26
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +70 -30
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +85 -21
- package/third-party-notices.txt +29 -2
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { latestObservationCohort } from "../packages/evidence/latest-observations.mjs";
|
|
3
|
+
import { canonicalCaaRecord, canonicalDsRecord, canonicalMxRecord, canonicalDomainControlRecords, canonicalPostureRecords } from "../packages/evidence/domain-control-runtime.mjs";
|
|
4
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
|
+
import { postureTransferRestriction } from "../packages/evidence/domain-posture-context.mjs";
|
|
6
|
+
import { MAX_DOMAIN_CONTROL_INPUT_RECORDS, PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION } from "../packages/contracts/domain-control-manifest.mjs";
|
|
2
7
|
import { scanBoundedJson, } from "../lib/bounded-json.mjs";
|
|
3
|
-
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, 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";
|
|
4
|
-
import {
|
|
8
|
+
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";
|
|
5
10
|
import { DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, reviewDomainControlManifest, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
6
11
|
import { CliUsageError } from "./errors.mjs";
|
|
7
12
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
8
13
|
export { CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_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, };
|
|
9
14
|
const CLI_REVIEW_INPUT_KEY_SET = new Set(CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS);
|
|
15
|
+
export const DOMAIN_CONTROL_REVIEW_SOURCE_FIELDS = Object.freeze({
|
|
16
|
+
registry_nameservers: 'nameservers', delegation_ds: 'ds', mail_exchangers: 'mx', caa_policy: 'caa',
|
|
17
|
+
tls_certificate: 'tlsIssuer', tls_public_key: 'tlsSpkiSha256', registrar_lock: 'registrarLock',
|
|
18
|
+
});
|
|
10
19
|
function record(value) {
|
|
11
20
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
12
21
|
}
|
|
@@ -21,116 +30,147 @@ function state(value) {
|
|
|
21
30
|
return 'unavailable';
|
|
22
31
|
}
|
|
23
32
|
function text(value, maximum = 500) {
|
|
24
|
-
if (typeof value !== 'string')
|
|
33
|
+
if (typeof value !== 'string' || value.length > maximum || /[\u0000-\u001f\u007f]/u.test(value))
|
|
25
34
|
return null;
|
|
26
|
-
const normalized = value.
|
|
35
|
+
const normalized = value.trim();
|
|
27
36
|
return normalized || null;
|
|
28
37
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
38
|
+
function sourceClock(...values) {
|
|
39
|
+
const declared = values.filter((value) => value !== undefined && value !== null);
|
|
40
|
+
const times = declared.map(normalizeExplicitIsoTimestamp);
|
|
41
|
+
return times.length && times.every((time) => time !== null && time === times[0]) ? times[0] : null;
|
|
42
|
+
}
|
|
43
|
+
function sourceState(value) {
|
|
44
|
+
const result = state(value.status);
|
|
45
|
+
return result === 'observed' && (value.complete === false || value.truncated === true) ? 'partial' : result;
|
|
46
|
+
}
|
|
47
|
+
function dnsRecordState(dns, name) {
|
|
48
|
+
const query = record(record(dns.diagnostics)[name]);
|
|
49
|
+
if (!Object.keys(query).length)
|
|
50
|
+
return sourceState(dns);
|
|
51
|
+
const values = record(dns.records)[name];
|
|
52
|
+
const contradictoryAbsence = query.status === 'not_found' && Array.isArray(values) && values.length > 0;
|
|
53
|
+
if ((query.status === 'success' || query.status === 'not_found') && query.truncated === false && query.discarded === 0
|
|
54
|
+
&& !contradictoryAbsence && (query.error === undefined || query.error === null))
|
|
55
|
+
return 'observed';
|
|
56
|
+
return state(query.status) === 'observed' || query.status === 'not_found' ? 'partial' : state(query.status);
|
|
57
|
+
}
|
|
58
|
+
function field(id, source, sourceState, values, observedAt) {
|
|
59
|
+
return Object.freeze({ id, source, state: sourceState, values: Object.freeze([...values].sort()), observedAt });
|
|
60
|
+
}
|
|
61
|
+
function recordField(id, source, sourceState, input, normalize, observedAt) {
|
|
62
|
+
const rows = Array.isArray(input) ? input.slice(0, MAX_DOMAIN_CONTROL_INPUT_RECORDS) : [];
|
|
63
|
+
const values = new Set();
|
|
64
|
+
let invalid = Array.isArray(input) || (input === undefined && sourceState !== 'observed') ? 0 : 1;
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
let value = '';
|
|
67
|
+
try {
|
|
68
|
+
value = normalize(row);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!(error instanceof TypeError))
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
if (value)
|
|
75
|
+
values.add(value);
|
|
76
|
+
else
|
|
77
|
+
invalid += 1;
|
|
78
|
+
}
|
|
79
|
+
const sorted = [...values].sort();
|
|
80
|
+
const omitted = Math.max(0, (Array.isArray(input) ? input.length : 0) - rows.length)
|
|
81
|
+
+ Math.max(0, sorted.length - MAX_FLIGHT_RECORDER_VALUES);
|
|
82
|
+
return field(id, invalid || omitted ? `${source} (${invalid} invalid; ${omitted} omitted records)` : source, sourceState === 'observed' && (invalid || omitted) ? 'partial' : sourceState, sorted.slice(0, MAX_FLIGHT_RECORDER_VALUES), observedAt);
|
|
69
83
|
}
|
|
70
84
|
function diagnostic(document, key) {
|
|
71
|
-
return
|
|
85
|
+
return sourceState(record(record(document.diagnostics)[key]));
|
|
86
|
+
}
|
|
87
|
+
function registryClock(document, key) {
|
|
88
|
+
const detail = record(record(document.diagnostics)[key]);
|
|
89
|
+
const publication = record(document[key]);
|
|
90
|
+
const named = key === 'rdap' ? 'fetchedAt' : 'queriedAt';
|
|
91
|
+
return sourceClock(detail.observedAt, detail[named], publication.observedAt, publication[named]);
|
|
72
92
|
}
|
|
73
93
|
function registryParsed(document) {
|
|
74
94
|
const rdapState = diagnostic(document, 'rdap');
|
|
75
95
|
const whoisState = diagnostic(document, 'whois');
|
|
76
96
|
const rdap = record(record(document.rdap).parsed);
|
|
77
97
|
if (rdapState === 'observed' && Object.keys(rdap).length)
|
|
78
|
-
return { parsed: rdap, source: 'Registry RDAP', sourceState: rdapState };
|
|
98
|
+
return { parsed: rdap, source: 'Registry RDAP', sourceState: rdapState, observedAt: registryClock(document, 'rdap') };
|
|
79
99
|
const whois = record(record(document.whois).parsed);
|
|
80
|
-
return { parsed: whois, source: 'WHOIS', sourceState: whoisState };
|
|
100
|
+
return { parsed: whois, source: 'WHOIS', sourceState: whoisState, observedAt: registryClock(document, 'whois') };
|
|
81
101
|
}
|
|
82
102
|
function lockValue(parsed) {
|
|
83
|
-
const
|
|
84
|
-
|
|
103
|
+
const input = parsed.statuses ?? parsed.status;
|
|
104
|
+
const statuses = Array.isArray(input) ? canonicalPostureRecords('registration_lock', input) : null;
|
|
105
|
+
return statuses ? postureTransferRestriction(statuses) : null;
|
|
85
106
|
}
|
|
86
107
|
function pageIdentityValue(availability) {
|
|
87
108
|
const identity = record(availability.pageIdentity);
|
|
88
109
|
const components = [
|
|
89
|
-
text(identity.bodySha256
|
|
90
|
-
text(identity.faviconHash
|
|
91
|
-
text(identity.title
|
|
110
|
+
text(identity.bodySha256, 64),
|
|
111
|
+
text(identity.faviconHash, 128),
|
|
112
|
+
text(identity.title, 240),
|
|
92
113
|
].filter((item) => Boolean(item));
|
|
93
|
-
return components.length ? [components.join(' | ')
|
|
114
|
+
return components.length ? [components.join(' | ')] : [];
|
|
94
115
|
}
|
|
95
116
|
export function domainControlObservationFromSavedLookup(document) {
|
|
96
117
|
const availability = record(document.availability);
|
|
97
118
|
const dns = record(availability.dns);
|
|
98
119
|
const dnsRecords = record(dns.records);
|
|
120
|
+
const dnsAt = sourceClock(dns.observedAt, dns.checkedAt);
|
|
99
121
|
const delegation = record(dns.delegation);
|
|
100
122
|
const delegationRecords = record(delegation.records);
|
|
101
123
|
const tls = record(availability.tls);
|
|
102
|
-
const tlsState =
|
|
124
|
+
const tlsState = sourceState(tls);
|
|
125
|
+
const tlsAt = sourceClock(tls.observedAt, tls.checkedAt);
|
|
103
126
|
const certificate = record(tls.certificate);
|
|
104
127
|
const publicKey = record(certificate.publicKey);
|
|
105
128
|
const http = record(availability.http);
|
|
106
|
-
const httpState = state(http.status);
|
|
107
129
|
const registry = registryParsed(document);
|
|
108
130
|
const rdapParsed = record(record(document.rdap).parsed);
|
|
109
131
|
const rdapState = diagnostic(document, 'rdap');
|
|
110
|
-
const
|
|
132
|
+
const rdapAt = registryClock(document, 'rdap');
|
|
133
|
+
const scalar = (id, source, status, value, observedAt, maximum = 500) => recordField(id, source, status, [value], (item) => text(item, maximum) ?? '', observedAt);
|
|
134
|
+
const hostname = (value) => canonicalDomainControlRecords([value], 'nameservers')[0] ?? '';
|
|
135
|
+
const identity = record(availability.pageIdentity);
|
|
136
|
+
const identityValues = pageIdentityValue(availability);
|
|
111
137
|
const fields = [
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
field('page_identity', 'Static page identity',
|
|
138
|
+
scalar('registrar', registry.source, registry.sourceState, record(registry.parsed.registrar).name ?? registry.parsed.registrar, registry.observedAt, 300),
|
|
139
|
+
scalar('registrar_lock', registry.source, registry.sourceState, lockValue(registry.parsed), registry.observedAt, 20),
|
|
140
|
+
scalar('registry_dnssec', 'Registry RDAP', rdapState, rdapParsed.dnssec, rdapAt, 80),
|
|
141
|
+
recordField('registry_nameservers', 'Registry RDAP', rdapState, rdapParsed.nameservers, hostname, rdapAt),
|
|
142
|
+
recordField('whois_nameservers', 'WHOIS', diagnostic(document, 'whois'), record(record(document.whois).parsed).nameservers, hostname, registryClock(document, 'whois')),
|
|
143
|
+
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
|
+
recordField('mail_exchangers', 'DNS MX', dnsRecordState(dns, 'mx'), dnsRecords.mx, canonicalMxRecord, dnsAt),
|
|
146
|
+
recordField('caa_policy', 'DNS CAA', dnsRecordState(dns, 'caa'), dnsRecords.caa, canonicalCaaRecord, dnsAt),
|
|
147
|
+
scalar('tls_certificate', 'TLS', tlsState, certificate.fingerprintSha256 ?? tls.fingerprintSha256, tlsAt, 128),
|
|
148
|
+
scalar('tls_public_key', 'TLS', tlsState, publicKey.fingerprintSha256 ?? tls.spkiSha256, tlsAt, 128),
|
|
149
|
+
scalar('http_origin', 'HTTP', sourceState(http), http.finalOrigin, sourceClock(http.observedAt, http.checkedAt)),
|
|
150
|
+
field('page_identity', 'Static page identity', sourceState(identity) === 'observed' && !identityValues.length ? 'partial' : sourceState(identity), identityValues, sourceClock(identity.observedAt)),
|
|
125
151
|
];
|
|
126
152
|
return Object.freeze({
|
|
127
|
-
domain: document.registrableDomain,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
domain: document.registrableDomain, capturedAt: document.generatedAt, collectionDepth: document.mode,
|
|
154
|
+
fields: Object.freeze(fields.map((item) => item.state === 'observed' && item.observedAt !== null
|
|
155
|
+
&& Date.parse(item.observedAt) > Date.parse(document.generatedAt) ? Object.freeze({ ...item, state: 'partial' }) : item)),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function reviewFields(document, observation) {
|
|
159
|
+
return observation.fields.map((candidate) => {
|
|
160
|
+
if (candidate.id !== 'tls_certificate')
|
|
161
|
+
return candidate;
|
|
162
|
+
const issuer = record(record(record(document.availability).tls).certificate).issuer;
|
|
163
|
+
const value = record(issuer);
|
|
164
|
+
const commonNames = Array.isArray(value.commonNames) ? value.commonNames : [];
|
|
165
|
+
const organisationNames = Array.isArray(value.organizationNames) ? value.organizationNames : [];
|
|
166
|
+
return recordField(candidate.id, candidate.source, candidate.state, [commonNames[0] ?? organisationNames[0] ?? issuer], (item) => text(item, 300) ?? '', candidate.observedAt);
|
|
131
167
|
});
|
|
132
168
|
}
|
|
133
169
|
export function buildCliDomainControlReview(inputText, generatedAt = new Date().toISOString()) {
|
|
170
|
+
const reviewTime = normalizeExplicitIsoTimestamp(generatedAt);
|
|
171
|
+
if (!reviewTime)
|
|
172
|
+
throw new CliUsageError('Domain-control review time must include a valid explicit timezone.');
|
|
173
|
+
generatedAt = reviewTime;
|
|
134
174
|
if (Buffer.byteLength(inputText, 'utf8') > MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES) {
|
|
135
175
|
throw new CliUsageError(`Domain-control review input is limited to ${MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES} bytes.`);
|
|
136
176
|
}
|
|
@@ -161,7 +201,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
161
201
|
}
|
|
162
202
|
const input = record(parsed);
|
|
163
203
|
if (input.schema !== CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
|
|
164
|
-
||
|
|
204
|
+
|| !SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS.some((version) => version === input.version)
|
|
165
205
|
|| !Array.isArray(input.lookups)) {
|
|
166
206
|
throw new CliUsageError(`Domain-control review input must use ${CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA} version ${CLI_DOMAIN_CONTROL_REVIEW_VERSION}.`);
|
|
167
207
|
}
|
|
@@ -173,42 +213,49 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
173
213
|
throw new CliUsageError(`Domain-control review requires from ${MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS} to ${MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS} saved Lookup documents.`);
|
|
174
214
|
}
|
|
175
215
|
const manifest = verifyDomainControlManifest(input.manifest);
|
|
216
|
+
if (input.version === PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION && manifest.version !== PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION) {
|
|
217
|
+
throw new CliUsageError(`Public domain-control review input version ${PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION} requires manifest version ${PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION}.`);
|
|
218
|
+
}
|
|
176
219
|
const lookups = input.lookups.map((item, index) => parseSavedLookupDocument(JSON.stringify(item), { label: `Lookup ${index + 1}` }));
|
|
177
|
-
const
|
|
220
|
+
const byDomain = new Map();
|
|
178
221
|
for (const lookup of lookups) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
222
|
+
const group = byDomain.get(lookup.registrableDomain) ?? [];
|
|
223
|
+
group.push(lookup);
|
|
224
|
+
byDomain.set(lookup.registrableDomain, group);
|
|
182
225
|
}
|
|
183
|
-
|
|
226
|
+
let ignoredHistoricalLookups = 0;
|
|
227
|
+
const selected = [...byDomain.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([domain, documents]) => {
|
|
228
|
+
const cohort = latestObservationCohort(documents, (document) => document.generatedAt);
|
|
229
|
+
ignoredHistoricalLookups += cohort.superseded;
|
|
230
|
+
const captures = cohort.latest.map((document) => ({ document, observation: domainControlObservationFromSavedLookup(document) }));
|
|
231
|
+
const first = captures[0];
|
|
232
|
+
if (!first || !cohort.observedAt || cohort.undated.length)
|
|
233
|
+
throw new CliUsageError('Saved Lookup observation times could not be ordered.');
|
|
234
|
+
const mergedFields = mergeConcurrentDomainControlFields(captures.map((capture) => capture.observation.fields));
|
|
235
|
+
const futureCapture = Date.parse(cohort.observedAt) > Date.parse(generatedAt);
|
|
236
|
+
const observation = Object.freeze({
|
|
237
|
+
domain,
|
|
238
|
+
capturedAt: cohort.observedAt,
|
|
239
|
+
collectionDepth: captures.every((capture) => capture.observation.collectionDepth === first.observation.collectionDepth)
|
|
240
|
+
? first.observation.collectionDepth : 'unknown',
|
|
241
|
+
fields: futureCapture ? mergedFields.map((field) => ({ ...field, state: 'partial' })) : mergedFields,
|
|
242
|
+
});
|
|
243
|
+
const fields = mergeConcurrentDomainControlFields(captures.map((capture) => reviewFields(capture.document, capture.observation)));
|
|
244
|
+
return { observation, reviewFields: futureCapture || observation.collectionDepth === 'unknown'
|
|
245
|
+
? fields.map((field) => ({ ...field, state: 'partial' })) : fields };
|
|
246
|
+
});
|
|
247
|
+
const observations = selected.map((item) => item.observation);
|
|
184
248
|
const review = reviewDomainControlManifest({
|
|
185
249
|
schema: DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
|
|
186
250
|
version: DOMAIN_CONTROL_REVIEW_VERSION,
|
|
187
251
|
manifest,
|
|
188
|
-
observations:
|
|
189
|
-
domain:
|
|
190
|
-
fields: Object.fromEntries(
|
|
191
|
-
const
|
|
192
|
-
registry_nameservers: 'nameservers',
|
|
193
|
-
delegation_ds: 'ds',
|
|
194
|
-
mail_exchangers: 'mx',
|
|
195
|
-
caa_policy: 'caa',
|
|
196
|
-
tls_certificate: 'tlsIssuer',
|
|
197
|
-
tls_public_key: 'tlsSpkiSha256',
|
|
198
|
-
registrar_lock: 'registrarLock',
|
|
199
|
-
};
|
|
200
|
-
const target = mapping[candidate.id];
|
|
252
|
+
observations: selected.map(({ observation, reviewFields: fields }) => ({
|
|
253
|
+
domain: observation.domain,
|
|
254
|
+
fields: Object.fromEntries(fields.flatMap((candidate) => {
|
|
255
|
+
const target = DOMAIN_CONTROL_REVIEW_SOURCE_FIELDS[candidate.id];
|
|
201
256
|
if (!target)
|
|
202
257
|
return [];
|
|
203
|
-
|
|
204
|
-
? list([record(record(record(latest.get(item.domain)?.availability).tls).certificate).issuer], (value) => {
|
|
205
|
-
const issuer = record(value);
|
|
206
|
-
const commonNames = Array.isArray(issuer.commonNames) ? issuer.commonNames : [];
|
|
207
|
-
const organisationNames = Array.isArray(issuer.organizationNames) ? issuer.organizationNames : [];
|
|
208
|
-
return text(commonNames[0] ?? organisationNames[0] ?? value, 300);
|
|
209
|
-
})
|
|
210
|
-
: candidate.values;
|
|
211
|
-
return [[target, { state: candidate.state, values: candidateValues, source: candidate.source, observedAt: item.observedAt }]];
|
|
258
|
+
return [[target, { state: candidate.state, values: candidate.values, source: candidate.source, observedAt: candidate.observedAt }]];
|
|
212
259
|
})),
|
|
213
260
|
})),
|
|
214
261
|
}, generatedAt);
|
|
@@ -221,7 +268,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
221
268
|
input: Object.freeze({
|
|
222
269
|
lookupsReceived: lookups.length,
|
|
223
270
|
latestDomainObservations: observations.length,
|
|
224
|
-
ignoredHistoricalLookups
|
|
271
|
+
ignoredHistoricalLookups,
|
|
225
272
|
}),
|
|
226
273
|
limitations: CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS,
|
|
227
274
|
});
|
package/cli/errors.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
1
2
|
const MAX_CLI_ERROR_MESSAGE_LENGTH = 300;
|
|
3
|
+
const MAX_CLI_DIAGNOSTIC_BYTES = 4_096;
|
|
2
4
|
const CLI_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/u;
|
|
3
5
|
const CLI_DEFAULT_IGNORABLE_GLOBAL_RE = /\p{Default_Ignorable_Code_Point}/gu;
|
|
4
6
|
class CliUsageError extends Error {
|
|
@@ -23,10 +25,40 @@ function boundedCliErrorMessage(error, fallback = 'Unexpected command failure')
|
|
|
23
25
|
function hasUnsafeCliText(value) {
|
|
24
26
|
return /[\x00-\x1f\x7f-\x9f]/u.test(value) || CLI_DEFAULT_IGNORABLE_RE.test(value);
|
|
25
27
|
}
|
|
28
|
+
// A transient stderr preview, never a checkpoint field. Bound before collecting
|
|
29
|
+
// and redact request URLs and credential-shaped fields before shortening text.
|
|
30
|
+
function createCliDiagnosticOutput() {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let remaining = MAX_CLI_DIAGNOSTIC_BYTES;
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
stream: {
|
|
35
|
+
isTTY: false,
|
|
36
|
+
columns: 80,
|
|
37
|
+
write(chunk) {
|
|
38
|
+
if (remaining > 0 && chunk.length > 0) {
|
|
39
|
+
const bytes = Buffer.from(chunk.slice(0, remaining), 'utf8').subarray(0, remaining);
|
|
40
|
+
chunks.push(bytes.toString('utf8'));
|
|
41
|
+
remaining -= bytes.byteLength;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
value() {
|
|
47
|
+
const source = stripVTControlCharacters(chunks.join('')).replace(CLI_DEFAULT_IGNORABLE_GLOBAL_RE, '');
|
|
48
|
+
if (!source.trim())
|
|
49
|
+
return '';
|
|
50
|
+
const redacted = source
|
|
51
|
+
.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"']*/giu, '[URL omitted]')
|
|
52
|
+
.replace(/\b(authorization|cookie|password|passphrase|token|secret|api[-_ ]?key)\s*[:=]\s*(?:"[^"]*(?:"|$)|'[^']*(?:'|$)|[^\r\n,;]*)/giu, '$1=[omitted]')
|
|
53
|
+
.replace(/\b(?:bearer|basic)\s+[^\s,;]+/giu, '[credential omitted]');
|
|
54
|
+
return boundedCliErrorMessage(redacted);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
26
58
|
function boundedCliInputError(error, label) {
|
|
27
59
|
if (error instanceof CliUsageError)
|
|
28
60
|
return error;
|
|
29
61
|
const safeLabel = label.replace(/[^A-Za-z0-9 ()_-]+/gu, '').trim().slice(0, 80) || 'Input';
|
|
30
62
|
return new CliUsageError(`${safeLabel} could not be read as a bounded regular file.`);
|
|
31
63
|
}
|
|
32
|
-
export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, hasUnsafeCliText, };
|
|
64
|
+
export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, createCliDiagnosticOutput, hasUnsafeCliText, };
|
|
@@ -5,6 +5,7 @@ import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
|
5
5
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
6
6
|
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
7
7
|
import { readCliTextInput } from "./input.mjs";
|
|
8
|
+
import { assessEvidenceSignerTrust, formatSignerTrustReport, MAX_SIGNER_TRUST_STORE_BYTES } from "./signer-trust.mjs";
|
|
8
9
|
import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
|
|
9
10
|
export function isEvidenceCommand(args) {
|
|
10
11
|
return args.action === 'inspect-archive'
|
|
@@ -18,12 +19,12 @@ export function evidenceCommandFailureLabel(action) {
|
|
|
18
19
|
return 'Evidence signing';
|
|
19
20
|
return 'Evidence signature verification';
|
|
20
21
|
}
|
|
21
|
-
async function readArtifact(source, label, dependencies) {
|
|
22
|
+
async function readArtifact(source, label, dependencies, maximumBytes = MAX_OFFLINE_ARTIFACT_BYTES) {
|
|
22
23
|
try {
|
|
23
24
|
return dependencies.readArtifactInput
|
|
24
25
|
? await dependencies.readArtifactInput(source)
|
|
25
26
|
: await readCliTextInput(source, dependencies.stdin, {
|
|
26
|
-
maximumBytes
|
|
27
|
+
maximumBytes,
|
|
27
28
|
label,
|
|
28
29
|
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
29
30
|
});
|
|
@@ -105,12 +106,22 @@ async function runVerifySignatureCommand(args, dependencies) {
|
|
|
105
106
|
? await readKey(args.publicKeySource, 'Public key file', dependencies.readPublicKeyFile, dependencies)
|
|
106
107
|
: null;
|
|
107
108
|
const report = await verifyEvidencePackageSignature(input, publicKey);
|
|
109
|
+
let trustReport = null;
|
|
110
|
+
if (args.trustStoreSource) {
|
|
111
|
+
const trustRaw = await readArtifact(args.trustStoreSource, 'Signer trust store', dependencies, MAX_SIGNER_TRUST_STORE_BYTES);
|
|
112
|
+
try {
|
|
113
|
+
trustReport = assessEvidenceSignerTrust(report, trustRaw, dependencies.now ? dependencies.now() : new Date().toISOString());
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Invalid signer trust store.'));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
108
119
|
if (!args.quiet) {
|
|
109
120
|
dependencies.stdout.write(args.output === 'json'
|
|
110
|
-
? formatJsonDocument(report)
|
|
111
|
-
: formatEvidenceSignatureVerification(report));
|
|
121
|
+
? formatJsonDocument(trustReport ?? report)
|
|
122
|
+
: trustReport ? formatSignerTrustReport(trustReport) : formatEvidenceSignatureVerification(report));
|
|
112
123
|
}
|
|
113
|
-
return EXIT_CODES.SUCCESS;
|
|
124
|
+
return trustReport && trustReport.state !== 'trusted' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
114
125
|
}
|
|
115
126
|
const EVIDENCE_COMMAND_HANDLERS = Object.freeze({
|
|
116
127
|
'inspect-archive': runInspectArchiveCommand,
|
package/cli/evidence-signing.mjs
CHANGED
|
@@ -216,11 +216,11 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
216
216
|
]),
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
|
-
export function formatEvidenceSignatureVerification(report) {
|
|
219
|
+
export function formatEvidenceSignatureVerification(report, includeStandaloneTrust = true) {
|
|
220
220
|
const lines = [
|
|
221
221
|
'WHOISleuth evidence signature verification',
|
|
222
222
|
`State: ${report.state}`,
|
|
223
|
-
`Signer trust: ${report.signature.signerTrust}
|
|
223
|
+
...(includeStandaloneTrust ? [`Signer trust: ${report.signature.signerTrust}`] : []),
|
|
224
224
|
`Artifact: ${report.artifact.kind} · ${report.artifact.schema ?? 'unrecognised'} v${report.artifact.version ?? 'unrecognised'}`,
|
|
225
225
|
`Artifact assurance: ${report.artifact.assurance.state}`,
|
|
226
226
|
`Signed: ${report.signature.signedAt}`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { domainToASCII } from 'node:url';
|
|
2
2
|
import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
|
|
3
|
-
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, parseExternalFindingsDocument, } from "../packages/interchange/external-findings-import.mjs";
|
|
3
|
+
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, parseExternalFindingsDocument, retainExternalFindingLimitations, } from "../packages/interchange/external-findings-import.mjs";
|
|
4
4
|
export const EXTERNAL_OBSERVATION_MAPPING_SCHEMA = 'whoisleuth.external-observation-mapping';
|
|
5
5
|
export const EXTERNAL_OBSERVATION_MAPPING_VERSION = 1;
|
|
6
6
|
export const MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES = 4 * 1024 * 1024;
|
|
@@ -49,9 +49,9 @@ function domain(value, label) {
|
|
|
49
49
|
function limitations(value) {
|
|
50
50
|
if (value === undefined)
|
|
51
51
|
return Object.freeze([]);
|
|
52
|
-
if (!Array.isArray(value) || value.length >
|
|
53
|
-
throw new TypeError(
|
|
54
|
-
return Object.freeze([...new Set(value.map((entry, index) => requireBoundedString(entry, `profile.limitations[${index}]`,
|
|
52
|
+
if (!Array.isArray(value) || value.length > MAX_EXTERNAL_FINDING_LIMITATIONS)
|
|
53
|
+
throw new TypeError(`profile.limitations must contain no more than ${MAX_EXTERNAL_FINDING_LIMITATIONS} entries.`);
|
|
54
|
+
return Object.freeze([...new Set(value.map((entry, index) => requireBoundedString(entry, `profile.limitations[${index}]`, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH)))]);
|
|
55
55
|
}
|
|
56
56
|
export function mapExternalObservations(inputRaw) {
|
|
57
57
|
const input = requireRecord(inputRaw, 'External observation mapping');
|
|
@@ -140,11 +140,10 @@ export function mapExternalObservations(inputRaw) {
|
|
|
140
140
|
source,
|
|
141
141
|
findings: selected.map((finding) => ({
|
|
142
142
|
...finding,
|
|
143
|
-
limitations: [
|
|
144
|
-
...finding.limitations,
|
|
143
|
+
limitations: retainExternalFindingLimitations(finding.limitations, [
|
|
145
144
|
`Mapped locally with declarative profile ${profile.id} version ${profile.version}; the profile does not verify the upstream field semantics.`,
|
|
146
|
-
...(truncated ? [
|
|
147
|
-
]
|
|
145
|
+
...(truncated ? [`The supplied records exceeded the browser import boundary; ${selected.length} of ${seen.size} unique findings were retained in deterministic order.`] : []),
|
|
146
|
+
]),
|
|
148
147
|
})),
|
|
149
148
|
};
|
|
150
149
|
return parseExternalFindingsDocument(document);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { registryAccessProfileLabel } from "../registry-access.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { appendTechnologyLines } from "./terminal-technology.mjs";
|
|
3
3
|
import { appendDeliveryMetadataLines, appendPublicationMetadataLines } from "./terminal-metadata.mjs";
|
|
4
|
+
import { securityPostureReview, summarizeSecurityPostureReview } from "../../lib/website-security-posture.mjs";
|
|
4
5
|
import { appendSection, boundedTerminalComponent, boundedTerminalList, boundedTerminalWithSuffix, MAX_TLS_TERMINAL_ALT_NAMES, MAX_TLS_TERMINAL_PURPOSES, safeTerminalValue, terminalCount, terminalCountSummary, terminalDisplayCount, terminalRecord, titleCase, } from "./terminal-shared.mjs";
|
|
5
6
|
const MAX_LOOKUP_TERMINAL_RECORDS = 5;
|
|
6
7
|
const MAX_LOOKUP_TERMINAL_NAMES = 5;
|
|
@@ -10,13 +11,6 @@ const MAX_LOOKUP_TERMINAL_LIMITATIONS = 3;
|
|
|
10
11
|
const MAX_LOOKUP_TERMINAL_CIDRS = 5;
|
|
11
12
|
const MAX_LOOKUP_TERMINAL_FINDINGS = 5;
|
|
12
13
|
const MAX_LOOKUP_TERMINAL_ABUSE_ROUTES = 6;
|
|
13
|
-
function terminalTechnologyRoleNames(findings, role) {
|
|
14
|
-
const names = findings
|
|
15
|
-
.filter((finding) => technologyEvidenceRoles(finding).includes(role))
|
|
16
|
-
.slice(0, 6)
|
|
17
|
-
.map((finding) => safeTerminalValue(terminalRecord(finding).name, 'Unnamed indicator'));
|
|
18
|
-
return names.length ? boundedTerminalList(names, Math.max(0, findings.filter((finding) => technologyEvidenceRoles(finding).includes(role)).length - names.length)) : 'None retained';
|
|
19
|
-
}
|
|
20
14
|
function formatLookupDnsRecord(value) {
|
|
21
15
|
if (typeof value === 'string' || typeof value === 'number')
|
|
22
16
|
return safeTerminalValue(value);
|
|
@@ -244,6 +238,11 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
|
|
|
244
238
|
targetLines.push(`Input host ${safeTerminalValue(document.inputHostname)}`);
|
|
245
239
|
targetLines.push(`Registry query ${safeTerminalValue(document.registrableDomain)}`);
|
|
246
240
|
}
|
|
241
|
+
if (availability.observationHostname) {
|
|
242
|
+
targetLines.push(`DNS/TLS/web ${safeTerminalValue(availability.observationHostname)}`);
|
|
243
|
+
}
|
|
244
|
+
if (availability.webObservationMode === 'selected_url')
|
|
245
|
+
targetLines.push('Website scope Explicitly selected URL; query omitted from provenance');
|
|
247
246
|
if (availability.applicable) {
|
|
248
247
|
targetLines.push(`Availability ${titleCase(availability.state)}`);
|
|
249
248
|
targetLines.push(`Confidence ${titleCase(availability.confidence)}`);
|
|
@@ -486,7 +485,7 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
|
|
|
486
485
|
const technology = terminalRecord(availability.technologyProfile);
|
|
487
486
|
const browserLibraries = terminalRecord(technology.browserLibraryProfile);
|
|
488
487
|
const posture = terminalRecord(availability.securityPosture);
|
|
489
|
-
const
|
|
488
|
+
const postureReview = summarizeSecurityPostureReview(posture.findings);
|
|
490
489
|
const pageIdentity = terminalRecord(availability.pageIdentity);
|
|
491
490
|
const pageRole = terminalRecord(availability.pageRoleProfile);
|
|
492
491
|
const clientBehavior = terminalRecord(availability.clientBehaviorProfile);
|
|
@@ -578,47 +577,20 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
|
|
|
578
577
|
if (detail !== 'summary' && visible.length)
|
|
579
578
|
websiteLines.push(`Declarations ${safeTerminalValue(visible.join('; '))}`);
|
|
580
579
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const item = terminalRecord(finding);
|
|
586
|
-
const qualifiers = [item.category, item.confidence ? `${item.confidence} signature strength` : null].filter(Boolean).map((value) => safeTerminalValue(value));
|
|
587
|
-
return `${safeTerminalValue(item.name, 'Unnamed indicator')}${qualifiers.length ? ` (${qualifiers.join(', ')})` : ''}`;
|
|
588
|
-
});
|
|
589
|
-
if (detail !== 'summary' && visible.length) {
|
|
590
|
-
const omitted = findings.length - visible.length;
|
|
591
|
-
websiteLines.push(`Indicators ${safeTerminalValue(`${visible.join('; ')}${omitted > 0 ? `; +${omitted} more` : ''}`)}`);
|
|
592
|
-
}
|
|
593
|
-
if (detail !== 'summary') {
|
|
594
|
-
const nameservers = Array.isArray(availability.nameservers)
|
|
595
|
-
? availability.nameservers.slice(0, MAX_LOOKUP_TERMINAL_NAMES).map((value) => safeTerminalValue(value))
|
|
596
|
-
: [];
|
|
597
|
-
websiteLines.push(`Nameservers ${nameservers.length ? boundedTerminalList(nameservers, Math.max(0, availability.nameservers.length - nameservers.length)) : 'Unavailable'} · identity does not establish operator or web-host ownership`);
|
|
598
|
-
websiteLines.push(`Observed edge ${terminalTechnologyRoleNames(findings, 'observed_edge')}`);
|
|
599
|
-
websiteLines.push(`App platform ${terminalTechnologyRoleNames(findings, 'application_platform')}`);
|
|
600
|
-
websiteLines.push(`Framework/run ${terminalTechnologyRoleNames(findings, 'framework_runtime')}`);
|
|
601
|
-
websiteLines.push(`Embedded deps ${terminalTechnologyRoleNames(findings, 'embedded_dependency')}`);
|
|
602
|
-
websiteLines.push('Origin host Not established from retained evidence');
|
|
603
|
-
}
|
|
604
|
-
if (detail !== 'summary' && (browserLibraries.profileVersion === 1 || browserLibraries.source === 'derived')) {
|
|
605
|
-
const libraries = Array.isArray(browserLibraries.findings) ? browserLibraries.findings : [];
|
|
606
|
-
const advisoryMatches = libraries.filter((finding) => terminalCount(terminalRecord(finding).advisoryCount) > 0).length;
|
|
607
|
-
websiteLines.push(`JS libraries ${titleCase(browserLibraries.status)} · ${libraries.length} apparent · `
|
|
608
|
-
+ `${advisoryMatches} with catalogue advisory match${advisoryMatches === 1 ? '' : 'es'}`);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
580
|
+
appendTechnologyLines(websiteLines, {
|
|
581
|
+
technology, libraries: browserLibraries, nameservers: availability.nameservers,
|
|
582
|
+
generatedAt: document.generatedAt, detail,
|
|
583
|
+
});
|
|
611
584
|
if (posture.status || posture.source === 'derived') {
|
|
612
585
|
websiteLines.push(`Posture ${titleCase(posture.status)}`);
|
|
613
|
-
if (detail !== 'summary' &&
|
|
614
|
-
websiteLines.push(`Posture
|
|
615
|
-
+
|
|
616
|
-
+
|
|
617
|
-
+ `${terminalCount(postureSummary.unavailable)} unavailable`);
|
|
586
|
+
if (detail !== 'summary' && Array.isArray(posture.findings))
|
|
587
|
+
websiteLines.push(`Posture checks Needs review ${postureReview.needsReview} · `
|
|
588
|
+
+ `Other findings ${postureReview.otherFindings} · `
|
|
589
|
+
+ `Could not assess ${postureReview.unavailable}`);
|
|
618
590
|
if (detail === 'verbose' && positiveSourceStatus(posture.status)) {
|
|
619
591
|
const findings = boundedFindingLabels(posture.findings, (finding) => {
|
|
620
|
-
const
|
|
621
|
-
return `${safeTerminalValue(finding.label, 'Unlabelled finding')}${
|
|
592
|
+
const review = securityPostureReview(finding);
|
|
593
|
+
return `${safeTerminalValue(finding.label, 'Unlabelled finding')}${review.label ? ` (${review.label})` : ''}`;
|
|
622
594
|
});
|
|
623
595
|
if (findings)
|
|
624
596
|
websiteLines.push(`Posture labels ${findings}`);
|
|
@@ -6,7 +6,9 @@ const MAX_TLS_TERMINAL_PURPOSES = 8;
|
|
|
6
6
|
// through soft hyphens, combining grapheme joiners, variation selectors, tag
|
|
7
7
|
// characters, bidi controls, or other zero-width formatting code points.
|
|
8
8
|
const TERMINAL_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/gu;
|
|
9
|
-
function safeTerminalValue(value, fallback = '—') {
|
|
9
|
+
function safeTerminalValue(value, fallback = '—', maximum = MAX_TERMINAL_VALUE_LENGTH) {
|
|
10
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1)
|
|
11
|
+
throw new TypeError('Terminal text requires a positive finite character bound.');
|
|
10
12
|
if (value === null || value === undefined || value === '')
|
|
11
13
|
return fallback;
|
|
12
14
|
const normalized = String(value)
|
|
@@ -16,8 +18,8 @@ function safeTerminalValue(value, fallback = '—') {
|
|
|
16
18
|
.trim();
|
|
17
19
|
if (!normalized)
|
|
18
20
|
return fallback;
|
|
19
|
-
return normalized.length >
|
|
20
|
-
? `${normalized.slice(0,
|
|
21
|
+
return normalized.length > maximum
|
|
22
|
+
? `${normalized.slice(0, maximum - 1)}…`
|
|
21
23
|
: normalized;
|
|
22
24
|
}
|
|
23
25
|
function boundedTerminalList(values, omitted) {
|