@slicedearth/whoisleuth-cli 1.47.4 → 2.0.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/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- 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 +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- 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 +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -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 +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- 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-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/project-metadata.mjs +1 -1
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- 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-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- 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 +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- 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 +13 -5
- package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- 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 -21
- 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} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- 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/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -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/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- 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} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- 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/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- 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} +71 -48
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -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} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- 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} +13 -13
- 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} +110 -89
- package/third-party-notices.txt +14 -14
- 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 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- 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 -255
- 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 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /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
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
// Pure browser-local custom detection-rule model. Rules are deliberately a
|
|
2
2
|
// small structured language: field names and operators come from allowlists,
|
|
3
3
|
// so imported rules cannot execute code or reach outside bounded case evidence.
|
|
4
|
-
import { latestCaseEvidence } from "
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
export const MAX_RULE_CONDITIONS = 8;
|
|
9
|
-
export const MAX_RULE_INPUT_RECORDS = 250;
|
|
10
|
-
export const MAX_RULE_IMPORT_BYTES = 2 * 1024 * 1024;
|
|
11
|
-
export const MAX_RULE_STORE_BYTES = 256 * 1024;
|
|
12
|
-
export const MAX_RULE_NAME_LENGTH = 100;
|
|
13
|
-
export const MAX_RULE_TAG_LENGTH = 40;
|
|
14
|
-
export const MAX_CONDITION_VALUE_LENGTH = 200;
|
|
15
|
-
export const MAX_RULE_RISK_DELTA = 25;
|
|
16
|
-
export const MAX_CUSTOM_RISK_TOTAL = 50;
|
|
4
|
+
import { latestCaseEvidence } from "../cases/case-model.mjs";
|
|
5
|
+
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
6
|
+
import { DETECTION_RULE_SCHEMA, DETECTION_RULE_SCHEMA_VERSION, MAX_CONDITION_VALUE_LENGTH, MAX_CUSTOM_RISK_TOTAL, MAX_DETECTION_RULES, MAX_RULE_CONDITIONS, MAX_RULE_INPUT_RECORDS, MAX_RULE_NAME_LENGTH, MAX_RULE_RISK_DELTA, MAX_RULE_STORE_BYTES, MAX_RULE_TAG_LENGTH, } from "../contracts/workspace-portability.mjs";
|
|
7
|
+
export { DETECTION_RULE_SCHEMA, DETECTION_RULE_SCHEMA_VERSION, MAX_CONDITION_VALUE_LENGTH, MAX_CUSTOM_RISK_TOTAL, MAX_DETECTION_RULES, MAX_RULE_CONDITIONS, MAX_RULE_IMPORT_BYTES, MAX_RULE_INPUT_RECORDS, MAX_RULE_NAME_LENGTH, MAX_RULE_RISK_DELTA, MAX_RULE_STORE_BYTES, MAX_RULE_TAG_LENGTH, } from "../contracts/workspace-portability.mjs";
|
|
17
8
|
const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
18
9
|
const TEXT_CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
19
10
|
export const RULE_FIELD_DEFINITIONS = Object.freeze([
|
|
@@ -41,9 +32,7 @@ export const RULE_FIELD_DEFINITIONS = Object.freeze([
|
|
|
41
32
|
]);
|
|
42
33
|
const FIELD_BY_VALUE = new Map(RULE_FIELD_DEFINITIONS.map((field) => [field.value, field]));
|
|
43
34
|
function record(value) {
|
|
44
|
-
return value
|
|
45
|
-
? value
|
|
46
|
-
: {};
|
|
35
|
+
return ordinaryWorkspaceRecord(value, 'Detection-rule input') ?? {};
|
|
47
36
|
}
|
|
48
37
|
export function operatorsForRuleField(field) {
|
|
49
38
|
const definition = typeof field === 'string' ? FIELD_BY_VALUE.get(field) : undefined;
|
|
@@ -145,6 +134,8 @@ export function detectionRuleStoreVersion(raw) {
|
|
|
145
134
|
return typeof item.version === 'number' && Number.isFinite(item.version) ? item.version : null;
|
|
146
135
|
}
|
|
147
136
|
export function normalizeDetectionRuleStore(raw) {
|
|
137
|
+
assertWorkspaceInputGraph(raw, 'Detection-rule store');
|
|
138
|
+
assertWorkspaceDeclaredVersion(raw, 'Detection-rule store');
|
|
148
139
|
const byId = new Map();
|
|
149
140
|
for (const item of ruleList(raw).slice(0, MAX_RULE_INPUT_RECORDS)) {
|
|
150
141
|
const rule = normalizeDetectionRule(item);
|
|
@@ -282,14 +273,21 @@ export function previewDetectionRule(records, existingRules, rawCandidate) {
|
|
|
282
273
|
});
|
|
283
274
|
}
|
|
284
275
|
export function mergeDetectionRules(localRaw, importedRaw) {
|
|
276
|
+
assertWorkspaceInputGraph(localRaw, 'Local detection-rule store');
|
|
277
|
+
assertWorkspaceInputGraph(importedRaw, 'Imported detection-rule document');
|
|
278
|
+
assertWorkspacePortableVersion(importedRaw, DETECTION_RULE_SCHEMA_VERSION, 'Imported detection-rule document');
|
|
285
279
|
const importedRecord = record(importedRaw);
|
|
286
|
-
if (
|
|
280
|
+
if (importedRecord.schema !== DETECTION_RULE_SCHEMA
|
|
281
|
+
|| !Array.isArray(importedRecord.rules)) {
|
|
287
282
|
throw new Error('This JSON file is not a WHOISleuth custom-rule export.');
|
|
288
283
|
}
|
|
289
284
|
const version = detectionRuleStoreVersion(importedRaw);
|
|
290
285
|
if (version !== null && version > DETECTION_RULE_SCHEMA_VERSION) {
|
|
291
286
|
throw new Error(`This custom-rule file uses newer schema ${version}. Update the app before importing it.`);
|
|
292
287
|
}
|
|
288
|
+
if (version !== DETECTION_RULE_SCHEMA_VERSION) {
|
|
289
|
+
throw new Error(`Expected custom-rule schema ${DETECTION_RULE_SCHEMA_VERSION}.`);
|
|
290
|
+
}
|
|
293
291
|
const local = normalizeDetectionRuleStore(localRaw).rules;
|
|
294
292
|
const byId = new Map(local.map((rule) => [rule.id, rule]));
|
|
295
293
|
const importedList = ruleList(importedRaw);
|
|
@@ -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
5
|
export const INVESTIGATION_GUIDE_VERSION = 5;
|
|
5
|
-
export const INVESTIGATION_GUIDE_LEGACY_VERSION = 1;
|
|
6
6
|
export const INVESTIGATION_GUIDE_EXPORT_VERSION = 4;
|
|
7
|
-
export const INVESTIGATION_GUIDE_SUPPORTED_VERSIONS = [
|
|
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;
|
|
@@ -16,7 +16,6 @@ const CONTROL_RE = /[\x00-\x1f\x7f]/u;
|
|
|
16
16
|
const SAFE_TEMPLATE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
17
17
|
const GUIDE_STATUSES = new Set(['active', 'paused']);
|
|
18
18
|
const GUIDE_OUTCOMES = new Set(['pending', 'complete', 'partial', 'skipped']);
|
|
19
|
-
const PRE_RESPONSE_RECIPE_IDS = new Set(['brand_sweep', 'infrastructure_pivot', 'new_domain_triage']);
|
|
20
19
|
function stage(id, workspace, label, detail, expectedEvidence, requestImpact, prerequisite, completionCriteria, instructions, requiresApproval) {
|
|
21
20
|
return Object.freeze({
|
|
22
21
|
id,
|
|
@@ -130,10 +129,7 @@ function record(value) {
|
|
|
130
129
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
131
130
|
}
|
|
132
131
|
function timestamp(value) {
|
|
133
|
-
|
|
134
|
-
return '';
|
|
135
|
-
const parsed = Date.parse(value);
|
|
136
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : '';
|
|
132
|
+
return normalizeExplicitIsoTimestamp(value) ?? '';
|
|
137
133
|
}
|
|
138
134
|
function nullableTimestamp(value) {
|
|
139
135
|
if (value === null || value === undefined || value === '')
|
|
@@ -324,53 +320,22 @@ export function createInvestigationGuide(domain, recipeId = 'new_domain_triage',
|
|
|
324
320
|
stages: stages.map((stageDefinition) => createStageProgress(stageDefinition, createdAt)),
|
|
325
321
|
};
|
|
326
322
|
}
|
|
327
|
-
function parseLegacyGuide(input) {
|
|
328
|
-
const domain = normalizeInvestigationGuideDomain(input.domain);
|
|
329
|
-
const createdAt = timestamp(input.createdAt);
|
|
330
|
-
const updatedAt = timestamp(input.updatedAt);
|
|
331
|
-
const recipe = investigationGuideRecipe('new_domain_triage');
|
|
332
|
-
if (!domain || !createdAt || !updatedAt || !recipe)
|
|
333
|
-
return null;
|
|
334
|
-
const opened = new Set((Array.isArray(input.visitedStages) ? input.visitedStages : [])
|
|
335
|
-
.slice(0, recipe.stages.length * 2)
|
|
336
|
-
.filter((value) => typeof value === 'string'));
|
|
337
|
-
return {
|
|
338
|
-
version: INVESTIGATION_GUIDE_VERSION,
|
|
339
|
-
recipeId: recipe.id,
|
|
340
|
-
template: null,
|
|
341
|
-
domain,
|
|
342
|
-
focusDomain: null,
|
|
343
|
-
reviewDomains: [domain],
|
|
344
|
-
reviewDomainsTruncated: false,
|
|
345
|
-
status: 'active',
|
|
346
|
-
createdAt,
|
|
347
|
-
updatedAt,
|
|
348
|
-
stages: recipe.stages.map((stageDefinition) => ({
|
|
349
|
-
...createStageProgress(stageDefinition, updatedAt),
|
|
350
|
-
openedAt: opened.has(stageDefinition.id) ? updatedAt : null,
|
|
351
|
-
})),
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
323
|
export function parseInvestigationGuide(value) {
|
|
355
324
|
const input = record(value);
|
|
356
325
|
if (!input)
|
|
357
326
|
return null;
|
|
358
|
-
if (input.version === INVESTIGATION_GUIDE_LEGACY_VERSION)
|
|
359
|
-
return parseLegacyGuide(input);
|
|
360
327
|
if (!INVESTIGATION_GUIDE_SUPPORTED_VERSIONS.includes(input.version))
|
|
361
328
|
return null;
|
|
362
329
|
const recipe = investigationGuideRecipe(input.recipeId);
|
|
363
330
|
const domain = normalizeInvestigationGuideDomain(input.domain);
|
|
364
331
|
const createdAt = timestamp(input.createdAt);
|
|
365
332
|
const updatedAt = timestamp(input.updatedAt);
|
|
366
|
-
if (!recipe || !domain || !createdAt || !updatedAt
|
|
367
|
-
|| (input.version !== INVESTIGATION_GUIDE_VERSION && !PRE_RESPONSE_RECIPE_IDS.has(recipe.id)))
|
|
333
|
+
if (!recipe || !domain || !createdAt || !updatedAt)
|
|
368
334
|
return null;
|
|
369
|
-
const
|
|
370
|
-
const template = supportsTemplate && input.template !== null && input.template !== undefined
|
|
335
|
+
const template = input.template !== null && input.template !== undefined
|
|
371
336
|
? normalizeInvestigationGuideTemplateSnapshot(input.template, recipe.id)
|
|
372
337
|
: null;
|
|
373
|
-
if (
|
|
338
|
+
if (input.template !== null && input.template !== undefined && !template)
|
|
374
339
|
return null;
|
|
375
340
|
const stageDefinitions = template?.stages || recipe.stages;
|
|
376
341
|
const normalizedReview = normalizeReviewDomains(input.reviewDomains);
|
|
@@ -401,9 +366,7 @@ export function parseInvestigationGuide(value) {
|
|
|
401
366
|
outcome: guideOutcome(item?.outcome),
|
|
402
367
|
approvedAt: nullableTimestamp(item?.approvedAt),
|
|
403
368
|
openedAt: nullableTimestamp(item?.openedAt),
|
|
404
|
-
reviewNote:
|
|
405
|
-
? boundedReviewNote(item?.reviewNote)
|
|
406
|
-
: null,
|
|
369
|
+
reviewNote: boundedReviewNote(item?.reviewNote),
|
|
407
370
|
updatedAt: timestamp(item?.updatedAt) || updatedAt,
|
|
408
371
|
};
|
|
409
372
|
}),
|
|
@@ -1,30 +1,23 @@
|
|
|
1
|
-
import { INVESTIGATION_RECIPES, normalizeInvestigationGuideTemplateSnapshot, } from "./investigation-guide.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export const MAX_INVESTIGATION_TEMPLATE_STORE_BYTES = 256 * 1024;
|
|
7
|
-
export const MAX_INVESTIGATION_TEMPLATE_IMPORT_BYTES = 384 * 1024;
|
|
8
|
-
const CONTROL_RE = /[\x00-\x1f\x7f]/u;
|
|
9
|
-
const LEGACY_RECIPE_IDS = new Set(['brand_sweep', 'infrastructure_pivot', 'new_domain_triage']);
|
|
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";
|
|
10
6
|
function record(value) {
|
|
11
|
-
return value
|
|
7
|
+
return ordinaryWorkspaceRecord(value, 'Investigation-template input');
|
|
12
8
|
}
|
|
13
9
|
function timestamp(value) {
|
|
14
|
-
|
|
15
|
-
return '';
|
|
16
|
-
const parsed = Date.parse(value);
|
|
17
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : '';
|
|
10
|
+
return normalizeExplicitIsoTimestamp(value) ?? '';
|
|
18
11
|
}
|
|
19
12
|
function byteLength(value) {
|
|
20
13
|
return new TextEncoder().encode(value).byteLength;
|
|
21
14
|
}
|
|
22
|
-
function template(raw
|
|
15
|
+
function template(raw) {
|
|
23
16
|
const value = record(raw);
|
|
24
17
|
const snapshot = normalizeInvestigationGuideTemplateSnapshot(value);
|
|
25
18
|
const createdAt = timestamp(value?.createdAt);
|
|
26
19
|
const updatedAt = timestamp(value?.updatedAt);
|
|
27
|
-
return snapshot && createdAt && updatedAt
|
|
20
|
+
return snapshot && createdAt && updatedAt
|
|
28
21
|
? { ...snapshot, createdAt, updatedAt }
|
|
29
22
|
: null;
|
|
30
23
|
}
|
|
@@ -32,17 +25,18 @@ export function normalizeInvestigationTemplate(raw) {
|
|
|
32
25
|
return template(raw);
|
|
33
26
|
}
|
|
34
27
|
export function normalizeInvestigationTemplateStore(raw) {
|
|
28
|
+
assertWorkspaceInputGraph(raw, 'Investigation-template store');
|
|
29
|
+
assertWorkspaceDeclaredVersion(raw, 'Investigation-template store');
|
|
35
30
|
const value = record(raw);
|
|
36
31
|
if (value?.schema === INVESTIGATION_TEMPLATE_SCHEMA
|
|
37
|
-
&& Number(value.version)
|
|
38
|
-
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.`);
|
|
39
34
|
}
|
|
40
|
-
const sourceVersion = Array.isArray(raw) ? INVESTIGATION_TEMPLATE_VERSION : Number(value?.version) || INVESTIGATION_TEMPLATE_VERSION;
|
|
41
35
|
const source = Array.isArray(raw) ? raw : Array.isArray(value?.templates) ? value.templates : [];
|
|
42
36
|
const templates = [];
|
|
43
37
|
const seen = new Set();
|
|
44
38
|
for (const candidate of source.slice(0, MAX_INVESTIGATION_TEMPLATES * 2)) {
|
|
45
|
-
const normalized = template(candidate
|
|
39
|
+
const normalized = template(candidate);
|
|
46
40
|
if (!normalized || seen.has(normalized.id))
|
|
47
41
|
continue;
|
|
48
42
|
seen.add(normalized.id);
|
|
@@ -119,11 +113,14 @@ function strictImport(raw) {
|
|
|
119
113
|
throw new Error('This investigation-template export uses a newer schema and cannot be imported safely.');
|
|
120
114
|
}
|
|
121
115
|
if (!INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS.includes(Number(value.version))) {
|
|
122
|
-
throw new Error(`Expected investigation-template schema
|
|
116
|
+
throw new Error(`Expected investigation-template schema ${INVESTIGATION_TEMPLATE_VERSION}.`);
|
|
123
117
|
}
|
|
124
118
|
return normalizeInvestigationTemplateStore(value).templates;
|
|
125
119
|
}
|
|
126
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');
|
|
127
124
|
const local = normalizeInvestigationTemplateStore(localRaw).templates;
|
|
128
125
|
const incoming = strictImport(incomingRaw);
|
|
129
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) {
|