@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1
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 +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +21 -6
- package/cli/artifact-validation/lookup-evidence.mjs +3 -3
- package/cli/artifact-validation/signed-review.mjs +59 -13
- package/cli/artifact-verify.mjs +2 -0
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -2
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +20 -3
- package/cli/case-pack.mjs +12 -2
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -210
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +3 -3
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +33 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +1 -1
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +18 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +68 -158
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +2 -2
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +8 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +223 -2
- package/docs/contextual-reviews.md +155 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +12 -8
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +9 -7
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +22 -5
- package/lib/domain-posture.mjs +16 -15
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +22 -15
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +4 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +7 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +69 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +27 -0
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +141 -139
- package/lib/static-html-analysis.mjs +1 -1
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +388 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +13 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +345 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +78 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-recheck-model.mjs +90 -32
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +40 -112
- package/packages/cases/case-record-projection.mjs +8 -1
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +63 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +103 -1
- package/packages/cases/case-response-model.mjs +9 -1
- package/packages/cases/case-response-outcomes.mjs +29 -8
- package/packages/cases/case-response-packet-render.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +25 -11
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +14 -3
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +28 -10
- package/packages/contracts/case-portability.mjs +118 -25
- package/packages/contracts/cli-command-semantics.mjs +2 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +63 -17
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +4 -12
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/intake-report.mjs +19 -0
- package/packages/investigation/investigation-manifest.mjs +2 -2
- package/packages/investigation/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +195 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/workspace/brand-profile-model.mjs +2 -2
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +23 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +28 -12
- package/packages/workspace/watchlist-store.mjs +17 -5
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- package/lib/hostname.mjs +0 -2
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Pure, bounded persistence model for analyst-saved Bulk investigations.
|
|
2
2
|
// Sessions retain compact derived rows and source states, never raw registry,
|
|
3
3
|
// WHOIS, HTTP, TLS, page, or provider payloads.
|
|
4
|
-
import { normalizeDomain } from "../
|
|
4
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
5
5
|
import { normalizeCaaCritical } from "./dns-record-normalization.mjs";
|
|
6
6
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { normalizeWebCollectionQuality, webCollectionAllowsComparison } from "../evidence/collection-quality.mjs";
|
|
8
|
+
import { RELATIONSHIP_EVIDENCE_VERSION, RELATIONSHIP_SOURCE_PROVENANCE_VERSION, SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS } from "../contracts/offline-comparison.mjs";
|
|
8
9
|
import { normalizeRelationshipSourceProjection } from "../comparison/relationship-provenance.mjs";
|
|
9
10
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
10
11
|
import { BULK_SESSION_SCHEMA, BULK_SESSION_SCHEMA_VERSION, MAX_BULK_PROFILE_CONTEXT_LIMITATION_LENGTH, MAX_BULK_SESSIONS, MAX_BULK_SESSION_ARRAY_VALUES, MAX_BULK_SESSION_NAME_LENGTH, MAX_BULK_SESSION_ROWS, MAX_BULK_SESSION_SOURCES, MAX_BULK_SESSION_STORE_BYTES, MAX_BULK_SESSION_TEXT_LENGTH, SUPPORTED_BULK_SESSION_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
@@ -94,7 +95,7 @@ export function normalizeBulkProfileContext(value, fallbackLimitation = BULK_PRO
|
|
|
94
95
|
}
|
|
95
96
|
return unavailableBulkProfileContext(profileContextLimitation(item?.limitation, fallbackLimitation));
|
|
96
97
|
}
|
|
97
|
-
function
|
|
98
|
+
export function bulkProfileContextsMatch(left, right) {
|
|
98
99
|
return left.sourceState === right.sourceState
|
|
99
100
|
&& left.activeProfileId === right.activeProfileId
|
|
100
101
|
&& left.profileUpdatedAt === right.profileUpdatedAt
|
|
@@ -104,7 +105,7 @@ export function summarizeBulkProfileContexts(rows) {
|
|
|
104
105
|
const first = rows[0]?.profileContext;
|
|
105
106
|
if (!first)
|
|
106
107
|
return unavailableBulkProfileContext('This saved Bulk session contains no settled row with profile-context provenance.');
|
|
107
|
-
if (rows.every((row) =>
|
|
108
|
+
if (rows.every((row) => bulkProfileContextsMatch(first, row.profileContext)))
|
|
108
109
|
return { ...first };
|
|
109
110
|
return {
|
|
110
111
|
sourceState: 'mixed',
|
|
@@ -200,7 +201,7 @@ function nullableHash(value, expression) {
|
|
|
200
201
|
}
|
|
201
202
|
function normalizeRelationship(value) {
|
|
202
203
|
const item = record(value);
|
|
203
|
-
if (item?.version !== undefined &&
|
|
204
|
+
if (item?.version !== undefined && !SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS.some(version => version === item.version)) {
|
|
204
205
|
throw new TypeError('This Bulk relationship projection uses an unsupported version. No source evidence was interpreted.');
|
|
205
206
|
}
|
|
206
207
|
return {
|
|
@@ -212,7 +213,7 @@ function normalizeRelationship(value) {
|
|
|
212
213
|
faviconHash: nullableHash(item?.faviconHash, HASH_64_RE),
|
|
213
214
|
faviconPHash: nullableHash(item?.faviconPHash, PHASH_RE),
|
|
214
215
|
certificateFingerprint: nullableHash(item?.certificateFingerprint, HASH_64_RE),
|
|
215
|
-
sourceEvidence: item?.version
|
|
216
|
+
sourceEvidence: Number(item?.version) >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION ? normalizeRelationshipSourceProjection(item?.sourceEvidence) : {},
|
|
216
217
|
truncated: item?.truncated === true,
|
|
217
218
|
};
|
|
218
219
|
}
|
|
@@ -304,6 +305,7 @@ export function normalizeBulkSessionResult(value, inheritedProfileContext) {
|
|
|
304
305
|
const profileClaimsUsable = profileContextReady && !impossibleNoProfileClaim;
|
|
305
306
|
if (!profileClaimsUsable || readyWithoutActiveProfile)
|
|
306
307
|
relationship.officialAssetHosts = [];
|
|
308
|
+
const webCollectionQuality = normalizeWebCollectionQuality(item.webCollectionQuality, scanDepth);
|
|
307
309
|
return {
|
|
308
310
|
domain,
|
|
309
311
|
status: status,
|
|
@@ -317,6 +319,7 @@ export function normalizeBulkSessionResult(value, inheritedProfileContext) {
|
|
|
317
319
|
trusted: profileClaimsUsable && !readyWithoutActiveProfile && TRUST_STATES.has(trusted) ? trusted : null,
|
|
318
320
|
error: boundedText(item.error),
|
|
319
321
|
scanDepth: scanDepth,
|
|
322
|
+
...(webCollectionQuality ? { webCollectionQuality } : {}),
|
|
320
323
|
observedAt: normalizeExplicitIsoTimestamp(item.observedAt),
|
|
321
324
|
createdDate: boundedText(item.createdDate, 64) || null,
|
|
322
325
|
expiryDate: boundedText(item.expiryDate, 64) || null,
|
|
@@ -381,7 +384,7 @@ export function normalizeBulkSession(value, sourceStoreVersion) {
|
|
|
381
384
|
// Only an explicitly versioned current document may omit identical row
|
|
382
385
|
// context. Bare runtime rows and public schema 4 must carry their own context;
|
|
383
386
|
// an explicit invalid/null row value never falls back to the session.
|
|
384
|
-
const inheritedProfileContext = sourceStoreVersion
|
|
387
|
+
const inheritedProfileContext = sourceStoreVersion !== undefined && sourceStoreVersion >= 5
|
|
385
388
|
&& declaredProfileContext.sourceState !== 'mixed'
|
|
386
389
|
&& rawProfileContext.sourceState === declaredProfileContext.sourceState
|
|
387
390
|
? declaredProfileContext
|
|
@@ -390,8 +393,11 @@ export function normalizeBulkSession(value, sourceStoreVersion) {
|
|
|
390
393
|
const results = [];
|
|
391
394
|
const seen = new Set();
|
|
392
395
|
for (const candidate of Array.isArray(item.results) ? item.results.slice(0, MAX_BULK_SESSION_ROWS * 2) : []) {
|
|
396
|
+
if (sourceStoreVersion !== undefined && sourceStoreVersion < 6 && record(candidate)?.webCollectionQuality !== undefined) {
|
|
397
|
+
throw new TypeError('Web collection quality requires Bulk schema 6 or later; historical evidence was not reinterpreted.');
|
|
398
|
+
}
|
|
393
399
|
const publicLegacyRow = sourceStoreVersion === 4;
|
|
394
|
-
if (publicLegacyRow && record(record(candidate)?.relationship)?.version
|
|
400
|
+
if (publicLegacyRow && Number(record(record(candidate)?.relationship)?.version) >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION) {
|
|
395
401
|
throw new TypeError('Bulk schema 4 cannot contain newer relationship source evidence; no evidence was interpreted.');
|
|
396
402
|
}
|
|
397
403
|
const result = normalizeBulkSessionResult(candidate, inheritedProfileContext);
|
|
@@ -411,7 +417,7 @@ export function normalizeBulkSession(value, sourceStoreVersion) {
|
|
|
411
417
|
if ((state === 'complete') !== (results.length === domains.length))
|
|
412
418
|
return null;
|
|
413
419
|
const profileContext = summarizeBulkProfileContexts(results);
|
|
414
|
-
if (!
|
|
420
|
+
if (!bulkProfileContextsMatch(declaredProfileContext, profileContext))
|
|
415
421
|
return null;
|
|
416
422
|
return {
|
|
417
423
|
id,
|
|
@@ -471,7 +477,7 @@ export function normalizeBulkSessionStore(raw) {
|
|
|
471
477
|
export function bulkSessionStorageValue(session) {
|
|
472
478
|
return {
|
|
473
479
|
...session,
|
|
474
|
-
results: session.results.map(({ profileContext, ...row }) =>
|
|
480
|
+
results: session.results.map(({ profileContext, ...row }) => bulkProfileContextsMatch(profileContext, session.profileContext)
|
|
475
481
|
? row
|
|
476
482
|
: { ...row, profileContext }),
|
|
477
483
|
};
|
|
@@ -558,8 +564,10 @@ function sourceStateMap(value) {
|
|
|
558
564
|
export function areBulkRiskScoresComparable(previous, current) {
|
|
559
565
|
return previous.profileContext.sourceState === 'ready'
|
|
560
566
|
&& current.profileContext.sourceState === 'ready'
|
|
561
|
-
&&
|
|
567
|
+
&& bulkProfileContextsMatch(previous.profileContext, current.profileContext)
|
|
562
568
|
&& previous.scanDepth === current.scanDepth
|
|
569
|
+
&& webCollectionAllowsComparison('riskScore', previous.webCollectionQuality, previous.scanDepth)
|
|
570
|
+
&& webCollectionAllowsComparison('riskScore', current.webCollectionQuality, current.scanDepth)
|
|
563
571
|
&& previous.risk !== null
|
|
564
572
|
&& current.risk !== null
|
|
565
573
|
&& previous.riskModelVersion !== null
|
|
@@ -574,7 +582,9 @@ function resultChanges(previous, current) {
|
|
|
574
582
|
}
|
|
575
583
|
if (previous.registrar !== current.registrar)
|
|
576
584
|
changes.push(`Registrar: ${previous.registrar} → ${current.registrar}`);
|
|
577
|
-
if (previous.activity !== current.activity
|
|
585
|
+
if (previous.activity !== current.activity
|
|
586
|
+
&& webCollectionAllowsComparison('activityStatus', previous.webCollectionQuality, previous.scanDepth)
|
|
587
|
+
&& webCollectionAllowsComparison('activityStatus', current.webCollectionQuality, current.scanDepth))
|
|
578
588
|
changes.push(`Website: ${previous.activity} → ${current.activity}`);
|
|
579
589
|
const previousComparison = previous.comparisonEvidence;
|
|
580
590
|
const currentComparison = current.comparisonEvidence;
|
|
@@ -644,7 +654,7 @@ export function compareBulkSessions(baselineRaw, currentRaw) {
|
|
|
644
654
|
'A source-state change may reflect collection availability rather than a change to the domain.',
|
|
645
655
|
'A missing row means it was not completed in that saved session; it does not establish domain removal.',
|
|
646
656
|
...(riskIncomparable
|
|
647
|
-
? ['Risk deltas are omitted unless both rows retain the same ready Brand Profile provenance, scan depth,
|
|
657
|
+
? ['Risk deltas are omitted unless both rows retain the same ready Brand Profile provenance, scan depth, versioned Risk model and complete web collection for Deep observations.']
|
|
648
658
|
: []),
|
|
649
659
|
],
|
|
650
660
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pure, framework-neutral browser-local campaign model. Campaigns deliberately
|
|
2
2
|
// reference normalized case domains rather than copying case evidence, notes,
|
|
3
3
|
// or mutable case ids. The browser wrapper owns persistence and downloads.
|
|
4
|
-
import { normalizeDomain } from "../
|
|
4
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
5
5
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
6
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
7
7
|
import { CAMPAIGN_SCHEMA, CAMPAIGN_SCHEMA_VERSION, MAX_CAMPAIGNS, MAX_CAMPAIGN_DESCRIPTION_LENGTH, MAX_CAMPAIGN_DOMAINS, MAX_CAMPAIGN_INPUT_RECORDS, MAX_CAMPAIGN_NAME_LENGTH, MAX_CAMPAIGN_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pure browser-local custom detection-rule model. Rules are deliberately a
|
|
2
2
|
// small structured language: field names and operators come from allowlists,
|
|
3
3
|
// so imported rules cannot execute code or reach outside bounded case evidence.
|
|
4
|
-
import { latestCaseEvidence } from "../cases/case-model.mjs";
|
|
4
|
+
import { latestCaseEvidence } from "../cases/case-evidence-model.mjs";
|
|
5
5
|
import { CASE_DISPOSITIONS, CASE_STATUSES } from "../cases/case-record-contracts.mjs";
|
|
6
6
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
7
7
|
import { DETECTION_RULE_SCHEMA, DETECTION_RULE_SCHEMA_VERSION, MAX_CONDITION_VALUE_LENGTH, MAX_CUSTOM_RISK_TOTAL, MAX_DETECTION_RULES, MAX_RULE_CONDITIONS, MAX_RULE_INPUT_RECORDS, MAX_RULE_NAME_LENGTH, MAX_RULE_RISK_DELTA, MAX_RULE_STORE_BYTES, MAX_RULE_TAG_LENGTH, } from "../contracts/workspace-portability.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MAX_WORKSPACE_INPUT_ARRAY_LENGTH, MAX_WORKSPACE_INPUT_GRAPH_DEPTH, MAX_WORKSPACE_INPUT_GRAPH_NODES, MAX_WORKSPACE_INPUT_OBJECT_KEYS, MAX_WORKSPACE_INPUT_STRING_CODE_UNITS, } from "../contracts/workspace-portability.mjs";
|
|
2
|
-
import { boundedJsonLimitsForBytes } from "
|
|
2
|
+
import { boundedJsonLimitsForBytes } from "../analysis/bounded-json.mjs";
|
|
3
3
|
function inputError(label, detail) {
|
|
4
4
|
return new TypeError(`${label} must be bounded ordinary JSON data; ${detail}.`);
|
|
5
5
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeDomain } from "../
|
|
1
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
2
2
|
import { parse } from 'tldts';
|
|
3
3
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
4
4
|
export const INVESTIGATION_GUIDE_SCHEMA = 'whoisleuth.investigation-recipe';
|
|
@@ -13,7 +13,7 @@ export const MAX_INVESTIGATION_GUIDE_TIMESTAMP_LENGTH = 64;
|
|
|
13
13
|
export const MAX_INVESTIGATION_GUIDE_SERIALIZED_BYTES = 12_288;
|
|
14
14
|
export const MAX_INVESTIGATION_GUIDE_EXPORT_BYTES = 16_384;
|
|
15
15
|
const CONTROL_RE = /[\x00-\x1f\x7f]/u;
|
|
16
|
-
const SAFE_TEMPLATE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
16
|
+
export const SAFE_TEMPLATE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
17
17
|
const GUIDE_STATUSES = new Set(['active', 'paused']);
|
|
18
18
|
const GUIDE_OUTCOMES = new Set(['pending', 'complete', 'partial', 'skipped']);
|
|
19
19
|
function stage(id, workspace, label, detail, expectedEvidence, requestImpact, prerequisite, completionCriteria, instructions, requiresApproval) {
|
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
import { INVESTIGATION_RECIPES, normalizeInvestigationGuideTemplateSnapshot, } from "./investigation-guide.mjs";
|
|
1
|
+
import { INVESTIGATION_RECIPES, normalizeInvestigationGuideTemplateSnapshot, SAFE_TEMPLATE_ID_RE, } from "./investigation-guide.mjs";
|
|
2
2
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
3
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
4
|
+
import { exact, HEX_DIGEST_RE, text } from "../evidence/artifact-structure.mjs";
|
|
5
|
+
import { SORTED_JSON_V2 } from "../evidence/artifact-integrity.mjs";
|
|
4
6
|
import { INVESTIGATION_TEMPLATE_SCHEMA, INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS, INVESTIGATION_TEMPLATE_VERSION, MAX_INVESTIGATION_TEMPLATES, MAX_INVESTIGATION_TEMPLATE_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
5
7
|
export { INVESTIGATION_TEMPLATE_SCHEMA, INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS, INVESTIGATION_TEMPLATE_VERSION, MAX_INVESTIGATION_TEMPLATES, MAX_INVESTIGATION_TEMPLATE_IMPORT_BYTES, MAX_INVESTIGATION_TEMPLATE_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
8
|
+
export function readTemplateLessonRevision(raw) {
|
|
9
|
+
if (raw === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
assertWorkspaceInputGraph(raw, 'Template revision provenance');
|
|
12
|
+
const value = exact(raw, ['parentTemplateId', 'parentContentSha256', 'lessonContentSha256', 'canonicalization', 'applicability', 'rationale'], 'Template revision provenance');
|
|
13
|
+
if (typeof value.parentTemplateId !== 'string' || !SAFE_TEMPLATE_ID_RE.test(value.parentTemplateId)
|
|
14
|
+
|| value.canonicalization !== SORTED_JSON_V2
|
|
15
|
+
|| typeof value.parentContentSha256 !== 'string' || !HEX_DIGEST_RE.test(value.parentContentSha256)
|
|
16
|
+
|| typeof value.lessonContentSha256 !== 'string' || !HEX_DIGEST_RE.test(value.lessonContentSha256))
|
|
17
|
+
throw new TypeError('Template revision provenance is invalid.');
|
|
18
|
+
const applicability = text(value.applicability, 'Template applicability', 400).trim();
|
|
19
|
+
const rationale = text(value.rationale, 'Template revision reason', 400).trim();
|
|
20
|
+
if (!applicability || !rationale)
|
|
21
|
+
throw new TypeError('Template applicability and revision reason must not be empty.');
|
|
22
|
+
return { parentTemplateId: value.parentTemplateId, parentContentSha256: value.parentContentSha256, lessonContentSha256: value.lessonContentSha256,
|
|
23
|
+
canonicalization: SORTED_JSON_V2, applicability, rationale };
|
|
24
|
+
}
|
|
6
25
|
function record(value) {
|
|
7
26
|
return ordinaryWorkspaceRecord(value, 'Investigation-template input');
|
|
8
27
|
}
|
|
@@ -17,11 +36,13 @@ function template(raw) {
|
|
|
17
36
|
const snapshot = normalizeInvestigationGuideTemplateSnapshot(value);
|
|
18
37
|
const createdAt = timestamp(value?.createdAt);
|
|
19
38
|
const updatedAt = timestamp(value?.updatedAt);
|
|
39
|
+
const lessonRevision = readTemplateLessonRevision(value?.lessonRevision);
|
|
20
40
|
return snapshot && createdAt && updatedAt
|
|
21
|
-
? { ...snapshot, createdAt, updatedAt }
|
|
41
|
+
? { ...snapshot, createdAt, updatedAt, ...(lessonRevision ? { lessonRevision } : {}) }
|
|
22
42
|
: null;
|
|
23
43
|
}
|
|
24
44
|
export function normalizeInvestigationTemplate(raw) {
|
|
45
|
+
assertWorkspaceInputGraph(raw, 'Investigation template');
|
|
25
46
|
return template(raw);
|
|
26
47
|
}
|
|
27
48
|
export function normalizeInvestigationTemplateStore(raw) {
|
|
@@ -33,6 +54,9 @@ export function normalizeInvestigationTemplateStore(raw) {
|
|
|
33
54
|
throw new Error(`Investigation-template schema ${String(value.version)} is unsupported; no data was changed.`);
|
|
34
55
|
}
|
|
35
56
|
const source = Array.isArray(raw) ? raw : Array.isArray(value?.templates) ? value.templates : [];
|
|
57
|
+
if (value?.version === 2 && source.some(candidate => record(candidate)?.lessonRevision !== undefined)) {
|
|
58
|
+
throw new Error('Template revision provenance requires schema 3; no data was changed.');
|
|
59
|
+
}
|
|
36
60
|
const templates = [];
|
|
37
61
|
const seen = new Set();
|
|
38
62
|
for (const candidate of source.slice(0, MAX_INVESTIGATION_TEMPLATES * 2)) {
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// deliberately accepts only bounded, normalized page-identity fields. Raw
|
|
3
3
|
// HTML, URLs, headers, redirect inventories, and parser diagnostics never
|
|
4
4
|
// cross this storage boundary.
|
|
5
|
-
import { normalizeDomain } from "../
|
|
6
|
-
import { isInformativePerceptualHash as isInformativeFaviconHash } from "
|
|
5
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
6
|
+
import { isInformativePerceptualHash as isInformativeFaviconHash } from "../analysis/perceptual-hash-comparison.mjs";
|
|
7
7
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
8
8
|
import { PAGE_FINGERPRINT_TOKEN_LIMITS } from "../contracts/page-fingerprints.mjs";
|
|
9
9
|
import { MAX_BASELINE_IDENTIFIERS, MAX_BASELINE_RESOURCE_HOSTS, MAX_BASELINE_TITLE_LENGTH, PAGE_BASELINE_VERSION, PAGE_FINGERPRINT_PARSERS, PAGE_IDENTITY_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { normalizeDomain } from "../
|
|
1
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
2
2
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
|
-
import { RELATIONSHIP_EVIDENCE_VERSION } from "../contracts/offline-comparison.mjs";
|
|
4
|
-
import { RELATIONSHIP_TYPES, qualifyRelationshipSources } from "../comparison/relationship-provenance.mjs";
|
|
3
|
+
import { RELATIONSHIP_EVIDENCE_VERSION, RELATIONSHIP_SOURCE_PROVENANCE_VERSION, RELATIONSHIP_PUBLIC_ADDRESS_VERSION } from "../contracts/offline-comparison.mjs";
|
|
4
|
+
import { RELATIONSHIP_TYPES, qualifyRelationshipSources, isNonPublicAddressRelationship, NON_PUBLIC_RELATIONSHIP_LIMITATION, FAVICON_RELATIONSHIP_DESCRIPTION } from "../comparison/relationship-provenance.mjs";
|
|
5
5
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
6
6
|
import { MAX_RELATIONSHIP_OBSERVATIONS, MAX_RELATIONSHIP_OBSERVATION_DOMAINS, MAX_RELATIONSHIP_OBSERVATION_INPUTS, MAX_RELATIONSHIP_OBSERVATION_LIMITATIONS, MAX_RELATIONSHIP_OBSERVATION_STORE_BYTES, MAX_RELATIONSHIP_OBSERVATION_VALUE_LENGTH, RELATIONSHIP_OBSERVATION_SCHEMA, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
7
7
|
export { MAX_RELATIONSHIP_OBSERVATIONS, MAX_RELATIONSHIP_OBSERVATION_DOMAINS, MAX_RELATIONSHIP_OBSERVATION_INPUTS, MAX_RELATIONSHIP_OBSERVATION_LIMITATIONS, MAX_RELATIONSHIP_OBSERVATION_STORE_BYTES, MAX_RELATIONSHIP_OBSERVATION_VALUE_LENGTH, RELATIONSHIP_OBSERVATION_SCHEMA, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
@@ -34,9 +34,9 @@ const TYPE_METADATA = Object.freeze({
|
|
|
34
34
|
description: 'These pages exposed the same recognised public tracking identifier in bounded static HTML.',
|
|
35
35
|
}),
|
|
36
36
|
favicon: Object.freeze({
|
|
37
|
-
label: '
|
|
37
|
+
label: 'Connected favicon matches',
|
|
38
38
|
method: 'Exact SHA-256 or perceptual dHash distance ≤ 6',
|
|
39
|
-
description:
|
|
39
|
+
description: FAVICON_RELATIONSHIP_DESCRIPTION,
|
|
40
40
|
}),
|
|
41
41
|
official_asset: Object.freeze({
|
|
42
42
|
label: 'Official asset host match',
|
|
@@ -234,16 +234,17 @@ export function normalizeRelationshipObservation(raw, sourceStoreVersion = RELAT
|
|
|
234
234
|
throw new TypeError('This retained relationship uses a newer source-evidence version; no evidence was interpreted.');
|
|
235
235
|
if (sourceStoreVersion === 1 && sourceVersion > 2)
|
|
236
236
|
throw new TypeError('Relationship schema 1 cannot contain newer source evidence; no evidence was interpreted.');
|
|
237
|
-
const qualified = qualifyRelationshipSources(sourceVersion
|
|
237
|
+
const qualified = qualifyRelationshipSources(sourceVersion >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION ? value.sourceEvidence : [], domains, { truncated: value.truncated === true, type });
|
|
238
|
+
const nonPublicAddress = isNonPublicAddressRelationship(type, normalizedValue);
|
|
238
239
|
return {
|
|
239
240
|
id,
|
|
240
241
|
type,
|
|
241
|
-
label: metadata.label,
|
|
242
|
+
label: nonPublicAddress ? 'Shared non-public DNS answer' : metadata.label,
|
|
242
243
|
method: metadata.method,
|
|
243
244
|
normalizedValue,
|
|
244
245
|
displayValue: type === 'favicon' ? '' : normalizedValue,
|
|
245
246
|
domains,
|
|
246
|
-
description: metadata.description,
|
|
247
|
+
description: nonPublicAddress ? NON_PUBLIC_RELATIONSHIP_LIMITATION : metadata.description,
|
|
247
248
|
classification: 'derived',
|
|
248
249
|
source: 'bulk_relationship_analysis',
|
|
249
250
|
sourceVersion,
|
|
@@ -255,7 +256,7 @@ export function normalizeRelationshipObservation(raw, sourceStoreVersion = RELAT
|
|
|
255
256
|
complete: value.complete === true && qualified.complete,
|
|
256
257
|
truncated: qualified.truncated
|
|
257
258
|
|| (Array.isArray(value.domains) && value.domains.length > MAX_RELATIONSHIP_OBSERVATION_DOMAINS),
|
|
258
|
-
limitations: observationLimitations(value.limitations),
|
|
259
|
+
limitations: observationLimitations([...(nonPublicAddress ? [NON_PUBLIC_RELATIONSHIP_LIMITATION] : []), ...(Array.isArray(value.limitations) ? value.limitations : [])]),
|
|
259
260
|
};
|
|
260
261
|
}
|
|
261
262
|
export function normalizeRelationshipObservationStore(raw) {
|
|
@@ -296,7 +297,10 @@ export function createRelationshipObservation(raw, options = {}) {
|
|
|
296
297
|
const sourceVersion = Object.hasOwn(options, 'sourceVersion') ? positiveInteger(options.sourceVersion) : RELATIONSHIP_EVIDENCE_VERSION;
|
|
297
298
|
if (!sourceVersion || sourceVersion > RELATIONSHIP_EVIDENCE_VERSION)
|
|
298
299
|
throw new TypeError('This relationship needs a supported source-evidence version.');
|
|
299
|
-
const
|
|
300
|
+
const nonPublicAddress = isNonPublicAddressRelationship(type, normalizedValue);
|
|
301
|
+
if (nonPublicAddress && sourceVersion >= RELATIONSHIP_PUBLIC_ADDRESS_VERSION)
|
|
302
|
+
throw new TypeError('Non-public DNS answers cannot be retained as new public-infrastructure relationships.');
|
|
303
|
+
const qualified = qualifyRelationshipSources(sourceVersion >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION ? raw.sourceEvidence : [], domains, { truncated: options.truncated === true, type });
|
|
300
304
|
// Older callers can supply their original scan timestamp. It does not
|
|
301
305
|
// qualify the missing source metadata, and an explicit current projection
|
|
302
306
|
// never substitutes that timestamp for unknown contributing-source times.
|
|
@@ -308,12 +312,12 @@ export function createRelationshipObservation(raw, options = {}) {
|
|
|
308
312
|
return {
|
|
309
313
|
id: observationId(canonical),
|
|
310
314
|
type,
|
|
311
|
-
label: metadata.label,
|
|
315
|
+
label: nonPublicAddress ? 'Shared non-public DNS answer' : metadata.label,
|
|
312
316
|
method: metadata.method,
|
|
313
317
|
normalizedValue,
|
|
314
318
|
displayValue: type === 'favicon' ? '' : normalizedValue,
|
|
315
319
|
domains,
|
|
316
|
-
description: metadata.description,
|
|
320
|
+
description: nonPublicAddress ? NON_PUBLIC_RELATIONSHIP_LIMITATION : metadata.description,
|
|
317
321
|
classification: 'derived',
|
|
318
322
|
source: 'bulk_relationship_analysis',
|
|
319
323
|
sourceVersion,
|
|
@@ -323,7 +327,7 @@ export function createRelationshipObservation(raw, options = {}) {
|
|
|
323
327
|
complete: options.complete !== false && qualified.complete,
|
|
324
328
|
truncated: qualified.truncated
|
|
325
329
|
|| (Array.isArray(raw.domains) && raw.domains.length > MAX_RELATIONSHIP_OBSERVATION_DOMAINS),
|
|
326
|
-
limitations: observationLimitations(options.limitations),
|
|
330
|
+
limitations: observationLimitations([...(nonPublicAddress ? [NON_PUBLIC_RELATIONSHIP_LIMITATION] : []), ...(Array.isArray(options.limitations) ? options.limitations : [])]),
|
|
327
331
|
};
|
|
328
332
|
}
|
|
329
333
|
function byteLength(value) {
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// and downloads; this module owns schema migration, record normalization,
|
|
3
3
|
// import merging, and exact serialized-byte accounting.
|
|
4
4
|
import { compactWatchlistResults } from "./watchlist-history.mjs";
|
|
5
|
-
import { normalizeOpportunityModelVersion } from "
|
|
6
|
-
import { normalizeRiskModelVersion } from "
|
|
5
|
+
import { normalizeOpportunityModelVersion } from "../analysis/opportunity-scoring.mjs";
|
|
6
|
+
import { normalizeRiskModelVersion } from "../analysis/risk-scoring.mjs";
|
|
7
7
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
8
8
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
9
9
|
import { MAX_SHORTLIST_ENTRIES, MAX_SHORTLIST_FACTORS, MAX_SHORTLIST_INPUTS, MAX_SHORTLIST_STORE_BYTES, SHORTLIST_SCHEMA, SHORTLIST_SCHEMA_VERSION, SUPPORTED_SHORTLIST_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
@@ -188,14 +188,23 @@ export function mergeShortlistStores(localRaw, importedRaw) {
|
|
|
188
188
|
skipped++;
|
|
189
189
|
continue;
|
|
190
190
|
}
|
|
191
|
-
|
|
191
|
+
const prior = imported.get(record.domain);
|
|
192
|
+
if (prior)
|
|
192
193
|
skipped++;
|
|
193
|
-
|
|
194
|
+
if (!prior || Date.parse(record.savedAt) > Date.parse(prior.savedAt))
|
|
195
|
+
imported.set(record.domain, record);
|
|
194
196
|
}
|
|
195
197
|
let added = 0;
|
|
196
198
|
let updated = 0;
|
|
197
199
|
for (const record of imported.values()) {
|
|
198
|
-
|
|
200
|
+
const existing = byDomain.get(record.domain);
|
|
201
|
+
// A backup must not roll back an observation. Equal-time conflicts retain
|
|
202
|
+
// the local record rather than choosing whichever file was imported last.
|
|
203
|
+
if (existing && Date.parse(record.savedAt) <= Date.parse(existing.savedAt)) {
|
|
204
|
+
skipped++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (existing)
|
|
199
208
|
updated++;
|
|
200
209
|
else if (byDomain.size >= MAX_SHORTLIST_ENTRIES) {
|
|
201
210
|
skipped++;
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// the watchlist entry for rescanning, while history stores compact changes
|
|
3
3
|
// against a last-known baseline so browser-local storage does not grow by one complete
|
|
4
4
|
// result set on every check.
|
|
5
|
-
import { explainRiskScore, normalizeRiskModelVersion } from "
|
|
5
|
+
import { explainRiskScore, normalizeRiskModelVersion } from "../analysis/risk-scoring.mjs";
|
|
6
6
|
import { HTTP_SECURITY_HEADER_TOKENS, normalizeHttpSummary } from "../cases/http-summary.mjs";
|
|
7
|
-
import { normalizeDomain } from "../
|
|
7
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
8
8
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
9
9
|
import { registryDateIso } from "../evidence/registry-dates.mjs";
|
|
10
|
+
import { mergeWebCollectionQuality, normalizeWebCollectionQuality, webCollectionAllowsComparison } from "../evidence/collection-quality.mjs";
|
|
10
11
|
import { MAX_WATCHLIST_CHANGES_PER_EVENT, MAX_WATCHLIST_DOMAINS, MAX_WATCHLIST_HISTORY_DOMAIN_OPTIONS, MAX_WATCHLIST_HISTORY_EVENTS, MAX_WATCHLIST_INPUT_RECORDS, MAX_WATCHLIST_MUTATION_TYPES, MAX_WATCHLIST_NAMESERVERS, } from "../contracts/workspace-portability.mjs";
|
|
11
12
|
export { MAX_WATCHLIST_CHANGES_PER_EVENT, MAX_WATCHLIST_DOMAINS, MAX_WATCHLIST_HISTORY_DOMAIN_OPTIONS, MAX_WATCHLIST_HISTORY_EVENTS, MAX_WATCHLIST_INPUT_RECORDS, MAX_WATCHLIST_MUTATION_TYPES, MAX_WATCHLIST_NAMESERVERS, } from "../contracts/workspace-portability.mjs";
|
|
12
13
|
const MAX_TEXT_LENGTH = 300;
|
|
@@ -145,9 +146,6 @@ function boundedText(value, maximum = MAX_TEXT_LENGTH, allowNull = true) {
|
|
|
145
146
|
const normalized = value.slice(0, maximum * 4).replace(/\s+/g, ' ').trim().slice(0, maximum).trim();
|
|
146
147
|
return normalized || (allowNull ? null : '');
|
|
147
148
|
}
|
|
148
|
-
function isoTimestamp(value, fallback = new Date(0).toISOString()) {
|
|
149
|
-
return normalizeExplicitIsoTimestamp(value) ?? fallback;
|
|
150
|
-
}
|
|
151
149
|
function boundedInteger(value, maximum, fallback = 0) {
|
|
152
150
|
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= maximum
|
|
153
151
|
? Number(value)
|
|
@@ -205,11 +203,14 @@ function compactRecord(value) {
|
|
|
205
203
|
if (!domain)
|
|
206
204
|
return null;
|
|
207
205
|
const scanDepth = inferredScanDepth(record);
|
|
206
|
+
const webCollectionQuality = normalizeWebCollectionQuality(record.webCollectionQuality, scanDepth);
|
|
208
207
|
const httpSummary = normalizeHttpSummary(record) || {};
|
|
209
208
|
const providedRiskScore = typeof record.riskScore === 'number' && Number.isFinite(record.riskScore)
|
|
210
209
|
? Math.max(0, Math.min(100, Math.round(record.riskScore)))
|
|
211
210
|
: null;
|
|
212
|
-
const computedRisk = scanDepth === 'deep' && providedRiskScore === null
|
|
211
|
+
const computedRisk = scanDepth === 'deep' && providedRiskScore === null
|
|
212
|
+
&& (!webCollectionQuality || (record.riskScore === undefined
|
|
213
|
+
&& webCollectionAllowsComparison('riskScore', webCollectionQuality, scanDepth))) ? explainRiskScore(record) : null;
|
|
213
214
|
const riskScore = scanDepth === 'deep' ? (providedRiskScore ?? computedRisk?.score ?? null) : null;
|
|
214
215
|
const riskModelVersion = riskScore === null
|
|
215
216
|
? null
|
|
@@ -219,6 +220,7 @@ function compactRecord(value) {
|
|
|
219
220
|
return {
|
|
220
221
|
domain,
|
|
221
222
|
scanDepth,
|
|
223
|
+
...(webCollectionQuality ? { webCollectionQuality } : {}),
|
|
222
224
|
availability: hasString(AVAILABILITY_VALUES, record.availability) ? record.availability : null,
|
|
223
225
|
registrarName: boundedText(record.registrarName),
|
|
224
226
|
nameservers: normalizeNameservers(record.nameservers),
|
|
@@ -260,6 +262,8 @@ export function compactWatchlistResults(results) {
|
|
|
260
262
|
function isComparable(field, value, record) {
|
|
261
263
|
if (DEEP_FIELDS.has(field) && record.scanDepth !== 'deep')
|
|
262
264
|
return false;
|
|
265
|
+
if (!webCollectionAllowsComparison(field, record.webCollectionQuality, record.scanDepth))
|
|
266
|
+
return false;
|
|
263
267
|
if (field === 'availability')
|
|
264
268
|
return hasString(CONCLUSIVE_AVAILABILITY, value);
|
|
265
269
|
if (field === 'nameservers')
|
|
@@ -358,7 +362,7 @@ export function mergeWatchlistBaseline(baseline, current) {
|
|
|
358
362
|
return current.map((next) => {
|
|
359
363
|
const previous = previousByDomain.get(next.domain);
|
|
360
364
|
const updated = {
|
|
361
|
-
...(previous ||
|
|
365
|
+
...(previous || next),
|
|
362
366
|
domain: next.domain,
|
|
363
367
|
scanDepth: previous?.scanDepth === 'deep' || next.scanDepth === 'deep' ? 'deep' : 'fast',
|
|
364
368
|
};
|
|
@@ -370,9 +374,12 @@ export function mergeWatchlistBaseline(baseline, current) {
|
|
|
370
374
|
// mean "unversioned" to the comparator, but retaining the key makes the
|
|
371
375
|
// stored/exported contract unambiguous and prevents consumers from having
|
|
372
376
|
// to infer why a score was deliberately excluded from comparison.
|
|
373
|
-
if (next.scanDepth === 'deep' && typeof next.riskScore === 'number') {
|
|
377
|
+
if (next.scanDepth === 'deep' && typeof next.riskScore === 'number' && isComparable('riskScore', next.riskScore, next)) {
|
|
374
378
|
updated.riskModelVersion = normalizeRiskModelVersion(next.riskModelVersion);
|
|
375
379
|
}
|
|
380
|
+
const quality = mergeWebCollectionQuality(previous?.webCollectionQuality, next.webCollectionQuality);
|
|
381
|
+
if (quality)
|
|
382
|
+
updated.webCollectionQuality = quality;
|
|
376
383
|
return updated;
|
|
377
384
|
});
|
|
378
385
|
}
|
|
@@ -452,12 +459,13 @@ function normalizeStoredFieldValue(field, value) {
|
|
|
452
459
|
}
|
|
453
460
|
function initialHistoryEvent(entry, baseline) {
|
|
454
461
|
return {
|
|
455
|
-
checkedAt: entry.updatedAt
|
|
462
|
+
checkedAt: entry.updatedAt,
|
|
456
463
|
mode: 'saved',
|
|
457
464
|
resultCount: Array.isArray(entry.results) ? entry.results.length : baseline.length,
|
|
458
465
|
conclusiveCount: baseline.filter((record) => hasString(CONCLUSIVE_AVAILABILITY, record.availability)).length,
|
|
459
466
|
changeCount: 0,
|
|
460
467
|
omittedChanges: 0,
|
|
468
|
+
webComparisonLimitedCount: limitedWebComparisonCount(baseline),
|
|
461
469
|
changes: [],
|
|
462
470
|
};
|
|
463
471
|
}
|
|
@@ -480,7 +488,7 @@ export function normalizeWatchlistEntry(entry) {
|
|
|
480
488
|
: [];
|
|
481
489
|
const rawMode = event.mode;
|
|
482
490
|
return {
|
|
483
|
-
checkedAt:
|
|
491
|
+
checkedAt: normalizeExplicitIsoTimestamp(event.checkedAt),
|
|
484
492
|
mode: typeof rawMode === 'string' && ['fast', 'deep', 'saved'].includes(rawMode)
|
|
485
493
|
? rawMode
|
|
486
494
|
: 'saved',
|
|
@@ -488,12 +496,14 @@ export function normalizeWatchlistEntry(entry) {
|
|
|
488
496
|
conclusiveCount: boundedInteger(event.conclusiveCount, MAX_WATCHLIST_DOMAINS),
|
|
489
497
|
changeCount: boundedInteger(event.changeCount, MAX_CHANGE_COUNT, changes.length),
|
|
490
498
|
omittedChanges: boundedInteger(event.omittedChanges, MAX_CHANGE_COUNT),
|
|
499
|
+
...(event.webComparisonLimitedCount !== undefined
|
|
500
|
+
? { webComparisonLimitedCount: boundedInteger(event.webComparisonLimitedCount, MAX_WATCHLIST_DOMAINS) } : {}),
|
|
491
501
|
changes,
|
|
492
502
|
};
|
|
493
503
|
}).slice(-MAX_WATCHLIST_HISTORY_EVENTS)
|
|
494
504
|
: [];
|
|
495
505
|
const normalized = {
|
|
496
|
-
updatedAt:
|
|
506
|
+
updatedAt: normalizeExplicitIsoTimestamp(input.updatedAt),
|
|
497
507
|
results,
|
|
498
508
|
baseline,
|
|
499
509
|
history,
|
|
@@ -502,13 +512,18 @@ export function normalizeWatchlistEntry(entry) {
|
|
|
502
512
|
normalized.history.push(initialHistoryEvent(normalized, baseline));
|
|
503
513
|
return normalized;
|
|
504
514
|
}
|
|
515
|
+
function limitedWebComparisonCount(current, baseline = []) {
|
|
516
|
+
const previous = new Map(baseline.map(record => [record.domain, record]));
|
|
517
|
+
return current.filter(record => record.scanDepth === 'deep' && (!webCollectionAllowsComparison('riskScore', record.webCollectionQuality, record.scanDepth)
|
|
518
|
+
|| (previous.has(record.domain) && !webCollectionAllowsComparison('riskScore', previous.get(record.domain).webCollectionQuality, previous.get(record.domain).scanDepth)))).length;
|
|
519
|
+
}
|
|
505
520
|
/**
|
|
506
521
|
* @param {object | null} existingEntry
|
|
507
522
|
* @param {object[]} results
|
|
508
523
|
* @param {{ checkedAt?: string, mode?: string, ignoredDomains?: Set<string> }} [options]
|
|
509
524
|
*/
|
|
510
525
|
export function appendWatchlistScan(existingEntry, results, options = {}) {
|
|
511
|
-
const checkedAt =
|
|
526
|
+
const checkedAt = options.checkedAt === undefined ? new Date().toISOString() : normalizeExplicitIsoTimestamp(options.checkedAt);
|
|
512
527
|
const mode = typeof options.mode === 'string' && ['fast', 'deep', 'saved'].includes(options.mode)
|
|
513
528
|
? options.mode
|
|
514
529
|
: 'saved';
|
|
@@ -525,6 +540,7 @@ export function appendWatchlistScan(existingEntry, results, options = {}) {
|
|
|
525
540
|
conclusiveCount: current.filter((record) => hasString(CONCLUSIVE_AVAILABILITY, record.availability)).length,
|
|
526
541
|
changeCount: changes.length,
|
|
527
542
|
omittedChanges: Math.max(0, changes.length - storedChanges.length),
|
|
543
|
+
webComparisonLimitedCount: limitedWebComparisonCount(current, previous?.baseline),
|
|
528
544
|
changes: storedChanges,
|
|
529
545
|
};
|
|
530
546
|
const history = [...(previous?.history || []), event].slice(-MAX_WATCHLIST_HISTORY_EVENTS);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { MAX_WATCHLIST_DOMAINS, normalizeWatchlistEntry } from "./watchlist-history.mjs";
|
|
5
5
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
6
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
7
|
-
import { MAX_WATCHLIST_INPUTS, MAX_WATCHLIST_NAME_LENGTH, MAX_WATCHLISTS, MAX_WATCHLIST_STORE_BYTES, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
7
|
+
import { MAX_WATCHLIST_INPUTS, MAX_WATCHLIST_NAME_LENGTH, MAX_WATCHLISTS, MAX_WATCHLIST_STORE_BYTES, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, WATCHLIST_BROWSER_SUPPORTED_VERSIONS, WATCHLIST_EXPORT_SUPPORTED_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
8
8
|
export { MAX_WATCHLIST_INPUTS, MAX_WATCHLIST_NAME_LENGTH, MAX_WATCHLISTS, MAX_WATCHLIST_STORE_BYTES, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
9
9
|
const BLOCKED_NAMES = new Set(['__proto__', 'prototype', 'constructor']);
|
|
10
10
|
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
@@ -39,12 +39,21 @@ export function watchlistStoreVersion(raw) {
|
|
|
39
39
|
function defineEntry(target, name, entry) {
|
|
40
40
|
Object.defineProperty(target, name, { value: entry, writable: true, enumerable: true, configurable: true });
|
|
41
41
|
}
|
|
42
|
+
function assertHistoricalQuality(entry, version) {
|
|
43
|
+
if (version !== 2)
|
|
44
|
+
return;
|
|
45
|
+
for (const values of [entry.results, entry.baseline]) {
|
|
46
|
+
if (Array.isArray(values) && values.some(value => plainRecord(value)?.webCollectionQuality !== undefined)) {
|
|
47
|
+
throw new TypeError('Web collection quality requires Watchlist schema 3 or later; historical evidence was not reinterpreted.');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
42
51
|
export function normalizeWatchlistStore(raw) {
|
|
43
52
|
assertWorkspaceInputGraph(raw, 'Watchlist store');
|
|
44
53
|
const root = plainRecord(raw);
|
|
45
54
|
if (root?.schema === WATCHLIST_SCHEMA)
|
|
46
55
|
assertWorkspaceDeclaredVersion(raw, 'Watchlist store');
|
|
47
|
-
if (root?.schema === WATCHLIST_SCHEMA && watchlistStoreVersion(root)
|
|
56
|
+
if (root?.schema === WATCHLIST_SCHEMA && !WATCHLIST_BROWSER_SUPPORTED_VERSIONS.includes(Number(watchlistStoreVersion(root)))) {
|
|
48
57
|
throw new Error(`Watchlist schema ${String(root.version)} is unsupported; no data was changed.`);
|
|
49
58
|
}
|
|
50
59
|
const source = watchlistMap(raw);
|
|
@@ -56,6 +65,7 @@ export function normalizeWatchlistStore(raw) {
|
|
|
56
65
|
const entry = plainRecord(rawEntry);
|
|
57
66
|
if (!name || !entry || !Array.isArray(entry.results) || entry.results.length > MAX_WATCHLIST_DOMAINS)
|
|
58
67
|
continue;
|
|
68
|
+
assertHistoricalQuality(entry, root?.schema === WATCHLIST_SCHEMA ? watchlistStoreVersion(root) : null);
|
|
59
69
|
defineEntry(watchlists, name, normalizeWatchlistEntry(entry));
|
|
60
70
|
if (Object.keys(watchlists).length >= MAX_WATCHLISTS)
|
|
61
71
|
break;
|
|
@@ -93,8 +103,8 @@ export function mergeWatchlistStores(localRaw, importedRaw) {
|
|
|
93
103
|
if (importedVersion !== null && importedVersion > WATCHLIST_SCHEMA_VERSION) {
|
|
94
104
|
throw new Error(`This watchlist file uses newer schema ${importedVersion}. Update the app before importing it.`);
|
|
95
105
|
}
|
|
96
|
-
if (importedVersion
|
|
97
|
-
throw new Error(
|
|
106
|
+
if (!WATCHLIST_EXPORT_SUPPORTED_VERSIONS.includes(Number(importedVersion))) {
|
|
107
|
+
throw new Error('Expected a supported WHOISleuth watchlist export.');
|
|
98
108
|
}
|
|
99
109
|
const local = normalizeWatchlistStore(localRaw).watchlists;
|
|
100
110
|
const source = watchlistMap(importedRaw) || {};
|
|
@@ -109,9 +119,11 @@ export function mergeWatchlistStores(localRaw, importedRaw) {
|
|
|
109
119
|
skipped++;
|
|
110
120
|
continue;
|
|
111
121
|
}
|
|
122
|
+
assertHistoricalQuality(entry, importedVersion);
|
|
112
123
|
const normalized = normalizeWatchlistEntry(entry);
|
|
113
124
|
if (Object.prototype.hasOwnProperty.call(local, name)) {
|
|
114
|
-
|
|
125
|
+
const localTime = local[name].updatedAt;
|
|
126
|
+
if (!normalized.updatedAt || !localTime || normalized.updatedAt <= localTime) {
|
|
115
127
|
skipped++;
|
|
116
128
|
continue;
|
|
117
129
|
}
|