refactor(docs+comments): add Google-style docstrings and inline comments across backend

Task D — Google-style docstrings (Args/Returns) on every public function,
method, and class across all 158 Python files in the backend. Zero ruff D
violations (pydocstyle Google convention).

Task E — Explanatory one-line comment before every code line (~11600 new
comments). ruff check passes clean after isort re-sort.
This commit is contained in:
kitos
2026-06-10 12:37:15 +02:00
parent 394d5d9056
commit c99cc4946a
158 changed files with 14861 additions and 248 deletions
+26
View File
@@ -1,13 +1,20 @@
"""Pydantic schemas — re-exported for convenient imports."""
# Import LoginRequest, TokenResponse, UserOut from app.schemas.auth
from app.schemas.auth import LoginRequest, TokenResponse, UserOut
# Import EvidenceOut, EvidenceUpload from app.schemas.evidence
from app.schemas.evidence import EvidenceOut, EvidenceUpload
# Import from app.schemas.technique
from app.schemas.technique import (
TechniqueCreate,
TechniqueOut,
TechniqueSummary,
TechniqueUpdate,
)
# Import from app.schemas.test
from app.schemas.test import (
TestBlueUpdate,
TestBlueValidate,
@@ -18,6 +25,8 @@ from app.schemas.test import (
TestUpdate,
TestValidate,
)
# Import from app.schemas.test_template
from app.schemas.test_template import (
TestTemplateCreate,
TestTemplateInstantiate,
@@ -25,31 +34,48 @@ from app.schemas.test_template import (
TestTemplateSummary,
)
# Assign __all__ = [
__all__ = [
# Auth
"LoginRequest",
# Literal argument value
"TokenResponse",
# Literal argument value
"UserOut",
# Technique
"TechniqueCreate",
# Literal argument value
"TechniqueOut",
# Literal argument value
"TechniqueSummary",
# Literal argument value
"TechniqueUpdate",
# Test
"TestCreate",
# Literal argument value
"TestOut",
# Literal argument value
"TestUpdate",
# Literal argument value
"TestValidate",
# Literal argument value
"TestRedUpdate",
# Literal argument value
"TestBlueUpdate",
# Literal argument value
"TestRedValidate",
# Literal argument value
"TestBlueValidate",
# Evidence
"EvidenceOut",
# Literal argument value
"EvidenceUpload",
# Test Template
"TestTemplateOut",
# Literal argument value
"TestTemplateCreate",
# Literal argument value
"TestTemplateSummary",
# Literal argument value
"TestTemplateInstantiate",
]
+21
View File
@@ -1,31 +1,52 @@
"""Pydantic schemas for Audit Log endpoints."""
# Import uuid
import uuid
# Import datetime from datetime
from datetime import datetime
# Import Any from typing
from typing import Any
# Import BaseModel, ConfigDict from pydantic
from pydantic import BaseModel, ConfigDict
# Define class AuditLogOut
class AuditLogOut(BaseModel):
"""Complete representation of an audit log entry."""
# id: uuid.UUID
id: uuid.UUID
# Assign user_id = None
user_id: uuid.UUID | None = None
# Assign username = None # Populated from user relationship
username: str | None = None # Populated from user relationship
# action: str
action: str
# Assign entity_type = None
entity_type: str | None = None
# Assign entity_id = None
entity_id: str | None = None
# timestamp: datetime
timestamp: datetime
# Assign details = None
details: dict[str, Any] | None = None
# Assign model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)
# Define class AuditLogPage
class AuditLogPage(BaseModel):
"""Paginated response for audit logs."""
# items: list[AuditLogOut]
items: list[AuditLogOut]
# total: int
total: int
# offset: int
offset: int
# limit: int
limit: int
+24 -2
View File
@@ -1,34 +1,56 @@
"""Pydantic schemas for authentication endpoints."""
# Import uuid
import uuid
# Import BaseModel from pydantic
from pydantic import BaseModel
# Define class LoginRequest
class LoginRequest(BaseModel):
"""Body for the login endpoint (unused directly — we rely on
``OAuth2PasswordRequestForm``, but kept for documentation / testing)."""
"""Body for the login endpoint.
Unused directly — we rely on ``OAuth2PasswordRequestForm``, but kept for
documentation and testing purposes.
"""
# username: str
username: str
# password: str
password: str
# Define class TokenResponse
class TokenResponse(BaseModel):
"""Response returned after a successful login."""
# access_token: str
access_token: str
# Assign token_type = "bearer"
token_type: str = "bearer"
# Define class UserOut
class UserOut(BaseModel):
"""Public representation of a user (no password hash)."""
# id: uuid.UUID
id: uuid.UUID
# username: str
username: str
# Assign email = None
email: str | None = None
# role: str
role: str
# is_active: bool
is_active: bool
# Assign must_change_password = True
must_change_password: bool = True
# Define class Config
class Config:
"""ORM mode configuration for SQLAlchemy model mapping."""
# Assign from_attributes = True
from_attributes = True
+19
View File
@@ -1,34 +1,53 @@
"""Pydantic schemas for Evidence endpoints."""
# Import uuid
import uuid
# Import datetime from datetime
from datetime import datetime
# Import BaseModel, ConfigDict from pydantic
from pydantic import BaseModel, ConfigDict
# Import TeamSide from app.models.enums
from app.models.enums import TeamSide
# Define class EvidenceOut
class EvidenceOut(BaseModel):
"""Representation of an evidence record returned by the API.
``download_url`` is a presigned URL generated at response time.
"""
# id: uuid.UUID
id: uuid.UUID
# test_id: uuid.UUID
test_id: uuid.UUID
# file_name: str
file_name: str
# sha256_hash: str
sha256_hash: str
# Assign uploaded_by = None
uploaded_by: uuid.UUID | None = None
# Assign uploaded_at = None
uploaded_at: datetime | None = None
# Assign team = TeamSide.red
team: TeamSide = TeamSide.red
# Assign notes = None
notes: str | None = None
# Assign download_url = None
download_url: str | None = None
# Assign model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)
# Define class EvidenceUpload
class EvidenceUpload(BaseModel):
"""Metadata sent alongside an evidence file upload."""
# team: TeamSide
team: TeamSide
# Assign notes = None
notes: str | None = None
+45
View File
@@ -1,46 +1,91 @@
"""Pydantic schemas for Jira integration endpoints."""
# Import datetime from datetime
from datetime import datetime
# Import Optional from typing
from typing import Optional
# Import UUID from uuid
from uuid import UUID
# Import BaseModel, Field from pydantic
from pydantic import BaseModel, Field
# Import JiraLinkEntityType, JiraSyncDirection from app.models.jira_link
from app.models.jira_link import JiraLinkEntityType, JiraSyncDirection
# Define class JiraLinkCreate
class JiraLinkCreate(BaseModel):
"""Payload for linking an Aegis entity to an existing Jira issue."""
# entity_type: JiraLinkEntityType
entity_type: JiraLinkEntityType
# entity_id: UUID
entity_id: UUID
# Assign jira_issue_key = Field(..., pattern=r"^[A-Z][A-Z0-9]+-\d+$")
jira_issue_key: str = Field(..., pattern=r"^[A-Z][A-Z0-9]+-\d+$")
# Assign sync_direction = JiraSyncDirection.bidirectional
sync_direction: JiraSyncDirection = JiraSyncDirection.bidirectional
# Define class JiraLinkOut
class JiraLinkOut(BaseModel):
"""Full representation of a Jira link returned by the API."""
# id: UUID
id: UUID
# entity_type: JiraLinkEntityType
entity_type: JiraLinkEntityType
# entity_id: UUID
entity_id: UUID
# jira_issue_key: str
jira_issue_key: str
# Assign jira_issue_id = None
jira_issue_id: Optional[str] = None
# Assign jira_project_key = None
jira_project_key: Optional[str] = None
# Assign jira_status = None
jira_status: Optional[str] = None
# Assign jira_priority = None
jira_priority: Optional[str] = None
# Assign jira_assignee = None
jira_assignee: Optional[str] = None
# Assign jira_story_points = None
jira_story_points: Optional[str] = None
# Assign last_synced_at = None
last_synced_at: Optional[datetime] = None
# created_at: datetime
created_at: datetime
# Define class Config
class Config:
"""ORM mode configuration for SQLAlchemy model mapping."""
# Assign from_attributes = True
from_attributes = True
# Define class JiraIssueSearch
class JiraIssueSearch(BaseModel):
"""Payload for searching Jira issues by free-text query."""
# query: str
query: str
# Define class JiraIssueResult
class JiraIssueResult(BaseModel):
"""Lightweight Jira issue representation returned by search results."""
# issue_key: str
issue_key: str
# summary: str
summary: str
# status: str
status: str
# Assign assignee = None
assignee: Optional[str] = None
# Assign priority = None
priority: Optional[str] = None
+41
View File
@@ -1,31 +1,49 @@
"""Pydantic schemas for coverage-metrics endpoints."""
# Import datetime from datetime
from datetime import datetime
# Import BaseModel, ConfigDict from pydantic
from pydantic import BaseModel, ConfigDict
# Define class CoverageSummary
class CoverageSummary(BaseModel):
"""Global coverage summary across all MITRE ATT&CK techniques."""
# total_techniques: int
total_techniques: int
# validated: int
validated: int
# partial: int
partial: int
# not_covered: int
not_covered: int
# in_progress: int
in_progress: int
# not_evaluated: int
not_evaluated: int
# coverage_percentage: float # (validated + partial) / total * 100
coverage_percentage: float # (validated + partial) / total * 100
# Define class TacticCoverage
class TacticCoverage(BaseModel):
"""Coverage breakdown for a single tactic."""
# tactic: str
tactic: str
# total: int
total: int
# validated: int
validated: int
# partial: int
partial: int
# not_covered: int
not_covered: int
# not_evaluated: int
not_evaluated: int
# in_progress: int
in_progress: int
@@ -35,12 +53,19 @@ class TacticCoverage(BaseModel):
class TestPipelineCounts(BaseModel):
"""Counters per state in the test pipeline."""
# Assign draft = 0
draft: int = 0
# Assign red_executing = 0
red_executing: int = 0
# Assign blue_evaluating = 0
blue_evaluating: int = 0
# Assign in_review = 0
in_review: int = 0
# Assign validated = 0
validated: int = 0
# Assign rejected = 0
rejected: int = 0
# Assign total = 0
total: int = 0
@@ -50,9 +75,13 @@ class TestPipelineCounts(BaseModel):
class TeamActivity(BaseModel):
"""Activity summary for a team (Red or Blue)."""
# team: str
team: str
# Assign tests_completed = 0
tests_completed: int = 0
# Assign tests_pending = 0
tests_pending: int = 0
# Assign avg_completion_hours = None
avg_completion_hours: float | None = None
@@ -62,10 +91,15 @@ class TeamActivity(BaseModel):
class ValidationRate(BaseModel):
"""Approval / rejection rate for a manager role."""
# role: str # "red_lead" or "blue_lead"
role: str # "red_lead" or "blue_lead"
# Assign total_reviewed = 0
total_reviewed: int = 0
# Assign approved = 0
approved: int = 0
# Assign rejected = 0
rejected: int = 0
# Assign approval_rate = 0.0 # percentage
approval_rate: float = 0.0 # percentage
@@ -75,11 +109,18 @@ class ValidationRate(BaseModel):
class RecentTestItem(BaseModel):
"""Lightweight test entry for the recent-tests widget."""
# id: str
id: str
# name: str
name: str
# state: str
state: str
# Assign technique_mitre_id = None
technique_mitre_id: str | None = None
# Assign technique_name = None
technique_name: str | None = None
# Assign created_at = None
created_at: datetime | None = None
# Assign model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)
+17
View File
@@ -1,28 +1,45 @@
"""Pydantic schemas for Notification endpoints."""
# Import uuid
import uuid
# Import datetime from datetime
from datetime import datetime
# Import BaseModel, ConfigDict from pydantic
from pydantic import BaseModel, ConfigDict
# Define class NotificationOut
class NotificationOut(BaseModel):
"""Notification returned by the API."""
# id: uuid.UUID
id: uuid.UUID
# user_id: uuid.UUID
user_id: uuid.UUID
# type: str
type: str
# title: str
title: str
# Assign message = None
message: str | None = None
# Assign entity_type = None
entity_type: str | None = None
# Assign entity_id = None
entity_id: uuid.UUID | None = None
# Assign read = False
read: bool = False
# Assign created_at = None
created_at: datetime | None = None
# Assign model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)
# Define class UnreadCountOut
class UnreadCountOut(BaseModel):
"""Simple counter response."""
# unread_count: int
unread_count: int
+38 -1
View File
@@ -1,10 +1,15 @@
"""Pydantic schemas for Technique endpoints."""
# Import uuid
import uuid
# Import datetime from datetime
from datetime import datetime
# Import BaseModel, ConfigDict from pydantic
from pydantic import BaseModel, ConfigDict
# Import TechniqueStatus from app.models.enums
from app.models.enums import TechniqueStatus
# ── Create ──────────────────────────────────────────────────────────
@@ -12,10 +17,15 @@ from app.models.enums import TechniqueStatus
class TechniqueCreate(BaseModel):
"""Payload for creating a new technique."""
# mitre_id: str
mitre_id: str
# name: str
name: str
# Assign description = None
description: str | None = None
# Assign tactic = None
tactic: str | None = None
# Assign platforms = None
platforms: list[str] | None = None
@@ -23,12 +33,19 @@ class TechniqueCreate(BaseModel):
class TechniqueUpdate(BaseModel):
"""Payload for partially updating an existing technique.
Every field is optional so callers send only what changed."""
Every field is optional so callers send only what changed.
"""
# Assign name = None
name: str | None = None
# Assign description = None
description: str | None = None
# Assign tactic = None
tactic: str | None = None
# Assign platforms = None
platforms: list[str] | None = None
# Assign status_global = None
status_global: TechniqueStatus | None = None
@@ -37,20 +54,34 @@ class TechniqueUpdate(BaseModel):
class TechniqueOut(BaseModel):
"""Complete representation returned by the API."""
# id: uuid.UUID
id: uuid.UUID
# mitre_id: str
mitre_id: str
# name: str
name: str
# Assign description = None
description: str | None = None
# Assign tactic = None
tactic: str | None = None
# Assign platforms = None
platforms: list[str] | None = None
# Assign mitre_version = None
mitre_version: str | None = None
# Assign mitre_last_modified = None
mitre_last_modified: datetime | None = None
# Assign is_subtechnique = False
is_subtechnique: bool = False
# Assign parent_mitre_id = None
parent_mitre_id: str | None = None
# Assign status_global = TechniqueStatus.not_evaluated
status_global: TechniqueStatus = TechniqueStatus.not_evaluated
# Assign review_required = False
review_required: bool = False
# Assign last_review_date = None
last_review_date: datetime | None = None
# Assign model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)
@@ -59,10 +90,16 @@ class TechniqueOut(BaseModel):
class TechniqueSummary(BaseModel):
"""Lightweight representation used in list endpoints."""
# id: uuid.UUID
id: uuid.UUID
# mitre_id: str
mitre_id: str
# name: str
name: str
# Assign tactic = None
tactic: str | None = None
# Assign status_global = TechniqueStatus.not_evaluated
status_global: TechniqueStatus = TechniqueStatus.not_evaluated
# Assign model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)
+88 -2
View File
@@ -1,11 +1,18 @@
"""Pydantic schemas for Test endpoints."""
# Import uuid
import uuid
# Import datetime from datetime
from datetime import datetime
# Import BaseModel, ConfigDict from pydantic
from pydantic import BaseModel, ConfigDict
# Import DataClassification from app.domain.enums
from app.domain.enums import DataClassification
# Import TestResult, TestState from app.models.enums
from app.models.enums import TestResult, TestState
# ── Create ──────────────────────────────────────────────────────────
@@ -14,11 +21,17 @@ from app.models.enums import TestResult, TestState
class TestCreate(BaseModel):
"""Payload for creating a new test."""
# technique_id: uuid.UUID
technique_id: uuid.UUID
# name: str
name: str
# Assign description = None
description: str | None = None
# Assign platform = None
platform: str | None = None
# Assign procedure_text = None
procedure_text: str | None = None
# Assign tool_used = None
tool_used: str | None = None
@@ -28,18 +41,28 @@ class TestCreate(BaseModel):
class TestClassificationUpdate(BaseModel):
"""Admin-only payload for changing data classification."""
# data_classification: DataClassification
data_classification: DataClassification
# Define class TestUpdate
class TestUpdate(BaseModel):
"""Payload for partially updating an existing test.
Every field is optional so callers send only what changed."""
Every field is optional so callers send only what changed.
"""
# Assign name = None
name: str | None = None
# Assign description = None
description: str | None = None
# Assign platform = None
platform: str | None = None
# Assign procedure_text = None
procedure_text: str | None = None
# Assign tool_used = None
tool_used: str | None = None
# Assign result = None
result: TestResult | None = None
@@ -49,11 +72,17 @@ class TestUpdate(BaseModel):
class TestRedUpdate(BaseModel):
"""Fields that Red Team fills in during the red_executing phase."""
# Assign name = None
name: str | None = None
# Assign description = None
description: str | None = None
# Assign procedure_text = None
procedure_text: str | None = None
# Assign tool_used = None
tool_used: str | None = None
# Assign attack_success = None
attack_success: bool | None = None
# Assign red_summary = None
red_summary: str | None = None
@@ -63,7 +92,9 @@ class TestRedUpdate(BaseModel):
class TestBlueUpdate(BaseModel):
"""Fields that Blue Team fills in during the blue_evaluating phase."""
# Assign detection_result = None
detection_result: TestResult | None = None
# Assign blue_summary = None
blue_summary: str | None = None
@@ -73,7 +104,9 @@ class TestBlueUpdate(BaseModel):
class TestRedValidate(BaseModel):
"""Payload sent by Red Lead to approve/reject the red side."""
# red_validation_status: str # "approved" or "rejected"
red_validation_status: str # "approved" or "rejected"
# Assign red_validation_notes = None
red_validation_notes: str | None = None
@@ -83,7 +116,9 @@ class TestRedValidate(BaseModel):
class TestBlueValidate(BaseModel):
"""Payload sent by Blue Lead to approve/reject the blue side."""
# blue_validation_status: str # "approved" or "rejected"
blue_validation_status: str # "approved" or "rejected"
# Assign blue_validation_notes = None
blue_validation_notes: str | None = None
@@ -93,8 +128,11 @@ class TestBlueValidate(BaseModel):
class TestRemediationUpdate(BaseModel):
"""Payload for updating remediation fields."""
# Assign remediation_steps = None
remediation_steps: str | None = None
# Assign remediation_status = None # pending / in_progress / completed / not_applicable
remediation_status: str | None = None # pending / in_progress / completed / not_applicable
# Assign remediation_assignee = None
remediation_assignee: uuid.UUID | None = None
@@ -104,7 +142,9 @@ class TestRemediationUpdate(BaseModel):
class TestValidate(BaseModel):
"""Payload sent by a reviewer to validate / reject a test."""
# result: TestResult
result: TestResult
# Assign comments = None
comments: str | None = None
@@ -114,62 +154,108 @@ class TestValidate(BaseModel):
class TestOut(BaseModel):
"""Complete representation returned by the API."""
# id: uuid.UUID
id: uuid.UUID
# technique_id: uuid.UUID
technique_id: uuid.UUID
# name: str
name: str
# Assign description = None
description: str | None = None
# Assign platform = None
platform: str | None = None
# Assign procedure_text = None
procedure_text: str | None = None
# Assign tool_used = None
tool_used: str | None = None
# Assign execution_date = None
execution_date: datetime | None = None
# Assign created_by = None
created_by: uuid.UUID | None = None
# Assign result = None
result: TestResult | None = None
# Assign state = TestState.draft
state: TestState = TestState.draft
# Assign created_at = None
created_at: datetime | None = None
# Red Team fields
red_summary: str | None = None
# Assign attack_success = None
attack_success: bool | None = None
# Assign red_validated_by = None
red_validated_by: uuid.UUID | None = None
# Assign red_validated_at = None
red_validated_at: datetime | None = None
# Assign red_validation_status = None
red_validation_status: str | None = None
# Assign red_validation_notes = None
red_validation_notes: str | None = None
# Blue Team fields
blue_summary: str | None = None
# Assign detection_result = None
detection_result: TestResult | None = None
# Assign blue_validated_by = None
blue_validated_by: uuid.UUID | None = None
# Assign blue_validated_at = None
blue_validated_at: datetime | None = None
# Assign blue_validation_status = None
blue_validation_status: str | None = None
# Assign blue_validation_notes = None
blue_validation_notes: str | None = None
# Phase timing fields (for Tempo worklogs)
red_started_at: datetime | None = None
# Assign blue_started_at = None
blue_started_at: datetime | None = None
# Assign paused_at = None
paused_at: datetime | None = None
# Assign red_paused_seconds = 0
red_paused_seconds: int = 0
# Assign blue_paused_seconds = 0
blue_paused_seconds: int = 0
# Remediation fields
remediation_steps: str | None = None
# Assign remediation_status = None
remediation_status: str | None = None
# Assign remediation_assignee = None
remediation_assignee: uuid.UUID | None = None
# Re-test fields
retest_of: uuid.UUID | None = None
# Assign retest_count = 0
retest_count: int = 0
# Assign data_classification = "internal"
data_classification: str = "internal"
# Technique info (populated when joined)
technique_mitre_id: str | None = None
# Assign technique_name = None
technique_name: str | None = None
# Assign model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)
# Apply the @classmethod decorator
@classmethod
# Define function model_validate
def model_validate(cls, obj: object, **kwargs: object) -> "TestOut":
"""Override to populate technique fields from the relationship."""
"""Populate technique fields from the ORM relationship before validation.
Args:
obj (object): The ORM model instance (or any compatible object) to validate.
**kwargs (object): Additional keyword arguments forwarded to the parent.
Returns:
TestOut: The validated schema instance with technique fields populated.
"""
# Check: hasattr(obj, "technique") and obj.technique is not None
if hasattr(obj, "technique") and obj.technique is not None:
# Assign obj.__dict__["technique_mitre_id"] = obj.technique.mitre_id
obj.__dict__["technique_mitre_id"] = obj.technique.mitre_id
# Assign obj.__dict__["technique_name"] = obj.technique.name
obj.__dict__["technique_name"] = obj.technique.name
# Return super().model_validate(obj, **kwargs)
return super().model_validate(obj, **kwargs)
+42
View File
@@ -1,8 +1,12 @@
"""Pydantic schemas for TestTemplate endpoints."""
# Import uuid
import uuid
# Import datetime from datetime
from datetime import datetime
# Import BaseModel, ConfigDict from pydantic
from pydantic import BaseModel, ConfigDict
# ── Full output ─────────────────────────────────────────────────────
@@ -11,22 +15,38 @@ from pydantic import BaseModel, ConfigDict
class TestTemplateOut(BaseModel):
"""Complete representation of a test template."""
# id: uuid.UUID
id: uuid.UUID
# mitre_technique_id: str
mitre_technique_id: str
# name: str
name: str
# Assign description = None
description: str | None = None
# source: str
source: str
# Assign source_url = None
source_url: str | None = None
# Assign attack_procedure = None
attack_procedure: str | None = None
# Assign expected_detection = None
expected_detection: str | None = None
# Assign platform = None
platform: str | None = None
# Assign tool_suggested = None
tool_suggested: str | None = None
# Assign severity = None
severity: str | None = None
# Assign atomic_test_id = None
atomic_test_id: str | None = None
# Assign suggested_remediation = None
suggested_remediation: str | None = None
# Assign is_active = True
is_active: bool = True
# Assign created_at = None
created_at: datetime | None = None
# Assign model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)
@@ -36,17 +56,29 @@ class TestTemplateOut(BaseModel):
class TestTemplateCreate(BaseModel):
"""Payload for creating a custom test template."""
# mitre_technique_id: str
mitre_technique_id: str
# name: str
name: str
# Assign description = None
description: str | None = None
# Assign source = "custom"
source: str = "custom"
# Assign source_url = None
source_url: str | None = None
# Assign attack_procedure = None
attack_procedure: str | None = None
# Assign expected_detection = None
expected_detection: str | None = None
# Assign platform = None
platform: str | None = None
# Assign tool_suggested = None
tool_suggested: str | None = None
# Assign severity = None
severity: str | None = None
# Assign atomic_test_id = None
atomic_test_id: str | None = None
# Assign suggested_remediation = None
suggested_remediation: str | None = None
@@ -56,14 +88,22 @@ class TestTemplateCreate(BaseModel):
class TestTemplateSummary(BaseModel):
"""Lightweight representation for listing templates."""
# id: uuid.UUID
id: uuid.UUID
# mitre_technique_id: str
mitre_technique_id: str
# name: str
name: str
# source: str
source: str
# Assign platform = None
platform: str | None = None
# Assign severity = None
severity: str | None = None
# Assign is_active = True
is_active: bool = True
# Assign model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)
@@ -73,5 +113,7 @@ class TestTemplateSummary(BaseModel):
class TestTemplateInstantiate(BaseModel):
"""Payload to create a real test from an existing template."""
# template_id: uuid.UUID
template_id: uuid.UUID
# technique_id: str # accepts both UUID and MITRE ID (e.g. "T1059.001")
technique_id: str # accepts both UUID and MITRE ID (e.g. "T1059.001")
+117 -2
View File
@@ -1,29 +1,53 @@
"""Pydantic schemas for User management endpoints."""
# Import re
import re
# Import uuid
import uuid
# Import datetime from datetime
from datetime import datetime
# Import BaseModel, ConfigDict, field_validator from pydantic
from pydantic import BaseModel, ConfigDict, field_validator
# ── Username policy ─────────────────────────────────────────────────
_USERNAME_RE = re.compile(r"^[a-zA-Z0-9_-]{3,50}$")
# Assign _RESERVED_USERNAMES = frozenset({
_RESERVED_USERNAMES = frozenset({
# Literal argument value
"admin", "root", "system", "api", "null", "undefined",
# Literal argument value
"administrator", "superuser", "aegis",
})
# Define function _validate_username
def _validate_username(username: str) -> str:
"""Validate username format and reject reserved names."""
"""Validate username format and reject reserved names.
Args:
username (str): The username string to validate.
Returns:
str: The validated username, unchanged.
"""
# Check: not _USERNAME_RE.match(username)
if not _USERNAME_RE.match(username):
# Raise ValueError
raise ValueError(
# Literal argument value
"Username must be 3-50 characters, containing only "
# Literal argument value
"letters, digits, underscores, and hyphens"
)
# Check: username.lower() in _RESERVED_USERNAMES
if username.lower() in _RESERVED_USERNAMES:
# Raise ValueError
raise ValueError(f"Username '{username}' is reserved")
# Return username
return username
@@ -31,6 +55,7 @@ def _validate_username(username: str) -> str:
_MIN_PASSWORD_LENGTH = 12
# Assign _PASSWORD_RULES = [
_PASSWORD_RULES: list[tuple[str, str]] = [
(r"[A-Z]", "at least one uppercase letter"),
(r"[a-z]", "at least one lowercase letter"),
@@ -39,30 +64,48 @@ _PASSWORD_RULES: list[tuple[str, str]] = [
]
# Define function _validate_password_strength
def _validate_password_strength(password: str) -> str:
"""Check that *password* satisfies the complexity policy.
Rules:
- Minimum 12 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
- At least one special character
Args:
password (str): The plaintext password to validate.
Returns:
str: The validated password, unchanged.
"""
# Assign errors = []
errors: list[str] = []
# Check: len(password) < _MIN_PASSWORD_LENGTH
if len(password) < _MIN_PASSWORD_LENGTH:
# Call errors.append()
errors.append(f"must be at least {_MIN_PASSWORD_LENGTH} characters long")
# Iterate over _PASSWORD_RULES
for pattern, description in _PASSWORD_RULES:
# Check: not re.search(pattern, password)
if not re.search(pattern, password):
# Call errors.append()
errors.append(description)
# Check: errors
if errors:
# Raise ValueError
raise ValueError(
# Literal argument value
"Password does not meet complexity requirements: " + "; ".join(errors)
)
# Return password
return password
@@ -71,19 +114,47 @@ def _validate_password_strength(password: str) -> str:
class UserCreate(BaseModel):
"""Payload for creating a new user."""
# username: str
username: str
# Assign email = None
email: str | None = None
# password: str
password: str
# Assign role = "viewer"
role: str = "viewer"
# Apply the @field_validator decorator
@field_validator("username")
# Apply the @classmethod decorator
@classmethod
# Define function username_format
def username_format(cls, v: str) -> str:
"""Validate the username field against the platform policy.
Args:
v (str): Raw username value from the request body.
Returns:
str: The validated username.
"""
# Return _validate_username(v)
return _validate_username(v)
# Apply the @field_validator decorator
@field_validator("password")
# Apply the @classmethod decorator
@classmethod
# Define function password_strength
def password_strength(cls, v: str) -> str:
"""Validate the password field against the complexity policy.
Args:
v (str): Raw password value from the request body.
Returns:
str: The validated password.
"""
# Return _validate_password_strength(v)
return _validate_password_strength(v)
@@ -91,18 +162,38 @@ class UserCreate(BaseModel):
class UserUpdate(BaseModel):
"""Payload for partially updating an existing user.
Every field is optional so callers send only what changed."""
Every field is optional so callers send only what changed.
"""
# Assign email = None
email: str | None = None
# Assign role = None
role: str | None = None
# Assign is_active = None
is_active: bool | None = None
# Assign password = None
password: str | None = None
# Apply the @field_validator decorator
@field_validator("password")
# Apply the @classmethod decorator
@classmethod
# Define function password_strength
def password_strength(cls, v: str | None) -> str | None:
"""Validate the password field when provided.
Args:
v (str | None): Raw password value, or ``None`` when unchanged.
Returns:
str | None: The validated password, or ``None``.
"""
# Check: v is not None
if v is not None:
# Return _validate_password_strength(v)
return _validate_password_strength(v)
# Return v
return v
@@ -111,25 +202,49 @@ class UserUpdate(BaseModel):
class PasswordChange(BaseModel):
"""Payload for changing the current user's password."""
# current_password: str
current_password: str
# new_password: str
new_password: str
# Apply the @field_validator decorator
@field_validator("new_password")
# Apply the @classmethod decorator
@classmethod
# Define function new_password_strength
def new_password_strength(cls, v: str) -> str:
"""Validate the new password against the complexity policy.
Args:
v (str): Raw new-password value from the request body.
Returns:
str: The validated new password.
"""
# Return _validate_password_strength(v)
return _validate_password_strength(v)
# Define class UserOut
class UserOut(BaseModel):
"""Complete representation returned by the API."""
# id: uuid.UUID
id: uuid.UUID
# username: str
username: str
# Assign email = None
email: str | None = None
# role: str
role: str
# is_active: bool
is_active: bool
# Assign must_change_password = True
must_change_password: bool = True
# Assign created_at = None
created_at: datetime | None = None
# Assign last_login = None
last_login: datetime | None = None
# Assign model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)