s3db.js 13.5.1 → 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.
Files changed (105) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +30323 -24958
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +24026 -18654
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +216 -20
  7. package/src/concerns/id.js +90 -6
  8. package/src/concerns/index.js +2 -1
  9. package/src/concerns/password-hashing.js +150 -0
  10. package/src/database.class.js +4 -0
  11. package/src/plugins/api/auth/basic-auth.js +23 -1
  12. package/src/plugins/api/auth/index.js +49 -3
  13. package/src/plugins/api/auth/oauth2-auth.js +171 -0
  14. package/src/plugins/api/auth/oidc-auth.js +789 -0
  15. package/src/plugins/api/auth/oidc-client.js +462 -0
  16. package/src/plugins/api/auth/path-auth-matcher.js +284 -0
  17. package/src/plugins/api/concerns/event-emitter.js +134 -0
  18. package/src/plugins/api/concerns/failban-manager.js +651 -0
  19. package/src/plugins/api/concerns/guards-helpers.js +402 -0
  20. package/src/plugins/api/concerns/metrics-collector.js +346 -0
  21. package/src/plugins/api/index.js +503 -54
  22. package/src/plugins/api/middlewares/failban.js +305 -0
  23. package/src/plugins/api/middlewares/rate-limit.js +301 -0
  24. package/src/plugins/api/middlewares/request-id.js +74 -0
  25. package/src/plugins/api/middlewares/security-headers.js +120 -0
  26. package/src/plugins/api/middlewares/session-tracking.js +194 -0
  27. package/src/plugins/api/routes/auth-routes.js +23 -3
  28. package/src/plugins/api/routes/resource-routes.js +71 -29
  29. package/src/plugins/api/server.js +1017 -94
  30. package/src/plugins/api/utils/guards.js +213 -0
  31. package/src/plugins/api/utils/mime-types.js +154 -0
  32. package/src/plugins/api/utils/openapi-generator.js +44 -11
  33. package/src/plugins/api/utils/path-matcher.js +173 -0
  34. package/src/plugins/api/utils/static-filesystem.js +262 -0
  35. package/src/plugins/api/utils/static-s3.js +231 -0
  36. package/src/plugins/api/utils/template-engine.js +188 -0
  37. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  38. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  39. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  40. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  41. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  42. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  43. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  44. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  45. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  46. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  47. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  48. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  49. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  50. package/src/plugins/cloud-inventory/index.js +20 -0
  51. package/src/plugins/cloud-inventory/registry.js +146 -0
  52. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  53. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  54. package/src/plugins/concerns/plugin-dependencies.js +61 -1
  55. package/src/plugins/eventual-consistency/analytics.js +1 -0
  56. package/src/plugins/identity/README.md +335 -0
  57. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  58. package/src/plugins/identity/concerns/password.js +138 -0
  59. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  60. package/src/plugins/identity/concerns/token-generator.js +172 -0
  61. package/src/plugins/identity/email-service.js +422 -0
  62. package/src/plugins/identity/index.js +1052 -0
  63. package/src/plugins/identity/oauth2-server.js +1033 -0
  64. package/src/plugins/identity/oidc-discovery.js +285 -0
  65. package/src/plugins/identity/rsa-keys.js +323 -0
  66. package/src/plugins/identity/server.js +500 -0
  67. package/src/plugins/identity/session-manager.js +453 -0
  68. package/src/plugins/identity/ui/layouts/base.js +251 -0
  69. package/src/plugins/identity/ui/middleware.js +135 -0
  70. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  71. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  72. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  73. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  74. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  75. package/src/plugins/identity/ui/pages/consent.js +262 -0
  76. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  77. package/src/plugins/identity/ui/pages/login.js +144 -0
  78. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  79. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  80. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  81. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  82. package/src/plugins/identity/ui/pages/profile.js +361 -0
  83. package/src/plugins/identity/ui/pages/register.js +226 -0
  84. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  85. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  86. package/src/plugins/identity/ui/routes.js +2541 -0
  87. package/src/plugins/identity/ui/styles/main.css +465 -0
  88. package/src/plugins/index.js +4 -1
  89. package/src/plugins/ml/base-model.class.js +32 -7
  90. package/src/plugins/ml/classification-model.class.js +1 -1
  91. package/src/plugins/ml/timeseries-model.class.js +3 -1
  92. package/src/plugins/ml.plugin.js +124 -32
  93. package/src/plugins/shared/error-handler.js +147 -0
  94. package/src/plugins/shared/index.js +9 -0
  95. package/src/plugins/shared/middlewares/compression.js +117 -0
  96. package/src/plugins/shared/middlewares/cors.js +49 -0
  97. package/src/plugins/shared/middlewares/index.js +11 -0
  98. package/src/plugins/shared/middlewares/logging.js +54 -0
  99. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  100. package/src/plugins/shared/middlewares/security.js +158 -0
  101. package/src/plugins/shared/response-formatter.js +264 -0
  102. package/src/resource.class.js +140 -12
  103. package/src/schema.class.js +30 -1
  104. package/src/validator.class.js +57 -6
  105. package/dist/s3db.cjs.js.map +0 -1
