merge testing->tomerge/main #7

Open
anti wants to merge 242 commits from testing into tomerge/main
12 changed files with 27 additions and 26 deletions
Showing only changes of commit 29578d9d99 - Show all commits

View File

@@ -290,7 +290,7 @@ def deploy(
subprocess.Popen( # nosec B603 subprocess.Popen( # nosec B603
[sys.executable, "-m", "decnet.cli", "collect", "--log-file", str(effective_log_file)], [sys.executable, "-m", "decnet.cli", "collect", "--log-file", str(effective_log_file)],
stdin=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
stdout=open(_collector_err, "a"), # nosec B603 stdout=open(_collector_err, "a"),
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
start_new_session=True, start_new_session=True,
) )
@@ -781,7 +781,7 @@ def serve_web(
finally: finally:
try: try:
conn.close() conn.close()
except Exception: except Exception: # nosec B110 — best-effort conn cleanup
pass pass
def log_message(self, fmt: str, *args: object) -> None: def log_message(self, fmt: str, *args: object) -> None:
@@ -874,7 +874,7 @@ async def _db_reset_mysql_async(dsn: str, mode: str, confirm: bool) -> None:
async with engine.connect() as conn: async with engine.connect() as conn:
for tbl in _DB_RESET_TABLES: for tbl in _DB_RESET_TABLES:
try: try:
result = await conn.execute(text(f"SELECT COUNT(*) FROM `{tbl}`")) result = await conn.execute(text(f"SELECT COUNT(*) FROM `{tbl}`")) # nosec B608
rows[tbl] = result.scalar() or 0 rows[tbl] = result.scalar() or 0
except Exception: # noqa: BLE001 — ProgrammingError for missing table varies by driver except Exception: # noqa: BLE001 — ProgrammingError for missing table varies by driver
rows[tbl] = -1 rows[tbl] = -1

View File

@@ -215,7 +215,7 @@ def _reopen_if_needed(path: Path, fh: Optional[Any]) -> Any:
if fh is not None: if fh is not None:
try: try:
fh.close() fh.close()
except Exception: except Exception: # nosec B110 — best-effort file handle cleanup
pass pass
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
return open(path, "a", encoding="utf-8") return open(path, "a", encoding="utf-8")
@@ -272,7 +272,7 @@ def _stream_container(container_id: str, log_path: Path, json_path: Path) -> Non
if fh is not None: if fh is not None:
try: try:
fh.close() fh.close()
except Exception: except Exception: # nosec B110 — best-effort file handle cleanup
pass pass

View File

@@ -60,13 +60,13 @@ DECNET_SYSTEM_LOGS: str = os.environ.get("DECNET_SYSTEM_LOGS", "decnet.system.lo
DECNET_EMBED_PROFILER: bool = os.environ.get("DECNET_EMBED_PROFILER", "").lower() == "true" DECNET_EMBED_PROFILER: bool = os.environ.get("DECNET_EMBED_PROFILER", "").lower() == "true"
# API Options # API Options
DECNET_API_HOST: str = os.environ.get("DECNET_API_HOST", "0.0.0.0") # nosec B104 DECNET_API_HOST: str = os.environ.get("DECNET_API_HOST", "127.0.0.1")
DECNET_API_PORT: int = _port("DECNET_API_PORT", 8000) DECNET_API_PORT: int = _port("DECNET_API_PORT", 8000)
DECNET_JWT_SECRET: str = _require_env("DECNET_JWT_SECRET") DECNET_JWT_SECRET: str = _require_env("DECNET_JWT_SECRET")
DECNET_INGEST_LOG_FILE: str | None = os.environ.get("DECNET_INGEST_LOG_FILE", "/var/log/decnet/decnet.log") DECNET_INGEST_LOG_FILE: str | None = os.environ.get("DECNET_INGEST_LOG_FILE", "/var/log/decnet/decnet.log")
# Web Dashboard Options # Web Dashboard Options
DECNET_WEB_HOST: str = os.environ.get("DECNET_WEB_HOST", "0.0.0.0") # nosec B104 DECNET_WEB_HOST: str = os.environ.get("DECNET_WEB_HOST", "127.0.0.1")
DECNET_WEB_PORT: int = _port("DECNET_WEB_PORT", 8080) DECNET_WEB_PORT: int = _port("DECNET_WEB_PORT", 8080)
DECNET_ADMIN_USER: str = os.environ.get("DECNET_ADMIN_USER", "admin") DECNET_ADMIN_USER: str = os.environ.get("DECNET_ADMIN_USER", "admin")
DECNET_ADMIN_PASSWORD: str = os.environ.get("DECNET_ADMIN_PASSWORD", "admin") DECNET_ADMIN_PASSWORD: str = os.environ.get("DECNET_ADMIN_PASSWORD", "admin")
@@ -90,7 +90,8 @@ DECNET_DB_PASSWORD: Optional[str] = os.environ.get("DECNET_DB_PASSWORD")
# CORS — comma-separated list of allowed origins for the web dashboard API. # CORS — comma-separated list of allowed origins for the web dashboard API.
# Defaults to the configured web host/port. Override with DECNET_CORS_ORIGINS if needed. # Defaults to the configured web host/port. Override with DECNET_CORS_ORIGINS if needed.
# Example: DECNET_CORS_ORIGINS=http://192.168.1.50:9090,https://dashboard.example.com # Example: DECNET_CORS_ORIGINS=http://192.168.1.50:9090,https://dashboard.example.com
_web_hostname: str = "localhost" if DECNET_WEB_HOST in ("0.0.0.0", "127.0.0.1", "::") else DECNET_WEB_HOST # nosec B104 _WILDCARD_ADDRS = {"0.0.0.0", "127.0.0.1", "::"} # nosec B104 — comparison only, not a bind
_web_hostname: str = "localhost" if DECNET_WEB_HOST in _WILDCARD_ADDRS else DECNET_WEB_HOST
_cors_default: str = f"http://{_web_hostname}:{DECNET_WEB_PORT}" _cors_default: str = f"http://{_web_hostname}:{DECNET_WEB_PORT}"
_cors_raw: str = os.environ.get("DECNET_CORS_ORIGINS", _cors_default) _cors_raw: str = os.environ.get("DECNET_CORS_ORIGINS", _cors_default)
DECNET_CORS_ORIGINS: list[str] = [o.strip() for o in _cors_raw.split(",") if o.strip()] DECNET_CORS_ORIGINS: list[str] = [o.strip() for o in _cors_raw.split(",") if o.strip()]

View File

@@ -211,7 +211,7 @@ def _compute_hassh(kex: str, enc: str, mac: str, comp: str) -> str:
Returns 32-character lowercase hex digest. Returns 32-character lowercase hex digest.
""" """
raw = f"{kex};{enc};{mac};{comp}" raw = f"{kex};{enc};{mac};{comp}"
return hashlib.md5(raw.encode("utf-8")).hexdigest() # nosec B324 return hashlib.md5(raw.encode("utf-8"), usedforsecurity=False).hexdigest()
# ─── Public API ───────────────────────────────────────────────────────────── # ─── Public API ─────────────────────────────────────────────────────────────

View File

@@ -53,7 +53,7 @@ def _send_syn(
# Suppress scapy's noisy output # Suppress scapy's noisy output
conf.verb = 0 conf.verb = 0
src_port = random.randint(49152, 65535) src_port = random.randint(49152, 65535) # nosec B311 — ephemeral port, not crypto
pkt = ( pkt = (
IP(dst=host) IP(dst=host)
@@ -114,8 +114,8 @@ def _send_rst(
) )
) )
send(rst, verbose=0) send(rst, verbose=0)
except Exception: except Exception: # nosec B110 — best-effort RST cleanup
pass # Best-effort cleanup pass
# ─── Response parsing ─────────────────────────────────────────────────────── # ─── Response parsing ───────────────────────────────────────────────────────

View File

@@ -344,7 +344,7 @@ def detect_tools_from_headers(events: list[LogEvent]) -> list[str]:
headers = _parsed headers = _parsed
else: else:
continue continue
except Exception: except Exception: # nosec B112 — skip unparseable header values
continue continue
elif isinstance(raw_headers, dict): elif isinstance(raw_headers, dict):
headers = raw_headers headers = raw_headers

View File

@@ -513,7 +513,7 @@ def _extract_sans(cert_der: bytes, pos: int, end: int) -> list[str]:
else: else:
_, skip_start, skip_len = _der_read_tag_len(cert_der, pos) _, skip_start, skip_len = _der_read_tag_len(cert_der, pos)
pos = skip_start + skip_len pos = skip_start + skip_len
except Exception: except Exception: # nosec B110 — DER parse errors return partial results
pass pass
return sans return sans
@@ -533,7 +533,7 @@ def _parse_san_sequence(data: bytes, start: int, length: int) -> list[str]:
elif context_tag == 7 and val_len == 4: elif context_tag == 7 and val_len == 4:
names.append(".".join(str(b) for b in data[val_start: val_start + val_len])) names.append(".".join(str(b) for b in data[val_start: val_start + val_len]))
pos = val_start + val_len pos = val_start + val_len
except Exception: except Exception: # nosec B110 — SAN parse errors return partial results
pass pass
return names return names
@@ -561,7 +561,7 @@ def _ja3(ch: dict[str, Any]) -> tuple[str, str]:
"-".join(str(p) for p in ch["ec_point_formats"]), "-".join(str(p) for p in ch["ec_point_formats"]),
] ]
ja3_str = ",".join(parts) ja3_str = ",".join(parts)
return ja3_str, hashlib.md5(ja3_str.encode()).hexdigest() # nosec B324 return ja3_str, hashlib.md5(ja3_str.encode(), usedforsecurity=False).hexdigest()
@_traced("sniffer.ja3s") @_traced("sniffer.ja3s")
@@ -572,7 +572,7 @@ def _ja3s(sh: dict[str, Any]) -> tuple[str, str]:
"-".join(str(e) for e in sh["extensions"]), "-".join(str(e) for e in sh["extensions"]),
] ]
ja3s_str = ",".join(parts) ja3s_str = ",".join(parts)
return ja3s_str, hashlib.md5(ja3s_str.encode()).hexdigest() # nosec B324 return ja3s_str, hashlib.md5(ja3s_str.encode(), usedforsecurity=False).hexdigest()
# ─── JA4 / JA4S ───────────────────────────────────────────────────────────── # ─── JA4 / JA4S ─────────────────────────────────────────────────────────────

