@slicedearth/whoisleuth-cli 1.47.4 → 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 (235) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/provider-policy-admission.mjs +3 -1
  100. package/lib/rdap-bootstrap.mjs +76 -16
  101. package/lib/rdap-client.mjs +20 -12
  102. package/lib/rdap-normalization.mjs +306 -75
  103. package/lib/rdap-transport.mjs +2 -2
  104. package/lib/rdap-validation.mjs +65 -14
  105. package/lib/rdap.mjs +2 -2
  106. package/lib/registrable-domain.mjs +36 -0
  107. package/lib/registry-insights.mjs +7 -0
  108. package/lib/response-policy.mjs +27 -4
  109. package/lib/risk-calibration-summary.mjs +35 -33
  110. package/lib/risk-scoring.mjs +4 -3
  111. package/lib/safe-fetch.mjs +62 -40
  112. package/lib/security-txt.mjs +1 -1
  113. package/lib/smtp-transport-review.mjs +8 -5
  114. package/lib/sslbl-intelligence.mjs +6 -8
  115. package/lib/static-html-analysis.mjs +301 -4
  116. package/lib/structured-data-identity.mjs +51 -19
  117. package/lib/threat-intelligence-fixtures.mjs +2 -4
  118. package/lib/threat-intelligence-manifests.mjs +2 -7
  119. package/lib/threat-intelligence-runtime.mjs +3 -5
  120. package/lib/threat-intelligence-types.mjs +3 -0
  121. package/lib/threatfox-intelligence.mjs +7 -3
  122. package/lib/tls-intelligence.mjs +4 -4
  123. package/lib/urlhaus-intelligence.mjs +4 -2
  124. package/lib/urlscan-intelligence.mjs +6 -5
  125. package/lib/website-security-posture.mjs +1 -1
  126. package/lib/website-technology.mjs +1 -1
  127. package/lib/whois-authority.mjs +43 -8
  128. package/lib/whois-chain.mjs +25 -3
  129. package/lib/whois-normalization.mjs +3 -1
  130. package/lib/whois-transport.mjs +12 -3
  131. package/lib/zone-intent-review.mjs +1 -1
  132. package/package.json +12 -4
  133. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  134. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  135. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  136. package/packages/cases/case-migration-model.mjs +476 -0
  137. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  138. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  139. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  140. package/packages/cases/case-record-model.mjs +9 -0
  141. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  142. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  143. package/packages/cases/case-response-model.mjs +1604 -0
  144. package/packages/cases/case-response-packet.mjs +1312 -0
  145. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  146. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  147. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  148. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  149. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  150. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  151. package/packages/comparison/favicon-similarity.mjs +78 -0
  152. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  153. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  154. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  155. package/packages/contracts/analyst-review-state.mjs +50 -0
  156. package/packages/contracts/case-portability.mjs +1310 -0
  157. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  158. package/packages/contracts/cli-lookup.mjs +394 -0
  159. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  160. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  161. package/packages/contracts/domain-control-monitor.mjs +310 -0
  162. package/packages/contracts/domain-control-review.mjs +828 -0
  163. package/packages/contracts/domain-name.mjs +4 -0
  164. package/packages/contracts/external-observation-interchange.mjs +224 -0
  165. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  166. package/packages/contracts/investigation-portability.mjs +331 -0
  167. package/packages/contracts/offline-comparison.mjs +160 -0
  168. package/packages/contracts/risk-calibration.mjs +1288 -0
  169. package/packages/contracts/schema-compatibility.mjs +71 -0
  170. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  171. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  172. package/packages/contracts/workspace-portability.mjs +1347 -0
  173. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  174. package/packages/evidence/decision-fact.mjs +735 -0
  175. package/packages/evidence/domain-control-runtime.mjs +547 -0
  176. package/packages/evidence/domain-name.mjs +41 -0
  177. package/packages/evidence/observation.mjs +384 -0
  178. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  179. package/packages/interchange/retained-text.mjs +8 -0
  180. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  181. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  182. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  183. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  184. package/packages/investigation/bulk-review-policy.mjs +1 -0
  185. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  186. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  187. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  188. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  189. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  190. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  191. package/packages/monitoring/analyst-review-state.mjs +680 -0
  192. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
  193. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  194. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  195. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  196. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  197. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  198. package/packages/workspace/hostile-input.mjs +172 -0
  199. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  200. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  201. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  202. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  203. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  204. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  205. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  206. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  207. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  208. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  209. package/third-party-notices.txt +14 -14
  210. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  211. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  212. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  213. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  214. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  215. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  216. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  217. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  218. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  219. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  220. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  221. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  222. package/frontend/src/lib/analysis/scoring.js +0 -55
  223. package/frontend/src/lib/analysis/utils.js +0 -255
  224. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  225. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  226. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  227. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  228. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  229. package/lib/bounded-json-response.mjs +0 -170
  230. package/lib/lookup-response-contract.mjs +0 -1034
  231. package/lib/observation.mjs +0 -115
  232. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  233. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -4,6 +4,7 @@
4
4
  // Only normalized start-tag markup and capped script indicators are retained.
5
5
  import { Tokenizer, TokenizerMode } from 'parse5';
6
6
  import { MAX_CSP_META_POLICIES, MAX_RESPONSE_POLICY_HEADER_BYTES, } from "./response-policy.mjs";
7
+ import { MAX_PAGE_PUBLICATION_DECLARATIONS, MAX_PAGE_PUBLICATION_META_ELEMENTS, MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES, } from "./homepage-metadata-contract.mjs";
7
8
  const MAX_STATIC_HTML_CHARS = 300_000;
8
9
  const MAX_STATIC_HTML_TAGS = 8_192;