@@ -0,0 +1,99 @@
1
+ /**
2
+ * BaseCloudDriver - abstract class for cloud inventory drivers
3
+ *
4
+ * Concrete drivers must implement at least:
5
+ * - async initialize(): Perform any lazy connections or credential checks
6
+ * - async listResources(options): Return an iterable of discovered resources
7
+ *
8
+ * A discovered resource should follow the shape:
9
+ * {
10
+ * provider: 'aws' | 'gcp' | ...,
11
+ * accountId: string,
12
+ * subscriptionId?: string,
13
+ * organizationId?: string,
14
+ * projectId?: string,
15
+ * region?: string,
16
+ * service?: string,
17
+ * resourceType: string,
18
+ * resourceId: string,
19
+ * name?: string,
20
+ * tags?: Record<string,string>,
21
+ * labels?: Record<string,string>,
22
+ * attributes?: Record<string, unknown>,
23
+ * configuration: Record<string, unknown>,
24
+ * raw?: unknown
25
+ * }
26
+ *
27
+ * The plugin normalizes the payload, computes configuration digests and
28
+ * manages versioning/diffing.
29
+ */
30
+ export class BaseCloudDriver {
31
+ /**
32
+ * @param {Object} options
33
+ * @param {string} options.id - Unique identifier for this cloud source
34
+ * @param {string} options.driver - Driver name (aws, gcp, do, ...)
35
+ * @param {Object} options.credentials - Authentication material
36
+ * @param {Object} options.config - Driver specific configuration
37
+ * @param {Object} options.globals - Global plugin options
38
+ * @param {Function} options.logger - Optional logger fn (level, msg, meta)
39
+ */
40
+ constructor(options = {}) {
41
+ const {
42
+ id,
43
+ driver,
44
+ credentials = {},
45
+ config = {},
46
+ globals = {},
47
+ logger = null
48
+ } = options;
49
+
50
+ if (!driver) {
51
+ throw new Error('Cloud driver requires a "driver" identifier');
52
+ }
53
+
54
+ this.id = id || driver;
55
+ this.driver = driver;
56
+ this.credentials = credentials;
57
+ this.config = config;
58
+ this.globals = globals;
59
+ this.logger = typeof logger === 'function'
60
+ ? logger
61
+ : () => {};
62
+ }
63
+
64
+ /**
65
+ * Perform driver bootstrapping (auth warm-up, SDK clients, etc).
66
+ * Default implementation is a no-op.
67
+ */
68
+ async initialize() {
69
+ return;
70
+ }
71
+
72
+ /**
73
+ * Fetch resources from the cloud API.
74
+ * Must be implemented by subclasses.
75
+ * @param {Object} options
76
+ * @returns {Promise<Array<Object>|AsyncIterable<Object>>}
77
+ */
78
+ // eslint-disable-next-line no-unused-vars
79
+ async listResources(options = {}) {
80
+ throw new Error(`Driver "${this.driver}" does not implement listResources()`);
81
+ }
82
+
83
+ /**
84
+ * Optional health check hook.
85
+ * @returns {Promise<{ok: boolean, details?: any}>}
86
+ */
87
+ async healthCheck() {
88
+ return { ok: true };
89
+ }
90
+
91
+ /**
92
+ * Graceful shutdown hook for long-lived SDK clients.
93
+ */
94
+ async destroy() {
95
+ return;
96
+ }
97
+ }
98
+
99
+ export default BaseCloudDriver;
@@ -0,0 +1,620 @@
1
+ import { BaseCloudDriver } from './base-driver.js';
2
+
3
+ /**
4
+ * Production-ready Cloudflare inventory driver using official cloudflare SDK.
5
+ *
6
+ * Covers 11+ services with 15+ edge computing resource types:
7
+ * - Edge Computing (Workers, Pages, Durable Objects)
8
+ * - Storage (R2 buckets, KV namespaces, D1 databases)
9
+ * - Networking (Zones, DNS records, Load Balancers)
10
+ * - Security (SSL/TLS Certificates, WAF Rulesets, Access Applications/Policies)
11
+ *
12
+ * @see https://developers.cloudflare.com/api/
13
+ * @see https://www.npmjs.com/package/cloudflare
14
+ */
15
+ export class CloudflareInventoryDriver extends BaseCloudDriver {
16
+ constructor(options = {}) {
17
+ super({ ...options, driver: options.driver || 'cloudflare' });
18
+
19
+ this._apiToken = null;
20
+ this._accountId = null;
21
+ this._client = null;
22
+
23
+ // Services to collect (can be filtered via config.services)
24
+ this._services = this.config?.services || [
25
+ 'workers',
26
+ 'r2',
27
+ 'pages',
28
+ 'd1',
29
+ 'kv',
30
+ 'durable-objects',
31
+ 'zones',
32
+ 'loadbalancers',
33
+ 'certificates',
34
+ 'waf',
35
+ 'access'
36
+ ];
37
+ }
38
+
39
+ /**
40
+ * Initialize Cloudflare API client.
41
+ */
42
+ async _initializeClient() {
43
+ if (this._client) return;
44
+
45
+ const credentials = this.credentials || {};
46
+ this._apiToken = credentials.apiToken || credentials.token || process.env.CLOUDFLARE_API_TOKEN;
47
+ this._accountId = credentials.accountId || this.config?.accountId || process.env.CLOUDFLARE_ACCOUNT_ID;
48
+
49
+ if (!this._apiToken) {
50
+ throw new Error('Cloudflare API token is required. Provide via credentials.apiToken or CLOUDFLARE_API_TOKEN env var.');
51
+ }
52
+
53
+ // Lazy import
54
+ const Cloudflare = await import('cloudflare');
55
+ this._client = new Cloudflare.default({
56
+ apiToken: this._apiToken
57
+ });
58
+
59
+ this.logger('info', 'Cloudflare API client initialized', {
60
+ accountId: this._accountId,
61
+ services: this._services.length
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Main entry point - lists all resources from configured services.
67
+ */
68
+ async *listResources(options = {}) {
69
+ await this._initializeClient();
70
+
71
+ const serviceCollectors = {
72
+ workers: () => this._collectWorkers(),
73
+ r2: () => this._collectR2(),
74
+ pages: () => this._collectPages(),
75
+ 'd1': () => this._collectD1(),
76
+ kv: () => this._collectKV(),
77
+ 'durable-objects': () => this._collectDurableObjects(),
78
+ zones: () => this._collectZones(),
79
+ loadbalancers: () => this._collectLoadBalancers(),
80
+ certificates: () => this._collectCertificates(),
81
+ waf: () => this._collectWAF(),
82
+ access: () => this._collectAccess()
83
+ };
84
+
85
+ for (const service of this._services) {
86
+ const collector = serviceCollectors[service];
87
+ if (!collector) {
88
+ this.logger('warn', `Unknown Cloudflare service: ${service}`, { service });
89
+ continue;
90
+ }
91
+
92
+ try {
93
+ this.logger('info', `Collecting Cloudflare ${service} resources`, { service });
94
+ yield* collector();
95
+ } catch (err) {
96
+ // Continue with next service instead of failing entire sync
97
+ this.logger('error', `Cloudflare service collection failed, skipping to next service`, {
98
+ service,
99
+ error: err.message,
100
+ errorName: err.name,
101
+ stack: err.stack
102
+ });
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Collect Workers scripts.
109
+ */
110
+ async *_collectWorkers() {
111
+ try {
112
+ if (!this._accountId) {
113
+ this.logger('warn', 'Account ID required for Workers collection, skipping');
114
+ return;
115
+ }
116
+
117
+ const scripts = await this._client.workers.scripts.list({ account_id: this._accountId });
118
+
119
+ for (const script of scripts) {
120
+ yield {
121
+ provider: 'cloudflare',
122
+ accountId: this._accountId,
123
+ region: 'global', // Workers are global/edge
124
+ service: 'workers',
125
+ resourceType: 'cloudflare.workers.script',
126
+ resourceId: script.id,
127
+ name: script.id,
128
+ tags: script.tags || [],
129
+ configuration: this._sanitize(script)
130
+ };
131
+ }
132
+
133
+ this.logger('info', `Collected ${scripts.length || 0} Cloudflare Workers scripts`);
134
+ } catch (err) {
135
+ this.logger('error', 'Failed to collect Cloudflare Workers', {
136
+ error: err.message,
137
+ stack: err.stack
138
+ });
139
+ throw err;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Collect R2 buckets.
145
+ */
146
+ async *_collectR2() {
147
+ try {
148
+ if (!this._accountId) {
149
+ this.logger('warn', 'Account ID required for R2 collection, skipping');
150
+ return;
151
+ }
152
+
153
+ const buckets = await this._client.r2.buckets.list({ account_id: this._accountId });
154
+
155
+ for (const bucket of buckets) {
156
+ yield {
157
+ provider: 'cloudflare',
158
+ accountId: this._accountId,
159
+ region: bucket.location || 'global',
160
+ service: 'r2',
161
+ resourceType: 'cloudflare.r2.bucket',
162
+ resourceId: bucket.name,
163
+ name: bucket.name,
164
+ tags: [],
165
+ configuration: this._sanitize(bucket)
166
+ };
167
+ }
168
+
169
+ this.logger('info', `Collected ${buckets.length || 0} Cloudflare R2 buckets`);
170
+ } catch (err) {
171
+ this.logger('error', 'Failed to collect Cloudflare R2', {
172
+ error: err.message,
173
+ stack: err.stack
174
+ });
175
+ throw err;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Collect Pages projects.
181
+ */
182
+ async *_collectPages() {
183
+ try {
184
+ if (!this._accountId) {
185
+ this.logger('warn', 'Account ID required for Pages collection, skipping');
186
+ return;
187
+ }
188
+
189
+ const projects = await this._client.pages.projects.list({ account_id: this._accountId });
190
+
191
+ for (const project of projects) {
192
+ yield {
193
+ provider: 'cloudflare',
194
+ accountId: this._accountId,
195
+ region: 'global',
196
+ service: 'pages',
197
+ resourceType: 'cloudflare.pages.project',
198
+ resourceId: project.id || project.name,
199
+ name: project.name,
200
+ tags: [],
201
+ configuration: this._sanitize(project)
202
+ };
203
+ }
204
+
205
+ this.logger('info', `Collected ${projects.length || 0} Cloudflare Pages projects`);
206
+ } catch (err) {
207
+ this.logger('error', 'Failed to collect Cloudflare Pages', {
208
+ error: err.message,
209
+ stack: err.stack
210
+ });
211
+ throw err;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Collect D1 databases.
217
+ */
218
+ async *_collectD1() {
219
+ try {
220
+ if (!this._accountId) {
221
+ this.logger('warn', 'Account ID required for D1 collection, skipping');
222
+ return;
223
+ }
224
+
225
+ const databases = await this._client.d1.database.list({ account_id: this._accountId });
226
+
227
+ for (const database of databases) {
228
+ yield {
229
+ provider: 'cloudflare',
230
+ accountId: this._accountId,
231
+ region: 'global',
232
+ service: 'd1',
233
+ resourceType: 'cloudflare.d1.database',
234
+ resourceId: database.uuid,
235
+ name: database.name,
236
+ tags: [],
237
+ configuration: this._sanitize(database)
238
+ };
239
+ }
240
+
241
+ this.logger('info', `Collected ${databases.length || 0} Cloudflare D1 databases`);
242
+ } catch (err) {
243
+ this.logger('error', 'Failed to collect Cloudflare D1', {
244
+ error: err.message,
245
+ stack: err.stack
246
+ });
247
+ throw err;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Collect KV namespaces.
253
+ */
254
+ async *_collectKV() {
255
+ try {
256
+ if (!this._accountId) {
257
+ this.logger('warn', 'Account ID required for KV collection, skipping');
258
+ return;
259
+ }
260
+
261
+ const namespaces = await this._client.kv.namespaces.list({ account_id: this._accountId });
262
+
263
+ for (const namespace of namespaces) {
264
+ yield {
265
+ provider: 'cloudflare',
266
+ accountId: this._accountId,
267
+ region: 'global',
268
+ service: 'kv',
269
+ resourceType: 'cloudflare.kv.namespace',
270
+ resourceId: namespace.id,
271
+ name: namespace.title,
272
+ tags: [],
273
+ configuration: this._sanitize(namespace)
274
+ };
275
+ }
276
+
277
+ this.logger('info', `Collected ${namespaces.length || 0} Cloudflare KV namespaces`);
278
+ } catch (err) {
279
+ this.logger('error', 'Failed to collect Cloudflare KV', {
280
+ error: err.message,
281
+ stack: err.stack
282
+ });
283
+ throw err;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Collect Durable Objects namespaces.
289
+ */
290
+ async *_collectDurableObjects() {
291
+ try {
292
+ if (!this._accountId) {
293
+ this.logger('warn', 'Account ID required for Durable Objects collection, skipping');
294
+ return;
295
+ }
296
+
297
+ const namespaces = await this._client.durableObjects.namespaces.list({ account_id: this._accountId });
298
+
299
+ for (const namespace of namespaces) {
300
+ yield {
301
+ provider: 'cloudflare',
302
+ accountId: this._accountId,
303
+ region: 'global',
304
+ service: 'durable-objects',
305
+ resourceType: 'cloudflare.durableobjects.namespace',
306
+ resourceId: namespace.id,
307
+ name: namespace.name,
308
+ tags: [],
309
+ configuration: this._sanitize(namespace)
310
+ };
311
+ }
312
+
313
+ this.logger('info', `Collected ${namespaces.length || 0} Cloudflare Durable Objects namespaces`);
314
+ } catch (err) {
315
+ this.logger('error', 'Failed to collect Cloudflare Durable Objects', {
316
+ error: err.message,
317
+ stack: err.stack
318
+ });
319
+ throw err;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Collect Zones (domains) and DNS records.
325
+ */
326
+ async *_collectZones() {
327
+ try {
328
+ const zones = await this._client.zones.list();
329
+
330
+ for (const zone of zones) {
331
+ yield {
332
+ provider: 'cloudflare',
333
+ accountId: this._accountId || zone.account?.id,
334
+ region: 'global',
335
+ service: 'zones',
336
+ resourceType: 'cloudflare.zone',
337
+ resourceId: zone.id,
338
+ name: zone.name,
339
+ tags: [],
340
+ configuration: this._sanitize(zone)
341
+ };
342
+
343
+ // Collect DNS records for this zone
344
+ try {
345
+ const records = await this._client.dns.records.list({ zone_id: zone.id });
346
+
347
+ for (const record of records) {
348
+ yield {
349
+ provider: 'cloudflare',
350
+ accountId: this._accountId || zone.account?.id,
351
+ region: 'global',
352
+ service: 'zones',
353
+ resourceType: 'cloudflare.dns.record',
354
+ resourceId: record.id,
355
+ name: `${record.name} (${record.type})`,
356
+ tags: record.tags || [],
357
+ metadata: { zoneId: zone.id, zoneName: zone.name },
358
+ configuration: this._sanitize(record)
359
+ };
360
+ }
361
+ } catch (recordErr) {
362
+ this.logger('warn', `Failed to collect DNS records for zone ${zone.name}`, {
363
+ zoneId: zone.id,
364
+ error: recordErr.message
365
+ });
366
+ }
367
+ }
368
+
369
+ this.logger('info', `Collected ${zones.length || 0} Cloudflare zones`);
370
+ } catch (err) {
371
+ this.logger('error', 'Failed to collect Cloudflare zones', {
372
+ error: err.message,
373
+ stack: err.stack
374
+ });
375
+ throw err;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Collect Load Balancers.
381
+ */
382
+ async *_collectLoadBalancers() {
383
+ try {
384
+ const zones = await this._client.zones.list();
385
+
386
+ for (const zone of zones) {
387
+ try {
388
+ const loadBalancers = await this._client.loadBalancers.list({ zone_id: zone.id });
389
+
390
+ for (const lb of loadBalancers) {
391
+ yield {
392
+ provider: 'cloudflare',
393
+ accountId: this._accountId || zone.account?.id,
394
+ region: 'global',
395
+ service: 'loadbalancers',
396
+ resourceType: 'cloudflare.loadbalancer',
397
+ resourceId: lb.id,
398
+ name: lb.name,
399
+ tags: [],
400
+ metadata: { zoneId: zone.id, zoneName: zone.name },
401
+ configuration: this._sanitize(lb)
402
+ };
403
+ }
404
+ } catch (lbErr) {
405
+ this.logger('debug', `No load balancers in zone ${zone.name}`, {
406
+ zoneId: zone.id,
407
+ error: lbErr.message
408
+ });
409
+ }
410
+ }
411
+
412
+ this.logger('info', `Collected Cloudflare load balancers`);
413
+ } catch (err) {
414
+ this.logger('error', 'Failed to collect Cloudflare load balancers', {
415
+ error: err.message,
416
+ stack: err.stack
417
+ });
418
+ throw err;
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Collect SSL/TLS Certificates.
424
+ */
425
+ async *_collectCertificates() {
426
+ try {
427
+ const zones = await this._client.zones.list();
428
+
429
+ for (const zone of zones) {
430
+ try {
431
+ // Collect SSL/TLS certificates for each zone
432
+ const certificates = await this._client.ssl.certificatePacks.list({ zone_id: zone.id });
433
+
434
+ for (const cert of certificates) {
435
+ yield {
436
+ provider: 'cloudflare',
437
+ accountId: this._accountId || zone.account?.id,
438
+ region: 'global',
439
+ service: 'certificates',
440
+ resourceType: 'cloudflare.ssl.certificate',
441
+ resourceId: cert.id,
442
+ name: `${zone.name} - ${cert.type}`,
443
+ tags: [],
444
+ metadata: {
445
+ zoneId: zone.id,
446
+ zoneName: zone.name,
447
+ type: cert.type,
448
+ status: cert.status
449
+ },
450
+ configuration: this._sanitize(cert)
451
+ };
452
+ }
453
+ } catch (certErr) {
454
+ this.logger('debug', `No certificates in zone ${zone.name}`, {
455
+ zoneId: zone.id,
456
+ error: certErr.message
457
+ });
458
+ }
459
+ }
460
+
461
+ this.logger('info', `Collected Cloudflare certificates`);
462
+ } catch (err) {
463
+ this.logger('error', 'Failed to collect Cloudflare certificates', {
464
+ error: err.message,
465
+ stack: err.stack
466
+ });
467
+ throw err;
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Collect WAF (Web Application Firewall) Rulesets via new Rulesets API (2025).
473
+ */
474
+ async *_collectWAF() {
475
+ try {
476
+ const zones = await this._client.zones.list();
477
+
478
+ for (const zone of zones) {
479
+ try {
480
+ // Collect WAF rulesets using the new Rulesets API
481
+ const rulesets = await this._client.rulesets.list({ zone_id: zone.id });
482
+
483
+ for (const ruleset of rulesets) {
484
+ // Only collect WAF-related rulesets
485
+ if (ruleset.phase && (ruleset.phase.includes('http_') || ruleset.phase.includes('firewall'))) {
486
+ yield {
487
+ provider: 'cloudflare',
488
+ accountId: this._accountId || zone.account?.id,
489
+ region: 'global',
490
+ service: 'waf',
491
+ resourceType: 'cloudflare.waf.ruleset',
492
+ resourceId: ruleset.id,
493
+ name: ruleset.name,
494
+ tags: [],
495
+ metadata: {
496
+ zoneId: zone.id,
497
+ zoneName: zone.name,
498
+ phase: ruleset.phase,
499
+ kind: ruleset.kind
500
+ },
501
+ configuration: this._sanitize(ruleset)
502
+ };
503
+ }
504
+ }
505
+ } catch (wafErr) {
506
+ this.logger('debug', `No WAF rulesets in zone ${zone.name}`, {
507
+ zoneId: zone.id,
508
+ error: wafErr.message
509
+ });
510
+ }
511
+ }
512
+
513
+ this.logger('info', `Collected Cloudflare WAF rulesets`);
514
+ } catch (err) {
515
+ this.logger('error', 'Failed to collect Cloudflare WAF', {
516
+ error: err.message,
517
+ stack: err.stack
518
+ });
519
+ throw err;
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Collect Cloudflare Access Applications (Zero Trust).
525
+ */
526
+ async *_collectAccess() {
527
+ try {
528
+ if (!this._accountId) {
529
+ this.logger('warn', 'Account ID required for Access collection, skipping');
530
+ return;
531
+ }
532
+
533
+ // Collect Access Applications
534
+ const applications = await this._client.access.applications.list({ account_id: this._accountId });
535
+
536
+ for (const app of applications) {
537
+ yield {
538
+ provider: 'cloudflare',
539
+ accountId: this._accountId,
540
+ region: 'global',
541
+ service: 'access',
542
+ resourceType: 'cloudflare.access.application',
543
+ resourceId: app.id,
544
+ name: app.name,
545
+ tags: [],
546
+ metadata: {
547
+ domain: app.domain,
548
+ type: app.type
549
+ },
550
+ configuration: this._sanitize(app)
551
+ };
552
+
553
+ // Collect Access Policies for this application
554
+ try {
555
+ const policies = await this._client.access.policies.list({
556
+ account_id: this._accountId,
557
+ application_id: app.id
558
+ });
559
+
560
+ for (const policy of policies) {
561
+ yield {
562
+ provider: 'cloudflare',
563
+ accountId: this._accountId,
564
+ region: 'global',
565
+ service: 'access',
566
+ resourceType: 'cloudflare.access.policy',
567
+ resourceId: policy.id,
568
+ name: policy.name,
569
+ tags: [],
570
+ metadata: {
571
+ applicationId: app.id,
572
+ applicationName: app.name,
573
+ decision: policy.decision
574
+ },
575
+ configuration: this._sanitize(policy)
576
+ };
577
+ }
578
+ } catch (policyErr) {
579
+ this.logger('warn', `Failed to collect policies for Access application ${app.name}`, {
580
+ applicationId: app.id,
581
+ error: policyErr.message
582
+ });
583
+ }
584
+ }
585
+
586
+ this.logger('info', `Collected Cloudflare Access applications`);
587
+ } catch (err) {
588
+ this.logger('error', 'Failed to collect Cloudflare Access', {
589
+ error: err.message,
590
+ stack: err.stack
591
+ });
592
+ throw err;
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Sanitize configuration by removing sensitive data.
598
+ */
599
+ _sanitize(config) {
600
+ if (!config || typeof config !== 'object') return config;
601
+
602
+ const sanitized = { ...config };
603
+ const sensitiveFields = [
604
+ 'api_token',
605
+ 'api_key',
606
+ 'token',
607
+ 'secret',
608
+ 'password',
609
+ 'private_key'
610
+ ];
611
+
612
+ for (const field of sensitiveFields) {
613
+ if (field in sanitized) {
614
+ sanitized[field] = '***REDACTED***';
615
+ }
616
+ }
617
+
618
+ return sanitized;
619
+ }
620
+ }