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,207 @@
|
|
|
1
|
+
"""Network security checks for Lambda functions (C.1-C.3)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, List
|
|
5
|
+
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from .base import BaseChecker
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("lambda_security_scanner")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NetworkSecurityChecker(BaseChecker):
|
|
14
|
+
"""Check network security configuration for Lambda functions.
|
|
15
|
+
|
|
16
|
+
Implements checks C.1 (VPC config), C.2 (multi-AZ),
|
|
17
|
+
and C.3 (security group egress).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def check_vpc_config(
|
|
21
|
+
self, function_config: Dict
|
|
22
|
+
) -> Dict:
|
|
23
|
+
"""C.1 - Check if function has VPC configuration.
|
|
24
|
+
|
|
25
|
+
Extracts VPC configuration from the function config dict.
|
|
26
|
+
No API call required.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
function_config: Lambda function configuration dict.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Dict with in_vpc, vpc_id, subnet_count,
|
|
33
|
+
subnet_ids, security_group_count,
|
|
34
|
+
security_group_ids.
|
|
35
|
+
"""
|
|
36
|
+
vpc_config = function_config.get("VpcConfig", {})
|
|
37
|
+
subnet_ids = vpc_config.get("SubnetIds", [])
|
|
38
|
+
security_group_ids = vpc_config.get(
|
|
39
|
+
"SecurityGroupIds", []
|
|
40
|
+
)
|
|
41
|
+
vpc_id = vpc_config.get("VpcId")
|
|
42
|
+
|
|
43
|
+
in_vpc = bool(subnet_ids and security_group_ids)
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"in_vpc": in_vpc,
|
|
47
|
+
"vpc_id": vpc_id if in_vpc else None,
|
|
48
|
+
"subnet_count": len(subnet_ids),
|
|
49
|
+
"subnet_ids": subnet_ids,
|
|
50
|
+
"security_group_count": len(security_group_ids),
|
|
51
|
+
"security_group_ids": security_group_ids,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def check_multi_az(
|
|
55
|
+
self, vpc_result: Dict, region: str
|
|
56
|
+
) -> Dict:
|
|
57
|
+
"""C.2 - Check if VPC Lambda uses multiple AZs.
|
|
58
|
+
|
|
59
|
+
Only applicable if the function is in a VPC.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
vpc_result: Result from check_vpc_config.
|
|
63
|
+
region: AWS region name.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Dict with applicable, is_multi_az, az_count,
|
|
67
|
+
availability_zones.
|
|
68
|
+
"""
|
|
69
|
+
if not vpc_result.get("in_vpc"):
|
|
70
|
+
return {
|
|
71
|
+
"applicable": False,
|
|
72
|
+
"is_multi_az": False,
|
|
73
|
+
"az_count": 0,
|
|
74
|
+
"availability_zones": [],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
subnet_ids = vpc_result.get("subnet_ids", [])
|
|
78
|
+
if not subnet_ids:
|
|
79
|
+
return {
|
|
80
|
+
"applicable": True,
|
|
81
|
+
"is_multi_az": False,
|
|
82
|
+
"az_count": 0,
|
|
83
|
+
"availability_zones": [],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
ec2 = self.get_client("ec2", region)
|
|
88
|
+
response = ec2.describe_subnets(
|
|
89
|
+
SubnetIds=subnet_ids
|
|
90
|
+
)
|
|
91
|
+
azs = list(
|
|
92
|
+
{
|
|
93
|
+
s["AvailabilityZone"]
|
|
94
|
+
for s in response.get("Subnets", [])
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
return {
|
|
98
|
+
"applicable": True,
|
|
99
|
+
"is_multi_az": len(azs) > 1,
|
|
100
|
+
"az_count": len(azs),
|
|
101
|
+
"availability_zones": sorted(azs),
|
|
102
|
+
}
|
|
103
|
+
except ClientError as e:
|
|
104
|
+
return self.handle_client_error(
|
|
105
|
+
e,
|
|
106
|
+
{
|
|
107
|
+
"applicable": True,
|
|
108
|
+
"is_multi_az": False,
|
|
109
|
+
"az_count": 0,
|
|
110
|
+
"availability_zones": [],
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def check_security_groups(
|
|
115
|
+
self, vpc_result: Dict, region: str
|
|
116
|
+
) -> Dict:
|
|
117
|
+
"""C.3 - Check for unrestricted security group egress.
|
|
118
|
+
|
|
119
|
+
Only applicable if the function is in a VPC.
|
|
120
|
+
Flags rules with IpProtocol="-1" and
|
|
121
|
+
CidrIp="0.0.0.0/0" or CidrIpv6="::/0".
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
vpc_result: Result from check_vpc_config.
|
|
125
|
+
region: AWS region name.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dict with applicable, unrestricted_egress,
|
|
129
|
+
security_groups.
|
|
130
|
+
"""
|
|
131
|
+
if not vpc_result.get("in_vpc"):
|
|
132
|
+
return {
|
|
133
|
+
"applicable": False,
|
|
134
|
+
"unrestricted_egress": False,
|
|
135
|
+
"security_groups": [],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
sg_ids = vpc_result.get("security_group_ids", [])
|
|
139
|
+
if not sg_ids:
|
|
140
|
+
return {
|
|
141
|
+
"applicable": True,
|
|
142
|
+
"unrestricted_egress": False,
|
|
143
|
+
"security_groups": [],
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
ec2 = self.get_client("ec2", region)
|
|
148
|
+
response = ec2.describe_security_groups(
|
|
149
|
+
GroupIds=sg_ids
|
|
150
|
+
)
|
|
151
|
+
except ClientError as e:
|
|
152
|
+
return self.handle_client_error(
|
|
153
|
+
e,
|
|
154
|
+
{
|
|
155
|
+
"applicable": True,
|
|
156
|
+
"unrestricted_egress": False,
|
|
157
|
+
"security_groups": [],
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
security_groups = []
|
|
162
|
+
unrestricted_egress = False
|
|
163
|
+
|
|
164
|
+
for sg in response.get("SecurityGroups", []):
|
|
165
|
+
sg_info = {
|
|
166
|
+
"group_id": sg.get("GroupId"),
|
|
167
|
+
"group_name": sg.get("GroupName"),
|
|
168
|
+
"unrestricted_egress_rules": [],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for rule in sg.get("IpPermissionsEgress", []):
|
|
172
|
+
if rule.get("IpProtocol") != "-1":
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
for ip_range in rule.get(
|
|
176
|
+
"IpRanges", []
|
|
177
|
+
):
|
|
178
|
+
if (
|
|
179
|
+
ip_range.get("CidrIp")
|
|
180
|
+
== "0.0.0.0/0"
|
|
181
|
+
):
|
|
182
|
+
sg_info[
|
|
183
|
+
"unrestricted_egress_rules"
|
|
184
|
+
].append(rule)
|
|
185
|
+
unrestricted_egress = True
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
for ip_range in rule.get(
|
|
189
|
+
"Ipv6Ranges", []
|
|
190
|
+
):
|
|
191
|
+
if (
|
|
192
|
+
ip_range.get("CidrIpv6")
|
|
193
|
+
== "::/0"
|
|
194
|
+
):
|
|
195
|
+
sg_info[
|
|
196
|
+
"unrestricted_egress_rules"
|
|
197
|
+
].append(rule)
|
|
198
|
+
unrestricted_egress = True
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
security_groups.append(sg_info)
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
"applicable": True,
|
|
205
|
+
"unrestricted_egress": unrestricted_egress,
|
|
206
|
+
"security_groups": security_groups,
|
|
207
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Command-line interface for Lambda Security Scanner."""
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
import traceback
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from .scanner import LambdaSecurityScanner
|
|
12
|
+
from . import __version__
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
BANNER = """[bold red]╔══════════════════════════════════════════════════════════╗
|
|
17
|
+
║ Lambda Security Scanner ║
|
|
18
|
+
║ Comprehensive Lambda Security Auditing ║
|
|
19
|
+
╚══════════════════════════════════════════════════════════╝[/bold red]"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def print_banner():
|
|
23
|
+
console.print(BANNER)
|
|
24
|
+
console.print(
|
|
25
|
+
f"[dim] Version {__version__} | "
|
|
26
|
+
"https://github.com/TocConsulting/"
|
|
27
|
+
"lambda-security-scanner[/dim]\n"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Shared options decorators (same pattern as EC2)
|
|
32
|
+
def shared_aws_options(f):
|
|
33
|
+
f = click.option(
|
|
34
|
+
"-r",
|
|
35
|
+
"--region",
|
|
36
|
+
default=None,
|
|
37
|
+
help=(
|
|
38
|
+
"AWS region "
|
|
39
|
+
"(default: AWS_DEFAULT_REGION or us-east-1)"
|
|
40
|
+
),
|
|
41
|
+
)(f)
|
|
42
|
+
f = click.option(
|
|
43
|
+
"-p",
|
|
44
|
+
"--profile",
|
|
45
|
+
default=None,
|
|
46
|
+
help="AWS profile name",
|
|
47
|
+
)(f)
|
|
48
|
+
return f
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def shared_output_options(f):
|
|
52
|
+
f = click.option(
|
|
53
|
+
"-o",
|
|
54
|
+
"--output-dir",
|
|
55
|
+
default="./output",
|
|
56
|
+
help="Directory for output files (default: ./output)",
|
|
57
|
+
)(f)
|
|
58
|
+
f = click.option(
|
|
59
|
+
"-f",
|
|
60
|
+
"--output-format",
|
|
61
|
+
type=click.Choice(
|
|
62
|
+
["json", "csv", "html", "all"],
|
|
63
|
+
case_sensitive=False,
|
|
64
|
+
),
|
|
65
|
+
default="all",
|
|
66
|
+
help="Report format (default: all)",
|
|
67
|
+
)(f)
|
|
68
|
+
return f
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def shared_performance_options(f):
|
|
72
|
+
f = click.option(
|
|
73
|
+
"-w",
|
|
74
|
+
"--max-workers",
|
|
75
|
+
default=5,
|
|
76
|
+
type=int,
|
|
77
|
+
help="Worker threads (default: 5)",
|
|
78
|
+
)(f)
|
|
79
|
+
f = click.option(
|
|
80
|
+
"-q",
|
|
81
|
+
"--quiet",
|
|
82
|
+
is_flag=True,
|
|
83
|
+
help="Suppress console output except errors",
|
|
84
|
+
)(f)
|
|
85
|
+
f = click.option(
|
|
86
|
+
"-d",
|
|
87
|
+
"--debug",
|
|
88
|
+
is_flag=True,
|
|
89
|
+
help="Enable debug logging",
|
|
90
|
+
)(f)
|
|
91
|
+
return f
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def shared_options(f):
|
|
95
|
+
f = shared_aws_options(f)
|
|
96
|
+
f = shared_output_options(f)
|
|
97
|
+
f = shared_performance_options(f)
|
|
98
|
+
return f
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class CustomGroup(click.Group):
|
|
102
|
+
def format_help(self, ctx, formatter):
|
|
103
|
+
print_banner()
|
|
104
|
+
super().format_help(ctx, formatter)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@click.group(
|
|
108
|
+
cls=CustomGroup,
|
|
109
|
+
context_settings=dict(
|
|
110
|
+
help_option_names=["-h", "--help"]
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
@click.version_option(
|
|
114
|
+
version=__version__,
|
|
115
|
+
prog_name="Lambda Security Scanner",
|
|
116
|
+
)
|
|
117
|
+
def cli():
|
|
118
|
+
"""
|
|
119
|
+
Comprehensive AWS Lambda security scanner for
|
|
120
|
+
vulnerability detection and multi-framework compliance
|
|
121
|
+
auditing.
|
|
122
|
+
|
|
123
|
+
\b
|
|
124
|
+
FRAMEWORKS
|
|
125
|
+
═══════════════════════════════════════════════════════
|
|
126
|
+
AWS-FSBP, CIS, PCI DSS v4.0.1, HIPAA, SOC 2,
|
|
127
|
+
ISO 27001:2022, ISO 27017, ISO 27018, GDPR,
|
|
128
|
+
NIST 800-53
|
|
129
|
+
|
|
130
|
+
\b
|
|
131
|
+
QUICK START
|
|
132
|
+
═══════════════════════════════════════════════════════
|
|
133
|
+
Scan all functions:
|
|
134
|
+
lambda-security-scanner security
|
|
135
|
+
Use AWS profile:
|
|
136
|
+
lambda-security-scanner security -p prod
|
|
137
|
+
Specific region:
|
|
138
|
+
lambda-security-scanner security -r eu-west-1
|
|
139
|
+
Specific functions:
|
|
140
|
+
lambda-security-scanner security -n my-func
|
|
141
|
+
|
|
142
|
+
\b
|
|
143
|
+
MORE INFO
|
|
144
|
+
═══════════════════════════════════════════════════════
|
|
145
|
+
Run COMMAND --help for detailed options
|
|
146
|
+
Docs: https://github.com/TocConsulting/
|
|
147
|
+
lambda-security-scanner
|
|
148
|
+
"""
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@cli.command()
|
|
153
|
+
@click.option(
|
|
154
|
+
"--function-name",
|
|
155
|
+
"-n",
|
|
156
|
+
multiple=True,
|
|
157
|
+
help="Specific function name(s) to scan",
|
|
158
|
+
)
|
|
159
|
+
@click.option(
|
|
160
|
+
"--exclude-function",
|
|
161
|
+
multiple=True,
|
|
162
|
+
help="Function name(s) to exclude from scanning",
|
|
163
|
+
)
|
|
164
|
+
@click.option(
|
|
165
|
+
"--compliance-only",
|
|
166
|
+
is_flag=True,
|
|
167
|
+
help="Generate compliance report only",
|
|
168
|
+
)
|
|
169
|
+
@shared_options
|
|
170
|
+
def security(
|
|
171
|
+
function_name,
|
|
172
|
+
exclude_function,
|
|
173
|
+
compliance_only,
|
|
174
|
+
region,
|
|
175
|
+
profile,
|
|
176
|
+
output_dir,
|
|
177
|
+
output_format,
|
|
178
|
+
max_workers,
|
|
179
|
+
quiet,
|
|
180
|
+
debug,
|
|
181
|
+
):
|
|
182
|
+
"""
|
|
183
|
+
Scan Lambda functions for security vulnerabilities
|
|
184
|
+
and compliance issues.
|
|
185
|
+
|
|
186
|
+
\b
|
|
187
|
+
Runs 19 security checks across 5 categories and
|
|
188
|
+
evaluates compliance against 10 frameworks with
|
|
189
|
+
81 controls.
|
|
190
|
+
|
|
191
|
+
\b
|
|
192
|
+
EXAMPLES:
|
|
193
|
+
lambda-security-scanner security
|
|
194
|
+
lambda-security-scanner security -p prod -r us-west-2
|
|
195
|
+
lambda-security-scanner security -n my-func -n other
|
|
196
|
+
lambda-security-scanner security --compliance-only
|
|
197
|
+
lambda-security-scanner security -f html -o ./reports
|
|
198
|
+
"""
|
|
199
|
+
import os as _os
|
|
200
|
+
if region is None:
|
|
201
|
+
region = _os.environ.get(
|
|
202
|
+
"AWS_DEFAULT_REGION", "us-east-1"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if debug:
|
|
206
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
207
|
+
logging.getLogger(
|
|
208
|
+
"lambda_security_scanner"
|
|
209
|
+
).setLevel(logging.DEBUG)
|
|
210
|
+
elif quiet:
|
|
211
|
+
logging.getLogger().setLevel(logging.ERROR)
|
|
212
|
+
|
|
213
|
+
if not quiet:
|
|
214
|
+
print_banner()
|
|
215
|
+
console.print(
|
|
216
|
+
"[bold cyan]Starting Lambda security "
|
|
217
|
+
"analysis...[/bold cyan]\n"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
scanner = LambdaSecurityScanner(
|
|
222
|
+
region=region,
|
|
223
|
+
profile=profile,
|
|
224
|
+
output_dir=output_dir,
|
|
225
|
+
max_workers=max_workers,
|
|
226
|
+
quiet=quiet,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Get functions
|
|
230
|
+
all_functions = scanner.get_all_functions()
|
|
231
|
+
|
|
232
|
+
# Apply name filter
|
|
233
|
+
if function_name:
|
|
234
|
+
all_functions = [
|
|
235
|
+
f
|
|
236
|
+
for f in all_functions
|
|
237
|
+
if f["FunctionName"] in function_name
|
|
238
|
+
]
|
|
239
|
+
if not all_functions:
|
|
240
|
+
console.print(
|
|
241
|
+
"[red]None of the specified "
|
|
242
|
+
"functions were found[/red]"
|
|
243
|
+
)
|
|
244
|
+
sys.exit(1)
|
|
245
|
+
|
|
246
|
+
# Apply exclusions
|
|
247
|
+
if exclude_function:
|
|
248
|
+
original = len(all_functions)
|
|
249
|
+
all_functions = [
|
|
250
|
+
f
|
|
251
|
+
for f in all_functions
|
|
252
|
+
if f["FunctionName"]
|
|
253
|
+
not in exclude_function
|
|
254
|
+
]
|
|
255
|
+
excluded = original - len(all_functions)
|
|
256
|
+
if not quiet and excluded > 0:
|
|
257
|
+
console.print(
|
|
258
|
+
f"[yellow]Excluded {excluded} "
|
|
259
|
+
f"function(s)[/yellow]"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if not all_functions:
|
|
263
|
+
console.print(
|
|
264
|
+
"[red]No functions found to scan[/red]"
|
|
265
|
+
)
|
|
266
|
+
sys.exit(1)
|
|
267
|
+
|
|
268
|
+
if not quiet:
|
|
269
|
+
console.print(
|
|
270
|
+
f"[green]Scanning {len(all_functions)} "
|
|
271
|
+
f"function(s)...[/green]\n"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
results = scanner.scan_all_functions(all_functions)
|
|
275
|
+
|
|
276
|
+
if not results:
|
|
277
|
+
console.print(
|
|
278
|
+
"[red]No results generated[/red]"
|
|
279
|
+
)
|
|
280
|
+
sys.exit(1)
|
|
281
|
+
|
|
282
|
+
report_files = scanner.generate_reports(
|
|
283
|
+
results, output_format
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if not quiet:
|
|
287
|
+
scanner.print_summary(results)
|
|
288
|
+
|
|
289
|
+
console.print(
|
|
290
|
+
"\n[bold green]Reports "
|
|
291
|
+
"Generated:[/bold green]"
|
|
292
|
+
)
|
|
293
|
+
for report_type, file_path in (
|
|
294
|
+
report_files.items()
|
|
295
|
+
):
|
|
296
|
+
console.print(
|
|
297
|
+
f" {report_type.upper()}: "
|
|
298
|
+
f"{file_path}"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if compliance_only:
|
|
302
|
+
_print_compliance_detail(results)
|
|
303
|
+
|
|
304
|
+
console.print(
|
|
305
|
+
"\n[bold green]Security scan completed "
|
|
306
|
+
"successfully![/bold green]"
|
|
307
|
+
)
|
|
308
|
+
console.print(
|
|
309
|
+
f"[dim]Reports saved to: {output_dir}[/dim]"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
except KeyboardInterrupt:
|
|
313
|
+
console.print(
|
|
314
|
+
"\n[yellow]Scan interrupted by user[/yellow]"
|
|
315
|
+
)
|
|
316
|
+
sys.exit(130)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
console.print(f"\n[red]Error: {str(e)}[/red]")
|
|
319
|
+
if debug:
|
|
320
|
+
console.print(
|
|
321
|
+
f"[red]{traceback.format_exc()}[/red]"
|
|
322
|
+
)
|
|
323
|
+
sys.exit(1)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _print_compliance_detail(results):
|
|
327
|
+
from rich.table import Table
|
|
328
|
+
|
|
329
|
+
frameworks = [
|
|
330
|
+
"AWS-FSBP",
|
|
331
|
+
"CIS",
|
|
332
|
+
"PCI-DSS-v4.0.1",
|
|
333
|
+
"HIPAA",
|
|
334
|
+
"SOC2",
|
|
335
|
+
"ISO27001",
|
|
336
|
+
"ISO27017",
|
|
337
|
+
"ISO27018",
|
|
338
|
+
"GDPR",
|
|
339
|
+
"NIST-800-53",
|
|
340
|
+
]
|
|
341
|
+
valid = [
|
|
342
|
+
r
|
|
343
|
+
for r in results
|
|
344
|
+
if not r.get("scan_error", False)
|
|
345
|
+
]
|
|
346
|
+
for fw in frameworks:
|
|
347
|
+
all_failed = {}
|
|
348
|
+
for r in valid:
|
|
349
|
+
fw_status = r.get(
|
|
350
|
+
"compliance_status", {}
|
|
351
|
+
).get(fw, {})
|
|
352
|
+
for ctrl in fw_status.get("failed", []):
|
|
353
|
+
ctrl_id = ctrl["control_id"]
|
|
354
|
+
if ctrl_id not in all_failed:
|
|
355
|
+
all_failed[ctrl_id] = {
|
|
356
|
+
"description": ctrl[
|
|
357
|
+
"description"
|
|
358
|
+
],
|
|
359
|
+
"severity": ctrl.get(
|
|
360
|
+
"severity", "MEDIUM"
|
|
361
|
+
),
|
|
362
|
+
"functions": [],
|
|
363
|
+
}
|
|
364
|
+
all_failed[ctrl_id]["functions"].append(
|
|
365
|
+
r.get("function_name", "")
|
|
366
|
+
)
|
|
367
|
+
if all_failed:
|
|
368
|
+
table = Table(
|
|
369
|
+
title=f"{fw} - Failed Controls"
|
|
370
|
+
)
|
|
371
|
+
table.add_column(
|
|
372
|
+
"Control", style="cyan", width=20
|
|
373
|
+
)
|
|
374
|
+
table.add_column("Description", width=40)
|
|
375
|
+
table.add_column("Severity", width=10)
|
|
376
|
+
table.add_column(
|
|
377
|
+
"Affected", justify="right", width=10
|
|
378
|
+
)
|
|
379
|
+
for ctrl_id, info in sorted(
|
|
380
|
+
all_failed.items()
|
|
381
|
+
):
|
|
382
|
+
table.add_row(
|
|
383
|
+
ctrl_id,
|
|
384
|
+
info["description"],
|
|
385
|
+
info["severity"],
|
|
386
|
+
str(len(info["functions"])),
|
|
387
|
+
)
|
|
388
|
+
console.print(table)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
main = cli
|
|
392
|
+
|
|
393
|
+
if __name__ == "__main__":
|
|
394
|
+
cli()
|