@slicedearth/whoisleuth-cli 1.43.0 → 1.47.4
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/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +67 -10
- package/bin/whoisleuth.mjs +22 -9
- package/cli/arguments.mjs +256 -9
- package/cli/artifact-structure.mjs +1833 -0
- package/cli/artifact-verify.mjs +242 -49
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +67 -31
- package/cli/bulk-command-runner.mjs +16 -6
- package/cli/bulk.mjs +18 -7
- package/cli/case-pack.mjs +819 -34
- package/cli/command-reference.mjs +48 -24
- package/cli/completion.mjs +50 -11
- package/cli/config-profile.mjs +15 -12
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discovery-observation-snapshot.mjs +8 -14
- package/cli/discovery-scan-command-runner.mjs +18 -6
- package/cli/discovery-snapshot.mjs +8 -15
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +21 -15
- package/cli/domain-control-observations.mjs +25 -2
- package/cli/errors.mjs +7 -1
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +73 -45
- package/cli/export-evidence.mjs +11 -4
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +12 -4
- package/cli/formatters/terminal.mjs +4 -1
- package/cli/http.mjs +2 -1
- package/cli/input.mjs +57 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-run.mjs +71 -23
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookup-command-runner.mjs +2 -2
- package/cli/lookup-diff.mjs +9 -0
- package/cli/lookup-reconcile.mjs +1 -1
- package/cli/mail-review.mjs +5 -5
- package/cli/manual.mjs +1 -1
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +28 -8
- package/cli/page-compare.mjs +8 -5
- package/cli/registry-cohort.mjs +385 -61
- package/cli/registry-fixture-scaffold.mjs +4 -3
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +376 -0
- package/cli/risk-calibration.mjs +7 -5
- package/cli/runner.mjs +150 -177
- package/cli/saved-lookup.mjs +26 -3
- package/cli/sharing-review.mjs +12 -6
- package/docs/cli-reference.md +467 -74
- package/docs/cli.md +25 -5
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
- package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +33 -7
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
- package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
- package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
- package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
- package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
- package/frontend/src/lib/analysis/bulk-sort.js +4 -0
- package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +9 -1
- package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
- package/frontend/src/lib/analysis/case-migration-model.js +83 -31
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-core.js +1 -1
- package/frontend/src/lib/analysis/case-record-model.js +2 -0
- package/frontend/src/lib/analysis/case-record-operations.js +43 -13
- package/frontend/src/lib/analysis/case-report.js +39 -6
- package/frontend/src/lib/analysis/case-response-model.js +66 -7
- package/frontend/src/lib/analysis/case-response-packet.js +15 -5
- package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
- package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
- package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
- package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
- package/frontend/src/lib/analysis/domain-control-records.js +88 -0
- package/frontend/src/lib/analysis/external-findings-import.js +70 -6
- package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
- package/frontend/src/lib/analysis/investigation-guide.js +52 -10
- package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
- package/frontend/src/lib/analysis/lookup-asset-graph.js +8 -8
- package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
- package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +63 -8
- package/frontend/src/lib/analysis/lookup-presentation.js +70 -14
- package/frontend/src/lib/analysis/lookup-source-refresh.js +36 -63
- package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
- package/frontend/src/lib/analysis/utils.js +27 -4
- package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
- package/frontend/src/lib/analysis/visualization-models.js +63 -94
- package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
- package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -4
- package/frontend/src/lib/analysis/workspace-archive.js +105 -19
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +14 -12
- package/lib/bounded-file.mjs +82 -0
- package/lib/bounded-json-response.mjs +170 -0
- package/lib/bounded-json.mjs +249 -0
- package/lib/browser-library-profile.mjs +167 -38
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/cryptographic-assurance.mjs +155 -0
- package/lib/ct-search.mjs +87 -47
- package/lib/dns-intelligence.mjs +3 -2
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1053 -0
- package/lib/dnssec-evidence-validation.mjs +10 -3
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-manifest.mjs +17 -129
- package/lib/domain-posture.mjs +7 -4
- package/lib/evidence-export.mjs +751 -78
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/favicon.mjs +8 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +2 -5
- package/lib/interchange-fidelity-registry.mjs +122 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +5 -1
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup-response-contract.mjs +691 -3
- package/lib/lookup.mjs +2 -2
- package/lib/observation.mjs +7 -6
- package/lib/outbound-identity.mjs +2 -3
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +21 -0
- package/lib/provider-policy-admission.mjs +9 -4
- package/lib/rdap-normalization.mjs +18 -5
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +6 -6
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/risk-calibration-summary.mjs +338 -0
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +96 -42
- package/lib/security-txt.mjs +26 -3
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +784 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/threat-intelligence-runtime.mjs +4 -13
- package/lib/threat-intelligence-types.mjs +18 -1
- package/lib/tls-intelligence.mjs +11 -5
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/website-technology.mjs +2 -6
- package/package.json +1 -1
- package/third-party-notices.txt +95 -5817
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
export const LOOKUP_PRESENTATION_STORAGE_KEY = 'whoisleuth:lookup-presentation:v1';
|
|
2
|
-
export const
|
|
3
|
-
|
|
4
|
-
Object.freeze({ id: 'standard', label: 'Evidence', detail: 'Settled evidence is grouped by task while source detail and raw evidence stay collapsed until you inspect them.' }),
|
|
5
|
-
Object.freeze({ id: 'full', label: 'All evidence', detail: 'Every settled evidence family remains available, including advanced and bounded raw evidence.' }),
|
|
6
|
-
]);
|
|
2
|
+
export const MAX_LOOKUP_PRESENTATION_SERIALIZED_BYTES = 1_024;
|
|
3
|
+
export const MAX_LOOKUP_URL_QUERY_LENGTH = 4_096;
|
|
7
4
|
export const LOOKUP_TASK_VIEWS = Object.freeze([
|
|
8
5
|
Object.freeze({ id: 'general', label: 'General investigation' }),
|
|
9
6
|
Object.freeze({ id: 'acquisition', label: 'Acquisition review' }),
|
|
@@ -11,24 +8,84 @@ export const LOOKUP_TASK_VIEWS = Object.freeze([
|
|
|
11
8
|
Object.freeze({ id: 'incident', label: 'Incident response' }),
|
|
12
9
|
Object.freeze({ id: 'owned', label: 'Owned-domain posture' }),
|
|
13
10
|
]);
|
|
14
|
-
const DENSITIES = new Set(LOOKUP_EVIDENCE_DENSITIES.map((option) => option.id));
|
|
15
11
|
const TASKS = new Set(LOOKUP_TASK_VIEWS.map((option) => option.id));
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/u;
|
|
13
|
+
export function parseLookupTaskContext(value) {
|
|
14
|
+
if (typeof value !== 'string' || value.length > 16)
|
|
15
|
+
return null;
|
|
16
|
+
return TASKS.has(value) ? value : null;
|
|
20
17
|
}
|
|
21
18
|
export function normalizeLookupTaskView(value) {
|
|
22
19
|
return typeof value === 'string' && TASKS.has(value)
|
|
23
20
|
? value
|
|
24
21
|
: 'general';
|
|
25
22
|
}
|
|
23
|
+
function lookupUrlQuery(value) {
|
|
24
|
+
if (value === null || value.length > MAX_LOOKUP_URL_QUERY_LENGTH || CONTROL_CHARACTERS.test(value))
|
|
25
|
+
return null;
|
|
26
|
+
const normalized = value.trim();
|
|
27
|
+
return normalized ? normalized : null;
|
|
28
|
+
}
|
|
29
|
+
function lookupUrlDepth(value) {
|
|
30
|
+
return value === 'fast' || value === 'deep' ? value : null;
|
|
31
|
+
}
|
|
32
|
+
function record(value) {
|
|
33
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
34
|
+
? value
|
|
35
|
+
: null;
|
|
36
|
+
}
|
|
37
|
+
export function lookupResultDepth(value, fallback = null) {
|
|
38
|
+
const result = record(value);
|
|
39
|
+
if (!result)
|
|
40
|
+
return fallback;
|
|
41
|
+
const availability = record(result.availability);
|
|
42
|
+
if (availability?.deepScanComplete === false)
|
|
43
|
+
return 'fast';
|
|
44
|
+
if (availability?.deepScanComplete === true)
|
|
45
|
+
return 'deep';
|
|
46
|
+
const whois = record(result.whois);
|
|
47
|
+
if (whois?.skipped === true
|
|
48
|
+
&& typeof whois.detail === 'string'
|
|
49
|
+
&& /fast rdap-only mode/iu.test(whois.detail))
|
|
50
|
+
return 'fast';
|
|
51
|
+
const scanModes = [
|
|
52
|
+
record(result.reverseDns)?.scanMode,
|
|
53
|
+
record(availability?.dns)?.scanMode,
|
|
54
|
+
record(availability?.http)?.scanMode,
|
|
55
|
+
];
|
|
56
|
+
if (scanModes.includes('deep'))
|
|
57
|
+
return 'deep';
|
|
58
|
+
if (scanModes.includes('fast'))
|
|
59
|
+
return 'fast';
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
export function reconcileLookupUrlState(current, searchParams, preferredTask) {
|
|
63
|
+
const explicitQuery = lookupUrlQuery(searchParams.get('q'));
|
|
64
|
+
const explicitDepth = lookupUrlDepth(searchParams.get('depth'));
|
|
65
|
+
const queryChanged = explicitQuery !== null && explicitQuery !== current.query;
|
|
66
|
+
const depthChanged = explicitDepth !== null && explicitDepth !== current.depth;
|
|
67
|
+
const conflictsWithResult = current.result !== null && ((explicitQuery !== null && explicitQuery !== current.completedTarget)
|
|
68
|
+
|| (explicitDepth !== null && explicitDepth !== current.retainedResultDepth));
|
|
69
|
+
return {
|
|
70
|
+
query: explicitQuery ?? current.query,
|
|
71
|
+
depth: explicitDepth ?? current.depth,
|
|
72
|
+
task: parseLookupTaskContext(searchParams.get('task')) ?? normalizeLookupTaskView(preferredTask),
|
|
73
|
+
result: conflictsWithResult ? null : current.result,
|
|
74
|
+
completedTarget: conflictsWithResult ? '' : current.completedTarget,
|
|
75
|
+
error: conflictsWithResult || queryChanged || depthChanged ? '' : current.error,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
26
78
|
export function readLookupPresentation(storage) {
|
|
27
79
|
try {
|
|
28
|
-
const
|
|
80
|
+
const serialized = storage.getItem(LOOKUP_PRESENTATION_STORAGE_KEY);
|
|
81
|
+
if (serialized === null
|
|
82
|
+
|| serialized.length > MAX_LOOKUP_PRESENTATION_SERIALIZED_BYTES
|
|
83
|
+
|| new TextEncoder().encode(serialized).byteLength > MAX_LOOKUP_PRESENTATION_SERIALIZED_BYTES) {
|
|
84
|
+
return { task: 'general' };
|
|
85
|
+
}
|
|
86
|
+
const stored = JSON.parse(serialized);
|
|
29
87
|
if (stored?.version === 1) {
|
|
30
88
|
return {
|
|
31
|
-
density: normalizeLookupEvidenceDensity(stored.density),
|
|
32
89
|
task: normalizeLookupTaskView(stored.task),
|
|
33
90
|
};
|
|
34
91
|
}
|
|
@@ -36,13 +93,12 @@ export function readLookupPresentation(storage) {
|
|
|
36
93
|
catch {
|
|
37
94
|
// Invalid or unavailable browser storage falls back to the stable defaults.
|
|
38
95
|
}
|
|
39
|
-
return {
|
|
96
|
+
return { task: 'general' };
|
|
40
97
|
}
|
|
41
98
|
export function writeLookupPresentation(storage, state) {
|
|
42
99
|
try {
|
|
43
100
|
storage.setItem(LOOKUP_PRESENTATION_STORAGE_KEY, JSON.stringify({
|
|
44
101
|
version: 1,
|
|
45
|
-
density: normalizeLookupEvidenceDensity(state.density),
|
|
46
102
|
task: normalizeLookupTaskView(state.task),
|
|
47
103
|
}));
|
|
48
104
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BoundedJsonResponseError, requestJsonCapped, } from "../bounded-json-response.js";
|
|
1
2
|
export const LOOKUP_SOURCE_REFRESH_VERSION = 1;
|
|
2
3
|
export const LOOKUP_SOURCE_STALE_AFTER_DAYS = 7;
|
|
3
4
|
export const LOOKUP_FRESHNESS_POLICY_VERSION = 1;
|
|
@@ -141,47 +142,6 @@ function record(value) {
|
|
|
141
142
|
? value
|
|
142
143
|
: {};
|
|
143
144
|
}
|
|
144
|
-
async function boundedJson(response) {
|
|
145
|
-
const declaredLength = Number(response.headers.get('content-length'));
|
|
146
|
-
if (Number.isFinite(declaredLength) && declaredLength > MAX_LOOKUP_SOURCE_REFRESH_BYTES) {
|
|
147
|
-
throw new Error('oversized');
|
|
148
|
-
}
|
|
149
|
-
if (!response.body)
|
|
150
|
-
return {};
|
|
151
|
-
const reader = response.body.getReader();
|
|
152
|
-
const chunks = [];
|
|
153
|
-
let total = 0;
|
|
154
|
-
try {
|
|
155
|
-
while (true) {
|
|
156
|
-
const { done, value } = await reader.read();
|
|
157
|
-
if (done)
|
|
158
|
-
break;
|
|
159
|
-
if (!value)
|
|
160
|
-
continue;
|
|
161
|
-
total += value.byteLength;
|
|
162
|
-
if (total > MAX_LOOKUP_SOURCE_REFRESH_BYTES)
|
|
163
|
-
throw new Error('oversized');
|
|
164
|
-
chunks.push(value);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
finally {
|
|
168
|
-
if (total > MAX_LOOKUP_SOURCE_REFRESH_BYTES)
|
|
169
|
-
await reader.cancel().catch(() => { });
|
|
170
|
-
reader.releaseLock();
|
|
171
|
-
}
|
|
172
|
-
const bytes = new Uint8Array(total);
|
|
173
|
-
let offset = 0;
|
|
174
|
-
for (const chunk of chunks) {
|
|
175
|
-
bytes.set(chunk, offset);
|
|
176
|
-
offset += chunk.byteLength;
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
return JSON.parse(new TextDecoder().decode(bytes));
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
return {};
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
145
|
function text(value, maximum = 180) {
|
|
186
146
|
return String(value ?? '')
|
|
187
147
|
.replace(/[\u0000-\u001f\u007f]+/gu, ' ')
|
|
@@ -194,6 +154,7 @@ function summarizeSource(plan, body, observedAt, depth, supersedesObservedAt) {
|
|
|
194
154
|
version: LOOKUP_SOURCE_REFRESH_VERSION,
|
|
195
155
|
id: plan.id,
|
|
196
156
|
observedAt,
|
|
157
|
+
attemptedAt: observedAt,
|
|
197
158
|
reason: plan.reason,
|
|
198
159
|
evidenceIds: plan.evidenceIds.slice(0, 12),
|
|
199
160
|
supersedesObservedAt,
|
|
@@ -208,7 +169,6 @@ function summarizeSource(plan, body, observedAt, depth, supersedesObservedAt) {
|
|
|
208
169
|
detail: complete
|
|
209
170
|
? `Registry RDAP returned a validated ${upstreamStatus} response.`
|
|
210
171
|
: `Registry RDAP returned ${Number.isFinite(upstreamStatus) ? `HTTP ${upstreamStatus}` : 'no complete structured record'}.`,
|
|
211
|
-
observedAt,
|
|
212
172
|
};
|
|
213
173
|
}
|
|
214
174
|
if (plan.id === 'whois') {
|
|
@@ -221,7 +181,6 @@ function summarizeSource(plan, body, observedAt, depth, supersedesObservedAt) {
|
|
|
221
181
|
detail: complete
|
|
222
182
|
? `WHOIS returned a complete ${chain.length}-hop referral chain.`
|
|
223
183
|
: `WHOIS returned ${chain.length} hop${chain.length === 1 ? '' : 's'} with ${chainStatus || 'unknown'} chain status.`,
|
|
224
|
-
observedAt,
|
|
225
184
|
};
|
|
226
185
|
}
|
|
227
186
|
const state = text(body.state, 40) || 'unknown';
|
|
@@ -234,7 +193,19 @@ function summarizeSource(plan, body, observedAt, depth, supersedesObservedAt) {
|
|
|
234
193
|
...base,
|
|
235
194
|
state: complete ? 'complete' : 'limited',
|
|
236
195
|
detail: `Domain evidence returned ${state}${sourceStates.length ? `; DNS, HTTP, and TLS states: ${sourceStates.join(', ')}` : ''}.`,
|
|
237
|
-
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function unavailableRefreshResult(plan, attemptedAt, supersedesObservedAt, detail) {
|
|
199
|
+
return {
|
|
200
|
+
version: LOOKUP_SOURCE_REFRESH_VERSION,
|
|
201
|
+
id: plan.id,
|
|
202
|
+
state: 'unavailable',
|
|
203
|
+
detail,
|
|
204
|
+
observedAt: null,
|
|
205
|
+
attemptedAt,
|
|
206
|
+
reason: plan.reason,
|
|
207
|
+
evidenceIds: plan.evidenceIds.slice(0, 12),
|
|
208
|
+
supersedesObservedAt,
|
|
238
209
|
};
|
|
239
210
|
}
|
|
240
211
|
export async function requestLookupSourceRefresh(plan, query, depth, options = {}) {
|
|
@@ -244,19 +215,24 @@ export async function requestLookupSourceRefresh(plan, query, depth, options = {
|
|
|
244
215
|
const timeoutMs = Number.isFinite(options.timeoutMs)
|
|
245
216
|
? Math.max(1, Math.min(LOOKUP_SOURCE_REFRESH_TIMEOUT_MS, Math.round(Number(options.timeoutMs))))
|
|
246
217
|
: LOOKUP_SOURCE_REFRESH_TIMEOUT_MS;
|
|
247
|
-
const
|
|
248
|
-
const
|
|
218
|
+
const attemptedAt = options.now?.() ?? new Date().toISOString();
|
|
219
|
+
const supersedesObservedAt = options.supersedesObservedAt === undefined
|
|
220
|
+
? plan.supersedesObservedAt
|
|
221
|
+
: options.supersedesObservedAt;
|
|
249
222
|
try {
|
|
250
223
|
const url = `${plan.endpoint}?q=${encodeURIComponent(normalizedQuery)}${plan.id === 'availability' && depth === 'fast' ? '&fast=true' : ''}`;
|
|
251
|
-
const response = await (
|
|
224
|
+
const { response, body: rawBody } = await requestJsonCapped(url, {
|
|
252
225
|
credentials: 'same-origin',
|
|
253
|
-
|
|
226
|
+
}, {
|
|
227
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
228
|
+
maximumBytes: MAX_LOOKUP_SOURCE_REFRESH_BYTES,
|
|
229
|
+
timeoutMs,
|
|
254
230
|
});
|
|
255
|
-
const body = record(
|
|
231
|
+
const body = record(rawBody);
|
|
256
232
|
if (!response.ok) {
|
|
257
233
|
return {
|
|
258
|
-
ok:
|
|
259
|
-
|
|
234
|
+
ok: true,
|
|
235
|
+
value: unavailableRefreshResult(plan, attemptedAt, supersedesObservedAt, text(body.error, 240) || `The source returned HTTP ${response.status}; no new observation was recorded.`),
|
|
260
236
|
};
|
|
261
237
|
}
|
|
262
238
|
if (Object.keys(body).length > MAX_LOOKUP_SOURCE_REFRESH_KEYS) {
|
|
@@ -264,23 +240,20 @@ export async function requestLookupSourceRefresh(plan, query, depth, options = {
|
|
|
264
240
|
}
|
|
265
241
|
return {
|
|
266
242
|
ok: true,
|
|
267
|
-
value: summarizeSource(plan, body,
|
|
268
|
-
? plan.supersedesObservedAt
|
|
269
|
-
: options.supersedesObservedAt),
|
|
243
|
+
value: summarizeSource(plan, body, attemptedAt, depth, supersedesObservedAt),
|
|
270
244
|
};
|
|
271
245
|
}
|
|
272
246
|
catch (cause) {
|
|
273
|
-
if (cause instanceof
|
|
274
|
-
return { ok:
|
|
247
|
+
if (cause instanceof BoundedJsonResponseError && cause.code === 'response_too_large') {
|
|
248
|
+
return { ok: true, value: unavailableRefreshResult(plan, attemptedAt, supersedesObservedAt, 'The source response exceeded the local limit; no new observation was recorded.') };
|
|
275
249
|
}
|
|
276
250
|
return {
|
|
277
|
-
ok:
|
|
278
|
-
|
|
251
|
+
ok: true,
|
|
252
|
+
value: unavailableRefreshResult(plan, attemptedAt, supersedesObservedAt, cause instanceof BoundedJsonResponseError && cause.code === 'timeout'
|
|
253
|
+
? 'The source refresh timed out; no new observation was recorded.'
|
|
254
|
+
: 'The source refresh could not be completed; no new observation was recorded.'),
|
|
279
255
|
};
|
|
280
256
|
}
|
|
281
|
-
finally {
|
|
282
|
-
clearTimeout(timeout);
|
|
283
|
-
}
|
|
284
257
|
}
|
|
285
258
|
export function mergeLookupSourceRefreshLedger(current, result) {
|
|
286
259
|
const candidates = [
|
|
@@ -289,10 +262,10 @@ export function mergeLookupSourceRefreshLedger(current, result) {
|
|
|
289
262
|
];
|
|
290
263
|
const byObservation = new Map();
|
|
291
264
|
for (const entry of candidates.slice(-MAX_LOOKUP_SOURCE_REFRESH_HISTORY * 4)) {
|
|
292
|
-
byObservation.set(`${entry.id}:${entry.
|
|
265
|
+
byObservation.set(`${entry.id}:${entry.attemptedAt}`, entry);
|
|
293
266
|
}
|
|
294
267
|
const ordered = [...byObservation.values()]
|
|
295
|
-
.sort((left, right) => Date.parse(left.
|
|
268
|
+
.sort((left, right) => Date.parse(left.attemptedAt) - Date.parse(right.attemptedAt));
|
|
296
269
|
const truncated = Boolean(current?.truncated)
|
|
297
270
|
|| ordered.length > MAX_LOOKUP_SOURCE_REFRESH_HISTORY
|
|
298
271
|
|| candidates.length > MAX_LOOKUP_SOURCE_REFRESH_HISTORY;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Opaque browser-local identifiers shared by independently owned record
|
|
2
|
+
// models. Validation is intentionally exact: callers must not trim, case-fold,
|
|
3
|
+
// repair, resolve, or otherwise reinterpret an identifier.
|
|
4
|
+
export const OPAQUE_REFERENCE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
5
|
+
export function normalizeOpaqueReferenceId(value) {
|
|
6
|
+
return typeof value === 'string' && OPAQUE_REFERENCE_ID_PATTERN.test(value)
|
|
7
|
+
? value
|
|
8
|
+
: null;
|
|
9
|
+
}
|
|
@@ -5,7 +5,7 @@ import { hammingDistanceHex, isInformativePerceptualHash, isPerceptualHash, } fr
|
|
|
5
5
|
// this only gates whether a WHOIS/RDAP-sourced string is safe to drop into a
|
|
6
6
|
// mailto: URI as the recipient, not a general email validator. mailto:
|
|
7
7
|
// treats a comma as an additional-recipient separator (RFC 6068), so a
|
|
8
|
-
// registrant/abuse-contact field containing e.g. "
|
|
8
|
+
// registrant/abuse-contact field containing e.g. "person@example.test,cc@hostile.example"
|
|
9
9
|
// would silently add a second recipient to the outreach/abuse draft the user
|
|
10
10
|
// opens - rejecting anything outside a single plain address closes that off.
|
|
11
11
|
const SIMPLE_EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
|
|
@@ -151,6 +151,10 @@ function splitDelimitedLine(line, delimiter = ',') {
|
|
|
151
151
|
return result.map((s) => s.trim());
|
|
152
152
|
}
|
|
153
153
|
const DOMAIN_HEADER_NAMES = ['domain', 'domain_name', 'domain name', 'hostname', 'name'];
|
|
154
|
+
export const MAX_DOMAIN_INPUT_CHARACTERS = 2 * 1024 * 1024;
|
|
155
|
+
export const MAX_DOMAIN_INPUT_BYTES = 2 * 1024 * 1024;
|
|
156
|
+
export const MAX_DOMAIN_INPUT_LINES = 10_000;
|
|
157
|
+
export const MAX_DOMAIN_INPUT_SEPARATORS = 20_000;
|
|
154
158
|
function detectDelimiter(line) {
|
|
155
159
|
const candidates = [',', ';', '\t'];
|
|
156
160
|
let best = null;
|
|
@@ -182,10 +186,29 @@ function looksLikeLookupToken(value) {
|
|
|
182
186
|
}
|
|
183
187
|
}
|
|
184
188
|
export function parseDomainInput(text) {
|
|
185
|
-
const
|
|
189
|
+
const source = String(text || '');
|
|
190
|
+
if (source.length > MAX_DOMAIN_INPUT_CHARACTERS) {
|
|
191
|
+
return { entries: [], duplicates: 0, usedHeader: false, tooLarge: true };
|
|
192
|
+
}
|
|
193
|
+
if (new TextEncoder().encode(source).byteLength > MAX_DOMAIN_INPUT_BYTES) {
|
|
194
|
+
return { entries: [], duplicates: 0, usedHeader: false, tooLarge: true };
|
|
195
|
+
}
|
|
196
|
+
let lineCount = 1;
|
|
197
|
+
let separatorCount = 0;
|
|
198
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
199
|
+
const character = source[index];
|
|
200
|
+
if (character === '\n')
|
|
201
|
+
lineCount += 1;
|
|
202
|
+
if (character === ',' || character === ';' || character === '\t')
|
|
203
|
+
separatorCount += 1;
|
|
204
|
+
if (lineCount > MAX_DOMAIN_INPUT_LINES || separatorCount > MAX_DOMAIN_INPUT_SEPARATORS) {
|
|
205
|
+
return { entries: [], duplicates: 0, usedHeader: false, tooLarge: true };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const normalized = source.replace(/^\uFEFF/, '');
|
|
186
209
|
const lines = normalized.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
187
210
|
if (lines.length === 0)
|
|
188
|
-
return { entries: [], duplicates: 0, usedHeader: false };
|
|
211
|
+
return { entries: [], duplicates: 0, usedHeader: false, tooLarge: false };
|
|
189
212
|
const firstLine = lines[0] ?? '';
|
|
190
213
|
const delimiter = detectDelimiter(firstLine);
|
|
191
214
|
const firstCells = delimiter ? splitDelimitedLine(firstLine, delimiter) : [firstLine];
|
|
@@ -228,5 +251,5 @@ export function parseDomainInput(text) {
|
|
|
228
251
|
seen.add(key);
|
|
229
252
|
entries.push(value);
|
|
230
253
|
}
|
|
231
|
-
return { entries, duplicates, usedHeader };
|
|
254
|
+
return { entries, duplicates, usedHeader, tooLarge: false };
|
|
232
255
|
}
|
|
@@ -2,7 +2,14 @@ import { scaleBand } from 'd3-scale';
|
|
|
2
2
|
import { boundedVisualizationId as boundedId, boundedVisualizationText as boundedText, } from "./visualization-bounds.js";
|
|
3
3
|
export const MAX_VISUAL_MATRIX_ROWS = 24;
|
|
4
4
|
export const MAX_VISUAL_MATRIX_COLUMNS = 6;
|
|
5
|
-
|
|
5
|
+
// A row is one source-pair lane, so it can publish at most one cell for each
|
|
6
|
+
// allowed column. This keeps output at 24 × 6 = 144 cells and prevents hostile
|
|
7
|
+
// duplicate inputs from creating sub-pixel marks or unbounded cell arrays.
|
|
8
|
+
export const MAX_VISUAL_MATRIX_CELLS = MAX_VISUAL_MATRIX_ROWS * MAX_VISUAL_MATRIX_COLUMNS;
|
|
9
|
+
const MAX_VISUAL_MATRIX_COLUMN_INPUTS = MAX_VISUAL_MATRIX_COLUMNS * 4;
|
|
10
|
+
const MAX_VISUAL_MATRIX_ROW_INPUTS = MAX_VISUAL_MATRIX_ROWS * 4;
|
|
11
|
+
const MAX_VISUAL_MATRIX_CELL_INPUTS_PER_ROW = MAX_VISUAL_MATRIX_COLUMNS * 4;
|
|
12
|
+
function normalizedMatrixTone(value) {
|
|
6
13
|
const state = boundedText(value, 30).toLowerCase();
|
|
7
14
|
if (state === 'equal' || state === 'equivalent' || state === 'same')
|
|
8
15
|
return 'equal';
|
|
@@ -22,45 +29,79 @@ function normalizedMatrixState(value) {
|
|
|
22
29
|
return 'unknown';
|
|
23
30
|
}
|
|
24
31
|
export function projectEvidenceMatrix(rawColumns, rawRows) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
const columnInputs = Array.isArray(rawColumns) ? rawColumns : [];
|
|
33
|
+
const columns = [];
|
|
34
|
+
const seenColumns = new Set();
|
|
35
|
+
for (const rawColumn of columnInputs.slice(0, MAX_VISUAL_MATRIX_COLUMN_INPUTS)) {
|
|
36
|
+
const column = boundedText(rawColumn, 40);
|
|
37
|
+
if (!column || seenColumns.has(column) || columns.length >= MAX_VISUAL_MATRIX_COLUMNS)
|
|
38
|
+
continue;
|
|
39
|
+
seenColumns.add(column);
|
|
40
|
+
columns.push(column);
|
|
41
|
+
}
|
|
29
42
|
const columnSet = new Set(columns);
|
|
30
43
|
const seenRows = new Set();
|
|
31
|
-
const
|
|
32
|
-
|
|
44
|
+
const rowInputs = Array.isArray(rawRows) ? rawRows : [];
|
|
45
|
+
const rows = [];
|
|
46
|
+
let discardedCellInputs = 0;
|
|
47
|
+
const scannedRows = rowInputs.slice(0, MAX_VISUAL_MATRIX_ROW_INPUTS);
|
|
48
|
+
for (const [index, row] of scannedRows.entries()) {
|
|
49
|
+
if (rows.length >= MAX_VISUAL_MATRIX_ROWS)
|
|
50
|
+
break;
|
|
33
51
|
const id = boundedId(row?.id) || `row-${index}`;
|
|
34
52
|
const label = boundedText(row?.label, 56);
|
|
35
53
|
if (!label || seenRows.has(id))
|
|
36
|
-
|
|
54
|
+
continue;
|
|
37
55
|
seenRows.add(id);
|
|
38
56
|
const rowCells = (Array.isArray(row.cells) ? row.cells : []);
|
|
39
|
-
const
|
|
40
|
-
|
|
57
|
+
const scannedCells = rowCells.slice(0, MAX_VISUAL_MATRIX_CELL_INPUTS_PER_ROW);
|
|
58
|
+
discardedCellInputs += Math.max(0, rowCells.length - scannedCells.length);
|
|
59
|
+
const byColumn = new Map();
|
|
60
|
+
for (const cell of scannedCells) {
|
|
41
61
|
const column = boundedText(cell?.column, 40);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
if (!columnSet.has(column) || byColumn.has(column)) {
|
|
63
|
+
discardedCellInputs += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const exactState = boundedText(cell?.state, 40).toLowerCase() || 'unknown';
|
|
67
|
+
byColumn.set(column, {
|
|
68
|
+
column,
|
|
69
|
+
state: exactState,
|
|
70
|
+
tone: normalizedMatrixTone(cell?.tone ?? exactState),
|
|
71
|
+
detail: boundedText(cell?.detail, 320),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const cells = columns.flatMap((column) => {
|
|
75
|
+
const value = byColumn.get(column);
|
|
76
|
+
if (value)
|
|
77
|
+
return [value];
|
|
78
|
+
if (row.sparse === true)
|
|
79
|
+
return [];
|
|
80
|
+
return [{
|
|
81
|
+
column,
|
|
82
|
+
state: 'not_collected',
|
|
83
|
+
tone: 'not_collected',
|
|
84
|
+
detail: '',
|
|
45
85
|
}];
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
return {
|
|
86
|
+
});
|
|
87
|
+
rows.push({
|
|
49
88
|
id,
|
|
50
89
|
label,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
|
|
90
|
+
context: boundedText(row.context, 90),
|
|
91
|
+
status: boundedText(row.status, 40).toLowerCase() || 'unknown',
|
|
92
|
+
assessment: boundedText(row.assessment, 80),
|
|
93
|
+
cells,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const discarded = {
|
|
97
|
+
columnInputs: Math.max(0, columnInputs.length - columns.length),
|
|
98
|
+
rowInputs: Math.max(0, rowInputs.length - rows.length),
|
|
99
|
+
cellInputs: discardedCellInputs,
|
|
100
|
+
};
|
|
60
101
|
const width = 900;
|
|
61
102
|
const top = 54;
|
|
62
|
-
const left =
|
|
63
|
-
const rowHeight =
|
|
103
|
+
const left = 250;
|
|
104
|
+
const rowHeight = 40;
|
|
64
105
|
const x = scaleBand().domain(columns).range([left, 870]).padding(0.12);
|
|
65
106
|
const y = scaleBand().domain(rows.map((row) => row.id))
|
|
66
107
|
.range([top, top + Math.max(1, rows.length) * rowHeight])
|
|
@@ -83,6 +124,8 @@ export function projectEvidenceMatrix(rawColumns, rawRows) {
|
|
|
83
124
|
width: x.bandwidth(),
|
|
84
125
|
})),
|
|
85
126
|
})),
|
|
86
|
-
|
|
127
|
+
discarded,
|
|
128
|
+
projectedCellCount: rows.reduce((sum, row) => sum + row.cells.length, 0),
|
|
129
|
+
truncated: discarded.columnInputs > 0 || discarded.rowInputs > 0 || discarded.cellInputs > 0,
|
|
87
130
|
};
|
|
88
131
|
}
|