@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
|
@@ -1,30 +1,14 @@
|
|
|
1
1
|
// Pure Brand Profile normalization and storage model. The browser wrapper owns
|
|
2
2
|
// persistence and downloads; this module owns schema migration, semantic field
|
|
3
3
|
// bounds, import merging, and exact serialized-byte accounting.
|
|
4
|
-
import { normalizeDomain } from "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export
|
|
12
|
-
// Profiles share the origin's browser-storage quota with cases, campaigns,
|
|
13
|
-
// watchlists, and CT history. Fail at a predictable one-megabyte boundary.
|
|
14
|
-
export const MAX_PROFILE_STORE_BYTES = 1024 * 1024;
|
|
15
|
-
export const MAX_PROFILE_NAME_LENGTH = 100;
|
|
16
|
-
export const MAX_PROFILE_TEXT_LENGTH = 200;
|
|
17
|
-
export const MAX_PROFILE_DOMAIN_LENGTH = 253;
|
|
18
|
-
export const MAX_PROFILE_TLD_LENGTH = 63;
|
|
19
|
-
export const MAX_DKIM_SELECTOR_LENGTH = 253;
|
|
20
|
-
export const MAX_DKIM_SELECTORS = 10;
|
|
21
|
-
export const MAX_PROTECTION_ATTESTATIONS = 6;
|
|
22
|
-
export const MAX_DESIRED_POSTURE_BASELINES = 20;
|
|
23
|
-
export const MAX_DESIRED_POSTURE_RECORDS = 32;
|
|
24
|
-
export const MAX_DESIRED_POSTURE_SUPPRESSIONS = 12;
|
|
25
|
-
export const MAX_DESIRED_POSTURE_OBSERVATIONS = 12;
|
|
26
|
-
export const MAX_DESIRED_POSTURE_CHANGE_WINDOWS = 8;
|
|
27
|
-
const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
4
|
+
import { normalizeDomain } from "../cases/case-model.mjs";
|
|
5
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
|
+
import { normalizeOpaqueReferenceId } from "../cases/opaque-reference-id.mjs";
|
|
7
|
+
import { normalizePageBaseline } from "./page-baseline.mjs";
|
|
8
|
+
import { isInformativePerceptualHash as isInformativeFaviconHash } from "../../lib/perceptual-hash-comparison.mjs";
|
|
9
|
+
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";
|
|
28
12
|
const SHA256_RE = /^[a-f0-9]{64}$/i;
|
|
29
13
|
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
30
14
|
const DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -45,7 +29,7 @@ const PROTECTION_ATTESTATION_STATES = new Set([
|
|
|
45
29
|
'unavailable',
|
|
46
30
|
'not_applicable',
|
|
47
31
|
]);
|
|
48
|
-
const
|
|
32
|
+
export const DESIRED_POSTURE_SUPPRESSION_FIELDS = Object.freeze([
|
|
49
33
|
'nameservers',
|
|
50
34
|
'ds',
|
|
51
35
|
'mx',
|
|
@@ -56,11 +40,10 @@ const DESIRED_POSTURE_FIELDS = new Set([
|
|
|
56
40
|
'registrar_lock',
|
|
57
41
|
'renewal_review',
|
|
58
42
|
]);
|
|
43
|
+
const DESIRED_POSTURE_FIELDS = new Set(DESIRED_POSTURE_SUPPRESSION_FIELDS);
|
|
59
44
|
const POSTURE_CHECK_STATUSES = new Set(['danger', 'info', 'pass', 'warning']);
|
|
60
45
|
function record(value) {
|
|
61
|
-
return value
|
|
62
|
-
? value
|
|
63
|
-
: {};
|
|
46
|
+
return ordinaryWorkspaceRecord(value, 'Brand Profile input') ?? {};
|
|
64
47
|
}
|
|
65
48
|
function boundedText(value, maximum = MAX_PROFILE_TEXT_LENGTH) {
|
|
66
49
|
if (typeof value !== 'string' || CONTROL_RE.test(value))
|
|
@@ -68,15 +51,16 @@ function boundedText(value, maximum = MAX_PROFILE_TEXT_LENGTH) {
|
|
|
68
51
|
return value.slice(0, maximum * 4).replace(/\s+/g, ' ').trim().slice(0, maximum).trim();
|
|
69
52
|
}
|
|
70
53
|
export function normalizeBrandProfileId(value) {
|
|
71
|
-
return
|
|
54
|
+
return normalizeOpaqueReferenceId(value);
|
|
72
55
|
}
|
|
73
56
|
function timestamp(value, fallback) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
57
|
+
return normalizeExplicitIsoTimestamp(value) ?? fallback;
|
|
58
|
+
}
|
|
59
|
+
function calendarTimestamp(value, fallback) {
|
|
60
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/u.test(value)) {
|
|
61
|
+
return normalizeExplicitIsoTimestamp(`${value}T00:00:00Z`) ?? fallback;
|
|
78
62
|
}
|
|
79
|
-
return fallback;
|
|
63
|
+
return timestamp(value, fallback);
|
|
80
64
|
}
|
|
81
65
|
function normalizeTld(value) {
|
|
82
66
|
if (typeof value !== 'string' || CONTROL_RE.test(value))
|
|
@@ -156,7 +140,7 @@ export function normalizeProtectionAttestations(value) {
|
|
|
156
140
|
control: candidate.control,
|
|
157
141
|
state: candidate.state,
|
|
158
142
|
assertedAt,
|
|
159
|
-
expiresAt:
|
|
143
|
+
expiresAt: calendarTimestamp(candidate.expiresAt, null),
|
|
160
144
|
note: boundedText(candidate.note),
|
|
161
145
|
});
|
|
162
146
|
if (output.length >= MAX_PROTECTION_ATTESTATIONS)
|
|
@@ -217,10 +201,25 @@ function normalizeDesiredPostureObservationHistory(value, previous) {
|
|
|
217
201
|
.sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt))
|
|
218
202
|
.slice(-MAX_DESIRED_POSTURE_OBSERVATIONS);
|
|
219
203
|
}
|
|
220
|
-
function
|
|
204
|
+
function deterministicChangeWindowId(seed) {
|
|
205
|
+
const states = [0x811c9dc5, 0x9e3779b9, 0x85ebca6b, 0xc2b2ae35];
|
|
206
|
+
const factors = [0x01000193, 0x85ebca6b, 0xc2b2ae35, 0x27d4eb2f];
|
|
207
|
+
for (let index = 0; index < seed.length; index += 1) {
|
|
208
|
+
const code = seed.charCodeAt(index);
|
|
209
|
+
for (let stateIndex = 0; stateIndex < states.length; stateIndex += 1) {
|
|
210
|
+
const state = states[stateIndex] ?? 0;
|
|
211
|
+
const mixed = state ^ (code + Math.imul(index + 1, stateIndex + 17));
|
|
212
|
+
states[stateIndex] = Math.imul(mixed, factors[stateIndex] ?? 0x01000193);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return `cw-${states.map((state) => (state >>> 0).toString(16).padStart(8, '0')).join('')}`;
|
|
216
|
+
}
|
|
217
|
+
function normalizeDesiredPostureChangeWindows(value, identityScope = '') {
|
|
221
218
|
if (!Array.isArray(value))
|
|
222
219
|
return [];
|
|
223
220
|
const output = [];
|
|
221
|
+
const seenIds = new Set();
|
|
222
|
+
const duplicateMaterial = new Map();
|
|
224
223
|
for (const item of value.slice(0, MAX_DESIRED_POSTURE_CHANGE_WINDOWS * 4)) {
|
|
225
224
|
const candidate = record(item);
|
|
226
225
|
const startsAt = timestamp(candidate.startsAt, null);
|
|
@@ -228,7 +227,17 @@ function normalizeDesiredPostureChangeWindows(value) {
|
|
|
228
227
|
const summary = boundedText(candidate.summary, 300);
|
|
229
228
|
if (!startsAt || !endsAt || Date.parse(endsAt) <= Date.parse(startsAt) || !summary)
|
|
230
229
|
continue;
|
|
231
|
-
|
|
230
|
+
const material = `${startsAt}\u001f${endsAt}\u001f${summary}`;
|
|
231
|
+
const occurrence = duplicateMaterial.get(material) ?? 0;
|
|
232
|
+
duplicateMaterial.set(material, occurrence + 1);
|
|
233
|
+
let id = normalizeOpaqueReferenceId(candidate.id);
|
|
234
|
+
let salt = 0;
|
|
235
|
+
while (!id || seenIds.has(id)) {
|
|
236
|
+
id = deterministicChangeWindowId(`${identityScope}\u001f${material}\u001f${occurrence}\u001f${salt}`);
|
|
237
|
+
salt += 1;
|
|
238
|
+
}
|
|
239
|
+
seenIds.add(id);
|
|
240
|
+
output.push({ id, startsAt, endsAt, summary });
|
|
232
241
|
if (output.length >= MAX_DESIRED_POSTURE_CHANGE_WINDOWS)
|
|
233
242
|
break;
|
|
234
243
|
}
|
|
@@ -250,7 +259,7 @@ function normalizeTlsSanPatterns(value) {
|
|
|
250
259
|
}
|
|
251
260
|
return [...output].sort();
|
|
252
261
|
}
|
|
253
|
-
export function normalizeDesiredPostureBaselines(value, officialDomains, fallbackNow = new Date().toISOString()) {
|
|
262
|
+
export function normalizeDesiredPostureBaselines(value, officialDomains, fallbackNow = new Date().toISOString(), identityScope = '') {
|
|
254
263
|
if (!Array.isArray(value))
|
|
255
264
|
return [];
|
|
256
265
|
const allowedDomains = new Set(officialDomains);
|
|
@@ -275,7 +284,7 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
|
|
|
275
284
|
suppressions.push({
|
|
276
285
|
field,
|
|
277
286
|
reason,
|
|
278
|
-
expiresAt:
|
|
287
|
+
expiresAt: calendarTimestamp(suppression.expiresAt, null),
|
|
279
288
|
});
|
|
280
289
|
if (suppressions.length >= MAX_DESIRED_POSTURE_SUPPRESSIONS)
|
|
281
290
|
break;
|
|
@@ -298,7 +307,7 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
|
|
|
298
307
|
registrarLock: ['required', 'not_required'].includes(String(candidate.registrarLock))
|
|
299
308
|
? candidate.registrarLock
|
|
300
309
|
: 'unconfigured',
|
|
301
|
-
renewalReviewAt:
|
|
310
|
+
renewalReviewAt: calendarTimestamp(candidate.renewalReviewAt, null),
|
|
302
311
|
zoneIntent: ['active_service', 'defensive_registration', 'no_service', 'parked', 'redirect_only'].includes(String(candidate.zoneIntent))
|
|
303
312
|
? candidate.zoneIntent
|
|
304
313
|
: 'unconfigured',
|
|
@@ -306,7 +315,7 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
|
|
|
306
315
|
? candidate.lifecycle
|
|
307
316
|
: 'active',
|
|
308
317
|
recoveryDependency: boundedText(candidate.recoveryDependency, 200),
|
|
309
|
-
approvedChangeWindows: normalizeDesiredPostureChangeWindows(candidate.approvedChangeWindows),
|
|
318
|
+
approvedChangeWindows: normalizeDesiredPostureChangeWindows(candidate.approvedChangeWindows, `${identityScope}\u001f${domain}`),
|
|
310
319
|
suppressions,
|
|
311
320
|
note: boundedText(candidate.note, MAX_PROFILE_TEXT_LENGTH),
|
|
312
321
|
previousObservation: observationHistory.at(-1) ?? previousObservation,
|
|
@@ -331,9 +340,12 @@ export function normalizeBrandProfile(raw, options = {}) {
|
|
|
331
340
|
const now = timestamp(options.nowIso, new Date().toISOString());
|
|
332
341
|
const officialDomains = normalizeProfileDomains(value.officialDomains);
|
|
333
342
|
const dkimSelectors = normalizeDkimSelectors(value.dkimSelectors);
|
|
334
|
-
const
|
|
343
|
+
const suppliedBaseline = Object.prototype.hasOwnProperty.call(value, 'pageBaseline');
|
|
344
|
+
const candidateBaseline = suppliedBaseline
|
|
335
345
|
? normalizePageBaseline(value.pageBaseline)
|
|
336
346
|
: normalizePageBaseline(existing?.pageBaseline);
|
|
347
|
+
const baselineDomainMismatch = suppliedBaseline && candidateBaseline !== null
|
|
348
|
+
&& !officialDomains.includes(candidateBaseline.domain);
|
|
337
349
|
const pageBaseline = candidateBaseline && officialDomains.includes(candidateBaseline.domain)
|
|
338
350
|
? candidateBaseline
|
|
339
351
|
: null;
|
|
@@ -358,11 +370,11 @@ export function normalizeBrandProfile(raw, options = {}) {
|
|
|
358
370
|
.filter((selector) => !dkimSelectors.includes(selector)),
|
|
359
371
|
mailProtectionProfile: normalizeMailProtectionProfile(value.mailProtectionProfile),
|
|
360
372
|
protectionAttestations: normalizeProtectionAttestations(value.protectionAttestations),
|
|
361
|
-
desiredPostureBaselines: normalizeDesiredPostureBaselines(value.desiredPostureBaselines, officialDomains, now),
|
|
373
|
+
desiredPostureBaselines: normalizeDesiredPostureBaselines(value.desiredPostureBaselines, officialDomains, now, profileId),
|
|
362
374
|
trademarkOwner: boundedText(value.trademarkOwner),
|
|
363
375
|
trademarkRegistration: boundedText(value.trademarkRegistration),
|
|
364
|
-
officialFaviconHash: normalizeFaviconHash(value.officialFaviconHash),
|
|
365
|
-
officialFaviconPHash: normalizeFaviconPHash(value.officialFaviconPHash),
|
|
376
|
+
officialFaviconHash: baselineDomainMismatch ? '' : normalizeFaviconHash(value.officialFaviconHash),
|
|
377
|
+
officialFaviconPHash: baselineDomainMismatch ? '' : normalizeFaviconPHash(value.officialFaviconPHash),
|
|
366
378
|
pageBaseline,
|
|
367
379
|
createdAt,
|
|
368
380
|
updatedAt: options.touch === true ? now : timestamp(value.updatedAt, createdAt),
|
|
@@ -376,15 +388,24 @@ function profileList(raw) {
|
|
|
376
388
|
}
|
|
377
389
|
export function brandProfileStoreVersion(raw) {
|
|
378
390
|
if (Array.isArray(raw))
|
|
379
|
-
return
|
|
391
|
+
return BRAND_PROFILE_SCHEMA_VERSION;
|
|
380
392
|
const value = record(raw);
|
|
381
393
|
return typeof value.version === 'number' && Number.isFinite(value.version) && value.version > 0 ? value.version : null;
|
|
382
394
|
}
|
|
383
395
|
/** Normalize an internal profile collection or current stored envelope. */
|
|
384
396
|
export function normalizeBrandProfileStore(raw) {
|
|
397
|
+
assertWorkspaceInputGraph(raw, 'Brand Profile store');
|
|
398
|
+
assertWorkspaceDeclaredVersion(raw, 'Brand Profile store');
|
|
399
|
+
const sourceVersion = brandProfileStoreVersion(raw);
|
|
400
|
+
if (!Array.isArray(raw) && sourceVersion !== null
|
|
401
|
+
&& !SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS.includes(sourceVersion)) {
|
|
402
|
+
throw new Error(`Brand Profile schema ${sourceVersion} is unsupported; no data was changed.`);
|
|
403
|
+
}
|
|
385
404
|
const byId = new Map();
|
|
386
405
|
for (const item of profileList(raw).slice(0, MAX_PROFILES * 4)) {
|
|
387
|
-
const profile = normalizeBrandProfile(item
|
|
406
|
+
const profile = normalizeBrandProfile(item, {
|
|
407
|
+
nowIso: new Date(0).toISOString(),
|
|
408
|
+
});
|
|
388
409
|
if (!profile)
|
|
389
410
|
continue;
|
|
390
411
|
const previous = byId.get(profile.id);
|
|
@@ -409,6 +430,9 @@ export function assertBrandProfileStoreBudget(profiles) {
|
|
|
409
430
|
return store;
|
|
410
431
|
}
|
|
411
432
|
export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
433
|
+
assertWorkspaceInputGraph(localRaw, 'Local Brand Profile store');
|
|
434
|
+
assertWorkspaceInputGraph(importedRaw, 'Imported Brand Profile document');
|
|
435
|
+
assertWorkspacePortableVersion(importedRaw, BRAND_PROFILE_SCHEMA_VERSION, 'Imported Brand Profile document');
|
|
412
436
|
const imported = record(importedRaw);
|
|
413
437
|
if (imported.schema !== BRAND_PROFILE_SCHEMA) {
|
|
414
438
|
throw new Error('This JSON file is not a WHOISleuth Brand Profile export.');
|
|
@@ -420,17 +444,39 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
|
420
444
|
if (importedVersion !== null && importedVersion > BRAND_PROFILE_SCHEMA_VERSION) {
|
|
421
445
|
throw new Error(`This Brand Profile file uses newer schema ${importedVersion}. Update the app before importing it.`);
|
|
422
446
|
}
|
|
423
|
-
if (!
|
|
424
|
-
throw new Error(`Expected a WHOISleuth Brand Profile export using schema
|
|
447
|
+
if (!SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS.includes(importedVersion ?? 0)) {
|
|
448
|
+
throw new Error(`Expected a WHOISleuth Brand Profile export using schema ${SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS.join(', ').replace(/, ([^,]+)$/u, ', or $1')}.`);
|
|
449
|
+
}
|
|
450
|
+
const idNames = new Map();
|
|
451
|
+
const retainIdName = (id, name) => {
|
|
452
|
+
const nameKey = name.toLowerCase();
|
|
453
|
+
const previous = idNames.get(id);
|
|
454
|
+
if (previous !== undefined && previous !== nameKey) {
|
|
455
|
+
throw new Error('Brand Profile import reuses one exact identifier for different normalised profile names. No profiles were imported.');
|
|
456
|
+
}
|
|
457
|
+
idNames.set(id, nameKey);
|
|
458
|
+
};
|
|
459
|
+
for (const item of profileList(localRaw).slice(0, MAX_PROFILES * 4)) {
|
|
460
|
+
const value = record(item);
|
|
461
|
+
const id = normalizeBrandProfileId(value.id);
|
|
462
|
+
const name = boundedText(value.name, MAX_PROFILE_NAME_LENGTH);
|
|
463
|
+
if (id && name)
|
|
464
|
+
retainIdName(id, name);
|
|
425
465
|
}
|
|
426
466
|
const local = normalizeBrandProfileStore(localRaw).profiles;
|
|
467
|
+
for (const profile of local)
|
|
468
|
+
retainIdName(profile.id, profile.name);
|
|
427
469
|
const byName = new Map(local.map((profile) => [profile.name.toLowerCase(), profile]));
|
|
428
470
|
const input = profileList(importedRaw);
|
|
429
471
|
let added = 0;
|
|
430
472
|
let updated = 0;
|
|
431
473
|
let skipped = Math.max(0, input.length - MAX_PROFILES * 4);
|
|
432
474
|
for (const item of input.slice(0, MAX_PROFILES * 4)) {
|
|
433
|
-
const
|
|
475
|
+
const value = record(item);
|
|
476
|
+
const rawName = boundedText(value.name, MAX_PROFILE_NAME_LENGTH);
|
|
477
|
+
const rawId = normalizeBrandProfileId(value.id);
|
|
478
|
+
if (rawId && rawName)
|
|
479
|
+
retainIdName(rawId, rawName);
|
|
434
480
|
const existing = rawName ? byName.get(rawName.toLowerCase()) : null;
|
|
435
481
|
const profile = normalizeBrandProfile(item, {
|
|
436
482
|
existing,
|
|
@@ -442,6 +488,7 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
|
442
488
|
skipped++;
|
|
443
489
|
continue;
|
|
444
490
|
}
|
|
491
|
+
retainIdName(profile.id, profile.name);
|
|
445
492
|
if (existing) {
|
|
446
493
|
byName.set(profile.name.toLowerCase(), profile);
|
|
447
494
|
updated++;
|
package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs}
RENAMED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { normalizeDomain } from "
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
export const MAX_BULK_REVIEW_NAME_LENGTH = 80;
|
|
1
|
+
import { normalizeDomain } from "../cases/case-model.mjs";
|
|
2
|
+
import { normalizeBulkPresentationSortKey } from "./bulk-sort.mjs";
|
|
3
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
4
|
+
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";
|
|
6
|
+
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";
|
|
8
7
|
export const BULK_REVIEW_STATES = ['unreviewed', 'reviewing', 'reviewed', 'deferred'];
|
|
9
8
|
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
10
9
|
const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
11
|
-
const PRIMARY_FILTERS = new Set(['all', 'available', 'registered', 'high_risk', 'trusted', 'errors']);
|
|
10
|
+
const PRIMARY_FILTERS = new Set(['all', 'available', 'registered', 'high_risk', 'trusted', 'profile_unevaluated', 'errors']);
|
|
12
11
|
const SOURCE_FILTERS = new Set(['', 'complete', 'limited', 'failed']);
|
|
13
12
|
const LIFECYCLE_FILTERS = new Set(['', 'new', 'expiring', 'aged', 'unknown']);
|
|
14
13
|
const AGE_FILTERS = new Set(['', '7d', '30d', '90d', '365d', 'older', 'unknown']);
|
|
@@ -19,9 +18,7 @@ const REVIEW_STATES = new Set(BULK_REVIEW_STATES);
|
|
|
19
18
|
const CASE_DISPOSITIONS = new Set(['', 'untracked', 'unreviewed', 'suspicious', 'confirmed_abuse', 'false_positive', 'expected', 'closed_no_action']);
|
|
20
19
|
const SIGNAL_FILTERS = new Set(['favicon', 'password', 'phishing', 'asset_reuse', 'idn']);
|
|
21
20
|
function record(value) {
|
|
22
|
-
return value
|
|
23
|
-
? value
|
|
24
|
-
: {};
|
|
21
|
+
return ordinaryWorkspaceRecord(value, 'Bulk-review input') ?? {};
|
|
25
22
|
}
|
|
26
23
|
function text(value, maximum) {
|
|
27
24
|
if (typeof value !== 'string' || CONTROL_RE.test(value))
|
|
@@ -29,9 +26,7 @@ function text(value, maximum) {
|
|
|
29
26
|
return value.trim().slice(0, maximum);
|
|
30
27
|
}
|
|
31
28
|
function timestamp(value, fallback) {
|
|
32
|
-
|
|
33
|
-
const parsed = Date.parse(normalized);
|
|
34
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : fallback;
|
|
29
|
+
return normalizeExplicitIsoTimestamp(value) ?? fallback;
|
|
35
30
|
}
|
|
36
31
|
function safeId(value) {
|
|
37
32
|
return typeof value === 'string' && SAFE_ID_RE.test(value) ? value : '';
|
|
@@ -50,7 +45,7 @@ function normalizeView(raw) {
|
|
|
50
45
|
? [...new Set(value.signalFilters.filter((item) => typeof item === 'string' && SIGNAL_FILTERS.has(item)))].slice(0, SIGNAL_FILTERS.size)
|
|
51
46
|
: [];
|
|
52
47
|
const sortKey = typeof value.sortKey === 'string' && SORT_KEYS.has(value.sortKey)
|
|
53
|
-
? value.sortKey
|
|
48
|
+
? normalizeBulkPresentationSortKey(value.sortKey)
|
|
54
49
|
: 'risk';
|
|
55
50
|
return {
|
|
56
51
|
primaryFilter: setValue(value.primaryFilter, PRIMARY_FILTERS, 'all'),
|
|
@@ -110,6 +105,8 @@ function sourceLists(raw) {
|
|
|
110
105
|
};
|
|
111
106
|
}
|
|
112
107
|
export function normalizeBulkReviewStore(raw) {
|
|
108
|
+
assertWorkspaceInputGraph(raw, 'Bulk-review store');
|
|
109
|
+
assertWorkspaceDeclaredVersion(raw, 'Bulk-review store');
|
|
113
110
|
const fallback = new Date(0).toISOString();
|
|
114
111
|
const source = sourceLists(raw);
|
|
115
112
|
const presets = new Map();
|
|
@@ -207,6 +204,9 @@ export function buildBulkReviewExport(raw) {
|
|
|
207
204
|
return enforceBulkReviewBudget(raw);
|
|
208
205
|
}
|
|
209
206
|
export function mergeBulkReviewStores(localRaw, importedRaw) {
|
|
207
|
+
assertWorkspaceInputGraph(localRaw, 'Local Bulk-review store');
|
|
208
|
+
assertWorkspaceInputGraph(importedRaw, 'Imported Bulk-review document');
|
|
209
|
+
assertWorkspacePortableVersion(importedRaw, BULK_REVIEW_SCHEMA_VERSION, 'Imported Bulk-review document');
|
|
210
210
|
const importedRecord = record(importedRaw);
|
|
211
211
|
if (importedRecord.schema !== BULK_REVIEW_SCHEMA) {
|
|
212
212
|
throw new Error('This file is not a WHOISleuth Bulk review export.');
|