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,104 @@
|
|
|
1
|
+
"""Base class for all security checkers."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("lambda_security_scanner")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseChecker:
|
|
14
|
+
"""Base class for all security checkers.
|
|
15
|
+
|
|
16
|
+
Provides thread-safe AWS client creation via session_factory
|
|
17
|
+
and standardized error handling.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, session_factory=None):
|
|
21
|
+
"""Initialize the checker with optional session factory.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
session_factory: Callable that returns a boto3 session
|
|
25
|
+
(for thread safety) or a boto3 session object
|
|
26
|
+
(for backward compatibility).
|
|
27
|
+
"""
|
|
28
|
+
self.session_factory = session_factory
|
|
29
|
+
|
|
30
|
+
def get_client(self, service_name: str, region_name: str = None):
|
|
31
|
+
"""Get AWS client for the specified service.
|
|
32
|
+
|
|
33
|
+
Uses the thread-safe session factory to create clients,
|
|
34
|
+
ensuring each thread gets its own session.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
service_name: AWS service name (e.g., 'lambda', 'iam')
|
|
38
|
+
region_name: AWS region name (optional)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
boto3 client for the service
|
|
42
|
+
"""
|
|
43
|
+
if self.session_factory:
|
|
44
|
+
session = (
|
|
45
|
+
self.session_factory()
|
|
46
|
+
if callable(self.session_factory)
|
|
47
|
+
else self.session_factory
|
|
48
|
+
)
|
|
49
|
+
kwargs = (
|
|
50
|
+
{"region_name": region_name} if region_name else {}
|
|
51
|
+
)
|
|
52
|
+
return session.client(service_name, **kwargs)
|
|
53
|
+
else:
|
|
54
|
+
kwargs = (
|
|
55
|
+
{"region_name": region_name} if region_name else {}
|
|
56
|
+
)
|
|
57
|
+
return boto3.client(service_name, **kwargs)
|
|
58
|
+
|
|
59
|
+
def handle_client_error(
|
|
60
|
+
self, e: ClientError, default_response: Dict[str, Any] = None
|
|
61
|
+
) -> Dict[str, Any]:
|
|
62
|
+
"""Handle ClientError exceptions consistently.
|
|
63
|
+
|
|
64
|
+
Logs the error and returns a safe default response dict.
|
|
65
|
+
Special handling for AccessDeniedException and
|
|
66
|
+
ResourceNotFoundException.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
e: ClientError exception
|
|
70
|
+
default_response: Default response dict to return
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Error response dict with 'error' key
|
|
74
|
+
"""
|
|
75
|
+
error_code = e.response.get("Error", {}).get(
|
|
76
|
+
"Code", "Unknown"
|
|
77
|
+
)
|
|
78
|
+
error_msg = str(e)
|
|
79
|
+
|
|
80
|
+
if error_code in (
|
|
81
|
+
"AccessDeniedException",
|
|
82
|
+
"AccessDenied",
|
|
83
|
+
"UnauthorizedOperation",
|
|
84
|
+
"AuthFailure",
|
|
85
|
+
):
|
|
86
|
+
logger.warning(
|
|
87
|
+
f"Access denied ({error_code}): {error_msg} - "
|
|
88
|
+
"scan will continue with limited results"
|
|
89
|
+
)
|
|
90
|
+
elif error_code == "ResourceNotFoundException":
|
|
91
|
+
logger.debug(
|
|
92
|
+
f"Resource not found (expected): {error_msg}"
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
logger.warning(f"AWS API error: {error_msg}")
|
|
96
|
+
|
|
97
|
+
if default_response is None:
|
|
98
|
+
default_response = {
|
|
99
|
+
"error": error_msg,
|
|
100
|
+
}
|
|
101
|
+
else:
|
|
102
|
+
default_response["error"] = error_msg
|
|
103
|
+
|
|
104
|
+
return default_response
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Code security checks for Lambda functions (E.1-E.2)."""
|
|
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 CodeSecurityChecker(BaseChecker):
|
|
14
|
+
"""Check code security configuration for Lambda functions.
|
|
15
|
+
|
|
16
|
+
Implements checks E.1 (code signing) and
|
|
17
|
+
E.2 (event source mapping failure destinations).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def check_code_signing(
|
|
21
|
+
self,
|
|
22
|
+
function_name: str,
|
|
23
|
+
region: str,
|
|
24
|
+
package_type: str = "Zip",
|
|
25
|
+
) -> Dict:
|
|
26
|
+
"""E.1 - Check code signing configuration.
|
|
27
|
+
|
|
28
|
+
Skips container image functions (package_type=Image).
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
function_name: Lambda function name.
|
|
32
|
+
region: AWS region name.
|
|
33
|
+
package_type: "Zip" or "Image".
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Dict with configured, policy, config_arn,
|
|
37
|
+
is_enforced.
|
|
38
|
+
"""
|
|
39
|
+
if package_type == "Image":
|
|
40
|
+
# AWS Lambda Code Signing is not supported for container
|
|
41
|
+
# image functions (only Zip packages). Returning
|
|
42
|
+
# applicable=False so scoring/issues skip the check
|
|
43
|
+
# rather than falsely reporting "enforced".
|
|
44
|
+
return {
|
|
45
|
+
"configured": False,
|
|
46
|
+
"policy": None,
|
|
47
|
+
"config_arn": None,
|
|
48
|
+
"is_enforced": False,
|
|
49
|
+
"applicable": False,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lambda_client = self.get_client("lambda", region)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
response = (
|
|
56
|
+
lambda_client
|
|
57
|
+
.get_function_code_signing_config(
|
|
58
|
+
FunctionName=function_name
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
except ClientError as e:
|
|
62
|
+
error_code = e.response.get("Error", {}).get(
|
|
63
|
+
"Code", ""
|
|
64
|
+
)
|
|
65
|
+
if error_code == "ResourceNotFoundException":
|
|
66
|
+
logger.debug(
|
|
67
|
+
"No code signing config for %s",
|
|
68
|
+
function_name,
|
|
69
|
+
)
|
|
70
|
+
return {
|
|
71
|
+
"configured": False,
|
|
72
|
+
"policy": None,
|
|
73
|
+
"config_arn": None,
|
|
74
|
+
"is_enforced": False,
|
|
75
|
+
}
|
|
76
|
+
return self.handle_client_error(
|
|
77
|
+
e,
|
|
78
|
+
{
|
|
79
|
+
"configured": False,
|
|
80
|
+
"policy": None,
|
|
81
|
+
"config_arn": None,
|
|
82
|
+
"is_enforced": False,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
config_arn = response.get("CodeSigningConfigArn")
|
|
87
|
+
if not config_arn:
|
|
88
|
+
return {
|
|
89
|
+
"configured": False,
|
|
90
|
+
"policy": None,
|
|
91
|
+
"config_arn": None,
|
|
92
|
+
"is_enforced": False,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
csc_response = (
|
|
97
|
+
lambda_client.get_code_signing_config(
|
|
98
|
+
CodeSigningConfigArn=config_arn
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
except ClientError as e:
|
|
102
|
+
return self.handle_client_error(
|
|
103
|
+
e,
|
|
104
|
+
{
|
|
105
|
+
"configured": True,
|
|
106
|
+
"policy": None,
|
|
107
|
+
"config_arn": config_arn,
|
|
108
|
+
"is_enforced": False,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
csc = csc_response.get("CodeSigningConfig", {})
|
|
113
|
+
policies = csc.get("CodeSigningPolicies", {})
|
|
114
|
+
policy = policies.get(
|
|
115
|
+
"UntrustedArtifactOnDeployment"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"configured": True,
|
|
120
|
+
"policy": policy,
|
|
121
|
+
"config_arn": config_arn,
|
|
122
|
+
"is_enforced": policy == "Enforce",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
def check_event_source_mappings(
|
|
126
|
+
self, function_name: str, region: str
|
|
127
|
+
) -> Dict:
|
|
128
|
+
"""E.2 - Check event source mappings for failure dest.
|
|
129
|
+
|
|
130
|
+
Uses paginator to list all event source mappings.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
function_name: Lambda function name.
|
|
134
|
+
region: AWS region name.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dict with mapping_count, mappings,
|
|
138
|
+
missing_failure_dest_count,
|
|
139
|
+
missing_failure_destinations, has_mappings.
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
lambda_client = self.get_client(
|
|
143
|
+
"lambda", region
|
|
144
|
+
)
|
|
145
|
+
paginator = lambda_client.get_paginator(
|
|
146
|
+
"list_event_source_mappings"
|
|
147
|
+
)
|
|
148
|
+
page_iterator = paginator.paginate(
|
|
149
|
+
FunctionName=function_name
|
|
150
|
+
)
|
|
151
|
+
except ClientError as e:
|
|
152
|
+
return self.handle_client_error(
|
|
153
|
+
e,
|
|
154
|
+
{
|
|
155
|
+
"mapping_count": 0,
|
|
156
|
+
"mappings": [],
|
|
157
|
+
"missing_failure_dest_count": 0,
|
|
158
|
+
"missing_failure_destinations": [],
|
|
159
|
+
"has_mappings": False,
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
mappings = []
|
|
164
|
+
missing_failure_destinations = []
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
for page in page_iterator:
|
|
168
|
+
for esm in page.get(
|
|
169
|
+
"EventSourceMappings", []
|
|
170
|
+
):
|
|
171
|
+
esm_info = {
|
|
172
|
+
"EventSourceArn": esm.get(
|
|
173
|
+
"EventSourceArn"
|
|
174
|
+
),
|
|
175
|
+
"UUID": esm.get("UUID"),
|
|
176
|
+
}
|
|
177
|
+
mappings.append(esm_info)
|
|
178
|
+
|
|
179
|
+
dest = (
|
|
180
|
+
esm.get(
|
|
181
|
+
"DestinationConfig", {}
|
|
182
|
+
)
|
|
183
|
+
.get("OnFailure", {})
|
|
184
|
+
.get("Destination")
|
|
185
|
+
)
|
|
186
|
+
if not dest:
|
|
187
|
+
missing_failure_destinations.append(
|
|
188
|
+
esm.get("UUID")
|
|
189
|
+
)
|
|
190
|
+
except ClientError as e:
|
|
191
|
+
return self.handle_client_error(
|
|
192
|
+
e,
|
|
193
|
+
{
|
|
194
|
+
"mapping_count": 0,
|
|
195
|
+
"mappings": [],
|
|
196
|
+
"missing_failure_dest_count": 0,
|
|
197
|
+
"missing_failure_destinations": [],
|
|
198
|
+
"has_mappings": False,
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
"mapping_count": len(mappings),
|
|
204
|
+
"mappings": mappings,
|
|
205
|
+
"missing_failure_dest_count": len(
|
|
206
|
+
missing_failure_destinations
|
|
207
|
+
),
|
|
208
|
+
"missing_failure_destinations": (
|
|
209
|
+
missing_failure_destinations
|
|
210
|
+
),
|
|
211
|
+
"has_mappings": len(mappings) > 0,
|
|
212
|
+
}
|