@slicedearth/whoisleuth-cli 2.0.0 → 2.1.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/cli/arguments.mjs +21 -5
- package/cli/artifact-validation/case-response.mjs +771 -0
- package/cli/artifact-validation/investigation-capsule.mjs +426 -0
- package/cli/artifact-validation/lookup-evidence.mjs +718 -0
- package/cli/artifact-validation/signed-review.mjs +598 -0
- package/cli/artifact-validation/structure-primitives.mjs +105 -0
- package/cli/artifact-verify.mjs +1 -1
- package/cli/assurance-command-runner.mjs +324 -0
- package/cli/command-reference.mjs +1175 -843
- package/cli/completion.mjs +56 -9
- package/cli/discriminated-command-handlers.mjs +11 -0
- package/cli/doctor.mjs +2 -2
- package/cli/evidence-command-runner.mjs +41 -31
- package/cli/export-evidence.mjs +1 -1
- package/cli/fail-policy.mjs +33 -8
- package/cli/formatters/terminal-command-formats.mjs +437 -0
- package/cli/formatters/terminal-lookup.mjs +758 -0
- package/cli/formatters/terminal-metadata.mjs +58 -0
- package/cli/formatters/terminal-shared.mjs +83 -0
- package/cli/formatters/terminal.mjs +5 -1232
- package/cli/history-command-runner.mjs +132 -0
- package/cli/inline-command-families.mjs +45 -0
- package/cli/inline-command-runner.mjs +55 -0
- package/cli/investigation-plan.mjs +9 -2
- package/cli/investigation-run.mjs +114 -9
- package/cli/lookup-brief.mjs +141 -23
- package/cli/lookup-browser-view.mjs +289 -0
- package/cli/lookup-browser.mjs +3 -288
- package/cli/mail-review.mjs +2 -6
- package/cli/offline-artifact-validation.mjs +19 -0
- package/cli/review-command-runner.mjs +269 -0
- package/cli/runner.mjs +15 -972
- package/cli/saved-lookup.mjs +19 -2
- package/cli/support-command-runner.mjs +229 -0
- package/cli/workflow-command-runner.mjs +129 -0
- package/docs/cli-reference.md +17 -10
- package/docs/cli.md +5 -3
- package/lib/availability.mjs +12 -9
- package/lib/bounded-contract-normalizers.mjs +2 -9
- package/lib/browser-library-profile.mjs +1 -2
- package/lib/client-behavior-profile.mjs +1 -2
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/domain-posture-analysis.mjs +10 -1
- package/lib/domain-posture.mjs +51 -23
- package/lib/evidence-export.mjs +46 -5
- package/lib/evidence-report-markdown.mjs +4 -0
- package/lib/evidence-report.mjs +50 -0
- package/lib/generated/cisa-kev-catalog.mjs +34 -4
- package/lib/generated/registrar-standing-catalogue.mjs +367 -0
- package/lib/json-record.mjs +12 -0
- package/lib/lookup-child-profile-contract.mjs +858 -0
- package/lib/lookup-contract-primitives.mjs +98 -0
- package/lib/lookup-diagnostics.mjs +96 -0
- package/lib/lookup-network-evidence-bounds.mjs +1 -0
- package/lib/lookup-response.mjs +367 -0
- package/lib/lookup.mjs +28 -443
- package/lib/page-role-profile.mjs +1 -3
- package/lib/project-metadata.mjs +1 -1
- package/lib/registrar-standing-catalogue-contract.mjs +70 -0
- package/lib/registrar-standing-contract.mjs +300 -0
- package/lib/registrar-standing.mjs +269 -0
- package/lib/registry-insights.mjs +2 -8
- package/lib/risk-scoring.mjs +26 -10
- package/lib/sslbl-certificates.generated.mjs +138 -136
- package/lib/structured-data-identity.mjs +1 -3
- package/lib/technology-evidence-role.mjs +51 -0
- package/lib/threat-intelligence-runtime.mjs +3 -3
- package/lib/threat-intelligence-types.mjs +13 -0
- package/lib/threatfox-intelligence.mjs +1 -3
- package/lib/tls-intelligence.mjs +1 -2
- package/lib/urlhaus-intelligence.mjs +1 -3
- package/lib/urlscan-intelligence.mjs +1 -3
- package/lib/website-security-posture.mjs +1 -2
- package/lib/website-technology.mjs +72 -23
- package/lib/whois-common-formats.mjs +276 -0
- package/lib/whois-dialects.mjs +534 -0
- package/lib/whois-parser-support.mjs +305 -0
- package/lib/whois-parser.mjs +135 -0
- package/lib/whois.mjs +6 -1187
- package/package.json +3 -3
- package/packages/cases/case-evidence-model.mjs +9 -0
- package/packages/cases/case-migration-model.mjs +17 -11
- package/packages/cases/case-response-model.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +297 -112
- package/packages/comparison/page-similarity.mjs +23 -0
- package/packages/contracts/case-portability.mjs +227 -63
- package/packages/contracts/cli-lookup.mjs +4 -3
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/investigation-portability.mjs +1 -1
- package/packages/contracts/risk-calibration.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +34 -6
- package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
- package/packages/investigation/lookup-claim-readiness.mjs +2 -2
- package/packages/workspace/brand-profile-model.mjs +1 -1
- package/packages/workspace/website-snapshot-model.mjs +79 -6
- package/third-party-notices.txt +1 -1
- package/cli/artifact-structure.mjs +0 -2507
package/lib/domain-posture.mjs
CHANGED
|
@@ -62,15 +62,18 @@ function normalizeDkimSelectors(rawSelectors) {
|
|
|
62
62
|
function normalizeMailProtectionProfile(value) {
|
|
63
63
|
return value === 'defensive_no_mail' || value === 'parked' ? value : 'standard';
|
|
64
64
|
}
|
|
65
|
-
function withTimeout(promise, label, timeoutMs = DNS_TIMEOUT_MS
|
|
65
|
+
function withTimeout(promise, label, timeoutMs = DNS_TIMEOUT_MS, timers = {
|
|
66
|
+
setTimer: (callback, milliseconds) => setTimeout(callback, milliseconds),
|
|
67
|
+
clearTimer: (handle) => clearTimeout(handle),
|
|
68
|
+
}) {
|
|
66
69
|
return new Promise((resolve, reject) => {
|
|
67
|
-
const timeout =
|
|
68
|
-
promise.then((value) => {
|
|
70
|
+
const timeout = timers.setTimer(() => reject(new Error(`${label} timed out`)), timeoutMs);
|
|
71
|
+
promise.then((value) => { timers.clearTimer(timeout); resolve(value); }, (err) => { timers.clearTimer(timeout); reject(err); });
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
|
-
async function resolveDns(label, factory, timeoutMs = DNS_TIMEOUT_MS) {
|
|
74
|
+
async function resolveDns(label, factory, timeoutMs = DNS_TIMEOUT_MS, timers) {
|
|
72
75
|
try {
|
|
73
|
-
return { records: await withTimeout(factory(), label, timeoutMs), error: null };
|
|
76
|
+
return { records: await withTimeout(factory(), label, timeoutMs, timers), error: null };
|
|
74
77
|
}
|
|
75
78
|
catch (err) {
|
|
76
79
|
const error = errorRecord(err);
|
|
@@ -559,7 +562,10 @@ function registrationLockCheck(registry) {
|
|
|
559
562
|
function nameserverCheck(query, registry) {
|
|
560
563
|
if (query.error)
|
|
561
564
|
return queryFailureCheck('nameservers', 'Nameserver delegation', query.error);
|
|
562
|
-
const records = query.records
|
|
565
|
+
const records = [...new Set(query.records
|
|
566
|
+
.map((record) => String(record || '').toLowerCase().replace(/\.+$/u, ''))
|
|
567
|
+
.filter(Boolean))]
|
|
568
|
+
.sort((left, right) => left.localeCompare(right));
|
|
563
569
|
if (records.length === 0) {
|
|
564
570
|
return check('nameservers', 'Nameserver delegation', 'warning', 'No nameserver delegation returned', {
|
|
565
571
|
detail: 'A missing answer may reflect resolver or publication state and is not proof that a provider account or zone is absent.',
|
|
@@ -639,7 +645,17 @@ async function fetchMtaStsPolicy(domain, fetcher = safeFetch) {
|
|
|
639
645
|
clearTimeout(timeout);
|
|
640
646
|
}
|
|
641
647
|
}
|
|
642
|
-
async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelectors = [], mailProtectionProfile = 'standard', } = {}
|
|
648
|
+
async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelectors = [], mailProtectionProfile = 'standard', } = {}, dependencies = {
|
|
649
|
+
resolveTxt: (name) => dns.resolveTxt(name),
|
|
650
|
+
resolveMx: (name) => dns.resolveMx(name),
|
|
651
|
+
resolveNs: (name) => dns.resolveNs(name),
|
|
652
|
+
resolveCaa: (name) => dns.resolveCaa(name),
|
|
653
|
+
fetchRdapRecord,
|
|
654
|
+
fetchMtaStsPolicy: (name) => fetchMtaStsPolicy(name),
|
|
655
|
+
now: () => new Date(),
|
|
656
|
+
setTimer: (callback, milliseconds) => setTimeout(callback, milliseconds),
|
|
657
|
+
clearTimer: (handle) => clearTimeout(handle),
|
|
658
|
+
}) {
|
|
643
659
|
const normalizedDomain = normalizeAuditDomain(domain);
|
|
644
660
|
if (!normalizedDomain)
|
|
645
661
|
throw new Error('Invalid domain name for posture audit.');
|
|
@@ -650,40 +666,48 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
|
|
|
650
666
|
.slice(0, Math.max(0, MAX_DKIM_SELECTORS - selectors.length));
|
|
651
667
|
const normalizedMailProfile = normalizeMailProtectionProfile(mailProtectionProfile);
|
|
652
668
|
const [spf, dmarc, mx, nameservers, caa, mtaStsDns, tlsRpt, bimi, dkim, rdap] = await Promise.all([
|
|
653
|
-
resolveDns(`TXT ${domain}`, () =>
|
|
654
|
-
resolveDns(`TXT _dmarc.${domain}`, () =>
|
|
655
|
-
resolveDns(`MX ${domain}`, () =>
|
|
656
|
-
resolveDns(`NS ${domain}`, () =>
|
|
657
|
-
resolveDns(`CAA ${domain}`, () =>
|
|
658
|
-
resolveDns(`TXT _mta-sts.${domain}`, () =>
|
|
659
|
-
resolveDns(`TXT _smtp._tls.${domain}`, () =>
|
|
660
|
-
resolveDns(`TXT default._bimi.${domain}`, () =>
|
|
669
|
+
resolveDns(`TXT ${domain}`, () => dependencies.resolveTxt(domain), DNS_TIMEOUT_MS, dependencies),
|
|
670
|
+
resolveDns(`TXT _dmarc.${domain}`, () => dependencies.resolveTxt(`_dmarc.${domain}`), DNS_TIMEOUT_MS, dependencies),
|
|
671
|
+
resolveDns(`MX ${domain}`, () => dependencies.resolveMx(domain), DNS_TIMEOUT_MS, dependencies),
|
|
672
|
+
resolveDns(`NS ${domain}`, () => dependencies.resolveNs(domain), DNS_TIMEOUT_MS, dependencies),
|
|
673
|
+
resolveDns(`CAA ${domain}`, () => dependencies.resolveCaa(domain), DNS_TIMEOUT_MS, dependencies),
|
|
674
|
+
resolveDns(`TXT _mta-sts.${domain}`, () => dependencies.resolveTxt(`_mta-sts.${domain}`), DNS_TIMEOUT_MS, dependencies),
|
|
675
|
+
resolveDns(`TXT _smtp._tls.${domain}`, () => dependencies.resolveTxt(`_smtp._tls.${domain}`), DNS_TIMEOUT_MS, dependencies),
|
|
676
|
+
resolveDns(`TXT default._bimi.${domain}`, () => dependencies.resolveTxt(`default._bimi.${domain}`), DNS_TIMEOUT_MS, dependencies),
|
|
661
677
|
Promise.all([
|
|
662
678
|
...selectors.map(async (selector) => ({
|
|
663
679
|
selector,
|
|
664
680
|
retired: false,
|
|
665
|
-
...await resolveDns(`TXT ${selector}._domainkey.${domain}`, () =>
|
|
681
|
+
...await resolveDns(`TXT ${selector}._domainkey.${domain}`, () => dependencies.resolveTxt(`${selector}._domainkey.${domain}`), DNS_TIMEOUT_MS, dependencies),
|
|
666
682
|
})),
|
|
667
683
|
...retiredSelectors.map(async (selector) => ({
|
|
668
684
|
selector,
|
|
669
685
|
retired: true,
|
|
670
|
-
...await resolveDns(`TXT ${selector}._domainkey.${domain}`, () =>
|
|
686
|
+
...await resolveDns(`TXT ${selector}._domainkey.${domain}`, () => dependencies.resolveTxt(`${selector}._domainkey.${domain}`), DNS_TIMEOUT_MS, dependencies),
|
|
671
687
|
})),
|
|
672
688
|
]),
|
|
673
|
-
fetchRdapRecord('domain', domain).catch((err) => ({
|
|
689
|
+
dependencies.fetchRdapRecord('domain', domain).catch((err) => ({
|
|
674
690
|
error: nonEmptyErrorMessage(err, String(err)),
|
|
675
691
|
})),
|
|
676
692
|
]);
|
|
677
693
|
const parsedMtaDns = mtaStsDns.error ? null : parseMtaStsDnsRecords(mtaStsDns.records);
|
|
678
|
-
const
|
|
694
|
+
const enrichmentStartedAt = dependencies.now();
|
|
695
|
+
if (!(enrichmentStartedAt instanceof Date) || !Number.isFinite(enrichmentStartedAt.getTime())) {
|
|
696
|
+
throw new TypeError('Domain-posture collection time must be valid.');
|
|
697
|
+
}
|
|
698
|
+
const enrichmentDeadline = enrichmentStartedAt.getTime() + POSTURE_ENRICHMENT_DEADLINE_MS;
|
|
679
699
|
const resolveEnrichmentTxt = (name) => {
|
|
680
|
-
const
|
|
700
|
+
const observedAt = dependencies.now();
|
|
701
|
+
if (!(observedAt instanceof Date) || !Number.isFinite(observedAt.getTime())) {
|
|
702
|
+
return Promise.resolve({ records: [], error: 'The bounded posture-enrichment clock was unavailable.' });
|
|
703
|
+
}
|
|
704
|
+
const remaining = Math.max(1, enrichmentDeadline - observedAt.getTime());
|
|
681
705
|
if (remaining <= 1)
|
|
682
706
|
return Promise.resolve({ records: [], error: 'The bounded posture-enrichment deadline was reached.' });
|
|
683
|
-
return resolveDns(`TXT ${name}`, () =>
|
|
707
|
+
return resolveDns(`TXT ${name}`, () => dependencies.resolveTxt(name), Math.min(DNS_TIMEOUT_MS, remaining), dependencies);
|
|
684
708
|
};
|
|
685
709
|
const [mtaStsPolicy, spfExpansion, dmarcAuthorizations] = await Promise.all([
|
|
686
|
-
parsedMtaDns?.valid ? fetchMtaStsPolicy(domain) : Promise.resolve(null),
|
|
710
|
+
parsedMtaDns?.valid ? dependencies.fetchMtaStsPolicy(domain) : Promise.resolve(null),
|
|
687
711
|
expandSpfPolicy(domain, spf, resolveEnrichmentTxt),
|
|
688
712
|
validateDmarcExternalReporting(domain, dmarc, resolveEnrichmentTxt),
|
|
689
713
|
]);
|
|
@@ -735,9 +759,13 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
|
|
|
735
759
|
dmarcAuthorizations,
|
|
736
760
|
mailProtectionProfile: normalizedMailProfile,
|
|
737
761
|
});
|
|
762
|
+
const checkedAt = dependencies.now();
|
|
763
|
+
if (!(checkedAt instanceof Date) || !Number.isFinite(checkedAt.getTime())) {
|
|
764
|
+
throw new TypeError('Domain-posture completion time must be valid.');
|
|
765
|
+
}
|
|
738
766
|
return {
|
|
739
767
|
...report,
|
|
740
|
-
checkedAt:
|
|
768
|
+
checkedAt: checkedAt.toISOString(),
|
|
741
769
|
dkimSelectors: selectors,
|
|
742
770
|
retiredDkimSelectors: retiredSelectors,
|
|
743
771
|
mailProtectionProfile: normalizedMailProfile,
|
package/lib/evidence-export.mjs
CHANGED
|
@@ -6,11 +6,15 @@ import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "./home
|
|
|
6
6
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
7
7
|
import { defineSchemaCompatibility } from "../packages/contracts/schema-compatibility.mjs";
|
|
8
8
|
import { isValidAsciiHostname } from "./hostname.mjs";
|
|
9
|
+
import { REGISTRAR_STANDING_SCHEMA, REGISTRAR_STANDING_VERSION, registrarIanaIds, registrarStandingObservedBy, resolveRegistrarIanaId, validRegistrarStanding, } from "./registrar-standing-contract.mjs";
|
|
9
10
|
export const LOOKUP_EVIDENCE_SCHEMA = 'whoisleuth.lookup-evidence';
|
|
10
|
-
export const
|
|
11
|
-
export const
|
|
11
|
+
export const V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION = 26;
|
|
12
|
+
export const PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION = 27;
|
|
13
|
+
export const PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION = PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION;
|
|
14
|
+
export const LOOKUP_EVIDENCE_SCHEMA_VERSION = 28;
|
|
12
15
|
export const SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS = Object.freeze([
|
|
13
|
-
|
|
16
|
+
V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
17
|
+
PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
14
18
|
LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
15
19
|
]);
|
|
16
20
|
export const LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES = 5 * 1024 * 1024;
|
|
@@ -31,7 +35,7 @@ export const LOOKUP_EVIDENCE_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
31
35
|
writeSemantics: 'read_only',
|
|
32
36
|
byteBudget: LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES,
|
|
33
37
|
owner: 'lib/evidence-export.mts',
|
|
34
|
-
note: 'Exact v1.47.4 version 26
|
|
38
|
+
note: 'Exact v1.47.4 version 26 and published v2.0.1 version 27 remain replayable; version 28 adds a bounded official-source registrar-standing projection.',
|
|
35
39
|
});
|
|
36
40
|
const REGISTRAR_RDAP_STATUSES = new Set([
|
|
37
41
|
'success', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled',
|
|
@@ -113,7 +117,7 @@ const LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS = new Set([
|
|
|
113
117
|
'version', 'profileVersion', 'status', 'state', 'source', 'observedAt',
|
|
114
118
|
'scanMode', 'durationMs', 'complete', 'truncated', 'limitations',
|
|
115
119
|
'diagnostics', 'detail', 'error', 'count', 'total', 'discarded', 'summary',
|
|
116
|
-
'findings', 'id', 'label', 'tone', 'category', 'confidence', 'evidence',
|
|
120
|
+
'findings', 'id', 'label', 'tone', 'category', 'confidence', 'compatibility', 'role', 'roles', 'evidence',
|
|
117
121
|
'value', 'values', 'name', 'type', 'types', 'mode', 'owner', 'target',
|
|
118
122
|
'url', 'uri', 'href',
|
|
119
123
|
'priority', 'ttl', 'records',
|
|
@@ -1513,6 +1517,42 @@ function registrarPublicationComparison(body, registryParsed) {
|
|
|
1513
1517
|
registrarStatus,
|
|
1514
1518
|
});
|
|
1515
1519
|
}
|
|
1520
|
+
function projectRegistrarStanding(value, rdapParsed, whoisParsed, generatedAt) {
|
|
1521
|
+
if (value === undefined || value === null)
|
|
1522
|
+
return null;
|
|
1523
|
+
if (!validRegistrarStanding(value))
|
|
1524
|
+
throw new TypeError('Lookup registrar standing is malformed or unsupported.');
|
|
1525
|
+
if (!registrarStandingObservedBy(value, generatedAt)) {
|
|
1526
|
+
throw new TypeError('Lookup registrar standing was observed after the evidence export time.');
|
|
1527
|
+
}
|
|
1528
|
+
const retainedIanaIds = registrarIanaIds(rdapParsed, whoisParsed);
|
|
1529
|
+
const retainedIanaId = resolveRegistrarIanaId(rdapParsed, whoisParsed);
|
|
1530
|
+
// A partial/error source can leave a valid response-level interpretation
|
|
1531
|
+
// whose identifying publication is deliberately omitted from the portable
|
|
1532
|
+
// export. Do not create an orphaned claim in that case. A contradictory
|
|
1533
|
+
// retained identifier remains a hard failure.
|
|
1534
|
+
if (value.ianaId !== null && retainedIanaIds.length === 0)
|
|
1535
|
+
return null;
|
|
1536
|
+
if (retainedIanaIds.length > 1) {
|
|
1537
|
+
throw new TypeError('Lookup registrar standing cannot be exported with conflicting retained registrar identifiers.');
|
|
1538
|
+
}
|
|
1539
|
+
if (value.ianaId !== retainedIanaId) {
|
|
1540
|
+
throw new TypeError('Lookup registrar standing does not match the retained registration sources.');
|
|
1541
|
+
}
|
|
1542
|
+
return {
|
|
1543
|
+
schema: REGISTRAR_STANDING_SCHEMA,
|
|
1544
|
+
version: REGISTRAR_STANDING_VERSION,
|
|
1545
|
+
ianaId: value.ianaId,
|
|
1546
|
+
accreditation: { ...value.accreditation },
|
|
1547
|
+
compliance: {
|
|
1548
|
+
...value.compliance,
|
|
1549
|
+
actions: value.compliance.actions.map((action) => ({ ...action })),
|
|
1550
|
+
},
|
|
1551
|
+
assessment: { ...value.assessment },
|
|
1552
|
+
limitations: [...value.limitations],
|
|
1553
|
+
nextActions: [...value.nextActions],
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1516
1556
|
export function buildLookupEvidence(response, options = {}) {
|
|
1517
1557
|
assertBoundedJsonStructure(response, 'Lookup response');
|
|
1518
1558
|
const { generatedAt: generatedAtValue = new Date().toISOString(), idnAnalysis = null, applicationVersion = null } = options;
|
|
@@ -1582,6 +1622,7 @@ export function buildLookupEvidence(response, options = {}) {
|
|
|
1582
1622
|
whoisStatus: whoisDiagnostics?.status,
|
|
1583
1623
|
})),
|
|
1584
1624
|
registrarPublicationComparison: projectLookupEvidencePrivacySafeTree(registrarPublicationComparison(body, rdapParsed)),
|
|
1625
|
+
registrarStanding: projectRegistrarStanding(body.registrarStanding, rdapParsed, whoisParsed, generatedAt),
|
|
1585
1626
|
},
|
|
1586
1627
|
};
|
|
1587
1628
|
assertLookupEvidencePortableTree(evidence);
|
|
@@ -31,6 +31,10 @@ function formatLookupEvidenceMarkdown(document, options = {}) {
|
|
|
31
31
|
appendFields(lines, report.assessment);
|
|
32
32
|
lines.push('', '## Registry sources');
|
|
33
33
|
appendGroups(lines, report.registryGroups);
|
|
34
|
+
if (report.registrarStanding.length) {
|
|
35
|
+
lines.push('', '## Registrar standing');
|
|
36
|
+
appendFields(lines, report.registrarStanding);
|
|
37
|
+
}
|
|
34
38
|
if (report.registryInterpretation.length) {
|
|
35
39
|
lines.push('', '## Registry interpretation');
|
|
36
40
|
appendFields(lines, report.registryInterpretation);
|
package/lib/evidence-report.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registryAccessProfileLabel } from "./registry-access.mjs";
|
|
2
2
|
import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "./portable-generator.mjs";
|
|
3
|
+
import { technologyEvidenceRoles, } from "./technology-evidence-role.mjs";
|
|
3
4
|
const MAX_REPORT_VALUE_LENGTH = 300;
|
|
4
5
|
const MAX_REPORT_LIST_ITEMS = 50;
|
|
5
6
|
const MAX_REPORT_COMPARISON_FIELDS = 20;
|
|
@@ -7,6 +8,12 @@ const MAX_REPORT_RISK_FACTORS = 20;
|
|
|
7
8
|
function objectOrEmpty(value) {
|
|
8
9
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
9
10
|
}
|
|
11
|
+
function reportTechnologyNames(findings, role) {
|
|
12
|
+
return listText(findings
|
|
13
|
+
.filter((finding) => technologyEvidenceRoles(finding).includes(role))
|
|
14
|
+
.map((finding) => objectOrEmpty(finding).name)
|
|
15
|
+
.filter((name) => typeof name === 'string' && name.length > 0));
|
|
16
|
+
}
|
|
10
17
|
function cleanReportText(value, fallback = 'Not reported') {
|
|
11
18
|
if (value === null || value === undefined || value === '')
|
|
12
19
|
return fallback;
|
|
@@ -125,6 +132,22 @@ function buildLookupEvidenceReport(document, options = {}) {
|
|
|
125
132
|
const rdapComparisonHealth = objectOrEmpty(comparisonHealth.rdap);
|
|
126
133
|
const whoisComparisonHealth = objectOrEmpty(comparisonHealth.whois);
|
|
127
134
|
const registrarComparison = objectOrEmpty(analysis.registrarPublicationComparison);
|
|
135
|
+
const registrarStanding = objectOrEmpty(analysis.registrarStanding);
|
|
136
|
+
const registrarAccreditation = objectOrEmpty(registrarStanding.accreditation);
|
|
137
|
+
const registrarCompliance = objectOrEmpty(registrarStanding.compliance);
|
|
138
|
+
const registrarStandingAssessment = objectOrEmpty(registrarStanding.assessment);
|
|
139
|
+
const registrarActions = Array.isArray(registrarCompliance.actions)
|
|
140
|
+
? registrarCompliance.actions.slice(0, 5).map(objectOrEmpty)
|
|
141
|
+
: [];
|
|
142
|
+
const registrarNoticeSummary = registrarCompliance.state === 'reviewed_no_match'
|
|
143
|
+
? `None in the reviewed ${cleanReportText(registrarCompliance.catalogueYear, 'current-year')} catalogue`
|
|
144
|
+
: registrarCompliance.state === 'not_applicable'
|
|
145
|
+
? 'Not assessed without one unambiguous registrar IANA ID'
|
|
146
|
+
: registrarCompliance.state === 'stale'
|
|
147
|
+
? 'No matching action retained; the reviewed catalogue is stale'
|
|
148
|
+
: registrarCompliance.state === 'unavailable'
|
|
149
|
+
? 'Unavailable because the retained catalogue could not be validated'
|
|
150
|
+
: 'No matching action was represented';
|
|
128
151
|
const registrarComparisonHealth = objectOrEmpty(registrarComparison.sourceHealth);
|
|
129
152
|
const registryPublicationHealth = objectOrEmpty(registrarComparisonHealth.registry);
|
|
130
153
|
const registrarPublicationHealth = objectOrEmpty(registrarComparisonHealth.registrar);
|
|
@@ -140,6 +163,8 @@ function buildLookupEvidenceReport(document, options = {}) {
|
|
|
140
163
|
const tlsHostname = objectOrEmpty(tls.hostname);
|
|
141
164
|
const tlsValidity = objectOrEmpty(tls.validity);
|
|
142
165
|
const tlsCertificate = objectOrEmpty(tls.certificate);
|
|
166
|
+
const technology = objectOrEmpty(availability.technologyProfile);
|
|
167
|
+
const technologyFindings = Array.isArray(technology.findings) ? technology.findings : [];
|
|
143
168
|
const risk = riskReport(options.risk);
|
|
144
169
|
const titleTarget = query.registrableDomain || query.submitted || 'Unknown domain';
|
|
145
170
|
const registryAccessSuffix = [5, 6, 7, 8].includes(Number(diagnostics.version)) && registryAccess.authority === 'context_only'
|
|
@@ -239,6 +264,12 @@ function buildLookupEvidenceReport(document, options = {}) {
|
|
|
239
264
|
reportField('Redirects', http.redirectCount),
|
|
240
265
|
reportField('Page title', availability.pageTitle),
|
|
241
266
|
reportField('Password field observed', yesNoUnknown(availability.hasPasswordField)),
|
|
267
|
+
reportField('Authoritative nameserver evidence', listText(availability.nameservers)),
|
|
268
|
+
reportField('Observed edge, CDN, reverse proxy or WAF', reportTechnologyNames(technologyFindings, 'observed_edge')),
|
|
269
|
+
reportField('Application-platform indicators', reportTechnologyNames(technologyFindings, 'application_platform')),
|
|
270
|
+
reportField('Framework or runtime indicators', reportTechnologyNames(technologyFindings, 'framework_runtime')),
|
|
271
|
+
reportField('Embedded or third-party dependencies', reportTechnologyNames(technologyFindings, 'embedded_dependency')),
|
|
272
|
+
reportField('Origin host', 'Not established from retained evidence'),
|
|
242
273
|
],
|
|
243
274
|
},
|
|
244
275
|
tls: {
|
|
@@ -333,6 +364,24 @@ function buildLookupEvidenceReport(document, options = {}) {
|
|
|
333
364
|
...(Object.keys(registrarRdap).length ? [groups.registrarRdap] : []),
|
|
334
365
|
groups.whois,
|
|
335
366
|
],
|
|
367
|
+
registrarStanding: registrarStanding.version === 1 ? [
|
|
368
|
+
reportField('Assessment', displayLabel(registrarStandingAssessment.label || registrarStandingAssessment.state)),
|
|
369
|
+
reportField('Detail', registrarStandingAssessment.detail),
|
|
370
|
+
reportField('Registrar IANA ID', registrarStanding.ianaId),
|
|
371
|
+
reportField('IANA accreditation', displayLabel(registrarAccreditation.state)),
|
|
372
|
+
reportField('IANA source health', displayLabel(registrarAccreditation.sourceHealth)),
|
|
373
|
+
reportField('IANA source', registrarAccreditation.sourceUrl),
|
|
374
|
+
reportField('IANA catalogue reviewed', registrarAccreditation.observedAt),
|
|
375
|
+
reportField('ICANN notice state', displayLabel(registrarCompliance.state)),
|
|
376
|
+
reportField('ICANN source health', displayLabel(registrarCompliance.sourceHealth)),
|
|
377
|
+
reportField('ICANN source', registrarCompliance.sourceUrl),
|
|
378
|
+
reportField('ICANN catalogue reviewed', registrarCompliance.reviewedAt),
|
|
379
|
+
...(registrarActions.length
|
|
380
|
+
? registrarActions.map((action, index) => reportField(`Official notice ${index + 1}`, `${displayLabel(action.type)} issued ${cleanReportText(action.issuedOn)}; source ${cleanReportText(action.sourceUrl)}${action.indexOutcome ? `; index outcome ${cleanReportText(action.indexOutcome)}` : ''}`))
|
|
381
|
+
: [reportField('Matching official notices', registrarNoticeSummary)]),
|
|
382
|
+
reportField('Standing limitations', listText(registrarStanding.limitations)),
|
|
383
|
+
reportField('Review steps', listText(registrarStanding.nextActions)),
|
|
384
|
+
] : [],
|
|
336
385
|
registryInterpretation: registryInsights.version === 1 ? [
|
|
337
386
|
reportField('Lifecycle', displayLabel(registryLifecycle.label)),
|
|
338
387
|
reportField('Raw lifecycle statuses', listText(registryLifecycle.rawStatuses)),
|
|
@@ -387,6 +436,7 @@ function buildLookupEvidenceReport(document, options = {}) {
|
|
|
387
436
|
...(networkContext.contextVersion === 1 ? [
|
|
388
437
|
'Observed network registration describes one point-in-time public endpoint. It may identify an edge or shared network rather than the origin host and does not prove control, ownership, intent, or maliciousness.',
|
|
389
438
|
] : []),
|
|
439
|
+
'Nameserver identity does not establish operator or web-host ownership. Edge and application-platform indicators do not establish concealed origin infrastructure.',
|
|
390
440
|
...(registryAccessFields.length ? [
|
|
391
441
|
'Registry access constraints describe collection reachability only. They do not decide registration, availability, ownership, safety, or maliciousness.',
|
|
392
442
|
] : []),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Generated by tools/cisa-kev-catalog.mts from the pinned CISA KEV feed.
|
|
2
2
|
// United States Government work; do not edit by hand.
|
|
3
3
|
const CISA_KEV_CATALOG = Object.freeze({
|
|
4
|
-
catalogVersion: "2026.08.
|
|
5
|
-
releasedAt: "2026-08-
|
|
6
|
-
sourceSha256: "
|
|
4
|
+
catalogVersion: "2026.08.31",
|
|
5
|
+
releasedAt: "2026-08-31T14:55:13.3856Z",
|
|
6
|
+
sourceSha256: "2043ec4073019154e781f687359bfd9759e4d1ca9a18befc99fbe0c4c93bbde8",
|
|
7
7
|
sourceUrl: "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
|
|
8
8
|
identifiers: Object.freeze([
|
|
9
9
|
"CVE-2002-0367",
|
|
@@ -193,6 +193,7 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
193
193
|
"CVE-2015-3035",
|
|
194
194
|
"CVE-2015-3043",
|
|
195
195
|
"CVE-2015-3113",
|
|
196
|
+
"CVE-2015-3246",
|
|
196
197
|
"CVE-2015-4068",
|
|
197
198
|
"CVE-2015-4495",
|
|
198
199
|
"CVE-2015-4852",
|
|
@@ -200,6 +201,7 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
200
201
|
"CVE-2015-5119",
|
|
201
202
|
"CVE-2015-5122",
|
|
202
203
|
"CVE-2015-5123",
|
|
204
|
+
"CVE-2015-5287",
|
|
203
205
|
"CVE-2015-5317",
|
|
204
206
|
"CVE-2015-6175",
|
|
205
207
|
"CVE-2015-7450",
|
|
@@ -463,6 +465,7 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
463
465
|
"CVE-2019-10068",
|
|
464
466
|
"CVE-2019-10149",
|
|
465
467
|
"CVE-2019-1064",
|
|
468
|
+
"CVE-2019-1068",
|
|
466
469
|
"CVE-2019-1069",
|
|
467
470
|
"CVE-2019-10758",
|
|
468
471
|
"CVE-2019-11001",
|
|
@@ -760,6 +763,7 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
760
763
|
"CVE-2021-22941",
|
|
761
764
|
"CVE-2021-22986",
|
|
762
765
|
"CVE-2021-22991",
|
|
766
|
+
"CVE-2021-23758",
|
|
763
767
|
"CVE-2021-23874",
|
|
764
768
|
"CVE-2021-25296",
|
|
765
769
|
"CVE-2021-25297",
|
|
@@ -924,6 +928,7 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
924
928
|
"CVE-2022-0543",
|
|
925
929
|
"CVE-2022-0609",
|
|
926
930
|
"CVE-2022-0847",
|
|
931
|
+
"CVE-2022-0995",
|
|
927
932
|
"CVE-2022-1040",
|
|
928
933
|
"CVE-2022-1096",
|
|
929
934
|
"CVE-2022-1364",
|
|
@@ -1199,6 +1204,7 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
1199
1204
|
"CVE-2023-4863",
|
|
1200
1205
|
"CVE-2023-48788",
|
|
1201
1206
|
"CVE-2023-49103",
|
|
1207
|
+
"CVE-2023-49105",
|
|
1202
1208
|
"CVE-2023-4911",
|
|
1203
1209
|
"CVE-2023-4966",
|
|
1204
1210
|
"CVE-2023-49897",
|
|
@@ -1542,6 +1548,7 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
1542
1548
|
"CVE-2025-6218",
|
|
1543
1549
|
"CVE-2025-62215",
|
|
1544
1550
|
"CVE-2025-62221",
|
|
1551
|
+
"CVE-2025-62593",
|
|
1545
1552
|
"CVE-2025-64328",
|
|
1546
1553
|
"CVE-2025-64446",
|
|
1547
1554
|
"CVE-2025-6543",
|
|
@@ -1575,6 +1582,7 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
1575
1582
|
"CVE-2026-16232",
|
|
1576
1583
|
"CVE-2026-16812",
|
|
1577
1584
|
"CVE-2026-1731",
|
|
1585
|
+
"CVE-2026-18556",
|
|
1578
1586
|
"CVE-2026-18577",
|
|
1579
1587
|
"CVE-2026-20045",
|
|
1580
1588
|
"CVE-2026-20122",
|
|
@@ -1588,6 +1596,7 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
1588
1596
|
"CVE-2026-20253",
|
|
1589
1597
|
"CVE-2026-20262",
|
|
1590
1598
|
"CVE-2026-20316",
|
|
1599
|
+
"CVE-2026-20349",
|
|
1591
1600
|
"CVE-2026-20700",
|
|
1592
1601
|
"CVE-2026-20805",
|
|
1593
1602
|
"CVE-2026-20963",
|
|
@@ -1600,6 +1609,7 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
1600
1609
|
"CVE-2026-21525",
|
|
1601
1610
|
"CVE-2026-21533",
|
|
1602
1611
|
"CVE-2026-21643",
|
|
1612
|
+
"CVE-2026-21962",
|
|
1603
1613
|
"CVE-2026-22719",
|
|
1604
1614
|
"CVE-2026-22769",
|
|
1605
1615
|
"CVE-2026-23760",
|
|
@@ -1616,8 +1626,10 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
1616
1626
|
"CVE-2026-32202",
|
|
1617
1627
|
"CVE-2026-33017",
|
|
1618
1628
|
"CVE-2026-33634",
|
|
1629
|
+
"CVE-2026-33824",
|
|
1619
1630
|
"CVE-2026-33825",
|
|
1620
1631
|
"CVE-2026-34197",
|
|
1632
|
+
"CVE-2026-34486",
|
|
1621
1633
|
"CVE-2026-34621",
|
|
1622
1634
|
"CVE-2026-34908",
|
|
1623
1635
|
"CVE-2026-34909",
|
|
@@ -1650,19 +1662,37 @@ const CISA_KEV_CATALOG = Object.freeze({
|
|
|
1650
1662
|
"CVE-2026-50522",
|
|
1651
1663
|
"CVE-2026-50751",
|
|
1652
1664
|
"CVE-2026-5281",
|
|
1665
|
+
"CVE-2026-53362",
|
|
1653
1666
|
"CVE-2026-54420",
|
|
1667
|
+
"CVE-2026-55040",
|
|
1654
1668
|
"CVE-2026-55255",
|
|
1655
1669
|
"CVE-2026-56155",
|
|
1656
1670
|
"CVE-2026-56164",
|
|
1657
1671
|
"CVE-2026-56290",
|
|
1658
1672
|
"CVE-2026-56291",
|
|
1659
1673
|
"CVE-2026-58644",
|
|
1674
|
+
"CVE-2026-59310",
|
|
1675
|
+
"CVE-2026-60004",
|
|
1660
1676
|
"CVE-2026-60137",
|
|
1661
1677
|
"CVE-2026-63030",
|
|
1678
|
+
"CVE-2026-63077",
|
|
1679
|
+
"CVE-2026-64849",
|
|
1680
|
+
"CVE-2026-65400",
|
|
1681
|
+
"CVE-2026-66384",
|
|
1682
|
+
"CVE-2026-68820",
|
|
1662
1683
|
"CVE-2026-6973",
|
|
1684
|
+
"CVE-2026-72529",
|
|
1685
|
+
"CVE-2026-72530",
|
|
1686
|
+
"CVE-2026-72898",
|
|
1687
|
+
"CVE-2026-73570",
|
|
1663
1688
|
"CVE-2026-7473",
|
|
1689
|
+
"CVE-2026-8037",
|
|
1690
|
+
"CVE-2026-81578",
|
|
1691
|
+
"CVE-2026-82078",
|
|
1664
1692
|
"CVE-2026-8398",
|
|
1665
|
-
"CVE-2026-
|
|
1693
|
+
"CVE-2026-8452",
|
|
1694
|
+
"CVE-2026-9082",
|
|
1695
|
+
"CVE-2026-9198"
|
|
1666
1696
|
]),
|
|
1667
1697
|
});
|
|
1668
1698
|
export { CISA_KEV_CATALOG };
|