@slicedearth/whoisleuth-cli 2.0.1 → 2.1.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 (95) hide show
  1. package/cli/arguments.mjs +21 -5
  2. package/cli/artifact-validation/case-response.mjs +771 -0
  3. package/cli/artifact-validation/investigation-capsule.mjs +426 -0
  4. package/cli/artifact-validation/lookup-evidence.mjs +718 -0
  5. package/cli/artifact-validation/signed-review.mjs +598 -0
  6. package/cli/artifact-validation/structure-primitives.mjs +105 -0
  7. package/cli/artifact-verify.mjs +1 -1
  8. package/cli/assurance-command-runner.mjs +324 -0
  9. package/cli/command-reference.mjs +1175 -843
  10. package/cli/completion.mjs +56 -9
  11. package/cli/discriminated-command-handlers.mjs +11 -0
  12. package/cli/doctor.mjs +2 -2
  13. package/cli/evidence-command-runner.mjs +41 -31
  14. package/cli/export-evidence.mjs +1 -1
  15. package/cli/fail-policy.mjs +33 -8
  16. package/cli/formatters/terminal-command-formats.mjs +437 -0
  17. package/cli/formatters/terminal-lookup.mjs +758 -0
  18. package/cli/formatters/terminal-metadata.mjs +58 -0
  19. package/cli/formatters/terminal-shared.mjs +83 -0
  20. package/cli/formatters/terminal.mjs +5 -1232
  21. package/cli/history-command-runner.mjs +132 -0
  22. package/cli/inline-command-families.mjs +45 -0
  23. package/cli/inline-command-runner.mjs +55 -0
  24. package/cli/investigation-plan.mjs +9 -2
  25. package/cli/investigation-run.mjs +114 -9
  26. package/cli/lookup-brief.mjs +141 -23
  27. package/cli/lookup-browser-view.mjs +289 -0
  28. package/cli/lookup-browser.mjs +3 -288
  29. package/cli/mail-review.mjs +2 -6
  30. package/cli/offline-artifact-validation.mjs +19 -0
  31. package/cli/review-command-runner.mjs +269 -0
  32. package/cli/runner.mjs +15 -972
  33. package/cli/saved-lookup.mjs +19 -2
  34. package/cli/support-command-runner.mjs +229 -0
  35. package/cli/workflow-command-runner.mjs +129 -0
  36. package/docs/cli-reference.md +16 -9
  37. package/docs/cli.md +4 -2
  38. package/lib/availability.mjs +12 -9
  39. package/lib/bounded-contract-normalizers.mjs +2 -9
  40. package/lib/browser-library-profile.mjs +1 -2
  41. package/lib/client-behavior-profile.mjs +1 -2
  42. package/lib/credential-surface-profile.mjs +1 -1
  43. package/lib/domain-posture-analysis.mjs +10 -1
  44. package/lib/domain-posture.mjs +51 -23
  45. package/lib/evidence-export.mjs +46 -5
  46. package/lib/evidence-report-markdown.mjs +4 -0
  47. package/lib/evidence-report.mjs +50 -0
  48. package/lib/generated/cisa-kev-catalog.mjs +34 -4
  49. package/lib/generated/registrar-standing-catalogue.mjs +367 -0
  50. package/lib/json-record.mjs +12 -0
  51. package/lib/lookup-child-profile-contract.mjs +858 -0
  52. package/lib/lookup-contract-primitives.mjs +98 -0
  53. package/lib/lookup-diagnostics.mjs +96 -0
  54. package/lib/lookup-network-evidence-bounds.mjs +1 -0
  55. package/lib/lookup-response.mjs +367 -0
  56. package/lib/lookup.mjs +28 -443
  57. package/lib/page-role-profile.mjs +1 -3
  58. package/lib/registrar-standing-catalogue-contract.mjs +70 -0
  59. package/lib/registrar-standing-contract.mjs +300 -0
  60. package/lib/registrar-standing.mjs +269 -0
  61. package/lib/registry-insights.mjs +2 -8
  62. package/lib/risk-scoring.mjs +26 -10
  63. package/lib/sslbl-certificates.generated.mjs +138 -136
  64. package/lib/structured-data-identity.mjs +1 -3
  65. package/lib/technology-evidence-role.mjs +51 -0
  66. package/lib/threat-intelligence-runtime.mjs +3 -3
  67. package/lib/threat-intelligence-types.mjs +13 -0
  68. package/lib/threatfox-intelligence.mjs +1 -3
  69. package/lib/tls-intelligence.mjs +1 -2
  70. package/lib/urlhaus-intelligence.mjs +1 -3
  71. package/lib/urlscan-intelligence.mjs +1 -3
  72. package/lib/website-security-posture.mjs +1 -2
  73. package/lib/website-technology.mjs +72 -23
  74. package/lib/whois-common-formats.mjs +276 -0
  75. package/lib/whois-dialects.mjs +534 -0
  76. package/lib/whois-parser-support.mjs +305 -0
  77. package/lib/whois-parser.mjs +135 -0
  78. package/lib/whois.mjs +6 -1187
  79. package/package.json +2 -2
  80. package/packages/cases/case-evidence-model.mjs +9 -0
  81. package/packages/cases/case-migration-model.mjs +17 -11
  82. package/packages/cases/case-response-model.mjs +13 -9
  83. package/packages/cases/case-response-packet.mjs +297 -112
  84. package/packages/comparison/page-similarity.mjs +23 -0
  85. package/packages/contracts/case-portability.mjs +225 -61
  86. package/packages/contracts/cli-lookup.mjs +4 -3
  87. package/packages/contracts/domain-control-manifest.mjs +1 -1
  88. package/packages/contracts/investigation-portability.mjs +1 -1
  89. package/packages/contracts/risk-calibration.mjs +2 -2
  90. package/packages/contracts/workspace-portability.mjs +34 -6
  91. package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
  92. package/packages/investigation/lookup-claim-readiness.mjs +2 -2
  93. package/packages/workspace/website-snapshot-model.mjs +79 -6
  94. package/third-party-notices.txt +1 -1
  95. package/cli/artifact-structure.mjs +0 -2507
