@slicedearth/whoisleuth-cli 1.47.4 → 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/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- 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 +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- 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 +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -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 +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- 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-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- 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-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- 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 +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- 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/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- 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 -21
- 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} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- 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/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -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/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- 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} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- 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/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- 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} +70 -47
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -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} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- 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} +13 -13
- 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} +110 -89
- package/third-party-notices.txt +14 -14
- 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 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- 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 -255
- 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 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /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/lib/availability.mjs
CHANGED
|
@@ -32,10 +32,24 @@ function errorRecord(value) {
|
|
|
32
32
|
? value
|
|
33
33
|
: {};
|
|
34
34
|
}
|
|
35
|
+
function registryStatusToken(value) {
|
|
36
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/gu, '');
|
|
37
|
+
}
|
|
35
38
|
function rdapEventDate(events, action) {
|
|
36
39
|
const date = events.find((event) => event.action === action)?.date;
|
|
37
40
|
return typeof date === 'string' ? date : null;
|
|
38
41
|
}
|
|
42
|
+
function withoutHttpDeliveryMetadata(value) {
|
|
43
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
44
|
+
return value;
|
|
45
|
+
const output = { ...value };
|
|
46
|
+
if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
|
|
47
|
+
const response = { ...output.response };
|
|
48
|
+
delete response.deliveryMetadata;
|
|
49
|
+
output.response = response;
|
|
50
|
+
}
|
|
51
|
+
return output;
|
|
52
|
+
}
|
|
39
53
|
// No marketplace (Afternic/Sedo/Dan.com/GoDaddy Auctions/etc.) offers a
|
|
40
54
|
// free, no-auth API to check "is this specific domain listed for sale" -
|
|
41
55
|
// what's checkable without credentials is the parking/landing-page
|
|
@@ -448,40 +462,39 @@ async function checkDomainAvailability(domain, options = {}) {
|
|
|
448
462
|
source: 'whois',
|
|
449
463
|
};
|
|
450
464
|
}
|
|
451
|
-
if (parsed.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
registrar
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|| registrar || createdDate || expiryDate || nameservers.length > 0 || statuses.length > 0) {
|
|
465
|
+
if (parsed.registrationStatus === 'registered') {
|
|
466
|
+
if (parsed.nameservers.length)
|
|
467
|
+
nameservers = parsed.nameservers;
|
|
468
|
+
if (parsed.statuses.length)
|
|
469
|
+
statuses = parsed.statuses.map((status) => status.toLowerCase());
|
|
470
|
+
if (parsed.registrar) {
|
|
471
|
+
registrar = {
|
|
472
|
+
handle: null,
|
|
473
|
+
ianaId: parsed.registrarIanaId || null,
|
|
474
|
+
name: parsed.registrar,
|
|
475
|
+
org: null,
|
|
476
|
+
email: parsed.abuseEmail || null,
|
|
477
|
+
phone: parsed.abusePhone || null,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
if (parsed.registrantName || parsed.registrantOrg || parsed.registrantEmail || parsed.registrantPhone) {
|
|
481
|
+
registrant = {
|
|
482
|
+
handle: null,
|
|
483
|
+
name: parsed.registrantName || null,
|
|
484
|
+
org: parsed.registrantOrg || null,
|
|
485
|
+
email: parsed.registrantEmail || null,
|
|
486
|
+
phone: parsed.registrantPhone || null,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (parsed.abuseEmail || parsed.abusePhone) {
|
|
490
|
+
abuse = { handle: null, name: null, org: parsed.registrar || null, email: parsed.abuseEmail || null, phone: parsed.abusePhone || null };
|
|
491
|
+
}
|
|
492
|
+
createdDate = parsed.createdDate || null;
|
|
493
|
+
expiryDate = parsed.expiryDate || null;
|
|
494
|
+
createdDateIso = parsed.createdDateIso || parsed.lifecycle?.createdDateIso || registryDateIso(createdDate);
|
|
495
|
+
expiryDateIso = parsed.expiryDateIso || parsed.lifecycle?.expiryDateIso || registryDateIso(expiryDate);
|
|
496
|
+
if (!dnssec)
|
|
497
|
+
dnssec = parsed.dnssec || null;
|
|
485
498
|
registrationSource = 'whois';
|
|
486
499
|
}
|
|
487
500
|
}
|
|
@@ -493,9 +506,7 @@ async function checkDomainAvailability(domain, options = {}) {
|
|
|
493
506
|
// registration evidence either - e.g. the registry answered inconclusively
|
|
494
507
|
// or every referral hop failed/rate-limited. Report "unknown" rather than
|
|
495
508
|
// fabricating "registered" from an empty record.
|
|
496
|
-
const hasWhoisRegistrationData = whoisParsed?.registrationStatus === 'registered'
|
|
497
|
-
|| Boolean(registrar) || Boolean(createdDate) || Boolean(expiryDate)
|
|
498
|
-
|| nameservers.length > 0 || statuses.length > 0;
|
|
509
|
+
const hasWhoisRegistrationData = whoisParsed?.registrationStatus === 'registered';
|
|
499
510
|
let dnsDelegated = false;
|
|
500
511
|
if (!rdapFound && !hasWhoisRegistrationData && dnsDelegationPromise) {
|
|
501
512
|
const delegation = await dnsDelegationPromise;
|
|
@@ -549,7 +560,10 @@ async function checkDomainAvailability(domain, options = {}) {
|
|
|
549
560
|
dnssec,
|
|
550
561
|
source: registrationSource,
|
|
551
562
|
};
|
|
552
|
-
if (statuses.some((
|
|
563
|
+
if (statuses.some((status) => {
|
|
564
|
+
const normalizedStatus = registryStatusToken(status);
|
|
565
|
+
return normalizedStatus.includes('pendingdelete') || normalizedStatus.includes('redemptionperiod');
|
|
566
|
+
})) {
|
|
553
567
|
return {
|
|
554
568
|
state: 'expiring',
|
|
555
569
|
confidence: 'medium',
|
|
@@ -648,6 +662,7 @@ async function checkDomainAvailability(domain, options = {}) {
|
|
|
648
662
|
responseHeaders: homepage.technologyHeaders,
|
|
649
663
|
activityStatus,
|
|
650
664
|
includePageIdentity: pageIdentityEligible,
|
|
665
|
+
includePublicationMetadata: options.includePublicationMetadata !== false,
|
|
651
666
|
...(options.includeCredentialSurfaceProfile !== undefined
|
|
652
667
|
? { includeCredentialSurfaceProfile: options.includeCredentialSurfaceProfile }
|
|
653
668
|
: {}),
|
|
@@ -661,6 +676,9 @@ async function checkDomainAvailability(domain, options = {}) {
|
|
|
661
676
|
}
|
|
662
677
|
}
|
|
663
678
|
const responsePolicy = qualifyResponsePolicyWithCspMeta(homepage.responsePolicy, htmlSignals.cspMetaPolicy);
|
|
679
|
+
const retainedHttp = options.includeDeliveryMetadata === false
|
|
680
|
+
? withoutHttpDeliveryMetadata(homepage.http)
|
|
681
|
+
: homepage.http;
|
|
664
682
|
const { cspMetaPolicy: _cspMetaPolicy, ...retainedHtmlSignals } = htmlSignals;
|
|
665
683
|
const securityPosture = options.includeSecurityPosture === false ? null : analyzeWebsiteSecurityPosture({
|
|
666
684
|
http: homepage.http,
|
|
@@ -694,7 +712,7 @@ async function checkDomainAvailability(domain, options = {}) {
|
|
|
694
712
|
activityStatus,
|
|
695
713
|
websiteProbeStatus,
|
|
696
714
|
websiteProbeDetail,
|
|
697
|
-
http:
|
|
715
|
+
http: retainedHttp,
|
|
698
716
|
deepScanComplete,
|
|
699
717
|
faviconHash,
|
|
700
718
|
faviconPHash,
|
|
@@ -718,7 +736,7 @@ async function checkDomainAvailability(domain, options = {}) {
|
|
|
718
736
|
activityStatus,
|
|
719
737
|
websiteProbeStatus,
|
|
720
738
|
websiteProbeDetail,
|
|
721
|
-
http:
|
|
739
|
+
http: retainedHttp,
|
|
722
740
|
deepScanComplete,
|
|
723
741
|
faviconHash,
|
|
724
742
|
faviconPHash,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// not infer missing values and keep every string, object, enum, and counter
|
|
3
3
|
// bounded before a provider or connector result reaches its public model.
|
|
4
4
|
import { domainToASCII } from 'node:url';
|
|
5
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
6
|
export function isRecord(value) {
|
|
6
7
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
7
8
|
}
|
|
@@ -26,10 +27,7 @@ export function strictBoundedString(value, maxLength) {
|
|
|
26
27
|
return normalized && normalized.length <= maxLength ? normalized : null;
|
|
27
28
|
}
|
|
28
29
|
export function isoTimestamp(value) {
|
|
29
|
-
|
|
30
|
-
return null;
|
|
31
|
-
const timestamp = Date.parse(value);
|
|
32
|
-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
30
|
+
return normalizeExplicitIsoTimestamp(value);
|
|
33
31
|
}
|
|
34
32
|
export function boundedHttpsUrl(value, maxLength) {
|
|
35
33
|
const raw = strictBoundedString(value, maxLength);
|
package/lib/bounded-file.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
|
-
import { open } from 'node:fs/promises';
|
|
2
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
const MAX_BOUNDED_FILE_BYTES = 64 * 1024 * 1024;
|
|
4
5
|
function boundedSize(value, fallback) {
|
|
5
6
|
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= MAX_BOUNDED_FILE_BYTES
|
|
@@ -13,7 +14,7 @@ function throwIfAborted(signal) {
|
|
|
13
14
|
? signal.reason
|
|
14
15
|
: new DOMException('The operation was aborted.', 'AbortError');
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
async function readBoundedRegularFileIdentity(filePath, options, dependencies = {}) {
|
|
17
18
|
const label = options.label?.trim().slice(0, 120) || 'Input file';
|
|
18
19
|
const maximumBytes = boundedSize(options.maximumBytes, 0);
|
|
19
20
|
const minimumBytes = boundedSize(options.minimumBytes, 0);
|
|
@@ -70,13 +71,86 @@ export async function readBoundedRegularFile(filePath, options, dependencies = {
|
|
|
70
71
|
|| after.ctimeMs !== before.ctimeMs) {
|
|
71
72
|
throw new TypeError(`${label} changed while it was being read.`);
|
|
72
73
|
}
|
|
73
|
-
return
|
|
74
|
+
return {
|
|
75
|
+
bytes: Buffer.from(storage.subarray(0, offset)),
|
|
76
|
+
device: before.dev,
|
|
77
|
+
inode: before.ino,
|
|
78
|
+
};
|
|
74
79
|
}
|
|
75
80
|
finally {
|
|
76
81
|
await handle.close();
|
|
77
82
|
}
|
|
78
83
|
}
|
|
84
|
+
export async function readBoundedRegularFile(filePath, options, dependencies = {}) {
|
|
85
|
+
return (await readBoundedRegularFileIdentity(filePath, options, dependencies)).bytes;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Decode admitted bytes without replacement-character normalisation. The BOM
|
|
89
|
+
* is deliberately retained so re-encoding the returned string preserves the
|
|
90
|
+
* exact bytes used by artefact identity and manifest checks.
|
|
91
|
+
*/
|
|
92
|
+
export function decodeBoundedUtf8(bytes, label = 'Input') {
|
|
93
|
+
try {
|
|
94
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new TypeError(`${label.trim().slice(0, 120) || 'Input'} must contain valid UTF-8 text.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
79
100
|
export async function readBoundedRegularTextFile(filePath, options, dependencies = {}) {
|
|
80
|
-
return (await readBoundedRegularFile(filePath, options, dependencies)
|
|
101
|
+
return decodeBoundedUtf8(await readBoundedRegularFile(filePath, options, dependencies), options.label);
|
|
102
|
+
}
|
|
103
|
+
function safeRelativeFilePath(value, label) {
|
|
104
|
+
if (!value || path.isAbsolute(value) || value.includes('\\')
|
|
105
|
+
|| value.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
106
|
+
throw new TypeError(`${label} must be a safe relative file path.`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Reads one regular file beneath a canonical root while rejecting final and
|
|
112
|
+
* intermediate symbolic links. The ordinary descriptor-level size/change
|
|
113
|
+
* checks still apply after path confinement is established.
|
|
114
|
+
*/
|
|
115
|
+
export async function readBoundedRegularFileWithin(rootDirectory, relativeFilePath, options, dependencies = {}) {
|
|
116
|
+
const label = options.label?.trim().slice(0, 120) || 'Input file';
|
|
117
|
+
const relativePath = safeRelativeFilePath(relativeFilePath, label);
|
|
118
|
+
const canonicalRoot = await realpath(rootDirectory);
|
|
119
|
+
const targetPath = path.resolve(canonicalRoot, relativePath);
|
|
120
|
+
const confined = path.relative(canonicalRoot, targetPath);
|
|
121
|
+
if (!confined || confined.startsWith(`..${path.sep}`) || confined === '..' || path.isAbsolute(confined)) {
|
|
122
|
+
throw new TypeError(`${label} escapes its configured root.`);
|
|
123
|
+
}
|
|
124
|
+
let cursor = canonicalRoot;
|
|
125
|
+
const components = relativePath.split('/');
|
|
126
|
+
let admittedDevice = -1;
|
|
127
|
+
let admittedInode = -1;
|
|
128
|
+
for (const [index, component] of components.entries()) {
|
|
129
|
+
cursor = path.join(cursor, component);
|
|
130
|
+
const metadata = await lstat(cursor);
|
|
131
|
+
if (metadata.isSymbolicLink())
|
|
132
|
+
throw new TypeError(`${label} must not traverse a symbolic link.`);
|
|
133
|
+
if (index === components.length - 1) {
|
|
134
|
+
admittedDevice = metadata.dev;
|
|
135
|
+
admittedInode = metadata.ino;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const canonicalTarget = await realpath(targetPath);
|
|
139
|
+
if (canonicalTarget !== targetPath)
|
|
140
|
+
throw new TypeError(`${label} must not traverse a symbolic link.`);
|
|
141
|
+
const identity = await readBoundedRegularFileIdentity(targetPath, {
|
|
142
|
+
...options,
|
|
143
|
+
allowSymbolicLink: false,
|
|
144
|
+
}, dependencies);
|
|
145
|
+
const finalMetadata = await lstat(targetPath);
|
|
146
|
+
if (finalMetadata.isSymbolicLink()
|
|
147
|
+
|| identity.device !== admittedDevice
|
|
148
|
+
|| identity.inode !== admittedInode
|
|
149
|
+
|| identity.device !== finalMetadata.dev
|
|
150
|
+
|| identity.inode !== finalMetadata.ino
|
|
151
|
+
|| await realpath(targetPath) !== canonicalTarget) {
|
|
152
|
+
throw new TypeError(`${label} changed while it was being read.`);
|
|
153
|
+
}
|
|
154
|
+
return identity.bytes;
|
|
81
155
|
}
|
|
82
156
|
export { MAX_BOUNDED_FILE_BYTES };
|
package/lib/bounded-json.mjs
CHANGED
|
@@ -36,27 +36,62 @@ export function assertBoundedJsonStructure(value, label = 'JSON value', limits =
|
|
|
36
36
|
throw new TypeError(`${label} contains a non-JSON value.`);
|
|
37
37
|
if (ancestors.has(current))
|
|
38
38
|
throw new TypeError(`${label} contains a cyclic object reference.`);
|
|
39
|
+
let prototype;
|
|
40
|
+
let ownKeys;
|
|
41
|
+
let descriptors;
|
|
42
|
+
try {
|
|
43
|
+
prototype = Object.getPrototypeOf(current);
|
|
44
|
+
ownKeys = Reflect.ownKeys(current);
|
|
45
|
+
descriptors = Object.getOwnPropertyDescriptors(current);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new TypeError(`${label} could not be inspected safely.`);
|
|
49
|
+
}
|
|
50
|
+
if (ownKeys.some((key) => typeof key !== 'string')) {
|
|
51
|
+
throw new TypeError(`${label} contains a symbol key.`);
|
|
52
|
+
}
|
|
53
|
+
if (ownKeys.some((key) => {
|
|
54
|
+
const descriptor = descriptors[String(key)];
|
|
55
|
+
return !descriptor || !Object.hasOwn(descriptor, 'value');
|
|
56
|
+
})) {
|
|
57
|
+
throw new TypeError(`${label} contains an accessor property.`);
|
|
58
|
+
}
|
|
39
59
|
ancestors.add(current);
|
|
40
60
|
if (Array.isArray(current)) {
|
|
41
|
-
if (
|
|
61
|
+
if (prototype !== Array.prototype) {
|
|
62
|
+
throw new TypeError(`${label} contains an array with a custom prototype.`);
|
|
63
|
+
}
|
|
64
|
+
const length = descriptors.length?.value;
|
|
65
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > maximumContainerItems) {
|
|
42
66
|
throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
|
|
43
67
|
}
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
const indexes = ownKeys.filter((key) => key !== 'length');
|
|
69
|
+
if (indexes.length !== length
|
|
70
|
+
|| indexes.some((key, index) => key !== String(index) || descriptors[key]?.enumerable !== true)) {
|
|
71
|
+
throw new TypeError(`${label} contains a sparse, extended, or non-enumerable array.`);
|
|
72
|
+
}
|
|
73
|
+
for (const key of indexes)
|
|
74
|
+
visit(descriptors[key].value, depth + 1);
|
|
46
75
|
}
|
|
47
76
|
else {
|
|
48
|
-
|
|
49
|
-
|
|
77
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
78
|
+
throw new TypeError(`${label} contains an object with a custom prototype.`);
|
|
79
|
+
}
|
|
80
|
+
const objectKeys = ownKeys;
|
|
81
|
+
if (objectKeys.length > maximumContainerItems) {
|
|
50
82
|
throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
|
|
51
83
|
}
|
|
52
|
-
|
|
84
|
+
if (objectKeys.some((key) => descriptors[key]?.enumerable !== true)) {
|
|
85
|
+
throw new TypeError(`${label} contains a non-enumerable object property.`);
|
|
86
|
+
}
|
|
87
|
+
keys += objectKeys.length;
|
|
53
88
|
if (keys > maximumKeys) {
|
|
54
89
|
throw new TypeError(`${label} exceeds the ${maximumKeys}-key limit.`);
|
|
55
90
|
}
|
|
56
|
-
for (const
|
|
91
|
+
for (const key of objectKeys) {
|
|
57
92
|
if (!isSafeJsonObjectKey(key))
|
|
58
93
|
throw new TypeError(`${label} contains an unsafe object key.`);
|
|
59
|
-
visit(
|
|
94
|
+
visit(descriptors[key].value, depth + 1);
|
|
60
95
|
}
|
|
61
96
|
}
|
|
62
97
|
ancestors.delete(current);
|
|
@@ -226,7 +261,7 @@ export function scanBoundedJson(raw, limits = {}) {
|
|
|
226
261
|
if (index !== raw.length)
|
|
227
262
|
syntaxError();
|
|
228
263
|
}
|
|
229
|
-
export function
|
|
264
|
+
export function parseBoundedJson(raw, options) {
|
|
230
265
|
const label = options.label ?? 'Artefact input';
|
|
231
266
|
if (typeof raw !== 'string')
|
|
232
267
|
throw new TypeError(`${label} must be UTF-8 JSON text.`);
|
|
@@ -234,14 +269,17 @@ export function parseBoundedJsonObject(raw, options) {
|
|
|
234
269
|
if (bytes < 1 || bytes > options.maximumBytes) {
|
|
235
270
|
throw new TypeError(`${label} must be between 1 byte and ${options.maximumBytes} bytes.`);
|
|
236
271
|
}
|
|
237
|
-
scanBoundedJson(raw);
|
|
238
|
-
let parsed;
|
|
272
|
+
scanBoundedJson(raw, options.limits);
|
|
239
273
|
try {
|
|
240
|
-
|
|
274
|
+
return JSON.parse(raw);
|
|
241
275
|
}
|
|
242
276
|
catch {
|
|
243
|
-
syntaxError();
|
|
277
|
+
return syntaxError();
|
|
244
278
|
}
|
|
279
|
+
}
|
|
280
|
+
export function parseBoundedJsonObject(raw, options) {
|
|
281
|
+
const label = options.label ?? 'Artefact input';
|
|
282
|
+
const parsed = parseBoundedJson(raw, options);
|
|
245
283
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
246
284
|
throw new TypeError(`${label} must contain one JSON object.`);
|
|
247
285
|
}
|
|
@@ -7,7 +7,7 @@ import { createHash } from 'node:crypto';
|
|
|
7
7
|
import { Worker } from 'node:worker_threads';
|
|
8
8
|
import { RETIRE_BROWSER_CATALOG } from "./generated/retire-browser-catalog.mjs";
|
|
9
9
|
import { CISA_KEV_CATALOG } from "./generated/cisa-kev-catalog.mjs";
|
|
10
|
-
import { createObservation } from "
|
|
10
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
11
11
|
import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
12
12
|
// Catalogue expressions are reviewed third-party inputs. Keep every individual
|
|
13
13
|
// regular-expression evaluation and the aggregate inline-signature work small
|
package/lib/classify.mjs
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// original input hostname and the resolved registrable domain are returned so
|
|
15
15
|
// callers can look up the latter while still showing the user what they typed.
|
|
16
16
|
import { isIP } from 'node:net';
|
|
17
|
+
import { domainToASCII } from 'node:url';
|
|
17
18
|
import { parse } from 'tldts';
|
|
19
|
+
import { canonicalRegistrableDomain } from "./registrable-domain.mjs";
|
|
18
20
|
// A CR/LF here would let one query become multiple lines once it reaches
|
|
19
21
|
// lib/whois.mts's raw `socket.write(query + '\r\n')` - a normal URL-encoded
|
|
20
22
|
// query string (?q=example.com%0D%0AHELP) decodes back into literal control
|
|
@@ -24,6 +26,9 @@ const CONTROL_CHAR_RE = /[\x00-\x1f\x7f]/;
|
|
|
24
26
|
// 32-bit ASN ceiling (RFC 6793 four-octet ASNs). AS0 and AS4294967295 are
|
|
25
27
|
// reserved but syntactically valid; anything above is not an ASN at all.
|
|
26
28
|
const MAX_ASN = 4294967295;
|
|
29
|
+
const MAX_DIRECT_LOOKUP_TARGET_LENGTH = 1024;
|
|
30
|
+
const DIRECT_LOOKUP_RESERVED_SUFFIXES = new Set(['example', 'test']);
|
|
31
|
+
const DIRECT_LOOKUP_EXCLUDED_PUBLIC_SUFFIXES = new Set(['onion']);
|
|
27
32
|
// A registrable-domain label: LDH (letter/digit/hyphen), 1-63 characters, no
|
|
28
33
|
// leading or trailing hyphen. tldts already rejects empty labels, over-long
|
|
29
34
|
// labels, leading/trailing hyphens, and public-suffix-only inputs (all yield
|
|
@@ -33,6 +38,52 @@ const MAX_ASN = 4294967295;
|
|
|
33
38
|
function isValidRegistrableLabel(label) {
|
|
34
39
|
return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label);
|
|
35
40
|
}
|
|
41
|
+
// Direct CLI shorthand is deliberately narrower than normal query
|
|
42
|
+
// classification. classifyQuery() accepts pasted URLs and other convenient
|
|
43
|
+
// input forms after normalisation, while a value occupying command position
|
|
44
|
+
// must be unambiguously a target before it may select the networked Lookup
|
|
45
|
+
// command. This predicate therefore accepts only a complete hostname, literal
|
|
46
|
+
// IP address, or bounded ASN and never attempts to repair URL-like input.
|
|
47
|
+
function isDirectLookupTarget(raw) {
|
|
48
|
+
if (typeof raw !== 'string'
|
|
49
|
+
|| !raw
|
|
50
|
+
|| raw.length > MAX_DIRECT_LOOKUP_TARGET_LENGTH
|
|
51
|
+
|| raw.startsWith('-')
|
|
52
|
+
|| raw.trim() !== raw
|
|
53
|
+
|| /\s/u.test(raw)
|
|
54
|
+
|| CONTROL_CHAR_RE.test(raw)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (isIP(raw) !== 0)
|
|
58
|
+
return !raw.includes('%') && !raw.startsWith('[');
|
|
59
|
+
const asnMatch = raw.match(/^(?:AS)?(\d+)$/i);
|
|
60
|
+
if (asnMatch) {
|
|
61
|
+
const number = Number(asnMatch[1]);
|
|
62
|
+
return Number.isInteger(number) && number >= 0 && number <= MAX_ASN;
|
|
63
|
+
}
|
|
64
|
+
if (/[\\/@:?#%\[\]]/u.test(raw) || raw.endsWith('..'))
|
|
65
|
+
return false;
|
|
66
|
+
const hostname = raw.endsWith('.') ? raw.slice(0, -1) : raw;
|
|
67
|
+
if (!hostname.includes('.'))
|
|
68
|
+
return false;
|
|
69
|
+
const ascii = domainToASCII(hostname).toLowerCase();
|
|
70
|
+
if (!ascii || ascii.length > 253 || !ascii.split('.').every(isValidRegistrableLabel))
|
|
71
|
+
return false;
|
|
72
|
+
const parsed = parse(ascii);
|
|
73
|
+
const publicSuffix = parsed.publicSuffix || '';
|
|
74
|
+
if (DIRECT_LOOKUP_EXCLUDED_PUBLIC_SUFFIXES.has(publicSuffix)
|
|
75
|
+
|| publicSuffix === 'arpa'
|
|
76
|
+
|| publicSuffix.endsWith('.arpa'))
|
|
77
|
+
return false;
|
|
78
|
+
if (parsed.isIcann !== true && !DIRECT_LOOKUP_RESERVED_SUFFIXES.has(publicSuffix))
|
|
79
|
+
return false;
|
|
80
|
+
try {
|
|
81
|
+
return classifyQuery(raw).type === 'domain';
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
36
87
|
function classifyQuery(raw) {
|
|
37
88
|
const trimmed = raw.trim();
|
|
38
89
|
if (CONTROL_CHAR_RE.test(trimmed)) {
|
|
@@ -75,13 +126,13 @@ function classifyQuery(raw) {
|
|
|
75
126
|
if (!inputHostname || !inputHostname.includes('.') || inputHostname.length > 253) {
|
|
76
127
|
throw new Error(`"${trimmed}" is not a valid domain, IP, or ASN.`);
|
|
77
128
|
}
|
|
78
|
-
|
|
129
|
+
if (!inputHostname.split('.').every(isValidRegistrableLabel)) {
|
|
130
|
+
throw new Error(`"${trimmed}" contains an invalid domain label (underscores, empty, or malformed labels are not registrable).`);
|
|
131
|
+
}
|
|
132
|
+
const registrableDomain = canonicalRegistrableDomain(inputHostname);
|
|
79
133
|
if (!registrableDomain) {
|
|
80
134
|
throw new Error(`"${trimmed}" is not a registrable domain (no public-suffix match).`);
|
|
81
135
|
}
|
|
82
|
-
if (!registrableDomain.split('.').every(isValidRegistrableLabel)) {
|
|
83
|
-
throw new Error(`"${trimmed}" contains an invalid domain label (underscores, empty, or malformed labels are not registrable).`);
|
|
84
|
-
}
|
|
85
136
|
return {
|
|
86
137
|
type: 'domain',
|
|
87
138
|
value: registrableDomain,
|
|
@@ -90,4 +141,4 @@ function classifyQuery(raw) {
|
|
|
90
141
|
isSubdomain: inputHostname !== registrableDomain,
|
|
91
142
|
};
|
|
92
143
|
}
|
|
93
|
-
export { classifyQuery, MAX_ASN };
|
|
144
|
+
export { classifyQuery, isDirectLookupTarget, MAX_ASN };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// HTML tokenizer pass. Scripts are never fetched or executed. Only fixed
|
|
3
3
|
// indicator identifiers, descriptions, evidence classes, and bounded counts
|
|
4
4
|
// leave this module; script references and contents are discarded.
|
|
5
|
-
import { createObservation } from "
|
|
5
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
6
6
|
import { analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
7
7
|
const CLIENT_BEHAVIOR_PROFILE_VERSION = 1;
|
|
8
8
|
const MAX_CLIENT_BEHAVIOR_INDICATORS = 12;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// shared tokenizer projects only fixed categories and bounded counts; field
|
|
4
4
|
// names, values, labels, placeholders, arbitrary attributes, and action URLs
|
|
5
5
|
// never enter this profile.
|
|
6
|
-
import { createObservation } from "
|
|
6
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
7
7
|
import { MAX_STATIC_FORMS, MAX_STATIC_INPUTS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
8
8
|
const CREDENTIAL_SURFACE_PROFILE_VERSION = 1;
|
|
9
9
|
function analyzeCredentialSurfaceProfile(input = {}) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { validateDnssecEvidence } from "./dnssec-evidence-validation.mjs";
|
|
2
2
|
import { reviewRpkiRoute } from "./rpki-evidence.mjs";
|
|
3
3
|
import { analyzeTlsaEvidence } from "./tlsa-evidence.mjs";
|
|
4
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
4
5
|
const CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA = 'whoisleuth.cryptographic-assurance.input';
|
|
5
6
|
const CRYPTOGRAPHIC_ASSURANCE_SCHEMA = 'whoisleuth.cryptographic-assurance.review';
|
|
6
|
-
const
|
|
7
|
+
const CRYPTOGRAPHIC_ASSURANCE_INPUT_VERSION = 1;
|
|
8
|
+
const CRYPTOGRAPHIC_ASSURANCE_REVIEW_VERSION = 2;
|
|
7
9
|
function exactRecord(value, keys, label) {
|
|
8
10
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
9
11
|
throw new TypeError(`${label} must be one object.`);
|
|
@@ -22,10 +24,11 @@ function sourceLabel(value, label) {
|
|
|
22
24
|
return normalized;
|
|
23
25
|
}
|
|
24
26
|
function observationTime(value, label) {
|
|
25
|
-
|
|
27
|
+
const normalized = normalizeExplicitIsoTimestamp(value);
|
|
28
|
+
if (!normalized) {
|
|
26
29
|
throw new TypeError(`${label}.observedAt must be one ISO-compatible timestamp.`);
|
|
27
30
|
}
|
|
28
|
-
return
|
|
31
|
+
return normalized;
|
|
29
32
|
}
|
|
30
33
|
function suppliedEvidence(value, label) {
|
|
31
34
|
if (value === null || value === undefined)
|
|
@@ -59,7 +62,16 @@ function dnssecCompleteness(report) {
|
|
|
59
62
|
: report.state === 'invalid' ? 'unavailable' : 'partial';
|
|
60
63
|
}
|
|
61
64
|
function rpkiCompleteness(report) {
|
|
62
|
-
return report.state === '
|
|
65
|
+
return report.state === 'invalid_input' ? 'unavailable' : 'partial';
|
|
66
|
+
}
|
|
67
|
+
function rpkiCardState(report) {
|
|
68
|
+
if (report.state === 'valid')
|
|
69
|
+
return 'matches_supplied_rows';
|
|
70
|
+
if (report.state === 'invalid')
|
|
71
|
+
return 'differs_from_supplied_rows';
|
|
72
|
+
if (report.state === 'not_found')
|
|
73
|
+
return 'not_found_in_supplied_rows';
|
|
74
|
+
return report.state;
|
|
63
75
|
}
|
|
64
76
|
function tlsaCompleteness(report) {
|
|
65
77
|
const comparisonMaterialComplete = !report.truncated
|
|
@@ -74,8 +86,8 @@ function tlsaCompleteness(report) {
|
|
|
74
86
|
}
|
|
75
87
|
function buildCryptographicAssuranceReview(inputValue, generatedAt = new Date().toISOString()) {
|
|
76
88
|
const input = exactRecord(inputValue, new Set(['schema', 'version', 'dnssec', 'routeOrigin', 'tlsa']), 'Cryptographic assurance input');
|
|
77
|
-
if (input.schema !== CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA || input.version !==
|
|
78
|
-
throw new TypeError(`Cryptographic assurance input must use ${CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA} version ${
|
|
89
|
+
if (input.schema !== CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA || input.version !== CRYPTOGRAPHIC_ASSURANCE_INPUT_VERSION) {
|
|
90
|
+
throw new TypeError(`Cryptographic assurance input must use ${CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA} version ${CRYPTOGRAPHIC_ASSURANCE_INPUT_VERSION}.`);
|
|
79
91
|
}
|
|
80
92
|
const dnssecInput = suppliedEvidence(input.dnssec, 'dnssec');
|
|
81
93
|
const routeInput = suppliedEvidence(input.routeOrigin, 'routeOrigin');
|
|
@@ -97,8 +109,8 @@ function buildCryptographicAssuranceReview(inputValue, generatedAt = new Date().
|
|
|
97
109
|
}) : null;
|
|
98
110
|
const tlsa = tlsaInput ? analyzeTlsaEvidence({
|
|
99
111
|
serviceName: tlsaInput.evidence.serviceName,
|
|
100
|
-
dnssecState:
|
|
101
|
-
pkixValidationState:
|
|
112
|
+
dnssecState: 'unavailable',
|
|
113
|
+
pkixValidationState: 'unavailable',
|
|
102
114
|
records: tlsaInput.evidence.records,
|
|
103
115
|
certificateDerBase64: tlsaInput.evidence.certificateDerBase64,
|
|
104
116
|
spkiDerBase64: tlsaInput.evidence.spkiDerBase64,
|
|
@@ -119,29 +131,35 @@ function buildCryptographicAssuranceReview(inputValue, generatedAt = new Date().
|
|
|
119
131
|
routeOrigin && routeInput ? Object.freeze({
|
|
120
132
|
family: 'route_origin_authorisation',
|
|
121
133
|
label: 'Route-origin authorisation',
|
|
122
|
-
authority: 'Analyst-supplied
|
|
134
|
+
authority: 'Analyst-supplied route-origin rows',
|
|
123
135
|
source: routeInput.source,
|
|
124
136
|
observedAt: routeInput.observedAt,
|
|
125
|
-
state: routeOrigin
|
|
137
|
+
state: rpkiCardState(routeOrigin),
|
|
126
138
|
completeness: rpkiCompleteness(routeOrigin),
|
|
127
139
|
result: routeOrigin,
|
|
128
|
-
limitations:
|
|
129
|
-
|
|
140
|
+
limitations: Object.freeze([
|
|
141
|
+
...routeOrigin.limitations,
|
|
142
|
+
'The supplied rows do not include bounded validator provenance, implementation identity, validation time, or a complete authenticated snapshot, so this card reports only their local comparison and remains partial.',
|
|
143
|
+
]),
|
|
144
|
+
}) : unavailableCard('route_origin_authorisation', 'Route-origin authorisation', 'Analyst-supplied route-origin rows'),
|
|
130
145
|
tlsa && tlsaInput ? Object.freeze({
|
|
131
146
|
family: 'dane_tlsa',
|
|
132
147
|
label: 'DANE and TLSA evidence review',
|
|
133
|
-
authority: 'Supplied TLSA
|
|
148
|
+
authority: 'Supplied TLSA and certificate association material',
|
|
134
149
|
source: tlsaInput.source,
|
|
135
150
|
observedAt: tlsaInput.observedAt,
|
|
136
151
|
state: tlsa.state,
|
|
137
152
|
completeness: tlsaCompleteness(tlsa),
|
|
138
153
|
result: tlsa,
|
|
139
|
-
limitations:
|
|
140
|
-
|
|
154
|
+
limitations: Object.freeze([
|
|
155
|
+
...tlsa.limitations,
|
|
156
|
+
'Caller-provided DNSSEC and PKIX state labels are not accepted as validator evidence. Positive DANE state requires separately validated prerequisite artefacts from an explicit collector.',
|
|
157
|
+
]),
|
|
158
|
+
}) : unavailableCard('dane_tlsa', 'DANE and TLSA evidence review', 'Supplied TLSA and certificate association material'),
|
|
141
159
|
];
|
|
142
160
|
return Object.freeze({
|
|
143
161
|
schema: CRYPTOGRAPHIC_ASSURANCE_SCHEMA,
|
|
144
|
-
version:
|
|
162
|
+
version: CRYPTOGRAPHIC_ASSURANCE_REVIEW_VERSION,
|
|
145
163
|
generatedAt: observationTime(generatedAt, 'generatedAt'),
|
|
146
164
|
cards: Object.freeze(cards),
|
|
147
165
|
combinedState: null,
|
|
@@ -152,4 +170,4 @@ function buildCryptographicAssuranceReview(inputValue, generatedAt = new Date().
|
|
|
152
170
|
]),
|
|
153
171
|
});
|
|
154
172
|
}
|
|
155
|
-
export { CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA,
|
|
173
|
+
export { CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA, CRYPTOGRAPHIC_ASSURANCE_INPUT_VERSION, CRYPTOGRAPHIC_ASSURANCE_REVIEW_VERSION, CRYPTOGRAPHIC_ASSURANCE_SCHEMA, buildCryptographicAssuranceReview, };
|
package/lib/ct-query.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const MAX_CT_QUERY_LENGTH = 200;
|
|
2
2
|
const CT_QUERY_ERROR_CODE = 'INVALID_CT_QUERY';
|
|
3
|
-
const CT_QUERY_ERROR_MESSAGE = `Certificate Transparency query must be at most ${MAX_CT_QUERY_LENGTH} characters and contain no control characters.`;
|
|
4
|
-
const
|
|
3
|
+
const CT_QUERY_ERROR_MESSAGE = `Certificate Transparency query must be at most ${MAX_CT_QUERY_LENGTH} characters and contain no control or invisible formatting characters.`;
|
|
4
|
+
const UNSAFE_QUERY_CHARACTER_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
|
|
5
5
|
class CtQueryError extends Error {
|
|
6
6
|
code = CT_QUERY_ERROR_CODE;
|
|
7
7
|
constructor() {
|
|
@@ -14,7 +14,7 @@ function normalizeCtQuery(value) {
|
|
|
14
14
|
return '';
|
|
15
15
|
if (typeof value !== 'string'
|
|
16
16
|
|| value.length > MAX_CT_QUERY_LENGTH
|
|
17
|
-
||
|
|
17
|
+
|| UNSAFE_QUERY_CHARACTER_RE.test(value)) {
|
|
18
18
|
throw new CtQueryError();
|
|
19
19
|
}
|
|
20
20
|
return value.trim();
|