csrd-utils 0.5.2__tar.gz → 0.5.4__tar.gz

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 (108) hide show
  1. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/PKG-INFO +1 -1
  2. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/pyproject.toml +1 -1
  3. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/audit.py +130 -4
  4. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/compose/operations.py +5 -4
  5. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/compose/renderer.py +9 -0
  6. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/gateway/middleware/auth_guard.py.template +4 -4
  7. csrd_utils-0.5.4/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/Dockerfile +11 -0
  8. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -3
  9. csrd_utils-0.5.2/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -7
  10. csrd_utils-0.5.2/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/models/health.py +0 -10
  11. csrd_utils-0.5.2/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/tests/test_health.py +0 -7
  12. csrd_utils-0.5.2/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/views/health_view.py +0 -11
  13. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/.gitignore +0 -0
  14. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/README.md +0 -0
  15. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/AGENTS.md +0 -0
  16. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/__init__.py +0 -0
  17. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/__main__.py +0 -0
  18. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/compose/__init__.py +0 -0
  19. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/compose/augments.py +0 -0
  20. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/compose/git.py +0 -0
  21. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/compose/infra.py +0 -0
  22. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/compose/loader.py +0 -0
  23. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/compose/presets.py +0 -0
  24. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/compose/scaffolder.py +0 -0
  25. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/compose/service_renderers.py +0 -0
  26. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/compose/yaml_editor.py +0 -0
  27. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/doctor.py +0 -0
  28. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/generate/__init__.py +0 -0
  29. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/generate/handlers.py +0 -0
  30. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/generate/helpers.py +0 -0
  31. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/generate/menu.py +0 -0
  32. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/models/__init__.py +0 -0
  33. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/models/base.py +0 -0
  34. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/models/spec.py +0 -0
  35. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/models/types.py +0 -0
  36. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/__init__.py +0 -0
  37. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/__init__.py +0 -0
  38. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/auth-passthrough/__init__.py +0 -0
  39. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/auth-passthrough/delegates/__init__.py +0 -0
  40. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/auth-passthrough/delegates/auth_delegate.py +0 -0
  41. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/auth-passthrough/dependencies/__init__.py +0 -0
  42. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/auth-passthrough/dependencies/auth_passthrough.py +0 -0
  43. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/auth-passthrough/models/__init__.py +0 -0
  44. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/auth-passthrough/models/auth_passthrough.py +0 -0
  45. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/auth-passthrough/views/__init__.py +0 -0
  46. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/auth-passthrough/views/auth_passthrough_view.py +0 -0
  47. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/caching/dependencies/cache.py.template +0 -0
  48. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/caching/views/cache_view.py.template +0 -0
  49. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/celery-dispatcher/celery_client.py.template +0 -0
  50. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/celery-dispatcher/models/an/__init__.py +0 -0
  51. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/celery-dispatcher/models/an/tasks.py.template +0 -0
  52. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/celery-dispatcher/models/tasks.py.template +0 -0
  53. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/celery-dispatcher/views/tasks_view.py.template +0 -0
  54. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/celery-worker/celery_app.py.template +0 -0
  55. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/celery-worker/tasks/__init__.py +0 -0
  56. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/celery-worker/tasks/example.py.template +0 -0
  57. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/crud-scaffold/dependencies/${entity_name_snake}_repository.py.template +0 -0
  58. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/crud-scaffold/migrations_${entity_name_snake}.py.template +0 -0
  59. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/crud-scaffold/models/${entity_name_snake}.py.template +0 -0
  60. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/crud-scaffold/repositories/${entity_name_snake}_repository.py.template +0 -0
  61. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/crud-scaffold/views/${entity_name_snake}_view.py.template +0 -0
  62. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/db-config/dependencies/db.py.template +0 -0
  63. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/gateway/dependencies/proxy.py.template +0 -0
  64. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/gateway/views/proxy_view.py.template +0 -0
  65. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-consumer/dependencies/auth.py.template +0 -0
  66. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/AUTH.md +0 -0
  67. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/__init__.py +0 -0
  68. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/dependencies/__init__.py +0 -0
  69. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/dependencies/auth.py +0 -0
  70. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/dependencies/db.py.template +0 -0
  71. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/dependencies/token_service.py +0 -0
  72. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/migrations.py.template +0 -0
  73. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/models/__init__.py +0 -0
  74. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/models/auth.py +0 -0
  75. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/models/jwks.py +0 -0
  76. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/models/users.py +0 -0
  77. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/repositories/__init__.py +0 -0
  78. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/repositories/user_repository.py +0 -0
  79. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/services/__init__.py +0 -0
  80. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/services/token_service.py +0 -0
  81. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/views/__init__.py +0 -0
  82. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/views/auth_view.py +0 -0
  83. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/views/jwks_view.py +0 -0
  84. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/views/users_admin_view.py +0 -0
  85. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/jwt-auth-provider/views/users_view.py +0 -0
  86. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/metrics/middleware/metrics.py.template +0 -0
  87. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/metrics/views/metrics_view.py.template +0 -0
  88. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/rabbit-messaging/dependencies/rabbit.py.template +0 -0
  89. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/rabbit-messaging/handlers/example_handler.py.template +0 -0
  90. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/rabbit-messaging/handlers/ping_handler.py.template +0 -0
  91. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/rabbit-messaging/views/messaging_view.py.template +0 -0
  92. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/augments/tracing/middleware/tracing.py.template +0 -0
  93. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/service/cookiecutter.json +0 -0
  94. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  95. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  96. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
  97. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/models/__init__.py +0 -0
  98. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  99. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  100. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/tests/__init__.py +0 -0
  101. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/csrd_utils/templates/service/{{cookiecutter.__service_name_snake}}/tests/conftest.py +0 -0
  102. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/tui_wizard/__init__.py +0 -0
  103. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/tui_wizard/exceptions.py +0 -0
  104. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/tui_wizard/menu.py +0 -0
  105. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/tui_wizard/models.py +0 -0
  106. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/tui_wizard/prompts.py +0 -0
  107. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/tui_wizard/terminal.py +0 -0
  108. {csrd_utils-0.5.2 → csrd_utils-0.5.4}/src/tui_wizard/wizard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csrd-utils
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: CLI utilities for csrd service generation and feature augmentation
5
5
  Project-URL: Repository, https://github.com/csrd-api/fastapi-common
