@slicedearth/whoisleuth-cli 1.39.1 → 1.43.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/README.md +15 -0
- package/bin/whoisleuth.mjs +13 -2
- package/cli/archive-inspect.mjs +37 -2
- package/cli/arguments.mjs +361 -256
- package/cli/artifact-verify.mjs +35 -1
- package/cli/bulk-checkpoint.mjs +7 -11
- package/cli/bulk-command-runner.mjs +19 -2
- package/cli/case-pack.mjs +136 -0
- package/cli/ci-report.mjs +73 -0
- package/cli/collection-preflight.mjs +59 -0
- package/cli/command-reference.mjs +395 -0
- package/cli/completion.mjs +33 -10
- package/cli/config-profile.mjs +180 -0
- package/cli/ct-event-intake.mjs +159 -0
- package/cli/discovery-scan-command-runner.mjs +15 -1
- package/cli/domain-control-monitor.mjs +104 -0
- package/cli/domain-control-observations.mjs +235 -0
- package/cli/evidence-command-arguments.mjs +11 -0
- package/cli/evidence-command-runner.mjs +1 -0
- package/cli/external-observation-mapping.mjs +162 -0
- package/cli/fail-policy.mjs +64 -0
- package/cli/formatters/html.mjs +1 -1
- package/cli/investigation-manifest.mjs +113 -0
- package/cli/investigation-run.mjs +112 -0
- package/cli/lookup-brief.mjs +116 -0
- package/cli/lookup-command-runner.mjs +8 -1
- package/cli/lookup-diff.mjs +17 -19
- package/cli/network-command-runner.mjs +4 -1
- package/cli/offline-evidence-review.mjs +105 -8
- package/cli/open-asset-model-bridge.mjs +169 -0
- package/cli/registry-cohort.mjs +121 -0
- package/cli/registry-doctor.mjs +106 -11
- package/cli/registry-fixture-scaffold.mjs +73 -0
- package/cli/runner.mjs +368 -306
- package/docs/cli-reference.md +240 -14
- package/docs/cli.md +42 -7
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
- package/frontend/src/lib/analysis/brand-profile-model.js +46 -4
- package/frontend/src/lib/analysis/case-report.js +573 -0
- package/frontend/src/lib/analysis/case-response-model.js +30 -1
- package/frontend/src/lib/analysis/detection-rule-model.js +36 -0
- package/frontend/src/lib/analysis/external-findings-import.js +320 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +7 -0
- package/frontend/src/lib/analysis/lookup-presentation.js +10 -20
- package/frontend/src/lib/analysis/scoring.js +1 -1
- package/frontend/src/lib/analysis/visualization-models.js +2 -3
- package/frontend/src/lib/analysis/workspace-archive.js +2 -0
- package/lib/abusech-auth.mjs +1 -1
- package/lib/availability.mjs +1 -1
- package/lib/bounded-contract-normalizers.mjs +33 -0
- package/lib/dns-convergence-review.mjs +183 -0
- package/lib/dns-delegation-health.mjs +196 -3
- package/lib/domain-change-packet.mjs +133 -0
- package/lib/domain-change-review.mjs +307 -0
- package/lib/domain-control-flight-recorder.mjs +268 -0
- package/lib/domain-portfolio-review.mjs +151 -0
- package/lib/evidence-export.mjs +1 -1
- package/lib/html-signals.mjs +5 -11
- package/lib/nameserver-preflight-review.mjs +159 -0
- package/lib/page-language-signals.mjs +124 -0
- package/lib/risk-scoring.mjs +42 -1
- package/lib/static-html-analysis.mjs +29 -1
- package/lib/trust-store-comparison.mjs +146 -0
- package/lib/website-technology.mjs +24 -10
- package/lib/zone-intent-review.mjs +534 -0
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ const MAX_SCRIPT_REFERENCE_LENGTH = 2_048;
|
|
|
15
15
|
const MAX_SCRIPT_MEDIA_TYPE_LENGTH = 120;
|
|
16
16
|
const MAX_INLINE_SCRIPT_CHARS = 32_768;
|
|
17
17
|
const MAX_INLINE_SCRIPT_TOTAL_CHARS = 65_536;
|
|
18
|
+
const MAX_STATIC_VISIBLE_TEXT_CHARS = MAX_STATIC_HTML_CHARS;
|
|
18
19
|
const MAX_STATIC_FORMS = 50;
|
|
19
20
|
const MAX_STATIC_INPUTS = 500;
|
|
20
21
|
const MAX_FORM_ATTRIBUTE_LENGTH = 2_048;
|
|
@@ -48,6 +49,7 @@ const VOID_TAGS = new Set([
|
|
|
48
49
|
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta',
|
|
49
50
|
'param', 'source', 'track', 'wbr',
|
|
50
51
|
]);
|
|
52
|
+
const NON_VISIBLE_TEXT_TAGS = new Set(['script', 'style', 'template']);
|
|
51
53
|
function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LENGTH) {
|
|
52
54
|
const attribute = attributes
|
|
53
55
|
.slice(0, MAX_ATTRIBUTES_PER_TAG)
|
|
@@ -190,7 +192,9 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
190
192
|
const inputLimitReached = supplied.length > MAX_STATIC_HTML_CHARS;
|
|
191
193
|
const html = supplied.slice(0, MAX_STATIC_HTML_CHARS);
|
|
192
194
|
const baseUrl = safeBaseUrl(options.baseUrl);
|
|
195
|
+
const includeVisibleText = options.includeVisibleText === true;
|
|
193
196
|
const markup = [];
|
|
197
|
+
const visibleTextParts = [];
|
|
194
198
|
const structureTokens = [];
|
|
195
199
|
const scripts = [];
|
|
196
200
|
const cspMetaPolicies = [];
|
|
@@ -211,9 +215,12 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
211
215
|
let structureLimitReached = false;
|
|
212
216
|
let scriptLimitReached = false;
|
|
213
217
|
let inlineLimitReached = false;
|
|
218
|
+
let visibleTextLimitReached = false;
|
|
214
219
|
let cspMetaLimitReached = false;
|
|
215
220
|
let insideExplicitHead = false;
|
|
216
221
|
let scriptElementSeen = false;
|
|
222
|
+
let nonVisibleTextDepth = 0;
|
|
223
|
+
let visibleTextCharacters = 0;
|
|
217
224
|
function appendInlineScript(chars) {
|
|
218
225
|
if (!activeInlineScript || !chars)
|
|
219
226
|
return;
|
|
@@ -234,12 +241,26 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
234
241
|
}
|
|
235
242
|
structureTokens.push(value);
|
|
236
243
|
}
|
|
244
|
+
function appendVisibleText(chars) {
|
|
245
|
+
if (!includeVisibleText || nonVisibleTextDepth > 0 || !chars)
|
|
246
|
+
return;
|
|
247
|
+
const remaining = MAX_STATIC_VISIBLE_TEXT_CHARS - visibleTextCharacters;
|
|
248
|
+
const retainedLength = Math.max(0, Math.min(chars.length, remaining));
|
|
249
|
+
if (retainedLength > 0) {
|
|
250
|
+
visibleTextParts.push(chars.slice(0, retainedLength));
|
|
251
|
+
visibleTextCharacters += retainedLength;
|
|
252
|
+
}
|
|
253
|
+
if (retainedLength < chars.length)
|
|
254
|
+
visibleTextLimitReached = true;
|
|
255
|
+
}
|
|
237
256
|
const handler = {
|
|
238
257
|
onStartTag(token) {
|
|
239
258
|
const tagName = token.tagName.toLowerCase();
|
|
240
259
|
const rawTextMode = RAW_TEXT_MODES[tagName];
|
|
241
260
|
if (!token.selfClosing && rawTextMode !== undefined)
|
|
242
261
|
tokenizer.state = rawTextMode;
|
|
262
|
+
if (!token.selfClosing && NON_VISIBLE_TEXT_TAGS.has(tagName))
|
|
263
|
+
nonVisibleTextDepth += 1;
|
|
243
264
|
if (tagsExamined >= MAX_STATIC_HTML_TAGS) {
|
|
244
265
|
tagLimitReached = true;
|
|
245
266
|
activeInlineScript = null;
|
|
@@ -332,15 +353,20 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
332
353
|
insideExplicitHead = false;
|
|
333
354
|
if (!VOID_TAGS.has(tagName))
|
|
334
355
|
appendStructureToken(`/${tagName}`);
|
|
356
|
+
if (NON_VISIBLE_TEXT_TAGS.has(tagName) && nonVisibleTextDepth > 0)
|
|
357
|
+
nonVisibleTextDepth -= 1;
|
|
335
358
|
},
|
|
336
359
|
onCharacter(token) {
|
|
337
360
|
appendInlineScript(token.chars);
|
|
361
|
+
appendVisibleText(token.chars);
|
|
338
362
|
},
|
|
339
363
|
onNullCharacter(token) {
|
|
340
364
|
appendInlineScript(token.chars);
|
|
365
|
+
appendVisibleText(token.chars);
|
|
341
366
|
},
|
|
342
367
|
onWhitespaceCharacter(token) {
|
|
343
368
|
appendInlineScript(token.chars);
|
|
369
|
+
appendVisibleText(token.chars);
|
|
344
370
|
},
|
|
345
371
|
onComment() { },
|
|
346
372
|
onDoctype() { },
|
|
@@ -350,6 +376,7 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
350
376
|
tokenizer.write(html, true);
|
|
351
377
|
return {
|
|
352
378
|
markup: markup.join('\n'),
|
|
379
|
+
visibleText: visibleTextParts.join(''),
|
|
353
380
|
structureTokens,
|
|
354
381
|
scripts,
|
|
355
382
|
cspMetaPolicies,
|
|
@@ -360,8 +387,9 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
360
387
|
structureLimitReached,
|
|
361
388
|
scriptLimitReached,
|
|
362
389
|
inlineLimitReached,
|
|
390
|
+
visibleTextLimitReached,
|
|
363
391
|
tagsExamined,
|
|
364
392
|
inlineCharactersExamined,
|
|
365
393
|
};
|
|
366
394
|
}
|
|
367
|
-
export { MAX_ATTRIBUTES_PER_TAG, MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_MEDIA_TYPE_LENGTH, MAX_STATIC_FORMS, MAX_STATIC_HTML_CHARS, MAX_STATIC_INPUTS, MAX_STATIC_HTML_TAGS, MAX_STATIC_STRUCTURE_TOKENS, MAX_TAG_LENGTH, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, };
|
|
395
|
+
export { MAX_ATTRIBUTES_PER_TAG, MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_MEDIA_TYPE_LENGTH, MAX_STATIC_FORMS, MAX_STATIC_HTML_CHARS, MAX_STATIC_INPUTS, MAX_STATIC_HTML_TAGS, MAX_STATIC_STRUCTURE_TOKENS, MAX_STATIC_VISIBLE_TEXT_CHARS, MAX_TAG_LENGTH, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { enumValue, exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "./bounded-contract-normalizers.mjs";
|
|
2
|
+
export const TRUST_STORE_COMPARISON_INPUT_SCHEMA = 'whoisleuth.trust-store-comparison.input';
|
|
3
|
+
export const TRUST_STORE_COMPARISON_REVIEW_SCHEMA = 'whoisleuth.trust-store-comparison.review';
|
|
4
|
+
export const TRUST_STORE_COMPARISON_REVIEW_VERSION = 1;
|
|
5
|
+
export const MAX_TRUST_STORE_COMPARISON_STORES = 16;
|
|
6
|
+
export const MAX_TRUST_STORE_ANCHORS = 2_048;
|
|
7
|
+
export const MAX_TRUST_STORE_CHAIN_CERTIFICATES = 16;
|
|
8
|
+
const ROOT_KEYS = new Set(['schema', 'version', 'certificate', 'stores']);
|
|
9
|
+
const CERTIFICATE_KEYS = new Set([
|
|
10
|
+
'source', 'observedAt', 'state', 'chainTruncated',
|
|
11
|
+
'fingerprintsSha256', 'runtimeAuthorisation',
|
|
12
|
+
]);
|
|
13
|
+
const STORE_KEYS = new Set(['name', 'version', 'source', 'reviewedAt', 'state', 'anchorsSha256']);
|
|
14
|
+
const EVIDENCE_STATES = new Set(['observed', 'partial', 'unavailable']);
|
|
15
|
+
const RUNTIME_AUTHORISATION = new Set(['authorised', 'unauthorised', 'unknown']);
|
|
16
|
+
const SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
17
|
+
function digest(value, label) {
|
|
18
|
+
const normalised = requireBoundedString(value, label, 64).toLowerCase();
|
|
19
|
+
if (!SHA256_RE.test(normalised))
|
|
20
|
+
throw new TypeError(`${label} must be a SHA-256 hexadecimal digest.`);
|
|
21
|
+
return normalised;
|
|
22
|
+
}
|
|
23
|
+
function digests(value, label, maximum, sort = true) {
|
|
24
|
+
if (!Array.isArray(value) || value.length > maximum) {
|
|
25
|
+
throw new TypeError(`${label} must contain no more than ${maximum} SHA-256 digests.`);
|
|
26
|
+
}
|
|
27
|
+
const values = [...new Set(value.map((entry, index) => digest(entry, `${label}[${index}]`)))];
|
|
28
|
+
return Object.freeze(sort ? values.sort() : values);
|
|
29
|
+
}
|
|
30
|
+
export function compareTrustStoreEvidence(inputRaw, generatedAtValue = new Date().toISOString()) {
|
|
31
|
+
const input = requireRecord(inputRaw, 'Trust-store comparison input');
|
|
32
|
+
if (input.schema !== TRUST_STORE_COMPARISON_INPUT_SCHEMA || input.version !== 1) {
|
|
33
|
+
throw new TypeError(`Trust-store comparison input must use ${TRUST_STORE_COMPARISON_INPUT_SCHEMA} version 1.`);
|
|
34
|
+
}
|
|
35
|
+
exactKeys(input, ROOT_KEYS, 'Trust-store comparison input');
|
|
36
|
+
const certificateInput = requireRecord(input.certificate, 'certificate');
|
|
37
|
+
exactKeys(certificateInput, CERTIFICATE_KEYS, 'certificate');
|
|
38
|
+
const certificateState = enumValue(certificateInput.state, EVIDENCE_STATES, 'certificate.state');
|
|
39
|
+
if (typeof certificateInput.chainTruncated !== 'boolean')
|
|
40
|
+
throw new TypeError('certificate.chainTruncated must be a Boolean.');
|
|
41
|
+
const certificateFingerprints = digests(certificateInput.fingerprintsSha256, 'certificate.fingerprintsSha256', MAX_TRUST_STORE_CHAIN_CERTIFICATES, false);
|
|
42
|
+
if (certificateState === 'unavailable' && certificateFingerprints.length) {
|
|
43
|
+
throw new TypeError('An unavailable certificate observation cannot contain fingerprints.');
|
|
44
|
+
}
|
|
45
|
+
if (certificateState !== 'unavailable' && !certificateFingerprints.length) {
|
|
46
|
+
throw new TypeError('An observed or partial certificate observation requires at least one fingerprint.');
|
|
47
|
+
}
|
|
48
|
+
const certificate = Object.freeze({
|
|
49
|
+
source: requireBoundedString(certificateInput.source, 'certificate.source', 240),
|
|
50
|
+
observedAt: requireIsoTimestamp(certificateInput.observedAt, 'certificate.observedAt'),
|
|
51
|
+
state: certificateState,
|
|
52
|
+
chainTruncated: certificateInput.chainTruncated,
|
|
53
|
+
leafSha256: certificateFingerprints[0] ?? null,
|
|
54
|
+
fingerprintsSha256: certificateFingerprints,
|
|
55
|
+
runtimeAuthorisation: enumValue(certificateInput.runtimeAuthorisation, RUNTIME_AUTHORISATION, 'certificate.runtimeAuthorisation'),
|
|
56
|
+
});
|
|
57
|
+
if (!Array.isArray(input.stores) || input.stores.length < 1 || input.stores.length > MAX_TRUST_STORE_COMPARISON_STORES) {
|
|
58
|
+
throw new TypeError(`stores must contain between 1 and ${MAX_TRUST_STORE_COMPARISON_STORES} entries.`);
|
|
59
|
+
}
|
|
60
|
+
const storeKeys = new Set();
|
|
61
|
+
const stores = input.stores.map((raw, index) => {
|
|
62
|
+
const store = requireRecord(raw, `stores[${index}]`);
|
|
63
|
+
exactKeys(store, STORE_KEYS, `stores[${index}]`);
|
|
64
|
+
const name = requireBoundedString(store.name, `stores[${index}].name`, 120);
|
|
65
|
+
const version = requireBoundedString(store.version, `stores[${index}].version`, 120);
|
|
66
|
+
const key = `${name}\u0000${version}`;
|
|
67
|
+
if (storeKeys.has(key))
|
|
68
|
+
throw new TypeError(`stores[${index}] repeats a store name and version.`);
|
|
69
|
+
storeKeys.add(key);
|
|
70
|
+
const storeState = enumValue(store.state, EVIDENCE_STATES, `stores[${index}].state`);
|
|
71
|
+
const anchors = digests(store.anchorsSha256, `stores[${index}].anchorsSha256`, MAX_TRUST_STORE_ANCHORS);
|
|
72
|
+
if (storeState === 'unavailable' && anchors.length)
|
|
73
|
+
throw new TypeError(`stores[${index}] cannot contain anchors when unavailable.`);
|
|
74
|
+
if (storeState === 'observed' && !anchors.length)
|
|
75
|
+
throw new TypeError(`stores[${index}] requires at least one anchor when observed.`);
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
name,
|
|
78
|
+
version,
|
|
79
|
+
source: requireBoundedString(store.source, `stores[${index}].source`, 240),
|
|
80
|
+
reviewedAt: requireIsoTimestamp(store.reviewedAt, `stores[${index}].reviewedAt`),
|
|
81
|
+
state: storeState,
|
|
82
|
+
anchors,
|
|
83
|
+
});
|
|
84
|
+
}).sort((left, right) => left.name.localeCompare(right.name) || left.version.localeCompare(right.version));
|
|
85
|
+
const chainSet = new Set(certificate.fingerprintsSha256);
|
|
86
|
+
const comparisons = stores.map((store) => {
|
|
87
|
+
const matchedAnchorSha256 = store.anchors.filter((anchor) => chainSet.has(anchor));
|
|
88
|
+
const completeInputs = certificate.state === 'observed'
|
|
89
|
+
&& certificate.chainTruncated === false
|
|
90
|
+
&& store.state === 'observed';
|
|
91
|
+
const state = matchedAnchorSha256.length
|
|
92
|
+
? 'anchor_observed'
|
|
93
|
+
: completeInputs
|
|
94
|
+
? 'not_observed'
|
|
95
|
+
: 'inconclusive';
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
store: Object.freeze({
|
|
98
|
+
name: store.name,
|
|
99
|
+
version: store.version,
|
|
100
|
+
source: store.source,
|
|
101
|
+
reviewedAt: store.reviewedAt,
|
|
102
|
+
state: store.state,
|
|
103
|
+
anchorCount: store.anchors.length,
|
|
104
|
+
}),
|
|
105
|
+
state,
|
|
106
|
+
matchedAnchorSha256: Object.freeze(matchedAnchorSha256),
|
|
107
|
+
explanation: state === 'anchor_observed'
|
|
108
|
+
? 'At least one supplied chain fingerprint exactly matched a supplied trust-anchor fingerprint.'
|
|
109
|
+
: state === 'not_observed'
|
|
110
|
+
? 'No supplied chain fingerprint matched this complete supplied trust-store snapshot.'
|
|
111
|
+
: 'Certificate-chain or trust-store evidence was incomplete or unavailable, so non-observation cannot be established.',
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
const incomplete = certificate.state !== 'observed'
|
|
115
|
+
|| certificate.chainTruncated
|
|
116
|
+
|| stores.some((store) => store.state !== 'observed');
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
schema: TRUST_STORE_COMPARISON_REVIEW_SCHEMA,
|
|
119
|
+
version: TRUST_STORE_COMPARISON_REVIEW_VERSION,
|
|
120
|
+
generatedAt: requireIsoTimestamp(generatedAtValue, 'generatedAt'),
|
|
121
|
+
state: incomplete ? 'partial' : 'complete',
|
|
122
|
+
certificate: Object.freeze({
|
|
123
|
+
source: certificate.source,
|
|
124
|
+
observedAt: certificate.observedAt,
|
|
125
|
+
state: certificate.state,
|
|
126
|
+
chainTruncated: certificate.chainTruncated,
|
|
127
|
+
leafSha256: certificate.leafSha256,
|
|
128
|
+
chainFingerprintCount: certificate.fingerprintsSha256.length,
|
|
129
|
+
runtimeAuthorisation: certificate.runtimeAuthorisation,
|
|
130
|
+
}),
|
|
131
|
+
comparisons: Object.freeze(comparisons),
|
|
132
|
+
counts: Object.freeze({
|
|
133
|
+
stores: comparisons.length,
|
|
134
|
+
anchorObserved: comparisons.filter((comparison) => comparison.state === 'anchor_observed').length,
|
|
135
|
+
notObserved: comparisons.filter((comparison) => comparison.state === 'not_observed').length,
|
|
136
|
+
inconclusive: comparisons.filter((comparison) => comparison.state === 'inconclusive').length,
|
|
137
|
+
}),
|
|
138
|
+
limitations: Object.freeze([
|
|
139
|
+
'This local comparison uses only supplied SHA-256 fingerprints and makes no TLS, certificate-authority, operating-system, or trust-store request.',
|
|
140
|
+
'An exact anchor fingerprint intersection does not build or validate a certificate path, check signatures, names, validity, constraints, revocation, policy, or application-specific trust.',
|
|
141
|
+
'A complete supplied chain can omit its root certificate. A not-observed result therefore does not mean the runtime, browser, operating system, or another client would reject the certificate.',
|
|
142
|
+
'Trust stores differ by product, platform, version, policy, update state, and local administration. The analyst-supplied source and version remain part of every comparison.',
|
|
143
|
+
'Runtime authorisation is preserved as separate context and is not overridden by this fingerprint comparison.',
|
|
144
|
+
]),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { analyzeBrowserLibraries } from "./browser-library-profile.mjs";
|
|
8
8
|
import { createObservation } from "./observation.mjs";
|
|
9
9
|
import { MAX_STATIC_HTML_CHARS, MAX_TAG_LENGTH, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
10
|
-
const TECHNOLOGY_PROFILE_VERSION =
|
|
10
|
+
const TECHNOLOGY_PROFILE_VERSION = 8;
|
|
11
11
|
const MAX_TECHNOLOGY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
|
|
12
12
|
const MAX_TECHNOLOGY_TAG_LENGTH = MAX_TAG_LENGTH;
|
|
13
13
|
const MAX_TECHNOLOGY_FINDINGS = 24;
|
|
@@ -40,15 +40,17 @@ function normalizedResourceHosts(value) {
|
|
|
40
40
|
}
|
|
41
41
|
return hosts;
|
|
42
42
|
}
|
|
43
|
-
const
|
|
43
|
+
const PASSIVE_TECHNOLOGY_HEADER_NAMES = Object.freeze([
|
|
44
44
|
'cf-ray',
|
|
45
45
|
'x-drupal-cache',
|
|
46
|
-
'x-
|
|
46
|
+
'x-served-by',
|
|
47
|
+
'x-nf-request-id',
|
|
47
48
|
'x-powered-by',
|
|
48
49
|
'x-shopify-stage',
|
|
49
50
|
'x-sorting-hat-podid',
|
|
50
51
|
'x-vercel-id',
|
|
51
52
|
]);
|
|
53
|
+
const PASSIVE_HEADER_NAMES = new Set(PASSIVE_TECHNOLOGY_HEADER_NAMES);
|
|
52
54
|
function normalizedResponseHeaders(value) {
|
|
53
55
|
const input = value && typeof value === 'object' && !Array.isArray(value)
|
|
54
56
|
? value
|
|
@@ -148,7 +150,10 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
148
150
|
},
|
|
149
151
|
{
|
|
150
152
|
id: 'craft-cms', name: 'Craft CMS', category: 'content management',
|
|
151
|
-
evidence: [
|
|
153
|
+
evidence: [
|
|
154
|
+
generatorEvidence(/^craft cms(?:\s|$)/i, 'Generator metadata identifies Craft CMS.'),
|
|
155
|
+
responseHeaderEvidence('x-powered-by', /(?:^|,\s*)craft cms(?:\s|$|\/)/i, 'The passive X-Powered-By response header identifies Craft CMS.'),
|
|
156
|
+
],
|
|
152
157
|
},
|
|
153
158
|
{
|
|
154
159
|
id: 'typo3', name: 'TYPO3 CMS', category: 'content management',
|
|
@@ -198,17 +203,23 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
198
203
|
},
|
|
199
204
|
{
|
|
200
205
|
id: 'opencart', name: 'OpenCart', category: 'commerce',
|
|
201
|
-
evidence: [
|
|
206
|
+
evidence: [
|
|
207
|
+
generatorEvidence(/^opencart(?:\s|$)/i, 'Generator metadata identifies OpenCart.'),
|
|
208
|
+
htmlEvidence(['index.php?route=common/home', 'image/catalog/opencart-logo.png'], 'Static markup contains OpenCart routing or default asset conventions.'),
|
|
209
|
+
],
|
|
202
210
|
},
|
|
203
211
|
{
|
|
204
212
|
id: 'prestashop', name: 'PrestaShop', category: 'commerce',
|
|
205
|
-
evidence: [
|
|
213
|
+
evidence: [
|
|
214
|
+
generatorEvidence(/^prestashop(?:\s|$)/i, 'Generator metadata identifies PrestaShop.'),
|
|
215
|
+
htmlEvidence(['/modules/ps_'], 'Static resource paths use PrestaShop module conventions.'),
|
|
216
|
+
],
|
|
206
217
|
},
|
|
207
218
|
{
|
|
208
219
|
id: 'wix', name: 'Wix', category: 'site builder',
|
|
209
220
|
requiresNonResourceEvidence: true,
|
|
210
221
|
evidence: [
|
|
211
|
-
generatorEvidence(/^wix(?:\s|$)/i, 'Generator metadata identifies Wix.'),
|
|
222
|
+
generatorEvidence(/^wix(?:\.com)?(?:\s|$)/i, 'Generator metadata identifies Wix.'),
|
|
212
223
|
htmlEvidence(['data-mesh-id='], 'Static markup contains a Wix-specific document attribute.', 'medium'),
|
|
213
224
|
resourceEvidence(['static.parastorage.com', 'wixstatic.com'], 'A retained resource origin uses Wix delivery infrastructure.'),
|
|
214
225
|
],
|
|
@@ -316,7 +327,10 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
316
327
|
},
|
|
317
328
|
{
|
|
318
329
|
id: 'netlify', name: 'Netlify', category: 'delivery platform',
|
|
319
|
-
evidence: [
|
|
330
|
+
evidence: [
|
|
331
|
+
serverEvidence(/^netlify(?:\s|$|\/)/i, 'The selected response server header identifies Netlify.'),
|
|
332
|
+
responseHeaderEvidence('x-nf-request-id', null, 'A Netlify request identifier response header was observed.', 'medium'),
|
|
333
|
+
],
|
|
320
334
|
},
|
|
321
335
|
{
|
|
322
336
|
id: 'vercel', name: 'Vercel', category: 'delivery platform',
|
|
@@ -327,7 +341,7 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
327
341
|
},
|
|
328
342
|
{
|
|
329
343
|
id: 'fastly', name: 'Fastly', category: 'delivery platform',
|
|
330
|
-
evidence: [responseHeaderEvidence('x-
|
|
344
|
+
evidence: [responseHeaderEvidence('x-served-by', /(?:^|,\s*)cache-[a-z0-9-]+-[a-z]{3}(?:\s*,|$)/i, 'The passive X-Served-By response header contains a Fastly cache-node identifier.', 'medium')],
|
|
331
345
|
},
|
|
332
346
|
{
|
|
333
347
|
id: 'nginx', name: 'nginx', category: 'web server',
|
|
@@ -434,4 +448,4 @@ function analyzeWebsiteTechnology(input = {}) {
|
|
|
434
448
|
browserLibraryProfile,
|
|
435
449
|
};
|
|
436
450
|
}
|
|
437
|
-
export { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, MAX_TECHNOLOGY_HTML_CHARS, MAX_TECHNOLOGY_TAGS, TECHNOLOGY_PROFILE_VERSION, TECHNOLOGY_SIGNATURE_CATALOGUE, analyzeWebsiteTechnology, };
|
|
451
|
+
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, };
|