@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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/NOTICE +6 -0
- package/README.md +20 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +454 -1748
- package/cli/artifact-validation/case-response.mjs +66 -35
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +112 -72
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +66 -111
- package/cli/command-argument-grammar.mjs +215 -0
- package/cli/command-reference.mjs +342 -285
- package/cli/completion.mjs +303 -80
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +59 -18
- package/cli/discovery-scan-command-runner.mjs +21 -0
- package/cli/discovery-scan.mjs +13 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +2 -0
- package/cli/input.mjs +116 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +18 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-header-review.mjs +467 -0
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +70 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +62 -7
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +77 -24
- package/docs/cli.md +300 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +51 -8
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +73 -26
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +49 -16
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +221 -117
- package/lib/domain-control-manifest.mjs +129 -69
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +201 -8
- package/lib/domain-posture.mjs +147 -29
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -543
- package/lib/evidence-report.mjs +5 -3
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +111 -108
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +8 -10
- package/lib/lookup-source-progress.mjs +40 -14
- package/lib/lookup.mjs +48 -27
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +85 -34
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +18 -6
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +42 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +178 -151
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +153 -45
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +127 -44
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +12 -22
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +95 -31
- package/packages/cases/case-record-projection.mjs +221 -0
- package/packages/cases/case-report.mjs +70 -54
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +64 -1115
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +74 -572
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +31 -2
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
- package/packages/comparison/page-similarity.mjs +5 -2
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +366 -83
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +94 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +15 -11
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +183 -211
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +201 -33
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +92 -51
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +193 -165
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +276 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +23 -11
- package/packages/workspace/bulk-session-model.mjs +100 -24
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -23
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +164 -26
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +87 -21
- package/third-party-notices.txt +29 -2
- package/cli/evidence-command-arguments.mjs +0 -156
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
// bounds, import merging, and exact serialized-byte accounting.
|
|
4
4
|
import { normalizeDomain } from "../cases/case-model.mjs";
|
|
5
5
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
|
+
import { canonicalDomainControlRecords, normalizeDeclaredDomainControlRecordModes, normalizeDomainControlRecordModes } from "../evidence/domain-control-runtime.mjs";
|
|
7
|
+
import { DOMAIN_CONTROL_RECORD_LIST_FIELDS } from "../contracts/domain-control-manifest.mjs";
|
|
8
|
+
import { latestObservationCohort } from "../evidence/latest-observations.mjs";
|
|
9
|
+
import { MAX_IDENTITY_DIGEST_BYTES, sha256IdentityHex } from "../evidence/record-identity.mjs";
|
|
10
|
+
import { DOMAIN_POSTURE_COMPARISON_VERSION, MAX_POSTURE_CHECKS, MAX_POSTURE_CHECK_RECORDS, MAX_POSTURE_RECORD_LENGTH, normalizeDomainPostureSourceContext, normalizeDomainPostureProfileContext } from "../evidence/domain-posture-context.mjs";
|
|
6
11
|
import { normalizeOpaqueReferenceId } from "../cases/opaque-reference-id.mjs";
|
|
7
12
|
import { normalizePageBaseline } from "./page-baseline.mjs";
|
|
8
13
|
import { isInformativePerceptualHash as isInformativeFaviconHash } from "../../lib/perceptual-hash-comparison.mjs";
|
|
9
14
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
10
|
-
import { BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, MAX_DESIRED_POSTURE_BASELINES, MAX_DESIRED_POSTURE_CHANGE_WINDOWS, MAX_DESIRED_POSTURE_OBSERVATIONS, MAX_DESIRED_POSTURE_RECORDS, MAX_DESIRED_POSTURE_SUPPRESSIONS, MAX_DKIM_SELECTOR_LENGTH, MAX_DKIM_SELECTORS, MAX_PROFILES, MAX_PROFILE_DOMAIN_LENGTH, MAX_PROFILE_NAME_LENGTH, MAX_PROFILE_STORE_BYTES, MAX_PROFILE_TEXT_LENGTH, MAX_PROFILE_TLD_LENGTH, MAX_PROFILE_VALUE_INPUTS, MAX_PROFILE_VALUES, MAX_PROTECTION_ATTESTATIONS, SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
11
|
-
export { BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, MAX_DESIRED_POSTURE_BASELINES, MAX_DESIRED_POSTURE_CHANGE_WINDOWS, MAX_DESIRED_POSTURE_OBSERVATIONS, MAX_DESIRED_POSTURE_RECORDS, MAX_DESIRED_POSTURE_SUPPRESSIONS, MAX_DKIM_SELECTOR_LENGTH, MAX_DKIM_SELECTORS, MAX_PROFILES, MAX_PROFILE_DOMAIN_LENGTH, MAX_PROFILE_NAME_LENGTH, MAX_PROFILE_STORE_BYTES, MAX_PROFILE_TEXT_LENGTH, MAX_PROFILE_TLD_LENGTH, MAX_PROFILE_VALUE_INPUTS, MAX_PROFILE_VALUES, MAX_PROTECTION_ATTESTATIONS, SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
15
|
+
import { BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, MAX_DESIRED_POSTURE_BASELINES, MAX_DESIRED_POSTURE_CHANGE_WINDOWS, MAX_DESIRED_POSTURE_OBSERVATIONS, MAX_DESIRED_POSTURE_RECORDS, MAX_DESIRED_POSTURE_SUPPRESSIONS, MAX_DKIM_SELECTOR_LENGTH, MAX_DKIM_SELECTORS, MAX_OFFICIAL_CHANNELS, MAX_PROFILES, MAX_PROFILE_DOMAIN_LENGTH, MAX_PROFILE_NAME_LENGTH, MAX_PROFILE_STORE_BYTES, MAX_PROFILE_TEXT_LENGTH, MAX_PROFILE_TLD_LENGTH, MAX_PROFILE_URL_LENGTH, MAX_PROFILE_VALUE_INPUTS, MAX_PROFILE_VALUES, MAX_PROTECTION_ATTESTATIONS, MAX_RIGHTS_REFERENCES, SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
16
|
+
export { BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, MAX_DESIRED_POSTURE_BASELINES, MAX_DESIRED_POSTURE_CHANGE_WINDOWS, MAX_DESIRED_POSTURE_OBSERVATIONS, MAX_DESIRED_POSTURE_RECORDS, MAX_DESIRED_POSTURE_SUPPRESSIONS, MAX_DKIM_SELECTOR_LENGTH, MAX_DKIM_SELECTORS, MAX_OFFICIAL_CHANNELS, MAX_PROFILES, MAX_PROFILE_DOMAIN_LENGTH, MAX_PROFILE_NAME_LENGTH, MAX_PROFILE_STORE_BYTES, MAX_PROFILE_TEXT_LENGTH, MAX_PROFILE_TLD_LENGTH, MAX_PROFILE_URL_LENGTH, MAX_PROFILE_VALUE_INPUTS, MAX_PROFILE_VALUES, MAX_PROTECTION_ATTESTATIONS, MAX_RIGHTS_REFERENCES, SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
12
17
|
const SHA256_RE = /^[a-f0-9]{64}$/i;
|
|
13
18
|
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
14
19
|
const DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -42,6 +47,17 @@ export const DESIRED_POSTURE_SUPPRESSION_FIELDS = Object.freeze([
|
|
|
42
47
|
]);
|
|
43
48
|
const DESIRED_POSTURE_FIELDS = new Set(DESIRED_POSTURE_SUPPRESSION_FIELDS);
|
|
44
49
|
const POSTURE_CHECK_STATUSES = new Set(['danger', 'info', 'pass', 'warning']);
|
|
50
|
+
export const OFFICIAL_CHANNEL_PLATFORMS = [
|
|
51
|
+
'facebook',
|
|
52
|
+
'instagram',
|
|
53
|
+
'linkedin',
|
|
54
|
+
'telegram',
|
|
55
|
+
'tiktok',
|
|
56
|
+
'x',
|
|
57
|
+
'youtube',
|
|
58
|
+
'other',
|
|
59
|
+
];
|
|
60
|
+
export const RIGHTS_REFERENCE_KINDS = ['trademark', 'copyright', 'design', 'other'];
|
|
45
61
|
function record(value) {
|
|
46
62
|
return ordinaryWorkspaceRecord(value, 'Brand Profile input') ?? {};
|
|
47
63
|
}
|
|
@@ -107,6 +123,32 @@ export function normalizeProfileDomains(value) {
|
|
|
107
123
|
export function normalizeProfileTextValues(value) {
|
|
108
124
|
return normalizeList(value, (item) => boundedText(item));
|
|
109
125
|
}
|
|
126
|
+
export const MAX_ALLOWLIST_DRAFT_CHARACTERS = (MAX_PROFILE_DOMAIN_LENGTH + 2) * MAX_PROFILE_VALUE_INPUTS;
|
|
127
|
+
/** Admit the complete submitted list or leave the draft unchanged. */
|
|
128
|
+
export function addBrandAllowlistValues(profile, kind, current, raw) {
|
|
129
|
+
if (raw.length > MAX_ALLOWLIST_DRAFT_CHARACTERS)
|
|
130
|
+
throw new RangeError('The submitted allowlist text is too large.');
|
|
131
|
+
const inputs = raw.split(/[\n,]+/u).map((item) => item.trim()).filter(Boolean);
|
|
132
|
+
if (inputs.length > MAX_PROFILE_VALUE_INPUTS)
|
|
133
|
+
throw new RangeError(`Review at most ${MAX_PROFILE_VALUE_INPUTS} entries at a time.`);
|
|
134
|
+
const next = new Map(current.map((value) => [value.toLowerCase(), value]));
|
|
135
|
+
const trusted = new Set([...profile.officialDomains, ...profile.approvedPartnerDomains]);
|
|
136
|
+
for (const [index, input] of inputs.entries()) {
|
|
137
|
+
const value = kind === 'domains' ? normalizeProfileDomains([input])[0] : normalizeProfileTextValues([input])[0];
|
|
138
|
+
if (!value || (kind === 'registrars' && input.replace(/\s+/gu, ' ').length > MAX_PROFILE_TEXT_LENGTH)) {
|
|
139
|
+
throw new TypeError(`Allowlist entry ${index + 1} is invalid or too long. No entries were added.`);
|
|
140
|
+
}
|
|
141
|
+
if (kind === 'domains' && trusted.has(value))
|
|
142
|
+
continue;
|
|
143
|
+
if (!next.has(value.toLowerCase()))
|
|
144
|
+
next.set(value.toLowerCase(), value);
|
|
145
|
+
}
|
|
146
|
+
if (next.size > MAX_PROFILE_VALUES)
|
|
147
|
+
throw new RangeError(`This list would contain ${next.size} entries; the current profile supports ${MAX_PROFILE_VALUES}. No entries were added.`);
|
|
148
|
+
if (next.size === current.length)
|
|
149
|
+
throw new TypeError('No new entries remain after excluding existing and trusted values.');
|
|
150
|
+
return [...next.values()];
|
|
151
|
+
}
|
|
110
152
|
export function normalizeProfileTlds(value) {
|
|
111
153
|
return normalizeList(value, normalizeTld);
|
|
112
154
|
}
|
|
@@ -148,6 +190,30 @@ export function normalizeProtectionAttestations(value) {
|
|
|
148
190
|
}
|
|
149
191
|
return output;
|
|
150
192
|
}
|
|
193
|
+
/** Only submitted, validated reviews acquire a new analyst-review clock. */
|
|
194
|
+
export function reviewProtectionAttestations(existing, reviews, nowIso) {
|
|
195
|
+
const assertedAt = timestamp(nowIso, null);
|
|
196
|
+
if (!assertedAt)
|
|
197
|
+
throw new TypeError('The account-control review time is invalid.');
|
|
198
|
+
if (reviews.length > MAX_PROTECTION_ATTESTATIONS)
|
|
199
|
+
throw new RangeError('Too many account-control reviews were submitted.');
|
|
200
|
+
const next = new Map(existing.map((item) => [item.control, { ...item }]));
|
|
201
|
+
const seen = new Set();
|
|
202
|
+
for (const review of reviews) {
|
|
203
|
+
if (!PROTECTION_ATTESTATION_CONTROL_SET.has(review.control) || seen.has(review.control)
|
|
204
|
+
|| !PROTECTION_ATTESTATION_STATES.has(review.state))
|
|
205
|
+
throw new TypeError('An account-control review is invalid or repeated.');
|
|
206
|
+
const expiresAt = review.expiresAt === null ? null : timestamp(review.expiresAt, null);
|
|
207
|
+
if (review.expiresAt !== null && !expiresAt)
|
|
208
|
+
throw new TypeError('Enter a valid account-control expiry date.');
|
|
209
|
+
if (typeof review.note !== 'string' || CONTROL_RE.test(review.note)
|
|
210
|
+
|| review.note.replace(/\s+/gu, ' ').trim().length > MAX_PROFILE_TEXT_LENGTH)
|
|
211
|
+
throw new TypeError(`Account-control notes must contain at most ${MAX_PROFILE_TEXT_LENGTH} characters and no control characters.`);
|
|
212
|
+
seen.add(review.control);
|
|
213
|
+
next.set(review.control, { control: review.control, state: review.state, assertedAt, expiresAt, note: boundedText(review.note) });
|
|
214
|
+
}
|
|
215
|
+
return [...next.values()];
|
|
216
|
+
}
|
|
151
217
|
function normalizeDesiredPostureRecords(value, normalizer) {
|
|
152
218
|
if (!Array.isArray(value))
|
|
153
219
|
return [];
|
|
@@ -165,9 +231,10 @@ function normalizeDesiredPostureRecords(value, normalizer) {
|
|
|
165
231
|
}
|
|
166
232
|
function normalizeDesiredPostureObservation(value) {
|
|
167
233
|
const candidate = record(value);
|
|
168
|
-
const observedAt =
|
|
169
|
-
if (!
|
|
234
|
+
const observedAt = normalizeExplicitIsoTimestamp(candidate.observedAt) ?? '';
|
|
235
|
+
if (!Array.isArray(candidate.checks))
|
|
170
236
|
return null;
|
|
237
|
+
const context = normalizeDomainPostureProfileContext(candidate.context);
|
|
171
238
|
const checks = [];
|
|
172
239
|
const seen = new Set();
|
|
173
240
|
for (const item of candidate.checks.slice(0, 64)) {
|
|
@@ -179,28 +246,88 @@ function normalizeDesiredPostureObservation(value) {
|
|
|
179
246
|
|| !POSTURE_CHECK_STATUSES.has(check.status))
|
|
180
247
|
continue;
|
|
181
248
|
seen.add(id);
|
|
249
|
+
if (check.sourceContext !== undefined && !Array.isArray(check.records))
|
|
250
|
+
throw new TypeError('Source-qualified posture records must be an array.');
|
|
251
|
+
const inputRecords = Array.isArray(check.records) ? check.records : [];
|
|
252
|
+
const records = [];
|
|
253
|
+
for (const item of inputRecords.slice(0, MAX_POSTURE_CHECK_RECORDS)) {
|
|
254
|
+
if (typeof item === 'string' && item.length <= MAX_POSTURE_RECORD_LENGTH && !CONTROL_RE.test(item))
|
|
255
|
+
records.push(item);
|
|
256
|
+
}
|
|
257
|
+
const omitted = Math.max(0, inputRecords.length - records.length);
|
|
258
|
+
const source = normalizeDomainPostureSourceContext(check.sourceContext);
|
|
259
|
+
const sourceContext = source && omitted ? {
|
|
260
|
+
...source,
|
|
261
|
+
state: source.state === 'unavailable' ? 'unavailable' : 'partial',
|
|
262
|
+
omittedRecords: source.omittedRecords === null ? null : Math.min(Number.MAX_SAFE_INTEGER, source.omittedRecords + omitted),
|
|
263
|
+
} : source;
|
|
182
264
|
checks.push({
|
|
183
265
|
id,
|
|
184
266
|
status: check.status,
|
|
185
|
-
records
|
|
267
|
+
records,
|
|
268
|
+
...(sourceContext ? { sourceContext } : {}),
|
|
186
269
|
});
|
|
187
|
-
if (checks.length >=
|
|
270
|
+
if (checks.length >= MAX_POSTURE_CHECKS)
|
|
188
271
|
break;
|
|
189
272
|
}
|
|
190
|
-
|
|
273
|
+
const omittedChecks = Math.max(0, candidate.checks.length - checks.length)
|
|
274
|
+
+ (Number.isSafeInteger(candidate.omittedChecks) && Number(candidate.omittedChecks) > 0 ? Number(candidate.omittedChecks) : 0);
|
|
275
|
+
return checks.length ? { observedAt, ...(context ? { context } : {}), checks, ...(omittedChecks ? { omittedChecks: Math.min(Number.MAX_SAFE_INTEGER, omittedChecks) } : {}) } : null;
|
|
191
276
|
}
|
|
192
|
-
function normalizeDesiredPostureObservationHistory(value, previous) {
|
|
277
|
+
export function normalizeDesiredPostureObservationHistory(value, previous) {
|
|
193
278
|
const candidates = Array.isArray(value) ? value : previous ? [previous] : [];
|
|
194
279
|
const byTime = new Map();
|
|
195
280
|
for (const item of candidates.slice(0, MAX_DESIRED_POSTURE_OBSERVATIONS * 4)) {
|
|
196
281
|
const normalized = normalizeDesiredPostureObservation(item);
|
|
197
|
-
if (normalized)
|
|
198
|
-
|
|
282
|
+
if (!normalized)
|
|
283
|
+
continue;
|
|
284
|
+
if (new TextEncoder().encode(JSON.stringify(normalized)).byteLength > MAX_IDENTITY_DIGEST_BYTES) {
|
|
285
|
+
throw new RangeError('Record identity exceeds its byte limit.');
|
|
286
|
+
}
|
|
287
|
+
const cohort = byTime.get(normalized.observedAt) ?? [];
|
|
288
|
+
cohort.push(normalized);
|
|
289
|
+
byTime.set(normalized.observedAt, cohort);
|
|
199
290
|
}
|
|
200
|
-
return [...byTime.
|
|
201
|
-
.sort((left, right) => Date.parse(left
|
|
291
|
+
return [...byTime.entries()]
|
|
292
|
+
.sort(([left], [right]) => !left ? 1 : !right ? -1 : Date.parse(left) - Date.parse(right))
|
|
293
|
+
.flatMap(([, cohort]) => {
|
|
294
|
+
// Different capture times already distinguish observations. Digest-based
|
|
295
|
+
// deduplication and ordering remain necessary only within a tied cohort.
|
|
296
|
+
if (cohort.length === 1)
|
|
297
|
+
return cohort;
|
|
298
|
+
const byIdentity = new Map(cohort.map((observation) => [desiredPostureObservationIdentity(observation), observation]));
|
|
299
|
+
return [...byIdentity.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
300
|
+
.map(([, observation]) => observation);
|
|
301
|
+
})
|
|
202
302
|
.slice(-MAX_DESIRED_POSTURE_OBSERVATIONS);
|
|
203
303
|
}
|
|
304
|
+
export function desiredPostureObservationIdentity(observation) {
|
|
305
|
+
return sha256IdentityHex(new TextEncoder().encode(JSON.stringify(observation)));
|
|
306
|
+
}
|
|
307
|
+
export function desiredPostureObservations(baseline) {
|
|
308
|
+
return baseline.observationHistory?.length ? baseline.observationHistory : baseline.previousObservation ? [baseline.previousObservation] : [];
|
|
309
|
+
}
|
|
310
|
+
export function currentDesiredPostureObservation(baseline) {
|
|
311
|
+
const history = desiredPostureObservations(baseline);
|
|
312
|
+
const cohort = latestObservationCohort(history, (item) => item.observedAt);
|
|
313
|
+
return {
|
|
314
|
+
observation: !cohort.undated.length && cohort.latest.length === 1 ? cohort.latest[0] : null,
|
|
315
|
+
candidates: [...cohort.latest, ...cohort.undated],
|
|
316
|
+
limitation: cohort.undated.length ? 'Retained observations include an unknown capture time; no unique latest review is selected.'
|
|
317
|
+
: cohort.latest.length > 1 ? 'Distinct observations share the latest capture time; no unique latest review is selected.' : null,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export function brandPostureCollectionFingerprint(profile) {
|
|
321
|
+
return sha256IdentityHex(new TextEncoder().encode(JSON.stringify([
|
|
322
|
+
profile.id, [...profile.officialDomains].sort(), profile.mailProtectionProfile,
|
|
323
|
+
[...profile.dkimSelectors].sort(), [...profile.retiredDkimSelectors].sort(),
|
|
324
|
+
])));
|
|
325
|
+
}
|
|
326
|
+
export function brandPostureObservationContext(profile, domain) {
|
|
327
|
+
if (!profile.officialDomains.includes(domain))
|
|
328
|
+
throw new TypeError('Posture observation target is not an official domain of this profile.');
|
|
329
|
+
return { version: DOMAIN_POSTURE_COMPARISON_VERSION, domain, profileId: profile.id, profileFingerprint: brandPostureCollectionFingerprint(profile) };
|
|
330
|
+
}
|
|
204
331
|
function deterministicChangeWindowId(seed) {
|
|
205
332
|
const states = [0x811c9dc5, 0x9e3779b9, 0x85ebca6b, 0xc2b2ae35];
|
|
206
333
|
const factors = [0x01000193, 0x85ebca6b, 0xc2b2ae35, 0x27d4eb2f];
|
|
@@ -259,6 +386,23 @@ function normalizeTlsSanPatterns(value) {
|
|
|
259
386
|
}
|
|
260
387
|
return [...output].sort();
|
|
261
388
|
}
|
|
389
|
+
function normalizeBaselineRecords(candidate) {
|
|
390
|
+
const records = {
|
|
391
|
+
nameservers: normalizeDesiredPostureRecords(candidate.nameservers, (entry) => normalizeDomain(entry)),
|
|
392
|
+
ds: normalizeDesiredPostureRecords(candidate.ds),
|
|
393
|
+
mx: normalizeDesiredPostureRecords(candidate.mx),
|
|
394
|
+
caa: normalizeDesiredPostureRecords(candidate.caa),
|
|
395
|
+
};
|
|
396
|
+
if (!Object.hasOwn(candidate, 'recordModes'))
|
|
397
|
+
return records;
|
|
398
|
+
const recordModes = normalizeDeclaredDomainControlRecordModes(candidate.recordModes);
|
|
399
|
+
for (const field of DOMAIN_CONTROL_RECORD_LIST_FIELDS) {
|
|
400
|
+
if (Object.hasOwn(recordModes, field))
|
|
401
|
+
records[field] = canonicalDomainControlRecords(candidate[field], field);
|
|
402
|
+
}
|
|
403
|
+
normalizeDomainControlRecordModes(recordModes, records);
|
|
404
|
+
return { ...records, recordModes };
|
|
405
|
+
}
|
|
262
406
|
export function normalizeDesiredPostureBaselines(value, officialDomains, fallbackNow = new Date().toISOString(), identityScope = '') {
|
|
263
407
|
if (!Array.isArray(value))
|
|
264
408
|
return [];
|
|
@@ -295,10 +439,7 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
|
|
|
295
439
|
output.push({
|
|
296
440
|
version: 1,
|
|
297
441
|
domain,
|
|
298
|
-
|
|
299
|
-
ds: normalizeDesiredPostureRecords(candidate.ds),
|
|
300
|
-
mx: normalizeDesiredPostureRecords(candidate.mx),
|
|
301
|
-
caa: normalizeDesiredPostureRecords(candidate.caa),
|
|
442
|
+
...normalizeBaselineRecords(candidate),
|
|
302
443
|
tlsIssuer: boundedText(candidate.tlsIssuer, MAX_PROFILE_TEXT_LENGTH),
|
|
303
444
|
tlsSanPatterns: normalizeTlsSanPatterns(candidate.tlsSanPatterns),
|
|
304
445
|
tlsSpkiSha256: typeof candidate.tlsSpkiSha256 === 'string' && SHA256_RE.test(candidate.tlsSpkiSha256)
|
|
@@ -318,7 +459,7 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
|
|
|
318
459
|
approvedChangeWindows: normalizeDesiredPostureChangeWindows(candidate.approvedChangeWindows, `${identityScope}\u001f${domain}`),
|
|
319
460
|
suppressions,
|
|
320
461
|
note: boundedText(candidate.note, MAX_PROFILE_TEXT_LENGTH),
|
|
321
|
-
previousObservation:
|
|
462
|
+
previousObservation: currentDesiredPostureObservation({ observationHistory, previousObservation }).observation,
|
|
322
463
|
observationHistory,
|
|
323
464
|
updatedAt: timestamp(candidate.updatedAt, fallback),
|
|
324
465
|
});
|
|
@@ -333,6 +474,82 @@ function normalizeFaviconHash(value) {
|
|
|
333
474
|
function normalizeFaviconPHash(value) {
|
|
334
475
|
return typeof value === 'string' && isInformativeFaviconHash(value) ? value.toLowerCase() : '';
|
|
335
476
|
}
|
|
477
|
+
function normalizePublicUrl(value) {
|
|
478
|
+
if (typeof value !== 'string' || CONTROL_RE.test(value) || value.length > MAX_PROFILE_URL_LENGTH * 2)
|
|
479
|
+
return '';
|
|
480
|
+
try {
|
|
481
|
+
const parsed = new URL(value.trim());
|
|
482
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
483
|
+
return '';
|
|
484
|
+
const normalized = parsed.toString();
|
|
485
|
+
return normalized.length <= MAX_PROFILE_URL_LENGTH ? normalized : '';
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return '';
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function normalizeOfficialChannels(value) {
|
|
492
|
+
if (!Array.isArray(value))
|
|
493
|
+
return [];
|
|
494
|
+
const output = new Map();
|
|
495
|
+
for (const candidate of value.slice(0, MAX_OFFICIAL_CHANNELS * 4)) {
|
|
496
|
+
const item = record(candidate);
|
|
497
|
+
const platform = typeof item.platform === 'string'
|
|
498
|
+
&& OFFICIAL_CHANNEL_PLATFORMS.includes(item.platform)
|
|
499
|
+
? item.platform
|
|
500
|
+
: null;
|
|
501
|
+
const url = normalizePublicUrl(item.url);
|
|
502
|
+
if (!platform || !url)
|
|
503
|
+
continue;
|
|
504
|
+
const key = url.toLowerCase();
|
|
505
|
+
if (output.has(key))
|
|
506
|
+
continue;
|
|
507
|
+
output.set(key, {
|
|
508
|
+
platform,
|
|
509
|
+
url,
|
|
510
|
+
handle: boundedText(item.handle),
|
|
511
|
+
role: boundedText(item.role),
|
|
512
|
+
reviewedAt: timestamp(item.reviewedAt, null),
|
|
513
|
+
});
|
|
514
|
+
if (output.size >= MAX_OFFICIAL_CHANNELS)
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
return [...output.values()];
|
|
518
|
+
}
|
|
519
|
+
function normalizeRightsReferences(value) {
|
|
520
|
+
if (!Array.isArray(value))
|
|
521
|
+
return [];
|
|
522
|
+
const output = new Map();
|
|
523
|
+
for (const candidate of value.slice(0, MAX_RIGHTS_REFERENCES * 4)) {
|
|
524
|
+
const item = record(candidate);
|
|
525
|
+
const kind = typeof item.kind === 'string' && RIGHTS_REFERENCE_KINDS.includes(item.kind)
|
|
526
|
+
? item.kind
|
|
527
|
+
: null;
|
|
528
|
+
const owner = boundedText(item.owner);
|
|
529
|
+
const identifier = boundedText(item.identifier);
|
|
530
|
+
if (!kind || (!owner && !identifier))
|
|
531
|
+
continue;
|
|
532
|
+
const jurisdiction = boundedText(item.jurisdiction);
|
|
533
|
+
const sourceUrl = item.sourceUrl == null || item.sourceUrl === '' ? '' : normalizePublicUrl(item.sourceUrl);
|
|
534
|
+
if (item.sourceUrl && !sourceUrl)
|
|
535
|
+
continue;
|
|
536
|
+
const key = `${kind}\u0000${owner.toLowerCase()}\u0000${identifier.toLowerCase()}\u0000${jurisdiction.toLowerCase()}`;
|
|
537
|
+
if (output.has(key))
|
|
538
|
+
continue;
|
|
539
|
+
output.set(key, {
|
|
540
|
+
kind,
|
|
541
|
+
owner,
|
|
542
|
+
identifier,
|
|
543
|
+
jurisdiction,
|
|
544
|
+
sourceUrl,
|
|
545
|
+
reviewedAt: timestamp(item.reviewedAt, null),
|
|
546
|
+
note: boundedText(item.note),
|
|
547
|
+
});
|
|
548
|
+
if (output.size >= MAX_RIGHTS_REFERENCES)
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
return [...output.values()];
|
|
552
|
+
}
|
|
336
553
|
/** Normalize one profile while retaining only known, bounded fields. */
|
|
337
554
|
export function normalizeBrandProfile(raw, options = {}) {
|
|
338
555
|
const value = record(raw);
|
|
@@ -360,6 +577,7 @@ export function normalizeBrandProfile(raw, options = {}) {
|
|
|
360
577
|
id: profileId,
|
|
361
578
|
name,
|
|
362
579
|
officialDomains,
|
|
580
|
+
officialChannels: normalizeOfficialChannels(value.officialChannels),
|
|
363
581
|
productNames: normalizeProfileTextValues(value.productNames),
|
|
364
582
|
tlds: normalizeProfileTlds(value.tlds),
|
|
365
583
|
approvedPartnerDomains: normalizeProfileDomains(value.approvedPartnerDomains),
|
|
@@ -373,6 +591,7 @@ export function normalizeBrandProfile(raw, options = {}) {
|
|
|
373
591
|
desiredPostureBaselines: normalizeDesiredPostureBaselines(value.desiredPostureBaselines, officialDomains, now, profileId),
|
|
374
592
|
trademarkOwner: boundedText(value.trademarkOwner),
|
|
375
593
|
trademarkRegistration: boundedText(value.trademarkRegistration),
|
|
594
|
+
rightsReferences: normalizeRightsReferences(value.rightsReferences),
|
|
376
595
|
officialFaviconHash: baselineDomainMismatch ? '' : normalizeFaviconHash(value.officialFaviconHash),
|
|
377
596
|
officialFaviconPHash: baselineDomainMismatch ? '' : normalizeFaviconPHash(value.officialFaviconPHash),
|
|
378
597
|
pageBaseline,
|
|
@@ -380,6 +599,33 @@ export function normalizeBrandProfile(raw, options = {}) {
|
|
|
380
599
|
updatedAt: options.touch === true ? now : timestamp(value.updatedAt, createdAt),
|
|
381
600
|
};
|
|
382
601
|
}
|
|
602
|
+
const BRAND_PROFILE_FIELD_PATCH_KEYS = Object.freeze([
|
|
603
|
+
'allowlistedDomains',
|
|
604
|
+
'allowlistedRegistrars',
|
|
605
|
+
'desiredPostureBaselines',
|
|
606
|
+
'protectionAttestations',
|
|
607
|
+
]);
|
|
608
|
+
/**
|
|
609
|
+
* Applies one field-owned edit to the transaction-current profile. Identity,
|
|
610
|
+
* timestamps, and every field outside the explicit patch remain owned by that
|
|
611
|
+
* current record rather than by the caller's potentially stale snapshot.
|
|
612
|
+
*/
|
|
613
|
+
export function applyBrandProfileFieldPatch(existing, patch, options = {}) {
|
|
614
|
+
const merged = { ...existing };
|
|
615
|
+
const supplied = patch;
|
|
616
|
+
for (const key of BRAND_PROFILE_FIELD_PATCH_KEYS) {
|
|
617
|
+
if (Object.hasOwn(supplied, key))
|
|
618
|
+
merged[key] = supplied[key];
|
|
619
|
+
}
|
|
620
|
+
const normalized = normalizeBrandProfile(merged, {
|
|
621
|
+
existing,
|
|
622
|
+
nowIso: options.nowIso,
|
|
623
|
+
touch: true,
|
|
624
|
+
});
|
|
625
|
+
if (!normalized)
|
|
626
|
+
throw new Error('The Brand Profile field update is invalid.');
|
|
627
|
+
return normalized;
|
|
628
|
+
}
|
|
383
629
|
function profileList(raw) {
|
|
384
630
|
if (Array.isArray(raw))
|
|
385
631
|
return raw;
|
|
@@ -394,7 +640,7 @@ export function brandProfileStoreVersion(raw) {
|
|
|
394
640
|
}
|
|
395
641
|
/** Normalize an internal profile collection or current stored envelope. */
|
|
396
642
|
export function normalizeBrandProfileStore(raw) {
|
|
397
|
-
assertWorkspaceInputGraph(raw, 'Brand Profile store');
|
|
643
|
+
assertWorkspaceInputGraph(raw, 'Brand Profile store', { maximumBytes: MAX_PROFILE_STORE_BYTES });
|
|
398
644
|
assertWorkspaceDeclaredVersion(raw, 'Brand Profile store');
|
|
399
645
|
const sourceVersion = brandProfileStoreVersion(raw);
|
|
400
646
|
if (!Array.isArray(raw) && sourceVersion !== null
|
|
@@ -416,6 +662,10 @@ export function normalizeBrandProfileStore(raw) {
|
|
|
416
662
|
}
|
|
417
663
|
return { version: BRAND_PROFILE_SCHEMA_VERSION, profiles: [...byId.values()] };
|
|
418
664
|
}
|
|
665
|
+
/** New authored/imported records receive an identity only at the mutation boundary. */
|
|
666
|
+
export function createBrandProfileId() {
|
|
667
|
+
return crypto.randomUUID ? crypto.randomUUID() : `bp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
668
|
+
}
|
|
419
669
|
export function serializeBrandProfileStore(profiles) {
|
|
420
670
|
return JSON.stringify(assertBrandProfileStoreBudget(profiles));
|
|
421
671
|
}
|
|
@@ -430,8 +680,8 @@ export function assertBrandProfileStoreBudget(profiles) {
|
|
|
430
680
|
return store;
|
|
431
681
|
}
|
|
432
682
|
export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
433
|
-
|
|
434
|
-
assertWorkspaceInputGraph(importedRaw, 'Imported Brand Profile document');
|
|
683
|
+
const local = normalizeBrandProfileStore(localRaw).profiles;
|
|
684
|
+
assertWorkspaceInputGraph(importedRaw, 'Imported Brand Profile document', { maximumBytes: MAX_PROFILE_STORE_BYTES });
|
|
435
685
|
assertWorkspacePortableVersion(importedRaw, BRAND_PROFILE_SCHEMA_VERSION, 'Imported Brand Profile document');
|
|
436
686
|
const imported = record(importedRaw);
|
|
437
687
|
if (imported.schema !== BRAND_PROFILE_SCHEMA) {
|
|
@@ -463,7 +713,6 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
|
463
713
|
if (id && name)
|
|
464
714
|
retainIdName(id, name);
|
|
465
715
|
}
|
|
466
|
-
const local = normalizeBrandProfileStore(localRaw).profiles;
|
|
467
716
|
for (const profile of local)
|
|
468
717
|
retainIdName(profile.id, profile.name);
|
|
469
718
|
const byName = new Map(local.map((profile) => [profile.name.toLowerCase(), profile]));
|
|
@@ -478,9 +727,14 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
|
478
727
|
if (rawId && rawName)
|
|
479
728
|
retainIdName(rawId, rawName);
|
|
480
729
|
const existing = rawName ? byName.get(rawName.toLowerCase()) : null;
|
|
730
|
+
const incomingUpdatedAt = timestamp(value.updatedAt, null);
|
|
731
|
+
if (existing && (!incomingUpdatedAt || incomingUpdatedAt <= existing.updatedAt)) {
|
|
732
|
+
skipped++;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
481
735
|
const profile = normalizeBrandProfile(item, {
|
|
482
736
|
existing,
|
|
483
|
-
touch:
|
|
737
|
+
touch: false,
|
|
484
738
|
nowIso: options.nowIso,
|
|
485
739
|
makeId: options.makeId,
|
|
486
740
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Optional result columns. Domain identity and actions are always available. */
|
|
2
|
+
export const BULK_RESULT_COLUMNS = Object.freeze([
|
|
3
|
+
{ id: 'registration', label: 'Registration' },
|
|
4
|
+
{ id: 'risk', label: 'Risk' },
|
|
5
|
+
{ id: 'website', label: 'Website' },
|
|
6
|
+
{ id: 'registrar', label: 'Registrar' },
|
|
7
|
+
{ id: 'mutation', label: 'Mutation' },
|
|
8
|
+
{ id: 'review', label: 'Review' },
|
|
9
|
+
{ id: 'case', label: 'Case' },
|
|
10
|
+
]);
|
|
11
|
+
export const DEFAULT_BULK_RESULT_COLUMNS = Object.freeze(BULK_RESULT_COLUMNS.map((column) => column.id));
|
|
12
|
+
export function normalizeBulkResultColumns(value) {
|
|
13
|
+
// Missing choices preserve the full presentation used by public version 1.
|
|
14
|
+
if (!Array.isArray(value))
|
|
15
|
+
return [...DEFAULT_BULK_RESULT_COLUMNS];
|
|
16
|
+
return BULK_RESULT_COLUMNS.filter((column) => value.includes(column.id)).map((column) => column.id);
|
|
17
|
+
}
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { normalizeDomain } from "../cases/case-model.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { CASE_DISPOSITIONS as CASE_DISPOSITION_OPTIONS } from "../cases/case-record-contracts.mjs";
|
|
3
|
+
import { BULK_SORT_KEYS, normalizeBulkPresentationSortKey } from "./bulk-sort.mjs";
|
|
4
|
+
import { normalizeBulkResultColumns } from "./bulk-columns.mjs";
|
|
3
5
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
4
6
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
5
|
-
import { BULK_REVIEW_SCHEMA, BULK_REVIEW_SCHEMA_VERSION, MAX_BULK_REVIEW_NAME_LENGTH, MAX_BULK_REVIEW_PRESETS, MAX_BULK_REVIEW_ROWS, MAX_BULK_REVIEW_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
7
|
+
import { BULK_REVIEW_SCHEMA, BULK_REVIEW_SCHEMA_VERSION, BULK_REVIEW_EXPORT_SUPPORTED_VERSIONS, MAX_BULK_REVIEW_NAME_LENGTH, MAX_BULK_REVIEW_PRESETS, MAX_BULK_REVIEW_ROWS, MAX_BULK_REVIEW_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
6
8
|
export { BULK_REVIEW_SCHEMA, BULK_REVIEW_SCHEMA_VERSION, MAX_BULK_REVIEW_NAME_LENGTH, MAX_BULK_REVIEW_PRESETS, MAX_BULK_REVIEW_ROWS, MAX_BULK_REVIEW_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
7
9
|
export const BULK_REVIEW_STATES = ['unreviewed', 'reviewing', 'reviewed', 'deferred'];
|
|
10
|
+
export const BULK_SOURCE_FILTERS = ['', 'complete', 'limited', 'unrecorded'];
|
|
11
|
+
export const BULK_LIFECYCLE_FILTERS = ['', 'registered', 'for_sale', 'expiring', 'available', 'unknown', 'error'];
|
|
12
|
+
export const BULK_AGE_FILTERS = ['', 'new_30', 'new_365', 'older_365', 'unknown'];
|
|
13
|
+
export const BULK_MAIL_FILTERS = ['', 'mail', 'no_mail', 'authenticated', 'auth_gap', 'unknown'];
|
|
14
|
+
export const BULK_GROUP_OPTIONS = ['', 'mutation', 'tld', 'registrar', 'nameserver'];
|
|
8
15
|
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
9
16
|
const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
10
17
|
const PRIMARY_FILTERS = new Set(['all', 'available', 'registered', 'high_risk', 'trusted', 'profile_unevaluated', 'errors']);
|
|
11
|
-
const SOURCE_FILTERS = new Set(
|
|
12
|
-
const LIFECYCLE_FILTERS = new Set(
|
|
13
|
-
const AGE_FILTERS = new Set(
|
|
14
|
-
const MAIL_FILTERS = new Set(
|
|
15
|
-
const GROUPS = new Set(
|
|
16
|
-
const SORT_KEYS = new Set(
|
|
18
|
+
const SOURCE_FILTERS = new Set(BULK_SOURCE_FILTERS);
|
|
19
|
+
const LIFECYCLE_FILTERS = new Set(BULK_LIFECYCLE_FILTERS);
|
|
20
|
+
const AGE_FILTERS = new Set(BULK_AGE_FILTERS);
|
|
21
|
+
const MAIL_FILTERS = new Set(BULK_MAIL_FILTERS);
|
|
22
|
+
const GROUPS = new Set(BULK_GROUP_OPTIONS);
|
|
23
|
+
const SORT_KEYS = new Set(BULK_SORT_KEYS);
|
|
17
24
|
const REVIEW_STATES = new Set(BULK_REVIEW_STATES);
|
|
18
|
-
const CASE_DISPOSITIONS = new Set(['', 'untracked',
|
|
25
|
+
const CASE_DISPOSITIONS = new Set(['', 'untracked', ...CASE_DISPOSITION_OPTIONS.map((item) => item.value)]);
|
|
19
26
|
const SIGNAL_FILTERS = new Set(['favicon', 'password', 'phishing', 'asset_reuse', 'idn']);
|
|
20
27
|
function record(value) {
|
|
21
28
|
return ordinaryWorkspaceRecord(value, 'Bulk-review input') ?? {};
|
|
@@ -61,6 +68,7 @@ function normalizeView(raw) {
|
|
|
61
68
|
groupBy: setValue(value.groupBy, GROUPS),
|
|
62
69
|
sortKey,
|
|
63
70
|
sortDirection: value.sortDirection === 1 ? 1 : -1,
|
|
71
|
+
columns: normalizeBulkResultColumns(value.columns),
|
|
64
72
|
};
|
|
65
73
|
}
|
|
66
74
|
function normalizePreset(raw, fallback) {
|
|
@@ -107,6 +115,10 @@ function sourceLists(raw) {
|
|
|
107
115
|
export function normalizeBulkReviewStore(raw) {
|
|
108
116
|
assertWorkspaceInputGraph(raw, 'Bulk-review store');
|
|
109
117
|
assertWorkspaceDeclaredVersion(raw, 'Bulk-review store');
|
|
118
|
+
const declaredVersion = record(raw).version;
|
|
119
|
+
if (typeof declaredVersion === 'number' && declaredVersion > BULK_REVIEW_SCHEMA_VERSION) {
|
|
120
|
+
throw new Error(`This Bulk review store uses newer schema ${declaredVersion}. Update the app before changing it.`);
|
|
121
|
+
}
|
|
110
122
|
const fallback = new Date(0).toISOString();
|
|
111
123
|
const source = sourceLists(raw);
|
|
112
124
|
const presets = new Map();
|
|
@@ -148,7 +160,7 @@ export function enforceBulkReviewBudget(raw) {
|
|
|
148
160
|
while (byteLength(JSON.stringify(store)) > MAX_BULK_REVIEW_STORE_BYTES && store.presets.length > 1)
|
|
149
161
|
store.presets.pop();
|
|
150
162
|
if (byteLength(JSON.stringify(store)) > MAX_BULK_REVIEW_STORE_BYTES) {
|
|
151
|
-
throw new Error('Bulk review preferences exceed the
|
|
163
|
+
throw new Error('Bulk review preferences exceed the workspace storage limit.');
|
|
152
164
|
}
|
|
153
165
|
return store;
|
|
154
166
|
}
|
|
@@ -211,7 +223,7 @@ export function mergeBulkReviewStores(localRaw, importedRaw) {
|
|
|
211
223
|
if (importedRecord.schema !== BULK_REVIEW_SCHEMA) {
|
|
212
224
|
throw new Error('This file is not a WHOISleuth Bulk review export.');
|
|
213
225
|
}
|
|
214
|
-
if (
|
|
226
|
+
if (!BULK_REVIEW_EXPORT_SUPPORTED_VERSIONS.some((version) => version === importedRecord.version)) {
|
|
215
227
|
if (typeof importedRecord.version === 'number' && importedRecord.version > BULK_REVIEW_SCHEMA_VERSION) {
|
|
216
228
|
throw new Error(`This Bulk review export uses newer schema ${importedRecord.version}. Update the app before importing it.`);
|
|
217
229
|
}
|