From 29a2cf2738314c45c06851aa44e6a8a7039f3b5e Mon Sep 17 00:00:00 2001 From: anti Date: Thu, 9 Apr 2026 11:58:57 -0400 Subject: [PATCH] refactor: modularize API routes into separate files and clean up dependencies --- .hypothesis/constants/05ca8d3a23e4e828 | 4 + .hypothesis/constants/06cf3ea2c6ce1e47 | 4 + .hypothesis/constants/119d69517b6a1211 | 4 + .hypothesis/constants/1b93688452019c2a | 4 + .hypothesis/constants/281f7247b75633fa | 4 + .hypothesis/constants/413af714a6aa554a | 4 + .hypothesis/constants/5c94fe41bb0c3028 | 4 + .hypothesis/constants/671699dd596f1b47 | 4 + .hypothesis/constants/74d4bb0b083b64f7 | 4 + .hypothesis/constants/752e3c8f1c8255e2 | 4 + .hypothesis/constants/84a92b4c342503ba | 4 + .hypothesis/constants/a9e88f2e1ab12330 | 4 + .hypothesis/constants/e514b080d6ddcc0c | 4 + .hypothesis/constants/f4166372b8fe275d | 4 + .hypothesis/constants/f7527ac1d5cdef10 | 4 + .hypothesis/constants/fe31031a8c96e163 | 4 + .../unicode_data/16.0.0/codec-utf-8.json.gz | Bin 60 -> 60 bytes decnet/web/api.py | 346 +----------------- decnet/web/dependencies.py | 46 +++ decnet/web/models.py | 46 +++ decnet/web/router/__init__.py | 36 ++ decnet/web/router/auth/api_change_pass.py | 23 ++ decnet/web/router/auth/api_login.py | 36 ++ decnet/web/router/bounty/api_get_bounties.py | 27 ++ decnet/web/router/fleet/api_deploy_deckies.py | 80 ++++ decnet/web/router/fleet/api_get_deckies.py | 12 + decnet/web/router/fleet/api_mutate_decky.py | 14 + .../web/router/fleet/api_mutate_interval.py | 21 ++ decnet/web/router/logs/api_get_histogram.py | 18 + decnet/web/router/logs/api_get_logs.py | 27 ++ decnet/web/router/stats/api_get_stats.py | 13 + decnet/web/router/stream/api_stream_events.py | 63 ++++ decnet/web/sqlite_repository.py | 2 +- test_bounty_decnet.db-shm | Bin 32768 -> 32768 bytes test_bounty_decnet.db-wal | Bin 28872 -> 28872 bytes test_decnet.db-shm | Bin 32768 -> 32768 bytes test_decnet.db-wal | Bin 28872 -> 28872 bytes test_fleet_decnet.db-shm | Bin 32768 -> 32768 bytes test_fleet_decnet.db-wal | Bin 28872 -> 28872 bytes test_fuzz_decnet.db-shm | Bin 32768 -> 32768 bytes test_fuzz_decnet.db-wal | Bin 28872 -> 28872 bytes tests/test_bounty.py | 3 +- tests/test_fleet_api.py | 2 +- tests/test_web_api.py | 3 +- tests/test_web_api_fuzz.py | 3 +- 45 files changed, 541 insertions(+), 344 deletions(-) create mode 100644 .hypothesis/constants/05ca8d3a23e4e828 create mode 100644 .hypothesis/constants/06cf3ea2c6ce1e47 create mode 100644 .hypothesis/constants/119d69517b6a1211 create mode 100644 .hypothesis/constants/1b93688452019c2a create mode 100644 .hypothesis/constants/281f7247b75633fa create mode 100644 .hypothesis/constants/413af714a6aa554a create mode 100644 .hypothesis/constants/5c94fe41bb0c3028 create mode 100644 .hypothesis/constants/671699dd596f1b47 create mode 100644 .hypothesis/constants/74d4bb0b083b64f7 create mode 100644 .hypothesis/constants/752e3c8f1c8255e2 create mode 100644 .hypothesis/constants/84a92b4c342503ba create mode 100644 .hypothesis/constants/a9e88f2e1ab12330 create mode 100644 .hypothesis/constants/e514b080d6ddcc0c create mode 100644 .hypothesis/constants/f4166372b8fe275d create mode 100644 .hypothesis/constants/f7527ac1d5cdef10 create mode 100644 .hypothesis/constants/fe31031a8c96e163 create mode 100644 decnet/web/dependencies.py create mode 100644 decnet/web/models.py create mode 100644 decnet/web/router/__init__.py create mode 100644 decnet/web/router/auth/api_change_pass.py create mode 100644 decnet/web/router/auth/api_login.py create mode 100644 decnet/web/router/bounty/api_get_bounties.py create mode 100644 decnet/web/router/fleet/api_deploy_deckies.py create mode 100644 decnet/web/router/fleet/api_get_deckies.py create mode 100644 decnet/web/router/fleet/api_mutate_decky.py create mode 100644 decnet/web/router/fleet/api_mutate_interval.py create mode 100644 decnet/web/router/logs/api_get_histogram.py create mode 100644 decnet/web/router/logs/api_get_logs.py create mode 100644 decnet/web/router/stats/api_get_stats.py create mode 100644 decnet/web/router/stream/api_stream_events.py diff --git a/.hypothesis/constants/05ca8d3a23e4e828 b/.hypothesis/constants/05ca8d3a23e4e828 new file mode 100644 index 0000000..558c2bd --- /dev/null +++ b/.hypothesis/constants/05ca8d3a23e4e828 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/fleet/api_get_deckies.py +# hypothesis_version: 6.151.12 + +['/deckies', 'Fleet Management'] \ No newline at end of file diff --git a/.hypothesis/constants/06cf3ea2c6ce1e47 b/.hypothesis/constants/06cf3ea2c6ce1e47 new file mode 100644 index 0000000..8246b83 --- /dev/null +++ b/.hypothesis/constants/06cf3ea2c6ce1e47 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/api.py +# hypothesis_version: 6.151.12 + +[0.5, '*', '/api/v1', '/docs', '/openapi.json', '/redoc', '1.0.0'] \ No newline at end of file diff --git a/.hypothesis/constants/119d69517b6a1211 b/.hypothesis/constants/119d69517b6a1211 new file mode 100644 index 0000000..d930de8 --- /dev/null +++ b/.hypothesis/constants/119d69517b6a1211 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/stream/api_stream_events.py +# hypothesis_version: 6.151.12 + +['/stream', 'Observability', 'data', 'histogram', 'id', 'lastEventId', 'logs', 'stats', 'text/event-stream', 'type'] \ No newline at end of file diff --git a/.hypothesis/constants/1b93688452019c2a b/.hypothesis/constants/1b93688452019c2a new file mode 100644 index 0000000..72977fb --- /dev/null +++ b/.hypothesis/constants/1b93688452019c2a @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/logs/api_get_histogram.py +# hypothesis_version: 6.151.12 + +['/logs/histogram', 'Logs'] \ No newline at end of file diff --git a/.hypothesis/constants/281f7247b75633fa b/.hypothesis/constants/281f7247b75633fa new file mode 100644 index 0000000..4613b6d --- /dev/null +++ b/.hypothesis/constants/281f7247b75633fa @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/dependencies.py +# hypothesis_version: 6.151.12 + +['/api/v1/auth/login', 'Authorization', 'Bearer', 'Bearer ', 'WWW-Authenticate', 'decnet.db', 'token', 'uuid'] \ No newline at end of file diff --git a/.hypothesis/constants/413af714a6aa554a b/.hypothesis/constants/413af714a6aa554a new file mode 100644 index 0000000..ce0c520 --- /dev/null +++ b/.hypothesis/constants/413af714a6aa554a @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/mutator.py +# hypothesis_version: 6.151.12 + +[5.0, '--remove-orphans', '-d', '-f', 'compose', 'docker', 'up'] \ No newline at end of file diff --git a/.hypothesis/constants/5c94fe41bb0c3028 b/.hypothesis/constants/5c94fe41bb0c3028 new file mode 100644 index 0000000..6616f0b --- /dev/null +++ b/.hypothesis/constants/5c94fe41bb0c3028 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/models.py +# hypothesis_version: 6.151.12 + +[512, 1024] \ No newline at end of file diff --git a/.hypothesis/constants/671699dd596f1b47 b/.hypothesis/constants/671699dd596f1b47 new file mode 100644 index 0000000..9e49981 --- /dev/null +++ b/.hypothesis/constants/671699dd596f1b47 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/stats/api_get_stats.py +# hypothesis_version: 6.151.12 + +['/stats', 'Observability'] \ No newline at end of file diff --git a/.hypothesis/constants/74d4bb0b083b64f7 b/.hypothesis/constants/74d4bb0b083b64f7 new file mode 100644 index 0000000..8f4faa7 --- /dev/null +++ b/.hypothesis/constants/74d4bb0b083b64f7 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/fleet/api_mutate_decky.py +# hypothesis_version: 6.151.12 + +[404, 'Fleet Management', 'message'] \ No newline at end of file diff --git a/.hypothesis/constants/752e3c8f1c8255e2 b/.hypothesis/constants/752e3c8f1c8255e2 new file mode 100644 index 0000000..1d825ff --- /dev/null +++ b/.hypothesis/constants/752e3c8f1c8255e2 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/logs/api_get_logs.py +# hypothesis_version: 6.151.12 + +[1000, '/logs', 'Logs', 'data', 'limit', 'offset', 'total'] \ No newline at end of file diff --git a/.hypothesis/constants/84a92b4c342503ba b/.hypothesis/constants/84a92b4c342503ba new file mode 100644 index 0000000..e5f64e5 --- /dev/null +++ b/.hypothesis/constants/84a92b4c342503ba @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/__init__.py +# hypothesis_version: 6.151.12 + +[] \ No newline at end of file diff --git a/.hypothesis/constants/a9e88f2e1ab12330 b/.hypothesis/constants/a9e88f2e1ab12330 new file mode 100644 index 0000000..5046274 --- /dev/null +++ b/.hypothesis/constants/a9e88f2e1ab12330 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/fleet/api_mutate_interval.py +# hypothesis_version: 6.151.12 + +[404, 500, 'Decky not found', 'Fleet Management', 'No active deployment', 'message'] \ No newline at end of file diff --git a/.hypothesis/constants/e514b080d6ddcc0c b/.hypothesis/constants/e514b080d6ddcc0c new file mode 100644 index 0000000..46fc91c --- /dev/null +++ b/.hypothesis/constants/e514b080d6ddcc0c @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/auth/api_change_pass.py +# hypothesis_version: 6.151.12 + +['Authentication', 'message', 'password_hash'] \ No newline at end of file diff --git a/.hypothesis/constants/f4166372b8fe275d b/.hypothesis/constants/f4166372b8fe275d new file mode 100644 index 0000000..6907985 --- /dev/null +++ b/.hypothesis/constants/f4166372b8fe275d @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/bounty/api_get_bounties.py +# hypothesis_version: 6.151.12 + +[1000, '/bounty', 'Bounty Vault', 'data', 'limit', 'offset', 'total'] \ No newline at end of file diff --git a/.hypothesis/constants/f7527ac1d5cdef10 b/.hypothesis/constants/f7527ac1d5cdef10 new file mode 100644 index 0000000..cd00ccd --- /dev/null +++ b/.hypothesis/constants/f7527ac1d5cdef10 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/fleet/api_deploy_deckies.py +# hypothesis_version: 6.151.12 + +[400, 500, '/deckies/deploy', 'Fleet Management', 'decnet.web.api', 'message', 'unihost'] \ No newline at end of file diff --git a/.hypothesis/constants/fe31031a8c96e163 b/.hypothesis/constants/fe31031a8c96e163 new file mode 100644 index 0000000..07f72a4 --- /dev/null +++ b/.hypothesis/constants/fe31031a8c96e163 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/router/auth/api_login.py +# hypothesis_version: 6.151.12 + +['/auth/login', 'Authentication', 'Bearer', 'WWW-Authenticate', 'access_token', 'bearer', 'must_change_password', 'password_hash', 'token_type', 'uuid'] \ No newline at end of file diff --git a/.hypothesis/unicode_data/16.0.0/codec-utf-8.json.gz b/.hypothesis/unicode_data/16.0.0/codec-utf-8.json.gz index 3e4e79c5a40496306d5e66e580dbebfc463746df..864dfa3e7e14d1371a694c45dd7aee28c1868a39 100644 GIT binary patch delta 27 icmcDq5tZ-e;9z86U|{-Rl3P$%oST+wn3XY6R2=|W%m str: - _credentials_exception: HTTPException = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - - # Extract token from header or query param - token: str | None = None - auth_header = request.headers.get("Authorization") - if auth_header and auth_header.startswith("Bearer "): - token = auth_header.split(" ")[1] - elif request.query_params.get("token"): - token = request.query_params.get("token") - - if not token: - raise _credentials_exception - - try: - _payload: dict[str, Any] = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - _user_uuid: Optional[str] = _payload.get("uuid") - if _user_uuid is None: - raise _credentials_exception - return _user_uuid - except jwt.PyJWTError: - raise _credentials_exception - - -class Token(BaseModel): - access_token: str - token_type: str - must_change_password: bool = False - - -class LoginRequest(BaseModel): - username: str - password: str = Field(..., max_length=72) - - -class ChangePasswordRequest(BaseModel): - old_password: str = Field(..., max_length=72) - new_password: str = Field(..., max_length=72) - - -class LogsResponse(BaseModel): - total: int - limit: int - offset: int - data: list[dict[str, Any]] - - -class BountyResponse(BaseModel): - total: int - limit: int - offset: int - data: list[dict[str, Any]] - - -@app.post("/api/v1/auth/login", response_model=Token, tags=["Authentication"]) -async def login(request: LoginRequest) -> dict[str, Any]: - _user: Optional[dict[str, Any]] = await repo.get_user_by_username(request.username) - if not _user or not verify_password(request.password, _user["password_hash"]): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - - _access_token_expires: timedelta = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - # Token uses uuid instead of sub - _access_token: str = create_access_token( - data={"uuid": _user["uuid"]}, expires_delta=_access_token_expires - ) - return { - "access_token": _access_token, - "token_type": "bearer", # nosec B105 - "must_change_password": bool(_user.get("must_change_password", False)) - } - - -@app.post("/api/v1/auth/change-password", tags=["Authentication"]) -async def change_password(request: ChangePasswordRequest, current_user: str = Depends(get_current_user)) -> dict[str, str]: - _user: Optional[dict[str, Any]] = await repo.get_user_by_uuid(current_user) - if not _user or not verify_password(request.old_password, _user["password_hash"]): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect old password", - ) - - _new_hash: str = get_password_hash(request.new_password) - await repo.update_user_password(current_user, _new_hash, must_change_password=False) - return {"message": "Password updated successfully"} - - -@app.get("/api/v1/logs", response_model=LogsResponse, tags=["Logs"]) -async def get_logs( - limit: int = Query(50, ge=1, le=1000), - offset: int = Query(0, ge=0), - search: Optional[str] = None, - start_time: Optional[str] = None, - end_time: Optional[str] = None, - current_user: str = Depends(get_current_user) -) -> dict[str, Any]: - _logs: list[dict[str, Any]] = await repo.get_logs(limit=limit, offset=offset, search=search, start_time=start_time, end_time=end_time) - _total: int = await repo.get_total_logs(search=search, start_time=start_time, end_time=end_time) - return { - "total": _total, - "limit": limit, - "offset": offset, - "data": _logs - } - - -@app.get("/api/v1/bounty", response_model=BountyResponse, tags=["Bounty Vault"]) -async def get_bounties( - limit: int = Query(50, ge=1, le=1000), - offset: int = Query(0, ge=0), - bounty_type: Optional[str] = None, - search: Optional[str] = None, - current_user: str = Depends(get_current_user) -) -> dict[str, Any]: - """Retrieve collected bounties (harvested credentials, payloads, etc.).""" - _data = await repo.get_bounties(limit=limit, offset=offset, bounty_type=bounty_type, search=search) - _total = await repo.get_total_bounties(bounty_type=bounty_type, search=search) - return { - "total": _total, - "limit": limit, - "offset": offset, - "data": _data - } - - -@app.get("/api/v1/logs/histogram", tags=["Logs"]) -async def get_logs_histogram( - search: Optional[str] = None, - start_time: Optional[str] = None, - end_time: Optional[str] = None, - interval_minutes: int = Query(15, ge=1), - current_user: str = Depends(get_current_user) -) -> list[dict[str, Any]]: - return await repo.get_log_histogram(search=search, start_time=start_time, end_time=end_time, interval_minutes=interval_minutes) - - -class StatsResponse(BaseModel): - total_logs: int - unique_attackers: int - active_deckies: int - deployed_deckies: int - - -@app.get("/api/v1/stats", response_model=StatsResponse, tags=["Observability"]) -async def get_stats(current_user: str = Depends(get_current_user)) -> dict[str, Any]: - return await repo.get_stats_summary() - - -@app.get("/api/v1/deckies", tags=["Fleet Management"]) -async def get_deckies(current_user: str = Depends(get_current_user)) -> list[dict[str, Any]]: - return await repo.get_deckies() - - -class MutateIntervalRequest(BaseModel): - mutate_interval: int | None - - -@app.post("/api/v1/deckies/{decky_name}/mutate", tags=["Fleet Management"]) -async def api_mutate_decky(decky_name: str, current_user: str = Depends(get_current_user)) -> dict[str, str]: - from decnet.mutator import mutate_decky - success = mutate_decky(decky_name) - if success: - return {"message": f"Successfully mutated {decky_name}"} - raise HTTPException(status_code=404, detail=f"Decky {decky_name} not found or failed to mutate") - - -@app.put("/api/v1/deckies/{decky_name}/mutate-interval", tags=["Fleet Management"]) -async def api_update_mutate_interval(decky_name: str, req: MutateIntervalRequest, current_user: str = Depends(get_current_user)) -> dict[str, str]: - from decnet.config import load_state, save_state - state = load_state() - if not state: - raise HTTPException(status_code=500, detail="No active deployment") - config, compose_path = state - decky = next((d for d in config.deckies if d.name == decky_name), None) - if not decky: - raise HTTPException(status_code=404, detail="Decky not found") - decky.mutate_interval = req.mutate_interval - save_state(config, compose_path) - return {"message": "Mutation interval updated"} - - -@app.get("/api/v1/stream", tags=["Observability"]) -async def stream_events( - request: Request, - last_event_id: int = Query(0, alias="lastEventId"), - search: Optional[str] = None, - start_time: Optional[str] = None, - end_time: Optional[str] = None, - current_user: str = Depends(get_current_user) -) -> StreamingResponse: - import json - import asyncio - - async def event_generator() -> AsyncGenerator[str, None]: - # Start tracking from the provided ID, or current max if 0 - last_id = last_event_id - if last_id == 0: - last_id = await repo.get_max_log_id() - - stats_interval_sec = 10 - loops_since_stats = 0 - - while True: - if await request.is_disconnected(): - break - - # Poll for new logs - new_logs = await repo.get_logs_after_id(last_id, limit=50, search=search, start_time=start_time, end_time=end_time) - if new_logs: - # Update last_id to the max id in the fetched batch - last_id = max(log["id"] for log in new_logs) - payload = json.dumps({"type": "logs", "data": new_logs}) - yield f"event: message\ndata: {payload}\n\n" - - # If we have new logs, stats probably changed, so force a stats update - loops_since_stats = stats_interval_sec - - # Periodically poll for stats - if loops_since_stats >= stats_interval_sec: - stats = await repo.get_stats_summary() - payload = json.dumps({"type": "stats", "data": stats}) - yield f"event: message\ndata: {payload}\n\n" - - # Also yield histogram - histogram = await repo.get_log_histogram(search=search, start_time=start_time, end_time=end_time, interval_minutes=15) - hist_payload = json.dumps({"type": "histogram", "data": histogram}) - yield f"event: message\ndata: {hist_payload}\n\n" - - loops_since_stats = 0 - - loops_since_stats += 1 - await asyncio.sleep(1) - - return StreamingResponse(event_generator(), media_type="text/event-stream") - - -class DeployIniRequest(BaseModel): - ini_content: str = Field(..., min_length=5, max_length=512 * 1024) - -@app.post("/api/v1/deckies/deploy", tags=["Fleet Management"]) -async def api_deploy_deckies(req: DeployIniRequest, current_user: str = Depends(get_current_user)) -> dict[str, str]: - from decnet.ini_loader import load_ini_from_string - from decnet.cli import _build_deckies_from_ini - from decnet.config import load_state, DecnetConfig, DEFAULT_MUTATE_INTERVAL - from decnet.network import detect_interface, detect_subnet, get_host_ip - from decnet.deployer import deploy as _deploy - import logging - import os - - try: - ini = load_ini_from_string(req.ini_content) - except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to parse INI: {e}") - - state = load_state() - ingest_log_file = os.environ.get("DECNET_INGEST_LOG_FILE") - - if state: - config, _ = state - subnet_cidr = ini.subnet or config.subnet - gateway = ini.gateway or config.gateway - host_ip = get_host_ip(config.interface) - randomize_services = False - # Always sync config log_file with current API ingestion target - if ingest_log_file: - config.log_file = ingest_log_file - else: - # If no state exists, we need to infer network details - iface = ini.interface or detect_interface() - subnet_cidr, gateway = ini.subnet, ini.gateway - if not subnet_cidr or not gateway: - detected_subnet, detected_gateway = detect_subnet(iface) - subnet_cidr = subnet_cidr or detected_subnet - gateway = gateway or detected_gateway - host_ip = get_host_ip(iface) - randomize_services = False - config = DecnetConfig( - mode="unihost", - interface=iface, - subnet=subnet_cidr, - gateway=gateway, - deckies=[], - log_target=ini.log_target, - log_file=ingest_log_file, - ipvlan=False, - mutate_interval=ini.mutate_interval or DEFAULT_MUTATE_INTERVAL - ) - - try: - new_decky_configs = _build_deckies_from_ini( - ini, subnet_cidr, gateway, host_ip, randomize_services, cli_mutate_interval=None - ) - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - - # Merge deckies - existing_deckies_map = {d.name: d for d in config.deckies} - for new_decky in new_decky_configs: - existing_deckies_map[new_decky.name] = new_decky - - config.deckies = list(existing_deckies_map.values()) - - # We call deploy(config) which regenerates docker-compose and runs `up -d --remove-orphans`. - try: - _deploy(config) - except Exception as e: - logging.getLogger("decnet.web.api").error(f"Deployment failed: {e}") - raise HTTPException(status_code=500, detail=f"Deployment failed: {e}") - - return {"message": "Deckies deployed successfully"} +# Include the modular API router +app.include_router(api_router, prefix="/api/v1") diff --git a/decnet/web/dependencies.py b/decnet/web/dependencies.py new file mode 100644 index 0000000..9c21733 --- /dev/null +++ b/decnet/web/dependencies.py @@ -0,0 +1,46 @@ +from typing import Any, Optional +from pathlib import Path + +import jwt +from fastapi import HTTPException, status, Request +from fastapi.security import OAuth2PasswordBearer + +from decnet.web.auth import ALGORITHM, SECRET_KEY +from decnet.web.sqlite_repository import SQLiteRepository + +# Root directory for database +_ROOT_DIR = Path(__file__).parent.parent.parent.absolute() +DB_PATH = _ROOT_DIR / "decnet.db" + +# Shared repository instance +repo = SQLiteRepository(db_path=str(DB_PATH)) + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") + + +async def get_current_user(request: Request) -> str: + _credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Extract token from header or query param + token: str | None = None + auth_header = request.headers.get("Authorization") + if auth_header and auth_header.startswith("Bearer "): + token = auth_header.split(" ")[1] + elif request.query_params.get("token"): + token = request.query_params.get("token") + + if not token: + raise _credentials_exception + + try: + _payload: dict[str, Any] = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + _user_uuid: Optional[str] = _payload.get("uuid") + if _user_uuid is None: + raise _credentials_exception + return _user_uuid + except jwt.PyJWTError: + raise _credentials_exception diff --git a/decnet/web/models.py b/decnet/web/models.py new file mode 100644 index 0000000..ac45ea3 --- /dev/null +++ b/decnet/web/models.py @@ -0,0 +1,46 @@ +from typing import Any +from pydantic import BaseModel, Field + +class Token(BaseModel): + access_token: str + token_type: str + must_change_password: bool = False + + +class LoginRequest(BaseModel): + username: str + password: str = Field(..., max_length=72) + + +class ChangePasswordRequest(BaseModel): + old_password: str = Field(..., max_length=72) + new_password: str = Field(..., max_length=72) + + +class LogsResponse(BaseModel): + total: int + limit: int + offset: int + data: list[dict[str, Any]] + + +class BountyResponse(BaseModel): + total: int + limit: int + offset: int + data: list[dict[str, Any]] + + +class StatsResponse(BaseModel): + total_logs: int + unique_attackers: int + active_deckies: int + deployed_deckies: int + + +class MutateIntervalRequest(BaseModel): + mutate_interval: int | None + + +class DeployIniRequest(BaseModel): + ini_content: str = Field(..., min_length=5, max_length=512 * 1024) diff --git a/decnet/web/router/__init__.py b/decnet/web/router/__init__.py new file mode 100644 index 0000000..b1bd92e --- /dev/null +++ b/decnet/web/router/__init__.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter + +from .auth.api_login import router as login_router +from .auth.api_change_pass import router as change_pass_router +from .logs.api_get_logs import router as logs_router +from .logs.api_get_histogram import router as histogram_router +from .bounty.api_get_bounties import router as bounty_router +from .stats.api_get_stats import router as stats_router +from .fleet.api_get_deckies import router as get_deckies_router +from .fleet.api_mutate_decky import router as mutate_decky_router +from .fleet.api_mutate_interval import router as mutate_interval_router +from .fleet.api_deploy_deckies import router as deploy_deckies_router +from .stream.api_stream_events import router as stream_router + +api_router = APIRouter() + +# Authentication +api_router.include_router(login_router) +api_router.include_router(change_pass_router) + +# Logs & Analytics +api_router.include_router(logs_router) +api_router.include_router(histogram_router) + +# Bounty Vault +api_router.include_router(bounty_router) + +# Fleet Management +api_router.include_router(get_deckies_router) +api_router.include_router(mutate_decky_router) +api_router.include_router(mutate_interval_router) +api_router.include_router(deploy_deckies_router) + +# Observability +api_router.include_router(stats_router) +api_router.include_router(stream_router) diff --git a/decnet/web/router/auth/api_change_pass.py b/decnet/web/router/auth/api_change_pass.py new file mode 100644 index 0000000..556913a --- /dev/null +++ b/decnet/web/router/auth/api_change_pass.py @@ -0,0 +1,23 @@ +from typing import Any, Optional + +from fastapi import APIRouter, Depends, HTTPException, status + +from decnet.web.auth import get_password_hash, verify_password +from decnet.web.dependencies import get_current_user, repo +from decnet.web.models import ChangePasswordRequest + +router = APIRouter() + + +@router.post("/auth/change-password", tags=["Authentication"]) +async def change_password(request: ChangePasswordRequest, current_user: str = Depends(get_current_user)) -> dict[str, str]: + _user: Optional[dict[str, Any]] = await repo.get_user_by_uuid(current_user) + if not _user or not verify_password(request.old_password, _user["password_hash"]): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect old password", + ) + + _new_hash: str = get_password_hash(request.new_password) + await repo.update_user_password(current_user, _new_hash, must_change_password=False) + return {"message": "Password updated successfully"} diff --git a/decnet/web/router/auth/api_login.py b/decnet/web/router/auth/api_login.py new file mode 100644 index 0000000..fd1c630 --- /dev/null +++ b/decnet/web/router/auth/api_login.py @@ -0,0 +1,36 @@ +from datetime import timedelta +from typing import Any, Optional + +from fastapi import APIRouter, HTTPException, status + +from decnet.web.auth import ( + ACCESS_TOKEN_EXPIRE_MINUTES, + create_access_token, + verify_password, +) +from decnet.web.dependencies import repo +from decnet.web.models import LoginRequest, Token + +router = APIRouter() + + +@router.post("/auth/login", response_model=Token, tags=["Authentication"]) +async def login(request: LoginRequest) -> dict[str, Any]: + _user: Optional[dict[str, Any]] = await repo.get_user_by_username(request.username) + if not _user or not verify_password(request.password, _user["password_hash"]): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + _access_token_expires: timedelta = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + # Token uses uuid instead of sub + _access_token: str = create_access_token( + data={"uuid": _user["uuid"]}, expires_delta=_access_token_expires + ) + return { + "access_token": _access_token, + "token_type": "bearer", # nosec B105 + "must_change_password": bool(_user.get("must_change_password", False)) + } diff --git a/decnet/web/router/bounty/api_get_bounties.py b/decnet/web/router/bounty/api_get_bounties.py new file mode 100644 index 0000000..6607794 --- /dev/null +++ b/decnet/web/router/bounty/api_get_bounties.py @@ -0,0 +1,27 @@ +from typing import Any, Optional + +from fastapi import APIRouter, Depends, Query + +from decnet.web.dependencies import get_current_user, repo +from decnet.web.models import BountyResponse + +router = APIRouter() + + +@router.get("/bounty", response_model=BountyResponse, tags=["Bounty Vault"]) +async def get_bounties( + limit: int = Query(50, ge=1, le=1000), + offset: int = Query(0, ge=0), + bounty_type: Optional[str] = None, + search: Optional[str] = None, + current_user: str = Depends(get_current_user) +) -> dict[str, Any]: + """Retrieve collected bounties (harvested credentials, payloads, etc.).""" + _data = await repo.get_bounties(limit=limit, offset=offset, bounty_type=bounty_type, search=search) + _total = await repo.get_total_bounties(bounty_type=bounty_type, search=search) + return { + "total": _total, + "limit": limit, + "offset": offset, + "data": _data + } diff --git a/decnet/web/router/fleet/api_deploy_deckies.py b/decnet/web/router/fleet/api_deploy_deckies.py new file mode 100644 index 0000000..5a9cfea --- /dev/null +++ b/decnet/web/router/fleet/api_deploy_deckies.py @@ -0,0 +1,80 @@ +import logging +import os + +from fastapi import APIRouter, Depends, HTTPException + +from decnet.config import DEFAULT_MUTATE_INTERVAL, DecnetConfig, load_state +from decnet.deployer import deploy as _deploy +from decnet.ini_loader import load_ini_from_string +from decnet.network import detect_interface, detect_subnet, get_host_ip +from decnet.web.dependencies import get_current_user +from decnet.web.models import DeployIniRequest + +router = APIRouter() + + +@router.post("/deckies/deploy", tags=["Fleet Management"]) +async def api_deploy_deckies(req: DeployIniRequest, current_user: str = Depends(get_current_user)) -> dict[str, str]: + from decnet.cli import _build_deckies_from_ini + + try: + ini = load_ini_from_string(req.ini_content) + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to parse INI: {e}") + + state = load_state() + ingest_log_file = os.environ.get("DECNET_INGEST_LOG_FILE") + + if state: + config, _ = state + subnet_cidr = ini.subnet or config.subnet + gateway = ini.gateway or config.gateway + host_ip = get_host_ip(config.interface) + randomize_services = False + # Always sync config log_file with current API ingestion target + if ingest_log_file: + config.log_file = ingest_log_file + else: + # If no state exists, we need to infer network details + iface = ini.interface or detect_interface() + subnet_cidr, gateway = ini.subnet, ini.gateway + if not subnet_cidr or not gateway: + detected_subnet, detected_gateway = detect_subnet(iface) + subnet_cidr = subnet_cidr or detected_subnet + gateway = gateway or detected_gateway + host_ip = get_host_ip(iface) + randomize_services = False + config = DecnetConfig( + mode="unihost", + interface=iface, + subnet=subnet_cidr, + gateway=gateway, + deckies=[], + log_target=ini.log_target, + log_file=ingest_log_file, + ipvlan=False, + mutate_interval=ini.mutate_interval or DEFAULT_MUTATE_INTERVAL + ) + + try: + new_decky_configs = _build_deckies_from_ini( + ini, subnet_cidr, gateway, host_ip, randomize_services, cli_mutate_interval=None + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + # Merge deckies + existing_deckies_map = {d.name: d for d in config.deckies} + for new_decky in new_decky_configs: + existing_deckies_map[new_decky.name] = new_decky + + config.deckies = list(existing_deckies_map.values()) + + # We call deploy(config) which regenerates docker-compose and runs `up -d --remove-orphans`. + try: + _deploy(config) + except Exception as e: + logging.getLogger("decnet.web.api").error(f"Deployment failed: {e}") + raise HTTPException(status_code=500, detail=f"Deployment failed: {e}") + + return {"message": "Deckies deployed successfully"} diff --git a/decnet/web/router/fleet/api_get_deckies.py b/decnet/web/router/fleet/api_get_deckies.py new file mode 100644 index 0000000..ee7c9cf --- /dev/null +++ b/decnet/web/router/fleet/api_get_deckies.py @@ -0,0 +1,12 @@ +from typing import Any + +from fastapi import APIRouter, Depends + +from decnet.web.dependencies import get_current_user, repo + +router = APIRouter() + + +@router.get("/deckies", tags=["Fleet Management"]) +async def get_deckies(current_user: str = Depends(get_current_user)) -> list[dict[str, Any]]: + return await repo.get_deckies() diff --git a/decnet/web/router/fleet/api_mutate_decky.py b/decnet/web/router/fleet/api_mutate_decky.py new file mode 100644 index 0000000..3372769 --- /dev/null +++ b/decnet/web/router/fleet/api_mutate_decky.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter, Depends, HTTPException + +from decnet.mutator import mutate_decky +from decnet.web.dependencies import get_current_user + +router = APIRouter() + + +@router.post("/deckies/{decky_name}/mutate", tags=["Fleet Management"]) +async def api_mutate_decky(decky_name: str, current_user: str = Depends(get_current_user)) -> dict[str, str]: + success = mutate_decky(decky_name) + if success: + return {"message": f"Successfully mutated {decky_name}"} + raise HTTPException(status_code=404, detail=f"Decky {decky_name} not found or failed to mutate") diff --git a/decnet/web/router/fleet/api_mutate_interval.py b/decnet/web/router/fleet/api_mutate_interval.py new file mode 100644 index 0000000..44e9993 --- /dev/null +++ b/decnet/web/router/fleet/api_mutate_interval.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, Depends, HTTPException + +from decnet.config import load_state, save_state +from decnet.web.dependencies import get_current_user +from decnet.web.models import MutateIntervalRequest + +router = APIRouter() + + +@router.put("/deckies/{decky_name}/mutate-interval", tags=["Fleet Management"]) +async def api_update_mutate_interval(decky_name: str, req: MutateIntervalRequest, current_user: str = Depends(get_current_user)) -> dict[str, str]: + state = load_state() + if not state: + raise HTTPException(status_code=500, detail="No active deployment") + config, compose_path = state + decky = next((d for d in config.deckies if d.name == decky_name), None) + if not decky: + raise HTTPException(status_code=404, detail="Decky not found") + decky.mutate_interval = req.mutate_interval + save_state(config, compose_path) + return {"message": "Mutation interval updated"} diff --git a/decnet/web/router/logs/api_get_histogram.py b/decnet/web/router/logs/api_get_histogram.py new file mode 100644 index 0000000..383fa7e --- /dev/null +++ b/decnet/web/router/logs/api_get_histogram.py @@ -0,0 +1,18 @@ +from typing import Any, Optional + +from fastapi import APIRouter, Depends, Query + +from decnet.web.dependencies import get_current_user, repo + +router = APIRouter() + + +@router.get("/logs/histogram", tags=["Logs"]) +async def get_logs_histogram( + search: Optional[str] = None, + start_time: Optional[str] = None, + end_time: Optional[str] = None, + interval_minutes: int = Query(15, ge=1), + current_user: str = Depends(get_current_user) +) -> list[dict[str, Any]]: + return await repo.get_log_histogram(search=search, start_time=start_time, end_time=end_time, interval_minutes=interval_minutes) diff --git a/decnet/web/router/logs/api_get_logs.py b/decnet/web/router/logs/api_get_logs.py new file mode 100644 index 0000000..e22b711 --- /dev/null +++ b/decnet/web/router/logs/api_get_logs.py @@ -0,0 +1,27 @@ +from typing import Any, Optional + +from fastapi import APIRouter, Depends, Query + +from decnet.web.dependencies import get_current_user, repo +from decnet.web.models import LogsResponse + +router = APIRouter() + + +@router.get("/logs", response_model=LogsResponse, tags=["Logs"]) +async def get_logs( + limit: int = Query(50, ge=1, le=1000), + offset: int = Query(0, ge=0), + search: Optional[str] = None, + start_time: Optional[str] = None, + end_time: Optional[str] = None, + current_user: str = Depends(get_current_user) +) -> dict[str, Any]: + _logs: list[dict[str, Any]] = await repo.get_logs(limit=limit, offset=offset, search=search, start_time=start_time, end_time=end_time) + _total: int = await repo.get_total_logs(search=search, start_time=start_time, end_time=end_time) + return { + "total": _total, + "limit": limit, + "offset": offset, + "data": _logs + } diff --git a/decnet/web/router/stats/api_get_stats.py b/decnet/web/router/stats/api_get_stats.py new file mode 100644 index 0000000..bec4058 --- /dev/null +++ b/decnet/web/router/stats/api_get_stats.py @@ -0,0 +1,13 @@ +from typing import Any + +from fastapi import APIRouter, Depends + +from decnet.web.dependencies import get_current_user, repo +from decnet.web.models import StatsResponse + +router = APIRouter() + + +@router.get("/stats", response_model=StatsResponse, tags=["Observability"]) +async def get_stats(current_user: str = Depends(get_current_user)) -> dict[str, Any]: + return await repo.get_stats_summary() diff --git a/decnet/web/router/stream/api_stream_events.py b/decnet/web/router/stream/api_stream_events.py new file mode 100644 index 0000000..1e591c6 --- /dev/null +++ b/decnet/web/router/stream/api_stream_events.py @@ -0,0 +1,63 @@ +import json +import asyncio +from typing import AsyncGenerator, Optional + +from fastapi import APIRouter, Depends, Query, Request +from fastapi.responses import StreamingResponse + +from decnet.web.dependencies import get_current_user, repo + +router = APIRouter() + + +@router.get("/stream", tags=["Observability"]) +async def stream_events( + request: Request, + last_event_id: int = Query(0, alias="lastEventId"), + search: Optional[str] = None, + start_time: Optional[str] = None, + end_time: Optional[str] = None, + current_user: str = Depends(get_current_user) +) -> StreamingResponse: + + async def event_generator() -> AsyncGenerator[str, None]: + # Start tracking from the provided ID, or current max if 0 + last_id = last_event_id + if last_id == 0: + last_id = await repo.get_max_log_id() + + stats_interval_sec = 10 + loops_since_stats = 0 + + while True: + if await request.is_disconnected(): + break + + # Poll for new logs + new_logs = await repo.get_logs_after_id(last_id, limit=50, search=search, start_time=start_time, end_time=end_time) + if new_logs: + # Update last_id to the max id in the fetched batch + last_id = max(log["id"] for log in new_logs) + payload = json.dumps({"type": "logs", "data": new_logs}) + yield f"event: message\ndata: {payload}\n\n" + + # If we have new logs, stats probably changed, so force a stats update + loops_since_stats = stats_interval_sec + + # Periodically poll for stats + if loops_since_stats >= stats_interval_sec: + stats = await repo.get_stats_summary() + payload = json.dumps({"type": "stats", "data": stats}) + yield f"event: message\ndata: {payload}\n\n" + + # Also yield histogram + histogram = await repo.get_log_histogram(search=search, start_time=start_time, end_time=end_time, interval_minutes=15) + hist_payload = json.dumps({"type": "histogram", "data": histogram}) + yield f"event: message\ndata: {hist_payload}\n\n" + + loops_since_stats = 0 + + loops_since_stats += 1 + await asyncio.sleep(1) + + return StreamingResponse(event_generator(), media_type="text/event-stream") diff --git a/decnet/web/sqlite_repository.py b/decnet/web/sqlite_repository.py index cfcc087..5c8fb66 100644 --- a/decnet/web/sqlite_repository.py +++ b/decnet/web/sqlite_repository.py @@ -410,7 +410,7 @@ class SQLiteRepository(BaseRepository): _d = dict(_row) try: _d["payload"] = json.loads(_d["payload"]) - except Exception: + except Exception: # nosec B110 pass _results.append(_d) return _results diff --git a/test_bounty_decnet.db-shm b/test_bounty_decnet.db-shm index ec95b1bdd00594cc222b45786af772f5263a6f33..3d0aaf90182a1626d23dea26d17edba65b22f0e9 100644 GIT binary patch delta 64 wcmZo@U}|V!njj%jQ};biWHR6X;uY5p#XR4m^T+-TtH9BsCwQbbCM>820NVB-B>(^b delta 64 vcmZo@U}|V!njj%@HmucnqSMPxL!V}T`SXc9H=X<2>MWF(;E~#xu%I3QGoTtJ diff --git a/test_bounty_decnet.db-wal b/test_bounty_decnet.db-wal index e1b3655c0f5b4bb6619c4cbc817b6b9e63db4721..f5ce014a5d7698aee29c9f009593a9df8cbd36f0 100644 GIT binary patch delta 362 zcmX@{knzMrMho+Lwk8JMM-mJS0t_HfyyDuSnCE+Rrte+dR+E&~4isWU7Wehb{kz-2 z>-t6u4gmp};swvTWt;yKoZ^a^4Q90T+E(8QDy;A7GzV{r}4;m?6=q0++Z+` zzuwrw*u>1z&_LHR+1Nza#KO{8H_^htT-Vav$jrph+`!z}GBGhFH#1MgC`rZ8NW~|` zE4acdLq9A(*e};3$uK{%s-m(qG`y_LFWo07ST83a#5=^yw=g6sJv%+9!lk&%$;TON z0MHGr$nH42qe!2T+3Z#R<{1um_-%-B2v9p4vJn;wA8)$8gm2^K4FO1gkNaL%^Pg|B J$l?G20RZ{&f1>~Z delta 362 zcmX@{knzMrMho+Lwk8JMM-mJS0t_Hv=+n$Ee?F1tNtVCMip}3RfI^JO;`f!@Z|6+i zBeT(hLqGtgSgK0(W%iCP;mrX8NTSM*Ow$)GvO5M8Wfl-+K{loDdD6;%Q?E&GZZMd} zUvFxfoNAbyW}s`HYLupHVw#w!n`C5Wq-$!KYH661W@edUn3|Z9o0+F#l%!&4q+*g= zYF3h+Q<<4o=4e>PqW_vMySFPhEd`1)3y88Ho3dl^f-OfgA6IQ|Fqp<) zZ;@zZnqqEgu4`^+nWSrCkZ7u#WSN$xn_^^?W@?^lX`Gm7l9-a4nWtiuq+)2K5*gxR zQ5KMso^0%q6BJaQW}X=6pR4DdYM58$?P{6kpA_mJnU)mnUuhX?R#6;i99CjsWCS(< z=mu6~cWn5(`jHOv(#7hVXE@y9w;{$MK<#YEMzlv#S03^Tj^#&bpHu*Vc Iae#mT0GE4q$N&HU delta 362 zcmX@{knzMrMho+Lwk8JMM-mJS0t_G^Bdn`uak4CtSwu`2y(JU#TlEiV(E z9UCn;1O#A;AKY`!5ty6DzBxbuNwh&VL+*mdgJuBlvS{~!C)GH zeUf2vVro*Ng>JH`WvZ@;kx{a)Wty>>u4RgWrDsl8T{`ikEX? zwtk{nWMo-ku1l3$riq_lgr#0ogmHd~p=C*yer2IvsAplNen@J5ZhC%-Pk>oTr5V@& zpc`0`-J#;XO+}CS`Y-;?GaT;l+YsXrpmsK7Bb<}Eice2T<{9 delta 64 vcmZo@U}|V!njj(3v}ecelYBlZ%pU~>o$Ok=eH)$|F^QRc!XvdYVL?3rAFUZT diff --git a/test_fleet_decnet.db-wal b/test_fleet_decnet.db-wal index de50df44c7461b2f9c6f995df4934d70e711f493..c9dd302f0cb7909ac0ce9a65e2ae875f7aa11f4d 100644 GIT binary patch delta 362 zcmX@{knzMrMho+Lwk8JMM-mJS0t_JV_qoLT2o*n#!~b6kEHQn~3lw5R7JnEqYyHE# zgRL7aI0OV>inqNy%^ICG`NQS_0VL74E*9OBv>v4cMVSReS&&Vsa@;cE$(75zn;Q(K z@z=)0L0bVAo>jjiMdm4l!`Rkj3 z4FI}<715ngBnyH+BfkKSP;vqjP?!GDA z>blW_LqGtgSkG*owV=OA!{z`1BvH*}yqdSJUpNjFWfl-+K{iEqM*K!$iHXgd8w{rL z*PENCnHm@yCh4YGm?!C)SXfx!zidBqkb6WTnj;0+9T^d&iz86(7En Iivt7%0LX@IQUCw| diff --git a/test_fuzz_decnet.db-shm b/test_fuzz_decnet.db-shm index 1bd4ed6560b303a15196c396eedb99ff64fbea9c..ab3ab375339dac434b9bb4f7741709cb488a86af 100644 GIT binary patch delta 64 wcmZo@U}|V!njj(Z`q{3&i4)AHZ#&SY@#GNW^I2*>$x3gTkKmEon6RK80P-^*vj6}9 delta 64 wcmZo@U}|V!njj%DRn+0l>OHnjYn#ixC)ZpE{$J$#=9h2$Av{tW6Bg710K0AiVyj?e)DvftKA$RfFyc*_O$@M!+aZnqRaxKEXbz(XDEy6VVCCI++Z+` zzuwr$)HEs8#8THHCB;nF#K=5Z*TN#jQrFNZ*)%o9A~D4v(K0b5H#1MgC`rZ8NX6eH z%`nuc)V$Q$D=)y!J143vC@9iDI6N}P!oxq;UthmC$vn%YEW*d#J1^9&A}QFUAgL5= z0MHGr$nFTf`TW}t=1XTF*dN&O)@Yr)=f%GO){`dPE0X21`@fMc`8OpDuzZXe!j*A z-eCcbWqH|#1_ePzo?hhgIi8lmPEO$&f%;V$QKl~0xnKi; zZeT@r$8EdJ53bA;p7d>=;c$oFh8TwcwX-1`QJ-#V{`+T#$>t3KNPd6gAUf5_cF*d? H0RjR5ov?da diff --git a/tests/test_bounty.py b/tests/test_bounty.py index 3b53072..b35914b 100644 --- a/tests/test_bounty.py +++ b/tests/test_bounty.py @@ -2,7 +2,8 @@ import os from typing import Generator import pytest from fastapi.testclient import TestClient -from decnet.web.api import app, repo +from decnet.web.api import app +from decnet.web.dependencies import repo from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD @pytest.fixture(autouse=True) diff --git a/tests/test_fleet_api.py b/tests/test_fleet_api.py index 5cc14df..cb65881 100644 --- a/tests/test_fleet_api.py +++ b/tests/test_fleet_api.py @@ -5,7 +5,7 @@ from decnet.web.api import app import decnet.config from pathlib import Path from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD -from decnet.web.api import repo +from decnet.web.dependencies import repo @pytest.fixture(autouse=True) def setup_db(): diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 933ed09..5e22000 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -4,7 +4,8 @@ from typing import Generator import pytest from fastapi.testclient import TestClient -from decnet.web.api import app, repo +from decnet.web.api import app +from decnet.web.dependencies import repo @pytest.fixture(autouse=True) diff --git a/tests/test_web_api_fuzz.py b/tests/test_web_api_fuzz.py index c318ac1..59c2a3b 100644 --- a/tests/test_web_api_fuzz.py +++ b/tests/test_web_api_fuzz.py @@ -6,7 +6,8 @@ from fastapi.testclient import TestClient from hypothesis import given, strategies as st, settings, HealthCheck import httpx -from decnet.web.api import app, repo +from decnet.web.api import app +from decnet.web.dependencies import repo # Re-use setup from test_web_api @pytest.fixture(scope="function", autouse=True)