infrawise 0.8.2 → 0.8.3

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 CHANGED
@@ -404,53 +404,7 @@ Infrawise does not use an LLM to analyze your infrastructure. All extraction and
404
404
 
405
405
  ## Architecture overview
406
406
 
407
- ```mermaid
408
- flowchart LR
409
- subgraph IN["Your Infrastructure & Code"]
410
- direction TB
411
- D["DynamoDB"]
412
- L["Lambda · SQS · SNS\nEventBridge · RDS"]
413
- S["Secrets Manager · SSM\nCloudWatch"]
414
- P["PostgreSQL · MySQL"]
415
- M["MongoDB"]
416
- T["Terraform · CDK\nCloudFormation"]
417
- C["TypeScript / JS"]
418
- end
419
-
420
- A["Adapters"]
421
- G["Graph Engine"]
422
- AN["23 Analyzers"]
423
- CA["Cache"]
424
-
425
- subgraph SV["infrawise dev"]
426
- MCP["MCP Server\nlocalhost:3000/mcp"]
427
- end
428
-
429
- subgraph AI["AI Coding Assistants"]
430
- direction TB
431
- CC["Claude Code"]
432
- CU["Cursor"]
433
- WS["Windsurf"]
434
- end
435
-
436
- D & L & S & P & M & T & C --> A
437
- A --> G --> AN --> CA --> MCP
438
- MCP --> CC & CU & WS
439
-
440
- classDef aws fill:#FF9900,stroke:#232F3E,color:#000
441
- classDef db fill:#336791,stroke:#1a3a5c,color:#fff
442
- classDef iac fill:#7B42BC,stroke:#4a2080,color:#fff
443
- classDef code fill:#3178C6,stroke:#1a4a80,color:#fff
444
- classDef iw fill:#1a1a2e,stroke:#e94560,color:#fff
445
- classDef ai fill:#10a37f,stroke:#0a6b54,color:#fff
446
-
447
- class D,L,S aws
448
- class P,M db
449
- class T iac
450
- class C code
451
- class A,G,AN,CA,MCP iw
452
- class CC,CU,WS ai
453
- ```
407
+ ![Architecture](./docs/architecture.svg)
454
408
 
455
409
  ### Source layout
456
410
 
