@slicedearth/whoisleuth-cli 2.1.0 → 2.3.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 +2 -1
- package/cli/arguments.mjs +389 -1745
- package/cli/artifact-validation/case-response.mjs +55 -30
- package/cli/artifact-validation/signed-review.mjs +30 -12
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/case-pack.mjs +39 -92
- package/cli/command-argument-grammar.mjs +211 -0
- package/cli/command-reference.mjs +200 -213
- package/cli/completion.mjs +5 -1
- package/cli/discovery-observation-snapshot.mjs +16 -3
- package/cli/discovery-scan-command-runner.mjs +19 -0
- package/cli/discovery-scan.mjs +6 -0
- package/cli/domain-control-observations.mjs +1 -1
- package/cli/inline-command-families.mjs +1 -0
- package/cli/input.mjs +116 -0
- package/cli/interchange-report.mjs +14 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/mail-header-review.mjs +467 -0
- package/cli/review-command-runner.mjs +25 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +7 -1
- package/docs/cli-reference.md +8 -1
- package/docs/cli.md +24 -1
- package/lib/bounded-json.mjs +9 -6
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-intelligence.mjs +37 -3
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/domain-change-packet.mjs +42 -14
- package/lib/domain-control-flight-recorder.mjs +4 -1
- package/lib/domain-control-manifest.mjs +16 -16
- package/lib/domain-posture-parsers.mjs +114 -3
- package/lib/domain-posture.mjs +34 -3
- package/lib/evidence-export.mjs +63 -13
- package/lib/evidence-report.mjs +2 -2
- package/lib/html-signals.mjs +39 -33
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-response.mjs +5 -4
- package/lib/lookup-source-progress.mjs +27 -4
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/registry-insights.mjs +35 -22
- package/lib/risk-calibration-summary.mjs +1 -3
- package/lib/security-txt.mjs +33 -1
- package/lib/static-html-analysis.mjs +69 -13
- package/lib/website-technology.mjs +79 -11
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +1 -1
- package/packages/cases/case-migration-model.mjs +71 -22
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +10 -21
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +240 -3
- package/packages/cases/case-record-projection.mjs +218 -0
- package/packages/cases/case-report.mjs +11 -6
- package/packages/cases/case-response-model.mjs +43 -6
- package/packages/cases/case-response-packet.mjs +78 -27
- package/packages/cases/case-storage-model.mjs +6 -2
- package/packages/comparison/comparison-ledger-bulk.mjs +9 -10
- package/packages/comparison/page-similarity.mjs +3 -1
- package/packages/contracts/case-portability.mjs +183 -53
- package/packages/contracts/cli-command-semantics.mjs +93 -0
- package/packages/contracts/risk-calibration.mjs +15 -10
- package/packages/contracts/workspace-portability.mjs +44 -7
- package/packages/evidence/domain-control-runtime.mjs +46 -6
- package/packages/interchange/external-findings-import.mjs +115 -46
- package/packages/investigation/lookup-asset-graph.mjs +35 -27
- package/packages/workspace/brand-profile-model.mjs +118 -2
- package/packages/workspace/bulk-review-model.mjs +13 -7
- package/packages/workspace/bulk-session-model.mjs +5 -4
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/relationship-observation-model.mjs +4 -1
- package/packages/workspace/website-snapshot-model.mjs +106 -8
- package/packages/workspace/workspace-archive.mjs +2 -0
- package/cli/evidence-command-arguments.mjs +0 -156
package/lib/security-txt.mjs
CHANGED
|
@@ -50,6 +50,38 @@ function normalizedPublishedUri(value, schemes) {
|
|
|
50
50
|
return null;
|
|
51
51
|
if (url.protocol === 'https:' && !url.hostname)
|
|
52
52
|
return null;
|
|
53
|
+
if (url.protocol === 'mailto:') {
|
|
54
|
+
let recipients;
|
|
55
|
+
try {
|
|
56
|
+
recipients = decodeURIComponent(url.pathname).split(',');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (!recipients.length || recipients.some((recipient) => {
|
|
62
|
+
const separator = recipient.lastIndexOf('@');
|
|
63
|
+
if (separator <= 0 || separator === recipient.length - 1)
|
|
64
|
+
return true;
|
|
65
|
+
const local = recipient.slice(0, separator);
|
|
66
|
+
const mailDomain = recipient.slice(separator + 1).toLowerCase();
|
|
67
|
+
return !/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+$/iu.test(local)
|
|
68
|
+
|| local.startsWith('.') || local.endsWith('.') || local.includes('..')
|
|
69
|
+
|| !canonicalHostname(mailDomain);
|
|
70
|
+
}))
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (url.protocol === 'tel:') {
|
|
74
|
+
let number;
|
|
75
|
+
try {
|
|
76
|
+
number = decodeURIComponent(url.pathname);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (!/^\+?[0-9(). -]+(?:;[a-z0-9-]+=[a-z0-9.-]+)*$/iu.test(number)
|
|
82
|
+
|| (number.match(/[0-9]/gu)?.length ?? 0) < 3)
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
53
85
|
url.search = '';
|
|
54
86
|
url.hash = '';
|
|
55
87
|
return url.toString().slice(0, MAX_SECURITY_TXT_URL_LENGTH);
|
|
@@ -211,7 +243,7 @@ function parseSecurityTxt(text, options = {}) {
|
|
|
211
243
|
const finalComparisonUrl = options.finalUrl ? normalizedCanonicalComparisonUrl(options.finalUrl) : null;
|
|
212
244
|
const canonicalMatches = boundedCanonical.values.length && finalUrl && finalComparisonUrl
|
|
213
245
|
? canonicalComparison.slice(0, MAX_SECURITY_TXT_VALUES)
|
|
214
|
-
.some((value) => value
|
|
246
|
+
.some((value) => value === finalComparisonUrl)
|
|
215
247
|
: null;
|
|
216
248
|
const stale = Boolean(expiresAt && Date.parse(expiresAt) <= (options.now ?? Date.now()));
|
|
217
249
|
const requiredMalformed = boundedContacts.values.length === 0 || expires.length !== 1;
|
|
@@ -73,7 +73,7 @@ const JAVASCRIPT_MEDIA_TYPES = new Set([
|
|
|
73
73
|
'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4',
|
|
74
74
|
'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript',
|
|
75
75
|
]);
|
|
76
|
-
function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LENGTH) {
|
|
76
|
+
function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LENGTH, lowercase = true) {
|
|
77
77
|
const attribute = attributes
|
|
78
78
|
.slice(0, MAX_ATTRIBUTES_PER_TAG)
|
|
79
79
|
.find((candidate) => candidate.name.toLowerCase() === name);
|
|
@@ -84,7 +84,7 @@ function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LEN
|
|
|
84
84
|
}
|
|
85
85
|
return {
|
|
86
86
|
present: true,
|
|
87
|
-
value: attribute.value.trim().toLowerCase(),
|
|
87
|
+
value: lowercase ? attribute.value.trim().toLowerCase() : attribute.value.trim(),
|
|
88
88
|
truncated: attributes.length > MAX_ATTRIBUTES_PER_TAG,
|
|
89
89
|
};
|
|
90
90
|
}
|
|
@@ -116,8 +116,8 @@ function formMethod(attributes) {
|
|
|
116
116
|
truncated: method.truncated,
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
-
function formAction(attributes,
|
|
120
|
-
const action = attributeValue(attributes, 'action');
|
|
119
|
+
function formAction(attributes, resolutionBase, documentOrigin) {
|
|
120
|
+
const action = attributeValue(attributes, 'action', MAX_FORM_ATTRIBUTE_LENGTH, false);
|
|
121
121
|
if (!action.present && action.truncated) {
|
|
122
122
|
return { relationship: 'unclassified', cleartext: false, truncated: true };
|
|
123
123
|
}
|
|
@@ -127,16 +127,16 @@ function formAction(attributes, baseUrl) {
|
|
|
127
127
|
if (action.value === null) {
|
|
128
128
|
return { relationship: 'unclassified', cleartext: false, truncated: true };
|
|
129
129
|
}
|
|
130
|
-
if (!
|
|
130
|
+
if (!resolutionBase || !documentOrigin) {
|
|
131
131
|
return { relationship: 'unclassified', cleartext: false, truncated: action.truncated };
|
|
132
132
|
}
|
|
133
133
|
try {
|
|
134
|
-
const parsed = new URL(action.value,
|
|
134
|
+
const parsed = new URL(action.value, resolutionBase);
|
|
135
135
|
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname) {
|
|
136
136
|
return { relationship: 'unclassified', cleartext: false, truncated: action.truncated };
|
|
137
137
|
}
|
|
138
138
|
return {
|
|
139
|
-
relationship: parsed.origin ===
|
|
139
|
+
relationship: parsed.origin === documentOrigin ? 'sameOrigin' : 'external',
|
|
140
140
|
cleartext: parsed.protocol === 'http:',
|
|
141
141
|
truncated: action.truncated,
|
|
142
142
|
};
|
|
@@ -238,7 +238,9 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
238
238
|
const supplied = typeof value === 'string' ? value : '';
|
|
239
239
|
const inputLimitReached = supplied.length > MAX_STATIC_HTML_CHARS;
|
|
240
240
|
const html = supplied.slice(0, MAX_STATIC_HTML_CHARS);
|
|
241
|
-
const
|
|
241
|
+
const documentUrl = safeBaseUrl(options.baseUrl);
|
|
242
|
+
let effectiveBaseUrl = documentUrl;
|
|
243
|
+
let baseHrefState = 'absent';
|
|
242
244
|
const includeVisibleText = options.includeVisibleText === true;
|
|
243
245
|
const markup = [];
|
|
244
246
|
const visibleTextParts = [];
|
|
@@ -316,6 +318,17 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
316
318
|
let scriptElementSeen = false;
|
|
317
319
|
let nonVisibleTextDepth = 0;
|
|
318
320
|
let visibleTextCharacters = 0;
|
|
321
|
+
let titleDepth = 0;
|
|
322
|
+
let titleCharacters = 0;
|
|
323
|
+
const titleParts = [];
|
|
324
|
+
const pendingFormActions = [];
|
|
325
|
+
function appendTitle(chars) {
|
|
326
|
+
if (titleDepth === 0 || !chars || titleCharacters >= 201)
|
|
327
|
+
return;
|
|
328
|
+
const retained = chars.slice(0, 201 - titleCharacters);
|
|
329
|
+
titleParts.push(retained);
|
|
330
|
+
titleCharacters += retained.length;
|
|
331
|
+
}
|
|
319
332
|
function appendInlineScript(chars) {
|
|
320
333
|
if (!activeInlineScript || !chars)
|
|
321
334
|
return;
|
|
@@ -374,6 +387,29 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
374
387
|
implicitHeadTextDepth += 1;
|
|
375
388
|
}
|
|
376
389
|
const inPublicationHead = insideExplicitHead || (!headScopeClosed && !bodyContentStarted && HEAD_CONTEXT_TAGS.has(tagName));
|
|
390
|
+
if (tagName === 'base' && inPublicationHead && baseHrefState === 'absent') {
|
|
391
|
+
const href = attributeValue(token.attrs, 'href', MAX_FORM_ATTRIBUTE_LENGTH, false);
|
|
392
|
+
if (href.truncated && !href.present) {
|
|
393
|
+
effectiveBaseUrl = documentUrl;
|
|
394
|
+
baseHrefState = 'invalid';
|
|
395
|
+
}
|
|
396
|
+
else if (href.present) {
|
|
397
|
+
try {
|
|
398
|
+
const parsed = href.value !== null && documentUrl ? new URL(href.value, documentUrl) : null;
|
|
399
|
+
if (!parsed || !['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname) {
|
|
400
|
+
throw new Error('Invalid document base URL.');
|
|
401
|
+
}
|
|
402
|
+
effectiveBaseUrl = parsed;
|
|
403
|
+
baseHrefState = 'valid';
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
effectiveBaseUrl = documentUrl;
|
|
407
|
+
baseHrefState = 'invalid';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (tagName === 'title' && titleDepth === 0 && titleParts.length === 0)
|
|
412
|
+
titleDepth = 1;
|
|
377
413
|
if (/^h[1-6]$/u.test(tagName)) {
|
|
378
414
|
const key = tagName;
|
|
379
415
|
publicationMetadata.headings[key] += 1;
|
|
@@ -566,11 +602,8 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
566
602
|
forms.formsObserved += 1;
|
|
567
603
|
const method = formMethod(token.attrs);
|
|
568
604
|
forms.methods[method.value] += 1;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (action.cleartext)
|
|
572
|
-
forms.actions.cleartext += 1;
|
|
573
|
-
if (method.truncated || action.truncated)
|
|
605
|
+
pendingFormActions.push(token.attrs);
|
|
606
|
+
if (method.truncated || token.attrs.length > MAX_ATTRIBUTES_PER_TAG)
|
|
574
607
|
forms.truncated = true;
|
|
575
608
|
}
|
|
576
609
|
}
|
|
@@ -612,6 +645,8 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
612
645
|
const tagName = token.tagName.toLowerCase();
|
|
613
646
|
if (tagName === 'script')
|
|
614
647
|
activeInlineScript = null;
|
|
648
|
+
if (tagName === 'title' && titleDepth > 0)
|
|
649
|
+
titleDepth = 0;
|
|
615
650
|
if (tagName === 'head') {
|
|
616
651
|
insideExplicitHead = false;
|
|
617
652
|
headScopeClosed = true;
|
|
@@ -630,6 +665,7 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
630
665
|
headScopeClosed = true;
|
|
631
666
|
}
|
|
632
667
|
appendInlineScript(token.chars);
|
|
668
|
+
appendTitle(token.chars);
|
|
633
669
|
appendVisibleText(token.chars);
|
|
634
670
|
},
|
|
635
671
|
onNullCharacter(token) {
|
|
@@ -638,10 +674,12 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
638
674
|
headScopeClosed = true;
|
|
639
675
|
}
|
|
640
676
|
appendInlineScript(token.chars);
|
|
677
|
+
appendTitle(token.chars);
|
|
641
678
|
appendVisibleText(token.chars);
|
|
642
679
|
},
|
|
643
680
|
onWhitespaceCharacter(token) {
|
|
644
681
|
appendInlineScript(token.chars);
|
|
682
|
+
appendTitle(token.chars);
|
|
645
683
|
appendVisibleText(token.chars);
|
|
646
684
|
},
|
|
647
685
|
onComment() { },
|
|
@@ -650,6 +688,16 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
650
688
|
};
|
|
651
689
|
tokenizer = new Tokenizer({ sourceCodeLocationInfo: false }, handler);
|
|
652
690
|
tokenizer.write(html, true);
|
|
691
|
+
for (const attributes of pendingFormActions) {
|
|
692
|
+
const action = formAction(attributes, effectiveBaseUrl, documentUrl?.origin ?? null);
|
|
693
|
+
forms.actions[action.relationship] += 1;
|
|
694
|
+
if (action.cleartext)
|
|
695
|
+
forms.actions.cleartext += 1;
|
|
696
|
+
if (action.truncated)
|
|
697
|
+
forms.truncated = true;
|
|
698
|
+
}
|
|
699
|
+
if (baseHrefState === 'invalid')
|
|
700
|
+
forms.truncated = true;
|
|
653
701
|
publicationMetadata.truncated = publicationMetadata.truncated || inputLimitReached || publicationTagLimitReached;
|
|
654
702
|
publicationMetadata.documentTruncated = inputLimitReached || publicationTagLimitReached;
|
|
655
703
|
if (publicationMetadata.documentTruncated) {
|
|
@@ -671,6 +719,14 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
671
719
|
publicationMetadata.renderBlockingCandidates.total = publicationMetadata.renderBlockingCandidates.script
|
|
672
720
|
+ publicationMetadata.renderBlockingCandidates.stylesheet;
|
|
673
721
|
return {
|
|
722
|
+
title: (() => {
|
|
723
|
+
const value = titleParts.join('').replace(CONTROL_CHARACTER_RE_GLOBAL, ' ').replace(/\ufffd/gu, ' ').replace(/\s+/gu, ' ').trim();
|
|
724
|
+
if (!value)
|
|
725
|
+
return null;
|
|
726
|
+
return value.length > 200 ? `${value.slice(0, 200)}…` : value;
|
|
727
|
+
})(),
|
|
728
|
+
effectiveBaseUrl: effectiveBaseUrl?.toString() ?? null,
|
|
729
|
+
baseHrefState,
|
|
674
730
|
markup: markup.join('\n'),
|
|
675
731
|
visibleText: visibleTextParts.join('').replace(CONTROL_CHARACTER_RE_GLOBAL, ' ').replace(/\s+/gu, ' '),
|
|
676
732
|
structureTokens,
|
|
@@ -9,6 +9,23 @@ 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
11
|
import { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, TECHNOLOGY_PROFILE_VERSION, } from "./lookup-child-profile-contract.mjs";
|
|
12
|
+
const TECHNOLOGY_CATEGORIES = Object.freeze([
|
|
13
|
+
'application runtime',
|
|
14
|
+
'content management',
|
|
15
|
+
'commerce',
|
|
16
|
+
'site builder',
|
|
17
|
+
'web framework',
|
|
18
|
+
'static site generator',
|
|
19
|
+
'web server',
|
|
20
|
+
'delivery platform',
|
|
21
|
+
]);
|
|
22
|
+
const TECHNOLOGY_EVIDENCE_SOURCES = Object.freeze([
|
|
23
|
+
'generator metadata',
|
|
24
|
+
'static HTML',
|
|
25
|
+
'resource origin',
|
|
26
|
+
'HTTP server header',
|
|
27
|
+
'passive response header',
|
|
28
|
+
]);
|
|
12
29
|
const MAX_TECHNOLOGY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
|
|
13
30
|
const MAX_TECHNOLOGY_TAG_LENGTH = MAX_TAG_LENGTH;
|
|
14
31
|
const MAX_RESOURCE_ORIGINS = 30;
|
|
@@ -38,6 +55,29 @@ function normalizedResourceHosts(value) {
|
|
|
38
55
|
}
|
|
39
56
|
return hosts;
|
|
40
57
|
}
|
|
58
|
+
function applicationMarkup(markup, resourceHosts, effectiveBaseUrl, documentOrigin) {
|
|
59
|
+
let resolutionBase;
|
|
60
|
+
try {
|
|
61
|
+
resolutionBase = new URL(typeof effectiveBaseUrl === 'string' ? effectiveBaseUrl : 'https://document.invalid/');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
resolutionBase = new URL('https://document.invalid/');
|
|
65
|
+
}
|
|
66
|
+
const comparedOrigin = typeof documentOrigin === 'string' && /^https?:\/\//iu.test(documentOrigin)
|
|
67
|
+
? documentOrigin
|
|
68
|
+
: null;
|
|
69
|
+
return markup.replace(/\s(src|href|poster|data)="([^"]*)"/giu, (attribute, name, value) => {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = new URL(value, resolutionBase);
|
|
72
|
+
const knownExternalHost = resourceHosts.has(parsed.hostname.toLowerCase());
|
|
73
|
+
const offOrigin = comparedOrigin ? parsed.origin !== comparedOrigin : knownExternalHost;
|
|
74
|
+
return knownExternalHost && offOrigin ? ` ${name.toLowerCase()}=""` : attribute;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return attribute;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
41
81
|
const PASSIVE_TECHNOLOGY_HEADER_NAMES = Object.freeze([
|
|
42
82
|
'cf-ray',
|
|
43
83
|
'x-drupal-cache',
|
|
@@ -106,6 +146,17 @@ function htmlEvidence(markers, description, confidence = 'high') {
|
|
|
106
146
|
matches: ({ html }) => markers.some((marker) => html.includes(marker)),
|
|
107
147
|
};
|
|
108
148
|
}
|
|
149
|
+
function resourcePathEvidence(markers, description, confidence = 'high') {
|
|
150
|
+
return {
|
|
151
|
+
source: 'static HTML',
|
|
152
|
+
description,
|
|
153
|
+
confidence,
|
|
154
|
+
matches: ({ html }) => markers.some((marker) => html.includes(marker)),
|
|
155
|
+
roleFor: ({ applicationHtml }) => markers.some((marker) => applicationHtml.includes(marker))
|
|
156
|
+
? undefined
|
|
157
|
+
: 'embedded_dependency',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
109
160
|
function resourceEvidence(hosts, description) {
|
|
110
161
|
return {
|
|
111
162
|
source: 'resource origin',
|
|
@@ -142,7 +193,10 @@ function responseHeaderEvidence(name, pattern, description, confidence = 'high')
|
|
|
142
193
|
},
|
|
143
194
|
};
|
|
144
195
|
}
|
|
145
|
-
function evidenceRole(signature, evidence) {
|
|
196
|
+
function evidenceRole(signature, evidence, context) {
|
|
197
|
+
const contextualRole = context ? evidence.roleFor?.(context) : undefined;
|
|
198
|
+
if (contextualRole)
|
|
199
|
+
return contextualRole;
|
|
146
200
|
if (evidence.role)
|
|
147
201
|
return evidence.role;
|
|
148
202
|
if (evidence.source === 'resource origin')
|
|
@@ -159,7 +213,7 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
159
213
|
id: 'wordpress', name: 'WordPress', category: 'content management',
|
|
160
214
|
evidence: [
|
|
161
215
|
generatorEvidence(/^wordpress(?:\s|$)/i, 'Generator metadata identifies WordPress.'),
|
|
162
|
-
|
|
216
|
+
resourcePathEvidence(['/wp-content/', '/wp-includes/'], 'Static resource paths use WordPress conventions.', 'medium'),
|
|
163
217
|
],
|
|
164
218
|
},
|
|
165
219
|
{
|
|
@@ -222,8 +276,9 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
222
276
|
{
|
|
223
277
|
id: 'bigcommerce', name: 'BigCommerce', category: 'commerce',
|
|
224
278
|
minimumEvidenceMatches: 2,
|
|
279
|
+
allowEmbeddedOnly: true,
|
|
225
280
|
evidence: [
|
|
226
|
-
|
|
281
|
+
resourcePathEvidence(['cdn11.bigcommerce.com/s-', 'stencil-utils'], 'Static markup contains BigCommerce storefront asset markers.', 'medium'),
|
|
227
282
|
resourcePatternEvidence(/^cdn\d+\.bigcommerce\.com$/i, 'A retained resource origin uses BigCommerce storefront delivery infrastructure.'),
|
|
228
283
|
],
|
|
229
284
|
},
|
|
@@ -421,28 +476,41 @@ function analyzeWebsiteTechnology(input = {}) {
|
|
|
421
476
|
}) : null;
|
|
422
477
|
const context = {
|
|
423
478
|
html: htmlAnalysis.markup,
|
|
479
|
+
applicationHtml: '',
|
|
424
480
|
generator: boundedLowercase(input.generator, MAX_GENERATOR_INPUT),
|
|
425
481
|
httpServer: boundedLowercase(input.httpServer, MAX_SERVER_INPUT),
|
|
426
482
|
resourceHosts: normalizedResourceHosts(input.resourceOrigins),
|
|
427
483
|
responseHeaders: normalizedResponseHeaders(input.responseHeaders),
|
|
428
484
|
};
|
|
485
|
+
context.applicationHtml = applicationMarkup(context.html, context.resourceHosts, input.effectiveBaseUrl, input.documentOrigin);
|
|
429
486
|
const findings = [];
|
|
430
487
|
for (const signature of TECHNOLOGY_SIGNATURES) {
|
|
431
|
-
const matched = signature.evidence
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
488
|
+
const matched = signature.evidence
|
|
489
|
+
.filter((evidence) => evidence.matches(context))
|
|
490
|
+
.map((evidence) => ({ evidence, role: evidenceRole(signature, evidence, context) }));
|
|
491
|
+
const nonEmbeddedMatches = matched.filter((entry) => entry.role !== 'embedded_dependency');
|
|
492
|
+
const minimumEvidenceMatches = signature.minimumEvidenceMatches || 1;
|
|
493
|
+
const qualifiesForPlatform = matched.length >= minimumEvidenceMatches
|
|
494
|
+
&& (minimumEvidenceMatches === 1 || nonEmbeddedMatches.length > 0)
|
|
495
|
+
&& (!signature.requiresNonResourceEvidence || nonEmbeddedMatches.length > 0);
|
|
496
|
+
const contextualEmbeddedEvidence = matched.filter((entry) => (entry.role === 'embedded_dependency' && entry.evidence.roleFor !== undefined));
|
|
497
|
+
const selected = qualifiesForPlatform
|
|
498
|
+
? matched
|
|
499
|
+
: signature.allowEmbeddedOnly
|
|
500
|
+
? contextualEmbeddedEvidence.slice(0, 1)
|
|
501
|
+
: [];
|
|
502
|
+
if (selected.length === 0)
|
|
435
503
|
continue;
|
|
436
|
-
const matchedEvidence =
|
|
504
|
+
const matchedEvidence = selected.slice(0, MAX_EVIDENCE_PER_TECHNOLOGY).map(({ evidence, role }) => ({
|
|
437
505
|
source: evidence.source,
|
|
438
|
-
role
|
|
506
|
+
role,
|
|
439
507
|
description: evidence.description,
|
|
440
508
|
}));
|
|
441
509
|
findings.push({
|
|
442
510
|
id: signature.id,
|
|
443
511
|
name: signature.name,
|
|
444
512
|
category: signature.category,
|
|
445
|
-
confidence:
|
|
513
|
+
confidence: selected.some(({ evidence }) => evidence.confidence === 'high') ? 'high' : 'medium',
|
|
446
514
|
roles: TECHNOLOGY_EVIDENCE_ROLE_ORDER.filter((role) => matchedEvidence.some((evidence) => evidence.role === role)),
|
|
447
515
|
evidence: matchedEvidence,
|
|
448
516
|
});
|
|
@@ -493,4 +561,4 @@ function analyzeWebsiteTechnology(input = {}) {
|
|
|
493
561
|
browserLibraryProfile,
|
|
494
562
|
};
|
|
495
563
|
}
|
|
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, };
|
|
564
|
+
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_CATEGORIES, TECHNOLOGY_EVIDENCE_SOURCES, TECHNOLOGY_PROFILE_VERSION, TECHNOLOGY_SIGNATURE_CATALOGUE, analyzeWebsiteTechnology, captureTechnologyResponseHeaders, };
|
|
@@ -142,7 +142,11 @@ export function normaliseRdata(type, rawValue, origin, nameMode = 'absolute') {
|
|
|
142
142
|
const family = type === 'A' ? 4 : 6;
|
|
143
143
|
if (tokens.length !== 1 || isIP(tokens[0] ?? '') !== family)
|
|
144
144
|
throw new TypeError(`${type} data must contain one valid address.`);
|
|
145
|
-
|
|
145
|
+
const supplied = tokens[0];
|
|
146
|
+
const normalised = family === 4
|
|
147
|
+
? supplied.split('.').map((part) => String(Number(part))).join('.')
|
|
148
|
+
: new URL(`http://[${supplied}]/`).hostname.slice(1, -1).toLowerCase();
|
|
149
|
+
return { value: normalised, valueTreatment: 'normalised' };
|
|
146
150
|
}
|
|
147
151
|
if (type === 'CNAME' || type === 'NS') {
|
|
148
152
|
if (tokens.length !== 1)
|
|
@@ -152,7 +156,12 @@ export function normaliseRdata(type, rawValue, origin, nameMode = 'absolute') {
|
|
|
152
156
|
if (type === 'MX') {
|
|
153
157
|
if (tokens.length !== 2)
|
|
154
158
|
throw new TypeError('MX data must contain preference and exchange.');
|
|
155
|
-
|
|
159
|
+
const preference = integerToken(tokens[0], 0, 65_535, 'MX preference');
|
|
160
|
+
const exchangeToken = tokens[1];
|
|
161
|
+
if (exchangeToken === '.' && preference !== 0)
|
|
162
|
+
throw new TypeError('A Null MX exchange must use preference 0.');
|
|
163
|
+
const exchange = exchangeToken === '.' ? '.' : host(exchangeToken, 'MX exchange');
|
|
164
|
+
return { value: `${preference} ${exchange}`, valueTreatment: 'normalised' };
|
|
156
165
|
}
|
|
157
166
|
if (type === 'CAA') {
|
|
158
167
|
if (tokens.length < 3)
|
|
@@ -380,7 +389,6 @@ function parseBindZone(zoneText, initialOrigin) {
|
|
|
380
389
|
if (tokens.length !== 2)
|
|
381
390
|
throw new TypeError('$ORIGIN requires one domain name.');
|
|
382
391
|
origin = domainName(tokens[1], origin, '$ORIGIN', 'master-file');
|
|
383
|
-
lastOwner = origin;
|
|
384
392
|
continue;
|
|
385
393
|
}
|
|
386
394
|
if (directive === '$TTL')
|
|
@@ -388,10 +396,12 @@ function parseBindZone(zoneText, initialOrigin) {
|
|
|
388
396
|
if (directive?.startsWith('$'))
|
|
389
397
|
throw new TypeError(`${directive} is deliberately unsupported.`);
|
|
390
398
|
let offset = 0;
|
|
391
|
-
const ownerToken = entry.ownerOmitted ?
|
|
392
|
-
if (!ownerToken)
|
|
399
|
+
const ownerToken = entry.ownerOmitted ? null : tokens[offset++];
|
|
400
|
+
if (!entry.ownerOmitted && !ownerToken)
|
|
393
401
|
throw new TypeError('Record owner is missing.');
|
|
394
|
-
const owner =
|
|
402
|
+
const owner = entry.ownerOmitted
|
|
403
|
+
? lastOwner
|
|
404
|
+
: ownerName(ownerToken, origin, 'record owner', 'master-file');
|
|
395
405
|
lastOwner = owner;
|
|
396
406
|
let ttl = null;
|
|
397
407
|
if (/^\d+$/u.test(tokens[offset] ?? ''))
|