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. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user