@@ -43,7 +43,9 @@ export async function extractLogsMetadata(cfg = {}) {
43
43
  // Discover log groups
44
44
  const logGroups = [];
45
45
  try {
46
- const prefixes = cfg.logGroupPrefixes?.length ? cfg.logGroupPrefixes : [undefined];
46
+ const prefixes = cfg.logGroupPrefixes?.length
47
+ ? cfg.logGroupPrefixes
48
+ : [undefined];
47
49
  for (const prefix of prefixes) {
48
50
  let nextToken;
49
51
  do {
@@ -48,18 +48,27 @@ export async function extractS3Metadata(cfg = {}) {
48
48
  notifications.push(notification);
49
49
  }
50
50
  }
51
- const versioned = versionResult.status === 'fulfilled'
52
- ? versionResult.value.Status === 'Enabled'
53
- : false;
51
+ const versioned = versionResult.status === 'fulfilled' ? versionResult.value.Status === 'Enabled' : false;
54
52
  const encrypted = encryptResult.status === 'fulfilled'
55
53
  ? (encryptResult.value.ServerSideEncryptionConfiguration?.Rules?.length ?? 0) > 0
56
54
  : false;
57
55
  let publicAccessBlocked = false;
58
56
  if (pabResult.status === 'fulfilled') {
59
57
  const pab = pabResult.value.PublicAccessBlockConfiguration ?? {};
60
- publicAccessBlocked = !!(pab.BlockPublicAcls && pab.IgnorePublicAcls && pab.BlockPublicPolicy && pab.RestrictPublicBuckets);
58
+ publicAccessBlocked = !!(pab.BlockPublicAcls &&
59
+ pab.IgnorePublicAcls &&
60
+ pab.BlockPublicPolicy &&
61
+ pab.RestrictPublicBuckets);
61
62
  }
62
- buckets.push({ name, arn, createdAt, versioned, encrypted, publicAccessBlocked, notifications });
63
+ buckets.push({
64
+ name,
65
+ arn,
66
+ createdAt,
67
+ versioned,
68
+ encrypted,
69
+ publicAccessBlocked,
70
+ notifications,
71
+ });
63
72
  }
64
73
  }
65
74
  catch (err) {
@@ -1,10 +1,10 @@
1
- import { SQSClient, ListQueuesCommand, GetQueueAttributesCommand, } from '@aws-sdk/client-sqs';
1
+ import { SQSClient, ListQueuesCommand, GetQueueAttributesCommand } from '@aws-sdk/client-sqs';
2
2
  import { SNSClient, ListTopicsCommand, GetTopicAttributesCommand, ListSubscriptionsByTopicCommand, } from '@aws-sdk/client-sns';
3
- import { SSMClient, DescribeParametersCommand, } from '@aws-sdk/client-ssm';
4
- import { SecretsManagerClient, ListSecretsCommand, } from '@aws-sdk/client-secrets-manager';
3
+ import { SSMClient, DescribeParametersCommand } from '@aws-sdk/client-ssm';
4
+ import { SecretsManagerClient, ListSecretsCommand } from '@aws-sdk/client-secrets-manager';
5
5
  import { LambdaClient, ListFunctionsCommand, ListEventSourceMappingsCommand, } from '@aws-sdk/client-lambda';
6
6
  import { EventBridgeClient, ListRulesCommand, ListTargetsByRuleCommand, } from '@aws-sdk/client-eventbridge';
7
- import { RDSClient, DescribeDBInstancesCommand, } from '@aws-sdk/client-rds';
7
+ import { RDSClient, DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
8
8
  import { fromIni } from '@aws-sdk/credential-providers';
9
9
  import { logger } from '../../core/index.js';
10
10
  function clientConfig(cfg) {
@@ -33,9 +33,14 @@ export async function extractSQSMetadata(cfg = {}) {
33
33
  const attrs = await client.send(new GetQueueAttributesCommand({
34
34
  QueueUrl: url,
35
35
  AttributeNames: [
36
- 'QueueArn', 'VisibilityTimeout', 'MessageRetentionPeriod',
37
- 'RedrivePolicy', 'KmsMasterKeyId', 'SqsManagedSseEnabled',
38
- 'ApproximateNumberOfMessages', 'ApproximateNumberOfMessagesNotVisible',
36
+ 'QueueArn',
37
+ 'VisibilityTimeout',
38
+ 'MessageRetentionPeriod',
39
+ 'RedrivePolicy',
40
+ 'KmsMasterKeyId',
41
+ 'SqsManagedSseEnabled',
42
+ 'ApproximateNumberOfMessages',
43
+ 'ApproximateNumberOfMessagesNotVisible',
39
44
  ],
40
45
  }));
41
46
  const a = attrs.Attributes ?? {};
@@ -25,9 +25,7 @@ export async function extractMongoMetadata(connectionString, databases) {
25
25
  else {
26
26
  const adminDb = client.db('admin');
27
27
  const dbList = await adminDb.admin().listDatabases();
28
- dbNames = dbList.databases
29
- .map((d) => d.name)
30
- .filter((name) => !SYSTEM_DATABASES.has(name));
28
+ dbNames = dbList.databases.map((d) => d.name).filter((name) => !SYSTEM_DATABASES.has(name));
31
29
  }
32
30
  logger.debug(`Introspecting ${dbNames.length} MongoDB database(s)`);
33
31
  const results = [];
@@ -96,7 +96,8 @@ export async function extractTerraformSchema(repoPath) {
96
96
  partitionKey: tfStr(body, 'hash_key'),
97
97
  sortKey: tfStr(body, 'range_key'),
98
98
  gsiNames: tfGSINames(body),
99
- source: 'terraform', filePath,
99
+ source: 'terraform',
100
+ filePath,
100
101
  });
101
102
  break;
102
103
  case 'aws_db_instance':
@@ -104,13 +105,15 @@ export async function extractTerraformSchema(repoPath) {
104
105
  schema.rdsInstances.push({
105
106
  identifier: tfStr(body, 'identifier') ?? tfStr(body, 'cluster_identifier') ?? resourceName,
106
107
  engine: tfStr(body, 'engine') ?? 'unknown',
107
- source: 'terraform', filePath,
108
+ source: 'terraform',
109
+ filePath,
108
110
  });
109
111
  break;
110
112
  case 'aws_docdb_cluster':
111
113
  schema.mongoClusters.push({
112
114
  identifier: tfStr(body, 'cluster_identifier') ?? resourceName,
113
- source: 'terraform', filePath,
115
+ source: 'terraform',
116
+ filePath,
114
117
  });
115
118
  break;
116
119
  case 'aws_sqs_queue': {
@@ -124,41 +127,47 @@ export async function extractTerraformSchema(repoPath) {
124
127
  schema.topics.push({
125
128
  name: tfStr(body, 'name') ?? resourceName,
126
129
  encrypted: body.includes('kms_master_key_id'),
127
- source: 'terraform', filePath,
130
+ source: 'terraform',
131
+ filePath,
128
132
  });
129
133
  break;
130
134
  case 'aws_lambda_function':
131
135
  schema.lambdas.push({
132
136
  name: tfStr(body, 'function_name') ?? resourceName,
133
137
  runtime: tfStr(body, 'runtime'),
134
- source: 'terraform', filePath,
138
+ source: 'terraform',
139
+ filePath,
135
140
  });
136
141
  break;
137
142
  case 'aws_s3_bucket':
138
143
  schema.buckets.push({
139
144
  name: tfStr(body, 'bucket') ?? tfStr(body, 'bucket_prefix') ?? resourceName,
140
145
  versioned: body.includes('versioning') && body.includes('enabled = true'),
141
- source: 'terraform', filePath,
146
+ source: 'terraform',
147
+ filePath,
142
148
  });
143
149
  break;
144
150
  case 'aws_ssm_parameter':
145
151
  schema.parameters.push({
146
152
  name: tfStr(body, 'name') ?? resourceName,
147
153
  type: tfStr(body, 'type') ?? 'String',
148
- source: 'terraform', filePath,
154
+ source: 'terraform',
155
+ filePath,
149
156
  });
150
157
  break;
151
158
  case 'aws_secretsmanager_secret':
152
159
  schema.secrets.push({
153
160
  name: tfStr(body, 'name') ?? resourceName,
154
- source: 'terraform', filePath,
161
+ source: 'terraform',
162
+ filePath,
155
163
  });
156
164
  break;
157
165
  case 'aws_api_gateway_rest_api':
158
166
  case 'aws_apigatewayv2_api':
159
167
  schema.apiGateways.push({
160
168
  name: tfStr(body, 'name') ?? resourceName,
161
- source: 'terraform', filePath,
169
+ source: 'terraform',
170
+ filePath,
162
171
  });
163
172
  break;
164
173
  }
@@ -171,7 +180,8 @@ function isCloudFormationTemplate(parsed) {
171
180
  if (typeof parsed !== 'object' || parsed === null)
172
181
  return false;
173
182
  const obj = parsed;
174
- return 'AWSTemplateFormatVersion' in obj || ('Resources' in obj && typeof obj['Resources'] === 'object');
183
+ return ('AWSTemplateFormatVersion' in obj ||
184
+ ('Resources' in obj && typeof obj['Resources'] === 'object'));
175
185
  }
176
186
  function parseCFNFile(filePath) {
177
187
  let content;
@@ -242,7 +252,8 @@ function processCFNResources(resources, schema, filePath, source) {
242
252
  partitionKey: pk,
243
253
  sortKey: sk,
244
254
  gsiNames,
245
- source, filePath,
255
+ source,
256
+ filePath,
246
257
  });
247
258
  break;
248
259
  }
@@ -250,20 +261,23 @@ function processCFNResources(resources, schema, filePath, source) {
250
261
  schema.rdsInstances.push({
251
262
  identifier: cfnStr(props, 'DBInstanceIdentifier') ?? logicalId,
252
263
  engine: cfnStr(props, 'Engine') ?? 'unknown',
253
- source, filePath,
264
+ source,
265
+ filePath,
254
266
  });
255
267
  break;
256
268
  case 'AWS::RDS::DBCluster':
257
269
  schema.rdsInstances.push({
258
270
  identifier: cfnStr(props, 'DBClusterIdentifier') ?? logicalId,
259
271
  engine: cfnStr(props, 'Engine') ?? 'aurora',
260
- source, filePath,
272
+ source,
273
+ filePath,
261
274
  });
262
275
  break;
263
276
  case 'AWS::DocDB::DBCluster':
264
277
  schema.mongoClusters.push({
265
278
  identifier: cfnStr(props, 'DBClusterIdentifier') ?? logicalId,
266
- source, filePath,
279
+ source,
280
+ filePath,
267
281
  });
268
282
  break;
269
283
  case 'AWS::SQS::Queue': {
@@ -277,14 +291,16 @@ function processCFNResources(resources, schema, filePath, source) {
277
291
  schema.topics.push({
278
292
  name: cfnStr(props, 'TopicName') ?? logicalId,
279
293
  encrypted: !!props['KmsMasterKeyId'],
280
- source, filePath,
294
+ source,
295
+ filePath,
281
296
  });
282
297
  break;
283
298
  case 'AWS::Lambda::Function':
284
299
  schema.lambdas.push({
285
300
  name: cfnStr(props, 'FunctionName') ?? logicalId,
286
301
  runtime: cfnStr(props, 'Runtime'),
287
- source, filePath,
302
+ source,
303
+ filePath,
288
304
  });
289
305
  break;
290
306
  case 'AWS::S3::Bucket': {
@@ -292,7 +308,8 @@ function processCFNResources(resources, schema, filePath, source) {
292
308
  schema.buckets.push({
293
309
  name: cfnStr(props, 'BucketName') ?? logicalId,
294
310
  versioned: versioningConfig?.['Status'] === 'Enabled',
295
- source, filePath,
311
+ source,
312
+ filePath,
296
313
  });
297
314
  break;
298
315
  }
@@ -300,20 +317,23 @@ function processCFNResources(resources, schema, filePath, source) {
300
317
  schema.parameters.push({
301
318
  name: cfnStr(props, 'Name') ?? logicalId,
302
319
  type: cfnStr(props, 'Type') ?? 'String',
303
- source, filePath,
320
+ source,
321
+ filePath,
304
322
  });
305
323
  break;
306
324
  case 'AWS::SecretsManager::Secret':
307
325
  schema.secrets.push({
308
326
  name: cfnStr(props, 'Name') ?? logicalId,
309
- source, filePath,
327
+ source,
328
+ filePath,
310
329
  });
311
330
  break;
312
331
  case 'AWS::ApiGateway::RestApi':
313
332
  case 'AWS::ApiGatewayV2::Api':
314
333
  schema.apiGateways.push({
315
334
  name: cfnStr(props, 'Name') ?? logicalId,
316
- source, filePath,
335
+ source,
336
+ filePath,
317
337
  });
318
338
  break;
319
339
  }
@@ -340,7 +360,8 @@ export async function extractCDKSchema(repoPath) {
340
360
  // Strategy 1: Parse cdk.out/*.template.json (synthesized CloudFormation)
341
361
  const cdkOutDir = path.join(repoPath, 'cdk.out');
342
362
  if (fs.existsSync(cdkOutDir)) {
343
- const templateFiles = fs.readdirSync(cdkOutDir)
363
+ const templateFiles = fs
364
+ .readdirSync(cdkOutDir)
344
365
  .filter((f) => f.endsWith('.template.json'))
345
366
  .map((f) => path.join(cdkOutDir, f));
346
367
  logger.debug(`Found ${templateFiles.length} CDK synthesized template(s) in cdk.out/`);
@@ -459,10 +480,16 @@ export async function extractIaCSchema(repoPath) {
459
480
  extractCDKSchema(repoPath),
460
481
  ]);
461
482
  const merged = mergeSchemas(tfSchema, cfnSchema, cdkSchema);
462
- const total = merged.dynamoTables.length + merged.rdsInstances.length +
463
- merged.mongoClusters.length + merged.queues.length + merged.topics.length +
464
- merged.lambdas.length + merged.buckets.length + merged.parameters.length +
465
- merged.secrets.length + merged.apiGateways.length;
483
+ const total = merged.dynamoTables.length +
484
+ merged.rdsInstances.length +
485
+ merged.mongoClusters.length +
486
+ merged.queues.length +
487
+ merged.topics.length +
488
+ merged.lambdas.length +
489
+ merged.buckets.length +
490
+ merged.parameters.length +
491
+ merged.secrets.length +
492
+ merged.apiGateways.length;
466
493
  logger.debug(`IaC schema total: ${total} resource(s) across TF/CFN/CDK`);
467
494
  return merged;
468
495
  }
@@ -155,7 +155,11 @@ export class LambdaMissingTriggerDLQAnalyzer {
155
155
  issue: `Lambda "${node.name}" is triggered by "${trigger.sourceName}" which has no DLQ`,
156
156
  description: `"${node.name}" receives events from "${trigger.sourceName}" (${trigger.type.toUpperCase()}). If the Lambda handler fails, messages will be retried and eventually discarded with no failure record.`,
157
157
  recommendation: `Add a DLQ to "${trigger.sourceName}" and set a destination config on the event source mapping so failed batches are captured and inspectable.`,
158
- metadata: { functionName: node.name, triggerSource: trigger.sourceName, triggerType: trigger.type },
158
+ metadata: {
159
+ functionName: node.name,
160
+ triggerSource: trigger.sourceName,
161
+ triggerType: trigger.type,
162
+ },
159
163
  });
160
164
  }
161
165
  }
@@ -12,7 +12,11 @@ export class RDSPubliclyAccessibleAnalyzer {
12
12
  issue: `RDS instance "${node.name}" is publicly accessible`,
13
13
  description: `"${node.name}" (${node.engine}) has PubliclyAccessible=true, meaning it is reachable from the internet. This exposes the database to brute-force and credential-stuffing attacks.`,
14
14
  recommendation: `Set PubliclyAccessible=false on "${node.name}" and use a bastion host, VPN, or VPC peering for private access. If public access is required, enforce strong passwords, IP allowlisting, and TLS.`,
15
- metadata: { dbInstanceIdentifier: node.name, engine: node.engine, instanceClass: node.instanceClass },
15
+ metadata: {
16
+ dbInstanceIdentifier: node.name,
17
+ engine: node.engine,
18
+ instanceClass: node.instanceClass,
19
+ },
16
20
  });
17
21
  }
18
22
  }
@@ -32,7 +36,11 @@ export class RDSNoBackupAnalyzer {
32
36
  issue: `RDS instance "${node.name}" has automated backups disabled`,
33
37
  description: `"${node.name}" has a backup retention period of 0, meaning automated backups are off. Any accidental deletion or corruption is unrecoverable without a manual snapshot.`,
34
38
  recommendation: `Enable automated backups on "${node.name}" with at least 7 days retention (35 days for production workloads). Enable point-in-time recovery.`,
35
- metadata: { dbInstanceIdentifier: node.name, engine: node.engine, backupRetentionDays: node.backupRetentionDays },
39
+ metadata: {
40
+ dbInstanceIdentifier: node.name,
41
+ engine: node.engine,
42
+ backupRetentionDays: node.backupRetentionDays,
43
+ },
36
44
  });
37
45
  }
38
46
  }
@@ -21,7 +21,12 @@ export class IaCDriftAnalyzer {
21
21
  issue: `IaC drift: DynamoDB table "${name}" defined in IaC but not deployed`,
22
22
  description: `"${name}" is in ${fp} but not found in AWS. It may be undeployed or deleted manually.`,
23
23
  recommendation: 'Run `terraform apply` / deploy your stack, or remove the definition from IaC.',
24
- metadata: { resourceType: 'dynamodb_table', name, filePath: fp, driftType: 'defined_not_deployed' },
24
+ metadata: {
25
+ resourceType: 'dynamodb_table',
26
+ name,
27
+ filePath: fp,
28
+ driftType: 'defined_not_deployed',
29
+ },
25
30
  });
