driftwatch-cli 0.1.0__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.
File without changes
@@ -0,0 +1,241 @@
1
+ import sys
2
+ import boto3
3
+ from datetime import datetime, timedelta
4
+
5
+ def fetch_live_ec2_instances(region: str) -> dict:
6
+ ec2 = boto3.client("ec2", region_name=region)
7
+ live = {}
8
+ try:
9
+ paginator = ec2.get_paginator("describe_instances")
10
+ for page in paginator.paginate():
11
+ for reservation in page.get("Reservations", []):
12
+ for instance in reservation.get("Instances", []):
13
+ if instance.get("State", {}).get("Name") == "terminated":
14
+ continue
15
+
16
+ tags_list = instance.get("Tags", [])
17
+ tags_dict = {t["Key"]: t["Value"] for t in tags_list if "Key" in t and "Value" in t}
18
+ name = tags_dict.get("Name", "Unknown")
19
+
20
+ sg_ids = []
21
+ for sg in instance.get("SecurityGroups", []):
22
+ if "GroupId" in sg:
23
+ sg_ids.append(sg.get("GroupId"))
24
+ sg_ids.sort()
25
+
26
+ instance_id = instance["InstanceId"]
27
+ live[instance_id] = {
28
+ "type": "aws_instance",
29
+ "name": name,
30
+ "attributes": {
31
+ "id": instance_id,
32
+ "instance_type": instance.get("InstanceType"),
33
+ "ami": instance.get("ImageId"),
34
+ "tags": tags_dict,
35
+ "vpc_security_group_ids": sg_ids
36
+ },
37
+ }
38
+ except Exception as e:
39
+ print(f"Failed to fetch aws_instance: {e}", file=sys.stderr)
40
+ return None
41
+ return live
42
+
43
+ def fetch_live_s3_buckets(region: str) -> dict:
44
+ s3 = boto3.client("s3", region_name=region)
45
+ live = {}
46
+ try:
47
+ response = s3.list_buckets()
48
+ for bucket in response.get("Buckets", []):
49
+ bucket_name = bucket["Name"]
50
+ try:
51
+ tags_response = s3.get_bucket_tagging(Bucket=bucket_name)
52
+ tags_list = tags_response.get("TagSet", [])
53
+ tags_dict = {t["Key"]: t["Value"] for t in tags_list if "Key" in t and "Value" in t}
54
+ name = tags_dict.get("Name", bucket_name)
55
+ except Exception:
56
+ tags_dict = {}
57
+ name = bucket_name
58
+
59
+ live[bucket_name] = {
60
+ "type": "aws_s3_bucket",
61
+ "name": name,
62
+ "attributes": {
63
+ "id": bucket_name,
64
+ "bucket": bucket_name,
65
+ "tags": tags_dict,
66
+ },
67
+ }
68
+ except Exception as e:
69
+ print(f"Failed to fetch aws_s3_bucket: {e}", file=sys.stderr)
70
+ return None
71
+ return live
72
+
73
+ def fetch_live_security_groups(region: str) -> dict:
74
+ ec2 = boto3.client("ec2", region_name=region)
75
+ live = {}
76
+ try:
77
+ paginator = ec2.get_paginator("describe_security_groups")
78
+ for page in paginator.paginate():
79
+ for sg in page.get("SecurityGroups", []):
80
+ sg_id = sg["GroupId"]
81
+ sg_name = sg.get("GroupName", "")
82
+
83
+ tags_list = sg.get("Tags", [])
84
+ tags_dict = {t["Key"]: t["Value"] for t in tags_list if "Key" in t and "Value" in t}
85
+ name_tag = tags_dict.get("Name", sg_name)
86
+
87
+ ingress_rules = []
88
+ for perm in sg.get("IpPermissions", []):
89
+ cidrs = [ip.get("CidrIp") for ip in perm.get("IpRanges", []) if ip.get("CidrIp")]
90
+ if cidrs:
91
+ ingress_rules.append({
92
+ "from_port": perm.get("FromPort", 0),
93
+ "to_port": perm.get("ToPort", 0),
94
+ "protocol": perm.get("IpProtocol", "-1"),
95
+ "cidr_blocks": cidrs
96
+ })
97
+
98
+ egress_rules = []
99
+ for perm in sg.get("IpPermissionsEgress", []):
100
+ cidrs = [ip.get("CidrIp") for ip in perm.get("IpRanges", []) if ip.get("CidrIp")]
101
+ if cidrs:
102
+ egress_rules.append({
103
+ "from_port": perm.get("FromPort", 0),
104
+ "to_port": perm.get("ToPort", 0),
105
+ "protocol": perm.get("IpProtocol", "-1"),
106
+ "cidr_blocks": cidrs
107
+ })
108
+
109
+ live[sg_id] = {
110
+ "type": "aws_security_group",
111
+ "name": name_tag,
112
+ "attributes": {
113
+ "id": sg_id,
114
+ "name": sg_name,
115
+ "description": sg.get("Description", ""),
116
+ "tags": tags_dict,
117
+ "ingress": ingress_rules,
118
+ "egress": egress_rules,
119
+ },
120
+ }
121
+ except Exception as e:
122
+ print(f"Failed to fetch aws_security_group: {e}", file=sys.stderr)
123
+ return None
124
+ return live
125
+
126
+ def fetch_live_rds_instances(region: str) -> dict:
127
+ rds = boto3.client("rds", region_name=region)
128
+ live = {}
129
+ try:
130
+ paginator = rds.get_paginator("describe_db_instances")
131
+ for page in paginator.paginate():
132
+ for db in page.get("DBInstances", []):
133
+ db_id = db.get("DBInstanceIdentifier")
134
+ if not db_id:
135
+ continue
136
+ live[db_id] = {
137
+ "type": "aws_db_instance",
138
+ "name": db_id,
139
+ "attributes": {
140
+ "id": db_id,
141
+ "identifier": db_id,
142
+ "allocated_storage": db.get("AllocatedStorage"),
143
+ "engine": db.get("Engine"),
144
+ "engine_version": db.get("EngineVersion"),
145
+ "instance_class": db.get("DBInstanceClass"),
146
+ "multi_az": db.get("MultiAZ"),
147
+ },
148
+ }
149
+ except Exception as e:
150
+ print(f"Failed to fetch aws_db_instance: {e}", file=sys.stderr)
151
+ return None
152
+ return live
153
+
154
+ def fetch_live_lambda_functions(region: str) -> dict:
155
+ lambda_client = boto3.client("lambda", region_name=region)
156
+ live = {}
157
+ try:
158
+ paginator = lambda_client.get_paginator("list_functions")
159
+ for page in paginator.paginate():
160
+ for func in page.get("Functions", []):
161
+ func_name = func.get("FunctionName")
162
+ if not func_name:
163
+ continue
164
+ live[func_name] = {
165
+ "type": "aws_lambda_function",
166
+ "name": func_name,
167
+ "attributes": {
168
+ "id": func_name,
169
+ "function_name": func_name,
170
+ "runtime": func.get("Runtime"),
171
+ "handler": func.get("Handler"),
172
+ "memory_size": func.get("MemorySize"),
173
+ "timeout": func.get("Timeout"),
174
+ "role": func.get("Role"),
175
+ },
176
+ }
177
+ except Exception as e:
178
+ print(f"Failed to fetch aws_lambda_function: {e}", file=sys.stderr)
179
+ return None
180
+ return live
181
+
182
+ def fetch_live_iam_roles(region: str) -> dict:
183
+ iam = boto3.client("iam", region_name=region)
184
+ live = {}
185
+ try:
186
+ paginator = iam.get_paginator("list_roles")
187
+ for page in paginator.paginate():
188
+ for role in page.get("Roles", []):
189
+ role_name = role.get("RoleName")
190
+ if not role_name:
191
+ continue
192
+ if role_name.startswith("AWSServiceRoleFor") or role.get("Path", "").startswith("/aws-service-role/"):
193
+ continue
194
+
195
+ try:
196
+ policies = iam.list_attached_role_policies(RoleName=role_name)
197
+ attached_policies = sorted(
198
+ p["PolicyArn"] for p in policies.get("AttachedPolicies", []) if "PolicyArn" in p
199
+ )
200
+ except Exception:
201
+ attached_policies = []
202
+
203
+ live[role_name] = {
204
+ "type": "aws_iam_role",
205
+ "name": role_name,
206
+ "attributes": {
207
+ "id": role_name,
208
+ "name": role_name,
209
+ "path": role.get("Path", "/"),
210
+ "arn": role.get("Arn", ""),
211
+ "attached_policies": attached_policies
212
+ }
213
+ }
214
+ except Exception as e:
215
+ print(f"Failed to fetch aws_iam_role: {e}", file=sys.stderr)
216
+ return None
217
+ return live
218
+
219
+ def get_resource_cost(resource_id: str) -> float:
220
+ try:
221
+ client = boto3.client("ce", region_name="us-east-1")
222
+
223
+ end_date = datetime.today().strftime("%Y-%m-%d")
224
+ start_date = (datetime.today() - timedelta(days=30)).strftime("%Y-%m-%d")
225
+
226
+ response = client.get_cost_and_usage(
227
+ TimePeriod={"Start": start_date, "End": end_date},
228
+ Granularity="MONTHLY",
229
+ Metrics=["UnblendedCost"],
230
+ Filter={
231
+ "Dimensions": {
232
+ "Key": "RESOURCE_ID",
233
+ "Values": [resource_id]
234
+ }
235
+ }
236
+ )
237
+
238
+ usd_cost = float(response["ResultsByTime"][0]["Total"]["UnblendedCost"]["Amount"])
239
+ return usd_cost
240
+ except Exception:
241
+ return 0.0
drift_engine/core.py ADDED
@@ -0,0 +1,190 @@
1
+ import os
2
+ import sys
3
+ from drift_engine.models import (
4
+ DriftResult,
5
+ DriftType,
6
+ MONITORED_ATTRIBUTES,
7
+ ATTRIBUTE_SEVERITY
8
+ )
9
+ from drift_engine.tf_parser import load_terraform_state
10
+ from drift_engine.aws_client import (
11
+ fetch_live_ec2_instances,
12
+ fetch_live_s3_buckets,
13
+ fetch_live_security_groups,
14
+ fetch_live_rds_instances,
15
+ fetch_live_lambda_functions,
16
+ fetch_live_iam_roles
17
+ )
18
+
19
+ SEVERITY_LEVELS = {"LOW": 0, "MEDIUM": 1, "HIGH": 2, "CRITICAL": 3}
20
+
21
+ def normalize_sg_rules(rules) -> list:
22
+ normalized = []
23
+ if not isinstance(rules, list):
24
+ return normalized
25
+
26
+ for rule in rules:
27
+ if isinstance(rule, dict):
28
+ cidrs = rule.get('cidr_blocks') or []
29
+ if isinstance(cidrs, list):
30
+ cidrs = tuple(sorted(cidrs))
31
+ else:
32
+ cidrs = tuple()
33
+
34
+ normalized.append({
35
+ 'from_port': rule.get('from_port'),
36
+ 'to_port': rule.get('to_port'),
37
+ 'protocol': rule.get('protocol'),
38
+ 'cidr_blocks': cidrs
39
+ })
40
+
41
+ final_rules = []
42
+ for t in {tuple(sorted(d.items())) for d in normalized}:
43
+ rule_dict = dict(t)
44
+ rule_dict['cidr_blocks'] = list(rule_dict['cidr_blocks'])
45
+ final_rules.append(rule_dict)
46
+
47
+ return final_rules
48
+
49
+ def compare_attributes(tf, live, r_type) -> dict:
50
+ monitored = MONITORED_ATTRIBUTES.get(r_type, set())
51
+ diff = {}
52
+
53
+ for key in monitored:
54
+ tf_val = tf.get(key)
55
+ live_val = live.get(key)
56
+
57
+ if (tf_val in [None, "", [], {}]) and (live_val in [None, "", [], {}]):
58
+ continue
59
+
60
+ if r_type == "aws_security_group" and key in ["ingress", "egress"]:
61
+ tf_norm = normalize_sg_rules(tf_val)
62
+ live_norm = normalize_sg_rules(live_val)
63
+ if tf_norm != live_norm:
64
+ diff[key] = {"terraform": tf_norm, "live": live_norm}
65
+ elif tf_val != live_val:
66
+ diff[key] = {"terraform": tf_val, "live": live_val}
67
+
68
+ return diff
69
+
70
+ def detect_drift(tf_state_path: str, region: str):
71
+ tf_resources = load_terraform_state(tf_state_path)
72
+
73
+ if not tf_resources:
74
+ return [], 0
75
+
76
+ failed_types = set()
77
+
78
+ live_ec2 = fetch_live_ec2_instances(region)
79
+ if live_ec2 is None:
80
+ failed_types.add("aws_instance")
81
+ live_ec2 = {}
82
+
83
+ live_s3 = fetch_live_s3_buckets(region)
84
+ if live_s3 is None:
85
+ failed_types.add("aws_s3_bucket")
86
+ live_s3 = {}
87
+
88
+ live_sg = fetch_live_security_groups(region)
89
+ if live_sg is None:
90
+ failed_types.add("aws_security_group")
91
+ live_sg = {}
92
+
93
+ live_rds = fetch_live_rds_instances(region)
94
+ if live_rds is None:
95
+ failed_types.add("aws_db_instance")
96
+ live_rds = {}
97
+
98
+ live_lambda = fetch_live_lambda_functions(region)
99
+ if live_lambda is None:
100
+ failed_types.add("aws_lambda_function")
101
+ live_lambda = {}
102
+
103
+ live_iam = fetch_live_iam_roles(region)
104
+ if live_iam is None:
105
+ failed_types.add("aws_iam_role")
106
+ live_iam = {}
107
+
108
+ live_resources = {**live_ec2, **live_s3, **live_sg, **live_rds, **live_lambda, **live_iam}
109
+
110
+ results = []
111
+ all_ids = set(tf_resources) | set(live_resources)
112
+ total_scanned = len(all_ids)
113
+
114
+ for rid in all_ids:
115
+ in_tf = rid in tf_resources
116
+ in_live = rid in live_resources
117
+
118
+ res_name = "Unknown"
119
+ if in_tf:
120
+ res_name = tf_resources[rid]["name"]
121
+ elif in_live:
122
+ res_name = live_resources[rid]["name"]
123
+
124
+ if in_tf and not in_live:
125
+ if tf_resources[rid]["type"] in failed_types:
126
+ continue
127
+ results.append(DriftResult(
128
+ resource_type=tf_resources[rid]["type"],
129
+ resource_id=rid,
130
+ drift_type=DriftType.MISSING,
131
+ resource_name=res_name,
132
+ tf_attributes=tf_resources[rid]["attributes"]
133
+ ))
134
+ elif in_live and not in_tf:
135
+ results.append(DriftResult(
136
+ resource_type=live_resources[rid]["type"],
137
+ resource_id=rid,
138
+ drift_type=DriftType.UNMANAGED,
139
+ resource_name=res_name,
140
+ live_attributes=live_resources[rid]["attributes"]
141
+ ))
142
+ else:
143
+ diff = compare_attributes(
144
+ tf_resources[rid]["attributes"],
145
+ live_resources[rid]["attributes"],
146
+ tf_resources[rid]["type"]
147
+ )
148
+ if diff:
149
+ results.append(DriftResult(
150
+ resource_type=tf_resources[rid]["type"],
151
+ resource_id=rid,
152
+ drift_type=DriftType.MODIFIED,
153
+ resource_name=res_name,
154
+ tf_attributes=tf_resources[rid]["attributes"],
155
+ live_attributes=live_resources[rid]["attributes"],
156
+ diff=diff
157
+ ))
158
+
159
+ return results, total_scanned
160
+
161
+ def get_severity(r_type: str, d_type: DriftType, diff: dict = None) -> str:
162
+ if d_type == DriftType.MISSING or d_type == DriftType.UNMANAGED:
163
+ if r_type in ["aws_security_group", "aws_iam_role"]:
164
+ return "CRITICAL"
165
+ return "HIGH"
166
+
167
+ if diff:
168
+ type_severity_map = ATTRIBUTE_SEVERITY.get(r_type, {})
169
+ highest_sev = "LOW"
170
+ for attr in diff.keys():
171
+ attr_sev = type_severity_map.get(attr, "MEDIUM")
172
+ if SEVERITY_LEVELS.get(attr_sev, 1) > SEVERITY_LEVELS.get(highest_sev, 0):
173
+ highest_sev = attr_sev
174
+ return highest_sev
175
+
176
+ if r_type in ["aws_security_group", "aws_iam_role"]:
177
+ return "CRITICAL"
178
+ if r_type == "aws_instance":
179
+ return "HIGH"
180
+ return "MEDIUM"
181
+
182
+ if __name__ == "__main__":
183
+ state_file = os.environ.get("TF_STATE_PATH", "terraform/terraform.tfstate")
184
+ scan_region = os.environ.get("AWS_DEFAULT_REGION", "ap-south-1")
185
+ print(f"Executing DriftWatch Engine on {state_file} ({scan_region})...")
186
+ drift_items, scanned = detect_drift(state_file, scan_region)
187
+ print(f"Scanned {scanned} resources. Found {len(drift_items)} drift items.")
188
+ for item in drift_items:
189
+ sev = get_severity(item.resource_type, item.drift_type, item.diff)
190
+ print(f" - [{item.drift_type.value}] {item.resource_type} ({item.resource_id}): Severity={sev}")
@@ -0,0 +1,54 @@
1
+ import os
2
+ import json
3
+ import psycopg2
4
+
5
+ def save_drift_to_db(drift_results: list):
6
+ if not drift_results:
7
+ return
8
+
9
+ db_user = os.environ.get("DB_USER")
10
+ db_password = os.environ.get("DB_PASSWORD")
11
+ db_name = os.environ.get("DB_NAME", "driftwatch")
12
+ db_host = os.environ.get("DB_HOST", "localhost")
13
+ db_port = os.environ.get("DB_PORT", "5432")
14
+
15
+ # Security Fix: Check if credentials exist before connecting
16
+ if not db_user or not db_password:
17
+ print("⚠️ DB_USER or DB_PASSWORD not found in environment. Skipping database save.")
18
+ return
19
+
20
+ try:
21
+ conn = psycopg2.connect(
22
+ dbname=db_name,
23
+ user=db_user,
24
+ password=db_password,
25
+ host=db_host,
26
+ port=db_port
27
+ )
28
+ cursor = conn.cursor()
29
+
30
+ cursor.execute('''
31
+ CREATE TABLE IF NOT EXISTS drift_history (
32
+ id SERIAL PRIMARY KEY,
33
+ scan_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
34
+ resource_type VARCHAR(255),
35
+ resource_id VARCHAR(255),
36
+ resource_name VARCHAR(255),
37
+ drift_type VARCHAR(50),
38
+ diff_details TEXT
39
+ )
40
+ ''')
41
+
42
+ for r in drift_results:
43
+ diff_json = json.dumps(r.diff) if r.diff else "{}"
44
+ cursor.execute('''
45
+ INSERT INTO drift_history (resource_type, resource_id, resource_name, drift_type, diff_details)
46
+ VALUES (%s, %s, %s, %s, %s)
47
+ ''', (r.resource_type, r.resource_id, r.resource_name, r.drift_type.value, diff_json))
48
+
49
+ conn.commit()
50
+ cursor.close()
51
+ conn.close()
52
+ print("✅ Drift history saved to PostgreSQL database.")
53
+ except Exception as e:
54
+ print(f"❌ Database error: {e}")
@@ -0,0 +1,72 @@
1
+ import os
2
+ import json
3
+ import requests
4
+
5
+ def get_deterministic_remediation_suggestion(resource_type: str, resource_id: str, diff_data: dict, drift_type: str) -> str:
6
+ safe_name = resource_id.replace("-", "_").replace(".", "_").replace("/", "_")
7
+
8
+ if drift_type == "UNMANAGED":
9
+ return (
10
+ f"# To bring this unmanaged {resource_type} into Terraform state:\n"
11
+ f"terraform import {resource_type}.{safe_name} {resource_id}"
12
+ )
13
+ elif drift_type == "MISSING":
14
+ return (
15
+ f"# To recreate this missing {resource_type} defined in IaC:\n"
16
+ f"terraform apply -target={resource_type}.{safe_name}"
17
+ )
18
+ elif drift_type == "MODIFIED":
19
+ lines = [f"# To align live {resource_type} ({resource_id}) with Terraform configuration:"]
20
+ if diff_data:
21
+ for attr, vals in diff_data.items():
22
+ if isinstance(vals, dict) and "terraform" in vals:
23
+ lines.append(f"# Set attribute '{attr}' to: {vals['terraform']}")
24
+ lines.append(f"terraform apply -target={resource_type}.{safe_name}")
25
+ return "\n".join(lines)
26
+
27
+ return f"terraform refresh"
28
+
29
+ def get_drift_explanation(resource_type: str, resource_id: str, diff_data: dict, drift_type: str) -> str:
30
+ api_key = os.environ.get("GROQ_API_KEY", "").strip()
31
+
32
+ if not api_key:
33
+ return "AI explanation unavailable: GROQ_API_KEY not set in environment."
34
+
35
+ url = "https://api.groq.com/openai/v1/chat/completions"
36
+ formatted_diff = json.dumps(diff_data, indent=2)
37
+
38
+ prompt = (
39
+ f"You are a strict AWS Cloud Security and Reliability expert. Analyze the following infrastructure drift:\n"
40
+ f"Resource Type: {resource_type}\n"
41
+ f"Resource ID: {resource_id}\n"
42
+ f"Drift Type: {drift_type}\n"
43
+ f"Diff Details:\n{formatted_diff}\n\n"
44
+ f"Provide a concise, plain-English summary (2-3 sentences max) explaining ONLY the security risks, "
45
+ f"compliance implications, or operational impact of this drift. "
46
+ f"Do NOT generate or guess CLI commands or Terraform scripts."
47
+ )
48
+
49
+ payload = {
50
+ "model": "llama-3.1-8b-instant",
51
+ "messages": [
52
+ {"role": "system", "content": "You are an AWS infrastructure and security analyst. Provide concise risk analyses only."},
53
+ {"role": "user", "content": prompt}
54
+ ],
55
+ "temperature": 0.2
56
+ }
57
+
58
+ headers = {
59
+ "Content-Type": "application/json",
60
+ "Authorization": f"Bearer {api_key}"
61
+ }
62
+
63
+ try:
64
+ response = requests.post(url, json=payload, headers=headers, timeout=15)
65
+ response.raise_for_status()
66
+ data = response.json()
67
+ return data.get("choices", [{}])[0].get("message", {}).get("content", "No risk analysis generated.")
68
+
69
+ except requests.exceptions.RequestException as req_err:
70
+ return f"AI API Network Error (Groq): {req_err}"
71
+ except Exception as e:
72
+ return f"AI API Error (Groq): {str(e)}"
drift_engine/models.py ADDED
@@ -0,0 +1,65 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+
4
+ class DriftType(Enum):
5
+ MISSING = "MISSING"
6
+ MODIFIED = "MODIFIED"
7
+ UNMANAGED = "UNMANAGED"
8
+
9
+ @dataclass
10
+ class DriftResult:
11
+ resource_type: str
12
+ resource_id: str
13
+ drift_type: DriftType
14
+ resource_name: str
15
+ tf_attributes: dict = field(default_factory=dict)
16
+ live_attributes: dict = field(default_factory=dict)
17
+ diff: dict = field(default_factory=dict)
18
+ ai_analysis: str = ""
19
+
20
+ MONITORED_ATTRIBUTES = {
21
+ "aws_instance": {"instance_type", "ami", "tags", "vpc_security_group_ids"},
22
+ "aws_security_group": {"ingress", "egress", "description"},
23
+ "aws_s3_bucket": {"bucket", "tags"},
24
+ "aws_db_instance": {"instance_class", "engine", "allocated_storage", "engine_version", "multi_az"},
25
+ "aws_lambda_function": {"runtime", "handler", "memory_size", "timeout", "role"},
26
+ "aws_iam_role": {"attached_policies", "path"}
27
+ }
28
+
29
+ MONITORED_RESOURCES = list(MONITORED_ATTRIBUTES.keys())
30
+
31
+ ATTRIBUTE_SEVERITY = {
32
+ "aws_security_group": {
33
+ "ingress": "CRITICAL",
34
+ "egress": "CRITICAL",
35
+ "description": "LOW"
36
+ },
37
+ "aws_instance": {
38
+ "instance_type": "HIGH",
39
+ "ami": "MEDIUM",
40
+ "vpc_security_group_ids": "HIGH",
41
+ "tags": "LOW"
42
+ },
43
+ "aws_iam_role": {
44
+ "attached_policies": "CRITICAL",
45
+ "path": "LOW"
46
+ },
47
+ "aws_db_instance": {
48
+ "instance_class": "HIGH",
49
+ "allocated_storage": "MEDIUM",
50
+ "engine": "HIGH",
51
+ "engine_version": "MEDIUM",
52
+ "multi_az": "HIGH"
53
+ },
54
+ "aws_s3_bucket": {
55
+ "bucket": "HIGH",
56
+ "tags": "LOW"
57
+ },
58
+ "aws_lambda_function": {
59
+ "runtime": "HIGH",
60
+ "handler": "HIGH",
61
+ "memory_size": "MEDIUM",
62
+ "timeout": "MEDIUM",
63
+ "role": "HIGH"
64
+ }
65
+ }