@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,30 +1,14 @@
1
1
  // Pure Brand Profile normalization and storage model. The browser wrapper owns
2
2
  // persistence and downloads; this module owns schema migration, semantic field
3
3
  // bounds, import merging, and exact serialized-byte accounting.
4
- import { normalizeDomain } from "./case-model.js";
5
- import { normalizePageBaseline } from "./page-baseline.js";
6
- import { isInformativeFaviconHash } from "./utils.js";
7
- export const BRAND_PROFILE_SCHEMA = 'whoisleuth.brand-profiles';
8
- export const BRAND_PROFILE_SCHEMA_VERSION = 6;
9
- export const MAX_PROFILES = 100;
10
- export const MAX_PROFILE_VALUES = 200;
11
- export const MAX_PROFILE_VALUE_INPUTS = MAX_PROFILE_VALUES * 4;
12
- // Profiles share the origin's browser-storage quota with cases, campaigns,
13
- // watchlists, and CT history. Fail at a predictable one-megabyte boundary.
14
- export const MAX_PROFILE_STORE_BYTES = 1024 * 1024;
15
- export const MAX_PROFILE_NAME_LENGTH = 100;
16
- export const MAX_PROFILE_TEXT_LENGTH = 200;
17
- export const MAX_PROFILE_DOMAIN_LENGTH = 253;
18
- export const MAX_PROFILE_TLD_LENGTH = 63;
19
- export const MAX_DKIM_SELECTOR_LENGTH = 253;
20
- export const MAX_DKIM_SELECTORS = 10;
21
- export const MAX_PROTECTION_ATTESTATIONS = 6;
22
- export const MAX_DESIRED_POSTURE_BASELINES = 20;
23
- export const MAX_DESIRED_POSTURE_RECORDS = 32;
24
- export const MAX_DESIRED_POSTURE_SUPPRESSIONS = 12;
25
- export const MAX_DESIRED_POSTURE_OBSERVATIONS = 12;
26
- export const MAX_DESIRED_POSTURE_CHANGE_WINDOWS = 8;
27
- const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
4
+ import { normalizeDomain } from "../cases/case-model.mjs";
5
+ import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
6
+ import { normalizeOpaqueReferenceId } from "../cases/opaque-reference-id.mjs";
7
+ import { normalizePageBaseline } from "./page-baseline.mjs";
8
+ import { isInformativePerceptualHash as isInformativeFaviconHash } from "../../lib/perceptual-hash-comparison.mjs";
9
+ import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
10
+ import { BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, MAX_DESIRED_POSTURE_BASELINES, MAX_DESIRED_POSTURE_CHANGE_WINDOWS, MAX_DESIRED_POSTURE_OBSERVATIONS, MAX_DESIRED_POSTURE_RECORDS, MAX_DESIRED_POSTURE_SUPPRESSIONS, MAX_DKIM_SELECTOR_LENGTH, MAX_DKIM_SELECTORS, MAX_PROFILES, MAX_PROFILE_DOMAIN_LENGTH, MAX_PROFILE_NAME_LENGTH, MAX_PROFILE_STORE_BYTES, MAX_PROFILE_TEXT_LENGTH, MAX_PROFILE_TLD_LENGTH, MAX_PROFILE_VALUE_INPUTS, MAX_PROFILE_VALUES, MAX_PROTECTION_ATTESTATIONS, SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
11
+ export { BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, MAX_DESIRED_POSTURE_BASELINES, MAX_DESIRED_POSTURE_CHANGE_WINDOWS, MAX_DESIRED_POSTURE_OBSERVATIONS, MAX_DESIRED_POSTURE_RECORDS, MAX_DESIRED_POSTURE_SUPPRESSIONS, MAX_DKIM_SELECTOR_LENGTH, MAX_DKIM_SELECTORS, MAX_PROFILES, MAX_PROFILE_DOMAIN_LENGTH, MAX_PROFILE_NAME_LENGTH, MAX_PROFILE_STORE_BYTES, MAX_PROFILE_TEXT_LENGTH, MAX_PROFILE_TLD_LENGTH, MAX_PROFILE_VALUE_INPUTS, MAX_PROFILE_VALUES, MAX_PROTECTION_ATTESTATIONS, SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
28
12
  const SHA256_RE = /^[a-f0-9]{64}$/i;
29
13
  const CONTROL_RE = /[\x00-\x1f\x7f]/;
30
14
  const DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -45,7 +29,7 @@ const PROTECTION_ATTESTATION_STATES = new Set([
45
29
  'unavailable',
46
30
  'not_applicable',
47
31
  ]);
48
- const DESIRED_POSTURE_FIELDS = new Set([
32
+ export const DESIRED_POSTURE_SUPPRESSION_FIELDS = Object.freeze([
49
33
  'nameservers',
50
34
  'ds',
51
35
  'mx',
@@ -56,11 +40,10 @@ const DESIRED_POSTURE_FIELDS = new Set([
56
40
  'registrar_lock',
57
41
  'renewal_review',
58
42
  ]);
43
+ const DESIRED_POSTURE_FIELDS = new Set(DESIRED_POSTURE_SUPPRESSION_FIELDS);
59
44
  const POSTURE_CHECK_STATUSES = new Set(['danger', 'info', 'pass', 'warning']);
60
45
  function record(value) {
61
- return value && typeof value === 'object' && !Array.isArray(value)
62
- ? value
63
- : {};
46
+ return ordinaryWorkspaceRecord(value, 'Brand Profile input') ?? {};
64
47
  }
65
48
  function boundedText(value, maximum = MAX_PROFILE_TEXT_LENGTH) {
66
49
  if (typeof value !== 'string' || CONTROL_RE.test(value))
@@ -68,15 +51,16 @@ function boundedText(value, maximum = MAX_PROFILE_TEXT_LENGTH) {
68
51
  return value.slice(0, maximum * 4).replace(/\s+/g, ' ').trim().slice(0, maximum).trim();
69
52
  }
70
53
  export function normalizeBrandProfileId(value) {
71
- return typeof value === 'string' && SAFE_ID_RE.test(value) ? value : null;
54
+ return normalizeOpaqueReferenceId(value);
72
55
  }
73
56
  function timestamp(value, fallback) {
74
- if (typeof value === 'string' && value.length <= 64 && !CONTROL_RE.test(value)) {
75
- const parsed = Date.parse(value);
76
- if (Number.isFinite(parsed))
77
- return new Date(parsed).toISOString();
57
+ return normalizeExplicitIsoTimestamp(value) ?? fallback;
58
+ }
59
+ function calendarTimestamp(value, fallback) {
60
+ if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/u.test(value)) {
61
+ return normalizeExplicitIsoTimestamp(`${value}T00:00:00Z`) ?? fallback;
78
62
  }
79
- return fallback;
63
+ return timestamp(value, fallback);
80
64
  }
81
65
  function normalizeTld(value) {
82
66
  if (typeof value !== 'string' || CONTROL_RE.test(value))
@@ -156,7 +140,7 @@ export function normalizeProtectionAttestations(value) {
156
140
  control: candidate.control,
157
141
  state: candidate.state,
158
142
  assertedAt,
159
- expiresAt: timestamp(candidate.expiresAt, null),
143
+ expiresAt: calendarTimestamp(candidate.expiresAt, null),
160
144
  note: boundedText(candidate.note),
161
145
  });
162
146
  if (output.length >= MAX_PROTECTION_ATTESTATIONS)
@@ -217,10 +201,25 @@ function normalizeDesiredPostureObservationHistory(value, previous) {
217
201
  .sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt))
218
202
  .slice(-MAX_DESIRED_POSTURE_OBSERVATIONS);
219
203
  }
220
- function normalizeDesiredPostureChangeWindows(value) {
204
+ function deterministicChangeWindowId(seed) {
205
+ const states = [0x811c9dc5, 0x9e3779b9, 0x85ebca6b, 0xc2b2ae35];
206
+ const factors = [0x01000193, 0x85ebca6b, 0xc2b2ae35, 0x27d4eb2f];
207
+ for (let index = 0; index < seed.length; index += 1) {
208
+ const code = seed.charCodeAt(index);
209
+ for (let stateIndex = 0; stateIndex < states.length; stateIndex += 1) {
210
+ const state = states[stateIndex] ?? 0;
211
+ const mixed = state ^ (code + Math.imul(index + 1, stateIndex + 17));
212
+ states[stateIndex] = Math.imul(mixed, factors[stateIndex] ?? 0x01000193);
213
+ }
214
+ }
215
+ return `cw-${states.map((state) => (state >>> 0).toString(16).padStart(8, '0')).join('')}`;
216
+ }
217
+ function normalizeDesiredPostureChangeWindows(value, identityScope = '') {
221
218
  if (!Array.isArray(value))
222
219
  return [];
223
220
  const output = [];
221
+ const seenIds = new Set();
222
+ const duplicateMaterial = new Map();
224
223
  for (const item of value.slice(0, MAX_DESIRED_POSTURE_CHANGE_WINDOWS * 4)) {
225
224
  const candidate = record(item);
226
225
  const startsAt = timestamp(candidate.startsAt, null);
@@ -228,7 +227,17 @@ function normalizeDesiredPostureChangeWindows(value) {
228
227
  const summary = boundedText(candidate.summary, 300);
229
228
  if (!startsAt || !endsAt || Date.parse(endsAt) <= Date.parse(startsAt) || !summary)
230
229
  continue;
231
- output.push({ startsAt, endsAt, summary });
230
+ const material = `${startsAt}\u001f${endsAt}\u001f${summary}`;
231
+ const occurrence = duplicateMaterial.get(material) ?? 0;
232
+ duplicateMaterial.set(material, occurrence + 1);
233
+ let id = normalizeOpaqueReferenceId(candidate.id);
234
+ let salt = 0;
235
+ while (!id || seenIds.has(id)) {
236
+ id = deterministicChangeWindowId(`${identityScope}\u001f${material}\u001f${occurrence}\u001f${salt}`);
237
+ salt += 1;
238
+ }
239
+ seenIds.add(id);
240
+ output.push({ id, startsAt, endsAt, summary });
232
241
  if (output.length >= MAX_DESIRED_POSTURE_CHANGE_WINDOWS)
233
242
  break;
234
243
  }
@@ -250,7 +259,7 @@ function normalizeTlsSanPatterns(value) {
250
259
  }
251
260
  return [...output].sort();
252
261
  }
253
- export function normalizeDesiredPostureBaselines(value, officialDomains, fallbackNow = new Date().toISOString()) {
262
+ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbackNow = new Date().toISOString(), identityScope = '') {
254
263
  if (!Array.isArray(value))
255
264
  return [];
256
265
  const allowedDomains = new Set(officialDomains);
@@ -275,7 +284,7 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
275
284
  suppressions.push({
276
285
  field,
277
286
  reason,
278
- expiresAt: timestamp(suppression.expiresAt, null),
287
+ expiresAt: calendarTimestamp(suppression.expiresAt, null),
279
288
  });
280
289
  if (suppressions.length >= MAX_DESIRED_POSTURE_SUPPRESSIONS)
281
290
  break;
@@ -298,7 +307,7 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
298
307
  registrarLock: ['required', 'not_required'].includes(String(candidate.registrarLock))
299
308
  ? candidate.registrarLock
300
309
  : 'unconfigured',
301
- renewalReviewAt: timestamp(candidate.renewalReviewAt, null),
310
+ renewalReviewAt: calendarTimestamp(candidate.renewalReviewAt, null),
302
311
  zoneIntent: ['active_service', 'defensive_registration', 'no_service', 'parked', 'redirect_only'].includes(String(candidate.zoneIntent))
303
312
  ? candidate.zoneIntent
304
313
  : 'unconfigured',
@@ -306,7 +315,7 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
306
315
  ? candidate.lifecycle
307
316
  : 'active',
308
317
  recoveryDependency: boundedText(candidate.recoveryDependency, 200),
309
- approvedChangeWindows: normalizeDesiredPostureChangeWindows(candidate.approvedChangeWindows),
318
+ approvedChangeWindows: normalizeDesiredPostureChangeWindows(candidate.approvedChangeWindows, `${identityScope}\u001f${domain}`),
310
319
  suppressions,
311
320
  note: boundedText(candidate.note, MAX_PROFILE_TEXT_LENGTH),
312
321
  previousObservation: observationHistory.at(-1) ?? previousObservation,
@@ -331,9 +340,12 @@ export function normalizeBrandProfile(raw, options = {}) {
331
340
  const now = timestamp(options.nowIso, new Date().toISOString());
332
341
  const officialDomains = normalizeProfileDomains(value.officialDomains);
333
342
  const dkimSelectors = normalizeDkimSelectors(value.dkimSelectors);
334
- const candidateBaseline = Object.prototype.hasOwnProperty.call(value, 'pageBaseline')
343
+ const suppliedBaseline = Object.prototype.hasOwnProperty.call(value, 'pageBaseline');
344
+ const candidateBaseline = suppliedBaseline
335
345
  ? normalizePageBaseline(value.pageBaseline)
336
346
  : normalizePageBaseline(existing?.pageBaseline);
347
+ const baselineDomainMismatch = suppliedBaseline && candidateBaseline !== null
348
+ && !officialDomains.includes(candidateBaseline.domain);
337
349
  const pageBaseline = candidateBaseline && officialDomains.includes(candidateBaseline.domain)
338
350
  ? candidateBaseline
339
351
  : null;
@@ -358,11 +370,11 @@ export function normalizeBrandProfile(raw, options = {}) {
358
370
  .filter((selector) => !dkimSelectors.includes(selector)),
359
371
  mailProtectionProfile: normalizeMailProtectionProfile(value.mailProtectionProfile),
360
372
  protectionAttestations: normalizeProtectionAttestations(value.protectionAttestations),
361
- desiredPostureBaselines: normalizeDesiredPostureBaselines(value.desiredPostureBaselines, officialDomains, now),
373
+ desiredPostureBaselines: normalizeDesiredPostureBaselines(value.desiredPostureBaselines, officialDomains, now, profileId),
362
374
  trademarkOwner: boundedText(value.trademarkOwner),
363
375
  trademarkRegistration: boundedText(value.trademarkRegistration),
364
- officialFaviconHash: normalizeFaviconHash(value.officialFaviconHash),
365
- officialFaviconPHash: normalizeFaviconPHash(value.officialFaviconPHash),
376
+ officialFaviconHash: baselineDomainMismatch ? '' : normalizeFaviconHash(value.officialFaviconHash),
377
+ officialFaviconPHash: baselineDomainMismatch ? '' : normalizeFaviconPHash(value.officialFaviconPHash),
366
378
  pageBaseline,
367
379
  createdAt,
368
380
  updatedAt: options.touch === true ? now : timestamp(value.updatedAt, createdAt),
@@ -376,15 +388,24 @@ function profileList(raw) {
376
388
  }
377
389
  export function brandProfileStoreVersion(raw) {
378
390
  if (Array.isArray(raw))
379
- return 1;
391
+ return BRAND_PROFILE_SCHEMA_VERSION;
380
392
  const value = record(raw);
381
393
  return typeof value.version === 'number' && Number.isFinite(value.version) && value.version > 0 ? value.version : null;
382
394
  }
383
395
  /** Normalize an internal profile collection or current stored envelope. */
384
396
  export function normalizeBrandProfileStore(raw) {
397
+ assertWorkspaceInputGraph(raw, 'Brand Profile store');
398
+ assertWorkspaceDeclaredVersion(raw, 'Brand Profile store');
399
+ const sourceVersion = brandProfileStoreVersion(raw);
400
+ if (!Array.isArray(raw) && sourceVersion !== null
401
+ && !SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS.includes(sourceVersion)) {
402
+ throw new Error(`Brand Profile schema ${sourceVersion} is unsupported; no data was changed.`);
403
+ }
385
404
  const byId = new Map();
386
405
  for (const item of profileList(raw).slice(0, MAX_PROFILES * 4)) {
387
- const profile = normalizeBrandProfile(item);
406
+ const profile = normalizeBrandProfile(item, {
407
+ nowIso: new Date(0).toISOString(),
408
+ });
388
409
  if (!profile)
389
410
  continue;
390
411
  const previous = byId.get(profile.id);
@@ -409,6 +430,9 @@ export function assertBrandProfileStoreBudget(profiles) {
409
430
  return store;
410
431
  }
411
432
  export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
433
+ assertWorkspaceInputGraph(localRaw, 'Local Brand Profile store');
434
+ assertWorkspaceInputGraph(importedRaw, 'Imported Brand Profile document');
435
+ assertWorkspacePortableVersion(importedRaw, BRAND_PROFILE_SCHEMA_VERSION, 'Imported Brand Profile document');
412
436
  const imported = record(importedRaw);
413
437
  if (imported.schema !== BRAND_PROFILE_SCHEMA) {
414
438
  throw new Error('This JSON file is not a WHOISleuth Brand Profile export.');
@@ -420,17 +444,39 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
420
444
  if (importedVersion !== null && importedVersion > BRAND_PROFILE_SCHEMA_VERSION) {
421
445
  throw new Error(`This Brand Profile file uses newer schema ${importedVersion}. Update the app before importing it.`);
422
446
  }
423
- if (![2, 3, 4, 5, BRAND_PROFILE_SCHEMA_VERSION].includes(importedVersion ?? 0)) {
424
- throw new Error(`Expected a WHOISleuth Brand Profile export using schema 2, 3, 4, 5, or ${BRAND_PROFILE_SCHEMA_VERSION}.`);
447
+ if (!SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS.includes(importedVersion ?? 0)) {
448
+ throw new Error(`Expected a WHOISleuth Brand Profile export using schema ${SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS.join(', ').replace(/, ([^,]+)$/u, ', or $1')}.`);
449
+ }
450
+ const idNames = new Map();
451
+ const retainIdName = (id, name) => {
452
+ const nameKey = name.toLowerCase();
453
+ const previous = idNames.get(id);
454
+ if (previous !== undefined && previous !== nameKey) {
455
+ throw new Error('Brand Profile import reuses one exact identifier for different normalised profile names. No profiles were imported.');
456
+ }
457
+ idNames.set(id, nameKey);
458
+ };
459
+ for (const item of profileList(localRaw).slice(0, MAX_PROFILES * 4)) {
460
+ const value = record(item);
461
+ const id = normalizeBrandProfileId(value.id);
462
+ const name = boundedText(value.name, MAX_PROFILE_NAME_LENGTH);
463
+ if (id && name)
464
+ retainIdName(id, name);
425
465
  }
426
466
  const local = normalizeBrandProfileStore(localRaw).profiles;
467
+ for (const profile of local)
468
+ retainIdName(profile.id, profile.name);
427
469
  const byName = new Map(local.map((profile) => [profile.name.toLowerCase(), profile]));
428
470
  const input = profileList(importedRaw);
429
471
  let added = 0;
430
472
  let updated = 0;
431
473
  let skipped = Math.max(0, input.length - MAX_PROFILES * 4);
432
474
  for (const item of input.slice(0, MAX_PROFILES * 4)) {
433
- const rawName = boundedText(record(item).name, MAX_PROFILE_NAME_LENGTH);
475
+ const value = record(item);
476
+ const rawName = boundedText(value.name, MAX_PROFILE_NAME_LENGTH);
477
+ const rawId = normalizeBrandProfileId(value.id);
478
+ if (rawId && rawName)
479
+ retainIdName(rawId, rawName);
434
480
  const existing = rawName ? byName.get(rawName.toLowerCase()) : null;
435
481
  const profile = normalizeBrandProfile(item, {
436
482
  existing,
@@ -442,6 +488,7 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
442
488
  skipped++;
443
489
  continue;
444
490
  }
491
+ retainIdName(profile.id, profile.name);
445
492
  if (existing) {
446
493
  byName.set(profile.name.toLowerCase(), profile);
447
494
  updated++;
@@ -1,14 +1,13 @@
1
- import { normalizeDomain } from "./case-model.js";
2
- export const BULK_REVIEW_SCHEMA = 'whoisleuth.bulk-review';
3
- export const BULK_REVIEW_SCHEMA_VERSION = 1;
4
- export const MAX_BULK_REVIEW_PRESETS = 24;
5
- export const MAX_BULK_REVIEW_ROWS = 1_900;
6
- export const MAX_BULK_REVIEW_STORE_BYTES = 512 * 1024;
7
- export const MAX_BULK_REVIEW_NAME_LENGTH = 80;
1
+ import { normalizeDomain } from "../cases/case-model.mjs";
2
+ import { normalizeBulkPresentationSortKey } from "./bulk-sort.mjs";
3
+ import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
4
+ import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
5
+ import { BULK_REVIEW_SCHEMA, BULK_REVIEW_SCHEMA_VERSION, MAX_BULK_REVIEW_NAME_LENGTH, MAX_BULK_REVIEW_PRESETS, MAX_BULK_REVIEW_ROWS, MAX_BULK_REVIEW_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
6
+ export { BULK_REVIEW_SCHEMA, BULK_REVIEW_SCHEMA_VERSION, MAX_BULK_REVIEW_NAME_LENGTH, MAX_BULK_REVIEW_PRESETS, MAX_BULK_REVIEW_ROWS, MAX_BULK_REVIEW_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
8
7
  export const BULK_REVIEW_STATES = ['unreviewed', 'reviewing', 'reviewed', 'deferred'];
9
8
  const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
10
9
  const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
11
- const PRIMARY_FILTERS = new Set(['all', 'available', 'registered', 'high_risk', 'trusted', 'errors']);
10
+ const PRIMARY_FILTERS = new Set(['all', 'available', 'registered', 'high_risk', 'trusted', 'profile_unevaluated', 'errors']);
12
11
  const SOURCE_FILTERS = new Set(['', 'complete', 'limited', 'failed']);
13
12
  const LIFECYCLE_FILTERS = new Set(['', 'new', 'expiring', 'aged', 'unknown']);
14
13
  const AGE_FILTERS = new Set(['', '7d', '30d', '90d', '365d', 'older', 'unknown']);
@@ -19,9 +18,7 @@ const REVIEW_STATES = new Set(BULK_REVIEW_STATES);
19
18
  const CASE_DISPOSITIONS = new Set(['', 'untracked', 'unreviewed', 'suspicious', 'confirmed_abuse', 'false_positive', 'expected', 'closed_no_action']);
20
19
  const SIGNAL_FILTERS = new Set(['favicon', 'password', 'phishing', 'asset_reuse', 'idn']);
21
20
  function record(value) {
22
- return value && typeof value === 'object' && !Array.isArray(value)
23
- ? value
24
- : {};
21
+ return ordinaryWorkspaceRecord(value, 'Bulk-review input') ?? {};
25
22
  }
26
23
  function text(value, maximum) {
27
24
  if (typeof value !== 'string' || CONTROL_RE.test(value))
@@ -29,9 +26,7 @@ function text(value, maximum) {
29
26
  return value.trim().slice(0, maximum);
30
27
  }
31
28
  function timestamp(value, fallback) {
32
- const normalized = text(value, 64);
33
- const parsed = Date.parse(normalized);
34
- return Number.isFinite(parsed) ? new Date(parsed).toISOString() : fallback;
29
+ return normalizeExplicitIsoTimestamp(value) ?? fallback;
35
30
  }
36
31
  function safeId(value) {
37
32
  return typeof value === 'string' && SAFE_ID_RE.test(value) ? value : '';
@@ -50,7 +45,7 @@ function normalizeView(raw) {
50
45
  ? [...new Set(value.signalFilters.filter((item) => typeof item === 'string' && SIGNAL_FILTERS.has(item)))].slice(0, SIGNAL_FILTERS.size)
51
46
  : [];
52
47
  const sortKey = typeof value.sortKey === 'string' && SORT_KEYS.has(value.sortKey)
53
- ? value.sortKey
48
+ ? normalizeBulkPresentationSortKey(value.sortKey)
54
49
  : 'risk';
55
50
  return {
56
51
  primaryFilter: setValue(value.primaryFilter, PRIMARY_FILTERS, 'all'),
@@ -110,6 +105,8 @@ function sourceLists(raw) {
110
105
  };
111
106
  }
112
107
  export function normalizeBulkReviewStore(raw) {
108
+ assertWorkspaceInputGraph(raw, 'Bulk-review store');
109
+ assertWorkspaceDeclaredVersion(raw, 'Bulk-review store');
113
110
  const fallback = new Date(0).toISOString();
114
111
  const source = sourceLists(raw);
115
112
  const presets = new Map();
@@ -207,6 +204,9 @@ export function buildBulkReviewExport(raw) {
207
204
  return enforceBulkReviewBudget(raw);
208
205
  }
209
206
  export function mergeBulkReviewStores(localRaw, importedRaw) {
207
+ assertWorkspaceInputGraph(localRaw, 'Local Bulk-review store');
208
+ assertWorkspaceInputGraph(importedRaw, 'Imported Bulk-review document');
209
+ assertWorkspacePortableVersion(importedRaw, BULK_REVIEW_SCHEMA_VERSION, 'Imported Bulk-review document');
210
210
  const importedRecord = record(importedRaw);
211
211
  if (importedRecord.schema !== BULK_REVIEW_SCHEMA) {
212
212
  throw new Error('This file is not a WHOISleuth Bulk review export.');