@slicedearth/whoisleuth-cli 1.45.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 +59 -6
- package/bin/whoisleuth.mjs +5 -3
- 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 +2 -3
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/case-pack.mjs +819 -34
- package/cli/command-reference.mjs +48 -24
- package/cli/completion.mjs +16 -8
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discovery-scan-command-runner.mjs +4 -5
- 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/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/terminal.mjs +3 -0
- 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/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-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +376 -0
- package/cli/risk-calibration.mjs +7 -5
- package/cli/runner.mjs +147 -185
- package/cli/saved-lookup.mjs +26 -3
- package/cli/sharing-review.mjs +12 -6
- package/docs/cli-reference.md +444 -70
- package/docs/cli.md +24 -5
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
- package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +29 -5
- 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 +8 -0
- 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-model.js +2 -0
- package/frontend/src/lib/analysis/case-record-operations.js +42 -12
- 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-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 +61 -6
- package/frontend/src/lib/analysis/lookup-presentation.js +62 -0
- package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
- package/frontend/src/lib/analysis/utils.js +26 -3
- 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.js +104 -14
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +14 -12
- package/lib/bounded-file.mjs +13 -0
- package/lib/bounded-json-response.mjs +17 -4
- package/lib/bounded-json.mjs +249 -0
- 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-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/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 +59 -10
- 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 +3 -11
- package/lib/threat-intelligence-types.mjs +18 -1
- package/lib/tls-intelligence.mjs +11 -5
- package/lib/tlsa-evidence.mjs +37 -7
- package/package.json +1 -1
- package/third-party-notices.txt +95 -5817
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// Pure, bounded workspace archive composition. Existing typed store models remain the
|
|
2
2
|
// authority for normalization and merge semantics; this module only packages
|
|
3
3
|
// their portable contracts, verifies integrity, and previews conflicts.
|
|
4
|
-
import { buildCaseExport, CASE_SCHEMA_VERSION, enforceStoreBudget, mergeCases, } from "./case-model.js";
|
|
5
|
-
import { assertBrandProfileStoreBudget, BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, buildBrandProfileExport, mergeBrandProfiles, normalizeBrandProfileId, } from "./brand-profile-model.js";
|
|
4
|
+
import { buildCaseExport, CASE_SCHEMA_VERSION, CASE_IMPORT_VERSIONS, enforceStoreBudget, mergeCases, } from "./case-model.js";
|
|
5
|
+
import { assertBrandProfileStoreBudget, BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS, buildBrandProfileExport, mergeBrandProfiles, normalizeBrandProfileId, } from "./brand-profile-model.js";
|
|
6
6
|
import { assertCampaignStoreBudget, buildCampaignExport, CAMPAIGN_SCHEMA, CAMPAIGN_SCHEMA_VERSION, mergeCampaigns, } from "./campaign-model.js";
|
|
7
7
|
import { assertWatchlistStoreBudget, buildWatchlistExport, mergeWatchlistStores, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, } from "./watchlist-store.js";
|
|
8
|
-
import { assertShortlistStoreBudget, buildShortlistExport, mergeShortlistStores, SHORTLIST_SCHEMA, SHORTLIST_SCHEMA_VERSION, } from "./shortlist-model.js";
|
|
8
|
+
import { assertShortlistStoreBudget, buildShortlistExport, mergeShortlistStores, SHORTLIST_SCHEMA, SHORTLIST_SCHEMA_VERSION, SUPPORTED_SHORTLIST_SCHEMA_VERSIONS, } from "./shortlist-model.js";
|
|
9
9
|
import { assertDetectionRuleStoreBudget, buildDetectionRuleExport, DETECTION_RULE_SCHEMA, DETECTION_RULE_SCHEMA_VERSION, mergeDetectionRules, } from "./detection-rule-model.js";
|
|
10
10
|
import { buildRelationshipObservationExport, mergeRelationshipObservations, RELATIONSHIP_OBSERVATION_SCHEMA, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, } from "./relationship-observation-model.js";
|
|
11
|
-
import { BULK_SESSION_SCHEMA, BULK_SESSION_SCHEMA_VERSION, buildBulkSessionExport, enforceBulkSessionStoreBudget, mergeBulkSessions, } from "./bulk-session-model.js";
|
|
12
|
-
import { WEBSITE_SNAPSHOT_SCHEMA, WEBSITE_SNAPSHOT_SCHEMA_VERSION, buildWebsiteSnapshotExport, mergeWebsiteSnapshots, } from "./website-snapshot-model.js";
|
|
11
|
+
import { BULK_SESSION_SCHEMA, BULK_SESSION_SCHEMA_VERSION, SUPPORTED_BULK_SESSION_SCHEMA_VERSIONS, buildBulkSessionExport, enforceBulkSessionStoreBudget, mergeBulkSessions, } from "./bulk-session-model.js";
|
|
12
|
+
import { WEBSITE_SNAPSHOT_SCHEMA, WEBSITE_SNAPSHOT_SCHEMA_VERSION, SUPPORTED_WEBSITE_SNAPSHOT_SCHEMA_VERSIONS, buildWebsiteSnapshotExport, mergeWebsiteSnapshots, } from "./website-snapshot-model.js";
|
|
13
13
|
import { INVESTIGATION_TEMPLATE_SCHEMA, INVESTIGATION_TEMPLATE_VERSION, buildInvestigationTemplateExport, mergeInvestigationTemplates, } from "./investigation-template-model.js";
|
|
14
14
|
import { BULK_REVIEW_SCHEMA, BULK_REVIEW_SCHEMA_VERSION, buildBulkReviewExport, mergeBulkReviewStores, } from "./bulk-review-model.js";
|
|
15
15
|
export const WORKSPACE_ARCHIVE_SCHEMA = 'whoisleuth.workspace-archive';
|
|
@@ -41,9 +41,24 @@ export function isSupportedWorkspaceArchiveVersion(value) {
|
|
|
41
41
|
}
|
|
42
42
|
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
43
43
|
const CHECKSUM_RE = /^sha256:[a-f0-9]{64}$/;
|
|
44
|
+
const WORKSPACE_ARCHIVE_ROOT_KEYS = Object.freeze({
|
|
45
|
+
1: Object.freeze(['schema', 'version', 'generatedAt', 'manifest', 'sections', 'limitations']),
|
|
46
|
+
2: Object.freeze(['schema', 'version', 'generatedAt', 'manifest', 'sections', 'limitations']),
|
|
47
|
+
3: Object.freeze(['schema', 'version', 'generatedAt', 'manifest', 'sections', 'limitations']),
|
|
48
|
+
4: Object.freeze(['schema', 'version', 'generatedAt', 'manifest', 'sections', 'limitations']),
|
|
49
|
+
5: Object.freeze(['schema', 'version', 'generatedAt', 'manifest', 'sections', 'limitations']),
|
|
50
|
+
});
|
|
51
|
+
const WORKSPACE_ARCHIVE_MANIFEST_KEYS = Object.freeze(['sectionCount', 'totalRecords', 'sections']);
|
|
52
|
+
const WORKSPACE_ARCHIVE_MANIFEST_ENTRY_KEYS = Object.freeze(['id', 'schema', 'version', 'recordCount', 'bytes', 'checksum']);
|
|
44
53
|
function record(value) {
|
|
45
54
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
46
55
|
}
|
|
56
|
+
function assertExactKeys(value, expected, label) {
|
|
57
|
+
const keys = Object.keys(value);
|
|
58
|
+
if (keys.length !== expected.length || keys.some((key) => !expected.includes(key))) {
|
|
59
|
+
throw new Error(`The workspace archive ${label} contains missing or undeclared fields.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
47
62
|
function byteLength(value) {
|
|
48
63
|
return new TextEncoder().encode(value).byteLength;
|
|
49
64
|
}
|
|
@@ -139,6 +154,7 @@ function objectCount(data, key) {
|
|
|
139
154
|
const SECTION_DEFINITIONS = [
|
|
140
155
|
{
|
|
141
156
|
id: 'cases', label: 'Cases', schema: null, version: CASE_SCHEMA_VERSION,
|
|
157
|
+
supportedVersions: CASE_IMPORT_VERSIONS,
|
|
142
158
|
count: (data) => arrayCount(data, 'cases'),
|
|
143
159
|
merge: (local, data) => {
|
|
144
160
|
const result = mergeCases(local.cases, data);
|
|
@@ -156,6 +172,7 @@ const SECTION_DEFINITIONS = [
|
|
|
156
172
|
},
|
|
157
173
|
{
|
|
158
174
|
id: 'brandProfiles', label: 'Brand profiles', schema: BRAND_PROFILE_SCHEMA, version: BRAND_PROFILE_SCHEMA_VERSION,
|
|
175
|
+
supportedVersions: SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS,
|
|
159
176
|
count: (data) => arrayCount(data, 'profiles'),
|
|
160
177
|
merge: (local, data, now) => {
|
|
161
178
|
const result = mergeBrandProfiles(local.brandProfiles, data, { nowIso: now });
|
|
@@ -172,6 +189,7 @@ const SECTION_DEFINITIONS = [
|
|
|
172
189
|
},
|
|
173
190
|
{
|
|
174
191
|
id: 'shortlist', label: 'Shortlist', schema: SHORTLIST_SCHEMA, version: SHORTLIST_SCHEMA_VERSION,
|
|
192
|
+
supportedVersions: SUPPORTED_SHORTLIST_SCHEMA_VERSIONS,
|
|
175
193
|
count: (data) => arrayCount(data, 'entries'),
|
|
176
194
|
merge: (local, data) => {
|
|
177
195
|
const result = mergeShortlistStores(local.shortlist, data);
|
|
@@ -199,6 +217,7 @@ const SECTION_DEFINITIONS = [
|
|
|
199
217
|
label: 'Saved Bulk sessions',
|
|
200
218
|
schema: BULK_SESSION_SCHEMA,
|
|
201
219
|
version: BULK_SESSION_SCHEMA_VERSION,
|
|
220
|
+
supportedVersions: SUPPORTED_BULK_SESSION_SCHEMA_VERSIONS,
|
|
202
221
|
count: (data) => arrayCount(data, 'sessions'),
|
|
203
222
|
merge: (local, data) => {
|
|
204
223
|
const result = mergeBulkSessions(local.bulkSessions, data);
|
|
@@ -211,6 +230,7 @@ const SECTION_DEFINITIONS = [
|
|
|
211
230
|
label: 'Website profile snapshots',
|
|
212
231
|
schema: WEBSITE_SNAPSHOT_SCHEMA,
|
|
213
232
|
version: WEBSITE_SNAPSHOT_SCHEMA_VERSION,
|
|
233
|
+
supportedVersions: SUPPORTED_WEBSITE_SNAPSHOT_SCHEMA_VERSIONS,
|
|
214
234
|
count: (data) => arrayCount(data, 'snapshots'),
|
|
215
235
|
merge: (local, data) => mergeWebsiteSnapshots(local.websiteSnapshots, data),
|
|
216
236
|
},
|
|
@@ -339,11 +359,13 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
339
359
|
}
|
|
340
360
|
const sourceVersion = value.version;
|
|
341
361
|
const { bytes } = ensureArchiveBudget(value);
|
|
362
|
+
assertExactKeys(value, WORKSPACE_ARCHIVE_ROOT_KEYS[sourceVersion] ?? [], `version ${sourceVersion} envelope`);
|
|
342
363
|
const manifest = record(value.manifest);
|
|
343
364
|
const sectionValues = record(value.sections);
|
|
344
365
|
if (!manifest || !sectionValues || !Array.isArray(manifest.sections)) {
|
|
345
366
|
throw new Error('The workspace archive manifest is missing or malformed.');
|
|
346
367
|
}
|
|
368
|
+
assertExactKeys(manifest, WORKSPACE_ARCHIVE_MANIFEST_KEYS, 'manifest');
|
|
347
369
|
if (manifest.sections.length > MAX_WORKSPACE_ARCHIVE_SECTIONS) {
|
|
348
370
|
throw new Error(`Workspace archives are limited to ${MAX_WORKSPACE_ARCHIVE_SECTIONS} sections.`);
|
|
349
371
|
}
|
|
@@ -351,6 +373,10 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
351
373
|
const sections = [];
|
|
352
374
|
let totalRecords = 0;
|
|
353
375
|
for (const rawEntry of manifest.sections) {
|
|
376
|
+
const rawEntryRecord = record(rawEntry);
|
|
377
|
+
if (!rawEntryRecord)
|
|
378
|
+
throw new Error('The workspace archive manifest contains a malformed section entry.');
|
|
379
|
+
assertExactKeys(rawEntryRecord, WORKSPACE_ARCHIVE_MANIFEST_ENTRY_KEYS, 'manifest section entry');
|
|
354
380
|
const entry = manifestEntry(rawEntry);
|
|
355
381
|
if (!entry || seen.has(entry.id) || !Object.prototype.hasOwnProperty.call(sectionValues, entry.id)) {
|
|
356
382
|
throw new Error('The workspace archive manifest contains an invalid, duplicate, or missing section.');
|
|
@@ -363,6 +389,21 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
363
389
|
if (await checksum(data, options.cryptoProvider) !== entry.checksum) {
|
|
364
390
|
throw new Error(`${entry.id} failed its archive checksum check.`);
|
|
365
391
|
}
|
|
392
|
+
const embedded = record(data);
|
|
393
|
+
const embeddedVersion = embedded?.version;
|
|
394
|
+
const embeddedSchema = embedded && Object.prototype.hasOwnProperty.call(embedded, 'schema')
|
|
395
|
+
? embedded.schema
|
|
396
|
+
: null;
|
|
397
|
+
if (!embedded
|
|
398
|
+
|| typeof embeddedVersion !== 'number'
|
|
399
|
+
|| !Number.isSafeInteger(embeddedVersion)
|
|
400
|
+
|| embeddedVersion < 1
|
|
401
|
+
|| embeddedVersion > 1000
|
|
402
|
+
|| (embeddedSchema !== null && typeof embeddedSchema !== 'string')
|
|
403
|
+
|| embeddedVersion !== entry.version
|
|
404
|
+
|| embeddedSchema !== entry.schema) {
|
|
405
|
+
throw new Error(`${entry.id} section contract does not match its archive manifest.`);
|
|
406
|
+
}
|
|
366
407
|
const definition = DEFINITION_BY_ID.get(entry.id);
|
|
367
408
|
let status = 'ready';
|
|
368
409
|
let reason = '';
|
|
@@ -370,7 +411,7 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
370
411
|
status = 'unsupported';
|
|
371
412
|
reason = 'This app does not recognise the archive section.';
|
|
372
413
|
}
|
|
373
|
-
else if (
|
|
414
|
+
else if (!(definition.supportedVersions ?? [definition.version]).includes(entry.version) || entry.schema !== definition.schema) {
|
|
374
415
|
status = 'unsupported';
|
|
375
416
|
reason = entry.version > definition.version
|
|
376
417
|
? `This section uses newer schema ${entry.version}.`
|
|
@@ -403,14 +444,31 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
403
444
|
}
|
|
404
445
|
function settingsPreview(data, local, mergedProfiles) {
|
|
405
446
|
const value = record(data) || {};
|
|
406
|
-
const
|
|
447
|
+
const rawActiveProfileId = value.activeProfileId;
|
|
448
|
+
const intentionalClear = rawActiveProfileId === '';
|
|
449
|
+
const normalizedActiveProfileId = normalizeBrandProfileId(rawActiveProfileId);
|
|
450
|
+
const validActiveProfileId = typeof rawActiveProfileId === 'string'
|
|
451
|
+
&& rawActiveProfileId.length > 0
|
|
452
|
+
&& normalizedActiveProfileId === rawActiveProfileId;
|
|
407
453
|
const theme = normalizeTheme(value.theme);
|
|
454
|
+
const retainedActiveProfileId = normalizeBrandProfileId(local.settings?.activeProfileId) || '';
|
|
455
|
+
let activeProfileId = retainedActiveProfileId;
|
|
408
456
|
let skipped = 0;
|
|
409
|
-
|
|
410
|
-
|
|
457
|
+
let reason = '';
|
|
458
|
+
if (!intentionalClear && !validActiveProfileId) {
|
|
459
|
+
skipped = 1;
|
|
460
|
+
reason = 'The imported active Brand Profile preference is missing or malformed. Existing active-profile context is preserved.';
|
|
461
|
+
}
|
|
462
|
+
else if (validActiveProfileId && !mergedProfiles.some((profile) => profile?.id === normalizedActiveProfileId)) {
|
|
463
|
+
skipped = 1;
|
|
464
|
+
reason = 'The imported active Brand Profile preference will be skipped because its identifier is not available in the selected Profile data or the current browser. Existing active-profile context is preserved.';
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
activeProfileId = intentionalClear ? '' : normalizedActiveProfileId ?? retainedActiveProfileId;
|
|
468
|
+
}
|
|
411
469
|
const updated = Number(theme !== normalizeTheme(local.settings?.theme)
|
|
412
|
-
|| (
|
|
413
|
-
return { added: 0, updated, skipped, settings: { activeProfileId
|
|
470
|
+
|| (skipped === 0 && activeProfileId !== retainedActiveProfileId));
|
|
471
|
+
return { added: 0, updated, skipped, reason, settings: { activeProfileId, theme } };
|
|
414
472
|
}
|
|
415
473
|
/** Preview section-specific non-destructive merge outcomes without writing. */
|
|
416
474
|
export async function previewWorkspaceArchive(raw, localInput, options = {}) {
|
|
@@ -418,6 +476,9 @@ export async function previewWorkspaceArchive(raw, localInput, options = {}) {
|
|
|
418
476
|
const local = normalizedInput(localInput);
|
|
419
477
|
const results = [];
|
|
420
478
|
let mergedProfiles = local.brandProfiles;
|
|
479
|
+
const selectedIds = options.selectedSectionIds
|
|
480
|
+
? new Set(options.selectedSectionIds.slice(0, MAX_WORKSPACE_ARCHIVE_SECTIONS))
|
|
481
|
+
: null;
|
|
421
482
|
for (const section of archive.sections) {
|
|
422
483
|
if (section.status !== 'ready') {
|
|
423
484
|
results.push({ ...section, added: 0, updated: 0, skipped: section.recordCount, selected: false });
|
|
@@ -432,17 +493,21 @@ export async function previewWorkspaceArchive(raw, localInput, options = {}) {
|
|
|
432
493
|
: definition.merge
|
|
433
494
|
? definition.merge(local, section.data, archive.generatedAt)
|
|
434
495
|
: { added: 0, updated: 0, skipped: section.recordCount };
|
|
435
|
-
|
|
496
|
+
const selected = selectedIds ? selectedIds.has(section.id) : true;
|
|
497
|
+
if (definition.id === 'brandProfiles' && selected && result.profiles)
|
|
436
498
|
mergedProfiles = result.profiles;
|
|
437
499
|
results.push({
|
|
438
500
|
...section,
|
|
439
501
|
status: 'ready',
|
|
440
|
-
reason: ''
|
|
502
|
+
reason: definition.id === 'settings' && selected && (result.skipped ?? 0) > 0
|
|
503
|
+
? result.reason ?? 'The imported workspace preference was skipped. Existing browser-local context is preserved.'
|
|
504
|
+
: '',
|
|
441
505
|
added: result.added ?? 0,
|
|
442
506
|
updated: result.updated ?? 0,
|
|
443
507
|
skipped: result.skipped ?? 0,
|
|
444
508
|
pruned: result.pruned ?? 0,
|
|
445
|
-
|
|
509
|
+
brandProfileReferencesOmitted: result.brandProfileReferencesOmitted ?? 0,
|
|
510
|
+
selected,
|
|
446
511
|
normalizedSettings: result.settings || null,
|
|
447
512
|
});
|
|
448
513
|
}
|
|
@@ -461,6 +526,31 @@ export async function previewWorkspaceArchive(raw, localInput, options = {}) {
|
|
|
461
526
|
});
|
|
462
527
|
}
|
|
463
528
|
}
|
|
529
|
+
const profileCollision = results.find((section) => section.id === 'brandProfiles'
|
|
530
|
+
&& section.status === 'blocked'
|
|
531
|
+
&& /reuses one exact identifier for different normalised profile names/iu.test(section.reason));
|
|
532
|
+
if (profileCollision) {
|
|
533
|
+
for (const dependentId of ['cases', 'settings']) {
|
|
534
|
+
const dependentIndex = results.findIndex((section) => section.id === dependentId && section.status === 'ready');
|
|
535
|
+
const dependentSection = results[dependentIndex];
|
|
536
|
+
if (dependentIndex < 0 || !dependentSection)
|
|
537
|
+
continue;
|
|
538
|
+
results[dependentIndex] = {
|
|
539
|
+
...dependentSection,
|
|
540
|
+
status: 'blocked',
|
|
541
|
+
reason: dependentId === 'cases'
|
|
542
|
+
? 'Cases were not selected because a Brand Profile identifier collision must be resolved before their opaque references can be imported safely.'
|
|
543
|
+
: 'Workspace settings were not selected because an active Brand Profile preference could otherwise bind to a different local profile that reuses the same identifier.',
|
|
544
|
+
added: 0,
|
|
545
|
+
updated: 0,
|
|
546
|
+
skipped: dependentSection.recordCount,
|
|
547
|
+
pruned: 0,
|
|
548
|
+
brandProfileReferencesOmitted: 0,
|
|
549
|
+
selected: false,
|
|
550
|
+
normalizedSettings: dependentId === 'settings' ? null : dependentSection.normalizedSettings ?? null,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
464
554
|
return {
|
|
465
555
|
...archive,
|
|
466
556
|
sections: results,
|
package/lib/availability.mjs
CHANGED
|
@@ -115,18 +115,17 @@ const PRIVACY_MARKERS = [
|
|
|
115
115
|
/not disclosed/i,
|
|
116
116
|
/withheld for privacy/i,
|
|
117
117
|
];
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// same practical answer as an explicit privacy-proxy service: no.
|
|
118
|
+
// Explicit privacy/redaction markers establish a positive privacy signal.
|
|
119
|
+
// Usable public contact data establishes a negative one. A missing or blank
|
|
120
|
+
// contact can instead reflect registry policy, access tier, parser coverage,
|
|
121
|
+
// or ordinary omission, so it remains inconclusive rather than being promoted
|
|
122
|
+
// to an affirmative privacy claim.
|
|
124
123
|
function isPrivacyProtected(registrant) {
|
|
125
124
|
if (!registrant)
|
|
126
|
-
return
|
|
125
|
+
return null;
|
|
127
126
|
const blob = [registrant.name, registrant.org, registrant.email].filter(Boolean).join(' ');
|
|
128
127
|
if (!blob)
|
|
129
|
-
return
|
|
128
|
+
return null;
|
|
130
129
|
return PRIVACY_MARKERS.some((re) => re.test(blob));
|
|
131
130
|
}
|
|
132
131
|
// RDAP Lookup retains a bounded multi-value contact inventory, but Bulk and
|
|
@@ -197,7 +196,10 @@ async function checkDnsDelegation(domain, { resolver = dns.resolveNs } = {}) {
|
|
|
197
196
|
// domain has "no site": transient DNS/TLS/network failures, a slow origin, or
|
|
198
197
|
// an HTTP error can all produce the same null body. Keeping that distinction
|
|
199
198
|
// avoids turning an inconclusive probe into a false inactivity claim.
|
|
200
|
-
async function fetchHomepage(domain, { fetcher = safeFetchDetailed } = {}) {
|
|
199
|
+
async function fetchHomepage(domain, { fetcher = safeFetchDetailed, timeoutMs = HOMEPAGE_FETCH_TIMEOUT_MS } = {}) {
|
|
200
|
+
const requestTimeoutMs = Number.isInteger(timeoutMs) && timeoutMs >= 10 && timeoutMs <= HOMEPAGE_FETCH_TIMEOUT_MS
|
|
201
|
+
? timeoutMs
|
|
202
|
+
: HOMEPAGE_FETCH_TIMEOUT_MS;
|
|
201
203
|
const headers = whoisleuthRequestHeaders();
|
|
202
204
|
const failures = [];
|
|
203
205
|
const probeStartedAt = Date.now();
|
|
@@ -205,7 +207,7 @@ async function fetchHomepage(domain, { fetcher = safeFetchDetailed } = {}) {
|
|
|
205
207
|
const requestUrl = `${scheme}://${domain}`;
|
|
206
208
|
const attemptStartedAt = Date.now();
|
|
207
209
|
const controller = new AbortController();
|
|
208
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
210
|
+
const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
209
211
|
try {
|
|
210
212
|
const fetched = await fetcher(requestUrl, { signal: controller.signal, headers });
|
|
211
213
|
const fetchedResponse = fetched instanceof Response ? fetched : fetched.response;
|
|
@@ -274,7 +276,7 @@ async function fetchHomepage(domain, { fetcher = safeFetchDetailed } = {}) {
|
|
|
274
276
|
catch (err) {
|
|
275
277
|
const error = errorRecord(err);
|
|
276
278
|
const reason = error.name === 'AbortError'
|
|
277
|
-
?
|
|
279
|
+
? `timed out after ${requestTimeoutMs} milliseconds`
|
|
278
280
|
: nonEmptyErrorMessage(err, 'request failed')
|
|
279
281
|
.replace(/[\u0000-\u001f\u007f]+/g, ' ')
|
|
280
282
|
.slice(0, 180);
|
|
@@ -733,4 +735,4 @@ async function checkDomainAvailability(domain, options = {}) {
|
|
|
733
735
|
hasDmarc: dnsIntelligence.hasDmarc,
|
|
734
736
|
};
|
|
735
737
|
}
|
|
736
|
-
export { checkDomainAvailability, checkDnsDelegation, fetchHomepage, deriveWebsiteActivity, forSaleRedirectSignal, parseRegistryDate as parseWhoisDate, };
|
|
738
|
+
export { checkDomainAvailability, checkDnsDelegation, fetchHomepage, isPrivacyProtected, deriveWebsiteActivity, forSaleRedirectSignal, parseRegistryDate as parseWhoisDate, };
|
package/lib/bounded-file.mjs
CHANGED
|
@@ -6,6 +6,13 @@ function boundedSize(value, fallback) {
|
|
|
6
6
|
? Number(value)
|
|
7
7
|
: fallback;
|
|
8
8
|
}
|
|
9
|
+
function throwIfAborted(signal) {
|
|
10
|
+
if (!signal?.aborted)
|
|
11
|
+
return;
|
|
12
|
+
throw signal.reason instanceof Error
|
|
13
|
+
? signal.reason
|
|
14
|
+
: new DOMException('The operation was aborted.', 'AbortError');
|
|
15
|
+
}
|
|
9
16
|
export async function readBoundedRegularFile(filePath, options, dependencies = {}) {
|
|
10
17
|
const label = options.label?.trim().slice(0, 120) || 'Input file';
|
|
11
18
|
const maximumBytes = boundedSize(options.maximumBytes, 0);
|
|
@@ -19,6 +26,7 @@ export async function readBoundedRegularFile(filePath, options, dependencies = {
|
|
|
19
26
|
if (expectedBytes !== null && (expectedBytes < minimumBytes || expectedBytes > maximumBytes)) {
|
|
20
27
|
throw new TypeError(`${label} expected size is outside its byte boundary.`);
|
|
21
28
|
}
|
|
29
|
+
throwIfAborted(options.signal);
|
|
22
30
|
// Non-blocking open prevents a named pipe from stalling the process before
|
|
23
31
|
// the descriptor can be classified. Final-component symlinks remain denied
|
|
24
32
|
// unless an analyst-facing input explicitly opts in; artefacts resolved
|
|
@@ -28,7 +36,9 @@ export async function readBoundedRegularFile(filePath, options, dependencies = {
|
|
|
28
36
|
| (options.allowSymbolicLink ? 0 : constants.O_NOFOLLOW);
|
|
29
37
|
const handle = await (dependencies.openFile ?? open)(filePath, flags);
|
|
30
38
|
try {
|
|
39
|
+
throwIfAborted(options.signal);
|
|
31
40
|
const before = await handle.stat();
|
|
41
|
+
throwIfAborted(options.signal);
|
|
32
42
|
if (!before.isFile())
|
|
33
43
|
throw new TypeError(`${label} must be a regular file.`);
|
|
34
44
|
if (before.size < minimumBytes)
|
|
@@ -44,12 +54,15 @@ export async function readBoundedRegularFile(filePath, options, dependencies = {
|
|
|
44
54
|
const storage = Buffer.allocUnsafe(Math.min(maximumBytes + 1, before.size + 1));
|
|
45
55
|
let offset = 0;
|
|
46
56
|
while (offset < storage.length) {
|
|
57
|
+
throwIfAborted(options.signal);
|
|
47
58
|
const { bytesRead } = await handle.read(storage, offset, storage.length - offset, null);
|
|
48
59
|
if (bytesRead === 0)
|
|
49
60
|
break;
|
|
50
61
|
offset += bytesRead;
|
|
51
62
|
}
|
|
63
|
+
throwIfAborted(options.signal);
|
|
52
64
|
const after = await handle.stat();
|
|
65
|
+
throwIfAborted(options.signal);
|
|
53
66
|
if (offset > maximumBytes
|
|
54
67
|
|| after.size !== before.size
|
|
55
68
|
|| offset !== after.size
|
|
@@ -42,7 +42,7 @@ function boundedPositiveInteger(value, fallback, maximum) {
|
|
|
42
42
|
? Math.max(1, Math.min(maximum, Math.round(value)))
|
|
43
43
|
: fallback;
|
|
44
44
|
}
|
|
45
|
-
export async function readJsonResponseCapped(response, maximumBytes = STANDARD_JSON_RESPONSE_BYTES, signal) {
|
|
45
|
+
export async function readJsonResponseCapped(response, maximumBytes = STANDARD_JSON_RESPONSE_BYTES, signal, validateRawJson) {
|
|
46
46
|
const maxBytes = boundedPositiveInteger(maximumBytes, STANDARD_JSON_RESPONSE_BYTES, LARGE_JSON_RESPONSE_BYTES);
|
|
47
47
|
const declared = response.headers.get('content-length');
|
|
48
48
|
if (declared && /^\d+$/u.test(declared)) {
|
|
@@ -85,11 +85,24 @@ export async function readJsonResponseCapped(response, maximumBytes = STANDARD_J
|
|
|
85
85
|
bytes.set(chunk, offset);
|
|
86
86
|
offset += chunk.byteLength;
|
|
87
87
|
}
|
|
88
|
+
let raw;
|
|
88
89
|
try {
|
|
89
|
-
|
|
90
|
+
raw = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
90
91
|
}
|
|
91
92
|
catch {
|
|
92
|
-
throw new BoundedJsonResponseError('invalid_json', 'Response body was not valid UTF-8
|
|
93
|
+
throw new BoundedJsonResponseError('invalid_json', 'Response body was not valid UTF-8.');
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
validateRawJson?.(raw);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new BoundedJsonResponseError('invalid_json', 'Response body did not satisfy the bounded JSON structure contract.');
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(raw);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new BoundedJsonResponseError('invalid_json', 'Response body was not valid JSON.');
|
|
93
106
|
}
|
|
94
107
|
}
|
|
95
108
|
export async function requestJsonCapped(input, init = {}, options = {}) {
|
|
@@ -122,7 +135,7 @@ export async function requestJsonCapped(input, init = {}, options = {}) {
|
|
|
122
135
|
}
|
|
123
136
|
let body;
|
|
124
137
|
try {
|
|
125
|
-
body = await readJsonResponseCapped(response, options.maximumBytes, controller.signal);
|
|
138
|
+
body = await readJsonResponseCapped(response, options.maximumBytes, controller.signal, options.validateRawJson);
|
|
126
139
|
}
|
|
127
140
|
catch (cause) {
|
|
128
141
|
// Reverse proxies and hosting adapters can replace an upstream JSON
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
export const MAX_BOUNDED_JSON_DEPTH = 48;
|
|
2
|
+
export const MAX_BOUNDED_JSON_KEYS = 50_000;
|
|
3
|
+
export const MAX_BOUNDED_JSON_VALUES = 100_000;
|
|
4
|
+
export const MAX_BOUNDED_JSON_CONTAINER_ITEMS = 10_000;
|
|
5
|
+
export const UNSAFE_JSON_OBJECT_KEYS = Object.freeze(['__proto__']);
|
|
6
|
+
function syntaxError() {
|
|
7
|
+
throw new TypeError('Artefact input is not valid JSON.');
|
|
8
|
+
}
|
|
9
|
+
export function isSafeJsonObjectKey(value) {
|
|
10
|
+
return !UNSAFE_JSON_OBJECT_KEYS.includes(value);
|
|
11
|
+
}
|
|
12
|
+
export function assertBoundedJsonStructure(value, label = 'JSON value', limits = {}) {
|
|
13
|
+
const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
|
|
14
|
+
const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
|
|
15
|
+
const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
|
|
16
|
+
const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
|
|
17
|
+
let keys = 0;
|
|
18
|
+
let values = 0;
|
|
19
|
+
const ancestors = new Set();
|
|
20
|
+
const visit = (current, depth) => {
|
|
21
|
+
values += 1;
|
|
22
|
+
if (values > maximumValues) {
|
|
23
|
+
throw new TypeError(`${label} exceeds the ${maximumValues}-value limit.`);
|
|
24
|
+
}
|
|
25
|
+
if (depth > maximumDepth) {
|
|
26
|
+
throw new TypeError(`${label} exceeds the ${maximumDepth}-level nesting limit.`);
|
|
27
|
+
}
|
|
28
|
+
if (current === null || typeof current === 'string' || typeof current === 'boolean')
|
|
29
|
+
return;
|
|
30
|
+
if (typeof current === 'number') {
|
|
31
|
+
if (!Number.isFinite(current))
|
|
32
|
+
throw new TypeError(`${label} contains a non-JSON number.`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (typeof current !== 'object')
|
|
36
|
+
throw new TypeError(`${label} contains a non-JSON value.`);
|
|
37
|
+
if (ancestors.has(current))
|
|
38
|
+
throw new TypeError(`${label} contains a cyclic object reference.`);
|
|
39
|
+
ancestors.add(current);
|
|
40
|
+
if (Array.isArray(current)) {
|
|
41
|
+
if (current.length > maximumContainerItems) {
|
|
42
|
+
throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
|
|
43
|
+
}
|
|
44
|
+
for (const item of current)
|
|
45
|
+
visit(item, depth + 1);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const entries = Object.entries(current);
|
|
49
|
+
if (entries.length > maximumContainerItems) {
|
|
50
|
+
throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
|
|
51
|
+
}
|
|
52
|
+
keys += entries.length;
|
|
53
|
+
if (keys > maximumKeys) {
|
|
54
|
+
throw new TypeError(`${label} exceeds the ${maximumKeys}-key limit.`);
|
|
55
|
+
}
|
|
56
|
+
for (const [key, item] of entries) {
|
|
57
|
+
if (!isSafeJsonObjectKey(key))
|
|
58
|
+
throw new TypeError(`${label} contains an unsafe object key.`);
|
|
59
|
+
visit(item, depth + 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
ancestors.delete(current);
|
|
63
|
+
};
|
|
64
|
+
visit(value, 0);
|
|
65
|
+
}
|
|
66
|
+
export function scanBoundedJson(raw, limits = {}) {
|
|
67
|
+
const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
|
|
68
|
+
const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
|
|
69
|
+
const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
|
|
70
|
+
const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
|
|
71
|
+
let index = 0;
|
|
72
|
+
let keys = 0;
|
|
73
|
+
let values = 0;
|
|
74
|
+
const whitespace = () => {
|
|
75
|
+
while (index < raw.length && /[\t\n\r ]/u.test(raw[index]))
|
|
76
|
+
index += 1;
|
|
77
|
+
};
|
|
78
|
+
const stringToken = () => {
|
|
79
|
+
const start = index;
|
|
80
|
+
index += 1;
|
|
81
|
+
while (index < raw.length) {
|
|
82
|
+
const character = raw[index];
|
|
83
|
+
if (character === '"') {
|
|
84
|
+
index += 1;
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(raw.slice(start, index));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
syntaxError();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (character === '\\') {
|
|
93
|
+
index += 1;
|
|
94
|
+
if (index >= raw.length)
|
|
95
|
+
syntaxError();
|
|
96
|
+
if (raw[index] === 'u') {
|
|
97
|
+
if (!/^[a-fA-F0-9]{4}$/u.test(raw.slice(index + 1, index + 5)))
|
|
98
|
+
syntaxError();
|
|
99
|
+
index += 5;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
if (!/["\\/bfnrt]/u.test(raw[index]))
|
|
103
|
+
syntaxError();
|
|
104
|
+
index += 1;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (character.charCodeAt(0) <= 0x1f)
|
|
109
|
+
syntaxError();
|
|
110
|
+
index += 1;
|
|
111
|
+
}
|
|
112
|
+
return syntaxError();
|
|
113
|
+
};
|
|
114
|
+
const literal = (token) => {
|
|
115
|
+
if (raw.slice(index, index + token.length) !== token)
|
|
116
|
+
syntaxError();
|
|
117
|
+
index += token.length;
|
|
118
|
+
};
|
|
119
|
+
const number = () => {
|
|
120
|
+
const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u.exec(raw.slice(index));
|
|
121
|
+
if (!match)
|
|
122
|
+
syntaxError();
|
|
123
|
+
if (!Number.isFinite(Number(match[0]))) {
|
|
124
|
+
throw new TypeError('Artefact JSON contains a non-finite number.');
|
|
125
|
+
}
|
|
126
|
+
index += match[0].length;
|
|
127
|
+
};
|
|
128
|
+
const value = (depth) => {
|
|
129
|
+
values += 1;
|
|
130
|
+
if (values > maximumValues) {
|
|
131
|
+
throw new TypeError(`Artefact JSON exceeds the ${maximumValues}-value limit.`);
|
|
132
|
+
}
|
|
133
|
+
if (depth > maximumDepth) {
|
|
134
|
+
throw new TypeError(`Artefact JSON exceeds the ${maximumDepth}-level nesting limit.`);
|
|
135
|
+
}
|
|
136
|
+
whitespace();
|
|
137
|
+
const character = raw[index];
|
|
138
|
+
if (character === '"') {
|
|
139
|
+
stringToken();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (character === '{') {
|
|
143
|
+
index += 1;
|
|
144
|
+
whitespace();
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
if (raw[index] === '}') {
|
|
147
|
+
index += 1;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
while (index < raw.length) {
|
|
151
|
+
if (raw[index] !== '"')
|
|
152
|
+
syntaxError();
|
|
153
|
+
const key = stringToken();
|
|
154
|
+
if (seen.size >= maximumContainerItems) {
|
|
155
|
+
throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
|
|
156
|
+
}
|
|
157
|
+
keys += 1;
|
|
158
|
+
if (keys > maximumKeys) {
|
|
159
|
+
throw new TypeError(`Artefact JSON exceeds the ${maximumKeys}-key limit.`);
|
|
160
|
+
}
|
|
161
|
+
if (seen.has(key))
|
|
162
|
+
throw new TypeError('Artefact JSON contains a duplicate object key.');
|
|
163
|
+
if (!isSafeJsonObjectKey(key))
|
|
164
|
+
throw new TypeError('Artefact JSON contains an unsafe object key.');
|
|
165
|
+
seen.add(key);
|
|
166
|
+
whitespace();
|
|
167
|
+
if (raw[index] !== ':')
|
|
168
|
+
syntaxError();
|
|
169
|
+
index += 1;
|
|
170
|
+
value(depth + 1);
|
|
171
|
+
whitespace();
|
|
172
|
+
if (raw[index] === '}') {
|
|
173
|
+
index += 1;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (raw[index] !== ',')
|
|
177
|
+
syntaxError();
|
|
178
|
+
index += 1;
|
|
179
|
+
whitespace();
|
|
180
|
+
}
|
|
181
|
+
syntaxError();
|
|
182
|
+
}
|
|
183
|
+
if (character === '[') {
|
|
184
|
+
index += 1;
|
|
185
|
+
whitespace();
|
|
186
|
+
if (raw[index] === ']') {
|
|
187
|
+
index += 1;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
let items = 0;
|
|
191
|
+
while (index < raw.length) {
|
|
192
|
+
items += 1;
|
|
193
|
+
if (items > maximumContainerItems) {
|
|
194
|
+
throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
|
|
195
|
+
}
|
|
196
|
+
value(depth + 1);
|
|
197
|
+
whitespace();
|
|
198
|
+
if (raw[index] === ']') {
|
|
199
|
+
index += 1;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (raw[index] !== ',')
|
|
203
|
+
syntaxError();
|
|
204
|
+
index += 1;
|
|
205
|
+
whitespace();
|
|
206
|
+
}
|
|
207
|
+
syntaxError();
|
|
208
|
+
}
|
|
209
|
+
if (character === 't') {
|
|
210
|
+
literal('true');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (character === 'f') {
|
|
214
|
+
literal('false');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (character === 'n') {
|
|
218
|
+
literal('null');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
number();
|
|
222
|
+
};
|
|
223
|
+
whitespace();
|
|
224
|
+
value(0);
|
|
225
|
+
whitespace();
|
|
226
|
+
if (index !== raw.length)
|
|
227
|
+
syntaxError();
|
|
228
|
+
}
|
|
229
|
+
export function parseBoundedJsonObject(raw, options) {
|
|
230
|
+
const label = options.label ?? 'Artefact input';
|
|
231
|
+
if (typeof raw !== 'string')
|
|
232
|
+
throw new TypeError(`${label} must be UTF-8 JSON text.`);
|
|
233
|
+
const bytes = new TextEncoder().encode(raw).byteLength;
|
|
234
|
+
if (bytes < 1 || bytes > options.maximumBytes) {
|
|
235
|
+
throw new TypeError(`${label} must be between 1 byte and ${options.maximumBytes} bytes.`);
|
|
236
|
+
}
|
|
237
|
+
scanBoundedJson(raw);
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = JSON.parse(raw);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
syntaxError();
|
|
244
|
+
}
|
|
245
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
246
|
+
throw new TypeError(`${label} must contain one JSON object.`);
|
|
247
|
+
}
|
|
248
|
+
return parsed;
|
|
249
|
+
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// retained; malformed extensions fail soft and leave an explicit partial flag.
|
|
4
4
|
import 'reflect-metadata';
|
|
5
5
|
import { CertificatePolicyExtension, CRLDistributionPointsExtension, X509Certificate, } from '@peculiar/x509';
|
|
6
|
-
|
|
6
|
+
import { MAX_LOOKUP_TLS_CERTIFICATE_POLICIES } from "./lookup-network-evidence-bounds.mjs";
|
|
7
|
+
export const MAX_CERTIFICATE_POLICIES = MAX_LOOKUP_TLS_CERTIFICATE_POLICIES;
|
|
7
8
|
export const MAX_CRL_DISTRIBUTION_POINTS = 32;
|
|
8
9
|
function emptyProfile(partial = false) {
|
|
9
10
|
return Object.freeze({
|