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,397 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Lambda Security Scanner Report</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0f172a; --card: #1e293b; --border: #334155;
|
|
10
|
+
--text: #e2e8f0; --text-heading: #e2e8f0;
|
|
11
|
+
--text-muted: #94a3b8; --text-dim: #64748b;
|
|
12
|
+
--header-gradient: linear-gradient(135deg, #1e293b, #334155);
|
|
13
|
+
--th-bg: #334155; --hover-bg: #253349;
|
|
14
|
+
--accent: #38bdf8; --danger: #f87171;
|
|
15
|
+
--warning: #fbbf24; --success: #34d399;
|
|
16
|
+
--critical: #ef4444; --high: #f97316;
|
|
17
|
+
--medium: #eab308; --low: #3b82f6; --info: #06b6d4;
|
|
18
|
+
--bar-track: #334155;
|
|
19
|
+
--badge-critical-bg: #7f1d1d; --badge-critical-fg: #fca5a5;
|
|
20
|
+
--badge-high-bg: #7c2d12; --badge-high-fg: #fdba74;
|
|
21
|
+
--badge-medium-bg: #713f12; --badge-medium-fg: #fde68a;
|
|
22
|
+
--badge-low-bg: #1e3a5f; --badge-low-fg: #93c5fd;
|
|
23
|
+
--badge-info-bg: #164e63; --badge-info-fg: #67e8f9;
|
|
24
|
+
--score-high-bg: #166534; --score-high-fg: #bbf7d0;
|
|
25
|
+
--score-mid-bg: #854d0e; --score-mid-fg: #fef08a;
|
|
26
|
+
--score-low-bg: #991b1b; --score-low-fg: #fecaca;
|
|
27
|
+
--fn-name: #e2e8f0;
|
|
28
|
+
--pct-good: #34d399; --pct-warn: #fbbf24; --pct-bad: #f87171;
|
|
29
|
+
--link: #64748b;
|
|
30
|
+
}
|
|
31
|
+
[data-theme="light"] {
|
|
32
|
+
--bg: #f8fafc; --card: #ffffff; --border: #e2e8f0;
|
|
33
|
+
--text: #1e293b; --text-heading: #0f172a;
|
|
34
|
+
--text-muted: #64748b; --text-dim: #94a3b8;
|
|
35
|
+
--header-gradient: linear-gradient(135deg, #e2e8f0, #cbd5e1);
|
|
36
|
+
--th-bg: #f1f5f9; --hover-bg: #f1f5f9;
|
|
37
|
+
--accent: #0284c7; --danger: #dc2626;
|
|
38
|
+
--warning: #d97706; --success: #16a34a;
|
|
39
|
+
--critical: #dc2626; --high: #ea580c;
|
|
40
|
+
--medium: #ca8a04; --low: #2563eb; --info: #0891b2;
|
|
41
|
+
--bar-track: #e2e8f0;
|
|
42
|
+
--badge-critical-bg: #fee2e2; --badge-critical-fg: #dc2626;
|
|
43
|
+
--badge-high-bg: #ffedd5; --badge-high-fg: #ea580c;
|
|
44
|
+
--badge-medium-bg: #fef9c3; --badge-medium-fg: #ca8a04;
|
|
45
|
+
--badge-low-bg: #dbeafe; --badge-low-fg: #2563eb;
|
|
46
|
+
--badge-info-bg: #cffafe; --badge-info-fg: #0891b2;
|
|
47
|
+
--score-high-bg: #dcfce7; --score-high-fg: #166534;
|
|
48
|
+
--score-mid-bg: #fef9c3; --score-mid-fg: #854d0e;
|
|
49
|
+
--score-low-bg: #fee2e2; --score-low-fg: #991b1b;
|
|
50
|
+
--fn-name: #0f172a;
|
|
51
|
+
--pct-good: #16a34a; --pct-warn: #d97706; --pct-bad: #dc2626;
|
|
52
|
+
--link: #3b82f6;
|
|
53
|
+
}
|
|
54
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
55
|
+
body {
|
|
56
|
+
font-family: -apple-system, BlinkMacSystemFont,
|
|
57
|
+
"Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
58
|
+
background: var(--bg); color: var(--text);
|
|
59
|
+
line-height: 1.6; padding: 20px;
|
|
60
|
+
}
|
|
61
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
62
|
+
h1 { font-size: 1.8rem; margin-bottom: 5px; color: var(--text-heading); }
|
|
63
|
+
h2 {
|
|
64
|
+
font-size: 1.3rem; margin: 30px 0 15px;
|
|
65
|
+
border-bottom: 2px solid var(--border);
|
|
66
|
+
padding-bottom: 8px; color: var(--text-heading);
|
|
67
|
+
}
|
|
68
|
+
.header {
|
|
69
|
+
background: var(--header-gradient);
|
|
70
|
+
padding: 25px 30px; border-radius: 12px;
|
|
71
|
+
margin-bottom: 25px;
|
|
72
|
+
}
|
|
73
|
+
.header .subtitle {
|
|
74
|
+
color: var(--text-muted); font-size: 0.9rem;
|
|
75
|
+
}
|
|
76
|
+
.cards {
|
|
77
|
+
display: grid;
|
|
78
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
79
|
+
gap: 15px; margin-bottom: 25px;
|
|
80
|
+
}
|
|
81
|
+
.card {
|
|
82
|
+
background: var(--card); border-radius: 10px;
|
|
83
|
+
padding: 20px; text-align: center;
|
|
84
|
+
border: 1px solid var(--border);
|
|
85
|
+
}
|
|
86
|
+
.card .value {
|
|
87
|
+
font-size: 2rem; font-weight: 700;
|
|
88
|
+
color: var(--accent);
|
|
89
|
+
}
|
|
90
|
+
.card .label {
|
|
91
|
+
color: var(--text-muted); font-size: 0.85rem;
|
|
92
|
+
margin-top: 4px;
|
|
93
|
+
}
|
|
94
|
+
.card.danger .value { color: var(--danger); }
|
|
95
|
+
.card.warning .value { color: var(--warning); }
|
|
96
|
+
.card.success .value { color: var(--success); }
|
|
97
|
+
table {
|
|
98
|
+
width: 100%; border-collapse: collapse;
|
|
99
|
+
background: var(--card); border-radius: 8px;
|
|
100
|
+
overflow: hidden; margin-bottom: 20px;
|
|
101
|
+
}
|
|
102
|
+
th {
|
|
103
|
+
background: var(--th-bg); padding: 12px 15px;
|
|
104
|
+
text-align: left; font-weight: 600;
|
|
105
|
+
font-size: 0.85rem; text-transform: uppercase;
|
|
106
|
+
letter-spacing: 0.05em; color: var(--text-muted);
|
|
107
|
+
}
|
|
108
|
+
td {
|
|
109
|
+
padding: 10px 15px; border-bottom: 1px solid var(--border);
|
|
110
|
+
font-size: 0.9rem;
|
|
111
|
+
}
|
|
112
|
+
tr:last-child td { border-bottom: none; }
|
|
113
|
+
tr:hover td { background: var(--hover-bg); }
|
|
114
|
+
.bar-container {
|
|
115
|
+
background: var(--bar-track); border-radius: 4px;
|
|
116
|
+
height: 22px; overflow: hidden;
|
|
117
|
+
}
|
|
118
|
+
.bar {
|
|
119
|
+
height: 100%; border-radius: 4px;
|
|
120
|
+
min-width: 2px; transition: width 0.3s;
|
|
121
|
+
}
|
|
122
|
+
.bar-critical { background: var(--critical); }
|
|
123
|
+
.bar-high { background: var(--high); }
|
|
124
|
+
.bar-medium { background: var(--medium); }
|
|
125
|
+
.bar-low { background: var(--low); }
|
|
126
|
+
.bar-info { background: var(--info); }
|
|
127
|
+
.score-bar-0-20 { background: var(--critical); }
|
|
128
|
+
.score-bar-21-40 { background: var(--high); }
|
|
129
|
+
.score-bar-41-60 { background: var(--medium); }
|
|
130
|
+
.score-bar-61-80 { background: var(--low); }
|
|
131
|
+
.score-bar-81-100 { background: var(--success); }
|
|
132
|
+
.badge {
|
|
133
|
+
display: inline-block; padding: 2px 10px;
|
|
134
|
+
border-radius: 12px; font-size: 0.75rem;
|
|
135
|
+
font-weight: 600; text-transform: uppercase;
|
|
136
|
+
}
|
|
137
|
+
.badge-critical { background: var(--badge-critical-bg); color: var(--badge-critical-fg); }
|
|
138
|
+
.badge-high { background: var(--badge-high-bg); color: var(--badge-high-fg); }
|
|
139
|
+
.badge-medium { background: var(--badge-medium-bg); color: var(--badge-medium-fg); }
|
|
140
|
+
.badge-low { background: var(--badge-low-bg); color: var(--badge-low-fg); }
|
|
141
|
+
.badge-info { background: var(--badge-info-bg); color: var(--badge-info-fg); }
|
|
142
|
+
.function-card {
|
|
143
|
+
background: var(--card); border-radius: 10px;
|
|
144
|
+
padding: 20px; margin-bottom: 15px;
|
|
145
|
+
border: 1px solid var(--border);
|
|
146
|
+
}
|
|
147
|
+
.function-card .fn-header {
|
|
148
|
+
display: flex; justify-content: space-between;
|
|
149
|
+
align-items: center; margin-bottom: 12px;
|
|
150
|
+
}
|
|
151
|
+
.function-card .fn-name {
|
|
152
|
+
font-weight: 700; font-size: 1.05rem;
|
|
153
|
+
color: var(--fn-name);
|
|
154
|
+
}
|
|
155
|
+
.score-badge {
|
|
156
|
+
display: inline-block; padding: 4px 14px;
|
|
157
|
+
border-radius: 16px; font-weight: 700;
|
|
158
|
+
font-size: 0.9rem;
|
|
159
|
+
}
|
|
160
|
+
.score-high { background: var(--score-high-bg); color: var(--score-high-fg); }
|
|
161
|
+
.score-mid { background: var(--score-mid-bg); color: var(--score-mid-fg); }
|
|
162
|
+
.score-low { background: var(--score-low-bg); color: var(--score-low-fg); }
|
|
163
|
+
.issue-list { list-style: none; padding: 0; }
|
|
164
|
+
.issue-list li {
|
|
165
|
+
padding: 6px 0; border-bottom: 1px solid var(--border);
|
|
166
|
+
font-size: 0.88rem;
|
|
167
|
+
}
|
|
168
|
+
.issue-list li:last-child { border-bottom: none; }
|
|
169
|
+
.pct-cell { text-align: right; font-weight: 600; }
|
|
170
|
+
.pct-good { color: var(--pct-good); }
|
|
171
|
+
.pct-warn { color: var(--pct-warn); }
|
|
172
|
+
.pct-bad { color: var(--pct-bad); }
|
|
173
|
+
.footer {
|
|
174
|
+
text-align: center; color: var(--text-dim);
|
|
175
|
+
font-size: 0.8rem; margin-top: 40px;
|
|
176
|
+
padding: 15px;
|
|
177
|
+
}
|
|
178
|
+
#theme-toggle {
|
|
179
|
+
position: fixed; top: 15px; right: 15px; z-index: 1000;
|
|
180
|
+
background: var(--card); border: 1px solid var(--border);
|
|
181
|
+
color: var(--text); font-size: 1.3rem; width: 40px; height: 40px;
|
|
182
|
+
border-radius: 50%; cursor: pointer; display: flex;
|
|
183
|
+
align-items: center; justify-content: center;
|
|
184
|
+
}
|
|
185
|
+
</style>
|
|
186
|
+
</head>
|
|
187
|
+
<body>
|
|
188
|
+
<button id="theme-toggle" onclick="toggleTheme()">🌙</button>
|
|
189
|
+
<div class="container">
|
|
190
|
+
|
|
191
|
+
<div class="header">
|
|
192
|
+
<h1>Lambda Security Scanner Report</h1>
|
|
193
|
+
<div class="subtitle">
|
|
194
|
+
Generated {{ summary.get('scan_timestamp', '') }}
|
|
195
|
+
· Region: {{ summary.get('region', 'N/A') }}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<!-- Summary Cards -->
|
|
200
|
+
<div class="cards">
|
|
201
|
+
<div class="card">
|
|
202
|
+
<div class="value">
|
|
203
|
+
{{ summary.get('total_functions', 0) }}
|
|
204
|
+
</div>
|
|
205
|
+
<div class="label">Total Functions</div>
|
|
206
|
+
</div>
|
|
207
|
+
<div class="card {% if summary.get('average_security_score', 0) < 50 %}danger{% elif summary.get('average_security_score', 0) < 75 %}warning{% else %}success{% endif %}">
|
|
208
|
+
<div class="value">
|
|
209
|
+
{{ summary.get('average_security_score', 0) }}
|
|
210
|
+
</div>
|
|
211
|
+
<div class="label">Avg Security Score</div>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="card {% if summary.get('public_functions', 0) > 0 %}danger{% endif %}">
|
|
214
|
+
<div class="value">
|
|
215
|
+
{{ summary.get('public_functions', 0) }}
|
|
216
|
+
</div>
|
|
217
|
+
<div class="label">Public Functions</div>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="card {% if summary.get('secrets_found', 0) > 0 %}danger{% endif %}">
|
|
220
|
+
<div class="value">
|
|
221
|
+
{{ summary.get('secrets_found', 0) }}
|
|
222
|
+
</div>
|
|
223
|
+
<div class="label">Secrets Found</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<!-- Severity Distribution -->
|
|
228
|
+
<h2>Finding Severity Distribution</h2>
|
|
229
|
+
<table>
|
|
230
|
+
<thead>
|
|
231
|
+
<tr>
|
|
232
|
+
<th>Severity</th>
|
|
233
|
+
<th>Count</th>
|
|
234
|
+
<th style="width: 60%">Distribution</th>
|
|
235
|
+
</tr>
|
|
236
|
+
</thead>
|
|
237
|
+
<tbody>
|
|
238
|
+
{% set sev_labels = ['CRITICAL','HIGH','MEDIUM','LOW','INFO'] %}
|
|
239
|
+
{% set sev_classes = ['bar-critical','bar-high','bar-medium','bar-low','bar-info'] %}
|
|
240
|
+
{% set badge_classes = ['badge-critical','badge-high','badge-medium','badge-low','badge-info'] %}
|
|
241
|
+
{% set total_issues = severity_counts | sum %}
|
|
242
|
+
{% for i in range(5) %}
|
|
243
|
+
<tr>
|
|
244
|
+
<td>
|
|
245
|
+
<span class="badge {{ badge_classes[i] }}">
|
|
246
|
+
{{ sev_labels[i] }}
|
|
247
|
+
</span>
|
|
248
|
+
</td>
|
|
249
|
+
<td>{{ severity_counts[i] }}</td>
|
|
250
|
+
<td>
|
|
251
|
+
<div class="bar-container">
|
|
252
|
+
<div class="bar {{ sev_classes[i] }}"
|
|
253
|
+
style="width: {{ (severity_counts[i] / total_issues * 100) if total_issues > 0 else 0 }}%">
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</td>
|
|
257
|
+
</tr>
|
|
258
|
+
{% endfor %}
|
|
259
|
+
</tbody>
|
|
260
|
+
</table>
|
|
261
|
+
|
|
262
|
+
<!-- Score Distribution -->
|
|
263
|
+
<h2>Security Score Distribution</h2>
|
|
264
|
+
<table>
|
|
265
|
+
<thead>
|
|
266
|
+
<tr>
|
|
267
|
+
<th>Score Range</th>
|
|
268
|
+
<th>Functions</th>
|
|
269
|
+
<th style="width: 60%">Distribution</th>
|
|
270
|
+
</tr>
|
|
271
|
+
</thead>
|
|
272
|
+
<tbody>
|
|
273
|
+
{% set score_labels = ['0-20','21-40','41-60','61-80','81-100'] %}
|
|
274
|
+
{% set score_classes = ['score-bar-0-20','score-bar-21-40','score-bar-41-60','score-bar-61-80','score-bar-81-100'] %}
|
|
275
|
+
{% set total_funcs = security_score_distribution | sum %}
|
|
276
|
+
{% for i in range(5) %}
|
|
277
|
+
<tr>
|
|
278
|
+
<td>{{ score_labels[i] }}</td>
|
|
279
|
+
<td>{{ security_score_distribution[i] }}</td>
|
|
280
|
+
<td>
|
|
281
|
+
<div class="bar-container">
|
|
282
|
+
<div class="bar {{ score_classes[i] }}"
|
|
283
|
+
style="width: {{ (security_score_distribution[i] / total_funcs * 100) if total_funcs > 0 else 0 }}%">
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</td>
|
|
287
|
+
</tr>
|
|
288
|
+
{% endfor %}
|
|
289
|
+
</tbody>
|
|
290
|
+
</table>
|
|
291
|
+
|
|
292
|
+
<!-- Compliance Summary -->
|
|
293
|
+
{% if compliance_summary %}
|
|
294
|
+
<h2>Compliance Framework Summary</h2>
|
|
295
|
+
<table>
|
|
296
|
+
<thead>
|
|
297
|
+
<tr>
|
|
298
|
+
<th>Framework</th>
|
|
299
|
+
<th>Compliant</th>
|
|
300
|
+
<th>Non-Compliant</th>
|
|
301
|
+
<th>Total</th>
|
|
302
|
+
<th>Compliance %</th>
|
|
303
|
+
<th>Avg Score %</th>
|
|
304
|
+
</tr>
|
|
305
|
+
</thead>
|
|
306
|
+
<tbody>
|
|
307
|
+
{% for fw, data in compliance_summary.items() %}
|
|
308
|
+
<tr>
|
|
309
|
+
<td><strong>{{ fw }}</strong></td>
|
|
310
|
+
<td>{{ data.compliant_functions }}</td>
|
|
311
|
+
<td>{{ data.non_compliant_functions }}</td>
|
|
312
|
+
<td>{{ data.total_functions }}</td>
|
|
313
|
+
<td class="pct-cell {% if data.compliance_percentage >= 80 %}pct-good{% elif data.compliance_percentage >= 50 %}pct-warn{% else %}pct-bad{% endif %}">
|
|
314
|
+
{{ data.compliance_percentage }}%
|
|
315
|
+
</td>
|
|
316
|
+
<td class="pct-cell {% if data.average_compliance_percentage >= 80 %}pct-good{% elif data.average_compliance_percentage >= 50 %}pct-warn{% else %}pct-bad{% endif %}">
|
|
317
|
+
{{ data.average_compliance_percentage }}%
|
|
318
|
+
</td>
|
|
319
|
+
</tr>
|
|
320
|
+
{% endfor %}
|
|
321
|
+
</tbody>
|
|
322
|
+
</table>
|
|
323
|
+
{% endif %}
|
|
324
|
+
|
|
325
|
+
<!-- Per-Function Details -->
|
|
326
|
+
<h2>Function Details</h2>
|
|
327
|
+
{% for r in results %}
|
|
328
|
+
<div class="function-card">
|
|
329
|
+
<div class="fn-header">
|
|
330
|
+
<span class="fn-name">
|
|
331
|
+
{{ r.get('function_name', 'Unknown') }}
|
|
332
|
+
</span>
|
|
333
|
+
{% set score = r.get('security_score', 0) or 0 %}
|
|
334
|
+
<span class="score-badge {% if score >= 80 %}score-high{% elif score >= 50 %}score-mid{% else %}score-low{% endif %}">
|
|
335
|
+
Score: {{ score }}
|
|
336
|
+
</span>
|
|
337
|
+
</div>
|
|
338
|
+
<div style="color: var(--text-muted); font-size: 0.82rem; margin-bottom: 10px;">
|
|
339
|
+
Runtime: {{ r.get('runtime', 'N/A') }}
|
|
340
|
+
· Memory: {{ r.get('memory_size', 'N/A') }} MB
|
|
341
|
+
· Timeout: {{ r.get('timeout', 'N/A') }}s
|
|
342
|
+
</div>
|
|
343
|
+
{% set issues = r.get('issues', []) %}
|
|
344
|
+
{% if issues %}
|
|
345
|
+
<ul class="issue-list">
|
|
346
|
+
{% for issue in issues %}
|
|
347
|
+
<li>
|
|
348
|
+
<span class="badge badge-{{ issue.get('severity', 'INFO') | lower }}">
|
|
349
|
+
{{ issue.get('severity', 'INFO') }}
|
|
350
|
+
</span>
|
|
351
|
+
{{ issue.get('title', '') }}
|
|
352
|
+
{% if issue.get('description') %}
|
|
353
|
+
<span style="color: var(--text-dim);">
|
|
354
|
+
— {{ issue.get('description', '') }}
|
|
355
|
+
</span>
|
|
356
|
+
{% endif %}
|
|
357
|
+
</li>
|
|
358
|
+
{% endfor %}
|
|
359
|
+
</ul>
|
|
360
|
+
{% else %}
|
|
361
|
+
<div style="color: var(--success); font-size: 0.9rem;">
|
|
362
|
+
No issues found.
|
|
363
|
+
</div>
|
|
364
|
+
{% endif %}
|
|
365
|
+
</div>
|
|
366
|
+
{% endfor %}
|
|
367
|
+
|
|
368
|
+
<div class="footer">
|
|
369
|
+
Lambda Security Scanner ·
|
|
370
|
+
Toc Consulting ·
|
|
371
|
+
<a href="https://github.com/TocConsulting/lambda-security-scanner"
|
|
372
|
+
style="color: var(--link);">GitHub</a>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<script>
|
|
378
|
+
function toggleTheme() {
|
|
379
|
+
const html = document.documentElement;
|
|
380
|
+
const current = html.getAttribute('data-theme');
|
|
381
|
+
const next = current === 'light' ? 'dark' : 'light';
|
|
382
|
+
if (next === 'dark') html.removeAttribute('data-theme');
|
|
383
|
+
else html.setAttribute('data-theme', 'light');
|
|
384
|
+
localStorage.setItem('lambda-report-theme', next);
|
|
385
|
+
document.getElementById('theme-toggle').textContent = next === 'light' ? '\u2600\uFE0F' : '\uD83C\uDF19';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
(function() {
|
|
389
|
+
const saved = localStorage.getItem('lambda-report-theme');
|
|
390
|
+
if (saved === 'light') {
|
|
391
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
|
392
|
+
document.getElementById('theme-toggle').textContent = '\u2600\uFE0F';
|
|
393
|
+
}
|
|
394
|
+
})();
|
|
395
|
+
</script>
|
|
396
|
+
</body>
|
|
397
|
+
</html>
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Utility functions for Lambda Security Scanner."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def setup_logging(
|
|
10
|
+
output_dir: str,
|
|
11
|
+
log_level: int = logging.INFO,
|
|
12
|
+
) -> logging.Logger:
|
|
13
|
+
"""Setup logging with console and file handlers."""
|
|
14
|
+
logger = logging.getLogger("lambda_security_scanner")
|
|
15
|
+
logger.setLevel(log_level)
|
|
16
|
+
logger.propagate = False
|
|
17
|
+
|
|
18
|
+
for handler in logger.handlers[:]:
|
|
19
|
+
logger.removeHandler(handler)
|
|
20
|
+
|
|
21
|
+
console_handler = logging.StreamHandler()
|
|
22
|
+
console_handler.setLevel(log_level)
|
|
23
|
+
console_formatter = logging.Formatter(
|
|
24
|
+
"%(asctime)s - %(levelname)s - %(message)s"
|
|
25
|
+
)
|
|
26
|
+
console_handler.setFormatter(console_formatter)
|
|
27
|
+
logger.addHandler(console_handler)
|
|
28
|
+
|
|
29
|
+
log_file = os.path.join(
|
|
30
|
+
output_dir,
|
|
31
|
+
f'lambda_scan_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log',
|
|
32
|
+
)
|
|
33
|
+
file_handler = logging.FileHandler(log_file)
|
|
34
|
+
file_handler.setLevel(logging.DEBUG)
|
|
35
|
+
file_formatter = logging.Formatter(
|
|
36
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
37
|
+
)
|
|
38
|
+
file_handler.setFormatter(file_formatter)
|
|
39
|
+
logger.addHandler(file_handler)
|
|
40
|
+
|
|
41
|
+
return logger
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def calculate_security_score(checks: Dict[str, Any]) -> int:
|
|
45
|
+
"""Calculate security score from check results.
|
|
46
|
+
|
|
47
|
+
Starts at 100 and applies deductions based on findings.
|
|
48
|
+
Mutual-exclusion groups take only the highest penalty.
|
|
49
|
+
"""
|
|
50
|
+
score = 100
|
|
51
|
+
|
|
52
|
+
def get(check_name, key, default=False):
|
|
53
|
+
check = checks.get(check_name, {})
|
|
54
|
+
if isinstance(check, dict) and "error" not in check:
|
|
55
|
+
return check.get(key, default)
|
|
56
|
+
return default
|
|
57
|
+
|
|
58
|
+
# MUTUAL EXCLUSION: A.1 runtime (highest penalty only)
|
|
59
|
+
status = get("runtime", "status", "supported")
|
|
60
|
+
if status == "blocked":
|
|
61
|
+
score -= 15
|
|
62
|
+
elif status == "deprecated":
|
|
63
|
+
score -= 10
|
|
64
|
+
elif status == "near_eol":
|
|
65
|
+
score -= 3
|
|
66
|
+
|
|
67
|
+
# MUTUAL EXCLUSION: A.3 secrets
|
|
68
|
+
if get("environment_secrets", "has_secrets"):
|
|
69
|
+
if not get("environment_secrets", "has_kms_key"):
|
|
70
|
+
score -= 20 # CRITICAL: secrets + no KMS
|
|
71
|
+
else:
|
|
72
|
+
score -= 10 # HIGH: secrets + has KMS
|
|
73
|
+
|
|
74
|
+
# MUTUAL EXCLUSION: E.1 code signing
|
|
75
|
+
# Skip entirely when not applicable (container image functions —
|
|
76
|
+
# AWS Lambda Code Signing only applies to Zip packages).
|
|
77
|
+
if get("code_signing", "applicable", default=True):
|
|
78
|
+
if not get("code_signing", "configured"):
|
|
79
|
+
score -= 5
|
|
80
|
+
elif not get("code_signing", "is_enforced"):
|
|
81
|
+
score -= 3
|
|
82
|
+
|
|
83
|
+
# B.1: Public resource policy
|
|
84
|
+
if get("resource_policy", "is_public"):
|
|
85
|
+
score -= 25
|
|
86
|
+
|
|
87
|
+
# B.2: Public function URL
|
|
88
|
+
if get("function_url", "is_public"):
|
|
89
|
+
score -= 25
|
|
90
|
+
|
|
91
|
+
# B.4: Execution role overprivilege
|
|
92
|
+
if get("execution_role", "has_full_admin"):
|
|
93
|
+
score -= 20 # CRITICAL: admin-equivalent ("*", Admin/PowerUser)
|
|
94
|
+
elif get("execution_role", "has_wildcard_actions"):
|
|
95
|
+
score -= 10 # HIGH: single-service wildcard (e.g. s3:*)
|
|
96
|
+
elif get("execution_role", "has_privilege_escalation"):
|
|
97
|
+
score -= 10
|
|
98
|
+
|
|
99
|
+
# B.3: Function URL CORS allow all origins
|
|
100
|
+
if get("function_url_cors", "allow_all_origins"):
|
|
101
|
+
score -= 10
|
|
102
|
+
|
|
103
|
+
# B.5: Shared role
|
|
104
|
+
if get("shared_role", "is_shared"):
|
|
105
|
+
score -= 10
|
|
106
|
+
|
|
107
|
+
# A.6: Tracing not enabled (observability hygiene, LOW)
|
|
108
|
+
if not get("tracing", "enabled"):
|
|
109
|
+
score -= 2
|
|
110
|
+
|
|
111
|
+
# A.7: Dead letter config not configured (resilience, LOW)
|
|
112
|
+
if not get("dead_letter_config", "configured"):
|
|
113
|
+
score -= 2
|
|
114
|
+
|
|
115
|
+
# C.2: Multi-AZ
|
|
116
|
+
if get("multi_az", "applicable") and not get(
|
|
117
|
+
"multi_az", "is_multi_az"
|
|
118
|
+
):
|
|
119
|
+
score -= 5
|
|
120
|
+
|
|
121
|
+
# C.3: Unrestricted egress
|
|
122
|
+
if get("security_groups", "applicable") and get(
|
|
123
|
+
"security_groups", "unrestricted_egress"
|
|
124
|
+
):
|
|
125
|
+
score -= 5
|
|
126
|
+
|
|
127
|
+
# D.1: Log group (max -5 total)
|
|
128
|
+
if not get("log_group", "exists") or not get(
|
|
129
|
+
"log_group", "has_retention"
|
|
130
|
+
):
|
|
131
|
+
score -= 5
|
|
132
|
+
|
|
133
|
+
# D.2: Reserved concurrency (availability hygiene, LOW; the public +
|
|
134
|
+
# no-concurrency combination is penalized separately as CRITICAL)
|
|
135
|
+
if not get("reserved_concurrency", "configured"):
|
|
136
|
+
score -= 2
|
|
137
|
+
|
|
138
|
+
# E.2: Event source mappings missing failure destinations
|
|
139
|
+
if get(
|
|
140
|
+
"event_source_mappings", "has_mappings"
|
|
141
|
+
) and get(
|
|
142
|
+
"event_source_mappings",
|
|
143
|
+
"missing_failure_dest_count",
|
|
144
|
+
0,
|
|
145
|
+
) > 0:
|
|
146
|
+
score -= 5
|
|
147
|
+
|
|
148
|
+
# A.5: External layers
|
|
149
|
+
if get("layers", "has_external_layers"):
|
|
150
|
+
score -= 3
|
|
151
|
+
|
|
152
|
+
# C.1: Not in VPC
|
|
153
|
+
if not get("vpc_config", "in_vpc"):
|
|
154
|
+
score -= 3
|
|
155
|
+
|
|
156
|
+
# A.2: Max timeout
|
|
157
|
+
if get("timeout", "is_max_timeout"):
|
|
158
|
+
score -= 2
|
|
159
|
+
|
|
160
|
+
# A.4: Large ephemeral storage
|
|
161
|
+
if get("ephemeral_storage", "is_large"):
|
|
162
|
+
score -= 2
|
|
163
|
+
|
|
164
|
+
return max(0, score)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_severity_color(severity: str) -> str:
|
|
168
|
+
"""Return Rich color string for a severity level."""
|
|
169
|
+
colors = {
|
|
170
|
+
"CRITICAL": "bold red",
|
|
171
|
+
"HIGH": "red",
|
|
172
|
+
"MEDIUM": "yellow",
|
|
173
|
+
"LOW": "blue",
|
|
174
|
+
"INFO": "cyan",
|
|
175
|
+
"ERROR": "magenta",
|
|
176
|
+
}
|
|
177
|
+
return colors.get(severity, "white")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def format_datetime(dt) -> str:
|
|
181
|
+
"""Format a datetime object or ISO string to readable UTC."""
|
|
182
|
+
if isinstance(dt, str):
|
|
183
|
+
try:
|
|
184
|
+
dt = datetime.fromisoformat(
|
|
185
|
+
dt.replace("Z", "+00:00")
|
|
186
|
+
)
|
|
187
|
+
except ValueError:
|
|
188
|
+
return dt
|
|
189
|
+
if isinstance(dt, datetime):
|
|
190
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
191
|
+
return str(dt)
|