kollabor 0.4.9__py3-none-any.whl → 0.4.15__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.
Files changed (192) hide show
  1. agents/__init__.py +2 -0
  2. agents/coder/__init__.py +0 -0
  3. agents/coder/agent.json +4 -0
  4. agents/coder/api-integration.md +2150 -0
  5. agents/coder/cli-pretty.md +765 -0
  6. agents/coder/code-review.md +1092 -0
  7. agents/coder/database-design.md +1525 -0
  8. agents/coder/debugging.md +1102 -0
  9. agents/coder/dependency-management.md +1397 -0
  10. agents/coder/git-workflow.md +1099 -0
  11. agents/coder/refactoring.md +1454 -0
  12. agents/coder/security-hardening.md +1732 -0
  13. agents/coder/system_prompt.md +1448 -0
  14. agents/coder/tdd.md +1367 -0
  15. agents/creative-writer/__init__.py +0 -0
  16. agents/creative-writer/agent.json +4 -0
  17. agents/creative-writer/character-development.md +1852 -0
  18. agents/creative-writer/dialogue-craft.md +1122 -0
  19. agents/creative-writer/plot-structure.md +1073 -0
  20. agents/creative-writer/revision-editing.md +1484 -0
  21. agents/creative-writer/system_prompt.md +690 -0
  22. agents/creative-writer/worldbuilding.md +2049 -0
  23. agents/data-analyst/__init__.py +30 -0
  24. agents/data-analyst/agent.json +4 -0
  25. agents/data-analyst/data-visualization.md +992 -0
  26. agents/data-analyst/exploratory-data-analysis.md +1110 -0
  27. agents/data-analyst/pandas-data-manipulation.md +1081 -0
  28. agents/data-analyst/sql-query-optimization.md +881 -0
  29. agents/data-analyst/statistical-analysis.md +1118 -0
  30. agents/data-analyst/system_prompt.md +928 -0
  31. agents/default/__init__.py +0 -0
  32. agents/default/agent.json +4 -0
  33. agents/default/dead-code.md +794 -0
  34. agents/default/explore-agent-system.md +585 -0
  35. agents/default/system_prompt.md +1448 -0
  36. agents/kollabor/__init__.py +0 -0
  37. agents/kollabor/analyze-plugin-lifecycle.md +175 -0
  38. agents/kollabor/analyze-terminal-rendering.md +388 -0
  39. agents/kollabor/code-review.md +1092 -0
  40. agents/kollabor/debug-mcp-integration.md +521 -0
  41. agents/kollabor/debug-plugin-hooks.md +547 -0
  42. agents/kollabor/debugging.md +1102 -0
  43. agents/kollabor/dependency-management.md +1397 -0
  44. agents/kollabor/git-workflow.md +1099 -0
  45. agents/kollabor/inspect-llm-conversation.md +148 -0
  46. agents/kollabor/monitor-event-bus.md +558 -0
  47. agents/kollabor/profile-performance.md +576 -0
  48. agents/kollabor/refactoring.md +1454 -0
  49. agents/kollabor/system_prompt copy.md +1448 -0
  50. agents/kollabor/system_prompt.md +757 -0
  51. agents/kollabor/trace-command-execution.md +178 -0
  52. agents/kollabor/validate-config.md +879 -0
  53. agents/research/__init__.py +0 -0
  54. agents/research/agent.json +4 -0
  55. agents/research/architecture-mapping.md +1099 -0
  56. agents/research/codebase-analysis.md +1077 -0
  57. agents/research/dependency-audit.md +1027 -0
  58. agents/research/performance-profiling.md +1047 -0
  59. agents/research/security-review.md +1359 -0
  60. agents/research/system_prompt.md +492 -0
  61. agents/technical-writer/__init__.py +0 -0
  62. agents/technical-writer/agent.json +4 -0
  63. agents/technical-writer/api-documentation.md +2328 -0
  64. agents/technical-writer/changelog-management.md +1181 -0
  65. agents/technical-writer/readme-writing.md +1360 -0
  66. agents/technical-writer/style-guide.md +1410 -0
  67. agents/technical-writer/system_prompt.md +653 -0
  68. agents/technical-writer/tutorial-creation.md +1448 -0
  69. core/__init__.py +0 -2
  70. core/application.py +343 -88
  71. core/cli.py +229 -10
  72. core/commands/menu_renderer.py +463 -59
  73. core/commands/registry.py +14 -9
  74. core/commands/system_commands.py +2461 -14
  75. core/config/loader.py +151 -37
  76. core/config/service.py +18 -6
  77. core/events/bus.py +29 -9
  78. core/events/executor.py +205 -75
  79. core/events/models.py +27 -8
  80. core/fullscreen/command_integration.py +20 -24
  81. core/fullscreen/components/__init__.py +10 -1
  82. core/fullscreen/components/matrix_components.py +1 -2
  83. core/fullscreen/components/space_shooter_components.py +654 -0
  84. core/fullscreen/plugin.py +5 -0
  85. core/fullscreen/renderer.py +52 -13
  86. core/fullscreen/session.py +52 -15
  87. core/io/__init__.py +29 -5
  88. core/io/buffer_manager.py +6 -1
  89. core/io/config_status_view.py +7 -29
  90. core/io/core_status_views.py +267 -347
  91. core/io/input/__init__.py +25 -0
  92. core/io/input/command_mode_handler.py +711 -0
  93. core/io/input/display_controller.py +128 -0
  94. core/io/input/hook_registrar.py +286 -0
  95. core/io/input/input_loop_manager.py +421 -0
  96. core/io/input/key_press_handler.py +502 -0
  97. core/io/input/modal_controller.py +1011 -0
  98. core/io/input/paste_processor.py +339 -0
  99. core/io/input/status_modal_renderer.py +184 -0
  100. core/io/input_errors.py +5 -1
  101. core/io/input_handler.py +211 -2452
  102. core/io/key_parser.py +7 -0
  103. core/io/layout.py +15 -3
  104. core/io/message_coordinator.py +111 -2
  105. core/io/message_renderer.py +129 -4
  106. core/io/status_renderer.py +147 -607
  107. core/io/terminal_renderer.py +97 -51
  108. core/io/terminal_state.py +21 -4
  109. core/io/visual_effects.py +816 -165
  110. core/llm/agent_manager.py +1063 -0
  111. core/llm/api_adapters/__init__.py +44 -0
  112. core/llm/api_adapters/anthropic_adapter.py +432 -0
  113. core/llm/api_adapters/base.py +241 -0
  114. core/llm/api_adapters/openai_adapter.py +326 -0
  115. core/llm/api_communication_service.py +167 -113
  116. core/llm/conversation_logger.py +322 -16
  117. core/llm/conversation_manager.py +556 -30
  118. core/llm/file_operations_executor.py +84 -32
  119. core/llm/llm_service.py +934 -103
  120. core/llm/mcp_integration.py +541 -57
  121. core/llm/message_display_service.py +135 -18
  122. core/llm/plugin_sdk.py +1 -2
  123. core/llm/profile_manager.py +1183 -0
  124. core/llm/response_parser.py +274 -56
  125. core/llm/response_processor.py +16 -3
  126. core/llm/tool_executor.py +6 -1
  127. core/logging/__init__.py +2 -0
  128. core/logging/setup.py +34 -6
  129. core/models/resume.py +54 -0
  130. core/plugins/__init__.py +4 -2
  131. core/plugins/base.py +127 -0
  132. core/plugins/collector.py +23 -161
  133. core/plugins/discovery.py +37 -3
  134. core/plugins/factory.py +6 -12
  135. core/plugins/registry.py +5 -17
  136. core/ui/config_widgets.py +128 -28
  137. core/ui/live_modal_renderer.py +2 -1
  138. core/ui/modal_actions.py +5 -0
  139. core/ui/modal_overlay_renderer.py +0 -60
  140. core/ui/modal_renderer.py +268 -7
  141. core/ui/modal_state_manager.py +29 -4
  142. core/ui/widgets/base_widget.py +7 -0
  143. core/updates/__init__.py +10 -0
  144. core/updates/version_check_service.py +348 -0
  145. core/updates/version_comparator.py +103 -0
  146. core/utils/config_utils.py +685 -526
  147. core/utils/plugin_utils.py +1 -1
  148. core/utils/session_naming.py +111 -0
  149. fonts/LICENSE +21 -0
  150. fonts/README.md +46 -0
  151. fonts/SymbolsNerdFont-Regular.ttf +0 -0
  152. fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
  153. fonts/__init__.py +44 -0
  154. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
  155. kollabor-0.4.15.dist-info/RECORD +228 -0
  156. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
  157. plugins/agent_orchestrator/__init__.py +39 -0
  158. plugins/agent_orchestrator/activity_monitor.py +181 -0
  159. plugins/agent_orchestrator/file_attacher.py +77 -0
  160. plugins/agent_orchestrator/message_injector.py +135 -0
  161. plugins/agent_orchestrator/models.py +48 -0
  162. plugins/agent_orchestrator/orchestrator.py +403 -0
  163. plugins/agent_orchestrator/plugin.py +976 -0
  164. plugins/agent_orchestrator/xml_parser.py +191 -0
  165. plugins/agent_orchestrator_plugin.py +9 -0
  166. plugins/enhanced_input/box_styles.py +1 -0
  167. plugins/enhanced_input/color_engine.py +19 -4
  168. plugins/enhanced_input/config.py +2 -2
  169. plugins/enhanced_input_plugin.py +61 -11
  170. plugins/fullscreen/__init__.py +6 -2
  171. plugins/fullscreen/example_plugin.py +1035 -222
  172. plugins/fullscreen/setup_wizard_plugin.py +592 -0
  173. plugins/fullscreen/space_shooter_plugin.py +131 -0
  174. plugins/hook_monitoring_plugin.py +436 -78
  175. plugins/query_enhancer_plugin.py +66 -30
  176. plugins/resume_conversation_plugin.py +1494 -0
  177. plugins/save_conversation_plugin.py +98 -32
  178. plugins/system_commands_plugin.py +70 -56
  179. plugins/tmux_plugin.py +154 -78
  180. plugins/workflow_enforcement_plugin.py +94 -92
  181. system_prompt/default.md +952 -886
  182. core/io/input_mode_manager.py +0 -402
  183. core/io/modal_interaction_handler.py +0 -315
  184. core/io/raw_input_processor.py +0 -946
  185. core/storage/__init__.py +0 -5
  186. core/storage/state_manager.py +0 -84
  187. core/ui/widget_integration.py +0 -222
  188. core/utils/key_reader.py +0 -171
  189. kollabor-0.4.9.dist-info/RECORD +0 -128
  190. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
  191. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
  192. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1732 @@
