lambda-security-scanner 1.0.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.
- lambda_security_scanner/__init__.py +11 -0
- lambda_security_scanner/checks/__init__.py +0 -0
- lambda_security_scanner/checks/access_control.py +636 -0
- lambda_security_scanner/checks/base.py +104 -0
- lambda_security_scanner/checks/code_security.py +212 -0
- lambda_security_scanner/checks/function_config.py +454 -0
- lambda_security_scanner/checks/logging_monitoring.py +175 -0
- lambda_security_scanner/checks/network_security.py +207 -0
- lambda_security_scanner/cli.py +394 -0
- lambda_security_scanner/compliance.py +203 -0
- lambda_security_scanner/html_reporter.py +214 -0
- lambda_security_scanner/scanner.py +1154 -0
- lambda_security_scanner/templates/report.html +397 -0
- lambda_security_scanner/utils.py +191 -0
- lambda_security_scanner-1.0.0.dist-info/METADATA +497 -0
- lambda_security_scanner-1.0.0.dist-info/RECORD +20 -0
- lambda_security_scanner-1.0.0.dist-info/WHEEL +5 -0
- lambda_security_scanner-1.0.0.dist-info/entry_points.txt +2 -0
- lambda_security_scanner-1.0.0.dist-info/licenses/LICENSE +21 -0
- lambda_security_scanner-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Compliance Engine - 10 frameworks, 81 Lambda-mapped controls.
|
|
2
|
+
|
|
3
|
+
Evaluates function check results against compliance framework controls.
|
|
4
|
+
Each control is a lambda that reads from the checks dict built during
|
|
5
|
+
scan_function(). The checks dict key names are the contract between
|
|
6
|
+
checkers and compliance.
|
|
7
|
+
|
|
8
|
+
Frameworks: AWS-FSBP, CIS, PCI-DSS-v4.0.1, HIPAA, SOC2,
|
|
9
|
+
ISO27001, ISO27017, ISO27018, GDPR, NIST-800-53
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ComplianceChecker:
|
|
16
|
+
"""Evaluate function security checks against 10 frameworks."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize compliance checker with all frameworks."""
|
|
20
|
+
self.frameworks = {}
|
|
21
|
+
self._define_frameworks()
|
|
22
|
+
|
|
23
|
+
def _define_frameworks(self):
|
|
24
|
+
"""Define all 10 compliance frameworks."""
|
|
25
|
+
self.frameworks = {
|
|
26
|
+
"AWS-FSBP": {
|
|
27
|
+
"name": "AWS Foundational Security Best Practices",
|
|
28
|
+
"controls": {
|
|
29
|
+
"Lambda.1": {"description": "Function policies should prohibit public access", "severity": "CRITICAL", "check": lambda r: not r.get("resource_policy", {}).get("is_public", True)},
|
|
30
|
+
"Lambda.2": {"description": "Functions should use supported runtimes", "severity": "MEDIUM", "check": lambda r: r.get("runtime", {}).get("status", "blocked") == "supported"},
|
|
31
|
+
"Lambda.3": {"description": "Functions should be in a VPC", "severity": "LOW", "check": lambda r: r.get("vpc_config", {}).get("in_vpc", False)},
|
|
32
|
+
"Lambda.5": {"description": "VPC Lambda functions should operate in multiple AZs", "severity": "MEDIUM", "check": lambda r: r.get("multi_az", {}).get("is_multi_az", False) if r.get("vpc_config", {}).get("in_vpc", False) else True},
|
|
33
|
+
"Lambda.7": {"description": "Functions should have X-Ray active tracing enabled", "severity": "LOW", "check": lambda r: r.get("tracing", {}).get("enabled", False)},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
"CIS": {
|
|
37
|
+
"name": "CIS AWS Compute Services Benchmark",
|
|
38
|
+
"controls": {
|
|
39
|
+
"CIS-Lambda.1": {"description": "Functions should use supported runtimes", "severity": "HIGH", "check": lambda r: r.get("runtime", {}).get("status", "blocked") in ("supported", "near_eol")},
|
|
40
|
+
"CIS-Lambda.2": {"description": "Functions should not be publicly accessible", "severity": "CRITICAL", "check": lambda r: not r.get("resource_policy", {}).get("is_public", True) and not r.get("function_url", {}).get("is_public", True)},
|
|
41
|
+
"CIS-Lambda.3": {"description": "Execution roles should follow least privilege", "severity": "HIGH", "check": lambda r: not r.get("execution_role", {}).get("has_admin_access", True) and not r.get("execution_role", {}).get("has_wildcard_actions", True)},
|
|
42
|
+
"CIS-Lambda.4": {"description": "Functions should have dead-letter queues", "severity": "MEDIUM", "check": lambda r: r.get("dead_letter_config", {}).get("configured", False)},
|
|
43
|
+
"CIS-Lambda.5": {"description": "Functions should be deployed in a VPC", "severity": "LOW", "check": lambda r: r.get("vpc_config", {}).get("in_vpc", False)},
|
|
44
|
+
"CIS-Lambda.6": {"description": "Functions should have X-Ray tracing enabled", "severity": "MEDIUM", "check": lambda r: r.get("tracing", {}).get("enabled", False)},
|
|
45
|
+
"CIS-Lambda.7": {"description": "Env vars should not contain sensitive data", "severity": "CRITICAL", "check": lambda r: not r.get("environment_secrets", {}).get("has_secrets", True)},
|
|
46
|
+
"CIS-Lambda.8": {"description": "Functions should have code signing enabled (N/A for container images)", "severity": "MEDIUM", "check": lambda r: True if not r.get("code_signing", {}).get("applicable", True) else r.get("code_signing", {}).get("configured", False)},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
"PCI-DSS-v4.0.1": {
|
|
50
|
+
"name": "PCI DSS v4.0.1",
|
|
51
|
+
"controls": {
|
|
52
|
+
"PCI-Lambda.6.3.3": {"description": "Security patches/updates installed (supported runtimes)", "severity": "HIGH", "check": lambda r: r.get("runtime", {}).get("status", "blocked") in ("supported", "near_eol")},
|
|
53
|
+
"PCI-Lambda.8.6.2": {"description": "Application/system account secrets not hard-coded in config (no plaintext secrets in env vars)", "severity": "CRITICAL", "check": lambda r: not r.get("environment_secrets", {}).get("has_secrets", True)},
|
|
54
|
+
"PCI-Lambda.7.2.1": {"description": "Access roles defined with least privilege", "severity": "HIGH", "check": lambda r: not r.get("execution_role", {}).get("has_admin_access", True) and not r.get("execution_role", {}).get("has_wildcard_actions", True)},
|
|
55
|
+
"PCI-Lambda.1.4.1": {"description": "NSC implemented between trusted and untrusted networks (no public resource policy)", "severity": "CRITICAL", "check": lambda r: not r.get("resource_policy", {}).get("is_public", True)},
|
|
56
|
+
"PCI-Lambda.8.3.1": {"description": "User access to system components is authenticated (function URL auth required)", "severity": "CRITICAL", "check": lambda r: not r.get("function_url", {}).get("is_public", True)},
|
|
57
|
+
"PCI-Lambda.1.3.2": {"description": "Outbound traffic from the CDE is restricted (VPC SG egress)", "severity": "MEDIUM", "check": lambda r: not r.get("security_groups", {}).get("unrestricted_egress", True) if r.get("security_groups", {}).get("applicable", False) else True},
|
|
58
|
+
"PCI-Lambda.10.2.1": {"description": "Audit logging enabled (tracing)", "severity": "MEDIUM", "check": lambda r: r.get("tracing", {}).get("enabled", False)},
|
|
59
|
+
"PCI-Lambda.10.5.1": {"description": "Audit log history retained for at least 12 months (retention set)", "severity": "MEDIUM", "check": lambda r: r.get("log_group", {}).get("exists", False) and r.get("log_group", {}).get("has_retention", False)},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
"HIPAA": {
|
|
63
|
+
"name": "HIPAA Security Rule",
|
|
64
|
+
"controls": {
|
|
65
|
+
"164.312(a)(1)-ACCESS": {"description": "Access control - prohibit public access to ePHI", "severity": "CRITICAL", "check": lambda r: not r.get("resource_policy", {}).get("is_public", True)},
|
|
66
|
+
"164.312(a)(1)-URL": {"description": "Access control - function URL authentication", "severity": "CRITICAL", "check": lambda r: not r.get("function_url", {}).get("is_public", True)},
|
|
67
|
+
"164.312(a)(1)-SECRETS": {"description": "No PHI/secrets in env vars", "severity": "CRITICAL", "check": lambda r: not r.get("environment_secrets", {}).get("has_secrets", True)},
|
|
68
|
+
"164.312(a)(1)-IAM": {"description": "Access control - least privilege execution roles", "severity": "HIGH", "check": lambda r: not r.get("execution_role", {}).get("has_admin_access", True) and not r.get("execution_role", {}).get("has_wildcard_actions", True)},
|
|
69
|
+
"164.312(b)-TRACING": {"description": "Audit controls - tracing enabled", "severity": "MEDIUM", "check": lambda r: r.get("tracing", {}).get("enabled", False)},
|
|
70
|
+
"164.312(b)-LOGGING": {"description": "Audit controls - log retention", "severity": "MEDIUM", "check": lambda r: r.get("log_group", {}).get("exists", False) and r.get("log_group", {}).get("has_retention", False)},
|
|
71
|
+
"164.308(a)(7)-DLQ": {"description": "Contingency plan - dead letter queue for failure capture", "severity": "MEDIUM", "check": lambda r: r.get("dead_letter_config", {}).get("configured", False)},
|
|
72
|
+
"164.312(c)(1)-SIGNING": {"description": "Integrity - code signing prevents improper alteration (N/A for container images)", "severity": "MEDIUM", "check": lambda r: True if not r.get("code_signing", {}).get("applicable", True) else r.get("code_signing", {}).get("configured", False)},
|
|
73
|
+
"164.308(a)(5)(ii)(B)-RUNTIME": {"description": "Protection from malicious software - use supported/patched runtimes", "severity": "HIGH", "check": lambda r: r.get("runtime", {}).get("status", "blocked") in ("supported", "near_eol")},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
"SOC2": {
|
|
77
|
+
"name": "SOC 2 Type II",
|
|
78
|
+
"controls": {
|
|
79
|
+
"SOC2-CC6.1-ACCESS": {"description": "Restrict public access to functions", "severity": "CRITICAL", "check": lambda r: not r.get("resource_policy", {}).get("is_public", True)},
|
|
80
|
+
"SOC2-CC6.1-URL": {"description": "Function URL authentication required", "severity": "CRITICAL", "check": lambda r: not r.get("function_url", {}).get("is_public", True)},
|
|
81
|
+
"SOC2-CC6.1-IAM": {"description": "Least privilege execution roles", "severity": "HIGH", "check": lambda r: not r.get("execution_role", {}).get("has_admin_access", True) and not r.get("execution_role", {}).get("has_wildcard_actions", True)},
|
|
82
|
+
"SOC2-CC6.1-ROLE": {"description": "Unique execution roles per function", "severity": "HIGH", "check": lambda r: not r.get("shared_role", {}).get("is_shared", True)},
|
|
83
|
+
"SOC2-CC6.8-SIGNING": {"description": "Code signing for software integrity (N/A for container images)", "severity": "MEDIUM", "check": lambda r: True if not r.get("code_signing", {}).get("applicable", True) else r.get("code_signing", {}).get("configured", False)},
|
|
84
|
+
"SOC2-CC6.8-RUNTIME": {"description": "Use current supported runtimes", "severity": "HIGH", "check": lambda r: r.get("runtime", {}).get("status", "blocked") in ("supported", "near_eol")},
|
|
85
|
+
"SOC2-CC7.1-LOGGING": {"description": "CloudWatch log retention configured", "severity": "MEDIUM", "check": lambda r: r.get("log_group", {}).get("exists", False) and r.get("log_group", {}).get("has_retention", False)},
|
|
86
|
+
"SOC2-CC7.2-TRACING": {"description": "X-Ray tracing for anomaly detection", "severity": "MEDIUM", "check": lambda r: r.get("tracing", {}).get("enabled", False)},
|
|
87
|
+
"SOC2-CC7.3-DLQ": {"description": "Dead letter queue for failure capture", "severity": "MEDIUM", "check": lambda r: r.get("dead_letter_config", {}).get("configured", False)},
|
|
88
|
+
"SOC2-CC7.3-ESM": {"description": "ESM failure destinations configured", "severity": "MEDIUM", "check": lambda r: r.get("event_source_mappings", {}).get("missing_failure_dest_count", 1) == 0 if r.get("event_source_mappings", {}).get("has_mappings", False) else True},
|
|
89
|
+
"SOC2-A1.1-CONCUR": {"description": "Reserved concurrency for availability", "severity": "MEDIUM", "check": lambda r: r.get("reserved_concurrency", {}).get("configured", False)},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
"ISO27001": {
|
|
93
|
+
"name": "ISO 27001:2022",
|
|
94
|
+
"controls": {
|
|
95
|
+
"A.5.15": {"description": "Access control - restrict public access", "severity": "CRITICAL", "check": lambda r: not r.get("resource_policy", {}).get("is_public", True)},
|
|
96
|
+
"A.5.21": {"description": "Information security in the ICT supply chain - external layer verification", "severity": "MEDIUM", "check": lambda r: not r.get("layers", {}).get("has_external_layers", True)},
|
|
97
|
+
"A.8.2": {"description": "Privileged access rights - unique execution role per function", "severity": "HIGH", "check": lambda r: not r.get("shared_role", {}).get("is_shared", True)},
|
|
98
|
+
"A.8.3": {"description": "Information access restriction - least privilege", "severity": "HIGH", "check": lambda r: not r.get("execution_role", {}).get("has_admin_access", True) and not r.get("execution_role", {}).get("has_wildcard_actions", True)},
|
|
99
|
+
"A.8.5": {"description": "Secure authentication - function URL auth", "severity": "CRITICAL", "check": lambda r: not r.get("function_url", {}).get("is_public", True)},
|
|
100
|
+
"A.8.7": {"description": "Protection against malware - code signing (N/A for container images)", "severity": "MEDIUM", "check": lambda r: True if not r.get("code_signing", {}).get("applicable", True) else r.get("code_signing", {}).get("configured", False)},
|
|
101
|
+
"A.8.12": {"description": "Data leakage prevention - no secrets in env vars", "severity": "CRITICAL", "check": lambda r: not r.get("environment_secrets", {}).get("has_secrets", True)},
|
|
102
|
+
"A.8.15-T": {"description": "Logging - tracing enabled", "severity": "MEDIUM", "check": lambda r: r.get("tracing", {}).get("enabled", False)},
|
|
103
|
+
"A.8.15-L": {"description": "Logging - log group retention configured", "severity": "MEDIUM", "check": lambda r: r.get("log_group", {}).get("exists", False) and r.get("log_group", {}).get("has_retention", False)},
|
|
104
|
+
"A.8.20": {"description": "Network security - VPC configuration", "severity": "LOW", "check": lambda r: r.get("vpc_config", {}).get("in_vpc", False)},
|
|
105
|
+
"A.8.24": {"description": "Use of cryptography - KMS on env vars", "severity": "MEDIUM", "check": lambda r: r.get("environment_secrets", {}).get("has_kms_key", False) if r.get("environment_secrets", {}).get("has_env_vars", False) else True},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
"ISO27017": {
|
|
109
|
+
"name": "ISO 27017 Cloud Security",
|
|
110
|
+
"controls": {
|
|
111
|
+
"CLD.9.5.1": {"description": "Segregation in virtual environments - VPC", "severity": "LOW", "check": lambda r: r.get("vpc_config", {}).get("in_vpc", False)},
|
|
112
|
+
"CLD.9.5.2": {"description": "Virtual machine hardening - runtime security", "severity": "HIGH", "check": lambda r: r.get("runtime", {}).get("status", "blocked") in ("supported", "near_eol")},
|
|
113
|
+
"CLD.12.1.5": {"description": "Administrator operational security - logging", "severity": "MEDIUM", "check": lambda r: r.get("log_group", {}).get("exists", False) and r.get("log_group", {}).get("has_retention", False)},
|
|
114
|
+
"CLD.12.4.5": {"description": "Monitoring of cloud services - tracing", "severity": "MEDIUM", "check": lambda r: r.get("tracing", {}).get("enabled", False)},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
"ISO27018": {
|
|
118
|
+
"name": "ISO 27018 PII Protection",
|
|
119
|
+
"controls": {
|
|
120
|
+
"ISO27018-ENC": {"description": "Encryption of PII - KMS on env vars", "severity": "CRITICAL", "check": lambda r: r.get("environment_secrets", {}).get("has_kms_key", False) if r.get("environment_secrets", {}).get("has_env_vars", False) else True},
|
|
121
|
+
"ISO27018-ACCESS": {"description": "Access to data - restrict public function access", "severity": "CRITICAL", "check": lambda r: not r.get("resource_policy", {}).get("is_public", True)},
|
|
122
|
+
"ISO27018-AUTH": {"description": "Secure transmission - function URL authentication", "severity": "CRITICAL", "check": lambda r: not r.get("function_url", {}).get("is_public", True)},
|
|
123
|
+
"ISO27018-TRACE": {"description": "Audit logging - tracing enabled", "severity": "MEDIUM", "check": lambda r: r.get("tracing", {}).get("enabled", False)},
|
|
124
|
+
"ISO27018-LOG": {"description": "Audit logging - log retention configured", "severity": "MEDIUM", "check": lambda r: r.get("log_group", {}).get("exists", False) and r.get("log_group", {}).get("has_retention", False)},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
"GDPR": {
|
|
128
|
+
"name": "General Data Protection Regulation",
|
|
129
|
+
"controls": {
|
|
130
|
+
"GDPR-Art5": {"description": "Data integrity and confidentiality - no secrets in env vars", "severity": "CRITICAL", "check": lambda r: not r.get("environment_secrets", {}).get("has_secrets", True)},
|
|
131
|
+
"GDPR-Art25": {"description": "Data protection by design - least privilege execution role", "severity": "HIGH", "check": lambda r: not r.get("execution_role", {}).get("has_admin_access", True) and not r.get("execution_role", {}).get("has_wildcard_actions", True)},
|
|
132
|
+
"GDPR-Art32-1a-KMS": {"description": "Art 32(1)(a) - encryption of personal data (KMS on env vars)", "severity": "HIGH", "check": lambda r: r.get("environment_secrets", {}).get("has_kms_key", False) if r.get("environment_secrets", {}).get("has_env_vars", False) else True},
|
|
133
|
+
"GDPR-Art32-1b-ACCESS": {"description": "Art 32(1)(b) - confidentiality (restrict public access)", "severity": "CRITICAL", "check": lambda r: not r.get("resource_policy", {}).get("is_public", True)},
|
|
134
|
+
"GDPR-Art32-1b-CONCUR": {"description": "Art 32(1)(b) - resilience (reserved concurrency)", "severity": "MEDIUM", "check": lambda r: r.get("reserved_concurrency", {}).get("configured", False)},
|
|
135
|
+
"GDPR-Art32-1b-ESM": {"description": "Art 32(1)(b) - resilience (ESM failure destinations)", "severity": "MEDIUM", "check": lambda r: r.get("event_source_mappings", {}).get("missing_failure_dest_count", 1) == 0 if r.get("event_source_mappings", {}).get("has_mappings", False) else True},
|
|
136
|
+
"GDPR-Art32-1b-TRACE": {"description": "Art 32(1)(b) - integrity signal (X-Ray tracing)", "severity": "MEDIUM", "check": lambda r: r.get("tracing", {}).get("enabled", False)},
|
|
137
|
+
"GDPR-Art32-1b-LOG": {"description": "Art 32(1)(b) - availability/integrity (CloudWatch log retention for forensic analysis)", "severity": "MEDIUM", "check": lambda r: r.get("log_group", {}).get("exists", False) and r.get("log_group", {}).get("has_retention", False)},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
"NIST-800-53": {
|
|
141
|
+
"name": "NIST 800-53 Rev5",
|
|
142
|
+
"controls": {
|
|
143
|
+
"AC-3": {"description": "Access enforcement - no public access", "severity": "CRITICAL", "check": lambda r: not r.get("resource_policy", {}).get("is_public", True)},
|
|
144
|
+
"AC-6": {"description": "Least privilege - execution role", "severity": "HIGH", "check": lambda r: not r.get("execution_role", {}).get("has_admin_access", True) and not r.get("execution_role", {}).get("has_wildcard_actions", True)},
|
|
145
|
+
"AC-17": {"description": "Remote access - function URL auth", "severity": "CRITICAL", "check": lambda r: not r.get("function_url", {}).get("is_public", True)},
|
|
146
|
+
"AU-2": {"description": "Event logging - tracing enabled", "severity": "MEDIUM", "check": lambda r: r.get("tracing", {}).get("enabled", False)},
|
|
147
|
+
"AU-9": {"description": "Protection of audit info - log retention", "severity": "MEDIUM", "check": lambda r: r.get("log_group", {}).get("exists", False) and r.get("log_group", {}).get("has_retention", False)},
|
|
148
|
+
"CM-7": {"description": "Least functionality - no deprecated runtimes", "severity": "HIGH", "check": lambda r: r.get("runtime", {}).get("status", "blocked") in ("supported", "near_eol")},
|
|
149
|
+
"IA-5": {"description": "Authenticator management - no secrets in env vars", "severity": "CRITICAL", "check": lambda r: not r.get("environment_secrets", {}).get("has_secrets", True)},
|
|
150
|
+
"SC-5": {"description": "DoS protection - reserved concurrency", "severity": "MEDIUM", "check": lambda r: r.get("reserved_concurrency", {}).get("configured", False)},
|
|
151
|
+
"SC-7": {"description": "Boundary protection - VPC configuration", "severity": "LOW", "check": lambda r: r.get("vpc_config", {}).get("in_vpc", False)},
|
|
152
|
+
"SC-7(5)": {"description": "Boundary protection - deny by default / allow by exception (SG egress controls)", "severity": "MEDIUM", "check": lambda r: not r.get("security_groups", {}).get("unrestricted_egress", True) if r.get("security_groups", {}).get("applicable", False) else True},
|
|
153
|
+
"SI-7": {"description": "Software integrity - code signing (N/A for container images)", "severity": "MEDIUM", "check": lambda r: True if not r.get("code_signing", {}).get("applicable", True) else r.get("code_signing", {}).get("configured", False)},
|
|
154
|
+
"SR-3": {"description": "Supply chain protection - layer verification", "severity": "MEDIUM", "check": lambda r: not r.get("layers", {}).get("has_external_layers", True)},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
def check_function_compliance(
|
|
160
|
+
self, checks: Dict[str, Any]
|
|
161
|
+
) -> Dict[str, Any]:
|
|
162
|
+
"""Evaluate all frameworks against function checks."""
|
|
163
|
+
results = {}
|
|
164
|
+
for fw_key, framework in self.frameworks.items():
|
|
165
|
+
results[fw_key] = self._check_framework(
|
|
166
|
+
checks, framework
|
|
167
|
+
)
|
|
168
|
+
return results
|
|
169
|
+
|
|
170
|
+
def _check_framework(
|
|
171
|
+
self, checks: Dict[str, Any], framework: Dict
|
|
172
|
+
) -> Dict[str, Any]:
|
|
173
|
+
"""Evaluate a single framework against check results."""
|
|
174
|
+
passed = []
|
|
175
|
+
failed = []
|
|
176
|
+
for ctrl_id, ctrl in framework["controls"].items():
|
|
177
|
+
try:
|
|
178
|
+
result = ctrl["check"](checks)
|
|
179
|
+
except Exception:
|
|
180
|
+
result = False # fail-closed
|
|
181
|
+
entry = {
|
|
182
|
+
"control_id": ctrl_id,
|
|
183
|
+
"description": ctrl["description"],
|
|
184
|
+
"severity": ctrl["severity"],
|
|
185
|
+
}
|
|
186
|
+
if result:
|
|
187
|
+
passed.append(entry)
|
|
188
|
+
else:
|
|
189
|
+
failed.append(entry)
|
|
190
|
+
total = len(passed) + len(failed)
|
|
191
|
+
return {
|
|
192
|
+
"is_compliant": len(failed) == 0,
|
|
193
|
+
"passed_controls": len(passed),
|
|
194
|
+
"failed_controls": len(failed),
|
|
195
|
+
"total_controls": total,
|
|
196
|
+
"compliance_percentage": round(
|
|
197
|
+
len(passed) / total * 100, 1
|
|
198
|
+
)
|
|
199
|
+
if total > 0
|
|
200
|
+
else 0,
|
|
201
|
+
"passed": passed,
|
|
202
|
+
"failed": failed,
|
|
203
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""HTML report generator for Lambda Security Scanner."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
from jinja2 import (
|
|
8
|
+
Environment,
|
|
9
|
+
FileSystemLoader,
|
|
10
|
+
select_autoescape,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .utils import format_datetime, get_severity_color
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HTMLReporter:
|
|
17
|
+
"""Generate HTML dashboard reports."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, template_dir: str = None):
|
|
20
|
+
if template_dir is None:
|
|
21
|
+
template_dir = os.path.join(
|
|
22
|
+
os.path.dirname(__file__), "templates"
|
|
23
|
+
)
|
|
24
|
+
self.env = Environment(
|
|
25
|
+
loader=FileSystemLoader(template_dir),
|
|
26
|
+
autoescape=select_autoescape(
|
|
27
|
+
["html", "xml"]
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
self.env.filters["format_datetime"] = (
|
|
31
|
+
format_datetime
|
|
32
|
+
)
|
|
33
|
+
self.env.filters["get_severity_color"] = (
|
|
34
|
+
get_severity_color
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def _calculate_chart_data(
|
|
38
|
+
self, results: List[Dict[str, Any]]
|
|
39
|
+
) -> Dict[str, Any]:
|
|
40
|
+
valid = [
|
|
41
|
+
r
|
|
42
|
+
for r in results
|
|
43
|
+
if not r.get("scan_error", False)
|
|
44
|
+
]
|
|
45
|
+
if not valid:
|
|
46
|
+
return {
|
|
47
|
+
"security_score_distribution": [
|
|
48
|
+
0, 0, 0, 0, 0,
|
|
49
|
+
],
|
|
50
|
+
"compliance_labels": [],
|
|
51
|
+
"compliance_percentages": [],
|
|
52
|
+
"severity_counts": [0, 0, 0, 0, 0],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
score_ranges = [0, 0, 0, 0, 0]
|
|
56
|
+
for r in valid:
|
|
57
|
+
score = r.get("security_score", 0) or 0
|
|
58
|
+
if score <= 20:
|
|
59
|
+
score_ranges[0] += 1
|
|
60
|
+
elif score <= 40:
|
|
61
|
+
score_ranges[1] += 1
|
|
62
|
+
elif score <= 60:
|
|
63
|
+
score_ranges[2] += 1
|
|
64
|
+
elif score <= 80:
|
|
65
|
+
score_ranges[3] += 1
|
|
66
|
+
else:
|
|
67
|
+
score_ranges[4] += 1
|
|
68
|
+
|
|
69
|
+
frameworks = [
|
|
70
|
+
"AWS-FSBP",
|
|
71
|
+
"CIS",
|
|
72
|
+
"PCI-DSS-v4.0.1",
|
|
73
|
+
"HIPAA",
|
|
74
|
+
"SOC2",
|
|
75
|
+
"ISO27001",
|
|
76
|
+
"ISO27017",
|
|
77
|
+
"ISO27018",
|
|
78
|
+
"GDPR",
|
|
79
|
+
"NIST-800-53",
|
|
80
|
+
]
|
|
81
|
+
compliance_labels = []
|
|
82
|
+
compliance_percentages = []
|
|
83
|
+
for fw in frameworks:
|
|
84
|
+
pcts = []
|
|
85
|
+
for r in valid:
|
|
86
|
+
fw_status = r.get(
|
|
87
|
+
"compliance_status", {}
|
|
88
|
+
).get(fw, {})
|
|
89
|
+
if fw_status:
|
|
90
|
+
pcts.append(
|
|
91
|
+
fw_status.get(
|
|
92
|
+
"compliance_percentage", 0
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
if pcts:
|
|
96
|
+
compliance_labels.append(fw)
|
|
97
|
+
compliance_percentages.append(
|
|
98
|
+
round(sum(pcts) / len(pcts), 1)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
severity_counts = {
|
|
102
|
+
"CRITICAL": 0,
|
|
103
|
+
"HIGH": 0,
|
|
104
|
+
"MEDIUM": 0,
|
|
105
|
+
"LOW": 0,
|
|
106
|
+
"INFO": 0,
|
|
107
|
+
}
|
|
108
|
+
for r in valid:
|
|
109
|
+
for issue in r.get("issues", []):
|
|
110
|
+
sev = issue.get("severity", "INFO")
|
|
111
|
+
if sev in severity_counts:
|
|
112
|
+
severity_counts[sev] += 1
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"security_score_distribution": score_ranges,
|
|
116
|
+
"compliance_labels": compliance_labels,
|
|
117
|
+
"compliance_percentages": (
|
|
118
|
+
compliance_percentages
|
|
119
|
+
),
|
|
120
|
+
"severity_counts": [
|
|
121
|
+
severity_counts["CRITICAL"],
|
|
122
|
+
severity_counts["HIGH"],
|
|
123
|
+
severity_counts["MEDIUM"],
|
|
124
|
+
severity_counts["LOW"],
|
|
125
|
+
severity_counts["INFO"],
|
|
126
|
+
],
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def _calculate_compliance_summary(
|
|
130
|
+
self, results: List[Dict[str, Any]]
|
|
131
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
132
|
+
if not results:
|
|
133
|
+
return {}
|
|
134
|
+
frameworks = [
|
|
135
|
+
"AWS-FSBP",
|
|
136
|
+
"CIS",
|
|
137
|
+
"PCI-DSS-v4.0.1",
|
|
138
|
+
"HIPAA",
|
|
139
|
+
"SOC2",
|
|
140
|
+
"ISO27001",
|
|
141
|
+
"ISO27017",
|
|
142
|
+
"ISO27018",
|
|
143
|
+
"GDPR",
|
|
144
|
+
"NIST-800-53",
|
|
145
|
+
]
|
|
146
|
+
summary = {}
|
|
147
|
+
for fw in frameworks:
|
|
148
|
+
compliant = 0
|
|
149
|
+
total = 0
|
|
150
|
+
pcts = []
|
|
151
|
+
for r in results:
|
|
152
|
+
fw_status = r.get(
|
|
153
|
+
"compliance_status", {}
|
|
154
|
+
).get(fw, {})
|
|
155
|
+
if fw_status:
|
|
156
|
+
total += 1
|
|
157
|
+
if fw_status.get(
|
|
158
|
+
"is_compliant", False
|
|
159
|
+
):
|
|
160
|
+
compliant += 1
|
|
161
|
+
pcts.append(
|
|
162
|
+
fw_status.get(
|
|
163
|
+
"compliance_percentage", 0
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
if total > 0:
|
|
167
|
+
summary[fw] = {
|
|
168
|
+
"compliant_functions": compliant,
|
|
169
|
+
"total_functions": total,
|
|
170
|
+
"non_compliant_functions": (
|
|
171
|
+
total - compliant
|
|
172
|
+
),
|
|
173
|
+
"compliance_percentage": round(
|
|
174
|
+
compliant / total * 100, 1
|
|
175
|
+
),
|
|
176
|
+
"average_compliance_percentage": (
|
|
177
|
+
round(sum(pcts) / len(pcts), 1)
|
|
178
|
+
if pcts
|
|
179
|
+
else 0
|
|
180
|
+
),
|
|
181
|
+
}
|
|
182
|
+
return summary
|
|
183
|
+
|
|
184
|
+
def generate_report(
|
|
185
|
+
self,
|
|
186
|
+
results: List[Dict[str, Any]],
|
|
187
|
+
summary: Dict[str, Any],
|
|
188
|
+
output_file: str,
|
|
189
|
+
) -> str:
|
|
190
|
+
template = self.env.get_template("report.html")
|
|
191
|
+
valid_results = [
|
|
192
|
+
r
|
|
193
|
+
for r in results
|
|
194
|
+
if not r.get("scan_error", False)
|
|
195
|
+
]
|
|
196
|
+
chart_data = self._calculate_chart_data(
|
|
197
|
+
valid_results
|
|
198
|
+
)
|
|
199
|
+
compliance_summary = (
|
|
200
|
+
self._calculate_compliance_summary(
|
|
201
|
+
valid_results
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
html_content = template.render(
|
|
205
|
+
summary=summary,
|
|
206
|
+
results=valid_results,
|
|
207
|
+
compliance_summary=compliance_summary,
|
|
208
|
+
**chart_data,
|
|
209
|
+
)
|
|
210
|
+
with open(
|
|
211
|
+
output_file, "w", encoding="utf-8"
|
|
212
|
+
) as f:
|
|
213
|
+
f.write(html_content)
|
|
214
|
+
return output_file
|