s3db.js 13.4.0 → 13.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -10
- package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +38653 -32291
- package/dist/s3db.es.js.map +1 -1
- package/package.json +218 -22
- package/src/concerns/id.js +90 -6
- package/src/concerns/index.js +2 -1
- package/src/concerns/password-hashing.js +150 -0
- package/src/database.class.js +6 -2
- package/src/plugins/api/auth/basic-auth.js +40 -10
- package/src/plugins/api/auth/index.js +49 -3
- package/src/plugins/api/auth/oauth2-auth.js +171 -0
- package/src/plugins/api/auth/oidc-auth.js +789 -0
- package/src/plugins/api/auth/oidc-client.js +462 -0
- package/src/plugins/api/auth/path-auth-matcher.js +284 -0
- package/src/plugins/api/concerns/event-emitter.js +134 -0
- package/src/plugins/api/concerns/failban-manager.js +651 -0
- package/src/plugins/api/concerns/guards-helpers.js +402 -0
- package/src/plugins/api/concerns/metrics-collector.js +346 -0
- package/src/plugins/api/index.js +510 -57
- package/src/plugins/api/middlewares/failban.js +305 -0
- package/src/plugins/api/middlewares/rate-limit.js +301 -0
- package/src/plugins/api/middlewares/request-id.js +74 -0
- package/src/plugins/api/middlewares/security-headers.js +120 -0
- package/src/plugins/api/middlewares/session-tracking.js +194 -0
- package/src/plugins/api/routes/auth-routes.js +119 -78
- package/src/plugins/api/routes/resource-routes.js +73 -30
- package/src/plugins/api/server.js +1139 -45
- package/src/plugins/api/utils/custom-routes.js +102 -0
- package/src/plugins/api/utils/guards.js +213 -0
- package/src/plugins/api/utils/mime-types.js +154 -0
- package/src/plugins/api/utils/openapi-generator.js +91 -12
- package/src/plugins/api/utils/path-matcher.js +173 -0
- package/src/plugins/api/utils/static-filesystem.js +262 -0
- package/src/plugins/api/utils/static-s3.js +231 -0
- package/src/plugins/api/utils/template-engine.js +188 -0
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
- package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
- package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
- package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
- package/src/plugins/cloud-inventory/index.js +20 -0
- package/src/plugins/cloud-inventory/registry.js +146 -0
- package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
- package/src/plugins/cloud-inventory.plugin.js +1333 -0
- package/src/plugins/concerns/plugin-dependencies.js +62 -2
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/eventual-consistency/consolidation.js +2 -2
- package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
- package/src/plugins/eventual-consistency/install.js +2 -2
- package/src/plugins/identity/README.md +335 -0
- package/src/plugins/identity/concerns/mfa-manager.js +204 -0
- package/src/plugins/identity/concerns/password.js +138 -0
- package/src/plugins/identity/concerns/resource-schemas.js +273 -0
- package/src/plugins/identity/concerns/token-generator.js +172 -0
- package/src/plugins/identity/email-service.js +422 -0
- package/src/plugins/identity/index.js +1052 -0
- package/src/plugins/identity/oauth2-server.js +1033 -0
- package/src/plugins/identity/oidc-discovery.js +285 -0
- package/src/plugins/identity/rsa-keys.js +323 -0
- package/src/plugins/identity/server.js +500 -0
- package/src/plugins/identity/session-manager.js +453 -0
- package/src/plugins/identity/ui/layouts/base.js +251 -0
- package/src/plugins/identity/ui/middleware.js +135 -0
- package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
- package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
- package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
- package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
- package/src/plugins/identity/ui/pages/admin/users.js +263 -0
- package/src/plugins/identity/ui/pages/consent.js +262 -0
- package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
- package/src/plugins/identity/ui/pages/login.js +144 -0
- package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
- package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
- package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
- package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
- package/src/plugins/identity/ui/pages/profile.js +361 -0
- package/src/plugins/identity/ui/pages/register.js +226 -0
- package/src/plugins/identity/ui/pages/reset-password.js +128 -0
- package/src/plugins/identity/ui/pages/verify-email.js +172 -0
- package/src/plugins/identity/ui/routes.js +2541 -0
- package/src/plugins/identity/ui/styles/main.css +465 -0
- package/src/plugins/index.js +4 -1
- package/src/plugins/ml/base-model.class.js +65 -16
- package/src/plugins/ml/classification-model.class.js +1 -1
- package/src/plugins/ml/timeseries-model.class.js +3 -1
- package/src/plugins/ml.plugin.js +584 -31
- package/src/plugins/shared/error-handler.js +147 -0
- package/src/plugins/shared/index.js +9 -0
- package/src/plugins/shared/middlewares/compression.js +117 -0
- package/src/plugins/shared/middlewares/cors.js +49 -0
- package/src/plugins/shared/middlewares/index.js +11 -0
- package/src/plugins/shared/middlewares/logging.js +54 -0
- package/src/plugins/shared/middlewares/rate-limit.js +73 -0
- package/src/plugins/shared/middlewares/security.js +158 -0
- package/src/plugins/shared/response-formatter.js +264 -0
- package/src/plugins/state-machine.plugin.js +57 -2
- package/src/resource.class.js +140 -12
- package/src/schema.class.js +30 -1
- package/src/validator.class.js +57 -6
- package/dist/s3db.cjs.js.map +0 -1
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import { BaseCloudDriver } from './base-driver.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Production-ready MongoDB Atlas inventory driver using mongodb-atlas-api-client.
|
|
5
|
+
*
|
|
6
|
+
* Covers 10+ services with 15+ resource types:
|
|
7
|
+
* - Compute (Clusters, Serverless Instances)
|
|
8
|
+
* - Storage (Backups, Snapshots, Data Lakes)
|
|
9
|
+
* - Security (Database Users, IP Access Lists, Custom Roles)
|
|
10
|
+
* - Monitoring (Alerts, Events)
|
|
11
|
+
* - Search (Atlas Search Indexes)
|
|
12
|
+
* - Projects & Organizations
|
|
13
|
+
*
|
|
14
|
+
* @see https://www.mongodb.com/docs/atlas/api/
|
|
15
|
+
* @see https://www.npmjs.com/package/mongodb-atlas-api-client
|
|
16
|
+
*/
|
|
17
|
+
export class MongoDBAtlasInventoryDriver extends BaseCloudDriver {
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
super({ ...options, driver: options.driver || 'mongodb-atlas' });
|
|
20
|
+
|
|
21
|
+
this._publicKey = null;
|
|
22
|
+
this._privateKey = null;
|
|
23
|
+
this._baseUrl = 'https://cloud.mongodb.com/api/atlas/v2';
|
|
24
|
+
this._organizationId = this.config?.organizationId || null;
|
|
25
|
+
|
|
26
|
+
// Services to collect (can be filtered via config.services)
|
|
27
|
+
this._services = this.config?.services || [
|
|
28
|
+
'projects',
|
|
29
|
+
'clusters',
|
|
30
|
+
'serverless',
|
|
31
|
+
'users',
|
|
32
|
+
'accesslists',
|
|
33
|
+
'backups',
|
|
34
|
+
'alerts',
|
|
35
|
+
'datalakes',
|
|
36
|
+
'search',
|
|
37
|
+
'customroles',
|
|
38
|
+
'events'
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Projects to scan (null = all accessible projects)
|
|
42
|
+
this._projectIds = this.config?.projectIds || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Initialize MongoDB Atlas credentials.
|
|
47
|
+
*/
|
|
48
|
+
async _initializeCredentials() {
|
|
49
|
+
if (this._publicKey) return;
|
|
50
|
+
|
|
51
|
+
const credentials = this.credentials || {};
|
|
52
|
+
this._publicKey = credentials.publicKey || process.env.MONGODB_ATLAS_PUBLIC_KEY;
|
|
53
|
+
this._privateKey = credentials.privateKey || process.env.MONGODB_ATLAS_PRIVATE_KEY;
|
|
54
|
+
this._organizationId = credentials.organizationId || this._organizationId;
|
|
55
|
+
|
|
56
|
+
if (!this._publicKey || !this._privateKey) {
|
|
57
|
+
throw new Error('MongoDB Atlas API keys are required. Provide via credentials.publicKey/privateKey or env vars.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.logger('info', 'MongoDB Atlas credentials initialized', {
|
|
61
|
+
organizationId: this._organizationId || 'auto-discover',
|
|
62
|
+
services: this._services.length
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create Atlas API client.
|
|
68
|
+
*/
|
|
69
|
+
async _createClient() {
|
|
70
|
+
const AtlasClient = await import('mongodb-atlas-api-client');
|
|
71
|
+
|
|
72
|
+
return AtlasClient.default({
|
|
73
|
+
publicKey: this._publicKey,
|
|
74
|
+
privateKey: this._privateKey,
|
|
75
|
+
baseUrl: this._baseUrl
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Make authenticated request to Atlas API.
|
|
81
|
+
*/
|
|
82
|
+
async _makeRequest(endpoint, options = {}) {
|
|
83
|
+
const crypto = await import('crypto');
|
|
84
|
+
const https = await import('https');
|
|
85
|
+
|
|
86
|
+
const url = new URL(endpoint, this._baseUrl);
|
|
87
|
+
const method = options.method || 'GET';
|
|
88
|
+
|
|
89
|
+
// HTTP Digest Authentication
|
|
90
|
+
const nonce = crypto.randomBytes(16).toString('hex');
|
|
91
|
+
const timestamp = new Date().toISOString();
|
|
92
|
+
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const req = https.request({
|
|
95
|
+
hostname: url.hostname,
|
|
96
|
+
path: url.pathname + url.search,
|
|
97
|
+
method,
|
|
98
|
+
headers: {
|
|
99
|
+
'Accept': 'application/vnd.atlas.2025-03-12+json',
|
|
100
|
+
'Content-Type': 'application/json'
|
|
101
|
+
},
|
|
102
|
+
auth: `${this._publicKey}:${this._privateKey}`
|
|
103
|
+
}, (res) => {
|
|
104
|
+
let data = '';
|
|
105
|
+
res.on('data', chunk => data += chunk);
|
|
106
|
+
res.on('end', () => {
|
|
107
|
+
try {
|
|
108
|
+
resolve(JSON.parse(data));
|
|
109
|
+
} catch (err) {
|
|
110
|
+
resolve(data);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
req.on('error', reject);
|
|
116
|
+
|
|
117
|
+
if (options.body) {
|
|
118
|
+
req.write(JSON.stringify(options.body));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
req.end();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Main entry point - lists all resources from configured services.
|
|
127
|
+
*/
|
|
128
|
+
async *listResources(options = {}) {
|
|
129
|
+
await this._initializeCredentials();
|
|
130
|
+
|
|
131
|
+
const serviceCollectors = {
|
|
132
|
+
projects: () => this._collectProjects(),
|
|
133
|
+
clusters: () => this._collectClusters(),
|
|
134
|
+
serverless: () => this._collectServerless(),
|
|
135
|
+
users: () => this._collectUsers(),
|
|
136
|
+
accesslists: () => this._collectAccessLists(),
|
|
137
|
+
backups: () => this._collectBackups(),
|
|
138
|
+
alerts: () => this._collectAlerts(),
|
|
139
|
+
datalakes: () => this._collectDataLakes(),
|
|
140
|
+
search: () => this._collectSearchIndexes(),
|
|
141
|
+
customroles: () => this._collectCustomRoles(),
|
|
142
|
+
events: () => this._collectEvents()
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
for (const service of this._services) {
|
|
146
|
+
const collector = serviceCollectors[service];
|
|
147
|
+
if (!collector) {
|
|
148
|
+
this.logger('warn', `Unknown MongoDB Atlas service: ${service}`, { service });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
this.logger('info', `Collecting MongoDB Atlas ${service} resources`, { service });
|
|
154
|
+
yield* collector();
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// Continue with next service instead of failing entire sync
|
|
157
|
+
this.logger('error', `MongoDB Atlas service collection failed, skipping to next service`, {
|
|
158
|
+
service,
|
|
159
|
+
error: err.message,
|
|
160
|
+
errorName: err.name,
|
|
161
|
+
stack: err.stack
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get list of projects to iterate.
|
|
169
|
+
*/
|
|
170
|
+
async _getProjects() {
|
|
171
|
+
if (this._projectIds) {
|
|
172
|
+
return this._projectIds.map(id => ({ id }));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const response = await this._makeRequest('/groups');
|
|
177
|
+
return response.results || [];
|
|
178
|
+
} catch (err) {
|
|
179
|
+
this.logger('error', 'Failed to fetch projects list', { error: err.message });
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Collect Projects (Groups).
|
|
186
|
+
*/
|
|
187
|
+
async *_collectProjects() {
|
|
188
|
+
try {
|
|
189
|
+
const projects = await this._getProjects();
|
|
190
|
+
|
|
191
|
+
for (const project of projects) {
|
|
192
|
+
yield {
|
|
193
|
+
provider: 'mongodb-atlas',
|
|
194
|
+
accountId: this._organizationId || project.orgId,
|
|
195
|
+
region: null, // Projects are global
|
|
196
|
+
service: 'projects',
|
|
197
|
+
resourceType: 'mongodb-atlas.project',
|
|
198
|
+
resourceId: project.id,
|
|
199
|
+
name: project.name,
|
|
200
|
+
tags: {},
|
|
201
|
+
metadata: {
|
|
202
|
+
orgId: project.orgId,
|
|
203
|
+
clusterCount: project.clusterCount
|
|
204
|
+
},
|
|
205
|
+
configuration: this._sanitize(project)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.logger('info', `Collected ${projects.length} MongoDB Atlas projects`);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
this.logger('error', 'Failed to collect MongoDB Atlas projects', {
|
|
212
|
+
error: err.message,
|
|
213
|
+
stack: err.stack
|
|
214
|
+
});
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Collect Clusters.
|
|
221
|
+
*/
|
|
222
|
+
async *_collectClusters() {
|
|
223
|
+
try {
|
|
224
|
+
const projects = await this._getProjects();
|
|
225
|
+
|
|
226
|
+
for (const project of projects) {
|
|
227
|
+
try {
|
|
228
|
+
const response = await this._makeRequest(`/groups/${project.id}/clusters`);
|
|
229
|
+
const clusters = response.results || [];
|
|
230
|
+
|
|
231
|
+
for (const cluster of clusters) {
|
|
232
|
+
yield {
|
|
233
|
+
provider: 'mongodb-atlas',
|
|
234
|
+
accountId: this._organizationId || project.orgId,
|
|
235
|
+
region: cluster.providerSettings?.regionName || null,
|
|
236
|
+
service: 'clusters',
|
|
237
|
+
resourceType: 'mongodb-atlas.cluster',
|
|
238
|
+
resourceId: cluster.id || cluster.name,
|
|
239
|
+
name: cluster.name,
|
|
240
|
+
tags: cluster.tags || {},
|
|
241
|
+
metadata: {
|
|
242
|
+
projectId: project.id,
|
|
243
|
+
projectName: project.name,
|
|
244
|
+
tier: cluster.providerSettings?.instanceSizeName,
|
|
245
|
+
provider: cluster.providerSettings?.providerName,
|
|
246
|
+
mongoDBVersion: cluster.mongoDBVersion,
|
|
247
|
+
clusterType: cluster.clusterType,
|
|
248
|
+
state: cluster.stateName
|
|
249
|
+
},
|
|
250
|
+
configuration: this._sanitize(cluster)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
} catch (projectErr) {
|
|
254
|
+
this.logger('warn', `Failed to collect clusters for project ${project.id}`, {
|
|
255
|
+
projectId: project.id,
|
|
256
|
+
error: projectErr.message
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.logger('info', `Collected MongoDB Atlas clusters`);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
this.logger('error', 'Failed to collect MongoDB Atlas clusters', {
|
|
264
|
+
error: err.message,
|
|
265
|
+
stack: err.stack
|
|
266
|
+
});
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Collect Serverless Instances.
|
|
273
|
+
*/
|
|
274
|
+
async *_collectServerless() {
|
|
275
|
+
try {
|
|
276
|
+
const projects = await this._getProjects();
|
|
277
|
+
|
|
278
|
+
for (const project of projects) {
|
|
279
|
+
try {
|
|
280
|
+
const response = await this._makeRequest(`/groups/${project.id}/serverless`);
|
|
281
|
+
const instances = response.results || [];
|
|
282
|
+
|
|
283
|
+
for (const instance of instances) {
|
|
284
|
+
yield {
|
|
285
|
+
provider: 'mongodb-atlas',
|
|
286
|
+
accountId: this._organizationId || project.orgId,
|
|
287
|
+
region: instance.providerSettings?.regionName || null,
|
|
288
|
+
service: 'serverless',
|
|
289
|
+
resourceType: 'mongodb-atlas.serverless',
|
|
290
|
+
resourceId: instance.id || instance.name,
|
|
291
|
+
name: instance.name,
|
|
292
|
+
tags: instance.tags || {},
|
|
293
|
+
metadata: {
|
|
294
|
+
projectId: project.id,
|
|
295
|
+
projectName: project.name,
|
|
296
|
+
provider: instance.providerSettings?.providerName,
|
|
297
|
+
state: instance.stateName
|
|
298
|
+
},
|
|
299
|
+
configuration: this._sanitize(instance)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
} catch (projectErr) {
|
|
303
|
+
this.logger('debug', `No serverless instances in project ${project.id}`, {
|
|
304
|
+
projectId: project.id,
|
|
305
|
+
error: projectErr.message
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this.logger('info', `Collected MongoDB Atlas serverless instances`);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
this.logger('error', 'Failed to collect MongoDB Atlas serverless', {
|
|
313
|
+
error: err.message,
|
|
314
|
+
stack: err.stack
|
|
315
|
+
});
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Collect Database Users.
|
|
322
|
+
*/
|
|
323
|
+
async *_collectUsers() {
|
|
324
|
+
try {
|
|
325
|
+
const projects = await this._getProjects();
|
|
326
|
+
|
|
327
|
+
for (const project of projects) {
|
|
328
|
+
try {
|
|
329
|
+
const response = await this._makeRequest(`/groups/${project.id}/databaseUsers`);
|
|
330
|
+
const users = response.results || [];
|
|
331
|
+
|
|
332
|
+
for (const user of users) {
|
|
333
|
+
yield {
|
|
334
|
+
provider: 'mongodb-atlas',
|
|
335
|
+
accountId: this._organizationId || project.orgId,
|
|
336
|
+
region: null,
|
|
337
|
+
service: 'users',
|
|
338
|
+
resourceType: 'mongodb-atlas.user',
|
|
339
|
+
resourceId: `${project.id}/${user.username}`,
|
|
340
|
+
name: user.username,
|
|
341
|
+
tags: {},
|
|
342
|
+
metadata: {
|
|
343
|
+
projectId: project.id,
|
|
344
|
+
projectName: project.name,
|
|
345
|
+
databaseName: user.databaseName,
|
|
346
|
+
roles: user.roles?.map(r => r.roleName)
|
|
347
|
+
},
|
|
348
|
+
configuration: this._sanitize(user)
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
} catch (projectErr) {
|
|
352
|
+
this.logger('warn', `Failed to collect users for project ${project.id}`, {
|
|
353
|
+
projectId: project.id,
|
|
354
|
+
error: projectErr.message
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
this.logger('info', `Collected MongoDB Atlas database users`);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
this.logger('error', 'Failed to collect MongoDB Atlas users', {
|
|
362
|
+
error: err.message,
|
|
363
|
+
stack: err.stack
|
|
364
|
+
});
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Collect IP Access Lists (Whitelists).
|
|
371
|
+
*/
|
|
372
|
+
async *_collectAccessLists() {
|
|
373
|
+
try {
|
|
374
|
+
const projects = await this._getProjects();
|
|
375
|
+
|
|
376
|
+
for (const project of projects) {
|
|
377
|
+
try {
|
|
378
|
+
const response = await this._makeRequest(`/groups/${project.id}/accessList`);
|
|
379
|
+
const entries = response.results || [];
|
|
380
|
+
|
|
381
|
+
for (const entry of entries) {
|
|
382
|
+
yield {
|
|
383
|
+
provider: 'mongodb-atlas',
|
|
384
|
+
accountId: this._organizationId || project.orgId,
|
|
385
|
+
region: null,
|
|
386
|
+
service: 'accesslists',
|
|
387
|
+
resourceType: 'mongodb-atlas.accesslist',
|
|
388
|
+
resourceId: `${project.id}/${entry.ipAddress || entry.cidrBlock}`,
|
|
389
|
+
name: entry.comment || entry.ipAddress || entry.cidrBlock,
|
|
390
|
+
tags: {},
|
|
391
|
+
metadata: {
|
|
392
|
+
projectId: project.id,
|
|
393
|
+
projectName: project.name,
|
|
394
|
+
ipAddress: entry.ipAddress,
|
|
395
|
+
cidrBlock: entry.cidrBlock
|
|
396
|
+
},
|
|
397
|
+
configuration: this._sanitize(entry)
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
} catch (projectErr) {
|
|
401
|
+
this.logger('warn', `Failed to collect access lists for project ${project.id}`, {
|
|
402
|
+
projectId: project.id,
|
|
403
|
+
error: projectErr.message
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.logger('info', `Collected MongoDB Atlas IP access lists`);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
this.logger('error', 'Failed to collect MongoDB Atlas access lists', {
|
|
411
|
+
error: err.message,
|
|
412
|
+
stack: err.stack
|
|
413
|
+
});
|
|
414
|
+
throw err;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Collect Cloud Backups.
|
|
420
|
+
*/
|
|
421
|
+
async *_collectBackups() {
|
|
422
|
+
try {
|
|
423
|
+
const projects = await this._getProjects();
|
|
424
|
+
|
|
425
|
+
for (const project of projects) {
|
|
426
|
+
try {
|
|
427
|
+
// Get clusters first
|
|
428
|
+
const clustersResponse = await this._makeRequest(`/groups/${project.id}/clusters`);
|
|
429
|
+
const clusters = clustersResponse.results || [];
|
|
430
|
+
|
|
431
|
+
for (const cluster of clusters) {
|
|
432
|
+
try {
|
|
433
|
+
const response = await this._makeRequest(
|
|
434
|
+
`/groups/${project.id}/clusters/${cluster.name}/backup/snapshots`
|
|
435
|
+
);
|
|
436
|
+
const snapshots = response.results || [];
|
|
437
|
+
|
|
438
|
+
for (const snapshot of snapshots) {
|
|
439
|
+
yield {
|
|
440
|
+
provider: 'mongodb-atlas',
|
|
441
|
+
accountId: this._organizationId || project.orgId,
|
|
442
|
+
region: cluster.providerSettings?.regionName || null,
|
|
443
|
+
service: 'backups',
|
|
444
|
+
resourceType: 'mongodb-atlas.backup',
|
|
445
|
+
resourceId: snapshot.id,
|
|
446
|
+
name: `${cluster.name}-${snapshot.id}`,
|
|
447
|
+
tags: {},
|
|
448
|
+
metadata: {
|
|
449
|
+
projectId: project.id,
|
|
450
|
+
projectName: project.name,
|
|
451
|
+
clusterName: cluster.name,
|
|
452
|
+
type: snapshot.type,
|
|
453
|
+
status: snapshot.status
|
|
454
|
+
},
|
|
455
|
+
configuration: this._sanitize(snapshot)
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
} catch (clusterErr) {
|
|
459
|
+
this.logger('debug', `No backups for cluster ${cluster.name}`, {
|
|
460
|
+
clusterName: cluster.name,
|
|
461
|
+
error: clusterErr.message
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch (projectErr) {
|
|
466
|
+
this.logger('warn', `Failed to collect backups for project ${project.id}`, {
|
|
467
|
+
projectId: project.id,
|
|
468
|
+
error: projectErr.message
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
this.logger('info', `Collected MongoDB Atlas backups`);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
this.logger('error', 'Failed to collect MongoDB Atlas backups', {
|
|
476
|
+
error: err.message,
|
|
477
|
+
stack: err.stack
|
|
478
|
+
});
|
|
479
|
+
throw err;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Collect Alert Configurations.
|
|
485
|
+
*/
|
|
486
|
+
async *_collectAlerts() {
|
|
487
|
+
try {
|
|
488
|
+
const projects = await this._getProjects();
|
|
489
|
+
|
|
490
|
+
for (const project of projects) {
|
|
491
|
+
try {
|
|
492
|
+
const response = await this._makeRequest(`/groups/${project.id}/alertConfigs`);
|
|
493
|
+
const alerts = response.results || [];
|
|
494
|
+
|
|
495
|
+
for (const alert of alerts) {
|
|
496
|
+
yield {
|
|
497
|
+
provider: 'mongodb-atlas',
|
|
498
|
+
accountId: this._organizationId || project.orgId,
|
|
499
|
+
region: null,
|
|
500
|
+
service: 'alerts',
|
|
501
|
+
resourceType: 'mongodb-atlas.alert',
|
|
502
|
+
resourceId: alert.id,
|
|
503
|
+
name: alert.eventTypeName,
|
|
504
|
+
tags: {},
|
|
505
|
+
metadata: {
|
|
506
|
+
projectId: project.id,
|
|
507
|
+
projectName: project.name,
|
|
508
|
+
enabled: alert.enabled,
|
|
509
|
+
eventTypeName: alert.eventTypeName
|
|
510
|
+
},
|
|
511
|
+
configuration: this._sanitize(alert)
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
} catch (projectErr) {
|
|
515
|
+
this.logger('warn', `Failed to collect alerts for project ${project.id}`, {
|
|
516
|
+
projectId: project.id,
|
|
517
|
+
error: projectErr.message
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
this.logger('info', `Collected MongoDB Atlas alerts`);
|
|
523
|
+
} catch (err) {
|
|
524
|
+
this.logger('error', 'Failed to collect MongoDB Atlas alerts', {
|
|
525
|
+
error: err.message,
|
|
526
|
+
stack: err.stack
|
|
527
|
+
});
|
|
528
|
+
throw err;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Collect Data Lakes (Federated Database Instances).
|
|
534
|
+
*/
|
|
535
|
+
async *_collectDataLakes() {
|
|
536
|
+
try {
|
|
537
|
+
const projects = await this._getProjects();
|
|
538
|
+
|
|
539
|
+
for (const project of projects) {
|
|
540
|
+
try {
|
|
541
|
+
const response = await this._makeRequest(`/groups/${project.id}/dataLakes`);
|
|
542
|
+
const dataLakes = response || [];
|
|
543
|
+
|
|
544
|
+
for (const lake of dataLakes) {
|
|
545
|
+
yield {
|
|
546
|
+
provider: 'mongodb-atlas',
|
|
547
|
+
accountId: this._organizationId || project.orgId,
|
|
548
|
+
region: lake.cloudProviderConfig?.aws?.roleId ? 'aws' : null,
|
|
549
|
+
service: 'datalakes',
|
|
550
|
+
resourceType: 'mongodb-atlas.datalake',
|
|
551
|
+
resourceId: lake.name,
|
|
552
|
+
name: lake.name,
|
|
553
|
+
tags: {},
|
|
554
|
+
metadata: {
|
|
555
|
+
projectId: project.id,
|
|
556
|
+
projectName: project.name,
|
|
557
|
+
state: lake.state
|
|
558
|
+
},
|
|
559
|
+
configuration: this._sanitize(lake)
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
} catch (projectErr) {
|
|
563
|
+
this.logger('debug', `No data lakes in project ${project.id}`, {
|
|
564
|
+
projectId: project.id,
|
|
565
|
+
error: projectErr.message
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
this.logger('info', `Collected MongoDB Atlas data lakes`);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
this.logger('error', 'Failed to collect MongoDB Atlas data lakes', {
|
|
573
|
+
error: err.message,
|
|
574
|
+
stack: err.stack
|
|
575
|
+
});
|
|
576
|
+
throw err;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Collect Atlas Search Indexes.
|
|
582
|
+
*/
|
|
583
|
+
async *_collectSearchIndexes() {
|
|
584
|
+
try {
|
|
585
|
+
const projects = await this._getProjects();
|
|
586
|
+
|
|
587
|
+
for (const project of projects) {
|
|
588
|
+
try {
|
|
589
|
+
// Get clusters first
|
|
590
|
+
const clustersResponse = await this._makeRequest(`/groups/${project.id}/clusters`);
|
|
591
|
+
const clusters = clustersResponse.results || [];
|
|
592
|
+
|
|
593
|
+
for (const cluster of clusters) {
|
|
594
|
+
try {
|
|
595
|
+
const response = await this._makeRequest(
|
|
596
|
+
`/groups/${project.id}/clusters/${cluster.name}/fts/indexes`
|
|
597
|
+
);
|
|
598
|
+
const indexes = response || [];
|
|
599
|
+
|
|
600
|
+
for (const index of indexes) {
|
|
601
|
+
yield {
|
|
602
|
+
provider: 'mongodb-atlas',
|
|
603
|
+
accountId: this._organizationId || project.orgId,
|
|
604
|
+
region: cluster.providerSettings?.regionName || null,
|
|
605
|
+
service: 'search',
|
|
606
|
+
resourceType: 'mongodb-atlas.search.index',
|
|
607
|
+
resourceId: index.indexID,
|
|
608
|
+
name: index.name,
|
|
609
|
+
tags: {},
|
|
610
|
+
metadata: {
|
|
611
|
+
projectId: project.id,
|
|
612
|
+
projectName: project.name,
|
|
613
|
+
clusterName: cluster.name,
|
|
614
|
+
collectionName: index.collectionName,
|
|
615
|
+
database: index.database,
|
|
616
|
+
status: index.status
|
|
617
|
+
},
|
|
618
|
+
configuration: this._sanitize(index)
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
} catch (clusterErr) {
|
|
622
|
+
this.logger('debug', `No search indexes for cluster ${cluster.name}`, {
|
|
623
|
+
clusterName: cluster.name,
|
|
624
|
+
error: clusterErr.message
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} catch (projectErr) {
|
|
629
|
+
this.logger('warn', `Failed to collect search indexes for project ${project.id}`, {
|
|
630
|
+
projectId: project.id,
|
|
631
|
+
error: projectErr.message
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
this.logger('info', `Collected MongoDB Atlas search indexes`);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
this.logger('error', 'Failed to collect MongoDB Atlas search indexes', {
|
|
639
|
+
error: err.message,
|
|
640
|
+
stack: err.stack
|
|
641
|
+
});
|
|
642
|
+
throw err;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Collect Custom Database Roles.
|
|
648
|
+
*/
|
|
649
|
+
async *_collectCustomRoles() {
|
|
650
|
+
try {
|
|
651
|
+
const projects = await this._getProjects();
|
|
652
|
+
|
|
653
|
+
for (const project of projects) {
|
|
654
|
+
try {
|
|
655
|
+
const response = await this._makeRequest(`/groups/${project.id}/customDBRoles/roles`);
|
|
656
|
+
const roles = response || [];
|
|
657
|
+
|
|
658
|
+
for (const role of roles) {
|
|
659
|
+
yield {
|
|
660
|
+
provider: 'mongodb-atlas',
|
|
661
|
+
accountId: this._organizationId || project.orgId,
|
|
662
|
+
region: null,
|
|
663
|
+
service: 'customroles',
|
|
664
|
+
resourceType: 'mongodb-atlas.customrole',
|
|
665
|
+
resourceId: `${project.id}/${role.roleName}`,
|
|
666
|
+
name: role.roleName,
|
|
667
|
+
tags: {},
|
|
668
|
+
metadata: {
|
|
669
|
+
projectId: project.id,
|
|
670
|
+
projectName: project.name
|
|
671
|
+
},
|
|
672
|
+
configuration: this._sanitize(role)
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
} catch (projectErr) {
|
|
676
|
+
this.logger('debug', `No custom roles in project ${project.id}`, {
|
|
677
|
+
projectId: project.id,
|
|
678
|
+
error: projectErr.message
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
this.logger('info', `Collected MongoDB Atlas custom roles`);
|
|
684
|
+
} catch (err) {
|
|
685
|
+
this.logger('error', 'Failed to collect MongoDB Atlas custom roles', {
|
|
686
|
+
error: err.message,
|
|
687
|
+
stack: err.stack
|
|
688
|
+
});
|
|
689
|
+
throw err;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Collect Events (audit logs).
|
|
695
|
+
*/
|
|
696
|
+
async *_collectEvents() {
|
|
697
|
+
try {
|
|
698
|
+
const projects = await this._getProjects();
|
|
699
|
+
|
|
700
|
+
for (const project of projects) {
|
|
701
|
+
try {
|
|
702
|
+
// Limit to recent events (last 7 days)
|
|
703
|
+
const minDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
704
|
+
const response = await this._makeRequest(
|
|
705
|
+
`/groups/${project.id}/events?minDate=${minDate}&itemsPerPage=100`
|
|
706
|
+
);
|
|
707
|
+
const events = response.results || [];
|
|
708
|
+
|
|
709
|
+
for (const event of events) {
|
|
710
|
+
yield {
|
|
711
|
+
provider: 'mongodb-atlas',
|
|
712
|
+
accountId: this._organizationId || project.orgId,
|
|
713
|
+
region: null,
|
|
714
|
+
service: 'events',
|
|
715
|
+
resourceType: 'mongodb-atlas.event',
|
|
716
|
+
resourceId: event.id,
|
|
717
|
+
name: event.eventTypeName,
|
|
718
|
+
tags: {},
|
|
719
|
+
metadata: {
|
|
720
|
+
projectId: project.id,
|
|
721
|
+
projectName: project.name,
|
|
722
|
+
eventTypeName: event.eventTypeName,
|
|
723
|
+
created: event.created
|
|
724
|
+
},
|
|
725
|
+
configuration: this._sanitize(event)
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
} catch (projectErr) {
|
|
729
|
+
this.logger('debug', `No recent events in project ${project.id}`, {
|
|
730
|
+
projectId: project.id,
|
|
731
|
+
error: projectErr.message
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
this.logger('info', `Collected MongoDB Atlas events`);
|
|
737
|
+
} catch (err) {
|
|
738
|
+
this.logger('error', 'Failed to collect MongoDB Atlas events', {
|
|
739
|
+
error: err.message,
|
|
740
|
+
stack: err.stack
|
|
741
|
+
});
|
|
742
|
+
throw err;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Sanitize configuration by removing sensitive data.
|
|
748
|
+
*/
|
|
749
|
+
_sanitize(config) {
|
|
750
|
+
if (!config || typeof config !== 'object') return config;
|
|
751
|
+
|
|
752
|
+
const sanitized = { ...config };
|
|
753
|
+
const sensitiveFields = [
|
|
754
|
+
'password',
|
|
755
|
+
'privateKey',
|
|
756
|
+
'apiKey',
|
|
757
|
+
'connectionStrings',
|
|
758
|
+
'mongoURI',
|
|
759
|
+
'mongoURIUpdated',
|
|
760
|
+
'mongoURIWithOptions'
|
|
761
|
+
];
|
|
762
|
+
|
|
763
|
+
for (const field of sensitiveFields) {
|
|
764
|
+
if (field in sanitized) {
|
|
765
|
+
sanitized[field] = '***REDACTED***';
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return sanitized;
|
|
770
|
+
}
|
|
771
|
+
}
|