@slicedearth/whoisleuth-cli 2.4.0 → 2.6.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 (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -0,0 +1,129 @@
1
+ // Pure STIX 2.1 interchange for locally reviewed Bulk candidates. Direct
2
+ // observations and heuristic inferences remain separate objects so consumers
3
+ // do not mistake a Risk score for confirmed maliciousness.
4
+ import { collectDefensiveIndicatorCandidates, defensiveIndicatorProvenance, } from "./defensive-indicator-export.mjs";
5
+ import { STIX_INDICATOR_EXPORT_VERSION } from "../contracts/analyst-interchange.mjs";
6
+ export { STIX_INDICATOR_EXPORT_VERSION } from "../contracts/analyst-interchange.mjs";
7
+ export const MAX_STIX_INDICATORS = 1000;
8
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
9
+ const CONTROL_RE = /[\x00-\x1f\x7f]/;
10
+ const WARNING = 'Heuristic finding; review before operational use because false positives are possible.';
11
+ function record(value) {
12
+ return value && typeof value === 'object' && !Array.isArray(value)
13
+ ? value
14
+ : {};
15
+ }
16
+ function isoTimestamp(value) {
17
+ if (typeof value !== 'string' || value.length > 64 || CONTROL_RE.test(value))
18
+ return null;
19
+ const parsed = Date.parse(value);
20
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
21
+ }
22
+ function defaultIdFactory(type) {
23
+ const uuid = globalThis.crypto?.randomUUID?.();
24
+ if (!uuid)
25
+ throw new Error('Secure random identifiers are unavailable for the STIX export.');
26
+ return `${type}--${uuid}`;
27
+ }
28
+ function stixId(type, idFactory) {
29
+ const value = idFactory(type);
30
+ const prefix = `${type}--`;
31
+ if (typeof value !== 'string' || !value.startsWith(prefix) || !UUID_RE.test(value.slice(prefix.length))) {
32
+ throw new Error(`The STIX identifier factory returned an invalid ${type} identifier.`);
33
+ }
34
+ return value.toLowerCase();
35
+ }
36
+ export function stixIndicatorProducer(producerId, generatedAt) {
37
+ return {
38
+ type: 'identity', spec_version: '2.1', id: producerId,
39
+ created: generatedAt, modified: generatedAt,
40
+ name: 'WHOISleuth', identity_class: 'system',
41
+ description: 'Producer of locally generated defensive-domain observations and heuristic inferences.',
42
+ x_whoisleuth_export_version: STIX_INDICATOR_EXPORT_VERSION,
43
+ x_whoisleuth_generated_at: generatedAt,
44
+ x_whoisleuth_false_positive_warning: WARNING,
45
+ };
46
+ }
47
+ /** The caller owns validated candidates and either fresh or retained identities. */
48
+ export function stixIndicatorObjects(domain, availability, provenance, generatedAt, producerId, nextId) {
49
+ const { observedAt, riskScore: score, riskModelVersion: modelVersion } = provenance;
50
+ const domainId = nextId('domain-name');
51
+ const observationId = nextId(observedAt ? 'observed-data' : 'note');
52
+ const indicatorId = nextId('indicator');
53
+ const observationContext = {
54
+ created_by_ref: producerId, created: generatedAt, modified: generatedAt,
55
+ x_whoisleuth_observed_at_basis: observedAt ? 'scan' : 'unknown',
56
+ x_whoisleuth_source: 'bulk', x_whoisleuth_availability: availability,
57
+ x_whoisleuth_scan_depth: provenance.scanDepth,
58
+ };
59
+ const objects = [
60
+ { type: 'domain-name', spec_version: '2.1', id: domainId, value: domain },
61
+ observedAt ? {
62
+ type: 'observed-data', spec_version: '2.1', id: observationId, ...observationContext,
63
+ first_observed: observedAt, last_observed: observedAt, number_observed: 1, object_refs: [domainId],
64
+ x_whoisleuth_evidence_kind: 'direct-observation',
65
+ } : {
66
+ type: 'note', spec_version: '2.1', id: observationId, ...observationContext,
67
+ content: 'Observation time was not retained. This candidate is supplied for heuristic review; its export time is not a sighting time.',
68
+ object_refs: [domainId, indicatorId], x_whoisleuth_evidence_kind: 'observation-context',
69
+ },
70
+ {
71
+ type: 'indicator', spec_version: '2.1', id: indicatorId,
72
+ created_by_ref: producerId, created: generatedAt, modified: generatedAt,
73
+ name: `Heuristic domain candidate: ${domain}`, description: WARNING,
74
+ pattern: `[domain-name:value = '${domain}']`, pattern_type: 'stix', pattern_version: '2.1',
75
+ valid_from: generatedAt, x_whoisleuth_validity_basis: 'export',
76
+ labels: ['heuristic', 'defensive-review'], x_whoisleuth_evidence_kind: 'heuristic-inference',
77
+ x_whoisleuth_risk_score: score,
78
+ ...(modelVersion === null ? {} : { x_whoisleuth_risk_model_version: modelVersion }),
79
+ x_whoisleuth_false_positive_warning: WARNING,
80
+ },
81
+ ];
82
+ if (observedAt)
83
+ objects.push({
84
+ type: 'relationship', spec_version: '2.1', id: nextId('relationship'),
85
+ created_by_ref: producerId, created: generatedAt, modified: generatedAt,
86
+ relationship_type: 'based-on', source_ref: indicatorId, target_ref: observationId,
87
+ description: 'The heuristic Indicator is based on the separately represented domain observation.',
88
+ });
89
+ return objects;
90
+ }
91
+ export function buildStixIndicatorExport(records, options = {}) {
92
+ const generatedAt = isoTimestamp(options.generatedAt) || new Date().toISOString();
93
+ const idFactory = typeof options.idFactory === 'function'
94
+ ? options.idFactory
95
+ : defaultIdFactory;
96
+ let collected;
97
+ try {
98
+ collected = collectDefensiveIndicatorCandidates(records, MAX_STIX_INDICATORS);
99
+ }
100
+ catch (cause) {
101
+ if (cause instanceof TypeError)
102
+ throw new TypeError('STIX indicator export requires an array of Bulk results.');
103
+ throw cause;
104
+ }
105
+ const usedIds = new Set();
106
+ const nextId = (type) => {
107
+ const id = stixId(type, idFactory);
108
+ if (usedIds.has(id))
109
+ throw new Error('The STIX identifier factory returned a duplicate identifier.');
110
+ usedIds.add(id);
111
+ return id;
112
+ };
113
+ const producerId = nextId('identity');
114
+ const objects = [stixIndicatorProducer(producerId, generatedAt)];
115
+ for (const { domain, source } of collected.entries) {
116
+ objects.push(...stixIndicatorObjects(domain, record(source).availability, defensiveIndicatorProvenance(source), generatedAt, producerId, nextId));
117
+ }
118
+ const bundle = { type: 'bundle', id: nextId('bundle'), objects };
119
+ return {
120
+ version: STIX_INDICATOR_EXPORT_VERSION,
121
+ format: 'stix',
122
+ generatedAt,
123
+ domains: collected.domains,
124
+ truncated: collected.truncated,
125
+ filename: `whoisleuth-defensive-domains-${generatedAt.slice(0, 10)}.stix.json`,
126
+ mimeType: 'application/stix+json;charset=utf-8',
127
+ content: `${JSON.stringify(bundle, null, 2)}\n`,
128
+ };
129
+ }
@@ -1,7 +1,8 @@
1
- import { normalizeDomain } from "../cases/case-model.mjs";
2
- import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_SUMMARY_LENGTH, parseExternalFindingsDocument, } from "./external-findings-import.mjs";
3
- import { MAX_WEB_CAPTURE_DOM_DIGEST_BYTES, MAX_WEB_CAPTURE_SCREENSHOT_BYTES, WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "../contracts/web-capture.mjs";
1
+ import { normalizeDomain } from "../evidence/domain-name.mjs";
2
+ import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_SUMMARY_LENGTH, parseExternalFindingsDocument, retainExternalFindingLimitations, } from "./external-findings-import.mjs";
3
+ import { MAX_WEB_CAPTURE_DOM_DIGEST_BYTES, MAX_WEB_CAPTURE_SCREENSHOT_BYTES, WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "../contracts/web-capture.mjs";
4
4
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
5
+ import { readManifestPageBehaviour } from "../investigation/page-behaviour.mjs";
5
6
  import { readCaptureConditions, readObservationLabel } from "../comparison/capture-context.mjs";
6
7
  export { WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "../contracts/web-capture.mjs";
7
8
  export const MAX_WEB_CAPTURE_SUMMARIES = 50;
@@ -25,6 +26,7 @@ const CAPTURE_KEYS = new Set([
25
26
  'networkOrigins',
26
27
  ]);
27
28
  const MANIFEST_CAPTURE_KEYS = new Set([
29
+ 'pageBehaviour',
28
30
  'conditions', 'observerLabel', 'vantageLabel',
29
31
  'domain',
30
32
  'capturedAt',
@@ -151,17 +153,17 @@ export function parseWebCaptureSummary(value) {
151
153
  ].filter(Boolean);
152
154
  if (!summaries.length)
153
155
  throw new Error(`Web capture ${index + 1} contains no supported summary evidence.`);
156
+ const retainedLimitations = retainExternalFindingLimitations(limitations, [
157
+ 'Imported sanitised capture summary; WHOISleuth did not collect or independently verify this observation.',
158
+ ]);
154
159
  findings.push({
155
160
  domain,
156
161
  category: technologies.length && summaries.length === 1 ? 'http' : 'page',
157
162
  evidenceClass: 'deployment_observation',
158
163
  summary: summaries.join(' '),
159
164
  observedAt,
160
- completeness,
161
- limitations: [
162
- 'Imported sanitised capture summary; WHOISleuth did not collect or independently verify this observation.',
163
- ...limitations,
164
- ].slice(0, 8),
165
+ completeness: limitations.every((item) => retainedLimitations.includes(item)) ? completeness : 'partial',
166
+ limitations: retainedLimitations,
165
167
  reference: sourceReference,
166
168
  });
167
169
  }
@@ -230,7 +232,7 @@ export function readWebCaptureManifest(value) {
230
232
  if (!root
231
233
  || !onlyKeys(root, ROOT_KEYS)
232
234
  || root.schema !== WEB_CAPTURE_MANIFEST_SCHEMA
233
- || root.schemaVersion !== WEB_CAPTURE_MANIFEST_VERSION) {
235
+ || !WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS.includes(root.schemaVersion)) {
234
236
  throw new Error(`Web capture manifests must use ${WEB_CAPTURE_MANIFEST_SCHEMA} schema version ${WEB_CAPTURE_MANIFEST_VERSION}.`);
235
237
  }
236
238
  const source = record(root.source);
@@ -270,7 +272,10 @@ export function readWebCaptureManifest(value) {
270
272
  ? capture.completeness
271
273
  : 'unknown';
272
274
  const limitations = stringList(capture.limitations, 8, 240, `Web capture manifest ${index + 1} limitations`);
273
- contexts.push({ domain, observedAt, completeness: String(completeness), conditions, observerLabel, vantageLabel });
275
+ const pageBehaviour = readManifestPageBehaviour(capture.pageBehaviour, root.schemaVersion);
276
+ if (pageBehaviour?.state === 'partial' && completeness === 'complete')
277
+ throw new Error('Partial page observations cannot declare a complete capture.');
278
+ contexts.push({ domain, observedAt, completeness: String(completeness), conditions, observerLabel, vantageLabel, pageBehaviour });
274
279
  const page = record(capture.page);
275
280
  if (page && !onlyKeys(page, PAGE_KEYS))
276
281
  throw new Error(`Web capture manifest ${index + 1} page metadata contains unsupported fields.`);
@@ -334,6 +339,8 @@ export function readWebCaptureManifest(value) {
334
339
  : '',
335
340
  ...listSummaryFragments('Observed technology labels', technologies),
336
341
  ...listSummaryFragments('Observed request domains', requestDomains),
342
+ ...(pageBehaviour ? [`Page observations: ${pageBehaviour.requests.length} navigation/script/frame responses; ${pageBehaviour.elements.length} script/frame/form elements; ${pageBehaviour.clipboardWriteAttempts} blocked Clipboard API write attempts. State: ${pageBehaviour.state}. Full observations remain in the selected manifest.`,
343
+ ...listSummaryFragments('Requested-action wording in body text', pageBehaviour.actionHints)] : []),
337
344
  ...artifactSummaries,
338
345
  ].filter(Boolean);
339
346
  const summaries = partitionSummary(summaryFragments);
@@ -342,6 +349,9 @@ export function readWebCaptureManifest(value) {
342
349
  throw new Error(`Web capture manifests exceed the ${MAX_EXTERNAL_FINDINGS_PER_DOMAIN}-finding per-domain import limit after preserving bounded metadata.`);
343
350
  }
344
351
  findingCounts.set(domain, priorFindingCount + summaries.length);
352
+ const retainedLimitations = retainExternalFindingLimitations(limitations, [
353
+ 'Imported capture metadata, not independently collected website evidence; artefact bytes and separate byte checks are not retained in these findings.',
354
+ ]);
345
355
  for (const summary of summaries) {
346
356
  findings.push({
347
357
  domain,
@@ -349,11 +359,8 @@ export function readWebCaptureManifest(value) {
349
359
  evidenceClass: 'deployment_observation',
350
360
  summary,
351
361
  observedAt,
352
- completeness,
353
- limitations: [
354
- 'Imported capture metadata, not independently collected website evidence; artefact bytes and separate byte checks are not retained in these findings.',
355
- ...limitations,
356
- ].slice(0, 8),
362
+ completeness: limitations.every((item) => retainedLimitations.includes(item)) ? completeness : 'partial',
363
+ limitations: retainedLimitations,
357
364
  reference: sourceReference,
358
365
  });
359
366
  }
@@ -1,7 +1,11 @@
1
1
  import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
2
2
  import { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION, } from "../contracts/investigation-portability.mjs";
3
- import { isValidAsciiDomainName } from "../../lib/hostname.mjs";
3
+ import { isValidAsciiDomainName } from "../contracts/domain-name.mjs";
4
+ import { parseBoundedJsonObject } from "../analysis/bounded-json.mjs";
5
+ import { MAX_INVESTIGATION_PORTABLE_BYTES } from "../contracts/investigation-portability.mjs";
6
+ import { array, boolean, domain, enumeration, exact, fail, iso, sameValues, strings, text as structureText, validateIntegrity } from "../evidence/artifact-structure.mjs";
4
7
  export { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION };
8
+ export { MAX_INVESTIGATION_PORTABLE_BYTES as MAX_ACQUISITION_DECISION_PACKET_BYTES };
5
9
  export const ACQUISITION_DECISIONS = [
6
10
  'unresolved',
7
11
  'continue_manual_review',
@@ -18,6 +22,80 @@ export const ACQUISITION_MANUAL_CHECKS = [
18
22
  const DECISION_SET = new Set(ACQUISITION_DECISIONS);
19
23
  const CHECK_SET = new Set(ACQUISITION_MANUAL_CHECKS);
20
24
  const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
25
+ function validateAcquisitionItem(value, label) {
26
+ const item = exact(value, ['id', 'label', 'state', 'detail', 'provenance'], label);
27
+ const id = enumeration(item.id, ['availability', 'contacts', 'lifecycle', 'mail', 'nameservers', 'operations', 'policy_eligibility', 'policy_lifecycle', 'policy_transfer', 'tls', 'transfer', 'web'], label);
28
+ enumeration(item.state, ['authoritative', 'observed', 'review', 'unavailable'], label);
29
+ structureText(item.label, label, 200);
30
+ structureText(item.detail, label, 2_000);
31
+ structureText(item.provenance, label, 300);
32
+ return id;
33
+ }
34
+ export function validateAcquisitionDecisionPacketStructure(value) {
35
+ const root = exact(value, ['schema', 'version', 'generatedAt', 'target', 'synthetic', 'evidenceObservedAt', 'analystReview', 'evidenceReview', 'limitations', 'integrity'], 'Acquisition decision packet');
36
+ if (root.schema !== ACQUISITION_DECISION_PACKET_SCHEMA)
37
+ fail('Acquisition decision packet schema');
38
+ iso(root.generatedAt, 'Acquisition decision packet generatedAt');
39
+ domain(root.target, 'Acquisition decision packet target');
40
+ boolean(root.synthetic, 'Acquisition decision packet synthetic');
41
+ iso(root.evidenceObservedAt, 'Acquisition decision packet evidenceObservedAt', true);
42
+ const review = exact(root.analystReview, ['decision', 'rationale', 'reviewedChecks', 'outstandingChecks', 'state'], 'Acquisition analyst review');
43
+ const decision = enumeration(review.decision, ACQUISITION_DECISIONS, 'Acquisition analyst review decision');
44
+ structureText(review.rationale, 'Acquisition analyst review rationale', 2_000, true);
45
+ const reviewed = strings(review.reviewedChecks, 'Acquisition reviewed checks', ACQUISITION_MANUAL_CHECKS.length, 40);
46
+ const outstanding = strings(review.outstandingChecks, 'Acquisition outstanding checks', ACQUISITION_MANUAL_CHECKS.length, 40);
47
+ const expectedReviewed = ACQUISITION_MANUAL_CHECKS.filter((item) => reviewed.includes(item));
48
+ const expectedOutstanding = ACQUISITION_MANUAL_CHECKS.filter((item) => !reviewed.includes(item));
49
+ if (!sameValues(reviewed, expectedReviewed) || !sameValues(outstanding, expectedOutstanding))
50
+ fail('Acquisition analyst review checks');
51
+ const expectedState = decision === 'unresolved' || reviewed.length < ACQUISITION_MANUAL_CHECKS.length ? 'draft' : 'reviewed';
52
+ if (review.state !== expectedState)
53
+ fail('Acquisition analyst review state');
54
+ const evidence = exact(root.evidenceReview, ['version', 'label', 'state', 'items', 'transitionDependencies', 'policyChecks', 'nextSteps', 'limitations'], 'Acquisition evidence review');
55
+ if (evidence.version !== 2)
56
+ fail('Acquisition evidence review');
57
+ structureText(evidence.label, 'Acquisition evidence review label', 200);
58
+ enumeration(evidence.state, ['incomplete', 'registered', 'review_transition', 'sale_signal', 'unregistered_observation'], 'Acquisition evidence review state');
59
+ const expectedEvidenceIds = {
60
+ items: ['availability', 'lifecycle', 'transfer', 'operations', 'contacts'],
61
+ transitionDependencies: ['nameservers', 'web', 'mail', 'tls'],
62
+ policyChecks: ['policy_eligibility', 'policy_lifecycle', 'policy_transfer'],
63
+ };
64
+ for (const key of ['items', 'transitionDependencies', 'policyChecks']) {
65
+ const expected = expectedEvidenceIds[key];
66
+ const values = array(evidence[key], `Acquisition evidence review ${key}`, expected.length, expected.length);
67
+ const ids = values.map((item, index) => validateAcquisitionItem(item, `Acquisition evidence review ${key}[${index}]`));
68
+ if (!sameValues(ids, expected))
69
+ fail(`Acquisition evidence review ${key}`);
70
+ }
71
+ strings(evidence.nextSteps, 'Acquisition next steps', 6, 600);
72
+ strings(evidence.limitations, 'Acquisition evidence limitations', 12, 600);
73
+ strings(root.limitations, 'Acquisition limitations', 8, 600);
74
+ validateIntegrity(root.integrity, 'Acquisition integrity', root.version, ACQUISITION_DECISION_PACKET_VERSION);
75
+ }
76
+ function freezePacket(value) {
77
+ if (value && typeof value === 'object') {
78
+ for (const item of Object.values(value))
79
+ freezePacket(item);
80
+ Object.freeze(value);
81
+ }
82
+ }
83
+ /** Validate the existing supported format and full canonical digest before preview. */
84
+ export async function readAcquisitionDecisionPacket(raw, expected) {
85
+ const value = parseBoundedJsonObject(raw, { label: 'Acquisition decision packet', maximumBytes: MAX_INVESTIGATION_PORTABLE_BYTES });
86
+ if (value.schema !== ACQUISITION_DECISION_PACKET_SCHEMA)
87
+ fail('Acquisition decision packet schema');
88
+ validateAcquisitionDecisionPacketStructure(value);
89
+ const { integrity, ...unsigned } = value;
90
+ if (await sha256ArtifactDigestV2(unsigned) !== integrity.digestSha256) {
91
+ throw new TypeError('The acquisition decision packet failed its integrity check.');
92
+ }
93
+ if (value.target !== target(expected.target) || value.synthetic !== expected.synthetic) {
94
+ throw new TypeError('This packet belongs to a different target or demonstration context. The current review is unchanged.');
95
+ }
96
+ freezePacket(value);
97
+ return value;
98
+ }
21
99
  function text(value, maximum) {
22
100
  return typeof value === 'string'
23
101
  ? value.replace(CONTROL_CHARACTERS, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
@@ -1,15 +1,8 @@
1
1
  import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
2
+ import { PASSIVE_MAIL_INTERPRETATION, PASSIVE_MAIL_LABELS } from "../contracts/passive-mail.mjs";
2
3
  import { BULK_PROFILE_CONTEXT_MISMATCH_LIMITATION, normalizeBulkProfileContext, normalizeBulkSessionResult, } from "../workspace/bulk-session-model.mjs";
3
4
  import { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_VERSION, MAX_BULK_MAIL_EXPOSURE_ROWS, } from "../contracts/investigation-portability.mjs";
4
5
  export { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_VERSION, MAX_BULK_MAIL_EXPOSURE_ROWS };
5
- const STATE_LABELS = {
6
- authenticated_mail: 'Receiving mail with SPF and DMARC',
7
- evidence_incomplete: 'Mail evidence incomplete',
8
- mail_auth_gap: 'Receiving mail with an authentication gap',
9
- mail_auth_incomplete: 'Receiving mail with incomplete authentication evidence',
10
- no_explicit_mx: 'No explicit MX observed',
11
- null_mx: 'Null MX observed',
12
- };
13
6
  function timestamp(value) {
14
7
  if (typeof value !== 'string' || !Number.isFinite(Date.parse(value)))
15
8
  return null;
@@ -51,16 +44,16 @@ function exposureState(row) {
51
44
  }
52
45
  function stateDetail(state) {
53
46
  if (state === 'authenticated_mail') {
54
- return 'An explicit receiving MX plus SPF and DMARC publications were observed.';
47
+ return 'Explicit MX, SPF and DMARC publications were observed.';
55
48
  }
56
49
  if (state === 'null_mx') {
57
50
  return 'A null MX explicitly declaring no inbound mail was observed.';
58
51
  }
59
52
  if (state === 'mail_auth_gap') {
60
- return 'A receiving MX was observed while SPF or DMARC was observed absent.';
53
+ return 'An MX publication was observed while SPF or DMARC was observed absent.';
61
54
  }
62
55
  if (state === 'mail_auth_incomplete') {
63
- return 'A receiving MX was observed, but SPF or DMARC collection was incomplete.';
56
+ return 'An MX publication was observed, but SPF or DMARC collection was incomplete.';
64
57
  }
65
58
  if (state === 'no_explicit_mx') {
66
59
  return 'DNS collection completed without an explicit MX. This is not proof that SMTP delivery is impossible.';
@@ -173,7 +166,7 @@ export function buildBulkMailExposureReport(rowsRaw, options = {}) {
173
166
  return {
174
167
  domain: item.domain,
175
168
  state,
176
- label: STATE_LABELS[state],
169
+ label: PASSIVE_MAIL_LABELS[state],
177
170
  detail: stateDetail(state),
178
171
  ...relation(state, profile, profileSourceState, profileContextComparable, profileContextLimitation),
179
172
  mutationTypes: item.mutationTypes.slice(0, 40),
@@ -217,8 +210,9 @@ export function buildBulkMailExposureReport(rowsRaw, options = {}) {
217
210
  counts,
218
211
  profileContextUnevaluatedCount,
219
212
  limitations: [
213
+ PASSIVE_MAIL_INTERPRETATION,
220
214
  'This review uses compact Bulk evidence already collected and makes no additional request.',
221
- 'Null MX, no explicit MX, receiving mail, authentication gaps, and incomplete evidence remain separate states.',
215
+ 'Null MX, no explicit MX, published mail policies, policy gaps, and incomplete evidence remain separate states.',
222
216
  'SMTP delivery, mailbox existence, catch-all behaviour, and message acceptance were not tested.',
223
217
  ],
224
218
  };
@@ -0,0 +1,57 @@
1
+ import { array, boolean, enumeration, exact, integer, text } from "../evidence/artifact-structure.mjs";
2
+ export const MAX_CAPTURE_CHANNEL_OBSERVATIONS = 500;
3
+ export const CAPTURE_REQUEST_CHANNELS = ['navigation', 'document', 'frame', 'script', 'stylesheet', 'image', 'font', 'media', 'xhr', 'fetch', 'beacon', 'event_source', 'other'];
4
+ export const CAPTURE_DISABLED_SURFACES = ['service_workers', 'dedicated_workers', 'shared_workers', 'websockets', 'webrtc', 'webtransport', 'downloads'];
5
+ export function emptyCaptureCoverage() {
6
+ return { attempts: [], omittedAttempts: 0, disabledSurfaces: [...CAPTURE_DISABLED_SURFACES], interactions: 'not_exercised', websocketRefusals: 0, directConnectionRefusals: 0 };
7
+ }
8
+ export function captureCoverageIsPartial(value) {
9
+ return Boolean(value.omittedAttempts || value.websocketRefusals || value.directConnectionRefusals || value.attempts.some(row => row.state !== 'observed'));
10
+ }
11
+ export function readCaptureCoverage(raw) {
12
+ const value = exact(raw, ['attempts', 'omittedAttempts', 'disabledSurfaces', 'interactions', 'websocketRefusals', 'directConnectionRefusals'], 'Capture coverage');
13
+ const disabledSurfaces = array(value.disabledSurfaces, 'Disabled capture surfaces', CAPTURE_DISABLED_SURFACES.length).map(value => enumeration(value, CAPTURE_DISABLED_SURFACES, 'Disabled capture surface'));
14
+ if (new Set(disabledSurfaces).size !== CAPTURE_DISABLED_SURFACES.length)
15
+ throw new TypeError('Capture coverage must identify every required disabled surface.');
16
+ const attempts = array(value.attempts, 'Capture request attempts', MAX_CAPTURE_CHANNEL_OBSERVATIONS).map(raw => {
17
+ const row = exact(raw, ['position', 'channel', 'method', 'origin', 'state', 'reason', 'collectionStarted'], 'Capture request attempt');
18
+ const state = enumeration(row.state, ['observed', 'refused', 'unavailable'], 'Capture request state');
19
+ const method = enumeration(row.method, ['read', 'non_read', 'unknown'], 'Capture method category');
20
+ const reason = row.reason === null ? null : enumeration(row.reason, ['method', 'request_bound', 'host_bound', 'response_bound', 'shutdown', 'address_or_transport'], 'Capture request qualification');
21
+ boolean(row.collectionStarted, 'Collector-started flag');
22
+ let origin = null;
23
+ if (row.origin !== null) {
24
+ const candidate = text(row.origin, 'Capture request origin', 500), parsed = new URL(candidate);
25
+ if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin !== candidate || parsed.username || parsed.password)
26
+ throw new TypeError('Capture request coverage retains origins only.');
27
+ origin = candidate;
28
+ }
29
+ if (state === 'observed' && (reason !== null || method !== 'read' || !origin || row.collectionStarted !== true)
30
+ || state !== 'observed' && reason === null
31
+ || reason === 'method' && (method === 'read' || row.collectionStarted)
32
+ || ['request_bound', 'host_bound', 'shutdown'].includes(reason ?? '') && row.collectionStarted
33
+ || state === 'unavailable' && reason !== 'address_or_transport')
34
+ throw new TypeError('Capture request coverage has contradictory collection states.');
35
+ return { position: integer(row.position, 'Capture request position', 1, 1_000_000), channel: enumeration(row.channel, CAPTURE_REQUEST_CHANNELS, 'Capture request channel'),
36
+ method, origin, state, reason, collectionStarted: row.collectionStarted };
37
+ });
38
+ if (attempts.some((row, index) => index > 0 && row.position <= attempts[index - 1].position))
39
+ throw new TypeError('Capture request positions must be unique and ordered.');
40
+ return { attempts, omittedAttempts: integer(value.omittedAttempts, 'Omitted capture attempts', 0, 1_000_000), disabledSurfaces: [...CAPTURE_DISABLED_SURFACES],
41
+ interactions: enumeration(value.interactions, ['not_exercised'], 'Capture interaction coverage'),
42
+ websocketRefusals: integer(value.websocketRefusals, 'WebSocket refusals', 0, 1_000_000), directConnectionRefusals: integer(value.directConnectionRefusals, 'Direct-connection refusals', 0, 1_000_000) };
43
+ }
44
+ export function captureAttemptDescription(value) {
45
+ const status = value.state === 'observed' ? 'Response supplied' : value.state === 'refused' ? 'Response refused' : 'Response unavailable';
46
+ const reason = { method: 'non-read method', request_bound: 'request limit', host_bound: 'host limit', response_bound: 'response byte limit', shutdown: 'capture closing', address_or_transport: 'address or transport check' };
47
+ return `${status}${value.reason ? ` · ${reason[value.reason]}` : ''} · collector ${value.collectionStarted ? 'started' : 'not started'}`;
48
+ }
49
+ /** Unseen in a bounded capture is not the same as disabled or absent. */
50
+ export function captureChannelSummary(value) {
51
+ return CAPTURE_REQUEST_CHANNELS.map(channel => {
52
+ const rows = value.attempts.filter(row => row.channel === channel);
53
+ return { channel, observed: rows.filter(row => row.state === 'observed').length, refused: rows.filter(row => row.state === 'refused').length,
54
+ unavailable: rows.filter(row => row.state === 'unavailable').length,
55
+ coverage: rows.length ? 'recorded' : value.omittedAttempts ? 'unknown' : 'not_observed' };
56
+ });
57
+ }
@@ -0,0 +1,100 @@
1
+ import { array, exact, iso, record, text } from "../evidence/artifact-structure.mjs";
2
+ import { pageObservationOrigin } from "./page-behaviour.mjs";
3
+ import { CONTEXT_REVIEW_SCHEMA, CONTEXT_REVIEW_VERSION, MAX_CONTEXT_RECORDS, MAX_CONTEXT_INPUT_BYTES } from "../contracts/context-review.mjs";
4
+ import { parseBoundedJson } from "../analysis/bounded-json.mjs";
5
+ export { CONNECTOR_INPUT_SCHEMA, CONNECTOR_INPUT_VERSION } from "../contracts/context-review.mjs";
6
+ const KNOWN_FIELDS = new Set(['url', 'serverUrl', 'command', 'args', 'env', 'headers', 'type', 'transport', 'disabled', 'enabled', 'capabilities', 'package', 'oauth', 'auth']);
7
+ const CAPABILITIES = ['tools', 'resources', 'prompts', 'logging', 'completions'];
8
+ const PACKAGE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[a-zA-Z0-9][a-zA-Z0-9.+_-]*)?$/u;
9
+ function countObject(value, label) {
10
+ if (value === undefined)
11
+ return 0;
12
+ const keys = Object.keys(record(value, label));
13
+ if (keys.length > MAX_CONTEXT_RECORDS)
14
+ throw new TypeError(`${label} exceeds the metadata bound.`);
15
+ return keys.length;
16
+ }
17
+ /** Configuration is data only. Values of arguments, environment, headers and auth are never returned. */
18
+ export function readConnectorConfiguration(raw) {
19
+ const root = record(raw, 'Connector configuration');
20
+ const roots = ['mcpServers', 'servers'].filter(key => Object.hasOwn(root, key));
21
+ if (roots.length !== 1)
22
+ throw new TypeError('Select a configuration with exactly one mcpServers or servers object.');
23
+ if (Object.keys(root).some(key => !roots.includes(key)))
24
+ throw new TypeError('Select only the connector configuration section; unrelated settings are not reviewed.');
25
+ const entries = Object.entries(record(root[roots[0]], 'Connector entries'));
26
+ if (entries.length > MAX_CONTEXT_RECORDS)
27
+ throw new TypeError('Connector count exceeds the review bound.');
28
+ return entries.map(([name, value]) => {
29
+ text(name, 'Connector name', 200);
30
+ const item = record(value, 'Connector definition');
31
+ if (Object.keys(item).length > MAX_CONTEXT_RECORDS)
32
+ throw new TypeError('Connector definition exceeds the field bound.');
33
+ if (item.enabled !== undefined && typeof item.enabled !== 'boolean' || item.disabled !== undefined && typeof item.disabled !== 'boolean'
34
+ || item.enabled !== undefined && item.disabled !== undefined && item.enabled === item.disabled)
35
+ throw new TypeError('Connector enablement declarations are invalid or conflicting.');
36
+ const suppliedUrl = item.url ?? item.serverUrl;
37
+ if (item.url !== undefined && item.serverUrl !== undefined && item.url !== item.serverUrl)
38
+ throw new TypeError('Conflicting connector URL declarations require a deliberate selection.');
39
+ const origin = suppliedUrl === undefined ? null : pageObservationOrigin(suppliedUrl);
40
+ if (suppliedUrl !== undefined && !origin)
41
+ throw new TypeError('Remote connector URL must be bounded HTTP(S) without credentials.');
42
+ const command = item.command === undefined ? null : text(item.command, 'Local connector command', 2_000);
43
+ const basename = command?.split(/[\\/]/u).at(-1) ?? null;
44
+ const executable = basename && /^[A-Za-z0-9._-]{1,120}$/u.test(basename) ? basename : null;
45
+ const args = item.args === undefined ? [] : array(item.args, 'Connector arguments', MAX_CONTEXT_RECORDS).map(value => text(value, 'Connector argument', 8_192, true));
46
+ let packageIdentity = item.package === undefined ? null : text(item.package, 'Declared connector package', 240);
47
+ if (packageIdentity !== null && !PACKAGE.test(packageIdentity))
48
+ throw new TypeError('Declared package must be a package identity, not an executable expression or URL.');
49
+ if (packageIdentity === null && ['npx', 'npx.cmd', 'uvx'].includes(executable ?? '')) {
50
+ // Only a simple first package operand is interpreted. Complex invocations remain unknown.
51
+ const operands = args.filter(value => !['--yes', '-y', '--no-install'].includes(value));
52
+ if (operands[0] && PACKAGE.test(operands[0]))
53
+ packageIdentity = operands[0];
54
+ }
55
+ const capabilityObject = item.capabilities === undefined ? {} : record(item.capabilities, 'Declared capabilities');
56
+ if (Object.keys(capabilityObject).length > MAX_CONTEXT_RECORDS)
57
+ throw new TypeError('Declared capabilities exceed the bound.');
58
+ return { name, transport: origin && command ? 'ambiguous' : origin ? 'remote' : command ? 'local' : 'unknown', enabled: item.enabled === false || item.disabled === true ? 'disabled' : item.enabled === true || item.disabled === false ? 'enabled' : 'unknown', origin, executable, package: packageIdentity,
59
+ argumentCount: args.length, environmentCount: countObject(item.env, 'Environment metadata'), headerCount: countObject(item.headers, 'Header metadata'),
60
+ authenticationDeclared: item.auth !== undefined || item.oauth !== undefined,
61
+ declaredCapabilities: CAPABILITIES.filter(key => Object.hasOwn(capabilityObject, key) && capabilityObject[key] !== false && capabilityObject[key] !== null),
62
+ unsupportedFields: Object.keys(item).filter(key => !KNOWN_FIELDS.has(key)).length + Object.keys(capabilityObject).filter(key => !CAPABILITIES.includes(key)).length,
63
+ privateLocationOmitted: suppliedUrl !== undefined && new URL(suppliedUrl).href !== `${origin}/` || command !== null && command !== executable,
64
+ };
65
+ });
66
+ }
67
+ export function connectorProvenancePresentation(raw, reviewedAt) {
68
+ iso(reviewedAt, 'Review time');
69
+ const input = exact(raw, ['current', 'previous'], 'Connector review');
70
+ const current = readConnectorConfiguration(input.current), previous = input.previous === null ? null : readConnectorConfiguration(input.previous);
71
+ const signature = (row) => JSON.stringify([row.transport, row.enabled, row.origin, row.executable, row.package, row.argumentCount, row.environmentCount, row.headerCount, row.authenticationDeclared, row.declaredCapabilities]);
72
+ const observations = current.map(row => {
73
+ const old = previous?.find(value => value.name === row.name);
74
+ const changed = old ? signature(old) !== signature(row) : false;
75
+ return { label: `${row.name} · ${row.transport} connector`, state: row.transport === 'unknown' || row.transport === 'ambiguous' || row.unsupportedFields ? 'partial' : changed ? 'changed' : 'reported',
76
+ detail: `Declared enablement: ${row.enabled}. Endpoint: ${row.origin ?? 'not supplied'}; executable: ${row.executable ?? 'not retained or unknown'}; package: ${row.package ?? 'not established'}. Arguments: ${row.argumentCount}; environment entries: ${row.environmentCount}; headers: ${row.headerCount}; auth declaration ${row.authenticationDeclared ? 'present' : 'not supplied'}. Capabilities: ${row.declaredCapabilities.join(', ') || 'not declared'}. ${previous ? old ? changed ? 'Retained metadata changed.' : 'Retained metadata matches; excluded values were not compared.' : 'Newly listed connector.' : 'No earlier configuration selected.'}${row.unsupportedFields ? ` Uninterpreted fields: ${row.unsupportedFields}.` : ''}${row.privateLocationOmitted ? ' Private location detail omitted.' : ''}`,
77
+ source: 'Analyst-selected local configuration; declarations were not negotiated with a server', observedAt: null, hostname: row.origin ? new URL(row.origin).hostname : null };
78
+ });
79
+ for (const row of previous ?? [])
80
+ if (!current.some(value => value.name === row.name))
81
+ observations.push({ label: `${row.name} · no longer listed`, state: 'changed', detail: 'Absent from the selected current configuration; this does not establish that a process or endpoint has stopped.', source: 'Selected configuration comparison', observedAt: null, hostname: row.origin ? new URL(row.origin).hostname : null });
82
+ const report = { schema: CONTEXT_REVIEW_SCHEMA, version: CONTEXT_REVIEW_VERSION, kind: 'connector', reviewedAt, title: 'Connector provenance', state: !current.length || observations.some(row => row.state === 'partial') ? 'partial' : 'reviewed',
83
+ summary: `Configured connectors: ${current.length}${previous ? `; earlier configuration entries: ${previous.length}` : ''}. Commands were not run and endpoints were not contacted.`, observations,
84
+ nextSteps: ['Verify package publisher, exact version, installation source and requested privileges through a trusted source before enabling a local connector.', 'For remote connectors, investigate the endpoint domain separately and review authentication and data-sharing scope before granting access.', 'Review tool, resource and prompt capabilities against the task. Configuration declarations are not evidence of negotiated capabilities or trustworthy behaviour.'],
85
+ limitations: ['Arguments, local paths, environment values, header values, auth material and URL paths or queries are excluded from this report. Names and package identities remain visible; review them before sharing.', 'Matching retained metadata does not mean excluded values are unchanged. This is not code execution, a server connection, a protocol conformance test or safety certification.'] };
86
+ return { report, connectors: current };
87
+ }
88
+ export function reviewConnectorProvenance(raw, reviewedAt) {
89
+ return connectorProvenancePresentation(raw, reviewedAt).report;
90
+ }
91
+ export function reviewConnectorConfigurationText(current, previous, reviewedAt) {
92
+ return connectorConfigurationPresentation(current, previous, reviewedAt).report;
93
+ }
94
+ /** Browser presentation receives only the same minimised metadata as the report. */
95
+ export function connectorConfigurationPresentation(current, previous, reviewedAt) {
96
+ if (typeof current !== 'string' || typeof previous !== 'string')
97
+ throw new TypeError('Select connector configuration text.');
98
+ const parse = (value) => parseBoundedJson(value, { label: 'Connector configuration', maximumBytes: MAX_CONTEXT_INPUT_BYTES });
99
+ return connectorProvenancePresentation({ current: parse(current), previous: previous.trim() ? parse(previous) : null }, reviewedAt);
100
+ }
@@ -0,0 +1,35 @@
1
+ import { parseBoundedJson } from "../analysis/bounded-json.mjs";
2
+ import { exact, text } from "../evidence/artifact-structure.mjs";
3
+ import { readEditableCaseExport } from "../cases/case-export-input.mjs";
4
+ import { MAX_CONTEXT_INPUT_BYTES, DOMAIN_HISTORY_LEGACY_INPUT_VERSION } from "../contracts/context-review.mjs";
5
+ import { DOMAIN_HISTORY_INPUT_SCHEMA, DOMAIN_HISTORY_INPUT_VERSION, reviewDomainHistory } from "./domain-history-review.mjs";
6
+ import { PLATFORM_CONTINUITY_INPUT_SCHEMA, PLATFORM_CONTINUITY_INPUT_VERSION, reviewPlatformContinuity } from "./platform-continuity-review.mjs";
7
+ import { STOREFRONT_INPUT_SCHEMA, STOREFRONT_INPUT_VERSION, reviewStorefront } from "./storefront-review.mjs";
8
+ import { CONNECTOR_INPUT_SCHEMA, CONNECTOR_INPUT_VERSION, reviewConnectorProvenance } from "./connector-provenance-review.mjs";
9
+ import { INCIDENT_SEQUENCE_INPUT_SCHEMA, INCIDENT_SEQUENCE_INPUT_VERSION, reviewIncidentSequence } from "./incident-sequence-review.mjs";
10
+ export { CONTEXT_INPUT_SCHEMAS } from "../contracts/context-review.mjs";
11
+ /** Adapters share the same explicit dispatch; each domain owns its validation. */
12
+ export function reviewContextInput(raw, reviewedAt) {
13
+ const input = exact(raw, ['schema', 'version', 'evidence'], 'Context review input');
14
+ if (input.schema === DOMAIN_HISTORY_INPUT_SCHEMA && (input.version === DOMAIN_HISTORY_INPUT_VERSION || input.version === DOMAIN_HISTORY_LEGACY_INPUT_VERSION)) {
15
+ const evidence = exact(input.evidence, ['caseExport', 'caseId', 'declarations'], 'Domain history input');
16
+ const caseId = text(evidence.caseId, 'Selected Case ID', 240);
17
+ const cases = readEditableCaseExport(JSON.stringify(evidence.caseExport));
18
+ const selected = cases.find(record => record.id === caseId);
19
+ if (!selected)
20
+ throw new TypeError('The selected Case is not present in this export.');
21
+ return reviewDomainHistory(selected, evidence.declarations, reviewedAt, input.version);
22
+ }
23
+ if (input.schema === PLATFORM_CONTINUITY_INPUT_SCHEMA && input.version === PLATFORM_CONTINUITY_INPUT_VERSION)
24
+ return reviewPlatformContinuity(input.evidence, reviewedAt);
25
+ if (input.schema === STOREFRONT_INPUT_SCHEMA && input.version === STOREFRONT_INPUT_VERSION)
26
+ return reviewStorefront(input.evidence, reviewedAt);
27
+ if (input.schema === CONNECTOR_INPUT_SCHEMA && input.version === CONNECTOR_INPUT_VERSION)
28
+ return reviewConnectorProvenance(input.evidence, reviewedAt);
29
+ if (input.schema === INCIDENT_SEQUENCE_INPUT_SCHEMA && input.version === INCIDENT_SEQUENCE_INPUT_VERSION)
30
+ return reviewIncidentSequence(input.evidence, reviewedAt);
31
+ throw new TypeError('Unsupported context review schema or version.');
32
+ }
33
+ export function parseContextInput(input) {
34
+ return parseBoundedJson(input, { label: 'Context review input', maximumBytes: MAX_CONTEXT_INPUT_BYTES });
35
+ }