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,853 @@
|
|
|
1
|
+
import { BaseCloudDriver } from './base-driver.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Production-ready Alibaba Cloud (Aliyun) inventory driver using @alicloud SDK.
|
|
5
|
+
*
|
|
6
|
+
* Covers 15+ services with 40+ resource types:
|
|
7
|
+
* - Compute (ECS instances, ACK clusters, snapshots)
|
|
8
|
+
* - Storage (OSS buckets, disks)
|
|
9
|
+
* - Databases (RDS instances, Redis)
|
|
10
|
+
* - Networking (VPC, vSwitches, SLB, EIP, Security Groups, NAT Gateway)
|
|
11
|
+
* - Auto Scaling (Scaling Groups, Scaling Configurations)
|
|
12
|
+
* - Container Registry (ACR repositories)
|
|
13
|
+
* - CDN, DNS
|
|
14
|
+
*
|
|
15
|
+
* @see https://www.alibabacloud.com/help/doc-detail/57342.htm
|
|
16
|
+
* @see https://github.com/aliyun/aliyun-openapi-nodejs-sdk
|
|
17
|
+
*/
|
|
18
|
+
export class AlibabaInventoryDriver extends BaseCloudDriver {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
super({ ...options, driver: options.driver || 'alibaba' });
|
|
21
|
+
|
|
22
|
+
this._accessKeyId = null;
|
|
23
|
+
this._accessKeySecret = null;
|
|
24
|
+
this._accountId = this.config?.accountId || 'alibaba';
|
|
25
|
+
|
|
26
|
+
// Services to collect (can be filtered via config.services)
|
|
27
|
+
this._services = this.config?.services || [
|
|
28
|
+
'ecs',
|
|
29
|
+
'ack',
|
|
30
|
+
'oss',
|
|
31
|
+
'rds',
|
|
32
|
+
'redis',
|
|
33
|
+
'vpc',
|
|
34
|
+
'slb',
|
|
35
|
+
'eip',
|
|
36
|
+
'cdn',
|
|
37
|
+
'dns',
|
|
38
|
+
'securitygroups',
|
|
39
|
+
'snapshots',
|
|
40
|
+
'autoscaling',
|
|
41
|
+
'natgateway',
|
|
42
|
+
'acr'
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Regions to scan (can be filtered via config.regions)
|
|
46
|
+
this._regions = this.config?.regions || ['cn-hangzhou', 'cn-shanghai', 'cn-beijing'];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize Alibaba Cloud credentials.
|
|
51
|
+
*/
|
|
52
|
+
async _initializeCredentials() {
|
|
53
|
+
if (this._accessKeyId) return;
|
|
54
|
+
|
|
55
|
+
const credentials = this.credentials || {};
|
|
56
|
+
this._accessKeyId = credentials.accessKeyId || process.env.ALIBABA_CLOUD_ACCESS_KEY_ID;
|
|
57
|
+
this._accessKeySecret = credentials.accessKeySecret || process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET;
|
|
58
|
+
|
|
59
|
+
if (!this._accessKeyId || !this._accessKeySecret) {
|
|
60
|
+
throw new Error('Alibaba Cloud AccessKeyId and AccessKeySecret are required. Provide via credentials or env vars.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.logger('info', 'Alibaba Cloud credentials initialized', {
|
|
64
|
+
accountId: this._accountId,
|
|
65
|
+
services: this._services.length,
|
|
66
|
+
regions: this._regions.length
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create RPC client for a specific service.
|
|
72
|
+
*/
|
|
73
|
+
async _createRPCClient(endpoint, apiVersion) {
|
|
74
|
+
const RPCClient = await import('@alicloud/pop-core');
|
|
75
|
+
|
|
76
|
+
return new RPCClient.default({
|
|
77
|
+
accessKeyId: this._accessKeyId,
|
|
78
|
+
accessKeySecret: this._accessKeySecret,
|
|
79
|
+
endpoint,
|
|
80
|
+
apiVersion
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Main entry point - lists all resources from configured services.
|
|
86
|
+
*/
|
|
87
|
+
async *listResources(options = {}) {
|
|
88
|
+
await this._initializeCredentials();
|
|
89
|
+
|
|
90
|
+
const serviceCollectors = {
|
|
91
|
+
ecs: () => this._collectECS(),
|
|
92
|
+
ack: () => this._collectACK(),
|
|
93
|
+
oss: () => this._collectOSS(),
|
|
94
|
+
rds: () => this._collectRDS(),
|
|
95
|
+
redis: () => this._collectRedis(),
|
|
96
|
+
vpc: () => this._collectVPC(),
|
|
97
|
+
slb: () => this._collectSLB(),
|
|
98
|
+
eip: () => this._collectEIP(),
|
|
99
|
+
cdn: () => this._collectCDN(),
|
|
100
|
+
dns: () => this._collectDNS(),
|
|
101
|
+
securitygroups: () => this._collectSecurityGroups(),
|
|
102
|
+
snapshots: () => this._collectSnapshots(),
|
|
103
|
+
autoscaling: () => this._collectAutoScaling(),
|
|
104
|
+
natgateway: () => this._collectNATGateway(),
|
|
105
|
+
acr: () => this._collectACR()
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (const service of this._services) {
|
|
109
|
+
const collector = serviceCollectors[service];
|
|
110
|
+
if (!collector) {
|
|
111
|
+
this.logger('warn', `Unknown Alibaba Cloud service: ${service}`, { service });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
this.logger('info', `Collecting Alibaba Cloud ${service} resources`, { service });
|
|
117
|
+
yield* collector();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
// Continue with next service instead of failing entire sync
|
|
120
|
+
this.logger('error', `Alibaba Cloud service collection failed, skipping to next service`, {
|
|
121
|
+
service,
|
|
122
|
+
error: err.message,
|
|
123
|
+
errorName: err.name,
|
|
124
|
+
stack: err.stack
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Collect ECS instances.
|
|
132
|
+
*/
|
|
133
|
+
async *_collectECS() {
|
|
134
|
+
try {
|
|
135
|
+
for (const region of this._regions) {
|
|
136
|
+
const client = await this._createRPCClient(`https://ecs.${region}.aliyuncs.com`, '2014-05-26');
|
|
137
|
+
|
|
138
|
+
const params = {
|
|
139
|
+
RegionId: region,
|
|
140
|
+
PageSize: 100
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const response = await client.request('DescribeInstances', params, { method: 'POST' });
|
|
144
|
+
const instances = response.Instances?.Instance || [];
|
|
145
|
+
|
|
146
|
+
for (const instance of instances) {
|
|
147
|
+
yield {
|
|
148
|
+
provider: 'alibaba',
|
|
149
|
+
accountId: this._accountId,
|
|
150
|
+
region,
|
|
151
|
+
service: 'ecs',
|
|
152
|
+
resourceType: 'alibaba.ecs.instance',
|
|
153
|
+
resourceId: instance.InstanceId,
|
|
154
|
+
name: instance.InstanceName,
|
|
155
|
+
tags: this._extractTags(instance.Tags?.Tag),
|
|
156
|
+
configuration: this._sanitize(instance)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.logger('info', `Collected ${instances.length} ECS instances in ${region}`);
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
this.logger('error', 'Failed to collect Alibaba Cloud ECS', {
|
|
164
|
+
error: err.message,
|
|
165
|
+
stack: err.stack
|
|
166
|
+
});
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Collect ACK (Container Service for Kubernetes) clusters.
|
|
173
|
+
*/
|
|
174
|
+
async *_collectACK() {
|
|
175
|
+
try {
|
|
176
|
+
for (const region of this._regions) {
|
|
177
|
+
const client = await this._createRPCClient(`https://cs.${region}.aliyuncs.com`, '2015-12-15');
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const response = await client.request('DescribeClustersV1', {}, { method: 'GET' });
|
|
181
|
+
const clusters = response.clusters || [];
|
|
182
|
+
|
|
183
|
+
for (const cluster of clusters) {
|
|
184
|
+
yield {
|
|
185
|
+
provider: 'alibaba',
|
|
186
|
+
accountId: this._accountId,
|
|
187
|
+
region,
|
|
188
|
+
service: 'ack',
|
|
189
|
+
resourceType: 'alibaba.ack.cluster',
|
|
190
|
+
resourceId: cluster.cluster_id,
|
|
191
|
+
name: cluster.name,
|
|
192
|
+
tags: this._extractTags(cluster.tags),
|
|
193
|
+
configuration: this._sanitize(cluster)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.logger('info', `Collected ${clusters.length} ACK clusters in ${region}`);
|
|
198
|
+
} catch (regionErr) {
|
|
199
|
+
// ACK may not be available in all regions
|
|
200
|
+
this.logger('debug', `ACK not available in ${region}`, { region, error: regionErr.message });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
this.logger('error', 'Failed to collect Alibaba Cloud ACK', {
|
|
205
|
+
error: err.message,
|
|
206
|
+
stack: err.stack
|
|
207
|
+
});
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Collect OSS buckets.
|
|
214
|
+
*/
|
|
215
|
+
async *_collectOSS() {
|
|
216
|
+
try {
|
|
217
|
+
const OSS = await import('ali-oss');
|
|
218
|
+
|
|
219
|
+
// OSS client uses a different pattern
|
|
220
|
+
const ossClient = new OSS.default({
|
|
221
|
+
accessKeyId: this._accessKeyId,
|
|
222
|
+
accessKeySecret: this._accessKeySecret,
|
|
223
|
+
region: this._regions[0] // Use first region as default
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const response = await ossClient.listBuckets();
|
|
227
|
+
const buckets = response.buckets || [];
|
|
228
|
+
|
|
229
|
+
for (const bucket of buckets) {
|
|
230
|
+
yield {
|
|
231
|
+
provider: 'alibaba',
|
|
232
|
+
accountId: this._accountId,
|
|
233
|
+
region: bucket.region,
|
|
234
|
+
service: 'oss',
|
|
235
|
+
resourceType: 'alibaba.oss.bucket',
|
|
236
|
+
resourceId: bucket.name,
|
|
237
|
+
name: bucket.name,
|
|
238
|
+
tags: {},
|
|
239
|
+
configuration: this._sanitize(bucket)
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.logger('info', `Collected ${buckets.length} OSS buckets`);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
this.logger('error', 'Failed to collect Alibaba Cloud OSS', {
|
|
246
|
+
error: err.message,
|
|
247
|
+
stack: err.stack
|
|
248
|
+
});
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Collect RDS instances.
|
|
255
|
+
*/
|
|
256
|
+
async *_collectRDS() {
|
|
257
|
+
try {
|
|
258
|
+
for (const region of this._regions) {
|
|
259
|
+
const client = await this._createRPCClient(`https://rds.${region}.aliyuncs.com`, '2014-08-15');
|
|
260
|
+
|
|
261
|
+
const params = {
|
|
262
|
+
RegionId: region,
|
|
263
|
+
PageSize: 100
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const response = await client.request('DescribeDBInstances', params, { method: 'POST' });
|
|
267
|
+
const instances = response.Items?.DBInstance || [];
|
|
268
|
+
|
|
269
|
+
for (const instance of instances) {
|
|
270
|
+
yield {
|
|
271
|
+
provider: 'alibaba',
|
|
272
|
+
accountId: this._accountId,
|
|
273
|
+
region,
|
|
274
|
+
service: 'rds',
|
|
275
|
+
resourceType: 'alibaba.rds.instance',
|
|
276
|
+
resourceId: instance.DBInstanceId,
|
|
277
|
+
name: instance.DBInstanceDescription || instance.DBInstanceId,
|
|
278
|
+
tags: this._extractTags(instance.Tags?.Tag),
|
|
279
|
+
configuration: this._sanitize(instance)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.logger('info', `Collected ${instances.length} RDS instances in ${region}`);
|
|
284
|
+
}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
this.logger('error', 'Failed to collect Alibaba Cloud RDS', {
|
|
287
|
+
error: err.message,
|
|
288
|
+
stack: err.stack
|
|
289
|
+
});
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Collect Redis instances.
|
|
296
|
+
*/
|
|
297
|
+
async *_collectRedis() {
|
|
298
|
+
try {
|
|
299
|
+
for (const region of this._regions) {
|
|
300
|
+
const client = await this._createRPCClient(`https://r-kvstore.${region}.aliyuncs.com`, '2015-01-01');
|
|
301
|
+
|
|
302
|
+
const params = {
|
|
303
|
+
RegionId: region,
|
|
304
|
+
PageSize: 100
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const response = await client.request('DescribeInstances', params, { method: 'POST' });
|
|
308
|
+
const instances = response.Instances?.KVStoreInstance || [];
|
|
309
|
+
|
|
310
|
+
for (const instance of instances) {
|
|
311
|
+
yield {
|
|
312
|
+
provider: 'alibaba',
|
|
313
|
+
accountId: this._accountId,
|
|
314
|
+
region,
|
|
315
|
+
service: 'redis',
|
|
316
|
+
resourceType: 'alibaba.redis.instance',
|
|
317
|
+
resourceId: instance.InstanceId,
|
|
318
|
+
name: instance.InstanceName,
|
|
319
|
+
tags: this._extractTags(instance.Tags?.Tag),
|
|
320
|
+
configuration: this._sanitize(instance)
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this.logger('info', `Collected ${instances.length} Redis instances in ${region}`);
|
|
325
|
+
}
|
|
326
|
+
} catch (err) {
|
|
327
|
+
this.logger('error', 'Failed to collect Alibaba Cloud Redis', {
|
|
328
|
+
error: err.message,
|
|
329
|
+
stack: err.stack
|
|
330
|
+
});
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Collect VPC resources.
|
|
337
|
+
*/
|
|
338
|
+
async *_collectVPC() {
|
|
339
|
+
try {
|
|
340
|
+
for (const region of this._regions) {
|
|
341
|
+
const client = await this._createRPCClient(`https://vpc.${region}.aliyuncs.com`, '2016-04-28');
|
|
342
|
+
|
|
343
|
+
// VPCs
|
|
344
|
+
const vpcParams = {
|
|
345
|
+
RegionId: region,
|
|
346
|
+
PageSize: 50
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const vpcResponse = await client.request('DescribeVpcs', vpcParams, { method: 'POST' });
|
|
350
|
+
const vpcs = vpcResponse.Vpcs?.Vpc || [];
|
|
351
|
+
|
|
352
|
+
for (const vpc of vpcs) {
|
|
353
|
+
yield {
|
|
354
|
+
provider: 'alibaba',
|
|
355
|
+
accountId: this._accountId,
|
|
356
|
+
region,
|
|
357
|
+
service: 'vpc',
|
|
358
|
+
resourceType: 'alibaba.vpc.network',
|
|
359
|
+
resourceId: vpc.VpcId,
|
|
360
|
+
name: vpc.VpcName,
|
|
361
|
+
tags: this._extractTags(vpc.Tags?.Tag),
|
|
362
|
+
configuration: this._sanitize(vpc)
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// vSwitches (subnets)
|
|
366
|
+
try {
|
|
367
|
+
const vswitchParams = {
|
|
368
|
+
VpcId: vpc.VpcId,
|
|
369
|
+
PageSize: 50
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const vswitchResponse = await client.request('DescribeVSwitches', vswitchParams, { method: 'POST' });
|
|
373
|
+
const vswitches = vswitchResponse.VSwitches?.VSwitch || [];
|
|
374
|
+
|
|
375
|
+
for (const vswitch of vswitches) {
|
|
376
|
+
yield {
|
|
377
|
+
provider: 'alibaba',
|
|
378
|
+
accountId: this._accountId,
|
|
379
|
+
region,
|
|
380
|
+
service: 'vpc',
|
|
381
|
+
resourceType: 'alibaba.vpc.vswitch',
|
|
382
|
+
resourceId: vswitch.VSwitchId,
|
|
383
|
+
name: vswitch.VSwitchName,
|
|
384
|
+
tags: this._extractTags(vswitch.Tags?.Tag),
|
|
385
|
+
metadata: { vpcId: vpc.VpcId, vpcName: vpc.VpcName },
|
|
386
|
+
configuration: this._sanitize(vswitch)
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
} catch (vswitchErr) {
|
|
390
|
+
this.logger('warn', `Failed to collect vSwitches for VPC ${vpc.VpcId}`, {
|
|
391
|
+
vpcId: vpc.VpcId,
|
|
392
|
+
error: vswitchErr.message
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.logger('info', `Collected ${vpcs.length} VPCs in ${region}`);
|
|
398
|
+
}
|
|
399
|
+
} catch (err) {
|
|
400
|
+
this.logger('error', 'Failed to collect Alibaba Cloud VPC', {
|
|
401
|
+
error: err.message,
|
|
402
|
+
stack: err.stack
|
|
403
|
+
});
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Collect SLB (Server Load Balancer) instances.
|
|
410
|
+
*/
|
|
411
|
+
async *_collectSLB() {
|
|
412
|
+
try {
|
|
413
|
+
for (const region of this._regions) {
|
|
414
|
+
const client = await this._createRPCClient(`https://slb.${region}.aliyuncs.com`, '2014-05-15');
|
|
415
|
+
|
|
416
|
+
const params = {
|
|
417
|
+
RegionId: region,
|
|
418
|
+
PageSize: 50
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const response = await client.request('DescribeLoadBalancers', params, { method: 'POST' });
|
|
422
|
+
const loadBalancers = response.LoadBalancers?.LoadBalancer || [];
|
|
423
|
+
|
|
424
|
+
for (const lb of loadBalancers) {
|
|
425
|
+
yield {
|
|
426
|
+
provider: 'alibaba',
|
|
427
|
+
accountId: this._accountId,
|
|
428
|
+
region,
|
|
429
|
+
service: 'slb',
|
|
430
|
+
resourceType: 'alibaba.slb.loadbalancer',
|
|
431
|
+
resourceId: lb.LoadBalancerId,
|
|
432
|
+
name: lb.LoadBalancerName,
|
|
433
|
+
tags: this._extractTags(lb.Tags?.Tag),
|
|
434
|
+
configuration: this._sanitize(lb)
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
this.logger('info', `Collected ${loadBalancers.length} SLB instances in ${region}`);
|
|
439
|
+
}
|
|
440
|
+
} catch (err) {
|
|
441
|
+
this.logger('error', 'Failed to collect Alibaba Cloud SLB', {
|
|
442
|
+
error: err.message,
|
|
443
|
+
stack: err.stack
|
|
444
|
+
});
|
|
445
|
+
throw err;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Collect EIP (Elastic IP) addresses.
|
|
451
|
+
*/
|
|
452
|
+
async *_collectEIP() {
|
|
453
|
+
try {
|
|
454
|
+
for (const region of this._regions) {
|
|
455
|
+
const client = await this._createRPCClient(`https://vpc.${region}.aliyuncs.com`, '2016-04-28');
|
|
456
|
+
|
|
457
|
+
const params = {
|
|
458
|
+
RegionId: region,
|
|
459
|
+
PageSize: 50
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const response = await client.request('DescribeEipAddresses', params, { method: 'POST' });
|
|
463
|
+
const eips = response.EipAddresses?.EipAddress || [];
|
|
464
|
+
|
|
465
|
+
for (const eip of eips) {
|
|
466
|
+
yield {
|
|
467
|
+
provider: 'alibaba',
|
|
468
|
+
accountId: this._accountId,
|
|
469
|
+
region,
|
|
470
|
+
service: 'eip',
|
|
471
|
+
resourceType: 'alibaba.eip',
|
|
472
|
+
resourceId: eip.AllocationId,
|
|
473
|
+
name: eip.Name || eip.IpAddress,
|
|
474
|
+
tags: this._extractTags(eip.Tags?.Tag),
|
|
475
|
+
configuration: this._sanitize(eip)
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.logger('info', `Collected ${eips.length} EIPs in ${region}`);
|
|
480
|
+
}
|
|
481
|
+
} catch (err) {
|
|
482
|
+
this.logger('error', 'Failed to collect Alibaba Cloud EIP', {
|
|
483
|
+
error: err.message,
|
|
484
|
+
stack: err.stack
|
|
485
|
+
});
|
|
486
|
+
throw err;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Collect CDN domains.
|
|
492
|
+
*/
|
|
493
|
+
async *_collectCDN() {
|
|
494
|
+
try {
|
|
495
|
+
// CDN is global, not region-specific
|
|
496
|
+
const client = await this._createRPCClient('https://cdn.aliyuncs.com', '2018-05-10');
|
|
497
|
+
|
|
498
|
+
const params = {
|
|
499
|
+
PageSize: 50
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const response = await client.request('DescribeUserDomains', params, { method: 'POST' });
|
|
503
|
+
const domains = response.Domains?.PageData || [];
|
|
504
|
+
|
|
505
|
+
for (const domain of domains) {
|
|
506
|
+
yield {
|
|
507
|
+
provider: 'alibaba',
|
|
508
|
+
accountId: this._accountId,
|
|
509
|
+
region: null, // CDN is global
|
|
510
|
+
service: 'cdn',
|
|
511
|
+
resourceType: 'alibaba.cdn.domain',
|
|
512
|
+
resourceId: domain.DomainName,
|
|
513
|
+
name: domain.DomainName,
|
|
514
|
+
tags: {},
|
|
515
|
+
configuration: this._sanitize(domain)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
this.logger('info', `Collected ${domains.length} CDN domains`);
|
|
520
|
+
} catch (err) {
|
|
521
|
+
this.logger('error', 'Failed to collect Alibaba Cloud CDN', {
|
|
522
|
+
error: err.message,
|
|
523
|
+
stack: err.stack
|
|
524
|
+
});
|
|
525
|
+
throw err;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Collect DNS domains.
|
|
531
|
+
*/
|
|
532
|
+
async *_collectDNS() {
|
|
533
|
+
try {
|
|
534
|
+
// DNS is global, not region-specific
|
|
535
|
+
const client = await this._createRPCClient('https://alidns.aliyuncs.com', '2015-01-09');
|
|
536
|
+
|
|
537
|
+
const params = {
|
|
538
|
+
PageSize: 100
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const response = await client.request('DescribeDomains', params, { method: 'POST' });
|
|
542
|
+
const domains = response.Domains?.Domain || [];
|
|
543
|
+
|
|
544
|
+
for (const domain of domains) {
|
|
545
|
+
yield {
|
|
546
|
+
provider: 'alibaba',
|
|
547
|
+
accountId: this._accountId,
|
|
548
|
+
region: null, // DNS is global
|
|
549
|
+
service: 'dns',
|
|
550
|
+
resourceType: 'alibaba.dns.domain',
|
|
551
|
+
resourceId: domain.DomainId,
|
|
552
|
+
name: domain.DomainName,
|
|
553
|
+
tags: this._extractTags(domain.Tags?.Tag),
|
|
554
|
+
configuration: this._sanitize(domain)
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
this.logger('info', `Collected ${domains.length} DNS domains`);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
this.logger('error', 'Failed to collect Alibaba Cloud DNS', {
|
|
561
|
+
error: err.message,
|
|
562
|
+
stack: err.stack
|
|
563
|
+
});
|
|
564
|
+
throw err;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Collect Security Groups.
|
|
570
|
+
*/
|
|
571
|
+
async *_collectSecurityGroups() {
|
|
572
|
+
try {
|
|
573
|
+
for (const region of this._regions) {
|
|
574
|
+
const client = await this._createRPCClient(`https://ecs.${region}.aliyuncs.com`, '2014-05-26');
|
|
575
|
+
|
|
576
|
+
const params = {
|
|
577
|
+
RegionId: region,
|
|
578
|
+
PageSize: 50
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const response = await client.request('DescribeSecurityGroups', params, { method: 'POST' });
|
|
582
|
+
const securityGroups = response.SecurityGroups?.SecurityGroup || [];
|
|
583
|
+
|
|
584
|
+
for (const sg of securityGroups) {
|
|
585
|
+
yield {
|
|
586
|
+
provider: 'alibaba',
|
|
587
|
+
accountId: this._accountId,
|
|
588
|
+
region,
|
|
589
|
+
service: 'securitygroups',
|
|
590
|
+
resourceType: 'alibaba.ecs.securitygroup',
|
|
591
|
+
resourceId: sg.SecurityGroupId,
|
|
592
|
+
name: sg.SecurityGroupName,
|
|
593
|
+
tags: this._extractTags(sg.Tags?.Tag),
|
|
594
|
+
metadata: { vpcId: sg.VpcId },
|
|
595
|
+
configuration: this._sanitize(sg)
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
this.logger('info', `Collected ${securityGroups.length} security groups in ${region}`);
|
|
600
|
+
}
|
|
601
|
+
} catch (err) {
|
|
602
|
+
this.logger('error', 'Failed to collect Alibaba Cloud security groups', {
|
|
603
|
+
error: err.message,
|
|
604
|
+
stack: err.stack
|
|
605
|
+
});
|
|
606
|
+
throw err;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Collect Disk Snapshots.
|
|
612
|
+
*/
|
|
613
|
+
async *_collectSnapshots() {
|
|
614
|
+
try {
|
|
615
|
+
for (const region of this._regions) {
|
|
616
|
+
const client = await this._createRPCClient(`https://ecs.${region}.aliyuncs.com`, '2014-05-26');
|
|
617
|
+
|
|
618
|
+
const params = {
|
|
619
|
+
RegionId: region,
|
|
620
|
+
PageSize: 50
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const response = await client.request('DescribeSnapshots', params, { method: 'POST' });
|
|
624
|
+
const snapshots = response.Snapshots?.Snapshot || [];
|
|
625
|
+
|
|
626
|
+
for (const snapshot of snapshots) {
|
|
627
|
+
yield {
|
|
628
|
+
provider: 'alibaba',
|
|
629
|
+
accountId: this._accountId,
|
|
630
|
+
region,
|
|
631
|
+
service: 'snapshots',
|
|
632
|
+
resourceType: 'alibaba.ecs.snapshot',
|
|
633
|
+
resourceId: snapshot.SnapshotId,
|
|
634
|
+
name: snapshot.SnapshotName || snapshot.SnapshotId,
|
|
635
|
+
tags: this._extractTags(snapshot.Tags?.Tag),
|
|
636
|
+
metadata: { sourceDiskId: snapshot.SourceDiskId },
|
|
637
|
+
configuration: this._sanitize(snapshot)
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
this.logger('info', `Collected ${snapshots.length} snapshots in ${region}`);
|
|
642
|
+
}
|
|
643
|
+
} catch (err) {
|
|
644
|
+
this.logger('error', 'Failed to collect Alibaba Cloud snapshots', {
|
|
645
|
+
error: err.message,
|
|
646
|
+
stack: err.stack
|
|
647
|
+
});
|
|
648
|
+
throw err;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Collect Auto Scaling Groups.
|
|
654
|
+
*/
|
|
655
|
+
async *_collectAutoScaling() {
|
|
656
|
+
try {
|
|
657
|
+
for (const region of this._regions) {
|
|
658
|
+
const client = await this._createRPCClient(`https://ess.${region}.aliyuncs.com`, '2014-08-28');
|
|
659
|
+
|
|
660
|
+
const params = {
|
|
661
|
+
RegionId: region,
|
|
662
|
+
PageSize: 50
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// Collect Scaling Groups
|
|
666
|
+
const response = await client.request('DescribeScalingGroups', params, { method: 'POST' });
|
|
667
|
+
const scalingGroups = response.ScalingGroups?.ScalingGroup || [];
|
|
668
|
+
|
|
669
|
+
for (const group of scalingGroups) {
|
|
670
|
+
yield {
|
|
671
|
+
provider: 'alibaba',
|
|
672
|
+
accountId: this._accountId,
|
|
673
|
+
region,
|
|
674
|
+
service: 'autoscaling',
|
|
675
|
+
resourceType: 'alibaba.ess.scalinggroup',
|
|
676
|
+
resourceId: group.ScalingGroupId,
|
|
677
|
+
name: group.ScalingGroupName,
|
|
678
|
+
tags: this._extractTags(group.Tags?.Tag),
|
|
679
|
+
configuration: this._sanitize(group)
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// Collect Scaling Configurations for this group
|
|
683
|
+
try {
|
|
684
|
+
const configParams = {
|
|
685
|
+
ScalingGroupId: group.ScalingGroupId,
|
|
686
|
+
PageSize: 50
|
|
687
|
+
};
|
|
688
|
+
const configResponse = await client.request('DescribeScalingConfigurations', configParams, { method: 'POST' });
|
|
689
|
+
const configurations = configResponse.ScalingConfigurations?.ScalingConfiguration || [];
|
|
690
|
+
|
|
691
|
+
for (const config of configurations) {
|
|
692
|
+
yield {
|
|
693
|
+
provider: 'alibaba',
|
|
694
|
+
accountId: this._accountId,
|
|
695
|
+
region,
|
|
696
|
+
service: 'autoscaling',
|
|
697
|
+
resourceType: 'alibaba.ess.scalingconfiguration',
|
|
698
|
+
resourceId: config.ScalingConfigurationId,
|
|
699
|
+
name: config.ScalingConfigurationName,
|
|
700
|
+
tags: {},
|
|
701
|
+
metadata: { scalingGroupId: group.ScalingGroupId },
|
|
702
|
+
configuration: this._sanitize(config)
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
} catch (configErr) {
|
|
706
|
+
this.logger('warn', `Failed to collect scaling configurations for group ${group.ScalingGroupId}`, {
|
|
707
|
+
error: configErr.message
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
this.logger('info', `Collected ${scalingGroups.length} auto scaling groups in ${region}`);
|
|
713
|
+
}
|
|
714
|
+
} catch (err) {
|
|
715
|
+
this.logger('error', 'Failed to collect Alibaba Cloud auto scaling', {
|
|
716
|
+
error: err.message,
|
|
717
|
+
stack: err.stack
|
|
718
|
+
});
|
|
719
|
+
throw err;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Collect NAT Gateways.
|
|
725
|
+
*/
|
|
726
|
+
async *_collectNATGateway() {
|
|
727
|
+
try {
|
|
728
|
+
for (const region of this._regions) {
|
|
729
|
+
const client = await this._createRPCClient(`https://vpc.${region}.aliyuncs.com`, '2016-04-28');
|
|
730
|
+
|
|
731
|
+
const params = {
|
|
732
|
+
RegionId: region,
|
|
733
|
+
PageSize: 50
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const response = await client.request('DescribeNatGateways', params, { method: 'POST' });
|
|
737
|
+
const natGateways = response.NatGateways?.NatGateway || [];
|
|
738
|
+
|
|
739
|
+
for (const nat of natGateways) {
|
|
740
|
+
yield {
|
|
741
|
+
provider: 'alibaba',
|
|
742
|
+
accountId: this._accountId,
|
|
743
|
+
region,
|
|
744
|
+
service: 'natgateway',
|
|
745
|
+
resourceType: 'alibaba.vpc.natgateway',
|
|
746
|
+
resourceId: nat.NatGatewayId,
|
|
747
|
+
name: nat.Name || nat.NatGatewayId,
|
|
748
|
+
tags: this._extractTags(nat.Tags?.Tag),
|
|
749
|
+
metadata: { vpcId: nat.VpcId },
|
|
750
|
+
configuration: this._sanitize(nat)
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
this.logger('info', `Collected ${natGateways.length} NAT gateways in ${region}`);
|
|
755
|
+
}
|
|
756
|
+
} catch (err) {
|
|
757
|
+
this.logger('error', 'Failed to collect Alibaba Cloud NAT gateways', {
|
|
758
|
+
error: err.message,
|
|
759
|
+
stack: err.stack
|
|
760
|
+
});
|
|
761
|
+
throw err;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Collect Container Registry (ACR) repositories.
|
|
767
|
+
*/
|
|
768
|
+
async *_collectACR() {
|
|
769
|
+
try {
|
|
770
|
+
// ACR API is region-specific but uses different endpoint pattern
|
|
771
|
+
for (const region of this._regions) {
|
|
772
|
+
const client = await this._createRPCClient(`https://cr.${region}.aliyuncs.com`, '2018-12-01');
|
|
773
|
+
|
|
774
|
+
const params = {
|
|
775
|
+
RegionId: region,
|
|
776
|
+
PageSize: 50
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
const response = await client.request('ListRepository', params, { method: 'POST' });
|
|
781
|
+
const repositories = response.Repositories?.Repository || [];
|
|
782
|
+
|
|
783
|
+
for (const repo of repositories) {
|
|
784
|
+
yield {
|
|
785
|
+
provider: 'alibaba',
|
|
786
|
+
accountId: this._accountId,
|
|
787
|
+
region,
|
|
788
|
+
service: 'acr',
|
|
789
|
+
resourceType: 'alibaba.acr.repository',
|
|
790
|
+
resourceId: repo.RepoId || `${repo.RepoNamespace}/${repo.RepoName}`,
|
|
791
|
+
name: `${repo.RepoNamespace}/${repo.RepoName}`,
|
|
792
|
+
tags: {},
|
|
793
|
+
configuration: this._sanitize(repo)
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
this.logger('info', `Collected ${repositories.length} ACR repositories in ${region}`);
|
|
798
|
+
} catch (regionErr) {
|
|
799
|
+
// ACR might not be available in all regions
|
|
800
|
+
this.logger('debug', `ACR not available or no repositories in ${region}`, {
|
|
801
|
+
error: regionErr.message
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
} catch (err) {
|
|
806
|
+
this.logger('error', 'Failed to collect Alibaba Cloud ACR', {
|
|
807
|
+
error: err.message,
|
|
808
|
+
stack: err.stack
|
|
809
|
+
});
|
|
810
|
+
throw err;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Extract tags from Alibaba Cloud tag format.
|
|
816
|
+
*/
|
|
817
|
+
_extractTags(tags) {
|
|
818
|
+
if (!tags || !Array.isArray(tags)) return {};
|
|
819
|
+
|
|
820
|
+
const tagMap = {};
|
|
821
|
+
for (const tag of tags) {
|
|
822
|
+
if (tag.TagKey) {
|
|
823
|
+
tagMap[tag.TagKey] = tag.TagValue || '';
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return tagMap;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Sanitize configuration by removing sensitive data.
|
|
831
|
+
*/
|
|
832
|
+
_sanitize(config) {
|
|
833
|
+
if (!config || typeof config !== 'object') return config;
|
|
834
|
+
|
|
835
|
+
const sanitized = { ...config };
|
|
836
|
+
const sensitiveFields = [
|
|
837
|
+
'Password',
|
|
838
|
+
'MasterUserPassword',
|
|
839
|
+
'AccessKeySecret',
|
|
840
|
+
'SecretAccessKey',
|
|
841
|
+
'PrivateKey',
|
|
842
|
+
'Certificate'
|
|
843
|
+
];
|
|
844
|
+
|
|
845
|
+
for (const field of sensitiveFields) {
|
|
846
|
+
if (field in sanitized) {
|
|
847
|
+
sanitized[field] = '***REDACTED***';
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return sanitized;
|
|
852
|
+
}
|
|
853
|
+
}
|