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.
- {scriptgini-1.5.1 → scriptgini-1.5.2}/PKG-INFO +4 -4
- {scriptgini-1.5.1 → scriptgini-1.5.2}/README.md +3 -3
- scriptgini-1.5.2/app/__init__.py +3 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/api_key.py +28 -1
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/scripts.py +75 -15
- {scriptgini-1.5.1 → scriptgini-1.5.2}/pyproject.toml +1 -1
- {scriptgini-1.5.1 → scriptgini-1.5.2}/scriptgini.egg-info/PKG-INFO +4 -4
- {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_auth.py +74 -2
- {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_coverage.py +17 -2
- scriptgini-1.5.1/app/__init__.py +0 -3
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/agents/__init__.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/agents/prompts.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/agents/script_gini_agent.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/cache.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/celery_app.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/config.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/database.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/llm/__init__.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/llm/provider.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/main.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/__init__.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/api_key.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/bulk_job.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/execution_job.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/generated_script.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/membership.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/organization.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/project.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/script_revision.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/script_run.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/test_case.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/models/user.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/__init__.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/analytics.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/auth.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/bulk_jobs.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/demo.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/execution.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/organizations.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/projects.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/reports.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/routers/test_cases.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/__init__.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/analytics.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/api_key.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/auth.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/bulk_job.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/execution.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/generated_script.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/membership.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/organization.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/project.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/reports.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/script_revision.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/schemas/test_case.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/services/api_key.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/services/auth.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/services/auth_dependencies.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/services/git_export.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/services/rbac.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/app/tasks.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/scriptgini.egg-info/SOURCES.txt +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/scriptgini.egg-info/dependency_links.txt +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/scriptgini.egg-info/top_level.txt +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/setup.cfg +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_api.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_infra_services_coverage.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_sprint2_rbac.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_sprint3_execution.py +0 -0
- {scriptgini-1.5.1 → scriptgini-1.5.2}/tests/test_sprint5_reporting_analytics.py +0 -0
- {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.
|
|
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.
|
|
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 |
|
|
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** |
|
|
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.
|
|
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 |
|
|
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** |
|
|
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
|
|
|
@@ -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.
|
|
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
|
-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
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 = [
|
|
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
|
|
242
|
-
raise ValueError(f"
|
|
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
|
|
246
|
-
raise ValueError(f"
|
|
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.
|
|
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.
|
|
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.
|
|
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 |
|
|
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** |
|
|
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
|
|
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 ==
|
|
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="
|
|
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="
|
|
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
|
|
scriptgini-1.5.1/app/__init__.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|