@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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 +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/cli/evidence-signing.mjs
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { createHash, createPrivateKey, createPublicKey, sign, verify, } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
import { MAX_OFFLINE_ARTIFACT_BYTES, verifyOfflineArtifact, } from "./artifact-verify.mjs";
|
|
3
|
+
import { canonicalArtifactJsonFor, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
|
|
4
|
+
import { hasVerifiedApplicableIntegrity, hasVerifiedArtifactStructure, MAX_OFFLINE_ARTIFACT_BYTES, verifyOfflineArtifact, } from "./artifact-verify.mjs";
|
|
5
|
+
import { parseBoundedJsonObject } from "./bounded-json.mjs";
|
|
6
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
7
|
export const SIGNED_EVIDENCE_PACKAGE_SCHEMA = 'whoisleuth.signed-evidence-package';
|
|
6
|
-
export const SIGNED_EVIDENCE_PACKAGE_VERSION =
|
|
8
|
+
export const SIGNED_EVIDENCE_PACKAGE_VERSION = 2;
|
|
9
|
+
export const EVIDENCE_SIGNATURE_VERIFICATION_SCHEMA = 'whoisleuth.evidence-signature-verification';
|
|
10
|
+
export const EVIDENCE_SIGNATURE_VERIFICATION_VERSION = 2;
|
|
7
11
|
export const EVIDENCE_SIGNATURE_ALGORITHM = 'Ed25519';
|
|
8
|
-
export const EVIDENCE_SIGNATURE_CANONICALIZATION =
|
|
12
|
+
export const EVIDENCE_SIGNATURE_CANONICALIZATION = SORTED_JSON_V2;
|
|
9
13
|
export const MAX_SIGNING_KEY_FILE_BYTES = 16 * 1024;
|
|
10
14
|
const SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
11
15
|
const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/u;
|
|
@@ -23,36 +27,23 @@ function record(value) {
|
|
|
23
27
|
: null;
|
|
24
28
|
}
|
|
25
29
|
function parseObject(raw, label) {
|
|
26
|
-
|
|
27
|
-
throw new TypeError(`${label} must be UTF-8 JSON text.`);
|
|
28
|
-
const bytes = Buffer.byteLength(raw, 'utf8');
|
|
29
|
-
if (bytes === 0 || bytes > MAX_OFFLINE_ARTIFACT_BYTES) {
|
|
30
|
-
throw new TypeError(`${label} must be between 1 byte and ${MAX_OFFLINE_ARTIFACT_BYTES} bytes.`);
|
|
31
|
-
}
|
|
32
|
-
let parsed;
|
|
33
|
-
try {
|
|
34
|
-
parsed = JSON.parse(raw);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
throw new TypeError(`${label} must be valid JSON.`);
|
|
38
|
-
}
|
|
39
|
-
const value = record(parsed);
|
|
40
|
-
if (!value)
|
|
41
|
-
throw new TypeError(`${label} must contain one JSON object.`);
|
|
42
|
-
return value;
|
|
30
|
+
return parseBoundedJsonObject(raw, { label, maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES });
|
|
43
31
|
}
|
|
44
32
|
function hasExactKeys(value, expected) {
|
|
45
33
|
const keys = Object.keys(value);
|
|
46
34
|
return keys.length === expected.size && keys.every((key) => expected.has(key));
|
|
47
35
|
}
|
|
48
36
|
function timestamp(value) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
37
|
+
const normalized = normalizeExplicitIsoTimestamp(value);
|
|
38
|
+
if (!normalized)
|
|
39
|
+
throw new TypeError('Signature time must be a valid timestamp with an explicit timezone.');
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
function canonicalTimestamp(value) {
|
|
43
|
+
const normalized = timestamp(value);
|
|
44
|
+
if (value !== normalized)
|
|
45
|
+
throw new TypeError('Signature time must use canonical UTC timestamp text.');
|
|
46
|
+
return normalized;
|
|
56
47
|
}
|
|
57
48
|
function importPrivateKey(pem) {
|
|
58
49
|
if (Buffer.byteLength(pem, 'utf8') > MAX_SIGNING_KEY_FILE_BYTES) {
|
|
@@ -96,8 +87,8 @@ function spkiDer(key) {
|
|
|
96
87
|
function keyId(publicDer) {
|
|
97
88
|
return createHash('sha256').update(publicDer).digest('hex');
|
|
98
89
|
}
|
|
99
|
-
function payloadBytes(payload) {
|
|
100
|
-
return Buffer.from(
|
|
90
|
+
function payloadBytes(payload, canonicalization) {
|
|
91
|
+
return Buffer.from(canonicalArtifactJsonFor(payload, canonicalization), 'utf8');
|
|
101
92
|
}
|
|
102
93
|
async function signableArtifact(raw) {
|
|
103
94
|
const artifact = parseObject(raw, 'Evidence artefact');
|
|
@@ -105,6 +96,9 @@ async function signableArtifact(raw) {
|
|
|
105
96
|
if (!['case_response_packet', 'investigation_capsule', 'signed_review_artifact'].includes(verification.artifact.kind)) {
|
|
106
97
|
throw new TypeError('Only reviewed response packets, investigation capsules, and supported review manifests can be signed.');
|
|
107
98
|
}
|
|
99
|
+
if (!hasVerifiedArtifactStructure(verification) || !hasVerifiedApplicableIntegrity(verification)) {
|
|
100
|
+
throw new TypeError('Evidence signing requires verified structure and a verified applicable integrity contract.');
|
|
101
|
+
}
|
|
108
102
|
return { artifact, verification };
|
|
109
103
|
}
|
|
110
104
|
export async function signEvidencePackage(raw, privateKeyPem, signedAt = new Date().toISOString()) {
|
|
@@ -124,7 +118,7 @@ export async function signEvidencePackage(raw, privateKeyPem, signedAt = new Dat
|
|
|
124
118
|
canonicalization: EVIDENCE_SIGNATURE_CANONICALIZATION,
|
|
125
119
|
publicKeySpkiDerBase64: publicDer.toString('base64'),
|
|
126
120
|
keyIdSha256: keyId(publicDer),
|
|
127
|
-
valueBase64: sign(null, payloadBytes(payload), privateKey).toString('base64'),
|
|
121
|
+
valueBase64: sign(null, payloadBytes(payload, SORTED_JSON_V2), privateKey).toString('base64'),
|
|
128
122
|
}),
|
|
129
123
|
});
|
|
130
124
|
}
|
|
@@ -139,7 +133,6 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
139
133
|
|| !signature
|
|
140
134
|
|| !hasExactKeys(signature, SIGNATURE_KEYS)
|
|
141
135
|
|| signature.algorithm !== EVIDENCE_SIGNATURE_ALGORITHM
|
|
142
|
-
|| signature.canonicalization !== EVIDENCE_SIGNATURE_CANONICALIZATION
|
|
143
136
|
|| typeof signature.publicKeySpkiDerBase64 !== 'string'
|
|
144
137
|
|| signature.publicKeySpkiDerBase64.length > 4096
|
|
145
138
|
|| !BASE64_RE.test(signature.publicKeySpkiDerBase64)
|
|
@@ -150,7 +143,10 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
150
143
|
|| !SHA256_RE.test(signature.keyIdSha256)) {
|
|
151
144
|
throw new TypeError('Signed evidence package has an unsupported or malformed envelope.');
|
|
152
145
|
}
|
|
153
|
-
|
|
146
|
+
if (signature.canonicalization !== EVIDENCE_SIGNATURE_CANONICALIZATION) {
|
|
147
|
+
throw new TypeError('Signed evidence package has an unsupported or malformed envelope.');
|
|
148
|
+
}
|
|
149
|
+
const signedAt = canonicalTimestamp(value.signedAt);
|
|
154
150
|
const publicDer = Buffer.from(signature.publicKeySpkiDerBase64, 'base64');
|
|
155
151
|
if (keyId(publicDer) !== signature.keyIdSha256) {
|
|
156
152
|
throw new TypeError('Signed evidence package public-key identifier does not match its embedded key.');
|
|
@@ -164,10 +160,14 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
164
160
|
};
|
|
165
161
|
const signatureBytes = Buffer.from(signature.valueBase64, 'base64');
|
|
166
162
|
if (signatureBytes.length !== 64
|
|
167
|
-
|| !verify(null, payloadBytes(payload), embeddedPublicKey, signatureBytes)) {
|
|
163
|
+
|| !verify(null, payloadBytes(payload, EVIDENCE_SIGNATURE_CANONICALIZATION), embeddedPublicKey, signatureBytes)) {
|
|
168
164
|
throw new TypeError('Signed evidence package failed Ed25519 verification.');
|
|
169
165
|
}
|
|
170
|
-
|
|
166
|
+
let artifactVerification = null;
|
|
167
|
+
try {
|
|
168
|
+
artifactVerification = await verifyOfflineArtifact(JSON.stringify(artifact));
|
|
169
|
+
}
|
|
170
|
+
catch { /* Signature validity remains independently reportable. */ }
|
|
171
171
|
let publicKeyMatched = null;
|
|
172
172
|
if (trustedPublicKeyPem) {
|
|
173
173
|
const trustedDer = spkiDer(importPublicKey(trustedPublicKeyPem, 'Trusted public key file'));
|
|
@@ -176,24 +176,41 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
176
176
|
throw new TypeError('Signed evidence package does not match the trusted public key.');
|
|
177
177
|
}
|
|
178
178
|
return Object.freeze({
|
|
179
|
-
schema:
|
|
180
|
-
version:
|
|
181
|
-
valid: true,
|
|
179
|
+
schema: EVIDENCE_SIGNATURE_VERIFICATION_SCHEMA,
|
|
180
|
+
version: EVIDENCE_SIGNATURE_VERIFICATION_VERSION,
|
|
182
181
|
state: 'signature_valid',
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
signature: Object.freeze({
|
|
183
|
+
state: 'valid',
|
|
184
|
+
signerTrust: trustedPublicKeyPem ? 'trusted_key' : 'embedded_key_only',
|
|
185
|
+
signedAt,
|
|
186
|
+
keyIdSha256: signature.keyIdSha256,
|
|
187
|
+
publicKeyMatched,
|
|
188
|
+
}),
|
|
187
189
|
artifact: Object.freeze({
|
|
188
|
-
schema: artifactVerification
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
schema: artifactVerification?.artifact.schema
|
|
191
|
+
?? (typeof artifact.schema === 'string' && /^[a-z0-9.-]{1,160}$/u.test(artifact.schema) ? artifact.schema : null),
|
|
192
|
+
version: artifactVerification?.artifact.version
|
|
193
|
+
?? (Number.isSafeInteger(artifact.version ?? artifact.schemaVersion)
|
|
194
|
+
&& Number(artifact.version ?? artifact.schemaVersion) >= 1
|
|
195
|
+
&& Number(artifact.version ?? artifact.schemaVersion) <= 1_000
|
|
196
|
+
? Number(artifact.version ?? artifact.schemaVersion)
|
|
197
|
+
: null),
|
|
198
|
+
kind: artifactVerification?.artifact.kind ?? 'unverified_artifact',
|
|
199
|
+
assurance: Object.freeze({
|
|
200
|
+
state: artifactVerification?.state ?? 'not_verified',
|
|
201
|
+
structure: artifactVerification?.checks.structure ?? 'not_checked',
|
|
202
|
+
contentIntegrity: artifactVerification?.checks.contentIntegrity ?? 'not_checked',
|
|
203
|
+
contentIntegrityScope: artifactVerification?.checks.contentIntegrityScope ?? 'not_checked',
|
|
204
|
+
}),
|
|
191
205
|
}),
|
|
192
206
|
limitations: Object.freeze([
|
|
193
207
|
trustedPublicKeyPem
|
|
194
208
|
? 'The signature and supplied public key match; recipient trust still depends on obtaining that public key through an authenticated channel.'
|
|
195
209
|
: 'The signature is internally valid for the embedded public key, but signer identity was not authenticated because no trusted public key was supplied.',
|
|
196
210
|
'The signature authenticates the canonical sorted-JSON content, not the original whitespace, indentation, or object-key order of the serialised file.',
|
|
211
|
+
...(artifactVerification
|
|
212
|
+
? [`Embedded-artifact assurance is reported separately as ${artifactVerification.state}; it does not change the cryptographic signature result.`]
|
|
213
|
+
: ['The embedded artefact did not pass a supported local assurance contract. The cryptographic signature remains valid for its bytes, but no structure or inner-integrity claim is made.']),
|
|
197
214
|
'A valid signature proves the canonical package content has not changed since signing; it does not establish that the retained observations or analyst statements are accurate.',
|
|
198
215
|
'WHOISleuth does not generate, store, recover, rotate, or publish signing keys.',
|
|
199
216
|
]),
|
|
@@ -203,11 +220,12 @@ export function formatEvidenceSignatureVerification(report) {
|
|
|
203
220
|
const lines = [
|
|
204
221
|
'WHOISleuth evidence signature verification',
|
|
205
222
|
`State: ${report.state}`,
|
|
206
|
-
`Signer trust: ${report.signerTrust}`,
|
|
207
|
-
`Artifact: ${report.artifact.kind} · ${report.artifact.schema} v${report.artifact.version}`,
|
|
208
|
-
`
|
|
209
|
-
`
|
|
210
|
-
`
|
|
223
|
+
`Signer trust: ${report.signature.signerTrust}`,
|
|
224
|
+
`Artifact: ${report.artifact.kind} · ${report.artifact.schema ?? 'unrecognised'} v${report.artifact.version ?? 'unrecognised'}`,
|
|
225
|
+
`Artifact assurance: ${report.artifact.assurance.state}`,
|
|
226
|
+
`Signed: ${report.signature.signedAt}`,
|
|
227
|
+
`Key ID: sha256:${report.signature.keyIdSha256}`,
|
|
228
|
+
`Trusted public key: ${report.signature.publicKeyMatched === null ? 'not supplied' : 'matched'}`,
|
|
211
229
|
];
|
|
212
230
|
for (const limitation of report.limitations)
|
|
213
231
|
lines.push(`Limitation: ${limitation}`);
|
package/cli/export-evidence.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createRequire } from 'node:module';
|
|
2
1
|
import { projectCliLookupComparisonInput } from "./compare.mjs";
|
|
3
2
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { validateLookupEvidenceArtifactStructure } from "./artifact-structure.mjs";
|
|
4
|
+
import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
|
|
5
|
+
import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, serializeLookupEvidence, } from "../lib/evidence-export.mjs";
|
|
6
|
+
const APPLICATION_VERSION = WHOISLEUTH_APPLICATION_VERSION;
|
|
6
7
|
function objectOrNull(value) {
|
|
7
8
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
8
9
|
}
|
|
@@ -27,9 +28,15 @@ function buildCliEvidenceExport(text, evidenceModule, generatedAt = new Date().t
|
|
|
27
28
|
|| result.schemaVersion !== dependency.LOOKUP_EVIDENCE_SCHEMA_VERSION) {
|
|
28
29
|
throw new TypeError('Lookup evidence builder returned an unsupported report contract.');
|
|
29
30
|
}
|
|
31
|
+
validateLookupEvidenceArtifactStructure(result);
|
|
32
|
+
serializeLookupEvidence(result);
|
|
30
33
|
return result;
|
|
31
34
|
}
|
|
32
35
|
function formatCliEvidenceExport(document, compact = false) {
|
|
33
|
-
|
|
36
|
+
const output = `${serializeLookupEvidence(document, !compact)}\n`;
|
|
37
|
+
if (new TextEncoder().encode(output).byteLength > LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES) {
|
|
38
|
+
throw new TypeError('Lookup evidence exceeds the 5 MiB portable file limit.');
|
|
39
|
+
}
|
|
40
|
+
return output;
|
|
34
41
|
}
|
|
35
42
|
export { APPLICATION_VERSION, buildCliEvidenceExport, formatCliEvidenceExport };
|
|
@@ -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_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, parseExternalFindingsDocument, } from "../
|
|
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";
|
|
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;
|
|
@@ -135,8 +135,8 @@ export function mapExternalObservations(inputRaw) {
|
|
|
135
135
|
}
|
|
136
136
|
const truncated = selected.length < seen.size;
|
|
137
137
|
const document = {
|
|
138
|
-
schema:
|
|
139
|
-
schemaVersion:
|
|
138
|
+
schema: EXTERNAL_FINDINGS_SCHEMA,
|
|
139
|
+
schemaVersion: EXTERNAL_FINDINGS_VERSION,
|
|
140
140
|
source,
|
|
141
141
|
findings: selected.map((finding) => ({
|
|
142
142
|
...finding,
|
package/cli/formatters/html.mjs
CHANGED
|
@@ -26,8 +26,8 @@ function renderRegistrarComparison(comparison) {
|
|
|
26
26
|
: '';
|
|
27
27
|
return `<div class="table-scroll"><table><caption>Normalized registry and registrar RDAP publication comparison</caption><thead><tr><th scope="col">Field</th><th scope="col">Result</th><th scope="col">Registry RDAP</th><th scope="col">Registrar RDAP</th></tr></thead><tbody>${rows}</tbody></table></div>${omitted}`;
|
|
28
28
|
}
|
|
29
|
-
function formatLookupEvidenceHtml(document) {
|
|
30
|
-
const report = buildLookupEvidenceReport(document);
|
|
29
|
+
function formatLookupEvidenceHtml(document, options = {}) {
|
|
30
|
+
const report = buildLookupEvidenceReport(document, options);
|
|
31
31
|
const title = `Lookup evidence report: ${report.title}`;
|
|
32
32
|
return `<!doctype html>
|
|
33
33
|
<html lang="en">
|
|
@@ -60,7 +60,7 @@ function formatLookupEvidenceHtml(document) {
|
|
|
60
60
|
<section class="block"><h2>Network evidence</h2>${renderGroups(report.networkGroups)}</section>
|
|
61
61
|
<section class="block"><h2>Collection diagnostics</h2>${renderFields(report.diagnostics)}</section>
|
|
62
62
|
<section class="block"><h2>Limitations</h2><ul>${report.limitations.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul></section>
|
|
63
|
-
<footer>Offline, self-contained report. No scripts, forms, external resources, or active links are included
|
|
63
|
+
<footer>Offline, self-contained report. No scripts, forms, external resources, or active links are included.${report.attribution ? `<br>${escapeHtml(report.attribution)}` : ''}</footer>
|
|
64
64
|
</main>
|
|
65
65
|
</body>
|
|
66
66
|
</html>
|
package/cli/formatters/json.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bulkDnsSummary } from "../bulk-output.mjs";
|
|
2
|
-
|
|
2
|
+
import { SAVED_LOOKUP_SCHEMA_VERSION, buildCliLookupDocument, } from "../saved-lookup.mjs";
|
|
3
|
+
import { CLI_LOOKUP_SCHEMA } from "../../packages/contracts/cli-lookup.mjs";
|
|
3
4
|
const CLI_BULK_SCHEMA = 'whoisleuth.cli.bulk';
|
|
4
5
|
const CLI_BULK_ITEM_SCHEMA = 'whoisleuth.cli.bulk.item';
|
|
5
6
|
const CLI_CT_SEARCH_SCHEMA = 'whoisleuth.cli.ct-search';
|
|
@@ -9,42 +10,29 @@ const CLI_POSTURE_SCHEMA = 'whoisleuth.cli.posture';
|
|
|
9
10
|
const CLI_HTTP_SCHEMA = 'whoisleuth.cli.http';
|
|
10
11
|
const CLI_TLS_SCHEMA = 'whoisleuth.cli.tls';
|
|
11
12
|
const CLI_COMPARE_SCHEMA = 'whoisleuth.cli.compare';
|
|
12
|
-
const CLI_LOOKUP_SCHEMA_VERSION =
|
|
13
|
+
const CLI_LOOKUP_SCHEMA_VERSION = SAVED_LOOKUP_SCHEMA_VERSION;
|
|
13
14
|
const CLI_BULK_SCHEMA_VERSION = 3;
|
|
14
15
|
const CLI_CT_SEARCH_SCHEMA_VERSION = 1;
|
|
15
16
|
const CLI_DISCOVER_SCHEMA_VERSION = 2;
|
|
16
17
|
const CLI_POSTURE_SCHEMA_VERSION = 1;
|
|
17
|
-
const CLI_HTTP_SCHEMA_VERSION =
|
|
18
|
+
const CLI_HTTP_SCHEMA_VERSION = 3;
|
|
18
19
|
const CLI_TLS_SCHEMA_VERSION = 1;
|
|
19
20
|
const CLI_COMPARE_SCHEMA_VERSION = 3;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
mode: mode === 'deep' ? 'deep' : 'fast',
|
|
33
|
-
query,
|
|
34
|
-
type: classified.type,
|
|
35
|
-
inputHostname: classified.inputHostname,
|
|
36
|
-
registrableDomain: classified.registrableDomain,
|
|
37
|
-
isSubdomain: classified.isSubdomain,
|
|
38
|
-
...((observerLabel || vantageLabel) ? {
|
|
39
|
-
collectionContext: {
|
|
40
|
-
...(observerLabel ? { observerLabel } : {}),
|
|
41
|
-
...(vantageLabel ? { vantageLabel } : {}),
|
|
42
|
-
},
|
|
43
|
-
} : {}),
|
|
44
|
-
};
|
|
21
|
+
const TERMINAL_UNSAFE_JSON_RE = /[\u007f-\u009f\u2028\u2029]|\p{Default_Ignorable_Code_Point}/gu;
|
|
22
|
+
function jsonUnicodeEscape(value) {
|
|
23
|
+
const codePoint = value.codePointAt(0);
|
|
24
|
+
if (codePoint <= 0xffff)
|
|
25
|
+
return `\\u${codePoint.toString(16).padStart(4, '0')}`;
|
|
26
|
+
const offset = codePoint - 0x10000;
|
|
27
|
+
const high = 0xd800 + (offset >> 10);
|
|
28
|
+
const low = 0xdc00 + (offset & 0x3ff);
|
|
29
|
+
return `\\u${high.toString(16).padStart(4, '0')}\\u${low.toString(16).padStart(4, '0')}`;
|
|
30
|
+
}
|
|
31
|
+
function terminalSafeJson(value, space) {
|
|
32
|
+
return JSON.stringify(value, null, space).replace(TERMINAL_UNSAFE_JSON_RE, jsonUnicodeEscape);
|
|
45
33
|
}
|
|
46
34
|
function formatJsonDocument(document) {
|
|
47
|
-
return `${
|
|
35
|
+
return `${terminalSafeJson(document, 2)}\n`;
|
|
48
36
|
}
|
|
49
37
|
function buildCliCtSearchDocument(keyword, result, generatedAt = new Date().toISOString()) {
|
|
50
38
|
return { ...result, schema: CLI_CT_SEARCH_SCHEMA, version: CLI_CT_SEARCH_SCHEMA_VERSION, generatedAt, keyword };
|
|
@@ -83,7 +71,7 @@ function discoverJsonItem(candidate, metadata) {
|
|
|
83
71
|
function formatDiscoverJsonLines(candidates, metadata) {
|
|
84
72
|
if (!candidates.length)
|
|
85
73
|
return '';
|
|
86
|
-
return `${candidates.map((candidate) =>
|
|
74
|
+
return `${candidates.map((candidate) => terminalSafeJson(discoverJsonItem(candidate, metadata))).join('\n')}\n`;
|
|
87
75
|
}
|
|
88
76
|
function formatDiscoverDomainList(candidates) {
|
|
89
77
|
const domains = [...new Set(candidates.flatMap((candidate) => (typeof candidate.domain === 'string' && candidate.domain ? [candidate.domain] : [])))];
|
|
@@ -149,7 +137,7 @@ function buildCliBulkDocument(items, metadata) {
|
|
|
149
137
|
}
|
|
150
138
|
function formatJsonLines(items, metadata) {
|
|
151
139
|
return items.length
|
|
152
|
-
? `${items.map((item) =>
|
|
140
|
+
? `${items.map((item) => terminalSafeJson(bulkJsonItem(item, metadata))).join('\n')}\n`
|
|
153
141
|
: '';
|
|
154
142
|
}
|
|
155
|
-
export { CLI_BULK_ITEM_SCHEMA, CLI_BULK_SCHEMA, CLI_BULK_SCHEMA_VERSION, CLI_COMPARE_SCHEMA, CLI_COMPARE_SCHEMA_VERSION, CLI_CT_SEARCH_SCHEMA, CLI_CT_SEARCH_SCHEMA_VERSION, CLI_DISCOVER_ITEM_SCHEMA, CLI_DISCOVER_SCHEMA, CLI_DISCOVER_SCHEMA_VERSION, CLI_HTTP_SCHEMA, CLI_HTTP_SCHEMA_VERSION, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, CLI_TLS_SCHEMA, CLI_TLS_SCHEMA_VERSION, CLI_LOOKUP_SCHEMA, CLI_LOOKUP_SCHEMA_VERSION, buildCliBulkDocument, buildCliCompareDocument, buildCliCtSearchDocument, buildCliDiscoverDocument, buildCliHttpDocument, buildCliLookupDocument, buildCliPostureDocument, buildCliTlsDocument, bulkJsonItem, discoverJsonItem, formatDiscoverDomainList, formatDiscoverJsonLines, formatJsonDocument, formatJsonLines, };
|
|
143
|
+
export { CLI_BULK_ITEM_SCHEMA, CLI_BULK_SCHEMA, CLI_BULK_SCHEMA_VERSION, CLI_COMPARE_SCHEMA, CLI_COMPARE_SCHEMA_VERSION, CLI_CT_SEARCH_SCHEMA, CLI_CT_SEARCH_SCHEMA_VERSION, CLI_DISCOVER_ITEM_SCHEMA, CLI_DISCOVER_SCHEMA, CLI_DISCOVER_SCHEMA_VERSION, CLI_HTTP_SCHEMA, CLI_HTTP_SCHEMA_VERSION, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, CLI_TLS_SCHEMA, CLI_TLS_SCHEMA_VERSION, CLI_LOOKUP_SCHEMA, CLI_LOOKUP_SCHEMA_VERSION, buildCliBulkDocument, buildCliCompareDocument, buildCliCtSearchDocument, buildCliDiscoverDocument, buildCliHttpDocument, buildCliLookupDocument, buildCliPostureDocument, buildCliTlsDocument, bulkJsonItem, discoverJsonItem, formatDiscoverDomainList, formatDiscoverJsonLines, formatJsonDocument, formatJsonLines, terminalSafeJson, };
|