@slicedearth/whoisleuth-cli 1.36.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 +32 -2
- package/bin/whoisleuth.mjs +52 -24
- package/cli/archive-inspect.mjs +37 -2
- package/cli/arguments.mjs +493 -157
- package/cli/artifact-verify.mjs +91 -15
- 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/compare.mjs +1 -1
- package/cli/completion.mjs +49 -11
- 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/doctor.mjs +11 -5
- package/cli/domain-control-monitor.mjs +104 -0
- package/cli/domain-control-observations.mjs +235 -0
- package/cli/evidence-command-arguments.mjs +12 -1
- package/cli/evidence-command-runner.mjs +3 -2
- package/cli/evidence-signing.mjs +4 -4
- package/cli/external-observation-mapping.mjs +162 -0
- package/cli/fail-policy.mjs +64 -0
- package/cli/formatters/html.mjs +2 -2
- package/cli/formatters/json.mjs +13 -1
- package/cli/formatters/terminal.mjs +7 -3
- package/cli/investigation-manifest.mjs +113 -0
- package/cli/investigation-plan.mjs +2 -2
- package/cli/investigation-run.mjs +112 -0
- package/cli/local-mmdb-review.mjs +76 -0
- package/cli/lookalike-calibration.mjs +121 -0
- package/cli/lookup-brief.mjs +116 -0
- package/cli/lookup-command-runner.mjs +12 -2
- package/cli/lookup-diff.mjs +17 -19
- 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/network-command-runner.mjs +4 -1
- package/cli/offline-evidence-review.mjs +125 -10
- package/cli/open-asset-model-bridge.mjs +169 -0
- package/cli/registry-cohort.mjs +121 -0
- package/cli/registry-doctor.mjs +220 -0
- package/cli/registry-fixture-scaffold.mjs +73 -0
- package/cli/risk-calibration.mjs +117 -9
- package/cli/runner.mjs +561 -280
- 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 +1336 -0
- package/docs/cli.md +259 -940
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +7 -7
- package/frontend/src/lib/analysis/brand-profile-model.js +46 -4
- 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-report.js +573 -0
- package/frontend/src/lib/analysis/case-response-model.js +30 -1
- package/frontend/src/lib/analysis/case-response-packet.js +7 -7
- package/frontend/src/lib/analysis/detection-rule-model.js +36 -0
- package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
- package/frontend/src/lib/analysis/external-findings-import.js +320 -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 +487 -0
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
- package/frontend/src/lib/analysis/lookup-presentation.js +64 -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 +9 -89
- 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 +369 -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 +4 -2
- package/lib/abusech-auth.mjs +1 -1
- package/lib/analyst-taxonomy.mjs +40 -0
- package/lib/availability.mjs +2 -2
- package/lib/bounded-contract-normalizers.mjs +33 -0
- 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/dns-convergence-review.mjs +183 -0
- package/lib/dns-delegation-health.mjs +196 -3
- package/lib/domain-assurance.mjs +368 -0
- 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-control-manifest.mjs +50 -7
- package/lib/domain-portfolio-review.mjs +151 -0
- package/lib/domain-posture.mjs +9 -9
- package/lib/encrypted-dns-contract.mjs +1 -1
- package/lib/evidence-export.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/html-signals.mjs +5 -11
- 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/nameserver-preflight-review.mjs +159 -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/page-language-signals.mjs +124 -0
- 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 +247 -83
- package/lib/scoring-evidence-quality.mjs +107 -0
- package/lib/security-txt.mjs +3 -3
- package/lib/static-html-analysis.mjs +29 -1
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threatfox-intelligence.mjs +1 -1
- package/lib/tls-intelligence.mjs +13 -3
- package/lib/trust-store-comparison.mjs +146 -0
- package/lib/urlhaus-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +5 -5
- package/lib/website-technology.mjs +25 -11
- package/lib/whois-chain.mjs +1 -1
- package/lib/zone-intent-review.mjs +534 -0
- package/package.json +5 -1
- package/third-party-notices.txt +911 -19
package/lib/risk-scoring.mjs
CHANGED
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
import { calibrateExternalIntelligenceRisk } from "./external-intelligence-risk.mjs";
|
|
2
|
+
import { buildScoreEvidenceQuality, } from "./scoring-evidence-quality.mjs";
|
|
2
3
|
const RISK_STATES = new Set(['registered', 'for_sale', 'expiring']);
|
|
3
4
|
const STATE_LABELS = Object.freeze({
|
|
4
5
|
for_sale: 'for sale',
|
|
5
6
|
expiring: 'expiring/pending delete',
|
|
6
7
|
registered: 'registered',
|
|
7
8
|
});
|
|
8
|
-
// Version
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
// on matching model versions so formula changes are not mistaken for changes
|
|
13
|
-
// in the observed domain.
|
|
14
|
-
export const RISK_MODEL_VERSION = 6;
|
|
9
|
+
// Version 7 caps each independent evidence family, treats generic operational
|
|
10
|
+
// properties as supporting context, and reports collection quality separately
|
|
11
|
+
// from the heuristic score. Missing evidence never contributes points.
|
|
12
|
+
export const RISK_MODEL_VERSION = 7;
|
|
15
13
|
export const RISK_REVIEW_THRESHOLD = 70;
|
|
16
14
|
const RISK_STATE_BASE = Object.freeze({
|
|
17
|
-
registered:
|
|
18
|
-
for_sale:
|
|
19
|
-
expiring:
|
|
15
|
+
registered: 6,
|
|
16
|
+
for_sale: 4,
|
|
17
|
+
expiring: 5,
|
|
20
18
|
});
|
|
19
|
+
const FAMILY_CAPS = Object.freeze({
|
|
20
|
+
'domain-resemblance': 20,
|
|
21
|
+
'brand-presentation': 24,
|
|
22
|
+
'credential-lure': 18,
|
|
23
|
+
'external-intelligence': 18,
|
|
24
|
+
'operational-support': 12,
|
|
25
|
+
});
|
|
26
|
+
const REMOVABLE_RISK_FAMILY_MAP = Object.freeze({
|
|
27
|
+
'brand-presentation': true,
|
|
28
|
+
'credential-lure': true,
|
|
29
|
+
'domain-resemblance': true,
|
|
30
|
+
'external-intelligence': true,
|
|
31
|
+
'operational-support': true,
|
|
32
|
+
});
|
|
33
|
+
const REMOVABLE_RISK_FAMILIES = Object.freeze(Object.keys(REMOVABLE_RISK_FAMILY_MAP));
|
|
21
34
|
const HIGH_CONTEXT_MUTATIONS = new Set([
|
|
22
35
|
'unicode_homoglyph',
|
|
23
36
|
'unicode_homoglyph_depth_2',
|
|
@@ -49,7 +62,7 @@ export function normalizeRiskModelVersion(value) {
|
|
|
49
62
|
}
|
|
50
63
|
function formatAge(days) {
|
|
51
64
|
if (days < 60)
|
|
52
|
-
return `${days}d old`;
|
|
65
|
+
return `${Math.round(days)}d old`;
|
|
53
66
|
const years = days / 365.25;
|
|
54
67
|
return years < 1 ? `${Math.round(days / 30)}mo old` : `${years.toFixed(1)}y old`;
|
|
55
68
|
}
|
|
@@ -68,83 +81,234 @@ function mutationContext(mutationTypes) {
|
|
|
68
81
|
}
|
|
69
82
|
return null;
|
|
70
83
|
}
|
|
84
|
+
function hasBoolean(value) {
|
|
85
|
+
return typeof value === 'boolean';
|
|
86
|
+
}
|
|
87
|
+
function scoreQuality(input) {
|
|
88
|
+
const observedFamilies = ['registration'];
|
|
89
|
+
const expectedFamilies = ['registration', 'operational-support'];
|
|
90
|
+
if (Array.isArray(input.mutationTypes) || hasBoolean(input.idnReferenceMatch))
|
|
91
|
+
observedFamilies.push('domain-resemblance');
|
|
92
|
+
if (input.hasActiveBrandProfile === true)
|
|
93
|
+
expectedFamilies.push('domain-resemblance', 'brand-presentation');
|
|
94
|
+
if ([input.faviconMatch, input.faviconNearMatch, input.reusesOfficialAssets, input.pageBaselineMatch].some(hasBoolean)) {
|
|
95
|
+
observedFamilies.push('brand-presentation');
|
|
96
|
+
}
|
|
97
|
+
if ([input.hasPasswordField, input.hasExternalFormAction].some(hasBoolean) || typeof input.phishingLanguageMatch === 'string') {
|
|
98
|
+
observedFamilies.push('credential-lure');
|
|
99
|
+
}
|
|
100
|
+
if (input.threatIntelligence !== undefined && input.threatIntelligence !== null)
|
|
101
|
+
observedFamilies.push('external-intelligence');
|
|
102
|
+
if ([input.hasMx, input.hasSpf, input.hasDmarc].some(hasBoolean)
|
|
103
|
+
|| typeof input.activityStatus === 'string'
|
|
104
|
+
|| typeof input.domainAgeDays === 'number')
|
|
105
|
+
observedFamilies.push('operational-support');
|
|
106
|
+
if (input.scanDepth === 'deep')
|
|
107
|
+
expectedFamilies.push('credential-lure');
|
|
108
|
+
return buildScoreEvidenceQuality(input, {
|
|
109
|
+
expectedDepth: 'deep',
|
|
110
|
+
observedFamilies,
|
|
111
|
+
expectedFamilies,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
71
114
|
// Risk prioritizes a registered lookalike/typosquat domain for analyst review.
|
|
72
115
|
// It is a heuristic indicator, never a maliciousness or safety verdict.
|
|
73
|
-
|
|
74
|
-
const state =
|
|
116
|
+
function explainRiskScoreInternal(input, excludedFamily) {
|
|
117
|
+
const state = input.availability ?? input.state;
|
|
75
118
|
if (typeof state !== 'string' || !RISK_STATES.has(state))
|
|
76
119
|
return null;
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
120
|
+
const factors = [];
|
|
121
|
+
const familyTotals = new Map();
|
|
122
|
+
const add = (family, label, requestedDelta) => {
|
|
123
|
+
if (family === excludedFamily)
|
|
124
|
+
return 0;
|
|
125
|
+
const current = familyTotals.get(family) ?? 0;
|
|
126
|
+
const cap = FAMILY_CAPS[family] ?? Number.POSITIVE_INFINITY;
|
|
127
|
+
const delta = Math.max(0, Math.min(requestedDelta, Math.max(0, cap - current)));
|
|
128
|
+
const capped = delta !== requestedDelta;
|
|
129
|
+
factors.push({ family, label: capped ? `${label} (family cap reached)` : label, delta });
|
|
130
|
+
familyTotals.set(family, current + delta);
|
|
131
|
+
return delta;
|
|
132
|
+
};
|
|
133
|
+
add('registration', `Base context for “${STATE_LABELS[state] ?? state}”`, RISK_STATE_BASE[state] ?? 0);
|
|
134
|
+
const mutation = mutationContext(input.mutationTypes);
|
|
135
|
+
if (mutation)
|
|
136
|
+
add('domain-resemblance', mutation.label, mutation.delta);
|
|
137
|
+
if (input.idnReferenceMatch === true)
|
|
138
|
+
add('domain-resemblance', 'IDN skeleton matches an official Brand Profile domain', 20);
|
|
139
|
+
if (input.faviconMatch === true)
|
|
140
|
+
add('brand-presentation', 'Favicon matches an official Brand Profile site', 18);
|
|
141
|
+
else if (input.faviconNearMatch === true)
|
|
142
|
+
add('brand-presentation', 'Favicon resembles an official Brand Profile site', 14);
|
|
143
|
+
if (input.reusesOfficialAssets === true)
|
|
144
|
+
add('brand-presentation', 'Official asset-host relationship observed', 6);
|
|
145
|
+
if (input.pageBaselineMatch === true)
|
|
146
|
+
add('brand-presentation', 'Multiple independently compared page-identity components match the official baseline', 14);
|
|
147
|
+
if (typeof input.phishingLanguageMatch === 'string' && input.phishingLanguageMatch.trim()) {
|
|
148
|
+
add('credential-lure', 'Suspicious urgency language observed', 8);
|
|
149
|
+
}
|
|
150
|
+
if (input.hasPasswordField === true && input.hasExternalFormAction === true) {
|
|
151
|
+
add('credential-lure', 'Password form submits to an external origin', 10);
|
|
152
|
+
}
|
|
153
|
+
else if (input.hasPasswordField === true) {
|
|
154
|
+
add('credential-lure', 'Login/password form present', 5);
|
|
155
|
+
}
|
|
156
|
+
else if (input.hasExternalFormAction === true) {
|
|
157
|
+
add('credential-lure', 'External form destination observed without a password field', 0);
|
|
158
|
+
}
|
|
159
|
+
const external = calibrateExternalIntelligenceRisk(input.threatIntelligence);
|
|
160
|
+
if (external.factor)
|
|
161
|
+
add('external-intelligence', external.factor.label, external.contribution);
|
|
162
|
+
const primaryFamilies = ['domain-resemblance', 'brand-presentation', 'credential-lure', 'external-intelligence']
|
|
163
|
+
.filter((family) => (familyTotals.get(family) ?? 0) > 0);
|
|
164
|
+
if (primaryFamilies.length >= 2) {
|
|
165
|
+
add('corroboration', `Corroborating context across ${primaryFamilies.length} independent evidence families`, primaryFamilies.length >= 3 ? 18 : 10);
|
|
166
|
+
}
|
|
167
|
+
const hasIndependentContext = primaryFamilies.length > 0;
|
|
168
|
+
const supporting = (label, delta) => {
|
|
169
|
+
add('operational-support', hasIndependentContext ? label : `${label}; neutral without independent suspicious context`, hasIndependentContext ? delta : 0);
|
|
170
|
+
};
|
|
171
|
+
if (input.activityStatus === 'active')
|
|
172
|
+
supporting('Active website observed', 4);
|
|
173
|
+
if (input.hasMx === true)
|
|
174
|
+
supporting('Mail server configured', 4);
|
|
175
|
+
if (input.hasSpf === true && input.hasDmarc === true)
|
|
176
|
+
supporting('SPF and DMARC records present', 1);
|
|
177
|
+
else if (input.hasSpf === true || input.hasDmarc === true)
|
|
178
|
+
supporting('SPF or DMARC record present', 0);
|
|
179
|
+
if (typeof input.domainAgeDays === 'number' && Number.isFinite(input.domainAgeDays) && input.domainAgeDays >= 0) {
|
|
180
|
+
if (input.domainAgeDays < 90)
|
|
181
|
+
supporting(`Recently registered (${formatAge(input.domainAgeDays)})`, 6);
|
|
182
|
+
else if (input.domainAgeDays < 365)
|
|
183
|
+
supporting(`Registered under a year ago (${formatAge(input.domainAgeDays)})`, 2);
|
|
184
|
+
}
|
|
185
|
+
if (typeof input.privacyProtected === 'boolean') {
|
|
186
|
+
add('registration', 'Registration privacy is neutral context', 0);
|
|
187
|
+
}
|
|
188
|
+
const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
|
|
189
|
+
const score = Math.max(0, Math.min(100, rawScore));
|
|
190
|
+
const familyOrder = [
|
|
191
|
+
'registration', 'domain-resemblance', 'brand-presentation', 'credential-lure',
|
|
192
|
+
'external-intelligence', 'corroboration', 'operational-support',
|
|
193
|
+
];
|
|
194
|
+
return {
|
|
195
|
+
modelVersion: RISK_MODEL_VERSION,
|
|
196
|
+
score,
|
|
197
|
+
rawScore,
|
|
198
|
+
capped: score !== rawScore,
|
|
199
|
+
factors,
|
|
200
|
+
families: familyOrder
|
|
201
|
+
.filter((id) => familyTotals.has(id))
|
|
202
|
+
.map((id) => ({ id, contribution: familyTotals.get(id) ?? 0, cap: FAMILY_CAPS[id] ?? null })),
|
|
203
|
+
evidenceQuality: scoreQuality(input),
|
|
85
204
|
};
|
|
86
|
-
const context = mutationContext(r.mutationTypes);
|
|
87
|
-
if (context)
|
|
88
|
-
addContextualFactor('domain-resemblance', context.label, context.delta);
|
|
89
|
-
if (r.faviconMatch === true) {
|
|
90
|
-
addContextualFactor('brand-presentation', 'Favicon matches your Brand Profile official site', 18);
|
|
91
|
-
}
|
|
92
|
-
else if (r.faviconNearMatch === true) {
|
|
93
|
-
addContextualFactor('brand-presentation', 'Favicon resembles your official site (perceptual match)', 14);
|
|
94
|
-
}
|
|
95
|
-
if (r.reusesOfficialAssets === true) {
|
|
96
|
-
addContextualFactor('brand-presentation', 'Official asset host relationship observed', 6);
|
|
97
|
-
}
|
|
98
|
-
if (typeof r.phishingLanguageMatch === 'string' && r.phishingLanguageMatch.trim()) {
|
|
99
|
-
addContextualFactor('credential-lure', 'Suspicious urgency language observed', 8);
|
|
100
|
-
}
|
|
101
|
-
if (r.hasPasswordField === true) {
|
|
102
|
-
addContextualFactor('credential-lure', 'Login/password form present', 5);
|
|
103
|
-
}
|
|
104
|
-
if (contextualFamilies.size >= 2) {
|
|
105
|
-
const bonus = contextualFamilies.size >= 3 ? 20 : 10;
|
|
106
|
-
factors.push({ label: `Corroborating context across ${contextualFamilies.size} distinct evidence families`, delta: bonus });
|
|
107
|
-
score += bonus;
|
|
108
|
-
}
|
|
109
|
-
const externalEvidence = calibrateExternalIntelligenceRisk(r.threatIntelligence);
|
|
110
|
-
if (externalEvidence.factor) {
|
|
111
|
-
factors.push(externalEvidence.factor);
|
|
112
|
-
score += externalEvidence.contribution;
|
|
113
|
-
}
|
|
114
|
-
if (r.activityStatus === 'active') {
|
|
115
|
-
factors.push({ label: 'Active site in use', delta: 8 });
|
|
116
|
-
score += 8;
|
|
117
|
-
}
|
|
118
|
-
if (r.hasMx === true) {
|
|
119
|
-
factors.push({ label: 'Mail server configured', delta: 8 });
|
|
120
|
-
score += 8;
|
|
121
|
-
}
|
|
122
|
-
if (r.hasSpf === true && r.hasDmarc === true) {
|
|
123
|
-
factors.push({ label: 'SPF + DMARC records present', delta: 3 });
|
|
124
|
-
score += 3;
|
|
125
|
-
}
|
|
126
|
-
else if (r.hasSpf === true || r.hasDmarc === true) {
|
|
127
|
-
factors.push({ label: 'SPF or DMARC record present', delta: 1 });
|
|
128
|
-
score += 1;
|
|
129
|
-
}
|
|
130
|
-
if (r.privacyProtected === true) {
|
|
131
|
-
factors.push({ label: 'WHOIS privacy protected', delta: 3 });
|
|
132
|
-
score += 3;
|
|
133
|
-
}
|
|
134
|
-
if (typeof r.domainAgeDays === 'number' && Number.isFinite(r.domainAgeDays) && r.domainAgeDays >= 0) {
|
|
135
|
-
if (r.domainAgeDays < 90) {
|
|
136
|
-
factors.push({ label: `Recently registered (${formatAge(r.domainAgeDays)})`, delta: 10 });
|
|
137
|
-
score += 10;
|
|
138
|
-
}
|
|
139
|
-
else if (r.domainAgeDays < 365) {
|
|
140
|
-
factors.push({ label: `Registered under a year ago (${formatAge(r.domainAgeDays)})`, delta: 4 });
|
|
141
|
-
score += 4;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return { modelVersion: RISK_MODEL_VERSION, score: Math.max(0, Math.min(100, Math.round(score))), factors };
|
|
145
205
|
}
|
|
146
|
-
export function
|
|
147
|
-
return
|
|
206
|
+
export function explainRiskScore(input) {
|
|
207
|
+
return explainRiskScoreInternal(input, null);
|
|
208
|
+
}
|
|
209
|
+
export function buildRiskScoreSensitivity(input) {
|
|
210
|
+
const baseline = explainRiskScoreInternal(input, null);
|
|
211
|
+
if (!baseline)
|
|
212
|
+
return null;
|
|
213
|
+
const removableFamilies = REMOVABLE_RISK_FAMILIES
|
|
214
|
+
.filter((family) => baseline.families.some((item) => item.id === family && item.contribution > 0));
|
|
215
|
+
const scenarios = removableFamilies.map((excludedFamily) => {
|
|
216
|
+
const result = explainRiskScoreInternal(input, excludedFamily);
|
|
217
|
+
const score = result?.score ?? baseline.score;
|
|
218
|
+
return Object.freeze({ excludedFamily, score, difference: score - baseline.score });
|
|
219
|
+
}).sort((left, right) => left.score - right.score || left.excludedFamily.localeCompare(right.excludedFamily));
|
|
220
|
+
const minimumScenarioScore = scenarios[0]?.score ?? baseline.score;
|
|
221
|
+
const thresholdState = baseline.score < RISK_REVIEW_THRESHOLD
|
|
222
|
+
? 'below'
|
|
223
|
+
: minimumScenarioScore < RISK_REVIEW_THRESHOLD ? 'crosses' : 'stable_above';
|
|
224
|
+
return Object.freeze({
|
|
225
|
+
version: 1,
|
|
226
|
+
baselineScore: baseline.score,
|
|
227
|
+
reviewThreshold: RISK_REVIEW_THRESHOLD,
|
|
228
|
+
minimumScenarioScore,
|
|
229
|
+
thresholdState,
|
|
230
|
+
scenarios: Object.freeze(scenarios),
|
|
231
|
+
limitations: Object.freeze([
|
|
232
|
+
'Each scenario removes one observed evidence family and recalculates family caps and corroboration; it does not predict missing evidence.',
|
|
233
|
+
'The sensitivity review qualifies prioritisation only and does not determine maliciousness, safety, ownership, or intent.',
|
|
234
|
+
]),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
export function computeRiskScore(input) {
|
|
238
|
+
return explainRiskScore(input)?.score ?? null;
|
|
239
|
+
}
|
|
240
|
+
// Retained only for deterministic offline comparison of reviewed calibration
|
|
241
|
+
// datasets. Runtime lookups always use the current model above.
|
|
242
|
+
export function explainRiskScoreV6(input) {
|
|
243
|
+
const state = input.availability ?? input.state;
|
|
244
|
+
if (typeof state !== 'string' || !RISK_STATES.has(state))
|
|
245
|
+
return null;
|
|
246
|
+
const legacyBase = { registered: 10, for_sale: 5, expiring: 8 };
|
|
247
|
+
const factors = [];
|
|
248
|
+
const totals = new Map();
|
|
249
|
+
const add = (family, label, delta) => {
|
|
250
|
+
factors.push({ family, label, delta });
|
|
251
|
+
totals.set(family, (totals.get(family) ?? 0) + delta);
|
|
252
|
+
};
|
|
253
|
+
add('registration', `Base score for “${STATE_LABELS[state] ?? state}”`, legacyBase[state] ?? 0);
|
|
254
|
+
const contextual = new Set();
|
|
255
|
+
const context = mutationContext(input.mutationTypes);
|
|
256
|
+
if (context) {
|
|
257
|
+
add('domain-resemblance', context.label, context.delta);
|
|
258
|
+
contextual.add('domain-resemblance');
|
|
259
|
+
}
|
|
260
|
+
if (input.faviconMatch === true) {
|
|
261
|
+
add('brand-presentation', 'Favicon matches an official Brand Profile site', 18);
|
|
262
|
+
contextual.add('brand-presentation');
|
|
263
|
+
}
|
|
264
|
+
else if (input.faviconNearMatch === true) {
|
|
265
|
+
add('brand-presentation', 'Favicon resembles an official Brand Profile site', 14);
|
|
266
|
+
contextual.add('brand-presentation');
|
|
267
|
+
}
|
|
268
|
+
if (input.reusesOfficialAssets === true) {
|
|
269
|
+
add('brand-presentation', 'Official asset-host relationship observed', 6);
|
|
270
|
+
contextual.add('brand-presentation');
|
|
271
|
+
}
|
|
272
|
+
if (typeof input.phishingLanguageMatch === 'string' && input.phishingLanguageMatch.trim()) {
|
|
273
|
+
add('credential-lure', 'Suspicious urgency language observed', 8);
|
|
274
|
+
contextual.add('credential-lure');
|
|
275
|
+
}
|
|
276
|
+
if (input.hasPasswordField === true) {
|
|
277
|
+
add('credential-lure', 'Login/password form present', 5);
|
|
278
|
+
contextual.add('credential-lure');
|
|
279
|
+
}
|
|
280
|
+
if (contextual.size >= 2)
|
|
281
|
+
add('corroboration', `Corroborating context across ${contextual.size} distinct evidence families`, contextual.size >= 3 ? 20 : 10);
|
|
282
|
+
const external = calibrateExternalIntelligenceRisk(input.threatIntelligence);
|
|
283
|
+
if (external.factor)
|
|
284
|
+
add('external-intelligence', external.factor.label, external.contribution);
|
|
285
|
+
if (input.activityStatus === 'active')
|
|
286
|
+
add('operational-support', 'Active site in use', 8);
|
|
287
|
+
if (input.hasMx === true)
|
|
288
|
+
add('operational-support', 'Mail server configured', 8);
|
|
289
|
+
if (input.hasSpf === true && input.hasDmarc === true)
|
|
290
|
+
add('operational-support', 'SPF and DMARC records present', 3);
|
|
291
|
+
else if (input.hasSpf === true || input.hasDmarc === true)
|
|
292
|
+
add('operational-support', 'SPF or DMARC record present', 1);
|
|
293
|
+
if (input.privacyProtected === true)
|
|
294
|
+
add('registration', 'WHOIS privacy protected', 3);
|
|
295
|
+
if (typeof input.domainAgeDays === 'number' && Number.isFinite(input.domainAgeDays) && input.domainAgeDays >= 0) {
|
|
296
|
+
if (input.domainAgeDays < 90)
|
|
297
|
+
add('operational-support', `Recently registered (${formatAge(input.domainAgeDays)})`, 10);
|
|
298
|
+
else if (input.domainAgeDays < 365)
|
|
299
|
+
add('operational-support', `Registered under a year ago (${formatAge(input.domainAgeDays)})`, 4);
|
|
300
|
+
}
|
|
301
|
+
const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
|
|
302
|
+
const score = Math.max(0, Math.min(100, rawScore));
|
|
303
|
+
return {
|
|
304
|
+
modelVersion: 6,
|
|
305
|
+
score,
|
|
306
|
+
rawScore,
|
|
307
|
+
capped: score !== rawScore,
|
|
308
|
+
factors,
|
|
309
|
+
families: [...totals].map(([id, contribution]) => ({ id, contribution, cap: null })),
|
|
310
|
+
evidenceQuality: scoreQuality(input),
|
|
311
|
+
};
|
|
148
312
|
}
|
|
149
313
|
export function riskTone(score) {
|
|
150
314
|
if (score === null)
|
|
@@ -159,6 +323,6 @@ export function formatScoreBreakdown(explained, separator = '\n') {
|
|
|
159
323
|
if (!explained)
|
|
160
324
|
return '';
|
|
161
325
|
const parts = explained.factors.map((factor) => `${factor.label} ${factor.delta >= 0 ? '+' : ''}${Math.round(factor.delta)}`);
|
|
162
|
-
parts.push(`Total ${explained.score}`);
|
|
326
|
+
parts.push(`Total ${explained.score} · Risk model v${explained.modelVersion} · evidence ${explained.evidenceQuality.state}`);
|
|
163
327
|
return parts.join(separator);
|
|
164
328
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export const SCORE_EVIDENCE_QUALITY_VERSION = 1;
|
|
2
|
+
const MAX_SOURCES = 24;
|
|
3
|
+
const MAX_FAMILIES = 12;
|
|
4
|
+
const SOURCE_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/u;
|
|
5
|
+
const COMPLETE_STATES = new Set(['complete', 'not_found', 'success']);
|
|
6
|
+
const LIMITED_STATES = new Set(['partial', 'unknown']);
|
|
7
|
+
const UNAVAILABLE_STATES = new Set(['error', 'rate_limited', 'unavailable']);
|
|
8
|
+
const SKIPPED_STATES = new Set(['skipped', 'unsupported']);
|
|
9
|
+
const ACCEPTED_SOURCE_STATES = new Set([
|
|
10
|
+
...COMPLETE_STATES,
|
|
11
|
+
...LIMITED_STATES,
|
|
12
|
+
...UNAVAILABLE_STATES,
|
|
13
|
+
...SKIPPED_STATES,
|
|
14
|
+
]);
|
|
15
|
+
function boundedFamilyList(value) {
|
|
16
|
+
const output = new Set();
|
|
17
|
+
for (const item of value.slice(0, MAX_FAMILIES * 2)) {
|
|
18
|
+
const normalized = String(item).trim().toLowerCase().replace(/[^a-z0-9_-]+/gu, '-').slice(0, 64);
|
|
19
|
+
if (SOURCE_ID_RE.test(normalized))
|
|
20
|
+
output.add(normalized);
|
|
21
|
+
if (output.size >= MAX_FAMILIES)
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
return [...output].sort();
|
|
25
|
+
}
|
|
26
|
+
function sourceCoverage(value) {
|
|
27
|
+
if (!Array.isArray(value))
|
|
28
|
+
return [];
|
|
29
|
+
const output = [];
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
for (const candidate of value.slice(0, MAX_SOURCES * 4)) {
|
|
32
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
|
|
33
|
+
continue;
|
|
34
|
+
const item = candidate;
|
|
35
|
+
const source = typeof item.source === 'string' ? item.source.trim().toLowerCase() : '';
|
|
36
|
+
const state = typeof item.state === 'string' ? item.state.trim().toLowerCase() : '';
|
|
37
|
+
if (!SOURCE_ID_RE.test(source) || seen.has(source))
|
|
38
|
+
continue;
|
|
39
|
+
if (!ACCEPTED_SOURCE_STATES.has(state))
|
|
40
|
+
continue;
|
|
41
|
+
seen.add(source);
|
|
42
|
+
output.push({ source, state });
|
|
43
|
+
if (output.length >= MAX_SOURCES)
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
return output;
|
|
47
|
+
}
|
|
48
|
+
function scanDepth(value) {
|
|
49
|
+
return value === 'deep' || value === 'fast' ? value : 'unknown';
|
|
50
|
+
}
|
|
51
|
+
function observedFreshness(value) {
|
|
52
|
+
if (typeof value !== 'string' || value.length > 64)
|
|
53
|
+
return 'unknown';
|
|
54
|
+
return Number.isFinite(Date.parse(value)) ? 'observed' : 'unknown';
|
|
55
|
+
}
|
|
56
|
+
export function buildScoreEvidenceQuality(input, options) {
|
|
57
|
+
const depth = scanDepth(input.scanDepth);
|
|
58
|
+
const coverage = sourceCoverage(input.sourceCoverage);
|
|
59
|
+
const completeSources = coverage.filter((item) => COMPLETE_STATES.has(item.state)).length;
|
|
60
|
+
const limitedSources = coverage.filter((item) => LIMITED_STATES.has(item.state)).length;
|
|
61
|
+
const unavailableSources = coverage.filter((item) => UNAVAILABLE_STATES.has(item.state)).length;
|
|
62
|
+
const skippedSources = coverage.filter((item) => SKIPPED_STATES.has(item.state)).length;
|
|
63
|
+
const observedFamilies = boundedFamilyList(options.observedFamilies ?? []);
|
|
64
|
+
const expectedFamilies = boundedFamilyList(options.expectedFamilies ?? []);
|
|
65
|
+
const observedSet = new Set(observedFamilies);
|
|
66
|
+
const missingFamilies = expectedFamilies.filter((family) => !observedSet.has(family));
|
|
67
|
+
const limitations = [];
|
|
68
|
+
let state = 'complete';
|
|
69
|
+
if (!coverage.length || depth === 'unknown')
|
|
70
|
+
state = 'unknown';
|
|
71
|
+
else if (unavailableSources || limitedSources)
|
|
72
|
+
state = 'partial';
|
|
73
|
+
else if (options.expectedDepth === 'deep' && depth !== 'deep')
|
|
74
|
+
state = 'limited';
|
|
75
|
+
if (missingFamilies.length && state === 'complete')
|
|
76
|
+
state = 'limited';
|
|
77
|
+
if (options.expectedDepth === 'deep' && depth !== 'deep') {
|
|
78
|
+
limitations.push('A fast or unknown-depth observation does not evaluate every website and mail scoring family.');
|
|
79
|
+
}
|
|
80
|
+
if (limitedSources || unavailableSources) {
|
|
81
|
+
limitations.push('One or more score-relevant sources were partial, unavailable, or indeterminate.');
|
|
82
|
+
}
|
|
83
|
+
if (skippedSources) {
|
|
84
|
+
limitations.push('One or more reported sources were skipped or unsupported and did not contribute to the score.');
|
|
85
|
+
}
|
|
86
|
+
if (!coverage.length)
|
|
87
|
+
limitations.push('Source-level coverage was not supplied with this score.');
|
|
88
|
+
if (missingFamilies.length)
|
|
89
|
+
limitations.push(`No usable observation was supplied for: ${missingFamilies.join(', ')}.`);
|
|
90
|
+
const boundedLimitations = [
|
|
91
|
+
...limitations.slice(0, 5),
|
|
92
|
+
'Evidence coverage qualifies the score; it never adds points or turns missing evidence into a negative finding.',
|
|
93
|
+
];
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
version: SCORE_EVIDENCE_QUALITY_VERSION,
|
|
96
|
+
state,
|
|
97
|
+
scanDepth: depth,
|
|
98
|
+
freshness: observedFreshness(input.observedAt),
|
|
99
|
+
completeSources,
|
|
100
|
+
limitedSources,
|
|
101
|
+
unavailableSources,
|
|
102
|
+
skippedSources,
|
|
103
|
+
observedFamilies: Object.freeze(observedFamilies),
|
|
104
|
+
missingFamilies: Object.freeze(missingFamilies),
|
|
105
|
+
limitations: Object.freeze(boundedLimitations),
|
|
106
|
+
});
|
|
107
|
+
}
|
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
|
}
|
|
@@ -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, };
|
|
@@ -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: [
|