@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.
Files changed (282) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +20 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +454 -1748
  7. package/cli/artifact-validation/case-response.mjs +66 -35
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +112 -72
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-checkpoint.mjs +34 -3
  18. package/cli/bulk-command-runner.mjs +2 -0
  19. package/cli/bulk-output.mjs +29 -3
  20. package/cli/case-command.mjs +195 -0
  21. package/cli/case-pack.mjs +66 -111
  22. package/cli/command-argument-grammar.mjs +215 -0
  23. package/cli/command-reference.mjs +342 -285
  24. package/cli/completion.mjs +303 -80
  25. package/cli/config-profile.mjs +7 -5
  26. package/cli/ct-event-intake.mjs +12 -12
  27. package/cli/discover.mjs +4 -3
  28. package/cli/discovery-observation-snapshot.mjs +59 -18
  29. package/cli/discovery-scan-command-runner.mjs +21 -0
  30. package/cli/discovery-scan.mjs +13 -4
  31. package/cli/discovery-workflow.mjs +1 -1
  32. package/cli/domain-control-monitor.mjs +50 -20
  33. package/cli/domain-control-observations.mjs +149 -102
  34. package/cli/errors.mjs +33 -1
  35. package/cli/evidence-command-runner.mjs +16 -5
  36. package/cli/evidence-signing.mjs +2 -2
  37. package/cli/external-observation-mapping.mjs +7 -8
  38. package/cli/formatters/terminal-lookup.mjs +18 -46
  39. package/cli/formatters/terminal-shared.mjs +5 -3
  40. package/cli/formatters/terminal-technology.mjs +114 -0
  41. package/cli/inline-command-families.mjs +2 -0
  42. package/cli/input.mjs +116 -0
  43. package/cli/interactive-launcher.mjs +2 -160
  44. package/cli/interchange-report.mjs +18 -8
  45. package/cli/investigation-artifacts.mjs +176 -0
  46. package/cli/investigation-checkpoint.mjs +10 -0
  47. package/cli/investigation-folder.mjs +114 -0
  48. package/cli/investigation-manifest.mjs +1 -112
  49. package/cli/investigation-package-review.mjs +103 -0
  50. package/cli/investigation-plan.mjs +32 -19
  51. package/cli/investigation-run.mjs +188 -33
  52. package/cli/local-document-checkpoint.mjs +170 -0
  53. package/cli/lookalike-calibration.mjs +7 -2
  54. package/cli/lookup-brief.mjs +4 -2
  55. package/cli/lookup-browser-view.mjs +1 -1
  56. package/cli/lookup-browser.mjs +5 -12
  57. package/cli/lookup-command-runner.mjs +17 -6
  58. package/cli/lookup-diff.mjs +37 -3
  59. package/cli/lookup-plan.mjs +11 -5
  60. package/cli/lookup-reconcile.mjs +10 -8
  61. package/cli/mail-header-review.mjs +467 -0
  62. package/cli/mail-review.mjs +66 -14
  63. package/cli/manual.mjs +1 -0
  64. package/cli/network-command-runner.mjs +2 -0
  65. package/cli/output-file.mjs +31 -5
  66. package/cli/registry-doctor.mjs +3 -1
  67. package/cli/registry-support.mjs +1 -1
  68. package/cli/retained-artifact-diff.mjs +2 -2
  69. package/cli/review-command-runner.mjs +70 -0
  70. package/cli/risk-calibration.mjs +19 -17
  71. package/cli/runner.mjs +62 -7
  72. package/cli/sharing-review.mjs +21 -34
  73. package/cli/signer-trust.mjs +104 -0
  74. package/cli/source-reliability.mjs +74 -83
  75. package/cli/terminal-input.mjs +168 -0
  76. package/cli/workflow-command-runner.mjs +68 -13
  77. package/docs/cli-reference.md +77 -24
  78. package/docs/cli.md +300 -25
  79. package/lib/abort.mjs +4 -1
  80. package/lib/availability.mjs +161 -112
  81. package/lib/bounded-file.mjs +3 -1
  82. package/lib/bounded-html-document.mjs +125 -0
  83. package/lib/bounded-json.mjs +51 -8
  84. package/lib/browser-library-profile.mjs +42 -51
  85. package/lib/browser-library-worker.mjs +112 -0
  86. package/lib/client-behavior-profile.mjs +3 -3
  87. package/lib/ct-search.mjs +11 -4
  88. package/lib/dns-convergence-review.mjs +1 -0
  89. package/lib/dns-delegation-health.mjs +24 -14
  90. package/lib/dns-inheritance-review.mjs +217 -0
  91. package/lib/dns-intelligence.mjs +73 -26
  92. package/lib/dns-mx.mjs +1 -1
  93. package/lib/dnssec-chain-validation.mjs +6 -0
  94. package/lib/dnssec-evidence-validation.mjs +11 -3
  95. package/lib/domain-change-packet.mjs +49 -16
  96. package/lib/domain-change-review.mjs +14 -8
  97. package/lib/domain-control-flight-recorder.mjs +221 -117
  98. package/lib/domain-control-manifest.mjs +129 -69
  99. package/lib/domain-posture-analysis.mjs +20 -7
  100. package/lib/domain-posture-parsers.mjs +201 -8
  101. package/lib/domain-posture.mjs +147 -29
  102. package/lib/evidence-export-privacy.mjs +561 -0
  103. package/lib/evidence-export.mjs +11 -543
  104. package/lib/evidence-report.mjs +5 -3
  105. package/lib/favicon.mjs +117 -41
  106. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  107. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  108. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  109. package/lib/hostname.mjs +2 -38
  110. package/lib/html-signals.mjs +111 -108
  111. package/lib/http-evidence-bounds.mjs +2 -1
  112. package/lib/http-intelligence.mjs +39 -26
  113. package/lib/local-geoip-evidence.mjs +19 -3
  114. package/lib/lookup-cache.mjs +34 -11
  115. package/lib/lookup-child-profile-contract.mjs +24 -15
  116. package/lib/lookup-diagnostics.mjs +24 -1
  117. package/lib/lookup-progress.mjs +30 -13
  118. package/lib/lookup-response-contract.mjs +1048 -0
  119. package/lib/lookup-response.mjs +8 -10
  120. package/lib/lookup-source-progress.mjs +40 -14
  121. package/lib/lookup.mjs +48 -27
  122. package/lib/nameserver-preflight-review.mjs +1 -0
  123. package/lib/observed-network-context.mjs +43 -27
  124. package/lib/outbound-request-bounds.mjs +4 -1
  125. package/lib/page-fingerprints.mjs +69 -133
  126. package/lib/page-role-profile.mjs +12 -8
  127. package/lib/perceptual-hash.mjs +43 -14
  128. package/lib/rdap-bootstrap.mjs +72 -22
  129. package/lib/rdap-capabilities.mjs +1 -1
  130. package/lib/rdap-client.mjs +15 -7
  131. package/lib/rdap-normalization.mjs +33 -18
  132. package/lib/rdap-search-workbench.mjs +1 -1
  133. package/lib/rdap-transport.mjs +47 -20
  134. package/lib/rdap-types.mjs +2 -1
  135. package/lib/rdap.mjs +18 -3
  136. package/lib/registrable-domain.mjs +21 -1
  137. package/lib/registrar-standing-contract.mjs +1 -0
  138. package/lib/registry-capabilities.mjs +5 -1
  139. package/lib/registry-capability-catalogue.mjs +2 -1
  140. package/lib/registry-dates.mjs +2 -190
  141. package/lib/registry-insights.mjs +85 -34
  142. package/lib/response-policy.mjs +86 -48
  143. package/lib/risk-calibration-summary.mjs +18 -6
  144. package/lib/rpki-evidence.mjs +8 -5
  145. package/lib/safe-fetch.mjs +54 -47
  146. package/lib/security-txt.mjs +42 -7
  147. package/lib/service-binding-dns.mjs +8 -0
  148. package/lib/smtp-transport-review.mjs +27 -15
  149. package/lib/sslbl-certificates.generated.mjs +136 -135
  150. package/lib/static-html-analysis.mjs +178 -151
  151. package/lib/structured-data-identity.mjs +1 -0
  152. package/lib/technology-markup.mjs +100 -0
  153. package/lib/threat-intelligence-manifests.mjs +11 -139
  154. package/lib/threat-intelligence-runtime.mjs +24 -52
  155. package/lib/threat-intelligence-targets.mjs +6 -34
  156. package/lib/threat-intelligence-types.mjs +39 -0
  157. package/lib/typosquat-generator.mjs +19 -15
  158. package/lib/urlhaus-intelligence.mjs +8 -3
  159. package/lib/urlscan-intelligence.mjs +1 -1
  160. package/lib/website-security-posture.mjs +30 -6
  161. package/lib/website-technology.mjs +153 -45
  162. package/lib/whois-authority.mjs +14 -2
  163. package/lib/whois-chain.mjs +9 -2
  164. package/lib/whois-common-formats.mjs +5 -5
  165. package/lib/whois-contracts.mjs +2 -1
  166. package/lib/whois-dialects.mjs +2 -2
  167. package/lib/whois-parser-support.mjs +8 -1
  168. package/lib/whois-transport.mjs +23 -7
  169. package/lib/zone-intent-review.mjs +16 -6
  170. package/package.json +2 -1
  171. package/packages/cases/case-attachment-model.mjs +83 -0
  172. package/packages/cases/case-evidence-model.mjs +156 -62
  173. package/packages/cases/case-export-input.mjs +53 -0
  174. package/packages/cases/case-migration-model.mjs +127 -44
  175. package/packages/cases/case-model.mjs +1 -0
  176. package/packages/cases/case-recheck-model.mjs +91 -0
  177. package/packages/cases/case-record-contracts.mjs +5 -20
  178. package/packages/cases/case-record-core.mjs +12 -22
  179. package/packages/cases/case-record-decisions.mjs +72 -0
  180. package/packages/cases/case-record-model.mjs +1 -0
  181. package/packages/cases/case-record-operations.mjs +95 -31
  182. package/packages/cases/case-record-projection.mjs +221 -0
  183. package/packages/cases/case-report.mjs +70 -54
  184. package/packages/cases/case-response-actions.mjs +654 -0
  185. package/packages/cases/case-response-model.mjs +64 -1115
  186. package/packages/cases/case-response-outcomes.mjs +317 -0
  187. package/packages/cases/case-response-packet-render.mjs +153 -0
  188. package/packages/cases/case-response-packet-types.mjs +1 -0
  189. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  190. package/packages/cases/case-response-packet.mjs +74 -572
  191. package/packages/cases/case-response-records.mjs +94 -0
  192. package/packages/cases/case-response-review-inputs.mjs +371 -0
  193. package/packages/cases/case-response-values.mjs +85 -0
  194. package/packages/cases/case-review-package.mjs +45 -0
  195. package/packages/cases/case-selection.mjs +46 -0
  196. package/packages/cases/case-storage-model.mjs +31 -2
  197. package/packages/cases/response-route-freshness.mjs +16 -0
  198. package/packages/comparison/capture-context.mjs +61 -0
  199. package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
  200. package/packages/comparison/page-similarity.mjs +5 -2
  201. package/packages/comparison/relationship-evidence.mjs +85 -8
  202. package/packages/comparison/relationship-provenance.mjs +93 -0
  203. package/packages/contracts/analyst-interchange.mjs +216 -0
  204. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  205. package/packages/contracts/analyst-review-state.mjs +5 -4
  206. package/packages/contracts/case-portability.mjs +366 -83
  207. package/packages/contracts/case-views-contract.mjs +12 -0
  208. package/packages/contracts/case-views.mjs +26 -0
  209. package/packages/contracts/cli-command-semantics.mjs +94 -0
  210. package/packages/contracts/cli-lookup.mjs +0 -1
  211. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  212. package/packages/contracts/domain-control-manifest.mjs +127 -73
  213. package/packages/contracts/domain-control-monitor.mjs +48 -45
  214. package/packages/contracts/domain-control-review.mjs +179 -121
  215. package/packages/contracts/domain-name.mjs +21 -0
  216. package/packages/contracts/external-observation-interchange.mjs +2 -1
  217. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  218. package/packages/contracts/http-url.mjs +2 -0
  219. package/packages/contracts/investigation-package-limits.mjs +24 -0
  220. package/packages/contracts/investigation-portability.mjs +56 -27
  221. package/packages/contracts/investigation-run.mjs +16 -0
  222. package/packages/contracts/ip-address.mjs +34 -0
  223. package/packages/contracts/lookup-evidence.mjs +35 -0
  224. package/packages/contracts/offline-comparison.mjs +1 -2
  225. package/packages/contracts/page-fingerprints.mjs +15 -0
  226. package/packages/contracts/risk-calibration.mjs +15 -11
  227. package/packages/contracts/schema-compatibility.mjs +1 -1
  228. package/packages/contracts/schema-lifecycle.mjs +83 -153
  229. package/packages/contracts/selected-file-limits.mjs +6 -0
  230. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  231. package/packages/contracts/web-capture.mjs +10 -0
  232. package/packages/contracts/workspace-portability.mjs +183 -211
  233. package/packages/evidence/artifact-integrity.mjs +8 -2
  234. package/packages/evidence/artifact-structure.mjs +105 -0
  235. package/packages/evidence/decision-fact.mjs +4 -1
  236. package/packages/evidence/domain-control-runtime.mjs +201 -33
  237. package/packages/evidence/domain-posture-context.mjs +79 -0
  238. package/packages/evidence/image-regions.mjs +56 -0
  239. package/packages/evidence/latest-observations.mjs +28 -0
  240. package/packages/evidence/lookup-target.mjs +104 -0
  241. package/packages/evidence/observation.mjs +16 -1
  242. package/packages/evidence/passphrase-encryption.mjs +44 -0
  243. package/packages/evidence/record-identity.mjs +72 -0
  244. package/packages/evidence/registry-dates.mjs +190 -0
  245. package/packages/evidence/retained-file.mjs +69 -0
  246. package/packages/evidence/review-age.mjs +14 -0
  247. package/packages/interchange/bagit.mjs +342 -0
  248. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  249. package/packages/interchange/crc32.mjs +17 -0
  250. package/packages/interchange/external-findings-import.mjs +92 -51
  251. package/packages/interchange/web-capture-import.mjs +382 -0
  252. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  253. package/packages/investigation/investigation-bagit.mjs +14 -0
  254. package/packages/investigation/investigation-capsule.mjs +33 -5
  255. package/packages/investigation/investigation-manifest.mjs +200 -0
  256. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  257. package/packages/investigation/investigation-package.mjs +200 -0
  258. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  259. package/packages/investigation/lookup-asset-graph.mjs +193 -165
  260. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  261. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  262. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  263. package/packages/monitoring/analyst-review-state.mjs +38 -90
  264. package/packages/workspace/brand-profile-model.mjs +276 -22
  265. package/packages/workspace/bulk-columns.mjs +17 -0
  266. package/packages/workspace/bulk-review-model.mjs +23 -11
  267. package/packages/workspace/bulk-session-model.mjs +100 -24
  268. package/packages/workspace/bulk-sort.mjs +1 -0
  269. package/packages/workspace/case-views.mjs +102 -0
  270. package/packages/workspace/detection-rule-model.mjs +70 -22
  271. package/packages/workspace/hostile-input.mjs +29 -8
  272. package/packages/workspace/investigation-template-model.mjs +19 -4
  273. package/packages/workspace/page-baseline.mjs +18 -15
  274. package/packages/workspace/relationship-observation-model.mjs +41 -23
  275. package/packages/workspace/watchlist-history.mjs +2 -2
  276. package/packages/workspace/watchlist-store.mjs +8 -2
  277. package/packages/workspace/website-snapshot-model.mjs +164 -26
  278. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  279. package/packages/workspace/workspace-archive.mjs +87 -21
  280. package/third-party-notices.txt +29 -2
  281. package/cli/evidence-command-arguments.mjs +0 -156
  282. 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), 12);
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 boundedText(value, maximum, label, required = true) {
126
- if (typeof value !== 'string' || value.length > maximum * 4 || CONTROL_RE.test(value)) {
127
- if (!required && (value === null || value === undefined || value === ''))
128
- return '';
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 ((required && !normalized) || normalized.length > maximum)
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
- if (typeof value !== 'string' || value.length > 64 || CONTROL_RE.test(value))
140
- throw inputError(`${label} is invalid`);
141
- const parsed = Date.parse(value);
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: boundedText(value.rationale, MAX_ANALYST_REVIEW_RATIONALE_LENGTH, `${label}.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 browser-local storage limit.');
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 sha256Hex(new TextEncoder().encode(canonicalAnalystReviewIdentityJson(parts)));
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.completeness !== 'complete' || item.age === 'stale')) {
449
- throw new Error('Partial, inconclusive, or stale evidence cannot resolve a Review Item. Refresh or attach current complete evidence first.');
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 = boundedText(input.rationale, MAX_ANALYST_REVIEW_RATIONALE_LENGTH, '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 = timestamp(nowRaw, '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.completeness !== 'complete' || item.age === 'stale')) {
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 stale, so the retained resolved decision cannot close this item.',
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: boundedText(value.rationale, MAX_ANALYST_REVIEW_RATIONALE_LENGTH, `${label}.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,