6
6
  Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/utils
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "csrd-utils"
3
- version = "0.5.2"
3
+ version = "0.5.4"
4
4
  description = "CLI utilities for csrd service generation and feature augmentation"
5
5
  license = { text = "MIT" }
6
6
  requires-python = ">=3.12"
@@ -38,7 +38,14 @@ class AuditReport:
38
38
 
39
39
 
40
40
  _WEAK_PASSWORDS = r"(?:change_me|change_me_root|app|sandbox|password|root)"
41
- _WEAK_IDENTIFIERS = r"(?:service_user|service_db|app|sandbox)"
41
+ _WEAK_IDENTIFIERS = r"(?:service_user|service_db|service_rabbit|app|sandbox)"
42
+ _WEAK_JWT_SECRETS = r"(?:changeme[^\s]*|secret|test|jwt[_-]?secret|please[_-]?change)"
43
+
44
+ # Infra ports that should not be host-bound in production
45
+ _INFRA_PORT_PATTERNS = re.compile(
46
+ r'"(?:0\.0\.0\.0:)?(\d+):(\d+)"',
47
+ )
48
+ _SENSITIVE_INFRA_PORTS = {"5432", "3306", "6379", "5672", "15672", "27017"}
42
49
 
43
50
 
44
51
  def _scan_file(path: Path, root: Path) -> list[AuditFinding]:
@@ -86,7 +93,7 @@ def _scan_file(path: Path, root: Path) -> list[AuditFinding]:
86
93
  )
87
94
 
