@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.
- package/README.md +17 -2
- package/bin/whoisleuth.mjs +41 -24
- package/cli/arguments.mjs +236 -5
- package/cli/artifact-verify.mjs +56 -14
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +18 -3
- package/cli/doctor.mjs +11 -5
- package/cli/evidence-command-arguments.mjs +1 -1
- package/cli/evidence-command-runner.mjs +2 -2
- package/cli/evidence-signing.mjs +4 -4
- package/cli/formatters/html.mjs +1 -1
- package/cli/formatters/json.mjs +13 -1
- package/cli/formatters/terminal.mjs +7 -3
- package/cli/investigation-plan.mjs +2 -2
- package/cli/local-mmdb-review.mjs +76 -0
- package/cli/lookalike-calibration.mjs +121 -0
- package/cli/lookup-command-runner.mjs +4 -1
- package/cli/lookup-plan.mjs +3 -3
- package/cli/lookup-reconcile.mjs +141 -0
- package/cli/lookup-timeline.mjs +1 -1
- package/cli/mail-review.mjs +1 -1
- package/cli/offline-evidence-review.mjs +20 -2
- package/cli/registry-doctor.mjs +125 -0
- package/cli/risk-calibration.mjs +117 -9
- package/cli/runner.mjs +236 -17
- package/cli/saved-lookup.mjs +2 -2
- package/cli/sharing-review.mjs +202 -0
- package/cli/source-reliability.mjs +1 -1
- package/docs/cli-reference.md +1110 -0
- package/docs/cli.md +224 -940
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +6 -6
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -12
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +1 -1
- package/frontend/src/lib/analysis/bulk-session-model.js +7 -2
- package/frontend/src/lib/analysis/case-evidence-model.js +26 -4
- package/frontend/src/lib/analysis/case-migration-model.js +6 -1
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-core.js +5 -1
- package/frontend/src/lib/analysis/case-record-operations.js +6 -1
- package/frontend/src/lib/analysis/case-response-packet.js +7 -7
- package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
- package/frontend/src/lib/analysis/investigation-capsule.js +105 -0
- package/frontend/src/lib/analysis/lookup-asset-graph.js +806 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +480 -0
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
- package/frontend/src/lib/analysis/lookup-presentation.js +74 -0
- package/frontend/src/lib/analysis/lookup-response.js +2 -0
- package/frontend/src/lib/analysis/lookup-source-refresh.js +304 -0
- package/frontend/src/lib/analysis/lookup-summary-model.js +398 -0
- package/frontend/src/lib/analysis/page-similarity.js +1 -1
- package/frontend/src/lib/analysis/relationship-evidence.js +3 -3
- package/frontend/src/lib/analysis/relationship-observation-model.js +4 -4
- package/frontend/src/lib/analysis/scoring.js +8 -88
- package/frontend/src/lib/analysis/shortlist-model.js +8 -4
- package/frontend/src/lib/analysis/visualization-bounds.js +26 -0
- package/frontend/src/lib/analysis/visualization-force-graph.js +352 -0
- package/frontend/src/lib/analysis/visualization-matrix.js +88 -0
- package/frontend/src/lib/analysis/visualization-models.js +370 -0
- package/frontend/src/lib/analysis/visualization-time-series.js +144 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -1
- package/frontend/src/lib/analysis/workspace-archive.js +2 -2
- package/lib/analyst-taxonomy.mjs +40 -0
- package/lib/availability.mjs +1 -1
- package/lib/browser-library-profile.mjs +15 -2
- package/lib/certificate-extension-profile.mjs +71 -0
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/domain-assurance.mjs +368 -0
- package/lib/domain-control-manifest.mjs +50 -7
- package/lib/domain-posture.mjs +9 -9
- package/lib/encrypted-dns-contract.mjs +1 -1
- package/lib/evidence-report-markdown.mjs +1 -1
- package/lib/evidence-report.mjs +1 -1
- package/lib/generated/cisa-kev-catalog.mjs +1668 -0
- package/lib/ip-prefix.mjs +34 -1
- package/lib/local-geoip-evidence.mjs +2 -2
- package/lib/lookup-response-contract.mjs +346 -0
- package/lib/observed-network-context.mjs +1 -1
- package/lib/opportunity-scoring.mjs +107 -0
- package/lib/page-fingerprints.mjs +1 -1
- package/lib/rdap-capabilities.mjs +6 -6
- package/lib/rdap-search-workbench.mjs +96 -6
- package/lib/registry-access.mjs +1 -1
- package/lib/registry-capability-catalogue.mjs +4 -4
- package/lib/registry-insights.mjs +6 -6
- package/lib/risk-scoring.mjs +206 -83
- package/lib/scoring-evidence-quality.mjs +107 -0
- package/lib/security-txt.mjs +3 -3
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threatfox-intelligence.mjs +1 -1
- package/lib/tls-intelligence.mjs +13 -3
- package/lib/urlhaus-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +5 -5
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-chain.mjs +1 -1
- package/package.json +5 -1
- package/third-party-notices.txt +911 -19
package/lib/security-txt.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
285
|
+
: 'The provider reported a match but no returned record could be safely normalised.',
|
|
286
286
|
upstreamStatus,
|
|
287
287
|
findings,
|
|
288
288
|
limitations: [
|
package/lib/tls-intelligence.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
'
|
|
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
|
|
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
|
|
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
|
|
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
|
|
227
|
-
: finding('certificate_authorization_unavailable', 'certificate', 'unavailable', 'neutral', 'Certificate
|
|
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
|
|
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
|
|
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
|
{
|
package/lib/whois-chain.mjs
CHANGED
|
@@ -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
|
|
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
|
+
"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",
|