mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
114 lines
5.5 KiB
Python
114 lines
5.5 KiB
Python
"""Flask API contracts with stubbed authentication, temporary DB, no probes."""
|
|||
|
|
import importlib
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import types
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
try:
|
||
|
|
from flask import Flask
|
||
|
|
except ImportError:
|
||
|
|
Flask = None
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "AppImage/scripts"))
|
||
|
|
import audit_store as store
|
||
|
|
|
||
|
|
|
||
|
|
@unittest.skipIf(Flask is None, "Flask runtime required")
|
||
|
|
class AuditApiTests(unittest.TestCase):
|
||
|
|
def setUp(self):
|
||
|
|
self.temp = tempfile.TemporaryDirectory()
|
||
|
|
self.addCleanup(self.temp.cleanup)
|
||
|
|
self.dbpatch = patch.object(store, "DB_PATH", Path(self.temp.name) / "audit.db")
|
||
|
|
self.dbpatch.start()
|
||
|
|
self.addCleanup(self.dbpatch.stop)
|
||
|
|
store._schema_ready = False
|
||
|
|
self.addCleanup(lambda: setattr(store, "_schema_ready", False))
|
||
|
|
auth = types.ModuleType("auth_manager")
|
||
|
|
auth.load_auth_config = lambda: {"enabled": True}
|
||
|
|
auth.verify_token = lambda token: "verified-operator"
|
||
|
|
middleware = types.ModuleType("jwt_middleware")
|
||
|
|
middleware.require_auth = lambda f: f
|
||
|
|
middleware.require_admin_scope = lambda f: f
|
||
|
|
with patch.dict(sys.modules, auth_manager=auth, jwt_middleware=middleware):
|
||
|
|
sys.modules.pop("flask_audit_routes", None)
|
||
|
|
self.routes = importlib.import_module("flask_audit_routes")
|
||
|
|
self.addCleanup(lambda: sys.modules.pop("flask_audit_routes", None))
|
||
|
|
self.app = Flask(__name__)
|
||
|
|
self.app.register_blueprint(self.routes.audit_bp)
|
||
|
|
self.client = self.app.test_client()
|
||
|
|
self.headers = {"Authorization": "Bearer fixture"}
|
||
|
|
self.finding = {"check_id": "guests.privileged_containers", "area": "guests",
|
||
|
|
"severity": "WARNING", "state": "warn", "raw_state": "warn",
|
||
|
|
"classification": "warning", "raw_classification": "warning",
|
||
|
|
"affected": [{"vmid": 101}], "scope": "fixture-scope"}
|
||
|
|
self.run = store.start_run("full")
|
||
|
|
store.record_findings(self.run, [self.finding])
|
||
|
|
store.finish_run(self.run, checks_total=1)
|
||
|
|
|
||
|
|
def accept(self, **extra):
|
||
|
|
return self.client.post("/api/audit/exceptions", headers=self.headers, json={
|
||
|
|
"check_id": self.finding["check_id"], "reason": "intentional lab", "run_id": self.run,
|
||
|
|
"accepted_by": "forged-author", **extra})
|
||
|
|
|
||
|
|
def test_actor_is_from_authentication_and_live_view_updates(self):
|
||
|
|
self.assertEqual(self.accept().status_code, 200)
|
||
|
|
row = self.client.get(f"/api/audit/runs/{self.run}?effective=1").json["findings"][0]
|
||
|
|
self.assertEqual(row["state"], "accepted")
|
||
|
|
self.assertEqual(row["exception"]["accepted_by"], "verified-operator")
|
||
|
|
self.assertEqual(row["classification"], "warning")
|
||
|
|
self.assertEqual(row["raw_classification"], "warning")
|
||
|
|
historical = self.client.get(f"/api/audit/runs/{self.run}").json["findings"][0]
|
||
|
|
self.assertEqual(historical["state"], "warn")
|
||
|
|
status = self.client.get("/api/audit/status").json
|
||
|
|
self.assertEqual(status["summary"], {"accepted": 1})
|
||
|
|
|
||
|
|
def test_revoke_is_immediate(self):
|
||
|
|
self.accept()
|
||
|
|
self.client.delete(f"/api/audit/exceptions/{self.finding['check_id']}", headers=self.headers)
|
||
|
|
self.assertEqual(self.client.get("/api/audit/status").json["summary"], {"warning": 1})
|
||
|
|
self.assertEqual(len(self.client.get("/api/audit/exceptions").json["history"]), 2)
|
||
|
|
|
||
|
|
def test_expiry_must_be_positive_integer(self):
|
||
|
|
for days in (0, False, -1, 1.5, True, 999999, "invalid"):
|
||
|
|
with self.subTest(days=days):
|
||
|
|
self.assertEqual(self.accept(expires_in_days=days).status_code, 400)
|
||
|
|
|
||
|
|
def test_stale_run_cannot_accept_new_results(self):
|
||
|
|
self.assertEqual(self.accept(run_id="old-run").status_code, 409)
|
||
|
|
|
||
|
|
def test_observation_cannot_be_accepted_as_a_risk(self):
|
||
|
|
finding = {**self.finding, 'classification':'observation', 'raw_classification':'observation'}
|
||
|
|
self.run = store.start_run('full')
|
||
|
|
store.record_findings(self.run, [finding])
|
||
|
|
store.finish_run(self.run, checks_total=1)
|
||
|
|
self.assertEqual(self.accept().status_code, 400)
|
||
|
|
self.assertFalse(store.active_exceptions())
|
||
|
|
|
||
|
|
def test_invalid_profile_or_area_never_starts_worker(self):
|
||
|
|
with patch.object(self.routes.threading, "Thread") as worker:
|
||
|
|
for body in ({"profile": "invented"}, {"areas": ["invented"]}, {"areas": []}, {"areas": "system"}):
|
||
|
|
self.assertEqual(self.client.post("/api/audit/run", json=body).status_code, 400)
|
||
|
|
worker.assert_not_called()
|
||
|
|
|
||
|
|
def test_run_returns_id_before_worker_finishes(self):
|
||
|
|
with patch.object(self.routes.threading, "Thread"):
|
||
|
|
response = self.client.post("/api/audit/run", json={"profile": "full"})
|
||
|
|
self.assertEqual(response.status_code, 200)
|
||
|
|
self.assertTrue(response.json["run_id"])
|
||
|
|
self.assertEqual(self.client.post("/api/audit/run", json={}).status_code, 409)
|
||
|
|
|
||
|
|
def test_audit_database_failure_does_not_prevent_monitor_startup(self):
|
||
|
|
with patch.object(store, "recover_interrupted_runs", side_effect=OSError("read-only filesystem")):
|
||
|
|
another_app = Flask("audit-startup-failure")
|
||
|
|
another_app.register_blueprint(self.routes.audit_bp)
|
||
|
|
self.assertEqual(self.client.get("/api/audit/status").status_code, 500)
|
||
|
|
self.assertEqual(self.client.post("/api/audit/run", json={}).status_code, 500)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|