88
95
  identifier_hits = re.findall(
89
- rf"(?:DB_USER|POSTGRES_USER|MYSQL_USER|MARIADB_USER|DB_NAME|POSTGRES_DB|MYSQL_DATABASE|MARIADB_DATABASE)\s*[:=]\s*['\"]?({_WEAK_IDENTIFIERS})['\"]?",
96
+ rf"(?:DB_USER|POSTGRES_USER|MYSQL_USER|MARIADB_USER|DB_NAME|POSTGRES_DB|MYSQL_DATABASE|MARIADB_DATABASE|RABBITMQ_(?:DEFAULT_)?USER)\s*[:=]\s*['\"]?({_WEAK_IDENTIFIERS})['\"]?",
90
97
  text,
91
98
  flags=re.IGNORECASE,
92
99
  )
@@ -150,6 +157,114 @@ def _scan_file(path: Path, root: Path) -> list[AuditFinding]:
150
157
  )
151
158
  )
152
159
 
160
+ # ── New rules (M3 expansion) ──────────────────────────────────────
161
+
162
+ # Weak JWT secret
163
+ jwt_hits = re.findall(
164
+ rf"JWT_SECRET\s*[:=]\s*['\"]?({_WEAK_JWT_SECRETS})['\"]?",
165
+ text,
166
+ flags=re.IGNORECASE,
167
+ )
168
+ if jwt_hits:
169
+ findings.append(
170
+ AuditFinding(
171
+ rule="weak-jwt-secret",
172
+ severity="high",
173
+ message="JWT signing secret uses a weak or default value.",
174
+ file=rel,
175
+ evidence=jwt_hits[0],
176
+ remediation='Generate a strong random secret: python -c "import secrets; print(secrets.token_urlsafe(48))"',
177
+ )
178
+ )
179
+
180
+ # Weak RabbitMQ password
181
+ rabbit_pw_hits = re.findall(
182
+ rf"RABBITMQ_(?:DEFAULT_)?PASS(?:WORD)?\s*[:=]\s*['\"]?({_WEAK_PASSWORDS})['\"]?",
183
+ text,
184
+ flags=re.IGNORECASE,
185
+ )
186
+ if rabbit_pw_hits:
187
+ findings.append(
188
+ AuditFinding(
189
+ rule="weak-rabbitmq-password",
190
+ severity="high",
191
+ message="RabbitMQ password uses a weak default.",
192
+ file=rel,
193
+ evidence=rabbit_pw_hits[0],
194
+ remediation="Set a strong RABBITMQ_PASSWORD via environment configuration.",
195
+ )
196
+ )
197
+
198
+ # Redis URL without authentication
199
+ redis_no_auth = re.findall(
200
+ r"(?:REDIS_URL|BROKER_URL|CELERY_BROKER_URL|RESULT_BACKEND)\s*[:=]\s*['\"]?(redis://[^:@\s]*:\d+)",
201
+ text,
202
+ flags=re.IGNORECASE,
203
+ )
204
+ if redis_no_auth:
205
+ findings.append(
206
+ AuditFinding(
207
+ rule="redis-no-auth",
208
+ severity="medium",
209
+ message="Redis URL has no password — any network client can read/write data.",
210
+ file=rel,
211
+ evidence=redis_no_auth[0],
212
+ remediation="Add requirepass to Redis and use redis://:PASSWORD@host:port in URLs.",
213
+ )
214
+ )
215
+
216
+ # Exposed infra ports in docker-compose (only scan compose files)
217
+ if path.name in ("docker-compose.yml", "docker-compose.yaml"):
218
+ for match in _INFRA_PORT_PATTERNS.finditer(text):
219
+ host_port = match.group(1)
220
+ if host_port in _SENSITIVE_INFRA_PORTS:
221
+ # Check if bound to 127.0.0.1 — that's acceptable
222
+ full_match = match.group(0)
223
+ if "127.0.0.1" not in full_match:
224
+ findings.append(
225
+ AuditFinding(
226
+ rule="exposed-infra-port",
227
+ severity="medium",
228
+ message=f"Infrastructure port {host_port} is bound to all interfaces.",
229
+ file=rel,
230
+ evidence=full_match,
231
+ remediation="Bind to 127.0.0.1 for dev or remove host mapping for prod.",
232
+ )
233
+ )
234
+
235
+ # Debug mode enabled
236
+ debug_hits = re.findall(
237
+ r"(?:^|\n)\s*DEBUG\s*[:=]\s*['\"]?(?:true|1|yes)['\"]?",
238
+ text,
239
+ flags=re.IGNORECASE,
240
+ )
241
+ if debug_hits:
242
+ findings.append(
243
+ AuditFinding(
244
+ rule="debug-mode-enabled",
245
+ severity="medium",
246
+ message="Debug mode is enabled — may expose stack traces and internal state.",
247
+ file=rel,
248
+ evidence="DEBUG=true",
249
+ remediation="Set DEBUG=false in production environments.",
250
+ )
251
+ )
252
+
253
+ # Shell-form CMD in Dockerfiles (no signal forwarding)
254
+ if path.name.startswith("Dockerfile"):
255
+ cmd_hits = re.findall(r"^CMD\s+(?!exec\b)(?!\[)(.+)$", text, flags=re.MULTILINE)
256
+ if cmd_hits:
257
+ findings.append(
258
+ AuditFinding(
259
+ rule="shell-form-cmd",
260
+ severity="medium",
261
+ message="Dockerfile CMD uses shell form without exec — no signal forwarding.",
262
+ file=rel,
263
+ evidence=f"CMD {cmd_hits[0][:60]}",
264
+ remediation="Use 'CMD exec ...' or CMD in JSON array form for proper signal handling.",
265
+ )
266
+ )
267
+
153
268
  return findings