9
10
  const MAX_STATIC_STRUCTURE_TOKENS = 4_096;
@@ -18,8 +19,12 @@ const MAX_INLINE_SCRIPT_TOTAL_CHARS = 65_536;
18
19
  const MAX_STATIC_VISIBLE_TEXT_CHARS = MAX_STATIC_HTML_CHARS;
19
20
  const MAX_STATIC_FORMS = 50;
20
21
  const MAX_STATIC_INPUTS = 500;
22
+ const MAX_STATIC_PUBLICATION_META_ELEMENTS = MAX_PAGE_PUBLICATION_META_ELEMENTS;
23
+ const MAX_STATIC_PUBLICATION_DECLARATIONS = MAX_PAGE_PUBLICATION_DECLARATIONS;
24
+ const MAX_STATIC_ROBOTS_DIRECTIVES = MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES;
21
25
  const MAX_FORM_ATTRIBUTE_LENGTH = 2_048;
22
- const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]/;
26
+ const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
27
+ const CONTROL_CHARACTER_RE_GLOBAL = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
23
28
  const PAYMENT_AUTOCOMPLETE_TOKENS = new Set([
24
29
  'cc-name',
25
30
  'cc-given-name',
@@ -50,6 +55,24 @@ const VOID_TAGS = new Set([
50
55
  'param', 'source', 'track', 'wbr',
51
56
  ]);
52
57
  const NON_VISIBLE_TEXT_TAGS = new Set(['script', 'style', 'template']);
58
+ const HEAD_CONTEXT_TAGS = new Set([
59
+ 'html', 'head', 'base', 'basefont', 'bgsound', 'link', 'meta', 'title',
60
+ 'noscript', 'noframes', 'style', 'template', 'script',
61
+ ]);
62
+ const IMPLICIT_HEAD_TEXT_TAGS = new Set([
63
+ 'title', 'noscript', 'noframes', 'style', 'template', 'script',
64
+ ]);
65
+ const ROBOTS_DIRECTIVES = new Set([
66
+ 'all', 'follow', 'index', 'max-image-preview', 'max-snippet', 'max-video-preview',
67
+ 'noarchive', 'nocache', 'nofollow', 'noimageindex', 'noindex', 'none', 'nositelinkssearchbox',
68
+ 'nosnippet', 'notranslate', 'unavailable_after',
69
+ ]);
70
+ const JAVASCRIPT_MEDIA_TYPES = new Set([
71
+ 'application/ecmascript', 'application/javascript', 'application/x-ecmascript',
72
+ 'application/x-javascript', 'text/ecmascript', 'text/javascript', 'text/javascript1.0',
73
+ 'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4',
74
+ 'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript',
75
+ ]);
53
76
  function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LENGTH) {
54
77
  const attribute = attributes
55
78
  .slice(0, MAX_ATTRIBUTES_PER_TAG)
@@ -171,6 +194,30 @@ function scriptMediaType(attributes) {
171
194
  }
172
195
  return null;
173
196
  }
197
+ function classicScriptCandidate(attributes) {
198
+ const type = attributeValue(attributes, 'type', MAX_SCRIPT_MEDIA_TYPE_LENGTH);
199
+ const asyncAttribute = attributeValue(attributes, 'async', 10);
200
+ const deferAttribute = attributeValue(attributes, 'defer', 10);
201
+ const truncated = type.truncated || asyncAttribute.truncated || deferAttribute.truncated;
202
+ if (asyncAttribute.present || deferAttribute.present || type.value === 'module')
203
+ return { candidate: false, truncated };
204
+ return {
205
+ candidate: !type.present || type.value === '' || Boolean(type.value && JAVASCRIPT_MEDIA_TYPES.has(type.value)),
206
+ truncated,
207
+ };
208
+ }
209
+ function stylesheetCandidate(attributes) {
210
+ const rel = attributeValue(attributes, 'rel', 320);
211
+ const href = attributeValue(attributes, 'href', MAX_SCRIPT_REFERENCE_LENGTH);
212
+ const disabled = attributeValue(attributes, 'disabled', 10);
213
+ const media = attributeValue(attributes, 'media', 120);
214
+ return {
215
+ candidate: rel.value?.split(/\s+/u).includes('stylesheet') === true
216
+ && href.present && Boolean(href.value) && !disabled.present
217
+ && (!media.present || media.value === '' || media.value === 'all' || media.value === 'screen'),
218
+ truncated: rel.truncated || href.truncated || disabled.truncated || media.truncated,
219
+ };
220
+ }
174
221
  function serializedStartTag(tagName, attributes) {
175
222
  let serialized = `<${tagName.toLowerCase()}`;
176
223
  let limitReached = attributes.length > MAX_ATTRIBUTES_PER_TAG;
@@ -207,6 +254,48 @@ function analyzeStaticHtml(value, options = {}) {
207
254
  actions: { sameOrigin: 0, external: 0, missing: 0, cleartext: 0, unclassified: 0 },
208
255
  truncated: false,
209
256
  };
257
+ const publicationMetadata = {
258
+ truncated: false,
259
+ documentTruncated: false,
260
+ robots: {
261
+ observed: false,
262
+ malformed: false,
263
+ truncated: false,
264
+ directives: [],
265
+ recognizedDirectiveCount: 0,
266
+ unknownDirectiveCount: 0,
267
+ conflicting: false,
268
+ },
269
+ twitterCard: {
270
+ observed: false,
271
+ malformed: false,
272
+ truncated: false,
273
+ cardTypes: [],
274
+ declarationCount: 0,
275
+ titlePresent: false,
276
+ descriptionPresent: false,
277
+ imagePresent: false,
278
+ imageAltPresent: false,
279
+ sitePresent: false,
280
+ creatorPresent: false,
281
+ playerPresent: false,
282
+ appPresent: false,
283
+ },
284
+ headings: { truncated: false, total: 0, h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 },
285
+ images: {
286
+ totalComplete: true,
287
+ classificationComplete: true,
288
+ truncated: false,
289
+ total: 0,
290
+ altMissing: 0,
291
+ altEmpty: 0,
292
+ altNonEmpty: 0,
293
+ altUnclassified: 0,
294
+ },
295
+ renderBlockingCandidates: { truncated: false, script: 0, stylesheet: 0, total: 0 },
296
+ };
297
+ const robotDirectives = new Set();
298
+ const twitterCardTypes = new Set();
210
299
  let tokenizer;
211
300
  let activeInlineScript = null;
212
301
  let tagsExamined = 0;
@@ -217,7 +306,13 @@ function analyzeStaticHtml(value, options = {}) {
217
306
  let inlineLimitReached = false;
218
307
  let visibleTextLimitReached = false;
219
308
  let cspMetaLimitReached = false;
309
+ let publicationMetaElements = 0;
310
+ let robotDirectiveTokens = 0;
311
+ let publicationTagLimitReached = false;
220
312
  let insideExplicitHead = false;
313
+ let implicitHeadTextDepth = 0;
314
+ let headScopeClosed = false;
315
+ let bodyContentStarted = false;
221
316
  let scriptElementSeen = false;
222
317
  let nonVisibleTextDepth = 0;
223
318
  let visibleTextCharacters = 0;
@@ -263,6 +358,7 @@ function analyzeStaticHtml(value, options = {}) {
263
358
  nonVisibleTextDepth += 1;
264
359
  if (tagsExamined >= MAX_STATIC_HTML_TAGS) {
265
360
  tagLimitReached = true;
361
+ publicationTagLimitReached = true;
266
362
  activeInlineScript = null;
267
363
  return;
268
364
  }
@@ -270,6 +366,54 @@ function analyzeStaticHtml(value, options = {}) {
270
366
  appendStructureToken(token.selfClosing || VOID_TAGS.has(tagName) ? `${tagName}/` : tagName);
271
367
  if (tagName === 'head')
272
368
  insideExplicitHead = true;
369
+ if (tagName === 'body' || (!insideExplicitHead && !HEAD_CONTEXT_TAGS.has(tagName))) {
370
+ bodyContentStarted = true;
371
+ headScopeClosed = true;
372
+ }
373
+ if (!insideExplicitHead && !headScopeClosed && !token.selfClosing && IMPLICIT_HEAD_TEXT_TAGS.has(tagName)) {
374
+ implicitHeadTextDepth += 1;
375
+ }
376
+ const inPublicationHead = insideExplicitHead || (!headScopeClosed && !bodyContentStarted && HEAD_CONTEXT_TAGS.has(tagName));
377
+ if (/^h[1-6]$/u.test(tagName)) {
378
+ const key = tagName;
379
+ publicationMetadata.headings[key] += 1;
380
+ publicationMetadata.headings.total += 1;
381
+ }
382
+ if (tagName === 'img') {
383
+ publicationMetadata.images.total += 1;
384
+ const alt = attributeValue(token.attrs, 'alt', MAX_FORM_ATTRIBUTE_LENGTH);
385
+ if (alt.truncated) {
386
+ publicationMetadata.truncated = true;
387
+ publicationMetadata.images.classificationComplete = false;
388
+ publicationMetadata.images.truncated = true;
389
+ }
390
+ if (alt.truncated && (!alt.present || alt.value === null))
391
+ publicationMetadata.images.altUnclassified += 1;
392
+ else if (!alt.present)
393
+ publicationMetadata.images.altMissing += 1;
394
+ else if (alt.value === '')
395
+ publicationMetadata.images.altEmpty += 1;
396
+ else if (alt.value !== null)
397
+ publicationMetadata.images.altNonEmpty += 1;
398
+ }
399
+ if (insideExplicitHead && tagName === 'script') {
400
+ const candidate = classicScriptCandidate(token.attrs);
401
+ if (candidate.truncated) {
402
+ publicationMetadata.truncated = true;
403
+ publicationMetadata.renderBlockingCandidates.truncated = true;
404
+ }
405
+ if (candidate.candidate)
406
+ publicationMetadata.renderBlockingCandidates.script += 1;
407
+ }
408
+ else if (insideExplicitHead && tagName === 'link') {
409
+ const candidate = stylesheetCandidate(token.attrs);
410
+ if (candidate.truncated) {
411
+ publicationMetadata.truncated = true;
412
+ publicationMetadata.renderBlockingCandidates.truncated = true;
413
+ }
414
+ if (candidate.candidate)
415
+ publicationMetadata.renderBlockingCandidates.stylesheet += 1;
416
+ }
273
417
  if (tagName === 'meta' && insideExplicitHead) {
274
418
  const httpEquiv = attributeValue(token.attrs, 'http-equiv', 64);
275
419
  if (httpEquiv.value === 'content-security-policy') {
@@ -285,6 +429,125 @@ function analyzeStaticHtml(value, options = {}) {
285
429
  }
286
430
  }
287
431
  }
432
+ if (tagName === 'meta' && inPublicationHead) {
433
+ const name = attributeValue(token.attrs, 'name', 120);
434
+ const property = attributeValue(token.attrs, 'property', 120);
435
+ const suppliedKeys = [name.value, property.value].filter((value) => Boolean(value));
436
+ const key = suppliedKeys[0] || null;
437
+ const conflictingKeys = suppliedKeys.length > 1 && new Set(suppliedKeys).size > 1;
438
+ if (name.truncated || property.truncated) {
439
+ publicationMetadata.truncated = true;
440
+ // An over-bound or control-bearing key can hide which declaration
441
+ // family it belongs to. Mark both bounded declaration families
442
+ // partial instead of emitting a root state its children contradict.
443
+ publicationMetadata.robots.truncated = true;
444
+ publicationMetadata.twitterCard.truncated = true;
445
+ }
446
+ const publicationKey = suppliedKeys.some((value) => value === 'robots' || value.startsWith('twitter:'));
447
+ const processPublication = publicationKey
448
+ && publicationMetaElements < MAX_STATIC_PUBLICATION_META_ELEMENTS;
449
+ if (publicationKey && !processPublication) {
450
+ publicationMetadata.truncated = true;
451
+ if (suppliedKeys.includes('robots'))
452
+ publicationMetadata.robots.truncated = true;
453
+ if (suppliedKeys.some((value) => value.startsWith('twitter:')))
454
+ publicationMetadata.twitterCard.truncated = true;
455
+ }
456
+ else if (processPublication) {
457
+ publicationMetaElements += 1;
458
+ }
459
+ if (processPublication && conflictingKeys) {
460
+ if (suppliedKeys.includes('robots')) {
461
+ publicationMetadata.robots.observed = true;
462
+ publicationMetadata.robots.malformed = true;
463
+ }
464
+ if (suppliedKeys.some((value) => value.startsWith('twitter:'))) {
465
+ publicationMetadata.twitterCard.observed = true;
466
+ publicationMetadata.twitterCard.malformed = true;
467
+ }
468
+ }
469
+ else if (processPublication && key === 'robots') {
470
+ publicationMetadata.robots.observed = true;
471
+ const content = attributeValue(token.attrs, 'content', 1_024);
472
+ if (content.value === null || content.truncated) {
473
+ publicationMetadata.robots.malformed = true;
474
+ publicationMetadata.truncated = publicationMetadata.truncated || content.truncated;
475
+ publicationMetadata.robots.truncated = publicationMetadata.robots.truncated || content.truncated;
476
+ }
477
+ else {
478
+ for (const suppliedDirective of content.value.split(/[,\t\n\f\r ]+/u)) {
479
+ if (robotDirectiveTokens >= MAX_STATIC_ROBOTS_DIRECTIVES) {
480
+ publicationMetadata.truncated = true;
481
+ publicationMetadata.robots.truncated = true;
482
+ break;
483
+ }
484
+ robotDirectiveTokens += 1;
485
+ const directive = suppliedDirective.trim();
486
+ if (!directive) {
487
+ publicationMetadata.robots.malformed = true;
488
+ continue;
489
+ }
490
+ const directiveName = directive.split(':', 1)[0].trim().replace(/\s+/gu, '_');
491
+ if (ROBOTS_DIRECTIVES.has(directiveName)) {
492
+ publicationMetadata.robots.recognizedDirectiveCount += 1;
493
+ robotDirectives.add(directiveName);
494
+ }
495
+ else {
496
+ publicationMetadata.robots.unknownDirectiveCount += 1;
497
+ }
498
+ }
499
+ }
500
+ }
501
+ else if (processPublication && key?.startsWith('twitter:')) {
502
+ const category = key === 'twitter:card' ? 'card'
503
+ : key === 'twitter:title' ? 'title'
504
+ : key === 'twitter:description' ? 'description'
505
+ : key === 'twitter:image:alt' ? 'imageAlt'
506
+ : key === 'twitter:image' || key.startsWith('twitter:image:') ? 'image'
507
+ : key === 'twitter:site' || key.startsWith('twitter:site:') ? 'site'
508
+ : key === 'twitter:creator' || key.startsWith('twitter:creator:') ? 'creator'
509
+ : key === 'twitter:player' || key.startsWith('twitter:player:') ? 'player'
510
+ : key === 'twitter:app' || key.startsWith('twitter:app:') ? 'app'
511
+ : null;
512
+ if (category) {
513
+ publicationMetadata.twitterCard.observed = true;
514
+ if (publicationMetadata.twitterCard.declarationCount >= MAX_STATIC_PUBLICATION_DECLARATIONS) {
515
+ publicationMetadata.truncated = true;
516
+ publicationMetadata.twitterCard.truncated = true;
517
+ }
518
+ else {
519
+ publicationMetadata.twitterCard.declarationCount += 1;
520
+ const content = attributeValue(token.attrs, 'content', 1_024);
521
+ if (content.value === null || content.truncated || !content.value) {
522
+ publicationMetadata.twitterCard.malformed = true;
523
+ publicationMetadata.truncated = publicationMetadata.truncated || content.truncated;
524
+ publicationMetadata.twitterCard.truncated = publicationMetadata.twitterCard.truncated || content.truncated;
525
+ }
526
+ else if (category === 'card') {
527
+ twitterCardTypes.add(['summary', 'summary_large_image', 'player', 'app'].includes(content.value)
528
+ ? content.value
529
+ : 'other');
530
+ }
531
+ else if (category === 'title')
532
+ publicationMetadata.twitterCard.titlePresent = true;
533
+ else if (category === 'description')
534
+ publicationMetadata.twitterCard.descriptionPresent = true;
535
+ else if (category === 'image')
536
+ publicationMetadata.twitterCard.imagePresent = true;
537
+ else if (category === 'imageAlt')
538
+ publicationMetadata.twitterCard.imageAltPresent = true;
539
+ else if (category === 'site')
540
+ publicationMetadata.twitterCard.sitePresent = true;
541
+ else if (category === 'creator')
542
+ publicationMetadata.twitterCard.creatorPresent = true;
543
+ else if (category === 'player')
544
+ publicationMetadata.twitterCard.playerPresent = true;
545
+ else if (category === 'app')
546
+ publicationMetadata.twitterCard.appPresent = true;
547
+ }
548
+ }
549
+ }
550
+ }
288
551
  if (markup.length < MAX_TECHNOLOGY_TAGS) {
289
552
  const serialized = serializedStartTag(tagName, token.attrs);
290
553
  if (serialized.limitReached)
@@ -349,18 +612,31 @@ function analyzeStaticHtml(value, options = {}) {
349
612
  const tagName = token.tagName.toLowerCase();
350
613
  if (tagName === 'script')
351
614
  activeInlineScript = null;
352
- if (tagName === 'head')
615
+ if (tagName === 'head') {
353
616
  insideExplicitHead = false;
617
+ headScopeClosed = true;
618
+ }
619
+ if (!insideExplicitHead && IMPLICIT_HEAD_TEXT_TAGS.has(tagName) && implicitHeadTextDepth > 0) {
620
+ implicitHeadTextDepth -= 1;
621
+ }
354
622
  if (!VOID_TAGS.has(tagName))
355
623
  appendStructureToken(`/${tagName}`);
356
624
  if (NON_VISIBLE_TEXT_TAGS.has(tagName) && nonVisibleTextDepth > 0)
357
625
  nonVisibleTextDepth -= 1;
358
626
  },
359
627
  onCharacter(token) {
628
+ if (!insideExplicitHead && !headScopeClosed && implicitHeadTextDepth === 0 && token.chars.trim()) {
629
+ bodyContentStarted = true;
630
+ headScopeClosed = true;
631
+ }
360
632
  appendInlineScript(token.chars);
361
633
  appendVisibleText(token.chars);
362
634
  },
363
635
  onNullCharacter(token) {
636
+ if (!insideExplicitHead && !headScopeClosed && implicitHeadTextDepth === 0) {
637
+ bodyContentStarted = true;
638
+ headScopeClosed = true;
639
+ }
364
640
  appendInlineScript(token.chars);
365
641
  appendVisibleText(token.chars);
366
642
  },
@@ -374,14 +650,35 @@ function analyzeStaticHtml(value, options = {}) {
374
650
  };
375
651
  tokenizer = new Tokenizer({ sourceCodeLocationInfo: false }, handler);
376
652
  tokenizer.write(html, true);
653
+ publicationMetadata.truncated = publicationMetadata.truncated || inputLimitReached || publicationTagLimitReached;
654
+ publicationMetadata.documentTruncated = inputLimitReached || publicationTagLimitReached;
655
+ if (publicationMetadata.documentTruncated) {
656
+ publicationMetadata.robots.truncated = true;
657
+ publicationMetadata.twitterCard.truncated = true;
658
+ publicationMetadata.headings.truncated = true;
659
+ publicationMetadata.images.totalComplete = false;
660
+ publicationMetadata.images.classificationComplete = false;
661
+ publicationMetadata.images.truncated = true;
662
+ publicationMetadata.renderBlockingCandidates.truncated = true;
663
+ }
664
+ publicationMetadata.robots.directives = [...robotDirectives].sort();
665
+ publicationMetadata.robots.conflicting = ((robotDirectives.has('index') || robotDirectives.has('all'))
666
+ && (robotDirectives.has('noindex') || robotDirectives.has('none'))) || ((robotDirectives.has('follow') || robotDirectives.has('all'))
667
+ && (robotDirectives.has('nofollow') || robotDirectives.has('none')));
668
+ publicationMetadata.twitterCard.cardTypes = [...twitterCardTypes].sort().slice(0, 8);
669
+ if (twitterCardTypes.size > 8)
670
+ publicationMetadata.truncated = true;
671
+ publicationMetadata.renderBlockingCandidates.total = publicationMetadata.renderBlockingCandidates.script
672
+ + publicationMetadata.renderBlockingCandidates.stylesheet;
377
673
  return {
378
674
  markup: markup.join('\n'),
379
- visibleText: visibleTextParts.join(''),
675
+ visibleText: visibleTextParts.join('').replace(CONTROL_CHARACTER_RE_GLOBAL, ' ').replace(/\s+/gu, ' '),
380
676
  structureTokens,
381
677
  scripts,
382
678
  cspMetaPolicies,
383
679
  cspMetaLimitReached,
384
680
  forms,
681
+ publicationMetadata,
385
682
  inputLimitReached,
386
683
  tagLimitReached,
387
684
  structureLimitReached,
@@ -392,4 +689,4 @@ function analyzeStaticHtml(value, options = {}) {
392
689
  inlineCharactersExamined,
393
690
  };
394
691
  }
395
- export { MAX_ATTRIBUTES_PER_TAG, MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_MEDIA_TYPE_LENGTH, MAX_STATIC_FORMS, MAX_STATIC_HTML_CHARS, MAX_STATIC_INPUTS, MAX_STATIC_HTML_TAGS, MAX_STATIC_STRUCTURE_TOKENS, MAX_STATIC_VISIBLE_TEXT_CHARS, MAX_TAG_LENGTH, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, };
692
+ export { MAX_ATTRIBUTES_PER_TAG, MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_MEDIA_TYPE_LENGTH, MAX_STATIC_FORMS, MAX_STATIC_HTML_CHARS, MAX_STATIC_INPUTS, MAX_STATIC_PUBLICATION_META_ELEMENTS, MAX_STATIC_PUBLICATION_DECLARATIONS, MAX_STATIC_ROBOTS_DIRECTIVES, MAX_STATIC_HTML_TAGS, MAX_STATIC_STRUCTURE_TOKENS, MAX_STATIC_VISIBLE_TEXT_CHARS, MAX_TAG_LENGTH, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, };
@@ -4,7 +4,8 @@
4
4
  // and fixed limitations. Raw JSON-LD and arbitrary publisher properties are
5
5
  // discarded immediately after analysis.
6
6
  import { domainToASCII } from 'node:url';
7
- import { createObservation } from "./observation.mjs";
7
+ import { createObservation } from "../packages/evidence/observation.mjs";
8
+ import { isUriShapedLabel } from "./portable-generator.mjs";
8
9
  import { analyzeStaticHtml, } from "./static-html-analysis.mjs";
9
10
  const STRUCTURED_DATA_IDENTITY_VERSION = 1;
10
11
  const MAX_STRUCTURED_DATA_SCRIPTS = 8;
@@ -19,8 +20,8 @@ const MAX_STRUCTURED_DATA_TYPES = 8;
19
20
  const MAX_STRUCTURED_DATA_NAME_LENGTH = 160;
20
21
  const MAX_STRUCTURED_DATA_URL_LENGTH = 2_048;
21
22
  const MAX_STRUCTURED_DATA_SAME_AS_HOSTS = 12;
22
- const CONTROL_AND_DIRECTIONAL_RE = /[\u0000-\u001f\u007f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/u;
23
- const CONTROL_AND_DIRECTIONAL_GLOBAL_RE = /[\u0000-\u001f\u007f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/gu;
23
+ const CONTROL_AND_DIRECTIONAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
24
+ const CONTROL_AND_DIRECTIONAL_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
24
25
  const HOSTNAME_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/iu;
25
26
  const CURATED_TYPES = new Map([
26
27
  ['brand', 'Brand'],
@@ -44,17 +45,21 @@ function record(value) {
44
45
  : null;
45
46
  }
46
47
  function boundedName(value) {
47
- if (typeof value !== 'string' || value.length > MAX_STRUCTURED_DATA_URL_LENGTH)
48
- return null;
48
+ if (typeof value !== 'string')
49
+ return { value: null, truncated: false };
50
+ if (value.length > MAX_STRUCTURED_DATA_URL_LENGTH)
51
+ return { value: null, truncated: true };
52
+ if (isUriShapedLabel(value))
53
+ return { value: null, truncated: false };
49
54
  const normalized = value
50
55
  .replace(CONTROL_AND_DIRECTIONAL_GLOBAL_RE, ' ')
51
56
  .replace(/\s+/gu, ' ')
52
57
  .trim();
53
58
  if (!normalized)
54
- return null;
59
+ return { value: null, truncated: false };
55
60
  return normalized.length <= MAX_STRUCTURED_DATA_NAME_LENGTH
56
- ? normalized
57
- : `${normalized.slice(0, MAX_STRUCTURED_DATA_NAME_LENGTH)}…`;
61
+ ? { value: normalized, truncated: false }
62
+ : { value: `${normalized.slice(0, MAX_STRUCTURED_DATA_NAME_LENGTH - 1)}…`, truncated: true };
58
63
  }
59
64
  function safeBaseUrl(value) {
60
65
  if (typeof value !== 'string' || value.length > MAX_STRUCTURED_DATA_URL_LENGTH)
@@ -116,7 +121,7 @@ function curatedTypes(value) {
116
121
  if (normalized)
117
122
  types.add(normalized);
118
123
  }
119
- return [...types].sort();
124
+ return { value: [...types].sort(), truncated: values.length > MAX_STRUCTURED_DATA_TYPES };
120
125
  }
121
126
  function sameAsHosts(value) {
122
127
  const values = Array.isArray(value) ? value : [value];
@@ -126,7 +131,7 @@ function sameAsHosts(value) {
126
131
  if (host)
127
132
  hosts.add(host);
128
133
  }
129
- return [...hosts].sort();
134
+ return { value: [...hosts].sort(), truncated: values.length > MAX_STRUCTURED_DATA_SAME_AS_HOSTS };
130
135
  }
131
136
  function jsonStructureWithinBounds(value) {
132
137
  let depth = 0;
@@ -161,14 +166,21 @@ function jsonStructureWithinBounds(value) {
161
166
  }
162
167
  function entityFromObject(value, baseUrl) {
163
168
  const types = curatedTypes(value['@type']);
164
- if (!types.length)
165
- return null;
166
169
  const name = boundedName(value.name);
167
170
  const declaredOrigin = normalizedOrigin(value.url, baseUrl);
168
171
  const hosts = sameAsHosts(value.sameAs);
169
- if (!name && !declaredOrigin && !hosts.length)
170
- return null;
171
- return { types, name, declaredOrigin, sameAsHosts: hosts };
172
+ const projection = {
173
+ nameTruncated: name.truncated,
174
+ typesTruncated: types.truncated,
175
+ sameAsTruncated: hosts.truncated,
176
+ };
177
+ if (!types.value.length || (!name.value && !declaredOrigin && !hosts.value.length)) {
178
+ return { entity: null, ...projection };
179
+ }
180
+ return {
181
+ entity: { types: types.value, name: name.value, declaredOrigin, sameAsHosts: hosts.value },
182
+ ...projection,
183
+ };
172
184
  }
173
185
  function analyzeStructuredDataIdentity(input = {}) {
174
186
  const htmlAnalysis = input.htmlAnalysis ?? analyzeStaticHtml(input.html);
@@ -183,6 +195,9 @@ function analyzeStructuredDataIdentity(input = {}) {
183
195
  let objectsExamined = 0;
184
196
  let arrayItemsExamined = 0;
185
197
  let discardedProperties = 0;
198
+ let truncatedNames = 0;
199
+ let truncatedTypeLists = 0;
200
+ let truncatedSameAsLists = 0;
186
201
  let entityLimitReached = false;
187
202
  let limitReached = scripts.length > MAX_STRUCTURED_DATA_SCRIPTS;
188
203
  for (const script of selectedScripts) {
@@ -242,12 +257,20 @@ function analyzeStructuredDataIdentity(input = {}) {
242
257
  continue;
243
258
  }
244
259
  objectsExamined += 1;
245
- const entity = entityFromObject(object, baseUrl);
246
- if (entity) {
247
- const key = JSON.stringify(entity);
260
+ const projection = entityFromObject(object, baseUrl);
261
+ if (projection.nameTruncated)
262
+ truncatedNames += 1;
263
+ if (projection.typesTruncated)
264
+ truncatedTypeLists += 1;
265
+ if (projection.sameAsTruncated)
266
+ truncatedSameAsLists += 1;
267
+ if (projection.nameTruncated || projection.typesTruncated || projection.sameAsTruncated)
268
+ limitReached = true;
269
+ if (projection.entity) {
270
+ const key = JSON.stringify(projection.entity);
248
271
  if (!entities.has(key)) {
249
272
  if (entities.size < MAX_STRUCTURED_DATA_ENTITIES)
250
- entities.set(key, entity);
273
+ entities.set(key, projection.entity);
251
274
  else {
252
275
  entityLimitReached = true;
253
276
  limitReached = true;
@@ -294,6 +317,12 @@ function analyzeStructuredDataIdentity(input = {}) {
294
317
  if (entityLimitReached) {
295
318
  limitations.push(`Only the first ${MAX_STRUCTURED_DATA_ENTITIES} structured identity entities were retained.`);
296
319
  }
320
+ if (truncatedNames > 0)
321
+ limitations.push(`${truncatedNames} structured identity label${truncatedNames === 1 ? ' was' : 's were'} shortened to the ${MAX_STRUCTURED_DATA_NAME_LENGTH}-character retention limit.`);
322
+ if (truncatedTypeLists > 0)
323
+ limitations.push(`${truncatedTypeLists} structured identity type list${truncatedTypeLists === 1 ? ' exceeded' : 's exceeded'} the ${MAX_STRUCTURED_DATA_TYPES}-item inspection limit.`);
324
+ if (truncatedSameAsLists > 0)
325
+ limitations.push(`${truncatedSameAsLists} structured identity sameAs list${truncatedSameAsLists === 1 ? ' exceeded' : 's exceeded'} the ${MAX_STRUCTURED_DATA_SAME_AS_HOSTS}-item inspection limit.`);
297
326
  return {
298
327
  structuredDataVersion: STRUCTURED_DATA_IDENTITY_VERSION,
299
328
  ...createObservation({
@@ -314,6 +343,9 @@ function analyzeStructuredDataIdentity(input = {}) {
314
343
  objectsExamined,
315
344
  arrayItemsExamined,
316
345
  discardedProperties,
346
+ truncatedNames,
347
+ truncatedTypeLists,
348
+ truncatedSameAsLists,
317
349
  entities: entities.size,
318
350
  },
319
351
  }),
@@ -1,5 +1,6 @@
1
1
  import { createCuratedConnectorResult, normalizeCuratedConnectorTarget, } from "./threat-intelligence-runtime.mjs";
2
2
  import { assertCuratedConnectorDefinition } from "./threat-intelligence-definition-registry.mjs";
3
+ import { normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
3
4
  const MAX_CONNECTOR_KEY_LENGTH = 80;
4
5
  const MAX_CONNECTOR_FIXTURE_BYTES = 512 * 1024;
5
6
  function isRecord(value) {
@@ -24,10 +25,7 @@ function fixtureId(value) {
24
25
  return value;
25
26
  }
26
27
  function fixtureTimestamp(value) {
27
- if (typeof value !== 'string' || value.length > 50)
28
- return null;
29
- const timestamp = Date.parse(value);
30
- return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
28
+ return normalizeLegacyIsoTimestamp(value);
31
29
  }
32
30
  export function runCuratedConnectorFixture(connector, fixture, normalizeFixture) {
33
31
  assertCuratedConnectorDefinition(connector);
@@ -1,5 +1,6 @@
1
1
  import { registerCuratedConnector, registerThreatIntelligenceProvider, isCuratedConnectorDefinition, isThreatIntelligenceProvider, } from "./threat-intelligence-definition-registry.mjs";
2
2
  import { CURATED_CONNECTOR_CONTRACT_VERSION, MAX_CACHE_TTL_MS, MAX_CONNECTOR_ENTITIES, MAX_CONNECTOR_RELATIONSHIPS, MAX_RESPONSE_BYTES, MAX_TIMEOUT_MS, THREAT_INTELLIGENCE_CONTRACT_VERSION, } from "./threat-intelligence-runtime.mjs";
3
+ import { normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
3
4
  const MAX_PROVIDER_ID_LENGTH = 40;
4
5
  const MAX_PROVIDER_LABEL_LENGTH = 100;
5
6
  const MAX_URL_LENGTH = 2_048;
@@ -137,13 +138,7 @@ function strictBoundedString(value, maximum) {
137
138
  return normalized && normalized.length <= maximum ? normalized : null;
138
139
  }
139
140
  function isoTimestamp(value) {
140
- if (typeof value !== 'string' ||
141
- value.length > 64 ||
142
- /[\u0000-\u001f\u007f]/u.test(value)) {
143
- return null;
144
- }
145
- const timestamp = Date.parse(value);
146
- return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
141
+ return normalizeLegacyIsoTimestamp(value);
147
142
  }
148
143
  function httpsUrl(value) {
149
144
  const raw = strictBoundedString(value, MAX_URL_LENGTH);
@@ -3,13 +3,11 @@
3
3
  // network requests and contains no provider credentials. Adapters added later
4
4
  // must declare exact input exposure, reviewed terms, and bounded request and
5
5
  // output budgets before producing separately attributed normalized evidence.
6
- import { createObservation } from "./observation.mjs";
7
- import { THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_RESULT_STATES, } from "./threat-intelligence-types.mjs";
6
+ import { createObservation } from "../packages/evidence/observation.mjs";
7
+ import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "./threat-intelligence-types.mjs";
8
8
  import { assertCuratedConnectorDefinition, assertThreatIntelligenceProvider, } from "./threat-intelligence-definition-registry.mjs";
9
9
  import { boundedHttpsUrl, boundedString, isRecord, isoTimestamp, strictBoundedString, } from "./bounded-contract-normalizers.mjs";
10
10
  import { CURATED_CONNECTOR_ENTITY_TYPES, MAX_THREAT_INTELLIGENCE_URL_LENGTH, normalizeCuratedConnectorTarget, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-targets.mjs";
11
- const THREAT_INTELLIGENCE_CONTRACT_VERSION = 1;
12
- const THREAT_INTELLIGENCE_SCHEMA = 'whoisleuth.threat-intelligence-result';
13
11
  const CURATED_CONNECTOR_CONTRACT_VERSION = 1;
14
12
  const CURATED_CONNECTOR_RESULT_SCHEMA = 'whoisleuth.curated-connector-result';
15
13
  const RESULT_STATES = new Set(THREAT_INTELLIGENCE_RESULT_STATES);
@@ -650,4 +648,4 @@ function createCuratedConnectorResult(connector, target, input = {}, observedAt
650
648
  }),
651
649
  };
652
650
  }
653
- export { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_SCHEMA, CURATED_CONNECTOR_CONTRACT_VERSION, CURATED_CONNECTOR_RESULT_SCHEMA, MAX_FINDINGS, MAX_INPUT_FINDINGS, MAX_TIMEOUT_MS, MAX_RESPONSE_BYTES, MAX_CACHE_TTL_MS, MAX_CONNECTOR_ENTITIES, MAX_CONNECTOR_RELATIONSHIPS, MAX_CONNECTOR_INPUT_ENTITIES, MAX_CONNECTOR_INPUT_RELATIONSHIPS, normalizeThreatIntelligenceTarget, normalizeCuratedConnectorTarget, createThreatIntelligenceResult, createCuratedConnectorResult, };
651
+ export { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_SCHEMA, CURATED_CONNECTOR_CONTRACT_VERSION, CURATED_CONNECTOR_RESULT_SCHEMA, MAX_FINDINGS, MAX_INPUT_FINDINGS, MAX_TIMEOUT_MS, MAX_RESPONSE_BYTES, MAX_CACHE_TTL_MS, MAX_CONNECTOR_ENTITIES, MAX_CONNECTOR_RELATIONSHIPS, MAX_CONNECTOR_INPUT_ENTITIES, MAX_CONNECTOR_INPUT_RELATIONSHIPS, normalizeThreatIntelligenceTarget, normalizeCuratedConnectorTarget, createThreatIntelligenceResult, createCuratedConnectorResult, };
@@ -1,5 +1,8 @@
1
1
  // Provider-neutral types shared by threat-intelligence and curated connector
2
2
  // manifests, fixture harnesses, normalizers, and result builders.
3
+ export const THREAT_INTELLIGENCE_SCHEMA = 'whoisleuth.threat-intelligence-result';
4
+ export const THREAT_INTELLIGENCE_CONTRACT_VERSION = 1;
5
+ export const THREAT_INTELLIGENCE_ENVELOPE_VERSION = 1;
3
6
  export const THREAT_INTELLIGENCE_CATEGORIES = Object.freeze([
4
7
  'phishing',
5
8
  'malware',
@@ -6,6 +6,7 @@ import { safeFetchDetailed, readTextCapped } from "./safe-fetch.mjs";
6
6
  import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
7
7
  import { providerPolicyAdmission } from "./provider-policy-admission.mjs";
8
8
  import { createThreatIntelligenceResult, defineThreatIntelligenceProvider, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-contract.mjs";
9
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
9
10
  const THREATFOX_SEARCH_ENDPOINT = 'https://threatfox-api.abuse.ch/api/v1/';
10
11
  const THREATFOX_MAX_RESULTS = 20;
11
12
  const THREATFOX_MAX_INPUT_RESULTS = 100;
@@ -75,8 +76,7 @@ function isoTimestamp(value) {
75
76
  const normalized = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?: UTC)?$/u.test(raw)
76
77
  ? `${raw.slice(0, 10)}T${raw.slice(11, 19)}Z`
77
78
  : raw;
78
- const timestamp = Date.parse(normalized);
79
- return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
79
+ return normalizeExplicitIsoTimestamp(normalized);
80
80
  }
81
81
  function normalizedTags(value) {
82
82
  if (!Array.isArray(value))
@@ -112,7 +112,11 @@ function normalizeIocFinding(value, targetDomain) {
112
112
  if (!id || !/^\d{1,32}$/u.test(id) || ioc !== targetDomain || iocType !== 'domain' || !threatType)
113
113
  return null;
114
114
  const firstObservedAt = isoTimestamp(value.first_seen);
115
- const lastObservedAt = isoTimestamp(value.last_seen) || firstObservedAt;
115
+ const parsedLastObservedAt = isoTimestamp(value.last_seen);
116
+ if ((value.first_seen !== undefined && value.first_seen !== null && !firstObservedAt)
117
+ || (value.last_seen !== undefined && value.last_seen !== null && !parsedLastObservedAt))
118
+ return null;
119
+ const lastObservedAt = parsedLastObservedAt || firstObservedAt;
116
120
  const tags = normalizedTags(value.tags);
117
121
  tags.push(`role:${threatType}`);
118
122
  if (malware)