@slicedearth/whoisleuth-cli 1.36.1 → 1.39.1

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/README.md +17 -2
  2. package/bin/whoisleuth.mjs +41 -24
  3. package/cli/arguments.mjs +236 -5
  4. package/cli/artifact-verify.mjs +56 -14
  5. package/cli/compare.mjs +1 -1
  6. package/cli/completion.mjs +18 -3
  7. package/cli/doctor.mjs +11 -5
  8. package/cli/evidence-command-arguments.mjs +1 -1
  9. package/cli/evidence-command-runner.mjs +2 -2
  10. package/cli/evidence-signing.mjs +4 -4
  11. package/cli/formatters/html.mjs +1 -1
  12. package/cli/formatters/json.mjs +13 -1
  13. package/cli/formatters/terminal.mjs +7 -3
  14. package/cli/investigation-plan.mjs +2 -2
  15. package/cli/local-mmdb-review.mjs +76 -0
  16. package/cli/lookalike-calibration.mjs +121 -0
  17. package/cli/lookup-command-runner.mjs +4 -1
  18. package/cli/lookup-plan.mjs +3 -3
  19. package/cli/lookup-reconcile.mjs +141 -0
  20. package/cli/lookup-timeline.mjs +1 -1
  21. package/cli/mail-review.mjs +1 -1
  22. package/cli/offline-evidence-review.mjs +20 -2
  23. package/cli/registry-doctor.mjs +125 -0
  24. package/cli/risk-calibration.mjs +117 -9
  25. package/cli/runner.mjs +236 -17
  26. package/cli/saved-lookup.mjs +2 -2
  27. package/cli/sharing-review.mjs +202 -0
  28. package/cli/source-reliability.mjs +1 -1
  29. package/docs/cli-reference.md +1110 -0
  30. package/docs/cli.md +224 -940
  31. package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
  32. package/frontend/src/lib/analysis/acquisition-due-diligence.js +6 -6
  33. package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -12
  34. package/frontend/src/lib/analysis/bulk-mail-exposure.js +1 -1
  35. package/frontend/src/lib/analysis/bulk-session-model.js +7 -2
  36. package/frontend/src/lib/analysis/case-evidence-model.js +26 -4
  37. package/frontend/src/lib/analysis/case-migration-model.js +6 -1
  38. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  39. package/frontend/src/lib/analysis/case-record-core.js +5 -1
  40. package/frontend/src/lib/analysis/case-record-operations.js +6 -1
  41. package/frontend/src/lib/analysis/case-response-packet.js +7 -7
  42. package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
  43. package/frontend/src/lib/analysis/investigation-capsule.js +105 -0
  44. package/frontend/src/lib/analysis/lookup-asset-graph.js +806 -0
  45. package/frontend/src/lib/analysis/lookup-decision-support.js +480 -0
  46. package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
  47. package/frontend/src/lib/analysis/lookup-presentation.js +74 -0
  48. package/frontend/src/lib/analysis/lookup-response.js +2 -0
  49. package/frontend/src/lib/analysis/lookup-source-refresh.js +304 -0
  50. package/frontend/src/lib/analysis/lookup-summary-model.js +398 -0
  51. package/frontend/src/lib/analysis/page-similarity.js +1 -1
  52. package/frontend/src/lib/analysis/relationship-evidence.js +3 -3
  53. package/frontend/src/lib/analysis/relationship-observation-model.js +4 -4
  54. package/frontend/src/lib/analysis/scoring.js +8 -88
  55. package/frontend/src/lib/analysis/shortlist-model.js +8 -4
  56. package/frontend/src/lib/analysis/visualization-bounds.js +26 -0
  57. package/frontend/src/lib/analysis/visualization-force-graph.js +352 -0
  58. package/frontend/src/lib/analysis/visualization-matrix.js +88 -0
  59. package/frontend/src/lib/analysis/visualization-models.js +370 -0
  60. package/frontend/src/lib/analysis/visualization-time-series.js +144 -0
  61. package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -1
  62. package/frontend/src/lib/analysis/workspace-archive.js +2 -2
  63. package/lib/analyst-taxonomy.mjs +40 -0
  64. package/lib/availability.mjs +1 -1
  65. package/lib/browser-library-profile.mjs +15 -2
  66. package/lib/certificate-extension-profile.mjs +71 -0
  67. package/lib/credential-surface-profile.mjs +1 -1
  68. package/lib/domain-assurance.mjs +368 -0
  69. package/lib/domain-control-manifest.mjs +50 -7
  70. package/lib/domain-posture.mjs +9 -9
  71. package/lib/encrypted-dns-contract.mjs +1 -1
  72. package/lib/evidence-report-markdown.mjs +1 -1
  73. package/lib/evidence-report.mjs +1 -1
  74. package/lib/generated/cisa-kev-catalog.mjs +1668 -0
  75. package/lib/ip-prefix.mjs +34 -1
  76. package/lib/local-geoip-evidence.mjs +2 -2
  77. package/lib/lookup-response-contract.mjs +346 -0
  78. package/lib/observed-network-context.mjs +1 -1
  79. package/lib/opportunity-scoring.mjs +107 -0
  80. package/lib/page-fingerprints.mjs +1 -1
  81. package/lib/rdap-capabilities.mjs +6 -6
  82. package/lib/rdap-search-workbench.mjs +96 -6
  83. package/lib/registry-access.mjs +1 -1
  84. package/lib/registry-capability-catalogue.mjs +4 -4
  85. package/lib/registry-insights.mjs +6 -6
  86. package/lib/risk-scoring.mjs +206 -83
  87. package/lib/scoring-evidence-quality.mjs +107 -0
  88. package/lib/security-txt.mjs +3 -3
  89. package/lib/threat-intelligence-manifests.mjs +1 -1
  90. package/lib/threatfox-intelligence.mjs +1 -1
  91. package/lib/tls-intelligence.mjs +13 -3
  92. package/lib/urlhaus-intelligence.mjs +1 -1
  93. package/lib/website-security-posture.mjs +5 -5
  94. package/lib/website-technology.mjs +1 -1
  95. package/lib/whois-chain.mjs +1 -1
  96. package/package.json +5 -1
  97. package/third-party-notices.txt +911 -19