26
31
  }
27
32
  }
@@ -46,7 +51,12 @@ export class IaCDriftAnalyzer {
46
51
  issue: `IaC drift: SQS queue "${name}" defined in IaC but not deployed`,
47
52
  description: `SQS queue "${name}" is defined in ${fp} but not found in the live account.`,
48
53
  recommendation: 'Deploy the queue via `terraform apply` or your CFN/CDK stack.',
49
- metadata: { resourceType: 'sqs_queue', name, filePath: fp, driftType: 'defined_not_deployed' },
54
+ metadata: {
55
+ resourceType: 'sqs_queue',
56
+ name,
57
+ filePath: fp,
58
+ driftType: 'defined_not_deployed',
59
+ },
50
60
  });
51
61
  }
52
62
  }
@@ -71,7 +81,12 @@ export class IaCDriftAnalyzer {
71
81
  issue: `IaC drift: Lambda "${name}" defined in IaC but not deployed`,
72
82
  description: `Lambda function "${name}" is defined in ${fp} but not found in the live account.`,
73
83
  recommendation: 'Deploy the function via `terraform apply` or your CFN/CDK stack.',
74
- metadata: { resourceType: 'lambda_function', name, filePath: fp, driftType: 'defined_not_deployed' },
84
+ metadata: {
85
+ resourceType: 'lambda_function',
86
+ name,
87
+ filePath: fp,
88
+ driftType: 'defined_not_deployed',
89
+ },
75
90
  });
76
91
  }
77
92
  }