guardrail-compliance 1.0.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/dist/audit/emitter.d.ts +97 -0
- package/dist/audit/emitter.d.ts.map +1 -0
- package/dist/audit/emitter.js +197 -0
- package/dist/audit/events.d.ts +304 -0
- package/dist/audit/events.d.ts.map +1 -0
- package/dist/audit/events.js +267 -0
- package/dist/audit/index.d.ts +11 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +51 -0
- package/dist/audit/storage.d.ts +93 -0
- package/dist/audit/storage.d.ts.map +1 -0
- package/dist/audit/storage.js +337 -0
- package/dist/automation/__tests__/compliance-scheduler.test.d.ts +2 -0
- package/dist/automation/__tests__/compliance-scheduler.test.d.ts.map +1 -0
- package/dist/automation/__tests__/compliance-scheduler.test.js +140 -0
- package/dist/automation/audit-logger.d.ts +129 -0
- package/dist/automation/audit-logger.d.ts.map +1 -0
- package/dist/automation/audit-logger.js +473 -0
- package/dist/automation/compliance-scheduler-fixed.d.ts +1 -0
- package/dist/automation/compliance-scheduler-fixed.d.ts.map +1 -0
- package/dist/automation/compliance-scheduler-fixed.js +1 -0
- package/dist/automation/compliance-scheduler.d.ts +83 -0
- package/dist/automation/compliance-scheduler.d.ts.map +1 -0
- package/dist/automation/compliance-scheduler.js +414 -0
- package/dist/automation/dashboard.d.ts +194 -0
- package/dist/automation/dashboard.d.ts.map +1 -0
- package/dist/automation/dashboard.js +768 -0
- package/dist/automation/email-service.d.ts +69 -0
- package/dist/automation/email-service.d.ts.map +1 -0
- package/dist/automation/email-service.js +218 -0
- package/dist/automation/evidence-collector.d.ts +140 -0
- package/dist/automation/evidence-collector.d.ts.map +1 -0
- package/dist/automation/evidence-collector.js +682 -0
- package/dist/automation/index.d.ts +8 -0
- package/dist/automation/index.d.ts.map +1 -0
- package/dist/automation/index.js +24 -0
- package/dist/automation/pdf-exporter.d.ts +90 -0
- package/dist/automation/pdf-exporter.d.ts.map +1 -0
- package/dist/automation/pdf-exporter.js +381 -0
- package/dist/automation/reporting-engine.d.ts +116 -0
- package/dist/automation/reporting-engine.d.ts.map +1 -0
- package/dist/automation/reporting-engine.js +329 -0
- package/dist/container/index.d.ts +4 -0
- package/dist/container/index.d.ts.map +1 -0
- package/dist/container/index.js +19 -0
- package/dist/container/kubernetes.d.ts +94 -0
- package/dist/container/kubernetes.d.ts.map +1 -0
- package/dist/container/kubernetes.js +268 -0
- package/dist/container/rules.d.ts +27 -0
- package/dist/container/rules.d.ts.map +1 -0
- package/dist/container/rules.js +216 -0
- package/dist/container/scanner.d.ts +50 -0
- package/dist/container/scanner.d.ts.map +1 -0
- package/dist/container/scanner.js +143 -0
- package/dist/frameworks/engine.d.ts +108 -0
- package/dist/frameworks/engine.d.ts.map +1 -0
- package/dist/frameworks/engine.js +206 -0
- package/dist/frameworks/gdpr.d.ts +6 -0
- package/dist/frameworks/gdpr.d.ts.map +1 -0
- package/dist/frameworks/gdpr.js +198 -0
- package/dist/frameworks/hipaa.d.ts +6 -0
- package/dist/frameworks/hipaa.d.ts.map +1 -0
- package/dist/frameworks/hipaa.js +183 -0
- package/dist/frameworks/index.d.ts +8 -0
- package/dist/frameworks/index.d.ts.map +1 -0
- package/dist/frameworks/index.js +30 -0
- package/dist/frameworks/iso27001.d.ts +63 -0
- package/dist/frameworks/iso27001.d.ts.map +1 -0
- package/dist/frameworks/iso27001.js +331 -0
- package/dist/frameworks/nist.d.ts +62 -0
- package/dist/frameworks/nist.d.ts.map +1 -0
- package/dist/frameworks/nist.js +424 -0
- package/dist/frameworks/pci.d.ts +6 -0
- package/dist/frameworks/pci.d.ts.map +1 -0
- package/dist/frameworks/pci.js +201 -0
- package/dist/frameworks/soc2.d.ts +7 -0
- package/dist/frameworks/soc2.d.ts.map +1 -0
- package/dist/frameworks/soc2.js +248 -0
- package/dist/iac/drift-detector.d.ts +64 -0
- package/dist/iac/drift-detector.d.ts.map +1 -0
- package/dist/iac/drift-detector.js +134 -0
- package/dist/iac/index.d.ts +4 -0
- package/dist/iac/index.d.ts.map +1 -0
- package/dist/iac/index.js +19 -0
- package/dist/iac/rules.d.ts +17 -0
- package/dist/iac/rules.d.ts.map +1 -0
- package/dist/iac/rules.js +385 -0
- package/dist/iac/scanner.d.ts +104 -0
- package/dist/iac/scanner.d.ts.map +1 -0
- package/dist/iac/scanner.js +343 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/pii/data-flow.d.ts +58 -0
- package/dist/pii/data-flow.d.ts.map +1 -0
- package/dist/pii/data-flow.js +154 -0
- package/dist/pii/detector.d.ts +60 -0
- package/dist/pii/detector.d.ts.map +1 -0
- package/dist/pii/detector.js +267 -0
- package/dist/pii/index.d.ts +4 -0
- package/dist/pii/index.d.ts.map +1 -0
- package/dist/pii/index.js +19 -0
- package/dist/pii/patterns.d.ts +36 -0
- package/dist/pii/patterns.d.ts.map +1 -0
- package/dist/pii/patterns.js +108 -0
- package/dist/policy/index.d.ts +5 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +20 -0
- package/dist/policy/opa-engine.d.ts +121 -0
- package/dist/policy/opa-engine.d.ts.map +1 -0
- package/dist/policy/opa-engine.js +423 -0
- package/package.json +31 -0
- package/src/audit/emitter.ts +383 -0
- package/src/audit/events.ts +351 -0
- package/src/audit/index.ts +35 -0
- package/src/audit/storage.ts +394 -0
- package/src/automation/__tests__/compliance-scheduler.test.ts +183 -0
- package/src/automation/audit-logger.ts +629 -0
- package/src/automation/compliance-scheduler-fixed.ts +0 -0
- package/src/automation/compliance-scheduler.ts +516 -0
- package/src/automation/dashboard.ts +947 -0
- package/src/automation/email-service.ts +230 -0
- package/src/automation/evidence-collector.ts +866 -0
- package/src/automation/index.ts +8 -0
- package/src/automation/pdf-exporter.ts +434 -0
- package/src/automation/reporting-engine.ts +462 -0
- package/src/container/index.ts +3 -0
- package/src/container/kubernetes.ts +379 -0
- package/src/container/rules.ts +244 -0
- package/src/container/scanner.ts +202 -0
- package/src/frameworks/engine.ts +298 -0
- package/src/frameworks/gdpr.ts +204 -0
- package/src/frameworks/hipaa.ts +209 -0
- package/src/frameworks/index.ts +23 -0
- package/src/frameworks/iso27001.ts +398 -0
- package/src/frameworks/nist.ts +518 -0
- package/src/frameworks/pci.ts +226 -0
- package/src/frameworks/soc2.ts +281 -0
- package/src/iac/drift-detector.ts +197 -0
- package/src/iac/index.ts +3 -0
- package/src/iac/rules.ts +420 -0
- package/src/iac/scanner.ts +445 -0
- package/src/index.ts +17 -0
- package/src/pii/data-flow.ts +216 -0
- package/src/pii/detector.ts +327 -0
- package/src/pii/index.ts +3 -0
- package/src/pii/patterns.ts +128 -0
- package/src/policy/index.ts +5 -0
- package/src/policy/opa-engine.ts +504 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { prisma } from '@guardrail/database';
|
|
2
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { K8S_RULES } from './rules';
|
|
5
|
+
|
|
6
|
+
export interface K8sManifest {
|
|
7
|
+
apiVersion: string;
|
|
8
|
+
kind: string;
|
|
9
|
+
metadata: {
|
|
10
|
+
name: string;
|
|
11
|
+
namespace?: string;
|
|
12
|
+
};
|
|
13
|
+
spec: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface K8sFinding {
|
|
17
|
+
ruleId: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
21
|
+
resourceType: string;
|
|
22
|
+
resourceName: string;
|
|
23
|
+
namespace?: string;
|
|
24
|
+
filePath: string;
|
|
25
|
+
recommendation: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RBACAnalysis {
|
|
29
|
+
roles: Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
namespace?: string;
|
|
32
|
+
rules: any[];
|
|
33
|
+
riskyPermissions: string[];
|
|
34
|
+
}>;
|
|
35
|
+
roleBindings: Array<{
|
|
36
|
+
name: string;
|
|
37
|
+
namespace?: string;
|
|
38
|
+
subjects: any[];
|
|
39
|
+
roleRef: any;
|
|
40
|
+
}>;
|
|
41
|
+
findings: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PodSecurityAnalysis {
|
|
45
|
+
totalPods: number;
|
|
46
|
+
privilegedPods: number;
|
|
47
|
+
hostNetworkPods: number;
|
|
48
|
+
runAsRootPods: number;
|
|
49
|
+
findings: K8sFinding[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface NetworkPolicyAnalysis {
|
|
53
|
+
hasNetworkPolicies: boolean;
|
|
54
|
+
totalPolicies: number;
|
|
55
|
+
unprotectedNamespaces: string[];
|
|
56
|
+
findings: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface KubernetesScanResult {
|
|
60
|
+
projectId: string;
|
|
61
|
+
summary: {
|
|
62
|
+
totalResources: number;
|
|
63
|
+
critical: number;
|
|
64
|
+
high: number;
|
|
65
|
+
medium: number;
|
|
66
|
+
low: number;
|
|
67
|
+
};
|
|
68
|
+
findings: K8sFinding[];
|
|
69
|
+
rbacAnalysis?: RBACAnalysis;
|
|
70
|
+
podSecurity?: PodSecurityAnalysis;
|
|
71
|
+
networkPolicies?: NetworkPolicyAnalysis;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class KubernetesScanner {
|
|
75
|
+
/**
|
|
76
|
+
* Scan Kubernetes manifests
|
|
77
|
+
*/
|
|
78
|
+
async scanManifests(projectPath: string, projectId: string): Promise<KubernetesScanResult> {
|
|
79
|
+
// Find all Kubernetes manifest files
|
|
80
|
+
const manifestFiles = this.findManifestFiles(projectPath);
|
|
81
|
+
const manifests: Array<{ manifest: K8sManifest; filePath: string }> = [];
|
|
82
|
+
|
|
83
|
+
for (const file of manifestFiles) {
|
|
84
|
+
try {
|
|
85
|
+
const content = readFileSync(file, 'utf-8');
|
|
86
|
+
const docs = this.parseYAML(content);
|
|
87
|
+
|
|
88
|
+
// Handle multiple documents in one file
|
|
89
|
+
const docArray = Array.isArray(docs) ? docs : [docs];
|
|
90
|
+
|
|
91
|
+
for (const doc of docArray) {
|
|
92
|
+
if (doc && doc.kind && doc.metadata) {
|
|
93
|
+
manifests.push({
|
|
94
|
+
manifest: doc,
|
|
95
|
+
filePath: file
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(`Error parsing manifest ${file}:`, error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Scan for security issues
|
|
105
|
+
const findings = this.scanForSecurityIssues(manifests);
|
|
106
|
+
|
|
107
|
+
// Analyze RBAC
|
|
108
|
+
const rbacAnalysis = await this.analyzeRBAC(manifests.map(m => m.manifest));
|
|
109
|
+
|
|
110
|
+
// Check pod security
|
|
111
|
+
const podSecurity = await this.checkPodSecurity(manifests.map(m => m.manifest));
|
|
112
|
+
|
|
113
|
+
// Validate network policies
|
|
114
|
+
const networkPolicies = await this.validateNetworkPolicies(manifests.map(m => m.manifest));
|
|
115
|
+
|
|
116
|
+
const summary = {
|
|
117
|
+
totalResources: manifests.length,
|
|
118
|
+
critical: findings.filter(f => f.severity === 'critical').length,
|
|
119
|
+
high: findings.filter(f => f.severity === 'high').length,
|
|
120
|
+
medium: findings.filter(f => f.severity === 'medium').length,
|
|
121
|
+
low: findings.filter(f => f.severity === 'low').length
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Save to database
|
|
125
|
+
try {
|
|
126
|
+
await (prisma as any).kubernetesScan.create({
|
|
127
|
+
data: {
|
|
128
|
+
projectId,
|
|
129
|
+
clusterName: 'default',
|
|
130
|
+
findings: findings as any,
|
|
131
|
+
results: {
|
|
132
|
+
findings,
|
|
133
|
+
summary,
|
|
134
|
+
rbacAnalysis,
|
|
135
|
+
podSecurity,
|
|
136
|
+
networkPolicies
|
|
137
|
+
} as any,
|
|
138
|
+
status: summary.critical > 0 ? 'failed' : summary.high > 0 ? 'warning' : 'passed'
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
// Table may not exist - continue
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
projectId,
|
|
147
|
+
summary,
|
|
148
|
+
findings,
|
|
149
|
+
rbacAnalysis,
|
|
150
|
+
podSecurity,
|
|
151
|
+
networkPolicies
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Analyze RBAC configuration
|
|
157
|
+
*/
|
|
158
|
+
async analyzeRBAC(manifests: K8sManifest[]): Promise<RBACAnalysis> {
|
|
159
|
+
const roles = manifests.filter(m => m.kind === 'Role' || m.kind === 'ClusterRole');
|
|
160
|
+
const roleBindings = manifests.filter(m => m.kind === 'RoleBinding' || m.kind === 'ClusterRoleBinding');
|
|
161
|
+
|
|
162
|
+
const findings: string[] = [];
|
|
163
|
+
const analyzedRoles = [];
|
|
164
|
+
|
|
165
|
+
for (const role of roles) {
|
|
166
|
+
const riskyPermissions: string[] = [];
|
|
167
|
+
const rules = role.spec?.rules || [];
|
|
168
|
+
|
|
169
|
+
for (const rule of rules) {
|
|
170
|
+
// Check for wildcard permissions
|
|
171
|
+
if (rule.verbs?.includes('*')) {
|
|
172
|
+
riskyPermissions.push('Wildcard verb permissions');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (rule.resources?.includes('*')) {
|
|
176
|
+
riskyPermissions.push('Wildcard resource permissions');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for dangerous permissions
|
|
180
|
+
const dangerousVerbs = ['create', 'delete', 'deletecollection'];
|
|
181
|
+
const dangerousResources = ['secrets', 'pods/exec', 'pods/portforward'];
|
|
182
|
+
|
|
183
|
+
if (dangerousVerbs.some(v => rule.verbs?.includes(v)) &&
|
|
184
|
+
dangerousResources.some(r => rule.resources?.includes(r))) {
|
|
185
|
+
riskyPermissions.push(`Dangerous permissions: ${rule.verbs.join(',')} on ${rule.resources.join(',')}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
analyzedRoles.push({
|
|
190
|
+
name: role.metadata.name,
|
|
191
|
+
namespace: role.metadata.namespace,
|
|
192
|
+
rules,
|
|
193
|
+
riskyPermissions
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (riskyPermissions.length > 0) {
|
|
197
|
+
findings.push(`Role ${role.metadata.name} has risky permissions: ${riskyPermissions.join(', ')}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
roles: analyzedRoles,
|
|
203
|
+
roleBindings: roleBindings.map(rb => ({
|
|
204
|
+
name: rb.metadata.name,
|
|
205
|
+
namespace: rb.metadata.namespace,
|
|
206
|
+
subjects: rb.spec?.subjects || [],
|
|
207
|
+
roleRef: rb.spec?.roleRef || {}
|
|
208
|
+
})),
|
|
209
|
+
findings
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check pod security
|
|
215
|
+
*/
|
|
216
|
+
async checkPodSecurity(manifests: K8sManifest[]): Promise<PodSecurityAnalysis> {
|
|
217
|
+
const pods = manifests.filter(m =>
|
|
218
|
+
m.kind === 'Pod' || m.kind === 'Deployment' || m.kind === 'StatefulSet' || m.kind === 'DaemonSet'
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
let privilegedPods = 0;
|
|
222
|
+
let hostNetworkPods = 0;
|
|
223
|
+
let runAsRootPods = 0;
|
|
224
|
+
const findings: K8sFinding[] = [];
|
|
225
|
+
|
|
226
|
+
for (const pod of pods) {
|
|
227
|
+
// Get pod spec (handle Deployment/StatefulSet template)
|
|
228
|
+
const podSpec = pod.kind === 'Pod' ? pod.spec : pod.spec?.template?.spec;
|
|
229
|
+
|
|
230
|
+
if (!podSpec) continue;
|
|
231
|
+
|
|
232
|
+
// Check for privileged containers
|
|
233
|
+
const containers = podSpec.containers || [];
|
|
234
|
+
const isPrivileged = containers.some((c: any) => c.securityContext?.privileged === true);
|
|
235
|
+
if (isPrivileged) privilegedPods++;
|
|
236
|
+
|
|
237
|
+
// Check for host network
|
|
238
|
+
if (podSpec.hostNetwork === true) hostNetworkPods++;
|
|
239
|
+
|
|
240
|
+
// Check for running as root
|
|
241
|
+
const runAsRoot = containers.some((c: any) =>
|
|
242
|
+
!c.securityContext?.runAsNonRoot && c.securityContext?.runAsUser === 0
|
|
243
|
+
);
|
|
244
|
+
if (runAsRoot) runAsRootPods++;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
totalPods: pods.length,
|
|
249
|
+
privilegedPods,
|
|
250
|
+
hostNetworkPods,
|
|
251
|
+
runAsRootPods,
|
|
252
|
+
findings
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Validate network policies
|
|
258
|
+
*/
|
|
259
|
+
async validateNetworkPolicies(manifests: K8sManifest[]): Promise<NetworkPolicyAnalysis> {
|
|
260
|
+
const networkPolicies = manifests.filter(m => m.kind === 'NetworkPolicy');
|
|
261
|
+
const namespaces = new Set<string>();
|
|
262
|
+
const findings: string[] = [];
|
|
263
|
+
|
|
264
|
+
// Collect all namespaces
|
|
265
|
+
for (const manifest of manifests) {
|
|
266
|
+
if (manifest.metadata.namespace) {
|
|
267
|
+
namespaces.add(manifest.metadata.namespace);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check which namespaces have network policies
|
|
272
|
+
const namespacesWithPolicies = new Set(
|
|
273
|
+
networkPolicies.map(np => np.metadata.namespace).filter(Boolean)
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const unprotectedNamespaces = Array.from(namespaces).filter(ns =>
|
|
277
|
+
!namespacesWithPolicies.has(ns)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (unprotectedNamespaces.length > 0) {
|
|
281
|
+
findings.push(`${unprotectedNamespaces.length} namespaces without network policies`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
hasNetworkPolicies: networkPolicies.length > 0,
|
|
286
|
+
totalPolicies: networkPolicies.length,
|
|
287
|
+
unprotectedNamespaces,
|
|
288
|
+
findings
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Scan for security issues using rules
|
|
294
|
+
*/
|
|
295
|
+
private scanForSecurityIssues(manifests: Array<{ manifest: K8sManifest; filePath: string }>): K8sFinding[] {
|
|
296
|
+
const findings: K8sFinding[] = [];
|
|
297
|
+
|
|
298
|
+
for (const { manifest, filePath } of manifests) {
|
|
299
|
+
// Skip non-pod resources for pod security checks
|
|
300
|
+
const isPodResource = ['Pod', 'Deployment', 'StatefulSet', 'DaemonSet', 'Job', 'CronJob'].includes(manifest.kind);
|
|
301
|
+
|
|
302
|
+
if (!isPodResource) continue;
|
|
303
|
+
|
|
304
|
+
// Get pod spec
|
|
305
|
+
const podSpec = manifest.kind === 'Pod' ? manifest.spec : manifest.spec?.template?.spec;
|
|
306
|
+
|
|
307
|
+
if (!podSpec) continue;
|
|
308
|
+
|
|
309
|
+
// Create a resource object compatible with rules
|
|
310
|
+
const resource = {
|
|
311
|
+
kind: manifest.kind,
|
|
312
|
+
metadata: manifest.metadata,
|
|
313
|
+
spec: podSpec
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Check against rules
|
|
317
|
+
for (const rule of K8S_RULES) {
|
|
318
|
+
if (rule.check(resource)) {
|
|
319
|
+
findings.push({
|
|
320
|
+
ruleId: rule.id,
|
|
321
|
+
title: rule.title,
|
|
322
|
+
description: rule.description,
|
|
323
|
+
severity: rule.severity,
|
|
324
|
+
resourceType: manifest.kind,
|
|
325
|
+
resourceName: manifest.metadata.name,
|
|
326
|
+
namespace: manifest.metadata.namespace,
|
|
327
|
+
filePath,
|
|
328
|
+
recommendation: rule.recommendation
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return findings;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Find Kubernetes manifest files
|
|
339
|
+
*/
|
|
340
|
+
private findManifestFiles(dir: string): string[] {
|
|
341
|
+
const files: string[] = [];
|
|
342
|
+
const extensions = ['.yaml', '.yml'];
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const entries = readdirSync(dir);
|
|
346
|
+
|
|
347
|
+
for (const entry of entries) {
|
|
348
|
+
const fullPath = join(dir, entry);
|
|
349
|
+
const stat = statSync(fullPath);
|
|
350
|
+
|
|
351
|
+
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
|
|
352
|
+
files.push(...this.findManifestFiles(fullPath));
|
|
353
|
+
} else if (stat.isFile()) {
|
|
354
|
+
if (extensions.some(ext => entry.endsWith(ext))) {
|
|
355
|
+
files.push(fullPath);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
// Ignore permission errors
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return files;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Simple YAML parser (in production, use proper YAML library)
|
|
368
|
+
*/
|
|
369
|
+
private parseYAML(content: string): any {
|
|
370
|
+
try {
|
|
371
|
+
return JSON.parse(content);
|
|
372
|
+
} catch {
|
|
373
|
+
// Simplified - would use yaml library in production
|
|
374
|
+
return {};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export const kubernetesScanner = new KubernetesScanner();
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
export interface DockerfileRule {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
6
|
+
category: string;
|
|
7
|
+
check: (instruction: string, value: string) => boolean;
|
|
8
|
+
recommendation: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface K8sRule {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
16
|
+
category: string;
|
|
17
|
+
check: (resource: any) => boolean;
|
|
18
|
+
recommendation: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Dockerfile Security Rules
|
|
23
|
+
*/
|
|
24
|
+
export const DOCKERFILE_RULES: DockerfileRule[] = [
|
|
25
|
+
{
|
|
26
|
+
id: 'DOCKER-001',
|
|
27
|
+
title: 'Running as Root',
|
|
28
|
+
description: 'Container runs as root user',
|
|
29
|
+
severity: 'high',
|
|
30
|
+
category: 'user-security',
|
|
31
|
+
check: (_instruction, _value) => {
|
|
32
|
+
// Check if USER instruction is missing or set to root
|
|
33
|
+
return _instruction === 'USER' && (_value === 'root' || _value === '0');
|
|
34
|
+
},
|
|
35
|
+
recommendation: 'Use a non-root user: USER node or USER 1000'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'DOCKER-002',
|
|
39
|
+
title: 'Using Latest Tag',
|
|
40
|
+
description: 'Base image uses :latest tag',
|
|
41
|
+
severity: 'medium',
|
|
42
|
+
category: 'versioning',
|
|
43
|
+
check: (_instruction, _value) => {
|
|
44
|
+
return _instruction === 'FROM' && _value.endsWith(':latest');
|
|
45
|
+
},
|
|
46
|
+
recommendation: 'Pin to specific version: FROM node:20.11.0'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'DOCKER-003',
|
|
50
|
+
title: 'Missing Health Check',
|
|
51
|
+
description: 'No HEALTHCHECK instruction defined',
|
|
52
|
+
severity: 'low',
|
|
53
|
+
category: 'reliability',
|
|
54
|
+
check: (_instruction, _value) => {
|
|
55
|
+
// This would be checked at the Dockerfile level, not per instruction
|
|
56
|
+
return false;
|
|
57
|
+
},
|
|
58
|
+
recommendation: 'Add HEALTHCHECK instruction'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'DOCKER-004',
|
|
62
|
+
title: 'Secrets in ENV',
|
|
63
|
+
description: 'Potential secrets in ENV instruction',
|
|
64
|
+
severity: 'critical',
|
|
65
|
+
category: 'secrets',
|
|
66
|
+
check: (_instruction, _value) => {
|
|
67
|
+
if (_instruction !== 'ENV') return false;
|
|
68
|
+
const secretKeywords = ['password', 'secret', 'key', 'token', 'api_key'];
|
|
69
|
+
return secretKeywords.some(keyword => _value.toLowerCase().includes(keyword));
|
|
70
|
+
},
|
|
71
|
+
recommendation: 'Use build-time secrets or runtime secret management'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'DOCKER-005',
|
|
75
|
+
title: 'ADD Instead of COPY',
|
|
76
|
+
description: 'Using ADD instead of COPY',
|
|
77
|
+
severity: 'low',
|
|
78
|
+
category: 'best-practices',
|
|
79
|
+
check: (_instruction, _value) => {
|
|
80
|
+
return _instruction === 'ADD';
|
|
81
|
+
},
|
|
82
|
+
recommendation: 'Use COPY instead of ADD unless you need tar extraction or URL fetching'
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'DOCKER-006',
|
|
86
|
+
title: 'Missing Multi-Stage Build',
|
|
87
|
+
description: 'Not using multi-stage builds',
|
|
88
|
+
severity: 'low',
|
|
89
|
+
category: 'optimization',
|
|
90
|
+
check: (_instruction, _value) => {
|
|
91
|
+
// Would check if multiple FROM instructions exist
|
|
92
|
+
return false;
|
|
93
|
+
},
|
|
94
|
+
recommendation: 'Use multi-stage builds to reduce image size'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'DOCKER-007',
|
|
98
|
+
title: 'Exposing Privileged Ports',
|
|
99
|
+
description: 'Exposing ports below 1024',
|
|
100
|
+
severity: 'medium',
|
|
101
|
+
category: 'security',
|
|
102
|
+
check: (_instruction, _value) => {
|
|
103
|
+
if (_instruction !== 'EXPOSE' || !_value) return false;
|
|
104
|
+
const port = parseInt(_value.split('/')[0] || '', 10);
|
|
105
|
+
return port < 1024;
|
|
106
|
+
},
|
|
107
|
+
recommendation: 'Use ports above 1024 to avoid running as root'
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Kubernetes Security Rules
|
|
113
|
+
*/
|
|
114
|
+
export const K8S_RULES: K8sRule[] = [
|
|
115
|
+
{
|
|
116
|
+
id: 'K8S-SEC-001',
|
|
117
|
+
title: 'Privileged Container',
|
|
118
|
+
description: 'Container running in privileged mode',
|
|
119
|
+
severity: 'critical',
|
|
120
|
+
category: 'pod-security',
|
|
121
|
+
check: (resource) => {
|
|
122
|
+
const containers = resource.spec?.containers || [];
|
|
123
|
+
return containers.some((c: any) => c.securityContext?.privileged === true);
|
|
124
|
+
},
|
|
125
|
+
recommendation: 'Remove privileged: true from securityContext'
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'K8S-SEC-002',
|
|
129
|
+
title: 'Host Network',
|
|
130
|
+
description: 'Pod has access to host network',
|
|
131
|
+
severity: 'high',
|
|
132
|
+
category: 'pod-security',
|
|
133
|
+
check: (resource) => {
|
|
134
|
+
return resource.spec?.hostNetwork === true;
|
|
135
|
+
},
|
|
136
|
+
recommendation: 'Set hostNetwork: false or remove the field'
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'K8S-SEC-003',
|
|
140
|
+
title: 'Host PID Namespace',
|
|
141
|
+
description: 'Pod has access to host PID namespace',
|
|
142
|
+
severity: 'high',
|
|
143
|
+
category: 'pod-security',
|
|
144
|
+
check: (resource) => {
|
|
145
|
+
return resource.spec?.hostPID === true;
|
|
146
|
+
},
|
|
147
|
+
recommendation: 'Set hostPID: false or remove the field'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'K8S-SEC-004',
|
|
151
|
+
title: 'Host IPC Namespace',
|
|
152
|
+
description: 'Pod has access to host IPC namespace',
|
|
153
|
+
severity: 'high',
|
|
154
|
+
category: 'pod-security',
|
|
155
|
+
check: (resource) => {
|
|
156
|
+
return resource.spec?.hostIPC === true;
|
|
157
|
+
},
|
|
158
|
+
recommendation: 'Set hostIPC: false or remove the field'
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'K8S-SEC-005',
|
|
162
|
+
title: 'Root User',
|
|
163
|
+
description: 'Container running as root',
|
|
164
|
+
severity: 'high',
|
|
165
|
+
category: 'pod-security',
|
|
166
|
+
check: (resource) => {
|
|
167
|
+
const containers = resource.spec?.containers || [];
|
|
168
|
+
return containers.some((c: any) =>
|
|
169
|
+
!c.securityContext?.runAsNonRoot && c.securityContext?.runAsUser === 0
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
recommendation: 'Set runAsNonRoot: true in securityContext'
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'K8S-SEC-006',
|
|
176
|
+
title: 'Dangerous Capabilities',
|
|
177
|
+
description: 'Container has dangerous Linux capabilities',
|
|
178
|
+
severity: 'high',
|
|
179
|
+
category: 'pod-security',
|
|
180
|
+
check: (resource) => {
|
|
181
|
+
const containers = resource.spec?.containers || [];
|
|
182
|
+
const dangerous = ['SYS_ADMIN', 'NET_ADMIN', 'SYS_MODULE'];
|
|
183
|
+
return containers.some((c: any) =>
|
|
184
|
+
c.securityContext?.capabilities?.add?.some((cap: string) =>
|
|
185
|
+
dangerous.includes(cap)
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
recommendation: 'Remove dangerous capabilities or use allowPrivilegeEscalation: false'
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: 'K8S-SEC-007',
|
|
193
|
+
title: 'Read-Only Root Filesystem',
|
|
194
|
+
description: 'Root filesystem is not read-only',
|
|
195
|
+
severity: 'medium',
|
|
196
|
+
category: 'pod-security',
|
|
197
|
+
check: (resource) => {
|
|
198
|
+
const containers = resource.spec?.containers || [];
|
|
199
|
+
return containers.some((c: any) =>
|
|
200
|
+
c.securityContext?.readOnlyRootFilesystem !== true
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
recommendation: 'Set readOnlyRootFilesystem: true in securityContext'
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'K8S-RES-001',
|
|
207
|
+
title: 'Missing Resource Limits',
|
|
208
|
+
description: 'Container missing CPU/memory limits',
|
|
209
|
+
severity: 'medium',
|
|
210
|
+
category: 'resource-management',
|
|
211
|
+
check: (resource) => {
|
|
212
|
+
const containers = resource.spec?.containers || [];
|
|
213
|
+
return containers.some((c: any) =>
|
|
214
|
+
!c.resources?.limits || !c.resources?.limits?.cpu || !c.resources?.limits?.memory
|
|
215
|
+
);
|
|
216
|
+
},
|
|
217
|
+
recommendation: 'Set resources.limits.cpu and resources.limits.memory'
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'K8S-RES-002',
|
|
221
|
+
title: 'Missing Resource Requests',
|
|
222
|
+
description: 'Container missing CPU/memory requests',
|
|
223
|
+
severity: 'low',
|
|
224
|
+
category: 'resource-management',
|
|
225
|
+
check: (resource) => {
|
|
226
|
+
const containers = resource.spec?.containers || [];
|
|
227
|
+
return containers.some((c: any) =>
|
|
228
|
+
!c.resources?.requests || !c.resources?.requests?.cpu || !c.resources?.requests?.memory
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
recommendation: 'Set resources.requests.cpu and resources.requests.memory'
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: 'K8S-SA-001',
|
|
235
|
+
title: 'Service Account Token Auto-Mount',
|
|
236
|
+
description: 'Service account token auto-mounted',
|
|
237
|
+
severity: 'medium',
|
|
238
|
+
category: 'pod-security',
|
|
239
|
+
check: (resource) => {
|
|
240
|
+
return resource.spec?.automountServiceAccountToken !== false;
|
|
241
|
+
},
|
|
242
|
+
recommendation: 'Set automountServiceAccountToken: false unless needed'
|
|
243
|
+
}
|
|
244
|
+
];
|