@@ -202,7 +202,7 @@ function parseSecurityTxt(text, options = {}) {
202
202
  const limitations = [
203
203
  ...(signedPayload.signed ? ['A cleartext signature was present but was not cryptographically verified.'] : []),
204
204
  ...(canonicalMatches === false ? ['The fetched URL was not listed by the file as a Canonical location.'] : []),
205
- ...(valuesTruncated ? ['The response or normalized field set exceeded a collection bound.'] : []),
205
+ ...(valuesTruncated ? ['The response or normalised field set exceeded a collection bound.'] : []),
206
206
  ];
207
207
  const detail = state === 'malformed'
208
208
  ? 'The file did not contain at least one valid Contact field and exactly one valid Expires field.'
@@ -314,7 +314,7 @@ async function collectSecurityTxt(hostname, dependencies = {}) {
314
314
  }
315
315
  if (response.status === 404 || response.status === 410) {
316
316
  await response.body?.cancel().catch(() => { });
317
- return emptyResult('absent', 'No security.txt file was published at the standardized location for this hostname.', {
317
+ return emptyResult('absent', 'No security.txt file was published at the standardised location for this hostname.', {
318
318
  observedAt, requestedUrl, finalUrl: result.finalUrl, httpStatus: response.status, durationMs: now() - startedAt,
319
319
  });
320
320
  }
@@ -328,7 +328,7 @@ async function collectSecurityTxt(hostname, dependencies = {}) {
328
328
  const charset = contentType.match(/;\s*charset\s*=\s*"?([^;"\s]+)/iu)?.[1]?.toLowerCase() || null;
329
329
  if (!/^text\/plain(?:\s*;|$)/iu.test(contentType) || (charset !== null && charset !== 'utf-8')) {
330
330
  await response.body?.cancel().catch(() => { });
331
- return emptyResult('unsupported', 'The standardized endpoint did not return a UTF-8 text/plain security.txt document.', {
331
+ return emptyResult('unsupported', 'The standardised endpoint did not return a UTF-8 text/plain security.txt document.', {
332
332
  observedAt, requestedUrl, finalUrl: result.finalUrl, httpStatus: response.status, durationMs: now() - startedAt,
333
333
  });
334
334
  }
@@ -352,7 +352,7 @@ function normalizeConnectorOutputs(value) {
352
352
  throw new TypeError('Connector outputs must be unique');
353
353
  }
354
354
  if (!entities.length) {
355
- throw new TypeError('Connector outputs require at least one normalized entity type');
355
+ throw new TypeError('Connector outputs require at least one normalised entity type');
356
356
  }
357
357
  const entityTypes = new Set(entities);
358
358
  for (const relationship of relationships) {
@@ -282,7 +282,7 @@ function createThreatfoxIntelligenceAdapter(dependencies = {}) {
282
282
  truncated,
283
283
  detail: findings.length
284
284
  ? `Found ${findings.length} bounded malware-IOC record${findings.length === 1 ? '' : 's'} for this domain${truncated ? '; additional provider records may exist' : ''}.`
285
- : 'The provider reported a match but no returned record could be safely normalized.',
285
+ : 'The provider reported a match but no returned record could be safely normalised.',
286
286
  upstreamStatus,
287
287
  findings,
288
288
  limitations: [
@@ -12,7 +12,8 @@ import * as tls from 'node:tls';
12
12
  import { domainToASCII } from 'node:url';
13
13
  import { createObservation } from "./observation.mjs";
14
14
  import { isPrivateAddress, resolvePublicAddresses } from "./safe-fetch.mjs";
15
- const TLS_PROFILE_VERSION = 2;
15
+ import { parseCertificateExtensionProfile, } from "./certificate-extension-profile.mjs";
16
+ const TLS_PROFILE_VERSION = 3;
16
17
  const TLS_PORT = 443;
17
18
  const TLS_TIMEOUT_MS = 5000;
18
19
  const MAX_RESOLVED_ADDRESSES = 64;
@@ -486,6 +487,15 @@ function normalizeCertificate(value, state, options = {}) {
486
487
  };
487
488
  certificate.extendedKeyUsage = certificatePurposes(parsedCertificate?.keyUsage || record.ext_key_usage, state);
488
489
  certificate.authorityInformationAccess = authorityInformationAccess(parsedCertificate?.infoAccess || record.infoAccess, state);
490
+ const extensionProfile = parseCertificateExtensionProfile(record.raw);
491
+ if (extensionProfile.parsed || extensionProfile.partial) {
492
+ certificate.extensionProfile = extensionProfile;
493
+ if (extensionProfile.partial)
494
+ state.discarded += 1;
495
+ if (extensionProfile.certificatePolicies.truncated || extensionProfile.crlDistributionPoints.truncated) {
496
+ state.truncated = true;
497
+ }
498
+ }
489
499
  }
490
500
  return certificate;
491
501
  }
@@ -562,7 +572,7 @@ function tlsFindings(profile) {
562
572
  if (profile.validity.status === 'not_yet_valid')
563
573
  findings.push({ id: 'certificate_not_yet_valid', tone: 'warning', label: 'Certificate not yet valid', detail: 'The leaf certificate validity period had not started at observation time.' });
564
574
  if (profile.authorization.authorized === false)
565
- findings.push({ id: 'certificate_unauthorized', tone: 'warning', label: 'Certificate not authorized', detail: 'The runtime trust store did not authorize the observed certificate chain. This can reflect an incomplete chain, private CA, self-signed certificate, expiry, or another validation failure.' });
575
+ findings.push({ id: 'certificate_unauthorized', tone: 'warning', label: 'Certificate not authorised', detail: 'The runtime trust store did not authorise the observed certificate chain. This can reflect an incomplete chain, private CA, self-signed certificate, expiry, or another validation failure.' });
566
576
  if (profile.hostname.matches === false)
567
577
  findings.push({ id: 'hostname_mismatch', tone: 'warning', label: 'Hostname mismatch', detail: 'The observed leaf certificate did not match the SNI hostname.' });
568
578
  if (profile.certificate?.subjectAltNames?.dnsNames.some((name) => name.startsWith('*.')))
@@ -631,7 +641,7 @@ function buildTlsObservation(handshake = {}, options = {}) {
631
641
  truncated: state.truncated,
632
642
  limitations: [
633
643
  'This is a point-in-time TLS handshake to one validated public address; other addresses or edge locations may present different results.',
634
- 'Authorization reflects the certificate authorities and verification behavior available to this runtime.',
644
+ 'Authorisation reflects the certificate authorities and verification behaviour available to this runtime.',
635
645
  'The profile records the negotiated connection only and does not enumerate supported TLS versions or cipher suites.',
636
646
  ...(state.truncated ? ['One or more certificate fields or chain entries reached a retention limit.'] : []),
637
647
  ],
@@ -333,7 +333,7 @@ function createUrlhausIntelligenceAdapter(dependencies = {}) {
333
333
  truncated,
334
334
  detail: findings.length
335
335
  ? `Found ${findings.length} bounded malware-distribution record${findings.length === 1 ? '' : 's'} for this host${truncated ? '; additional provider records may exist' : ''}.`
336
- : 'The provider reported this host but no returned record could be safely normalized.',
336
+ : 'The provider reported this host but no returned record could be safely normalised.',
337
337
  upstreamStatus,
338
338
  findings,
339
339
  limitations: [
@@ -50,7 +50,7 @@ function httpFindings(httpValue) {
50
50
  || !response.securityHeaders
51
51
  || typeof response.securityHeaders !== 'object'
52
52
  || Array.isArray(response.securityHeaders)) {
53
- findings.push(finding('http_headers_unavailable', 'response headers', 'unavailable', 'neutral', 'Response-header posture unavailable', 'The selected HTTP observation did not include the normalized security-header set.', ['Selected HTTP response headers']));
53
+ findings.push(finding('http_headers_unavailable', 'response headers', 'unavailable', 'neutral', 'Response-header posture unavailable', 'The selected HTTP observation did not include the normalised security-header set.', ['Selected HTTP response headers']));
54
54
  return findings;
55
55
  }
56
56
  const headerChecks = [
@@ -221,10 +221,10 @@ function tlsFindings(tlsValue) {
221
221
  : finding('modern_tls_negotiated', 'certificate', 'observed', 'configured', 'Modern TLS negotiated', 'The single retained connection negotiated TLS 1.2 or later. This does not enumerate every protocol the endpoint supports.', ['TLS handshake']));
222
222
  }
223
223
  findings.push(authorization.authorized === true
224
- ? finding('certificate_authorized', 'certificate', 'observed', 'configured', 'Certificate chain authorized', 'The runtime trust store authorized the observed certificate chain for the retained connection.', ['TLS handshake'])
224
+ ? finding('certificate_authorized', 'certificate', 'observed', 'configured', 'Certificate chain authorised', 'The runtime trust store authorised the observed certificate chain for the retained connection.', ['TLS handshake'])
225
225
  : authorization.authorized === false
226
- ? finding('certificate_not_authorized', 'certificate', 'potential_exposure', 'review', 'Certificate chain not authorized', 'The runtime trust store did not authorize the observed certificate chain. Review the separately attributed TLS evidence for context.', ['TLS handshake'])
227
- : finding('certificate_authorization_unavailable', 'certificate', 'unavailable', 'neutral', 'Certificate authorization unavailable', 'The retained TLS observation did not include a conclusive chain-authorization result.', ['TLS handshake']));
226
+ ? finding('certificate_not_authorized', 'certificate', 'potential_exposure', 'review', 'Certificate chain not authorised', 'The runtime trust store did not authorise the observed certificate chain. Review the separately attributed TLS evidence for context.', ['TLS handshake'])
227
+ : finding('certificate_authorization_unavailable', 'certificate', 'unavailable', 'neutral', 'Certificate authorisation unavailable', 'The retained TLS observation did not include a conclusive chain-authorization result.', ['TLS handshake']));
228
228
  findings.push(hostname.matches === true
229
229
  ? finding('certificate_hostname_match', 'certificate', 'observed', 'configured', 'Certificate hostname matched', 'The observed leaf certificate matched the hostname used for the retained TLS connection.', ['TLS handshake'])
230
230
  : hostname.matches === false
@@ -274,7 +274,7 @@ function dnsFindings(dnsValue, dnssecValue) {
274
274
  ? finding('dnssec_signed', 'domain controls', 'observed', 'configured', 'DNSSEC signing reported', 'The registry or WHOIS source reported a signed DNSSEC delegation.', ['Registry or WHOIS DNSSEC publication'])
275
275
  : dnssec === 'unsigned'
276
276
  ? finding('dnssec_unsigned', 'domain controls', 'observed_absence', 'review', 'DNSSEC signing not reported', 'The registry or WHOIS source reported an unsigned DNSSEC delegation.', ['Registry or WHOIS DNSSEC publication'])
277
- : finding('dnssec_unavailable', 'domain controls', 'unavailable', 'neutral', 'DNSSEC state unavailable', 'The retained registry and WHOIS evidence did not provide a recognized DNSSEC state.', ['Registry or WHOIS DNSSEC publication']));
277
+ : finding('dnssec_unavailable', 'domain controls', 'unavailable', 'neutral', 'DNSSEC state unavailable', 'The retained registry and WHOIS evidence did not provide a recognised DNSSEC state.', ['Registry or WHOIS DNSSEC publication']));
278
278
  findings.push(!caaAvailable
279
279
  ? finding('caa_unavailable', 'domain controls', 'unavailable', 'neutral', 'CAA posture unavailable', 'The CAA query did not produce a conclusive response for this observation.', [caaSource])
280
280
  : caaCount > 0
@@ -179,7 +179,7 @@ const TECHNOLOGY_SIGNATURES = [
179
179
  {
180
180
  id: 'adobe-commerce-magento', name: 'Adobe Commerce / Magento Open Source', category: 'commerce',
181
181
  evidence: [
182
- htmlEvidence(['data-mage-init=', 'type="text/x-magento-init"', "type='text/x-magento-init'"], 'Static markup contains Commerce frontend initialization markers.'),
182
+ htmlEvidence(['data-mage-init=', 'type="text/x-magento-init"', "type='text/x-magento-init'"], 'Static markup contains Commerce frontend initialisation markers.'),
183
183
  ],
184
184
  },
185
185
  {
@@ -54,7 +54,7 @@ export async function buildWhoisChainUncached(query, options = {}) {
54
54
  queryProfile: 'not-issued',
55
55
  responseEncoding: 'utf-8',
56
56
  error: admission.state === 'permission_required'
57
- ? 'WHOIS collection requires registry permission or source authorization; no socket was opened.'
57
+ ? 'WHOIS collection requires registry permission or source authorisation; no socket was opened.'
58
58
  : 'IANA publishes no domain WHOIS service for this suffix; no socket was opened.',
59
59
  }];
60
60
  }
package/package.json CHANGED
@@ -33,9 +33,12 @@
33
33
  "bugs": {
34
34
  "url": "https://github.com/slicedearth/whoisleuth/issues"
35
35
  },
36
- "version": "1.36.1",
36
+ "version": "1.39.1",
37
37
  "dependencies": {
38
+ "@peculiar/x509": "2.0.0",
39
+ "maxmind": "5.0.7",
38
40
  "parse5": "8.0.1",
41
+ "reflect-metadata": "0.2.2",
39
42
  "tldts": "7.4.10",
40
43
  "undici": "8.9.0"
41
44
  },
@@ -45,6 +48,7 @@
45
48
  "lib/**/*.mjs",
46
49
  "frontend/src/lib/analysis/**/*.js",
47
50
  "docs/cli.md",
51
+ "docs/cli-reference.md",
48
52
  "DISCLOSURE",
49
53
  "LICENSE",
50
54
  "LICENSES/*.txt",