@slicedearth/whoisleuth-cli 2.5.1 → 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 (122) hide show
  1. package/DISCLOSURE +1 -1
  2. package/cli/artifact-validation/case-response.mjs +51 -8
  3. package/cli/artifact-validation/lookup-evidence.mjs +15 -1
  4. package/cli/artifact-validation/signed-review.mjs +2 -51
  5. package/cli/artifact-verify.mjs +9 -2
  6. package/cli/bulk-command-runner.mjs +4 -6
  7. package/cli/case-command.mjs +79 -3
  8. package/cli/case-pack.mjs +7 -2
  9. package/cli/command-definition.mjs +2 -1
  10. package/cli/discovery-scan-command-runner.mjs +4 -6
  11. package/cli/domain-feed.mjs +61 -0
  12. package/cli/formatters/json.mjs +1 -13
  13. package/cli/indicator-set-command.mjs +1 -2
  14. package/cli/intake-command.mjs +43 -3
  15. package/cli/investigation-artifacts.mjs +2 -3
  16. package/cli/investigation-package-review.mjs +2 -0
  17. package/cli/local-mmdb-review.mjs +27 -4
  18. package/cli/lookup-command-runner.mjs +1 -0
  19. package/cli/offline-evidence-review.mjs +51 -5
  20. package/cli/review-arguments.mjs +3 -2
  21. package/cli/review-command-definitions.mjs +14 -8
  22. package/cli/review-command-runner.mjs +6 -3
  23. package/cli/risk-calibration.mjs +6 -4
  24. package/cli/selected-input-worker.mjs +12 -2
  25. package/cli/support-command-runner.mjs +3 -3
  26. package/cli/watchlist-review.mjs +43 -0
  27. package/cli/workflow-arguments.mjs +14 -0
  28. package/cli/workflow-command-definitions.mjs +26 -1
  29. package/cli/workflow-command-runner.mjs +32 -0
  30. package/docs/cli.md +99 -2
  31. package/docs/contextual-reviews.md +28 -5
  32. package/docs/infrastructure-observations.md +126 -0
  33. package/docs/offline-intake-context.md +213 -0
  34. package/lib/credential-form-attribution.mjs +136 -0
  35. package/lib/credential-surface-profile.mjs +6 -2
  36. package/lib/dns-delegation-health.mjs +23 -7
  37. package/lib/dns-intelligence.mjs +18 -4
  38. package/lib/domain-posture-analysis.mjs +53 -2
  39. package/lib/domain-posture-parsers.mjs +9 -0
  40. package/lib/domain-posture.mjs +29 -10
  41. package/lib/evidence-export-privacy.mjs +5 -2
  42. package/lib/evidence-report.mjs +1 -0
  43. package/lib/favicon.mjs +13 -4
  44. package/lib/html-signals.mjs +6 -0
  45. package/lib/lookup-child-profile-contract.mjs +28 -11
  46. package/lib/lookup-response-contract.mjs +1 -0
  47. package/lib/operation-budget.mjs +4 -0
  48. package/lib/service-binding-dns.mjs +19 -2
  49. package/lib/sslbl-certificates.generated.mjs +138 -137
  50. package/lib/static-html-analysis.mjs +32 -4
  51. package/lib/technology-evidence-role.mjs +1 -51
  52. package/lib/tls-intelligence.mjs +30 -5
  53. package/lib/website-enrichment.mjs +39 -23
  54. package/lib/whois-chain.mjs +12 -3
  55. package/lib/whois-common-formats.mjs +3 -1
  56. package/lib/whois-contacts.mjs +13 -2
  57. package/package.json +7 -5
  58. package/packages/analysis/risk-scoring.mjs +12 -9
  59. package/packages/cases/case-containment-handoff.mjs +195 -0
  60. package/packages/cases/case-evidence-links.mjs +16 -0
  61. package/packages/cases/case-evidence-model.mjs +14 -3
  62. package/packages/cases/case-object-coverage.mjs +33 -0
  63. package/packages/cases/case-packet-correction.mjs +106 -0
  64. package/packages/cases/case-recheck-model.mjs +35 -5
  65. package/packages/cases/case-record-operations.mjs +74 -9
  66. package/packages/cases/case-record-projection.mjs +15 -3
  67. package/packages/cases/case-record-version-input.mjs +23 -1
  68. package/packages/cases/case-report.mjs +30 -0
  69. package/packages/cases/case-requested-evidence.mjs +1 -1
  70. package/packages/cases/case-response-actions.mjs +57 -1
  71. package/packages/cases/case-response-model.mjs +7 -0
  72. package/packages/cases/case-response-object.mjs +119 -0
  73. package/packages/cases/case-response-outcomes.mjs +81 -14
  74. package/packages/cases/case-response-packet-render.mjs +22 -3
  75. package/packages/cases/case-response-packet.mjs +30 -2
  76. package/packages/cases/case-response-review-inputs.mjs +48 -7
  77. package/packages/cases/case-workflow-metadata.mjs +1 -9
  78. package/packages/contracts/analyst-interchange.mjs +7 -5
  79. package/packages/contracts/candidate-watch-review.mjs +54 -0
  80. package/packages/contracts/case-portability.mjs +260 -42
  81. package/packages/contracts/cli-command-semantics.mjs +3 -0
  82. package/packages/contracts/context-review.mjs +2 -1
  83. package/packages/contracts/external-observation-interchange.mjs +96 -28
  84. package/packages/contracts/lookup-evidence.mjs +3 -2
  85. package/packages/contracts/message-intake.mjs +7 -1
  86. package/packages/contracts/risk-calibration.mjs +44 -29
  87. package/packages/contracts/schema-lifecycle.mjs +3 -1
  88. package/packages/contracts/web-capture.mjs +1 -1
  89. package/packages/contracts/workspace-portability.mjs +86 -16
  90. package/packages/evidence/collection-quality.mjs +1 -1
  91. package/packages/evidence/credential-form-attribution.mjs +60 -0
  92. package/packages/evidence/image-regions.mjs +15 -0
  93. package/packages/evidence/record-identity.mjs +60 -16
  94. package/packages/evidence/technology-evidence-role.mjs +51 -0
  95. package/packages/interchange/external-findings-import.mjs +32 -17
  96. package/packages/interchange/json-output.mjs +14 -0
  97. package/packages/interchange/managed-indicator-export.mjs +3 -2
  98. package/packages/interchange/managed-indicator-set.mjs +13 -0
  99. package/packages/investigation/acquisition-decision-packet.mjs +78 -0
  100. package/packages/investigation/context-review.mjs +3 -3
  101. package/packages/investigation/document-intake.mjs +9 -3
  102. package/packages/investigation/domain-history-review.mjs +43 -11
  103. package/packages/investigation/domain-transition-review.mjs +2 -0
  104. package/packages/investigation/infrastructure-observation.mjs +234 -0
  105. package/packages/investigation/intake-context.mjs +284 -0
  106. package/packages/investigation/intake-indicators.mjs +116 -0
  107. package/packages/investigation/intake-phones.mjs +155 -0
  108. package/packages/investigation/intake-report.mjs +2 -1
  109. package/packages/investigation/investigation-manifest.mjs +33 -7
  110. package/packages/investigation/link-intake.mjs +38 -8
  111. package/packages/investigation/message-intake.mjs +55 -15
  112. package/packages/monitoring/domain-feed.mjs +321 -0
  113. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  114. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  115. package/packages/workspace/brand-profile-model.mjs +46 -2
  116. package/packages/workspace/bulk-session-model.mjs +4 -0
  117. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  118. package/packages/workspace/detection-rule-model.mjs +10 -2
  119. package/packages/workspace/shortlist-model.mjs +10 -2
  120. package/packages/workspace/watchlist-history.mjs +31 -4
  121. package/packages/workspace/watchlist-store.mjs +136 -7
  122. package/third-party-notices.txt +29 -29
