security-use 0.1.1__py3-none-any.whl → 0.2.9__py3-none-any.whl
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.
- security_use/__init__.py +9 -1
- security_use/auth/__init__.py +16 -0
- security_use/auth/client.py +223 -0
- security_use/auth/config.py +177 -0
- security_use/auth/oauth.py +317 -0
- security_use/cli.py +699 -34
- security_use/compliance/__init__.py +10 -0
- security_use/compliance/mapper.py +275 -0
- security_use/compliance/models.py +50 -0
- security_use/dependency_scanner.py +76 -30
- security_use/fixers/iac_fixer.py +173 -95
- security_use/iac/rules/azure.py +246 -0
- security_use/iac/rules/gcp.py +255 -0
- security_use/iac/rules/kubernetes.py +429 -0
- security_use/iac/rules/registry.py +56 -0
- security_use/parsers/__init__.py +18 -0
- security_use/parsers/base.py +2 -0
- security_use/parsers/composer.py +101 -0
- security_use/parsers/conda.py +97 -0
- security_use/parsers/dotnet.py +89 -0
- security_use/parsers/gradle.py +90 -0
- security_use/parsers/maven.py +108 -0
- security_use/parsers/npm.py +196 -0
- security_use/parsers/yarn.py +108 -0
- security_use/reporter.py +29 -1
- security_use/sbom/__init__.py +10 -0
- security_use/sbom/generator.py +340 -0
- security_use/sbom/models.py +40 -0
- security_use/scanner.py +15 -2
- security_use/sensor/__init__.py +125 -0
- security_use/sensor/alert_queue.py +207 -0
- security_use/sensor/config.py +217 -0
- security_use/sensor/dashboard_alerter.py +246 -0
- security_use/sensor/detector.py +415 -0
- security_use/sensor/endpoint_analyzer.py +339 -0
- security_use/sensor/middleware.py +521 -0
- security_use/sensor/models.py +140 -0
- security_use/sensor/webhook.py +227 -0
- security_use-0.2.9.dist-info/METADATA +531 -0
- security_use-0.2.9.dist-info/RECORD +60 -0
- security_use-0.2.9.dist-info/licenses/LICENSE +21 -0
- security_use-0.1.1.dist-info/METADATA +0 -92
- security_use-0.1.1.dist-info/RECORD +0 -30
- {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/WHEEL +0 -0
- {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""GCP security rules for IaC scanning."""
|
|
2
|
+
|
|
3
|
+
from security_use.models import Severity
|
|
4
|
+
from security_use.iac.base import IaCResource
|
|
5
|
+
from security_use.iac.rules.base import Rule, RuleResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GCSBucketPublicAccessRule(Rule):
|
|
9
|
+
"""Check that GCS buckets do not allow public access."""
|
|
10
|
+
|
|
11
|
+
RULE_ID = "CKV_GCP_5"
|
|
12
|
+
TITLE = "Cloud Storage bucket with public access"
|
|
13
|
+
SEVERITY = Severity.CRITICAL
|
|
14
|
+
DESCRIPTION = (
|
|
15
|
+
"Cloud Storage bucket is publicly accessible. This can expose "
|
|
16
|
+
"sensitive data to unauthorized users."
|
|
17
|
+
)
|
|
18
|
+
REMEDIATION = (
|
|
19
|
+
"Remove allUsers and allAuthenticatedUsers from bucket IAM bindings. "
|
|
20
|
+
"Use uniform bucket-level access."
|
|
21
|
+
)
|
|
22
|
+
RESOURCE_TYPES = [
|
|
23
|
+
"google_storage_bucket",
|
|
24
|
+
"google_storage_bucket_iam_binding",
|
|
25
|
+
"google_storage_bucket_iam_member",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
29
|
+
"""Check if GCS bucket allows public access."""
|
|
30
|
+
has_public_access = False
|
|
31
|
+
|
|
32
|
+
# Check IAM bindings
|
|
33
|
+
members = resource.get_config("members", default=[])
|
|
34
|
+
member = resource.get_config("member", default="")
|
|
35
|
+
|
|
36
|
+
public_principals = ["allUsers", "allAuthenticatedUsers"]
|
|
37
|
+
|
|
38
|
+
if any(m in members for m in public_principals):
|
|
39
|
+
has_public_access = True
|
|
40
|
+
|
|
41
|
+
if member in public_principals:
|
|
42
|
+
has_public_access = True
|
|
43
|
+
|
|
44
|
+
fix_code = None
|
|
45
|
+
if has_public_access:
|
|
46
|
+
fix_code = "# Remove allUsers and allAuthenticatedUsers from members"
|
|
47
|
+
|
|
48
|
+
return self._create_result(not has_public_access, resource, fix_code)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GCSBucketEncryptionRule(Rule):
|
|
52
|
+
"""Check that GCS buckets have encryption configured."""
|
|
53
|
+
|
|
54
|
+
RULE_ID = "CKV_GCP_6"
|
|
55
|
+
TITLE = "Cloud Storage bucket without customer-managed encryption"
|
|
56
|
+
SEVERITY = Severity.HIGH
|
|
57
|
+
DESCRIPTION = (
|
|
58
|
+
"Cloud Storage bucket does not use customer-managed encryption keys (CMEK). "
|
|
59
|
+
"While GCS encrypts data by default, CMEK provides additional control."
|
|
60
|
+
)
|
|
61
|
+
REMEDIATION = (
|
|
62
|
+
"Configure a Cloud KMS key for bucket encryption."
|
|
63
|
+
)
|
|
64
|
+
RESOURCE_TYPES = ["google_storage_bucket"]
|
|
65
|
+
|
|
66
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
67
|
+
"""Check if GCS bucket uses CMEK."""
|
|
68
|
+
encryption = resource.get_config("encryption", default={})
|
|
69
|
+
default_kms_key = encryption.get("default_kms_key_name")
|
|
70
|
+
|
|
71
|
+
has_cmek = bool(default_kms_key)
|
|
72
|
+
|
|
73
|
+
fix_code = None
|
|
74
|
+
if not has_cmek:
|
|
75
|
+
fix_code = '''encryption {
|
|
76
|
+
default_kms_key_name = "projects/PROJECT/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY"
|
|
77
|
+
}'''
|
|
78
|
+
|
|
79
|
+
return self._create_result(has_cmek, resource, fix_code)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class GCPFirewallOpenIngressRule(Rule):
|
|
83
|
+
"""Check that GCP firewall rules don't allow unrestricted ingress."""
|
|
84
|
+
|
|
85
|
+
RULE_ID = "CKV_GCP_2"
|
|
86
|
+
TITLE = "Firewall rule allows unrestricted ingress"
|
|
87
|
+
SEVERITY = Severity.HIGH
|
|
88
|
+
DESCRIPTION = (
|
|
89
|
+
"Firewall rule allows ingress from 0.0.0.0/0 on sensitive ports. "
|
|
90
|
+
"This exposes services to the entire internet."
|
|
91
|
+
)
|
|
92
|
+
REMEDIATION = (
|
|
93
|
+
"Restrict source_ranges to specific IP ranges. "
|
|
94
|
+
"Avoid using 0.0.0.0/0 as the source."
|
|
95
|
+
)
|
|
96
|
+
RESOURCE_TYPES = ["google_compute_firewall"]
|
|
97
|
+
|
|
98
|
+
SENSITIVE_PORTS = ["22", "3389", "3306", "5432", "1433", "27017", "6379"]
|
|
99
|
+
|
|
100
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
101
|
+
"""Check if firewall rule has open ingress on sensitive ports."""
|
|
102
|
+
has_open_ingress = False
|
|
103
|
+
|
|
104
|
+
direction = resource.get_config("direction", default="INGRESS")
|
|
105
|
+
if direction.upper() != "INGRESS":
|
|
106
|
+
return self._create_result(True, resource)
|
|
107
|
+
|
|
108
|
+
source_ranges = resource.get_config("source_ranges", default=[])
|
|
109
|
+
|
|
110
|
+
if "0.0.0.0/0" not in source_ranges:
|
|
111
|
+
return self._create_result(True, resource)
|
|
112
|
+
|
|
113
|
+
# Check if sensitive ports are exposed
|
|
114
|
+
allow_rules = resource.get_config("allow", default=[])
|
|
115
|
+
if isinstance(allow_rules, list):
|
|
116
|
+
for rule in allow_rules:
|
|
117
|
+
ports = rule.get("ports", [])
|
|
118
|
+
if not ports:
|
|
119
|
+
# No port restriction means all ports
|
|
120
|
+
has_open_ingress = True
|
|
121
|
+
break
|
|
122
|
+
for port in ports:
|
|
123
|
+
if port in self.SENSITIVE_PORTS or "-" in str(port):
|
|
124
|
+
has_open_ingress = True
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
fix_code = None
|
|
128
|
+
if has_open_ingress:
|
|
129
|
+
fix_code = '# Restrict source_ranges to specific IP ranges instead of ["0.0.0.0/0"]'
|
|
130
|
+
|
|
131
|
+
return self._create_result(not has_open_ingress, resource, fix_code)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class GCPCloudSQLEncryptionRule(Rule):
|
|
135
|
+
"""Check that Cloud SQL instances have encryption enabled."""
|
|
136
|
+
|
|
137
|
+
RULE_ID = "CKV_GCP_14"
|
|
138
|
+
TITLE = "Cloud SQL instance without customer-managed encryption"
|
|
139
|
+
SEVERITY = Severity.HIGH
|
|
140
|
+
DESCRIPTION = (
|
|
141
|
+
"Cloud SQL instance does not use customer-managed encryption keys (CMEK). "
|
|
142
|
+
"While Cloud SQL encrypts data by default, CMEK provides additional control."
|
|
143
|
+
)
|
|
144
|
+
REMEDIATION = (
|
|
145
|
+
"Configure a Cloud KMS key for Cloud SQL encryption."
|
|
146
|
+
)
|
|
147
|
+
RESOURCE_TYPES = ["google_sql_database_instance"]
|
|
148
|
+
|
|
149
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
150
|
+
"""Check if Cloud SQL uses CMEK."""
|
|
151
|
+
settings = resource.get_config("settings", default={})
|
|
152
|
+
ip_config = settings.get("ip_configuration", {})
|
|
153
|
+
|
|
154
|
+
# Check for encryption key
|
|
155
|
+
encryption_key = resource.get_config("encryption_key_name")
|
|
156
|
+
|
|
157
|
+
has_cmek = bool(encryption_key)
|
|
158
|
+
|
|
159
|
+
fix_code = None
|
|
160
|
+
if not has_cmek:
|
|
161
|
+
fix_code = 'encryption_key_name = "projects/PROJECT/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY"'
|
|
162
|
+
|
|
163
|
+
return self._create_result(has_cmek, resource, fix_code)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class GCPKMSKeyRotationRule(Rule):
|
|
167
|
+
"""Check that Cloud KMS keys have rotation configured."""
|
|
168
|
+
|
|
169
|
+
RULE_ID = "CKV_GCP_43"
|
|
170
|
+
TITLE = "KMS key without rotation"
|
|
171
|
+
SEVERITY = Severity.MEDIUM
|
|
172
|
+
DESCRIPTION = (
|
|
173
|
+
"Cloud KMS key does not have automatic rotation configured. "
|
|
174
|
+
"Regular key rotation limits the impact of key compromise."
|
|
175
|
+
)
|
|
176
|
+
REMEDIATION = (
|
|
177
|
+
"Configure automatic key rotation with a rotation period of 90 days or less."
|
|
178
|
+
)
|
|
179
|
+
RESOURCE_TYPES = ["google_kms_crypto_key"]
|
|
180
|
+
|
|
181
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
182
|
+
"""Check if KMS key has rotation configured."""
|
|
183
|
+
rotation_period = resource.get_config("rotation_period")
|
|
184
|
+
|
|
185
|
+
has_rotation = bool(rotation_period)
|
|
186
|
+
|
|
187
|
+
fix_code = None
|
|
188
|
+
if not has_rotation:
|
|
189
|
+
fix_code = 'rotation_period = "7776000s" # 90 days'
|
|
190
|
+
|
|
191
|
+
return self._create_result(has_rotation, resource, fix_code)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class GCPServiceAccountKeyRule(Rule):
|
|
195
|
+
"""Check that service account keys are managed properly."""
|
|
196
|
+
|
|
197
|
+
RULE_ID = "CKV_GCP_41"
|
|
198
|
+
TITLE = "Service account with user-managed keys"
|
|
199
|
+
SEVERITY = Severity.MEDIUM
|
|
200
|
+
DESCRIPTION = (
|
|
201
|
+
"Service account has user-managed keys. User-managed keys are a security "
|
|
202
|
+
"risk as they can be leaked or stolen. Prefer using attached service accounts."
|
|
203
|
+
)
|
|
204
|
+
REMEDIATION = (
|
|
205
|
+
"Use attached service accounts or workload identity instead of user-managed keys."
|
|
206
|
+
)
|
|
207
|
+
RESOURCE_TYPES = ["google_service_account_key"]
|
|
208
|
+
|
|
209
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
210
|
+
"""Flag user-managed service account keys."""
|
|
211
|
+
# Any google_service_account_key resource is a user-managed key
|
|
212
|
+
fix_code = "# Remove user-managed keys and use workload identity or attached service accounts"
|
|
213
|
+
|
|
214
|
+
return self._create_result(False, resource, fix_code)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class GCPAuditLoggingRule(Rule):
|
|
218
|
+
"""Check that audit logging is enabled for GCP projects."""
|
|
219
|
+
|
|
220
|
+
RULE_ID = "CKV_GCP_32"
|
|
221
|
+
TITLE = "Audit logging not enabled"
|
|
222
|
+
SEVERITY = Severity.MEDIUM
|
|
223
|
+
DESCRIPTION = (
|
|
224
|
+
"Audit logging is not enabled for all services. "
|
|
225
|
+
"Audit logs are essential for security monitoring and compliance."
|
|
226
|
+
)
|
|
227
|
+
REMEDIATION = (
|
|
228
|
+
"Enable audit logging for all services using google_project_iam_audit_config."
|
|
229
|
+
)
|
|
230
|
+
RESOURCE_TYPES = ["google_project_iam_audit_config"]
|
|
231
|
+
|
|
232
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
233
|
+
"""Check if comprehensive audit logging is configured."""
|
|
234
|
+
service = resource.get_config("service", default="")
|
|
235
|
+
audit_log_configs = resource.get_config("audit_log_config", default=[])
|
|
236
|
+
|
|
237
|
+
# Check if auditing all services
|
|
238
|
+
if service == "allServices":
|
|
239
|
+
if isinstance(audit_log_configs, list) and len(audit_log_configs) > 0:
|
|
240
|
+
return self._create_result(True, resource)
|
|
241
|
+
|
|
242
|
+
fix_code = '''resource "google_project_iam_audit_config" "all" {
|
|
243
|
+
service = "allServices"
|
|
244
|
+
audit_log_config {
|
|
245
|
+
log_type = "ADMIN_READ"
|
|
246
|
+
}
|
|
247
|
+
audit_log_config {
|
|
248
|
+
log_type = "DATA_READ"
|
|
249
|
+
}
|
|
250
|
+
audit_log_config {
|
|
251
|
+
log_type = "DATA_WRITE"
|
|
252
|
+
}
|
|
253
|
+
}'''
|
|
254
|
+
|
|
255
|
+
return self._create_result(False, resource, fix_code)
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"""Kubernetes security rules for IaC scanning."""
|
|
2
|
+
|
|
3
|
+
from security_use.models import Severity
|
|
4
|
+
from security_use.iac.base import IaCResource
|
|
5
|
+
from security_use.iac.rules.base import Rule, RuleResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class K8sRunAsRootRule(Rule):
|
|
9
|
+
"""Check that containers don't run as root."""
|
|
10
|
+
|
|
11
|
+
RULE_ID = "CKV_K8S_6"
|
|
12
|
+
TITLE = "Container running as root"
|
|
13
|
+
SEVERITY = Severity.HIGH
|
|
14
|
+
DESCRIPTION = (
|
|
15
|
+
"Container is configured to run as root user. Running as root "
|
|
16
|
+
"increases the risk of container breakout and privilege escalation."
|
|
17
|
+
)
|
|
18
|
+
REMEDIATION = (
|
|
19
|
+
"Set securityContext.runAsNonRoot to true and specify a non-root runAsUser."
|
|
20
|
+
)
|
|
21
|
+
RESOURCE_TYPES = [
|
|
22
|
+
"kubernetes_pod",
|
|
23
|
+
"kubernetes_deployment",
|
|
24
|
+
"kubernetes_stateful_set",
|
|
25
|
+
"kubernetes_daemon_set",
|
|
26
|
+
"kubernetes_job",
|
|
27
|
+
"kubernetes_cron_job",
|
|
28
|
+
"Pod",
|
|
29
|
+
"Deployment",
|
|
30
|
+
"StatefulSet",
|
|
31
|
+
"DaemonSet",
|
|
32
|
+
"Job",
|
|
33
|
+
"CronJob",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
37
|
+
"""Check if container runs as root."""
|
|
38
|
+
runs_as_root = False
|
|
39
|
+
|
|
40
|
+
# Get pod spec - handle different resource types
|
|
41
|
+
spec = self._get_pod_spec(resource)
|
|
42
|
+
if not spec:
|
|
43
|
+
return self._create_result(True, resource)
|
|
44
|
+
|
|
45
|
+
# Check pod-level security context
|
|
46
|
+
pod_security = spec.get("securityContext", {})
|
|
47
|
+
run_as_non_root = pod_security.get("runAsNonRoot", False)
|
|
48
|
+
run_as_user = pod_security.get("runAsUser")
|
|
49
|
+
|
|
50
|
+
# If pod-level says non-root, we're good
|
|
51
|
+
if run_as_non_root or (run_as_user is not None and run_as_user != 0):
|
|
52
|
+
return self._create_result(True, resource)
|
|
53
|
+
|
|
54
|
+
# Check container-level security context
|
|
55
|
+
containers = spec.get("containers", [])
|
|
56
|
+
for container in containers:
|
|
57
|
+
container_security = container.get("securityContext", {})
|
|
58
|
+
container_run_as_user = container_security.get("runAsUser")
|
|
59
|
+
container_run_as_non_root = container_security.get("runAsNonRoot", False)
|
|
60
|
+
|
|
61
|
+
if container_run_as_user == 0:
|
|
62
|
+
runs_as_root = True
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
if not container_run_as_non_root and run_as_user is None and container_run_as_user is None:
|
|
66
|
+
runs_as_root = True
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
fix_code = None
|
|
70
|
+
if runs_as_root:
|
|
71
|
+
fix_code = '''securityContext:
|
|
72
|
+
runAsNonRoot: true
|
|
73
|
+
runAsUser: 1000'''
|
|
74
|
+
|
|
75
|
+
return self._create_result(not runs_as_root, resource, fix_code)
|
|
76
|
+
|
|
77
|
+
def _get_pod_spec(self, resource: IaCResource) -> dict:
|
|
78
|
+
"""Extract pod spec from various resource types."""
|
|
79
|
+
config = resource.config
|
|
80
|
+
|
|
81
|
+
# Direct pod
|
|
82
|
+
if "spec" in config and "containers" in config.get("spec", {}):
|
|
83
|
+
return config["spec"]
|
|
84
|
+
|
|
85
|
+
# Deployment/StatefulSet/etc with template
|
|
86
|
+
spec = config.get("spec", {})
|
|
87
|
+
template = spec.get("template", {})
|
|
88
|
+
if "spec" in template:
|
|
89
|
+
return template["spec"]
|
|
90
|
+
|
|
91
|
+
# Terraform kubernetes_pod
|
|
92
|
+
pod_spec = config.get("spec", [{}])
|
|
93
|
+
if isinstance(pod_spec, list) and pod_spec:
|
|
94
|
+
return pod_spec[0]
|
|
95
|
+
|
|
96
|
+
return {}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class K8sPrivilegedContainerRule(Rule):
|
|
100
|
+
"""Check that containers don't run in privileged mode."""
|
|
101
|
+
|
|
102
|
+
RULE_ID = "CKV_K8S_1"
|
|
103
|
+
TITLE = "Privileged container"
|
|
104
|
+
SEVERITY = Severity.CRITICAL
|
|
105
|
+
DESCRIPTION = (
|
|
106
|
+
"Container is running in privileged mode. Privileged containers have "
|
|
107
|
+
"full access to the host and can escape container isolation."
|
|
108
|
+
)
|
|
109
|
+
REMEDIATION = (
|
|
110
|
+
"Set securityContext.privileged to false."
|
|
111
|
+
)
|
|
112
|
+
RESOURCE_TYPES = [
|
|
113
|
+
"kubernetes_pod",
|
|
114
|
+
"kubernetes_deployment",
|
|
115
|
+
"Pod",
|
|
116
|
+
"Deployment",
|
|
117
|
+
"StatefulSet",
|
|
118
|
+
"DaemonSet",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
122
|
+
"""Check if container runs privileged."""
|
|
123
|
+
is_privileged = False
|
|
124
|
+
|
|
125
|
+
spec = self._get_pod_spec(resource)
|
|
126
|
+
if not spec:
|
|
127
|
+
return self._create_result(True, resource)
|
|
128
|
+
|
|
129
|
+
containers = spec.get("containers", [])
|
|
130
|
+
for container in containers:
|
|
131
|
+
security = container.get("securityContext", {})
|
|
132
|
+
if security.get("privileged", False):
|
|
133
|
+
is_privileged = True
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
fix_code = None
|
|
137
|
+
if is_privileged:
|
|
138
|
+
fix_code = '''securityContext:
|
|
139
|
+
privileged: false'''
|
|
140
|
+
|
|
141
|
+
return self._create_result(not is_privileged, resource, fix_code)
|
|
142
|
+
|
|
143
|
+
def _get_pod_spec(self, resource: IaCResource) -> dict:
|
|
144
|
+
"""Extract pod spec from various resource types."""
|
|
145
|
+
config = resource.config
|
|
146
|
+
if "spec" in config and "containers" in config.get("spec", {}):
|
|
147
|
+
return config["spec"]
|
|
148
|
+
spec = config.get("spec", {})
|
|
149
|
+
template = spec.get("template", {})
|
|
150
|
+
if "spec" in template:
|
|
151
|
+
return template["spec"]
|
|
152
|
+
return {}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class K8sResourceLimitsRule(Rule):
|
|
156
|
+
"""Check that containers have resource limits defined."""
|
|
157
|
+
|
|
158
|
+
RULE_ID = "CKV_K8S_11"
|
|
159
|
+
TITLE = "Container without resource limits"
|
|
160
|
+
SEVERITY = Severity.MEDIUM
|
|
161
|
+
DESCRIPTION = (
|
|
162
|
+
"Container does not have resource limits defined. Without limits, "
|
|
163
|
+
"a container can consume all available resources on the node."
|
|
164
|
+
)
|
|
165
|
+
REMEDIATION = (
|
|
166
|
+
"Define resources.limits for CPU and memory."
|
|
167
|
+
)
|
|
168
|
+
RESOURCE_TYPES = [
|
|
169
|
+
"kubernetes_pod",
|
|
170
|
+
"kubernetes_deployment",
|
|
171
|
+
"Pod",
|
|
172
|
+
"Deployment",
|
|
173
|
+
"StatefulSet",
|
|
174
|
+
"DaemonSet",
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
178
|
+
"""Check if container has resource limits."""
|
|
179
|
+
has_limits = True
|
|
180
|
+
|
|
181
|
+
spec = self._get_pod_spec(resource)
|
|
182
|
+
if not spec:
|
|
183
|
+
return self._create_result(True, resource)
|
|
184
|
+
|
|
185
|
+
containers = spec.get("containers", [])
|
|
186
|
+
for container in containers:
|
|
187
|
+
resources = container.get("resources", {})
|
|
188
|
+
limits = resources.get("limits", {})
|
|
189
|
+
|
|
190
|
+
if not limits.get("cpu") or not limits.get("memory"):
|
|
191
|
+
has_limits = False
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
fix_code = None
|
|
195
|
+
if not has_limits:
|
|
196
|
+
fix_code = '''resources:
|
|
197
|
+
limits:
|
|
198
|
+
cpu: "500m"
|
|
199
|
+
memory: "512Mi"
|
|
200
|
+
requests:
|
|
201
|
+
cpu: "100m"
|
|
202
|
+
memory: "128Mi"'''
|
|
203
|
+
|
|
204
|
+
return self._create_result(has_limits, resource, fix_code)
|
|
205
|
+
|
|
206
|
+
def _get_pod_spec(self, resource: IaCResource) -> dict:
|
|
207
|
+
"""Extract pod spec from various resource types."""
|
|
208
|
+
config = resource.config
|
|
209
|
+
if "spec" in config and "containers" in config.get("spec", {}):
|
|
210
|
+
return config["spec"]
|
|
211
|
+
spec = config.get("spec", {})
|
|
212
|
+
template = spec.get("template", {})
|
|
213
|
+
if "spec" in template:
|
|
214
|
+
return template["spec"]
|
|
215
|
+
return {}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class K8sHostNetworkRule(Rule):
|
|
219
|
+
"""Check that pods don't use host network namespace."""
|
|
220
|
+
|
|
221
|
+
RULE_ID = "CKV_K8S_19"
|
|
222
|
+
TITLE = "Pod using host network"
|
|
223
|
+
SEVERITY = Severity.HIGH
|
|
224
|
+
DESCRIPTION = (
|
|
225
|
+
"Pod is configured to use the host network namespace. This allows "
|
|
226
|
+
"the container to access all network interfaces on the host."
|
|
227
|
+
)
|
|
228
|
+
REMEDIATION = (
|
|
229
|
+
"Set hostNetwork to false unless absolutely necessary."
|
|
230
|
+
)
|
|
231
|
+
RESOURCE_TYPES = [
|
|
232
|
+
"kubernetes_pod",
|
|
233
|
+
"kubernetes_deployment",
|
|
234
|
+
"Pod",
|
|
235
|
+
"Deployment",
|
|
236
|
+
"StatefulSet",
|
|
237
|
+
"DaemonSet",
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
241
|
+
"""Check if pod uses host network."""
|
|
242
|
+
spec = self._get_pod_spec(resource)
|
|
243
|
+
if not spec:
|
|
244
|
+
return self._create_result(True, resource)
|
|
245
|
+
|
|
246
|
+
host_network = spec.get("hostNetwork", False)
|
|
247
|
+
|
|
248
|
+
fix_code = None
|
|
249
|
+
if host_network:
|
|
250
|
+
fix_code = "hostNetwork: false"
|
|
251
|
+
|
|
252
|
+
return self._create_result(not host_network, resource, fix_code)
|
|
253
|
+
|
|
254
|
+
def _get_pod_spec(self, resource: IaCResource) -> dict:
|
|
255
|
+
"""Extract pod spec from various resource types."""
|
|
256
|
+
config = resource.config
|
|
257
|
+
if "spec" in config and "containers" in config.get("spec", {}):
|
|
258
|
+
return config["spec"]
|
|
259
|
+
spec = config.get("spec", {})
|
|
260
|
+
template = spec.get("template", {})
|
|
261
|
+
if "spec" in template:
|
|
262
|
+
return template["spec"]
|
|
263
|
+
return {}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class K8sSecretsEnvVarsRule(Rule):
|
|
267
|
+
"""Check that secrets are not exposed as environment variables."""
|
|
268
|
+
|
|
269
|
+
RULE_ID = "CKV_K8S_35"
|
|
270
|
+
TITLE = "Secrets exposed as environment variables"
|
|
271
|
+
SEVERITY = Severity.MEDIUM
|
|
272
|
+
DESCRIPTION = (
|
|
273
|
+
"Secrets are exposed as environment variables. Environment variables "
|
|
274
|
+
"can be logged or exposed through process listings. Use volume mounts instead."
|
|
275
|
+
)
|
|
276
|
+
REMEDIATION = (
|
|
277
|
+
"Mount secrets as volumes instead of using envFrom with secretRef."
|
|
278
|
+
)
|
|
279
|
+
RESOURCE_TYPES = [
|
|
280
|
+
"kubernetes_pod",
|
|
281
|
+
"kubernetes_deployment",
|
|
282
|
+
"Pod",
|
|
283
|
+
"Deployment",
|
|
284
|
+
"StatefulSet",
|
|
285
|
+
"DaemonSet",
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
289
|
+
"""Check if secrets are exposed as env vars."""
|
|
290
|
+
secrets_in_env = False
|
|
291
|
+
|
|
292
|
+
spec = self._get_pod_spec(resource)
|
|
293
|
+
if not spec:
|
|
294
|
+
return self._create_result(True, resource)
|
|
295
|
+
|
|
296
|
+
containers = spec.get("containers", [])
|
|
297
|
+
for container in containers:
|
|
298
|
+
# Check envFrom with secretRef
|
|
299
|
+
env_from = container.get("envFrom", [])
|
|
300
|
+
for env in env_from:
|
|
301
|
+
if "secretRef" in env:
|
|
302
|
+
secrets_in_env = True
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
# Check individual env vars with secretKeyRef
|
|
306
|
+
env_vars = container.get("env", [])
|
|
307
|
+
for env in env_vars:
|
|
308
|
+
value_from = env.get("valueFrom", {})
|
|
309
|
+
if "secretKeyRef" in value_from:
|
|
310
|
+
secrets_in_env = True
|
|
311
|
+
break
|
|
312
|
+
|
|
313
|
+
if secrets_in_env:
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
fix_code = None
|
|
317
|
+
if secrets_in_env:
|
|
318
|
+
fix_code = '''# Mount secrets as volumes instead
|
|
319
|
+
volumeMounts:
|
|
320
|
+
- name: secret-volume
|
|
321
|
+
mountPath: "/etc/secrets"
|
|
322
|
+
readOnly: true
|
|
323
|
+
volumes:
|
|
324
|
+
- name: secret-volume
|
|
325
|
+
secret:
|
|
326
|
+
secretName: my-secret'''
|
|
327
|
+
|
|
328
|
+
return self._create_result(not secrets_in_env, resource, fix_code)
|
|
329
|
+
|
|
330
|
+
def _get_pod_spec(self, resource: IaCResource) -> dict:
|
|
331
|
+
"""Extract pod spec from various resource types."""
|
|
332
|
+
config = resource.config
|
|
333
|
+
if "spec" in config and "containers" in config.get("spec", {}):
|
|
334
|
+
return config["spec"]
|
|
335
|
+
spec = config.get("spec", {})
|
|
336
|
+
template = spec.get("template", {})
|
|
337
|
+
if "spec" in template:
|
|
338
|
+
return template["spec"]
|
|
339
|
+
return {}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class K8sReadOnlyRootFilesystemRule(Rule):
|
|
343
|
+
"""Check that containers use read-only root filesystem."""
|
|
344
|
+
|
|
345
|
+
RULE_ID = "CKV_K8S_22"
|
|
346
|
+
TITLE = "Container without read-only root filesystem"
|
|
347
|
+
SEVERITY = Severity.MEDIUM
|
|
348
|
+
DESCRIPTION = (
|
|
349
|
+
"Container does not have a read-only root filesystem. A read-only "
|
|
350
|
+
"filesystem prevents malicious writes to the container filesystem."
|
|
351
|
+
)
|
|
352
|
+
REMEDIATION = (
|
|
353
|
+
"Set securityContext.readOnlyRootFilesystem to true."
|
|
354
|
+
)
|
|
355
|
+
RESOURCE_TYPES = [
|
|
356
|
+
"kubernetes_pod",
|
|
357
|
+
"kubernetes_deployment",
|
|
358
|
+
"Pod",
|
|
359
|
+
"Deployment",
|
|
360
|
+
"StatefulSet",
|
|
361
|
+
"DaemonSet",
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
365
|
+
"""Check if container has read-only root filesystem."""
|
|
366
|
+
has_readonly = True
|
|
367
|
+
|
|
368
|
+
spec = self._get_pod_spec(resource)
|
|
369
|
+
if not spec:
|
|
370
|
+
return self._create_result(True, resource)
|
|
371
|
+
|
|
372
|
+
containers = spec.get("containers", [])
|
|
373
|
+
for container in containers:
|
|
374
|
+
security = container.get("securityContext", {})
|
|
375
|
+
if not security.get("readOnlyRootFilesystem", False):
|
|
376
|
+
has_readonly = False
|
|
377
|
+
break
|
|
378
|
+
|
|
379
|
+
fix_code = None
|
|
380
|
+
if not has_readonly:
|
|
381
|
+
fix_code = '''securityContext:
|
|
382
|
+
readOnlyRootFilesystem: true'''
|
|
383
|
+
|
|
384
|
+
return self._create_result(has_readonly, resource, fix_code)
|
|
385
|
+
|
|
386
|
+
def _get_pod_spec(self, resource: IaCResource) -> dict:
|
|
387
|
+
"""Extract pod spec from various resource types."""
|
|
388
|
+
config = resource.config
|
|
389
|
+
if "spec" in config and "containers" in config.get("spec", {}):
|
|
390
|
+
return config["spec"]
|
|
391
|
+
spec = config.get("spec", {})
|
|
392
|
+
template = spec.get("template", {})
|
|
393
|
+
if "spec" in template:
|
|
394
|
+
return template["spec"]
|
|
395
|
+
return {}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class K8sNetworkPolicyRule(Rule):
|
|
399
|
+
"""Check that namespaces have network policies defined."""
|
|
400
|
+
|
|
401
|
+
RULE_ID = "CKV_K8S_24"
|
|
402
|
+
TITLE = "Missing network policy"
|
|
403
|
+
SEVERITY = Severity.MEDIUM
|
|
404
|
+
DESCRIPTION = (
|
|
405
|
+
"No network policy is defined for this namespace. Without network "
|
|
406
|
+
"policies, all pods can communicate with each other by default."
|
|
407
|
+
)
|
|
408
|
+
REMEDIATION = (
|
|
409
|
+
"Define NetworkPolicy resources to restrict pod-to-pod communication."
|
|
410
|
+
)
|
|
411
|
+
RESOURCE_TYPES = ["kubernetes_namespace", "Namespace"]
|
|
412
|
+
|
|
413
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
414
|
+
"""Flag namespaces that should have network policies."""
|
|
415
|
+
# This is a best-effort check - we can't verify NetworkPolicy exists
|
|
416
|
+
# from the namespace resource alone
|
|
417
|
+
|
|
418
|
+
fix_code = '''apiVersion: networking.k8s.io/v1
|
|
419
|
+
kind: NetworkPolicy
|
|
420
|
+
metadata:
|
|
421
|
+
name: default-deny-all
|
|
422
|
+
spec:
|
|
423
|
+
podSelector: {}
|
|
424
|
+
policyTypes:
|
|
425
|
+
- Ingress
|
|
426
|
+
- Egress'''
|
|
427
|
+
|
|
428
|
+
# Default to warning to encourage network policies
|
|
429
|
+
return self._create_result(False, resource, fix_code)
|