@spottoai/types-package 1.0.2-beta.354 → 1.0.2-beta.356

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 (154) hide show
  1. package/README.md +11 -0
  2. package/dist/aws/commitments-planning.d.ts +3 -0
  3. package/dist/aws/commitments-planning.d.ts.map +1 -0
  4. package/dist/aws/commitments-planning.js +6 -0
  5. package/dist/aws/commitments-planning.js.map +1 -0
  6. package/dist/aws/relationships.d.ts +4 -0
  7. package/dist/aws/relationships.d.ts.map +1 -0
  8. package/dist/aws/relationships.js +6 -0
  9. package/dist/aws/relationships.js.map +1 -0
  10. package/dist/esm/accounts/accounts.js +115 -0
  11. package/dist/esm/accounts/azureSpSetup.js +1 -0
  12. package/dist/esm/accounts/azureSpSetupDurable.js +1 -0
  13. package/dist/esm/accounts/index.js +5 -0
  14. package/dist/esm/accounts/readPermissions.js +198 -0
  15. package/dist/esm/accounts/validation.js +1 -0
  16. package/dist/esm/accounts/writePermissions.js +43 -0
  17. package/dist/esm/ai/index.js +2 -0
  18. package/dist/esm/assessments/artifact.js +1 -0
  19. package/dist/esm/assessments/catalogue.js +12 -0
  20. package/dist/esm/assessments/common.js +1 -0
  21. package/dist/esm/assessments/evidence.js +1 -0
  22. package/dist/esm/assessments/export.js +1 -0
  23. package/dist/esm/assessments/index.js +10 -0
  24. package/dist/esm/assessments/queue.js +2 -0
  25. package/dist/esm/assessments/review.js +1 -0
  26. package/dist/esm/assessments/run.js +1 -0
  27. package/dist/esm/assessments/sow.js +1 -0
  28. package/dist/esm/assessments/status.js +23 -0
  29. package/dist/esm/aws/commitments-planning.js +1 -0
  30. package/dist/esm/aws/commitmentsPlanningValidation.js +121 -0
  31. package/dist/esm/aws/estates.js +21 -0
  32. package/dist/esm/aws/index.js +18 -0
  33. package/dist/esm/aws/onboarding.js +1 -0
  34. package/dist/esm/aws/pluginPublicArtifactBodyValidation.js +217 -0
  35. package/dist/esm/aws/pluginPublicArtifactEvidence.js +1 -0
  36. package/dist/esm/aws/pluginPublicArtifactManifestValidation.js +255 -0
  37. package/dist/esm/aws/pluginPublicArtifactSectionValidation.js +464 -0
  38. package/dist/esm/aws/pluginPublicArtifactValidation.js +2 -0
  39. package/dist/esm/aws/pluginPublicArtifactValidationHelpers.js +216 -0
  40. package/dist/esm/aws/pluginPublicArtifacts.js +109 -0
  41. package/dist/esm/aws/portalCommitmentsPlanningPublicArtifactValidation.js +509 -0
  42. package/dist/esm/aws/portalCommitmentsPlanningPublicArtifacts.js +1 -0
  43. package/dist/esm/aws/portalPublicArtifactCurrentValidation.js +328 -0
  44. package/dist/esm/aws/portalPublicArtifactEvidenceValidation.js +294 -0
  45. package/dist/esm/aws/portalPublicArtifactHistoryAiValidation.js +368 -0
  46. package/dist/esm/aws/portalPublicArtifactHistoryTypes.js +40 -0
  47. package/dist/esm/aws/portalPublicArtifactNestedEvidence.js +1 -0
  48. package/dist/esm/aws/portalPublicArtifactNestedEvidenceValidation.js +397 -0
  49. package/dist/esm/aws/portalPublicArtifactValidation.js +2 -0
  50. package/dist/esm/aws/portalPublicArtifactValidationCommon.js +139 -0
  51. package/dist/esm/aws/portalPublicArtifacts.js +73 -0
  52. package/dist/esm/aws/portalRelationshipPublicArtifactValidation.js +334 -0
  53. package/dist/esm/aws/portalRelationshipPublicArtifacts.js +59 -0
  54. package/dist/esm/aws/portalRelationshipResourceNodeTypes.js +1 -0
  55. package/dist/esm/aws/portalRelationshipResourceNodeValidation.js +368 -0
  56. package/dist/esm/aws/publicArtifacts.js +13 -0
  57. package/dist/esm/aws/relationships.js +1 -0
  58. package/dist/esm/aws/requests.js +19 -0
  59. package/dist/esm/azure/activityLogs.js +2 -0
  60. package/dist/esm/azure/advisorScore.js +1 -0
  61. package/dist/esm/azure/benefits.js +1 -0
  62. package/dist/esm/azure/billingGeneration.js +1 -0
  63. package/dist/esm/azure/billingPlots.js +4 -0
  64. package/dist/esm/azure/budgets.js +1 -0
  65. package/dist/esm/azure/commitmentsPlanning.js +1 -0
  66. package/dist/esm/azure/common.js +1 -0
  67. package/dist/esm/azure/configs.js +1 -0
  68. package/dist/esm/azure/costComposition.js +1 -0
  69. package/dist/esm/azure/dataProtection.js +5 -0
  70. package/dist/esm/azure/governance.js +14 -0
  71. package/dist/esm/azure/licensing.js +1 -0
  72. package/dist/esm/azure/metrics.js +1 -0
  73. package/dist/esm/azure/payloads.js +1 -0
  74. package/dist/esm/azure/portalArtifacts.js +1 -0
  75. package/dist/esm/azure/prices.js +1 -0
  76. package/dist/esm/azure/publicIpAddresses.js +2 -0
  77. package/dist/esm/azure/recommendationAudit.js +16 -0
  78. package/dist/esm/azure/recommendationState.js +1 -0
  79. package/dist/esm/azure/recommendationTracks.js +40 -0
  80. package/dist/esm/azure/recommendationWorkflow.js +13 -0
  81. package/dist/esm/azure/recommendations.js +10 -0
  82. package/dist/esm/azure/regulatoryCompliance.js +6 -0
  83. package/dist/esm/azure/relationships.js +2 -0
  84. package/dist/esm/azure/reportingTemplates.js +1 -0
  85. package/dist/esm/azure/reports.js +12 -0
  86. package/dist/esm/azure/resourceHealth.js +12 -0
  87. package/dist/esm/azure/resourceOptimization.js +1 -0
  88. package/dist/esm/azure/resources.js +1 -0
  89. package/dist/esm/azure/reviewChecklist.js +1 -0
  90. package/dist/esm/azure/secureScore.js +1 -0
  91. package/dist/esm/azure/security.js +1 -0
  92. package/dist/esm/azure/serviceRetirement.js +1 -0
  93. package/dist/esm/azure/serviceRetirementState.js +1 -0
  94. package/dist/esm/azure/serviceRetirementWorkManagement.js +1 -0
  95. package/dist/esm/azure/storage-inventory.js +1 -0
  96. package/dist/esm/azure/subscriptions.js +1 -0
  97. package/dist/esm/azure/views.js +32 -0
  98. package/dist/esm/azure/workManagement.js +4 -0
  99. package/dist/esm/common/artifactGeneration.js +1 -0
  100. package/dist/esm/common/capabilityPassport.js +214 -0
  101. package/dist/esm/common/index.js +7 -0
  102. package/dist/esm/common/pagination.js +1 -0
  103. package/dist/esm/common/provider.js +11 -0
  104. package/dist/esm/common/providerSyncProgress.js +1 -0
  105. package/dist/esm/common/relationships.js +1 -0
  106. package/dist/esm/common/syncProgress.js +1 -0
  107. package/dist/esm/company/billingAccount.js +1 -0
  108. package/dist/esm/company/company.js +1 -0
  109. package/dist/esm/company/companyHierarchy.js +1 -0
  110. package/dist/esm/company/companyNotes.js +38 -0
  111. package/dist/esm/company/customProperties.js +1 -0
  112. package/dist/esm/company/index.js +6 -0
  113. package/dist/esm/company/notification.js +43 -0
  114. package/dist/esm/company/survey.js +1 -0
  115. package/dist/esm/customProperties/customProperties.js +1 -0
  116. package/dist/esm/customProperties/index.js +1 -0
  117. package/dist/esm/entries/aws.js +3 -0
  118. package/dist/esm/entries/commitments-planning.js +3 -0
  119. package/dist/esm/entries/provider.js +3 -0
  120. package/dist/esm/entries/recommendations.js +3 -0
  121. package/dist/esm/entries/relationships.js +3 -0
  122. package/dist/esm/entries/root.js +3 -0
  123. package/dist/esm/events/actionGroups.js +1 -0
  124. package/dist/esm/events/alertDefinitionRun.js +1 -0
  125. package/dist/esm/events/baseAlert.js +1 -0
  126. package/dist/esm/events/cost.js +1 -0
  127. package/dist/esm/events/costAnomalies.js +1 -0
  128. package/dist/esm/events/costViews.js +1 -0
  129. package/dist/esm/events/eventStatus.js +1 -0
  130. package/dist/esm/events/index.js +8 -0
  131. package/dist/esm/events/quickAlerts.js +2 -0
  132. package/dist/esm/features-and-permissions/access.js +1 -0
  133. package/dist/esm/features-and-permissions/index.js +1 -0
  134. package/dist/esm/feedbacks/feedback.js +1 -0
  135. package/dist/esm/identity/apikey.js +1 -0
  136. package/dist/esm/identity/index.js +1 -0
  137. package/dist/esm/index.js +54 -0
  138. package/dist/esm/index.vite.js +48 -0
  139. package/dist/esm/integrations/autotask.js +1 -0
  140. package/dist/esm/integrations/azureDevOps.js +1 -0
  141. package/dist/esm/integrations/github.js +1 -0
  142. package/dist/esm/integrations/halo.js +1 -0
  143. package/dist/esm/integrations/index.js +5 -0
  144. package/dist/esm/integrations/integrationTickets.js +1 -0
  145. package/dist/esm/package.json +3 -0
  146. package/dist/esm/scheduler/index.js +1 -0
  147. package/dist/esm/scheduler/scheduler.js +1 -0
  148. package/dist/esm/tags/index.js +1 -0
  149. package/dist/esm/tags/tags.js +6 -0
  150. package/dist/esm/unknown/services.js +1 -0
  151. package/dist/esm/users/companyUser.js +2 -0
  152. package/dist/esm/users/index.js +2 -0
  153. package/dist/esm/users/user.js +1 -0
  154. package/package.json +23 -6