@@ -0,0 +1,426 @@
1
+ import { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, INVESTIGATION_CAPSULE_VERSION, PUBLIC_INVESTIGATION_CAPSULE_VERSION, } from "../../packages/investigation/investigation-capsule.mjs";
2
+ import { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION, } from "../../packages/investigation/lookup-asset-graph.mjs";
3
+ import { PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION, LOOKUP_INVESTIGATION_BRIEF_SCHEMA, LOOKUP_INVESTIGATION_BRIEF_VERSION, MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES, } from "../../packages/investigation/lookup-investigation-brief.mjs";
4
+ import { DECISION_FACT_CONSISTENCY_STATES, DECISION_FACT_EVIDENCE_STATES, DECISION_FACT_FRESHNESS_STATES, DECISION_FACT_PROVENANCE_STATES, DECISION_FACT_PROJECTION_VERSION, DECISION_FACT_VERSION, MAX_DECISION_FACT_CONTRIBUTORS, MAX_DECISION_FACT_CONTRADICTIONS, MAX_DECISION_FACT_LIMITATIONS, MAX_DECISION_FACT_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_CONTRADICTIONS, MAX_DECISION_FACT_PROJECTION_BYTES, MAX_DECISION_FACT_PROJECTION_FACTS, MAX_DECISION_FACT_PROJECTION_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_REFERENCES, MAX_DECISION_FACT_PROJECTION_SOURCES, MAX_DECISION_FACT_PROJECTION_SOURCE_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_SOURCE_REFERENCES, MAX_DECISION_FACT_REFERENCES, MAX_DECISION_FACTS, decisionFactCompleteness, } from "../../packages/evidence/decision-fact.mjs";
5
+ import { MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_DECISION_PIN_REFERENCES, } from "../../packages/cases/case-response-model.mjs";
6
+ import { array, boolean, digest, enumeration, exact, exactOptional, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
7
+ function validateBriefFact(value, label) {
8
+ const fact = exact(value, ['label', 'value', 'detail', 'provenance'], label);
9
+ text(fact.label, label, 320);
10
+ text(fact.value, label, 2_000);
11
+ text(fact.detail, label, 1_000, true);
12
+ const provenance = exact(fact.provenance, ['sources', 'observedAt', 'fieldFamilies', 'normalization', 'completeness', 'limitations', 'conflicts', 'decisionImpact'], label);
13
+ strings(provenance.sources, label, 8, 320);
14
+ text(provenance.observedAt, label, 64);
15
+ strings(provenance.fieldFamilies, label, 8, 320);
16
+ text(provenance.normalization, label, 320);
17
+ text(provenance.completeness, label, 320);
18
+ strings(provenance.limitations, label, 8, 320);
19
+ strings(provenance.conflicts, label, 8, 320);
20
+ text(provenance.decisionImpact, label, 320);
21
+ }
22
+ function validateDecisionEntry(value, label) {
23
+ const entry = exact(value, ['id', 'state', 'importance', 'title', 'detail', 'sources', 'href'], label);
24
+ text(entry.id, label, 80);
25
+ enumeration(entry.state, ['conflict', 'uncertain'], label);
26
+ enumeration(entry.importance, ['high', 'medium', 'low'], label);
27
+ text(entry.title, label, 320);
28
+ text(entry.detail, label, 1_000);
29
+ strings(entry.sources, label, 12, 320);
30
+ if (typeof entry.href !== 'string' || !/^#[^\u0000-\u001f\u007f]{1,160}$/u.test(entry.href))
31
+ fail(label);
32
+ }
33
+ function validatePublicBrief(value) {
34
+ const brief = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'target', 'targetType', 'task', 'taskLabel', 'question', 'summary', 'observation', 'verifiedFacts', 'contradictions', 'unknowns', 'nextActions', 'relationships', 'limitations'], 'Investigation capsule brief');
35
+ if (brief.schema !== LOOKUP_INVESTIGATION_BRIEF_SCHEMA
36
+ || brief.schemaVersion !== PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION)
37
+ fail('Investigation capsule brief');
38
+ iso(brief.generatedAt, 'Investigation capsule brief generatedAt');
39
+ text(brief.target, 'Investigation capsule brief target', 253);
40
+ text(brief.targetType, 'Investigation capsule brief target type', 40);
41
+ enumeration(brief.task, ['general', 'acquisition', 'brand', 'incident', 'owned'], 'Investigation capsule brief task');
42
+ text(brief.taskLabel, 'Investigation capsule brief task label', 320);
43
+ text(brief.question, 'Investigation capsule brief question', 320);
44
+ text(brief.summary, 'Investigation capsule brief summary', 500);
45
+ const observation = exact(brief.observation, ['observedAt', 'evidenceAgeDays', 'completeSources', 'limitedSources', 'freshnessPolicy'], 'Investigation capsule observation');
46
+ iso(observation.observedAt, 'Investigation capsule observation time', true);
47
+ if (observation.evidenceAgeDays !== null)
48
+ integer(observation.evidenceAgeDays, 'Investigation capsule evidence age', 0, 1_000_000);
49
+ integer(observation.completeSources, 'Investigation capsule complete sources', 0, 100);
50
+ integer(observation.limitedSources, 'Investigation capsule limited sources', 0, 100);
51
+ const policy = exact(observation.freshnessPolicy, ['version', 'id', 'task', 'thresholdsDays'], 'Investigation capsule freshness policy');
52
+ if (policy.version !== 1)
53
+ fail('Investigation capsule freshness policy');
54
+ enumeration(policy.id, ['task-default', 'analyst-custom'], 'Investigation capsule freshness policy');
55
+ if (policy.task !== brief.task)
56
+ fail('Investigation capsule freshness policy');
57
+ const thresholds = exact(policy.thresholdsDays, ['registration', 'network', 'web'], 'Investigation capsule freshness thresholds');
58
+ for (const key of ['registration', 'network', 'web'])
59
+ integer(thresholds[key], 'Investigation capsule freshness threshold', 0, 3650);
60
+ array(brief.verifiedFacts, 'Investigation capsule verified facts', 12).forEach((item, index) => validateBriefFact(item, `Investigation capsule fact ${index + 1}`));
61
+ array(brief.contradictions, 'Investigation capsule contradictions', 24).forEach((item, index) => validateDecisionEntry(item, `Investigation capsule contradiction ${index + 1}`));
62
+ array(brief.unknowns, 'Investigation capsule unknowns', 24).forEach((item, index) => validateDecisionEntry(item, `Investigation capsule unknown ${index + 1}`));
63
+ for (const [index, candidate] of array(brief.nextActions, 'Investigation capsule next actions', 6).entries()) {
64
+ const action = exact(candidate, ['id', 'label', 'reason', 'expectedOutcome', 'href', 'priority'], `Investigation capsule next action ${index + 1}`);
65
+ text(action.id, 'Investigation capsule next action id', 80);
66
+ text(action.label, 'Investigation capsule next action label', 320);
67
+ text(action.reason, 'Investigation capsule next action reason', 500);
68
+ text(action.expectedOutcome, 'Investigation capsule next action outcome', 500);
69
+ if (typeof action.href !== 'string' || !/^#[^\u0000-\u001f\u007f]{1,160}$/u.test(action.href))
70
+ fail('Investigation capsule next action href');
71
+ enumeration(action.priority, ['high', 'medium', 'low'], 'Investigation capsule next action priority');
72
+ }
73
+ const relationships = exact(brief.relationships, ['nodes', 'edges', 'truncated', 'kinds'], 'Investigation capsule relationship summary');
74
+ integer(relationships.nodes, 'Investigation capsule relationship node count', 0, 72);
75
+ integer(relationships.edges, 'Investigation capsule relationship edge count', 0, 120);
76
+ boolean(relationships.truncated, 'Investigation capsule relationship truncation');
77
+ strings(relationships.kinds, 'Investigation capsule relationship kinds', 12, 320);
78
+ strings(brief.limitations, 'Investigation capsule brief limitations', 20, 320);
79
+ }
80
+ function validateProjectionCollection(value, label, totalMaximum, displayedMaximum, validateItem) {
81
+ const collection = exact(value, ['total', 'displayed', 'omitted', 'items'], label);
82
+ const total = integer(collection.total, `${label} total`, 0, totalMaximum);
83
+ const displayed = integer(collection.displayed, `${label} displayed`, 0, displayedMaximum);
84
+ const omitted = integer(collection.omitted, `${label} omitted`, 0, totalMaximum);
85
+ const items = array(collection.items, `${label} items`, displayedMaximum);
86
+ if (displayed > total || omitted !== total - displayed || items.length !== displayed)
87
+ fail(label);
88
+ items.forEach(validateItem);
89
+ return { total, displayed, omitted, items };
90
+ }
91
+ function validateSortedUnique(values, label) {
92
+ for (let index = 0; index < values.length; index += 1) {
93
+ if (index > 0 && values[index - 1] >= values[index])
94
+ fail(label);
95
+ }
96
+ }
97
+ function validateDecisionIdentifier(value, label) {
98
+ const identifier = text(value, label, 200);
99
+ if (!/^[a-z0-9](?:[a-z0-9._:-]{0,199})$/u.test(identifier))
100
+ fail(label);
101
+ return identifier;
102
+ }
103
+ function validateProjectedStringCollection(value, label, totalMaximum, displayedMaximum, textMaximum) {
104
+ const projection = validateProjectionCollection(value, label, totalMaximum, displayedMaximum, (item) => { text(item, `${label} item`, textMaximum); });
105
+ validateSortedUnique(projection.items, label);
106
+ return projection;
107
+ }
108
+ function validateDecisionFactSource(value, label) {
109
+ const source = exact(value, [
110
+ 'id',
111
+ 'label',
112
+ 'provenance',
113
+ 'evidenceState',
114
+ 'observedAt',
115
+ 'references',
116
+ 'limitations',
117
+ ], label);
118
+ const id = validateDecisionIdentifier(source.id, `${label} id`);
119
+ text(source.label, `${label} label`, 160);
120
+ enumeration(source.provenance, DECISION_FACT_PROVENANCE_STATES, `${label} provenance`);
121
+ enumeration(source.evidenceState, DECISION_FACT_EVIDENCE_STATES, `${label} evidence state`);
122
+ iso(source.observedAt, `${label} observation time`, true);
123
+ validateProjectedStringCollection(source.references, `${label} references`, MAX_DECISION_FACT_REFERENCES, MAX_DECISION_FACT_PROJECTION_SOURCE_REFERENCES, 200);
124
+ validateProjectedStringCollection(source.limitations, `${label} limitations`, MAX_DECISION_FACT_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_SOURCE_LIMITATIONS, 280);
125
+ return id;
126
+ }
127
+ function validateProjectedDecisionFact(value, label) {
128
+ const fact = exact(value, [
129
+ 'version',
130
+ 'id',
131
+ 'question',
132
+ 'conclusion',
133
+ 'importance',
134
+ 'evidenceState',
135
+ 'completeness',
136
+ 'freshness',
137
+ 'consistency',
138
+ 'dependencies',
139
+ 'sourceReferences',
140
+ 'sources',
141
+ 'contradictions',
142
+ 'limitations',
143
+ 'safeNextActions',
144
+ ], label);
145
+ if (fact.version !== DECISION_FACT_VERSION)
146
+ fail(`${label} version`);
147
+ const id = validateDecisionIdentifier(fact.id, `${label} id`);
148
+ text(fact.question, `${label} question`, 320);
149
+ text(fact.conclusion, `${label} conclusion`, 640);
150
+ enumeration(fact.importance, ['high', 'medium', 'low'], `${label} importance`);
151
+ const evidenceState = enumeration(fact.evidenceState, DECISION_FACT_EVIDENCE_STATES, `${label} evidence state`);
152
+ if (fact.completeness !== decisionFactCompleteness(evidenceState))
153
+ fail(`${label} completeness`);
154
+ enumeration(fact.freshness, DECISION_FACT_FRESHNESS_STATES, `${label} freshness`);
155
+ const consistency = enumeration(fact.consistency, DECISION_FACT_CONSISTENCY_STATES, `${label} consistency`);
156
+ const dependencies = validateProjectedStringCollection(fact.dependencies, `${label} dependencies`, MAX_DECISION_FACT_CONTRIBUTORS, MAX_DECISION_FACT_PROJECTION_SOURCES, 200);
157
+ dependencies.items.forEach((item) => validateDecisionIdentifier(item, `${label} dependency`));
158
+ validateProjectedStringCollection(fact.sourceReferences, `${label} source references`, MAX_DECISION_FACT_REFERENCES, MAX_DECISION_FACT_PROJECTION_REFERENCES, 200);
159
+ const sourceIds = [];
160
+ const sources = validateProjectionCollection(fact.sources, `${label} sources`, MAX_DECISION_FACT_CONTRIBUTORS, MAX_DECISION_FACT_PROJECTION_SOURCES, (item, index) => { sourceIds.push(validateDecisionFactSource(item, `${label} source ${index + 1}`)); });
161
+ validateSortedUnique(sourceIds, `${label} sources`);
162
+ if (dependencies.total !== sources.total
163
+ || dependencies.displayed !== sources.displayed
164
+ || dependencies.omitted !== sources.omitted
165
+ || !sameValues(dependencies.items, sourceIds))
166
+ fail(`${label} dependencies`);
167
+ validateProjectedStringCollection(fact.contradictions, `${label} contradictions`, MAX_DECISION_FACT_CONTRADICTIONS, MAX_DECISION_FACT_PROJECTION_CONTRADICTIONS, 640);
168
+ validateProjectedStringCollection(fact.limitations, `${label} limitations`, MAX_DECISION_FACT_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_LIMITATIONS, 280);
169
+ const actionIds = [];
170
+ validateProjectionCollection(fact.safeNextActions, `${label} safe next actions`, MAX_DECISION_FACT_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_NEXT_ACTIONS, (candidate, index) => {
171
+ const action = exact(candidate, ['id', 'label', 'reason', 'expectedOutcome', 'href', 'importance'], `${label} safe next action ${index + 1}`);
172
+ actionIds.push(validateDecisionIdentifier(action.id, `${label} safe next action id`));
173
+ text(action.label, `${label} safe next action label`, 160);
174
+ text(action.reason, `${label} safe next action reason`, 320);
175
+ text(action.expectedOutcome, `${label} safe next action outcome`, 320);
176
+ if (typeof action.href !== 'string' || !/^#[a-z0-9](?:[a-z0-9._:-]{0,159})$/u.test(action.href))
177
+ fail(`${label} safe next action href`);
178
+ enumeration(action.importance, ['high', 'medium', 'low'], `${label} safe next action importance`);
179
+ });
180
+ validateSortedUnique(actionIds, `${label} safe next actions`);
181
+ return { id, consistency };
182
+ }
183
+ function validateDecisionFactProjection(value) {
184
+ const projection = exact(value, [
185
+ 'version',
186
+ 'total',
187
+ 'displayed',
188
+ 'omitted',
189
+ 'contradictory',
190
+ 'unresolved',
191
+ 'facts',
192
+ ], 'Investigation capsule Decision Fact projection');
193
+ if (projection.version !== DECISION_FACT_PROJECTION_VERSION)
194
+ fail('Investigation capsule Decision Fact projection');
195
+ const total = integer(projection.total, 'Investigation capsule Decision Fact total', 0, MAX_DECISION_FACTS);
196
+ const displayed = integer(projection.displayed, 'Investigation capsule Decision Fact displayed', 0, MAX_DECISION_FACT_PROJECTION_FACTS);
197
+ const omitted = integer(projection.omitted, 'Investigation capsule Decision Fact omitted', 0, MAX_DECISION_FACTS);
198
+ const contradictory = integer(projection.contradictory, 'Investigation capsule Decision Fact contradictory', 0, total);
199
+ const unresolved = integer(projection.unresolved, 'Investigation capsule Decision Fact unresolved', 0, total);
200
+ const facts = array(projection.facts, 'Investigation capsule Decision Facts', MAX_DECISION_FACT_PROJECTION_FACTS);
201
+ if (displayed > total || omitted !== total - displayed || facts.length !== displayed
202
+ || contradictory + unresolved > total)
203
+ fail('Investigation capsule Decision Fact projection');
204
+ const factIds = [];
205
+ let displayedContradictory = 0;
206
+ let displayedUnresolved = 0;
207
+ for (const [index, candidate] of facts.entries()) {
208
+ const validated = validateProjectedDecisionFact(candidate, `Investigation capsule Decision Fact ${index + 1}`);
209
+ factIds.push(validated.id);
210
+ if (validated.consistency === 'contradictory')
211
+ displayedContradictory += 1;
212
+ if (validated.consistency === 'unknown')
213
+ displayedUnresolved += 1;
214
+ }
215
+ validateSortedUnique(factIds, 'Investigation capsule Decision Facts');
216
+ if (contradictory < displayedContradictory || unresolved < displayedUnresolved
217
+ || (omitted === 0 && (contradictory !== displayedContradictory || unresolved !== displayedUnresolved))) {
218
+ fail('Investigation capsule Decision Fact state counts');
219
+ }
220
+ if (new TextEncoder().encode(JSON.stringify(projection)).byteLength > MAX_DECISION_FACT_PROJECTION_BYTES) {
221
+ fail('Investigation capsule Decision Fact projection byte limit');
222
+ }
223
+ }
224
+ function validateCurrentBrief(value) {
225
+ const brief = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'target', 'targetType', 'task', 'taskLabel', 'question', 'summary', 'observation', 'decisionFacts', 'relationships', 'limitations'], 'Investigation capsule brief');
226
+ if (brief.schema !== LOOKUP_INVESTIGATION_BRIEF_SCHEMA
227
+ || brief.schemaVersion !== LOOKUP_INVESTIGATION_BRIEF_VERSION)
228
+ fail('Investigation capsule brief');
229
+ iso(brief.generatedAt, 'Investigation capsule brief generatedAt');
230
+ text(brief.target, 'Investigation capsule brief target', 253);
231
+ text(brief.targetType, 'Investigation capsule brief target type', 40);
232
+ enumeration(brief.task, ['general', 'acquisition', 'brand', 'incident', 'owned'], 'Investigation capsule brief task');
233
+ text(brief.taskLabel, 'Investigation capsule brief task label', 320);
234
+ text(brief.question, 'Investigation capsule brief question', 320);
235
+ text(brief.summary, 'Investigation capsule brief summary', 500);
236
+ const observation = exact(brief.observation, ['observedAt', 'evidenceAgeDays', 'completeSources', 'limitedSources', 'freshnessPolicy'], 'Investigation capsule observation');
237
+ iso(observation.observedAt, 'Investigation capsule observation time', true);
238
+ if (observation.evidenceAgeDays !== null)
239
+ integer(observation.evidenceAgeDays, 'Investigation capsule evidence age', 0, 1_000_000);
240
+ integer(observation.completeSources, 'Investigation capsule complete sources', 0, 100);
241
+ integer(observation.limitedSources, 'Investigation capsule limited sources', 0, 100);
242
+ const policy = exact(observation.freshnessPolicy, ['version', 'id', 'task', 'thresholdsDays'], 'Investigation capsule freshness policy');
243
+ if (policy.version !== 1)
244
+ fail('Investigation capsule freshness policy');
245
+ enumeration(policy.id, ['task-default', 'analyst-custom'], 'Investigation capsule freshness policy');
246
+ if (policy.task !== brief.task)
247
+ fail('Investigation capsule freshness policy');
248
+ const thresholds = exact(policy.thresholdsDays, ['registration', 'network', 'web'], 'Investigation capsule freshness thresholds');
249
+ for (const key of ['registration', 'network', 'web'])
250
+ integer(thresholds[key], 'Investigation capsule freshness threshold', 0, 3650);
251
+ validateDecisionFactProjection(brief.decisionFacts);
252
+ const relationships = exact(brief.relationships, ['nodes', 'edges', 'truncated', 'kinds'], 'Investigation capsule relationship summary');
253
+ integer(relationships.nodes, 'Investigation capsule relationship node count', 0, 72);
254
+ integer(relationships.edges, 'Investigation capsule relationship edge count', 0, 120);
255
+ boolean(relationships.truncated, 'Investigation capsule relationship truncation');
256
+ strings(relationships.kinds, 'Investigation capsule relationship kinds', 12, 320);
257
+ strings(brief.limitations, 'Investigation capsule brief limitations', 20, 320);
258
+ if (new TextEncoder().encode(JSON.stringify(brief)).byteLength > MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES) {
259
+ fail('Investigation capsule brief byte limit');
260
+ }
261
+ }
262
+ function validateGraph(value) {
263
+ const graph = exact(value, ['version', 'targetId', 'nodes', 'edges', 'sources', 'truncated', 'limitations'], 'Investigation capsule graph');
264
+ if (graph.version !== LOOKUP_ASSET_GRAPH_VERSION)
265
+ fail('Investigation capsule graph');
266
+ text(graph.targetId, 'Investigation capsule graph target', 160);
267
+ const nodes = array(graph.nodes, 'Investigation capsule graph nodes', 72, 1);
268
+ const nodeIds = new Set();
269
+ for (const candidate of nodes) {
270
+ const node = exact(candidate, ['id', 'label', 'kind', 'detail'], 'Investigation capsule graph node');
271
+ const id = text(node.id, 'Investigation capsule graph node id', 160);
272
+ if (nodeIds.has(id))
273
+ fail('Investigation capsule graph node ids');
274
+ nodeIds.add(id);
275
+ text(node.label, 'Investigation capsule graph node label', 320);
276
+ enumeration(node.kind, ['address', 'certificate', 'hostname', 'identity', 'issuer', 'key', 'network', 'observation', 'origin', 'prefix', 'registrar', 'target', 'tracker'], 'Investigation capsule graph node kind');
277
+ text(node.detail, 'Investigation capsule graph node detail', 500, true);
278
+ }
279
+ if (!nodeIds.has(graph.targetId))
280
+ fail('Investigation capsule graph target');
281
+ const sourceIds = new Set();
282
+ for (const candidate of array(graph.sources, 'Investigation capsule graph sources', 32)) {
283
+ const source = exact(candidate, ['id', 'label', 'href', 'observedAt', 'completeness', 'limitations'], 'Investigation capsule graph source');
284
+ const id = text(source.id, 'Investigation capsule graph source id', 160);
285
+ if (sourceIds.has(id))
286
+ fail('Investigation capsule graph source ids');
287
+ sourceIds.add(id);
288
+ text(source.label, 'Investigation capsule graph source label', 320);
289
+ if (typeof source.href !== 'string' || !/^#[^\u0000-\u001f\u007f]{1,160}$/u.test(source.href))
290
+ fail('Investigation capsule graph source href');
291
+ iso(source.observedAt, 'Investigation capsule graph source observedAt', true);
292
+ enumeration(source.completeness, ['complete', 'partial', 'unknown'], 'Investigation capsule graph source completeness');
293
+ strings(source.limitations, 'Investigation capsule graph source limitations', 5, 320);
294
+ }
295
+ const edges = array(graph.edges, 'Investigation capsule graph edges', 120);
296
+ const edgeIds = new Set();
297
+ for (const candidate of edges) {
298
+ const edge = exactOptional(candidate, ['id', 'sourceId', 'source', 'target', 'kind', 'label', 'sourceLabel', 'observedAt', 'completeness', 'limitations', 'lenses', 'href'], ['boundary'], 'Investigation capsule graph edge');
299
+ const id = text(edge.id, 'Investigation capsule graph edge id', 160);
300
+ if (edgeIds.has(id))
301
+ fail('Investigation capsule graph edge ids');
302
+ edgeIds.add(id);
303
+ const sourceId = text(edge.sourceId, 'Investigation capsule graph edge source id', 160);
304
+ if (!sourceIds.has(sourceId))
305
+ fail('Investigation capsule graph edge source');
306
+ if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target))
307
+ fail('Investigation capsule graph edge endpoints');
308
+ text(edge.kind, 'Investigation capsule graph edge kind', 160);
309
+ text(edge.label, 'Investigation capsule graph edge label', 320);
310
+ text(edge.sourceLabel, 'Investigation capsule graph edge source label', 320);
311
+ iso(edge.observedAt, 'Investigation capsule graph edge observedAt', true);
312
+ enumeration(edge.completeness, ['complete', 'partial', 'unknown'], 'Investigation capsule graph edge completeness');
313
+ strings(edge.limitations, 'Investigation capsule graph edge limitations', 5, 320);
314
+ strings(edge.lenses, 'Investigation capsule graph edge lenses', 4, 40).forEach((lens) => enumeration(lens, ['all', 'identity', 'delegation', 'certificate'], 'Investigation capsule graph edge lens'));
315
+ if (typeof edge.href !== 'string' || !/^#[^\u0000-\u001f\u007f]{1,160}$/u.test(edge.href))
316
+ fail('Investigation capsule graph edge href');
317
+ if (edge.boundary !== undefined)
318
+ enumeration(edge.boundary, ['external', 'reviewed_profile', 'same_origin', 'same_registrable_domain', 'unresolved'], 'Investigation capsule graph edge boundary');
319
+ }
320
+ boolean(graph.truncated, 'Investigation capsule graph truncation');
321
+ strings(graph.limitations, 'Investigation capsule graph limitations', 20, 320);
322
+ }
323
+ function validateAnalystRecords(value) {
324
+ if (value === null)
325
+ return;
326
+ const records = exact(value, ['caseId', 'status', 'disposition', 'decisions', 'assertions'], 'Investigation capsule analyst records');
327
+ text(records.caseId, 'Investigation capsule case id', 128);
328
+ text(records.status, 'Investigation capsule case status', 80);
329
+ text(records.disposition, 'Investigation capsule case disposition', 80);
330
+ for (const candidate of array(records.decisions, 'Investigation capsule decisions', MAX_CASE_DECISIONS)) {
331
+ const decision = exact(candidate, ['id', 'summary', 'rationale', 'evidencePinIds', 'createdAt'], 'Investigation capsule decision');
332
+ text(decision.id, 'Investigation capsule decision id', 64);
333
+ text(decision.summary, 'Investigation capsule decision summary', 500);
334
+ text(decision.rationale, 'Investigation capsule decision rationale', 2_000, true);
335
+ strings(decision.evidencePinIds, 'Investigation capsule decision evidence pins', MAX_DECISION_PIN_REFERENCES, 64);
336
+ iso(decision.createdAt, 'Investigation capsule decision createdAt');
337
+ }
338
+ for (const candidate of array(records.assertions, 'Investigation capsule assertions', MAX_CASE_ASSERTIONS)) {
339
+ const assertion = exact(candidate, ['id', 'kind', 'statement', 'rationale', 'evidencePinIds', 'state', 'createdAt', 'updatedAt'], 'Investigation capsule assertion');
340
+ text(assertion.id, 'Investigation capsule assertion id', 64);
341
+ text(assertion.kind, 'Investigation capsule assertion kind', 80);
342
+ text(assertion.statement, 'Investigation capsule assertion statement', 2_000);
343
+ optionalText(assertion.rationale, 'Investigation capsule assertion rationale', 2_000);
344
+ strings(assertion.evidencePinIds, 'Investigation capsule assertion evidence pins', MAX_DECISION_PIN_REFERENCES, 64);
345
+ text(assertion.state, 'Investigation capsule assertion state', 80);
346
+ iso(assertion.createdAt, 'Investigation capsule assertion createdAt');
347
+ iso(assertion.updatedAt, 'Investigation capsule assertion updatedAt');
348
+ }
349
+ }
350
+ export function validateInvestigationCapsuleStructure(value) {
351
+ const root = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'application', 'target', 'sourceContracts', 'investigationBrief', 'graphSnapshot', 'analystRecords', 'integrity', 'limitations'], 'Investigation capsule');
352
+ if (root.schemaVersion !== PUBLIC_INVESTIGATION_CAPSULE_VERSION
353
+ && root.schemaVersion !== INVESTIGATION_CAPSULE_VERSION)
354
+ fail('Investigation capsule');
355
+ iso(root.generatedAt, 'Investigation capsule generatedAt');
356
+ const application = exact(root.application, ['name', 'version'], 'Investigation capsule application');
357
+ if (application.name !== 'WHOISleuth' || typeof application.version !== 'string'
358
+ || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(application.version))
359
+ fail('Investigation capsule application');
360
+ const target = exact(root.target, ['value', 'type'], 'Investigation capsule target');
361
+ text(target.value, 'Investigation capsule target value', 253);
362
+ text(target.type, 'Investigation capsule target type', 40);
363
+ const contracts = array(root.sourceContracts, 'Investigation capsule source contracts', 4, 3);
364
+ const byId = new Map();
365
+ for (const candidate of contracts) {
366
+ const contract = exact(candidate, ['id', 'schema', 'version', 'digest', 'embedded'], 'Investigation capsule source contract');
367
+ const id = enumeration(contract.id, ['lookup-evidence', 'investigation-brief', 'asset-graph', 'analyst-records'], 'Investigation capsule source contract id');
368
+ if (byId.has(id))
369
+ fail('Investigation capsule source contracts');
370
+ byId.set(id, contract);
371
+ text(contract.schema, 'Investigation capsule source schema', 120);
372
+ integer(contract.version, 'Investigation capsule source version', 0, 10_000);
373
+ digest(contract.digest, 'Investigation capsule source digest');
374
+ boolean(contract.embedded, 'Investigation capsule embedded source');
375
+ }
376
+ const expectedContractIds = root.analystRecords === null
377
+ ? ['lookup-evidence', 'investigation-brief', 'asset-graph']
378
+ : ['lookup-evidence', 'investigation-brief', 'asset-graph', 'analyst-records'];
379
+ const expectedBriefVersion = root.schemaVersion === INVESTIGATION_CAPSULE_VERSION
380
+ ? LOOKUP_INVESTIGATION_BRIEF_VERSION
381
+ : PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION;
382
+ if (!sameValues(contracts.map((candidate) => candidate.id), expectedContractIds))
383
+ fail('Investigation capsule source contracts');
384
+ if (!byId.has('lookup-evidence') || !byId.has('investigation-brief') || !byId.has('asset-graph')
385
+ || byId.get('lookup-evidence')?.embedded !== false
386
+ || byId.get('investigation-brief')?.schema !== LOOKUP_INVESTIGATION_BRIEF_SCHEMA
387
+ || byId.get('investigation-brief')?.version !== expectedBriefVersion
388
+ || byId.get('investigation-brief')?.embedded !== true
389
+ || byId.get('asset-graph')?.schema !== LOOKUP_ASSET_GRAPH_SCHEMA
390
+ || byId.get('asset-graph')?.version !== LOOKUP_ASSET_GRAPH_VERSION
391
+ || byId.get('asset-graph')?.embedded !== true
392
+ || (byId.has('analyst-records') && (byId.get('analyst-records')?.schema !== INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA
393
+ || byId.get('analyst-records')?.version !== INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION
394
+ || byId.get('analyst-records')?.embedded !== true)))
395
+ fail('Investigation capsule source contracts');
396
+ if (root.schemaVersion === INVESTIGATION_CAPSULE_VERSION)
397
+ validateCurrentBrief(root.investigationBrief);
398
+ else
399
+ validatePublicBrief(root.investigationBrief);
400
+ validateGraph(root.graphSnapshot);
401
+ validateAnalystRecords(root.analystRecords);
402
+ const brief = root.investigationBrief;
403
+ const graph = root.graphSnapshot;
404
+ if (target.value !== brief.target || target.type !== brief.targetType
405
+ || graph.targetId === undefined
406
+ || brief.relationships.nodes !== graph.nodes.length
407
+ || brief.relationships.edges !== graph.edges.length)
408
+ fail('Investigation capsule projection linkage');
409
+ const integrity = exact(root.integrity, ['algorithm', 'canonicalization', 'scope', 'briefDigest', 'graphDigest', 'analystRecordsDigest', 'digestSha256'], 'Investigation capsule integrity');
410
+ if (integrity.algorithm !== 'SHA-256')
411
+ fail('Investigation capsule integrity');
412
+ if (integrity.canonicalization !== 'sorted-json-v2' || integrity.scope !== 'capsule excluding integrity')
413
+ fail('Investigation capsule integrity');
414
+ digest(integrity.digestSha256, 'Investigation capsule digest');
415
+ digest(integrity.briefDigest, 'Investigation capsule brief digest');
416
+ digest(integrity.graphDigest, 'Investigation capsule graph digest');
417
+ if (integrity.analystRecordsDigest !== null)
418
+ digest(integrity.analystRecordsDigest, 'Investigation capsule analyst digest');
419
+ if (byId.get('investigation-brief')?.digest !== integrity.briefDigest
420
+ || byId.get('asset-graph')?.digest !== integrity.graphDigest
421
+ || (root.analystRecords === null) !== (integrity.analystRecordsDigest === null)
422
+ || (root.analystRecords === null) !== !byId.has('analyst-records')
423
+ || (byId.get('analyst-records')?.digest ?? null) !== integrity.analystRecordsDigest)
424
+ fail('Investigation capsule digest linkage');
425
+ strings(root.limitations, 'Investigation capsule limitations', 8, 600);
426
+ }