aura-security 0.4.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/LICENSE +21 -0
- package/README.md +446 -0
- package/deploy/AWS-DEPLOYMENT.md +358 -0
- package/deploy/terraform/main.tf +362 -0
- package/deploy/terraform/terraform.tfvars.example +6 -0
- package/dist/agents/base.d.ts +44 -0
- package/dist/agents/base.js +96 -0
- package/dist/agents/index.d.ts +14 -0
- package/dist/agents/index.js +17 -0
- package/dist/agents/policy/evaluator.d.ts +15 -0
- package/dist/agents/policy/evaluator.js +183 -0
- package/dist/agents/policy/index.d.ts +12 -0
- package/dist/agents/policy/index.js +15 -0
- package/dist/agents/policy/validator.d.ts +15 -0
- package/dist/agents/policy/validator.js +182 -0
- package/dist/agents/scanners/gitleaks.d.ts +14 -0
- package/dist/agents/scanners/gitleaks.js +155 -0
- package/dist/agents/scanners/grype.d.ts +14 -0
- package/dist/agents/scanners/grype.js +109 -0
- package/dist/agents/scanners/index.d.ts +15 -0
- package/dist/agents/scanners/index.js +27 -0
- package/dist/agents/scanners/npm-audit.d.ts +13 -0
- package/dist/agents/scanners/npm-audit.js +129 -0
- package/dist/agents/scanners/semgrep.d.ts +14 -0
- package/dist/agents/scanners/semgrep.js +131 -0
- package/dist/agents/scanners/trivy.d.ts +14 -0
- package/dist/agents/scanners/trivy.js +122 -0
- package/dist/agents/types.d.ts +137 -0
- package/dist/agents/types.js +91 -0
- package/dist/auditor/index.d.ts +3 -0
- package/dist/auditor/index.js +2 -0
- package/dist/auditor/pipeline.d.ts +19 -0
- package/dist/auditor/pipeline.js +240 -0
- package/dist/auditor/validator.d.ts +17 -0
- package/dist/auditor/validator.js +58 -0
- package/dist/aura/client.d.ts +29 -0
- package/dist/aura/client.js +125 -0
- package/dist/aura/index.d.ts +4 -0
- package/dist/aura/index.js +2 -0
- package/dist/aura/server.d.ts +45 -0
- package/dist/aura/server.js +343 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +1433 -0
- package/dist/client/index.d.ts +41 -0
- package/dist/client/index.js +170 -0
- package/dist/compliance/index.d.ts +40 -0
- package/dist/compliance/index.js +292 -0
- package/dist/database/index.d.ts +77 -0
- package/dist/database/index.js +395 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +762 -0
- package/dist/integrations/aura-scanner.d.ts +69 -0
- package/dist/integrations/aura-scanner.js +155 -0
- package/dist/integrations/aws-scanner.d.ts +63 -0
- package/dist/integrations/aws-scanner.js +624 -0
- package/dist/integrations/config.d.ts +69 -0
- package/dist/integrations/config.js +212 -0
- package/dist/integrations/github.d.ts +45 -0
- package/dist/integrations/github.js +201 -0
- package/dist/integrations/gitlab.d.ts +36 -0
- package/dist/integrations/gitlab.js +110 -0
- package/dist/integrations/index.d.ts +11 -0
- package/dist/integrations/index.js +11 -0
- package/dist/integrations/local-scanner.d.ts +146 -0
- package/dist/integrations/local-scanner.js +1654 -0
- package/dist/integrations/notifications.d.ts +99 -0
- package/dist/integrations/notifications.js +305 -0
- package/dist/integrations/scanners.d.ts +57 -0
- package/dist/integrations/scanners.js +217 -0
- package/dist/integrations/slop-scanner.d.ts +69 -0
- package/dist/integrations/slop-scanner.js +155 -0
- package/dist/integrations/webhook.d.ts +37 -0
- package/dist/integrations/webhook.js +256 -0
- package/dist/orchestrator/index.d.ts +72 -0
- package/dist/orchestrator/index.js +187 -0
- package/dist/output/index.d.ts +152 -0
- package/dist/output/index.js +399 -0
- package/dist/pipeline/index.d.ts +72 -0
- package/dist/pipeline/index.js +313 -0
- package/dist/sbom/index.d.ts +94 -0
- package/dist/sbom/index.js +298 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/input.schema.d.ts +87 -0
- package/dist/schemas/input.schema.js +44 -0
- package/dist/schemas/output.schema.d.ts +115 -0
- package/dist/schemas/output.schema.js +64 -0
- package/dist/serve-visualizer.d.ts +2 -0
- package/dist/serve-visualizer.js +78 -0
- package/dist/slop/client.d.ts +29 -0
- package/dist/slop/client.js +125 -0
- package/dist/slop/index.d.ts +4 -0
- package/dist/slop/index.js +2 -0
- package/dist/slop/server.d.ts +45 -0
- package/dist/slop/server.js +343 -0
- package/dist/types/events.d.ts +62 -0
- package/dist/types/events.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/visualizer/index.d.ts +4 -0
- package/dist/visualizer/index.js +181 -0
- package/dist/websocket/index.d.ts +88 -0
- package/dist/websocket/index.js +195 -0
- package/dist/zones/index.d.ts +7 -0
- package/dist/zones/index.js +7 -0
- package/dist/zones/manager.d.ts +101 -0
- package/dist/zones/manager.js +304 -0
- package/dist/zones/types.d.ts +78 -0
- package/dist/zones/types.js +33 -0
- package/package.json +84 -0
- package/visualizer/app.js +0 -0
- package/visualizer/index-minimal.html +1771 -0
- package/visualizer/index.html +2933 -0
- package/visualizer/landing.html +1328 -0
- package/visualizer/styles.css +0 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Security Scanner
|
|
3
|
+
* Scans AWS infrastructure for security issues:
|
|
4
|
+
* - IAM: overly permissive policies, unused credentials, MFA status
|
|
5
|
+
* - S3: public buckets, unencrypted buckets, versioning
|
|
6
|
+
* - EC2: security groups, public IPs, unencrypted volumes
|
|
7
|
+
* - Lambda: public functions, environment secrets
|
|
8
|
+
* - RDS: public instances, unencrypted databases
|
|
9
|
+
*/
|
|
10
|
+
import { IAMClient, ListUsersCommand, ListAccessKeysCommand, GetAccessKeyLastUsedCommand, ListMFADevicesCommand, GetPolicyVersionCommand, ListPoliciesCommand, } from '@aws-sdk/client-iam';
|
|
11
|
+
import { S3Client, ListBucketsCommand, GetBucketEncryptionCommand, GetBucketVersioningCommand, GetBucketPolicyStatusCommand, GetPublicAccessBlockCommand, } from '@aws-sdk/client-s3';
|
|
12
|
+
import { EC2Client, DescribeSecurityGroupsCommand, DescribeInstancesCommand, DescribeVolumesCommand, } from '@aws-sdk/client-ec2';
|
|
13
|
+
import { LambdaClient, ListFunctionsCommand, GetPolicyCommand, } from '@aws-sdk/client-lambda';
|
|
14
|
+
import { RDSClient, DescribeDBInstancesCommand, } from '@aws-sdk/client-rds';
|
|
15
|
+
import { fromEnv, fromIni } from '@aws-sdk/credential-providers';
|
|
16
|
+
// ============ SCANNER CLASS ============
|
|
17
|
+
export class AWSScanner {
|
|
18
|
+
region;
|
|
19
|
+
iamClient;
|
|
20
|
+
s3Client;
|
|
21
|
+
ec2Client;
|
|
22
|
+
lambdaClient;
|
|
23
|
+
rdsClient;
|
|
24
|
+
config;
|
|
25
|
+
constructor(config = {}) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
this.region = config.region || process.env.AWS_REGION || 'us-east-1';
|
|
28
|
+
// Configure credentials
|
|
29
|
+
const credentialProvider = config.profile
|
|
30
|
+
? fromIni({ profile: config.profile })
|
|
31
|
+
: fromEnv();
|
|
32
|
+
const clientConfig = {
|
|
33
|
+
region: this.region,
|
|
34
|
+
credentials: credentialProvider,
|
|
35
|
+
};
|
|
36
|
+
// Initialize clients
|
|
37
|
+
this.iamClient = new IAMClient(clientConfig);
|
|
38
|
+
this.s3Client = new S3Client(clientConfig);
|
|
39
|
+
this.ec2Client = new EC2Client(clientConfig);
|
|
40
|
+
this.lambdaClient = new LambdaClient(clientConfig);
|
|
41
|
+
this.rdsClient = new RDSClient(clientConfig);
|
|
42
|
+
}
|
|
43
|
+
async scan() {
|
|
44
|
+
const findings = [];
|
|
45
|
+
const errors = [];
|
|
46
|
+
const scannedServices = [];
|
|
47
|
+
const services = this.config.services || ['iam', 's3', 'ec2', 'lambda', 'rds'];
|
|
48
|
+
const skip = this.config.skipServices || [];
|
|
49
|
+
console.log(`[AWS] Starting scan in region: ${this.region}`);
|
|
50
|
+
// IAM Scan
|
|
51
|
+
if (services.includes('iam') && !skip.includes('iam')) {
|
|
52
|
+
try {
|
|
53
|
+
console.log('[AWS] Scanning IAM...');
|
|
54
|
+
const iamFindings = await this.scanIAM();
|
|
55
|
+
findings.push(...iamFindings);
|
|
56
|
+
scannedServices.push('iam');
|
|
57
|
+
console.log(`[AWS] IAM: found ${iamFindings.length} findings`);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
61
|
+
errors.push({ service: 'iam', error: errorMsg });
|
|
62
|
+
console.error(`[AWS] IAM scan error: ${errorMsg}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// S3 Scan
|
|
66
|
+
if (services.includes('s3') && !skip.includes('s3')) {
|
|
67
|
+
try {
|
|
68
|
+
console.log('[AWS] Scanning S3...');
|
|
69
|
+
const s3Findings = await this.scanS3();
|
|
70
|
+
findings.push(...s3Findings);
|
|
71
|
+
scannedServices.push('s3');
|
|
72
|
+
console.log(`[AWS] S3: found ${s3Findings.length} findings`);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
76
|
+
errors.push({ service: 's3', error: errorMsg });
|
|
77
|
+
console.error(`[AWS] S3 scan error: ${errorMsg}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// EC2 Scan
|
|
81
|
+
if (services.includes('ec2') && !skip.includes('ec2')) {
|
|
82
|
+
try {
|
|
83
|
+
console.log('[AWS] Scanning EC2...');
|
|
84
|
+
const ec2Findings = await this.scanEC2();
|
|
85
|
+
findings.push(...ec2Findings);
|
|
86
|
+
scannedServices.push('ec2');
|
|
87
|
+
console.log(`[AWS] EC2: found ${ec2Findings.length} findings`);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
91
|
+
errors.push({ service: 'ec2', error: errorMsg });
|
|
92
|
+
console.error(`[AWS] EC2 scan error: ${errorMsg}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Lambda Scan
|
|
96
|
+
if (services.includes('lambda') && !skip.includes('lambda')) {
|
|
97
|
+
try {
|
|
98
|
+
console.log('[AWS] Scanning Lambda...');
|
|
99
|
+
const lambdaFindings = await this.scanLambda();
|
|
100
|
+
findings.push(...lambdaFindings);
|
|
101
|
+
scannedServices.push('lambda');
|
|
102
|
+
console.log(`[AWS] Lambda: found ${lambdaFindings.length} findings`);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
106
|
+
errors.push({ service: 'lambda', error: errorMsg });
|
|
107
|
+
console.error(`[AWS] Lambda scan error: ${errorMsg}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// RDS Scan
|
|
111
|
+
if (services.includes('rds') && !skip.includes('rds')) {
|
|
112
|
+
try {
|
|
113
|
+
console.log('[AWS] Scanning RDS...');
|
|
114
|
+
const rdsFindings = await this.scanRDS();
|
|
115
|
+
findings.push(...rdsFindings);
|
|
116
|
+
scannedServices.push('rds');
|
|
117
|
+
console.log(`[AWS] RDS: found ${rdsFindings.length} findings`);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
121
|
+
errors.push({ service: 'rds', error: errorMsg });
|
|
122
|
+
console.error(`[AWS] RDS scan error: ${errorMsg}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Calculate summary
|
|
126
|
+
const summary = {
|
|
127
|
+
critical: findings.filter(f => f.severity === 'critical').length,
|
|
128
|
+
high: findings.filter(f => f.severity === 'high').length,
|
|
129
|
+
medium: findings.filter(f => f.severity === 'medium').length,
|
|
130
|
+
low: findings.filter(f => f.severity === 'low').length,
|
|
131
|
+
info: findings.filter(f => f.severity === 'info').length,
|
|
132
|
+
total: findings.length,
|
|
133
|
+
};
|
|
134
|
+
console.log(`[AWS] Scan complete. Total findings: ${findings.length}`);
|
|
135
|
+
return {
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
region: this.region,
|
|
138
|
+
findings,
|
|
139
|
+
summary,
|
|
140
|
+
scannedServices,
|
|
141
|
+
errors,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// ============ IAM SCANNING ============
|
|
145
|
+
async scanIAM() {
|
|
146
|
+
const findings = [];
|
|
147
|
+
// List all users
|
|
148
|
+
const usersResponse = await this.iamClient.send(new ListUsersCommand({}));
|
|
149
|
+
const users = usersResponse.Users || [];
|
|
150
|
+
for (const user of users) {
|
|
151
|
+
if (!user.UserName)
|
|
152
|
+
continue;
|
|
153
|
+
// Check for MFA
|
|
154
|
+
const mfaResponse = await this.iamClient.send(new ListMFADevicesCommand({ UserName: user.UserName }));
|
|
155
|
+
if (!mfaResponse.MFADevices || mfaResponse.MFADevices.length === 0) {
|
|
156
|
+
findings.push({
|
|
157
|
+
service: 'iam',
|
|
158
|
+
resourceType: 'User',
|
|
159
|
+
resourceId: user.UserName,
|
|
160
|
+
resourceArn: user.Arn,
|
|
161
|
+
severity: 'high',
|
|
162
|
+
title: 'IAM User without MFA',
|
|
163
|
+
description: `User ${user.UserName} does not have MFA enabled`,
|
|
164
|
+
remediation: 'Enable MFA for this IAM user',
|
|
165
|
+
metadata: { userId: user.UserId },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// Check access keys
|
|
169
|
+
const keysResponse = await this.iamClient.send(new ListAccessKeysCommand({ UserName: user.UserName }));
|
|
170
|
+
const accessKeys = keysResponse.AccessKeyMetadata || [];
|
|
171
|
+
for (const key of accessKeys) {
|
|
172
|
+
if (!key.AccessKeyId)
|
|
173
|
+
continue;
|
|
174
|
+
// Check key age (over 90 days is a concern)
|
|
175
|
+
if (key.CreateDate) {
|
|
176
|
+
const keyAge = Date.now() - key.CreateDate.getTime();
|
|
177
|
+
const daysOld = Math.floor(keyAge / (1000 * 60 * 60 * 24));
|
|
178
|
+
if (daysOld > 90) {
|
|
179
|
+
findings.push({
|
|
180
|
+
service: 'iam',
|
|
181
|
+
resourceType: 'AccessKey',
|
|
182
|
+
resourceId: key.AccessKeyId,
|
|
183
|
+
severity: 'medium',
|
|
184
|
+
title: 'Old IAM Access Key',
|
|
185
|
+
description: `Access key ${key.AccessKeyId} for user ${user.UserName} is ${daysOld} days old`,
|
|
186
|
+
remediation: 'Rotate access keys regularly (every 90 days)',
|
|
187
|
+
metadata: { userName: user.UserName, daysOld },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Check if key was recently used
|
|
192
|
+
try {
|
|
193
|
+
const lastUsedResponse = await this.iamClient.send(new GetAccessKeyLastUsedCommand({ AccessKeyId: key.AccessKeyId }));
|
|
194
|
+
const lastUsed = lastUsedResponse.AccessKeyLastUsed?.LastUsedDate;
|
|
195
|
+
if (lastUsed) {
|
|
196
|
+
const daysSinceUse = Math.floor((Date.now() - lastUsed.getTime()) / (1000 * 60 * 60 * 24));
|
|
197
|
+
if (daysSinceUse > 90) {
|
|
198
|
+
findings.push({
|
|
199
|
+
service: 'iam',
|
|
200
|
+
resourceType: 'AccessKey',
|
|
201
|
+
resourceId: key.AccessKeyId,
|
|
202
|
+
severity: 'medium',
|
|
203
|
+
title: 'Unused IAM Access Key',
|
|
204
|
+
description: `Access key ${key.AccessKeyId} for user ${user.UserName} has not been used in ${daysSinceUse} days`,
|
|
205
|
+
remediation: 'Delete unused access keys',
|
|
206
|
+
metadata: { userName: user.UserName, daysSinceUse },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Ignore errors checking last used
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Check for overly permissive policies
|
|
217
|
+
try {
|
|
218
|
+
const policiesResponse = await this.iamClient.send(new ListPoliciesCommand({ Scope: 'Local' }));
|
|
219
|
+
const policies = policiesResponse.Policies || [];
|
|
220
|
+
for (const policy of policies) {
|
|
221
|
+
if (!policy.Arn || !policy.DefaultVersionId)
|
|
222
|
+
continue;
|
|
223
|
+
try {
|
|
224
|
+
const versionResponse = await this.iamClient.send(new GetPolicyVersionCommand({
|
|
225
|
+
PolicyArn: policy.Arn,
|
|
226
|
+
VersionId: policy.DefaultVersionId,
|
|
227
|
+
}));
|
|
228
|
+
const document = versionResponse.PolicyVersion?.Document;
|
|
229
|
+
if (document) {
|
|
230
|
+
const policyDoc = JSON.parse(decodeURIComponent(document));
|
|
231
|
+
const statements = policyDoc.Statement || [];
|
|
232
|
+
for (const statement of statements) {
|
|
233
|
+
if (statement.Effect === 'Allow' &&
|
|
234
|
+
statement.Action === '*' &&
|
|
235
|
+
statement.Resource === '*') {
|
|
236
|
+
findings.push({
|
|
237
|
+
service: 'iam',
|
|
238
|
+
resourceType: 'Policy',
|
|
239
|
+
resourceId: policy.PolicyName || policy.Arn,
|
|
240
|
+
resourceArn: policy.Arn,
|
|
241
|
+
severity: 'critical',
|
|
242
|
+
title: 'Overly Permissive IAM Policy',
|
|
243
|
+
description: `Policy ${policy.PolicyName} grants full access (Action: *, Resource: *)`,
|
|
244
|
+
remediation: 'Apply least privilege principle - limit actions and resources',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Ignore policy parsing errors
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// Ignore errors listing policies
|
|
257
|
+
}
|
|
258
|
+
return findings;
|
|
259
|
+
}
|
|
260
|
+
// ============ S3 SCANNING ============
|
|
261
|
+
async scanS3() {
|
|
262
|
+
const findings = [];
|
|
263
|
+
// List all buckets
|
|
264
|
+
const bucketsResponse = await this.s3Client.send(new ListBucketsCommand({}));
|
|
265
|
+
const buckets = bucketsResponse.Buckets || [];
|
|
266
|
+
for (const bucket of buckets) {
|
|
267
|
+
if (!bucket.Name)
|
|
268
|
+
continue;
|
|
269
|
+
// Check public access block
|
|
270
|
+
try {
|
|
271
|
+
const publicAccessResponse = await this.s3Client.send(new GetPublicAccessBlockCommand({ Bucket: bucket.Name }));
|
|
272
|
+
const config = publicAccessResponse.PublicAccessBlockConfiguration;
|
|
273
|
+
if (!config?.BlockPublicAcls ||
|
|
274
|
+
!config?.BlockPublicPolicy ||
|
|
275
|
+
!config?.IgnorePublicAcls ||
|
|
276
|
+
!config?.RestrictPublicBuckets) {
|
|
277
|
+
findings.push({
|
|
278
|
+
service: 's3',
|
|
279
|
+
resourceType: 'Bucket',
|
|
280
|
+
resourceId: bucket.Name,
|
|
281
|
+
severity: 'high',
|
|
282
|
+
title: 'S3 Bucket Public Access Not Fully Blocked',
|
|
283
|
+
description: `Bucket ${bucket.Name} does not have all public access blocks enabled`,
|
|
284
|
+
remediation: 'Enable all public access block settings',
|
|
285
|
+
metadata: {
|
|
286
|
+
blockPublicAcls: config?.BlockPublicAcls,
|
|
287
|
+
blockPublicPolicy: config?.BlockPublicPolicy,
|
|
288
|
+
ignorePublicAcls: config?.IgnorePublicAcls,
|
|
289
|
+
restrictPublicBuckets: config?.RestrictPublicBuckets,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
// If public access block is not configured, it's a finding
|
|
296
|
+
if (err.name === 'NoSuchPublicAccessBlockConfiguration') {
|
|
297
|
+
findings.push({
|
|
298
|
+
service: 's3',
|
|
299
|
+
resourceType: 'Bucket',
|
|
300
|
+
resourceId: bucket.Name,
|
|
301
|
+
severity: 'high',
|
|
302
|
+
title: 'S3 Bucket No Public Access Block',
|
|
303
|
+
description: `Bucket ${bucket.Name} has no public access block configuration`,
|
|
304
|
+
remediation: 'Configure public access block for this bucket',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Check encryption
|
|
309
|
+
try {
|
|
310
|
+
await this.s3Client.send(new GetBucketEncryptionCommand({ Bucket: bucket.Name }));
|
|
311
|
+
// If we get here, encryption is configured
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
if (err.name === 'ServerSideEncryptionConfigurationNotFoundError') {
|
|
315
|
+
findings.push({
|
|
316
|
+
service: 's3',
|
|
317
|
+
resourceType: 'Bucket',
|
|
318
|
+
resourceId: bucket.Name,
|
|
319
|
+
severity: 'medium',
|
|
320
|
+
title: 'S3 Bucket Not Encrypted',
|
|
321
|
+
description: `Bucket ${bucket.Name} does not have default encryption enabled`,
|
|
322
|
+
remediation: 'Enable server-side encryption for this bucket',
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Check versioning
|
|
327
|
+
try {
|
|
328
|
+
const versioningResponse = await this.s3Client.send(new GetBucketVersioningCommand({ Bucket: bucket.Name }));
|
|
329
|
+
if (versioningResponse.Status !== 'Enabled') {
|
|
330
|
+
findings.push({
|
|
331
|
+
service: 's3',
|
|
332
|
+
resourceType: 'Bucket',
|
|
333
|
+
resourceId: bucket.Name,
|
|
334
|
+
severity: 'low',
|
|
335
|
+
title: 'S3 Bucket Versioning Disabled',
|
|
336
|
+
description: `Bucket ${bucket.Name} does not have versioning enabled`,
|
|
337
|
+
remediation: 'Enable versioning for data protection and recovery',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// Ignore versioning check errors
|
|
343
|
+
}
|
|
344
|
+
// Check for public bucket policy
|
|
345
|
+
try {
|
|
346
|
+
const policyStatusResponse = await this.s3Client.send(new GetBucketPolicyStatusCommand({ Bucket: bucket.Name }));
|
|
347
|
+
if (policyStatusResponse.PolicyStatus?.IsPublic) {
|
|
348
|
+
findings.push({
|
|
349
|
+
service: 's3',
|
|
350
|
+
resourceType: 'Bucket',
|
|
351
|
+
resourceId: bucket.Name,
|
|
352
|
+
severity: 'critical',
|
|
353
|
+
title: 'S3 Bucket Has Public Policy',
|
|
354
|
+
description: `Bucket ${bucket.Name} has a policy that makes it publicly accessible`,
|
|
355
|
+
remediation: 'Review and restrict the bucket policy',
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// No policy or error - skip
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return findings;
|
|
364
|
+
}
|
|
365
|
+
// ============ EC2 SCANNING ============
|
|
366
|
+
async scanEC2() {
|
|
367
|
+
const findings = [];
|
|
368
|
+
// Check security groups
|
|
369
|
+
const sgResponse = await this.ec2Client.send(new DescribeSecurityGroupsCommand({}));
|
|
370
|
+
const securityGroups = sgResponse.SecurityGroups || [];
|
|
371
|
+
for (const sg of securityGroups) {
|
|
372
|
+
if (!sg.GroupId)
|
|
373
|
+
continue;
|
|
374
|
+
// Check for overly permissive inbound rules
|
|
375
|
+
for (const rule of sg.IpPermissions || []) {
|
|
376
|
+
for (const ipRange of rule.IpRanges || []) {
|
|
377
|
+
if (ipRange.CidrIp === '0.0.0.0/0') {
|
|
378
|
+
// Check if it's a sensitive port
|
|
379
|
+
const fromPort = rule.FromPort || 0;
|
|
380
|
+
const toPort = rule.ToPort || 65535;
|
|
381
|
+
const sensitivePort = this.isSensitivePort(fromPort, toPort);
|
|
382
|
+
if (sensitivePort) {
|
|
383
|
+
findings.push({
|
|
384
|
+
service: 'ec2',
|
|
385
|
+
resourceType: 'SecurityGroup',
|
|
386
|
+
resourceId: sg.GroupId,
|
|
387
|
+
severity: 'critical',
|
|
388
|
+
title: 'Security Group Allows Public Access to Sensitive Port',
|
|
389
|
+
description: `Security group ${sg.GroupName || sg.GroupId} allows 0.0.0.0/0 access to port ${fromPort}-${toPort}`,
|
|
390
|
+
remediation: 'Restrict access to specific IP ranges',
|
|
391
|
+
metadata: {
|
|
392
|
+
groupName: sg.GroupName,
|
|
393
|
+
fromPort,
|
|
394
|
+
toPort,
|
|
395
|
+
protocol: rule.IpProtocol,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
else if (fromPort === 0 && toPort === 65535) {
|
|
400
|
+
findings.push({
|
|
401
|
+
service: 'ec2',
|
|
402
|
+
resourceType: 'SecurityGroup',
|
|
403
|
+
resourceId: sg.GroupId,
|
|
404
|
+
severity: 'high',
|
|
405
|
+
title: 'Security Group Allows All Traffic from Internet',
|
|
406
|
+
description: `Security group ${sg.GroupName || sg.GroupId} allows all inbound traffic from 0.0.0.0/0`,
|
|
407
|
+
remediation: 'Restrict to specific ports and IP ranges',
|
|
408
|
+
metadata: { groupName: sg.GroupName },
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Check instances
|
|
416
|
+
const instancesResponse = await this.ec2Client.send(new DescribeInstancesCommand({}));
|
|
417
|
+
const reservations = instancesResponse.Reservations || [];
|
|
418
|
+
for (const reservation of reservations) {
|
|
419
|
+
for (const instance of reservation.Instances || []) {
|
|
420
|
+
if (!instance.InstanceId)
|
|
421
|
+
continue;
|
|
422
|
+
// Check for public IP
|
|
423
|
+
if (instance.PublicIpAddress) {
|
|
424
|
+
findings.push({
|
|
425
|
+
service: 'ec2',
|
|
426
|
+
resourceType: 'Instance',
|
|
427
|
+
resourceId: instance.InstanceId,
|
|
428
|
+
severity: 'info',
|
|
429
|
+
title: 'EC2 Instance Has Public IP',
|
|
430
|
+
description: `Instance ${instance.InstanceId} has public IP ${instance.PublicIpAddress}`,
|
|
431
|
+
remediation: 'Verify this instance needs public access',
|
|
432
|
+
metadata: {
|
|
433
|
+
publicIp: instance.PublicIpAddress,
|
|
434
|
+
instanceType: instance.InstanceType,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Check for unencrypted volumes
|
|
441
|
+
const volumesResponse = await this.ec2Client.send(new DescribeVolumesCommand({}));
|
|
442
|
+
const volumes = volumesResponse.Volumes || [];
|
|
443
|
+
for (const volume of volumes) {
|
|
444
|
+
if (!volume.VolumeId)
|
|
445
|
+
continue;
|
|
446
|
+
if (!volume.Encrypted) {
|
|
447
|
+
findings.push({
|
|
448
|
+
service: 'ec2',
|
|
449
|
+
resourceType: 'Volume',
|
|
450
|
+
resourceId: volume.VolumeId,
|
|
451
|
+
severity: 'medium',
|
|
452
|
+
title: 'EBS Volume Not Encrypted',
|
|
453
|
+
description: `Volume ${volume.VolumeId} is not encrypted`,
|
|
454
|
+
remediation: 'Enable encryption for EBS volumes',
|
|
455
|
+
metadata: {
|
|
456
|
+
size: volume.Size,
|
|
457
|
+
state: volume.State,
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return findings;
|
|
463
|
+
}
|
|
464
|
+
isSensitivePort(fromPort, toPort) {
|
|
465
|
+
const sensitivePorts = [22, 23, 3389, 3306, 5432, 1433, 27017, 6379];
|
|
466
|
+
return sensitivePorts.some(p => p >= fromPort && p <= toPort);
|
|
467
|
+
}
|
|
468
|
+
// ============ LAMBDA SCANNING ============
|
|
469
|
+
async scanLambda() {
|
|
470
|
+
const findings = [];
|
|
471
|
+
// List functions
|
|
472
|
+
const functionsResponse = await this.lambdaClient.send(new ListFunctionsCommand({}));
|
|
473
|
+
const functions = functionsResponse.Functions || [];
|
|
474
|
+
for (const func of functions) {
|
|
475
|
+
if (!func.FunctionName || !func.FunctionArn)
|
|
476
|
+
continue;
|
|
477
|
+
// Check for environment variables that look like secrets
|
|
478
|
+
const envVars = func.Environment?.Variables || {};
|
|
479
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
480
|
+
const keyLower = key.toLowerCase();
|
|
481
|
+
if (keyLower.includes('secret') ||
|
|
482
|
+
keyLower.includes('password') ||
|
|
483
|
+
keyLower.includes('key') ||
|
|
484
|
+
keyLower.includes('token')) {
|
|
485
|
+
findings.push({
|
|
486
|
+
service: 'lambda',
|
|
487
|
+
resourceType: 'Function',
|
|
488
|
+
resourceId: func.FunctionName,
|
|
489
|
+
resourceArn: func.FunctionArn,
|
|
490
|
+
severity: 'high',
|
|
491
|
+
title: 'Lambda Function Has Sensitive Environment Variable',
|
|
492
|
+
description: `Function ${func.FunctionName} has environment variable "${key}" that may contain secrets`,
|
|
493
|
+
remediation: 'Use AWS Secrets Manager or Parameter Store for sensitive values',
|
|
494
|
+
metadata: { envVarName: key },
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Check for public resource policy
|
|
499
|
+
try {
|
|
500
|
+
const policyResponse = await this.lambdaClient.send(new GetPolicyCommand({ FunctionName: func.FunctionName }));
|
|
501
|
+
if (policyResponse.Policy) {
|
|
502
|
+
const policy = JSON.parse(policyResponse.Policy);
|
|
503
|
+
for (const statement of policy.Statement || []) {
|
|
504
|
+
if (statement.Principal === '*') {
|
|
505
|
+
findings.push({
|
|
506
|
+
service: 'lambda',
|
|
507
|
+
resourceType: 'Function',
|
|
508
|
+
resourceId: func.FunctionName,
|
|
509
|
+
resourceArn: func.FunctionArn,
|
|
510
|
+
severity: 'critical',
|
|
511
|
+
title: 'Lambda Function Has Public Access',
|
|
512
|
+
description: `Function ${func.FunctionName} has a resource policy allowing public access`,
|
|
513
|
+
remediation: 'Restrict the resource policy to specific principals',
|
|
514
|
+
});
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
// No policy - that's fine
|
|
522
|
+
}
|
|
523
|
+
// Check runtime (old runtimes are security risks)
|
|
524
|
+
const runtime = func.Runtime || '';
|
|
525
|
+
const deprecatedRuntimes = [
|
|
526
|
+
'nodejs12.x',
|
|
527
|
+
'nodejs10.x',
|
|
528
|
+
'python2.7',
|
|
529
|
+
'python3.6',
|
|
530
|
+
'ruby2.5',
|
|
531
|
+
];
|
|
532
|
+
if (deprecatedRuntimes.some(r => runtime.includes(r))) {
|
|
533
|
+
findings.push({
|
|
534
|
+
service: 'lambda',
|
|
535
|
+
resourceType: 'Function',
|
|
536
|
+
resourceId: func.FunctionName,
|
|
537
|
+
resourceArn: func.FunctionArn,
|
|
538
|
+
severity: 'medium',
|
|
539
|
+
title: 'Lambda Function Uses Deprecated Runtime',
|
|
540
|
+
description: `Function ${func.FunctionName} uses deprecated runtime ${runtime}`,
|
|
541
|
+
remediation: 'Upgrade to a supported runtime version',
|
|
542
|
+
metadata: { runtime },
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return findings;
|
|
547
|
+
}
|
|
548
|
+
// ============ RDS SCANNING ============
|
|
549
|
+
async scanRDS() {
|
|
550
|
+
const findings = [];
|
|
551
|
+
// List DB instances
|
|
552
|
+
const dbResponse = await this.rdsClient.send(new DescribeDBInstancesCommand({}));
|
|
553
|
+
const instances = dbResponse.DBInstances || [];
|
|
554
|
+
for (const db of instances) {
|
|
555
|
+
if (!db.DBInstanceIdentifier)
|
|
556
|
+
continue;
|
|
557
|
+
// Check for public access
|
|
558
|
+
if (db.PubliclyAccessible) {
|
|
559
|
+
findings.push({
|
|
560
|
+
service: 'rds',
|
|
561
|
+
resourceType: 'DBInstance',
|
|
562
|
+
resourceId: db.DBInstanceIdentifier,
|
|
563
|
+
resourceArn: db.DBInstanceArn,
|
|
564
|
+
severity: 'critical',
|
|
565
|
+
title: 'RDS Instance Publicly Accessible',
|
|
566
|
+
description: `Database ${db.DBInstanceIdentifier} is publicly accessible`,
|
|
567
|
+
remediation: 'Disable public accessibility unless required',
|
|
568
|
+
metadata: {
|
|
569
|
+
engine: db.Engine,
|
|
570
|
+
endpoint: db.Endpoint?.Address,
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
// Check for encryption
|
|
575
|
+
if (!db.StorageEncrypted) {
|
|
576
|
+
findings.push({
|
|
577
|
+
service: 'rds',
|
|
578
|
+
resourceType: 'DBInstance',
|
|
579
|
+
resourceId: db.DBInstanceIdentifier,
|
|
580
|
+
resourceArn: db.DBInstanceArn,
|
|
581
|
+
severity: 'high',
|
|
582
|
+
title: 'RDS Instance Not Encrypted',
|
|
583
|
+
description: `Database ${db.DBInstanceIdentifier} storage is not encrypted`,
|
|
584
|
+
remediation: 'Enable storage encryption for the database',
|
|
585
|
+
metadata: { engine: db.Engine },
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
// Check for automated backups
|
|
589
|
+
if (db.BackupRetentionPeriod === 0) {
|
|
590
|
+
findings.push({
|
|
591
|
+
service: 'rds',
|
|
592
|
+
resourceType: 'DBInstance',
|
|
593
|
+
resourceId: db.DBInstanceIdentifier,
|
|
594
|
+
resourceArn: db.DBInstanceArn,
|
|
595
|
+
severity: 'medium',
|
|
596
|
+
title: 'RDS Instance Has No Automated Backups',
|
|
597
|
+
description: `Database ${db.DBInstanceIdentifier} has automated backups disabled`,
|
|
598
|
+
remediation: 'Enable automated backups with appropriate retention period',
|
|
599
|
+
metadata: { engine: db.Engine },
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
// Check for deletion protection
|
|
603
|
+
if (!db.DeletionProtection) {
|
|
604
|
+
findings.push({
|
|
605
|
+
service: 'rds',
|
|
606
|
+
resourceType: 'DBInstance',
|
|
607
|
+
resourceId: db.DBInstanceIdentifier,
|
|
608
|
+
resourceArn: db.DBInstanceArn,
|
|
609
|
+
severity: 'low',
|
|
610
|
+
title: 'RDS Instance Has No Deletion Protection',
|
|
611
|
+
description: `Database ${db.DBInstanceIdentifier} does not have deletion protection enabled`,
|
|
612
|
+
remediation: 'Enable deletion protection for production databases',
|
|
613
|
+
metadata: { engine: db.Engine },
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return findings;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Quick scan function
|
|
621
|
+
export async function scanAWS(config) {
|
|
622
|
+
const scanner = new AWSScanner(config);
|
|
623
|
+
return scanner.scan();
|
|
624
|
+
}
|