merge testing->tomerge/main #7
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()]
|
||||||
|
|||||||
@@ -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 ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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 ───────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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", ""):
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user