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 +2 -2
- package/dist/context/index.js +64 -13
- package/dist/context/scanner.py +70 -0
- package/dist/graph/index.js +6 -0
- package/dist/server/index.js +2 -1
- package/package.json +1 -1
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
|
|
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 [
|
|
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
|
|
package/dist/context/index.js
CHANGED
|
@@ -95,29 +95,76 @@ const PRISMA_METHODS = new Set([
|
|
|
95
95
|
'deleteMany',
|
|
96
96
|
'updateMany',
|
|
97
97
|
]);
|
|
98
|
-
function
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 (
|
|
114
|
-
|
|
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
|
|
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('.')) {
|
package/dist/context/scanner.py
CHANGED
|
@@ -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
|
package/dist/graph/index.js
CHANGED
|
@@ -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
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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.
|
|
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": [
|