View File

@@ -12,7 +12,7 @@ The API never depends on this worker being alive.
import asyncio import asyncio
import os import os
import subprocess import subprocess # nosec B404 — needed for interface checks
import threading import threading
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from pathlib import Path from pathlib import Path
@@ -44,7 +44,7 @@ def _load_ip_to_decky() -> dict[str, str]:
def _interface_exists(iface: str) -> bool: def _interface_exists(iface: str) -> bool:
"""Check if a network interface exists on this host.""" """Check if a network interface exists on this host."""
try: try:
result = subprocess.run( result = subprocess.run( # nosec B603 B607 — hardcoded args
["ip", "link", "show", iface], ["ip", "link", "show", iface],
capture_output=True, text=True, check=False, capture_output=True, text=True, check=False,
) )

View File

@@ -12,7 +12,7 @@ from __future__ import annotations
import asyncio import asyncio
import functools import functools
import inspect import inspect
from typing import Any, Callable, Optional, TypeVar, overload from typing import Any, Callable, TypeVar, overload
from decnet.env import DECNET_DEVELOPER_TRACING, DECNET_OTEL_ENDPOINT from decnet.env import DECNET_DEVELOPER_TRACING, DECNET_OTEL_ENDPOINT
from decnet.logging import get_logger from decnet.logging import get_logger
@@ -76,7 +76,7 @@ def shutdown_tracing() -> None:
if _tracer_provider is not None: if _tracer_provider is not None:
try: try:
_tracer_provider.shutdown() _tracer_provider.shutdown()
except Exception: except Exception: # nosec B110 — best-effort tracer shutdown
pass pass
@@ -272,7 +272,7 @@ def inject_context(record: dict[str, Any]) -> None:
inject(carrier) inject(carrier)
if carrier: if carrier:
record["_trace"] = carrier record["_trace"] = carrier
except Exception: except Exception: # nosec B110 — trace injection is optional
pass pass

