infrawise 0.17.0 → 0.18.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 CHANGED
@@ -185,7 +185,7 @@ Add to your editor's MCP config:
185
185
  | `get_queue_details` | SQS queues — DLQ status, encryption, FIFO type, visibility timeout, message counts |
186
186
  | `get_api_routes` | API Gateway APIs (REST, HTTP, WebSocket) — routes, HTTP methods, paths, and Lambda integrations |
187
187
  | `get_topic_details` | SNS topics — subscription counts, protocols, and filter policies (required message attributes per subscription) |
188
- | `get_secrets_overview` | Secrets Manager — names and rotation status (values never included) |
188
+ | `get_secrets_overview` | Secrets Manager — names, rotation status, and key names inferred from code (values never included) |
189
189
  | `get_parameter_overview` | SSM Parameter Store — names, types, tiers (values never included) |
190
190
  | `get_lambda_overview` | Lambda functions — runtime, memory, timeout, execution role ARN, triggers (SQS/SNS/DynamoDB/Kinesis/MSK/EventBridge/S3), env var key names |
191
191
  | `get_eventbridge_details` | EventBridge rules — name, state, schedule/event pattern, target functions |
@@ -518,7 +518,7 @@ src/
518
518
 
519
519
  ## Roadmap
520
520
 
521
- Feature roadmap is tracked in the [GitHub Project](https://github.com/users/Sidd27/projects/1). Feature requests and upvotes welcome.
521
+ Feature roadmap is tracked in the [Infrawise v1](https://github.com/users/Sidd27/projects/1) project board. Feature requests and upvotes welcome.
522
522
 
523
523
  ---
524
524
 
@@ -95,29 +95,76 @@ const PRISMA_METHODS = new Set([
95
95
  'deleteMany',
96
96
  'updateMany',
97
97
  ]);
98
- function getEnclosingFunctionName(node) {
98
+ function getEnclosingFunction(node) {
99
99
  let current = node.getParent();
100
100
  while (current) {
101
101
  if (Node.isFunctionDeclaration(current) ||
102
102
  Node.isFunctionExpression(current) ||
103
103
  Node.isArrowFunction(current) ||
104
104
  Node.isMethodDeclaration(current)) {
105
- if (Node.isFunctionDeclaration(current) || Node.isMethodDeclaration(current)) {
106
- return current.getName() ?? '<anonymous>';
107
- }
108
- // Arrow function or function expression — check if assigned to a variable
109
- const parent = current.getParent();
110
- if (parent && Node.isVariableDeclaration(parent)) {
111
- return parent.getName();
105
+ return current;
106
+ }
107
+ current = current.getParent();
108
+ }
109
+ return undefined;
110
+ }
111
+ function getEnclosingFunctionName(node) {
112
+ const current = getEnclosingFunction(node);
113
+ if (!current)
114
+ return '<module>';
115
+ if (Node.isFunctionDeclaration(current) || Node.isMethodDeclaration(current)) {
116
+ return current.getName() ?? '<anonymous>';
117
+ }
118
+ // Arrow function or function expression — check if assigned to a variable
119
+ const parent = current.getParent();
120
+ if (parent && Node.isVariableDeclaration(parent)) {
121
+ return parent.getName();
122
+ }
123
+ if (parent && Node.isPropertyAssignment(parent)) {
124
+ return parent.getName();
125
+ }
126
+ return '<anonymous>';
127
+ }
128
+ // Scans the enclosing function for JSON.parse(x.SecretString) usage and collects the property
129
+ // names accessed on the parsed result — never the secret values themselves.
130
+ function extractSecretKeys(func) {
131
+ if (!func)
132
+ return undefined;
133
+ const keys = new Set();
134
+ for (const call of func.getDescendantsOfKind(SyntaxKind.CallExpression)) {
135
+ const expr = call.getExpression();
136
+ if (!Node.isPropertyAccessExpression(expr) || expr.getName() !== 'parse')
137
+ continue;
138
+ if (expr.getExpression().getText() !== 'JSON')
139
+ continue;
140
+ const arg = call.getArguments()[0];
141
+ if (!arg || !arg.getText().includes('SecretString'))
142
+ continue;
143
+ const parent = call.getParent();
144
+ if (Node.isPropertyAccessExpression(parent) && parent.getExpression() === call) {
145
+ keys.add(parent.getName());
146
+ continue;
147
+ }
148
+ if (Node.isVariableDeclaration(parent)) {
149
+ const nameNode = parent.getNameNode();
150
+ if (Node.isObjectBindingPattern(nameNode)) {
151
+ for (const el of nameNode.getElements()) {
152
+ const propName = el.getPropertyNameNode();
153
+ keys.add(propName ? propName.getText() : el.getName());
154
+ }
155
+ continue;
112
156
  }
113
- if (parent && Node.isPropertyAssignment(parent)) {
114
- return parent.getName();
157
+ if (Node.isIdentifier(nameNode)) {
158
+ const varName = nameNode.getText();
159
+ for (const access of func.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
160
+ if (access.getExpression().getText() === varName) {
161
+ keys.add(access.getName());
162
+ }
163
+ }
115
164
  }
116
- return '<anonymous>';
117
165
  }
118
- current = current.getParent();
119
166
  }
120
- return '<module>';
167
+ return keys.size > 0 ? Array.from(keys).sort() : undefined;
121
168
  }
122
169
  function extractTableNameFromArg(arg) {
123
170
  if (Node.isStringLiteral(arg)) {
@@ -505,6 +552,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
505
552
  serviceType: 'secretsmanager',
506
553
  target: cmdArgs.length > 0 ? extractArgValue(cmdArgs[0], ...SECRETS_ARG_KEYS) : 'unknown',
507
554
  filePath,
555
+ keys: extractSecretKeys(getEnclosingFunction(callExpr)),
508
556
  };
509
557
  }
510
558
  if (LAMBDA_COMMANDS.has(className)) {
@@ -558,6 +606,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
558
606
  serviceType: 'secretsmanager',
559
607
  target: args.length > 0 ? extractArgValue(args[0], ...SECRETS_ARG_KEYS) : 'unknown',
560
608
  filePath,
609
+ keys: extractSecretKeys(getEnclosingFunction(callExpr)),
561
610
  };
562
611
  }
563
612
  if (LAMBDA_COMMANDS.has(methodName) && (objText.includes('lambda') || objText.includes('fn'))) {
@@ -586,6 +635,8 @@ function detectLanguages(root) {
586
635
  const stack = [root];
587
636
  while (stack.length > 0 && !(hasTypeScript && hasPython)) {
588
637
  const dir = stack.pop();
638
+ if (dir === undefined)
639
+ break;
589
640
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
590
641
  if (entry.isDirectory()) {
591
642
  if (!PROBE_EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
@@ -69,6 +69,33 @@ def sql_table(sql):
69
69
  return 'unknown'
70
70
 
71
71
 
72
+ def const_str(node):
73
+ if isinstance(node, ast.Index): # py3.8 compat
74
+ node = node.value
75
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
76
+ return node.value
77
+ return None
78
+
79
+
80
+ def is_secret_string_ref(node):
81
+ if isinstance(node, ast.Attribute) and node.attr == 'SecretString':
82
+ return True
83
+ if isinstance(node, ast.Subscript):
84
+ return const_str(node.slice) == 'SecretString'
85
+ return False
86
+
87
+
88
+ def is_json_loads_secret(node):
89
+ return (
90
+ isinstance(node, ast.Call)
91
+ and isinstance(node.func, ast.Attribute)
92
+ and node.func.attr == 'loads'
93
+ and expr_text(node.func.value) == 'json'
94
+ and node.args
95
+ and is_secret_string_ref(node.args[0])
96
+ )
97
+
98
+
72
99
  class Visitor(ast.NodeVisitor):
73
100
  def __init__(self, file_path):
74
101
  self.file_path = file_path
@@ -93,11 +120,54 @@ class Visitor(ast.NodeVisitor):
93
120
 
94
121
  def visit_FunctionDef(self, node):
95
122
  self.stack.append(node.name)
123
+ start = len(self.ops)
96
124
  self.generic_visit(node)
125
+ keys = self.find_secret_keys(node)
126
+ if keys:
127
+ for op in self.ops[start:]:
128
+ if op['serviceType'] == 'secretsmanager':
129
+ op['keys'] = keys
97
130
  self.stack.pop()
98
131
 
99
132
  visit_AsyncFunctionDef = visit_FunctionDef
100
133
 
134
+ # Finds json.loads(x['SecretString']) usage in this function and collects the key names
135
+ # accessed on the parsed result (via subscript or .get) — never the secret values.
136
+ def find_secret_keys(self, func_node):
137
+ keys = set()
138
+ tracked_vars = set()
139
+
140
+ for n in ast.walk(func_node):
141
+ if isinstance(n, ast.Assign) and len(n.targets) == 1 and isinstance(n.targets[0], ast.Name):
142
+ if is_json_loads_secret(n.value):
143
+ tracked_vars.add(n.targets[0].id)
144
+ elif isinstance(n.value, ast.Subscript) and is_json_loads_secret(n.value.value):
145
+ key = const_str(n.value.slice)
146
+ if key:
147
+ keys.add(key)
148
+
149
+ if not tracked_vars:
150
+ return sorted(keys)
151
+
152
+ for n in ast.walk(func_node):
153
+ if isinstance(n, ast.Subscript) and isinstance(n.value, ast.Name) and n.value.id in tracked_vars:
154
+ key = const_str(n.slice)
155
+ if key:
156
+ keys.add(key)
157
+ elif (
158
+ isinstance(n, ast.Call)
159
+ and isinstance(n.func, ast.Attribute)
160
+ and n.func.attr == 'get'
161
+ and isinstance(n.func.value, ast.Name)
162
+ and n.func.value.id in tracked_vars
163
+ and n.args
164
+ ):
165
+ key = const_str(n.args[0])
166
+ if key:
167
+ keys.add(key)
168
+
169
+ return sorted(keys)
170
+
101
171
  def resolve_string(self, node):
102
172
  if isinstance(node, ast.Constant) and isinstance(node.value, str):
103
173
  return node.value
@@ -386,6 +386,12 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
386
386
  provider: 'aws',
387
387
  rotationEnabled: false,
388
388
  });
389
+ if (op.keys && op.keys.length > 0) {
390
+ const node = nodeMap.get(secretId);
391
+ if (node && node.type === 'secret') {
392
+ node.referencedKeys = Array.from(new Set([...(node.referencedKeys ?? []), ...op.keys])).sort();
393
+ }
394
+ }
389
395
  edges.push({ from: funcNodeId, to: secretId, type: 'reads_secret' });
390
396
  continue;
391
397
  }
@@ -344,7 +344,7 @@ export function createMcpServer() {
344
344
  });
345
345
  }));
346
346
  mcp.registerTool('get_secrets_overview', {
347
- description: 'Returns all Secrets Manager secrets with rotation status and rotation interval. Secret values are never returned. Call this when checking which secrets exist, confirming rotation is enabled before a security review, or identifying secrets that lack rotation.',
347
+ description: 'Returns all Secrets Manager secrets with rotation status, rotation interval, and referencedKeys — key names (e.g. "password", "apiKey") inferred from application code that parses the secret, never the values. Call this when checking which secrets exist, confirming rotation is enabled before a security review, or before writing code that reads a secret so you use the correct key name instead of guessing.',
348
348
  inputSchema: z.object({}),
349
349
  }, logged('get_secrets_overview', async () => {
350
350
  const secrets = getSecretNodes(currentGraph);
@@ -357,6 +357,7 @@ export function createMcpServer() {
357
357
  provider: s.provider,
358
358
  rotationEnabled: s.rotationEnabled,
359
359
  rotationDays: s.rotationDays,
360
+ referencedKeys: s.referencedKeys ?? [],
360
361
  findings: secretFindings
361
362
  .filter((f) => f.metadata.secretName === s.name)
362
363
  .map((f) => ({ severity: f.severity, issue: f.issue })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infrawise",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "mcpName": "io.github.Sidd27/infrawise",
5
5
  "description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, S3, API Gateway, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
6
6
  "keywords": [