@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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/NOTICE +6 -0
- package/README.md +20 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +454 -1748
- package/cli/artifact-validation/case-response.mjs +66 -35
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +112 -72
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +66 -111
- package/cli/command-argument-grammar.mjs +215 -0
- package/cli/command-reference.mjs +342 -285
- package/cli/completion.mjs +303 -80
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +59 -18
- package/cli/discovery-scan-command-runner.mjs +21 -0
- package/cli/discovery-scan.mjs +13 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +2 -0
- package/cli/input.mjs +116 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +18 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-header-review.mjs +467 -0
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +70 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +62 -7
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +77 -24
- package/docs/cli.md +300 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +51 -8
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +73 -26
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +49 -16
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +221 -117
- package/lib/domain-control-manifest.mjs +129 -69
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +201 -8
- package/lib/domain-posture.mjs +147 -29
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -543
- package/lib/evidence-report.mjs +5 -3
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +111 -108
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +8 -10
- package/lib/lookup-source-progress.mjs +40 -14
- package/lib/lookup.mjs +48 -27
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +85 -34
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +18 -6
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +42 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +178 -151
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +153 -45
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +127 -44
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +12 -22
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +95 -31
- package/packages/cases/case-record-projection.mjs +221 -0
- package/packages/cases/case-report.mjs +70 -54
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +64 -1115
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +74 -572
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +31 -2
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
- package/packages/comparison/page-similarity.mjs +5 -2
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +366 -83
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +94 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +15 -11
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +183 -211
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +201 -33
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +92 -51
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +193 -165
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +276 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +23 -11
- package/packages/workspace/bulk-session-model.mjs +100 -24
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -23
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +164 -26
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +87 -21
- package/third-party-notices.txt +29 -2
- package/cli/evidence-command-arguments.mjs +0 -156
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -21,6 +21,24 @@ export const LOOKUP_CLAIM_REQUIREMENT_IDS = [
|
|
|
21
21
|
'reviewed-case-recipient',
|
|
22
22
|
'authoritative-rdap',
|
|
23
23
|
];
|
|
24
|
+
const CLAIM_QUESTIONS = Object.freeze({
|
|
25
|
+
'registration-state': 'What does the registration authority establish?',
|
|
26
|
+
'current-web-observation': 'What was the website doing at the observed time?',
|
|
27
|
+
'brand-resemblance': 'Does the observed page resemble the reviewed Brand?',
|
|
28
|
+
'controlled-change': 'Which registration and DNS dependencies need review?',
|
|
29
|
+
'incident-response': 'What evidence and recipient route support a response?',
|
|
30
|
+
'network-context': 'What does the authoritative network record establish?',
|
|
31
|
+
});
|
|
32
|
+
/** A presentation of existing readiness, not a new verdict or collection plan. */
|
|
33
|
+
export function lookupQuestionsNeedingEvidence(readiness) {
|
|
34
|
+
return readiness.entries.filter((entry) => entry.state !== 'ready').map((entry) => ({
|
|
35
|
+
id: entry.id,
|
|
36
|
+
question: CLAIM_QUESTIONS[entry.id],
|
|
37
|
+
href: entry.href,
|
|
38
|
+
requirements: entry.requirements.filter((requirement) => requirement.state !== 'complete'),
|
|
39
|
+
limitations: entry.limitations,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
24
42
|
const MAX_TEXT = 320;
|
|
25
43
|
const MAX_DIAGNOSTICS = 12;
|
|
26
44
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
|
|
@@ -40,7 +40,7 @@ export function buildLookupInvestigationBrief(input) {
|
|
|
40
40
|
const limitedSources = input.quality.entries
|
|
41
41
|
.filter((entry) => entry.state !== 'complete')
|
|
42
42
|
.map((entry) => `${entry.label}: ${entry.statusLabel}`);
|
|
43
|
-
const graphKinds = uniqueText(input.graph.edges.map((edge) => edge.label),
|
|
43
|
+
const graphKinds = uniqueText(input.graph.edges.map((edge) => edge.label), 64);
|
|
44
44
|
const brief = Object.freeze({
|
|
45
45
|
schema: LOOKUP_INVESTIGATION_BRIEF_SCHEMA,
|
|
46
46
|
schemaVersion: LOOKUP_INVESTIGATION_BRIEF_VERSION,
|
|
@@ -66,10 +66,10 @@ export function buildLookupInvestigationBrief(input) {
|
|
|
66
66
|
kinds: Object.freeze(graphKinds),
|
|
67
67
|
}),
|
|
68
68
|
limitations: Object.freeze(uniqueText([
|
|
69
|
-
...limitedSources,
|
|
70
|
-
...input.graph.limitations,
|
|
71
69
|
'This brief is a deterministic organisation of collected and derived evidence, not an attribution, ownership, safety, availability, or maliciousness conclusion.',
|
|
72
70
|
'Analyst assertions, hypotheses, and decisions must remain separate from observed evidence.',
|
|
71
|
+
...input.graph.limitations,
|
|
72
|
+
...limitedSources,
|
|
73
73
|
])),
|
|
74
74
|
});
|
|
75
75
|
if (new TextEncoder().encode(JSON.stringify(brief)).byteLength > MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const LOOKUP_GUIDANCE_TASKS = Object.freeze([
|
|
2
|
+
'general',
|
|
3
|
+
'acquisition',
|
|
4
|
+
'brand',
|
|
5
|
+
'incident',
|
|
6
|
+
'owned',
|
|
7
|
+
]);
|
|
8
|
+
const GUIDANCE = Object.freeze({
|
|
9
|
+
general: Object.freeze({
|
|
10
|
+
task: 'general',
|
|
11
|
+
label: 'General investigation',
|
|
12
|
+
recommendation: 'fast',
|
|
13
|
+
reason: 'Fast starts with authority-aware registration and availability evidence before a broader collection is justified.',
|
|
14
|
+
requestExplanation: 'Fast requests the existing lower-request authority and RDAP registration path and omits WHOIS, DNS, HTTP, TLS, network-context, and optional intelligence branches.',
|
|
15
|
+
limitation: 'A Fast result can still be partial, unavailable, or inconclusive. Use Deep only when the question needs the additional source families.',
|
|
16
|
+
}),
|
|
17
|
+
brand: Object.freeze({
|
|
18
|
+
task: 'brand',
|
|
19
|
+
label: 'Brand review',
|
|
20
|
+
recommendation: 'deep',
|
|
21
|
+
reason: 'Deep provides the existing source-qualified web, DNS, TLS, registration, and relationship context used by brand review.',
|
|
22
|
+
requestExplanation: 'Deep requests the existing registry RDAP, WHOIS, domain evidence, registrar RDAP, DNS, HTTP, TLS, and network-context branches. Optional intelligence sources receive data only when separately selected.',
|
|
23
|
+
limitation: 'Deep is broader, not complete or authoritative for every question. Missing and conflicting sources remain explicit.',
|
|
24
|
+
}),
|
|
25
|
+
acquisition: Object.freeze({
|
|
26
|
+
task: 'acquisition',
|
|
27
|
+
label: 'Acquisition review',
|
|
28
|
+
recommendation: 'deep',
|
|
29
|
+
reason: 'Deep provides the existing bounded registration, web, DNS, TLS, and uncertainty context expected by acquisition review.',
|
|
30
|
+
requestExplanation: 'Deep uses the existing request budget and source branches. It does not appraise a domain, contact a seller, or establish clean title or legal sufficiency.',
|
|
31
|
+
limitation: 'The recommendation organises due diligence; it does not make an acquisition recommendation or guarantee completeness.',
|
|
32
|
+
}),
|
|
33
|
+
incident: Object.freeze({
|
|
34
|
+
task: 'incident',
|
|
35
|
+
label: 'Incident response',
|
|
36
|
+
recommendation: 'deep',
|
|
37
|
+
reason: 'Deep collects the web, DNS, TLS, registration, and network evidence needed to review an incident and choose a response route.',
|
|
38
|
+
requestExplanation: 'Deep requests the existing source branches. It does not submit a report, test a published contact, or establish responsibility for the observed content.',
|
|
39
|
+
limitation: 'Collection supports review; it does not determine maliciousness or authorise a response.',
|
|
40
|
+
}),
|
|
41
|
+
owned: Object.freeze({
|
|
42
|
+
task: 'owned',
|
|
43
|
+
label: 'Owned-domain posture',
|
|
44
|
+
recommendation: 'deep',
|
|
45
|
+
reason: 'Deep collects the current registration, DNS, TLS, mail, and web observations needed for a source-by-source posture review.',
|
|
46
|
+
requestExplanation: 'Deep uses the existing bounded source branches. Retained baselines remain separate and must be compared using their observation times and source states.',
|
|
47
|
+
limitation: 'A changed or missing observation does not by itself establish compromise, remediation, or control.',
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
export const LOOKUP_TASK_VIEWS = Object.freeze(LOOKUP_GUIDANCE_TASKS.map((task) => Object.freeze({
|
|
51
|
+
id: task,
|
|
52
|
+
label: GUIDANCE[task].label,
|
|
53
|
+
})));
|
|
54
|
+
export function lookupTaskGuidance(task) {
|
|
55
|
+
return typeof task === 'string' && LOOKUP_GUIDANCE_TASKS.includes(task)
|
|
56
|
+
? GUIDANCE[task]
|
|
57
|
+
: GUIDANCE.general;
|
|
58
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sha256IdentityHex } from "../evidence/record-identity.mjs";
|
|
2
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
1
3
|
/**
|
|
2
4
|
* Canonical, framework-independent analyst review identity and lifecycle.
|
|
3
5
|
*
|
|
@@ -7,7 +9,12 @@
|
|
|
7
9
|
*/
|
|
8
10
|
import { ANALYST_REVIEW_DISPOSITIONS, ANALYST_REVIEW_EVIDENCE_FAMILIES, ANALYST_REVIEW_STATE_SCHEMA, ANALYST_REVIEW_STATE_SCHEMA_VERSION, MAX_ANALYST_REVIEW_ASSOCIATIONS, MAX_ANALYST_REVIEW_HISTORY, MAX_ANALYST_REVIEW_IDENTITY_BYTES, MAX_ANALYST_REVIEW_ITEMS, MAX_ANALYST_REVIEW_RATIONALE_LENGTH, MAX_ANALYST_REVIEW_STATE_BYTES, MAX_ANALYST_REVIEW_STATE_RECORDS, } from "../contracts/analyst-review-state-contract.mjs";
|
|
9
11
|
export * from "../contracts/analyst-review-state-contract.mjs";
|
|
12
|
+
export { ANALYST_REVIEW_AGING_AFTER_DAYS, ANALYST_REVIEW_STALE_AFTER_DAYS, analystReviewAgeAt } from "../evidence/review-age.mjs";
|
|
13
|
+
export function analystReviewCanResolve(item) {
|
|
14
|
+
return item.completeness === 'complete' && (item.age === 'current' || item.age === 'aging');
|
|
15
|
+
}
|
|
10
16
|
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
17
|
+
const RATIONALE_INPUT_CONTROL_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
11
18
|
const SAFE_REFERENCE_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
12
19
|
const SAFE_SUBJECT_RE = /^review:[a-z0-9_]{1,40}:[a-f0-9]{64}$/u;
|
|
13
20
|
const SAFE_FINGERPRINT_RE = /^material:[a-f0-9]{64}$/u;
|
|
@@ -122,28 +129,24 @@ export function assertAnalystReviewInputGraph(value) {
|
|
|
122
129
|
}
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
function boundedRationale(value, label, formInput = false) {
|
|
133
|
+
const maximum = MAX_ANALYST_REVIEW_RATIONALE_LENGTH;
|
|
134
|
+
const controls = formInput ? RATIONALE_INPUT_CONTROL_RE : CONTROL_RE;
|
|
135
|
+
if (typeof value !== 'string' || value.length > maximum * 4 || controls.test(value)) {
|
|
129
136
|
throw inputError(`${label} is invalid`);
|
|
130
137
|
}
|
|
138
|
+
// Forms may contain ordinary line breaks; stored rationale remains single-line.
|
|
131
139
|
const normalized = value.replace(/\s+/gu, ' ').trim();
|
|
132
|
-
if (
|
|
140
|
+
if (!normalized || normalized.length > maximum)
|
|
133
141
|
throw inputError(`${label} is invalid`);
|
|
134
142
|
return normalized;
|
|
135
143
|
}
|
|
136
144
|
function timestamp(value, label, nullable = false) {
|
|
137
145
|
if (nullable && (value === null || value === undefined || value === ''))
|
|
138
146
|
return null;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (!Number.isFinite(parsed))
|
|
143
|
-
throw inputError(`${label} must use an explicit valid date and time`);
|
|
144
|
-
const normalized = new Date(parsed).toISOString();
|
|
145
|
-
if (!/(?:Z|[+-]\d{2}:\d{2})$/u.test(value))
|
|
146
|
-
throw inputError(`${label} must use an explicit timezone`);
|
|
147
|
+
const normalized = normalizeExplicitIsoTimestamp(value);
|
|
148
|
+
if (!normalized)
|
|
149
|
+
throw inputError(`${label} must use an explicit valid date, time and timezone`);
|
|
147
150
|
return normalized;
|
|
148
151
|
}
|
|
149
152
|
function referenceList(value, label) {
|
|
@@ -181,7 +184,7 @@ function normalizeSnapshot(raw, label) {
|
|
|
181
184
|
const normalized = {
|
|
182
185
|
reviewedFingerprint: fingerprint(value.reviewedFingerprint, `${label}.reviewedFingerprint`),
|
|
183
186
|
disposition: normalizedDisposition,
|
|
184
|
-
rationale:
|
|
187
|
+
rationale: boundedRationale(value.rationale, `${label}.rationale`),
|
|
185
188
|
reviewedAt: timestamp(value.reviewedAt, `${label}.reviewedAt`),
|
|
186
189
|
reviewDueAt: timestamp(value.reviewDueAt, `${label}.reviewDueAt`, true),
|
|
187
190
|
expiresAt: timestamp(value.expiresAt, `${label}.expiresAt`, true),
|
|
@@ -278,7 +281,7 @@ export function normalizeAnalystReviewStateStore(raw) {
|
|
|
278
281
|
records,
|
|
279
282
|
};
|
|
280
283
|
if (byteLength(JSON.stringify(store)) > MAX_ANALYST_REVIEW_STATE_BYTES) {
|
|
281
|
-
throw new Error('Analyst review state exceeds the 512 KiB
|
|
284
|
+
throw new Error('Analyst review state exceeds the 512 KiB workspace storage limit.');
|
|
282
285
|
}
|
|
283
286
|
return store;
|
|
284
287
|
}
|
|
@@ -323,75 +326,8 @@ export function canonicalAnalystReviewIdentityJson(parts) {
|
|
|
323
326
|
}
|
|
324
327
|
return source;
|
|
325
328
|
}
|
|
326
|
-
const SHA256_CONSTANTS = Object.freeze([
|
|
327
|
-
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
328
|
-
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
329
|
-
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
330
|
-
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
331
|
-
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
332
|
-
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
333
|
-
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
334
|
-
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
335
|
-
]);
|
|
336
|
-
function rotateRight(value, bits) {
|
|
337
|
-
return (value >>> bits) | (value << (32 - bits));
|
|
338
|
-
}
|
|
339
|
-
function sha256Hex(bytes) {
|
|
340
|
-
// This digest is a collision-resistant record identity, not a password,
|
|
341
|
-
// signature, MAC, or substitute for the platform cryptography used there.
|
|
342
|
-
const paddedLength = Math.ceil((bytes.byteLength + 9) / 64) * 64;
|
|
343
|
-
const padded = new Uint8Array(paddedLength);
|
|
344
|
-
padded.set(bytes);
|
|
345
|
-
padded[bytes.byteLength] = 0x80;
|
|
346
|
-
const view = new DataView(padded.buffer);
|
|
347
|
-
const bitLength = bytes.byteLength * 8;
|
|
348
|
-
view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x1_0000_0000), false);
|
|
349
|
-
view.setUint32(paddedLength - 4, bitLength >>> 0, false);
|
|
350
|
-
const state = new Uint32Array([
|
|
351
|
-
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
|
352
|
-
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
353
|
-
]);
|
|
354
|
-
const words = new Uint32Array(64);
|
|
355
|
-
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
356
|
-
for (let index = 0; index < 16; index += 1)
|
|
357
|
-
words[index] = view.getUint32(offset + index * 4, false);
|
|
358
|
-
for (let index = 16; index < 64; index += 1) {
|
|
359
|
-
const left = words[index - 15];
|
|
360
|
-
const right = words[index - 2];
|
|
361
|
-
const sigma0 = rotateRight(left, 7) ^ rotateRight(left, 18) ^ (left >>> 3);
|
|
362
|
-
const sigma1 = rotateRight(right, 17) ^ rotateRight(right, 19) ^ (right >>> 10);
|
|
363
|
-
words[index] = (words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;
|
|
364
|
-
}
|
|
365
|
-
let [a, b, c, d, e, f, g, h] = state;
|
|
366
|
-
for (let index = 0; index < 64; index += 1) {
|
|
367
|
-
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
368
|
-
const choice = (e & f) ^ (~e & g);
|
|
369
|
-
const first = (h + sum1 + choice + SHA256_CONSTANTS[index] + words[index]) >>> 0;
|
|
370
|
-
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
371
|
-
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
372
|
-
const second = (sum0 + majority) >>> 0;
|
|
373
|
-
h = g;
|
|
374
|
-
g = f;
|
|
375
|
-
f = e;
|
|
376
|
-
e = (d + first) >>> 0;
|
|
377
|
-
d = c;
|
|
378
|
-
c = b;
|
|
379
|
-
b = a;
|
|
380
|
-
a = (first + second) >>> 0;
|
|
381
|
-
}
|
|
382
|
-
state[0] = (state[0] + a) >>> 0;
|
|
383
|
-
state[1] = (state[1] + b) >>> 0;
|
|
384
|
-
state[2] = (state[2] + c) >>> 0;
|
|
385
|
-
state[3] = (state[3] + d) >>> 0;
|
|
386
|
-
state[4] = (state[4] + e) >>> 0;
|
|
387
|
-
state[5] = (state[5] + f) >>> 0;
|
|
388
|
-
state[6] = (state[6] + g) >>> 0;
|
|
389
|
-
state[7] = (state[7] + h) >>> 0;
|
|
390
|
-
}
|
|
391
|
-
return [...state].map((value) => value.toString(16).padStart(8, '0')).join('');
|
|
392
|
-
}
|
|
393
329
|
function stableHash(parts) {
|
|
394
|
-
return
|
|
330
|
+
return sha256IdentityHex(new TextEncoder().encode(canonicalAnalystReviewIdentityJson(parts)));
|
|
395
331
|
}
|
|
396
332
|
export function analystReviewSubjectKey(evidenceFamily, stableParts) {
|
|
397
333
|
if (!FAMILY_VALUES.has(evidenceFamily))
|
|
@@ -421,6 +357,11 @@ function snapshotIdentity(snapshot) {
|
|
|
421
357
|
snapshot.expiresAt,
|
|
422
358
|
]);
|
|
423
359
|
}
|
|
360
|
+
export function analystReviewDecisionIdentity(subjectKey, snapshot) {
|
|
361
|
+
if (!SAFE_SUBJECT_RE.test(subjectKey))
|
|
362
|
+
throw inputError('Review subject identity is invalid');
|
|
363
|
+
return `decision:${stableHash([subjectKey, snapshotIdentity(snapshot)])}`;
|
|
364
|
+
}
|
|
424
365
|
function mergedReviewHistory(current, other) {
|
|
425
366
|
const currentIdentity = snapshotIdentity(currentSnapshot(current));
|
|
426
367
|
const byIdentity = new Map();
|
|
@@ -445,10 +386,10 @@ export function setAnalystReviewDecision(raw, item, input) {
|
|
|
445
386
|
}
|
|
446
387
|
if (!DISPOSITION_VALUES.has(input.disposition))
|
|
447
388
|
throw new TypeError('Choose a valid Review Item disposition.');
|
|
448
|
-
if (input.disposition === 'resolved' && (item
|
|
449
|
-
throw new Error('Partial, inconclusive, or
|
|
389
|
+
if (input.disposition === 'resolved' && !analystReviewCanResolve(item)) {
|
|
390
|
+
throw new Error('Partial, inconclusive, stale or undated evidence cannot resolve a Review Item. Refresh or attach current complete evidence first.');
|
|
450
391
|
}
|
|
451
|
-
const rationale =
|
|
392
|
+
const rationale = boundedRationale(input.rationale, 'rationale', true);
|
|
452
393
|
const reviewedAt = timestamp(input.reviewedAt ?? new Date().toISOString(), 'reviewedAt');
|
|
453
394
|
const expiresAt = timestamp(input.expiresAt, 'expiresAt', true);
|
|
454
395
|
const reviewDueAt = timestamp(input.reviewDueAt, 'reviewDueAt', true);
|
|
@@ -492,7 +433,14 @@ export function analystReviewLifecycle(item, raw, nowRaw = new Date().toISOStrin
|
|
|
492
433
|
if (!decision) {
|
|
493
434
|
return { state: 'open', effectiveDisposition: 'open', decision: null, reason: 'No analyst lifecycle decision is retained.', expired: false, invalidated: false, recurred: false, reviewDue: false };
|
|
494
435
|
}
|
|
495
|
-
const now =
|
|
436
|
+
const now = normalizeExplicitIsoTimestamp(nowRaw);
|
|
437
|
+
if (!now || Date.parse(now) < Date.parse(decision.reviewedAt)) {
|
|
438
|
+
return {
|
|
439
|
+
state: 'invalidated', effectiveDisposition: 'open', decision,
|
|
440
|
+
reason: 'The review clock is unavailable or earlier than the retained decision. Its disposition cannot hide current work.',
|
|
441
|
+
expired: false, invalidated: true, recurred: decision.disposition !== 'open', reviewDue: false,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
496
444
|
const invalidated = decision.reviewedFingerprint !== item.materialFingerprint;
|
|
497
445
|
const expired = decision.expiresAt !== null && Date.parse(decision.expiresAt) <= Date.parse(now);
|
|
498
446
|
const reviewDue = decision.reviewDueAt !== null && Date.parse(decision.reviewDueAt) <= Date.parse(now);
|
|
@@ -518,10 +466,10 @@ export function analystReviewLifecycle(item, raw, nowRaw = new Date().toISOStrin
|
|
|
518
466
|
expired: false, invalidated: false, recurred: true, reviewDue: true,
|
|
519
467
|
};
|
|
520
468
|
}
|
|
521
|
-
if (decision.disposition === 'resolved' && (item
|
|
469
|
+
if (decision.disposition === 'resolved' && !analystReviewCanResolve(item)) {
|
|
522
470
|
return {
|
|
523
471
|
state: 'invalidated', effectiveDisposition: 'open', decision,
|
|
524
|
-
reason: 'The current evidence is partial, inconclusive, or
|
|
472
|
+
reason: 'The current evidence is partial, inconclusive, stale or undated, so the retained resolved decision cannot close this item.',
|
|
525
473
|
expired: false, invalidated: true, recurred: true, reviewDue,
|
|
526
474
|
};
|
|
527
475
|
}
|
|
@@ -607,7 +555,7 @@ function normalizeDevelopmentSnapshot(raw, label) {
|
|
|
607
555
|
return {
|
|
608
556
|
reviewedFingerprint: developmentFingerprint(value.reviewedFingerprint, `${label}.reviewedFingerprint`),
|
|
609
557
|
disposition: disposition(value.disposition, `${label}.disposition`),
|
|
610
|
-
rationale:
|
|
558
|
+
rationale: boundedRationale(value.rationale, `${label}.rationale`),
|
|
611
559
|
reviewedAt,
|
|
612
560
|
reviewDueAt: reviewDueAt && Date.parse(reviewDueAt) > Date.parse(reviewedAt) ? reviewDueAt : null,
|
|
613
561
|
expiresAt: expiresAt && Date.parse(expiresAt) > Date.parse(reviewedAt) ? expiresAt : null,
|