@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +21 -6
- package/cli/artifact-validation/lookup-evidence.mjs +3 -3
- package/cli/artifact-validation/signed-review.mjs +59 -13
- package/cli/artifact-verify.mjs +2 -0
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -2
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +20 -3
- package/cli/case-pack.mjs +12 -2
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -210
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +3 -3
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +33 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +1 -1
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +18 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +68 -158
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +2 -2
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +8 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +223 -2
- package/docs/contextual-reviews.md +155 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +12 -8
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +9 -7
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +22 -5
- package/lib/domain-posture.mjs +16 -15
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +22 -15
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +4 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +7 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +69 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +27 -0
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +141 -139
- package/lib/static-html-analysis.mjs +1 -1
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +388 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +13 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +345 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +78 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-recheck-model.mjs +90 -32
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +40 -112
- package/packages/cases/case-record-projection.mjs +8 -1
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +63 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +103 -1
- package/packages/cases/case-response-model.mjs +9 -1
- package/packages/cases/case-response-outcomes.mjs +29 -8
- package/packages/cases/case-response-packet-render.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +25 -11
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +14 -3
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +28 -10
- package/packages/contracts/case-portability.mjs +118 -25
- package/packages/contracts/cli-command-semantics.mjs +2 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +63 -17
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +4 -12
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/intake-report.mjs +19 -0
- package/packages/investigation/investigation-manifest.mjs +2 -2
- package/packages/investigation/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +195 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/workspace/brand-profile-model.mjs +2 -2
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +23 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +28 -12
- package/packages/workspace/watchlist-store.mjs +17 -5
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- package/lib/hostname.mjs +0 -2
|
@@ -2,8 +2,13 @@ import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
|
|
|
2
2
|
import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
|
|
3
3
|
export const CLI_DOMAIN_CONTROL_MONITOR_SCHEMA = 'whoisleuth.cli.domain-control-monitor';
|
|
4
4
|
export const PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION = 1;
|
|
5
|
-
export const CLI_DOMAIN_CONTROL_MONITOR_VERSION =
|
|
6
|
-
export const SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, CLI_DOMAIN_CONTROL_MONITOR_VERSION];
|
|
5
|
+
export const CLI_DOMAIN_CONTROL_MONITOR_VERSION = 3;
|
|
6
|
+
export const SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, 2, CLI_DOMAIN_CONTROL_MONITOR_VERSION];
|
|
7
|
+
export const DOMAIN_CONTROL_MONITOR_EMBEDDED_VERSIONS = Object.freeze({
|
|
8
|
+
1: Object.freeze({ review: 1, flightRecorder: 1 }),
|
|
9
|
+
2: Object.freeze({ review: 2, flightRecorder: 2 }),
|
|
10
|
+
3: Object.freeze({ review: 3, flightRecorder: 3 }),
|
|
11
|
+
});
|
|
7
12
|
export const MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES = 16 * 1024 * 1024;
|
|
8
13
|
export const MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH = 48;
|
|
9
14
|
export const MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS = 50_000;
|
|
@@ -100,6 +105,12 @@ export const DOMAIN_CONTROL_MONITOR_SCHEMA_LIFECYCLE = defineSchemaLifecycleFami
|
|
|
100
105
|
{
|
|
101
106
|
id: 'domain-control-monitor-v2', path: 'test/fixtures/domain-control-monitor-v2.json',
|
|
102
107
|
bytes: 18_023, sha256: 'f4017f3dc84b3adb6b6114ea49c37ff05939779daabbbb56c868010145cac55c',
|
|
108
|
+
contentDigestSha256: null, schema: CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, version: 2,
|
|
109
|
+
role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null, scope: 'repository',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'domain-control-monitor-v3', path: 'test/fixtures/domain-control-monitor-v3.json',
|
|
113
|
+
bytes: 19_215, sha256: 'a7233a0b52b282e3ad8097b9bd71bf15125b743e60a421cfec765cb8b9456563',
|
|
103
114
|
contentDigestSha256: null, schema: CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, version: CLI_DOMAIN_CONTROL_MONITOR_VERSION,
|
|
104
115
|
role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null, scope: 'repository',
|
|
105
116
|
},
|
|
@@ -10,10 +10,10 @@ export const CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA = 'whoisleuth.cli.domain-con
|
|
|
10
10
|
export const CLI_DOMAIN_CONTROL_REVIEW_SCHEMA = 'whoisleuth.cli.domain-control-review';
|
|
11
11
|
export const PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION = 1;
|
|
12
12
|
export const PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION = 1;
|
|
13
|
-
export const DOMAIN_CONTROL_REVIEW_VERSION =
|
|
14
|
-
export const CLI_DOMAIN_CONTROL_REVIEW_VERSION =
|
|
15
|
-
export const SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION, DOMAIN_CONTROL_REVIEW_VERSION];
|
|
16
|
-
export const SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION, CLI_DOMAIN_CONTROL_REVIEW_VERSION];
|
|
13
|
+
export const DOMAIN_CONTROL_REVIEW_VERSION = 3;
|
|
14
|
+
export const CLI_DOMAIN_CONTROL_REVIEW_VERSION = 3;
|
|
15
|
+
export const SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION, 2, DOMAIN_CONTROL_REVIEW_VERSION];
|
|
16
|
+
export const SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION, 2, CLI_DOMAIN_CONTROL_REVIEW_VERSION];
|
|
17
17
|
export const DOMAIN_CONTROL_REVIEW_OBSERVATION_FACTOR = 2;
|
|
18
18
|
export const MAX_DOMAIN_CONTROL_REVIEW_OBSERVATIONS = MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES
|
|
19
19
|
* DOMAIN_CONTROL_REVIEW_OBSERVATION_FACTOR;
|
|
@@ -87,7 +87,8 @@ export const PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS = Object.freeze([
|
|
|
87
87
|
'not_configured',
|
|
88
88
|
'due',
|
|
89
89
|
]);
|
|
90
|
-
export const
|
|
90
|
+
export const DOMAIN_CONTROL_REVIEW_V2_COUNT_KEYS = Object.freeze([...PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS, 'observed']);
|
|
91
|
+
export const DOMAIN_CONTROL_REVIEW_COUNT_KEYS = Object.freeze([...DOMAIN_CONTROL_REVIEW_V2_COUNT_KEYS, 'not_due']);
|
|
91
92
|
export const DOMAIN_CONTROL_REVIEW_DOMAIN_KEYS = Object.freeze([
|
|
92
93
|
'domain',
|
|
93
94
|
'state',
|
|
@@ -108,11 +109,21 @@ export const PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS = Object.freeze([
|
|
|
108
109
|
'Only complete observations can produce drift. Partial, unavailable, unsupported, or missing evidence remains inconclusive.',
|
|
109
110
|
'A valid manifest digest or signature establishes file integrity, not the correctness of the desired state or supplied observations.',
|
|
110
111
|
]);
|
|
111
|
-
export const
|
|
112
|
+
export const DOMAIN_CONTROL_REVIEW_V2_LIMITATIONS = Object.freeze([
|
|
112
113
|
PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS[0],
|
|
113
114
|
'Only complete observations with valid source times within the current review-age window can establish alignment, drift or expected absence. Observation-only fields do not establish alignment.',
|
|
114
115
|
PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS[2],
|
|
115
116
|
]);
|
|
117
|
+
export const DOMAIN_CONTROL_REVIEW_LIMITATIONS = Object.freeze([
|
|
118
|
+
...DOMAIN_CONTROL_REVIEW_V2_LIMITATIONS,
|
|
119
|
+
'Renewal dates are analyst-authored reminders, not observations of configuration.',
|
|
120
|
+
]);
|
|
121
|
+
export function domainControlReviewCountKeys(version) {
|
|
122
|
+
return version === 1 ? PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS : version === 2 ? DOMAIN_CONTROL_REVIEW_V2_COUNT_KEYS : DOMAIN_CONTROL_REVIEW_COUNT_KEYS;
|
|
123
|
+
}
|
|
124
|
+
export function domainControlReviewLimitations(version) {
|
|
125
|
+
return version === 1 ? PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS : version === 2 ? DOMAIN_CONTROL_REVIEW_V2_LIMITATIONS : DOMAIN_CONTROL_REVIEW_LIMITATIONS;
|
|
126
|
+
}
|
|
116
127
|
export const CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS = Object.freeze([
|
|
117
128
|
'schema',
|
|
118
129
|
'version',
|
|
@@ -233,11 +244,11 @@ const CORE_REVIEW_SHAPES = SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS.flatMap((ver
|
|
|
233
244
|
objects: [
|
|
234
245
|
{ path: '$', requiredKeys: DOMAIN_CONTROL_REVIEW_ROOT_KEYS, optionalKeys: [], unknownKeys: 'reject' },
|
|
235
246
|
{ path: '$.manifest', requiredKeys: DOMAIN_CONTROL_REVIEW_MANIFEST_SUMMARY_KEYS, optionalKeys: [], unknownKeys: 'reject' },
|
|
236
|
-
{ path: '$.counts', requiredKeys: version
|
|
247
|
+
{ path: '$.counts', requiredKeys: domainControlReviewCountKeys(version), optionalKeys: [], unknownKeys: 'reject' },
|
|
237
248
|
{ path: '$.domains[]', requiredKeys: DOMAIN_CONTROL_REVIEW_DOMAIN_KEYS, optionalKeys: [], unknownKeys: 'reject' },
|
|
238
249
|
{ path: '$.domains[].comparisons[]', requiredKeys: version === PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION ? PUBLIC_DOMAIN_CONTROL_REVIEW_COMPARISON_KEYS : DOMAIN_CONTROL_REVIEW_COMPARISON_KEYS, optionalKeys: [], unknownKeys: 'reject' },
|
|
239
250
|
],
|
|
240
|
-
fixedArrays: [{ path: '$.limitations', values: version
|
|
251
|
+
fixedArrays: [{ path: '$.limitations', values: domainControlReviewLimitations(version) }],
|
|
241
252
|
normalisation: 'preserve_document',
|
|
242
253
|
target: null,
|
|
243
254
|
},
|
|
@@ -253,6 +264,10 @@ function reviewFixtureShapeId(schema, version) {
|
|
|
253
264
|
return `domain-control-review.${cli ? 'cli-' : ''}${schema.endsWith('-input') ? 'input' : 'document'}.v${version}`;
|
|
254
265
|
}
|
|
255
266
|
const CURRENT_REVIEW_FIXTURE_CONTENT = {
|
|
267
|
+
"domain-control-review-input-v3": { "bytes": 1879, "sha256": "747cccb9a0610dd5b77ebd2a01f44dc7bd6a929b09b6e52037596427d0c2275b" },
|
|
268
|
+
"domain-control-review-v3": { "bytes": 4496, "sha256": "736926cf402ca868ddc832c229b88d3c91b44a4c73563c356633a69c1ec8d06c" },
|
|
269
|
+
"cli-domain-control-review-input-v3": { "bytes": 4123, "sha256": "0283efe6f78551a8aa5a9449a608fb4f3b46b39a9f11f6ffe93244979b16d30b" },
|
|
270
|
+
"cli-domain-control-review-v3": { "bytes": 9194, "sha256": "31c263f17cd663be74ab9801970a8800894da4023842137e41f602fde633452e" },
|
|
256
271
|
"domain-control-review-input-v2": {
|
|
257
272
|
"bytes": 1879,
|
|
258
273
|
"sha256": "ac3b5913a44c47bd179cd69e4a8bf4268f10e9f01dd7f59c7d722a8c0d241ad1"
|
|
@@ -408,13 +423,19 @@ export const DOMAIN_CONTROL_REVIEW_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamil
|
|
|
408
423
|
scope: 'repository',
|
|
409
424
|
},
|
|
410
425
|
].flatMap((fixture) => {
|
|
411
|
-
const
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
426
|
+
const currentVersion = fixture.schema.startsWith('whoisleuth.cli.') ? CLI_DOMAIN_CONTROL_REVIEW_VERSION : DOMAIN_CONTROL_REVIEW_VERSION;
|
|
427
|
+
const versions = fixture.schema.startsWith('whoisleuth.cli.') ? SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS : SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS;
|
|
428
|
+
return versions.map((version) => {
|
|
429
|
+
const id = fixture.id.replace(/v\d+$/u, `v${version}`);
|
|
430
|
+
const current = version === currentVersion;
|
|
431
|
+
return {
|
|
432
|
+
...fixture, ...(version === 1 ? {} : CURRENT_REVIEW_FIXTURE_CONTENT[id]), id,
|
|
433
|
+
path: `test/fixtures/${id}.json`, version, shapeId: reviewFixtureShapeId(fixture.schema, version),
|
|
434
|
+
role: fixture.role === 'input' ? 'input' : current ? 'current' : 'historical',
|
|
435
|
+
expectation: current ? fixture.expectation : fixture.schema === CLI_DOMAIN_CONTROL_REVIEW_SCHEMA ? 'historical_output_exact' : 'accepted_exact',
|
|
436
|
+
expectedOutputFixtureId: current ? fixture.expectedOutputFixtureId?.replace(/v\d+$/u, `v${version}`) ?? null : null,
|
|
437
|
+
};
|
|
438
|
+
});
|
|
418
439
|
}),
|
|
419
440
|
metadata: {
|
|
420
441
|
enforcement: 'declarative_only',
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export const CLI_INVESTIGATION_RUN_SCHEMA = 'whoisleuth.cli.investigation-run';
|
|
2
2
|
export const CLI_INVESTIGATION_RUN_VERSION = 3;
|
|
3
|
+
export const CLI_INVESTIGATION_PREVIEW_SCHEMA = 'whoisleuth.cli.investigation-preview';
|
|
4
|
+
export const CLI_INVESTIGATION_PREVIEW_VERSION = 1;
|
|
3
5
|
export const SUPPORTED_CLI_INVESTIGATION_RUN_VERSIONS = Object.freeze([1, 2, 3]);
|
|
4
6
|
export const MAX_INVESTIGATION_RUN_BYTES = 24 * 1024 * 1024;
|
|
5
7
|
export const MAX_INVESTIGATION_RUN_SELECTIONS = 16;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/** Syntactic identity only: private and reserved addresses remain valid evidence. */
|
|
2
|
+
export function canonicalIpAddress(value) {
|
|
3
|
+
const candidate = typeof value === 'string' ? value.trim() : value;
|
|
4
|
+
const parsed = addressValue(candidate);
|
|
5
|
+
if (!parsed)
|
|
6
|
+
return null;
|
|
7
|
+
if (parsed.family === 4)
|
|
8
|
+
return [24, 16, 8, 0].map(shift => (parsed.value >>> shift) & 255).join('.');
|
|
9
|
+
return new URL(`https://[${candidate}]/`).hostname.slice(1, -1);
|
|
10
|
+
}
|
|
1
11
|
export function ipv6Groups(value) {
|
|
2
12
|
const address = addressValue(value);
|
|
3
13
|
if (address?.family !== 6)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const MAX_AUTHENTICATION_HEADERS = 512;
|
|
2
|
+
export const MAX_AUTHENTICATION_CLAUSES = 64;
|
|
3
|
+
export const MAX_AUTHENTICATION_VALUE_LENGTH = 16 * 1024;
|
|
4
|
+
export const MAIL_AUTHENTICATION_METHODS = Object.freeze(['spf', 'dkim', 'dmarc', 'arc']);
|
|
5
|
+
export const MAIL_AUTHENTICATION_PROPERTIES = ['smtp.mailfrom', 'smtp.helo', 'header.d', 'header.i', 'header.from'];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const MESSAGE_INTAKE_SCHEMA = 'whoisleuth.message-intake';
|
|
2
|
+
export const MESSAGE_INTAKE_VERSION = 1;
|
|
3
|
+
export const MESSAGE_INTAKE_KINDS = ['text', 'email', 'calendar', 'qr', 'pdf', 'docx', 'har', 'identity'];
|
|
4
|
+
export const MESSAGE_INTAKE_INPUTS = {
|
|
5
|
+
text: { label: 'Pasted text or links', accept: '.txt,text/plain', binary: false },
|
|
6
|
+
email: { label: 'Email (.eml, including nested messages)', accept: '.eml,message/rfc822', binary: false },
|
|
7
|
+
calendar: { label: 'Calendar invitation (.ics)', accept: '.ics,text/calendar', binary: false },
|
|
8
|
+
qr: { label: 'QR image (still PNG)', accept: '.png,image/png', binary: true },
|
|
9
|
+
pdf: { label: 'PDF document', accept: '.pdf,application/pdf', binary: true },
|
|
10
|
+
docx: { label: 'Word document (.docx)', accept: '.docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document', binary: true },
|
|
11
|
+
har: { label: 'HTTP archive (.har)', accept: '.har,application/json', binary: false },
|
|
12
|
+
identity: { label: 'Identity events (selected JSON)', accept: '.json,application/json', binary: false },
|
|
13
|
+
};
|
|
14
|
+
export const IDENTITY_ACTIONS = [
|
|
15
|
+
{ id: 'opened_link', label: 'Opened the link' },
|
|
16
|
+
{ id: 'entered_password', label: 'Entered a password' },
|
|
17
|
+
{ id: 'approved_signin', label: 'Approved a sign-in or MFA request' },
|
|
18
|
+
{ id: 'granted_consent', label: 'Granted application consent' },
|
|
19
|
+
{ id: 'entered_device_code', label: 'Entered a device code' },
|
|
20
|
+
{ id: 'executed_command', label: 'Ran a command or installed software' },
|
|
21
|
+
];
|
|
22
|
+
// Byte admission, extraction work and decoded pixels are independent bounds.
|
|
23
|
+
export const MAX_MESSAGE_INTAKE_BYTES = 16 * 1024 * 1024;
|
|
24
|
+
export const MAX_MESSAGE_PARTS = 256;
|
|
25
|
+
export const MAX_MESSAGE_DEPTH = 12;
|
|
26
|
+
export const MAX_MESSAGE_HTML_NODES = 50_000;
|
|
27
|
+
export const MAX_INTAKE_LINKS = 512;
|
|
28
|
+
export const MAX_INTAKE_URL_LENGTH = 8_192;
|
|
29
|
+
export const MAX_EMBEDDED_LINK_DEPTH = 8;
|
|
30
|
+
export const MAX_AUTH_SCOPES = 64;
|
|
31
|
+
export const MESSAGE_ACTION_HINTS = ['clipboard_instruction', 'shell_instruction', 'verification_prompt', 'device_code_instruction', 'consent_instruction'];
|
|
@@ -10,7 +10,10 @@ export const CLI_COMPARISON_LEDGER_SCHEMA = 'whoisleuth.cli.comparison-ledger';
|
|
|
10
10
|
export const CLI_COMPARISON_LEDGER_VERSION = 1;
|
|
11
11
|
export const MAX_RETAINED_ARTIFACT_DIFF_BYTES = 8 * 1024 * 1024;
|
|
12
12
|
export const RELATIONSHIP_EVIDENCE_SCHEMA = 'whoisleuth.relationship-evidence';
|
|
13
|
-
export const RELATIONSHIP_EVIDENCE_VERSION =
|
|
13
|
+
export const RELATIONSHIP_EVIDENCE_VERSION = 4;
|
|
14
|
+
export const SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS = Object.freeze([2, 3, RELATIONSHIP_EVIDENCE_VERSION]);
|
|
15
|
+
export const RELATIONSHIP_SOURCE_PROVENANCE_VERSION = 3;
|
|
16
|
+
export const RELATIONSHIP_PUBLIC_ADDRESS_VERSION = 4;
|
|
14
17
|
export const TLS_RELATIONSHIP_PROFILE_VERSION = 2;
|
|
15
18
|
export const SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS = Object.freeze([TLS_RELATIONSHIP_PROFILE_VERSION]);
|
|
16
19
|
export const OFFLINE_COMPARISON_COMPATIBILITY_FACADES = Object.freeze([
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Stable interchange identities describe DNS publication, not message authentication. */
|
|
2
|
+
export const PASSIVE_MAIL_LABELS = Object.freeze({
|
|
3
|
+
authenticated_mail: 'MX, SPF and DMARC observed',
|
|
4
|
+
evidence_incomplete: 'Mail DNS evidence incomplete',
|
|
5
|
+
mail_auth_gap: 'MX observed; SPF or DMARC absent',
|
|
6
|
+
mail_auth_incomplete: 'MX observed; policy evidence incomplete',
|
|
7
|
+
no_explicit_mx: 'No explicit MX observed',
|
|
8
|
+
null_mx: 'Null MX observed',
|
|
9
|
+
});
|
|
10
|
+
export const PASSIVE_MAIL_INTERPRETATION = 'The retained authenticated_mail state means MX, SPF and DMARC publications were observed; it does not establish policy validity, alignment, message authentication or delivery.';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { MAX_WEB_CAPTURE_MANIFEST_BYTES, WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "./analyst-interchange.mjs";
|
|
1
|
+
export { MAX_WEB_CAPTURE_MANIFEST_BYTES, WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "./analyst-interchange.mjs";
|
|
2
2
|
export const WEB_CAPTURE_DOM_DIGEST_SCHEMA = 'whoisleuth.dom-digest';
|
|
3
3
|
export const WEB_CAPTURE_DOM_DIGEST_VERSION = 1;
|
|
4
4
|
export const WEB_CAPTURE_COMPARISON_SCHEMA = 'whoisleuth.web-capture-comparison';
|
|
5
|
-
export const WEB_CAPTURE_COMPARISON_VERSION =
|
|
5
|
+
export const WEB_CAPTURE_COMPARISON_VERSION = 4;
|
|
6
6
|
export const MAX_WEB_CAPTURE_DOM_DIGEST_BYTES = 1024 * 1024;
|
|
7
7
|
export const MAX_WEB_CAPTURE_SCREENSHOT_BYTES = 10 * 1024 * 1024;
|
|
8
8
|
export const MAX_WEB_CAPTURE_DOM_ELEMENTS = 20_000;
|
|
@@ -47,9 +47,9 @@ export const MAX_CAMPAIGN_IMPORT_BYTES = 2 * 1024 * 1024;
|
|
|
47
47
|
export const MAX_CAMPAIGN_INPUT_RECORDS = 500;
|
|
48
48
|
export const MAX_CAMPAIGN_STORE_BYTES = 512 * 1024;
|
|
49
49
|
export const WATCHLIST_SCHEMA = 'whoisleuth.watchlists';
|
|
50
|
-
export const WATCHLIST_SCHEMA_VERSION =
|
|
51
|
-
export const WATCHLIST_BROWSER_SUPPORTED_VERSIONS = Object.freeze([WATCHLIST_SCHEMA_VERSION]);
|
|
52
|
-
export const WATCHLIST_EXPORT_SUPPORTED_VERSIONS =
|
|
50
|
+
export const WATCHLIST_SCHEMA_VERSION = 4;
|
|
51
|
+
export const WATCHLIST_BROWSER_SUPPORTED_VERSIONS = Object.freeze([2, 3, WATCHLIST_SCHEMA_VERSION]);
|
|
52
|
+
export const WATCHLIST_EXPORT_SUPPORTED_VERSIONS = WATCHLIST_BROWSER_SUPPORTED_VERSIONS;
|
|
53
53
|
export const MAX_WATCHLISTS = 100;
|
|
54
54
|
export const MAX_WATCHLIST_INPUTS = MAX_WATCHLISTS * 4;
|
|
55
55
|
export const MAX_WATCHLIST_NAME_LENGTH = 100;
|
|
@@ -114,8 +114,8 @@ export const MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN = 12;
|
|
|
114
114
|
export const MAX_WEBSITE_SNAPSHOT_STORE_BYTES = 512 * 1024;
|
|
115
115
|
export const MAX_WEBSITE_SNAPSHOT_IMPORT_BYTES = 768 * 1024;
|
|
116
116
|
export const BULK_SESSION_SCHEMA = 'whoisleuth.bulk-sessions';
|
|
117
|
-
export const BULK_SESSION_SCHEMA_VERSION =
|
|
118
|
-
export const BULK_SESSION_BROWSER_SUPPORTED_VERSIONS = Object.freeze([4, BULK_SESSION_SCHEMA_VERSION]);
|
|
117
|
+
export const BULK_SESSION_SCHEMA_VERSION = 6;
|
|
118
|
+
export const BULK_SESSION_BROWSER_SUPPORTED_VERSIONS = Object.freeze([4, 5, BULK_SESSION_SCHEMA_VERSION]);
|
|
119
119
|
export const SUPPORTED_BULK_SESSION_SCHEMA_VERSIONS = BULK_SESSION_BROWSER_SUPPORTED_VERSIONS;
|
|
120
120
|
export const MAX_BULK_SESSIONS = 10;
|
|
121
121
|
export const MAX_BULK_SESSION_ROWS = 2_000;
|
|
@@ -126,8 +126,8 @@ export const MAX_BULK_SESSION_ARRAY_VALUES = 100;
|
|
|
126
126
|
export const MAX_BULK_SESSION_SOURCES = 12;
|
|
127
127
|
export const MAX_BULK_PROFILE_CONTEXT_LIMITATION_LENGTH = 300;
|
|
128
128
|
export const INVESTIGATION_TEMPLATE_SCHEMA = 'whoisleuth.investigation-templates';
|
|
129
|
-
export const INVESTIGATION_TEMPLATE_VERSION =
|
|
130
|
-
export const INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS = Object.freeze([INVESTIGATION_TEMPLATE_VERSION]);
|
|
129
|
+
export const INVESTIGATION_TEMPLATE_VERSION = 3;
|
|
130
|
+
export const INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS = Object.freeze([2, INVESTIGATION_TEMPLATE_VERSION]);
|
|
131
131
|
export const MAX_INVESTIGATION_TEMPLATES = 20;
|
|
132
132
|
export const MAX_INVESTIGATION_TEMPLATE_STORE_BYTES = 256 * 1024;
|
|
133
133
|
export const MAX_INVESTIGATION_TEMPLATE_IMPORT_BYTES = 384 * 1024;
|
|
@@ -241,9 +241,9 @@ export const CAMPAIGN_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
241
241
|
export const WATCHLIST_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
|
|
242
242
|
id: 'browser.watchlists', kind: 'browser_store', schema: null, currentVersion: WATCHLIST_SCHEMA_VERSION,
|
|
243
243
|
supportedVersions: WATCHLIST_BROWSER_SUPPORTED_VERSIONS, acceptsUnversionedLegacy: false,
|
|
244
|
-
futureVersionBehavior: 'preserve_without_write', migration: '
|
|
244
|
+
futureVersionBehavior: 'preserve_without_write', migration: 'normalize_to_current', writeSemantics: 'normalized_rewrite',
|
|
245
245
|
byteBudget: MAX_WATCHLIST_STORE_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
|
|
246
|
-
note: '
|
|
246
|
+
note: 'Public version 2 remains readable without inferred collection outcomes. Version 3 retains explicit page and favicon collection quality; unversioned maps are outside the compatibility boundary.',
|
|
247
247
|
});
|
|
248
248
|
export const SHORTLIST_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
|
|
249
249
|
id: 'browser.shortlist', kind: 'browser_store', schema: null, currentVersion: SHORTLIST_SCHEMA_VERSION,
|
|
@@ -285,14 +285,14 @@ export const BULK_SESSION_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
285
285
|
supportedVersions: BULK_SESSION_BROWSER_SUPPORTED_VERSIONS, acceptsUnversionedLegacy: false,
|
|
286
286
|
futureVersionBehavior: 'preserve_without_write', migration: 'normalize_to_current', writeSemantics: 'normalized_rewrite',
|
|
287
287
|
byteBudget: MAX_BULK_SESSION_STORE_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
|
|
288
|
-
note: 'Public
|
|
288
|
+
note: 'Public schemas 4 and 5 remain readable with their original observation provenance. Schema 6 retains explicit page and favicon collection quality for saved evidence handoffs; future versions are preserved without rewrite.',
|
|
289
289
|
});
|
|
290
290
|
export const INVESTIGATION_TEMPLATE_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
|
|
291
291
|
id: 'browser.investigation-templates', kind: 'browser_store', schema: null,
|
|
292
292
|
currentVersion: INVESTIGATION_TEMPLATE_VERSION, supportedVersions: INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS,
|
|
293
|
-
acceptsUnversionedLegacy: false, futureVersionBehavior: 'preserve_without_write', migration: '
|
|
293
|
+
acceptsUnversionedLegacy: false, futureVersionBehavior: 'preserve_without_write', migration: 'normalize_to_current',
|
|
294
294
|
writeSemantics: 'normalized_rewrite', byteBudget: MAX_INVESTIGATION_TEMPLATE_STORE_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
|
|
295
|
-
note: '
|
|
295
|
+
note: 'Public version 2 remains readable. Version 3 retains optional lesson-revision provenance without Case identities or note text. Guidance remains bound to built-in stages and cannot execute actions or remove mandatory request gates.',
|
|
296
296
|
});
|
|
297
297
|
export const BULK_REVIEW_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
|
|
298
298
|
id: 'browser.bulk-review', kind: 'browser_store', schema: null, currentVersion: BULK_REVIEW_SCHEMA_VERSION,
|
|
@@ -318,9 +318,9 @@ export const CAMPAIGN_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
318
318
|
export const WATCHLIST_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
|
|
319
319
|
id: 'export.watchlists', kind: 'export', schema: WATCHLIST_SCHEMA, currentVersion: WATCHLIST_SCHEMA_VERSION,
|
|
320
320
|
supportedVersions: WATCHLIST_EXPORT_SUPPORTED_VERSIONS, acceptsUnversionedLegacy: false,
|
|
321
|
-
futureVersionBehavior: 'reject', migration: '
|
|
321
|
+
futureVersionBehavior: 'reject', migration: 'normalize_to_current', writeSemantics: 'non_destructive_merge',
|
|
322
322
|
byteBudget: MAX_WATCHLIST_IMPORT_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
|
|
323
|
-
note: '
|
|
323
|
+
note: 'Public version 2 and current version 3 merge non-destructively. Historical records have unknown collection quality; incomplete current checks cannot erase a usable baseline.',
|
|
324
324
|
});
|
|
325
325
|
export const SHORTLIST_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
|
|
326
326
|
id: 'export.shortlist', kind: 'export', schema: SHORTLIST_SCHEMA, currentVersion: SHORTLIST_SCHEMA_VERSION,
|
|
@@ -355,14 +355,14 @@ export const BULK_SESSION_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
355
355
|
supportedVersions: SUPPORTED_BULK_SESSION_SCHEMA_VERSIONS, acceptsUnversionedLegacy: false,
|
|
356
356
|
futureVersionBehavior: 'reject', migration: 'normalize_to_current', writeSemantics: 'non_destructive_merge',
|
|
357
357
|
byteBudget: MAX_BULK_SESSION_STORE_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
|
|
358
|
-
note: 'Public
|
|
358
|
+
note: 'Public schemas 4 and 5 migrate without invented observation times or collection outcomes. Current exports retain source-qualified relationship evidence, bounded profile context and collection quality.',
|
|
359
359
|
});
|
|
360
360
|
export const INVESTIGATION_TEMPLATE_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
|
|
361
361
|
id: 'export.investigation-templates', kind: 'export', schema: INVESTIGATION_TEMPLATE_SCHEMA,
|
|
362
362
|
currentVersion: INVESTIGATION_TEMPLATE_VERSION, supportedVersions: INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS,
|
|
363
|
-
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: '
|
|
363
|
+
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'normalize_to_current',
|
|
364
364
|
writeSemantics: 'non_destructive_merge', byteBudget: MAX_INVESTIGATION_TEMPLATE_IMPORT_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
|
|
365
|
-
note: '
|
|
365
|
+
note: 'Version 3 exports authored guidance and optional content-hash lesson provenance. Public version 2 remains readable; future versions fail closed.',
|
|
366
366
|
});
|
|
367
367
|
export const BULK_REVIEW_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
|
|
368
368
|
id: 'export.bulk-review', kind: 'export', schema: BULK_REVIEW_SCHEMA, currentVersion: BULK_REVIEW_SCHEMA_VERSION,
|
|
@@ -649,6 +649,36 @@ const WORKSPACE_LIFECYCLE_DEFINITIONS = Object.freeze([
|
|
|
649
649
|
},
|
|
650
650
|
]);
|
|
651
651
|
const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
|
|
652
|
+
{
|
|
653
|
+
id: 'workspace.browser.watchlist.v4', path: 'test/fixtures/workspace-lifecycle/browser-watchlist-v4.json',
|
|
654
|
+
bytes: 76, sha256: '059930ee54d1bc65ac68ef6d3a8ac8cd8e53f682fe9a857512e2b6c57e93d5fe',
|
|
655
|
+
schema: 'whoisleuth.browser.watchlist-store', version: 4,
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
id: 'workspace.portable.watchlist.v4', path: 'test/fixtures/workspace-lifecycle/portable-watchlist-v4.json',
|
|
659
|
+
bytes: 120, sha256: '496f1ec39add1d3fe080ad8f9f991639cfbbe659ddb1c58932ac6ff563ce32aa',
|
|
660
|
+
schema: 'whoisleuth.watchlists', version: 4,
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
id: 'workspace.browser.watchlist.v3', path: 'test/fixtures/workspace-lifecycle/browser-watchlist-v3.json',
|
|
664
|
+
bytes: 76, sha256: '740246c1666c332ae907a5b40364e03a4f008484fce7b1c6dce8082a6ac7b654',
|
|
665
|
+
schema: 'whoisleuth.browser.watchlist-store', version: 3,
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
id: 'workspace.portable.watchlist.v3', path: 'test/fixtures/workspace-lifecycle/portable-watchlist-v3.json',
|
|
669
|
+
bytes: 120, sha256: 'ad07c8a530c369b49192bd3d8dde1b17c58b737da4e3d00a834a64be6aa429bf',
|
|
670
|
+
schema: 'whoisleuth.watchlists', version: 3,
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
id: 'workspace.browser.bulk.v6', path: 'test/fixtures/workspace-lifecycle/browser-bulk-v6.json',
|
|
674
|
+
bytes: 77, sha256: '811ef0b05415f19dc7bd940a4786d53a6398121cbb7cd060fcd6d21ed562487b',
|
|
675
|
+
schema: 'whoisleuth.browser.bulk-session-store', version: 6,
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
id: 'workspace.portable.bulk.v6', path: 'test/fixtures/workspace-lifecycle/portable-bulk-v6.json',
|
|
679
|
+
bytes: 338, sha256: 'ec7f6445cf42c013163ea835d5f919213b69978124d6883c1f10d62a0868235f',
|
|
680
|
+
schema: 'whoisleuth.bulk-sessions', version: 6,
|
|
681
|
+
},
|
|
652
682
|
{
|
|
653
683
|
"id": "workspace.browser.brand.v6",
|
|
654
684
|
"path": "test/fixtures/workspace-lifecycle/browser-brand-v6.json",
|
|
@@ -761,6 +791,14 @@ const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
|
|
|
761
791
|
"schema": "whoisleuth.browser.investigation-template-store",
|
|
762
792
|
"version": 2
|
|
763
793
|
},
|
|
794
|
+
{
|
|
795
|
+
"id": "workspace.browser.template.v3",
|
|
796
|
+
"path": "test/fixtures/workspace-lifecycle/browser-template-v3.json",
|
|
797
|
+
"bytes": 88,
|
|
798
|
+
"sha256": "70a6dfe2c27c75d86c74a9dfe05148c3186be68191002cbea8c18816a64792c9",
|
|
799
|
+
"schema": "whoisleuth.browser.investigation-template-store",
|
|
800
|
+
"version": 3
|
|
801
|
+
},
|
|
764
802
|
{
|
|
765
803
|
"id": "workspace.browser.watchlist.v2",
|
|
766
804
|
"path": "test/fixtures/workspace-lifecycle/browser-watchlist-v2.json",
|
|
@@ -889,6 +927,14 @@ const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
|
|
|
889
927
|
"schema": "whoisleuth.investigation-templates",
|
|
890
928
|
"version": 2
|
|
891
929
|
},
|
|
930
|
+
{
|
|
931
|
+
"id": "workspace.portable.template.v3",
|
|
932
|
+
"path": "test/fixtures/workspace-lifecycle/portable-template-v3.json",
|
|
933
|
+
"bytes": 471,
|
|
934
|
+
"sha256": "6b211af44661594337731767cd212beae305d81e03e090a783633eebaa62d03e",
|
|
935
|
+
"schema": "whoisleuth.investigation-templates",
|
|
936
|
+
"version": 3
|
|
937
|
+
},
|
|
892
938
|
{
|
|
893
939
|
"id": "workspace.portable.watchlist.v2",
|
|
894
940
|
"path": "test/fixtures/workspace-lifecycle/portable-watchlist-v2.json",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const STATES = new Set(['complete', 'partial', 'unavailable', 'not_collected', 'unknown']);
|
|
2
|
+
export function isWebCollectionQuality(value) {
|
|
3
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
4
|
+
return false;
|
|
5
|
+
const prototype = Object.getPrototypeOf(value);
|
|
6
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
7
|
+
return false;
|
|
8
|
+
const record = value;
|
|
9
|
+
const keys = Object.keys(record);
|
|
10
|
+
if (keys.length !== 4 || keys.some(key => !['version', 'page', 'favicon', 'combined'].includes(key)
|
|
11
|
+
|| !Object.hasOwn(Object.getOwnPropertyDescriptor(record, key) ?? {}, 'value')))
|
|
12
|
+
return false;
|
|
13
|
+
return record.version === 1
|
|
14
|
+
&& typeof record.page === 'string' && STATES.has(record.page)
|
|
15
|
+
&& typeof record.favicon === 'string' && STATES.has(record.favicon)
|
|
16
|
+
&& typeof record.combined === 'string' && STATES.has(record.combined)
|
|
17
|
+
&& (record.combined !== 'complete' || (record.page === 'complete' && record.favicon === 'complete'));
|
|
18
|
+
}
|
|
19
|
+
export function normalizeWebCollectionQuality(value, depth) {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (!isWebCollectionQuality(value))
|
|
23
|
+
throw new TypeError('Unsupported or invalid web collection quality.');
|
|
24
|
+
if (depth === 'fast')
|
|
25
|
+
return capturedWebCollectionQuality('not_collected', 'not_collected');
|
|
26
|
+
return { version: 1, page: value.page, favicon: value.favicon, combined: value.combined };
|
|
27
|
+
}
|
|
28
|
+
export function capturedWebCollectionQuality(page, favicon) {
|
|
29
|
+
const combined = page === 'complete' && favicon === 'complete' ? 'complete'
|
|
30
|
+
: page === favicon ? page : 'partial';
|
|
31
|
+
return { version: 1, page, favicon, combined };
|
|
32
|
+
}
|
|
33
|
+
export function webCollectionQualityForCapture(value, depth) {
|
|
34
|
+
if (depth !== 'deep')
|
|
35
|
+
return capturedWebCollectionQuality('not_collected', 'not_collected');
|
|
36
|
+
return normalizeWebCollectionQuality(value) ?? capturedWebCollectionQuality('unknown', 'unknown');
|
|
37
|
+
}
|
|
38
|
+
const PAGE_FIELDS = new Set(['activityStatus', 'pageTitle', 'hasPasswordField', 'hasExternalFormAction', 'phishingLanguageMatch', 'pageBaselineMatch']);
|
|
39
|
+
const FAVICON_FIELDS = new Set(['faviconHash', 'faviconPHash', 'faviconMatch', 'faviconNearMatch']);
|
|
40
|
+
const COMBINED_FIELDS = new Set(['riskScore', 'riskFactors', 'riskModelVersion', 'opportunityScore', 'opportunityFactors', 'reusesOfficialAssets']);
|
|
41
|
+
/** Missing historical quality never proves that a false/null value was observed. */
|
|
42
|
+
export function webCollectionAllowsComparison(field, quality, depth) {
|
|
43
|
+
if (PAGE_FIELDS.has(field))
|
|
44
|
+
return quality?.page === 'complete';
|
|
45
|
+
if (FAVICON_FIELDS.has(field))
|
|
46
|
+
return quality?.favicon === 'complete';
|
|
47
|
+
if (COMBINED_FIELDS.has(field) && depth === 'deep')
|
|
48
|
+
return quality?.combined === 'complete';
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
/** Retain the measured score while qualifying incomplete inputs at presentation. */
|
|
52
|
+
export function webCollectionScoreLimitation(quality, depth) {
|
|
53
|
+
if (depth === 'unknown')
|
|
54
|
+
return 'Collection depth is unknown; score is not comparable.';
|
|
55
|
+
return webCollectionAllowsComparison('riskScore', quality, depth)
|
|
56
|
+
? null : 'Incomplete or unknown web collection; score is not comparable.';
|
|
57
|
+
}
|
|
58
|
+
/** An incomplete current attempt cannot erase the last comparable baseline. */
|
|
59
|
+
export function mergeWebCollectionQuality(previous, current) {
|
|
60
|
+
if (!previous && !current)
|
|
61
|
+
return undefined;
|
|
62
|
+
return { version: 1,
|
|
63
|
+
page: current?.page === 'complete' ? 'complete' : previous?.page ?? current?.page ?? 'unknown',
|
|
64
|
+
favicon: current?.favicon === 'complete' ? 'complete' : previous?.favicon ?? current?.favicon ?? 'unknown',
|
|
65
|
+
combined: current?.combined === 'complete' ? 'complete' : previous?.combined ?? current?.combined ?? 'unknown',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { MAX_LOOKUP_DNS_RECORDS_PER_TYPE } from "../analysis/lookup-network-evidence-bounds.mjs";
|
|
2
|
+
import { addressValue, canonicalIpAddress } from "../contracts/ip-address.mjs";
|
|
3
|
+
/** Fixed values retained in Observation v1's existing diagnostic detail slot. */
|
|
4
|
+
export const DNS_QUERY_OUTCOMES = Object.freeze({
|
|
5
|
+
records: { status: 'success', label: 'Records returned' },
|
|
6
|
+
empty_answer: { status: 'not_found', label: 'No records returned' },
|
|
7
|
+
no_data: { status: 'not_found', label: 'No data for this record type (NODATA)' },
|
|
8
|
+
name_not_found: { status: 'not_found', label: 'Name not found by resolver' },
|
|
9
|
+
timeout: { status: 'error', label: 'DNS query timed out' },
|
|
10
|
+
server_failure: { status: 'error', label: 'DNS server failed to answer (SERVFAIL)' },
|
|
11
|
+
refused: { status: 'error', label: 'DNS query refused' },
|
|
12
|
+
invalid_response: { status: 'error', label: 'DNS answer could not be read completely' },
|
|
13
|
+
error: { status: 'error', label: 'DNS query failed' },
|
|
14
|
+
skipped: { status: 'skipped', label: 'Not queried' },
|
|
15
|
+
});
|
|
16
|
+
function record(value) {
|
|
17
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
18
|
+
? value : {};
|
|
19
|
+
}
|
|
20
|
+
/** Legacy not_found diagnostics do not identify which negative answer occurred. */
|
|
21
|
+
export function readDnsQueryOutcome(value) {
|
|
22
|
+
const diagnostic = record(value);
|
|
23
|
+
const detail = diagnostic.detail;
|
|
24
|
+
return typeof detail === 'string' && Object.hasOwn(DNS_QUERY_OUTCOMES, detail)
|
|
25
|
+
&& DNS_QUERY_OUTCOMES[detail].status === diagnostic.status
|
|
26
|
+
? detail : null;
|
|
27
|
+
}
|
|
28
|
+
export function dnsQueryFailureOutcome(code) {
|
|
29
|
+
switch (code) {
|
|
30
|
+
case 'ENODATA': return 'no_data';
|
|
31
|
+
case 'ENOTFOUND': return 'name_not_found';
|
|
32
|
+
case 'ETIMEOUT': return 'timeout';
|
|
33
|
+
case 'ESERVFAIL': return 'server_failure';
|
|
34
|
+
case 'EREFUSED': return 'refused';
|
|
35
|
+
case 'EBADRESP': return 'invalid_response';
|
|
36
|
+
default: return 'error';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** One family, one source state. Unrelated mail/delegation failures cannot erase it. */
|
|
40
|
+
export function dnsAddressFamilyEvidence(dnsValue, family) {
|
|
41
|
+
const dns = record(dnsValue);
|
|
42
|
+
const diagnostic = record(record(dns.diagnostics)[family]);
|
|
43
|
+
const raw = record(dns.records)[family];
|
|
44
|
+
const candidates = Array.isArray(raw) ? raw.slice(0, MAX_LOOKUP_DNS_RECORDS_PER_TYPE) : [];
|
|
45
|
+
const addresses = candidates.flatMap(value => {
|
|
46
|
+
const address = canonicalIpAddress(value);
|
|
47
|
+
return address && addressValue(address)?.family === (family === 'a' ? 4 : 6) ? [address] : [];
|
|
48
|
+
});
|
|
49
|
+
const malformed = raw !== undefined && !Array.isArray(raw)
|
|
50
|
+
|| candidates.length !== addresses.length;
|
|
51
|
+
const truncated = diagnostic.truncated === true
|
|
52
|
+
|| typeof diagnostic.discarded === 'number' && diagnostic.discarded > 0
|
|
53
|
+
|| Array.isArray(raw) && raw.length > candidates.length;
|
|
54
|
+
const outcome = readDnsQueryOutcome(diagnostic);
|
|
55
|
+
const inconsistent = outcome === 'records' && !addresses.length
|
|
56
|
+
|| outcome !== null && outcome !== 'records' && addresses.length > 0;
|
|
57
|
+
const complete = !malformed && !truncated && !inconsistent && Array.isArray(raw)
|
|
58
|
+
&& outcome !== null && DNS_QUERY_OUTCOMES[outcome].status !== 'error' && outcome !== 'skipped';
|
|
59
|
+
const state = malformed || inconsistent ? 'error'
|
|
60
|
+
: truncated ? 'partial'
|
|
61
|
+
: outcome ? DNS_QUERY_OUTCOMES[outcome].status
|
|
62
|
+
: diagnostic.status === 'error' ? 'error'
|
|
63
|
+
: diagnostic.status === 'skipped' || dns.status === 'skipped' ? 'skipped' : 'unknown';
|
|
64
|
+
const value = addresses.length ? [...new Set(addresses)].sort().join(' · ')
|
|
65
|
+
: malformed || inconsistent ? DNS_QUERY_OUTCOMES.invalid_response.label
|
|
66
|
+
: truncated ? 'DNS answer incomplete'
|
|
67
|
+
: outcome ? DNS_QUERY_OUTCOMES[outcome].label
|
|
68
|
+
: state === 'error' ? DNS_QUERY_OUTCOMES.error.label
|
|
69
|
+
: state === 'skipped' ? DNS_QUERY_OUTCOMES.skipped.label
|
|
70
|
+
: diagnostic.status === 'not_found' ? 'Negative DNS outcome not recorded' : null;
|
|
71
|
+
return { outcome, value, state, complete, truncated };
|
|
72
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Single plain recipient admission for manually opened drafts, not delivery validation.
|
|
2
|
+
const SINGLE_ADDRESS = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/u;
|
|
3
|
+
export function emailRecipient(value) {
|
|
4
|
+
if (typeof value !== 'string' || value.length > 320 || /[\u0000-\u001f\u007f]/u.test(value))
|
|
5
|
+
return null;
|
|
6
|
+
const address = value.trim();
|
|
7
|
+
return SINGLE_ADDRESS.test(address) ? address : null;
|
|
8
|
+
}
|
|
9
|
+
/** Encode the recipient independently; upstream text never becomes a header. */
|
|
10
|
+
export function recipientMailto(value, headers) {
|
|
11
|
+
const address = emailRecipient(value);
|
|
12
|
+
if (!address)
|
|
13
|
+
return null;
|
|
14
|
+
const separator = address.indexOf('@');
|
|
15
|
+
const recipient = `${encodeURIComponent(address.slice(0, separator))}@${encodeURIComponent(address.slice(separator + 1))}`;
|
|
16
|
+
const query = headers ? `?subject=${encodeURIComponent(headers.subject)}&body=${encodeURIComponent(headers.body)}` : '';
|
|
17
|
+
return `mailto:${recipient}${query}`;
|
|
18
|
+
}
|
|
@@ -5,6 +5,10 @@ import { MAX_OUTBOUND_HTTP_URL_CHARACTERS } from "../contracts/http-url.mjs";
|
|
|
5
5
|
export const MAX_LOOKUP_INPUT_CHARACTERS = 2 * 1024 * 1024;
|
|
6
6
|
const URL_SCHEME = /^[a-z][a-z\d+.-]*:\/\//iu;
|
|
7
7
|
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
8
|
+
/** Lexical recognition only; protocol, credentials and target admission remain separate. */
|
|
9
|
+
export function hasLookupUrlScheme(value) {
|
|
10
|
+
return URL_SCHEME.test(value);
|
|
11
|
+
}
|
|
8
12
|
export function validWebObservationMode(value) {
|
|
9
13
|
return value === undefined || value === 'selected_url';
|
|
10
14
|
}
|
|
@@ -76,7 +80,7 @@ export function prepareLookupCollectionTarget(value) {
|
|
|
76
80
|
const raw = value.trim();
|
|
77
81
|
if (!raw)
|
|
78
82
|
throw new TypeError('Enter a domain, IP address or ASN.');
|
|
79
|
-
const absolute =
|
|
83
|
+
const absolute = hasLookupUrlScheme(raw);
|
|
80
84
|
// An unbracketed IPv6 literal is not a URL with a port. Use the same URL
|
|
81
85
|
// parser to admit its syntax, then leave address safety and ASN bounds to
|
|
82
86
|
// the classifier; malformed colon-separated text must not become a request.
|
|
@@ -106,6 +106,8 @@ const REGISTERED_DIAGNOSTIC_KEYS = [
|
|
|
106
106
|
'queriedAuthorityCount',
|
|
107
107
|
'redirectCount',
|
|
108
108
|
'referencesExamined',
|
|
109
|
+
'referenceSignatureTimedOut',
|
|
110
|
+
'referenceSignatureUnavailable',
|
|
109
111
|
'rejectedRows',
|
|
110
112
|
'relationshipTagsExamined',
|
|
111
113
|
'relationshipUrlsDiscarded',
|