@@ -2,8 +2,8 @@ import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
2
2
  import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
3
3
  export const EXTERNAL_OBSERVATION_INTERCHANGE_CONTRACT_OWNER = 'packages/contracts/external-observation-interchange.mts';
4
4
  export const EXTERNAL_FINDINGS_SCHEMA = 'whoisleuth.external-findings';
5
- export const EXTERNAL_FINDINGS_VERSION = 4;
6
- export const SUPPORTED_EXTERNAL_FINDINGS_VERSIONS = Object.freeze([EXTERNAL_FINDINGS_VERSION]);
5
+ export const EXTERNAL_FINDINGS_VERSION = 5;
6
+ export const SUPPORTED_EXTERNAL_FINDINGS_VERSIONS = Object.freeze([4, 5]);
7
7
  export const MAX_EXTERNAL_FINDINGS_IMPORT_BYTES = 384 * 1024;
8
8
  export const MAX_EXTERNAL_FINDINGS = 100;
9
9
  export const MAX_EXTERNAL_FINDINGS_PER_DOMAIN = 20;
@@ -17,6 +17,19 @@ export const DNS_OBSERVATION_ROWS_SCHEMA = 'whoisleuth.dns-observation-rows';
17
17
  export const CERTIFICATE_OBSERVATION_ROWS_SCHEMA = 'whoisleuth.certificate-observation-rows';
18
18
  export const SUPPORTED_OBSERVATION_ROWS_VERSION = 1;
19
19
  export const MAX_CONVERSION_INPUT_ROWS = MAX_EXTERNAL_FINDINGS * 4;
