@slicedearth/whoisleuth-cli 2.0.1 → 2.2.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 +16 -9
- package/docs/cli.md +16 -2
- 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 +47 -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/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 +2 -2
- package/packages/cases/case-evidence-model.mjs +9 -0
- package/packages/cases/case-migration-model.mjs +17 -11
- package/packages/cases/case-record-operations.mjs +235 -0
- 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 +235 -61
- 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/interchange/external-findings-import.mjs +113 -45
- package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
- package/packages/investigation/lookup-claim-readiness.mjs +2 -2
- package/packages/workspace/website-snapshot-model.mjs +79 -6
- package/third-party-notices.txt +1 -1
- package/cli/artifact-structure.mjs +0 -2507
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// discarded immediately after analysis.
|
|
6
6
|
import { domainToASCII } from 'node:url';
|
|
7
7
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
8
|
+
import { MAX_STRUCTURED_DATA_ENTITIES, MAX_STRUCTURED_DATA_SAME_AS_HOSTS, STRUCTURED_DATA_IDENTITY_VERSION, } from "./lookup-child-profile-contract.mjs";
|
|
8
9
|
import { isUriShapedLabel } from "./portable-generator.mjs";
|
|
9
10
|
import { analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
10
|
-
const STRUCTURED_DATA_IDENTITY_VERSION = 1;
|
|
11
11
|
const MAX_STRUCTURED_DATA_SCRIPTS = 8;
|
|
12
12
|
const MAX_STRUCTURED_DATA_SCRIPT_CHARS = 16_384;
|
|
13
13
|
const MAX_STRUCTURED_DATA_TOTAL_CHARS = 32_768;
|
|
@@ -15,11 +15,9 @@ const MAX_STRUCTURED_DATA_DEPTH = 16;
|
|
|
15
15
|
const MAX_STRUCTURED_DATA_OBJECTS = 256;
|
|
16
16
|
const MAX_STRUCTURED_DATA_ARRAY_ITEMS = 256;
|
|
17
17
|
const MAX_STRUCTURED_DATA_PROPERTIES = 64;
|
|
18
|
-
const MAX_STRUCTURED_DATA_ENTITIES = 16;
|
|
19
18
|
const MAX_STRUCTURED_DATA_TYPES = 8;
|
|
20
19
|
const MAX_STRUCTURED_DATA_NAME_LENGTH = 160;
|
|
21
20
|
const MAX_STRUCTURED_DATA_URL_LENGTH = 2_048;
|
|
22
|
-
const MAX_STRUCTURED_DATA_SAME_AS_HOSTS = 12;
|
|
23
21
|
const CONTROL_AND_DIRECTIONAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
|
|
24
22
|
const CONTROL_AND_DIRECTIONAL_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
|
|
25
23
|
const HOSTNAME_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/iu;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const TECHNOLOGY_EVIDENCE_ROLE_ORDER = Object.freeze([
|
|
2
|
+
'observed_edge',
|
|
3
|
+
'application_platform',
|
|
4
|
+
'framework_runtime',
|
|
5
|
+
'embedded_dependency',
|
|
6
|
+
]);
|
|
7
|
+
const TECHNOLOGY_EVIDENCE_ROLE_SET = new Set(TECHNOLOGY_EVIDENCE_ROLE_ORDER);
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
10
|
+
? value
|
|
11
|
+
: {};
|
|
12
|
+
}
|
|
13
|
+
function technologyEvidenceRoles(value) {
|
|
14
|
+
const finding = record(value);
|
|
15
|
+
const roles = new Set();
|
|
16
|
+
let hasExplicitRole = false;
|
|
17
|
+
for (const role of Array.isArray(finding.roles) ? finding.roles.slice(0, 4) : []) {
|
|
18
|
+
if (typeof role === 'string' && TECHNOLOGY_EVIDENCE_ROLE_SET.has(role)) {
|
|
19
|
+
roles.add(role);
|
|
20
|
+
hasExplicitRole = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const evidenceValues = Array.isArray(finding.evidence) ? finding.evidence.slice(0, 4) : [];
|
|
24
|
+
for (const evidenceValue of evidenceValues) {
|
|
25
|
+
const evidence = record(evidenceValue);
|
|
26
|
+
if (typeof evidence.role === 'string'
|
|
27
|
+
&& TECHNOLOGY_EVIDENCE_ROLE_SET.has(evidence.role)) {
|
|
28
|
+
roles.add(evidence.role);
|
|
29
|
+
hasExplicitRole = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!hasExplicitRole) {
|
|
33
|
+
const resourceOriginOnly = evidenceValues.length > 0
|
|
34
|
+
&& evidenceValues.every((evidence) => (String(record(evidence).source).toLowerCase() === 'resource origin'));
|
|
35
|
+
if (evidenceValues.some((evidence) => String(record(evidence).source).toLowerCase() === 'resource origin')) {
|
|
36
|
+
roles.add('embedded_dependency');
|
|
37
|
+
}
|
|
38
|
+
if (!resourceOriginOnly) {
|
|
39
|
+
const category = String(finding.category).toLowerCase();
|
|
40
|
+
if (category === 'delivery platform')
|
|
41
|
+
roles.add('observed_edge');
|
|
42
|
+
else if (['application runtime', 'web framework', 'static site generator', 'web server'].includes(category)) {
|
|
43
|
+
roles.add('framework_runtime');
|
|
44
|
+
}
|
|
45
|
+
else
|
|
46
|
+
roles.add('application_platform');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return TECHNOLOGY_EVIDENCE_ROLE_ORDER.filter((role) => roles.has(role));
|
|
50
|
+
}
|
|
51
|
+
export { TECHNOLOGY_EVIDENCE_ROLE_ORDER, technologyEvidenceRoles, };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// must declare exact input exposure, reviewed terms, and bounded request and
|
|
5
5
|
// output budgets before producing separately attributed normalized evidence.
|
|
6
6
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
7
|
-
import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "./threat-intelligence-types.mjs";
|
|
7
|
+
import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_CONFIDENCES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, THREAT_INTELLIGENCE_SEVERITIES, } from "./threat-intelligence-types.mjs";
|
|
8
8
|
import { assertCuratedConnectorDefinition, assertThreatIntelligenceProvider, } from "./threat-intelligence-definition-registry.mjs";
|
|
9
9
|
import { boundedHttpsUrl, boundedString, isRecord, isoTimestamp, strictBoundedString, } from "./bounded-contract-normalizers.mjs";
|
|
10
10
|
import { CURATED_CONNECTOR_ENTITY_TYPES, MAX_THREAT_INTELLIGENCE_URL_LENGTH, normalizeCuratedConnectorTarget, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-targets.mjs";
|
|
@@ -20,8 +20,8 @@ const TERMINAL_STATES_WITHOUT_FINDINGS = new Set([
|
|
|
20
20
|
'error',
|
|
21
21
|
]);
|
|
22
22
|
const CATEGORIES = new Set(THREAT_INTELLIGENCE_CATEGORIES);
|
|
23
|
-
const SEVERITIES = new Set(
|
|
24
|
-
const CONFIDENCES = new Set(
|
|
23
|
+
const SEVERITIES = new Set(THREAT_INTELLIGENCE_SEVERITIES);
|
|
24
|
+
const CONFIDENCES = new Set(THREAT_INTELLIGENCE_CONFIDENCES);
|
|
25
25
|
const CONNECTOR_RELATIONSHIP_TYPES = new Set([
|
|
26
26
|
'domain_resolves_to_ip',
|
|
27
27
|
'domain_uses_nameserver',
|
|
@@ -11,6 +11,19 @@ export const THREAT_INTELLIGENCE_CATEGORIES = Object.freeze([
|
|
|
11
11
|
'abuse',
|
|
12
12
|
'unknown',
|
|
13
13
|
]);
|
|
14
|
+
export const THREAT_INTELLIGENCE_SEVERITIES = Object.freeze([
|
|
15
|
+
'critical',
|
|
16
|
+
'high',
|
|
17
|
+
'medium',
|
|
18
|
+
'low',
|
|
19
|
+
'unknown',
|
|
20
|
+
]);
|
|
21
|
+
export const THREAT_INTELLIGENCE_CONFIDENCES = Object.freeze([
|
|
22
|
+
'high',
|
|
23
|
+
'medium',
|
|
24
|
+
'low',
|
|
25
|
+
'unknown',
|
|
26
|
+
]);
|
|
14
27
|
export const THREAT_INTELLIGENCE_RESULT_STATES = Object.freeze([
|
|
15
28
|
'success',
|
|
16
29
|
'partial',
|
|
@@ -7,6 +7,7 @@ import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
|
|
|
7
7
|
import { providerPolicyAdmission } from "./provider-policy-admission.mjs";
|
|
8
8
|
import { createThreatIntelligenceResult, defineThreatIntelligenceProvider, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-contract.mjs";
|
|
9
9
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
10
|
+
import { isRecord } from "./json-record.mjs";
|
|
10
11
|
const THREATFOX_SEARCH_ENDPOINT = 'https://threatfox-api.abuse.ch/api/v1/';
|
|
11
12
|
const THREATFOX_MAX_RESULTS = 20;
|
|
12
13
|
const THREATFOX_MAX_INPUT_RESULTS = 100;
|
|
@@ -39,9 +40,6 @@ const THREATFOX_PROVIDER = defineThreatIntelligenceProvider({
|
|
|
39
40
|
monthlyRequests: THREATFOX_MONTHLY_REQUESTS,
|
|
40
41
|
},
|
|
41
42
|
});
|
|
42
|
-
function isRecord(value) {
|
|
43
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
44
|
-
}
|
|
45
43
|
function enabledValue(value) {
|
|
46
44
|
return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
|
|
47
45
|
}
|
package/lib/tls-intelligence.mjs
CHANGED
|
@@ -11,10 +11,9 @@ import * as net from 'node:net';
|
|
|
11
11
|
import * as tls from 'node:tls';
|
|
12
12
|
import { domainToASCII } from 'node:url';
|
|
13
13
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
14
|
-
import { MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, } from "./lookup-network-evidence-bounds.mjs";
|
|
14
|
+
import { MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, TLS_PROFILE_VERSION, } from "./lookup-network-evidence-bounds.mjs";
|
|
15
15
|
import { isPrivateAddress, resolvePublicAddresses } from "./safe-fetch.mjs";
|
|
16
16
|
import { parseCertificateExtensionProfile, } from "./certificate-extension-profile.mjs";
|
|
17
|
-
const TLS_PROFILE_VERSION = 3;
|
|
18
17
|
const TLS_PORT = 443;
|
|
19
18
|
const TLS_TIMEOUT_MS = 5000;
|
|
20
19
|
const MAX_RESOLVED_ADDRESSES = 64;
|
|
@@ -10,6 +10,7 @@ import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
|
|
|
10
10
|
import { providerPolicyAdmission } from "./provider-policy-admission.mjs";
|
|
11
11
|
import { createThreatIntelligenceResult, defineThreatIntelligenceProvider, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-contract.mjs";
|
|
12
12
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
13
|
+
import { isRecord } from "./json-record.mjs";
|
|
13
14
|
const URLHAUS_HOST_ENDPOINT = 'https://urlhaus-api.abuse.ch/v1/host/';
|
|
14
15
|
const URLHAUS_MAX_RESULTS = 20;
|
|
15
16
|
const URLHAUS_MAX_INPUT_RESULTS = 100;
|
|
@@ -46,9 +47,6 @@ const URLHAUS_PROVIDER = defineThreatIntelligenceProvider({
|
|
|
46
47
|
monthlyRequests: URLHAUS_MONTHLY_REQUESTS,
|
|
47
48
|
},
|
|
48
49
|
});
|
|
49
|
-
function isRecord(value) {
|
|
50
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
51
|
-
}
|
|
52
50
|
function enabledValue(value) {
|
|
53
51
|
return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
|
|
54
52
|
}
|
|
@@ -8,6 +8,7 @@ import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
|
|
|
8
8
|
import { providerPolicyAdmission } from "./provider-policy-admission.mjs";
|
|
9
9
|
import { createThreatIntelligenceResult, defineThreatIntelligenceProvider, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-contract.mjs";
|
|
10
10
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
11
|
+
import { isRecord } from "./json-record.mjs";
|
|
11
12
|
const URLSCAN_SEARCH_ENDPOINT = 'https://urlscan.io/api/v1/search/';
|
|
12
13
|
const URLSCAN_SEARCH_DAYS = 90;
|
|
13
14
|
const URLSCAN_MAX_RESULTS = 20;
|
|
@@ -42,9 +43,6 @@ const URLSCAN_PROVIDER = defineThreatIntelligenceProvider({
|
|
|
42
43
|
monthlyRequests: URLSCAN_MONTHLY_REQUESTS,
|
|
43
44
|
},
|
|
44
45
|
});
|
|
45
|
-
function isRecord(value) {
|
|
46
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
47
|
-
}
|
|
48
46
|
function enabledValue(value) {
|
|
49
47
|
return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
|
|
50
48
|
}
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// Findings use fixed explanatory text and bounded counts; upstream header
|
|
4
4
|
// values, certificate errors, URLs, and DNS record contents are not copied.
|
|
5
5
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
6
|
-
|
|
7
|
-
const MAX_SECURITY_POSTURE_FINDINGS = 32;
|
|
6
|
+
import { MAX_SECURITY_POSTURE_FINDINGS, WEBSITE_SECURITY_POSTURE_VERSION, } from "./lookup-child-profile-contract.mjs";
|
|
8
7
|
const MAX_RETAINED_ORIGINS = 30;
|
|
9
8
|
function record(value) {
|
|
10
9
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
@@ -7,16 +7,14 @@
|
|
|
7
7
|
import { analyzeBrowserLibraries } from "./browser-library-profile.mjs";
|
|
8
8
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
9
9
|
import { MAX_STATIC_HTML_CHARS, MAX_TAG_LENGTH, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
10
|
-
|
|
10
|
+
import { TECHNOLOGY_EVIDENCE_ROLE_ORDER, } from "./technology-evidence-role.mjs";
|
|
11
|
+
import { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, TECHNOLOGY_PROFILE_VERSION, } from "./lookup-child-profile-contract.mjs";
|
|
11
12
|
const MAX_TECHNOLOGY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
|
|
12
13
|
const MAX_TECHNOLOGY_TAG_LENGTH = MAX_TAG_LENGTH;
|
|
13
|
-
const MAX_TECHNOLOGY_FINDINGS = 24;
|
|
14
|
-
const MAX_EVIDENCE_PER_TECHNOLOGY = 4;
|
|
15
14
|
const MAX_RESOURCE_ORIGINS = 30;
|
|
16
15
|
const MAX_GENERATOR_INPUT = 160;
|
|
17
16
|
const MAX_SERVER_INPUT = 240;
|
|
18
17
|
const MAX_PASSIVE_RESPONSE_HEADERS = 8;
|
|
19
|
-
const MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH = 180;
|
|
20
18
|
const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]/;
|
|
21
19
|
function boundedLowercase(value, maxLength) {
|
|
22
20
|
if (typeof value !== 'string' || value.length > maxLength || CONTROL_CHARACTER_RE.test(value))
|
|
@@ -51,14 +49,37 @@ const PASSIVE_TECHNOLOGY_HEADER_NAMES = Object.freeze([
|
|
|
51
49
|
'x-vercel-id',
|
|
52
50
|
]);
|
|
53
51
|
const PASSIVE_HEADER_NAMES = new Set(PASSIVE_TECHNOLOGY_HEADER_NAMES);
|
|
54
|
-
function
|
|
55
|
-
|
|
52
|
+
function technologyHeaderValue(value) {
|
|
53
|
+
if (typeof value !== 'string' || value.length > 240 || CONTROL_CHARACTER_RE.test(value))
|
|
54
|
+
return '';
|
|
55
|
+
return value.trim();
|
|
56
|
+
}
|
|
57
|
+
function captureTechnologyResponseHeaders(value) {
|
|
58
|
+
const output = {};
|
|
59
|
+
const getter = value && typeof value === 'object' && typeof value.get === 'function'
|
|
60
|
+
? value.get.bind(value)
|
|
61
|
+
: null;
|
|
62
|
+
const record = value && typeof value === 'object' && !Array.isArray(value)
|
|
56
63
|
? value
|
|
57
|
-
:
|
|
64
|
+
: null;
|
|
65
|
+
for (const name of PASSIVE_TECHNOLOGY_HEADER_NAMES) {
|
|
66
|
+
let rawValue;
|
|
67
|
+
try {
|
|
68
|
+
rawValue = getter ? getter(name) : record?.[name];
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
rawValue = undefined;
|
|
72
|
+
}
|
|
73
|
+
const headerValue = technologyHeaderValue(rawValue);
|
|
74
|
+
if (headerValue)
|
|
75
|
+
output[name] = headerValue;
|
|
76
|
+
}
|
|
77
|
+
return output;
|
|
78
|
+
}
|
|
79
|
+
function normalizedResponseHeaders(value) {
|
|
58
80
|
const output = new Map();
|
|
59
|
-
for (const [
|
|
60
|
-
|
|
61
|
-
if (!PASSIVE_HEADER_NAMES.has(name) || typeof rawValue !== 'string')
|
|
81
|
+
for (const [name, rawValue] of Object.entries(captureTechnologyResponseHeaders(value))) {
|
|
82
|
+
if (!PASSIVE_HEADER_NAMES.has(name))
|
|
62
83
|
continue;
|
|
63
84
|
const normalized = boundedLowercase(rawValue, 240);
|
|
64
85
|
if (!normalized)
|
|
@@ -121,6 +142,18 @@ function responseHeaderEvidence(name, pattern, description, confidence = 'high')
|
|
|
121
142
|
},
|
|
122
143
|
};
|
|
123
144
|
}
|
|
145
|
+
function evidenceRole(signature, evidence) {
|
|
146
|
+
if (evidence.role)
|
|
147
|
+
return evidence.role;
|
|
148
|
+
if (evidence.source === 'resource origin')
|
|
149
|
+
return 'embedded_dependency';
|
|
150
|
+
if (signature.category === 'delivery platform')
|
|
151
|
+
return 'observed_edge';
|
|
152
|
+
if (['application runtime', 'web framework', 'static site generator', 'web server'].includes(signature.category)) {
|
|
153
|
+
return 'framework_runtime';
|
|
154
|
+
}
|
|
155
|
+
return 'application_platform';
|
|
156
|
+
}
|
|
124
157
|
const TECHNOLOGY_SIGNATURES = [
|
|
125
158
|
{
|
|
126
159
|
id: 'wordpress', name: 'WordPress', category: 'content management',
|
|
@@ -325,14 +358,14 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
325
358
|
id: 'netlify', name: 'Netlify', category: 'delivery platform',
|
|
326
359
|
evidence: [
|
|
327
360
|
serverEvidence(/^netlify(?:\s|$|\/)/i, 'The selected response server header identifies Netlify.'),
|
|
328
|
-
responseHeaderEvidence('x-nf-request-id', null, 'A Netlify
|
|
361
|
+
{ ...responseHeaderEvidence('x-nf-request-id', null, 'A Netlify application-platform response header was observed.', 'medium'), role: 'application_platform' },
|
|
329
362
|
],
|
|
330
363
|
},
|
|
331
364
|
{
|
|
332
365
|
id: 'vercel', name: 'Vercel', category: 'delivery platform',
|
|
333
366
|
evidence: [
|
|
334
367
|
serverEvidence(/^vercel(?:\s|$|\/)/i, 'The selected response server header identifies Vercel.'),
|
|
335
|
-
responseHeaderEvidence('x-vercel-id', null, 'A Vercel
|
|
368
|
+
{ ...responseHeaderEvidence('x-vercel-id', null, 'A Vercel application-platform response header was observed.', 'medium'), role: 'application_platform' },
|
|
336
369
|
],
|
|
337
370
|
},
|
|
338
371
|
{
|
|
@@ -366,19 +399,26 @@ const TECHNOLOGY_SIGNATURE_CATALOGUE = Object.freeze(TECHNOLOGY_SIGNATURES.map((
|
|
|
366
399
|
category: signature.category,
|
|
367
400
|
minimumEvidenceMatches: signature.minimumEvidenceMatches || 1,
|
|
368
401
|
requiresNonResourceEvidence: signature.requiresNonResourceEvidence === true,
|
|
369
|
-
evidence: Object.freeze(signature.evidence.map((
|
|
370
|
-
source,
|
|
371
|
-
|
|
372
|
-
|
|
402
|
+
evidence: Object.freeze(signature.evidence.map((evidence) => Object.freeze({
|
|
403
|
+
source: evidence.source,
|
|
404
|
+
role: evidenceRole(signature, evidence),
|
|
405
|
+
description: evidence.description,
|
|
406
|
+
confidence: evidence.confidence,
|
|
373
407
|
}))),
|
|
374
408
|
})));
|
|
375
409
|
function analyzeWebsiteTechnology(input = {}) {
|
|
410
|
+
// Existing direct callers pass minimised, already-derived page evidence rather
|
|
411
|
+
// than the page body. The real collector declares false for header-only
|
|
412
|
+
// responses, while an explicit malformed declaration also fails closed.
|
|
413
|
+
const htmlAvailable = input.htmlAvailable === undefined
|
|
414
|
+
? true
|
|
415
|
+
: input.htmlAvailable === true;
|
|
376
416
|
const htmlAnalysis = input.htmlAnalysis ?? analyzeStaticHtml(input.html);
|
|
377
|
-
const browserLibraryProfile = analyzeBrowserLibraries({
|
|
417
|
+
const browserLibraryProfile = htmlAvailable ? analyzeBrowserLibraries({
|
|
378
418
|
htmlAnalysis,
|
|
379
419
|
observedAt: input.observedAt,
|
|
380
420
|
sourceTruncated: input.sourceTruncated,
|
|
381
|
-
});
|
|
421
|
+
}) : null;
|
|
382
422
|
const context = {
|
|
383
423
|
html: htmlAnalysis.markup,
|
|
384
424
|
generator: boundedLowercase(input.generator, MAX_GENERATOR_INPUT),
|
|
@@ -393,12 +433,18 @@ function analyzeWebsiteTechnology(input = {}) {
|
|
|
393
433
|
continue;
|
|
394
434
|
if (signature.requiresNonResourceEvidence && matched.every((evidence) => evidence.source === 'resource origin'))
|
|
395
435
|
continue;
|
|
436
|
+
const matchedEvidence = matched.slice(0, MAX_EVIDENCE_PER_TECHNOLOGY).map((evidence) => ({
|
|
437
|
+
source: evidence.source,
|
|
438
|
+
role: evidenceRole(signature, evidence),
|
|
439
|
+
description: evidence.description,
|
|
440
|
+
}));
|
|
396
441
|
findings.push({
|
|
397
442
|
id: signature.id,
|
|
398
443
|
name: signature.name,
|
|
399
444
|
category: signature.category,
|
|
400
445
|
confidence: matched.some((evidence) => evidence.confidence === 'high') ? 'high' : 'medium',
|
|
401
|
-
|
|
446
|
+
roles: TECHNOLOGY_EVIDENCE_ROLE_ORDER.filter((role) => matchedEvidence.some((evidence) => evidence.role === role)),
|
|
447
|
+
evidence: matchedEvidence,
|
|
402
448
|
});
|
|
403
449
|
}
|
|
404
450
|
findings.sort((left, right) => left.category.localeCompare(right.category) || left.name.localeCompare(right.name));
|
|
@@ -407,11 +453,14 @@ function analyzeWebsiteTechnology(input = {}) {
|
|
|
407
453
|
|| htmlAnalysis.inputLimitReached
|
|
408
454
|
|| htmlAnalysis.tagLimitReached
|
|
409
455
|
|| findingLimitReached;
|
|
456
|
+
const partial = truncated || !htmlAvailable;
|
|
410
457
|
const limitations = [
|
|
411
458
|
'Curated signature matching is selective; an unmatched technology may still be present.',
|
|
412
459
|
'Static response evidence cannot identify JavaScript-rendered or deliberately concealed technologies.',
|
|
413
460
|
'Technology indicators describe observed implementation clues, not ownership, safety, or maliciousness.',
|
|
414
461
|
];
|
|
462
|
+
if (!htmlAvailable)
|
|
463
|
+
limitations.push('HTML page identity was unavailable; only bounded permitted response-header indicators were evaluated.');
|
|
415
464
|
if (input.sourceTruncated === true)
|
|
416
465
|
limitations.push('The captured homepage body was truncated, so technology indicators may be incomplete.');
|
|
417
466
|
if (htmlAnalysis.inputLimitReached)
|
|
@@ -423,16 +472,16 @@ function analyzeWebsiteTechnology(input = {}) {
|
|
|
423
472
|
return {
|
|
424
473
|
profileVersion: TECHNOLOGY_PROFILE_VERSION,
|
|
425
474
|
...createObservation({
|
|
426
|
-
status:
|
|
475
|
+
status: partial ? 'partial' : 'success',
|
|
427
476
|
observedAt: input.observedAt,
|
|
428
477
|
scanMode: 'deep',
|
|
429
478
|
source: 'derived',
|
|
430
|
-
complete: !
|
|
479
|
+
complete: !partial,
|
|
431
480
|
truncated,
|
|
432
481
|
limitations,
|
|
433
482
|
diagnostics: {
|
|
434
483
|
findings: findings.length,
|
|
435
|
-
htmlEvaluated:
|
|
484
|
+
htmlEvaluated: htmlAvailable,
|
|
436
485
|
generatorEvaluated: Boolean(context.generator),
|
|
437
486
|
serverEvaluated: Boolean(context.httpServer),
|
|
438
487
|
resourceOriginsEvaluated: context.resourceHosts.size,
|
|
@@ -444,4 +493,4 @@ function analyzeWebsiteTechnology(input = {}) {
|
|
|
444
493
|
browserLibraryProfile,
|
|
445
494
|
};
|
|
446
495
|
}
|
|
447
|
-
export { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, MAX_TECHNOLOGY_HTML_CHARS, MAX_TECHNOLOGY_TAGS, PASSIVE_TECHNOLOGY_HEADER_NAMES, TECHNOLOGY_PROFILE_VERSION, TECHNOLOGY_SIGNATURE_CATALOGUE, analyzeWebsiteTechnology, };
|
|
496
|
+
export { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, MAX_TECHNOLOGY_HTML_CHARS, MAX_TECHNOLOGY_TAGS, PASSIVE_TECHNOLOGY_HEADER_NAMES, TECHNOLOGY_PROFILE_VERSION, TECHNOLOGY_SIGNATURE_CATALOGUE, analyzeWebsiteTechnology, captureTechnologyResponseHeaders, };
|