fix: resolve all 64 mypy errors across backend
- Add dict[str, Any] / list[Any] type params throughout (fingerprint, models, schemas, scanner, status_checker) - Add return type annotations to all route functions (nodes, edges, canvas, scan, auth, status, main) - Fix no-any-return in security.py: cast pwd/jwt results to bool/str explicitly - Fix canvas.py: use model_validate() for NodeResponse/EdgeResponse, rename db_node/db_edge upsert vars - Fix scheduler.py: rename 'result' → 'check_result' to avoid type collision - Fix get_db() return type: AsyncGenerator[AsyncSession, None] - Add types-PyYAML for yaml import stubs - Fix scanner.py: remove unnecessary type: ignore comment (nmap has stubs) - Fix scan.py: wrap scalars().all() with list() for Sequence→list compatibility
This commit is contained in:
@@ -26,7 +26,7 @@ def _load_credentials() -> tuple[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=TokenResponse)
|
@router.post("/login", response_model=TokenResponse)
|
||||||
async def login(body: LoginRequest):
|
async def login(body: LoginRequest) -> TokenResponse:
|
||||||
username, password_hash = _load_credentials()
|
username, password_hash = _load_credentials()
|
||||||
if body.username != username or not verify_password(body.password, password_hash):
|
if body.username != username or not verify_password(body.password, password_hash):
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -8,21 +9,29 @@ from app.api.deps import get_current_user
|
|||||||
from app.db.database import get_db
|
from app.db.database import get_db
|
||||||
from app.db.models import CanvasState, Edge, Node
|
from app.db.models import CanvasState, Edge, Node
|
||||||
from app.schemas.canvas import CanvasSaveRequest, CanvasStateResponse
|
from app.schemas.canvas import CanvasSaveRequest, CanvasStateResponse
|
||||||
|
from app.schemas.edges import EdgeResponse
|
||||||
|
from app.schemas.nodes import NodeResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=CanvasStateResponse)
|
@router.get("", response_model=CanvasStateResponse)
|
||||||
async def load_canvas(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def load_canvas(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> CanvasStateResponse:
|
||||||
nodes = (await db.execute(select(Node))).scalars().all()
|
nodes = (await db.execute(select(Node))).scalars().all()
|
||||||
edges = (await db.execute(select(Edge))).scalars().all()
|
edges = (await db.execute(select(Edge))).scalars().all()
|
||||||
state = await db.get(CanvasState, 1)
|
state = await db.get(CanvasState, 1)
|
||||||
viewport = state.viewport if state else {"x": 0, "y": 0, "zoom": 1}
|
viewport: dict[str, Any] = state.viewport if state else {"x": 0, "y": 0, "zoom": 1}
|
||||||
return CanvasStateResponse(nodes=list(nodes), edges=list(edges), viewport=viewport)
|
return CanvasStateResponse(
|
||||||
|
nodes=[NodeResponse.model_validate(n) for n in nodes],
|
||||||
|
edges=[EdgeResponse.model_validate(e) for e in edges],
|
||||||
|
viewport=viewport,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/save")
|
@router.post("/save")
|
||||||
async def save_canvas(body: CanvasSaveRequest, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def save_canvas(
|
||||||
|
body: CanvasSaveRequest, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)
|
||||||
|
) -> dict[str, bool]:
|
||||||
incoming_node_ids = {n.id for n in body.nodes}
|
incoming_node_ids = {n.id for n in body.nodes}
|
||||||
incoming_edge_ids = {e.id for e in body.edges}
|
incoming_edge_ids = {e.id for e in body.edges}
|
||||||
|
|
||||||
@@ -42,19 +51,19 @@ async def save_canvas(body: CanvasSaveRequest, db: AsyncSession = Depends(get_db
|
|||||||
|
|
||||||
# Upsert nodes
|
# Upsert nodes
|
||||||
for node_data in body.nodes:
|
for node_data in body.nodes:
|
||||||
node = await db.get(Node, node_data.id)
|
db_node = await db.get(Node, node_data.id)
|
||||||
if node:
|
if db_node:
|
||||||
for field, value in node_data.model_dump().items():
|
for field, value in node_data.model_dump().items():
|
||||||
setattr(node, field, value)
|
setattr(db_node, field, value)
|
||||||
else:
|
else:
|
||||||
db.add(Node(**node_data.model_dump()))
|
db.add(Node(**node_data.model_dump()))
|
||||||
|
|
||||||
# Upsert edges
|
# Upsert edges
|
||||||
for edge_data in body.edges:
|
for edge_data in body.edges:
|
||||||
edge = await db.get(Edge, edge_data.id)
|
db_edge = await db.get(Edge, edge_data.id)
|
||||||
if edge:
|
if db_edge:
|
||||||
for field, value in edge_data.model_dump().items():
|
for field, value in edge_data.model_dump().items():
|
||||||
setattr(edge, field, value)
|
setattr(db_edge, field, value)
|
||||||
else:
|
else:
|
||||||
db.add(Edge(**edge_data.model_dump()))
|
db.add(Edge(**edge_data.model_dump()))
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[EdgeResponse])
|
@router.get("", response_model=list[EdgeResponse])
|
||||||
async def list_edges(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def list_edges(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> list[Edge]:
|
||||||
result = await db.execute(select(Edge))
|
result = await db.execute(select(Edge))
|
||||||
return result.scalars().all()
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=EdgeResponse, status_code=status.HTTP_201_CREATED)
|
@router.post("", response_model=EdgeResponse, status_code=status.HTTP_201_CREATED)
|
||||||
async def create_edge(body: EdgeCreate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def create_edge(body: EdgeCreate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> Edge:
|
||||||
edge = Edge(**body.model_dump())
|
edge = Edge(**body.model_dump())
|
||||||
db.add(edge)
|
db.add(edge)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -26,7 +26,7 @@ async def create_edge(body: EdgeCreate, db: AsyncSession = Depends(get_db), _: s
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/{edge_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/{edge_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_edge(edge_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def delete_edge(edge_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> None:
|
||||||
edge = await db.get(Edge, edge_id)
|
edge = await db.get(Edge, edge_id)
|
||||||
if not edge:
|
if not edge:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Edge not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Edge not found")
|
||||||
@@ -37,7 +37,7 @@ async def delete_edge(edge_id: str, db: AsyncSession = Depends(get_db), _: str =
|
|||||||
@router.patch("/{edge_id}", response_model=EdgeResponse)
|
@router.patch("/{edge_id}", response_model=EdgeResponse)
|
||||||
async def update_edge(
|
async def update_edge(
|
||||||
edge_id: str, body: EdgeUpdate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)
|
edge_id: str, body: EdgeUpdate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)
|
||||||
):
|
) -> Edge:
|
||||||
edge = await db.get(Edge, edge_id)
|
edge = await db.get(Edge, edge_id)
|
||||||
if not edge:
|
if not edge:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Edge not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Edge not found")
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[NodeResponse])
|
@router.get("", response_model=list[NodeResponse])
|
||||||
async def list_nodes(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def list_nodes(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> list[Node]:
|
||||||
result = await db.execute(select(Node))
|
result = await db.execute(select(Node))
|
||||||
return result.scalars().all()
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=NodeResponse, status_code=status.HTTP_201_CREATED)
|
@router.post("", response_model=NodeResponse, status_code=status.HTTP_201_CREATED)
|
||||||
async def create_node(body: NodeCreate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def create_node(body: NodeCreate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> Node:
|
||||||
node = Node(**body.model_dump())
|
node = Node(**body.model_dump())
|
||||||
db.add(node)
|
db.add(node)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -26,7 +26,7 @@ async def create_node(body: NodeCreate, db: AsyncSession = Depends(get_db), _: s
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/{node_id}", response_model=NodeResponse)
|
@router.get("/{node_id}", response_model=NodeResponse)
|
||||||
async def get_node(node_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def get_node(node_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> Node:
|
||||||
node = await db.get(Node, node_id)
|
node = await db.get(Node, node_id)
|
||||||
if not node:
|
if not node:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Node not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Node not found")
|
||||||
@@ -36,7 +36,7 @@ async def get_node(node_id: str, db: AsyncSession = Depends(get_db), _: str = De
|
|||||||
@router.patch("/{node_id}", response_model=NodeResponse)
|
@router.patch("/{node_id}", response_model=NodeResponse)
|
||||||
async def update_node(
|
async def update_node(
|
||||||
node_id: str, body: NodeUpdate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)
|
node_id: str, body: NodeUpdate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)
|
||||||
):
|
) -> Node:
|
||||||
node = await db.get(Node, node_id)
|
node = await db.get(Node, node_id)
|
||||||
if not node:
|
if not node:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Node not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Node not found")
|
||||||
@@ -48,7 +48,7 @@ async def update_node(
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_node(node_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def delete_node(node_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> None:
|
||||||
node = await db.get(Node, node_id)
|
node = await db.get(Node, node_id)
|
||||||
if not node:
|
if not node:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Node not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Node not found")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
||||||
@@ -26,8 +27,8 @@ router = APIRouter()
|
|||||||
def _load_ranges() -> list[str]:
|
def _load_ranges() -> list[str]:
|
||||||
try:
|
try:
|
||||||
with open(settings.config_path) as f:
|
with open(settings.config_path) as f:
|
||||||
cfg = yaml.safe_load(f)
|
cfg: dict[str, Any] = yaml.safe_load(f) or {}
|
||||||
return cfg.get("scanner", {}).get("ranges", [])
|
return list(cfg.get("scanner", {}).get("ranges", []))
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ async def trigger_scan(
|
|||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: str = Depends(get_current_user),
|
_: str = Depends(get_current_user),
|
||||||
):
|
) -> ScanRun:
|
||||||
ranges = _load_ranges()
|
ranges = _load_ranges()
|
||||||
run = ScanRun(status="running", ranges=ranges)
|
run = ScanRun(status="running", ranges=ranges)
|
||||||
db.add(run)
|
db.add(run)
|
||||||
@@ -53,15 +54,15 @@ async def trigger_scan(
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/pending", response_model=list[PendingDeviceResponse])
|
@router.get("/pending", response_model=list[PendingDeviceResponse])
|
||||||
async def list_pending(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def list_pending(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> list[PendingDevice]:
|
||||||
result = await db.execute(select(PendingDevice).where(PendingDevice.status == "pending"))
|
result = await db.execute(select(PendingDevice).where(PendingDevice.status == "pending"))
|
||||||
return result.scalars().all()
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
@router.get("/hidden", response_model=list[PendingDeviceResponse])
|
@router.get("/hidden", response_model=list[PendingDeviceResponse])
|
||||||
async def list_hidden(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def list_hidden(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> list[PendingDevice]:
|
||||||
result = await db.execute(select(PendingDevice).where(PendingDevice.status == "hidden"))
|
result = await db.execute(select(PendingDevice).where(PendingDevice.status == "hidden"))
|
||||||
return result.scalars().all()
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pending/{device_id}/approve", response_model=dict)
|
@router.post("/pending/{device_id}/approve", response_model=dict)
|
||||||
@@ -70,7 +71,7 @@ async def approve_device(
|
|||||||
node_data: NodeCreate,
|
node_data: NodeCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: str = Depends(get_current_user),
|
_: str = Depends(get_current_user),
|
||||||
):
|
) -> dict[str, Any]:
|
||||||
device = await db.get(PendingDevice, device_id)
|
device = await db.get(PendingDevice, device_id)
|
||||||
if device:
|
if device:
|
||||||
device.status = "approved"
|
device.status = "approved"
|
||||||
@@ -82,7 +83,9 @@ async def approve_device(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/pending/{device_id}/hide")
|
@router.post("/pending/{device_id}/hide")
|
||||||
async def hide_device(device_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def hide_device(
|
||||||
|
device_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)
|
||||||
|
) -> dict[str, bool]:
|
||||||
device = await db.get(PendingDevice, device_id)
|
device = await db.get(PendingDevice, device_id)
|
||||||
if device:
|
if device:
|
||||||
device.status = "hidden"
|
device.status = "hidden"
|
||||||
@@ -91,7 +94,9 @@ async def hide_device(device_id: str, db: AsyncSession = Depends(get_db), _: str
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/pending/{device_id}/ignore")
|
@router.post("/pending/{device_id}/ignore")
|
||||||
async def ignore_device(device_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def ignore_device(
|
||||||
|
device_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)
|
||||||
|
) -> dict[str, bool]:
|
||||||
device = await db.get(PendingDevice, device_id)
|
device = await db.get(PendingDevice, device_id)
|
||||||
if device:
|
if device:
|
||||||
await db.delete(device)
|
await db.delete(device)
|
||||||
@@ -100,13 +105,13 @@ async def ignore_device(device_id: str, db: AsyncSession = Depends(get_db), _: s
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/runs", response_model=list[ScanRunResponse])
|
@router.get("/runs", response_model=list[ScanRunResponse])
|
||||||
async def list_runs(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)):
|
async def list_runs(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> list[ScanRun]:
|
||||||
result = await db.execute(select(ScanRun).order_by(ScanRun.started_at.desc()).limit(20))
|
result = await db.execute(select(ScanRun).order_by(ScanRun.started_at.desc()).limit(20))
|
||||||
return result.scalars().all()
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config", response_model=ScanConfig)
|
@router.get("/config", response_model=ScanConfig)
|
||||||
async def get_scan_config(_: str = Depends(get_current_user)):
|
async def get_scan_config(_: str = Depends(get_current_user)) -> ScanConfig:
|
||||||
try:
|
try:
|
||||||
with open(settings.config_path) as f:
|
with open(settings.config_path) as f:
|
||||||
cfg = yaml.safe_load(f)
|
cfg = yaml.safe_load(f)
|
||||||
@@ -118,7 +123,7 @@ async def get_scan_config(_: str = Depends(get_current_user)):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/config", response_model=ScanConfig)
|
@router.post("/config", response_model=ScanConfig)
|
||||||
async def update_scan_config(payload: ScanConfig, _: str = Depends(get_current_user)):
|
async def update_scan_config(payload: ScanConfig, _: str = Depends(get_current_user)) -> ScanConfig:
|
||||||
try:
|
try:
|
||||||
with open(settings.config_path) as f:
|
with open(settings.config_path) as f:
|
||||||
cfg = yaml.safe_load(f) or {}
|
cfg = yaml.safe_load(f) or {}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ _connections: list[WebSocket] = []
|
|||||||
|
|
||||||
|
|
||||||
@router.websocket("/ws/status")
|
@router.websocket("/ws/status")
|
||||||
async def ws_status(websocket: WebSocket):
|
async def ws_status(websocket: WebSocket) -> None:
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
_connections.append(websocket)
|
_connections.append(websocket)
|
||||||
try:
|
try:
|
||||||
@@ -19,7 +19,7 @@ async def ws_status(websocket: WebSocket):
|
|||||||
_connections.remove(websocket)
|
_connections.remove(websocket)
|
||||||
|
|
||||||
|
|
||||||
async def broadcast_status(node_id: str, status: str, checked_at: str, response_time_ms: int | None = None):
|
async def broadcast_status(node_id: str, status: str, checked_at: str, response_time_ms: int | None = None) -> None:
|
||||||
payload = json.dumps({
|
payload = json.dumps({
|
||||||
"node_id": node_id,
|
"node_id": node_id,
|
||||||
"status": status,
|
"status": status,
|
||||||
|
|||||||
@@ -28,19 +28,19 @@ async def _run_status_checks() -> None:
|
|||||||
if not node.check_method:
|
if not node.check_method:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
result = await check_node(node.check_method, node.check_target, node.ip)
|
check_result = await check_node(node.check_method, node.check_target, node.ip)
|
||||||
async with AsyncSessionLocal() as db:
|
async with AsyncSessionLocal() as db:
|
||||||
n = await db.get(Node, node.id)
|
n = await db.get(Node, node.id)
|
||||||
if n:
|
if n:
|
||||||
n.status = result["status"]
|
n.status = check_result["status"]
|
||||||
n.response_time_ms = result["response_time_ms"]
|
n.response_time_ms = check_result["response_time_ms"]
|
||||||
n.last_seen = datetime.now(UTC) if result["status"] == "online" else n.last_seen
|
n.last_seen = datetime.now(UTC) if check_result["status"] == "online" else n.last_seen
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await broadcast_status(
|
await broadcast_status(
|
||||||
node_id=node.id,
|
node_id=node.id,
|
||||||
status=result["status"],
|
status=check_result["status"],
|
||||||
checked_at=datetime.now(UTC).isoformat(),
|
checked_at=datetime.now(UTC).isoformat(),
|
||||||
response_time_ms=result["response_time_ms"],
|
response_time_ms=check_result["response_time_ms"],
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("Status check failed for node %s: %s", node.id, exc)
|
logger.error("Status check failed for node %s: %s", node.id, exc)
|
||||||
|
|||||||
@@ -9,22 +9,23 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|||||||
|
|
||||||
|
|
||||||
def verify_password(plain: str, hashed: str) -> bool:
|
def verify_password(plain: str, hashed: str) -> bool:
|
||||||
return pwd_context.verify(plain, hashed)
|
return bool(pwd_context.verify(plain, hashed))
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
return pwd_context.hash(password)
|
return str(pwd_context.hash(password))
|
||||||
|
|
||||||
|
|
||||||
def create_access_token(subject: str) -> str:
|
def create_access_token(subject: str) -> str:
|
||||||
expire = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes)
|
expire = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes)
|
||||||
payload = {"sub": subject, "exp": expire}
|
payload = {"sub": subject, "exp": expire}
|
||||||
return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)
|
return str(jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm))
|
||||||
|
|
||||||
|
|
||||||
def decode_token(token: str) -> str | None:
|
def decode_token(token: str) -> str | None:
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
|
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
|
||||||
return payload.get("sub")
|
sub = payload.get("sub")
|
||||||
|
return str(sub) if sub is not None else None
|
||||||
except JWTError:
|
except JWTError:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
@@ -31,6 +32,6 @@ async def init_db() -> None:
|
|||||||
await conn.exec_driver_sql("ALTER TABLE edges ADD COLUMN path_style TEXT")
|
await conn.exec_driver_sql("ALTER TABLE edges ADD COLUMN path_style TEXT")
|
||||||
|
|
||||||
|
|
||||||
async def get_db():
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
yield session
|
yield session
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import JSON, Boolean, DateTime, Float, ForeignKey, Integer, String, Text
|
from sqlalchemy import JSON, Boolean, DateTime, Float, ForeignKey, Integer, String, Text
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
@@ -28,13 +29,13 @@ class Node(Base):
|
|||||||
status: Mapped[str] = mapped_column(String, default="unknown")
|
status: Mapped[str] = mapped_column(String, default="unknown")
|
||||||
check_method: Mapped[str | None] = mapped_column(String)
|
check_method: Mapped[str | None] = mapped_column(String)
|
||||||
check_target: Mapped[str | None] = mapped_column(String)
|
check_target: Mapped[str | None] = mapped_column(String)
|
||||||
services: Mapped[list] = mapped_column(JSON, default=list)
|
services: Mapped[list[Any]] = mapped_column(JSON, default=list)
|
||||||
notes: Mapped[str | None] = mapped_column(Text)
|
notes: Mapped[str | None] = mapped_column(Text)
|
||||||
pos_x: Mapped[float] = mapped_column(Float, default=0)
|
pos_x: Mapped[float] = mapped_column(Float, default=0)
|
||||||
pos_y: Mapped[float] = mapped_column(Float, default=0)
|
pos_y: Mapped[float] = mapped_column(Float, default=0)
|
||||||
parent_id: Mapped[str | None] = mapped_column(String, ForeignKey("nodes.id"))
|
parent_id: Mapped[str | None] = mapped_column(String, ForeignKey("nodes.id"))
|
||||||
container_mode: Mapped[bool] = mapped_column(Boolean, default=False)
|
container_mode: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
custom_colors: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
custom_colors: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
||||||
last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
response_time_ms: Mapped[int | None] = mapped_column(Integer)
|
response_time_ms: Mapped[int | None] = mapped_column(Integer)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||||
@@ -63,7 +64,7 @@ class CanvasState(Base):
|
|||||||
__tablename__ = "canvas_state"
|
__tablename__ = "canvas_state"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
|
||||||
viewport: Mapped[dict] = mapped_column(JSON, default=dict)
|
viewport: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
||||||
saved_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
saved_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||||
|
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ class PendingDevice(Base):
|
|||||||
mac: Mapped[str | None] = mapped_column(String)
|
mac: Mapped[str | None] = mapped_column(String)
|
||||||
hostname: Mapped[str | None] = mapped_column(String)
|
hostname: Mapped[str | None] = mapped_column(String)
|
||||||
os: Mapped[str | None] = mapped_column(String)
|
os: Mapped[str | None] = mapped_column(String)
|
||||||
services: Mapped[list] = mapped_column(JSON, default=list)
|
services: Mapped[list[Any]] = mapped_column(JSON, default=list)
|
||||||
suggested_type: Mapped[str | None] = mapped_column(String)
|
suggested_type: Mapped[str | None] = mapped_column(String)
|
||||||
status: Mapped[str] = mapped_column(String, default="pending")
|
status: Mapped[str] = mapped_column(String, default="pending")
|
||||||
discovered_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
discovered_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||||
@@ -86,7 +87,7 @@ class ScanRun(Base):
|
|||||||
|
|
||||||
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
|
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
|
||||||
status: Mapped[str] = mapped_column(String, default="running")
|
status: Mapped[str] = mapped_column(String, default="running")
|
||||||
ranges: Mapped[list] = mapped_column(JSON, default=list)
|
ranges: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||||
devices_found: Mapped[int] = mapped_column(Integer, default=0)
|
devices_found: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
|
|||||||
+4
-2
@@ -1,4 +1,6 @@
|
|||||||
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
@@ -10,7 +12,7 @@ from app.db.database import init_db
|
|||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||||
await init_db()
|
await init_db()
|
||||||
start_scheduler()
|
start_scheduler()
|
||||||
yield
|
yield
|
||||||
@@ -40,5 +42,5 @@ app.include_router(status.router, prefix="/api/v1/status", tags=["status"])
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/health")
|
@app.get("/api/v1/health")
|
||||||
async def health():
|
async def health() -> dict[str, Any]:
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class NodeSave(BaseModel):
|
|||||||
notes: str | None = None
|
notes: str | None = None
|
||||||
parent_id: str | None = None
|
parent_id: str | None = None
|
||||||
container_mode: bool = False
|
container_mode: bool = False
|
||||||
custom_colors: dict | None = None
|
custom_colors: dict[str, Any] | None = None
|
||||||
pos_x: float = 0
|
pos_x: float = 0
|
||||||
pos_y: float = 0
|
pos_y: float = 0
|
||||||
|
|
||||||
@@ -41,10 +41,10 @@ class EdgeSave(BaseModel):
|
|||||||
class CanvasSaveRequest(BaseModel):
|
class CanvasSaveRequest(BaseModel):
|
||||||
nodes: list[NodeSave] = []
|
nodes: list[NodeSave] = []
|
||||||
edges: list[EdgeSave] = []
|
edges: list[EdgeSave] = []
|
||||||
viewport: dict = {}
|
viewport: dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
class CanvasStateResponse(BaseModel):
|
class CanvasStateResponse(BaseModel):
|
||||||
nodes: list[NodeResponse]
|
nodes: list[NodeResponse]
|
||||||
edges: list[EdgeResponse]
|
edges: list[EdgeResponse]
|
||||||
viewport: dict
|
viewport: dict[str, Any]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class NodeBase(BaseModel):
|
|||||||
pos_y: float = 0
|
pos_y: float = 0
|
||||||
parent_id: str | None = None
|
parent_id: str | None = None
|
||||||
container_mode: bool = False
|
container_mode: bool = False
|
||||||
custom_colors: dict | None = None
|
custom_colors: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
class NodeCreate(NodeBase):
|
class NodeCreate(NodeBase):
|
||||||
@@ -42,7 +42,7 @@ class NodeUpdate(BaseModel):
|
|||||||
pos_x: float | None = None
|
pos_x: float | None = None
|
||||||
pos_y: float | None = None
|
pos_y: float | None = None
|
||||||
container_mode: bool | None = None
|
container_mode: bool | None = None
|
||||||
custom_colors: dict | None = None
|
custom_colors: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
class NodeResponse(NodeBase):
|
class NodeResponse(NodeBase):
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
_SIGNATURES: list[dict] | None = None
|
_SIGNATURES: list[dict[str, Any]] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _load() -> list[dict]:
|
def _load() -> list[dict[str, Any]]:
|
||||||
global _SIGNATURES
|
global _SIGNATURES
|
||||||
if _SIGNATURES is None:
|
if _SIGNATURES is None:
|
||||||
path = Path(__file__).parent.parent.parent / "data" / "service_signatures.json"
|
path = Path(__file__).parent.parent.parent / "data" / "service_signatures.json"
|
||||||
@@ -15,7 +16,7 @@ def _load() -> list[dict]:
|
|||||||
return _SIGNATURES
|
return _SIGNATURES
|
||||||
|
|
||||||
|
|
||||||
def match_port(port: int, protocol: str, banner: str | None = None) -> dict | None:
|
def match_port(port: int, protocol: str, banner: str | None = None) -> dict[str, Any] | None:
|
||||||
"""Return the first signature matching port+protocol, optionally banner."""
|
"""Return the first signature matching port+protocol, optionally banner."""
|
||||||
for sig in _load():
|
for sig in _load():
|
||||||
if sig["port"] != port or sig["protocol"] != protocol:
|
if sig["port"] != port or sig["protocol"] != protocol:
|
||||||
@@ -26,7 +27,7 @@ def match_port(port: int, protocol: str, banner: str | None = None) -> dict | No
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def fingerprint_ports(open_ports: list[dict]) -> list[dict]:
|
def fingerprint_ports(open_ports: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Given a list of {port, protocol, banner?} dicts, return matched services.
|
Given a list of {port, protocol, banner?} dicts, return matched services.
|
||||||
Unknown ports are included as unknown_service.
|
Unknown ports are included as unknown_service.
|
||||||
@@ -53,7 +54,7 @@ def fingerprint_ports(open_ports: list[dict]) -> list[dict]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def suggest_node_type(open_ports: list[dict]) -> str:
|
def suggest_node_type(open_ports: list[dict[str, Any]]) -> str:
|
||||||
"""Suggest a node type based on the most specific matched signature."""
|
"""Suggest a node type based on the most specific matched signature."""
|
||||||
priority = ["proxmox", "nas", "router", "lxc", "vm", "server", "ap", "iot", "switch"]
|
priority = ["proxmox", "nas", "router", "lxc", "vm", "server", "ap", "iot", "switch"]
|
||||||
found: set[str] = set()
|
found: set[str] = set()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -11,14 +12,14 @@ from app.services.fingerprint import fingerprint_ports, suggest_node_type
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import nmap # type: ignore[import-untyped]
|
import nmap
|
||||||
_NMAP_AVAILABLE = True
|
_NMAP_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_NMAP_AVAILABLE = False
|
_NMAP_AVAILABLE = False
|
||||||
logger.warning("python-nmap not available — scanner will run in mock mode")
|
logger.warning("python-nmap not available — scanner will run in mock mode")
|
||||||
|
|
||||||
|
|
||||||
def _nmap_scan(target: str) -> list[dict]:
|
def _nmap_scan(target: str) -> list[dict[str, Any]]:
|
||||||
"""Run nmap -sV --open on target, return list of host dicts."""
|
"""Run nmap -sV --open on target, return list of host dicts."""
|
||||||
if not _NMAP_AVAILABLE:
|
if not _NMAP_AVAILABLE:
|
||||||
return _mock_scan(target)
|
return _mock_scan(target)
|
||||||
@@ -64,13 +65,13 @@ def _extract_os(nm: object, host: str) -> str | None:
|
|||||||
try:
|
try:
|
||||||
osmatch = nm[host].get("osmatch", []) # type: ignore[index]
|
osmatch = nm[host].get("osmatch", []) # type: ignore[index]
|
||||||
if osmatch:
|
if osmatch:
|
||||||
return osmatch[0]["name"]
|
return str(osmatch[0]["name"])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _mock_scan(target: str) -> list[dict]:
|
def _mock_scan(target: str) -> list[dict[str, Any]]:
|
||||||
"""Return fake results for dev/test environments without nmap."""
|
"""Return fake results for dev/test environments without nmap."""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def check_node(check_method: str, target: str | None, ip: str | None) -> dict:
|
async def check_node(check_method: str, target: str | None, ip: str | None) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Run the appropriate check and return {status, response_time_ms}.
|
Run the appropriate check and return {status, response_time_ms}.
|
||||||
status is one of: online, offline, unknown.
|
status is one of: online, offline, unknown.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ python-multipart==0.0.22
|
|||||||
apscheduler==3.10.4
|
apscheduler==3.10.4
|
||||||
python-nmap==0.7.1
|
python-nmap==0.7.1
|
||||||
pyyaml==6.0.2
|
pyyaml==6.0.2
|
||||||
|
types-PyYAML==6.0.12.20240917
|
||||||
websockets==13.1
|
websockets==13.1
|
||||||
httpx==0.27.2
|
httpx==0.27.2
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user