@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,7 +1,7 @@
|
|
|
1
1
|
// Pure Brand Profile normalization and storage model. The browser wrapper owns
|
|
2
2
|
// persistence and downloads; this module owns schema migration, semantic field
|
|
3
3
|
// bounds, import merging, and exact serialized-byte accounting.
|
|
4
|
-
import { normalizeDomain } from "../
|
|
4
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
5
5
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
6
|
import { canonicalDomainControlRecords, normalizeDeclaredDomainControlRecordModes, normalizeDomainControlRecordModes } from "../evidence/domain-control-runtime.mjs";
|
|
7
7
|
import { DOMAIN_CONTROL_RECORD_LIST_FIELDS } from "../contracts/domain-control-manifest.mjs";
|
|
@@ -10,7 +10,9 @@ import { MAX_IDENTITY_DIGEST_BYTES, sha256IdentityHex } from "../evidence/record
|
|
|
10
10
|
import { DOMAIN_POSTURE_COMPARISON_VERSION, MAX_POSTURE_CHECKS, MAX_POSTURE_CHECK_RECORDS, MAX_POSTURE_RECORD_LENGTH, normalizeDomainPostureSourceContext, normalizeDomainPostureProfileContext } from "../evidence/domain-posture-context.mjs";
|
|
11
11
|
import { normalizeOpaqueReferenceId } from "../cases/opaque-reference-id.mjs";
|
|
12
12
|
import { normalizePageBaseline } from "./page-baseline.mjs";
|
|
13
|
-
import {
|
|
13
|
+
import { normalizeCandidateObservations, normalizeCandidateExceptions } from "./brand-candidate-workflow.mjs";
|
|
14
|
+
import { normalizeBrandKeywordCampaigns, mergeBrandKeywordCampaigns } from "./brand-keyword-campaign.mjs";
|
|
15
|
+
import { isInformativePerceptualHash as isInformativeFaviconHash } from "../analysis/perceptual-hash-comparison.mjs";
|
|
14
16
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
15
17
|
import { BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, MAX_DESIRED_POSTURE_BASELINES, MAX_DESIRED_POSTURE_CHANGE_WINDOWS, MAX_DESIRED_POSTURE_OBSERVATIONS, MAX_DESIRED_POSTURE_RECORDS, MAX_DESIRED_POSTURE_SUPPRESSIONS, MAX_DKIM_SELECTOR_LENGTH, MAX_DKIM_SELECTORS, MAX_OFFICIAL_CHANNELS, MAX_PROFILES, MAX_PROFILE_DOMAIN_LENGTH, MAX_PROFILE_NAME_LENGTH, MAX_PROFILE_STORE_BYTES, MAX_PROFILE_TEXT_LENGTH, MAX_PROFILE_TLD_LENGTH, MAX_PROFILE_URL_LENGTH, MAX_PROFILE_VALUE_INPUTS, MAX_PROFILE_VALUES, MAX_PROTECTION_ATTESTATIONS, MAX_RIGHTS_REFERENCES, SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
16
18
|
export { BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, MAX_DESIRED_POSTURE_BASELINES, MAX_DESIRED_POSTURE_CHANGE_WINDOWS, MAX_DESIRED_POSTURE_OBSERVATIONS, MAX_DESIRED_POSTURE_RECORDS, MAX_DESIRED_POSTURE_SUPPRESSIONS, MAX_DKIM_SELECTOR_LENGTH, MAX_DKIM_SELECTORS, MAX_OFFICIAL_CHANNELS, MAX_PROFILES, MAX_PROFILE_DOMAIN_LENGTH, MAX_PROFILE_NAME_LENGTH, MAX_PROFILE_STORE_BYTES, MAX_PROFILE_TEXT_LENGTH, MAX_PROFILE_TLD_LENGTH, MAX_PROFILE_URL_LENGTH, MAX_PROFILE_VALUE_INPUTS, MAX_PROFILE_VALUES, MAX_PROTECTION_ATTESTATIONS, MAX_RIGHTS_REFERENCES, SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
@@ -583,6 +585,9 @@ export function normalizeBrandProfile(raw, options = {}) {
|
|
|
583
585
|
approvedPartnerDomains: normalizeProfileDomains(value.approvedPartnerDomains),
|
|
584
586
|
allowlistedDomains: normalizeProfileDomains(value.allowlistedDomains),
|
|
585
587
|
allowlistedRegistrars: normalizeProfileTextValues(value.allowlistedRegistrars),
|
|
588
|
+
candidateObservations: normalizeCandidateObservations(value.candidateObservations),
|
|
589
|
+
candidateExceptions: normalizeCandidateExceptions(value.candidateExceptions),
|
|
590
|
+
keywordCampaigns: existing ? mergeBrandKeywordCampaigns(existing.keywordCampaigns, value.keywordCampaigns) : normalizeBrandKeywordCampaigns(value.keywordCampaigns),
|
|
586
591
|
dkimSelectors,
|
|
587
592
|
retiredDkimSelectors: normalizeDkimSelectors(value.retiredDkimSelectors)
|
|
588
593
|
.filter((selector) => !dkimSelectors.includes(selector)),
|
|
@@ -602,6 +607,9 @@ export function normalizeBrandProfile(raw, options = {}) {
|
|
|
602
607
|
const BRAND_PROFILE_FIELD_PATCH_KEYS = Object.freeze([
|
|
603
608
|
'allowlistedDomains',
|
|
604
609
|
'allowlistedRegistrars',
|
|
610
|
+
'candidateObservations',
|
|
611
|
+
'candidateExceptions',
|
|
612
|
+
'keywordCampaigns',
|
|
605
613
|
'desiredPostureBaselines',
|
|
606
614
|
'protectionAttestations',
|
|
607
615
|
]);
|
|
@@ -638,11 +646,24 @@ export function brandProfileStoreVersion(raw) {
|
|
|
638
646
|
const value = record(raw);
|
|
639
647
|
return typeof value.version === 'number' && Number.isFinite(value.version) && value.version > 0 ? value.version : null;
|
|
640
648
|
}
|
|
649
|
+
function assertHistoricalCandidateFields(raw, version) {
|
|
650
|
+
if (Array.isArray(raw) || version === null || version >= 11)
|
|
651
|
+
return;
|
|
652
|
+
for (const input of profileList(raw).slice(0, MAX_PROFILES * 4)) {
|
|
653
|
+
const profile = record(input);
|
|
654
|
+
if (version < 10 && (Object.hasOwn(profile, 'candidateObservations') || Object.hasOwn(profile, 'candidateExceptions'))) {
|
|
655
|
+
throw new TypeError('Candidate observations and scoped exceptions require Brand Profile schema 10; historical records were not reinterpreted.');
|
|
656
|
+
}
|
|
657
|
+
if (Object.hasOwn(profile, 'keywordCampaigns'))
|
|
658
|
+
throw new TypeError('Keyword campaigns require Brand Profile schema 11; historical records were not reinterpreted.');
|
|
659
|
+
}
|
|
660
|
+
}
|
|
641
661
|
/** Normalize an internal profile collection or current stored envelope. */
|
|
642
662
|
export function normalizeBrandProfileStore(raw) {
|
|
643
663
|
assertWorkspaceInputGraph(raw, 'Brand Profile store', { maximumBytes: MAX_PROFILE_STORE_BYTES });
|
|
644
664
|
assertWorkspaceDeclaredVersion(raw, 'Brand Profile store');
|
|
645
665
|
const sourceVersion = brandProfileStoreVersion(raw);
|
|
666
|
+
assertHistoricalCandidateFields(raw, sourceVersion);
|
|
646
667
|
if (!Array.isArray(raw) && sourceVersion !== null
|
|
647
668
|
&& !SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS.includes(sourceVersion)) {
|
|
648
669
|
throw new Error(`Brand Profile schema ${sourceVersion} is unsupported; no data was changed.`);
|
|
@@ -655,8 +676,8 @@ export function normalizeBrandProfileStore(raw) {
|
|
|
655
676
|
if (!profile)
|
|
656
677
|
continue;
|
|
657
678
|
const previous = byId.get(profile.id);
|
|
658
|
-
|
|
659
|
-
|
|
679
|
+
const keywordCampaigns = mergeBrandKeywordCampaigns(previous?.keywordCampaigns, profile.keywordCampaigns);
|
|
680
|
+
byId.set(profile.id, { ...(previous && profile.updatedAt <= previous.updatedAt ? previous : profile), keywordCampaigns });
|
|
660
681
|
if (byId.size >= MAX_PROFILES)
|
|
661
682
|
break;
|
|
662
683
|
}
|
|
@@ -691,6 +712,7 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
|
691
712
|
throw new Error('Expected a current WHOISleuth Brand Profile export.');
|
|
692
713
|
}
|
|
693
714
|
const importedVersion = brandProfileStoreVersion(importedRaw);
|
|
715
|
+
assertHistoricalCandidateFields(importedRaw, importedVersion);
|
|
694
716
|
if (importedVersion !== null && importedVersion > BRAND_PROFILE_SCHEMA_VERSION) {
|
|
695
717
|
throw new Error(`This Brand Profile file uses newer schema ${importedVersion}. Update the app before importing it.`);
|
|
696
718
|
}
|
|
@@ -727,8 +749,17 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
|
727
749
|
if (rawId && rawName)
|
|
728
750
|
retainIdName(rawId, rawName);
|
|
729
751
|
const existing = rawName ? byName.get(rawName.toLowerCase()) : null;
|
|
752
|
+
if (existing && rawId !== existing.id && ((Array.isArray(value.candidateObservations) && value.candidateObservations.length) || (Array.isArray(value.candidateExceptions) && value.candidateExceptions.length) || (Array.isArray(value.keywordCampaigns) && value.keywordCampaigns.length))) {
|
|
753
|
+
throw new TypeError('Candidate metadata belongs to another Brand identifier with this name. Rename or review its exact Brand identity before importing; no decisions were reassigned.');
|
|
754
|
+
}
|
|
755
|
+
const keywordCampaigns = mergeBrandKeywordCampaigns(existing?.keywordCampaigns, value.keywordCampaigns);
|
|
730
756
|
const incomingUpdatedAt = timestamp(value.updatedAt, null);
|
|
731
757
|
if (existing && (!incomingUpdatedAt || incomingUpdatedAt <= existing.updatedAt)) {
|
|
758
|
+
if (JSON.stringify(keywordCampaigns) !== JSON.stringify(existing.keywordCampaigns ?? [])) {
|
|
759
|
+
byName.set(existing.name.toLowerCase(), { ...existing, keywordCampaigns });
|
|
760
|
+
updated++;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
732
763
|
skipped++;
|
|
733
764
|
continue;
|
|
734
765
|
}
|
|
@@ -742,6 +773,19 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
|
742
773
|
skipped++;
|
|
743
774
|
continue;
|
|
744
775
|
}
|
|
776
|
+
if (existing) {
|
|
777
|
+
profile.keywordCampaigns = keywordCampaigns;
|
|
778
|
+
profile.candidateObservations = normalizeCandidateObservations([...existing.candidateObservations, ...profile.candidateObservations]);
|
|
779
|
+
const exceptions = new Map(existing.candidateExceptions.map(exception => [exception.id, exception]));
|
|
780
|
+
for (const incoming of profile.candidateExceptions) {
|
|
781
|
+
const old = exceptions.get(incoming.id);
|
|
782
|
+
if (old && (old.domain !== incoming.domain || old.ruleKey !== incoming.ruleKey || old.purpose !== incoming.purpose))
|
|
783
|
+
throw new Error('An imported exception identifier changes its exact scope or purpose; no profiles were imported.');
|
|
784
|
+
if (!old || incoming.revision > old.revision)
|
|
785
|
+
exceptions.set(incoming.id, incoming);
|
|
786
|
+
}
|
|
787
|
+
profile.candidateExceptions = normalizeCandidateExceptions([...exceptions.values()]);
|
|
788
|
+
}
|
|
745
789
|
retainIdName(profile.id, profile.name);
|
|
746
790
|
if (existing) {
|
|
747
791
|
byName.set(profile.name.toLowerCase(), profile);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeDomain } from "../
|
|
1
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
2
2
|
import { CASE_DISPOSITIONS as CASE_DISPOSITION_OPTIONS } from "../cases/case-record-contracts.mjs";
|
|
3
3
|
import { BULK_SORT_KEYS, normalizeBulkPresentationSortKey } from "./bulk-sort.mjs";
|
|
4
4
|
import { normalizeBulkResultColumns } from "./bulk-columns.mjs";
|
|
@@ -235,37 +235,38 @@ export function mergeBulkReviewStores(localRaw, importedRaw) {
|
|
|
235
235
|
const rows = new Map(local.rows.map((item) => [item.domain, item]));
|
|
236
236
|
let added = 0;
|
|
237
237
|
let updated = 0;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
updated += 1;
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
238
|
+
const supplied = sourceLists(importedRaw);
|
|
239
|
+
let skipped = supplied.presets.length + supplied.rows.length - imported.presets.length - imported.rows.length;
|
|
240
|
+
let capacitySkipped = 0;
|
|
241
|
+
let bytes = byteLength(JSON.stringify(local));
|
|
242
|
+
if (bytes > MAX_BULK_REVIEW_STORE_BYTES)
|
|
243
|
+
throw new Error('Existing Bulk review preferences exceed the workspace storage limit; no imported changes were applied.');
|
|
244
|
+
function admit(target, key, candidate, maximum) {
|
|
245
|
+
const existing = target.get(key);
|
|
246
|
+
if (existing && candidate.updatedAt <= existing.updatedAt) {
|
|
250
247
|
skipped += 1;
|
|
248
|
+
return;
|
|
251
249
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (!existing) {
|
|
256
|
-
rows.set(candidate.domain, candidate);
|
|
257
|
-
added += 1;
|
|
258
|
-
}
|
|
259
|
-
else if (candidate.updatedAt > existing.updatedAt) {
|
|
260
|
-
rows.set(candidate.domain, candidate);
|
|
261
|
-
updated += 1;
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
250
|
+
const delta = byteLength(JSON.stringify(candidate)) - (existing ? byteLength(JSON.stringify(existing)) : 0)
|
|
251
|
+
+ (!existing && target.size ? 1 : 0);
|
|
252
|
+
if ((!existing && target.size >= maximum) || bytes + delta > MAX_BULK_REVIEW_STORE_BYTES) {
|
|
264
253
|
skipped += 1;
|
|
254
|
+
capacitySkipped += 1;
|
|
255
|
+
return;
|
|
265
256
|
}
|
|
257
|
+
target.set(key, candidate);
|
|
258
|
+
bytes += delta;
|
|
259
|
+
if (existing)
|
|
260
|
+
updated += 1;
|
|
261
|
+
else
|
|
262
|
+
added += 1;
|
|
266
263
|
}
|
|
264
|
+
for (const candidate of imported.presets)
|
|
265
|
+
admit(presets, candidate.id, candidate, MAX_BULK_REVIEW_PRESETS);
|
|
266
|
+
for (const candidate of imported.rows)
|
|
267
|
+
admit(rows, candidate.domain, candidate, MAX_BULK_REVIEW_ROWS);
|
|
267
268
|
return {
|
|
268
|
-
store:
|
|
269
|
+
store: normalizeBulkReviewStore({
|
|
269
270
|
schema: BULK_REVIEW_SCHEMA,
|
|
270
271
|
version: BULK_REVIEW_SCHEMA_VERSION,
|
|
271
272
|
presets: [...presets.values()],
|
|
@@ -274,5 +275,6 @@ export function mergeBulkReviewStores(localRaw, importedRaw) {
|
|
|
274
275
|
added,
|
|
275
276
|
updated,
|
|
276
277
|
skipped,
|
|
278
|
+
reason: capacitySkipped ? `${capacitySkipped} imported Bulk review records did not fit the remaining storage capacity. Existing local records are preserved.` : '',
|
|
277
279
|
};
|
|
278
280
|
}
|
|
@@ -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,
|
|
@@ -335,6 +338,7 @@ export function normalizeBulkSessionResult(value, inheritedProfileContext) {
|
|
|
335
338
|
reusesOfficialAssets: profileClaimsUsable ? nullableBoolean(item.reusesOfficialAssets) : null,
|
|
336
339
|
hasPasswordField: item.hasPasswordField === true,
|
|
337
340
|
hasExternalFormAction: nullableBoolean(item.hasExternalFormAction),
|
|
341
|
+
hasExternalPasswordForm: nullableBoolean(item.hasExternalPasswordForm),
|
|
338
342
|
phishingLanguageMatch: boundedText(item.phishingLanguageMatch, 300) || null,
|
|
339
343
|
idnReferenceMatch: profileClaimsUsable ? nullableBoolean(item.idnReferenceMatch) : null,
|
|
340
344
|
pageBaselineMatch: profileClaimsUsable ? nullableBoolean(item.pageBaselineMatch) : null,
|
|
@@ -381,7 +385,7 @@ export function normalizeBulkSession(value, sourceStoreVersion) {
|
|
|
381
385
|
// Only an explicitly versioned current document may omit identical row
|
|
382
386
|
// context. Bare runtime rows and public schema 4 must carry their own context;
|
|
383
387
|
// an explicit invalid/null row value never falls back to the session.
|
|
384
|
-
const inheritedProfileContext = sourceStoreVersion
|
|
388
|
+
const inheritedProfileContext = sourceStoreVersion !== undefined && sourceStoreVersion >= 5
|
|
385
389
|
&& declaredProfileContext.sourceState !== 'mixed'
|
|
386
390
|
&& rawProfileContext.sourceState === declaredProfileContext.sourceState
|
|
387
391
|
? declaredProfileContext
|
|
@@ -390,8 +394,14 @@ export function normalizeBulkSession(value, sourceStoreVersion) {
|
|
|
390
394
|
const results = [];
|
|
391
395
|
const seen = new Set();
|
|
392
396
|
for (const candidate of Array.isArray(item.results) ? item.results.slice(0, MAX_BULK_SESSION_ROWS * 2) : []) {
|
|
397
|
+
if (sourceStoreVersion !== undefined && sourceStoreVersion < 7 && Object.hasOwn(record(candidate) ?? {}, 'hasExternalPasswordForm')) {
|
|
398
|
+
throw new TypeError('Form attribution requires Bulk schema 7 or later; historical evidence was not reinterpreted.');
|
|
399
|
+
}
|
|
400
|
+
if (sourceStoreVersion !== undefined && sourceStoreVersion < 6 && record(candidate)?.webCollectionQuality !== undefined) {
|
|
401
|
+
throw new TypeError('Web collection quality requires Bulk schema 6 or later; historical evidence was not reinterpreted.');
|
|
402
|
+
}
|
|
393
403
|
const publicLegacyRow = sourceStoreVersion === 4;
|
|
394
|
-
if (publicLegacyRow && record(record(candidate)?.relationship)?.version
|
|
404
|
+
if (publicLegacyRow && Number(record(record(candidate)?.relationship)?.version) >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION) {
|
|
395
405
|
throw new TypeError('Bulk schema 4 cannot contain newer relationship source evidence; no evidence was interpreted.');
|
|
396
406
|
}
|
|
397
407
|
const result = normalizeBulkSessionResult(candidate, inheritedProfileContext);
|
|
@@ -411,7 +421,7 @@ export function normalizeBulkSession(value, sourceStoreVersion) {
|
|
|
411
421
|
if ((state === 'complete') !== (results.length === domains.length))
|
|
412
422
|
return null;
|
|
413
423
|
const profileContext = summarizeBulkProfileContexts(results);
|
|
414
|
-
if (!
|
|
424
|
+
if (!bulkProfileContextsMatch(declaredProfileContext, profileContext))
|
|
415
425
|
return null;
|
|
416
426
|
return {
|
|
417
427
|
id,
|
|
@@ -471,7 +481,7 @@ export function normalizeBulkSessionStore(raw) {
|
|
|
471
481
|
export function bulkSessionStorageValue(session) {
|
|
472
482
|
return {
|
|
473
483
|
...session,
|
|
474
|
-
results: session.results.map(({ profileContext, ...row }) =>
|
|
484
|
+
results: session.results.map(({ profileContext, ...row }) => bulkProfileContextsMatch(profileContext, session.profileContext)
|
|
475
485
|
? row
|
|
476
486
|
: { ...row, profileContext }),
|
|
477
487
|
};
|
|
@@ -558,8 +568,10 @@ function sourceStateMap(value) {
|
|
|
558
568
|
export function areBulkRiskScoresComparable(previous, current) {
|
|
559
569
|
return previous.profileContext.sourceState === 'ready'
|
|
560
570
|
&& current.profileContext.sourceState === 'ready'
|
|
561
|
-
&&
|
|
571
|
+
&& bulkProfileContextsMatch(previous.profileContext, current.profileContext)
|
|
562
572
|
&& previous.scanDepth === current.scanDepth
|
|
573
|
+
&& webCollectionAllowsComparison('riskScore', previous.webCollectionQuality, previous.scanDepth)
|
|
574
|
+
&& webCollectionAllowsComparison('riskScore', current.webCollectionQuality, current.scanDepth)
|
|
563
575
|
&& previous.risk !== null
|
|
564
576
|
&& current.risk !== null
|
|
565
577
|
&& previous.riskModelVersion !== null
|
|
@@ -574,7 +586,9 @@ function resultChanges(previous, current) {
|
|
|
574
586
|
}
|
|
575
587
|
if (previous.registrar !== current.registrar)
|
|
576
588
|
changes.push(`Registrar: ${previous.registrar} → ${current.registrar}`);
|
|
577
|
-
if (previous.activity !== current.activity
|
|
589
|
+
if (previous.activity !== current.activity
|
|
590
|
+
&& webCollectionAllowsComparison('activityStatus', previous.webCollectionQuality, previous.scanDepth)
|
|
591
|
+
&& webCollectionAllowsComparison('activityStatus', current.webCollectionQuality, current.scanDepth))
|
|
578
592
|
changes.push(`Website: ${previous.activity} → ${current.activity}`);
|
|
579
593
|
const previousComparison = previous.comparisonEvidence;
|
|
580
594
|
const currentComparison = current.comparisonEvidence;
|
|
@@ -644,7 +658,7 @@ export function compareBulkSessions(baselineRaw, currentRaw) {
|
|
|
644
658
|
'A source-state change may reflect collection availability rather than a change to the domain.',
|
|
645
659
|
'A missing row means it was not completed in that saved session; it does not establish domain removal.',
|
|
646
660
|
...(riskIncomparable
|
|
647
|
-
? ['Risk deltas are omitted unless both rows retain the same ready Brand Profile provenance, scan depth,
|
|
661
|
+
? ['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
662
|
: []),
|
|
649
663
|
],
|
|
650
664
|
};
|
|
@@ -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";
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
2
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
|
+
import { assertWorkspaceInputGraph } from "./hostile-input.mjs";
|
|
4
|
+
import { MAX_WATCHLISTS, normalizeWatchlistName, normalizeWatchlistStore, assertWatchlistStoreBudget, } from "./watchlist-store.mjs";
|
|
5
|
+
import { MAX_WATCHLIST_DOMAINS } from "../contracts/workspace-portability.mjs";
|
|
6
|
+
import { assertWatchlistEditable } from "./watchlist-history.mjs";
|
|
7
|
+
import { MAX_WATCH_CONTEXTS, WATCH_PRIORITIES, mergeWatchDomainMetadata, normalizeCandidateObservation, candidateMaterialFingerprint, } from "./brand-candidate-workflow.mjs";
|
|
8
|
+
function admitInput(input) {
|
|
9
|
+
assertWorkspaceInputGraph(input, 'Candidate watch handoff');
|
|
10
|
+
if (!normalizeWatchlistName(input.name))
|
|
11
|
+
throw new TypeError('Choose a safe watchlist name containing 1–100 characters.');
|
|
12
|
+
if (!Array.isArray(input.candidates) || input.candidates.length > MAX_WATCHLIST_DOMAINS)
|
|
13
|
+
throw new RangeError(`Select at most ${MAX_WATCHLIST_DOMAINS} candidate domains.`);
|
|
14
|
+
if (input.brandProfileId !== null &&
|
|
15
|
+
(typeof input.brandProfileId !== 'string' ||
|
|
16
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(input.brandProfileId)))
|
|
17
|
+
throw new TypeError('The Brand context identifier is invalid.');
|
|
18
|
+
if (!WATCH_PRIORITIES.some((option) => option.value === input.priority))
|
|
19
|
+
throw new TypeError('Choose an admitted review priority.');
|
|
20
|
+
if (typeof input.reason !== 'string' ||
|
|
21
|
+
/[\u0000-\u001f\u007f]/u.test(input.reason) ||
|
|
22
|
+
input.reason.trim().length > 300 ||
|
|
23
|
+
!input.reason.trim())
|
|
24
|
+
throw new TypeError('Enter a watch reason containing 1–300 characters and no control characters.');
|
|
25
|
+
if (input.reviewDueAt != null && !normalizeExplicitIsoTimestamp(input.reviewDueAt))
|
|
26
|
+
throw new TypeError('Use a valid explicit review time. A review time is not a scan schedule.');
|
|
27
|
+
if (input.replaceExistingContext !== undefined &&
|
|
28
|
+
typeof input.replaceExistingContext !== 'boolean')
|
|
29
|
+
throw new TypeError('Existing-context replacement must be selected explicitly.');
|
|
30
|
+
return {
|
|
31
|
+
...input,
|
|
32
|
+
name: normalizeWatchlistName(input.name),
|
|
33
|
+
reason: input.reason.trim(),
|
|
34
|
+
reviewDueAt: normalizeExplicitIsoTimestamp(input.reviewDueAt),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Pure exact-domain preview. It grants no collection authority and makes no requests. */
|
|
38
|
+
export function planCandidateWatchHandoff(localRaw, rawInput) {
|
|
39
|
+
const input = admitInput(rawInput), local = normalizeWatchlistStore(localRaw).watchlists;
|
|
40
|
+
const name = Object.keys(local).find((name) => name.toLowerCase() === input.name.toLowerCase()) ??
|
|
41
|
+
input.name;
|
|
42
|
+
const entry = local[name], existing = new Map(entry?.domainMetadata.map((record) => [record.domain, record]) ?? []);
|
|
43
|
+
assertWatchlistEditable(entry);
|
|
44
|
+
const seen = new Set(), rows = [];
|
|
45
|
+
let admitted = existing.size;
|
|
46
|
+
for (const raw of input.candidates) {
|
|
47
|
+
const candidate = normalizeCandidateObservation(raw), domain = candidate?.domain ?? normalizeDomain(raw?.domain) ?? '';
|
|
48
|
+
let reason = '';
|
|
49
|
+
if (!candidate || !domain)
|
|
50
|
+
reason = 'Invalid candidate domain or provenance.';
|
|
51
|
+
else if (seen.has(domain))
|
|
52
|
+
reason = 'Repeated selection; the first exact domain is retained.';
|
|
53
|
+
else if (!entry && Object.keys(local).length >= MAX_WATCHLISTS)
|
|
54
|
+
reason = 'Watchlist count capacity is exhausted.';
|
|
55
|
+
else if (!existing.has(domain) && admitted >= MAX_WATCHLIST_DOMAINS)
|
|
56
|
+
reason = 'Domain capacity is exhausted.';
|
|
57
|
+
else if (existing.get(domain)?.contexts.length === MAX_WATCH_CONTEXTS &&
|
|
58
|
+
!existing
|
|
59
|
+
.get(domain)
|
|
60
|
+
?.contexts.some((context) => context.brandProfileId === input.brandProfileId))
|
|
61
|
+
reason = 'This domain has reached its separate-context capacity.';
|
|
62
|
+
seen.add(domain);
|
|
63
|
+
const previousContext = existing
|
|
64
|
+
.get(domain)
|
|
65
|
+
?.contexts.find((context) => context.brandProfileId === input.brandProfileId) ?? null;
|
|
66
|
+
const materialFingerprint = candidate
|
|
67
|
+
? candidateMaterialFingerprint(candidate, input.brandProfileId ?? '')
|
|
68
|
+
: null;
|
|
69
|
+
if (reason)
|
|
70
|
+
rows.push({ domain, state: 'rejected', reason, previousContext, materialFingerprint });
|
|
71
|
+
else {
|
|
72
|
+
const context = existing
|
|
73
|
+
.get(domain)
|
|
74
|
+
?.contexts.find((context) => context.brandProfileId === input.brandProfileId);
|
|
75
|
+
rows.push({
|
|
76
|
+
domain,
|
|
77
|
+
state: existing.has(domain) ? 'existing' : 'add',
|
|
78
|
+
previousContext,
|
|
79
|
+
materialFingerprint,
|
|
80
|
+
reason: context
|
|
81
|
+
? input.replaceExistingContext
|
|
82
|
+
? `Explicitly replace this context (${context.priority}) with ${input.priority}; preserve evidence and other Brand contexts.`
|
|
83
|
+
: `Preserve existing deliberate ${context.priority} priority and reason; merge provenance only.`
|
|
84
|
+
: existing.has(domain)
|
|
85
|
+
? 'Already tracked; add this separate Brand context, preserving other contexts.'
|
|
86
|
+
: 'Add metadata only; no scan or baseline is invented.',
|
|
87
|
+
});
|
|
88
|
+
if (!existing.has(domain))
|
|
89
|
+
admitted++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
destination: name,
|
|
94
|
+
brandProfileId: input.brandProfileId,
|
|
95
|
+
priority: input.priority,
|
|
96
|
+
reason: input.reason,
|
|
97
|
+
reviewDueAt: input.reviewDueAt ?? null,
|
|
98
|
+
replaceExistingContext: input.replaceExistingContext ?? false,
|
|
99
|
+
rows,
|
|
100
|
+
additionalRequests: 0,
|
|
101
|
+
collectionAuthorised: false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function applyCandidateWatchHandoff(localRaw, rawInput, nowRaw) {
|
|
105
|
+
const input = admitInput(rawInput), now = normalizeExplicitIsoTimestamp(nowRaw);
|
|
106
|
+
if (!now)
|
|
107
|
+
throw new TypeError('The watch metadata change time must use an explicit timezone.');
|
|
108
|
+
if (input.reviewDueAt && input.reviewDueAt <= now)
|
|
109
|
+
throw new TypeError('The review time must be later than this change time.');
|
|
110
|
+
const local = normalizeWatchlistStore(localRaw).watchlists, plan = planCandidateWatchHandoff(local, input);
|
|
111
|
+
const candidates = new Map();
|
|
112
|
+
for (const raw of input.candidates) {
|
|
113
|
+
const candidate = normalizeCandidateObservation(raw);
|
|
114
|
+
if (candidate && !candidates.has(candidate.domain))
|
|
115
|
+
candidates.set(candidate.domain, candidate);
|
|
116
|
+
}
|
|
117
|
+
const context = {
|
|
118
|
+
brandProfileId: input.brandProfileId,
|
|
119
|
+
priority: input.priority,
|
|
120
|
+
reason: input.reason,
|
|
121
|
+
changedAt: now,
|
|
122
|
+
reviewDueAt: input.reviewDueAt ?? null,
|
|
123
|
+
};
|
|
124
|
+
const entry = local[plan.destination] ?? {
|
|
125
|
+
updatedAt: null,
|
|
126
|
+
results: [],
|
|
127
|
+
baseline: [],
|
|
128
|
+
history: [],
|
|
129
|
+
domainMetadata: [],
|
|
130
|
+
};
|
|
131
|
+
if (input.replaceExistingContext &&
|
|
132
|
+
plan.rows.some((row) => row.state !== 'rejected' &&
|
|
133
|
+
row.previousContext?.changedAt &&
|
|
134
|
+
now <= row.previousContext.changedAt))
|
|
135
|
+
throw new Error('The change clock does not follow the retained context. Review the clock before replacing a manual priority.');
|
|
136
|
+
const metadata = plan.rows
|
|
137
|
+
.filter((row) => row.state !== 'rejected')
|
|
138
|
+
.map((row) => ({
|
|
139
|
+
domain: row.domain,
|
|
140
|
+
contexts: !input.replaceExistingContext &&
|
|
141
|
+
entry.domainMetadata.some((record) => record.domain === row.domain &&
|
|
142
|
+
record.contexts.some((old) => old.brandProfileId === input.brandProfileId))
|
|
143
|
+
? []
|
|
144
|
+
: [context],
|
|
145
|
+
candidate: candidates.get(row.domain) ?? null,
|
|
146
|
+
}));
|
|
147
|
+
if (!metadata.length)
|
|
148
|
+
return { watchlists: local, plan };
|
|
149
|
+
Object.defineProperty(local, plan.destination, {
|
|
150
|
+
value: { ...entry, domainMetadata: mergeWatchDomainMetadata(entry.domainMetadata, metadata) },
|
|
151
|
+
enumerable: true,
|
|
152
|
+
configurable: true,
|
|
153
|
+
writable: true,
|
|
154
|
+
});
|
|
155
|
+
return { watchlists: assertWatchlistStoreBudget(local).watchlists, plan };
|
|
156
|
+
}
|
|
157
|
+
/** Field-scoped optimistic edits preserve unrelated domains and contexts. */
|
|
158
|
+
export function setWatchDomainContext(localRaw, name, domainRaw, input, expected) {
|
|
159
|
+
return setWatchDomainContexts(localRaw, name, [{ domain: domainRaw, input, expected }]);
|
|
160
|
+
}
|
|
161
|
+
export function setWatchDomainContexts(localRaw, name, edits) {
|
|
162
|
+
if (!Array.isArray(edits) || edits.length > MAX_WATCHLIST_DOMAINS)
|
|
163
|
+
throw new RangeError('The reviewed metadata edit exceeds its domain bound.');
|
|
164
|
+
const local = normalizeWatchlistStore(localRaw).watchlists, entry = local[name];
|
|
165
|
+
if (!entry)
|
|
166
|
+
throw new Error('The selected watchlist was deleted; nothing was overwritten.');
|
|
167
|
+
assertWatchlistEditable(entry);
|
|
168
|
+
const metadata = new Map(entry.domainMetadata.map((record) => [record.domain, record])), seen = new Set();
|
|
169
|
+
for (const edit of edits) {
|
|
170
|
+
const domain = normalizeDomain(edit.domain), record = domain ? metadata.get(domain) : null;
|
|
171
|
+
const key = `${domain}\0${edit.input.brandProfileId ?? ''}`, old = record?.contexts.find((context) => context.brandProfileId === edit.input.brandProfileId) ??
|
|
172
|
+
null;
|
|
173
|
+
if (!domain ||
|
|
174
|
+
!record ||
|
|
175
|
+
seen.has(key) ||
|
|
176
|
+
JSON.stringify(old) !== JSON.stringify(edit.expected))
|
|
177
|
+
throw new Error('A reviewed domain context changed, was repeated or was deleted. Reopen the selection; nothing was overwritten.');
|
|
178
|
+
const input = admitInput({ name, candidates: [], ...edit.input });
|
|
179
|
+
const changedAt = normalizeExplicitIsoTimestamp(edit.input.changedAt);
|
|
180
|
+
if (!changedAt ||
|
|
181
|
+
(old?.changedAt && changedAt <= old.changedAt) ||
|
|
182
|
+
(input.reviewDueAt && input.reviewDueAt <= changedAt))
|
|
183
|
+
throw new TypeError('Use a current change time and a later optional review time.');
|
|
184
|
+
seen.add(key);
|
|
185
|
+
metadata.set(domain, {
|
|
186
|
+
...record,
|
|
187
|
+
contexts: [
|
|
188
|
+
...record.contexts.filter((context) => context.brandProfileId !== edit.input.brandProfileId),
|
|
189
|
+
{ ...edit.input, reason: input.reason, changedAt, reviewDueAt: input.reviewDueAt ?? null },
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
local[name] = { ...entry, domainMetadata: [...metadata.values()] };
|
|
194
|
+
return assertWatchlistStoreBudget(local).watchlists;
|
|
195
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
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
|
-
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";
|
|
7
|
+
import { DETECTION_RULE_SCHEMA, DETECTION_RULE_SCHEMA_VERSION, DETECTION_RULE_EXPORT_SUPPORTED_VERSIONS, 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";
|
|
8
8
|
export { DETECTION_RULE_SCHEMA, DETECTION_RULE_SCHEMA_VERSION, MAX_CONDITION_VALUE_LENGTH, MAX_CUSTOM_RISK_TOTAL, MAX_DETECTION_RULES, MAX_RULE_CONDITIONS, MAX_RULE_IMPORT_BYTES, 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";
|
|
9
9
|
const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
10
10
|
const TEXT_CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
@@ -24,6 +24,7 @@ export const RULE_FIELD_DEFINITIONS = Object.freeze([
|
|
|
24
24
|
{ value: 'reusesOfficialAssets', label: 'Official assets reused', kind: 'boolean' },
|
|
25
25
|
{ value: 'phishingLanguageMatch', label: 'Phishing-language signal', kind: 'text' },
|
|
26
26
|
{ value: 'hasExternalFormAction', label: 'External form action observed', kind: 'boolean' },
|
|
27
|
+
{ value: 'hasExternalPasswordForm', label: 'Password form declares external destination', kind: 'boolean' },
|
|
27
28
|
{ value: 'mutationTypes', label: 'Mutation type', kind: 'list' },
|
|
28
29
|
{ value: 'nameservers', label: 'Nameserver', kind: 'list' },
|
|
29
30
|
{ value: 'httpSecurityHeaders', label: 'HTTP security header', kind: 'list' },
|
|
@@ -164,8 +165,12 @@ export function recoverDetectionRuleStore(raw) {
|
|
|
164
165
|
assertWorkspaceDeclaredVersion(raw, 'Detection-rule store');
|
|
165
166
|
const byId = new Map();
|
|
166
167
|
const source = ruleList(raw);
|
|
168
|
+
const sourceVersion = detectionRuleStoreVersion(raw);
|
|
167
169
|
let rejected = Math.max(0, source.length - MAX_RULE_INPUT_RECORDS);
|
|
168
170
|
for (const item of source.slice(0, MAX_RULE_INPUT_RECORDS)) {
|
|
171
|
+
if (sourceVersion === 1 && Array.isArray(record(item).conditions) && record(item).conditions.some(condition => record(condition).field === 'hasExternalPasswordForm')) {
|
|
172
|
+
throw new Error('Form-attribution conditions require custom-rule schema 2; historical rules were not reinterpreted.');
|
|
173
|
+
}
|
|
169
174
|
const rule = normalizeDetectionRule(item);
|
|
170
175
|
if (!rule?.id || byId.has(rule.id)) {
|
|
171
176
|
rejected += 1;
|
|
@@ -326,12 +331,15 @@ export function mergeDetectionRules(localRaw, importedRaw) {
|
|
|
326
331
|
if (version !== null && version > DETECTION_RULE_SCHEMA_VERSION) {
|
|
327
332
|
throw new Error(`This custom-rule file uses newer schema ${version}. Update the app before importing it.`);
|
|
328
333
|
}
|
|
329
|
-
if (version
|
|
334
|
+
if (version === null || !DETECTION_RULE_EXPORT_SUPPORTED_VERSIONS.includes(version)) {
|
|
330
335
|
throw new Error(`Expected custom-rule schema ${DETECTION_RULE_SCHEMA_VERSION}.`);
|
|
331
336
|
}
|
|
332
337
|
const local = normalizeDetectionRuleStore(localRaw).rules;
|
|
333
338
|
const byId = new Map(local.map((rule) => [rule.id, rule]));
|
|
334
339
|
const importedList = ruleList(importedRaw);
|
|
340
|
+
if (version === 1 && importedList.some(item => Array.isArray(record(item).conditions) && record(item).conditions.some(condition => record(condition).field === 'hasExternalPasswordForm'))) {
|
|
341
|
+
throw new Error('Form-attribution conditions require custom-rule schema 2; historical rules were not reinterpreted.');
|
|
342
|
+
}
|
|
335
343
|
let added = 0;
|
|
336
344
|
let updated = 0;
|
|
337
345
|
let skipped = Math.max(0, importedList.length - MAX_RULE_INPUT_RECORDS);
|
|
@@ -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) {
|