@@ -0,0 +1,334 @@
1
+ import { AWS_PORTAL_RELATIONSHIP_DISCOVERY_FAMILIES, AWS_PORTAL_RELATIONSHIP_LOGICAL_NAME, AWS_PORTAL_RELATIONSHIP_SCHEMA_VERSION, AWS_PORTAL_RELATIONSHIP_SYNTHETIC_TYPES, AWS_PORTAL_RELATIONSHIP_TYPES, } from './portalRelationshipPublicArtifacts.js';
2
+ import { validateAwsPortalRelationshipResourceNode } from './portalRelationshipResourceNodeValidation.js';
3
+ import { validateAwsPortalBillingBlock } from './portalPublicArtifactEvidenceValidation.js';
4
+ import { asRecord, assertExactKeys, assertPublicJson, assertUnique, assertValue, isoTimestamp, nonNegativeInteger, regionArray, requiredBoolean, requiredEnum, requiredString, validateGeneration, validatePortalEnvelope, validateResourceScope, } from './portalPublicArtifactValidationCommon.js';
5
+ import { assertAccount, sha256 } from './pluginPublicArtifactValidationHelpers.js';
6
+ const CURRENT_KEYS = [
7
+ 'schemaVersion',
8
+ 'portalSchemaVersion',
9
+ 'relationshipSchemaVersion',
10
+ 'provider',
11
+ 'accountId',
12
+ 'artifactType',
13
+ 'artifactGeneration',
14
+ 'generatedAt',
15
+ 'logicalName',
16
+ 'source',
17
+ 'scope',
18
+ 'currency',
19
+ 'currencySymbol',
20
+ 'nodes',
21
+ 'edges',
22
+ 'unresolved',
23
+ 'coverage',
24
+ 'costOverlay',
25
+ 'stats',
26
+ ];
27
+ /** Validates one untrusted, lossless AWS relationship Portal artifact. */
28
+ export function validateAwsPortalRelationshipArtifact(value) {
29
+ const artifact = asRecord(value, 'artifact');
30
+ const { accountId, portalGeneratedAt, bodyGeneratedAt } = validatePortalEnvelope(artifact, 'relationships', CURRENT_KEYS);
31
+ assertValue(artifact.relationshipSchemaVersion, AWS_PORTAL_RELATIONSHIP_SCHEMA_VERSION, 'artifact.relationshipSchemaVersion');
32
+ assertValue(artifact.logicalName, AWS_PORTAL_RELATIONSHIP_LOGICAL_NAME, 'artifact.logicalName');
33
+ validateSource(artifact.source, bodyGeneratedAt, portalGeneratedAt);
34
+ const regions = validateScope(artifact.scope, accountId);
35
+ const regionSet = new Set(regions);
36
+ if (artifact.currency !== undefined)
37
+ requiredString(artifact.currency, 'artifact.currency');
38
+ if (artifact.currencySymbol !== undefined)
39
+ requiredString(artifact.currencySymbol, 'artifact.currencySymbol');
40
+ const nodes = validateNodes(artifact.nodes, accountId, regions, regionSet);
41
+ validateEdges(artifact.edges, nodes);
42
+ validateUnresolved(artifact.unresolved, nodes);
43
+ validateCoverage(artifact.coverage, regions, portalGeneratedAt);
44
+ const matchedBillingNodeCount = artifact.costOverlay === undefined
45
+ ? undefined
46
+ : validateCostOverlay(artifact.costOverlay, accountId, regions, portalGeneratedAt, nodes.resourceCount);
47
+ if (nodes.billingNodeCount > 0 && matchedBillingNodeCount === undefined)
48
+ throw new Error('artifact.costOverlay is required when resource nodes contain billing.');
49
+ if (matchedBillingNodeCount !== undefined && matchedBillingNodeCount !== nodes.billingNodeCount)
50
+ throw new Error('artifact.costOverlay matched count must equal resource nodes containing billing.');
51
+ validateStats(artifact.stats, asArray(artifact.nodes, 'artifact.nodes').length, asArray(artifact.edges, 'artifact.edges').length, asArray(artifact.unresolved, 'artifact.unresolved').length);
52
+ assertPublicJson(artifact, 'artifact');
53
+ return value;
54
+ }
55
+ function validateSource(value, bodyGeneratedAt, portalGeneratedAt) {
56
+ const source = asRecord(value, 'artifact.source');
57
+ assertExactKeys(source, ['artifactType', 'artifactVersion', 'generatedAt'], 'artifact.source');
58
+ assertValue(source.artifactType, 'relationship-graph', 'artifact.source.artifactType');
59
+ assertValue(source.artifactVersion, 1, 'artifact.source.artifactVersion');
60
+ const generatedAt = isoTimestamp(source.generatedAt, 'artifact.source.generatedAt');
61
+ assertValue(generatedAt, bodyGeneratedAt, 'artifact.source.generatedAt');
62
+ if (Date.parse(generatedAt) > Date.parse(portalGeneratedAt))
63
+ throw new Error('artifact.source generation cannot exceed Portal output.');
64
+ }
65
+ function validateScope(value, accountId) {
66
+ const scope = asRecord(value, 'artifact.scope');
67
+ assertExactKeys(scope, ['provider', 'accountId', 'resourceRegions'], 'artifact.scope');
68
+ assertValue(scope.provider, 'aws', 'artifact.scope.provider');
69
+ assertAccount(scope.accountId, accountId, 'artifact.scope.accountId');
70
+ return regionArray(scope.resourceRegions, 'artifact.scope.resourceRegions');
71
+ }
72
+ function validateNodes(value, accountId, regions, regionSet) {
73
+ const nodes = asArray(value, 'artifact.nodes');
74
+ const byId = new Map();
75
+ const regionNodes = [];
76
+ let accountNodeCount = 0;
77
+ let resourceCount = 0;
78
+ let billingNodeCount = 0;
79
+ nodes.forEach((entry, index) => {
80
+ const field = `artifact.nodes[${index}]`;
81
+ const node = asRecord(entry, field);
82
+ assertExactKeys(node, ['id', 'kind', 'data'], field);
83
+ const id = requiredString(node.id, `${field}.id`);
84
+ if (byId.has(id))
85
+ throw new Error('artifact node ids must be unique.');
86
+ const kind = requiredEnum(node.kind, ['account', 'region', 'resource', 'synthetic'], `${field}.kind`);
87
+ const data = asRecord(node.data, `${field}.data`);
88
+ validateNodeBinding(data, accountId, `${field}.data`);
89
+ if (kind === 'account') {
90
+ accountNodeCount += 1;
91
+ assertExactKeys(data, ['provider', 'accountId', 'displayName'], `${field}.data`);
92
+ if (data.displayName !== undefined)
93
+ requiredString(data.displayName, `${field}.data.displayName`);
94
+ }
95
+ else if (kind === 'region') {
96
+ assertExactKeys(data, ['provider', 'accountId', 'resourceRegion', 'displayName'], `${field}.data`);
97
+ const region = validateNodeRegion(data, regionSet, `${field}.data`);
98
+ regionNodes.push(region);
99
+ if (data.displayName !== undefined)
100
+ requiredString(data.displayName, `${field}.data.displayName`);
101
+ }
102
+ else if (kind === 'synthetic') {
103
+ assertExactKeys(data, ['provider', 'accountId', 'resourceRegion', 'syntheticType', 'identifier', 'displayName'], `${field}.data`);
104
+ const resourceRegion = validateNodeRegion(data, regionSet, `${field}.data`);
105
+ requiredEnum(data.syntheticType, AWS_PORTAL_RELATIONSHIP_SYNTHETIC_TYPES, `${field}.data.syntheticType`);
106
+ requiredString(data.identifier, `${field}.data.identifier`);
107
+ if (data.displayName !== undefined)
108
+ requiredString(data.displayName, `${field}.data.displayName`);
109
+ byId.set(id, { kind, resourceRegion });
110
+ return;
111
+ }
112
+ else {
113
+ validateAwsPortalRelationshipResourceNode(data, accountId, regionSet, `${field}.data`);
114
+ resourceCount += 1;
115
+ if (data.billing !== undefined)
116
+ billingNodeCount += 1;
117
+ byId.set(id, { kind, resourceRegion: String(data.resourceRegion) });
118
+ return;
119
+ }
120
+ byId.set(id, kind === 'region' ? { kind, resourceRegion: String(data.resourceRegion) } : { kind });
121
+ });
122
+ if (accountNodeCount !== 1)
123
+ throw new Error('artifact must contain exactly one account node.');
124
+ assertUnique(regionNodes, 'artifact Region nodes');
125
+ assertExactRegionSet(regionNodes, regions, 'artifact Region nodes');
126
+ return { byId, resourceCount, billingNodeCount };
127
+ }
128
+ function validateNodeBinding(data, accountId, field) {
129
+ assertValue(data.provider, 'aws', `${field}.provider`);
130
+ assertAccount(data.accountId, accountId, `${field}.accountId`);
131
+ }
132
+ function validateNodeRegion(data, regions, field) {
133
+ const region = requiredString(data.resourceRegion, `${field}.resourceRegion`);
134
+ if (!regions.has(region))
135
+ throw new Error(`${field}.resourceRegion is outside artifact scope.`);
136
+ return region;
137
+ }
138
+ const STRUCTURAL_RELATIONSHIPS = {
139
+ 'account-region': ['region', 'account'],
140
+ 'region-resource': ['resource', 'region'],
141
+ 'region-topology': ['synthetic', 'region'],
142
+ };
143
+ function validateEdges(value, nodes) {
144
+ const edges = asArray(value, 'artifact.edges');
145
+ const ids = new Set();
146
+ const structuralParents = new Map();
147
+ edges.forEach((entry, index) => {
148
+ const field = `artifact.edges[${index}]`;
149
+ const edge = asRecord(entry, field);
150
+ assertExactKeys(edge, ['id', 'from', 'to', 'kind', 'relationshipTypes', 'confidence', 'evidence'], field);
151
+ const id = requiredString(edge.id, `${field}.id`);
152
+ if (ids.has(id))
153
+ throw new Error('artifact edge ids must be unique.');
154
+ ids.add(id);
155
+ const from = requiredString(edge.from, `${field}.from`);
156
+ const to = requiredString(edge.to, `${field}.to`);
157
+ const fromNode = nodes.byId.get(from);
158
+ const toNode = nodes.byId.get(to);
159
+ if (!fromNode || !toNode)
160
+ throw new Error(`${field} references a missing node.`);
161
+ if (from === to)
162
+ throw new Error(`${field} must not be a self edge.`);
163
+ const kind = requiredEnum(edge.kind, ['contains', 'depends_on'], `${field}.kind`);
164
+ const relationshipTypes = nonEmptyStringEnumArray(edge.relationshipTypes, AWS_PORTAL_RELATIONSHIP_TYPES, `${field}.relationshipTypes`);
165
+ assertValue(edge.confidence, 'high', `${field}.confidence`);
166
+ validateEdgeEvidence(edge.evidence, `${field}.evidence`);
167
+ const structuralTypes = relationshipTypes.filter(isStructuralRelationship);
168
+ if (structuralTypes.length > 0) {
169
+ if (kind !== 'contains' || structuralTypes.length !== relationshipTypes.length)
170
+ throw new Error(`${field} structural relationship types require a contains edge.`);
171
+ structuralTypes.forEach(type => validateContainmentDirection(type, fromNode.kind, toNode.kind, field));
172
+ structuralParents.set(from, (structuralParents.get(from) ?? 0) + 1);
173
+ }
174
+ else {
175
+ if (kind !== 'depends_on')
176
+ throw new Error(`${field} dependency relationship types require a depends_on edge.`);
177
+ if (fromNode.kind !== 'resource' || (toNode.kind !== 'resource' && toNode.kind !== 'synthetic'))
178
+ throw new Error(`${field} dependency edges must connect a resource to a resource or synthetic node.`);
179
+ if (fromNode.resourceRegion !== toNode.resourceRegion)
180
+ throw new Error(`${field} dependency edge crosses resource Regions.`);
181
+ }
182
+ });
183
+ nodes.byId.forEach((node, id) => {
184
+ const expected = node.kind === 'account' ? 0 : 1;
185
+ if ((structuralParents.get(id) ?? 0) !== expected)
186
+ throw new Error(`artifact node ${id} must have exactly ${expected} structural parent edge${expected === 1 ? '' : 's'}.`);
187
+ });
188
+ }
189
+ function isStructuralRelationship(value) {
190
+ return value in STRUCTURAL_RELATIONSHIPS;
191
+ }
192
+ function validateEdgeEvidence(value, field) {
193
+ const evidence = asArray(value, field, true);
194
+ evidence.forEach((entry, index) => {
195
+ const itemField = `${field}[${index}]`;
196
+ const item = asRecord(entry, itemField);
197
+ assertExactKeys(item, ['method', 'sourceFamily', 'field', 'matchedValue'], itemField);
198
+ requiredEnum(item.method, ['field-derived', 'field-reference', 'persisted-inventory', 'request-scope'], `${itemField}.method`);
199
+ requiredEnum(item.sourceFamily, [...AWS_PORTAL_RELATIONSHIP_DISCOVERY_FAMILIES, 'graph-scope'], `${itemField}.sourceFamily`);
200
+ if (item.field !== undefined)
201
+ requiredString(item.field, `${itemField}.field`);
202
+ if (item.matchedValue !== undefined)
203
+ requiredString(item.matchedValue, `${itemField}.matchedValue`);
204
+ });
205
+ }
206
+ function validateContainmentDirection(relationshipType, fromKind, toKind, field) {
207
+ const [expectedFrom, expectedTo] = STRUCTURAL_RELATIONSHIPS[relationshipType];
208
+ if (fromKind !== expectedFrom || toKind !== expectedTo)
209
+ throw new Error(`${field} ${relationshipType} containment must point child to parent.`);
210
+ }
211
+ function validateUnresolved(value, nodes) {
212
+ const expectedFamilies = [
213
+ 'ec2-instance',
214
+ 'vpc',
215
+ 'subnet',
216
+ 'internet-gateway',
217
+ 'virtual-private-gateway',
218
+ 'security-group',
219
+ 'rds-db-cluster',
220
+ 'rds-db-instance',
221
+ ];
222
+ asArray(value, 'artifact.unresolved').forEach((entry, index) => {
223
+ const field = `artifact.unresolved[${index}]`;
224
+ const unresolved = asRecord(entry, field);
225
+ assertExactKeys(unresolved, ['sourceNodeId', 'relationshipType', 'sourceFamily', 'field', 'matchedValue', 'expectedTargetFamily'], field);
226
+ const sourceNodeId = requiredString(unresolved.sourceNodeId, `${field}.sourceNodeId`);
227
+ if (!nodes.byId.has(sourceNodeId))
228
+ throw new Error(`${field}.sourceNodeId references a missing node.`);
229
+ requiredEnum(unresolved.relationshipType, AWS_PORTAL_RELATIONSHIP_TYPES, `${field}.relationshipType`);
230
+ requiredEnum(unresolved.sourceFamily, AWS_PORTAL_RELATIONSHIP_DISCOVERY_FAMILIES, `${field}.sourceFamily`);
231
+ requiredString(unresolved.field, `${field}.field`);
232
+ requiredString(unresolved.matchedValue, `${field}.matchedValue`);
233
+ requiredEnum(unresolved.expectedTargetFamily, expectedFamilies, `${field}.expectedTargetFamily`);
234
+ });
235
+ }
236
+ function validateCoverage(value, regions, portalGeneratedAt) {
237
+ const coverage = asRecord(value, 'artifact.coverage');
238
+ assertExactKeys(coverage, ['families'], 'artifact.coverage');
239
+ const familyNames = [];
240
+ asArray(coverage.families, 'artifact.coverage.families').forEach((entry, index) => {
241
+ const field = `artifact.coverage.families[${index}]`;
242
+ const family = asRecord(entry, field);
243
+ assertExactKeys(family, ['family', 'resourceRegions', 'status', 'lastSuccessfulRefreshAt', 'emptyScope', 'reason'], field);
244
+ const familyName = requiredEnum(family.family, AWS_PORTAL_RELATIONSHIP_DISCOVERY_FAMILIES, `${field}.family`);
245
+ familyNames.push(familyName);
246
+ const familyRegions = regionArray(family.resourceRegions, `${field}.resourceRegions`);
247
+ assertExactRegionSet(familyRegions, regions, `${field}.resourceRegions`);
248
+ const status = requiredEnum(family.status, ['available', 'incomplete'], `${field}.status`);
249
+ if (status === 'available') {
250
+ const refreshedAt = isoTimestamp(family.lastSuccessfulRefreshAt, `${field}.lastSuccessfulRefreshAt`);
251
+ if (Date.parse(refreshedAt) > Date.parse(portalGeneratedAt))
252
+ throw new Error(`${field}.lastSuccessfulRefreshAt cannot exceed Portal output.`);
253
+ requiredBoolean(family.emptyScope, `${field}.emptyScope`);
254
+ if (family.reason !== undefined)
255
+ throw new Error(`${field}.reason is not allowed for available coverage.`);
256
+ }
257
+ else {
258
+ assertValue(family.reason, 'source-refresh-incomplete', `${field}.reason`);
259
+ if (family.lastSuccessfulRefreshAt !== undefined || family.emptyScope !== undefined)
260
+ throw new Error(`${field} incomplete coverage cannot claim successful refresh evidence.`);
261
+ }
262
+ });
263
+ assertUnique(familyNames, 'artifact.coverage family identities');
264
+ if (familyNames.length !== AWS_PORTAL_RELATIONSHIP_DISCOVERY_FAMILIES.length ||
265
+ AWS_PORTAL_RELATIONSHIP_DISCOVERY_FAMILIES.some(family => !familyNames.includes(family)))
266
+ throw new Error('artifact.coverage must declare every relationship family.');
267
+ }
268
+ function validateCostOverlay(value, accountId, regions, portalGeneratedAt, resourceNodeCount) {
269
+ const overlay = asRecord(value, 'artifact.costOverlay');
270
+ assertExactKeys(overlay, ['source', 'coverage', 'billing'], 'artifact.costOverlay');
271
+ const source = asRecord(overlay.source, 'artifact.costOverlay.source');
272
+ assertExactKeys(source, ['artifactType', 'logicalName', 'artifactGeneration', 'sha256', 'scope'], 'artifact.costOverlay.source');
273
+ assertValue(source.artifactType, 'resource-collection', 'artifact.costOverlay.source.artifactType');
274
+ assertValue(source.logicalName, 'resources.json.gz', 'artifact.costOverlay.source.logicalName');
275
+ const generation = validateGeneration(source.artifactGeneration, 'artifact.costOverlay.source.artifactGeneration');
276
+ if (Date.parse(generation.generatedAt) > Date.parse(portalGeneratedAt))
277
+ throw new Error('artifact.costOverlay source cannot exceed Portal output.');
278
+ sha256(source.sha256, 'artifact.costOverlay.source.sha256');
279
+ const sourceScope = validateResourceScope(source.scope, accountId, 'artifact.costOverlay.source.scope');
280
+ assertExactRegionSet(sourceScope.resourceRegions, regions, 'artifact.costOverlay.source.scope.resourceRegions');
281
+ const coverage = asRecord(overlay.coverage, 'artifact.costOverlay.coverage');
282
+ const coverageKeys = ['totalResourceCount', 'billedResourceCount', 'matchedResourceNodeCount', 'unmatchedBillingExpenseCount'];
283
+ assertExactKeys(coverage, coverageKeys, 'artifact.costOverlay.coverage');
284
+ coverageKeys.forEach(key => nonNegativeInteger(coverage[key], `artifact.costOverlay.coverage.${key}`));
285
+ if (Number(coverage.billedResourceCount) > Number(coverage.totalResourceCount))
286
+ throw new Error('artifact.costOverlay billed count exceeds resource count.');
287
+ if (Number(coverage.matchedResourceNodeCount) > Number(coverage.billedResourceCount))
288
+ throw new Error('artifact.costOverlay matched count exceeds billed count.');
289
+ if (Number(coverage.matchedResourceNodeCount) > resourceNodeCount)
290
+ throw new Error('artifact.costOverlay matched count exceeds graph resource node count.');
291
+ validateAwsPortalBillingBlock(overlay.billing, 'artifact.costOverlay.billing', sourceScope.billing, accountId, false);
292
+ return Number(coverage.matchedResourceNodeCount);
293
+ }
294
+ function validateStats(value, nodeCount, edgeCount, unresolvedCount) {
295
+ const stats = asRecord(value, 'artifact.stats');
296
+ assertExactKeys(stats, ['totalNodes', 'totalEdges', 'unresolvedCount', 'truncated', 'truncation', 'buildMs', 'snapshotBytes'], 'artifact.stats');
297
+ nonNegativeInteger(stats.totalNodes, 'artifact.stats.totalNodes');
298
+ nonNegativeInteger(stats.totalEdges, 'artifact.stats.totalEdges');
299
+ nonNegativeInteger(stats.unresolvedCount, 'artifact.stats.unresolvedCount');
300
+ assertValue(stats.totalNodes, nodeCount, 'artifact.stats.totalNodes');
301
+ assertValue(stats.totalEdges, edgeCount, 'artifact.stats.totalEdges');
302
+ assertValue(stats.unresolvedCount, unresolvedCount, 'artifact.stats.unresolvedCount');
303
+ const truncated = requiredBoolean(stats.truncated, 'artifact.stats.truncated');
304
+ if (truncated) {
305
+ const truncation = asRecord(stats.truncation, 'artifact.stats.truncation');
306
+ assertExactKeys(truncation, ['reason', 'edgesDroppedCount', 'unresolvedDroppedCount', 'tagsRemovedFromNodeCount'], 'artifact.stats.truncation');
307
+ assertValue(truncation.reason, 'snapshot-size-limit', 'artifact.stats.truncation.reason');
308
+ const dropped = ['edgesDroppedCount', 'unresolvedDroppedCount', 'tagsRemovedFromNodeCount'].map(key => nonNegativeInteger(truncation[key], `artifact.stats.truncation.${key}`));
309
+ if (dropped.every(count => count === 0))
310
+ throw new Error('artifact.stats.truncation must report at least one omitted item.');
311
+ }
312
+ else if (stats.truncation !== undefined) {
313
+ throw new Error('artifact.stats.truncation is only allowed when truncated is true.');
314
+ }
315
+ if (stats.buildMs !== undefined)
316
+ nonNegativeInteger(stats.buildMs, 'artifact.stats.buildMs');
317
+ if (stats.snapshotBytes !== undefined)
318
+ nonNegativeInteger(stats.snapshotBytes, 'artifact.stats.snapshotBytes');
319
+ }
320
+ function assertExactRegionSet(actual, expected, field) {
321
+ const actualSet = new Set(actual);
322
+ if (actualSet.size !== expected.length || expected.some(region => !actualSet.has(region)))
323
+ throw new Error(`${field} must exactly match artifact scope Regions.`);
324
+ }
325
+ function nonEmptyStringEnumArray(value, allowed, field) {
326
+ const values = asArray(value, field, true).map((entry, index) => requiredEnum(entry, allowed, `${field}[${index}]`));
327
+ assertUnique(values, field);
328
+ return values;
329
+ }
330
+ function asArray(value, field, nonEmpty = false) {
331
+ if (!Array.isArray(value) || (nonEmpty && value.length === 0))
332
+ throw new Error(`${field} must be ${nonEmpty ? 'a non-empty' : 'an'} array.`);
333
+ return value;
334
+ }
@@ -0,0 +1,59 @@
1
+ export const AWS_PORTAL_RELATIONSHIP_LOGICAL_NAME = 'relationships.json.gz';
2
+ export const AWS_PORTAL_RELATIONSHIP_SCHEMA_VERSION = 2;
3
+ export const AWS_PORTAL_RELATIONSHIP_DISCOVERY_FAMILIES = [
4
+ 'ec2-instance',
5
+ 'ebs-volume',
6
+ 'vpc',
7
+ 'subnet',
8
+ 'route-table',
9
+ 'internet-gateway',
10
+ 'virtual-private-gateway',
11
+ 'network-interface',
12
+ 'nat-gateway',
13
+ 'security-group',
14
+ 'rds-db-cluster',
15
+ 'rds-db-instance',
16
+ 'load-balancer-v2',
17
+ 'classic-load-balancer',
18
+ 'elasticache-cache-cluster',
19
+ 'elasticache-serverless-cache',
20
+ 'efs-file-system',
21
+ ];
22
+ export const AWS_PORTAL_RELATIONSHIP_SYNTHETIC_TYPES = [
23
+ 'vpc',
24
+ 'subnet',
25
+ 'availability-zone',
26
+ 'db-subnet-group',
27
+ 'db-cluster',
28
+ 'elasticache-replication-group',
29
+ 'elasticache-global-datastore',
30
+ 'kms-key',
31
+ 'snapshot',
32
+ ];
33
+ export const AWS_PORTAL_RELATIONSHIP_TYPES = [
34
+ 'account-region',
35
+ 'region-resource',
36
+ 'region-topology',
37
+ 'vpc-membership',
38
+ 'subnet-membership',
39
+ 'availability-zone-placement',
40
+ 'kms-key-encryption',
41
+ 'ebs-snapshot-origin',
42
+ 'db-subnet-group-membership',
43
+ 'elasticache-replication-group-membership',
44
+ 'elasticache-global-datastore-membership',
45
+ 'security-group-association',
46
+ 'classic-load-balancer-registration',
47
+ 'ebs-attachment',
48
+ 'network-interface-attachment',
49
+ 'route-table-association',
50
+ 'gateway-route-table-association',
51
+ 'route-propagation-source',
52
+ 'rds-read-replica',
53
+ 'db-cluster-membership',
54
+ 'db-cluster-member',
55
+ 'security-group-peer-reference',
56
+ ];
57
+ export const AWS_PORTAL_RELATIONSHIP_CONFIDENCE_SCORES = {
58
+ high: 1,
59
+ };