@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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Typed analyst metadata; ordinary tags and assertions are always literal.
|
|
2
|
+
import { MAX_CASE_ASSERTIONS, MAX_RESPONSE_RATIONALE_LENGTH, MAX_CASE_OBJECTIVE_LENGTH } from "../contracts/case-portability.mjs";
|
|
3
|
+
import { caseInvestigationContext, prepareCaseInvestigationContext, parseIncidentUrlContext, normalizeCaseObjective } from "./case-incident-context.mjs";
|
|
4
|
+
import { safeId, makeId } from "./case-record-core.mjs";
|
|
5
|
+
import { array, enumeration, exact, iso, text } from "../evidence/artifact-structure.mjs";
|
|
6
|
+
import { assertWorkspaceInputGraph } from "../workspace/hostile-input.mjs";
|
|
7
|
+
// Preserve the maximum URL previously admitted inside a 21-character label.
|
|
8
|
+
export const MAX_CASE_INCIDENT_TARGET_URL_LENGTH = MAX_RESPONSE_RATIONALE_LENGTH - 21;
|
|
9
|
+
export const MAX_CASE_INCIDENT_TARGETS = 20;
|
|
10
|
+
export const MAX_CASE_INCIDENT_TARGET_HISTORY = MAX_CASE_ASSERTIONS;
|
|
11
|
+
export const CASE_TYPES = Object.freeze([
|
|
12
|
+
Object.freeze({ id: 'phishing', label: 'Phishing', description: 'Deceptive content or messages intended to obtain credentials or other sensitive information.' }),
|
|
13
|
+
Object.freeze({ id: 'impersonation', label: 'Impersonation', description: 'A domain, account or page presenting itself as another person or organisation.' }),
|
|
14
|
+
Object.freeze({ id: 'lookalike_cybersquatting', label: 'Lookalike or cybersquatting', description: 'A domain or account selected for review because it resembles or may exploit another name, brand or identifier.' }),
|
|
15
|
+
Object.freeze({ id: 'trademark_infringement', label: 'Trademark infringement', description: 'Potential unauthorised trademark use that may cause confusion about source or affiliation.' }),
|
|
16
|
+
Object.freeze({ id: 'copyright_infringement', label: 'Copyright infringement', description: 'Potential unauthorised copying or distribution of protected material.' }),
|
|
17
|
+
Object.freeze({ id: 'counterfeit_goods', label: 'Counterfeit goods', description: 'Promotion or sale of goods presented as genuine branded products.' }),
|
|
18
|
+
Object.freeze({ id: 'scam_fraud', label: 'Scam or fraud', description: 'Deceptive activity intended to obtain money, property or another benefit.' }),
|
|
19
|
+
Object.freeze({ id: 'malware_distribution', label: 'Malware distribution', description: 'Delivery, hosting or promotion of malicious software.' }),
|
|
20
|
+
Object.freeze({ id: 'credential_theft', label: 'Credential theft', description: 'Collection, sale or misuse of account credentials.' }),
|
|
21
|
+
Object.freeze({ id: 'spam_platform_abuse', label: 'Spam or platform abuse', description: 'Unsolicited or abusive platform activity that does not fit a narrower type.' }),
|
|
22
|
+
Object.freeze({ id: 'privacy_personal_data', label: 'Privacy or personal data', description: 'Potential exposure or misuse of personal information.' }),
|
|
23
|
+
Object.freeze({ id: 'account_compromise', label: 'Account compromise', description: 'Suspected unauthorised control or use of an account.' }),
|
|
24
|
+
Object.freeze({ id: 'other', label: 'Other', description: 'A reviewed type not covered by the current taxonomy.' }),
|
|
25
|
+
]);
|
|
26
|
+
const CASE_TYPE_IDS = new Set(CASE_TYPES.map((item) => item.id));
|
|
27
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
28
|
+
const CASE_REFERENCE_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
29
|
+
export function readCaseTypes(value) {
|
|
30
|
+
assertWorkspaceInputGraph(value, 'Case types');
|
|
31
|
+
const selected = array(value, 'Case types', CASE_TYPES.length).map(id => {
|
|
32
|
+
if (typeof id !== 'string' || !CASE_TYPE_IDS.has(id))
|
|
33
|
+
throw new TypeError('Unknown Case type.');
|
|
34
|
+
return id;
|
|
35
|
+
});
|
|
36
|
+
if (new Set(selected).size !== selected.length)
|
|
37
|
+
throw new TypeError('Case types must be unique.');
|
|
38
|
+
return CASE_TYPES.map(item => item.id).filter(id => selected.includes(id));
|
|
39
|
+
}
|
|
40
|
+
export function caseTypeIds(record) {
|
|
41
|
+
return [...(record.workflowMetadata?.types ?? [])];
|
|
42
|
+
}
|
|
43
|
+
export function caseTypeRecords(record) {
|
|
44
|
+
const selected = new Set(caseTypeIds(record));
|
|
45
|
+
return CASE_TYPES.filter((item) => selected.has(item.id));
|
|
46
|
+
}
|
|
47
|
+
export function caseTypeSummary(record) {
|
|
48
|
+
const labels = caseTypeRecords(record).map((item) => item.label);
|
|
49
|
+
if (!labels.length)
|
|
50
|
+
return '';
|
|
51
|
+
if (labels.length <= 2)
|
|
52
|
+
return labels.join(' and ');
|
|
53
|
+
return `${labels.slice(0, 2).join(', ')} and ${labels.length - 2} more`;
|
|
54
|
+
}
|
|
55
|
+
function encodeCaseReference(value, minimumLength) {
|
|
56
|
+
let remainder = value;
|
|
57
|
+
let encoded = '';
|
|
58
|
+
do {
|
|
59
|
+
encoded = CASE_REFERENCE_ALPHABET[Number(remainder % 32n)] + encoded;
|
|
60
|
+
remainder /= 32n;
|
|
61
|
+
} while (remainder > 0n);
|
|
62
|
+
return encoded.padStart(minimumLength, '0');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Returns an unambiguous reference derived injectively from the immutable Case
|
|
66
|
+
* id. UUIDs use their complete 128-bit value. Legacy ids use every UTF-8 byte,
|
|
67
|
+
* its encoded length and a distinct namespace, so no shared counter or server
|
|
68
|
+
* is needed and imports retain the same reference.
|
|
69
|
+
*/
|
|
70
|
+
export function caseNumber(caseId) {
|
|
71
|
+
const id = typeof caseId === 'string' && caseId ? caseId : 'unknown';
|
|
72
|
+
if (UUID_RE.test(id)) {
|
|
73
|
+
return `WS-${encodeCaseReference(BigInt(`0x${id.replaceAll('-', '')}`), 26)}`;
|
|
74
|
+
}
|
|
75
|
+
const bytes = new TextEncoder().encode(id);
|
|
76
|
+
let encoded = 0n;
|
|
77
|
+
for (const byte of bytes)
|
|
78
|
+
encoded = encoded * 256n + BigInt(byte);
|
|
79
|
+
return `WS-L${encodeCaseReference(BigInt(bytes.length), 2)}-${encodeCaseReference(encoded, Math.ceil(bytes.length * 8 / 5))}`;
|
|
80
|
+
}
|
|
81
|
+
export function formattedCaseNumber(caseId) {
|
|
82
|
+
const value = caseNumber(caseId);
|
|
83
|
+
if (value.startsWith('WS-L'))
|
|
84
|
+
return value;
|
|
85
|
+
const separator = value.indexOf('-');
|
|
86
|
+
const prefix = value.slice(0, separator + 1);
|
|
87
|
+
const body = value.slice(separator + 1);
|
|
88
|
+
return `${prefix}${body.match(/.{1,5}/gu)?.join('-') ?? body}`;
|
|
89
|
+
}
|
|
90
|
+
export function normalizeCaseIncidentTargetUrl(value) {
|
|
91
|
+
if (typeof value !== 'string' || value.length > MAX_CASE_INCIDENT_TARGET_URL_LENGTH || !value.trim())
|
|
92
|
+
return null;
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = new URL(value.trim());
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname || parsed.username || parsed.password)
|
|
101
|
+
return null;
|
|
102
|
+
const normalized = parsed.toString();
|
|
103
|
+
return normalized.length <= MAX_CASE_INCIDENT_TARGET_URL_LENGTH ? normalized : null;
|
|
104
|
+
}
|
|
105
|
+
export { buildCaseIncidentCoverage } from "./case-object-coverage.mjs";
|
|
106
|
+
export function emptyCaseWorkflowMetadata() {
|
|
107
|
+
return { types: [], incidentTargets: [], investigationContext: null };
|
|
108
|
+
}
|
|
109
|
+
function identifier(value) {
|
|
110
|
+
const id = safeId(value);
|
|
111
|
+
if (!id || id !== value)
|
|
112
|
+
throw new TypeError('Case metadata identity is invalid.');
|
|
113
|
+
return id;
|
|
114
|
+
}
|
|
115
|
+
/** Strict admission before persistence; no inferred defaults for typed fields. */
|
|
116
|
+
export function readCaseWorkflowMetadata(value, domain) {
|
|
117
|
+
if (value === undefined)
|
|
118
|
+
return undefined;
|
|
119
|
+
assertWorkspaceInputGraph(value, 'Case workflow metadata');
|
|
120
|
+
const row = exact(value, ['types', 'incidentTargets', 'investigationContext'], 'Case workflow metadata');
|
|
121
|
+
const types = readCaseTypes(row.types);
|
|
122
|
+
const incidentTargets = array(row.incidentTargets, 'Incident target history', MAX_CASE_INCIDENT_TARGET_HISTORY).map(raw => {
|
|
123
|
+
const item = exact(raw, ['id', 'url', 'state', 'createdAt', 'updatedAt'], 'Incident target');
|
|
124
|
+
const url = normalizeCaseIncidentTargetUrl(item.url);
|
|
125
|
+
if (!url || url !== item.url)
|
|
126
|
+
throw new TypeError('Incident target URL is invalid.');
|
|
127
|
+
iso(item.createdAt, 'Incident target creation time');
|
|
128
|
+
iso(item.updatedAt, 'Incident target update time');
|
|
129
|
+
if (Date.parse(item.updatedAt) < Date.parse(item.createdAt))
|
|
130
|
+
throw new TypeError('Incident target update precedes creation.');
|
|
131
|
+
return { id: identifier(item.id), url, state: enumeration(item.state, ['open', 'resolved'], 'Incident target state'),
|
|
132
|
+
createdAt: item.createdAt, updatedAt: item.updatedAt };
|
|
133
|
+
});
|
|
134
|
+
if (new Set(incidentTargets.map(item => item.id)).size !== incidentTargets.length)
|
|
135
|
+
throw new TypeError('Incident target identities must be unique.');
|
|
136
|
+
let investigationContext = null;
|
|
137
|
+
if (row.investigationContext !== null) {
|
|
138
|
+
const item = exact(row.investigationContext, ['id', 'objective', 'incidentUrl', 'urlRetention', 'updatedAt'], 'Incident context');
|
|
139
|
+
const objective = text(item.objective, 'Investigation objective', MAX_CASE_OBJECTIVE_LENGTH);
|
|
140
|
+
if (!objective || normalizeCaseObjective(objective) !== objective)
|
|
141
|
+
throw new TypeError('Investigation objective is invalid.');
|
|
142
|
+
const parsed = parseIncidentUrlContext(item.incidentUrl);
|
|
143
|
+
const urlRetention = enumeration(item.urlRetention, ['exact', 'origin_only'], 'Incident URL retention');
|
|
144
|
+
if (!parsed || parsed.registrableDomain !== domain
|
|
145
|
+
|| item.incidentUrl !== (urlRetention === 'exact' ? parsed.exactUrl : parsed.originUrl)) {
|
|
146
|
+
throw new TypeError('Incident context URL does not match its Case domain or retention choice.');
|
|
147
|
+
}
|
|
148
|
+
iso(item.updatedAt, 'Incident context update time');
|
|
149
|
+
investigationContext = { id: identifier(item.id), objective, incidentUrl: item.incidentUrl,
|
|
150
|
+
urlRetention, updatedAt: item.updatedAt };
|
|
151
|
+
}
|
|
152
|
+
return { types, incidentTargets, investigationContext };
|
|
153
|
+
}
|
|
154
|
+
/** Mutations use the existing Case save coordinator and compare-and-swap rules. */
|
|
155
|
+
export function updateCaseWorkflowMetadata(current, patch, domain, now) {
|
|
156
|
+
const next = structuredClone(current);
|
|
157
|
+
if (patch.caseTypes !== undefined) {
|
|
158
|
+
if (patch.expectedCaseTypes !== undefined && JSON.stringify(readCaseTypes(patch.expectedCaseTypes)) !== JSON.stringify(current.types)) {
|
|
159
|
+
throw new Error('The Case types changed after this edit was started. Reopen the Case before saving; your draft was not applied.');
|
|
160
|
+
}
|
|
161
|
+
next.types = readCaseTypes(patch.caseTypes);
|
|
162
|
+
}
|
|
163
|
+
if (patch.incidentTarget !== undefined) {
|
|
164
|
+
const url = normalizeCaseIncidentTargetUrl(patch.incidentTarget);
|
|
165
|
+
if (!url)
|
|
166
|
+
throw new TypeError('Enter an exact HTTP(S) incident URL without embedded credentials.');
|
|
167
|
+
if (next.incidentTargets.some(target => target.state === 'open' && target.url === url))
|
|
168
|
+
throw new Error('That exact incident URL is already active in this Case.');
|
|
169
|
+
if (next.incidentTargets.filter(target => target.state === 'open').length >= MAX_CASE_INCIDENT_TARGETS)
|
|
170
|
+
throw new Error(`A Case can retain at most ${MAX_CASE_INCIDENT_TARGETS} active incident links. Resolve one before adding another.`);
|
|
171
|
+
if (next.incidentTargets.length >= MAX_CASE_INCIDENT_TARGET_HISTORY)
|
|
172
|
+
throw new Error(`The retained incident link history is full (${MAX_CASE_INCIDENT_TARGET_HISTORY}). Export this Case and open another; no history was removed.`);
|
|
173
|
+
next.incidentTargets.push({ id: makeId(), url, state: 'open', createdAt: now, updatedAt: now });
|
|
174
|
+
}
|
|
175
|
+
if (patch.incidentTargetResolution !== undefined) {
|
|
176
|
+
const id = identifier(patch.incidentTargetResolution);
|
|
177
|
+
const target = next.incidentTargets.find(item => item.id === id);
|
|
178
|
+
if (!target || target.state !== 'open')
|
|
179
|
+
throw new Error('That incident target is missing or already resolved. Refresh the Case before continuing.');
|
|
180
|
+
next.incidentTargets = next.incidentTargets.map(item => item.id === id ? { ...item, state: 'resolved', updatedAt: now } : item);
|
|
181
|
+
}
|
|
182
|
+
if (patch.investigationContext !== undefined) {
|
|
183
|
+
assertWorkspaceInputGraph(patch.investigationContext, 'New incident context');
|
|
184
|
+
const raw = exact(patch.investigationContext, ['objective', 'incidentUrl', 'retainExactUrl'], 'New incident context');
|
|
185
|
+
if (typeof raw.retainExactUrl !== 'boolean')
|
|
186
|
+
throw new TypeError('Choose whether to retain the exact incident URL.');
|
|
187
|
+
next.investigationContext = { ...prepareCaseInvestigationContext({ objective: raw.objective, incidentUrl: raw.incidentUrl, retainExactUrl: raw.retainExactUrl }),
|
|
188
|
+
id: current.investigationContext?.id ?? makeId(), updatedAt: now };
|
|
189
|
+
}
|
|
190
|
+
return readCaseWorkflowMetadata(next, domain);
|
|
191
|
+
}
|
|
192
|
+
/** Add missing targets, retaining the newer state for an existing identity. */
|
|
193
|
+
export function mergeCaseWorkflowMetadata(local, incoming, importNewer, domain) {
|
|
194
|
+
const targets = new Map(local.incidentTargets.map(target => [target.id, target]));
|
|
195
|
+
for (const target of incoming.incidentTargets) {
|
|
196
|
+
const existing = targets.get(target.id);
|
|
197
|
+
if (existing && (existing.url !== target.url || existing.createdAt !== target.createdAt))
|
|
198
|
+
throw new TypeError('Imported incident target identity conflicts with retained history. No data was changed.');
|
|
199
|
+
if (!existing || Date.parse(target.updatedAt) > Date.parse(existing.updatedAt))
|
|
200
|
+
targets.set(target.id, target);
|
|
201
|
+
}
|
|
202
|
+
const left = local.investigationContext, right = incoming.investigationContext;
|
|
203
|
+
const investigationContext = right && (!left || Date.parse(right.updatedAt) > Date.parse(left.updatedAt)) ? right : left;
|
|
204
|
+
return readCaseWorkflowMetadata({ types: importNewer ? incoming.types : local.types,
|
|
205
|
+
incidentTargets: [...targets.values()], investigationContext }, domain);
|
|
206
|
+
}
|
|
207
|
+
export function caseIncidentTargets(record, options = {}) {
|
|
208
|
+
return (record.workflowMetadata?.incidentTargets ?? [])
|
|
209
|
+
.filter(target => options.includeResolved || target.state === 'open').map(target => ({ ...target }));
|
|
210
|
+
}
|
|
211
|
+
export function caseResponseIncidentUrls(record) {
|
|
212
|
+
const urls = new Set(caseIncidentTargets(record).map((target) => target.url));
|
|
213
|
+
const context = caseInvestigationContext(record);
|
|
214
|
+
if (context?.urlRetention === 'exact')
|
|
215
|
+
urls.add(context.incidentUrl);
|
|
216
|
+
return [...urls].slice(0, MAX_CASE_INCIDENT_TARGETS);
|
|
217
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Read legacy text conventions once at the versioned storage/import boundary.
|
|
2
|
+
// Keep the original assertions for references and historical evidence.
|
|
3
|
+
import { CASE_TYPES, normalizeCaseIncidentTargetUrl, readCaseWorkflowMetadata } from "./case-workflow-metadata.mjs";
|
|
4
|
+
import { normalizeTags } from "./case-record-core.mjs";
|
|
5
|
+
import { normalizeCaseObjective, parseIncidentUrlContext } from "./case-incident-context.mjs";
|
|
6
|
+
import { EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
7
|
+
const TYPE_PREFIX = 'case-type:';
|
|
8
|
+
const TARGET_PREFIX = 'Incident target URL: ';
|
|
9
|
+
const CONTEXT_PREFIX = 'Investigate incident URL: ';
|
|
10
|
+
const OBJECTIVE_PREFIX = 'Objective: ';
|
|
11
|
+
const RETENTION_SEPARATOR = ' | URL retained: ';
|
|
12
|
+
export function readCaseWorkflowFields(raw, domain, assertions, sourceVersion) {
|
|
13
|
+
const tags = normalizeTags(raw.tags);
|
|
14
|
+
if (raw.workflowMetadata !== undefined) {
|
|
15
|
+
if (sourceVersion != null && sourceVersion < EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION)
|
|
16
|
+
throw new TypeError('Typed workflow metadata requires the current Case schema.');
|
|
17
|
+
return { tags, workflowMetadata: readCaseWorkflowMetadata(raw.workflowMetadata, domain) };
|
|
18
|
+
}
|
|
19
|
+
const types = CASE_TYPES.filter(type => tags.some(tag => tag.toLowerCase() === `${TYPE_PREFIX}${type.id}`)).map(type => type.id);
|
|
20
|
+
const workflowMetadata = { types, incidentTargets: [], investigationContext: null };
|
|
21
|
+
for (const assertion of assertions) {
|
|
22
|
+
if (assertion.statement.startsWith(TARGET_PREFIX)) {
|
|
23
|
+
const url = normalizeCaseIncidentTargetUrl(assertion.statement.slice(TARGET_PREFIX.length));
|
|
24
|
+
if (url)
|
|
25
|
+
workflowMetadata.incidentTargets.push({ id: assertion.id, url, state: assertion.state, createdAt: assertion.createdAt, updatedAt: assertion.updatedAt });
|
|
26
|
+
}
|
|
27
|
+
if (assertion.kind !== 'next_step' || assertion.state !== 'open' || !assertion.statement.startsWith(CONTEXT_PREFIX))
|
|
28
|
+
continue;
|
|
29
|
+
const parsed = parseIncidentUrlContext(assertion.statement.slice(CONTEXT_PREFIX.length));
|
|
30
|
+
const rationale = assertion.rationale ?? '';
|
|
31
|
+
const index = rationale.lastIndexOf(RETENTION_SEPARATOR);
|
|
32
|
+
const objective = rationale.startsWith(OBJECTIVE_PREFIX) && index > OBJECTIVE_PREFIX.length
|
|
33
|
+
? normalizeCaseObjective(rationale.slice(OBJECTIVE_PREFIX.length, index)) : '';
|
|
34
|
+
const retention = index >= 0 ? rationale.slice(index + RETENTION_SEPARATOR.length) : '';
|
|
35
|
+
if (!parsed || parsed.registrableDomain !== domain || !objective || (retention !== 'exact' && retention !== 'origin_only'))
|
|
36
|
+
continue;
|
|
37
|
+
workflowMetadata.investigationContext = { id: assertion.id, objective, urlRetention: retention,
|
|
38
|
+
incidentUrl: retention === 'exact' ? parsed.exactUrl : parsed.originUrl, updatedAt: assertion.updatedAt };
|
|
39
|
+
}
|
|
40
|
+
// Preserve historical text byte-for-byte where already normalised. Typed
|
|
41
|
+
// metadata becomes authoritative; subsequent tag edits never change types.
|
|
42
|
+
return { tags, workflowMetadata: readCaseWorkflowMetadata(workflowMetadata, domain) };
|
|
43
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// connection attempts. Bulk/watchlist/case records deliberately keep only this
|
|
4
4
|
// smaller derived shape: no paths, queries, header values, attempt errors, or
|
|
5
5
|
// redirect inventories.
|
|
6
|
-
import { MAX_HTTP_EVIDENCE_REDIRECTS } from "
|
|
6
|
+
import { MAX_HTTP_EVIDENCE_REDIRECTS } from "../analysis/http-evidence-bounds.mjs";
|
|
7
7
|
export const HTTP_SUMMARY_VERSION = 1;
|
|
8
8
|
export const MAX_HTTP_SUMMARY_ORIGIN_LENGTH = 300;
|
|
9
9
|
export const MAX_HTTP_SUMMARY_CONTENT_TYPE_LENGTH = 100;
|
|
@@ -154,6 +154,11 @@ export function normalizeHttpSummary(value) {
|
|
|
154
154
|
httpSecurityHeaders: securityHeaderTokens(record.httpSecurityHeaders),
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
|
+
/** A partial body still permits comparison of observed response metadata. */
|
|
158
|
+
export function httpSummaryFieldIsObserved(field, value) {
|
|
159
|
+
const summary = normalizeHttpSummary(value);
|
|
160
|
+
return summary !== null && Object.hasOwn(summary, field) && summary[field] !== null;
|
|
161
|
+
}
|
|
157
162
|
export function httpSecurityHeaderLabel(token) {
|
|
158
163
|
const labels = {
|
|
159
164
|
'content-security-policy': 'Content Security Policy',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hammingDistanceHex, isInformativePerceptualHash, isPerceptualHash, } from "
|
|
1
|
+
import { hammingDistanceHex, isInformativePerceptualHash, isPerceptualHash, } from "../analysis/perceptual-hash-comparison.mjs";
|
|
2
2
|
function plainRecord(value) {
|
|
3
3
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
4
4
|
? value
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// produce an aggregate similarity score, risk factor, or maliciousness
|
|
4
4
|
// verdict. Inputs pass through the strict baseline normalizer before use.
|
|
5
5
|
import { normalizePageBaseline } from "../workspace/page-baseline.mjs";
|
|
6
|
-
import { hammingDistanceHex } from "
|
|
6
|
+
import { hammingDistanceHex } from "../analysis/perceptual-hash-comparison.mjs";
|
|
7
7
|
import { PAGE_COMPARISON_VERSION } from "../contracts/offline-comparison.mjs";
|
|
8
8
|
export { PAGE_COMPARISON_VERSION };
|
|
9
9
|
function component(id, label, method, status, outcome, detail, partial) {
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// proof of common ownership, coordination, intent, or maliciousness. This
|
|
4
4
|
// module deliberately produces no aggregate score and is never a persistence
|
|
5
5
|
// boundary.
|
|
6
|
-
import { normalizeDomain } from "../
|
|
6
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
7
|
+
import { canonicalPublicIpAddress } from "../evidence/public-address-policy.mjs";
|
|
7
8
|
import { groupBySimilarFavicon } from "./favicon-similarity.mjs";
|
|
8
|
-
import { qualifyRelationshipSources, relationshipSourceEvidence, normalizeRelationshipSourceProjection, unknownRelationshipSource, MAX_RELATIONSHIP_SOURCES_PER_DOMAIN, } from "./relationship-provenance.mjs";
|
|
9
|
-
import { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, } from "../contracts/offline-comparison.mjs";
|
|
10
|
-
export { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, };
|
|
9
|
+
import { qualifyRelationshipSources, relationshipSourceEvidence, normalizeRelationshipSourceProjection, FAVICON_RELATIONSHIP_DESCRIPTION, unknownRelationshipSource, MAX_RELATIONSHIP_SOURCES_PER_DOMAIN, } from "./relationship-provenance.mjs";
|
|
10
|
+
import { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS, RELATIONSHIP_SOURCE_PROVENANCE_VERSION, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, } from "../contracts/offline-comparison.mjs";
|
|
11
|
+
export { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, };
|
|
11
12
|
export const MAX_RELATIONSHIP_ROWS = 2000;
|
|
12
13
|
export const MAX_RELATIONSHIP_GROUPS = 100;
|
|
13
14
|
export const MAX_RELATIONSHIP_DOMAINS = 50;
|
|
@@ -217,7 +218,7 @@ export function buildScanRelationships(rawRows) {
|
|
|
217
218
|
for (const raw of input.slice(0, MAX_RELATIONSHIP_ROWS)) {
|
|
218
219
|
const domain = normalizeDomain(raw?.domain);
|
|
219
220
|
const observation = record(raw?.relationship);
|
|
220
|
-
if (!domain || raw?.trusted || !observation || !
|
|
221
|
+
if (!domain || raw?.trusted || !observation || !SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS.some((version) => version === observation.version))
|
|
221
222
|
continue;
|
|
222
223
|
rows.push({ domain, observation });
|
|
223
224
|
if (observation.truncated === true)
|
|
@@ -228,6 +229,7 @@ export function buildScanRelationships(rawRows) {
|
|
|
228
229
|
const identifiers = new Map();
|
|
229
230
|
const certificates = new Map();
|
|
230
231
|
const officialAssets = new Map();
|
|
232
|
+
let excludedNonPublicAddresses = 0;
|
|
231
233
|
for (const { domain, observation } of rows) {
|
|
232
234
|
const nameserverSource = Array.isArray(observation.nameservers) ? observation.nameservers : [];
|
|
233
235
|
const addressSource = Array.isArray(observation.ipAddresses) ? observation.ipAddresses : [];
|
|
@@ -239,8 +241,16 @@ export function buildScanRelationships(rawRows) {
|
|
|
239
241
|
const nameservers = nameserverSource.slice(0, MAX_NAMESERVERS_PER_ROW).map(hostname).filter(Boolean);
|
|
240
242
|
if (nameservers.length)
|
|
241
243
|
addBucket(nameserverSets, [...new Set(nameservers)].sort().join(' · '), domain);
|
|
242
|
-
for (const value of addressSource.slice(0, MAX_IPS_PER_ROW))
|
|
243
|
-
|
|
244
|
+
for (const value of addressSource.slice(0, MAX_IPS_PER_ROW)) {
|
|
245
|
+
const address = ipAddress(value);
|
|
246
|
+
if (!address)
|
|
247
|
+
continue;
|
|
248
|
+
const publicAddress = canonicalPublicIpAddress(address);
|
|
249
|
+
if (publicAddress)
|
|
250
|
+
addBucket(addresses, publicAddress, domain);
|
|
251
|
+
else
|
|
252
|
+
excludedNonPublicAddresses += 1;
|
|
253
|
+
}
|
|
244
254
|
for (const value of identifierSource.slice(0, MAX_TRACKING_IDS_PER_ROW)) {
|
|
245
255
|
if (typeof value === 'string' && /^[a-z-]{1,40}:[A-Z0-9-]{1,64}$/.test(value)) {
|
|
246
256
|
addBucket(identifiers, value, domain);
|
|
@@ -288,7 +298,7 @@ export function buildScanRelationships(rawRows) {
|
|
|
288
298
|
evidence?.faviconPHash ? `dhash:${evidence.faviconPHash}` : '',
|
|
289
299
|
].filter(Boolean).join(',')}`;
|
|
290
300
|
}).join('|');
|
|
291
|
-
output.push(group('favicon', '
|
|
301
|
+
output.push(group('favicon', 'Connected favicon matches', 'Exact SHA-256 or perceptual dHash distance ≤ 6', '', distinctDomains, FAVICON_RELATIONSHIP_DESCRIPTION, normalizedValue));
|
|
292
302
|
}
|
|
293
303
|
}
|
|
294
304
|
for (const [value, domains] of officialAssets)
|
|
@@ -317,7 +327,7 @@ export function buildScanRelationships(rawRows) {
|
|
|
317
327
|
const field = ARRAY_RELATIONSHIP_FIELDS[item.type];
|
|
318
328
|
if (field && Array.isArray(observation[field.field]) && observation[field.field].length > field.maximum)
|
|
319
329
|
groupTruncated = true;
|
|
320
|
-
const projection = observation.version
|
|
330
|
+
const projection = Number(observation.version) >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION
|
|
321
331
|
? normalizeRelationshipSourceProjection(observation.sourceEvidence) : {};
|
|
322
332
|
for (const source of projection[item.type] ?? [unknownRelationshipSource()]) {
|
|
323
333
|
if (contributors.length >= MAX_RELATIONSHIP_DOMAINS * MAX_RELATIONSHIP_SOURCES_PER_DOMAIN)
|
|
@@ -331,8 +341,10 @@ export function buildScanRelationships(rawRows) {
|
|
|
331
341
|
return {
|
|
332
342
|
version: RELATIONSHIP_EVIDENCE_VERSION,
|
|
333
343
|
groups,
|
|
344
|
+
excludedNonPublicAddresses,
|
|
334
345
|
truncated,
|
|
335
346
|
limitations: [
|
|
347
|
+
...(excludedNonPublicAddresses ? [`${excludedNonPublicAddresses} non-public address observations were excluded from public-infrastructure grouping; their DNS answers remain in the scan evidence.`] : []),
|
|
336
348
|
'Shared observations are investigation pivots, not proof of common ownership, coordination, intent, or maliciousness.',
|
|
337
349
|
'Certificate relationships use exact native TLS leaf-certificate SHA-256 values only. Certificate Transparency counts and hostnames are never treated as certificate reuse.',
|
|
338
350
|
'A shared certificate can reflect a multi-domain certificate, shared hosting, CDN, or managed platform and does not establish common control.',
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
// Only the source metadata needed to qualify an observed relationship crosses
|
|
2
2
|
// this boundary. Collection and retention times are never source-time fallbacks.
|
|
3
|
-
import { normalizeDomain } from "../
|
|
3
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
4
|
+
import { canonicalPublicIpAddress } from "../evidence/public-address-policy.mjs";
|
|
4
5
|
import { normalizeExplicitIsoTimestamp, readObservationEnvelope } from "../evidence/observation.mjs";
|
|
5
6
|
export const RELATIONSHIP_TYPES = Object.freeze([
|
|
6
7
|
'nameserver_set', 'ip_address', 'certificate', 'tracking_identifier', 'favicon', 'official_asset',
|
|
7
8
|
]);
|
|
9
|
+
export const NON_PUBLIC_RELATIONSHIP_LIMITATION = 'This is an equal non-public DNS answer, not evidence of shared public hosting. Private, loopback, reserved and sinkhole-style answers can recur independently.';
|
|
10
|
+
export const FAVICON_RELATIONSHIP_DESCRIPTION = 'Each domain is connected by exact or bounded perceptual favicon matches, directly or through another member. Not every pair necessarily matches.';
|
|
11
|
+
export function isNonPublicAddressRelationship(type, value) {
|
|
12
|
+
return type === 'ip_address' && !canonicalPublicIpAddress(value);
|
|
13
|
+
}
|
|
8
14
|
const SOURCES = ['dns', 'tls', 'http', 'page_identity', 'favicon', 'registration_or_dns', 'unknown'];
|
|
9
15
|
const SOURCES_BY_TYPE = {
|
|
10
16
|
nameserver_set: ['dns', 'registration_or_dns'], ip_address: ['dns'], certificate: ['tls'],
|
|
@@ -3,9 +3,9 @@ import { buildExtractedLifecycleFamily } from "./extracted-domain-lifecycle.mjs"
|
|
|
3
3
|
import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
|
|
4
4
|
export const ANALYST_INTERCHANGE_CONTRACT_OWNER = 'packages/contracts/analyst-interchange.mts';
|
|
5
5
|
export const INVESTIGATION_CACAO_SPEC_VERSION = 'cacao-2.0';
|
|
6
|
-
export const INVESTIGATION_CACAO_PROFILE_VERSION =
|
|
7
|
-
export const INVESTIGATION_CACAO_PROFILE_SEMVER = '
|
|
8
|
-
export const INVESTIGATION_CACAO_SUPPORTED_PROFILE_VERSIONS = [INVESTIGATION_CACAO_PROFILE_VERSION];
|
|
6
|
+
export const INVESTIGATION_CACAO_PROFILE_VERSION = 3;
|
|
7
|
+
export const INVESTIGATION_CACAO_PROFILE_SEMVER = '3.0.0';
|
|
8
|
+
export const INVESTIGATION_CACAO_SUPPORTED_PROFILE_VERSIONS = [2, INVESTIGATION_CACAO_PROFILE_VERSION];
|
|
9
9
|
export const MAX_INVESTIGATION_CACAO_IMPORT_BYTES = 384 * 1024;
|
|
10
10
|
export const BRAND_PROTECTION_OPERATIONS_REPORT_SCHEMA = 'whoisleuth.brand-protection-operations-report';
|
|
11
11
|
export const BRAND_PROTECTION_OPERATIONS_REPORT_VERSION = 2;
|
|
@@ -18,6 +18,10 @@ export const STIX_INDICATOR_LIFECYCLE_SCHEMA = 'whoisleuth.internal.stix-indicat
|
|
|
18
18
|
export const STIX_INDICATOR_EXPORT_VERSION = 2;
|
|
19
19
|
export const MISP_INDICATOR_LIFECYCLE_SCHEMA = 'whoisleuth.internal.misp-indicators';
|
|
20
20
|
export const MISP_INDICATOR_EXPORT_VERSION = 2;
|
|
21
|
+
export const MANAGED_INDICATOR_SET_SCHEMA = 'whoisleuth.managed-indicator-set';
|
|
22
|
+
export const MANAGED_INDICATOR_SET_VERSION = 1;
|
|
23
|
+
export const MAX_MANAGED_INDICATOR_SET_BYTES = 4 * 1024 * 1024;
|
|
24
|
+
export const MAX_MANAGED_INDICATOR_PLAN_BYTES = 16 * 1024 * 1024;
|
|
21
25
|
export const DNS_CHANGE_REHEARSAL_VERSION = 2;
|
|
22
26
|
export const DNS_CHANGE_REHEARSAL_EXPORT_SCHEMA = 'whoisleuth.dns-change-rehearsal';
|
|
23
27
|
export const MAIL_REPORT_SCHEMA = 'whoisleuth.mail-report-review';
|
|
@@ -29,20 +33,22 @@ export const MAIL_REPORT_CANONICALIZATION_ROUTES = Object.freeze([
|
|
|
29
33
|
export const REGISTRATION_DISCLOSURE_PLAN_SCHEMA = 'whoisleuth.registration-disclosure-plan';
|
|
30
34
|
export const REGISTRATION_DISCLOSURE_PLAN_VERSION = 2;
|
|
31
35
|
export const STATIC_PAGE_PATTERN_PACK_SCHEMA = 'whoisleuth.static-page-pattern-pack';
|
|
32
|
-
export const STATIC_PAGE_PATTERN_PACK_VERSION =
|
|
36
|
+
export const STATIC_PAGE_PATTERN_PACK_VERSION = 3;
|
|
37
|
+
export const STATIC_PAGE_PATTERN_PACK_SUPPORTED_VERSIONS = [2, STATIC_PAGE_PATTERN_PACK_VERSION];
|
|
33
38
|
export const MAX_STATIC_PAGE_PATTERN_PACK_BYTES = 256 * 1024;
|
|
34
39
|
export const WEB_CAPTURE_SUMMARY_SCHEMA = 'whoisleuth.web-capture-summary';
|
|
35
40
|
export const WEB_CAPTURE_SUMMARY_VERSION = 1;
|
|
36
41
|
export const WEB_CAPTURE_MANIFEST_SCHEMA = 'whoisleuth.web-capture-manifest';
|
|
37
|
-
export const WEB_CAPTURE_MANIFEST_VERSION =
|
|
42
|
+
export const WEB_CAPTURE_MANIFEST_VERSION = 3;
|
|
43
|
+
export const WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS = [2, WEB_CAPTURE_MANIFEST_VERSION];
|
|
38
44
|
export const MAX_WEB_CAPTURE_MANIFEST_BYTES = 1024 * 1024;
|
|
39
45
|
const CACAO_COMPATIBILITY = defineSchemaCompatibility({
|
|
40
46
|
id: 'export.investigation-cacao-profile', kind: 'export', schema: INVESTIGATION_CACAO_SPEC_VERSION,
|
|
41
47
|
currentVersion: INVESTIGATION_CACAO_PROFILE_VERSION, supportedVersions: INVESTIGATION_CACAO_SUPPORTED_PROFILE_VERSIONS,
|
|
42
|
-
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: '
|
|
48
|
+
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'normalize_to_current',
|
|
43
49
|
writeSemantics: 'non_destructive_merge', byteBudget: MAX_INVESTIGATION_CACAO_IMPORT_BYTES,
|
|
44
50
|
owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
|
|
45
|
-
note: 'Restricted CACAO 2.0 profile
|
|
51
|
+
note: 'Restricted CACAO 2.0 manual profile. Version 3 retains optional lesson-revision provenance; public profile 2 remains readable. Executable commands, branches, targets and credentials remain rejected.',
|
|
46
52
|
});
|
|
47
53
|
const OPERATIONS_REPORT_COMPATIBILITY = defineSchemaCompatibility({
|
|
48
54
|
id: 'export.brand-protection-operations-report', kind: 'export', schema: BRAND_PROTECTION_OPERATIONS_REPORT_SCHEMA,
|
|
@@ -72,6 +78,13 @@ const MISP_INDICATORS_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
72
78
|
writeSemantics: 'read_only', byteBudget: null, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
|
|
73
79
|
note: 'Unpublished, non-IDS, non-correlating event for reviewed import. Version 2 omits optional seen times when the source observation time is unknown.',
|
|
74
80
|
});
|
|
81
|
+
export const MANAGED_INDICATOR_SET_COMPATIBILITY = defineSchemaCompatibility({
|
|
82
|
+
id: 'export.managed-indicator-set', kind: 'export', schema: MANAGED_INDICATOR_SET_SCHEMA,
|
|
83
|
+
currentVersion: MANAGED_INDICATOR_SET_VERSION, supportedVersions: [MANAGED_INDICATOR_SET_VERSION],
|
|
84
|
+
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'exact_current_only',
|
|
85
|
+
writeSemantics: 'read_only', byteBudget: MAX_MANAGED_INDICATOR_SET_BYTES, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
|
|
86
|
+
note: 'File-backed managed indicator revisions retain original observations, stable identities, explicit renewals and permanent withdrawals. Known predecessors prevent history replacement; content digests do not authenticate authorship or an unavailable earlier chain.',
|
|
87
|
+
});
|
|
75
88
|
const WEB_CAPTURE_SUMMARY_COMPATIBILITY = defineSchemaCompatibility({
|
|
76
89
|
id: 'export.web-capture-summary', kind: 'export', schema: WEB_CAPTURE_SUMMARY_SCHEMA,
|
|
77
90
|
currentVersion: WEB_CAPTURE_SUMMARY_VERSION, supportedVersions: [WEB_CAPTURE_SUMMARY_VERSION],
|
|
@@ -81,11 +94,11 @@ const WEB_CAPTURE_SUMMARY_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
81
94
|
});
|
|
82
95
|
const WEB_CAPTURE_MANIFEST_COMPATIBILITY = defineSchemaCompatibility({
|
|
83
96
|
id: 'export.web-capture-manifest', kind: 'export', schema: WEB_CAPTURE_MANIFEST_SCHEMA,
|
|
84
|
-
currentVersion: WEB_CAPTURE_MANIFEST_VERSION, supportedVersions:
|
|
97
|
+
currentVersion: WEB_CAPTURE_MANIFEST_VERSION, supportedVersions: WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS,
|
|
85
98
|
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'read_only',
|
|
86
99
|
writeSemantics: 'read_only', byteBudget: MAX_WEB_CAPTURE_MANIFEST_BYTES,
|
|
87
100
|
owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
|
|
88
|
-
note: 'Metadata-only capture manifest
|
|
101
|
+
note: 'Metadata-only capture manifest. Version 3 adds bounded page and dependency observations; version 2 remains readable with those observations unavailable.',
|
|
89
102
|
});
|
|
90
103
|
const DEFENSIVE_MANIFEST_COMPATIBILITY = defineSchemaCompatibility({
|
|
91
104
|
id: 'export.defensive-indicator-manifest', kind: 'export', schema: DEFENSIVE_INDICATOR_MANIFEST_SCHEMA,
|
|
@@ -124,11 +137,11 @@ const REGISTRATION_DISCLOSURE_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
124
137
|
});
|
|
125
138
|
const STATIC_PATTERN_PACK_COMPATIBILITY = defineSchemaCompatibility({
|
|
126
139
|
id: 'export.static-page-pattern-pack', kind: 'export', schema: STATIC_PAGE_PATTERN_PACK_SCHEMA,
|
|
127
|
-
currentVersion: STATIC_PAGE_PATTERN_PACK_VERSION, supportedVersions:
|
|
128
|
-
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: '
|
|
140
|
+
currentVersion: STATIC_PAGE_PATTERN_PACK_VERSION, supportedVersions: STATIC_PAGE_PATTERN_PACK_SUPPORTED_VERSIONS,
|
|
141
|
+
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'read_only',
|
|
129
142
|
writeSemantics: 'non_destructive_merge', byteBudget: MAX_STATIC_PAGE_PATTERN_PACK_BYTES,
|
|
130
143
|
owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
|
|
131
|
-
note: '
|
|
144
|
+
note: 'Versions 2 and 3 remain readable. Version 3 can use attributed password-form destinations; older rules retain their declared meaning. Import remains explicit and does not execute code or start collection.',
|
|
132
145
|
});
|
|
133
146
|
export function serialiseAnalystInterchangeJson(value) {
|
|
134
147
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
@@ -141,12 +154,17 @@ export const ANALYST_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLifecycleFamily(
|
|
|
141
154
|
includedCategories: ['reviewed-indicators', 'source-identity', 'provenance', 'completeness', 'omitted-counts', 'limitations'],
|
|
142
155
|
excludedCategories: ['credentials', 'cookies', 'message-content', 'raw-upstream-responses', 'expanded-contacts'],
|
|
143
156
|
formats: [
|
|
157
|
+
{ descriptor: MANAGED_INDICATOR_SET_COMPATIBILITY, lifecycleSchema: MANAGED_INDICATOR_SET_SCHEMA,
|
|
158
|
+
requiredKeys: ['schema', 'version', 'id', 'producerId', 'revisionId', 'name', 'revision', 'createdAt', 'modifiedAt', 'previous', 'entries', 'integrity'], optionalKeys: [],
|
|
159
|
+
hook: { module: 'packages/interchange/managed-indicator-set.mts', exportName: 'validateManagedIndicatorSet', role: 'structure_validator', runtime: 'shared' },
|
|
160
|
+
fixtures: [{ id: 'managed-indicator-set-v1', path: `${F}managed-indicator-set-v1.json`, bytes: 1_193, sha256: '3a4ffb7256e696d1f1b3bae14743bb8c03ddeb0922186868e935f5a489ba3985', version: MANAGED_INDICATOR_SET_VERSION }] },
|
|
144
161
|
{ descriptor: CACAO_COMPATIBILITY, lifecycleSchema: INVESTIGATION_CACAO_SPEC_VERSION,
|
|
145
162
|
requiredKeys: ['type', 'spec_version', 'id', 'name', 'created', 'modified', 'workflow_start', 'workflow', 'extension_definitions'],
|
|
146
163
|
optionalKeys: ['description', 'playbook_types', 'playbook_activities', 'created_by', 'playbook_extensions', 'agent_definitions', 'x_whoisleuth_profile_version'],
|
|
147
164
|
hook: { module: 'packages/interchange/investigation-playbook-interchange.mts', exportName: 'parseCacaoInvestigationPlaybook', role: 'normaliser', runtime: 'shared' },
|
|
148
165
|
fixtures: [
|
|
149
|
-
{ id: 'investigation-cacao-profile-v2', path: `${F}investigation-cacao-profile-v2.json`, bytes: 4_091, sha256: '7e9a3de6e12b83477690c0c58e03e6efbbb8b0c11fd98dc8470bdb6784d42358', version:
|
|
166
|
+
{ id: 'investigation-cacao-profile-v2', path: `${F}investigation-cacao-profile-v2.json`, bytes: 4_091, sha256: '7e9a3de6e12b83477690c0c58e03e6efbbb8b0c11fd98dc8470bdb6784d42358', version: 2 },
|
|
167
|
+
{ id: 'investigation-cacao-profile-v3', path: `${F}investigation-cacao-profile-v3.json`, bytes: 4_067, sha256: '608cbb7875c3448ca11f6f8cb2f96ae098e18316532a8f651bbe540ca7f25a2d', version: INVESTIGATION_CACAO_PROFILE_VERSION },
|
|
150
168
|
] },
|
|
151
169
|
{ descriptor: OPERATIONS_REPORT_COMPATIBILITY, lifecycleSchema: BRAND_PROTECTION_OPERATIONS_REPORT_SCHEMA,
|
|
152
170
|
requiredKeys: ['schema', 'version', 'generatedAt', 'window', 'limitations'], optionalKeys: ['sourceState', 'summary', 'counts', 'states', 'actionTypes', 'durations', 'denominators', 'omissions'],
|
|
@@ -182,7 +200,8 @@ export const ANALYST_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLifecycleFamily(
|
|
|
182
200
|
requiredKeys: ['schema', 'schemaVersion', 'source', 'captures'], optionalKeys: [],
|
|
183
201
|
hook: { module: 'packages/interchange/web-capture-import.mts', exportName: 'parseWebCaptureManifest', role: 'normaliser', runtime: 'shared' },
|
|
184
202
|
fixtures: [
|
|
185
|
-
{ id: 'web-capture-manifest-v2', path: `${F}web-capture-manifest-v2.json`, bytes: 494, sha256: 'a2208dc956a832d70e37cd9a5929db81d29148856c485e4fd6ce5b123de5b8e6', version:
|
|
203
|
+
{ id: 'web-capture-manifest-v2', path: `${F}web-capture-manifest-v2.json`, bytes: 494, sha256: 'a2208dc956a832d70e37cd9a5929db81d29148856c485e4fd6ce5b123de5b8e6', version: 2 },
|
|
204
|
+
{ id: 'web-capture-manifest-v3', path: `${F}web-capture-manifest-v3.json`, bytes: 868, sha256: 'bca4e8cb539ac6035dd44586fa7e27774766a14dadbc4fc73cad2b71dace2ac8', version: WEB_CAPTURE_MANIFEST_VERSION },
|
|
186
205
|
] },
|
|
187
206
|
{ descriptor: DEFENSIVE_MANIFEST_COMPATIBILITY, lifecycleSchema: DEFENSIVE_INDICATOR_MANIFEST_SCHEMA,
|
|
188
207
|
requiredKeys: ['schema', 'version', 'generatedAt'], optionalKeys: ['expiresAt', 'reviewRequired', 'explicitSelection', 'includeWildcards', 'entries', 'indicators', 'exclusions', 'limitations'],
|
|
@@ -211,6 +230,7 @@ export const ANALYST_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLifecycleFamily(
|
|
|
211
230
|
{ descriptor: STATIC_PATTERN_PACK_COMPATIBILITY, lifecycleSchema: STATIC_PAGE_PATTERN_PACK_SCHEMA,
|
|
212
231
|
requiredKeys: ['schema', 'version', 'id', 'label', 'description', 'evidenceBoundary', 'relationship', 'confidence', 'rules'], optionalKeys: [],
|
|
213
232
|
hook: { module: 'packages/interchange/static-page-pattern-packs.mts', exportName: 'validateStaticPagePatternPack', role: 'structure_validator', runtime: 'shared' },
|
|
214
|
-
fixtures: [{ id: 'static-page-pattern-pack-v2', path: `${F}static-page-pattern-pack-v2.json`, bytes: 1_094, sha256: 'c409e527850bcb924f5be0708a58c5e2c32d98947c42150f6a79765991c0eb6f', version:
|
|
233
|
+
fixtures: [{ id: 'static-page-pattern-pack-v2', path: `${F}static-page-pattern-pack-v2.json`, bytes: 1_094, sha256: 'c409e527850bcb924f5be0708a58c5e2c32d98947c42150f6a79765991c0eb6f', version: 2 },
|
|
234
|
+
{ id: 'static-page-pattern-pack-v3', path: `${F}static-page-pattern-pack-v3.json`, bytes: 746, sha256: '5be19b621ad604de8c430191a34fcd2d624b22b8bdf9a5cb2653ef7d1b86c71a', version: 3 }] },
|
|
215
235
|
],
|
|
216
236
|
}));
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
|
|
2
|
+
import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
|
|
3
|
+
import { buildExtractedLifecycleFamily } from "./extracted-domain-lifecycle.mjs";
|
|
4
|
+
import { MAX_WATCHLIST_IMPORT_BYTES } from "./workspace-portability.mjs";
|
|
5
|
+
export const CANDIDATE_WATCH_INPUT_SCHEMA = 'whoisleuth.candidate-watch-input';
|
|
6
|
+
export const CANDIDATE_WATCH_INPUT_VERSION = 1;
|
|
7
|
+
export const CANDIDATE_WATCH_INPUT_COMPATIBILITY = defineSchemaCompatibility({
|
|
8
|
+
id: 'cli.candidate-watch-input',
|
|
9
|
+
kind: 'cli_document',
|
|
10
|
+
schema: CANDIDATE_WATCH_INPUT_SCHEMA,
|
|
11
|
+
currentVersion: 1,
|
|
12
|
+
supportedVersions: [1],
|
|
13
|
+
acceptsUnversionedLegacy: false,
|
|
14
|
+
futureVersionBehavior: 'reject',
|
|
15
|
+
migration: 'exact_current_only',
|
|
16
|
+
writeSemantics: 'read_only',
|
|
17
|
+
byteBudget: MAX_WATCHLIST_IMPORT_BYTES,
|
|
18
|
+
owner: 'packages/contracts/candidate-watch-review.mts',
|
|
19
|
+
note: 'Bounded offline analyst selection and optional portable Watchlist document; no collection or schedule authority.',
|
|
20
|
+
});
|
|
21
|
+
export const CANDIDATE_WATCH_REVIEW_LIFECYCLE_FAMILY = defineSchemaLifecycleFamily(buildExtractedLifecycleFamily({
|
|
22
|
+
id: 'candidate-watch-review',
|
|
23
|
+
owner: 'packages/contracts/candidate-watch-review.mts',
|
|
24
|
+
plane: 'node',
|
|
25
|
+
projection: 'review_output',
|
|
26
|
+
retention: 'transient_report',
|
|
27
|
+
serializerModule: 'cli/formatters/json.mts',
|
|
28
|
+
serializerExportName: 'formatJsonDocument',
|
|
29
|
+
includedCategories: ['candidate-provenance', 'analyst-watch-reasons', 'review-priorities'],
|
|
30
|
+
excludedCategories: ['raw-upstream-payloads', 'credentials', 'cookies', 'query-bearing-urls'],
|
|
31
|
+
formats: [
|
|
32
|
+
{
|
|
33
|
+
descriptor: CANDIDATE_WATCH_INPUT_COMPATIBILITY,
|
|
34
|
+
lifecycleSchema: CANDIDATE_WATCH_INPUT_SCHEMA,
|
|
35
|
+
requiredKeys: ['schema', 'version', 'watchlists', 'selection'],
|
|
36
|
+
optionalKeys: [],
|
|
37
|
+
hook: {
|
|
38
|
+
module: 'cli/watchlist-review.mts',
|
|
39
|
+
exportName: 'parseCandidateWatchInput',
|
|
40
|
+
role: 'structure_validator',
|
|
41
|
+
runtime: 'node',
|
|
42
|
+
},
|
|
43
|
+
fixtures: [
|
|
44
|
+
{
|
|
45
|
+
id: 'candidate-watch-input-v1',
|
|
46
|
+
path: 'test/fixtures/brand-candidate-workflow/candidate-watch-input-v1.json',
|
|
47
|
+
bytes: 895,
|
|
48
|
+
sha256: '40a0d73e5aa3afc3b6836bb289c79cf9556dd755a5086a9666506b31468b5d3d',
|
|
49
|
+
version: 1,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
}));
|