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,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
+ }