scriptgini 1.5.1__tar.gz → 1.5.2__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 (71) hide show
  1. {scriptgini-1.5.1 → scriptgini-1.5.2}/PKG-INFO +4 -4
  2. {scriptgini-1.5.1 → scriptgini-1.5.2}/README.md +3 -3
  3. scriptgini-1.5.2/app/__init__.py +3 -0
  4. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/api_key.py +28 -1
  5. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/scripts.py +75 -15
  6. {scriptgini-1.5.1 → scriptgini-1.5.2}/pyproject.toml +1 -1
  7. {scriptgini-1.5.1 → scriptgini-1.5.2}/scriptgini.egg-info/PKG-INFO +4 -4
  8. {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_auth.py +74 -2
  9. {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_coverage.py +17 -2
  10. scriptgini-1.5.1/app/__init__.py +0 -3
  11. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/agents/__init__.py +0 -0
  12. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/agents/prompts.py +0 -0
  13. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/agents/script_gini_agent.py +0 -0
  14. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/cache.py +0 -0
  15. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/celery_app.py +0 -0
  16. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/config.py +0 -0
  17. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/database.py +0 -0
  18. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/llm/__init__.py +0 -0
  19. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/llm/provider.py +0 -0
  20. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/main.py +0 -0
  21. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/__init__.py +0 -0
  22. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/api_key.py +0 -0
  23. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/bulk_job.py +0 -0
  24. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/execution_job.py +0 -0
  25. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/generated_script.py +0 -0
  26. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/membership.py +0 -0
  27. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/organization.py +0 -0
  28. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/project.py +0 -0
  29. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/script_revision.py +0 -0
  30. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/script_run.py +0 -0
  31. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/test_case.py +0 -0
  32. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/user.py +0 -0
  33. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/__init__.py +0 -0
  34. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/analytics.py +0 -0
  35. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/auth.py +0 -0
  36. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/bulk_jobs.py +0 -0
  37. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/demo.py +0 -0
  38. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/execution.py +0 -0
  39. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/organizations.py +0 -0
  40. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/projects.py +0 -0
  41. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/reports.py +0 -0
  42. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/test_cases.py +0 -0
  43. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/__init__.py +0 -0
  44. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/analytics.py +0 -0
  45. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/api_key.py +0 -0
  46. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/auth.py +0 -0
  47. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/bulk_job.py +0 -0
  48. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/execution.py +0 -0
  49. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/generated_script.py +0 -0
  50. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/membership.py +0 -0
  51. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/organization.py +0 -0
  52. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/project.py +0 -0
  53. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/reports.py +0 -0
  54. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/script_revision.py +0 -0
  55. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/test_case.py +0 -0
  56. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/services/api_key.py +0 -0
  57. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/services/auth.py +0 -0
  58. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/services/auth_dependencies.py +0 -0
  59. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/services/git_export.py +0 -0
  60. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/services/rbac.py +0 -0
  61. {scriptgini-1.5.1 → scriptgini-1.5.2}/app/tasks.py +0 -0
  62. {scriptgini-1.5.1 → scriptgini-1.5.2}/scriptgini.egg-info/SOURCES.txt +0 -0
  63. {scriptgini-1.5.1 → scriptgini-1.5.2}/scriptgini.egg-info/dependency_links.txt +0 -0
  64. {scriptgini-1.5.1 → scriptgini-1.5.2}/scriptgini.egg-info/top_level.txt +0 -0
  65. {scriptgini-1.5.1 → scriptgini-1.5.2}/setup.cfg +0 -0
  66. {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_api.py +0 -0
  67. {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_infra_services_coverage.py +0 -0
  68. {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_sprint2_rbac.py +0 -0
  69. {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_sprint3_execution.py +0 -0
  70. {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_sprint5_reporting_analytics.py +0 -0
  71. {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_sprint6_coverage_lifecycle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptgini
3
- Version: 1.5.1
3
+ Version: 1.5.2
4
4
  Summary: Agentic AI system that converts functional test cases into automation test scripts.
5
5
  Author: ScriptGini Team
6
6
  License: Proprietary
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
16
16
 
17
17
  > **Enterprise-grade Agentic AI system that converts functional test cases into high-quality, review-ready automation test scripts.**
18
18
 
19
- Current release: v1.5.1 (Sprint 6 coverage-complete patch)
19
+ Current release: v1.5.2 (Sprint 6 hardening + API key audit/revocation patch)
20
20
 
21
21
  ---
22
22
 
@@ -444,9 +444,9 @@ The project follows an **enterprise-grade development roadmap** with 6 sprints c
444
444
  | **Sprint 1** | IAM Core | 30-36pts | 🟡 Core delivered (auth hardening pending) |
445
445
  | **Sprint 2** | RBAC + Multi-Tenancy | 32-38pts | 🟡 Core delivered (RBAC hardening pending) |
446
446
  | **Sprint 3** | Durable Execution | 34-40pts | ✅ Completed (Redis + Celery queue foundation) |
447
- | **Sprint 4** | Security & Hardening | 30-36pts | 🟡 In progress (isolation boundary + breakout tests pending) |
447
+ | **Sprint 4** | Security & Hardening | 30-36pts | Hardening increment completed (isolation + negative tests + audit controls) |
448
448
  | **Sprint 5** | Reporting & Analytics | 28-34pts | ✅ Completed (Reports APIs, trends/flakiness, retention cleanup) |
449
- | **Sprint 6** | Advanced Features | 24-30pts | 🔲 Pending (Webhooks, defect sync, versioning) |
449
+ | **Sprint 6** | Coverage Analytics, Script Lifecycle, and DX | 24-30pts | Completed (coverage analytics, refactor/version history/diff/rollback, 100% quality gate) |
450
450
 
451
451
  ---
452
452
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Enterprise-grade Agentic AI system that converts functional test cases into high-quality, review-ready automation test scripts.**
4
4
 
5
- Current release: v1.5.1 (Sprint 6 coverage-complete patch)
5
+ Current release: v1.5.2 (Sprint 6 hardening + API key audit/revocation patch)
6
6
 
7
7
  ---
8
8
 
@@ -430,9 +430,9 @@ The project follows an **enterprise-grade development roadmap** with 6 sprints c
430
430
  | **Sprint 1** | IAM Core | 30-36pts | 🟡 Core delivered (auth hardening pending) |
431
431
  | **Sprint 2** | RBAC + Multi-Tenancy | 32-38pts | 🟡 Core delivered (RBAC hardening pending) |
432
432
  | **Sprint 3** | Durable Execution | 34-40pts | ✅ Completed (Redis + Celery queue foundation) |
433
- | **Sprint 4** | Security & Hardening | 30-36pts | 🟡 In progress (isolation boundary + breakout tests pending) |
433
+ | **Sprint 4** | Security & Hardening | 30-36pts | Hardening increment completed (isolation + negative tests + audit controls) |
434
434
  | **Sprint 5** | Reporting & Analytics | 28-34pts | ✅ Completed (Reports APIs, trends/flakiness, retention cleanup) |
435
- | **Sprint 6** | Advanced Features | 24-30pts | 🔲 Pending (Webhooks, defect sync, versioning) |
435
+ | **Sprint 6** | Coverage Analytics, Script Lifecycle, and DX | 24-30pts | Completed (coverage analytics, refactor/version history/diff/rollback, 100% quality gate) |
436
436
 
437
437
  ---
438
438
 
@@ -0,0 +1,3 @@
1
+ __version__ = "1.5.2"
2
+ __api_version__ = "v1.5.2"
3
+
@@ -1,6 +1,9 @@
1
+ import logging
2
+
1
3
  from fastapi import APIRouter, Depends, HTTPException, status
2
4
  from sqlalchemy.orm import Session
3
5
 
6
+ from app.config import settings
4
7
  from app.database import get_db
5
8
  from app.services import api_key as api_key_service
6
9
  from app.services.auth_dependencies import get_current_user
@@ -13,6 +16,26 @@ from app.schemas.api_key import (
13
16
  from app.models.api_key import APIKey
14
17
 
15
18
  router = APIRouter(prefix="/auth/api-keys", tags=["auth"])
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def _audit_api_key_event(
23
+ event: str,
24
+ actor_user_id: int,
25
+ api_key_id: int | None,
26
+ api_key_prefix: str | None,
27
+ result: str,
28
+ ) -> None:
29
+ if not settings.SECURITY_AUDIT_LOG_ENABLED:
30
+ return
31
+ logger.info(
32
+ "security_audit event=%s actor_user_id=%s api_key_id=%s api_key_prefix=%s result=%s",
33
+ event,
34
+ actor_user_id,
35
+ api_key_id,
36
+ api_key_prefix,
37
+ result,
38
+ )
16
39
 
17
40
 
18
41
  @router.post("", response_model=APIKeyRevealResponse, status_code=status.HTTP_201_CREATED)
@@ -36,6 +59,7 @@ def create_api_key(
36
59
  request.scopes,
37
60
  request.expires_at,
38
61
  )
62
+ _audit_api_key_event("api_key_created", current_user.id, api_key.id, api_key.prefix, "success")
39
63
 
40
64
  return APIKeyRevealResponse(
41
65
  id=api_key.id,
@@ -89,6 +113,7 @@ def update_api_key(
89
113
  existing_key = api_key_service.get_api_key_by_id(db, key_id)
90
114
 
91
115
  if not existing_key:
116
+ _audit_api_key_event("api_key_revoked", current_user.id, key_id, None, "not_found")
92
117
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API key not found")
93
118
 
94
119
  # Check that the key belongs to the current user
@@ -120,7 +145,9 @@ def delete_api_key(
120
145
 
121
146
  # Check that the key belongs to the current user
122
147
  if existing_key.user_id != current_user.id:
148
+ _audit_api_key_event("api_key_revoked", current_user.id, existing_key.id, existing_key.prefix, "forbidden")
123
149
  raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to delete this API key")
124
150
 
125
- api_key_service.delete_api_key(db, key_id)
151
+ api_key_service.revoke_api_key(db, key_id)
152
+ _audit_api_key_event("api_key_revoked", current_user.id, existing_key.id, existing_key.prefix, "success")
126
153
  return None
@@ -37,16 +37,58 @@ from app.tasks import process_script_generation_job
37
37
 
38
38
  logger = logging.getLogger(__name__)
39
39
 
40
- _BLOCKED_IMPORT_ROOTS = {
41
- "subprocess",
42
- "socket",
43
- "shutil",
44
- "requests",
45
- "httpx",
46
- "ftplib",
47
- "paramiko",
40
+ _ALLOWED_IMPORT_ROOTS = {
41
+ "collections",
42
+ "dataclasses",
43
+ "datetime",
44
+ "decimal",
45
+ "functools",
46
+ "itertools",
47
+ "json",
48
+ "math",
49
+ "pathlib",
50
+ "playwright",
51
+ "pytest",
52
+ "random",
53
+ "re",
54
+ "selenium",
55
+ "string",
56
+ "time",
57
+ "traceback",
58
+ "typing",
59
+ "urllib",
60
+ "uuid",
61
+ }
62
+ _BLOCKED_BUILTIN_CALLS = {"exec", "eval", "compile", "__import__", "open", "input"}
63
+ _BLOCKED_ATTRIBUTE_CALLS = {
64
+ "__subclasses__",
65
+ "connect",
66
+ "execv",
67
+ "execve",
68
+ "kill",
69
+ "listen",
70
+ "popen",
71
+ "recv",
72
+ "send",
73
+ "spawnv",
74
+ "spawnve",
75
+ "startfile",
76
+ "system",
77
+ }
78
+ _BLOCKED_SYMBOL_NAMES = {
79
+ "__builtins__",
80
+ "__globals__",
81
+ "__loader__",
82
+ "__spec__",
83
+ }
84
+ _BLOCKED_STRING_TOKENS = {
85
+ "__builtins__",
86
+ "__globals__",
87
+ "__import__",
88
+ "__loader__",
89
+ "__spec__",
90
+ "__subclasses__",
48
91
  }
49
- _BLOCKED_BUILTIN_CALLS = {"exec", "eval", "compile", "__import__"}
50
92
  _MAX_OUTPUT_CHARS = 20_000
51
93
 
52
94
 
@@ -195,11 +237,21 @@ def _run_python_script(script_content: str) -> dict:
195
237
 
196
238
  def _build_script_command(script_path: Path, script_content: str) -> list[str]:
197
239
  if _uses_pytest_playwright(script_content):
198
- command = [sys.executable, "-m", "pytest", script_path.name, "-s", "--browser", "chromium"]
240
+ command = [
241
+ sys.executable,
242
+ "-I",
243
+ "-B",
244
+ "-m",
245
+ "pytest",
246
+ script_path.name,
247
+ "-s",
248
+ "--browser",
249
+ "chromium",
250
+ ]
199
251
  if settings.PLAYWRIGHT_RUN_HEADED:
200
252
  command.append("--headed")
201
253
  return command
202
- return [sys.executable, "-I", str(script_path)]
254
+ return [sys.executable, "-I", "-B", str(script_path)]
203
255
 
204
256
 
205
257
  def _uses_pytest_playwright(script_content: str) -> bool:
@@ -238,15 +290,23 @@ def _validate_script_safety(script_content: str) -> None:
238
290
  if isinstance(node, ast.Import):
239
291
  for alias in node.names:
240
292
  root = alias.name.split(".")[0]
241
- if root in _BLOCKED_IMPORT_ROOTS:
242
- raise ValueError(f"Unsafe import detected: {root}")
293
+ if root not in _ALLOWED_IMPORT_ROOTS:
294
+ raise ValueError(f"Disallowed import detected: {root}")
243
295
  if isinstance(node, ast.ImportFrom):
244
296
  module_root = (node.module or "").split(".")[0]
245
- if module_root in _BLOCKED_IMPORT_ROOTS:
246
- raise ValueError(f"Unsafe import detected: {module_root}")
297
+ if module_root and module_root not in _ALLOWED_IMPORT_ROOTS:
298
+ raise ValueError(f"Disallowed import detected: {module_root}")
247
299
  if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
248
300
  if node.func.id in _BLOCKED_BUILTIN_CALLS:
249
301
  raise ValueError(f"Unsafe builtin call detected: {node.func.id}")
302
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
303
+ if node.func.attr in _BLOCKED_ATTRIBUTE_CALLS:
304
+ raise ValueError(f"Unsafe runtime call detected: {node.func.attr}")
305
+ if isinstance(node, ast.Name) and node.id in _BLOCKED_SYMBOL_NAMES:
306
+ raise ValueError(f"Unsafe symbol usage detected: {node.id}")
307
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
308
+ if node.value in _BLOCKED_STRING_TOKENS:
309
+ raise ValueError(f"Unsafe introspection token detected: {node.value}")
250
310
 
251
311
 
252
312
  def _create_script_run(db: Session, script: GeneratedScript, run_result: dict, status: ScriptRunStatus) -> ScriptRun:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scriptgini"
7
- version = "1.5.1"
7
+ version = "1.5.2"
8
8
  description = "Agentic AI system that converts functional test cases into automation test scripts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptgini
3
- Version: 1.5.1
3
+ Version: 1.5.2
4
4
  Summary: Agentic AI system that converts functional test cases into automation test scripts.
5
5
  Author: ScriptGini Team
6
6
  License: Proprietary
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
16
16
 
17
17
  > **Enterprise-grade Agentic AI system that converts functional test cases into high-quality, review-ready automation test scripts.**
18
18
 
19
- Current release: v1.5.1 (Sprint 6 coverage-complete patch)
19
+ Current release: v1.5.2 (Sprint 6 hardening + API key audit/revocation patch)
20
20
 
21
21
  ---
22
22
 
@@ -444,9 +444,9 @@ The project follows an **enterprise-grade development roadmap** with 6 sprints c
444
444
  | **Sprint 1** | IAM Core | 30-36pts | 🟡 Core delivered (auth hardening pending) |
445
445
  | **Sprint 2** | RBAC + Multi-Tenancy | 32-38pts | 🟡 Core delivered (RBAC hardening pending) |
446
446
  | **Sprint 3** | Durable Execution | 34-40pts | ✅ Completed (Redis + Celery queue foundation) |
447
- | **Sprint 4** | Security & Hardening | 30-36pts | 🟡 In progress (isolation boundary + breakout tests pending) |
447
+ | **Sprint 4** | Security & Hardening | 30-36pts | Hardening increment completed (isolation + negative tests + audit controls) |
448
448
  | **Sprint 5** | Reporting & Analytics | 28-34pts | ✅ Completed (Reports APIs, trends/flakiness, retention cleanup) |
449
- | **Sprint 6** | Advanced Features | 24-30pts | 🔲 Pending (Webhooks, defect sync, versioning) |
449
+ | **Sprint 6** | Coverage Analytics, Script Lifecycle, and DX | 24-30pts | Completed (coverage analytics, refactor/version history/diff/rollback, 100% quality gate) |
450
450
 
451
451
  ---
452
452
 
@@ -240,6 +240,21 @@ class TestLogout:
240
240
 
241
241
 
242
242
  class TestAPIKeyManagement:
243
+ def test_api_key_audit_helper_disabled_branch(self, monkeypatch):
244
+ """Audit helper should not log when security audit logging is disabled."""
245
+ from app.routers import api_key as api_key_router
246
+
247
+ called = {"ok": False}
248
+ monkeypatch.setattr("app.routers.api_key.logger.info", lambda *a, **k: called.update(ok=True))
249
+
250
+ old_enabled = api_key_router.settings.SECURITY_AUDIT_LOG_ENABLED
251
+ api_key_router.settings.SECURITY_AUDIT_LOG_ENABLED = False
252
+ try:
253
+ api_key_router._audit_api_key_event("api_key_created", 1, 2, "sg_test", "success")
254
+ assert called["ok"] is False
255
+ finally:
256
+ api_key_router.settings.SECURITY_AUDIT_LOG_ENABLED = old_enabled
257
+
243
258
  def test_create_api_key(self, db_session, test_user, auth_headers):
244
259
  """Test creating an API key."""
245
260
  response = client.post(
@@ -325,6 +340,23 @@ class TestAPIKeyManagement:
325
340
  )
326
341
  assert response.status_code == 404
327
342
 
343
+ def test_update_api_key_not_found_emits_audit_log(self, auth_headers, monkeypatch):
344
+ """Missing API key updates should emit a structured audit event."""
345
+ captured: list[tuple] = []
346
+
347
+ def _capture_log(*args, **kwargs):
348
+ captured.append(args)
349
+
350
+ monkeypatch.setattr("app.routers.api_key.logger.info", _capture_log)
351
+
352
+ response = client.put(
353
+ "/api/v1/auth/api-keys/99999",
354
+ headers=auth_headers,
355
+ json={"name": "Updated Name", "scopes": ["read"]},
356
+ )
357
+ assert response.status_code == 404
358
+ assert any(len(entry) > 5 and entry[1] == "api_key_revoked" and entry[5] == "not_found" for entry in captured)
359
+
328
360
  def test_update_api_key_not_accessible_by_other_user(
329
361
  self, db_session, test_user, test_user_2, auth_headers
330
362
  ):
@@ -368,9 +400,49 @@ class TestAPIKeyManagement:
368
400
  response = client.delete(f"/api/v1/auth/api-keys/{key_id}", headers=auth_headers)
369
401
  assert response.status_code == 204
370
402
 
371
- # Verify it's gone
403
+ # Verify it is revoked and retained for auditability
372
404
  response = client.get(f"/api/v1/auth/api-keys/{key_id}", headers=auth_headers)
373
- assert response.status_code == 404
405
+ assert response.status_code == 200
406
+ assert response.json()["is_active"] is False
407
+
408
+ def test_delete_api_key_revokes_authentication(self, db_session, test_user, auth_headers):
409
+ """Revoked API key can no longer authenticate requests."""
410
+ create_response = client.post(
411
+ "/api/v1/auth/api-keys",
412
+ headers=auth_headers,
413
+ json={"name": "Key to Revoke", "scopes": ["read"]},
414
+ )
415
+ key_payload = create_response.json()
416
+
417
+ revoke_response = client.delete(f"/api/v1/auth/api-keys/{key_payload['id']}", headers=auth_headers)
418
+ assert revoke_response.status_code == 204
419
+
420
+ verified = verify_api_key(db_session, key_payload["prefix"], key_payload["secret_key"])
421
+ assert verified is None
422
+
423
+ def test_api_key_audit_logs_create_and_revoke(self, db_session, test_user, auth_headers, monkeypatch):
424
+ """API key lifecycle writes structured security audit logs."""
425
+ captured: list[tuple] = []
426
+
427
+ def _capture_log(*args, **kwargs):
428
+ captured.append(args)
429
+
430
+ monkeypatch.setattr("app.routers.api_key.logger.info", _capture_log)
431
+
432
+ create_response = client.post(
433
+ "/api/v1/auth/api-keys",
434
+ headers=auth_headers,
435
+ json={"name": "Audited Key", "scopes": ["read"]},
436
+ )
437
+ assert create_response.status_code == 201
438
+
439
+ key_id = create_response.json()["id"]
440
+ revoke_response = client.delete(f"/api/v1/auth/api-keys/{key_id}", headers=auth_headers)
441
+ assert revoke_response.status_code == 204
442
+
443
+ events = [entry[1] for entry in captured if len(entry) > 1]
444
+ assert "api_key_created" in events
445
+ assert "api_key_revoked" in events
374
446
 
375
447
  def test_delete_api_key_not_found(self, auth_headers):
376
448
  """Test deleting a missing API key returns 404."""
@@ -1606,15 +1606,30 @@ class TestBulkJobsRouter:
1606
1606
  def test_validate_script_safety_blocks_imports_and_builtin_calls(self):
1607
1607
  from app.routers.scripts import _validate_script_safety
1608
1608
 
1609
- with pytest.raises(ValueError, match="Unsafe import detected: subprocess"):
1609
+ with pytest.raises(ValueError, match="Disallowed import detected: subprocess"):
1610
1610
  _validate_script_safety("import subprocess")
1611
1611
 
1612
- with pytest.raises(ValueError, match="Unsafe import detected: socket"):
1612
+ with pytest.raises(ValueError, match="Disallowed import detected: socket"):
1613
1613
  _validate_script_safety("from socket import socket")
1614
1614
 
1615
1615
  with pytest.raises(ValueError, match="Unsafe builtin call detected: exec"):
1616
1616
  _validate_script_safety("exec('print(1)')")
1617
1617
 
1618
+ def test_validate_script_safety_blocks_runtime_breakout_vectors(self):
1619
+ from app.routers.scripts import _validate_script_safety
1620
+
1621
+ with pytest.raises(ValueError, match="Disallowed import detected: os"):
1622
+ _validate_script_safety("import os\nos.system('whoami')")
1623
+
1624
+ with pytest.raises(ValueError, match="Unsafe symbol usage detected: __builtins__"):
1625
+ _validate_script_safety("x = __builtins__")
1626
+
1627
+ with pytest.raises(ValueError, match="Unsafe introspection token detected: __subclasses__"):
1628
+ _validate_script_safety("name = '__subclasses__'")
1629
+
1630
+ with pytest.raises(ValueError, match="Unsafe runtime call detected: system"):
1631
+ _validate_script_safety("obj.system('echo hi')")
1632
+
1618
1633
  def test_validate_script_safety_rejects_syntax_error(self):
1619
1634
  from app.routers.scripts import _validate_script_safety
1620
1635
 
@@ -1,3 +0,0 @@
1
- __version__ = "1.5.1"
2
- __api_version__ = "v1.5.1"
3
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes