Clean Core security audit findings

This commit is contained in:
2026-07-21 03:18:07 +02:00
parent 7eef52776c
commit 1153c9dd36
10 changed files with 24 additions and 25 deletions

View File

@@ -201,7 +201,7 @@ class AuthGroupsResponse(BaseModel):
class LoginResponse(BaseModel): class LoginResponse(BaseModel):
access_token: str access_token: str
token_type: str = "bearer" token_type: str = "bearer" # noqa: S105 - OAuth token type, not a credential.
expires_at: datetime expires_at: datetime
user: UserInfo user: UserInfo
# Backwards-compatible alias for the active tenant. # Backwards-compatible alias for the active tenant.

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
"""Core auth dependency facade. """Core auth dependency facade.
Routers depend on this module instead of a concrete access-provider package. Routers depend on this module instead of a concrete access-provider package.
@@ -7,6 +5,8 @@ The active auth module provides the request principal through the platform
capability registry. capability registry.
""" """
from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from fastapi import Depends, Header, HTTPException, Request, status from fastapi import Depends, Header, HTTPException, Request, status

View File

@@ -1,8 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from collections.abc import Mapping from typing import Literal
from typing import Any, Literal
from govoplan_core.security.permissions import scopes_grant from govoplan_core.security.permissions import scopes_grant
from govoplan_core.security.redaction import contains_plain_secret from govoplan_core.security.redaction import contains_plain_secret
@@ -22,7 +21,7 @@ class ConfigurationFieldSafety:
storage: str storage: str
ui_managed: bool ui_managed: bool
risk: ConfigurationRisk risk: ConfigurationRisk
secret_handling: SecretHandling = "none" secret_handling: SecretHandling = "none" # noqa: S105 - policy vocabulary.
required_scopes: tuple[str, ...] = () required_scopes: tuple[str, ...] = ()
dry_run_required: bool = False dry_run_required: bool = False
validation_required: bool = True validation_required: bool = True
@@ -69,7 +68,7 @@ class ConfigurationChangeSafetyPlan:
maintenance_required: bool = False maintenance_required: bool = False
maintenance_satisfied: bool = False maintenance_satisfied: bool = False
rollback_history_required: bool = False rollback_history_required: bool = False
secret_handling: SecretHandling = "none" secret_handling: SecretHandling = "none" # noqa: S105 - policy vocabulary.
audit_event: str | None = None audit_event: str | None = None
policy_explanation: str | None = None policy_explanation: str | None = None
blockers: tuple[str, ...] = () blockers: tuple[str, ...] = ()
@@ -240,7 +239,7 @@ _CONFIGURATION_FIELD_SAFETY: tuple[ConfigurationFieldSafety, ...] = (
storage="module_settings", storage="module_settings",
ui_managed=True, ui_managed=True,
risk="high", risk="high",
secret_handling="reference_only", secret_handling="reference_only", # noqa: S106 # nosec B106 - policy vocabulary.
required_scopes=("mail_servers:manage_credentials",), required_scopes=("mail_servers:manage_credentials",),
validation_required=True, validation_required=True,
audit_event="mail_server_profile.credential_updated", audit_event="mail_server_profile.credential_updated",
@@ -256,7 +255,7 @@ _CONFIGURATION_FIELD_SAFETY: tuple[ConfigurationFieldSafety, ...] = (
storage="module_settings", storage="module_settings",
ui_managed=True, ui_managed=True,
risk="high", risk="high",
secret_handling="reference_only", secret_handling="reference_only", # noqa: S106 # nosec B106 - policy vocabulary.
required_scopes=("files:file:admin",), required_scopes=("files:file:admin",),
dry_run_required=True, dry_run_required=True,
policy_explanation_required=True, policy_explanation_required=True,
@@ -273,7 +272,7 @@ _CONFIGURATION_FIELD_SAFETY: tuple[ConfigurationFieldSafety, ...] = (
storage="environment", storage="environment",
ui_managed=False, ui_managed=False,
risk="destructive", risk="destructive",
secret_handling="env_only", secret_handling="env_only", # noqa: S106 # nosec B106 - policy vocabulary.
maintenance_required=True, maintenance_required=True,
notes="Database connectivity remains deployment-managed and must not be changed from the running UI.", notes="Database connectivity remains deployment-managed and must not be changed from the running UI.",
), ),
@@ -285,7 +284,7 @@ _CONFIGURATION_FIELD_SAFETY: tuple[ConfigurationFieldSafety, ...] = (
storage="environment", storage="environment",
ui_managed=False, ui_managed=False,
risk="destructive", risk="destructive",
secret_handling="env_only", secret_handling="env_only", # noqa: S106 # nosec B106 - policy vocabulary.
maintenance_required=True, maintenance_required=True,
two_person_approval_required=True, two_person_approval_required=True,
notes="Encryption roots remain out of band; UI may only report missing/rotated state.", notes="Encryption roots remain out of band; UI may only report missing/rotated state.",
@@ -298,7 +297,7 @@ _CONFIGURATION_FIELD_SAFETY: tuple[ConfigurationFieldSafety, ...] = (
storage="environment", storage="environment",
ui_managed=False, ui_managed=False,
risk="high", risk="high",
secret_handling="env_only", secret_handling="env_only", # noqa: S106 # nosec B106 - policy vocabulary.
notes="Trust roots are deployment-managed; UI can validate catalogs but should not edit key material.", notes="Trust roots are deployment-managed; UI can validate catalogs but should not edit key material.",
), ),
ConfigurationFieldSafety( ConfigurationFieldSafety(
@@ -309,7 +308,7 @@ _CONFIGURATION_FIELD_SAFETY: tuple[ConfigurationFieldSafety, ...] = (
storage="environment", storage="environment",
ui_managed=False, ui_managed=False,
risk="high", risk="high",
secret_handling="env_only", secret_handling="env_only", # noqa: S106 # nosec B106 - policy vocabulary.
notes="Configuration package trust roots are deployment-managed.", notes="Configuration package trust roots are deployment-managed.",
), ),
) )
@@ -414,9 +413,9 @@ def _configuration_change_safety_state(
approval_satisfied = not approval_required or approval_count >= 2 approval_satisfied = not approval_required or approval_count >= 2
if approval_required and not approval_satisfied: if approval_required and not approval_satisfied:
blockers.append("two_person_approval_required") blockers.append("two_person_approval_required")
if field.secret_handling == "reference_only" and _contains_plain_secret(value): if field.secret_handling == "reference_only" and _contains_plain_secret(value): # noqa: S105 # nosec B105 - policy vocabulary.
blockers.append("secret_reference_required") blockers.append("secret_reference_required")
if field.secret_handling == "env_only" and value is not None: if field.secret_handling == "env_only" and value is not None: # noqa: S105 # nosec B105 - policy vocabulary.
blockers.append("env_only_secret") blockers.append("env_only_secret")
if field.rollback_history_required: if field.rollback_history_required:
warnings.append("rollback_history_required") warnings.append("rollback_history_required")
@@ -466,7 +465,7 @@ def _policy_explanation(field: ConfigurationFieldSafety) -> str:
parts.append("requires two-person approval") parts.append("requires two-person approval")
if field.maintenance_required: if field.maintenance_required:
parts.append("requires maintenance mode") parts.append("requires maintenance mode")
if field.secret_handling != "none": if field.secret_handling != "none": # noqa: S105 # nosec B105 - policy vocabulary.
parts.append(f"uses {field.secret_handling} secret handling") parts.append(f"uses {field.secret_handling} secret handling")
return "; ".join(parts) + "." return "; ".join(parts) + "."

View File

@@ -106,7 +106,8 @@ def installer_notification_body(event_kind: str, request: Mapping[str, object])
run_id = _result_run_id(request) run_id = _result_run_id(request)
if run_id: if run_id:
return ". Run: ".join((sentence, run_id)) return ". Run: ".join((sentence, run_id))
return sentence + "." # This helper returns plain notification text, not an HTTP/HTML response.
return sentence + "." # nosemgrep: python.flask.security.audit.directly-returned-format-string.directly-returned-format-string
def installer_notification_priority(status: str) -> int: def installer_notification_priority(status: str) -> int:

View File

@@ -6,7 +6,6 @@ from datetime import UTC, datetime
import json import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Any
from cryptography.exceptions import InvalidSignature from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization

View File

@@ -65,7 +65,7 @@ def bootstrap_dev_data(
api_key_secret: str | None = None, api_key_secret: str | None = None,
tenant_slug: str = "default", tenant_slug: str = "default",
user_email: str = "admin@example.local", user_email: str = "admin@example.local",
user_password: str = "dev-admin", user_password: str = "dev-admin", # noqa: S107 - development bootstrap only.
) -> BootstrapResult: ) -> BootstrapResult:
tenant = session.query(Tenant).filter(Tenant.slug == tenant_slug).one_or_none() tenant = session.query(Tenant).filter(Tenant.slug == tenant_slug).one_or_none()
if tenant is None: if tenant is None:

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Iterable, Mapping
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
import json import json
import logging import logging
@@ -634,7 +634,7 @@ def _backfill_user_lock_state_for_create_all_schema(database_url: str) -> None:
def _row_count(connection, table_name: str) -> int: def _row_count(connection, table_name: str) -> int:
quoted = _quoted_table_name(connection, table_name) quoted = _quoted_table_name(connection, table_name)
statement = text(f"SELECT COUNT(*) FROM {quoted}") # nosec B608 # nosemgrep: python.sqlalchemy.security.audit.avoid-sqlalchemy-text.avoid-sqlalchemy-text statement = text(f"SELECT COUNT(*) FROM {quoted}") # noqa: S608 # nosec B608 # nosemgrep: python.sqlalchemy.security.audit.avoid-sqlalchemy-text.avoid-sqlalchemy-text
return int(connection.execute(statement).scalar_one()) return int(connection.execute(statement).scalar_one())

View File

@@ -41,7 +41,7 @@ def fetch_http(
method: str = "GET", method: str = "GET",
headers: Mapping[str, str] | None = None, headers: Mapping[str, str] | None = None,
) -> HttpFetchResponse: ) -> HttpFetchResponse:
request = urllib.request.Request( request = urllib.request.Request( # noqa: S310 - URL is restricted to validated HTTP(S).
validate_http_url(url, label=label), validate_http_url(url, label=label),
headers=dict(headers or {}), headers=dict(headers or {}),
method=method, method=method,

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import re import re
from collections.abc import Mapping from collections.abc import Mapping
from typing import Any
def sensitive_key_tokens(key: object) -> set[str]: def sensitive_key_tokens(key: object) -> set[str]:
value = str(key).strip() value = str(key).strip()
@@ -40,7 +40,7 @@ def contains_plain_secret(value: object) -> bool:
normalized_key = str(key).strip().casefold().replace("-", "_") normalized_key = str(key).strip().casefold().replace("-", "_")
if normalized_key in {"credential_ref", "secret_ref", "secret_reference"}: if normalized_key in {"credential_ref", "secret_ref", "secret_reference"}:
continue continue
if is_sensitive_key(key) and item not in (None, "", {"secret_ref": ""}): if is_sensitive_key(key) and item not in (None, "", {"secret_ref": ""}): # nosec B105 - empty redaction sentinels.
return True return True
if isinstance(item, Mapping) and contains_plain_secret(item): if isinstance(item, Mapping) and contains_plain_secret(item):
return True return True

View File

@@ -18,7 +18,7 @@ class SecretDecryptionError(RuntimeError):
pass pass
CAPABILITY_SECURITY_SECRET_PROVIDER = "security.secretProvider" CAPABILITY_SECURITY_SECRET_PROVIDER = "security.secretProvider" # noqa: S105 # nosec B105 - capability identifier.
@runtime_checkable @runtime_checkable