infrawise 0.1.2 → 0.2.1
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/LICENSE +21 -0
- package/README.md +146 -74
- package/dist/adapters/aws.js +255 -0
- package/dist/adapters/dynamodb.js +97 -0
- package/dist/adapters/logs.js +119 -0
- package/dist/adapters/mongodb.js +109 -0
- package/dist/adapters/mysql.js +135 -0
- package/dist/adapters/postgres.js +131 -0
- package/dist/adapters/terraform.js +510 -0
- package/dist/analyzers/aws-services.js +168 -0
- package/dist/analyzers/dynamodb.js +144 -0
- package/dist/analyzers/index.js +59 -0
- package/dist/analyzers/mongodb.js +82 -0
- package/dist/analyzers/mysql.js +82 -0
- package/dist/analyzers/postgres.js +148 -0
- package/dist/analyzers/rds.js +109 -0
- package/dist/analyzers/terraform.js +95 -0
- package/dist/cli/commands/analyze.js +319 -0
- package/dist/cli/commands/auth.js +57 -0
- package/dist/cli/commands/dev.js +127 -0
- package/dist/cli/commands/doctor.js +338 -0
- package/dist/cli/commands/init.js +234 -0
- package/dist/cli/index.js +82 -0
- package/dist/cli/utils.js +165 -0
- package/dist/context/index.js +597 -0
- package/dist/core/cache.js +89 -0
- package/dist/core/config.js +163 -0
- package/dist/core/errors.js +97 -0
- package/dist/core/index.js +22 -0
- package/dist/core/logger.js +28 -0
- package/dist/graph/index.js +268 -0
- package/dist/server/index.js +348 -0
- package/dist/types.js +2 -0
- package/package.json +20 -30
- package/dist/index.js +0 -4800
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HotPartitionAnalyzer = exports.MissingGSIAnalyzer = exports.FullTableScanAnalyzer = void 0;
|
|
4
|
+
const graph_1 = require("../graph");
|
|
5
|
+
/**
|
|
6
|
+
* Detects full table scans on DynamoDB tables.
|
|
7
|
+
* A full scan without any filters is always a high-severity issue.
|
|
8
|
+
*/
|
|
9
|
+
class FullTableScanAnalyzer {
|
|
10
|
+
name = 'FullTableScanAnalyzer';
|
|
11
|
+
async analyze(graph) {
|
|
12
|
+
const findings = [];
|
|
13
|
+
const scanEdges = (0, graph_1.getScanEdges)(graph);
|
|
14
|
+
// Find which tables are being scanned
|
|
15
|
+
const scannedTableIds = new Set();
|
|
16
|
+
for (const edge of scanEdges) {
|
|
17
|
+
// edge.to should be a table node
|
|
18
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.to);
|
|
19
|
+
if (targetNode?.type === 'table' && targetNode.databaseType === 'dynamodb') {
|
|
20
|
+
scannedTableIds.add(targetNode.id);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const tableId of scannedTableIds) {
|
|
24
|
+
const tableNode = graph.nodes.find((n) => n.id === tableId);
|
|
25
|
+
if (!tableNode)
|
|
26
|
+
continue;
|
|
27
|
+
const tableScanEdges = scanEdges.filter((e) => e.to === tableId);
|
|
28
|
+
const callerFunctions = tableScanEdges
|
|
29
|
+
.map((e) => {
|
|
30
|
+
const node = graph.nodes.find((n) => n.id === e.from);
|
|
31
|
+
return node?.type === 'function' ? node.name : e.from;
|
|
32
|
+
})
|
|
33
|
+
.join(', ');
|
|
34
|
+
findings.push({
|
|
35
|
+
severity: 'high',
|
|
36
|
+
issue: `Full table scan detected on DynamoDB table "${tableNode.name}"`,
|
|
37
|
+
description: `The table "${tableNode.name}" is being scanned without any filter, which reads every item. This is expensive and slow for large tables. Called from: ${callerFunctions || 'unknown'}`,
|
|
38
|
+
recommendation: 'Replace Scan with a Query operation using a partition key or GSI. If filtering is required on non-key attributes, add a Global Secondary Index (GSI).',
|
|
39
|
+
metadata: {
|
|
40
|
+
tableName: tableNode.name,
|
|
41
|
+
scanCount: tableScanEdges.length,
|
|
42
|
+
callerFunctions,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return findings;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.FullTableScanAnalyzer = FullTableScanAnalyzer;
|
|
50
|
+
/**
|
|
51
|
+
* Detects when a DynamoDB table is accessed without a GSI covering
|
|
52
|
+
* the access pattern, which forces an expensive scan or inefficient query.
|
|
53
|
+
*/
|
|
54
|
+
class MissingGSIAnalyzer {
|
|
55
|
+
name = 'MissingGSIAnalyzer';
|
|
56
|
+
async analyze(graph) {
|
|
57
|
+
const findings = [];
|
|
58
|
+
// Find all DynamoDB tables
|
|
59
|
+
const dynamoTables = graph.nodes.filter((n) => n.type === 'table' && n.databaseType === 'dynamodb');
|
|
60
|
+
for (const table of dynamoTables) {
|
|
61
|
+
// Find edges coming INTO this table from function nodes
|
|
62
|
+
const incomingEdges = graph.edges.filter((e) => e.to === table.id);
|
|
63
|
+
const queryEdges = incomingEdges.filter((e) => e.type === 'query');
|
|
64
|
+
// Check if this table has any GSIs
|
|
65
|
+
const hasGSI = graph.edges.some((e) => e.from === table.id && e.type === 'uses_index');
|
|
66
|
+
// If there are query operations but no GSI, flag as medium severity
|
|
67
|
+
if (queryEdges.length > 0 && !hasGSI) {
|
|
68
|
+
const callerFunctions = queryEdges
|
|
69
|
+
.map((e) => {
|
|
70
|
+
const node = graph.nodes.find((n) => n.id === e.from);
|
|
71
|
+
return node?.type === 'function' ? node.name : e.from;
|
|
72
|
+
})
|
|
73
|
+
.join(', ');
|
|
74
|
+
findings.push({
|
|
75
|
+
severity: 'medium',
|
|
76
|
+
issue: `DynamoDB table "${table.name}" has no GSIs but is queried by multiple functions`,
|
|
77
|
+
description: `Table "${table.name}" is accessed by ${queryEdges.length} function(s) (${callerFunctions}) but has no Global Secondary Indexes defined. If queries filter on non-partition-key attributes, this will degrade to full scans.`,
|
|
78
|
+
recommendation: 'Analyze query access patterns and add GSIs for frequently filtered attributes. Consider using single-table design patterns with composite sort keys.',
|
|
79
|
+
metadata: {
|
|
80
|
+
tableName: table.name,
|
|
81
|
+
queryCount: queryEdges.length,
|
|
82
|
+
callerFunctions,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return findings;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.MissingGSIAnalyzer = MissingGSIAnalyzer;
|
|
91
|
+
/**
|
|
92
|
+
* Detects hot partition patterns — when the same table/partition
|
|
93
|
+
* is accessed with very high frequency from many code paths.
|
|
94
|
+
*/
|
|
95
|
+
class HotPartitionAnalyzer {
|
|
96
|
+
name = 'HotPartitionAnalyzer';
|
|
97
|
+
hotThreshold;
|
|
98
|
+
constructor(hotThreshold = 5) {
|
|
99
|
+
this.hotThreshold = hotThreshold;
|
|
100
|
+
}
|
|
101
|
+
async analyze(graph) {
|
|
102
|
+
const findings = [];
|
|
103
|
+
const edgeFrequency = (0, graph_1.getEdgeFrequency)(graph);
|
|
104
|
+
// Count how many distinct functions access each DynamoDB table
|
|
105
|
+
const tableAccessCount = new Map();
|
|
106
|
+
for (const edge of graph.edges) {
|
|
107
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.to);
|
|
108
|
+
if (targetNode?.type !== 'table' || targetNode.databaseType !== 'dynamodb')
|
|
109
|
+
continue;
|
|
110
|
+
let accessors = tableAccessCount.get(edge.to);
|
|
111
|
+
if (!accessors) {
|
|
112
|
+
accessors = new Set();
|
|
113
|
+
tableAccessCount.set(edge.to, accessors);
|
|
114
|
+
}
|
|
115
|
+
accessors.add(edge.from);
|
|
116
|
+
}
|
|
117
|
+
for (const [tableId, accessors] of tableAccessCount) {
|
|
118
|
+
if (accessors.size >= this.hotThreshold) {
|
|
119
|
+
const tableNode = graph.nodes.find((n) => n.id === tableId);
|
|
120
|
+
if (!tableNode)
|
|
121
|
+
continue;
|
|
122
|
+
// Also check edge frequency for repeated access patterns
|
|
123
|
+
let maxFreq = 0;
|
|
124
|
+
for (const [key, freq] of edgeFrequency) {
|
|
125
|
+
if (key.includes(tableId))
|
|
126
|
+
maxFreq = Math.max(maxFreq, freq);
|
|
127
|
+
}
|
|
128
|
+
findings.push({
|
|
129
|
+
severity: 'medium',
|
|
130
|
+
issue: `Potential hot partition detected on DynamoDB table "${tableNode.name}"`,
|
|
131
|
+
description: `Table "${tableNode.name}" is accessed by ${accessors.size} distinct code paths, which may create hot partition issues at scale. High access concentration on the same partition key can throttle requests.`,
|
|
132
|
+
recommendation: 'Consider adding a random suffix or timestamp to partition keys (write sharding). Use DynamoDB DAX for read-heavy workloads. Distribute access patterns across multiple partition key values.',
|
|
133
|
+
metadata: {
|
|
134
|
+
tableName: tableNode.name,
|
|
135
|
+
accessorCount: accessors.size,
|
|
136
|
+
maxEdgeFrequency: maxFreq,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return findings;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.HotPartitionAnalyzer = HotPartitionAnalyzer;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RDSNoMultiAZAnalyzer = exports.RDSNoDeletionProtectionAnalyzer = exports.RDSUnencryptedAnalyzer = exports.RDSNoBackupAnalyzer = exports.RDSPubliclyAccessibleAnalyzer = exports.LambdaHighTimeoutAnalyzer = exports.LambdaDefaultMemoryAnalyzer = exports.MissingLogRetentionAnalyzer = exports.MissingSecretRotationAnalyzer = exports.LargeQueueBacklogAnalyzer = exports.UnencryptedQueueAnalyzer = exports.MissingDLQAnalyzer = exports.IaCDriftAnalyzer = exports.MongoCollectionScanAnalyzer = exports.MissingMongoIndexAnalyzer = exports.MySQLFullTableScanAnalyzer = exports.MissingMySQLIndexAnalyzer = exports.LargeSelectAnalyzer = exports.NplusOneAnalyzer = exports.MissingIndexAnalyzer = exports.HotPartitionAnalyzer = exports.MissingGSIAnalyzer = exports.FullTableScanAnalyzer = void 0;
|
|
4
|
+
exports.runAllAnalyzers = runAllAnalyzers;
|
|
5
|
+
exports.summarizeFindings = summarizeFindings;
|
|
6
|
+
const core_1 = require("../core");
|
|
7
|
+
var dynamodb_1 = require("./dynamodb");
|
|
8
|
+
Object.defineProperty(exports, "FullTableScanAnalyzer", { enumerable: true, get: function () { return dynamodb_1.FullTableScanAnalyzer; } });
|
|
9
|
+
Object.defineProperty(exports, "MissingGSIAnalyzer", { enumerable: true, get: function () { return dynamodb_1.MissingGSIAnalyzer; } });
|
|
10
|
+
Object.defineProperty(exports, "HotPartitionAnalyzer", { enumerable: true, get: function () { return dynamodb_1.HotPartitionAnalyzer; } });
|
|
11
|
+
var postgres_1 = require("./postgres");
|
|
12
|
+
Object.defineProperty(exports, "MissingIndexAnalyzer", { enumerable: true, get: function () { return postgres_1.MissingIndexAnalyzer; } });
|
|
13
|
+
Object.defineProperty(exports, "NplusOneAnalyzer", { enumerable: true, get: function () { return postgres_1.NplusOneAnalyzer; } });
|
|
14
|
+
Object.defineProperty(exports, "LargeSelectAnalyzer", { enumerable: true, get: function () { return postgres_1.LargeSelectAnalyzer; } });
|
|
15
|
+
var mysql_1 = require("./mysql");
|
|
16
|
+
Object.defineProperty(exports, "MissingMySQLIndexAnalyzer", { enumerable: true, get: function () { return mysql_1.MissingMySQLIndexAnalyzer; } });
|
|
17
|
+
Object.defineProperty(exports, "MySQLFullTableScanAnalyzer", { enumerable: true, get: function () { return mysql_1.MySQLFullTableScanAnalyzer; } });
|
|
18
|
+
var mongodb_1 = require("./mongodb");
|
|
19
|
+
Object.defineProperty(exports, "MissingMongoIndexAnalyzer", { enumerable: true, get: function () { return mongodb_1.MissingMongoIndexAnalyzer; } });
|
|
20
|
+
Object.defineProperty(exports, "MongoCollectionScanAnalyzer", { enumerable: true, get: function () { return mongodb_1.MongoCollectionScanAnalyzer; } });
|
|
21
|
+
var terraform_1 = require("./terraform");
|
|
22
|
+
Object.defineProperty(exports, "IaCDriftAnalyzer", { enumerable: true, get: function () { return terraform_1.IaCDriftAnalyzer; } });
|
|
23
|
+
var aws_services_1 = require("./aws-services");
|
|
24
|
+
Object.defineProperty(exports, "MissingDLQAnalyzer", { enumerable: true, get: function () { return aws_services_1.MissingDLQAnalyzer; } });
|
|
25
|
+
Object.defineProperty(exports, "UnencryptedQueueAnalyzer", { enumerable: true, get: function () { return aws_services_1.UnencryptedQueueAnalyzer; } });
|
|
26
|
+
Object.defineProperty(exports, "LargeQueueBacklogAnalyzer", { enumerable: true, get: function () { return aws_services_1.LargeQueueBacklogAnalyzer; } });
|
|
27
|
+
Object.defineProperty(exports, "MissingSecretRotationAnalyzer", { enumerable: true, get: function () { return aws_services_1.MissingSecretRotationAnalyzer; } });
|
|
28
|
+
Object.defineProperty(exports, "MissingLogRetentionAnalyzer", { enumerable: true, get: function () { return aws_services_1.MissingLogRetentionAnalyzer; } });
|
|
29
|
+
Object.defineProperty(exports, "LambdaDefaultMemoryAnalyzer", { enumerable: true, get: function () { return aws_services_1.LambdaDefaultMemoryAnalyzer; } });
|
|
30
|
+
Object.defineProperty(exports, "LambdaHighTimeoutAnalyzer", { enumerable: true, get: function () { return aws_services_1.LambdaHighTimeoutAnalyzer; } });
|
|
31
|
+
var rds_1 = require("./rds");
|
|
32
|
+
Object.defineProperty(exports, "RDSPubliclyAccessibleAnalyzer", { enumerable: true, get: function () { return rds_1.RDSPubliclyAccessibleAnalyzer; } });
|
|
33
|
+
Object.defineProperty(exports, "RDSNoBackupAnalyzer", { enumerable: true, get: function () { return rds_1.RDSNoBackupAnalyzer; } });
|
|
34
|
+
Object.defineProperty(exports, "RDSUnencryptedAnalyzer", { enumerable: true, get: function () { return rds_1.RDSUnencryptedAnalyzer; } });
|
|
35
|
+
Object.defineProperty(exports, "RDSNoDeletionProtectionAnalyzer", { enumerable: true, get: function () { return rds_1.RDSNoDeletionProtectionAnalyzer; } });
|
|
36
|
+
Object.defineProperty(exports, "RDSNoMultiAZAnalyzer", { enumerable: true, get: function () { return rds_1.RDSNoMultiAZAnalyzer; } });
|
|
37
|
+
async function runAllAnalyzers(graph, analyzers) {
|
|
38
|
+
const allFindings = [];
|
|
39
|
+
for (const analyzer of analyzers) {
|
|
40
|
+
try {
|
|
41
|
+
core_1.logger.debug(`Running analyzer: ${analyzer.name}`);
|
|
42
|
+
const findings = await analyzer.analyze(graph);
|
|
43
|
+
core_1.logger.debug(`[${analyzer.name}] found ${findings.length} issue(s)`);
|
|
44
|
+
allFindings.push(...findings);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
core_1.logger.warn(`Analyzer "${analyzer.name}" failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
51
|
+
allFindings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
52
|
+
return allFindings;
|
|
53
|
+
}
|
|
54
|
+
function summarizeFindings(findings) {
|
|
55
|
+
const counts = { total: findings.length, high: 0, medium: 0, low: 0 };
|
|
56
|
+
for (const f of findings)
|
|
57
|
+
counts[f.severity]++;
|
|
58
|
+
return counts;
|
|
59
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MongoCollectionScanAnalyzer = exports.MissingMongoIndexAnalyzer = void 0;
|
|
4
|
+
const graph_1 = require("../graph");
|
|
5
|
+
/**
|
|
6
|
+
* Detects MongoDB collections that are queried without any secondary indexes.
|
|
7
|
+
*/
|
|
8
|
+
class MissingMongoIndexAnalyzer {
|
|
9
|
+
name = 'MissingMongoIndexAnalyzer';
|
|
10
|
+
async analyze(graph) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
const mongoCollections = graph.nodes.filter((n) => n.type === 'table' && n.databaseType === 'mongodb');
|
|
13
|
+
for (const coll of mongoCollections) {
|
|
14
|
+
const indexEdges = graph.edges.filter((e) => e.from === coll.id && e.type === 'uses_index');
|
|
15
|
+
const hasIndexes = indexEdges.length > 0;
|
|
16
|
+
const queryEdges = graph.edges.filter((e) => e.to === coll.id && (e.type === 'query' || e.type === 'scan'));
|
|
17
|
+
if (queryEdges.length > 0 && !hasIndexes) {
|
|
18
|
+
const callerFunctions = queryEdges
|
|
19
|
+
.map((e) => {
|
|
20
|
+
const node = graph.nodes.find((n) => n.id === e.from);
|
|
21
|
+
return node?.type === 'function' ? node.name : e.from;
|
|
22
|
+
})
|
|
23
|
+
.join(', ');
|
|
24
|
+
findings.push({
|
|
25
|
+
severity: 'medium',
|
|
26
|
+
issue: `MongoDB collection "${coll.name}" has no indexes but is queried by ${queryEdges.length} function(s)`,
|
|
27
|
+
description: `Collection "${coll.name}" is accessed by functions (${callerFunctions}) but has no secondary indexes defined. Queries on non-_id fields will result in full collection scans.`,
|
|
28
|
+
recommendation: 'Add indexes using db.collection.createIndex({ field: 1 }) for frequently queried fields. Use explain("executionStats") to verify query plan.',
|
|
29
|
+
metadata: {
|
|
30
|
+
collectionName: coll.name,
|
|
31
|
+
queryCount: queryEdges.length,
|
|
32
|
+
callerFunctions,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return findings;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.MissingMongoIndexAnalyzer = MissingMongoIndexAnalyzer;
|
|
41
|
+
/**
|
|
42
|
+
* Detects collection scan operations on MongoDB collections.
|
|
43
|
+
*/
|
|
44
|
+
class MongoCollectionScanAnalyzer {
|
|
45
|
+
name = 'MongoCollectionScanAnalyzer';
|
|
46
|
+
async analyze(graph) {
|
|
47
|
+
const findings = [];
|
|
48
|
+
const scanEdges = (0, graph_1.getScanEdges)(graph);
|
|
49
|
+
const scannedCollIds = new Set();
|
|
50
|
+
for (const edge of scanEdges) {
|
|
51
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.to);
|
|
52
|
+
if (targetNode?.type === 'table' && targetNode.databaseType === 'mongodb') {
|
|
53
|
+
scannedCollIds.add(targetNode.id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const collId of scannedCollIds) {
|
|
57
|
+
const collNode = graph.nodes.find((n) => n.id === collId);
|
|
58
|
+
if (!collNode)
|
|
59
|
+
continue;
|
|
60
|
+
const collScanEdges = scanEdges.filter((e) => e.to === collId);
|
|
61
|
+
const callerFunctions = collScanEdges
|
|
62
|
+
.map((e) => {
|
|
63
|
+
const node = graph.nodes.find((n) => n.id === e.from);
|
|
64
|
+
return node?.type === 'function' ? node.name : e.from;
|
|
65
|
+
})
|
|
66
|
+
.join(', ');
|
|
67
|
+
findings.push({
|
|
68
|
+
severity: 'high',
|
|
69
|
+
issue: `Collection scan detected on MongoDB collection "${collNode.name}"`,
|
|
70
|
+
description: `Collection "${collNode.name}" is being scanned without an index, reading every document. This is very expensive for large collections. Called from: ${callerFunctions || 'unknown'}`,
|
|
71
|
+
recommendation: 'Add an index on the field(s) used as query predicates. Use db.collection.createIndex({ field: 1 }) and verify with explain("executionStats").',
|
|
72
|
+
metadata: {
|
|
73
|
+
collectionName: collNode.name,
|
|
74
|
+
scanCount: collScanEdges.length,
|
|
75
|
+
callerFunctions,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.MongoCollectionScanAnalyzer = MongoCollectionScanAnalyzer;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MySQLFullTableScanAnalyzer = exports.MissingMySQLIndexAnalyzer = void 0;
|
|
4
|
+
const graph_1 = require("../graph");
|
|
5
|
+
/**
|
|
6
|
+
* Detects MySQL tables that are queried without any indexes defined.
|
|
7
|
+
*/
|
|
8
|
+
class MissingMySQLIndexAnalyzer {
|
|
9
|
+
name = 'MissingMySQLIndexAnalyzer';
|
|
10
|
+
async analyze(graph) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
const mysqlTables = graph.nodes.filter((n) => n.type === 'table' && n.databaseType === 'mysql');
|
|
13
|
+
for (const table of mysqlTables) {
|
|
14
|
+
const indexEdges = graph.edges.filter((e) => e.from === table.id && e.type === 'uses_index');
|
|
15
|
+
const hasIndexes = indexEdges.length > 0;
|
|
16
|
+
const queryEdges = graph.edges.filter((e) => e.to === table.id && (e.type === 'query' || e.type === 'scan'));
|
|
17
|
+
if (queryEdges.length > 0 && !hasIndexes) {
|
|
18
|
+
const callerFunctions = queryEdges
|
|
19
|
+
.map((e) => {
|
|
20
|
+
const node = graph.nodes.find((n) => n.id === e.from);
|
|
21
|
+
return node?.type === 'function' ? node.name : e.from;
|
|
22
|
+
})
|
|
23
|
+
.join(', ');
|
|
24
|
+
findings.push({
|
|
25
|
+
severity: 'medium',
|
|
26
|
+
issue: `MySQL table "${table.name}" has no indexes but is queried by ${queryEdges.length} function(s)`,
|
|
27
|
+
description: `Table "${table.name}" is accessed by functions (${callerFunctions}) but has no secondary indexes defined. Queries on non-primary-key columns will result in full table scans.`,
|
|
28
|
+
recommendation: 'Add indexes on columns used in WHERE clauses. Run EXPLAIN on slow queries. Consider composite indexes for multi-column filters.',
|
|
29
|
+
metadata: {
|
|
30
|
+
tableName: table.name,
|
|
31
|
+
queryCount: queryEdges.length,
|
|
32
|
+
callerFunctions,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return findings;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.MissingMySQLIndexAnalyzer = MissingMySQLIndexAnalyzer;
|
|
41
|
+
/**
|
|
42
|
+
* Detects full table scans on MySQL tables.
|
|
43
|
+
*/
|
|
44
|
+
class MySQLFullTableScanAnalyzer {
|
|
45
|
+
name = 'MySQLFullTableScanAnalyzer';
|
|
46
|
+
async analyze(graph) {
|
|
47
|
+
const findings = [];
|
|
48
|
+
const scanEdges = (0, graph_1.getScanEdges)(graph);
|
|
49
|
+
const scannedTableIds = new Set();
|
|
50
|
+
for (const edge of scanEdges) {
|
|
51
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.to);
|
|
52
|
+
if (targetNode?.type === 'table' && targetNode.databaseType === 'mysql') {
|
|
53
|
+
scannedTableIds.add(targetNode.id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const tableId of scannedTableIds) {
|
|
57
|
+
const tableNode = graph.nodes.find((n) => n.id === tableId);
|
|
58
|
+
if (!tableNode)
|
|
59
|
+
continue;
|
|
60
|
+
const tableScanEdges = scanEdges.filter((e) => e.to === tableId);
|
|
61
|
+
const callerFunctions = tableScanEdges
|
|
62
|
+
.map((e) => {
|
|
63
|
+
const node = graph.nodes.find((n) => n.id === e.from);
|
|
64
|
+
return node?.type === 'function' ? node.name : e.from;
|
|
65
|
+
})
|
|
66
|
+
.join(', ');
|
|
67
|
+
findings.push({
|
|
68
|
+
severity: 'high',
|
|
69
|
+
issue: `Full table scan detected on MySQL table "${tableNode.name}"`,
|
|
70
|
+
description: `The table "${tableNode.name}" is being scanned without a usable index, reading every row. This is expensive for large tables. Called from: ${callerFunctions || 'unknown'}`,
|
|
71
|
+
recommendation: 'Add an index on the column(s) used in the WHERE clause. Use EXPLAIN to verify the query plan uses the index after adding it.',
|
|
72
|
+
metadata: {
|
|
73
|
+
tableName: tableNode.name,
|
|
74
|
+
scanCount: tableScanEdges.length,
|
|
75
|
+
callerFunctions,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.MySQLFullTableScanAnalyzer = MySQLFullTableScanAnalyzer;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LargeSelectAnalyzer = exports.NplusOneAnalyzer = exports.MissingIndexAnalyzer = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Detects PostgreSQL table columns that are used in WHERE patterns
|
|
6
|
+
* but don't have a corresponding index.
|
|
7
|
+
*/
|
|
8
|
+
class MissingIndexAnalyzer {
|
|
9
|
+
name = 'MissingIndexAnalyzer';
|
|
10
|
+
async analyze(graph) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
const postgresTables = graph.nodes.filter((n) => n.type === 'table' && n.databaseType === 'postgres');
|
|
13
|
+
for (const table of postgresTables) {
|
|
14
|
+
// Find indexes for this table via uses_index edges
|
|
15
|
+
const indexEdges = graph.edges.filter((e) => e.from === table.id && e.type === 'uses_index');
|
|
16
|
+
const hasIndexes = indexEdges.length > 0;
|
|
17
|
+
// Find all query operations targeting this table
|
|
18
|
+
const queryEdges = graph.edges.filter((e) => e.to === table.id && (e.type === 'query' || e.type === 'scan'));
|
|
19
|
+
if (queryEdges.length > 0 && !hasIndexes) {
|
|
20
|
+
const callerFunctions = queryEdges
|
|
21
|
+
.map((e) => {
|
|
22
|
+
const node = graph.nodes.find((n) => n.id === e.from);
|
|
23
|
+
return node?.type === 'function' ? node.name : e.from;
|
|
24
|
+
})
|
|
25
|
+
.join(', ');
|
|
26
|
+
findings.push({
|
|
27
|
+
severity: 'medium',
|
|
28
|
+
issue: `PostgreSQL table "${table.name}" has no indexes but is queried by ${queryEdges.length} function(s)`,
|
|
29
|
+
description: `Table "${table.name}" is accessed by functions (${callerFunctions}) but has no secondary indexes defined. Queries on non-primary-key columns will result in sequential scans.`,
|
|
30
|
+
recommendation: 'Add indexes on columns used in WHERE clauses. Use EXPLAIN ANALYZE to identify slow queries. Consider composite indexes for multi-column filters.',
|
|
31
|
+
metadata: {
|
|
32
|
+
tableName: table.name,
|
|
33
|
+
queryCount: queryEdges.length,
|
|
34
|
+
callerFunctions,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return findings;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.MissingIndexAnalyzer = MissingIndexAnalyzer;
|
|
43
|
+
/**
|
|
44
|
+
* Detects N+1 query patterns — same function making multiple queries
|
|
45
|
+
* to the same table inside what is likely a loop.
|
|
46
|
+
*/
|
|
47
|
+
class NplusOneAnalyzer {
|
|
48
|
+
name = 'NplusOneAnalyzer';
|
|
49
|
+
async analyze(graph) {
|
|
50
|
+
const findings = [];
|
|
51
|
+
// Group edges by function node -> table node
|
|
52
|
+
const functionTableAccess = new Map();
|
|
53
|
+
for (const edge of graph.edges) {
|
|
54
|
+
if (edge.type !== 'query')
|
|
55
|
+
continue;
|
|
56
|
+
const fromNode = graph.nodes.find((n) => n.id === edge.from);
|
|
57
|
+
const toNode = graph.nodes.find((n) => n.id === edge.to);
|
|
58
|
+
if (fromNode?.type !== 'function' || toNode?.type !== 'table')
|
|
59
|
+
continue;
|
|
60
|
+
if (toNode.databaseType !== 'postgres')
|
|
61
|
+
continue;
|
|
62
|
+
let tableAccess = functionTableAccess.get(edge.from);
|
|
63
|
+
if (!tableAccess) {
|
|
64
|
+
tableAccess = new Map();
|
|
65
|
+
functionTableAccess.set(edge.from, tableAccess);
|
|
66
|
+
}
|
|
67
|
+
tableAccess.set(edge.to, (tableAccess.get(edge.to) ?? 0) + 1);
|
|
68
|
+
}
|
|
69
|
+
// Flag functions that access the same table more than once
|
|
70
|
+
for (const [funcId, tableAccess] of functionTableAccess) {
|
|
71
|
+
for (const [tableId, count] of tableAccess) {
|
|
72
|
+
if (count >= 2) {
|
|
73
|
+
const funcNode = graph.nodes.find((n) => n.id === funcId);
|
|
74
|
+
const tableNode = graph.nodes.find((n) => n.id === tableId);
|
|
75
|
+
if (!funcNode || !tableNode)
|
|
76
|
+
continue;
|
|
77
|
+
findings.push({
|
|
78
|
+
severity: 'high',
|
|
79
|
+
issue: `Potential N+1 query pattern in function "${funcNode.name}"`,
|
|
80
|
+
description: `Function "${funcNode.name}" in ${funcNode.file} appears to query table "${tableNode.name}" ${count} time(s), suggesting a potential N+1 query pattern. This can lead to exponential database load when called in a loop.`,
|
|
81
|
+
recommendation: 'Batch multiple queries into a single query using IN clauses, JOIN operations, or DataLoader patterns. Consider using a bulk-fetch approach and filtering in application code.',
|
|
82
|
+
metadata: {
|
|
83
|
+
functionName: funcNode.name,
|
|
84
|
+
filePath: funcNode.file,
|
|
85
|
+
tableName: tableNode.name,
|
|
86
|
+
queryCount: count,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return findings;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.NplusOneAnalyzer = NplusOneAnalyzer;
|
|
96
|
+
/**
|
|
97
|
+
* Detects SELECT * usage patterns which read all columns even when
|
|
98
|
+
* only a subset is needed, wasting I/O and memory.
|
|
99
|
+
*/
|
|
100
|
+
class LargeSelectAnalyzer {
|
|
101
|
+
name = 'LargeSelectAnalyzer';
|
|
102
|
+
async analyze(graph) {
|
|
103
|
+
const findings = [];
|
|
104
|
+
// Find query nodes that represent SELECT * patterns
|
|
105
|
+
const queryNodes = graph.nodes.filter((n) => n.type === 'query');
|
|
106
|
+
for (const queryNode of queryNodes) {
|
|
107
|
+
if (queryNode.operation.toLowerCase().includes('select *') ||
|
|
108
|
+
queryNode.operation.toLowerCase().includes('select_all')) {
|
|
109
|
+
findings.push({
|
|
110
|
+
severity: 'low',
|
|
111
|
+
issue: `SELECT * usage detected in query "${queryNode.operation}"`,
|
|
112
|
+
description: 'Using SELECT * reads all columns from the table, which can be expensive for wide tables. It also prevents index-only scans and increases network transfer.',
|
|
113
|
+
recommendation: 'Specify only the columns you need. For frequently accessed subsets of columns, consider creating a covering index that includes those columns.',
|
|
114
|
+
metadata: {
|
|
115
|
+
operation: queryNode.operation,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Also look for edges from functions to postgres tables with a high number of scan operations
|
|
121
|
+
const postgresTables = graph.nodes.filter((n) => n.type === 'table' && n.databaseType === 'postgres');
|
|
122
|
+
for (const table of postgresTables) {
|
|
123
|
+
const scanEdges = graph.edges.filter((e) => e.to === table.id && e.type === 'scan');
|
|
124
|
+
if (scanEdges.length > 0) {
|
|
125
|
+
const callerFunctions = scanEdges
|
|
126
|
+
.map((e) => {
|
|
127
|
+
const node = graph.nodes.find((n) => n.id === e.from);
|
|
128
|
+
return node?.type === 'function' ? node.name : e.from;
|
|
129
|
+
})
|
|
130
|
+
.filter((v, i, arr) => arr.indexOf(v) === i)
|
|
131
|
+
.join(', ');
|
|
132
|
+
findings.push({
|
|
133
|
+
severity: 'medium',
|
|
134
|
+
issue: `Sequential scan detected on PostgreSQL table "${table.name}"`,
|
|
135
|
+
description: `Table "${table.name}" is accessed via sequential scan by ${scanEdges.length} operation(s) from: ${callerFunctions}. Sequential scans read the entire table and are very slow on large tables.`,
|
|
136
|
+
recommendation: 'Add appropriate indexes to support the WHERE conditions in these queries. Run EXPLAIN ANALYZE to understand the query plan. Consider partial indexes for filtered queries.',
|
|
137
|
+
metadata: {
|
|
138
|
+
tableName: table.name,
|
|
139
|
+
scanCount: scanEdges.length,
|
|
140
|
+
callerFunctions,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return findings;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
exports.LargeSelectAnalyzer = LargeSelectAnalyzer;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RDSNoMultiAZAnalyzer = exports.RDSNoDeletionProtectionAnalyzer = exports.RDSUnencryptedAnalyzer = exports.RDSNoBackupAnalyzer = exports.RDSPubliclyAccessibleAnalyzer = void 0;
|
|
4
|
+
// ─── RDS ─────────────────────────────────────────────────────────────────────
|
|
5
|
+
class RDSPubliclyAccessibleAnalyzer {
|
|
6
|
+
name = 'RDSPubliclyAccessibleAnalyzer';
|
|
7
|
+
async analyze(graph) {
|
|
8
|
+
const findings = [];
|
|
9
|
+
for (const node of graph.nodes) {
|
|
10
|
+
if (node.type !== 'database_instance')
|
|
11
|
+
continue;
|
|
12
|
+
if (node.publiclyAccessible) {
|
|
13
|
+
findings.push({
|
|
14
|
+
severity: 'high',
|
|
15
|
+
issue: `RDS instance "${node.name}" is publicly accessible`,
|
|
16
|
+
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.`,
|
|
17
|
+
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.`,
|
|
18
|
+
metadata: { dbInstanceIdentifier: node.name, engine: node.engine, instanceClass: node.instanceClass },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return findings;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.RDSPubliclyAccessibleAnalyzer = RDSPubliclyAccessibleAnalyzer;
|
|
26
|
+
class RDSNoBackupAnalyzer {
|
|
27
|
+
name = 'RDSNoBackupAnalyzer';
|
|
28
|
+
async analyze(graph) {
|
|
29
|
+
const findings = [];
|
|
30
|
+
for (const node of graph.nodes) {
|
|
31
|
+
if (node.type !== 'database_instance')
|
|
32
|
+
continue;
|
|
33
|
+
if (node.backupRetentionDays === 0) {
|
|
34
|
+
findings.push({
|
|
35
|
+
severity: 'high',
|
|
36
|
+
issue: `RDS instance "${node.name}" has automated backups disabled`,
|
|
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.`,
|
|
38
|
+
recommendation: `Enable automated backups on "${node.name}" with at least 7 days retention (35 days for production workloads). Enable point-in-time recovery.`,
|
|
39
|
+
metadata: { dbInstanceIdentifier: node.name, engine: node.engine, backupRetentionDays: node.backupRetentionDays },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return findings;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.RDSNoBackupAnalyzer = RDSNoBackupAnalyzer;
|
|
47
|
+
class RDSUnencryptedAnalyzer {
|
|
48
|
+
name = 'RDSUnencryptedAnalyzer';
|
|
49
|
+
async analyze(graph) {
|
|
50
|
+
const findings = [];
|
|
51
|
+
for (const node of graph.nodes) {
|
|
52
|
+
if (node.type !== 'database_instance')
|
|
53
|
+
continue;
|
|
54
|
+
if (!node.storageEncrypted) {
|
|
55
|
+
findings.push({
|
|
56
|
+
severity: 'medium',
|
|
57
|
+
issue: `RDS instance "${node.name}" has unencrypted storage`,
|
|
58
|
+
description: `"${node.name}" does not have storage encryption enabled. Data at rest (including automated backups and read replicas) is stored unencrypted.`,
|
|
59
|
+
recommendation: `Enable storage encryption on "${node.name}" using an AWS KMS key. Note: encryption must be enabled at creation time — you'll need to create a new encrypted instance and migrate.`,
|
|
60
|
+
metadata: { dbInstanceIdentifier: node.name, engine: node.engine },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return findings;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.RDSUnencryptedAnalyzer = RDSUnencryptedAnalyzer;
|
|
68
|
+
class RDSNoDeletionProtectionAnalyzer {
|
|
69
|
+
name = 'RDSNoDeletionProtectionAnalyzer';
|
|
70
|
+
async analyze(graph) {
|
|
71
|
+
const findings = [];
|
|
72
|
+
for (const node of graph.nodes) {
|
|
73
|
+
if (node.type !== 'database_instance')
|
|
74
|
+
continue;
|
|
75
|
+
if (!node.deletionProtection) {
|
|
76
|
+
findings.push({
|
|
77
|
+
severity: 'medium',
|
|
78
|
+
issue: `RDS instance "${node.name}" has deletion protection disabled`,
|
|
79
|
+
description: `"${node.name}" can be deleted without any additional safeguard. A mistaken Terraform destroy, IaC misconfiguration, or human error could permanently drop the database.`,
|
|
80
|
+
recommendation: `Enable DeletionProtection on "${node.name}". This must be explicitly disabled before the instance can be deleted, preventing accidental data loss.`,
|
|
81
|
+
metadata: { dbInstanceIdentifier: node.name, engine: node.engine },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return findings;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.RDSNoDeletionProtectionAnalyzer = RDSNoDeletionProtectionAnalyzer;
|
|
89
|
+
class RDSNoMultiAZAnalyzer {
|
|
90
|
+
name = 'RDSNoMultiAZAnalyzer';
|
|
91
|
+
async analyze(graph) {
|
|
92
|
+
const findings = [];
|
|
93
|
+
for (const node of graph.nodes) {
|
|
94
|
+
if (node.type !== 'database_instance')
|
|
95
|
+
continue;
|
|
96
|
+
if (!node.multiAZ) {
|
|
97
|
+
findings.push({
|
|
98
|
+
severity: 'low',
|
|
99
|
+
issue: `RDS instance "${node.name}" is single-AZ`,
|
|
100
|
+
description: `"${node.name}" does not have Multi-AZ enabled. An AZ outage will cause downtime until the instance is recovered in the same AZ.`,
|
|
101
|
+
recommendation: `Enable Multi-AZ on "${node.name}" to get automatic failover to a standby in a separate Availability Zone. Typical failover is 60–120 seconds.`,
|
|
102
|
+
metadata: { dbInstanceIdentifier: node.name, engine: node.engine },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return findings;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.RDSNoMultiAZAnalyzer = RDSNoMultiAZAnalyzer;
|