infrawise 0.16.0 → 0.17.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 +6 -4
- package/dist/adapters/aws/services.js +3 -1
- package/dist/adapters/db/mongodb.js +8 -2
- package/dist/context/index.js +46 -0
- package/dist/context/python.js +42 -0
- package/dist/context/scanner.py +310 -0
- package/dist/server/index.js +1 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -427,10 +427,12 @@ Works from AWS APIs, database schema introspection, and IaC files — no depende
|
|
|
427
427
|
| Runtime signals (opt-in) | Lambda throttling/errors and stale queue messages from CloudWatch metrics |
|
|
428
428
|
| Terraform / CloudFormation / CDK | IaC drift vs deployed state; stack outputs and cross-stack exports |
|
|
429
429
|
|
|
430
|
-
### Code correlation analysis (TypeScript / JavaScript)
|
|
430
|
+
### Code correlation analysis (TypeScript / JavaScript / Python)
|
|
431
431
|
|
|
432
432
|
Uses [ts-morph](https://ts-morph.com/) AST analysis to detect which functions call which tables and how:
|
|
433
433
|
|
|
434
|
+
Python repositories are scanned with a bundled stdlib-`ast` scanner (requires `python3` on PATH; skipped with a warning otherwise): boto3 clients and `dynamodb.Table()` resources, `cursor.execute` SQL, pymongo collections, and kafka-python/confluent-kafka producers and consumers. Language detection is automatic — TypeScript and Python scans each run only when matching files exist.
|
|
435
|
+
|
|
434
436
|
| Analyzer | Severity | What it detects |
|
|
435
437
|
| -------------------------- | -------- | --------------------------------------------- |
|
|
436
438
|
| Full Table Scan (DynamoDB) | High | `.scan()` calls without filters |
|
|
@@ -447,7 +449,7 @@ Uses [ts-morph](https://ts-morph.com/) AST analysis to detect which functions ca
|
|
|
447
449
|
| Pipeline: repeated table access | Medium / Verify | Same table read by 2+ functions in one service pipeline |
|
|
448
450
|
| Pipeline: missing DLQ hop | Medium | Mid-pipeline queue (has producer and consumer) with no Dead Letter Queue |
|
|
449
451
|
|
|
450
|
-
|
|
452
|
+
Projects in other languages still get full value from infrastructure-level analyzers — code correlation (function-to-table mapping, N+1 patterns) currently supports TypeScript, JavaScript, and Python.
|
|
451
453
|
|
|
452
454
|
The scanner supports: AWS SDK v3/v2 for DynamoDB, `pg`/Prisma/Knex for PostgreSQL, `mysql2`/Knex for MySQL, driver/Mongoose for MongoDB, AWS SDK v3 for SQS/SNS/SSM/Secrets/Lambda, and `kafkajs` for Kafka topics (producer/consumer).
|
|
453
455
|
|
|
@@ -498,7 +500,7 @@ src/
|
|
|
498
500
|
db/ PostgreSQL, MySQL, MongoDB
|
|
499
501
|
iac/ Terraform, CDK, CloudFormation (local file parsing)
|
|
500
502
|
analyzers/ 34 rule-based analyzers
|
|
501
|
-
context/ Repository scanner (ts-morph AST)
|
|
503
|
+
context/ Repository scanner (ts-morph AST + Python stdlib-ast subprocess)
|
|
502
504
|
server/ Fastify MCP server (@modelcontextprotocol/sdk, Streamable HTTP)
|
|
503
505
|
cli/ CLI commands (Commander.js)
|
|
504
506
|
```
|
|
@@ -507,7 +509,7 @@ src/
|
|
|
507
509
|
|
|
508
510
|
## Current limitations
|
|
509
511
|
|
|
510
|
-
- Code-level correlation supports TypeScript and
|
|
512
|
+
- Code-level correlation supports TypeScript, JavaScript, and Python (Python requires python3 on PATH)
|
|
511
513
|
- Dynamically constructed queries may not always be resolved statically
|
|
512
514
|
- Runtime tracing is not yet implemented
|
|
513
515
|
- Large monorepos may require future incremental analysis optimization
|
|
@@ -234,13 +234,15 @@ function servicesFromDoc(doc) {
|
|
|
234
234
|
}
|
|
235
235
|
async function extractAllowedServices(roleArn, cfg) {
|
|
236
236
|
const client = new IAMClient(clientConfig(cfg));
|
|
237
|
-
const roleName = roleArn.split('/').pop();
|
|
237
|
+
const roleName = roleArn.split('/').pop() ?? roleArn;
|
|
238
238
|
const services = new Set();
|
|
239
239
|
try {
|
|
240
240
|
let marker;
|
|
241
241
|
do {
|
|
242
242
|
const res = await client.send(new ListAttachedRolePoliciesCommand({ RoleName: roleName, Marker: marker }));
|
|
243
243
|
for (const policy of res.AttachedPolicies ?? []) {
|
|
244
|
+
if (!policy.PolicyArn)
|
|
245
|
+
continue;
|
|
244
246
|
try {
|
|
245
247
|
const meta = await client.send(new GetPolicyCommand({ PolicyArn: policy.PolicyArn }));
|
|
246
248
|
const versionId = meta.Policy?.DefaultVersionId;
|
|
@@ -90,9 +90,15 @@ export async function validateMongoAccess(connectionString) {
|
|
|
90
90
|
serverSelectionTimeoutMS: 5000,
|
|
91
91
|
connectTimeoutMS: 5000,
|
|
92
92
|
});
|
|
93
|
+
// driver can exceed serverSelectionTimeoutMS when SYNs are dropped; hard-cap the probe
|
|
94
|
+
const deadline = new Promise((_, reject) => {
|
|
95
|
+
setTimeout(() => reject(new Error('validation timeout')), 6000).unref();
|
|
96
|
+
});
|
|
93
97
|
try {
|
|
94
|
-
await
|
|
95
|
-
|
|
98
|
+
await Promise.race([
|
|
99
|
+
client.connect().then(() => client.db('admin').command({ ping: 1 })),
|
|
100
|
+
deadline,
|
|
101
|
+
]);
|
|
96
102
|
return true;
|
|
97
103
|
}
|
|
98
104
|
catch {
|
package/dist/context/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as path from 'path';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import { Project, SyntaxKind, Node } from 'ts-morph';
|
|
4
4
|
import { RepositoryScanError, logger } from '../core/index.js';
|
|
5
|
+
import { scanPythonRepository } from './python.js';
|
|
5
6
|
const DYNAMO_OPERATIONS = new Set([
|
|
6
7
|
'query',
|
|
7
8
|
'scan',
|
|
@@ -571,11 +572,56 @@ function detectAWSServiceOperations(callExpr, filePath) {
|
|
|
571
572
|
}
|
|
572
573
|
return null;
|
|
573
574
|
}
|
|
575
|
+
const PROBE_EXCLUDED_DIRS = new Set([
|
|
576
|
+
'node_modules',
|
|
577
|
+
'venv',
|
|
578
|
+
'.venv',
|
|
579
|
+
'site-packages',
|
|
580
|
+
'__pycache__',
|
|
581
|
+
'dist',
|
|
582
|
+
]);
|
|
583
|
+
function detectLanguages(root) {
|
|
584
|
+
let hasTypeScript = fs.existsSync(path.join(root, 'tsconfig.json'));
|
|
585
|
+
let hasPython = false;
|
|
586
|
+
const stack = [root];
|
|
587
|
+
while (stack.length > 0 && !(hasTypeScript && hasPython)) {
|
|
588
|
+
const dir = stack.pop();
|
|
589
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
590
|
+
if (entry.isDirectory()) {
|
|
591
|
+
if (!PROBE_EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
592
|
+
stack.push(path.join(dir, entry.name));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
|
|
596
|
+
hasTypeScript = true;
|
|
597
|
+
}
|
|
598
|
+
else if (entry.name.endsWith('.py')) {
|
|
599
|
+
hasPython = true;
|
|
600
|
+
}
|
|
601
|
+
if (hasTypeScript && hasPython)
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return { hasTypeScript, hasPython };
|
|
606
|
+
}
|
|
574
607
|
export async function scanRepository(repoPath) {
|
|
575
608
|
const resolvedPath = path.resolve(repoPath);
|
|
576
609
|
if (!fs.existsSync(resolvedPath)) {
|
|
577
610
|
throw new RepositoryScanError(`Path does not exist: ${resolvedPath}`);
|
|
578
611
|
}
|
|
612
|
+
const { hasTypeScript, hasPython } = detectLanguages(resolvedPath);
|
|
613
|
+
const detected = [hasTypeScript && 'TypeScript', hasPython && 'Python'].filter(Boolean);
|
|
614
|
+
logger.info(`Detected languages: ${detected.length > 0 ? detected.join(', ') : 'none'}`);
|
|
615
|
+
const operations = [];
|
|
616
|
+
if (hasTypeScript) {
|
|
617
|
+
operations.push(...scanTypeScriptRepository(resolvedPath));
|
|
618
|
+
}
|
|
619
|
+
if (hasPython) {
|
|
620
|
+
operations.push(...(await scanPythonRepository(resolvedPath)));
|
|
621
|
+
}
|
|
622
|
+
return operations;
|
|
623
|
+
}
|
|
624
|
+
function scanTypeScriptRepository(resolvedPath) {
|
|
579
625
|
const tsconfigPath = path.join(resolvedPath, 'tsconfig.json');
|
|
580
626
|
const hasTsConfig = fs.existsSync(tsconfigPath);
|
|
581
627
|
const project = new Project({
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { logger } from '../core/index.js';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const SCANNER_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), 'scanner.py');
|
|
8
|
+
let cachedInterpreter;
|
|
9
|
+
async function findPythonInterpreter() {
|
|
10
|
+
if (cachedInterpreter !== undefined)
|
|
11
|
+
return cachedInterpreter;
|
|
12
|
+
for (const cmd of ['python3', 'python', 'py']) {
|
|
13
|
+
try {
|
|
14
|
+
await execFileAsync(cmd, ['--version']);
|
|
15
|
+
cachedInterpreter = cmd;
|
|
16
|
+
return cmd;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// try next candidate
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
cachedInterpreter = null;
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export async function scanPythonRepository(repoPath) {
|
|
26
|
+
const interpreter = await findPythonInterpreter();
|
|
27
|
+
if (interpreter === null) {
|
|
28
|
+
logger.warn('Python files found but no python3 on PATH — skipping Python scan');
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const { stdout } = await execFileAsync(interpreter, [SCANNER_PATH, repoPath], {
|
|
33
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
34
|
+
});
|
|
35
|
+
return JSON.parse(stdout);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const message = err instanceof Error ? err.message.slice(0, 200) : String(err);
|
|
39
|
+
logger.warn(`Python scan failed — skipping (${message})`);
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
EXCLUDED_DIRS = {'node_modules', 'venv', '.venv', 'site-packages', '__pycache__', 'dist'}
|
|
8
|
+
|
|
9
|
+
SQS_METHODS = {'send_message', 'send_message_batch', 'receive_message', 'delete_message'}
|
|
10
|
+
SNS_METHODS = {'publish', 'publish_batch'}
|
|
11
|
+
SSM_METHODS = {'get_parameter', 'get_parameters', 'get_parameters_by_path'}
|
|
12
|
+
SECRETS_METHODS = {'get_secret_value'}
|
|
13
|
+
LAMBDA_METHODS = {'invoke', 'invoke_async'}
|
|
14
|
+
DYNAMO_METHODS = {'query', 'scan', 'get_item', 'put_item', 'update_item', 'delete_item',
|
|
15
|
+
'batch_get_item', 'batch_write_item'}
|
|
16
|
+
SQL_METHODS = {'execute', 'executemany'}
|
|
17
|
+
SQL_RECEIVER_HINTS = ('cursor', 'cur', 'conn', 'connection', 'session', 'db', 'pool',
|
|
18
|
+
'pg', 'psycopg', 'asyncpg', 'engine', 'mysql', 'pymysql', 'maria')
|
|
19
|
+
MYSQL_HINTS = ('mysql', 'pymysql', 'maria')
|
|
20
|
+
SQL_TABLE_PATTERNS = [
|
|
21
|
+
re.compile(r'FROM\s+["\']?(\w+)["\']?', re.IGNORECASE),
|
|
22
|
+
re.compile(r'INTO\s+["\']?(\w+)["\']?', re.IGNORECASE),
|
|
23
|
+
re.compile(r'UPDATE\s+["\']?(\w+)["\']?', re.IGNORECASE),
|
|
24
|
+
re.compile(r'JOIN\s+["\']?(\w+)["\']?', re.IGNORECASE),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
MONGO_METHODS = {'find', 'find_one', 'insert_one', 'insert_many', 'update_one', 'update_many',
|
|
28
|
+
'delete_one', 'delete_many', 'aggregate', 'count_documents',
|
|
29
|
+
'estimated_document_count'}
|
|
30
|
+
MONGO_SCAN_METHODS = {'find', 'aggregate'}
|
|
31
|
+
MONGO_BASE_HINTS = ('db', 'mongo')
|
|
32
|
+
KAFKA_PRODUCER_METHODS = {'send', 'produce'}
|
|
33
|
+
KAFKA_HINTS = ('kafka', 'producer', 'consumer')
|
|
34
|
+
|
|
35
|
+
SERVICE_RULES = [
|
|
36
|
+
('sqs', SQS_METHODS, ('QueueUrl', 'QueueName'), ('sqs', 'queue')),
|
|
37
|
+
('sns', SNS_METHODS, ('TopicArn', 'TargetArn'), ('sns', 'topic')),
|
|
38
|
+
('ssm', SSM_METHODS, ('Name', 'Path'), ('ssm', 'parameter')),
|
|
39
|
+
('secretsmanager', SECRETS_METHODS, ('SecretId',), ('secret',)),
|
|
40
|
+
('lambda', LAMBDA_METHODS, ('FunctionName',), ('lambda', 'fn')),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def expr_text(node):
|
|
45
|
+
if isinstance(node, ast.Name):
|
|
46
|
+
return node.id
|
|
47
|
+
if isinstance(node, ast.Attribute):
|
|
48
|
+
return expr_text(node.value) + '.' + node.attr
|
|
49
|
+
if isinstance(node, ast.Call):
|
|
50
|
+
return expr_text(node.func) + '()'
|
|
51
|
+
if isinstance(node, ast.Subscript):
|
|
52
|
+
return expr_text(node.value) + '[]'
|
|
53
|
+
return ''
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def short_name(val):
|
|
57
|
+
if val.startswith('http://') or val.startswith('https://'):
|
|
58
|
+
return val.rstrip('/').rsplit('/', 1)[-1]
|
|
59
|
+
if ':' in val:
|
|
60
|
+
return val.rsplit(':', 1)[-1]
|
|
61
|
+
return val
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def sql_table(sql):
|
|
65
|
+
for pattern in SQL_TABLE_PATTERNS:
|
|
66
|
+
match = pattern.search(sql)
|
|
67
|
+
if match:
|
|
68
|
+
return match.group(1)
|
|
69
|
+
return 'unknown'
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Visitor(ast.NodeVisitor):
|
|
73
|
+
def __init__(self, file_path):
|
|
74
|
+
self.file_path = file_path
|
|
75
|
+
self.ops = []
|
|
76
|
+
self.stack = []
|
|
77
|
+
self.strings = {}
|
|
78
|
+
self.boto_clients = {}
|
|
79
|
+
self.dynamo_tables = {}
|
|
80
|
+
self.collections = {}
|
|
81
|
+
|
|
82
|
+
def function_name(self):
|
|
83
|
+
return self.stack[-1] if self.stack else '<module>'
|
|
84
|
+
|
|
85
|
+
def add(self, op_type, service, target):
|
|
86
|
+
self.ops.append({
|
|
87
|
+
'functionName': self.function_name(),
|
|
88
|
+
'operationType': op_type,
|
|
89
|
+
'serviceType': service,
|
|
90
|
+
'target': target,
|
|
91
|
+
'filePath': self.file_path,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
def visit_FunctionDef(self, node):
|
|
95
|
+
self.stack.append(node.name)
|
|
96
|
+
self.generic_visit(node)
|
|
97
|
+
self.stack.pop()
|
|
98
|
+
|
|
99
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
100
|
+
|
|
101
|
+
def resolve_string(self, node):
|
|
102
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
103
|
+
return node.value
|
|
104
|
+
if isinstance(node, ast.Name):
|
|
105
|
+
return self.strings.get(node.id)
|
|
106
|
+
if isinstance(node, ast.JoinedStr):
|
|
107
|
+
parts = []
|
|
108
|
+
for value in node.values:
|
|
109
|
+
if isinstance(value, ast.Constant) and isinstance(value.value, str):
|
|
110
|
+
parts.append(value.value)
|
|
111
|
+
elif isinstance(value, ast.FormattedValue):
|
|
112
|
+
resolved = self.resolve_string(value.value)
|
|
113
|
+
if resolved is None:
|
|
114
|
+
return None
|
|
115
|
+
parts.append(resolved)
|
|
116
|
+
else:
|
|
117
|
+
return None
|
|
118
|
+
return ''.join(parts)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def kwarg(self, node, keys):
|
|
122
|
+
for kw in node.keywords:
|
|
123
|
+
if kw.arg in keys:
|
|
124
|
+
return self.resolve_string(kw.value)
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def boto3_service(self, node):
|
|
128
|
+
if (
|
|
129
|
+
isinstance(node, ast.Call)
|
|
130
|
+
and isinstance(node.func, ast.Attribute)
|
|
131
|
+
and node.func.attr in ('client', 'resource')
|
|
132
|
+
and expr_text(node.func.value) == 'boto3'
|
|
133
|
+
and node.args
|
|
134
|
+
):
|
|
135
|
+
return self.resolve_string(node.args[0])
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
def table_name(self, node):
|
|
139
|
+
if (
|
|
140
|
+
isinstance(node, ast.Call)
|
|
141
|
+
and isinstance(node.func, ast.Attribute)
|
|
142
|
+
and node.func.attr == 'Table'
|
|
143
|
+
and node.args
|
|
144
|
+
):
|
|
145
|
+
return self.resolve_string(node.args[0])
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def collection_name(self, node):
|
|
149
|
+
if isinstance(node, ast.Subscript):
|
|
150
|
+
base = expr_text(node.value).lower()
|
|
151
|
+
if any(h in base for h in MONGO_BASE_HINTS):
|
|
152
|
+
return self.resolve_string(node.slice)
|
|
153
|
+
if (
|
|
154
|
+
isinstance(node, ast.Call)
|
|
155
|
+
and isinstance(node.func, ast.Attribute)
|
|
156
|
+
and node.func.attr == 'get_collection'
|
|
157
|
+
and node.args
|
|
158
|
+
):
|
|
159
|
+
return self.resolve_string(node.args[0])
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
def visit_Assign(self, node):
|
|
163
|
+
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
164
|
+
name = node.targets[0].id
|
|
165
|
+
resolved = self.resolve_string(node.value)
|
|
166
|
+
if resolved is not None:
|
|
167
|
+
self.strings[name] = resolved
|
|
168
|
+
else:
|
|
169
|
+
service = self.boto3_service(node.value)
|
|
170
|
+
table = self.table_name(node.value)
|
|
171
|
+
collection = self.collection_name(node.value)
|
|
172
|
+
if service is not None:
|
|
173
|
+
self.boto_clients[name] = service
|
|
174
|
+
elif table is not None:
|
|
175
|
+
self.dynamo_tables[name] = table
|
|
176
|
+
elif collection is not None:
|
|
177
|
+
self.collections[name] = collection
|
|
178
|
+
self.generic_visit(node)
|
|
179
|
+
|
|
180
|
+
def visit_Call(self, node):
|
|
181
|
+
if isinstance(node.func, ast.Attribute):
|
|
182
|
+
method = node.func.attr
|
|
183
|
+
recv = node.func.value
|
|
184
|
+
recv_text = expr_text(recv).lower()
|
|
185
|
+
_ = (
|
|
186
|
+
self.detect_dynamo(node, method, recv, recv_text)
|
|
187
|
+
or self.detect_sql(node, method, recv_text)
|
|
188
|
+
or self.detect_mongo(node, method, recv, recv_text)
|
|
189
|
+
or self.detect_kafka(node, method, recv_text)
|
|
190
|
+
or self.detect_aws_client(node, method, recv, recv_text)
|
|
191
|
+
)
|
|
192
|
+
self.generic_visit(node)
|
|
193
|
+
|
|
194
|
+
def detect_dynamo(self, node, method, recv, recv_text):
|
|
195
|
+
if method not in DYNAMO_METHODS:
|
|
196
|
+
return False
|
|
197
|
+
if isinstance(recv, ast.Name) and recv.id in self.dynamo_tables:
|
|
198
|
+
self.add(method, 'dynamodb', self.dynamo_tables[recv.id])
|
|
199
|
+
return True
|
|
200
|
+
inline = self.table_name(recv)
|
|
201
|
+
if inline is not None:
|
|
202
|
+
self.add(method, 'dynamodb', inline)
|
|
203
|
+
return True
|
|
204
|
+
kw = self.kwarg(node, ('TableName',))
|
|
205
|
+
tracked = isinstance(recv, ast.Name) and self.boto_clients.get(recv.id) == 'dynamodb'
|
|
206
|
+
if kw is not None or tracked or 'dynamo' in recv_text or 'ddb' in recv_text:
|
|
207
|
+
self.add(method, 'dynamodb', kw if kw else 'unknown')
|
|
208
|
+
return True
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
def detect_sql(self, node, method, recv_text):
|
|
212
|
+
if method not in SQL_METHODS:
|
|
213
|
+
return False
|
|
214
|
+
if not any(h in recv_text for h in SQL_RECEIVER_HINTS):
|
|
215
|
+
return False
|
|
216
|
+
sql = None
|
|
217
|
+
if node.args:
|
|
218
|
+
arg = node.args[0]
|
|
219
|
+
if (
|
|
220
|
+
isinstance(arg, ast.Call)
|
|
221
|
+
and isinstance(arg.func, ast.Name)
|
|
222
|
+
and arg.func.id == 'text'
|
|
223
|
+
and arg.args
|
|
224
|
+
):
|
|
225
|
+
arg = arg.args[0]
|
|
226
|
+
sql = self.resolve_string(arg)
|
|
227
|
+
service = 'mysql' if any(h in recv_text for h in MYSQL_HINTS) else 'postgres'
|
|
228
|
+
self.add('query', service, sql_table(sql) if sql else 'unknown')
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
def detect_mongo(self, node, method, recv, recv_text):
|
|
232
|
+
if method not in MONGO_METHODS:
|
|
233
|
+
return False
|
|
234
|
+
collection = None
|
|
235
|
+
if isinstance(recv, ast.Name) and recv.id in self.collections:
|
|
236
|
+
collection = self.collections[recv.id]
|
|
237
|
+
elif isinstance(recv, ast.Attribute):
|
|
238
|
+
base = expr_text(recv.value).lower()
|
|
239
|
+
if any(h in base for h in MONGO_BASE_HINTS):
|
|
240
|
+
collection = recv.attr
|
|
241
|
+
else:
|
|
242
|
+
collection = self.collection_name(recv)
|
|
243
|
+
if collection is None and ('collection' in recv_text or 'mongo' in recv_text):
|
|
244
|
+
collection = 'unknown'
|
|
245
|
+
if collection is None:
|
|
246
|
+
return False
|
|
247
|
+
op_type = 'scan' if method in MONGO_SCAN_METHODS else 'query'
|
|
248
|
+
self.add(op_type, 'mongodb', collection)
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
def detect_kafka(self, node, method, recv_text):
|
|
252
|
+
if not any(h in recv_text for h in KAFKA_HINTS):
|
|
253
|
+
return False
|
|
254
|
+
if method in KAFKA_PRODUCER_METHODS:
|
|
255
|
+
target = self.resolve_string(node.args[0]) if node.args else None
|
|
256
|
+
self.add(method, 'kafka', target if target else 'unknown')
|
|
257
|
+
return True
|
|
258
|
+
if method == 'subscribe':
|
|
259
|
+
topics = []
|
|
260
|
+
if node.args and isinstance(node.args[0], (ast.List, ast.Tuple)):
|
|
261
|
+
for element in node.args[0].elts:
|
|
262
|
+
resolved = self.resolve_string(element)
|
|
263
|
+
if resolved is not None:
|
|
264
|
+
topics.append(resolved)
|
|
265
|
+
elif node.args:
|
|
266
|
+
resolved = self.resolve_string(node.args[0])
|
|
267
|
+
if resolved is not None:
|
|
268
|
+
topics.append(resolved)
|
|
269
|
+
for topic in topics or ['unknown']:
|
|
270
|
+
self.add('subscribe', 'kafka', topic)
|
|
271
|
+
return True
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
def detect_aws_client(self, node, method, recv, recv_text):
|
|
275
|
+
for service, methods, keys, hints in SERVICE_RULES:
|
|
276
|
+
if method not in methods:
|
|
277
|
+
continue
|
|
278
|
+
tracked = isinstance(recv, ast.Name) and self.boto_clients.get(recv.id) == service
|
|
279
|
+
target = self.kwarg(node, keys)
|
|
280
|
+
if tracked or target is not None or any(h in recv_text for h in hints):
|
|
281
|
+
self.add(method, service, short_name(target) if target else 'unknown')
|
|
282
|
+
return True
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def find_py_files(root):
|
|
287
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
288
|
+
dirnames[:] = [d for d in dirnames if d not in EXCLUDED_DIRS and not d.startswith('.')]
|
|
289
|
+
for f in filenames:
|
|
290
|
+
if f.endswith('.py'):
|
|
291
|
+
yield os.path.join(dirpath, f)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def main():
|
|
295
|
+
root = os.path.abspath(sys.argv[1])
|
|
296
|
+
ops = []
|
|
297
|
+
for file_path in find_py_files(root):
|
|
298
|
+
try:
|
|
299
|
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as fh:
|
|
300
|
+
tree = ast.parse(fh.read())
|
|
301
|
+
except SyntaxError:
|
|
302
|
+
continue
|
|
303
|
+
visitor = Visitor(file_path)
|
|
304
|
+
visitor.visit(tree)
|
|
305
|
+
ops.extend(visitor.ops)
|
|
306
|
+
print(json.dumps(ops))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == '__main__':
|
|
310
|
+
main()
|
package/dist/server/index.js
CHANGED
|
@@ -501,8 +501,7 @@ export function createMcpServer() {
|
|
|
501
501
|
const indexNamesFor = (nodeId) => currentGraph.edges
|
|
502
502
|
.filter((e) => e.from === nodeId && e.type === 'uses_index')
|
|
503
503
|
.map((e) => currentGraph.nodes.find((n) => n.id === e.to))
|
|
504
|
-
.
|
|
505
|
-
.map((n) => n.name);
|
|
504
|
+
.flatMap((n) => (n?.type === 'index' ? [n.name] : []));
|
|
506
505
|
const results = tables.map((requested) => {
|
|
507
506
|
const lower = requested.toLowerCase();
|
|
508
507
|
const matches = tableNodes.filter((t) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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": [
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"packageManager": "pnpm@10.14.0",
|
|
58
58
|
"scripts": {
|
|
59
|
-
"build": "tsc --noEmit false --outDir dist",
|
|
59
|
+
"build": "tsc --noEmit false --outDir dist && cp src/context/scanner.py dist/context/scanner.py",
|
|
60
60
|
"test": "vitest run --passWithNoTests",
|
|
61
61
|
"coverage": "vitest run --coverage",
|
|
62
62
|
"lint": "eslint src",
|