@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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 +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { MAX_WORKSPACE_INPUT_ARRAY_LENGTH, MAX_WORKSPACE_INPUT_GRAPH_DEPTH, MAX_WORKSPACE_INPUT_GRAPH_NODES, MAX_WORKSPACE_INPUT_OBJECT_KEYS, MAX_WORKSPACE_INPUT_STRING_CODE_UNITS, } from "../contracts/workspace-portability.mjs";
|
|
2
|
+
function inputError(label, detail) {
|
|
3
|
+
return new TypeError(`${label} must be bounded ordinary JSON data; ${detail}.`);
|
|
4
|
+
}
|
|
5
|
+
function ownDataDescriptors(value, label) {
|
|
6
|
+
let keys;
|
|
7
|
+
try {
|
|
8
|
+
keys = Reflect.ownKeys(value);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw inputError(label, 'its property keys could not be inspected safely');
|
|
12
|
+
}
|
|
13
|
+
if (keys.some((key) => typeof key !== 'string')) {
|
|
14
|
+
throw inputError(label, 'symbol keys are not supported');
|
|
15
|
+
}
|
|
16
|
+
return keys.map((key) => {
|
|
17
|
+
let descriptor;
|
|
18
|
+
try {
|
|
19
|
+
descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw inputError(label, 'a property descriptor could not be inspected safely');
|
|
23
|
+
}
|
|
24
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
25
|
+
throw inputError(label, 'accessor properties are not supported');
|
|
26
|
+
}
|
|
27
|
+
return [key, descriptor];
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function ordinaryWorkspaceRecord(value, label = 'Workspace input') {
|
|
31
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
32
|
+
return null;
|
|
33
|
+
let prototype;
|
|
34
|
+
try {
|
|
35
|
+
prototype = Object.getPrototypeOf(value);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw inputError(label, 'its prototype could not be inspected safely');
|
|
39
|
+
}
|
|
40
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
41
|
+
throw inputError(label, 'custom object prototypes are not supported');
|
|
42
|
+
}
|
|
43
|
+
const descriptors = ownDataDescriptors(value, label);
|
|
44
|
+
if (descriptors.length > MAX_WORKSPACE_INPUT_OBJECT_KEYS) {
|
|
45
|
+
throw inputError(label, 'an object exceeds the key ceiling');
|
|
46
|
+
}
|
|
47
|
+
if (descriptors.some(([, descriptor]) => !descriptor.enumerable)) {
|
|
48
|
+
throw inputError(label, 'non-enumerable object fields are not supported');
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
export function assertWorkspaceInputGraph(value, label = 'Workspace input') {
|
|
53
|
+
const pending = [{ value, depth: 0, label }];
|
|
54
|
+
const seen = new WeakSet();
|
|
55
|
+
let nodes = 0;
|
|
56
|
+
let stringCodeUnits = 0;
|
|
57
|
+
while (pending.length) {
|
|
58
|
+
const current = pending.pop();
|
|
59
|
+
const candidate = current.value;
|
|
60
|
+
if (typeof candidate === 'string') {
|
|
61
|
+
stringCodeUnits += candidate.length;
|
|
62
|
+
if (stringCodeUnits > MAX_WORKSPACE_INPUT_STRING_CODE_UNITS) {
|
|
63
|
+
throw inputError(label, 'aggregate text exceeds the string ceiling');
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (candidate === null
|
|
68
|
+
|| candidate === undefined
|
|
69
|
+
|| typeof candidate === 'boolean'
|
|
70
|
+
|| typeof candidate === 'number')
|
|
71
|
+
continue;
|
|
72
|
+
if (typeof candidate !== 'object') {
|
|
73
|
+
throw inputError(current.label, 'non-JSON values are not supported');
|
|
74
|
+
}
|
|
75
|
+
if (current.depth > MAX_WORKSPACE_INPUT_GRAPH_DEPTH) {
|
|
76
|
+
throw inputError(label, 'the nesting depth exceeds the graph ceiling');
|
|
77
|
+
}
|
|
78
|
+
nodes += 1;
|
|
79
|
+
if (nodes > MAX_WORKSPACE_INPUT_GRAPH_NODES) {
|
|
80
|
+
throw inputError(label, 'the graph exceeds the node ceiling');
|
|
81
|
+
}
|
|
82
|
+
if (seen.has(candidate))
|
|
83
|
+
continue;
|
|
84
|
+
seen.add(candidate);
|
|
85
|
+
if (Array.isArray(candidate)) {
|
|
86
|
+
let prototype;
|
|
87
|
+
try {
|
|
88
|
+
prototype = Object.getPrototypeOf(candidate);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw inputError(current.label, 'its array prototype could not be inspected safely');
|
|
92
|
+
}
|
|
93
|
+
if (prototype !== Array.prototype) {
|
|
94
|
+
throw inputError(current.label, 'custom array prototypes are not supported');
|
|
95
|
+
}
|
|
96
|
+
const descriptors = ownDataDescriptors(candidate, current.label);
|
|
97
|
+
const lengthDescriptor = descriptors.find(([key]) => key === 'length')?.[1];
|
|
98
|
+
const length = lengthDescriptor?.value;
|
|
99
|
+
if (!Number.isSafeInteger(length)
|
|
100
|
+
|| Number(length) < 0
|
|
101
|
+
|| Number(length) > MAX_WORKSPACE_INPUT_ARRAY_LENGTH) {
|
|
102
|
+
throw inputError(current.label, 'an array exceeds the length ceiling');
|
|
103
|
+
}
|
|
104
|
+
if (descriptors.length !== Number(length) + 1) {
|
|
105
|
+
throw inputError(current.label, 'sparse arrays and custom array fields are not supported');
|
|
106
|
+
}
|
|
107
|
+
const byKey = new Map(descriptors);
|
|
108
|
+
for (let index = 0; index < Number(length); index += 1) {
|
|
109
|
+
const descriptor = byKey.get(String(index));
|
|
110
|
+
if (!descriptor?.enumerable) {
|
|
111
|
+
throw inputError(current.label, 'sparse or non-enumerable array entries are not supported');
|
|
112
|
+
}
|
|
113
|
+
pending.push({
|
|
114
|
+
value: descriptor.value,
|
|
115
|
+
depth: current.depth + 1,
|
|
116
|
+
label: `${current.label}[${index}]`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const record = ordinaryWorkspaceRecord(candidate, current.label);
|
|
122
|
+
const descriptors = ownDataDescriptors(record, current.label);
|
|
123
|
+
for (const [key, descriptor] of descriptors) {
|
|
124
|
+
pending.push({
|
|
125
|
+
value: descriptor.value,
|
|
126
|
+
depth: current.depth + 1,
|
|
127
|
+
label: `${current.label}.${key}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function assertWorkspacePortableVersion(value, currentVersion, label) {
|
|
133
|
+
if (!Number.isSafeInteger(currentVersion) || currentVersion < 1) {
|
|
134
|
+
throw new TypeError(`${label} current version is invalid.`);
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(value)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const record = ordinaryWorkspaceRecord(value, label);
|
|
140
|
+
if (!record)
|
|
141
|
+
throw inputError(label, 'the root must be an object');
|
|
142
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, 'version');
|
|
143
|
+
if (!descriptor)
|
|
144
|
+
return null;
|
|
145
|
+
if (!('value' in descriptor))
|
|
146
|
+
throw inputError(label, 'the version must be a positive safe integer');
|
|
147
|
+
if (typeof descriptor.value === 'number'
|
|
148
|
+
&& Number.isFinite(descriptor.value)
|
|
149
|
+
&& descriptor.value > currentVersion) {
|
|
150
|
+
return descriptor.value;
|
|
151
|
+
}
|
|
152
|
+
if (!Number.isSafeInteger(descriptor.value) || Number(descriptor.value) < 1) {
|
|
153
|
+
throw inputError(label, 'the version must be a positive safe integer');
|
|
154
|
+
}
|
|
155
|
+
const version = Number(descriptor.value);
|
|
156
|
+
return version;
|
|
157
|
+
}
|
|
158
|
+
export function assertWorkspaceDeclaredVersion(value, label) {
|
|
159
|
+
if (Array.isArray(value) || value === null || value === undefined)
|
|
160
|
+
return;
|
|
161
|
+
const record = ordinaryWorkspaceRecord(value, label);
|
|
162
|
+
if (!record)
|
|
163
|
+
return;
|
|
164
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, 'version');
|
|
165
|
+
if (!descriptor)
|
|
166
|
+
return;
|
|
167
|
+
if (!('value' in descriptor)
|
|
168
|
+
|| !Number.isSafeInteger(descriptor.value)
|
|
169
|
+
|| Number(descriptor.value) < 1) {
|
|
170
|
+
throw inputError(label, 'a declared version must be a positive safe integer');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { normalizeDomain } from "
|
|
1
|
+
import { normalizeDomain } from "../cases/case-model.mjs";
|
|
2
2
|
import { parse } from 'tldts';
|
|
3
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
4
|
export const INVESTIGATION_GUIDE_SCHEMA = 'whoisleuth.investigation-recipe';
|
|
4
|
-
export const INVESTIGATION_GUIDE_VERSION =
|
|
5
|
-
export const
|
|
6
|
-
export const
|
|
7
|
-
export const INVESTIGATION_GUIDE_SUPPORTED_VERSIONS = [2, 3, INVESTIGATION_GUIDE_VERSION];
|
|
5
|
+
export const INVESTIGATION_GUIDE_VERSION = 5;
|
|
6
|
+
export const INVESTIGATION_GUIDE_EXPORT_VERSION = 4;
|
|
7
|
+
export const INVESTIGATION_GUIDE_SUPPORTED_VERSIONS = [INVESTIGATION_GUIDE_VERSION];
|
|
8
8
|
export const INVESTIGATION_GUIDE_EXPORT_SCHEMA = 'whoisleuth.investigation-recipe-summary';
|
|
9
9
|
export const MAX_INVESTIGATION_GUIDE_DOMAIN_LENGTH = 253;
|
|
10
10
|
export const MAX_INVESTIGATION_GUIDE_REVIEW_DOMAINS = 25;
|
|
@@ -91,15 +91,45 @@ export const INVESTIGATION_RECIPES = Object.freeze([
|
|
|
91
91
|
stage('monitor', 'monitor', 'Record disposition', 'Review the bounded domain set carried from Bulk and create or update only the cases that need an analyst decision or follow-up plan.', 'A bounded review queue plus case records with dispositions, notes, evidence history, and optional monitoring intent.', 'Local case editing makes no request. Rescans and hosted-monitor changes remain separate explicit actions.', 'Review source provenance, limitations, and any scoring explanation before deciding.', 'The analyst records the required dispositions or explicitly skips retention; the recipe never decides automatically.', ['Review the domains carried from Bulk and open only the cases that need retention.', 'Record a disposition and any concise evidence-based follow-up for each retained case.', 'Mark the step reviewed when the retained cases reflect your decision.'], false),
|
|
92
92
|
]),
|
|
93
93
|
}),
|
|
94
|
+
Object.freeze({
|
|
95
|
+
id: 'credential_impersonation_response',
|
|
96
|
+
label: 'Credential impersonation response',
|
|
97
|
+
summary: 'Review observed impersonation evidence, confirm the affected brand boundary, and prepare a local response packet from explicitly recorded routes.',
|
|
98
|
+
targetLabel: 'Domain under review',
|
|
99
|
+
stages: Object.freeze([
|
|
100
|
+
stage('lookup', 'lookup', 'Review impersonation evidence', 'Collect separately attributed deep evidence for the domain without treating visual or naming similarity as attribution.', 'Current registry, DNS, certificate, HTTP, page-identity, and source-health observations where supported.', 'Deep Lookup can contact several public services within explicit deadlines and response caps.', 'Confirm the target is in scope and do not enter a credential-bearing URL or secret.', 'Observed facts and missing sources are reviewed without making an ownership, intent, or harm conclusion.', ['Run the pre-filled Lookup in Deep mode.', 'Review exact page, redirect, certificate, and registration observations with their source states.', 'Create or update a Case only when the retained observations need reviewed follow-up.'], true),
|
|
101
|
+
stage('brands', 'brands', 'Confirm affected brand boundary', 'Review the saved Brand Profile that defines official and allowlisted references; do not infer an association from similarity.', 'An analyst-selected Brand Profile boundary, or an explicit decision to continue without one.', 'Local-only. Opening Brands makes no analysis request.', 'Know which saved profile, if any, is relevant to the affected party.', 'The relevant profile boundary is reviewed, or the stage is marked partial or skipped without creating an association.', ['Open the saved Brand Profiles list.', 'Review the official references and allowlists for the affected party.', 'Return to the guide without adding a profile association unless you deliberately selected one in Monitor.'], false),
|
|
102
|
+
stage('monitor', 'monitor', 'Prepare reviewed response', 'Open the Case response workspace, record only analyst-selected published recipient routes, and review packet preflight.', 'A Case action with recipient provenance and limitations plus an optional local draft packet that remains unsubmitted.', 'Local Case edits and packet generation make no request. WHOISleuth never sends the packet.', 'Retain supporting facts and reasoning before recording an external route; confirm the route manually.', 'The reviewed action and packet preflight are recorded, or the stage is partial or skipped without implying submission.', ['Open or create the pre-filled Case, then review its evidence and reasoning.', 'Record an analyst-selected published route with its source and limitations; do not claim it is reachable.', 'Open response preflight and prepare a local draft only if its required facts are complete.'], false),
|
|
103
|
+
]),
|
|
104
|
+
}),
|
|
105
|
+
Object.freeze({
|
|
106
|
+
id: 'mail_abuse_response',
|
|
107
|
+
label: 'Mail abuse response',
|
|
108
|
+
summary: 'Review official mail posture and observed domain evidence before preparing a source-qualified, unsent response packet.',
|
|
109
|
+
targetLabel: 'Domain under review',
|
|
110
|
+
stages: Object.freeze([
|
|
111
|
+
stage('brands', 'brands', 'Review official mail posture', 'Review the affected Brand Profile and retained mail posture without treating a missing control as proof of abuse.', 'Analyst-selected official-domain, mail, and Brand Profile context with explicit availability states.', 'Local-only unless the analyst separately starts a Brand posture refresh.', 'Identify the affected profile and distinguish official infrastructure from the domain under review.', 'Relevant retained posture is reviewed, or unavailable context is marked partial rather than absent.', ['Open the saved Brand Profiles list.', 'Review official MX, SPF, DMARC, and approved-domain context where retained.', 'Do not refresh or change a profile unless that separate action is necessary and approved.'], false),
|
|
112
|
+
stage('lookup', 'lookup', 'Review domain and mail evidence', 'Collect current deep evidence for the domain and keep DNS, mail, registration, and page observations separately attributed.', 'Authority-aware availability plus bounded DNS, mail, TLS, HTTP, and page observations where supported.', 'Deep Lookup can contact several public services within explicit deadlines and response caps.', 'Confirm the domain is in scope; email content and mailbox data are outside this workflow.', 'Current observations and source failures are reviewed without claiming message origin, sender control, or maliciousness.', ['Run the pre-filled Lookup in Deep mode.', 'Review MX, SPF, DMARC, registration, certificate, HTTP, and page source states.', 'Retain only the evidence needed for a reviewed Case.'], true),
|
|
113
|
+
stage('monitor', 'monitor', 'Prepare mail-abuse response', 'Record a reviewed recipient route and use Case packet preflight without sending or claiming delivery.', 'A bounded Case action and optional local response draft with exact recipient provenance and limitations.', 'Local Case edits and packet generation make no request. WHOISleuth never sends the packet.', 'Confirm the intended recipient and retain the observed facts and limitations that support review.', 'The current action state and packet preflight are recorded, or the stage remains partial without a delivery conclusion.', ['Open or create the pre-filled Case and review its retained evidence.', 'Record only a deliberately selected published route, its source, and known limitations.', 'Use the response preflight to prepare a local draft; submission remains a separate manual action recorded later.'], false),
|
|
114
|
+
]),
|
|
115
|
+
}),
|
|
116
|
+
Object.freeze({
|
|
117
|
+
id: 'domain_control_change_response',
|
|
118
|
+
label: 'Domain-control change response',
|
|
119
|
+
summary: 'Review retained control posture and current authority-aware evidence before documenting a bounded internal or provider response.',
|
|
120
|
+
targetLabel: 'Official domain',
|
|
121
|
+
stages: Object.freeze([
|
|
122
|
+
stage('brands', 'brands', 'Review retained control posture', 'Review the saved domain-control passport, posture matrix, and source limitations before interpreting a change.', 'Retained official-domain control observations with separately attributed registry, DNS, certificate, mail, and web states.', 'Local-only unless the analyst separately starts a posture refresh.', 'Confirm the domain belongs to the reviewed Brand Profile; a saved association is analyst-selected, not inferred.', 'The retained baseline and its completeness are understood, or the stage is partial when required context is unavailable.', ['Open the relevant saved Brand Profile.', 'Review the domain-control passport, portfolio matrix, and observation times.', 'Identify which source actually supports the suspected change and preserve unavailable states.'], false),
|
|
123
|
+
stage('lookup', 'lookup', 'Confirm current domain evidence', 'Collect a deliberate deep Lookup so current authority-aware evidence remains distinct from the retained baseline.', 'Current registry, registrar, DNS, certificate, HTTP, and page observations with explicit source health.', 'Deep Lookup can contact several public services within explicit deadlines and response caps.', 'Confirm a fresh collection is necessary and review its request implications.', 'The current result is reviewed against the retained baseline without converting incomplete evidence into removal or compromise.', ['Run the pre-filled Lookup in Deep mode.', 'Compare current observations with the retained control evidence source by source.', 'Record only facts and limitations that need a Case response.'], true),
|
|
124
|
+
stage('monitor', 'monitor', 'Document control-change response', 'Record an internal or provider action and review an optional response packet without automatic enforcement or submission.', 'A bounded Case action, due or follow-up dates, recipient provenance when external, and an optional unsubmitted packet.', 'Local Case edits and packet generation make no request. WHOISleuth never changes DNS, registrar, hosting, mail, or defensive systems.', 'Document supporting evidence and confirm any recipient route or internal owner manually.', 'The reviewed action state is recorded, or the stage is partial without a compromise, delivery, or remediation claim.', ['Open or create the pre-filled Case and retain the relevant comparison facts.', 'Record the internal owner or analyst-selected provider route, source, limitations, and follow-up dates.', 'Review packet preflight when a draft is useful; record submission or resolution only after it actually occurs.'], false),
|
|
125
|
+
]),
|
|
126
|
+
}),
|
|
94
127
|
]);
|
|
95
128
|
function record(value) {
|
|
96
129
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
97
130
|
}
|
|
98
131
|
function timestamp(value) {
|
|
99
|
-
|
|
100
|
-
return '';
|
|
101
|
-
const parsed = Date.parse(value);
|
|
102
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : '';
|
|
132
|
+
return normalizeExplicitIsoTimestamp(value) ?? '';
|
|
103
133
|
}
|
|
104
134
|
function nullableTimestamp(value) {
|
|
105
135
|
if (value === null || value === undefined || value === '')
|
|
@@ -239,10 +269,15 @@ export function investigationGuideHref(stageId, domain, recipeId = 'new_domain_t
|
|
|
239
269
|
return `/discover?q=${encodeURIComponent(registrableGuideDomain(normalized))}#discovery-seed`;
|
|
240
270
|
if (stageDefinition.workspace === 'bulk')
|
|
241
271
|
return `/bulk?investigation=${encodeURIComponent(normalized)}#domains`;
|
|
242
|
-
if (stageDefinition.workspace === 'monitor')
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
272
|
+
if (stageDefinition.workspace === 'monitor') {
|
|
273
|
+
const response = typeof recipeId === 'string' && recipeId.endsWith('_response') ? '&response=1' : '';
|
|
274
|
+
return `/monitor?view=cases&investigation=1${response}&domain=${encodeURIComponent(workingDomain)}#case-review-queue`;
|
|
275
|
+
}
|
|
276
|
+
if (stageDefinition.workspace === 'brands') {
|
|
277
|
+
return typeof recipeId === 'string' && recipeId.endsWith('_response')
|
|
278
|
+
? '/brands'
|
|
279
|
+
: `/brands?new=1&domain=${encodeURIComponent(normalized)}#official-domains`;
|
|
280
|
+
}
|
|
246
281
|
return stageDefinition.path;
|
|
247
282
|
}
|
|
248
283
|
export function investigationGuideApprovedHref(guide, stageId) {
|
|
@@ -285,39 +320,10 @@ export function createInvestigationGuide(domain, recipeId = 'new_domain_triage',
|
|
|
285
320
|
stages: stages.map((stageDefinition) => createStageProgress(stageDefinition, createdAt)),
|
|
286
321
|
};
|
|
287
322
|
}
|
|
288
|
-
function parseLegacyGuide(input) {
|
|
289
|
-
const domain = normalizeInvestigationGuideDomain(input.domain);
|
|
290
|
-
const createdAt = timestamp(input.createdAt);
|
|
291
|
-
const updatedAt = timestamp(input.updatedAt);
|
|
292
|
-
const recipe = investigationGuideRecipe('new_domain_triage');
|
|
293
|
-
if (!domain || !createdAt || !updatedAt || !recipe)
|
|
294
|
-
return null;
|
|
295
|
-
const opened = new Set((Array.isArray(input.visitedStages) ? input.visitedStages : [])
|
|
296
|
-
.slice(0, recipe.stages.length * 2)
|
|
297
|
-
.filter((value) => typeof value === 'string'));
|
|
298
|
-
return {
|
|
299
|
-
version: INVESTIGATION_GUIDE_VERSION,
|
|
300
|
-
recipeId: recipe.id,
|
|
301
|
-
template: null,
|
|
302
|
-
domain,
|
|
303
|
-
focusDomain: null,
|
|
304
|
-
reviewDomains: [domain],
|
|
305
|
-
reviewDomainsTruncated: false,
|
|
306
|
-
status: 'active',
|
|
307
|
-
createdAt,
|
|
308
|
-
updatedAt,
|
|
309
|
-
stages: recipe.stages.map((stageDefinition) => ({
|
|
310
|
-
...createStageProgress(stageDefinition, updatedAt),
|
|
311
|
-
openedAt: opened.has(stageDefinition.id) ? updatedAt : null,
|
|
312
|
-
})),
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
323
|
export function parseInvestigationGuide(value) {
|
|
316
324
|
const input = record(value);
|
|
317
325
|
if (!input)
|
|
318
326
|
return null;
|
|
319
|
-
if (input.version === INVESTIGATION_GUIDE_LEGACY_VERSION)
|
|
320
|
-
return parseLegacyGuide(input);
|
|
321
327
|
if (!INVESTIGATION_GUIDE_SUPPORTED_VERSIONS.includes(input.version))
|
|
322
328
|
return null;
|
|
323
329
|
const recipe = investigationGuideRecipe(input.recipeId);
|
|
@@ -326,11 +332,10 @@ export function parseInvestigationGuide(value) {
|
|
|
326
332
|
const updatedAt = timestamp(input.updatedAt);
|
|
327
333
|
if (!recipe || !domain || !createdAt || !updatedAt)
|
|
328
334
|
return null;
|
|
329
|
-
const
|
|
330
|
-
const template = supportsTemplate && input.template !== null && input.template !== undefined
|
|
335
|
+
const template = input.template !== null && input.template !== undefined
|
|
331
336
|
? normalizeInvestigationGuideTemplateSnapshot(input.template, recipe.id)
|
|
332
337
|
: null;
|
|
333
|
-
if (
|
|
338
|
+
if (input.template !== null && input.template !== undefined && !template)
|
|
334
339
|
return null;
|
|
335
340
|
const stageDefinitions = template?.stages || recipe.stages;
|
|
336
341
|
const normalizedReview = normalizeReviewDomains(input.reviewDomains);
|
|
@@ -361,7 +366,7 @@ export function parseInvestigationGuide(value) {
|
|
|
361
366
|
outcome: guideOutcome(item?.outcome),
|
|
362
367
|
approvedAt: nullableTimestamp(item?.approvedAt),
|
|
363
368
|
openedAt: nullableTimestamp(item?.openedAt),
|
|
364
|
-
reviewNote:
|
|
369
|
+
reviewNote: boundedReviewNote(item?.reviewNote),
|
|
365
370
|
updatedAt: timestamp(item?.updatedAt) || updatedAt,
|
|
366
371
|
};
|
|
367
372
|
}),
|
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import { INVESTIGATION_RECIPES, normalizeInvestigationGuideTemplateSnapshot, } from "./investigation-guide.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export const MAX_INVESTIGATION_TEMPLATE_IMPORT_BYTES = 384 * 1024;
|
|
7
|
-
const CONTROL_RE = /[\x00-\x1f\x7f]/u;
|
|
1
|
+
import { INVESTIGATION_RECIPES, normalizeInvestigationGuideTemplateSnapshot, } from "./investigation-guide.mjs";
|
|
2
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
|
+
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
4
|
+
import { INVESTIGATION_TEMPLATE_SCHEMA, INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS, INVESTIGATION_TEMPLATE_VERSION, MAX_INVESTIGATION_TEMPLATES, MAX_INVESTIGATION_TEMPLATE_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
5
|
+
export { INVESTIGATION_TEMPLATE_SCHEMA, INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS, INVESTIGATION_TEMPLATE_VERSION, MAX_INVESTIGATION_TEMPLATES, MAX_INVESTIGATION_TEMPLATE_IMPORT_BYTES, MAX_INVESTIGATION_TEMPLATE_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
8
6
|
function record(value) {
|
|
9
|
-
return value
|
|
7
|
+
return ordinaryWorkspaceRecord(value, 'Investigation-template input');
|
|
10
8
|
}
|
|
11
9
|
function timestamp(value) {
|
|
12
|
-
|
|
13
|
-
return '';
|
|
14
|
-
const parsed = Date.parse(value);
|
|
15
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : '';
|
|
10
|
+
return normalizeExplicitIsoTimestamp(value) ?? '';
|
|
16
11
|
}
|
|
17
12
|
function byteLength(value) {
|
|
18
13
|
return new TextEncoder().encode(value).byteLength;
|
|
@@ -30,10 +25,12 @@ export function normalizeInvestigationTemplate(raw) {
|
|
|
30
25
|
return template(raw);
|
|
31
26
|
}
|
|
32
27
|
export function normalizeInvestigationTemplateStore(raw) {
|
|
28
|
+
assertWorkspaceInputGraph(raw, 'Investigation-template store');
|
|
29
|
+
assertWorkspaceDeclaredVersion(raw, 'Investigation-template store');
|
|
33
30
|
const value = record(raw);
|
|
34
31
|
if (value?.schema === INVESTIGATION_TEMPLATE_SCHEMA
|
|
35
|
-
&& Number(value.version)
|
|
36
|
-
throw new Error(
|
|
32
|
+
&& !INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS.includes(Number(value.version))) {
|
|
33
|
+
throw new Error(`Investigation-template schema ${String(value.version)} is unsupported; no data was changed.`);
|
|
37
34
|
}
|
|
38
35
|
const source = Array.isArray(raw) ? raw : Array.isArray(value?.templates) ? value.templates : [];
|
|
39
36
|
const templates = [];
|
|
@@ -115,12 +112,15 @@ function strictImport(raw) {
|
|
|
115
112
|
if (Number(value.version) > INVESTIGATION_TEMPLATE_VERSION) {
|
|
116
113
|
throw new Error('This investigation-template export uses a newer schema and cannot be imported safely.');
|
|
117
114
|
}
|
|
118
|
-
if (value.version
|
|
115
|
+
if (!INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS.includes(Number(value.version))) {
|
|
119
116
|
throw new Error(`Expected investigation-template schema ${INVESTIGATION_TEMPLATE_VERSION}.`);
|
|
120
117
|
}
|
|
121
118
|
return normalizeInvestigationTemplateStore(value).templates;
|
|
122
119
|
}
|
|
123
120
|
export function mergeInvestigationTemplates(localRaw, incomingRaw) {
|
|
121
|
+
assertWorkspaceInputGraph(localRaw, 'Local investigation-template store');
|
|
122
|
+
assertWorkspaceInputGraph(incomingRaw, 'Imported investigation-template document');
|
|
123
|
+
assertWorkspacePortableVersion(incomingRaw, INVESTIGATION_TEMPLATE_VERSION, 'Imported investigation-template document');
|
|
124
124
|
const local = normalizeInvestigationTemplateStore(localRaw).templates;
|
|
125
125
|
const incoming = strictImport(incomingRaw);
|
|
126
126
|
const byId = new Map(local.map((item) => [item.id, item]));
|
|
@@ -2,18 +2,14 @@
|
|
|
2
2
|
// deliberately accepts only bounded, normalized page-identity fields. Raw
|
|
3
3
|
// HTML, URLs, headers, redirect inventories, and parser diagnostics never
|
|
4
4
|
// cross this storage boundary.
|
|
5
|
-
import { normalizeDomain } from "
|
|
6
|
-
import { isInformativeFaviconHash } from "
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
export const MAX_BASELINE_TITLE_LENGTH = 200;
|
|
11
|
-
export const MAX_BASELINE_RESOURCE_HOSTS = 30;
|
|
12
|
-
export const MAX_BASELINE_IDENTIFIERS = 30;
|
|
5
|
+
import { normalizeDomain } from "../cases/case-model.mjs";
|
|
6
|
+
import { isInformativePerceptualHash as isInformativeFaviconHash } from "../../lib/perceptual-hash-comparison.mjs";
|
|
7
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
8
|
+
import { MAX_BASELINE_IDENTIFIERS, MAX_BASELINE_RESOURCE_HOSTS, MAX_BASELINE_TITLE_LENGTH, PAGE_BASELINE_VERSION, PAGE_FINGERPRINT_VERSION, PAGE_IDENTITY_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
9
|
+
export { MAX_BASELINE_IDENTIFIERS, MAX_BASELINE_RESOURCE_HOSTS, MAX_BASELINE_TITLE_LENGTH, PAGE_BASELINE_VERSION, PAGE_FINGERPRINT_VERSION, PAGE_IDENTITY_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
13
10
|
const SHA256_RE = /^[a-f0-9]{64}$/i;
|
|
14
11
|
const SIMHASH_RE = /^[a-f0-9]{16}$/i;
|
|
15
12
|
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
16
|
-
const MAX_TIMESTAMP_LENGTH = 64;
|
|
17
13
|
const MAX_HTML_TOKENS = 4096;
|
|
18
14
|
const MAX_TEXT_TOKENS = 8192;
|
|
19
15
|
const MAX_FORMS = 50;
|
|
@@ -24,10 +20,7 @@ function record(value) {
|
|
|
24
20
|
: null;
|
|
25
21
|
}
|
|
26
22
|
function timestamp(value) {
|
|
27
|
-
|
|
28
|
-
return null;
|
|
29
|
-
const parsed = new Date(value);
|
|
30
|
-
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
23
|
+
return normalizeExplicitIsoTimestamp(value);
|
|
31
24
|
}
|
|
32
25
|
function count(value, maximum) {
|
|
33
26
|
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= maximum
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import { normalizeDomain } from "
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export const MAX_RELATIONSHIP_OBSERVATION_DOMAINS = 50;
|
|
7
|
-
export const MAX_RELATIONSHIP_OBSERVATION_LIMITATIONS = 8;
|
|
8
|
-
export const MAX_RELATIONSHIP_OBSERVATION_VALUE_LENGTH = 20_000;
|
|
9
|
-
export const MAX_RELATIONSHIP_OBSERVATION_STORE_BYTES = 768 * 1024;
|
|
1
|
+
import { normalizeDomain } from "../cases/case-model.mjs";
|
|
2
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
|
+
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
4
|
+
import { MAX_RELATIONSHIP_OBSERVATIONS, MAX_RELATIONSHIP_OBSERVATION_DOMAINS, MAX_RELATIONSHIP_OBSERVATION_INPUTS, MAX_RELATIONSHIP_OBSERVATION_LIMITATIONS, MAX_RELATIONSHIP_OBSERVATION_STORE_BYTES, MAX_RELATIONSHIP_OBSERVATION_VALUE_LENGTH, RELATIONSHIP_OBSERVATION_SCHEMA, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
5
|
+
export { MAX_RELATIONSHIP_OBSERVATIONS, MAX_RELATIONSHIP_OBSERVATION_DOMAINS, MAX_RELATIONSHIP_OBSERVATION_INPUTS, MAX_RELATIONSHIP_OBSERVATION_LIMITATIONS, MAX_RELATIONSHIP_OBSERVATION_STORE_BYTES, MAX_RELATIONSHIP_OBSERVATION_VALUE_LENGTH, RELATIONSHIP_OBSERVATION_SCHEMA, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
10
6
|
export const RELATIONSHIP_OBSERVATION_TYPES = Object.freeze([
|
|
11
7
|
'nameserver_set',
|
|
12
8
|
'ip_address',
|
|
@@ -48,7 +44,7 @@ const TYPE_METADATA = Object.freeze({
|
|
|
48
44
|
description: 'These domains used an identical or perceptually similar favicon in the retained scan.',
|
|
49
45
|
}),
|
|
50
46
|
official_asset: Object.freeze({
|
|
51
|
-
label: 'Official asset
|
|
47
|
+
label: 'Official asset host match',
|
|
52
48
|
method: 'Configured-domain host match',
|
|
53
49
|
description: 'One or more pages loaded an asset from this configured official domain or its subdomain.',
|
|
54
50
|
}),
|
|
@@ -58,9 +54,7 @@ const FIXED_LIMITATIONS = Object.freeze([
|
|
|
58
54
|
'A shared observation does not prove ownership, coordination, intent, or maliciousness.',
|
|
59
55
|
]);
|
|
60
56
|
function record(value) {
|
|
61
|
-
return value
|
|
62
|
-
? value
|
|
63
|
-
: null;
|
|
57
|
+
return ordinaryWorkspaceRecord(value, 'Relationship-observation input');
|
|
64
58
|
}
|
|
65
59
|
function text(value, maximum) {
|
|
66
60
|
if (typeof value !== 'string' || value.length > maximum * 4 || CONTROL_RE.test(value))
|
|
@@ -68,10 +62,7 @@ function text(value, maximum) {
|
|
|
68
62
|
return value.replace(/\s+/gu, ' ').trim().slice(0, maximum).trim();
|
|
69
63
|
}
|
|
70
64
|
function timestamp(value) {
|
|
71
|
-
|
|
72
|
-
return '';
|
|
73
|
-
const parsed = Date.parse(value);
|
|
74
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : '';
|
|
65
|
+
return normalizeExplicitIsoTimestamp(value) ?? '';
|
|
75
66
|
}
|
|
76
67
|
function positiveInteger(value) {
|
|
77
68
|
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value <= 1_000
|
|
@@ -262,6 +253,8 @@ export function normalizeRelationshipObservation(raw) {
|
|
|
262
253
|
};
|
|
263
254
|
}
|
|
264
255
|
export function normalizeRelationshipObservationStore(raw) {
|
|
256
|
+
assertWorkspaceInputGraph(raw, 'Relationship-observation store');
|
|
257
|
+
assertWorkspaceDeclaredVersion(raw, 'Relationship-observation store');
|
|
265
258
|
const byId = new Map();
|
|
266
259
|
for (const candidate of observationList(raw).slice(0, MAX_RELATIONSHIP_OBSERVATION_INPUTS)) {
|
|
267
260
|
const observation = normalizeRelationshipObservation(candidate);
|
|
@@ -361,6 +354,9 @@ export function deleteRelationshipObservation(rawStore, id) {
|
|
|
361
354
|
return normalizeRelationshipObservationStore(rawStore).observations.filter((item) => item.id !== safeId);
|
|
362
355
|
}
|
|
363
356
|
export function mergeRelationshipObservations(localRaw, importedRaw) {
|
|
357
|
+
assertWorkspaceInputGraph(localRaw, 'Local relationship-observation store');
|
|
358
|
+
assertWorkspaceInputGraph(importedRaw, 'Imported relationship-observation document');
|
|
359
|
+
assertWorkspacePortableVersion(importedRaw, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, 'Imported relationship-observation document');
|
|
364
360
|
const importedRecord = record(importedRaw);
|
|
365
361
|
if (importedRecord?.schema !== undefined && importedRecord.schema !== RELATIONSHIP_OBSERVATION_SCHEMA) {
|
|
366
362
|
throw new Error('This JSON section is not a WHOISleuth relationship-observation export.');
|
package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs}
RENAMED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
// Pure browser-local shortlist model. The browser wrapper owns persistence
|
|
2
2
|
// and downloads; this module owns schema migration, record normalization,
|
|
3
3
|
// import merging, and exact serialized-byte accounting.
|
|
4
|
-
import { compactWatchlistResults } from "./watchlist-history.
|
|
5
|
-
import { normalizeOpportunityModelVersion
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
export const MAX_SHORTLIST_STORE_BYTES = 1024 * 1024;
|
|
12
|
-
export const MAX_SHORTLIST_FACTORS = 20;
|
|
4
|
+
import { compactWatchlistResults } from "./watchlist-history.mjs";
|
|
5
|
+
import { normalizeOpportunityModelVersion } from "../../lib/opportunity-scoring.mjs";
|
|
6
|
+
import { normalizeRiskModelVersion } from "../../lib/risk-scoring.mjs";
|
|
7
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
8
|
+
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
9
|
+
import { MAX_SHORTLIST_ENTRIES, MAX_SHORTLIST_FACTORS, MAX_SHORTLIST_INPUTS, MAX_SHORTLIST_STORE_BYTES, SHORTLIST_SCHEMA, SHORTLIST_SCHEMA_VERSION, SUPPORTED_SHORTLIST_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
10
|
+
export { MAX_SHORTLIST_ENTRIES, MAX_SHORTLIST_FACTORS, MAX_SHORTLIST_INPUTS, MAX_SHORTLIST_STORE_BYTES, SHORTLIST_SCHEMA, SHORTLIST_SCHEMA_VERSION, SUPPORTED_SHORTLIST_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
13
11
|
const MAX_FACTOR_LABEL_LENGTH = 200;
|
|
14
|
-
const MAX_TIMESTAMP_LENGTH = 64;
|
|
15
12
|
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
16
13
|
const EPOCH = new Date(0).toISOString();
|
|
17
14
|
function plainRecord(value) {
|
|
18
|
-
return value
|
|
19
|
-
? value
|
|
20
|
-
: null;
|
|
15
|
+
return ordinaryWorkspaceRecord(value, 'Shortlist input');
|
|
21
16
|
}
|
|
22
17
|
function isEnvelope(value) {
|
|
23
18
|
return Boolean(value && value.schema === SHORTLIST_SCHEMA && Array.isArray(value.entries));
|
|
@@ -30,7 +25,7 @@ function entryList(raw) {
|
|
|
30
25
|
}
|
|
31
26
|
export function shortlistStoreVersion(raw) {
|
|
32
27
|
if (Array.isArray(raw))
|
|
33
|
-
return
|
|
28
|
+
return SHORTLIST_SCHEMA_VERSION;
|
|
34
29
|
const value = plainRecord(raw);
|
|
35
30
|
if (!value || !isEnvelope(value))
|
|
36
31
|
return null;
|
|
@@ -44,10 +39,7 @@ function score(value) {
|
|
|
44
39
|
: null;
|
|
45
40
|
}
|
|
46
41
|
function timestamp(value, fallback = EPOCH) {
|
|
47
|
-
|
|
48
|
-
return fallback;
|
|
49
|
-
const parsed = Date.parse(value);
|
|
50
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : fallback;
|
|
42
|
+
return normalizeExplicitIsoTimestamp(value) ?? fallback;
|
|
51
43
|
}
|
|
52
44
|
function factors(value) {
|
|
53
45
|
if (!Array.isArray(value))
|
|
@@ -93,7 +85,14 @@ export function normalizeShortlistRecord(raw, options = {}) {
|
|
|
93
85
|
}
|
|
94
86
|
/** Normalize an internal record collection or current stored envelope. */
|
|
95
87
|
export function normalizeShortlistStore(raw) {
|
|
88
|
+
assertWorkspaceInputGraph(raw, 'Shortlist store');
|
|
89
|
+
assertWorkspaceDeclaredVersion(raw, 'Shortlist store');
|
|
96
90
|
const source = entryList(raw);
|
|
91
|
+
const sourceVersion = shortlistStoreVersion(raw);
|
|
92
|
+
if (!Array.isArray(raw) && sourceVersion !== null
|
|
93
|
+
&& !SUPPORTED_SHORTLIST_SCHEMA_VERSIONS.includes(sourceVersion)) {
|
|
94
|
+
throw new Error(`Shortlist schema ${sourceVersion} is unsupported; no data was changed.`);
|
|
95
|
+
}
|
|
97
96
|
const byDomain = new Map();
|
|
98
97
|
if (source) {
|
|
99
98
|
for (const item of source.slice(0, MAX_SHORTLIST_INPUTS)) {
|
|
@@ -167,6 +166,9 @@ function validateImport(raw) {
|
|
|
167
166
|
}
|
|
168
167
|
}
|
|
169
168
|
export function mergeShortlistStores(localRaw, importedRaw) {
|
|
169
|
+
assertWorkspaceInputGraph(localRaw, 'Local shortlist store');
|
|
170
|
+
assertWorkspaceInputGraph(importedRaw, 'Imported shortlist document');
|
|
171
|
+
assertWorkspacePortableVersion(importedRaw, SHORTLIST_SCHEMA_VERSION, 'Imported shortlist document');
|
|
170
172
|
validateImport(importedRaw);
|
|
171
173
|
const importedVersion = shortlistStoreVersion(importedRaw);
|
|
172
174
|
if (importedVersion !== null && importedVersion > SHORTLIST_SCHEMA_VERSION) {
|
package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs}
RENAMED
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
// the watchlist entry for rescanning, while history stores compact changes
|
|
3
3
|
// against a last-known baseline so browser-local storage does not grow by one complete
|
|
4
4
|
// result set on every check.
|
|
5
|
-
import { explainRiskScore, normalizeRiskModelVersion } from "
|
|
6
|
-
import { HTTP_SECURITY_HEADER_TOKENS, normalizeHttpSummary } from "
|
|
7
|
-
import { normalizeDomain } from "
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
export const MAX_WATCHLIST_INPUT_RECORDS = MAX_WATCHLIST_DOMAINS * 2;
|
|
12
|
-
export const MAX_WATCHLIST_NAMESERVERS = 12;
|
|
13
|
-
export const MAX_WATCHLIST_MUTATION_TYPES = 30;
|
|
14
|
-
export const MAX_WATCHLIST_HISTORY_DOMAIN_OPTIONS = MAX_WATCHLIST_DOMAINS;
|
|
5
|
+
import { explainRiskScore, normalizeRiskModelVersion } from "../../lib/risk-scoring.mjs";
|
|
6
|
+
import { HTTP_SECURITY_HEADER_TOKENS, normalizeHttpSummary } from "../cases/http-summary.mjs";
|
|
7
|
+
import { normalizeDomain } from "../cases/case-model.mjs";
|
|
8
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
9
|
+
import { MAX_WATCHLIST_CHANGES_PER_EVENT, MAX_WATCHLIST_DOMAINS, MAX_WATCHLIST_HISTORY_DOMAIN_OPTIONS, MAX_WATCHLIST_HISTORY_EVENTS, MAX_WATCHLIST_INPUT_RECORDS, MAX_WATCHLIST_MUTATION_TYPES, MAX_WATCHLIST_NAMESERVERS, } from "../contracts/workspace-portability.mjs";
|
|
10
|
+
export { MAX_WATCHLIST_CHANGES_PER_EVENT, MAX_WATCHLIST_DOMAINS, MAX_WATCHLIST_HISTORY_DOMAIN_OPTIONS, MAX_WATCHLIST_HISTORY_EVENTS, MAX_WATCHLIST_INPUT_RECORDS, MAX_WATCHLIST_MUTATION_TYPES, MAX_WATCHLIST_NAMESERVERS, } from "../contracts/workspace-portability.mjs";
|
|
15
11
|
const MAX_TEXT_LENGTH = 300;
|
|
16
12
|
const MAX_TITLE_LENGTH = 200;
|
|
17
13
|
const MAX_MUTATION_TYPE_LENGTH = 60;
|
|
@@ -149,10 +145,7 @@ function boundedText(value, maximum = MAX_TEXT_LENGTH, allowNull = true) {
|
|
|
149
145
|
return normalized || (allowNull ? null : '');
|
|
150
146
|
}
|
|
151
147
|
function isoTimestamp(value, fallback = new Date(0).toISOString()) {
|
|
152
|
-
|
|
153
|
-
return fallback;
|
|
154
|
-
const parsed = Date.parse(value);
|
|
155
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : fallback;
|
|
148
|
+
return normalizeExplicitIsoTimestamp(value) ?? fallback;
|
|
156
149
|
}
|
|
157
150
|
function boundedInteger(value, maximum, fallback = 0) {
|
|
158
151
|
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= maximum
|
|
@@ -487,7 +480,7 @@ export function normalizeWatchlistEntry(entry) {
|
|
|
487
480
|
: [];
|
|
488
481
|
const rawMode = event.mode;
|
|
489
482
|
return {
|
|
490
|
-
checkedAt: isoTimestamp(event.checkedAt),
|
|
483
|
+
checkedAt: isoTimestamp(event.checkedAt, new Date(0).toISOString()),
|
|
491
484
|
mode: typeof rawMode === 'string' && ['fast', 'deep', 'saved'].includes(rawMode)
|
|
492
485
|
? rawMode
|
|
493
486
|
: 'saved',
|
|
@@ -500,7 +493,7 @@ export function normalizeWatchlistEntry(entry) {
|
|
|
500
493
|
}).slice(-MAX_WATCHLIST_HISTORY_EVENTS)
|
|
501
494
|
: [];
|
|
502
495
|
const normalized = {
|
|
503
|
-
updatedAt: isoTimestamp(input.updatedAt),
|
|
496
|
+
updatedAt: isoTimestamp(input.updatedAt, new Date(0).toISOString()),
|
|
504
497
|
results,
|
|
505
498
|
baseline,
|
|
506
499
|
history,
|