1
+ <!-- Security Hardening skill - secure code from the start -->
2
+
3
+ security-hardening mode: SECURITY FIRST, ALWAYS
4
+
5
+ when this skill is active, you follow security best practices.
6
+ this is a comprehensive guide to writing secure, production-ready code.
7
+
8
+
9
+ PHASE 0: ENVIRONMENT VERIFICATION
10
+
11
+ before writing ANY code, verify security tools are available.
12
+
13
+
14
+ check for security linters
15
+
16
+ <terminal>python -m bandit --version 2>/dev/null || echo "bandit not installed"</terminal>
17
+
18
+ if bandit not installed:
19
+ <terminal>pip install bandit</terminal>
20
+
21
+
22
+ check for dependency vulnerability scanner
23
+
24
+ <terminal>python -m safety --version 2>/dev/null || echo "safety not installed"</terminal>
25
+
26
+ if safety not installed:
27
+ <terminal>pip install safety</terminal>
28
+
29
+
30
+ check for secrets detection
31
+
32
+ <terminal>git-secrets --version 2>/dev/null || echo "git-secrets not installed"</terminal>
33
+
34
+ git-secrets installation varies by platform:
35
+ <terminal>brew install git-secrets</terminal> # macOS
36
+ <terminal>apt install git-secrets</terminal> # Ubuntu/Debian
37
+
38
+ initialize git-secrets in repo if not already done:
39
+ <terminal>git secrets --install</terminal>
40
+ <terminal>git secrets --register-aws</terminal>
41
+
42
+
43
+ check for pre-commit hooks
44
+
45
+ <terminal>pre-commit --version 2>/dev/null || echo "pre-commit not installed"</terminal>
46
+
47
+ if pre-commit not installed:
48
+ <terminal>pip install pre-commit</terminal>
49
+
50
+
51
+ check for existing security configuration
52
+
53
+ <terminal>ls -la .bandit 2>/dev/null || echo "no bandit config"</terminal>
54
+ <terminal>cat .gitignore 2>/dev/null | grep -E "\.env|secret|key" || echo "no secrets in gitignore"</terminal>
55
+
56
+
57
+ verify project security setup
58
+
59
+ <terminal>find . -name "*.py" -type f | head -5 | xargs -I {} bandit {} 2>&1 | head -20</terminal>
60
+
61
+ <terminal>safety check --bare 2>&1 | head -20</terminal>
62
+
63
+
64
+ PHASE 1: THE SECURITY MINDSET
65
+
66
+
67
+ think like an attacker
68
+
69
+ every input is malicious until proven otherwise.
70
+ every user request could be an attack.
71
+ every external system might be compromised.
72
+
73
+ questions to ask for every feature:
74
+ [1] what if the input is malicious?
75
+ [2] what if the user is not who they claim?
76
+ [3] what if the database is compromised?
77
+ [4] what if the external API is down or compromised?
78
+ [5] what if secrets are leaked?
79
+
80
+ secure by default principles:
81
+
82
+ deny by default, allow by exception
83
+ fail securely (closed, not open)
84
+ principle of least privilege
85
+ defense in depth (multiple layers)
86
+ minimize attack surface
87
+
88
+
89
+ the owasp top 10 (2021)
90
+
91
+ [1] broken access control
92
+ [2] cryptographic failures
93
+ [3] injection
94
+ [4] insecure design
95
+ [5] security misconfiguration
96
+ [6] vulnerable and outdated components
97
+ [7] identification and authentication failures
98
+ [8] software and data integrity failures
99
+ [9] security logging and monitoring failures
100
+ [10] server-side request forgery
101
+
102
+ memorize these. they cover 90% of security vulnerabilities.
103
+
104
+
105
+ PHASE 2: INPUT VALIDATION FUNDAMENTALS
106
+
107
+
108
+ never trust input
109
+
110
+ sources of untrusted input:
111
+ [ok] user input forms
112
+ [ok] url parameters and query strings
113
+ [ok] http headers
114
+ [ok] cookies
115
+ [ok] file uploads
116
+ [ok] webhooks
117
+ [ok] api requests from any source
118
+ [ok] database data (may be historic/compromised)
119
+
120
+
121
+ validation layers
122
+
123
+ layer 1: schema validation
124
+ verify structure, types, required fields
125
+
126
+ layer 2: business rule validation
127
+ verify value ranges, relationships, constraints
128
+
129
+ layer 3: sanitization
130
+ remove/escape dangerous content
131
+
132
+ always validate in this order, never skip layers.
133
+
134
+
135
+ whitelist vs blacklist
136
+
137
+ # bad - blacklist (you'll miss something)
138
+ def sanitize_filename(filename):
139
+ dangerous = ["../", "..\\", "/etc/", "C:\\"]
140
+ for d in dangerous:
141
+ filename = filename.replace(d, "")
142
+ return filename
143
+
144
+ # good - whitelist (only allow known safe)
145
+ import re
146
+
147
+ def sanitize_filename(filename):
148
+ # only allow alphanumeric, dash, underscore, dot
149
+ cleaned = re.sub(r"[^a-zA-Z0-9._-]", "", filename)
150
+ # remove leading dots/dashes to prevent directory traversal
151
+ cleaned = cleaned.lstrip(".-")
152
+ return cleaned
153
+
154
+
155
+ validation examples
156
+
157
+ import re
158
+ from typing import Optional
159
+ from dataclasses import dataclass
160
+
161
+
162
+ @dataclass
163
+ class ValidatedEmail:
164
+ """A validated email address."""
165
+ value: str
166
+
167
+ def __post_init__(self):
168
+ if not self._is_valid():
169
+ raise ValueError(f"Invalid email: {self.value}")
170
+
171
+ def _is_valid(self) -> bool:
172
+ pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
173
+ return bool(re.match(pattern, self.value))
174
+
175
+ def __str__(self) -> str:
176
+ return self.value
177
+
178
+
179
+ # usage
180
+ try:
181
+ email = ValidatedEmail(user_input)
182
+ # safe to use email.value
183
+ except ValueError as e:
184
+ return {"error": str(e)}, 400
185
+
186
+
187
+ length limits always
188
+
189
+ def validate_username(username: str) -> str:
190
+ if not username:
191
+ raise ValueError("Username required")
192
+ if len(username) < 3:
193
+ raise ValueError("Username too short")
194
+ if len(username) > 50:
195
+ raise ValueError("Username too long")
196
+ if not re.match(r"^[a-zA-Z0-9_-]+$", username):
197
+ raise ValueError("Invalid characters")
198
+ return username
199
+
200
+
201
+ PHASE 3: PREVENTING INJECTION ATTACKS
202
+
203
+
204
+ sql injection
205
+
206
+ the classic vulnerability:
207
+
208
+ # vulnerable - never do this
209
+ def get_user(user_id):
210
+ query = f"SELECT * FROM users WHERE id = {user_id}"
211
+ return db.execute(query)
212
+
213
+ # attack: "1 OR 1=1; DROP TABLE users; --"
214
+
215
+
216
+ prevention: parameterized queries
217
+
218
+ # safe - using parameterized queries
219
+ import sqlite3
220
+
221
+ def get_user(user_id: int):
222
+ query = "SELECT * FROM users WHERE id = ?"
223
+ cursor = db.execute(query, (user_id,))
224
+ return cursor.fetchone()
225
+
226
+ # safe - with ORM
227
+ from sqlalchemy import text
228
+
229
+ def get_user(user_id: int):
230
+ return db.session.query(User).filter(User.id == user_id).first()
231
+
232
+
233
+ # safe - with explicit type conversion
234
+ def get_user(user_id: str) -> Optional[User]:
235
+ try:
236
+ user_id_int = int(user_id) # force integer
237
+ except ValueError:
238
+ raise ValueError("Invalid user ID")
239
+ return User.query.get(user_id_int)
240
+
241
+
242
+ dynamic queries still need parameters
243
+
244
+ # vulnerable even with some parameters
245
+ def search_users(column: str, value: str):
246
+ query = f"SELECT * FROM users WHERE {column} = ?"
247
+ return db.execute(query, (value,))
248
+
249
+ # attack: column = "id OR 1=1; DROP TABLE users; --"
250
+
251
+
252
+ # safe - validate column name against whitelist
253
+ ALLOWED_COLUMNS = {"id", "username", "email", "created_at"}
254
+
255
+ def search_users(column: str, value: str):
256
+ if column not in ALLOWED_COLUMNS:
257
+ raise ValueError(f"Invalid column: {column}")
258
+ query = f"SELECT * FROM users WHERE {column} = ?"
259
+ return db.execute(query, (value,))
260
+
261
+
262
+ nosql injection
263
+
264
+ # vulnerable - mongodb injection
265
+ def find_user(username, password):
266
+ query = {"username": username, "password": password}
267
+ return db.users.find_one(query)
268
+
269
+ # attack: {"$ne": null} for username finds first user
270
+
271
+
272
+ # safe - use type-safe queries
273
+ from typing import Dict, Any
274
+
275
+ def find_user(username: str, password: str) -> Optional[Dict[str, Any]]:
276
+ if not isinstance(username, str) or not isinstance(password, str):
277
+ raise TypeError("Username and password must be strings")
278
+ return db.users.find_one({
279
+ "username": username,
280
+ "password": password # hash this first!
281
+ })
282
+
283
+
284
+ command injection
285
+
286
+ # vulnerable - never pass user input to shell
287
+ import subprocess
288
+
289
+ def process_file(filename):
290
+ result = subprocess.run(
291
+ f"process_file {filename}",
292
+ shell=True, # DANGEROUS
293
+ capture_output=True
294
+ )
295
+ return result.stdout
296
+
297
+ # attack: filename = "file.txt; rm -rf /; #"
298
+
299
+
300
+ # safe - use list argument (no shell)
301
+ def process_file(filename: str):
302
+ # validate filename first
303
+ safe_filename = sanitize_filename(filename)
304
+ result = subprocess.run(
305
+ ["process_file", safe_filename],
306
+ shell=False, # safe default
307
+ capture_output=True
308
+ )
309
+ return result.stdout
310
+
311
+
312
+ # safe alternative - use python libraries
313
+ def process_pdf(filename: str):
314
+ safe_filename = sanitize_filename(filename)
315
+ # use PyPDF2 instead of calling external tool
316
+ import PyPDF2
317
+ with open(safe_filename, 'rb') as f:
318
+ reader = PyPDF2.PdfReader(f)
319
+ return reader.pages[0].extract_text()
320
+
321
+
322
+ template injection
323
+
324
+ # vulnerable - jinja2 with user input
325
+ from jinja2 import Template
326
+
327
+ def render_greeting(template_str, name):
328
+ template = Template(template_str) # user controls template!
329
+ return template.render(name=name)
330
+
331
+ # attack: template_str = "{{config.items()}}"
332
+
333
+
334
+ # safe - separate template from data
335
+ from jinja2 import Environment, FileSystemLoader
336
+
337
+ env = Environment(loader=FileSystemLoader('templates/'))
338
+ template = env.get_template('greeting.html') # fixed template
339
+
340
+ def render_greeting(name):
341
+ return template.render(name=name)
342
+
343
+
344
+ PHASE 4: CROSS-SITE SCRIPTING (XSS) PREVENTION
345
+
346
+
347
+ xss attack types
348
+
349
+ [1] stored xss - malicious code saved to database, shown to visitors
350
+ [2] reflected xss - malicious code in url, reflected back in response
351
+ [3] dom-based xss - malicious code executes in browser via javascript
352
+
353
+
354
+ output encoding
355
+
356
+ # vulnerable - raw output
357
+ def show_comment(comment):
358
+ return f"<div>{comment}</div>"
359
+
360
+ # attack: comment = "<script>alert('XSS')</script>"
361
+
362
+
363
+ # safe - html escape
364
+ from html import escape
365
+
366
+ def show_comment(comment):
367
+ escaped = escape(comment)
368
+ return f"<div>{escaped}</div>"
369
+
370
+ # result: &lt;script&gt;alert('XSS')&lt;/script&gt;
371
+
372
+
373
+ # safe - with template engine (auto-escaped)
374
+ from jinja2 import Template
375
+
376
+ template = Template("<div>{{ comment }}</div>", autoescape=True)
377
+ result = template.render(comment=user_input)
378
+
379
+
380
+ context matters
381
+
382
+ def render_attribute(value):
383
+ # html body context
384
+ body = f"<div>{escape(value)}</div>"
385
+ # attribute context needs different escaping
386
+ attr = f'<div data-value="{escape(value, quote=True)}">'
387
+ # url context needs url encoding
388
+ import urllib.parse
389
+ url = f"/search?q={urllib.parse.quote(value)}"
390
+ return body
391
+
392
+
393
+ content security policy
394
+
395
+ add csp headers:
396
+
397
+ from flask import Flask, Response
398
+
399
+ app = Flask(__name__)
400
+
401
+ @app.after_request
402
+ def add_security_headers(response):
403
+ csp = (
404
+ "default-src 'self'; "
405
+ "script-src 'self' 'unsafe-inline' cdn.jsdelivr.net; "
406
+ "style-src 'self' 'unsafe-inline' cdn.jsdelivr.net; "
407
+ "img-src 'self' data: https:; "
408
+ "font-src 'self' cdn.jsdelivr.net; "
409
+ "connect-src 'self' api.example.com; "
410
+ "frame-ancestors 'none'; "
411
+ "base-uri 'self'; "
412
+ "form-action 'self';"
413
+ )
414
+ response.headers['Content-Security-Policy'] = csp
415
+ response.headers['X-Content-Type-Options'] = 'nosniff'
416
+ response.headers['X-Frame-Options'] = 'DENY'
417
+ response.headers['X-XSS-Protection'] = '1; mode=block'
418
+ return response
419
+
420
+
421
+ httponly cookies
422
+
423
+ from flask import make_response
424
+
425
+ response = make_response("Login successful")
426
+ response.set_cookie(
427
+ 'session_token',
428
+ token,
429
+ httponly=True, # prevents javascript access
430
+ secure=True, # only send over https
431
+ samesite='Lax', # csrf protection
432
+ max_age=3600
433
+ )
434
+ return response
435
+
436
+
437
+ PHASE 5: CROSS-SITE REQUEST FORGERY (CSRF) PREVENTION
438
+
439
+
440
+ how csrf works
441
+
442
+ 1. user logs into bank.com, gets session cookie
443
+ 2. user visits evil.com
444
+ 3. evil.com has form/action to bank.com/transfer
445
+ 4. browser sends bank.com cookies automatically
446
+ 5. transfer executes with user's credentials
447
+
448
+
449
+ synchronizer token pattern
450
+
451
+ import secrets
452
+ from flask import session, request
453
+
454
+ def generate_csrf_token():
455
+ if 'csrf_token' not in session:
456
+ session['csrf_token'] = secrets.token_hex(32)
457
+ return session['csrf_token']
458
+
459
+
460
+ def validate_csrf_token():
461
+ token = session.get('csrf_token')
462
+ if not token or token != request.form.get('csrf_token'):
463
+ raise ValueError("Invalid CSRF token")
464
+
465
+
466
+ # form template
467
+ def render_transfer_form():
468
+ return f"""
469
+ <form method="POST" action="/transfer">
470
+ <input type="hidden" name="csrf_token" value="{generate_csrf_token()}">
471
+ <input name="to_account" placeholder="Recipient">
472
+ <input name="amount" type="number" placeholder="Amount">
473
+ <button type="submit">Transfer</button>
474
+ </form>
475
+ """
476
+
477
+
478
+ # form handler
479
+ def handle_transfer():
480
+ validate_csrf_token() # must be first
481
+ to_account = request.form['to_account']
482
+ amount = request.form['amount']
483
+ # ... process transfer ...
484
+
485
+
486
+ double submit cookie pattern
487
+
488
+ alternative when sessions aren't available:
489
+
490
+ import secrets
491
+
492
+ def set_csrf_cookie(response):
493
+ token = secrets.token_hex(32)
494
+ response.set_cookie(
495
+ 'csrf_token',
496
+ token,
497
+ httponly=True,
498
+ secure=True,
499
+ samesite='Strict'
500
+ )
501
+ return token
502
+
503
+
504
+ def validate_csrf_double_submit():
505
+ cookie_token = request.cookies.get('csrf_token')
506
+ form_token = request.form.get('csrf_token')
507
+ if not cookie_token or cookie_token != form_token:
508
+ raise ValueError("CSRF validation failed")
509
+
510
+
511
+ sameSite cookies
512
+
513
+ modern browsers support sameSite attribute:
514
+
515
+ # strict - best security
516
+ response.set_cookie('session', token, samesite='Strict')
517
+
518
+ # lax - allows top-level navigations
519
+ response.set_cookie('session', token, samesite='Lax')
520
+
521
+ # none - for cross-origin requests (requires secure)
522
+ response.set_cookie('session', token, samesite='None', secure=True)
523
+
524
+
525
+ PHASE 6: AUTHENTICATION AND AUTHORIZATION
526
+
527
+
528
+ secure password handling
529
+
530
+ import bcrypt
531
+ import secrets
532
+ from typing import Optional
533
+
534
+
535
+ def hash_password(plain_password: str) -> str:
536
+ """Hash a password using bcrypt."""
537
+ if len(plain_password) > 72:
538
+ raise ValueError("Password too long for bcrypt")
539
+ salt = bcrypt.gensalt(rounds=12)
540
+ return bcrypt.hashpw(plain_password.encode('utf-8'), salt).decode('utf-8')
541
+
542
+
543
+ def verify_password(plain_password: str, hashed: str) -> bool:
544
+ """Verify a password against its hash."""
545
+ try:
546
+ return bcrypt.checkpw(
547
+ plain_password.encode('utf-8'),
548
+ hashed.encode('utf-8')
549
+ )
550
+ except Exception:
551
+ return False
552
+
553
+
554
+ password requirements
555
+
556
+ import re
557
+
558
+ def validate_password_strength(password: str) -> list[str]:
559
+ """Validate password strength, returning list of errors."""
560
+ errors = []
561
+
562
+ if len(password) < 12:
563
+ errors.append("Password must be at least 12 characters")
564
+
565
+ if len(password) > 128:
566
+ errors.append("Password too long (max 128 characters)")
567
+
568
+ if not re.search(r'[a-z]', password):
569
+ errors.append("Password must contain lowercase letters")
570
+
571
+ if not re.search(r'[A-Z]', password):
572
+ errors.append("Password must contain uppercase letters")
573
+
574
+ if not re.search(r'[0-9]', password):
575
+ errors.append("Password must contain digits")
576
+
577
+ if not re.search(r'[^a-zA-Z0-9]', password):
578
+ errors.append("Password must contain special characters")
579
+
580
+ # check for common passwords
581
+ common = ["password", "123456", "qwerty", "admin", "welcome"]
582
+ lower = password.lower()
583
+ for common_pwd in common:
584
+ if common_pwd in lower:
585
+ errors.append(f"Password contains common word: {common_pwd}")
586
+
587
+ return errors
588
+
589
+
590
+ secure session management
591
+
592
+ import secrets
593
+ from datetime import datetime, timedelta
594
+ from typing import Optional
595
+
596
+ SESSION_DURATION = timedelta(hours=1)
597
+
598
+
599
+ def create_session(user_id: int) -> str:
600
+ """Create a new session token."""
601
+ token = secrets.token_urlsafe(32)
602
+ # store in database with expiration
603
+ db.execute(
604
+ "INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)",
605
+ (token, user_id, datetime.now() + SESSION_DURATION)
606
+ )
607
+ return token
608
+
609
+
610
+ def validate_session(token: str) -> Optional[int]:
611
+ """Validate session token and return user_id."""
612
+ session = db.execute(
613
+ "SELECT user_id, expires_at FROM sessions WHERE token = ?",
614
+ (token,)
615
+ ).fetchone()
616
+
617
+ if not session:
618
+ return None
619
+
620
+ if datetime.now() > session['expires_at']:
621
+ db.execute("DELETE FROM sessions WHERE token = ?", (token,))
622
+ return None
623
+
624
+ # rotate session periodically
625
+ if random.random() < 0.1: # 10% chance to rotate
626
+ new_token = create_session(session['user_id'])
627
+ db.execute("DELETE FROM sessions WHERE token = ?", (token,))
628
+ return new_token
629
+
630
+ return session['user_id']
631
+
632
+
633
+ def revoke_session(token: str) -> None:
634
+ """Revoke a session token."""
635
+ db.execute("DELETE FROM sessions WHERE token = ?", (token,))
636
+
637
+
638
+ def revoke_all_user_sessions(user_id: int) -> None:
639
+ """Revoke all sessions for a user."""
640
+ db.execute("DELETE FROM sessions WHERE user_id = ?", (user_id,))
641
+
642
+
643
+ authorization checks
644
+
645
+ from functools import wraps
646
+ from flask import g, jsonify
647
+
648
+ def require_role(*roles):
649
+ """Decorator to require specific user roles."""
650
+ def decorator(f):
651
+ @wraps(f)
652
+ def wrapped(*args, **kwargs):
653
+ if not hasattr(g, 'user') or g.user is None:
654
+ return jsonify({"error": "Authentication required"}), 401
655
+
656
+ if g.user['role'] not in roles:
657
+ return jsonify({"error": "Insufficient permissions"}), 403
658
+
659
+ return f(*args, **kwargs)
660
+ return wrapped
661
+ return decorator
662
+
663
+
664
+ def require_ownership(resource_type: str):
665
+ """Decorator to require user owns the resource."""
666
+ def decorator(f):
667
+ @wraps(f)
668
+ def wrapped(*args, **kwargs):
669
+ resource_id = kwargs.get('id')
670
+ user_id = g.user['id']
671
+
672
+ # check ownership in database
673
+ owner_id = db.execute(
674
+ f"SELECT user_id FROM {resource_type} WHERE id = ?",
675
+ (resource_id,)
676
+ ).fetchone()
677
+
678
+ if not owner_id or owner_id['user_id'] != user_id:
679
+ return jsonify({"error": "Access denied"}), 403
680
+
681
+ return f(*args, **kwargs)
682
+ return wrapped
683
+ return decorator
684
+
685
+
686
+ # usage
687
+ @app.route('/admin/users')
688
+ @require_role('admin')
689
+ def admin_users():
690
+ return jsonify({"users": list_users()})
691
+
692
+
693
+ @app.route('/api/posts/<int:id>', methods=['DELETE'])
694
+ @require_ownership('posts')
695
+ def delete_post(id):
696
+ return delete_post(id)
697
+
698
+
699
+ rate limiting login
700
+
701
+ from functools import lru_cache
702
+ import time
703
+
704
+ MAX_ATTEMPTS = 5
705
+ LOCKOUT_DURATION = 900 # 15 minutes
706
+
707
+ @lru_cache(maxsize=10000)
708
+ def get_login_attempts(identifier: str) -> tuple[int, float]:
709
+ """Get (attempt_count, last_attempt_time) for identifier."""
710
+ return (0, 0)
711
+
712
+
713
+ def record_failed_login(identifier: str) -> bool:
714
+ """Record failed login, return True if locked out."""
715
+ attempts, last_time = get_login_attempts(identifier)
716
+ now = time.time()
717
+
718
+ # reset if lockout period passed
719
+ if now - last_time > LOCKOUT_DURATION:
720
+ attempts = 0
721
+
722
+ attempts += 1
723
+ # cache with expiration would be better
724
+ return attempts >= MAX_ATTEMPTS
725
+
726
+
727
+ def is_locked_out(identifier: str) -> bool:
728
+ """Check if identifier is currently locked out."""
729
+ attempts, last_time = get_login_attempts(identifier)
730
+ if attempts >= MAX_ATTEMPTS:
731
+ if time.time() - last_time < LOCKOUT_DURATION:
732
+ return True
733
+ return False
734
+
735
+
736
+ PHASE 7: SECRETS MANAGEMENT
737
+
738
+
739
+ never hardcode secrets
740
+
741
+ [x] api keys in source code
742
+ [x] database passwords in source code
743
+ [x] jwt secrets in source code
744
+ [x] private keys in repository
745
+ [x] credentials in config files
746
+
747
+ [ok] environment variables
748
+ [ok] secret management services
749
+ [ok] encrypted config files
750
+ [ok] runtime secret injection
751
+
752
+
753
+ environment variables
754
+
755
+ import os
756
+ from typing import Optional
757
+
758
+ def get_required_env(key: str) -> str:
759
+ """Get required environment variable or raise error."""
760
+ value = os.getenv(key)
761
+ if not value:
762
+ raise ValueError(f"Required environment variable {key} not set")
763
+ return value
764
+
765
+
766
+ def get_optional_env(key: str, default: Optional[str] = None) -> Optional[str]:
767
+ """Get optional environment variable with default."""
768
+ return os.getenv(key, default)
769
+
770
+
771
+ # usage
772
+ database_url = get_required_env('DATABASE_URL')
773
+ api_key = get_optional_env('API_KEY', 'default-key')
774
+
775
+
776
+ secret validation
777
+
778
+ def validate_database_url(url: str) -> str:
779
+ """Validate database URL format."""
780
+ if not url.startswith(('postgresql://', 'postgres://', 'mysql://')):
781
+ raise ValueError("Invalid database URL scheme")
782
+
783
+ # check for credentials in url
784
+ # warn if not using ssl
785
+ if '?' not in url or 'sslmode' not in url:
786
+ raise ValueError("Database connection must use SSL")
787
+
788
+ return url
789
+
790
+
791
+ secrets file with git-secrets
792
+
793
+ # .gitignore should include:
794
+ .env
795
+ .env.local
796
+ .env.*.local
797
+ *.key
798
+ *.pem
799
+ secrets.json
800
+ .secrets/
801
+
802
+ # git-secrets patterns to add:
803
+ git secrets --add 'password\s*=\s*["\']?[^"\']+$'
804
+ git secrets --add 'api_key\s*=\s*["\']?[^"\']+$'
805
+ git secrets --add 'secret\s*=\s*["\']?[^"\']+$'
806
+ git secrets --add 'AKIA[0-9A-Z]{16}' # AWS access keys
807
+
808
+
809
+ rotate secrets
810
+
811
+ from datetime import datetime, timedelta
812
+ import secrets
813
+
814
+ class SecretRotator:
815
+ """Manage secret rotation."""
816
+
817
+ def __init__(self, max_age_days: int = 90):
818
+ self.max_age = timedelta(days=max_age_days)
819
+
820
+ def should_rotate(self, created_at: datetime) -> bool:
821
+ """Check if secret needs rotation."""
822
+ return datetime.now() - created_at > self.max_age
823
+
824
+ def generate_new_secret(self) -> str:
825
+ """Generate a new secret."""
826
+ return secrets.token_urlsafe(32)
827
+
828
+ def rotate_api_key(self, old_key: str) -> str:
829
+ """Rotate an API key."""
830
+ new_key = self.generate_new_secret()
831
+ # store both temporarily for graceful transition
832
+ db.execute(
833
+ "INSERT INTO api_keys (key, old_key, expires_at) VALUES (?, ?, ?)",
834
+ (new_key, old_key, datetime.now() + timedelta(hours=24))
835
+ )
836
+ return new_key
837
+
838
+
839
+ PHASE 8: CRYPTOGRAPHY BEST PRACTICES
840
+
841
+
842
+ use established libraries
843
+
844
+ [ok] cryptography.io - general cryptography
845
+ [ok] PyNaCl - modern crypto (libsodium bindings)
846
+ [ok] bcrypt - password hashing
847
+ [x] hashlib.md5 - broken for security
848
+ [x] hashlib.sha1 - broken for security
849
+ [x] custom crypto algorithms - never roll your own
850
+
851
+
852
+ secure hashing
853
+
854
+ from cryptography.hazmat.primitives import hashes
855
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
856
+
857
+ def derive_key(password: bytes, salt: bytes) -> bytes:
858
+ """Derive a key from password using PBKDF2."""
859
+ kdf = PBKDF2(
860
+ algorithm=hashes.SHA256(),
861
+ length=32,
862
+ salt=salt,
863
+ iterations=480000, # owasp recommendation
864
+ )
865
+ return kdf.derive(password)
866
+
867
+
868
+ secure random
869
+
870
+ import secrets
871
+
872
+ # for tokens, session ids, api keys
873
+ token = secrets.token_hex(32) # 64 hex characters
874
+ token = secrets.token_urlsafe(32) # url-safe base64
875
+ token = secrets.token_bytes(32) # raw bytes
876
+
877
+ # never use:
878
+ random.random() # predictable
879
+ random.randint() # predictable
880
+ os.urandom() # okay, but secrets is better
881
+
882
+
883
+ encryption at rest
884
+
885
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
886
+
887
+ def encrypt_data(data: bytes, key: bytes) -> bytes:
888
+ """Encrypt data using AES-GCM (authenticated encryption)."""
889
+ aesgcm = AESGCM(key)
890
+ nonce = secrets.token_bytes(12) # 96-bit nonce for GCM
891
+ ciphertext = aesgcm.encrypt(nonce, data, None)
892
+ return nonce + ciphertext # prepend nonce for storage
893
+
894
+
895
+ def decrypt_data(ciphertext: bytes, key: bytes) -> bytes:
896
+ """Decrypt data using AES-GCM."""
897
+ aesgcm = AESGCM(key)
898
+ nonce = ciphertext[:12] # extract nonce
899
+ return aesgcm.decrypt(nonce, ciphertext[12:], None)
900
+
901
+
902
+ # usage
903
+ key = secrets.token_bytes(32) # 256-bit key
904
+ encrypted = encrypt_data(sensitive_data.encode(), key)
905
+ decrypted = decrypt_data(encrypted, key)
906
+
907
+
908
+ PHASE 9: DEPENDENCY SECURITY
909
+
910
+
911
+ audit dependencies regularly
912
+
913
+ <terminal>safety check --full-report</terminal>
914
+
915
+ <terminal>pip install pip-audit</terminal>
916
+ <terminal>pip-audit</terminal>
917
+
918
+
919
+ <terminal>pip install bandit</terminal>
920
+ <terminal>bandit -r . -f json -o security-report.json</terminal>
921
+
922
+
923
+ pin dependencies
924
+
925
+ requirements.txt with hashes:
926
+
927
+ # generate with:
928
+ <terminal>pip freeze > requirements.txt</terminal>
929
+ <terminal>pip hash <requirements.txt > requirements-hashes.txt</terminal>
930
+
931
+ # or use pip-tools:
932
+ <terminal>pip install pip-tools</terminal>
933
+ <terminal>pip-compile requirements.in --generate-hashes</terminal>
934
+
935
+
936
+ lock files
937
+
938
+ use pyproject.toml with poetry or pipenv:
939
+
940
+ [tool.poetry.dependencies]
941
+ python = "^3.11"
942
+ fastapi = "^2.0.0"
943
+ pydantic = "^2.0.0"
944
+
945
+ # lock file pins exact versions:
946
+ # poetry.lock
947
+
948
+
949
+ vulnerability scanning in ci
950
+
951
+ # .github/workflows/security.yml
952
+ name: Security Scan
953
+
954
+ on: [push, pull_request]
955
+
956
+ jobs:
957
+ security:
958
+ runs-on: ubuntu-latest
959
+ steps:
960
+ - uses: actions/checkout@v3
961
+
962
+ - name: Run Safety Check
963
+ run: |
964
+ pip install safety
965
+ safety check --continue-on-error
966
+
967
+ - name: Run Bandit
968
+ run: |
969
+ pip install bandit
970
+ bandit -r . -f json -o bandit-report.json
971
+
972
+ - name: Upload Reports
973
+ uses: actions/upload-artifact@v3
974
+ with:
975
+ name: security-reports
976
+ path: |
977
+ bandit-report.json
978
+
979
+
980
+ abandonware detection
981
+
982
+ check for unmaintained packages:
983
+
984
+ <terminal>pip install depcheck</terminal>
985
+ <terminal>depcheck</terminal>
986
+
987
+ manually verify:
988
+ - last release date on pypi
989
+ - open issues/prs on github
990
+ - security advisories
991
+
992
+
993
+ PHASE 10: SECURE CONFIGURATION
994
+
995
+
996
+ secure defaults
997
+
998
+ import os
999
+
1000
+ class Config:
1001
+ """Secure configuration defaults."""
1002
+
1003
+ # security
1004
+ SECRET_KEY = os.getenv('SECRET_KEY') or secrets.token_hex(32)
1005
+ DEBUG = False # always false in production
1006
+ TESTING = False
1007
+
1008
+ # sessions
1009
+ SESSION_COOKIE_SECURE = True
1010
+ SESSION_COOKIE_HTTPONLY = True
1011
+ SESSION_COOKIE_SAMESITE = 'Lax'
1012
+ PERMANENT_SESSION_LIFETIME = timedelta(hours=1)
1013
+
1014
+ # ssl/tls
1015
+ FORCE_HTTPS = True
1016
+ HSTS_ENABLED = True
1017
+ HSTS_MAX_AGE = 31536000 # 1 year
1018
+
1019
+ # limits
1020
+ MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
1021
+ MAX_REQUEST_SIZE = 1024 * 1024 # 1MB
1022
+
1023
+ # rate limiting
1024
+ RATE_LIMIT_ENABLED = True
1025
+ RATE_LIMIT_PER_MINUTE = 60
1026
+
1027
+
1028
+ environment-specific configs
1029
+
1030
+ class DevelopmentConfig(Config):
1031
+ """Development configuration."""
1032
+ DEBUG = True
1033
+ TESTING = False
1034
+
1035
+ # allow http in dev
1036
+ FORCE_HTTPS = False
1037
+ SESSION_COOKIE_SECURE = False
1038
+
1039
+
1040
+ class TestingConfig(Config):
1041
+ """Testing configuration."""
1042
+ TESTING = True
1043
+ DEBUG = True
1044
+
1045
+ # use in-memory database for tests
1046
+ DATABASE_URL = 'sqlite:///:memory:'
1047
+
1048
+
1049
+ class ProductionConfig(Config):
1050
+ """Production configuration - most secure."""
1051
+ DEBUG = False
1052
+ TESTING = False
1053
+
1054
+ # enforce security
1055
+ FORCE_HTTPS = True
1056
+ HSTS_ENABLED = True
1057
+
1058
+ # production-specific
1059
+ SENTRY_DSN = os.getenv('SENTRY_DSN')
1060
+
1061
+
1062
+ config_by_env = {
1063
+ 'development': DevelopmentConfig,
1064
+ 'testing': TestingConfig,
1065
+ 'production': ProductionConfig,
1066
+ }
1067
+
1068
+ def get_config() -> Config:
1069
+ """Get config based on environment."""
1070
+ env = os.getenv('FLASK_ENV', 'production')
1071
+ return config_by_env.get(env, ProductionConfig)()
1072
+
1073
+
1074
+ disable debug in production
1075
+
1076
+ critical check:
1077
+
1078
+ def ensure_production_safety():
1079
+ """Fail fast if debug mode enabled in production."""
1080
+ env = os.getenv('FLASK_ENV', 'production')
1081
+ debug = os.getenv('DEBUG', 'false').lower() == 'true'
1082
+
1083
+ if env == 'production' and debug:
1084
+ raise RuntimeError(
1085
+ "DEBUG mode enabled in production. "
1086
+ "This exposes sensitive information and is a security risk."
1087
+ )
1088
+
1089
+
1090
+ PHASE 11: LOGGING AND MONITORING
1091
+
1092
+
1093
+ security event logging
1094
+
1095
+ import logging
1096
+ from datetime import datetime
1097
+
1098
+ security_logger = logging.getLogger('security')
1099
+
1100
+ class SecurityEvent:
1101
+ """Log security-relevant events."""
1102
+
1103
+ EVENTS = {
1104
+ 'auth_success',
1105
+ 'auth_failure',
1106
+ 'permission_denied',
1107
+ 'rate_limit_exceeded',
1108
+ 'suspicious_input',
1109
+ 'csrf_failure',
1110
+ 'injection_attempt',
1111
+ }
1112
+
1113
+ @classmethod
1114
+ def log(cls, event_type: str, user_id: Optional[int],
1115
+ details: dict, ip: str, user_agent: str):
1116
+ """Log a security event."""
1117
+ if event_type not in cls.EVENTS:
1118
+ raise ValueError(f"Unknown event type: {event_type}")
1119
+
1120
+ security_logger.warning({
1121
+ 'event': event_type,
1122
+ 'timestamp': datetime.now().isoformat(),
1123
+ 'user_id': user_id,
1124
+ 'ip': ip,
1125
+ 'user_agent': user_agent,
1126
+ 'details': details,
1127
+ })
1128
+
1129
+
1130
+ # usage
1131
+ SecurityEvent.log(
1132
+ 'auth_failure',
1133
+ user_id=None,
1134
+ details={'username': input_username},
1135
+ ip=request.remote_addr,
1136
+ user_agent=request.headers.get('User-Agent', '')
1137
+ )
1138
+
1139
+
1140
+ sanitize logs
1141
+
1142
+ def sanitize_log_data(data: dict) -> dict:
1143
+ """Remove sensitive data before logging."""
1144
+ sensitive_keys = {
1145
+ 'password', 'passwd', 'pwd',
1146
+ 'token', 'secret', 'api_key',
1147
+ 'ssn', 'credit_card', 'cc',
1148
+ 'session', 'cookie',
1149
+ }
1150
+
1151
+ cleaned = {}
1152
+ for key, value in data.items():
1153
+ if any(sensitive in key.lower() for sensitive in sensitive_keys):
1154
+ cleaned[key] = '[REDACTED]'
1155
+ elif isinstance(value, dict):
1156
+ cleaned[key] = sanitize_log_data(value)
1157
+ else:
1158
+ cleaned[key] = value
1159
+ return cleaned
1160
+
1161
+
1162
+ log injection prevention
1163
+
1164
+ import re
1165
+
1166
+ def sanitize_log_input(text: str) -> str:
1167
+ """Prevent log injection attacks."""
1168
+ # remove crlf characters
1169
+ text = re.sub(r'[\r\n]', '', text)
1170
+ # limit length
1171
+ text = text[:1000]
1172
+ return text
1173
+
1174
+
1175
+ PHASE 12: API SECURITY
1176
+
1177
+
1178
+ api key management
1179
+
1180
+ from typing import Optional
1181
+ import secrets
1182
+
1183
+ class APIKeyManager:
1184
+ """Manage API keys for external access."""
1185
+
1186
+ def create_key(self, user_id: int, name: str) -> str:
1187
+ """Create a new API key."""
1188
+ prefix = f"kk_{user_id}_"
1189
+ key_secret = secrets.token_urlsafe(32)
1190
+ full_key = f"{prefix}{key_secret}"
1191
+
1192
+ # store hash, not the key itself
1193
+ key_hash = hash_api_key(full_key)
1194
+ db.execute(
1195
+ "INSERT INTO api_keys (user_id, name, key_hash, created_at) "
1196
+ "VALUES (?, ?, ?, ?)",
1197
+ (user_id, name, key_hash, datetime.now())
1198
+ )
1199
+ return full_key
1200
+
1201
+ def validate_key(self, key: str) -> Optional[dict]:
1202
+ """Validate an API key and return user info."""
1203
+ key_hash = hash_api_key(key)
1204
+ result = db.execute(
1205
+ "SELECT user_id, name, scopes FROM api_keys WHERE key_hash = ? AND active = 1",
1206
+ (key_hash,)
1207
+ ).fetchone()
1208
+ return result
1209
+
1210
+
1211
+ def hash_api_key(key: str) -> str:
1212
+ """Hash an API key for storage."""
1213
+ import hashlib
1214
+ return hashlib.sha256(key.encode()).hexdigest()
1215
+
1216
+
1217
+ rate limiting
1218
+
1219
+ from functools import wraps
1220
+ import time
1221
+ from collections import defaultdict
1222
+
1223
+ class RateLimiter:
1224
+ """Simple in-memory rate limiter."""
1225
+
1226
+ def __init__(self, requests_per_minute: int = 60):
1227
+ self.rpm = requests_per_minute
1228
+ self.requests = defaultdict(list)
1229
+
1230
+ def is_allowed(self, identifier: str) -> bool:
1231
+ """Check if request is allowed."""
1232
+ now = time.time()
1233
+ minute_ago = now - 60
1234
+
1235
+ # clean old requests
1236
+ self.requests[identifier] = [
1237
+ t for t in self.requests[identifier] if t > minute_ago
1238
+ ]
1239
+
1240
+ if len(self.requests[identifier]) >= self.rpm:
1241
+ return False
1242
+
1243
+ self.requests[identifier].append(now)
1244
+ return True
1245
+
1246
+
1247
+ # decorator
1248
+ rate_limiter = RateLimiter(requests_per_minute=60)
1249
+
1250
+ def rate_limit(f):
1251
+ @wraps(f)
1252
+ def wrapped(*args, **kwargs):
1253
+ identifier = request.remote_addr
1254
+ if not rate_limiter.is_allowed(identifier):
1255
+ return jsonify({"error": "Rate limit exceeded"}), 429
1256
+ return f(*args, **kwargs)
1257
+ return wrapped
1258
+
1259
+
1260
+ api versioning
1261
+
1262
+ @app.route('/api/v1/users')
1263
+ def list_users_v1():
1264
+ # deprecated but maintained
1265
+ return jsonify({"users": get_users()})
1266
+
1267
+
1268
+ @app.route('/api/v2/users')
1269
+ def list_users_v2():
1270
+ # current version with security improvements
1271
+ return jsonify({"users": get_users_v2()})
1272
+
1273
+
1274
+ PHASE 13: FILE UPLOAD SECURITY
1275
+
1276
+
1277
+ validate file uploads
1278
+
1279
+ import imghdr
1280
+ import os
1281
+ from pathlib import Path
1282
+
1283
+ ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.pdf'}
1284
+ ALLOWED_MIMES = {
1285
+ 'image/jpeg', 'image/png', 'image/gif', 'application/pdf'
1286
+ }
1287
+ MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
1288
+
1289
+
1290
+ def validate_upload(file) -> tuple[bool, str]:
1291
+ """Validate an uploaded file."""
1292
+ # check file size
1293
+ file.seek(0, os.SEEK_END)
1294
+ size = file.tell()
1295
+ file.seek(0)
1296
+
1297
+ if size > MAX_FILE_SIZE:
1298
+ return False, f"File too large (max {MAX_FILE_SIZE} bytes)"
1299
+ if size == 0:
1300
+ return False, "Empty file"
1301
+
1302
+ # check extension
1303
+ filename = Path(file.filename)
1304
+ if filename.suffix.lower() not in ALLOWED_EXTENSIONS:
1305
+ return False, f"Invalid file type. Allowed: {ALLOWED_EXTENSIONS}"
1306
+
1307
+ # check mime type
1308
+ mime = file.content_type
1309
+ if mime not in ALLOWED_MIMES:
1310
+ return False, f"Invalid content type: {mime}"
1311
+
1312
+ # for images, verify actual content
1313
+ if mime.startswith('image/'):
1314
+ header = file.read(32)
1315
+ file.seek(0)
1316
+ if not imghdr.what(None, header):
1317
+ return False, "Invalid image file"
1318
+
1319
+ return True, "Valid"
1320
+
1321
+
1322
+ sanitize filenames
1323
+
1324
+ import re
1325
+ from datetime import datetime
1326
+
1327
+ def sanitize_upload_filename(filename: str) -> str:
1328
+ """Generate safe filename for upload."""
1329
+ # extract extension
1330
+ parts = filename.rsplit('.', 1)
1331
+ if len(parts) != 2:
1332
+ raise ValueError("File must have extension")
1333
+ name, ext = parts
1334
+
1335
+ # sanitize name
1336
+ name = re.sub(r'[^a-zA-Z0-9_-]', '_', name)
1337
+ name = name[:50] # limit length
1338
+
1339
+ # add timestamp for uniqueness
1340
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
1341
+
1342
+ return f"{timestamp}_{name}.{ext.lower()}"
1343
+
1344
+
1345
+ store outside webroot
1346
+
1347
+ def save_upload(file) -> str:
1348
+ """Save uploaded file securely."""
1349
+ is_valid, msg = validate_upload(file)
1350
+ if not is_valid:
1351
+ raise ValueError(msg)
1352
+
1353
+ safe_name = sanitize_upload_filename(file.filename)
1354
+
1355
+ # store outside web root
1356
+ upload_dir = Path('/var/app/uploads')
1357
+ upload_dir.mkdir(mode=0o750, exist_ok=True)
1358
+
1359
+ file_path = upload_dir / safe_name
1360
+ file.save(str(file_path))
1361
+
1362
+ # set restrictive permissions
1363
+ os.chmod(file_path, 0o640)
1364
+
1365
+ # return identifier, not path
1366
+ return safe_name
1367
+
1368
+
1369
+ serve via application
1370
+
1371
+ @app.route('/uploads/<filename>')
1372
+ def serve_upload(filename):
1373
+ """Serve uploaded files through application (not direct access)."""
1374
+ # verify user has access to this file
1375
+ # check permissions
1376
+ # log access
1377
+ # then serve
1378
+
1379
+ file_path = Path('/var/app/uploads') / filename
1380
+ if not file_path.exists():
1381
+ return "Not found", 404
1382
+
1383
+ return send_file(file_path)
1384
+
1385
+
1386
+ PHASE 14: DATABASE SECURITY
1387
+
1388
+
1389
+ database connection security
1390
+
1391
+ from urllib.parse import parse_qs
1392
+
1393
+ def validate_db_url(url: str) -> str:
1394
+ """Ensure database URL is secure."""
1395
+ if not url.startswith(('postgresql://', 'mysql://')):
1396
+ raise ValueError("Only postgresql and mysql are supported")
1397
+
1398
+ # require ssl
1399
+ if 'sslmode' not in url:
1400
+ raise ValueError("Database connection must use SSL")
1401
+
1402
+ parsed = parse_qs(url)
1403
+ # verify no plaintext credentials in logs
1404
+ return url
1405
+
1406
+
1407
+ least privilege database user
1408
+
1409
+ application should use limited database user:
1410
+
1411
+ CREATE USER kollabor_app WITH PASSWORD 'secure_password';
1412
+
1413
+ GRANT CONNECT ON DATABASE kollabor_db TO kollabor_app;
1414
+
1415
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO kollabor_app;
1416
+ GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO kollabor_app;
1417
+
1418
+ -- no create, drop, alter permissions
1419
+
1420
+
1421
+ query result limits
1422
+
1423
+ def paginated_query(query: str, page: int, per_page: int = 50):
1424
+ """Execute query with pagination limits."""
1425
+ if per_page > 100:
1426
+ per_page = 100 # max limit
1427
+ if page < 1:
1428
+ page = 1
1429
+
1430
+ offset = (page - 1) * per_page
1431
+ limited_query = f"{query} LIMIT {per_page} OFFSET {offset}"
1432
+
1433
+ return db.execute(limited_query).fetchall()
1434
+
1435
+
1436
+ PHASE 15: SECURITY TESTING
1437
+
1438
+
1439
+ security unit tests
1440
+
1441
+ import pytest
1442
+
1443
+ def test_sql_injection_prevented():
1444
+ """Test that SQL injection is prevented."""
1445
+ malicious_id = "1 OR 1=1; DROP TABLE users; --"
1446
+
1447
+ # should raise validation error
1448
+ with pytest.raises(ValueError):
1449
+ get_user(malicious_id)
1450
+
1451
+ # or should only find user 1
1452
+ result = get_user_safe(malicious_id)
1453
+ assert result is None or result['id'] == 1
1454
+
1455
+
1456
+ def test_xss_prevention():
1457
+ """Test that XSS is prevented."""
1458
+ xss_payload = "<script>alert('XSS')</script>"
1459
+
1460
+ rendered = render_comment(xss_payload)
1461
+
1462
+ assert "<script>" not in rendered
1463
+ assert "&lt;script&gt;" in rendered
1464
+
1465
+
1466
+ def test_csrf_protection():
1467
+ """Test CSRF token validation."""
1468
+ client = TestClient(app)
1469
+
1470
+ # request without token should fail
1471
+ response = client.post('/transfer', json={
1472
+ 'to': 'victim',
1473
+ 'amount': 100
1474
+ })
1475
+ assert response.status_code == 403
1476
+
1477
+ # request with invalid token should fail
1478
+ response = client.post('/transfer', json={
1479
+ 'csrf_token': 'invalid',
1480
+ 'to': 'victim',
1481
+ 'amount': 100
1482
+ })
1483
+ assert response.status_code == 403
1484
+
1485
+
1486
+ def test_password_validation():
1487
+ """Test password strength requirements."""
1488
+ weak_passwords = [
1489
+ 'password',
1490
+ 'Password1',
1491
+ 'short',
1492
+ 'noooooooonumbers',
1493
+ '123456789012',
1494
+ ]
1495
+
1496
+ for pwd in weak_passwords:
1497
+ errors = validate_password_strength(pwd)
1498
+ assert len(errors) > 0, f"Should reject: {pwd}"
1499
+
1500
+
1501
+ security integration tests
1502
+
1503
+ def test_authentication_flow():
1504
+ """Test secure authentication flow."""
1505
+ client = TestClient(app)
1506
+
1507
+ # register
1508
+ response = client.post('/register', json={
1509
+ 'username': 'testuser',
1510
+ 'password': 'SecurePass123!',
1511
+ 'email': 'test@example.com'
1512
+ })
1513
+ assert response.status_code == 201
1514
+
1515
+ # login with correct password
1516
+ response = client.post('/login', json={
1517
+ 'username': 'testuser',
1518
+ 'password': 'SecurePass123!'
1519
+ })
1520
+ assert response.status_code == 200
1521
+ assert 'token' in response.json()
1522
+
1523
+ # login with wrong password fails
1524
+ response = client.post('/login', json={
1525
+ 'username': 'testuser',
1526
+ 'password': 'WrongPass123!'
1527
+ })
1528
+ assert response.status_code == 401
1529
+
1530
+
1531
+ penetration testing
1532
+
1533
+ run security scans:
1534
+
1535
+ <terminal>bandit -r . -f json -o bandit-report.json</terminal>
1536
+
1537
+ <terminal>safety check --json > safety-report.json</terminal>
1538
+
1539
+ <terminal>pip-audit --format json > pip-audit-report.json</terminal>
1540
+
1541
+ manual test checklist:
1542
+ [ ] try sql injection in all inputs
1543
+ [ ] try xss payloads in text fields
1544
+ [ ] test csrf without tokens
1545
+ [ ] try authentication bypass
1546
+ [ ] test idor (insecure direct object reference)
1547
+ [ ] test rate limiting
1548
+ [ ] try file upload exploits
1549
+
1550
+
1551
+ PHASE 16: SECURITY RULES (STRICT MODE)
1552
+
1553
+
1554
+ while this skill is active, these rules are MANDATORY:
1555
+
1556
+ [1] VALIDATE ALL INPUT
1557
+ never trust data from user, api, or database
1558
+ whitelist allowed values, don't blacklist bad ones
1559
+
1560
+ [2] USE PARAMETERIZED QUERIES
1561
+ never concatenate strings into sql
1562
+ always use ? placeholders or orm
1563
+
1564
+ [3] ESCAPE ALL OUTPUT
1565
+ html escape user content before rendering
1566
+ use template engines with autoescape
1567
+
1568
+ [4] HASH PASSWORDS PROPERLY
1569
+ use bcrypt, argon2, or scrypt
1570
+ never store plaintext passwords
1571
+ never use md5, sha1, or custom algorithms
1572
+
1573
+ [5] USE CSRF TOKENS
1574
+ all state-changing requests need csrf protection
1575
+ validate tokens on server side
1576
+
1577
+ [6] ENABLE SECURITY HEADERS
1578
+ csp, x-frame-options, x-content-type-options
1579
+ httponly and secure cookies
1580
+
1581
+ [7] NEVER EXPOSE SECRETS
1582
+ no api keys in source code
1583
+ use environment variables or secret managers
1584
+ add sensitive patterns to git-secrets
1585
+
1586
+ [8] IMPLEMENT RATE LIMITING
1587
+ protect authentication endpoints
1588
+ protect api endpoints
1589
+ log rate limit violations
1590
+
1591
+ [9] LOG SECURITY EVENTS
1592
+ auth failures
1593
+ permission denials
1594
+ suspicious activity
1595
+
1596
+ [10] KEEP DEPENDENCIES UPDATED
1597
+ run security scans regularly
1598
+ update vulnerable packages promptly
1599
+
1600
+ [11] DISABLE DEBUG IN PRODUCTION
1601
+ debug mode exposes sensitive information
1602
+ fail fast if debug enabled in production
1603
+
1604
+ [12] USE HTTPS EVERYWHERE
1605
+ redirect http to https
1606
+ enable hsts
1607
+ secure cookies only
1608
+
1609
+ [13] PRINCIPLE OF LEAST PRIVILEGE
1610
+ users get minimum required access
1611
+ services get minimum required permissions
1612
+
1613
+ [14] DEFENSE IN DEPTH
1614
+ multiple layers of security
1615
+ if one layer fails, others protect
1616
+
1617
+ [15] SECURITY BY DEFAULT
1618
+ secure options should be default
1619
+ users should have to opt-in to less secure
1620
+
1621
+
1622
+ PHASE 17: SECURITY CHECKLIST
1623
+
1624
+
1625
+ before deploying to production:
1626
+
1627
+ input validation:
1628
+ [ ] all user input validated
1629
+ [ ] length limits enforced
1630
+ [ ] type checking implemented
1631
+ [ ] whitelist patterns used
1632
+ [ ] file uploads validated
1633
+
1634
+ authentication:
1635
+ [ ] passwords hashed with bcrypt/argon2
1636
+ [ ] session management secure
1637
+ [ ] csrf tokens implemented
1638
+ [ ] rate limiting on auth endpoints
1639
+ [ ] secure password requirements
1640
+
1641
+ authorization:
1642
+ [ ] access control on all endpoints
1643
+ [ ] ownership checks for resources
1644
+ [ ] role-based permissions
1645
+ [ ] admin actions protected
1646
+
1647
+ data protection:
1648
+ [ ] secrets in environment variables
1649
+ [ ] sensitive data encrypted at rest
1650
+ [ ] tls for data in transit
1651
+ [ ] database connections use ssl
1652
+ [ ] api keys stored hashed
1653
+
1654
+ output encoding:
1655
+ [ ] html escaping enabled
1656
+ [ ] template autoescape on
1657
+ [ ] json content-type headers
1658
+ [ ] csp headers configured
1659
+
1660
+ logging:
1661
+ [ ] security events logged
1662
+ [ ] sensitive data redacted
1663
+ [ ] log injection prevented
1664
+ [ ] log rotation configured
1665
+
1666
+ dependencies:
1667
+ [ ] no known vulnerabilities
1668
+ [ ] dependencies pinned
1669
+ [ ] security scanning in ci
1670
+ [ ] update process defined
1671
+
1672
+ infrastructure:
1673
+ [ ] https enforced
1674
+ [ ] hsts enabled
1675
+ [ ] security headers configured
1676
+ [ ] debug mode disabled
1677
+ [ ] backups encrypted
1678
+
1679
+ monitoring:
1680
+ [ ] failed auth alerts
1681
+ [ ] rate limit alerts
1682
+ [ ] anomaly detection
1683
+ [ ] incident response plan
1684
+
1685
+
1686
+ FINAL REMINDERS
1687
+
1688
+
1689
+ security is a process, not a feature
1690
+
1691
+ it starts at design.
1692
+ it continues through development.
1693
+ it doesn't end at deployment.
1694
+
1695
+
1696
+ there is no secure software
1697
+
1698
+ there is only software that has been:
1699
+ - analyzed for vulnerabilities
1700
+ - tested against attacks
1701
+ - monitored for intrusions
1702
+ - updated when issues found
1703
+
1704
+
1705
+ the attacker only needs to be right once
1706
+
1707
+ you need to be right every time.
1708
+ defense in depth is essential.
1709
+
1710
+
1711
+ when in doubt
1712
+
1713
+ err on the side of security.
1714
+ validate one more time.
1715
+ add one more layer of protection.
1716
+ log one more event.
1717
+
1718
+
1719
+ your responsibility
1720
+
1721
+ every line of code you write could introduce a vulnerability.
1722
+ every feature you build needs security consideration.
1723
+ every deployment needs security validation.
1724
+
1725
+ users trust you with their data.
1726
+ their privacy.
1727
+ their security.
1728
+
1729
+ don't betray that trust.
1730
+
1731
+
1732
+ now go write secure code.