20
+ export const INFRASTRUCTURE_OBSERVATION_SCHEMA = 'whoisleuth.infrastructure-observation';
21
+ export const INFRASTRUCTURE_OBSERVATION_VERSION = 1;
22
+ export const MAX_INFRASTRUCTURE_OBSERVATION_BYTES = 128 * 1024;
23
+ export const MAX_INFRASTRUCTURE_HOSTS = 128;
24
+ export const MAX_INFRASTRUCTURE_DNS_ROWS = 512;
25
+ export const MAX_INFRASTRUCTURE_CERTIFICATES = 32;
26
+ export const MAX_INFRASTRUCTURE_ROLES = 128;
27
+ export const MAX_INFRASTRUCTURE_SOURCES = 16;
28
+ export const MAX_INFRASTRUCTURE_LIMITATIONS = 12;
29
+ export const INFRASTRUCTURE_DNS_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR'];
30
+ export const INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA = 'whoisleuth.infrastructure-comparison.input';
31
+ export const INFRASTRUCTURE_COMPARISON_INPUT_VERSION = 1;
32
+ export const MAX_INFRASTRUCTURE_COMPARISON_INPUT_BYTES = 2 * MAX_INFRASTRUCTURE_OBSERVATION_BYTES + 1_024;
20
33
  export const EXTERNAL_OBSERVATION_INTERCHANGE_COMPATIBILITY_FACADES = Object.freeze([
21
34
  ['frontend/src/lib/analysis/external-findings-import.ts', 'packages/interchange/external-findings-import.mts'],
22
35
  ['frontend/src/lib/analysis/external-findings-converters.ts', 'packages/interchange/external-findings-converters.mts'],
@@ -24,9 +37,9 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_COMPATIBILITY_FACADES = Object.fre
24
37
  export const EXTERNAL_FINDINGS_COMPATIBILITY = defineSchemaCompatibility({
25
38
  id: 'export.external-findings', kind: 'export', schema: EXTERNAL_FINDINGS_SCHEMA,
26
39
  currentVersion: EXTERNAL_FINDINGS_VERSION, supportedVersions: SUPPORTED_EXTERNAL_FINDINGS_VERSIONS,
27
- acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'exact_current_only', writeSemantics: 'non_destructive_merge',
40
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'none', writeSemantics: 'non_destructive_merge',
28
41
  byteBudget: MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, owner: EXTERNAL_OBSERVATION_INTERCHANGE_CONTRACT_OWNER,
29
- note: 'Strict local findings import. Version 4 can retain bounded certificate event identity and name-completeness metadata while analyst assertions remain a separate case workflow.',
42
+ note: 'Strict local findings import. Historical version 4 is preserved exactly; version 5 adds an optional exact source-qualified infrastructure snapshot. No collection is initiated.',
30
43
  });
31
44
  export const EXTERNAL_FINDING_ROWS_COMPATIBILITY = defineSchemaCompatibility({
32
45
  id: 'import.external-finding-rows', kind: 'export', schema: EXTERNAL_FINDING_ROWS_SCHEMA,
@@ -62,17 +75,26 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_COMPATIBILITY = Object.freeze([
62
75
  DOMAIN_OBSERVATION_ROWS_COMPATIBILITY,
63
76
  DNS_OBSERVATION_ROWS_COMPATIBILITY,
64
77
  CERTIFICATE_OBSERVATION_ROWS_COMPATIBILITY,
78
+ defineSchemaCompatibility({ id: 'export.infrastructure-observation', kind: 'export', schema: INFRASTRUCTURE_OBSERVATION_SCHEMA, currentVersion: 1, supportedVersions: [1], acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'exact_current_only', writeSemantics: 'read_only', byteBudget: MAX_INFRASTRUCTURE_OBSERVATION_BYTES, owner: EXTERNAL_OBSERVATION_INTERCHANGE_CONTRACT_OWNER, note: 'Exact source-qualified selected infrastructure evidence retained through existing Case pins. Wildcards, DNS outcomes and independent provider roles remain distinct.' }),
79
+ defineSchemaCompatibility({ id: 'cli.infrastructure-comparison-input', kind: 'cli_document', schema: INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA, currentVersion: 1, supportedVersions: [1], acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'read_only', writeSemantics: 'read_only', byteBudget: MAX_INFRASTRUCTURE_COMPARISON_INPUT_BYTES, owner: EXTERNAL_OBSERVATION_INTERCHANGE_CONTRACT_OWNER, note: 'An explicit earlier/later snapshot pair reviewed offline without collection, persistence or current absence claims.' }),
65
80
  ]);
66
81
  const EXTERNAL_INTERCHANGE_FIXTURES = Object.freeze([
67
- { id: 'external-findings-v4', path: 'test/fixtures/external-observation-interchange/external-findings-v4.json', bytes: 634, sha256: 'ceab916e1dcf99704c94e1b0cd82f5a061f138ae6a07ce8335273080c73b7624', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
68
- { id: 'external-finding-rows-v1', path: 'test/fixtures/external-observation-interchange/external-finding-rows-v1.json', bytes: 435, sha256: '09048325d801fdc837134580fd887cacde28744c9f87a43afc388c96117a4659', schema: EXTERNAL_FINDING_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'external-finding-rows-v1-output' },
69
- { id: 'external-finding-rows-v1-output', path: 'test/fixtures/external-observation-interchange/external-finding-rows-v1-output.json', bytes: 581, sha256: '7f84a9f6177d3245acd6fc21275276694fbe9e8cd94922b1e6997a73afa3149a', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
70
- { id: 'domain-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/domain-observation-rows-v1.json', bytes: 390, sha256: '2f95ab21e57e1a6aaab03cea75a4d58fd9e1234ba87f5268a899b23103359474', schema: DOMAIN_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'domain-observation-rows-v1-output' },
71
- { id: 'domain-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/domain-observation-rows-v1-output.json', bytes: 1_027, sha256: 'bba949308e9bb5b58bf5611d6dec23f074979522627631948cffc2f0056ac817', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
72
- { id: 'dns-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/dns-observation-rows-v1.json', bytes: 369, sha256: 'afb1808a8a81339a1801d337259c19c77ed957583b4ae06579b38ea6a8f68ed1', schema: DNS_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'dns-observation-rows-v1-output' },
73
- { id: 'dns-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/dns-observation-rows-v1-output.json', bytes: 988, sha256: '4c9a49b96181d2907eb95319d15975758823994fe029c051486f1cb6869221a0', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
74
- { id: 'certificate-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/certificate-observation-rows-v1.json', bytes: 515, sha256: '68363a5fe074b08a386dbc95a0993a2933ebcf05ba2f96148df632842d1c5869', schema: CERTIFICATE_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'certificate-observation-rows-v1-output' },
75
- { id: 'certificate-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/certificate-observation-rows-v1-output.json', bytes: 1_244, sha256: '6c484601b872f10bfdb7d9519d4b216f14ef277e7a429e4cc131154ad0e07652', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
82
+ { id: 'external-findings-v4', path: 'test/fixtures/external-observation-interchange/external-findings-v4.json', bytes: 634, sha256: 'ceab916e1dcf99704c94e1b0cd82f5a061f138ae6a07ce8335273080c73b7624', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null },
83
+ { id: 'external-finding-rows-v1', path: 'test/fixtures/external-observation-interchange/external-finding-rows-v1.json', bytes: 435, sha256: '09048325d801fdc837134580fd887cacde28744c9f87a43afc388c96117a4659', schema: EXTERNAL_FINDING_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'external-finding-rows-v1-output-v5' },
84
+ { id: 'external-finding-rows-v1-output', path: 'test/fixtures/external-observation-interchange/external-finding-rows-v1-output.json', bytes: 581, sha256: '7f84a9f6177d3245acd6fc21275276694fbe9e8cd94922b1e6997a73afa3149a', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null },
85
+ { id: 'domain-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/domain-observation-rows-v1.json', bytes: 390, sha256: '2f95ab21e57e1a6aaab03cea75a4d58fd9e1234ba87f5268a899b23103359474', schema: DOMAIN_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'domain-observation-rows-v1-output-v5' },
86
+ { id: 'domain-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/domain-observation-rows-v1-output.json', bytes: 1_027, sha256: 'bba949308e9bb5b58bf5611d6dec23f074979522627631948cffc2f0056ac817', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null },
87
+ { id: 'dns-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/dns-observation-rows-v1.json', bytes: 369, sha256: 'afb1808a8a81339a1801d337259c19c77ed957583b4ae06579b38ea6a8f68ed1', schema: DNS_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'dns-observation-rows-v1-output-v5' },
88
+ { id: 'dns-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/dns-observation-rows-v1-output.json', bytes: 988, sha256: '4c9a49b96181d2907eb95319d15975758823994fe029c051486f1cb6869221a0', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null },
89
+ { id: 'certificate-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/certificate-observation-rows-v1.json', bytes: 515, sha256: '68363a5fe074b08a386dbc95a0993a2933ebcf05ba2f96148df632842d1c5869', schema: CERTIFICATE_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'certificate-observation-rows-v1-output-v5' },
90
+ { id: 'certificate-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/certificate-observation-rows-v1-output.json', bytes: 1_244, sha256: '6c484601b872f10bfdb7d9519d4b216f14ef277e7a429e4cc131154ad0e07652', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null },
91
+ { id: 'external-finding-rows-v1-output-v5', path: 'test/fixtures/external-observation-interchange/external-finding-rows-v1-output-v5.json', bytes: 442, sha256: '9996be2b9d832fca9aa523a1a424148ef762483ab3923d35ca73e83cd16a8195', schema: EXTERNAL_FINDINGS_SCHEMA, version: 5, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
92
+ { id: 'domain-observation-rows-v1-output-v5', path: 'test/fixtures/external-observation-interchange/domain-observation-rows-v1-output-v5.json', bytes: 771, sha256: 'd375353576b5990e835d53c14bf7fa5adb5205191d3969478a69ec68b5e1291b', schema: EXTERNAL_FINDINGS_SCHEMA, version: 5, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
93
+ { id: 'dns-observation-rows-v1-output-v5', path: 'test/fixtures/external-observation-interchange/dns-observation-rows-v1-output-v5.json', bytes: 732, sha256: '532ff6ac9e9255f14105eb0a0286434fadb94f6d7397f0fb96007ebb6b7d1522', schema: EXTERNAL_FINDINGS_SCHEMA, version: 5, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
94
+ { id: 'certificate-observation-rows-v1-output-v5', path: 'test/fixtures/external-observation-interchange/certificate-observation-rows-v1-output-v5.json', bytes: 988, sha256: '09904df103f83f64616556e653fa14c6f2f1dc392b1f75a85fd3099680840e37', schema: EXTERNAL_FINDINGS_SCHEMA, version: 5, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
95
+ { id: 'external-findings-v5', path: 'test/fixtures/external-observation-interchange/external-findings-v5.json', bytes: 3871, sha256: 'debd062c2d951d22c06c29bb076529fa69a2769b8bae7269ce3884550fb55ba3', schema: EXTERNAL_FINDINGS_SCHEMA, version: 5, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
96
+ { id: 'infrastructure-comparison-input-v1', path: 'test/fixtures/infrastructure-observations/infrastructure-comparison-input-v1.json', bytes: 6284, sha256: 'ef0022faf29586774bd1b2de1f95eb3839d382e424c7b3d446e4dc1058437104', schema: INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA, version: 1, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
97
+ { id: 'infrastructure-observation-v1', path: 'test/fixtures/infrastructure-observations/infrastructure-observation-v1.json', bytes: 3430, sha256: '1b76e43e5d5c889654faf12114217994c5cbd3a378685e7f9b9d18ebdde801f7', schema: INFRASTRUCTURE_OBSERVATION_SCHEMA, version: 1, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
76
98
  ].map((fixture) => Object.freeze({
77
99
  ...fixture,
78
100
  contentDigestSha256: null,
@@ -84,6 +106,7 @@ function externalFixtureIds(schema, version) {
84
106
  .map((fixture) => fixture.id));
85
107
  }
86
108
  const EXTERNAL_INTERCHANGE_CONTRACTS = Object.freeze([
109
+ ...[['export.infrastructure-observation', INFRASTRUCTURE_OBSERVATION_SCHEMA, true, MAX_INFRASTRUCTURE_OBSERVATION_BYTES], ['cli.infrastructure-comparison-input', INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA, false, MAX_INFRASTRUCTURE_COMPARISON_INPUT_BYTES]].map(([compatibilityId, schema, emitted, byteBudget]) => Object.freeze({ compatibilityId, schema, version: 1, role: 'document', lifecycle: 'current', readable: true, emitted, exactKeys: true, extensionPolicy: 'reject', futureVersionBehaviour: 'reject', migrationTarget: null, canonicalisation: null, byteBudget, fixtureIds: externalFixtureIds(schema, 1) })),
87
110
  ...SUPPORTED_EXTERNAL_FINDINGS_VERSIONS.map((version) => Object.freeze({
88
111
  compatibilityId: EXTERNAL_FINDINGS_COMPATIBILITY.id,
89
112
  schema: EXTERNAL_FINDINGS_SCHEMA,
@@ -95,7 +118,7 @@ const EXTERNAL_INTERCHANGE_CONTRACTS = Object.freeze([
95
118
  exactKeys: true,
96
119
  extensionPolicy: 'reject',
97
120
  futureVersionBehaviour: 'reject',
98
- migrationTarget: version === EXTERNAL_FINDINGS_VERSION ? null : { schema: EXTERNAL_FINDINGS_SCHEMA, version: EXTERNAL_FINDINGS_VERSION },
121
+ migrationTarget: null,
99
122
  canonicalisation: null,
100
123
  byteBudget: MAX_EXTERNAL_FINDINGS_IMPORT_BYTES,
101
124
  fixtureIds: externalFixtureIds(EXTERNAL_FINDINGS_SCHEMA, version),
@@ -151,18 +174,38 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLif
151
174
  metadata: {
152
175
  enforcement: 'declarative_only',
153
176
  shapes: [
154
- externalShape('external-interchange.findings.v4', EXTERNAL_FINDINGS_SCHEMA, [...SUPPORTED_EXTERNAL_FINDINGS_VERSIONS], ['schema', 'schemaVersion', 'source', 'findings'], 'preserve_document'),
177
+ externalShape('external-interchange.findings.v4', EXTERNAL_FINDINGS_SCHEMA, [4], ['schema', 'schemaVersion', 'source', 'findings'], 'preserve_document'),
178
+ externalShape('external-interchange.findings.v5', EXTERNAL_FINDINGS_SCHEMA, [5], ['schema', 'schemaVersion', 'source', 'findings'], 'preserve_document'),
179
+ externalShape('external-interchange.infrastructure.v1', INFRASTRUCTURE_OBSERVATION_SCHEMA, [1], ['schema', 'version', 'id', 'target', 'observedAt', 'mode', 'scope', 'coverage', 'sources', 'dns', 'certificates', 'roles', 'limitations'], 'preserve_document'),
180
+ externalShape('external-interchange.infrastructure-comparison.v1', INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA, [1], ['schema', 'version', 'earlier', 'later'], 'preserve_document'),
155
181
  externalShape('external-interchange.finding-rows.v1', EXTERNAL_FINDING_ROWS_SCHEMA, [1], ['schema', 'schemaVersion', 'source', 'rows'], 'input_to_current'),
156
182
  externalShape('external-interchange.domain-rows.v1', DOMAIN_OBSERVATION_ROWS_SCHEMA, [1], ['schema', 'schemaVersion', 'source', 'observations'], 'input_to_current'),
157
183
  externalShape('external-interchange.dns-rows.v1', DNS_OBSERVATION_ROWS_SCHEMA, [1], ['schema', 'schemaVersion', 'source', 'observations'], 'input_to_current'),
158
184
  externalShape('external-interchange.certificate-rows.v1', CERTIFICATE_OBSERVATION_ROWS_SCHEMA, [1], ['schema', 'schemaVersion', 'source', 'observations'], 'input_to_current'),
159
185
  ],
160
- boundProfiles: [{ id: 'external-interchange.document.bounds', bounds: [
186
+ boundProfiles: [{ id: 'external-interchange.infrastructure.bounds', bounds: [
187
+ { id: 'raw-bytes', path: '$', phase: 'raw_intake', unit: 'bytes', minimum: 1, maximum: MAX_INFRASTRUCTURE_OBSERVATION_BYTES, handling: 'reject' },
188
+ { id: 'serialised-bytes', path: '$', phase: 'serialised', unit: 'bytes', minimum: 1, maximum: MAX_INFRASTRUCTURE_OBSERVATION_BYTES, handling: 'reject' },
189
+ { id: 'hostnames', path: 'scope.hostnames', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: MAX_INFRASTRUCTURE_HOSTS, handling: 'reject' },
190
+ { id: 'dns-rows', path: 'dns', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: MAX_INFRASTRUCTURE_DNS_ROWS, handling: 'reject' },
191
+ { id: 'certificate-rows', path: 'certificates', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: MAX_INFRASTRUCTURE_CERTIFICATES, handling: 'reject' },
192
+ { id: 'role-rows', path: 'roles', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: MAX_INFRASTRUCTURE_ROLES, handling: 'reject' },
193
+ { id: 'sources', path: 'sources', phase: 'pre_accumulation', unit: 'items', minimum: 1, maximum: MAX_INFRASTRUCTURE_SOURCES, handling: 'reject' },
194
+ { id: 'dns-types', path: 'scope.dnsTypes', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: INFRASTRUCTURE_DNS_TYPES.length, handling: 'reject' },
195
+ { id: 'limitations', path: 'limitations', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: MAX_INFRASTRUCTURE_LIMITATIONS, handling: 'reject' },
196
+ ] }, { id: 'external-interchange.infrastructure-comparison.bounds', bounds: [
197
+ { id: 'raw-bytes', path: '$', phase: 'raw_intake', unit: 'bytes', minimum: 1, maximum: MAX_INFRASTRUCTURE_COMPARISON_INPUT_BYTES, handling: 'reject' },
198
+ { id: 'serialised-bytes', path: '$', phase: 'serialised', unit: 'bytes', minimum: 1, maximum: MAX_INFRASTRUCTURE_COMPARISON_INPUT_BYTES, handling: 'reject' },
199
+ ] }, { id: 'external-interchange.document.bounds', bounds: [
161
200
  { id: 'raw-bytes', path: '$', phase: 'raw_intake', unit: 'bytes', minimum: 1, maximum: MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, handling: 'reject' },
162
201
  { id: 'serialised-bytes', path: '$', phase: 'serialised', unit: 'bytes', minimum: 1, maximum: MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, handling: 'reject' },
163
202
  { id: 'rows', path: '$', phase: 'pre_accumulation', unit: 'items', minimum: 1, maximum: MAX_CONVERSION_INPUT_ROWS, handling: 'reject' },
164
203
  ] }],
165
204
  hooks: [
205
+ { id: 'external-interchange.infrastructure.read', role: 'normaliser', runtime: 'shared', module: 'packages/investigation/infrastructure-observation.mts', exportName: 'readInfrastructureObservation' },
206
+ { id: 'external-interchange.infrastructure.serialise', role: 'serialiser', runtime: 'shared', module: 'packages/investigation/infrastructure-observation.mts', exportName: 'serialiseInfrastructureObservation' },
207
+ { id: 'external-interchange.infrastructure.convert', role: 'normaliser', runtime: 'shared', module: 'packages/interchange/external-findings-converters.mts', exportName: 'convertInfrastructureObservation' },
208
+ { id: 'external-interchange.infrastructure.compare', role: 'normaliser', runtime: 'cli', module: 'cli/offline-evidence-review.mts', exportName: 'buildOfflineEvidenceReview' },
166
209
  { id: 'external-interchange.findings.normalise', role: 'normaliser', runtime: 'shared', module: 'packages/interchange/external-findings-import.mts', exportName: 'parseExternalFindingsDocument' },
167
210
  { id: 'external-interchange.findings.serialise', role: 'serialiser', runtime: 'shared', module: 'packages/interchange/external-findings-import.mts', exportName: 'serializeExternalFindingsDocument' },
168
211
  { id: 'external-interchange.finding-rows.convert', role: 'normaliser', runtime: 'shared', module: 'packages/interchange/external-findings-converters.mts', exportName: 'convertExternalFindingRows' },
@@ -170,10 +213,15 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLif
170
213
  { id: 'external-interchange.findings.merge', role: 'merger', runtime: 'shared', module: 'packages/interchange/external-findings-import.mts', exportName: 'mergeExternalFindingsIntoCases' },
171
214
  ],
172
215
  serialisationProfiles: [{
173
- id: 'external-interchange.findings.json.v4', schema: EXTERNAL_FINDINGS_SCHEMA,
174
- versions: [...SUPPORTED_EXTERNAL_FINDINGS_VERSIONS], mediaType: 'application/json', encoding: 'utf-8', bom: false,
175
- indentSpaces: 2, terminalLf: true, propertyOrder: 'normalised_fixed', canonicalisation: null, integrity: 'none',
216
+ id: 'external-interchange.findings.json.v5', schema: EXTERNAL_FINDINGS_SCHEMA,
217
+ versions: [5], mediaType: 'application/json', encoding: 'utf-8', bom: false,
218
+ indentSpaces: 0, terminalLf: true, propertyOrder: 'normalised_fixed', canonicalisation: null, integrity: 'none',
176
219
  serializerHookId: 'external-interchange.findings.serialise', verifierHookIds: [],
220
+ }, {
221
+ id: 'external-interchange.infrastructure.json.v1', schema: INFRASTRUCTURE_OBSERVATION_SCHEMA,
222
+ versions: [1], mediaType: 'application/json', encoding: 'utf-8', bom: false,
223
+ indentSpaces: 0, terminalLf: true, propertyOrder: 'normalised_fixed', canonicalisation: null, integrity: 'none',
224
+ serializerHookId: 'external-interchange.infrastructure.serialise', verifierHookIds: [],
177
225
  }],
178
226
  privacyProfiles: [
179
227
  { id: 'external-interchange.privacy.transient', classification: 'analyst_authored_sensitive', projection: 'browser_import', includedCategories: ['source-attribution', 'bounded-observations', 'completeness', 'limitations'], excludedCategories: ['raw-upstream-payloads', 'expanded-contacts', 'credentials', 'cookies', 'query-bearing-urls'], notePolicy: 'discarded', retention: 'transient_report', network: 'none', sharingReview: 'required' },
@@ -181,22 +229,42 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLif
181
229
  ],
182
230
  expiryProfiles: [{ id: 'external-interchange.expiry.not-applicable', field: null, anchor: null, handling: 'not_applicable', phase: 'not_applicable', maximumLifetimeDays: null }],
183
231
  consumerEdges: [
232
+ {
233
+ id: 'external-interchange.consumer.infrastructure-read', plane: 'shared', operation: 'read-and-export-exact-infrastructure-snapshot',
234
+ acceptedContracts: [{ schema: INFRASTRUCTURE_OBSERVATION_SCHEMA, versions: [1], mode: 'direct' }], emittedContract: { schema: INFRASTRUCTURE_OBSERVATION_SCHEMA, version: 1 },
235
+ shapeIds: ['external-interchange.infrastructure.v1'], boundProfileIds: ['external-interchange.infrastructure.bounds'], hookIds: ['external-interchange.infrastructure.read', 'external-interchange.infrastructure.serialise'], serialisationProfileId: 'external-interchange.infrastructure.json.v1', privacyProfileId: 'external-interchange.privacy.transient', retentionEffect: 'transient_report', ...externalConsumerCommon,
236
+ },
237
+ {
238
+ id: 'external-interchange.consumer.infrastructure-convert', plane: 'shared', operation: 'convert-exact-snapshot-to-one-finding',
239
+ acceptedContracts: [{ schema: INFRASTRUCTURE_OBSERVATION_SCHEMA, versions: [1], mode: 'direct' }], emittedContract: { schema: EXTERNAL_FINDINGS_SCHEMA, version: 5 },
240
+ shapeIds: ['external-interchange.infrastructure.v1', 'external-interchange.findings.v5'], boundProfileIds: ['external-interchange.infrastructure.bounds', 'external-interchange.document.bounds'], hookIds: ['external-interchange.infrastructure.convert', 'external-interchange.findings.serialise'], serialisationProfileId: 'external-interchange.findings.json.v5', privacyProfileId: 'external-interchange.privacy.transient', retentionEffect: 'transient_report', ...externalConsumerCommon,
241
+ },
242
+ {
243
+ id: 'external-interchange.consumer.infrastructure-compare', plane: 'cli', operation: 'offline-infrastructure-comparison',
244
+ acceptedContracts: [{ schema: INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA, versions: [1], mode: 'direct' }], emittedContract: null,
245
+ shapeIds: ['external-interchange.infrastructure-comparison.v1'], boundProfileIds: ['external-interchange.infrastructure-comparison.bounds'], hookIds: ['external-interchange.infrastructure.compare'], serialisationProfileId: null, privacyProfileId: 'external-interchange.privacy.transient', retentionEffect: 'transient_report', ...externalConsumerCommon,
246
+ },
247
+ {
248
+ id: 'external-interchange.consumer.findings-v4-read', plane: 'shared', operation: 'preserve-historical-findings',
249
+ acceptedContracts: [{ schema: EXTERNAL_FINDINGS_SCHEMA, versions: [4], mode: 'direct' }], emittedContract: null,
250
+ shapeIds: ['external-interchange.findings.v4'], boundProfileIds: ['external-interchange.document.bounds'], hookIds: ['external-interchange.findings.normalise'], serialisationProfileId: null, privacyProfileId: 'external-interchange.privacy.transient', retentionEffect: 'transient_report', ...externalConsumerCommon,
251
+ },
184
252
  {
185
253
  id: 'external-interchange.consumer.findings-normalise', plane: 'shared', operation: 'normalise-strict-findings',
186
- acceptedContracts: [{ schema: EXTERNAL_FINDINGS_SCHEMA, versions: [...SUPPORTED_EXTERNAL_FINDINGS_VERSIONS], mode: 'direct' }],
254
+ acceptedContracts: [{ schema: EXTERNAL_FINDINGS_SCHEMA, versions: [5], mode: 'direct' }],
187
255
  emittedContract: { schema: EXTERNAL_FINDINGS_SCHEMA, version: EXTERNAL_FINDINGS_VERSION },
188
- shapeIds: ['external-interchange.findings.v4'], boundProfileIds: ['external-interchange.document.bounds'],
256
+ shapeIds: ['external-interchange.findings.v5'], boundProfileIds: ['external-interchange.document.bounds'],
189
257
  hookIds: ['external-interchange.findings.normalise', 'external-interchange.findings.serialise'],
190
- serialisationProfileId: 'external-interchange.findings.json.v4', privacyProfileId: 'external-interchange.privacy.transient',
258
+ serialisationProfileId: 'external-interchange.findings.json.v5', privacyProfileId: 'external-interchange.privacy.transient',
191
259
  retentionEffect: 'transient_report', ...externalConsumerCommon,
192
260
  },
193
261
  {
194
262
  id: 'external-interchange.consumer.finding-rows', plane: 'shared', operation: 'convert-finding-rows',
195
263
  acceptedContracts: [{ schema: EXTERNAL_FINDING_ROWS_SCHEMA, versions: [1], mode: 'direct' }],
196
264
  emittedContract: { schema: EXTERNAL_FINDINGS_SCHEMA, version: EXTERNAL_FINDINGS_VERSION },
197
- shapeIds: ['external-interchange.finding-rows.v1', 'external-interchange.findings.v4'], boundProfileIds: ['external-interchange.document.bounds'],
265
+ shapeIds: ['external-interchange.finding-rows.v1', 'external-interchange.findings.v5'], boundProfileIds: ['external-interchange.document.bounds'],
198
266
  hookIds: ['external-interchange.finding-rows.convert', 'external-interchange.findings.serialise'],
199
- serialisationProfileId: 'external-interchange.findings.json.v4', privacyProfileId: 'external-interchange.privacy.transient',
267
+ serialisationProfileId: 'external-interchange.findings.json.v5', privacyProfileId: 'external-interchange.privacy.transient',
200
268
  retentionEffect: 'transient_report', ...externalConsumerCommon,
201
269
  },
202
270
  {
@@ -207,15 +275,15 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLif
207
275
  { schema: CERTIFICATE_OBSERVATION_ROWS_SCHEMA, versions: [1], mode: 'direct' },
208
276
  ],
209
277
  emittedContract: { schema: EXTERNAL_FINDINGS_SCHEMA, version: EXTERNAL_FINDINGS_VERSION },
210
- shapeIds: ['external-interchange.domain-rows.v1', 'external-interchange.dns-rows.v1', 'external-interchange.certificate-rows.v1', 'external-interchange.findings.v4'],
278
+ shapeIds: ['external-interchange.domain-rows.v1', 'external-interchange.dns-rows.v1', 'external-interchange.certificate-rows.v1', 'external-interchange.findings.v5'],
211
279
  boundProfileIds: ['external-interchange.document.bounds'], hookIds: ['external-interchange.observation-rows.convert', 'external-interchange.findings.serialise'],
212
- serialisationProfileId: 'external-interchange.findings.json.v4', privacyProfileId: 'external-interchange.privacy.transient',
280
+ serialisationProfileId: 'external-interchange.findings.json.v5', privacyProfileId: 'external-interchange.privacy.transient',
213
281
  retentionEffect: 'transient_report', ...externalConsumerCommon,
214
282
  },
215
283
  {
216
284
  id: 'external-interchange.consumer.case-merge', plane: 'shared', operation: 'non-destructive-case-merge',
217
- acceptedContracts: [{ schema: EXTERNAL_FINDINGS_SCHEMA, versions: [EXTERNAL_FINDINGS_VERSION], mode: 'direct' }], emittedContract: null,
218
- shapeIds: ['external-interchange.findings.v4'], boundProfileIds: ['external-interchange.document.bounds'],
285
+ acceptedContracts: [{ schema: EXTERNAL_FINDINGS_SCHEMA, versions: [...SUPPORTED_EXTERNAL_FINDINGS_VERSIONS], mode: 'direct' }], emittedContract: null,
286
+ shapeIds: ['external-interchange.findings.v4', 'external-interchange.findings.v5'], boundProfileIds: ['external-interchange.document.bounds'],
219
287
  hookIds: ['external-interchange.findings.merge'], serialisationProfileId: null,
220
288
  privacyProfileId: 'external-interchange.privacy.case-merge', retentionEffect: 'browser_indexeddb', ...externalConsumerCommon,
221
289
  },
@@ -6,11 +6,12 @@ export const REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION = 28;
6
6
  export const LATEST_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION = REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION;
7
7
  export const HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION = 29;
8
8
  export const PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION = PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION;
9
- export const LOOKUP_EVIDENCE_SCHEMA_VERSION = HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION;
9
+ export const LOOKUP_EVIDENCE_SCHEMA_VERSION = 30;
10
10
  export const SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS = Object.freeze([
11
11
  V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION,
12
12
  PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION,
13
13
  LATEST_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION,
14
+ HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION,
14
15
  LOOKUP_EVIDENCE_SCHEMA_VERSION,
15
16
  ]);
16
17
  export const LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES = 5 * 1024 * 1024;
@@ -31,5 +32,5 @@ export const LOOKUP_EVIDENCE_COMPATIBILITY = defineSchemaCompatibility({
31
32
  writeSemantics: 'read_only',
32
33
  byteBudget: LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES,
33
34
  owner: 'packages/contracts/lookup-evidence.mts',
34
- note: 'Exact published versions 26, 27 and 28 remain replayable with their original collection scope. Version 29 identifies the hostname used for supporting DNS, TLS and web observations; registration authority remains separate.',
35
+ note: 'Published versions 26–29 retain their original collection scope. Version 30 adds attributed password-form destinations; older page-wide flags are not form ownership evidence. Registration authority remains separate.',
35
36
  });
@@ -1,5 +1,6 @@
1
1
  export const MESSAGE_INTAKE_SCHEMA = 'whoisleuth.message-intake';
2
- export const MESSAGE_INTAKE_VERSION = 1;
2
+ export const MESSAGE_INTAKE_VERSION = 2;
3
+ export const MESSAGE_INTAKE_LEGACY_VERSION = 1;
3
4
  export const MESSAGE_INTAKE_KINDS = ['text', 'email', 'calendar', 'qr', 'pdf', 'docx', 'har', 'identity'];
4
5
  export const MESSAGE_INTAKE_INPUTS = {
5
6
  text: { label: 'Pasted text or links', accept: '.txt,text/plain', binary: false },
@@ -28,4 +29,9 @@ export const MAX_INTAKE_LINKS = 512;
28
29
  export const MAX_INTAKE_URL_LENGTH = 8_192;
29
30
  export const MAX_EMBEDDED_LINK_DEPTH = 8;
30
31
  export const MAX_AUTH_SCOPES = 64;
32
+ export const MAX_INTAKE_INDICATORS = 512;
33
+ export const MAX_INTAKE_INDICATOR_CANDIDATES = 4_096;
34
+ export const INTAKE_DISTRIBUTION_CHANNELS = ['email', 'sms', 'messaging', 'social', 'advertisement', 'website', 'document', 'other', 'unknown'];
35
+ export const INTAKE_TEXT_BASES = ['supplied_text', 'manual_transcription', 'ocr_text', 'unknown'];
36
+ export const INTAKE_PHONE_ROLES = ['advertised_support_contact', 'sender_or_caller_id_claim', 'unknown'];
31
37
  export const MESSAGE_ACTION_HINTS = ['clipboard_instruction', 'shell_instruction', 'verification_prompt', 'device_code_instruction', 'consent_instruction'];
@@ -1,8 +1,8 @@
1
1
  import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
2
2
  import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
3
3
  export const RISK_CALIBRATION_DATASET_SCHEMA = 'whoisleuth.risk-calibration-dataset';
4
- export const RISK_CALIBRATION_DATASET_VERSION = 2;
5
- export const SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS = Object.freeze([RISK_CALIBRATION_DATASET_VERSION]);
4
+ export const RISK_CALIBRATION_DATASET_VERSION = 3;
5
+ export const SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS = Object.freeze([2, RISK_CALIBRATION_DATASET_VERSION]);
6
6
  export const RISK_CALIBRATION_REPORT_SCHEMA = 'whoisleuth.cli.risk-calibration';
7
7
  export const RISK_CALIBRATION_REPORT_VERSION = 3;
8
8
  export const SUPPORTED_RISK_CALIBRATION_REPORT_VERSIONS = Object.freeze([RISK_CALIBRATION_REPORT_VERSION]);
@@ -82,7 +82,7 @@ export const RISK_CALIBRATION_EVIDENCE_OPTIONAL_KEYS = Object.freeze([
82
82
  'activityStatus', 'mutationTypes', 'domainAgeDays', 'faviconMatch',
83
83
  'faviconNearMatch', 'reusesOfficialAssets', 'hasPasswordField', 'hasMx',
84
84
  'hasSpf', 'hasDmarc', 'privacyProtected', 'phishingLanguageMatch',
85
- 'hasExternalFormAction', 'idnReferenceMatch', 'pageBaselineMatch',
85
+ 'hasExternalFormAction', 'hasExternalPasswordForm', 'idnReferenceMatch', 'pageBaselineMatch',
86
86
  'hasActiveBrandProfile', 'scanDepth', 'observedAt', 'threatIntelligence',
87
87
  ]);
88
88
  export const RISK_CALIBRATION_THREAT_INTELLIGENCE_KEYS = Object.freeze([
@@ -179,7 +179,7 @@ export const RISK_CALIBRATION_DATASET_COMPATIBILITY = defineSchemaCompatibility(
179
179
  writeSemantics: 'read_only',
180
180
  byteBudget: MAX_RISK_CALIBRATION_INPUT_BYTES,
181
181
  owner: 'packages/contracts/risk-calibration.mts',
182
- note: 'Offline labelled fixture input; version 2 adds bounded review context without live collection.',
182
+ note: 'Offline labelled fixture input. Version 2 retains bounded review context; version 3 adds explicit form attribution. Historical records remain readable without manufacturing missing associations.',
183
183
  });
184
184
  export const RISK_CALIBRATION_REPORT_COMPATIBILITY = defineSchemaCompatibility({
185
185
  id: 'cli.risk-calibration-report',
@@ -292,22 +292,22 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
292
292
  RISK_CALIBRATION_REPORT_COMPATIBILITY,
293
293
  ],
294
294
  contracts: [
295
- {
295
+ ...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS.map((version) => ({
296
296
  compatibilityId: RISK_CALIBRATION_DATASET_COMPATIBILITY.id,
297
297
  schema: RISK_CALIBRATION_DATASET_SCHEMA,
298
- version: RISK_CALIBRATION_DATASET_VERSION,
298
+ version,
299
299
  role: 'document',
300
- lifecycle: 'current',
300
+ lifecycle: version === RISK_CALIBRATION_DATASET_VERSION ? 'current' : 'legacy',
301
301
  readable: true,
302
- emitted: true,
302
+ emitted: version === RISK_CALIBRATION_DATASET_VERSION,
303
303
  exactKeys: false,
304
304
  extensionPolicy: 'discard_bounded',
305
305
  futureVersionBehaviour: 'reject',
306
306
  migrationTarget: null,
307
307
  canonicalisation: null,
308
308
  byteBudget: MAX_RISK_CALIBRATION_INPUT_BYTES,
309
- fixtureIds: ['risk-calibration-dataset-v2'],
310
- },
309
+ fixtureIds: [`risk-calibration-dataset-v${version}`],
310
+ })),
311
311
  {
312
312
  compatibilityId: RISK_CALIBRATION_REPORT_COMPATIBILITY.id,
313
313
  schema: RISK_CALIBRATION_REPORT_SCHEMA,
@@ -329,6 +329,20 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
329
329
  },
330
330
  ],
331
331
  fixtures: [
332
+ {
333
+ id: 'risk-calibration-dataset-v3',
334
+ path: 'test/fixtures/risk-calibration-dataset-v3.json',
335
+ bytes: 1_096,
336
+ sha256: 'a8e41c1325094aec5ef4731fdf9f487a20999729f6cc704e400c3d83db2a0753',
337
+ contentDigestSha256: null,
338
+ schema: RISK_CALIBRATION_DATASET_SCHEMA,
339
+ version: 3,
340
+ role: 'current',
341
+ expectation: 'accepted_exact',
342
+ expectedOutputFixtureId: null,
343
+ scope: 'repository',
344
+ shapeId: 'risk-calibration.dataset.v3',
345
+ },
332
346
  {
333
347
  id: 'risk-calibration-dataset-v2',
334
348
  path: 'test/fixtures/risk-calibration-dataset-v2.json',
@@ -336,8 +350,8 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
336
350
  sha256: 'bd650fe84923c61658d451c25d928bd5ff8e54ff585f4f059e6798c681f9a401',
337
351
  contentDigestSha256: null,
338
352
  schema: RISK_CALIBRATION_DATASET_SCHEMA,
339
- version: RISK_CALIBRATION_DATASET_VERSION,
340
- role: 'current',
353
+ version: 2,
354
+ role: 'historical',
341
355
  expectation: 'accepted_exact',
342
356
  expectedOutputFixtureId: null,
343
357
  scope: 'repository',
@@ -375,11 +389,11 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
375
389
  metadata: {
376
390
  enforcement: 'declarative_only',
377
391
  shapes: [
378
- ...[RISK_CALIBRATION_DATASET_VERSION].map((version) => ({
392
+ ...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS.map((version) => ({
379
393
  id: `risk-calibration.dataset.v${version}`,
380
394
  schema: RISK_CALIBRATION_DATASET_SCHEMA,
381
395
  versions: [version],
382
- objects: DATASET_SHAPE_OBJECTS,
396
+ objects: version === 2 ? DATASET_SHAPE_OBJECTS.map(object => ({ ...object, optionalKeys: object.optionalKeys.filter(key => key !== 'hasExternalPasswordForm') })) : DATASET_SHAPE_OBJECTS,
383
397
  fixedArrays: [],
384
398
  normalisation: 'project_known_fields',
385
399
  target: null,
@@ -609,7 +623,7 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
609
623
  operation: 'build-dataset-download',
610
624
  acceptedContracts: [],
611
625
  emittedContract: { schema: RISK_CALIBRATION_DATASET_SCHEMA, version: RISK_CALIBRATION_DATASET_VERSION, discriminator: null },
612
- shapeIds: ['risk-calibration.dataset.v2'],
626
+ shapeIds: [`risk-calibration.dataset.v${RISK_CALIBRATION_DATASET_VERSION}`],
613
627
  boundProfileIds: ['risk-calibration.dataset-bounds.v2'],
614
628
  hookIds: ['risk-calibration.browser.build-dataset', 'risk-calibration.browser.serialise-dataset'],
615
629
  serialisationProfileId: 'risk-calibration.dataset-json.v2',
@@ -624,9 +638,9 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
624
638
  id: 'risk-calibration.cli-detailed-json-stdout',
625
639
  plane: 'cli',
626
640
  operation: 'build-detailed-json-stdout',
627
- acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [RISK_CALIBRATION_DATASET_VERSION], mode: 'direct', discriminator: null }],
641
+ acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS], mode: 'direct', discriminator: null }],
628
642
  emittedContract: { schema: RISK_CALIBRATION_REPORT_SCHEMA, version: RISK_CALIBRATION_REPORT_VERSION, discriminator: { path: '$.mode', value: 'detailed' } },
629
- shapeIds: ['risk-calibration.dataset.v2', 'risk-calibration.report.v3-detailed'],
643
+ shapeIds: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS.map(version => `risk-calibration.dataset.v${version}`), 'risk-calibration.report.v3-detailed'],
630
644
  boundProfileIds: ['risk-calibration.dataset-bounds.v2', 'risk-calibration.report-detailed.v3', 'risk-calibration.report-detailed-json-output.v3'],
631
645
  hookIds: ['risk-calibration.cli.parse-dataset', 'risk-calibration.cli.build-detailed', 'risk-calibration.cli.serialise-report'],
632
646
  serialisationProfileId: 'risk-calibration.report-json.v3',
@@ -641,9 +655,9 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
641
655
  id: 'risk-calibration.cli-dataset-read',
642
656
  plane: 'cli',
643
657
  operation: 'read-dataset',
644
- acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [RISK_CALIBRATION_DATASET_VERSION], mode: 'direct', discriminator: null }],
658
+ acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS], mode: 'direct', discriminator: null }],
645
659
  emittedContract: null,
646
- shapeIds: ['risk-calibration.dataset.v2'],
660
+ shapeIds: SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS.map(version => `risk-calibration.dataset.v${version}`),
647
661
  boundProfileIds: ['risk-calibration.dataset-bounds.v2'],
648
662
  hookIds: ['risk-calibration.cli.parse-dataset'],
649
663
  serialisationProfileId: null,
@@ -658,9 +672,9 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
658
672
  id: 'risk-calibration.cli-detailed-terminal-stdout',
659
673
  plane: 'cli',
660
674
  operation: 'build-detailed-terminal-stdout',
661
- acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [RISK_CALIBRATION_DATASET_VERSION], mode: 'direct', discriminator: null }],
675
+ acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS], mode: 'direct', discriminator: null }],
662
676
  emittedContract: { schema: RISK_CALIBRATION_REPORT_SCHEMA, version: RISK_CALIBRATION_REPORT_VERSION, discriminator: { path: '$.mode', value: 'detailed' } },
663
- shapeIds: ['risk-calibration.dataset.v2', 'risk-calibration.report.v3-detailed'],
677
+ shapeIds: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS.map(version => `risk-calibration.dataset.v${version}`), 'risk-calibration.report.v3-detailed'],
664
678
  boundProfileIds: ['risk-calibration.dataset-bounds.v2', 'risk-calibration.report-detailed.v3'],
665
679
  hookIds: ['risk-calibration.cli.parse-dataset', 'risk-calibration.cli.build-detailed', 'risk-calibration.cli.format-terminal'],
666
680
  serialisationProfileId: null,
@@ -675,9 +689,9 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
675
689
  id: 'risk-calibration.cli-summary-json-stdout',
676
690
  plane: 'cli',
677
691
  operation: 'build-summary-json-stdout',
678
- acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [RISK_CALIBRATION_DATASET_VERSION], mode: 'direct', discriminator: null }],
692
+ acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS], mode: 'direct', discriminator: null }],
679
693
  emittedContract: { schema: RISK_CALIBRATION_REPORT_SCHEMA, version: RISK_CALIBRATION_REPORT_VERSION, discriminator: { path: '$.mode', value: 'summary' } },
680
- shapeIds: ['risk-calibration.dataset.v2', 'risk-calibration.report.v3-summary'],
694
+ shapeIds: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS.map(version => `risk-calibration.dataset.v${version}`), 'risk-calibration.report.v3-summary'],
681
695
  boundProfileIds: ['risk-calibration.dataset-bounds.v2', 'risk-calibration.report-summary-output.v3'],
682
696
  hookIds: ['risk-calibration.cli.parse-dataset', 'risk-calibration.cli.build-detailed', 'risk-calibration.shared.build-summary', 'risk-calibration.cli.serialise-report'],
683
697
  serialisationProfileId: 'risk-calibration.report-json.v3',
@@ -692,9 +706,9 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
692
706
  id: 'risk-calibration.cli-detailed-json-file',
693
707
  plane: 'cli',
694
708
  operation: 'build-detailed-json-file',
695
- acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [RISK_CALIBRATION_DATASET_VERSION], mode: 'direct', discriminator: null }],
709
+ acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS], mode: 'direct', discriminator: null }],
696
710
  emittedContract: { schema: RISK_CALIBRATION_REPORT_SCHEMA, version: RISK_CALIBRATION_REPORT_VERSION, discriminator: { path: '$.mode', value: 'detailed' } },
697
- shapeIds: ['risk-calibration.dataset.v2', 'risk-calibration.report.v3-detailed'],
711
+ shapeIds: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS.map(version => `risk-calibration.dataset.v${version}`), 'risk-calibration.report.v3-detailed'],
698
712
  boundProfileIds: ['risk-calibration.dataset-bounds.v2', 'risk-calibration.report-detailed.v3', 'risk-calibration.report-detailed-json-output.v3', 'risk-calibration.cli-private-output.v1'],
699
713
  hookIds: ['risk-calibration.cli.parse-dataset', 'risk-calibration.cli.build-detailed', 'risk-calibration.cli.serialise-report', 'risk-calibration.cli.write-private-file'],
700
714
  serialisationProfileId: 'risk-calibration.report-json.v3',
@@ -709,9 +723,9 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
709
723
  id: 'risk-calibration.cli-detailed-terminal-file',
710
724
  plane: 'cli',
711
725
  operation: 'build-detailed-terminal-file',
712
- acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [RISK_CALIBRATION_DATASET_VERSION], mode: 'direct', discriminator: null }],
726
+ acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS], mode: 'direct', discriminator: null }],
713
727
  emittedContract: { schema: RISK_CALIBRATION_REPORT_SCHEMA, version: RISK_CALIBRATION_REPORT_VERSION, discriminator: { path: '$.mode', value: 'detailed' } },
714
- shapeIds: ['risk-calibration.dataset.v2', 'risk-calibration.report.v3-detailed'],
728
+ shapeIds: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS.map(version => `risk-calibration.dataset.v${version}`), 'risk-calibration.report.v3-detailed'],
715
729
  boundProfileIds: ['risk-calibration.dataset-bounds.v2', 'risk-calibration.report-detailed.v3', 'risk-calibration.cli-private-output.v1'],
716
730
  hookIds: ['risk-calibration.cli.parse-dataset', 'risk-calibration.cli.build-detailed', 'risk-calibration.cli.format-terminal', 'risk-calibration.cli.write-private-file'],
717
731
  serialisationProfileId: null,
@@ -726,9 +740,9 @@ export const RISK_CALIBRATION_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
726
740
  id: 'risk-calibration.cli-summary-json-file',
727
741
  plane: 'cli',
728
742
  operation: 'build-summary-json-file',
729
- acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [RISK_CALIBRATION_DATASET_VERSION], mode: 'direct', discriminator: null }],
743
+ acceptedContracts: [{ schema: RISK_CALIBRATION_DATASET_SCHEMA, versions: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS], mode: 'direct', discriminator: null }],
730
744
  emittedContract: { schema: RISK_CALIBRATION_REPORT_SCHEMA, version: RISK_CALIBRATION_REPORT_VERSION, discriminator: { path: '$.mode', value: 'summary' } },
731
- shapeIds: ['risk-calibration.dataset.v2', 'risk-calibration.report.v3-summary'],
745
+ shapeIds: [...SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS.map(version => `risk-calibration.dataset.v${version}`), 'risk-calibration.report.v3-summary'],
732
746
  boundProfileIds: ['risk-calibration.dataset-bounds.v2', 'risk-calibration.report-summary-output.v3', 'risk-calibration.cli-private-output.v1'],
733
747
  hookIds: ['risk-calibration.cli.parse-dataset', 'risk-calibration.cli.build-detailed', 'risk-calibration.shared.build-summary', 'risk-calibration.cli.serialise-report', 'risk-calibration.cli.write-private-file'],
734
748
  serialisationProfileId: 'risk-calibration.report-json.v3',
@@ -1176,6 +1190,7 @@ const DATASET_EVIDENCE_SPEC = objectOf([
1176
1190
  { key: 'privacyProtected', value: BOOLEAN_SPEC, optional: true },
1177
1191
  { key: 'phishingLanguageMatch', value: boundedText(MAX_RISK_CALIBRATION_STRING_LENGTH), optional: true },
1178
1192
  { key: 'hasExternalFormAction', value: BOOLEAN_SPEC, optional: true },
1193
+ { key: 'hasExternalPasswordForm', value: BOOLEAN_SPEC, optional: true },
1179
1194
  { key: 'idnReferenceMatch', value: BOOLEAN_SPEC, optional: true },
1180
1195
  { key: 'pageBaselineMatch', value: BOOLEAN_SPEC, optional: true },
1181
1196
  { key: 'hasActiveBrandProfile', value: BOOLEAN_SPEC, optional: true },
@@ -640,11 +640,12 @@ function copyHook(value, index, budget) {
640
640
  }
641
641
  function copySerialisationProfile(value, index, budget) {
642
642
  const label = `Schema lifecycle serialisation profile ${index + 1}`;
643
- const source = ordinaryRecord(value, SERIALISATION_PROFILE_KEYS, label, budget);
643
+ const source = ordinaryRecord(value, SERIALISATION_PROFILE_KEYS, label, budget, ['compactFallback']);
644
644
  if (source.mediaType !== 'application/json'
645
645
  || source.encoding !== 'utf-8'
646
646
  || source.bom !== false
647
647
  || (source.indentSpaces !== 0 && source.indentSpaces !== 2)
648
+ || (source.compactFallback !== undefined && (source.compactFallback !== true || source.indentSpaces !== 2))
648
649
  || typeof source.terminalLf !== 'boolean'
649
650
  || (source.propertyOrder !== 'normalised_fixed' && source.propertyOrder !== 'source_insertion')
650
651
  || (source.integrity !== 'none'
@@ -664,6 +665,7 @@ function copySerialisationProfile(value, index, budget) {
664
665
  encoding: 'utf-8',
665
666
  bom: false,
666
667
  indentSpaces: source.indentSpaces,
668
+ ...(source.compactFallback === true ? { compactFallback: true } : {}),
667
669
  terminalLf: source.terminalLf,
668
670
  propertyOrder: source.propertyOrder,
669
671
  canonicalisation: source.canonicalisation === null
@@ -2,7 +2,7 @@ export { MAX_WEB_CAPTURE_MANIFEST_BYTES, WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTUR
2
2
  export const WEB_CAPTURE_DOM_DIGEST_SCHEMA = 'whoisleuth.dom-digest';
3
3
  export const WEB_CAPTURE_DOM_DIGEST_VERSION = 1;
4
4
  export const WEB_CAPTURE_COMPARISON_SCHEMA = 'whoisleuth.web-capture-comparison';
5
- export const WEB_CAPTURE_COMPARISON_VERSION = 4;
5
+ export const WEB_CAPTURE_COMPARISON_VERSION = 5;
6
6
  export const MAX_WEB_CAPTURE_DOM_DIGEST_BYTES = 1024 * 1024;
7
7
  export const MAX_WEB_CAPTURE_SCREENSHOT_BYTES = 10 * 1024 * 1024;
8
8
  export const MAX_WEB_CAPTURE_DOM_ELEMENTS = 20_000;