@slicedearth/whoisleuth-cli 2.4.0 → 2.6.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 +1 -1
- package/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +67 -9
- package/cli/artifact-validation/lookup-evidence.mjs +18 -4
- package/cli/artifact-validation/signed-review.mjs +61 -64
- package/cli/artifact-verify.mjs +11 -2
- 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 +6 -8
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +99 -6
- package/cli/case-pack.mjs +19 -4
- 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 +330 -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 +7 -9
- 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/domain-feed.mjs +61 -0
- 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/json.mjs +1 -13
- 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 +32 -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 +147 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +3 -4
- package/cli/investigation-package-review.mjs +2 -0
- 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 +220 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +19 -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 +85 -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 +132 -0
- package/cli/review-command-definitions.mjs +279 -0
- package/cli/review-command-runner.mjs +72 -159
- package/cli/risk-calibration.mjs +14 -12
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +73 -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 +4 -4
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +100 -0
- package/cli/workflow-command-definitions.mjs +103 -0
- package/cli/workflow-command-runner.mjs +40 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +322 -4
- package/docs/contextual-reviews.md +178 -0
- package/docs/infrastructure-observations.md +126 -0
- package/docs/offline-intake-context.md +213 -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/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- 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 +35 -15
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +27 -11
- 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 +75 -7
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +45 -25
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +27 -17
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +5 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +13 -4
- 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 +13 -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-child-profile-contract.mjs +28 -11
- 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 +70 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/operation-budget.mjs +4 -0
- 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 +46 -2
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +142 -139
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/technology-evidence-role.mjs +1 -51
- 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/tls-intelligence.mjs +30 -5
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +404 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-chain.mjs +12 -3
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +15 -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 +348 -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-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +151 -0
- package/packages/cases/case-evidence-model.mjs +89 -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-object-coverage.mjs +33 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +124 -36
- 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 +111 -118
- package/packages/cases/case-record-projection.mjs +23 -4
- package/packages/cases/case-record-version-input.mjs +41 -0
- package/packages/cases/case-report.mjs +93 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +159 -1
- package/packages/cases/case-response-model.mjs +16 -1
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +108 -20
- package/packages/cases/case-response-packet-render.mjs +35 -12
- package/packages/cases/case-response-packet.mjs +55 -13
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +60 -8
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +217 -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 +35 -15
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +360 -49
- package/packages/contracts/cli-command-semantics.mjs +5 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +25 -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/external-observation-interchange.mjs +96 -28
- 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/lookup-evidence.mjs +3 -2
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +37 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +144 -28
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +15 -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/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +36 -29
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +36 -0
- package/packages/interchange/managed-indicator-set.mjs +223 -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 +79 -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 +87 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +132 -0
- package/packages/investigation/domain-transition-review.mjs +49 -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/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +20 -0
- package/packages/investigation/investigation-manifest.mjs +35 -9
- package/packages/investigation/link-intake.mjs +209 -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 +235 -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/monitoring/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +231 -0
- package/packages/workspace/brand-profile-model.mjs +48 -4
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +27 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +11 -3
- 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 +24 -7
- package/packages/workspace/watchlist-history.mjs +59 -16
- package/packages/workspace/watchlist-store.mjs +152 -11
- 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,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";
|
|
@@ -69,9 +69,11 @@ export function normalizeShortlistRecord(raw, options = {}) {
|
|
|
69
69
|
const compact = compactWatchlistResults([value])[0];
|
|
70
70
|
if (!compact)
|
|
71
71
|
return null;
|
|
72
|
+
// Shortlist's published schema does not retain the newer local watch evidence.
|
|
73
|
+
const { hasExternalPasswordForm: _localWatchEvidence, ...shortlistCompact } = compact;
|
|
72
74
|
const riskScore = score(value.riskScore);
|
|
73
75
|
return {
|
|
74
|
-
...
|
|
76
|
+
...shortlistCompact,
|
|
75
77
|
availability: typeof compact.availability === 'string' ? compact.availability : 'unknown',
|
|
76
78
|
riskModelVersion: riskScore === null ? null : normalizeRiskModelVersion(value.riskModelVersion),
|
|
77
79
|
riskScore,
|
|
@@ -120,12 +122,13 @@ export function serializeShortlistStore(records) {
|
|
|
120
122
|
return JSON.stringify(assertShortlistStoreBudget(records));
|
|
121
123
|
}
|
|
122
124
|
/** Add, refresh, or remove an explicit bounded selection in one transaction. */
|
|
123
|
-
export function setShortlistSelection(localRaw, selectedRaw, selected, nowIso = new Date().toISOString()) {
|
|
125
|
+
export function setShortlistSelection(localRaw, selectedRaw, selected, nowIso = new Date().toISOString(), existing = 'refresh') {
|
|
124
126
|
const local = normalizeShortlistStore(localRaw).entries;
|
|
125
127
|
const byDomain = new Map(local.map((record) => [record.domain, record]));
|
|
126
128
|
const candidates = Array.isArray(selectedRaw) ? selectedRaw.slice(0, MAX_SHORTLIST_INPUTS) : [];
|
|
127
129
|
let added = 0;
|
|
128
130
|
let updated = 0;
|
|
131
|
+
let retained = 0;
|
|
129
132
|
let removed = 0;
|
|
130
133
|
let skipped = Math.max(0, (Array.isArray(selectedRaw) ? selectedRaw.length : 0) - MAX_SHORTLIST_INPUTS);
|
|
131
134
|
const seen = new Set();
|
|
@@ -137,6 +140,10 @@ export function setShortlistSelection(localRaw, selectedRaw, selected, nowIso =
|
|
|
137
140
|
}
|
|
138
141
|
seen.add(record.domain);
|
|
139
142
|
if (selected) {
|
|
143
|
+
if (existing === 'retain' && byDomain.has(record.domain)) {
|
|
144
|
+
retained += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
140
147
|
if (!byDomain.has(record.domain) && byDomain.size >= MAX_SHORTLIST_ENTRIES) {
|
|
141
148
|
skipped += 1;
|
|
142
149
|
continue;
|
|
@@ -155,6 +162,7 @@ export function setShortlistSelection(localRaw, selectedRaw, selected, nowIso =
|
|
|
155
162
|
entries: assertShortlistStoreBudget([...byDomain.values()]).entries,
|
|
156
163
|
added,
|
|
157
164
|
updated,
|
|
165
|
+
retained,
|
|
158
166
|
removed,
|
|
159
167
|
skipped,
|
|
160
168
|
};
|
|
@@ -188,14 +196,23 @@ export function mergeShortlistStores(localRaw, importedRaw) {
|
|
|
188
196
|
skipped++;
|
|
189
197
|
continue;
|
|
190
198
|
}
|
|
191
|
-
|
|
199
|
+
const prior = imported.get(record.domain);
|
|
200
|
+
if (prior)
|
|
192
201
|
skipped++;
|
|
193
|
-
|
|
202
|
+
if (!prior || Date.parse(record.savedAt) > Date.parse(prior.savedAt))
|
|
203
|
+
imported.set(record.domain, record);
|
|
194
204
|
}
|
|
195
205
|
let added = 0;
|
|
196
206
|
let updated = 0;
|
|
197
207
|
for (const record of imported.values()) {
|
|
198
|
-
|
|
208
|
+
const existing = byDomain.get(record.domain);
|
|
209
|
+
// A backup must not roll back an observation. Equal-time conflicts retain
|
|
210
|
+
// the local record rather than choosing whichever file was imported last.
|
|
211
|
+
if (existing && Date.parse(record.savedAt) <= Date.parse(existing.savedAt)) {
|
|
212
|
+
skipped++;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (existing)
|
|
199
216
|
updated++;
|
|
200
217
|
else if (byDomain.size >= MAX_SHORTLIST_ENTRIES) {
|
|
201
218
|
skipped++;
|
|
@@ -2,12 +2,14 @@
|
|
|
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 {
|
|
10
|
+
import { normalizeWatchDomainMetadata } from "./brand-candidate-workflow.mjs";
|
|
11
|
+
import { mergeWebCollectionQuality, normalizeWebCollectionQuality, webCollectionAllowsComparison } from "../evidence/collection-quality.mjs";
|
|
12
|
+
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, WATCHLIST_RECOVERY_METADATA, } from "../contracts/workspace-portability.mjs";
|
|
11
13
|
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
14
|
const MAX_TEXT_LENGTH = 300;
|
|
13
15
|
const MAX_TITLE_LENGTH = 200;
|
|
@@ -36,6 +38,7 @@ const DEEP_FIELDS = new Set([
|
|
|
36
38
|
'faviconMatch',
|
|
37
39
|
'faviconNearMatch',
|
|
38
40
|
'hasPasswordField',
|
|
41
|
+
'hasExternalPasswordForm',
|
|
39
42
|
'phishingLanguageMatch',
|
|
40
43
|
'reusesOfficialAssets',
|
|
41
44
|
'riskModelVersion',
|
|
@@ -67,6 +70,7 @@ const FIELD_LABELS = {
|
|
|
67
70
|
faviconMatch: 'Official favicon match',
|
|
68
71
|
faviconNearMatch: 'Official favicon near-match',
|
|
69
72
|
hasPasswordField: 'Password form',
|
|
73
|
+
hasExternalPasswordForm: 'External password-form destination',
|
|
70
74
|
phishingLanguageMatch: 'Phishing language',
|
|
71
75
|
reusesOfficialAssets: 'Official asset reuse',
|
|
72
76
|
riskScore: 'Risk score',
|
|
@@ -93,6 +97,7 @@ const HISTORY_CATEGORY_FIELDS = {
|
|
|
93
97
|
'faviconMatch',
|
|
94
98
|
'faviconNearMatch',
|
|
95
99
|
'hasPasswordField',
|
|
100
|
+
'hasExternalPasswordForm',
|
|
96
101
|
'phishingLanguageMatch',
|
|
97
102
|
'reusesOfficialAssets',
|
|
98
103
|
]),
|
|
@@ -145,9 +150,6 @@ function boundedText(value, maximum = MAX_TEXT_LENGTH, allowNull = true) {
|
|
|
145
150
|
const normalized = value.slice(0, maximum * 4).replace(/\s+/g, ' ').trim().slice(0, maximum).trim();
|
|
146
151
|
return normalized || (allowNull ? null : '');
|
|
147
152
|
}
|
|
148
|
-
function isoTimestamp(value, fallback = new Date(0).toISOString()) {
|
|
149
|
-
return normalizeExplicitIsoTimestamp(value) ?? fallback;
|
|
150
|
-
}
|
|
151
153
|
function boundedInteger(value, maximum, fallback = 0) {
|
|
152
154
|
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= maximum
|
|
153
155
|
? Number(value)
|
|
@@ -205,11 +207,14 @@ function compactRecord(value) {
|
|
|
205
207
|
if (!domain)
|
|
206
208
|
return null;
|
|
207
209
|
const scanDepth = inferredScanDepth(record);
|
|
210
|
+
const webCollectionQuality = normalizeWebCollectionQuality(record.webCollectionQuality, scanDepth);
|
|
208
211
|
const httpSummary = normalizeHttpSummary(record) || {};
|
|
209
212
|
const providedRiskScore = typeof record.riskScore === 'number' && Number.isFinite(record.riskScore)
|
|
210
213
|
? Math.max(0, Math.min(100, Math.round(record.riskScore)))
|
|
211
214
|
: null;
|
|
212
|
-
const computedRisk = scanDepth === 'deep' && providedRiskScore === null
|
|
215
|
+
const computedRisk = scanDepth === 'deep' && providedRiskScore === null
|
|
216
|
+
&& (!webCollectionQuality || (record.riskScore === undefined
|
|
217
|
+
&& webCollectionAllowsComparison('riskScore', webCollectionQuality, scanDepth))) ? explainRiskScore(record) : null;
|
|
213
218
|
const riskScore = scanDepth === 'deep' ? (providedRiskScore ?? computedRisk?.score ?? null) : null;
|
|
214
219
|
const riskModelVersion = riskScore === null
|
|
215
220
|
? null
|
|
@@ -219,6 +224,7 @@ function compactRecord(value) {
|
|
|
219
224
|
return {
|
|
220
225
|
domain,
|
|
221
226
|
scanDepth,
|
|
227
|
+
...(webCollectionQuality ? { webCollectionQuality } : {}),
|
|
222
228
|
availability: hasString(AVAILABILITY_VALUES, record.availability) ? record.availability : null,
|
|
223
229
|
registrarName: boundedText(record.registrarName),
|
|
224
230
|
nameservers: normalizeNameservers(record.nameservers),
|
|
@@ -235,6 +241,7 @@ function compactRecord(value) {
|
|
|
235
241
|
faviconMatch: typeof record.faviconMatch === 'boolean' ? record.faviconMatch : null,
|
|
236
242
|
faviconNearMatch: typeof record.faviconNearMatch === 'boolean' ? record.faviconNearMatch : null,
|
|
237
243
|
hasPasswordField: typeof record.hasPasswordField === 'boolean' ? record.hasPasswordField : null,
|
|
244
|
+
hasExternalPasswordForm: typeof record.hasExternalPasswordForm === 'boolean' ? record.hasExternalPasswordForm : null,
|
|
238
245
|
phishingLanguageMatch: boundedText(record.phishingLanguageMatch, MAX_TITLE_LENGTH),
|
|
239
246
|
reusesOfficialAssets: typeof record.reusesOfficialAssets === 'boolean' ? record.reusesOfficialAssets : null,
|
|
240
247
|
riskModelVersion,
|
|
@@ -260,6 +267,8 @@ export function compactWatchlistResults(results) {
|
|
|
260
267
|
function isComparable(field, value, record) {
|
|
261
268
|
if (DEEP_FIELDS.has(field) && record.scanDepth !== 'deep')
|
|
262
269
|
return false;
|
|
270
|
+
if (!webCollectionAllowsComparison(field, record.webCollectionQuality, record.scanDepth))
|
|
271
|
+
return false;
|
|
263
272
|
if (field === 'availability')
|
|
264
273
|
return hasString(CONCLUSIVE_AVAILABILITY, value);
|
|
265
274
|
if (field === 'nameservers')
|
|
@@ -297,7 +306,7 @@ function classifyChange(field, before, after) {
|
|
|
297
306
|
}
|
|
298
307
|
return { kind: 'availability_changed', tone: 'warn' };
|
|
299
308
|
}
|
|
300
|
-
if (['faviconMatch', 'faviconNearMatch', 'hasPasswordField', 'reusesOfficialAssets'].includes(field) && before === false && after === true) {
|
|
309
|
+
if (['faviconMatch', 'faviconNearMatch', 'hasPasswordField', 'hasExternalPasswordForm', 'reusesOfficialAssets'].includes(field) && before === false && after === true) {
|
|
301
310
|
return { kind: 'risk_signal_added', tone: 'danger' };
|
|
302
311
|
}
|
|
303
312
|
if (field === 'phishingLanguageMatch' && !before && after)
|
|
@@ -358,7 +367,7 @@ export function mergeWatchlistBaseline(baseline, current) {
|
|
|
358
367
|
return current.map((next) => {
|
|
359
368
|
const previous = previousByDomain.get(next.domain);
|
|
360
369
|
const updated = {
|
|
361
|
-
...(previous ||
|
|
370
|
+
...(previous || next),
|
|
362
371
|
domain: next.domain,
|
|
363
372
|
scanDepth: previous?.scanDepth === 'deep' || next.scanDepth === 'deep' ? 'deep' : 'fast',
|
|
364
373
|
};
|
|
@@ -370,9 +379,12 @@ export function mergeWatchlistBaseline(baseline, current) {
|
|
|
370
379
|
// mean "unversioned" to the comparator, but retaining the key makes the
|
|
371
380
|
// stored/exported contract unambiguous and prevents consumers from having
|
|
372
381
|
// to infer why a score was deliberately excluded from comparison.
|
|
373
|
-
if (next.scanDepth === 'deep' && typeof next.riskScore === 'number') {
|
|
382
|
+
if (next.scanDepth === 'deep' && typeof next.riskScore === 'number' && isComparable('riskScore', next.riskScore, next)) {
|
|
374
383
|
updated.riskModelVersion = normalizeRiskModelVersion(next.riskModelVersion);
|
|
375
384
|
}
|
|
385
|
+
const quality = mergeWebCollectionQuality(previous?.webCollectionQuality, next.webCollectionQuality);
|
|
386
|
+
if (quality)
|
|
387
|
+
updated.webCollectionQuality = quality;
|
|
376
388
|
return updated;
|
|
377
389
|
});
|
|
378
390
|
}
|
|
@@ -452,17 +464,28 @@ function normalizeStoredFieldValue(field, value) {
|
|
|
452
464
|
}
|
|
453
465
|
function initialHistoryEvent(entry, baseline) {
|
|
454
466
|
return {
|
|
455
|
-
checkedAt: entry.updatedAt
|
|
467
|
+
checkedAt: entry.updatedAt,
|
|
456
468
|
mode: 'saved',
|
|
457
469
|
resultCount: Array.isArray(entry.results) ? entry.results.length : baseline.length,
|
|
458
470
|
conclusiveCount: baseline.filter((record) => hasString(CONCLUSIVE_AVAILABILITY, record.availability)).length,
|
|
459
471
|
changeCount: 0,
|
|
460
472
|
omittedChanges: 0,
|
|
473
|
+
webComparisonLimitedCount: limitedWebComparisonCount(baseline),
|
|
461
474
|
changes: [],
|
|
462
475
|
};
|
|
463
476
|
}
|
|
477
|
+
/** Active membership excludes historical baselines and includes unobserved candidates. */
|
|
478
|
+
export function watchlistActiveDomains(entry) {
|
|
479
|
+
if (entry.membershipRecovery)
|
|
480
|
+
return [];
|
|
481
|
+
return normalizeWatchDomainMetadata(entry.domainMetadata, entry.results.map(record => record.domain)).map(record => record.domain);
|
|
482
|
+
}
|
|
483
|
+
export function assertWatchlistEditable(entry) {
|
|
484
|
+
if (entry?.membershipRecovery)
|
|
485
|
+
throw new Error('This older watchlist is paused for membership recovery. Export its preserved records and create a separate watchlist with at most 2,000 selected domains.');
|
|
486
|
+
}
|
|
464
487
|
/** @param {object} entry */
|
|
465
|
-
export function normalizeWatchlistEntry(entry) {
|
|
488
|
+
export function normalizeWatchlistEntry(entry, options = {}) {
|
|
466
489
|
const input = plainRecord(entry) || {};
|
|
467
490
|
const rawResults = Array.isArray(input.results) ? input.results : [];
|
|
468
491
|
const results = compactWatchlistResults(rawResults);
|
|
@@ -480,7 +503,7 @@ export function normalizeWatchlistEntry(entry) {
|
|
|
480
503
|
: [];
|
|
481
504
|
const rawMode = event.mode;
|
|
482
505
|
return {
|
|
483
|
-
checkedAt:
|
|
506
|
+
checkedAt: normalizeExplicitIsoTimestamp(event.checkedAt),
|
|
484
507
|
mode: typeof rawMode === 'string' && ['fast', 'deep', 'saved'].includes(rawMode)
|
|
485
508
|
? rawMode
|
|
486
509
|
: 'saved',
|
|
@@ -488,31 +511,47 @@ export function normalizeWatchlistEntry(entry) {
|
|
|
488
511
|
conclusiveCount: boundedInteger(event.conclusiveCount, MAX_WATCHLIST_DOMAINS),
|
|
489
512
|
changeCount: boundedInteger(event.changeCount, MAX_CHANGE_COUNT, changes.length),
|
|
490
513
|
omittedChanges: boundedInteger(event.omittedChanges, MAX_CHANGE_COUNT),
|
|
514
|
+
...(event.webComparisonLimitedCount !== undefined
|
|
515
|
+
? { webComparisonLimitedCount: boundedInteger(event.webComparisonLimitedCount, MAX_WATCHLIST_DOMAINS) } : {}),
|
|
491
516
|
changes,
|
|
492
517
|
};
|
|
493
518
|
}).slice(-MAX_WATCHLIST_HISTORY_EVENTS)
|
|
494
519
|
: [];
|
|
520
|
+
const metadata = normalizeWatchDomainMetadata(input.domainMetadata);
|
|
521
|
+
const membership = new Set([...metadata.map(record => record.domain), ...results.map(record => record.domain)]);
|
|
522
|
+
const recovery = membership.size > MAX_WATCHLIST_DOMAINS
|
|
523
|
+
&& (options.recoverLegacyMembership || input.membershipRecovery === 'legacy_overflow');
|
|
524
|
+
if (input.membershipRecovery !== undefined && (!recovery || input.membershipRecovery !== 'legacy_overflow'))
|
|
525
|
+
throw new TypeError('Watchlist membership recovery state is invalid.');
|
|
495
526
|
const normalized = {
|
|
496
|
-
updatedAt:
|
|
527
|
+
updatedAt: normalizeExplicitIsoTimestamp(input.updatedAt),
|
|
497
528
|
results,
|
|
498
529
|
baseline,
|
|
499
530
|
history,
|
|
531
|
+
domainMetadata: recovery ? metadata : normalizeWatchDomainMetadata(metadata, results.map(record => record.domain)),
|
|
532
|
+
...(recovery ? WATCHLIST_RECOVERY_METADATA : {}),
|
|
500
533
|
};
|
|
501
|
-
if (normalized.history.length === 0)
|
|
534
|
+
if (normalized.history.length === 0 && (results.length || baseline.length))
|
|
502
535
|
normalized.history.push(initialHistoryEvent(normalized, baseline));
|
|
503
536
|
return normalized;
|
|
504
537
|
}
|
|
538
|
+
function limitedWebComparisonCount(current, baseline = []) {
|
|
539
|
+
const previous = new Map(baseline.map(record => [record.domain, record]));
|
|
540
|
+
return current.filter(record => record.scanDepth === 'deep' && (!webCollectionAllowsComparison('riskScore', record.webCollectionQuality, record.scanDepth)
|
|
541
|
+
|| (previous.has(record.domain) && !webCollectionAllowsComparison('riskScore', previous.get(record.domain).webCollectionQuality, previous.get(record.domain).scanDepth)))).length;
|
|
542
|
+
}
|
|
505
543
|
/**
|
|
506
544
|
* @param {object | null} existingEntry
|
|
507
545
|
* @param {object[]} results
|
|
508
546
|
* @param {{ checkedAt?: string, mode?: string, ignoredDomains?: Set<string> }} [options]
|
|
509
547
|
*/
|
|
510
548
|
export function appendWatchlistScan(existingEntry, results, options = {}) {
|
|
511
|
-
const checkedAt =
|
|
549
|
+
const checkedAt = options.checkedAt === undefined ? new Date().toISOString() : normalizeExplicitIsoTimestamp(options.checkedAt);
|
|
512
550
|
const mode = typeof options.mode === 'string' && ['fast', 'deep', 'saved'].includes(options.mode)
|
|
513
551
|
? options.mode
|
|
514
552
|
: 'saved';
|
|
515
553
|
const previous = existingEntry ? normalizeWatchlistEntry(existingEntry) : null;
|
|
554
|
+
assertWatchlistEditable(previous);
|
|
516
555
|
const current = compactWatchlistResults(results);
|
|
517
556
|
const changes = previous
|
|
518
557
|
? diffWatchlistBaseline(previous.baseline, current, options.ignoredDomains || new Set())
|
|
@@ -525,6 +564,7 @@ export function appendWatchlistScan(existingEntry, results, options = {}) {
|
|
|
525
564
|
conclusiveCount: current.filter((record) => hasString(CONCLUSIVE_AVAILABILITY, record.availability)).length,
|
|
526
565
|
changeCount: changes.length,
|
|
527
566
|
omittedChanges: Math.max(0, changes.length - storedChanges.length),
|
|
567
|
+
webComparisonLimitedCount: limitedWebComparisonCount(current, previous?.baseline),
|
|
528
568
|
changes: storedChanges,
|
|
529
569
|
};
|
|
530
570
|
const history = [...(previous?.history || []), event].slice(-MAX_WATCHLIST_HISTORY_EVENTS);
|
|
@@ -534,6 +574,7 @@ export function appendWatchlistScan(existingEntry, results, options = {}) {
|
|
|
534
574
|
results: current,
|
|
535
575
|
baseline: mergeWatchlistBaseline(previous?.baseline || [], current),
|
|
536
576
|
history,
|
|
577
|
+
domainMetadata: normalizeWatchDomainMetadata(previous?.domainMetadata, current.map(record => record.domain)),
|
|
537
578
|
},
|
|
538
579
|
changes,
|
|
539
580
|
};
|
|
@@ -578,6 +619,8 @@ export function watchlistHistoryDomains(entry) {
|
|
|
578
619
|
if (record?.domain)
|
|
579
620
|
current.add(record.domain);
|
|
580
621
|
}
|
|
622
|
+
for (const metadata of normalized.domainMetadata)
|
|
623
|
+
current.add(metadata.domain);
|
|
581
624
|
for (const event of normalized.history) {
|
|
582
625
|
for (const change of event.changes) {
|
|
583
626
|
if (change.domain && !current.has(change.domain))
|