@gugananuvem/aws-local-simulator 1.0.12 → 1.0.14
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 +235 -11
- package/package.json +12 -2
- package/src/config/default-config.js +1 -0
- package/src/index.js +18 -2
- package/src/server.js +36 -32
- package/src/services/apigateway/index.js +5 -0
- package/src/services/apigateway/server.js +20 -0
- package/src/services/apigateway/simulator.js +13 -3
- package/src/services/athena/index.js +75 -0
- package/src/services/athena/server.js +101 -0
- package/src/services/athena/simulador.js +998 -0
- package/src/services/athena/simulator.js +346 -0
- package/src/services/cloudformation/index.js +106 -0
- package/src/services/cloudformation/server.js +417 -0
- package/src/services/cloudformation/simulador.js +1045 -0
- package/src/services/cloudtrail/index.js +84 -0
- package/src/services/cloudtrail/server.js +235 -0
- package/src/services/cloudtrail/simulador.js +719 -0
- package/src/services/cloudwatch/index.js +84 -0
- package/src/services/cloudwatch/server.js +366 -0
- package/src/services/cloudwatch/simulador.js +1173 -0
- package/src/services/cognito/index.js +5 -0
- package/src/services/cognito/simulator.js +4 -0
- package/src/services/config/index.js +96 -0
- package/src/services/config/server.js +215 -0
- package/src/services/config/simulador.js +1260 -0
- package/src/services/dynamodb/index.js +7 -3
- package/src/services/dynamodb/server.js +4 -2
- package/src/services/dynamodb/simulator.js +39 -29
- package/src/services/eventbridge/index.js +55 -51
- package/src/services/eventbridge/server.js +209 -0
- package/src/services/eventbridge/simulator.js +684 -0
- package/src/services/index.js +30 -4
- package/src/services/kms/index.js +75 -0
- package/src/services/kms/server.js +67 -0
- package/src/services/kms/simulator.js +324 -0
- package/src/services/lambda/index.js +5 -0
- package/src/services/lambda/simulator.js +48 -38
- package/src/services/parameter-store/index.js +80 -0
- package/src/services/parameter-store/server.js +50 -0
- package/src/services/parameter-store/simulator.js +201 -0
- package/src/services/s3/index.js +7 -3
- package/src/services/s3/server.js +20 -13
- package/src/services/s3/simulator.js +163 -407
- package/src/services/secret-manager/index.js +80 -0
- package/src/services/secret-manager/server.js +50 -0
- package/src/services/secret-manager/simulator.js +171 -0
- package/src/services/sns/index.js +55 -42
- package/src/services/sns/server.js +580 -0
- package/src/services/sns/simulator.js +1482 -0
- package/src/services/sqs/index.js +2 -4
- package/src/services/sqs/server.js +4 -2
- package/src/services/xray/index.js +83 -0
- package/src/services/xray/server.js +308 -0
- package/src/services/xray/simulador.js +994 -0
- package/src/utils/cloudtrail-audit.js +129 -0
- package/src/utils/local-store.js +18 -2
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview CloudTrail Simulator
|
|
5
|
+
*
|
|
6
|
+
* Suporta:
|
|
7
|
+
* Trails:
|
|
8
|
+
* - CreateTrail / DeleteTrail / UpdateTrail
|
|
9
|
+
* - DescribeTrails / GetTrail / GetTrailStatus
|
|
10
|
+
* - StartLogging / StopLogging
|
|
11
|
+
*
|
|
12
|
+
* Events:
|
|
13
|
+
* - LookupEvents (filtro por ResourceName, EventName, Username, etc.)
|
|
14
|
+
* - GetEventSelectors / PutEventSelectors
|
|
15
|
+
*
|
|
16
|
+
* Integração:
|
|
17
|
+
* - Registro automático de API calls dos outros serviços
|
|
18
|
+
* - Entrega de logs para S3 (simulado)
|
|
19
|
+
* - Integração com CloudWatch Logs
|
|
20
|
+
*
|
|
21
|
+
* Persistência via LocalStore
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const { randomUUID } = require('crypto');
|
|
25
|
+
|
|
26
|
+
// ─── Erros tipados ────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
class CloudTrailError extends Error {
|
|
29
|
+
constructor(code, message, statusCode = 400) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.statusCode = statusCode;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const Errors = {
|
|
37
|
+
TrailNotFound: (name) =>
|
|
38
|
+
new CloudTrailError('TrailNotFoundException', `Unknown trail: ${name}`, 404),
|
|
39
|
+
TrailAlreadyExists: (name) =>
|
|
40
|
+
new CloudTrailError('TrailAlreadyExistsException', `Trail already exists: ${name}`, 400),
|
|
41
|
+
InvalidTrailName: (name) =>
|
|
42
|
+
new CloudTrailError('InvalidTrailNameException', `Invalid trail name: ${name}`, 400),
|
|
43
|
+
InvalidParameter: (msg) =>
|
|
44
|
+
new CloudTrailError('InvalidParameterCombinationException', msg, 400),
|
|
45
|
+
MaximumNumberOfTrails: () =>
|
|
46
|
+
new CloudTrailError('MaximumNumberOfTrailsExceededException', 'Maximum number of trails exceeded (max: 5)', 400),
|
|
47
|
+
S3BucketNotFound: (bucket) =>
|
|
48
|
+
new CloudTrailError('S3BucketDoesNotExistException', `S3 bucket does not exist: ${bucket}`, 400),
|
|
49
|
+
InsuficientSnsTopicPolicy: () =>
|
|
50
|
+
new CloudTrailError('InsuficientSnsTopicPolicyException', 'Insufficient SNS topic policy', 400),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ─── Constantes ───────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const REGION = 'us-east-1';
|
|
56
|
+
const ACCOUNT_ID = '000000000000';
|
|
57
|
+
const MAX_TRAILS = 5;
|
|
58
|
+
const MAX_RESULTS_DEFAULT = 50;
|
|
59
|
+
const MAX_RESULTS_MAX = 50;
|
|
60
|
+
|
|
61
|
+
// ─── Utilitários ──────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function trailArn(name) {
|
|
64
|
+
return `arn:aws:cloudtrail:${REGION}:${ACCOUNT_ID}:trail/${name}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateTrailName(name) {
|
|
68
|
+
if (!name || typeof name !== 'string') throw Errors.InvalidTrailName(name);
|
|
69
|
+
if (name.length < 3 || name.length > 128) throw Errors.InvalidTrailName(name);
|
|
70
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(name)) throw Errors.InvalidTrailName(name);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function matchesFilter(event, filter) {
|
|
75
|
+
if (!filter) return true;
|
|
76
|
+
|
|
77
|
+
if (filter.AttributeKey && filter.AttributeValue) {
|
|
78
|
+
const key = filter.AttributeKey;
|
|
79
|
+
const value = filter.AttributeValue;
|
|
80
|
+
|
|
81
|
+
switch (key) {
|
|
82
|
+
case 'EventId':
|
|
83
|
+
if (event.EventId !== value) return false;
|
|
84
|
+
break;
|
|
85
|
+
case 'EventName':
|
|
86
|
+
if (event.EventName !== value) return false;
|
|
87
|
+
break;
|
|
88
|
+
case 'ReadOnly':
|
|
89
|
+
if (String(event.ReadOnly) !== value) return false;
|
|
90
|
+
break;
|
|
91
|
+
case 'Username':
|
|
92
|
+
if (event.Username !== value) return false;
|
|
93
|
+
break;
|
|
94
|
+
case 'ResourceType':
|
|
95
|
+
if (!event.Resources || !event.Resources.some(r => r.ResourceType === value)) return false;
|
|
96
|
+
break;
|
|
97
|
+
case 'ResourceName':
|
|
98
|
+
if (!event.Resources || !event.Resources.some(r => r.ResourceName === value)) return false;
|
|
99
|
+
break;
|
|
100
|
+
case 'EventSource':
|
|
101
|
+
if (event.EventSource !== value) return false;
|
|
102
|
+
break;
|
|
103
|
+
case 'AccessKeyId':
|
|
104
|
+
if (event.AccessKeyId !== value) return false;
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Simulador Principal ──────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
class CloudTrailSimulator {
|
|
117
|
+
/**
|
|
118
|
+
* @param {Object} config - Global simulator config
|
|
119
|
+
* @param {Object} store - LocalStore instance
|
|
120
|
+
* @param {Object} logger - Logger instance
|
|
121
|
+
*/
|
|
122
|
+
constructor(config, store, logger) {
|
|
123
|
+
this.config = config;
|
|
124
|
+
this.store = store;
|
|
125
|
+
this.logger = logger;
|
|
126
|
+
|
|
127
|
+
// Trails: Map<name, TrailConfig>
|
|
128
|
+
this.trails = new Map();
|
|
129
|
+
|
|
130
|
+
// Trail status: Map<name, { isLogging, latestDelivery, latestNotification }>
|
|
131
|
+
this.trailStatus = new Map();
|
|
132
|
+
|
|
133
|
+
// Event selectors: Map<name, EventSelector[]>
|
|
134
|
+
this.eventSelectors = new Map();
|
|
135
|
+
|
|
136
|
+
// Events log: Array de CloudTrail events
|
|
137
|
+
this.events = [];
|
|
138
|
+
|
|
139
|
+
// Injeções cross-service
|
|
140
|
+
this.s3Simulator = null;
|
|
141
|
+
this.cloudwatchSimulator = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── Persistência ───────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
async load() {
|
|
147
|
+
try {
|
|
148
|
+
const data = await this.store.load('cloudtrail');
|
|
149
|
+
if (data) {
|
|
150
|
+
if (data.trails) this.trails = new Map(Object.entries(data.trails));
|
|
151
|
+
if (data.trailStatus) this.trailStatus = new Map(Object.entries(data.trailStatus));
|
|
152
|
+
if (data.eventSelectors) this.eventSelectors = new Map(Object.entries(data.eventSelectors));
|
|
153
|
+
if (data.events) this.events = data.events;
|
|
154
|
+
this.logger.info(`[CloudTrail] Loaded ${this.trails.size} trails, ${this.events.length} events`);
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
this.logger.warn('[CloudTrail] No persisted data found, starting fresh');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Garante que existe um trail padrão com logging ativo
|
|
161
|
+
if (this.trails.size === 0) {
|
|
162
|
+
const defaultName = 'local-default-trail';
|
|
163
|
+
this.trails.set(defaultName, { Name: defaultName, S3BucketName: null });
|
|
164
|
+
this.trailStatus.set(defaultName, { isLogging: true });
|
|
165
|
+
// Event selectors: captura management events + todos os data events
|
|
166
|
+
this.eventSelectors.set(defaultName, [
|
|
167
|
+
{
|
|
168
|
+
ReadWriteType: 'All',
|
|
169
|
+
IncludeManagementEvents: true,
|
|
170
|
+
DataResources: [
|
|
171
|
+
{ Type: 'AWS::S3::Object', Values: ['arn:aws:s3:::*'] },
|
|
172
|
+
{ Type: 'AWS::DynamoDB::Table', Values: ['arn:aws:dynamodb:::*'] },
|
|
173
|
+
{ Type: 'AWS::Lambda::Function', Values: ['arn:aws:lambda:::*'] },
|
|
174
|
+
{ Type: 'AWS::APIGateway::Stage', Values: ['arn:aws:execute-api:::*'] },
|
|
175
|
+
{ Type: 'AWS::SecretsManager::Secret', Values: ['arn:aws:secretsmanager:::*'] },
|
|
176
|
+
{ Type: 'AWS::SSM::Parameter', Values: ['arn:aws:ssm:::*'] },
|
|
177
|
+
{ Type: 'AWS::Cognito::UserPool', Values: ['arn:aws:cognito-idp:::*'] },
|
|
178
|
+
{ Type: 'AWS::KMS::Key', Values: ['arn:aws:kms:::*'] },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
]);
|
|
182
|
+
this.logger.debug('[CloudTrail] Trail padrão criado com logging ativo');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async save() {
|
|
187
|
+
try {
|
|
188
|
+
const data = {
|
|
189
|
+
trails: Object.fromEntries(this.trails),
|
|
190
|
+
trailStatus: Object.fromEntries(this.trailStatus),
|
|
191
|
+
eventSelectors: Object.fromEntries(this.eventSelectors),
|
|
192
|
+
events: this.events.slice(-10000), // mantém os últimos 10.000 eventos
|
|
193
|
+
};
|
|
194
|
+
await this.store.save('cloudtrail', data);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this.logger.error('[CloudTrail] Failed to save data:', err.message);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
reset() {
|
|
201
|
+
this.trails.clear();
|
|
202
|
+
this.trailStatus.clear();
|
|
203
|
+
this.eventSelectors.clear();
|
|
204
|
+
this.events = [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── Registro de Evento (uso interno / cross-service) ───────────────────────
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Registra um API call como evento CloudTrail.
|
|
211
|
+
* Chamado pelos outros serviços via injeção.
|
|
212
|
+
*
|
|
213
|
+
* @param {Object} params
|
|
214
|
+
* @param {string} params.eventName - Ex: "CreateBucket"
|
|
215
|
+
* @param {string} params.eventSource - Ex: "s3.amazonaws.com"
|
|
216
|
+
* @param {string} params.username - Ex: "test-user"
|
|
217
|
+
* @param {boolean} params.readOnly - true se operação de leitura
|
|
218
|
+
* @param {Array} params.resources - [{ ResourceType, ResourceName }]
|
|
219
|
+
* @param {Object} params.requestParameters - Parâmetros da requisição
|
|
220
|
+
* @param {Object} params.responseElements - Resposta da operação
|
|
221
|
+
* @param {string} params.sourceIPAddress - IP da requisição
|
|
222
|
+
* @param {string} params.userAgent - User-Agent da requisição
|
|
223
|
+
*/
|
|
224
|
+
recordEvent(params = {}) {
|
|
225
|
+
const event = {
|
|
226
|
+
EventId: randomUUID(),
|
|
227
|
+
EventName: params.eventName || 'UnknownEvent',
|
|
228
|
+
EventSource: params.eventSource || 'local.amazonaws.com',
|
|
229
|
+
EventTime: new Date().toISOString(),
|
|
230
|
+
Username: params.username || 'local-user',
|
|
231
|
+
ReadOnly: params.readOnly !== undefined ? params.readOnly : false,
|
|
232
|
+
AccessKeyId: params.accessKeyId || 'AKIAIOSFODNN7EXAMPLE',
|
|
233
|
+
Resources: params.resources || [],
|
|
234
|
+
RequestParameters: params.requestParameters || null,
|
|
235
|
+
ResponseElements: params.responseElements || null,
|
|
236
|
+
SourceIPAddress: params.sourceIPAddress || '127.0.0.1',
|
|
237
|
+
UserAgent: params.userAgent || 'aws-local-simulator',
|
|
238
|
+
CloudTrailEvent: JSON.stringify({
|
|
239
|
+
eventVersion: '1.08',
|
|
240
|
+
userIdentity: {
|
|
241
|
+
type: 'IAMUser',
|
|
242
|
+
principalId: 'AIDIOSFODNN7EXAMPLE',
|
|
243
|
+
arn: `arn:aws:iam::${ACCOUNT_ID}:user/${params.username || 'local-user'}`,
|
|
244
|
+
accountId: ACCOUNT_ID,
|
|
245
|
+
accessKeyId: params.accessKeyId || 'AKIAIOSFODNN7EXAMPLE',
|
|
246
|
+
userName: params.username || 'local-user',
|
|
247
|
+
},
|
|
248
|
+
eventTime: new Date().toISOString(),
|
|
249
|
+
eventSource: params.eventSource || 'local.amazonaws.com',
|
|
250
|
+
eventName: params.eventName || 'UnknownEvent',
|
|
251
|
+
awsRegion: REGION,
|
|
252
|
+
sourceIPAddress: params.sourceIPAddress || '127.0.0.1',
|
|
253
|
+
userAgent: params.userAgent || 'aws-local-simulator',
|
|
254
|
+
requestParameters: params.requestParameters || null,
|
|
255
|
+
responseElements: params.responseElements || null,
|
|
256
|
+
requestID: randomUUID(),
|
|
257
|
+
eventID: randomUUID(),
|
|
258
|
+
readOnly: params.readOnly !== undefined ? params.readOnly : false,
|
|
259
|
+
resources: params.resources || [],
|
|
260
|
+
eventType: 'AwsApiCall',
|
|
261
|
+
managementEvent: true,
|
|
262
|
+
recipientAccountId: ACCOUNT_ID,
|
|
263
|
+
}),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
this.events.push(event);
|
|
267
|
+
|
|
268
|
+
// Persiste no disco
|
|
269
|
+
this.save();
|
|
270
|
+
|
|
271
|
+
// Entrega para S3 (simula escrita de log file)
|
|
272
|
+
this._deliverToS3(event);
|
|
273
|
+
|
|
274
|
+
// Entrega para CloudWatch Logs se configurado
|
|
275
|
+
this._deliverToCloudWatch(event);
|
|
276
|
+
|
|
277
|
+
return event;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async _deliverToS3(event) {
|
|
281
|
+
// Verifica se há trails ativos com S3 configurado
|
|
282
|
+
for (const [name, trail] of this.trails) {
|
|
283
|
+
const status = this.trailStatus.get(name) || {};
|
|
284
|
+
if (!status.isLogging) continue;
|
|
285
|
+
if (!trail.S3BucketName) continue;
|
|
286
|
+
|
|
287
|
+
if (this.s3Simulator) {
|
|
288
|
+
try {
|
|
289
|
+
const date = new Date(event.EventTime);
|
|
290
|
+
const key = `${trail.S3KeyPrefix || 'AWSLogs/'}${ACCOUNT_ID}/CloudTrail/${REGION}/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}/${randomUUID()}.json`;
|
|
291
|
+
|
|
292
|
+
await this.s3Simulator.putObject({
|
|
293
|
+
Bucket: trail.S3BucketName,
|
|
294
|
+
Key: key,
|
|
295
|
+
Body: JSON.stringify({ Records: [JSON.parse(event.CloudTrailEvent)] }),
|
|
296
|
+
ContentType: 'application/json',
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Atualiza status
|
|
300
|
+
const s = this.trailStatus.get(name) || {};
|
|
301
|
+
s.LatestDeliveryTime = new Date().toISOString();
|
|
302
|
+
s.LatestDeliveryAttemptTime = new Date().toISOString();
|
|
303
|
+
this.trailStatus.set(name, s);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
this.logger.warn(`[CloudTrail] Failed to deliver to S3 for trail ${name}: ${err.message}`);
|
|
306
|
+
const s = this.trailStatus.get(name) || {};
|
|
307
|
+
s.LatestDeliveryError = err.message;
|
|
308
|
+
s.LatestDeliveryAttemptTime = new Date().toISOString();
|
|
309
|
+
this.trailStatus.set(name, s);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
_deliverToCloudWatch(event) {
|
|
316
|
+
for (const [name, trail] of this.trails) {
|
|
317
|
+
const status = this.trailStatus.get(name) || {};
|
|
318
|
+
if (!status.isLogging) continue;
|
|
319
|
+
if (!trail.CloudWatchLogsLogGroupArn) continue;
|
|
320
|
+
|
|
321
|
+
if (this.cloudwatchSimulator) {
|
|
322
|
+
try {
|
|
323
|
+
// Extrai o log group name do ARN
|
|
324
|
+
const match = trail.CloudWatchLogsLogGroupArn.match(/log-group:([^:]+)/);
|
|
325
|
+
if (!match) continue;
|
|
326
|
+
const logGroupName = match[1];
|
|
327
|
+
|
|
328
|
+
this.cloudwatchSimulator.putLambdaLogs(logGroupName, 'cloudtrail', [
|
|
329
|
+
{ timestamp: Date.now(), message: event.CloudTrailEvent },
|
|
330
|
+
]);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
this.logger.warn(`[CloudTrail] Failed to deliver to CloudWatch for trail ${name}: ${err.message}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ─── Trails ─────────────────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
createTrail(params = {}) {
|
|
341
|
+
const name = params.Name;
|
|
342
|
+
validateTrailName(name);
|
|
343
|
+
|
|
344
|
+
if (this.trails.has(name)) throw Errors.TrailAlreadyExists(name);
|
|
345
|
+
if (this.trails.size >= MAX_TRAILS) throw Errors.MaximumNumberOfTrails();
|
|
346
|
+
|
|
347
|
+
const trail = {
|
|
348
|
+
Name: name,
|
|
349
|
+
S3BucketName: params.S3BucketName || null,
|
|
350
|
+
S3KeyPrefix: params.S3KeyPrefix || null,
|
|
351
|
+
SnsTopicName: params.SnsTopicName || null,
|
|
352
|
+
SnsTopicARN: params.SnsTopicName
|
|
353
|
+
? `arn:aws:sns:${REGION}:${ACCOUNT_ID}:${params.SnsTopicName}`
|
|
354
|
+
: null,
|
|
355
|
+
IncludeGlobalServiceEvents: params.IncludeGlobalServiceEvents !== false,
|
|
356
|
+
IsMultiRegionTrail: params.IsMultiRegionTrail === true,
|
|
357
|
+
HomeRegion: REGION,
|
|
358
|
+
TrailARN: trailArn(name),
|
|
359
|
+
LogFileValidationEnabled: params.EnableLogFileValidation === true,
|
|
360
|
+
CloudWatchLogsLogGroupArn: params.CloudWatchLogsLogGroupArn || null,
|
|
361
|
+
CloudWatchLogsRoleArn: params.CloudWatchLogsRoleArn || null,
|
|
362
|
+
HasCustomEventSelectors: false,
|
|
363
|
+
HasInsightSelectors: false,
|
|
364
|
+
IsOrganizationTrail: false,
|
|
365
|
+
CreatedAt: new Date().toISOString(),
|
|
366
|
+
Tags: params.TagsList || [],
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
this.trails.set(name, trail);
|
|
370
|
+
this.trailStatus.set(name, {
|
|
371
|
+
isLogging: false,
|
|
372
|
+
LatestDeliveryTime: null,
|
|
373
|
+
LatestDeliveryAttemptTime: null,
|
|
374
|
+
LatestDeliveryError: null,
|
|
375
|
+
LatestNotificationTime: null,
|
|
376
|
+
LatestNotificationAttemptTime: null,
|
|
377
|
+
LatestNotificationError: null,
|
|
378
|
+
LatestCloudWatchLogsDeliveryTime: null,
|
|
379
|
+
LatestCloudWatchLogsDeliveryError: null,
|
|
380
|
+
StartLoggingTime: null,
|
|
381
|
+
StopLoggingTime: null,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Seletores padrão
|
|
385
|
+
this.eventSelectors.set(name, [{
|
|
386
|
+
ReadWriteType: 'All',
|
|
387
|
+
IncludeManagementEvents: true,
|
|
388
|
+
DataResources: [],
|
|
389
|
+
ExcludeManagementEventSources: [],
|
|
390
|
+
}]);
|
|
391
|
+
|
|
392
|
+
this.logger.info(`[CloudTrail] Trail created: ${name}`);
|
|
393
|
+
this.save();
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
Name: trail.Name,
|
|
397
|
+
S3BucketName: trail.S3BucketName,
|
|
398
|
+
S3KeyPrefix: trail.S3KeyPrefix,
|
|
399
|
+
SnsTopicName: trail.SnsTopicName,
|
|
400
|
+
SnsTopicARN: trail.SnsTopicARN,
|
|
401
|
+
IncludeGlobalServiceEvents: trail.IncludeGlobalServiceEvents,
|
|
402
|
+
IsMultiRegionTrail: trail.IsMultiRegionTrail,
|
|
403
|
+
TrailARN: trail.TrailARN,
|
|
404
|
+
LogFileValidationEnabled: trail.LogFileValidationEnabled,
|
|
405
|
+
CloudWatchLogsLogGroupArn: trail.CloudWatchLogsLogGroupArn,
|
|
406
|
+
CloudWatchLogsRoleArn: trail.CloudWatchLogsRoleArn,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
updateTrail(params = {}) {
|
|
411
|
+
const name = params.Name;
|
|
412
|
+
if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
|
|
413
|
+
|
|
414
|
+
const trail = this.trails.get(name);
|
|
415
|
+
|
|
416
|
+
if (params.S3BucketName !== undefined) trail.S3BucketName = params.S3BucketName;
|
|
417
|
+
if (params.S3KeyPrefix !== undefined) trail.S3KeyPrefix = params.S3KeyPrefix;
|
|
418
|
+
if (params.SnsTopicName !== undefined) {
|
|
419
|
+
trail.SnsTopicName = params.SnsTopicName;
|
|
420
|
+
trail.SnsTopicARN = params.SnsTopicName
|
|
421
|
+
? `arn:aws:sns:${REGION}:${ACCOUNT_ID}:${params.SnsTopicName}`
|
|
422
|
+
: null;
|
|
423
|
+
}
|
|
424
|
+
if (params.IncludeGlobalServiceEvents !== undefined) trail.IncludeGlobalServiceEvents = params.IncludeGlobalServiceEvents;
|
|
425
|
+
if (params.IsMultiRegionTrail !== undefined) trail.IsMultiRegionTrail = params.IsMultiRegionTrail;
|
|
426
|
+
if (params.EnableLogFileValidation !== undefined) trail.LogFileValidationEnabled = params.EnableLogFileValidation;
|
|
427
|
+
if (params.CloudWatchLogsLogGroupArn !== undefined) trail.CloudWatchLogsLogGroupArn = params.CloudWatchLogsLogGroupArn;
|
|
428
|
+
if (params.CloudWatchLogsRoleArn !== undefined) trail.CloudWatchLogsRoleArn = params.CloudWatchLogsRoleArn;
|
|
429
|
+
|
|
430
|
+
this.trails.set(name, trail);
|
|
431
|
+
this.logger.info(`[CloudTrail] Trail updated: ${name}`);
|
|
432
|
+
this.save();
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
Name: trail.Name,
|
|
436
|
+
S3BucketName: trail.S3BucketName,
|
|
437
|
+
S3KeyPrefix: trail.S3KeyPrefix,
|
|
438
|
+
SnsTopicName: trail.SnsTopicName,
|
|
439
|
+
SnsTopicARN: trail.SnsTopicARN,
|
|
440
|
+
IncludeGlobalServiceEvents: trail.IncludeGlobalServiceEvents,
|
|
441
|
+
IsMultiRegionTrail: trail.IsMultiRegionTrail,
|
|
442
|
+
TrailARN: trail.TrailARN,
|
|
443
|
+
LogFileValidationEnabled: trail.LogFileValidationEnabled,
|
|
444
|
+
CloudWatchLogsLogGroupArn: trail.CloudWatchLogsLogGroupArn,
|
|
445
|
+
CloudWatchLogsRoleArn: trail.CloudWatchLogsRoleArn,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
deleteTrail(params = {}) {
|
|
450
|
+
const name = params.Name;
|
|
451
|
+
if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
|
|
452
|
+
|
|
453
|
+
this.trails.delete(name);
|
|
454
|
+
this.trailStatus.delete(name);
|
|
455
|
+
this.eventSelectors.delete(name);
|
|
456
|
+
|
|
457
|
+
this.logger.info(`[CloudTrail] Trail deleted: ${name}`);
|
|
458
|
+
this.save();
|
|
459
|
+
|
|
460
|
+
return {};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
describeTrails(params = {}) {
|
|
464
|
+
const includeShadow = params.includeShadowTrails !== false;
|
|
465
|
+
let trailList = [...this.trails.values()];
|
|
466
|
+
|
|
467
|
+
if (params.trailNameList && params.trailNameList.length > 0) {
|
|
468
|
+
trailList = trailList.filter(t =>
|
|
469
|
+
params.trailNameList.includes(t.Name) || params.trailNameList.includes(t.TrailARN)
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
trailList: trailList.map(t => ({
|
|
475
|
+
Name: t.Name,
|
|
476
|
+
S3BucketName: t.S3BucketName,
|
|
477
|
+
S3KeyPrefix: t.S3KeyPrefix,
|
|
478
|
+
SnsTopicName: t.SnsTopicName,
|
|
479
|
+
SnsTopicARN: t.SnsTopicARN,
|
|
480
|
+
IncludeGlobalServiceEvents: t.IncludeGlobalServiceEvents,
|
|
481
|
+
IsMultiRegionTrail: t.IsMultiRegionTrail,
|
|
482
|
+
HomeRegion: t.HomeRegion,
|
|
483
|
+
TrailARN: t.TrailARN,
|
|
484
|
+
LogFileValidationEnabled: t.LogFileValidationEnabled,
|
|
485
|
+
CloudWatchLogsLogGroupArn: t.CloudWatchLogsLogGroupArn,
|
|
486
|
+
CloudWatchLogsRoleArn: t.CloudWatchLogsRoleArn,
|
|
487
|
+
HasCustomEventSelectors: t.HasCustomEventSelectors,
|
|
488
|
+
HasInsightSelectors: t.HasInsightSelectors,
|
|
489
|
+
IsOrganizationTrail: t.IsOrganizationTrail,
|
|
490
|
+
})),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
getTrail(params = {}) {
|
|
495
|
+
const name = params.Name;
|
|
496
|
+
const trail = this.trails.get(name);
|
|
497
|
+
if (!trail) throw Errors.TrailNotFound(name);
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
Trail: {
|
|
501
|
+
Name: trail.Name,
|
|
502
|
+
S3BucketName: trail.S3BucketName,
|
|
503
|
+
S3KeyPrefix: trail.S3KeyPrefix,
|
|
504
|
+
SnsTopicName: trail.SnsTopicName,
|
|
505
|
+
SnsTopicARN: trail.SnsTopicARN,
|
|
506
|
+
IncludeGlobalServiceEvents: trail.IncludeGlobalServiceEvents,
|
|
507
|
+
IsMultiRegionTrail: trail.IsMultiRegionTrail,
|
|
508
|
+
HomeRegion: trail.HomeRegion,
|
|
509
|
+
TrailARN: trail.TrailARN,
|
|
510
|
+
LogFileValidationEnabled: trail.LogFileValidationEnabled,
|
|
511
|
+
CloudWatchLogsLogGroupArn: trail.CloudWatchLogsLogGroupArn,
|
|
512
|
+
CloudWatchLogsRoleArn: trail.CloudWatchLogsRoleArn,
|
|
513
|
+
HasCustomEventSelectors: trail.HasCustomEventSelectors,
|
|
514
|
+
HasInsightSelectors: trail.HasInsightSelectors,
|
|
515
|
+
IsOrganizationTrail: trail.IsOrganizationTrail,
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
getTrailStatus(params = {}) {
|
|
521
|
+
const name = params.Name;
|
|
522
|
+
if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
|
|
523
|
+
|
|
524
|
+
const status = this.trailStatus.get(name) || {};
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
IsLogging: status.isLogging === true,
|
|
528
|
+
LatestDeliveryError: status.LatestDeliveryError || null,
|
|
529
|
+
LatestNotificationError: status.LatestNotificationError || null,
|
|
530
|
+
LatestDeliveryTime: status.LatestDeliveryTime || null,
|
|
531
|
+
LatestNotificationTime: status.LatestNotificationTime || null,
|
|
532
|
+
StartLoggingTime: status.StartLoggingTime || null,
|
|
533
|
+
StopLoggingTime: status.StopLoggingTime || null,
|
|
534
|
+
LatestCloudWatchLogsDeliveryError: status.LatestCloudWatchLogsDeliveryError || null,
|
|
535
|
+
LatestCloudWatchLogsDeliveryTime: status.LatestCloudWatchLogsDeliveryTime || null,
|
|
536
|
+
LatestDeliveryAttemptTime: status.LatestDeliveryAttemptTime || '',
|
|
537
|
+
LatestNotificationAttemptTime: status.LatestNotificationAttemptTime || '',
|
|
538
|
+
LatestNotificationAttemptSucceeded: status.LatestNotificationAttemptSucceeded || '',
|
|
539
|
+
LatestDeliveryAttemptSucceeded: status.LatestDeliveryAttemptSucceeded || '',
|
|
540
|
+
TimeLoggingStarted: status.StartLoggingTime || '',
|
|
541
|
+
TimeLoggingStopped: status.StopLoggingTime || '',
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
startLogging(params = {}) {
|
|
546
|
+
const name = params.Name;
|
|
547
|
+
if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
|
|
548
|
+
|
|
549
|
+
const status = this.trailStatus.get(name) || {};
|
|
550
|
+
status.isLogging = true;
|
|
551
|
+
status.StartLoggingTime = new Date().toISOString();
|
|
552
|
+
this.trailStatus.set(name, status);
|
|
553
|
+
|
|
554
|
+
this.logger.info(`[CloudTrail] Started logging for trail: ${name}`);
|
|
555
|
+
this.save();
|
|
556
|
+
|
|
557
|
+
return {};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
stopLogging(params = {}) {
|
|
561
|
+
const name = params.Name;
|
|
562
|
+
if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
|
|
563
|
+
|
|
564
|
+
const status = this.trailStatus.get(name) || {};
|
|
565
|
+
status.isLogging = false;
|
|
566
|
+
status.StopLoggingTime = new Date().toISOString();
|
|
567
|
+
this.trailStatus.set(name, status);
|
|
568
|
+
|
|
569
|
+
this.logger.info(`[CloudTrail] Stopped logging for trail: ${name}`);
|
|
570
|
+
this.save();
|
|
571
|
+
|
|
572
|
+
return {};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ─── Event Selectors ────────────────────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
getEventSelectors(params = {}) {
|
|
578
|
+
const name = params.TrailName;
|
|
579
|
+
if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
|
|
580
|
+
|
|
581
|
+
const trail = this.trails.get(name);
|
|
582
|
+
const selectors = this.eventSelectors.get(name) || [];
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
TrailARN: trail.TrailARN,
|
|
586
|
+
EventSelectors: selectors,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
putEventSelectors(params = {}) {
|
|
591
|
+
const name = params.TrailName;
|
|
592
|
+
if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
|
|
593
|
+
|
|
594
|
+
const selectors = params.EventSelectors || [];
|
|
595
|
+
this.eventSelectors.set(name, selectors);
|
|
596
|
+
|
|
597
|
+
const trail = this.trails.get(name);
|
|
598
|
+
trail.HasCustomEventSelectors = selectors.length > 0;
|
|
599
|
+
this.trails.set(name, trail);
|
|
600
|
+
|
|
601
|
+
this.logger.info(`[CloudTrail] Event selectors updated for trail: ${name}`);
|
|
602
|
+
this.save();
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
TrailARN: trail.TrailARN,
|
|
606
|
+
EventSelectors: selectors,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ─── LookupEvents ───────────────────────────────────────────────────────────
|
|
611
|
+
|
|
612
|
+
lookupEvents(params = {}) {
|
|
613
|
+
let events = [...this.events];
|
|
614
|
+
|
|
615
|
+
// Filtro por tempo
|
|
616
|
+
if (params.StartTime) {
|
|
617
|
+
const startTime = new Date(params.StartTime).getTime();
|
|
618
|
+
events = events.filter(e => new Date(e.EventTime).getTime() >= startTime);
|
|
619
|
+
}
|
|
620
|
+
if (params.EndTime) {
|
|
621
|
+
const endTime = new Date(params.EndTime).getTime();
|
|
622
|
+
events = events.filter(e => new Date(e.EventTime).getTime() <= endTime);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Filtro por atributo
|
|
626
|
+
if (params.LookupAttributes && params.LookupAttributes.length > 0) {
|
|
627
|
+
for (const attr of params.LookupAttributes) {
|
|
628
|
+
events = events.filter(e => matchesFilter(e, attr));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Ordem: mais recente primeiro
|
|
633
|
+
events = events.sort((a, b) => new Date(b.EventTime) - new Date(a.EventTime));
|
|
634
|
+
|
|
635
|
+
// Paginação
|
|
636
|
+
const maxResults = Math.min(params.MaxResults || MAX_RESULTS_DEFAULT, MAX_RESULTS_MAX);
|
|
637
|
+
let startIdx = 0;
|
|
638
|
+
|
|
639
|
+
if (params.NextToken) {
|
|
640
|
+
try {
|
|
641
|
+
startIdx = parseInt(Buffer.from(params.NextToken, 'base64').toString(), 10);
|
|
642
|
+
} catch {
|
|
643
|
+
startIdx = 0;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const page = events.slice(startIdx, startIdx + maxResults);
|
|
648
|
+
const nextIdx = startIdx + maxResults;
|
|
649
|
+
const nextToken = nextIdx < events.length
|
|
650
|
+
? Buffer.from(String(nextIdx)).toString('base64')
|
|
651
|
+
: null;
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
Events: page,
|
|
655
|
+
NextToken: nextToken,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ─── Tags ───────────────────────────────────────────────────────────────────
|
|
660
|
+
|
|
661
|
+
addTags(params = {}) {
|
|
662
|
+
const arn = params.ResourceId;
|
|
663
|
+
// Encontra trail pelo ARN ou nome
|
|
664
|
+
for (const [name, trail] of this.trails) {
|
|
665
|
+
if (trail.TrailARN === arn || trail.Name === arn) {
|
|
666
|
+
trail.Tags = trail.Tags || [];
|
|
667
|
+
for (const tag of (params.TagsList || [])) {
|
|
668
|
+
const existing = trail.Tags.findIndex(t => t.Key === tag.Key);
|
|
669
|
+
if (existing >= 0) trail.Tags[existing] = tag;
|
|
670
|
+
else trail.Tags.push(tag);
|
|
671
|
+
}
|
|
672
|
+
this.trails.set(name, trail);
|
|
673
|
+
this.save();
|
|
674
|
+
return {};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
throw Errors.TrailNotFound(arn);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
removeTags(params = {}) {
|
|
681
|
+
const arn = params.ResourceId;
|
|
682
|
+
for (const [name, trail] of this.trails) {
|
|
683
|
+
if (trail.TrailARN === arn || trail.Name === arn) {
|
|
684
|
+
const keysToRemove = (params.TagsList || []).map(t => t.Key);
|
|
685
|
+
trail.Tags = (trail.Tags || []).filter(t => !keysToRemove.includes(t.Key));
|
|
686
|
+
this.trails.set(name, trail);
|
|
687
|
+
this.save();
|
|
688
|
+
return {};
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
throw Errors.TrailNotFound(arn);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
listTags(params = {}) {
|
|
695
|
+
const result = [];
|
|
696
|
+
for (const arn of (params.ResourceIdList || [])) {
|
|
697
|
+
for (const [, trail] of this.trails) {
|
|
698
|
+
if (trail.TrailARN === arn || trail.Name === arn) {
|
|
699
|
+
result.push({ ResourceId: trail.TrailARN, TagsList: trail.Tags || [] });
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return { ResourceTagList: result };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ─── Admin ──────────────────────────────────────────────────────────────────
|
|
708
|
+
|
|
709
|
+
getStatus() {
|
|
710
|
+
return {
|
|
711
|
+
service: 'cloudtrail',
|
|
712
|
+
trails: this.trails.size,
|
|
713
|
+
events: this.events.length,
|
|
714
|
+
activeTrails: [...this.trailStatus.values()].filter(s => s.isLogging).length,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
module.exports = { CloudTrailSimulator };
|