@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.
Files changed (97) hide show
  1. package/cli/arguments.mjs +21 -5
  2. package/cli/artifact-validation/case-response.mjs +771 -0
  3. package/cli/artifact-validation/investigation-capsule.mjs +426 -0
  4. package/cli/artifact-validation/lookup-evidence.mjs +718 -0
  5. package/cli/artifact-validation/signed-review.mjs +598 -0
  6. package/cli/artifact-validation/structure-primitives.mjs +105 -0
  7. package/cli/artifact-verify.mjs +1 -1
  8. package/cli/assurance-command-runner.mjs +324 -0
  9. package/cli/command-reference.mjs +1175 -843
  10. package/cli/completion.mjs +56 -9
  11. package/cli/discriminated-command-handlers.mjs +11 -0
  12. package/cli/doctor.mjs +2 -2
  13. package/cli/evidence-command-runner.mjs +41 -31
  14. package/cli/export-evidence.mjs +1 -1
  15. package/cli/fail-policy.mjs +33 -8
  16. package/cli/formatters/terminal-command-formats.mjs +437 -0
  17. package/cli/formatters/terminal-lookup.mjs +758 -0
  18. package/cli/formatters/terminal-metadata.mjs +58 -0
  19. package/cli/formatters/terminal-shared.mjs +83 -0
  20. package/cli/formatters/terminal.mjs +5 -1232
  21. package/cli/history-command-runner.mjs +132 -0
  22. package/cli/inline-command-families.mjs +45 -0
  23. package/cli/inline-command-runner.mjs +55 -0
  24. package/cli/investigation-plan.mjs +9 -2
  25. package/cli/investigation-run.mjs +114 -9
  26. package/cli/lookup-brief.mjs +141 -23
  27. package/cli/lookup-browser-view.mjs +289 -0
  28. package/cli/lookup-browser.mjs +3 -288
  29. package/cli/mail-review.mjs +2 -6
  30. package/cli/offline-artifact-validation.mjs +19 -0
  31. package/cli/review-command-runner.mjs +269 -0
  32. package/cli/runner.mjs +15 -972
  33. package/cli/saved-lookup.mjs +19 -2
  34. package/cli/support-command-runner.mjs +229 -0
  35. package/cli/workflow-command-runner.mjs +129 -0
  36. package/docs/cli-reference.md +17 -10
  37. package/docs/cli.md +5 -3
  38. package/lib/availability.mjs +12 -9
  39. package/lib/bounded-contract-normalizers.mjs +2 -9
  40. package/lib/browser-library-profile.mjs +1 -2
  41. package/lib/client-behavior-profile.mjs +1 -2
  42. package/lib/credential-surface-profile.mjs +1 -1
  43. package/lib/domain-posture-analysis.mjs +10 -1
  44. package/lib/domain-posture.mjs +51 -23
  45. package/lib/evidence-export.mjs +46 -5
  46. package/lib/evidence-report-markdown.mjs +4 -0
  47. package/lib/evidence-report.mjs +50 -0
  48. package/lib/generated/cisa-kev-catalog.mjs +34 -4
  49. package/lib/generated/registrar-standing-catalogue.mjs +367 -0
  50. package/lib/json-record.mjs +12 -0
  51. package/lib/lookup-child-profile-contract.mjs +858 -0
  52. package/lib/lookup-contract-primitives.mjs +98 -0
  53. package/lib/lookup-diagnostics.mjs +96 -0
  54. package/lib/lookup-network-evidence-bounds.mjs +1 -0
  55. package/lib/lookup-response.mjs +367 -0
  56. package/lib/lookup.mjs +28 -443
  57. package/lib/page-role-profile.mjs +1 -3
  58. package/lib/project-metadata.mjs +1 -1
  59. package/lib/registrar-standing-catalogue-contract.mjs +70 -0
  60. package/lib/registrar-standing-contract.mjs +300 -0
  61. package/lib/registrar-standing.mjs +269 -0
  62. package/lib/registry-insights.mjs +2 -8
  63. package/lib/risk-scoring.mjs +26 -10
  64. package/lib/sslbl-certificates.generated.mjs +138 -136
  65. package/lib/structured-data-identity.mjs +1 -3
  66. package/lib/technology-evidence-role.mjs +51 -0
  67. package/lib/threat-intelligence-runtime.mjs +3 -3
  68. package/lib/threat-intelligence-types.mjs +13 -0
  69. package/lib/threatfox-intelligence.mjs +1 -3
  70. package/lib/tls-intelligence.mjs +1 -2
  71. package/lib/urlhaus-intelligence.mjs +1 -3
  72. package/lib/urlscan-intelligence.mjs +1 -3
  73. package/lib/website-security-posture.mjs +1 -2
  74. package/lib/website-technology.mjs +72 -23
  75. package/lib/whois-common-formats.mjs +276 -0
  76. package/lib/whois-dialects.mjs +534 -0
  77. package/lib/whois-parser-support.mjs +305 -0
  78. package/lib/whois-parser.mjs +135 -0
  79. package/lib/whois.mjs +6 -1187
  80. package/package.json +3 -3
  81. package/packages/cases/case-evidence-model.mjs +9 -0
  82. package/packages/cases/case-migration-model.mjs +17 -11
  83. package/packages/cases/case-response-model.mjs +13 -9
  84. package/packages/cases/case-response-packet.mjs +297 -112
  85. package/packages/comparison/page-similarity.mjs +23 -0
  86. package/packages/contracts/case-portability.mjs +227 -63
  87. package/packages/contracts/cli-lookup.mjs +4 -3
  88. package/packages/contracts/domain-control-manifest.mjs +1 -1
  89. package/packages/contracts/investigation-portability.mjs +1 -1
  90. package/packages/contracts/risk-calibration.mjs +2 -2
  91. package/packages/contracts/workspace-portability.mjs +34 -6
  92. package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
  93. package/packages/investigation/lookup-claim-readiness.mjs +2 -2
  94. package/packages/workspace/brand-profile-model.mjs +1 -1
  95. package/packages/workspace/website-snapshot-model.mjs +79 -6
  96. package/third-party-notices.txt +1 -1
  97. package/cli/artifact-structure.mjs +0 -2507
