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.
@@ -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()