security-use 0.1.1__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 +15 -0
- security_use/cli.py +348 -0
- security_use/dependency_scanner.py +199 -0
- security_use/fixers/__init__.py +6 -0
- security_use/fixers/dependency_fixer.py +196 -0
- security_use/fixers/iac_fixer.py +191 -0
- security_use/iac/__init__.py +9 -0
- security_use/iac/base.py +69 -0
- security_use/iac/cloudformation.py +207 -0
- security_use/iac/rules/__init__.py +29 -0
- security_use/iac/rules/aws.py +338 -0
- security_use/iac/rules/base.py +96 -0
- security_use/iac/rules/registry.py +115 -0
- security_use/iac/terraform.py +177 -0
- security_use/iac_scanner.py +215 -0
- security_use/models.py +139 -0
- security_use/osv_client.py +386 -0
- security_use/parsers/__init__.py +16 -0
- security_use/parsers/base.py +43 -0
- security_use/parsers/pipfile.py +133 -0
- security_use/parsers/poetry_lock.py +42 -0
- security_use/parsers/pyproject.py +178 -0
- security_use/parsers/requirements.py +86 -0
- security_use/py.typed +0 -0
- security_use/reporter.py +368 -0
- security_use/scanner.py +74 -0
- security_use-0.1.1.dist-info/METADATA +92 -0
- security_use-0.1.1.dist-info/RECORD +30 -0
- security_use-0.1.1.dist-info/WHEEL +4 -0
- security_use-0.1.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""IaC security rules."""
|
|
2
|
+
|
|
3
|
+
from security_use.iac.rules.base import Rule, RuleResult
|
|
4
|
+
from security_use.iac.rules.registry import RuleRegistry, get_registry
|
|
5
|
+
from security_use.iac.rules.aws import (
|
|
6
|
+
S3BucketEncryptionRule,
|
|
7
|
+
S3BucketPublicAccessRule,
|
|
8
|
+
SecurityGroupOpenIngressRule,
|
|
9
|
+
IAMUserMFARule,
|
|
10
|
+
RDSEncryptionRule,
|
|
11
|
+
EBSEncryptionRule,
|
|
12
|
+
CloudTrailEnabledRule,
|
|
13
|
+
VPCFlowLogsRule,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Rule",
|
|
18
|
+
"RuleResult",
|
|
19
|
+
"RuleRegistry",
|
|
20
|
+
"get_registry",
|
|
21
|
+
"S3BucketEncryptionRule",
|
|
22
|
+
"S3BucketPublicAccessRule",
|
|
23
|
+
"SecurityGroupOpenIngressRule",
|
|
24
|
+
"IAMUserMFARule",
|
|
25
|
+
"RDSEncryptionRule",
|
|
26
|
+
"EBSEncryptionRule",
|
|
27
|
+
"CloudTrailEnabledRule",
|
|
28
|
+
"VPCFlowLogsRule",
|
|
29
|
+
]
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""AWS 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 S3BucketEncryptionRule(Rule):
|
|
9
|
+
"""Check that S3 buckets have server-side encryption enabled."""
|
|
10
|
+
|
|
11
|
+
RULE_ID = "CKV_AWS_19"
|
|
12
|
+
TITLE = "S3 bucket without encryption"
|
|
13
|
+
SEVERITY = Severity.HIGH
|
|
14
|
+
DESCRIPTION = (
|
|
15
|
+
"S3 bucket does not have server-side encryption enabled. "
|
|
16
|
+
"Data at rest should be encrypted to protect sensitive information."
|
|
17
|
+
)
|
|
18
|
+
REMEDIATION = (
|
|
19
|
+
"Enable server-side encryption using SSE-S3, SSE-KMS, or SSE-C. "
|
|
20
|
+
"Add a server_side_encryption_configuration block to the bucket."
|
|
21
|
+
)
|
|
22
|
+
RESOURCE_TYPES = ["aws_s3_bucket", "AWS::S3::Bucket"]
|
|
23
|
+
|
|
24
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
25
|
+
"""Check if S3 bucket has encryption enabled."""
|
|
26
|
+
has_encryption = False
|
|
27
|
+
|
|
28
|
+
# Terraform: Check for server_side_encryption_configuration
|
|
29
|
+
if "server_side_encryption_configuration" in resource.config:
|
|
30
|
+
has_encryption = True
|
|
31
|
+
|
|
32
|
+
# CloudFormation: Check for BucketEncryption
|
|
33
|
+
if "BucketEncryption" in resource.config:
|
|
34
|
+
has_encryption = True
|
|
35
|
+
|
|
36
|
+
fix_code = None
|
|
37
|
+
if not has_encryption:
|
|
38
|
+
if resource.provider == "aws":
|
|
39
|
+
fix_code = '''server_side_encryption_configuration {
|
|
40
|
+
rule {
|
|
41
|
+
apply_server_side_encryption_by_default {
|
|
42
|
+
sse_algorithm = "AES256"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}'''
|
|
46
|
+
|
|
47
|
+
return self._create_result(has_encryption, resource, fix_code)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class S3BucketPublicAccessRule(Rule):
|
|
51
|
+
"""Check that S3 buckets do not allow public access."""
|
|
52
|
+
|
|
53
|
+
RULE_ID = "CKV_AWS_20"
|
|
54
|
+
TITLE = "S3 bucket with public access"
|
|
55
|
+
SEVERITY = Severity.CRITICAL
|
|
56
|
+
DESCRIPTION = (
|
|
57
|
+
"S3 bucket allows public access. This can lead to data exposure "
|
|
58
|
+
"and security breaches."
|
|
59
|
+
)
|
|
60
|
+
REMEDIATION = (
|
|
61
|
+
"Set the bucket ACL to 'private' and enable block public access settings. "
|
|
62
|
+
"Remove any bucket policies that grant public access."
|
|
63
|
+
)
|
|
64
|
+
RESOURCE_TYPES = ["aws_s3_bucket", "AWS::S3::Bucket"]
|
|
65
|
+
|
|
66
|
+
PUBLIC_ACLS = ["public-read", "public-read-write", "authenticated-read"]
|
|
67
|
+
|
|
68
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
69
|
+
"""Check if S3 bucket has public access."""
|
|
70
|
+
has_public_access = False
|
|
71
|
+
|
|
72
|
+
# Check ACL
|
|
73
|
+
acl = resource.get_config("acl", default="private")
|
|
74
|
+
if acl in self.PUBLIC_ACLS:
|
|
75
|
+
has_public_access = True
|
|
76
|
+
|
|
77
|
+
# CloudFormation: Check AccessControl
|
|
78
|
+
access_control = resource.get_config("AccessControl", default="Private")
|
|
79
|
+
if access_control in ["PublicRead", "PublicReadWrite", "AuthenticatedRead"]:
|
|
80
|
+
has_public_access = True
|
|
81
|
+
|
|
82
|
+
fix_code = None
|
|
83
|
+
if has_public_access:
|
|
84
|
+
fix_code = 'acl = "private"'
|
|
85
|
+
|
|
86
|
+
return self._create_result(not has_public_access, resource, fix_code)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SecurityGroupOpenIngressRule(Rule):
|
|
90
|
+
"""Check that security groups don't allow unrestricted ingress on sensitive ports."""
|
|
91
|
+
|
|
92
|
+
RULE_ID = "CKV_AWS_23"
|
|
93
|
+
TITLE = "Security group allows unrestricted ingress"
|
|
94
|
+
SEVERITY = Severity.HIGH
|
|
95
|
+
DESCRIPTION = (
|
|
96
|
+
"Security group allows ingress from 0.0.0.0/0 on sensitive ports. "
|
|
97
|
+
"This exposes services to the entire internet."
|
|
98
|
+
)
|
|
99
|
+
REMEDIATION = (
|
|
100
|
+
"Restrict ingress rules to specific IP ranges or security groups. "
|
|
101
|
+
"Avoid using 0.0.0.0/0 as the source CIDR."
|
|
102
|
+
)
|
|
103
|
+
RESOURCE_TYPES = ["aws_security_group", "AWS::EC2::SecurityGroup"]
|
|
104
|
+
|
|
105
|
+
SENSITIVE_PORTS = [22, 3389, 3306, 5432, 1433, 27017, 6379, 11211]
|
|
106
|
+
|
|
107
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
108
|
+
"""Check if security group has open ingress on sensitive ports."""
|
|
109
|
+
has_open_ingress = False
|
|
110
|
+
|
|
111
|
+
# Terraform format
|
|
112
|
+
ingress_rules = resource.get_config("ingress", default=[])
|
|
113
|
+
if isinstance(ingress_rules, list):
|
|
114
|
+
for rule in ingress_rules:
|
|
115
|
+
if self._is_open_rule(rule):
|
|
116
|
+
has_open_ingress = True
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
# CloudFormation format
|
|
120
|
+
sg_ingress = resource.get_config("SecurityGroupIngress", default=[])
|
|
121
|
+
if isinstance(sg_ingress, list):
|
|
122
|
+
for rule in sg_ingress:
|
|
123
|
+
if self._is_open_cfn_rule(rule):
|
|
124
|
+
has_open_ingress = True
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
fix_code = None
|
|
128
|
+
if has_open_ingress:
|
|
129
|
+
fix_code = "# Restrict cidr_blocks 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
|
+
def _is_open_rule(self, rule: dict) -> bool:
|
|
134
|
+
"""Check if a Terraform ingress rule is open to the world."""
|
|
135
|
+
cidr_blocks = rule.get("cidr_blocks", [])
|
|
136
|
+
if "0.0.0.0/0" not in cidr_blocks and "::/0" not in cidr_blocks:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
from_port = rule.get("from_port", 0)
|
|
140
|
+
to_port = rule.get("to_port", 65535)
|
|
141
|
+
|
|
142
|
+
# Check if any sensitive port is in the range
|
|
143
|
+
for port in self.SENSITIVE_PORTS:
|
|
144
|
+
if from_port <= port <= to_port:
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
def _is_open_cfn_rule(self, rule: dict) -> bool:
|
|
150
|
+
"""Check if a CloudFormation ingress rule is open to the world."""
|
|
151
|
+
cidr = rule.get("CidrIp", "")
|
|
152
|
+
cidr_v6 = rule.get("CidrIpv6", "")
|
|
153
|
+
if cidr != "0.0.0.0/0" and cidr_v6 != "::/0":
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
from_port = rule.get("FromPort", 0)
|
|
157
|
+
to_port = rule.get("ToPort", 65535)
|
|
158
|
+
|
|
159
|
+
for port in self.SENSITIVE_PORTS:
|
|
160
|
+
if from_port <= port <= to_port:
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class IAMUserMFARule(Rule):
|
|
167
|
+
"""Check that IAM users have MFA enabled."""
|
|
168
|
+
|
|
169
|
+
RULE_ID = "CKV_AWS_14"
|
|
170
|
+
TITLE = "IAM user without MFA"
|
|
171
|
+
SEVERITY = Severity.MEDIUM
|
|
172
|
+
DESCRIPTION = (
|
|
173
|
+
"IAM user does not have MFA enabled. MFA provides an additional "
|
|
174
|
+
"layer of security for user authentication."
|
|
175
|
+
)
|
|
176
|
+
REMEDIATION = (
|
|
177
|
+
"Enable MFA for all IAM users with console access. "
|
|
178
|
+
"Use virtual MFA devices or hardware tokens."
|
|
179
|
+
)
|
|
180
|
+
RESOURCE_TYPES = ["aws_iam_user", "AWS::IAM::User"]
|
|
181
|
+
|
|
182
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
183
|
+
"""Check if IAM user has MFA configured."""
|
|
184
|
+
# For Terraform, we'd check for associated aws_iam_user_mfa_device
|
|
185
|
+
# For CloudFormation, MFA is typically configured separately
|
|
186
|
+
# This is a best-effort check
|
|
187
|
+
|
|
188
|
+
# If the user has login profile (console access), MFA should be required
|
|
189
|
+
has_login_profile = "login_profile" in resource.config or "LoginProfile" in resource.config
|
|
190
|
+
|
|
191
|
+
# We can't definitively check MFA from the resource alone
|
|
192
|
+
# Flag users with console access as needing review
|
|
193
|
+
if has_login_profile:
|
|
194
|
+
return self._create_result(
|
|
195
|
+
False,
|
|
196
|
+
resource,
|
|
197
|
+
fix_code="# Ensure MFA is configured for this user",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return self._create_result(True, resource)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class RDSEncryptionRule(Rule):
|
|
204
|
+
"""Check that RDS instances have encryption enabled."""
|
|
205
|
+
|
|
206
|
+
RULE_ID = "CKV_AWS_16"
|
|
207
|
+
TITLE = "RDS instance without encryption"
|
|
208
|
+
SEVERITY = Severity.HIGH
|
|
209
|
+
DESCRIPTION = (
|
|
210
|
+
"RDS instance does not have encryption at rest enabled. "
|
|
211
|
+
"Database data should be encrypted to protect sensitive information."
|
|
212
|
+
)
|
|
213
|
+
REMEDIATION = (
|
|
214
|
+
"Enable encryption at rest for the RDS instance. "
|
|
215
|
+
"Set storage_encrypted = true (Terraform) or StorageEncrypted: true (CloudFormation)."
|
|
216
|
+
)
|
|
217
|
+
RESOURCE_TYPES = ["aws_db_instance", "AWS::RDS::DBInstance"]
|
|
218
|
+
|
|
219
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
220
|
+
"""Check if RDS instance has encryption enabled."""
|
|
221
|
+
# Terraform
|
|
222
|
+
storage_encrypted = resource.get_config("storage_encrypted", default=False)
|
|
223
|
+
|
|
224
|
+
# CloudFormation
|
|
225
|
+
if not storage_encrypted:
|
|
226
|
+
storage_encrypted = resource.get_config("StorageEncrypted", default=False)
|
|
227
|
+
|
|
228
|
+
fix_code = None
|
|
229
|
+
if not storage_encrypted:
|
|
230
|
+
fix_code = "storage_encrypted = true"
|
|
231
|
+
|
|
232
|
+
return self._create_result(bool(storage_encrypted), resource, fix_code)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class EBSEncryptionRule(Rule):
|
|
236
|
+
"""Check that EBS volumes have encryption enabled."""
|
|
237
|
+
|
|
238
|
+
RULE_ID = "CKV_AWS_3"
|
|
239
|
+
TITLE = "EBS volume without encryption"
|
|
240
|
+
SEVERITY = Severity.HIGH
|
|
241
|
+
DESCRIPTION = (
|
|
242
|
+
"EBS volume does not have encryption enabled. "
|
|
243
|
+
"Data on EBS volumes should be encrypted at rest."
|
|
244
|
+
)
|
|
245
|
+
REMEDIATION = (
|
|
246
|
+
"Enable encryption for the EBS volume. "
|
|
247
|
+
"Set encrypted = true (Terraform) or Encrypted: true (CloudFormation)."
|
|
248
|
+
)
|
|
249
|
+
RESOURCE_TYPES = ["aws_ebs_volume", "AWS::EC2::Volume"]
|
|
250
|
+
|
|
251
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
252
|
+
"""Check if EBS volume has encryption enabled."""
|
|
253
|
+
# Terraform
|
|
254
|
+
encrypted = resource.get_config("encrypted", default=False)
|
|
255
|
+
|
|
256
|
+
# CloudFormation
|
|
257
|
+
if not encrypted:
|
|
258
|
+
encrypted = resource.get_config("Encrypted", default=False)
|
|
259
|
+
|
|
260
|
+
fix_code = None
|
|
261
|
+
if not encrypted:
|
|
262
|
+
fix_code = "encrypted = true"
|
|
263
|
+
|
|
264
|
+
return self._create_result(bool(encrypted), resource, fix_code)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class CloudTrailEnabledRule(Rule):
|
|
268
|
+
"""Check that CloudTrail is properly configured."""
|
|
269
|
+
|
|
270
|
+
RULE_ID = "CKV_AWS_35"
|
|
271
|
+
TITLE = "CloudTrail not logging all events"
|
|
272
|
+
SEVERITY = Severity.MEDIUM
|
|
273
|
+
DESCRIPTION = (
|
|
274
|
+
"CloudTrail is not configured to log all management events. "
|
|
275
|
+
"Complete audit logging is essential for security monitoring."
|
|
276
|
+
)
|
|
277
|
+
REMEDIATION = (
|
|
278
|
+
"Enable CloudTrail with is_multi_region_trail = true and "
|
|
279
|
+
"include_global_service_events = true."
|
|
280
|
+
)
|
|
281
|
+
RESOURCE_TYPES = ["aws_cloudtrail", "AWS::CloudTrail::Trail"]
|
|
282
|
+
|
|
283
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
284
|
+
"""Check if CloudTrail is properly configured."""
|
|
285
|
+
is_multi_region = resource.get_config("is_multi_region_trail", default=False)
|
|
286
|
+
include_global = resource.get_config("include_global_service_events", default=False)
|
|
287
|
+
|
|
288
|
+
# CloudFormation
|
|
289
|
+
if not is_multi_region:
|
|
290
|
+
is_multi_region = resource.get_config("IsMultiRegionTrail", default=False)
|
|
291
|
+
if not include_global:
|
|
292
|
+
include_global = resource.get_config("IncludeGlobalServiceEvents", default=False)
|
|
293
|
+
|
|
294
|
+
passed = is_multi_region and include_global
|
|
295
|
+
|
|
296
|
+
fix_code = None
|
|
297
|
+
if not passed:
|
|
298
|
+
fix_code = "is_multi_region_trail = true\ninclude_global_service_events = true"
|
|
299
|
+
|
|
300
|
+
return self._create_result(passed, resource, fix_code)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class VPCFlowLogsRule(Rule):
|
|
304
|
+
"""Check that VPC flow logs are enabled."""
|
|
305
|
+
|
|
306
|
+
RULE_ID = "CKV_AWS_12"
|
|
307
|
+
TITLE = "VPC without flow logs"
|
|
308
|
+
SEVERITY = Severity.MEDIUM
|
|
309
|
+
DESCRIPTION = (
|
|
310
|
+
"VPC does not have flow logs enabled. Flow logs provide visibility "
|
|
311
|
+
"into network traffic for security analysis and troubleshooting."
|
|
312
|
+
)
|
|
313
|
+
REMEDIATION = (
|
|
314
|
+
"Enable VPC flow logs by creating an aws_flow_log resource "
|
|
315
|
+
"(Terraform) or AWS::EC2::FlowLog (CloudFormation)."
|
|
316
|
+
)
|
|
317
|
+
RESOURCE_TYPES = ["aws_vpc", "AWS::EC2::VPC"]
|
|
318
|
+
|
|
319
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
320
|
+
"""Check if VPC has flow logs.
|
|
321
|
+
|
|
322
|
+
Note: This is a best-effort check. Flow logs are typically
|
|
323
|
+
defined as separate resources, so we flag VPCs for review.
|
|
324
|
+
"""
|
|
325
|
+
# VPCs should have associated flow logs
|
|
326
|
+
# We can't verify this from the VPC resource alone
|
|
327
|
+
# Return a warning to encourage flow log configuration
|
|
328
|
+
|
|
329
|
+
fix_code = '''resource "aws_flow_log" "example" {
|
|
330
|
+
vpc_id = aws_vpc.example.id
|
|
331
|
+
traffic_type = "ALL"
|
|
332
|
+
log_destination_type = "cloud-watch-logs"
|
|
333
|
+
log_destination = aws_cloudwatch_log_group.example.arn
|
|
334
|
+
iam_role_arn = aws_iam_role.example.arn
|
|
335
|
+
}'''
|
|
336
|
+
|
|
337
|
+
# Default to warning (not passed) to encourage flow logs
|
|
338
|
+
return self._create_result(False, resource, fix_code)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Base classes for IaC security rules."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from security_use.models import Severity
|
|
8
|
+
from security_use.iac.base import IaCResource
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class RuleResult:
|
|
13
|
+
"""Result of applying a rule to a resource."""
|
|
14
|
+
|
|
15
|
+
passed: bool
|
|
16
|
+
rule_id: str
|
|
17
|
+
title: str
|
|
18
|
+
severity: Severity
|
|
19
|
+
resource_type: str
|
|
20
|
+
resource_name: str
|
|
21
|
+
description: str
|
|
22
|
+
remediation: str
|
|
23
|
+
fix_code: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Rule(ABC):
|
|
27
|
+
"""Abstract base class for security rules."""
|
|
28
|
+
|
|
29
|
+
# Rule metadata - override in subclasses
|
|
30
|
+
RULE_ID: str = "UNKNOWN"
|
|
31
|
+
TITLE: str = "Unknown Rule"
|
|
32
|
+
SEVERITY: Severity = Severity.MEDIUM
|
|
33
|
+
DESCRIPTION: str = ""
|
|
34
|
+
REMEDIATION: str = ""
|
|
35
|
+
|
|
36
|
+
# Resource types this rule applies to
|
|
37
|
+
RESOURCE_TYPES: list[str] = []
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def evaluate(self, resource: IaCResource) -> RuleResult:
|
|
41
|
+
"""Evaluate the rule against a resource.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
resource: The IaC resource to evaluate.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
RuleResult indicating pass/fail and details.
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def applies_to(self, resource: IaCResource) -> bool:
|
|
52
|
+
"""Check if this rule applies to the given resource.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
resource: The IaC resource to check.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if the rule should be evaluated for this resource.
|
|
59
|
+
"""
|
|
60
|
+
if not self.RESOURCE_TYPES:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
# Handle both Terraform and CloudFormation resource types
|
|
64
|
+
resource_type = resource.resource_type.lower()
|
|
65
|
+
for rule_type in self.RESOURCE_TYPES:
|
|
66
|
+
if rule_type.lower() in resource_type:
|
|
67
|
+
return True
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def _create_result(
|
|
71
|
+
self,
|
|
72
|
+
passed: bool,
|
|
73
|
+
resource: IaCResource,
|
|
74
|
+
fix_code: Optional[str] = None,
|
|
75
|
+
) -> RuleResult:
|
|
76
|
+
"""Create a RuleResult for this rule.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
passed: Whether the rule passed.
|
|
80
|
+
resource: The evaluated resource.
|
|
81
|
+
fix_code: Optional suggested fix code.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
RuleResult with rule metadata.
|
|
85
|
+
"""
|
|
86
|
+
return RuleResult(
|
|
87
|
+
passed=passed,
|
|
88
|
+
rule_id=self.RULE_ID,
|
|
89
|
+
title=self.TITLE,
|
|
90
|
+
severity=self.SEVERITY,
|
|
91
|
+
resource_type=resource.resource_type,
|
|
92
|
+
resource_name=resource.name,
|
|
93
|
+
description=self.DESCRIPTION,
|
|
94
|
+
remediation=self.REMEDIATION,
|
|
95
|
+
fix_code=fix_code,
|
|
96
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Rule registry for managing security rules."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Type
|
|
4
|
+
|
|
5
|
+
from security_use.iac.rules.base import Rule
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RuleRegistry:
|
|
9
|
+
"""Registry for IaC security rules."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
"""Initialize the rule registry."""
|
|
13
|
+
self._rules: dict[str, Rule] = {}
|
|
14
|
+
|
|
15
|
+
def register(self, rule: Rule) -> None:
|
|
16
|
+
"""Register a rule.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
rule: Rule instance to register.
|
|
20
|
+
"""
|
|
21
|
+
self._rules[rule.RULE_ID] = rule
|
|
22
|
+
|
|
23
|
+
def register_class(self, rule_class: Type[Rule]) -> None:
|
|
24
|
+
"""Register a rule class (instantiates it).
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
rule_class: Rule class to register.
|
|
28
|
+
"""
|
|
29
|
+
rule = rule_class()
|
|
30
|
+
self.register(rule)
|
|
31
|
+
|
|
32
|
+
def get(self, rule_id: str) -> Optional[Rule]:
|
|
33
|
+
"""Get a rule by ID.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
rule_id: The rule ID.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Rule instance or None if not found.
|
|
40
|
+
"""
|
|
41
|
+
return self._rules.get(rule_id)
|
|
42
|
+
|
|
43
|
+
def get_all(self) -> list[Rule]:
|
|
44
|
+
"""Get all registered rules.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
List of all registered rules.
|
|
48
|
+
"""
|
|
49
|
+
return list(self._rules.values())
|
|
50
|
+
|
|
51
|
+
def get_for_resource(self, resource_type: str) -> list[Rule]:
|
|
52
|
+
"""Get rules that apply to a resource type.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
resource_type: The resource type to match.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of applicable rules.
|
|
59
|
+
"""
|
|
60
|
+
from security_use.iac.base import IaCResource
|
|
61
|
+
|
|
62
|
+
# Create a dummy resource to check applicability
|
|
63
|
+
dummy = IaCResource(
|
|
64
|
+
resource_type=resource_type,
|
|
65
|
+
name="",
|
|
66
|
+
config={},
|
|
67
|
+
file_path="",
|
|
68
|
+
line_number=0,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return [rule for rule in self._rules.values() if rule.applies_to(dummy)]
|
|
72
|
+
|
|
73
|
+
def clear(self) -> None:
|
|
74
|
+
"""Clear all registered rules."""
|
|
75
|
+
self._rules.clear()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Global registry instance
|
|
79
|
+
_registry: Optional[RuleRegistry] = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_registry() -> RuleRegistry:
|
|
83
|
+
"""Get the global rule registry.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The global RuleRegistry instance.
|
|
87
|
+
"""
|
|
88
|
+
global _registry
|
|
89
|
+
if _registry is None:
|
|
90
|
+
_registry = RuleRegistry()
|
|
91
|
+
_register_default_rules(_registry)
|
|
92
|
+
return _registry
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _register_default_rules(registry: RuleRegistry) -> None:
|
|
96
|
+
"""Register all default rules."""
|
|
97
|
+
from security_use.iac.rules.aws import (
|
|
98
|
+
S3BucketEncryptionRule,
|
|
99
|
+
S3BucketPublicAccessRule,
|
|
100
|
+
SecurityGroupOpenIngressRule,
|
|
101
|
+
IAMUserMFARule,
|
|
102
|
+
RDSEncryptionRule,
|
|
103
|
+
EBSEncryptionRule,
|
|
104
|
+
CloudTrailEnabledRule,
|
|
105
|
+
VPCFlowLogsRule,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
registry.register_class(S3BucketEncryptionRule)
|
|
109
|
+
registry.register_class(S3BucketPublicAccessRule)
|
|
110
|
+
registry.register_class(SecurityGroupOpenIngressRule)
|
|
111
|
+
registry.register_class(IAMUserMFARule)
|
|
112
|
+
registry.register_class(RDSEncryptionRule)
|
|
113
|
+
registry.register_class(EBSEncryptionRule)
|
|
114
|
+
registry.register_class(CloudTrailEnabledRule)
|
|
115
|
+
registry.register_class(VPCFlowLogsRule)
|