154
269
 
155
270
 
@@ -172,12 +287,23 @@ def run_audit(service_root: Path) -> AuditReport:
172
287
  return AuditReport(ok=False, findings=findings)
173
288
 
174
289
  candidates: list[Path] = []
175
- include_names = {"docker-compose.yml", ".env", ".env.example", "README.md", "settings.py"}
290
+ include_names = {
291
+ "docker-compose.yml",
292
+ "docker-compose.yaml",
293
+ ".env",
294
+ ".env.example",
295
+ "README.md",
296
+ "settings.py",
297
+ }
176
298
 
177
299
  for path in root.rglob("*"):
178
300
  if not path.is_file():
179
301
  continue
180
- if path.name in include_names or path.suffix in {".py", ".yml", ".yaml", ".env", ".md"}:
302
+ if (
303
+ path.name in include_names
304
+ or path.name.startswith("Dockerfile")
305
+ or path.suffix in {".py", ".yml", ".yaml", ".env", ".md"}
306
+ ):
181
307
  candidates.append(path)
182
308
 
183
309
  seen: set[tuple[str, str, str]] = set()
@@ -72,11 +72,12 @@ def render_workspace(output_dir: Path, *, git_init: bool = False) -> Path:
72
72
  env_example_content = _render_env_example(spec)
73
73
  (output_dir / ".env.example").write_text(env_example_content, encoding="utf-8")
74
74
 
75
- # Seed .env from .env.example so services can start immediately.
76
- # Always overwritten to stay in sync — user overrides belong in
77
- # docker-compose overrides or a .env.local (not tracked).
75
+ # Seed .env from .env.example only on first render so services can
76
+ # start immediately. On subsequent renders the user may have set
77
+ # strong passwords or API keys never overwrite those.
78
78
  env_path = output_dir / ".env"
79
- env_path.write_text(env_example_content, encoding="utf-8")
79
+ if not env_path.exists():
80
+ env_path.write_text(env_example_content, encoding="utf-8")
80
81
  (output_dir / "README.md").write_text(_render_readme(spec), encoding="utf-8")
81
82
  (output_dir / ".gitignore").write_text(_render_gitignore(), encoding="utf-8")
82
83
  (output_dir / ".dockerignore").write_text(_render_dockerignore(), encoding="utf-8")
@@ -614,7 +614,16 @@ def _render_gateway_routes(spec: ComposeSpec) -> str | None:
614
614
  if not routes:
615
615
  return None
616
616
 
617
+ gateway = next(s for s in spec.services if s.role == "gateway")
618
+
617
619
  lines = ["# Gateway route table — auto-generated, but safe to edit manually.", "routes:"]
620
+
621
+ # Health probe must bypass the JWT guard so Docker HEALTHCHECK and
622
+ # external load balancers can reach it without credentials.
623
+ lines.append(' - prefix: "/_info/health"')
624
+ lines.append(f' target: "http://{gateway.name}:{gateway.port}"')
625
+ lines.append(" public: true")
626
+
618
627
  for route in routes:
619
628
  lines.append(f' - prefix: "{route["prefix"]}"')
620
629
  lines.append(f' target: "{route["target"]}"')
@@ -30,7 +30,10 @@ _authenticator = JWTAuthenticator(
30
30
 
31
31
 
32
32
  def _load_public_prefixes() -> list[str]:
33
- """Load public prefixes (no JWT required) from gateway-routes.yaml."""
33
+ """Load public prefixes (no JWT required) from gateway-routes.yaml.
34
+
35
+ Loaded once at middleware init — changes require a gateway restart.
36
+ """
34
37
  candidates = [
35
38
  Path("/app/gateway-routes.yaml"),
36
39
  Path(__file__).resolve().parent.parent / "gateway-routes.yaml",
@@ -60,9 +63,6 @@ class GatewayAuthMiddleware(BaseHTTPMiddleware):
60
63
  if path.startswith(prefix):
61
64
  return await call_next(request)
62
65
 
63
- # Allow health/actuator endpoints on the gateway itself
64
- if path in ("/health", "/actuator/health", "/actuator/info"):
65
- return await call_next(request)
66
66
 
67
67
  # Validate JWT
68
68
  auth_header = request.headers.get("authorization", "")
@@ -0,0 +1,11 @@
1
+ FROM python:3.12-slim AS base
2
+ WORKDIR /app
3
+ COPY ./src/{{cookiecutter.__service_name_snake}}/requirements.txt /app/requirements.txt
4
+ RUN pip install --no-cache-dir -r /app/requirements.txt
5
+ COPY ./src/{{cookiecutter.__service_name_snake}} /app/{{cookiecutter.__service_name_snake}}
6
+ RUN adduser --system --no-create-home appuser
7
+ USER appuser
8
+ ENV PORT={{ cookiecutter.port }}
9
+ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \
10
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:${PORT}/_info/health')" || exit 1
11
+ CMD exec uvicorn {{cookiecutter.__service_name_snake}}:build_app --factory --host 0.0.0.0 --port ${PORT}
@@ -1,9 +1,6 @@
1
1
  from fastapi import FastAPI
2
2
 
3
- from . import health_view
4
-
5
3
  app = FastAPI(title="{{ cookiecutter.service_name }}")
6
4
 
7
- app.include_router(health_view.router)
8
5
 
9
6
  __all__ = ("app",)
@@ -1,7 +0,0 @@
1
- FROM python:3.12-slim
2
- WORKDIR /app
3
- COPY ./src/{{cookiecutter.__service_name_snake}}/requirements.txt /app/requirements.txt
4
- RUN pip install --no-cache-dir -r /app/requirements.txt
5
- COPY ./src/{{cookiecutter.__service_name_snake}} /app/{{cookiecutter.__service_name_snake}}
6
- ENV PORT={{ cookiecutter.port }}
7
- CMD uvicorn {{cookiecutter.__service_name_snake}}:build_app --factory --host 0.0.0.0 --port ${PORT}
@@ -1,10 +0,0 @@
1
- """Health check models."""
2
-
3
- from csrd.models import BaseModel
4
-
5
-
6
- class HealthResponse(BaseModel):
7
- """Health check response."""
8
-
9
- status: str
10
- service: str
@@ -1,7 +0,0 @@
1
- """Health endpoint tests for {{ cookiecutter.service_name }}."""
2
-
3
-
4
- def test_health(client):
5
- response = client.get("/health")
6
- assert response.status_code == 200
7
- assert response.json() == {"status": "ok", "service": "{{ cookiecutter.service_name }}"}
@@ -1,11 +0,0 @@
1
- from fastapi import APIRouter
2
-
3
- from ..models.health import HealthResponse
4
-
5
- router = APIRouter(tags=["Health"])
6
-
7
-
8
-
9
- @router.get("/health", response_model=HealthResponse)
10
- def health() -> HealthResponse:
11
- return HealthResponse(status="ok", service="{{ cookiecutter.service_name }}")
File without changes
File without changes