@@ -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 = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs);
68
- promise.then((value) => { clearTimeout(timeout); resolve(value); }, (err) => { clearTimeout(timeout); reject(err); });
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.map((record) => String(record || '').toLowerCase().replace(/\.+$/u, '')).filter(Boolean);
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}`, () => dns.resolveTxt(domain)),
654
- resolveDns(`TXT _dmarc.${domain}`, () => dns.resolveTxt(`_dmarc.${domain}`)),
655
- resolveDns(`MX ${domain}`, () => dns.resolveMx(domain)),
656
- resolveDns(`NS ${domain}`, () => dns.resolveNs(domain)),
657
- resolveDns(`CAA ${domain}`, () => dns.resolveCaa(domain)),
658
- resolveDns(`TXT _mta-sts.${domain}`, () => dns.resolveTxt(`_mta-sts.${domain}`)),
659
- resolveDns(`TXT _smtp._tls.${domain}`, () => dns.resolveTxt(`_smtp._tls.${domain}`)),
660
- resolveDns(`TXT default._bimi.${domain}`, () => dns.resolveTxt(`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}`, () => dns.resolveTxt(`${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}`, () => dns.resolveTxt(`${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 enrichmentDeadline = Date.now() + POSTURE_ENRICHMENT_DEADLINE_MS;
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 remaining = Math.max(1, enrichmentDeadline - Date.now());
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}`, () => dns.resolveTxt(name), Math.min(DNS_TIMEOUT_MS, remaining));
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: new Date().toISOString(),
768
+ checkedAt: checkedAt.toISOString(),
741
769
  dkimSelectors: selectors,
742
770
  retiredDkimSelectors: retiredSelectors,
743
771
  mailProtectionProfile: normalizedMailProfile,
@@ -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 PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION = 26;
11
- export const LOOKUP_EVIDENCE_SCHEMA_VERSION = 27;
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
- PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION,
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 remains replayable; version 27 is the single v2 successor with bounded homepage and privacy-minimised registration projections.',
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);
@@ -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.03",
5
- releasedAt: "2026-08-03T18:55:09.067Z",
6
- sourceSha256: "16acee8334e59e44ccbaed4da7d02b409144061542b64ad74a9443fb7842a828",
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-9082"
1693
+ "CVE-2026-8452",
1694
+ "CVE-2026-9082",
1695
+ "CVE-2026-9198"
1666
1696
  ]),
1667
1697
  });
1668
1698
  export { CISA_KEV_CATALOG };