View File

@@ -3,14 +3,14 @@ from typing import Literal, Optional, Any, List, Annotated
from sqlalchemy import Column, Text from sqlalchemy import Column, Text
from sqlalchemy.dialects.mysql import MEDIUMTEXT from sqlalchemy.dialects.mysql import MEDIUMTEXT
from sqlmodel import SQLModel, Field from sqlmodel import SQLModel, Field
from pydantic import BaseModel, ConfigDict, Field as PydanticField, BeforeValidator
from decnet.models import IniContent
# Use on columns that accumulate over an attacker's lifetime (commands, # Use on columns that accumulate over an attacker's lifetime (commands,
# fingerprints, state blobs). TEXT on MySQL caps at 64 KiB; MEDIUMTEXT # fingerprints, state blobs). TEXT on MySQL caps at 64 KiB; MEDIUMTEXT
# stretches to 16 MiB. SQLite has no fixed-width text types so Text() # stretches to 16 MiB. SQLite has no fixed-width text types so Text()
# stays unchanged there. # stays unchanged there.
_BIG_TEXT = Text().with_variant(MEDIUMTEXT(), "mysql") _BIG_TEXT = Text().with_variant(MEDIUMTEXT(), "mysql")
from pydantic import BaseModel, ConfigDict, Field as PydanticField, BeforeValidator
from decnet.models import IniContent
def _normalize_null(v: Any) -> Any: def _normalize_null(v: Any) -> Any:
if isinstance(v, str) and v.lower() in ("null", "undefined", ""): if isinstance(v, str) and v.lower() in ("null", "undefined", ""):

View File

@@ -42,6 +42,6 @@ async def login(request: LoginRequest) -> dict[str, Any]:
) )
return { return {
"access_token": _access_token, "access_token": _access_token,
"token_type": "bearer", # nosec B105 "token_type": "bearer", # nosec B105 — OAuth2 token type, not a password
"must_change_password": bool(_user.get("must_change_password", False)) "must_change_password": bool(_user.get("must_change_password", False))
} }

View File

@@ -40,7 +40,7 @@ async def api_create_user(
"username": req.username, "username": req.username,
"password_hash": get_password_hash(req.password), "password_hash": get_password_hash(req.password),
"role": req.role, "role": req.role,
"must_change_password": True, "must_change_password": True, # nosec B105 — not a password
}) })
return UserResponse( return UserResponse(
uuid=user_uuid, uuid=user_uuid,