fix(models,db): delegate timestamps to DB server and configure connection pool

- Replace default=datetime.utcnow with server_default=func.now() across all 16 models (17 columns) for consistent, timezone-aware timestamps from PostgreSQL

- Upgrade DateTime columns to DateTime(timezone=True) for timestamptz storage

- Configure SQLAlchemy engine pool: pool_size=20, max_overflow=10, pool_recycle=3600, pool_pre_ping=True

- Remove unused datetime imports from model files
This commit is contained in:
2026-02-18 11:52:15 +01:00
parent a4a2adccee
commit 51c927394d
18 changed files with 42 additions and 70 deletions

View File

@@ -14,7 +14,13 @@ def _get_engine():
global _engine global _engine
if _engine is None: if _engine is None:
from app.config import settings from app.config import settings
_engine = create_engine(settings.DATABASE_URL) _engine = create_engine(
settings.DATABASE_URL,
pool_size=20,
max_overflow=10,
pool_recycle=3600,
pool_pre_ping=True,
)
return _engine return _engine

View File

@@ -1,7 +1,5 @@
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, DateTime, ForeignKey, func
from sqlalchemy import Column, String, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -22,7 +20,7 @@ class AuditLog(Base):
action = Column(String, nullable=False) action = Column(String, nullable=False)
entity_type = Column(String, nullable=True) entity_type = Column(String, nullable=True)
entity_id = Column(String, nullable=True) entity_id = Column(String, nullable=True)
timestamp = Column(DateTime, default=datetime.utcnow) timestamp = Column(DateTime(timezone=True), server_default=func.now())
details = Column(JSONB, nullable=True) details = Column(JSONB, nullable=True)
# Relationships # Relationships

View File

@@ -5,11 +5,9 @@ enabling simulation of complete attack chains and APT emulations.
""" """
import uuid import uuid
from datetime import datetime
from sqlalchemy import ( from sqlalchemy import (
Column, String, Text, Integer, Boolean, DateTime, Column, String, Text, Integer, Boolean, DateTime,
ForeignKey, Index, ForeignKey, Index, func,
) )
from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -54,7 +52,7 @@ class Campaign(Base):
completed_at = Column(DateTime, nullable=True) completed_at = Column(DateTime, nullable=True)
target_platform = Column(String, nullable=True) target_platform = Column(String, nullable=True)
tags = Column(JSONB, nullable=True, default=[]) tags = Column(JSONB, nullable=True, default=[])
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
# Recurring scheduling fields # Recurring scheduling fields
is_recurring = Column(Boolean, default=False) is_recurring = Column(Boolean, default=False)

View File

@@ -5,11 +5,9 @@ MITRE ATT&CK techniques, enabling compliance gap analysis.
""" """
import uuid import uuid
from datetime import datetime
from sqlalchemy import ( from sqlalchemy import (
Column, String, Text, Boolean, DateTime, Column, String, Text, Boolean, DateTime,
ForeignKey, Index, UniqueConstraint, ForeignKey, Index, UniqueConstraint, func,
) )
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -27,7 +25,7 @@ class ComplianceFramework(Base):
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
url = Column(String, nullable=True) url = Column(String, nullable=True)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships # Relationships
controls = relationship( controls = relationship(

View File

@@ -6,11 +6,9 @@ per technique per snapshot) to avoid bloated JSONB fields.
""" """
import uuid import uuid
from datetime import datetime
from sqlalchemy import ( from sqlalchemy import (
Column, String, Float, Integer, DateTime, Column, String, Float, Integer, DateTime,
ForeignKey, Index, ForeignKey, Index, func,
) )
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -37,7 +35,7 @@ class CoverageSnapshot(Base):
ForeignKey("users.id", ondelete="SET NULL"), ForeignKey("users.id", ondelete="SET NULL"),
nullable=True, nullable=True,
) )
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships # Relationships
creator = relationship("User", foreign_keys=[created_by]) creator = relationship("User", foreign_keys=[created_by])

View File

@@ -1,9 +1,7 @@
"""DataSource model — registry of external data sources for import.""" """DataSource model — registry of external data sources for import."""
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, Text, Boolean, DateTime, Index, func
from sqlalchemy import Column, String, Text, Boolean, DateTime, Index
from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.dialects.postgresql import UUID, JSONB
from app.database import Base from app.database import Base
@@ -31,7 +29,7 @@ class DataSource(Base):
last_sync_stats = Column(JSONB, nullable=True) # {"imported": X, "updated": Y, ...} last_sync_stats = Column(JSONB, nullable=True) # {"imported": X, "updated": Y, ...}
sync_frequency = Column(String, nullable=True) # daily / weekly / monthly / manual sync_frequency = Column(String, nullable=True) # daily / weekly / monthly / manual
config = Column(JSONB, nullable=True) # source-specific configuration config = Column(JSONB, nullable=True) # source-specific configuration
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
__table_args__ = ( __table_args__ = (
Index('ix_data_sources_type', 'type'), Index('ix_data_sources_type', 'type'),

View File

@@ -5,11 +5,9 @@ ATT&CK techniques, enabling recommended countermeasure lookups.
""" """
import uuid import uuid
from datetime import datetime
from sqlalchemy import ( from sqlalchemy import (
Column, String, Text, DateTime, Column, String, Text, DateTime,
ForeignKey, Index, UniqueConstraint, ForeignKey, Index, UniqueConstraint, func,
) )
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -32,7 +30,7 @@ class DefensiveTechnique(Base):
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
tactic = Column(String, nullable=True) # Detect, Isolate, Deceive, Evict, etc. tactic = Column(String, nullable=True) # Detect, Isolate, Deceive, Evict, etc.
d3fend_url = Column(String, nullable=True) d3fend_url = Column(String, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships # Relationships
attack_mappings = relationship( attack_mappings = relationship(

View File

@@ -1,9 +1,7 @@
"""DetectionRule model — detection rules from multiple sources.""" """DetectionRule model — detection rules from multiple sources."""
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, Text, Boolean, DateTime, Index, func
from sqlalchemy import Column, String, Text, Boolean, DateTime, Index
from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.dialects.postgresql import UUID, JSONB
from app.database import Base from app.database import Base
@@ -33,7 +31,7 @@ class DetectionRule(Base):
log_sources = Column(JSONB, nullable=True) # e.g. {"product": "windows", "service": "sysmon"} log_sources = Column(JSONB, nullable=True) # e.g. {"product": "windows", "service": "sysmon"}
false_positive_rate = Column(String, nullable=True) # low / medium / high false_positive_rate = Column(String, nullable=True) # low / medium / high
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
__table_args__ = ( __table_args__ = (
Index('ix_detection_rules_mitre_technique_id', 'mitre_technique_id'), Index('ix_detection_rules_mitre_technique_id', 'mitre_technique_id'),

View File

@@ -1,7 +1,5 @@
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, Text, DateTime, ForeignKey, Enum, func
from sqlalchemy import Column, String, Text, DateTime, ForeignKey, Enum
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -27,7 +25,7 @@ class Evidence(Base):
file_path = Column(String, nullable=False) # Path in MinIO file_path = Column(String, nullable=False) # Path in MinIO
sha256_hash = Column(String, nullable=False) sha256_hash = Column(String, nullable=False)
uploaded_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) uploaded_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
uploaded_at = Column(DateTime, default=datetime.utcnow) uploaded_at = Column(DateTime(timezone=True), server_default=func.now())
team = Column(Enum(TeamSide, name="teamside"), nullable=False, default=TeamSide.red) team = Column(Enum(TeamSide, name="teamside"), nullable=False, default=TeamSide.red)
notes = Column(Text, nullable=True) notes = Column(Text, nullable=True)

View File

@@ -1,7 +1,5 @@
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, func
from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -22,7 +20,7 @@ class IntelItem(Base):
url = Column(String, nullable=False) url = Column(String, nullable=False)
title = Column(String, nullable=True) title = Column(String, nullable=True)
source = Column(String, nullable=True) source = Column(String, nullable=True)
detected_at = Column(DateTime, default=datetime.utcnow) detected_at = Column(DateTime(timezone=True), server_default=func.now())
reviewed = Column(Boolean, default=False) reviewed = Column(Boolean, default=False)
# Relationships # Relationships

View File

@@ -2,9 +2,7 @@
import enum import enum
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, DateTime, ForeignKey, Enum as SQLEnum, Index, func
from sqlalchemy import Column, String, DateTime, ForeignKey, Enum as SQLEnum, Index
from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -45,8 +43,8 @@ class JiraLink(Base):
last_synced_at = Column(DateTime) last_synced_at = Column(DateTime)
sync_metadata = Column(JSONB, default={}) sync_metadata = Column(JSONB, default={})
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"))
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
creator = relationship("User", foreign_keys=[created_by]) creator = relationship("User", foreign_keys=[created_by])

View File

@@ -1,9 +1,7 @@
"""Notification model — in-app notifications for user actions.""" """Notification model — in-app notifications for user actions."""
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, Index, func
from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, Index
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -27,7 +25,7 @@ class Notification(Base):
entity_type = Column(String, nullable=True) entity_type = Column(String, nullable=True)
entity_id = Column(UUID(as_uuid=True), nullable=True) entity_id = Column(UUID(as_uuid=True), nullable=True)
read = Column(Boolean, default=False) read = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships # Relationships
user = relationship("User") user = relationship("User")

View File

@@ -1,9 +1,7 @@
"""OSINT enrichment items — CVEs, blogs, PoCs, and advisories linked to techniques.""" """OSINT enrichment items — CVEs, blogs, PoCs, and advisories linked to techniques."""
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, func
from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -32,7 +30,7 @@ class OsintItem(Base):
title = Column(String(500), nullable=False) title = Column(String(500), nullable=False)
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
severity = Column(String(20), nullable=True) # CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN severity = Column(String(20), nullable=True) # CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN
discovered_at = Column(DateTime, default=datetime.utcnow, nullable=False) discovered_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
reviewed = Column(Boolean, default=False) reviewed = Column(Boolean, default=False)
metadata_ = Column("metadata", JSONB, default={}) metadata_ = Column("metadata", JSONB, default={})

View File

@@ -1,7 +1,5 @@
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, Text, Boolean, Integer, DateTime, ForeignKey, Enum, func
from sqlalchemy import Column, String, Text, Boolean, Integer, DateTime, ForeignKey, Enum
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -31,7 +29,7 @@ class Test(Base):
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
result = Column(Enum(TestResult, name="testresult"), nullable=True) result = Column(Enum(TestResult, name="testresult"), nullable=True)
state = Column(Enum(TestState, name="teststate"), default=TestState.draft) state = Column(Enum(TestState, name="teststate"), default=TestState.draft)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
# ── Red Team fields ───────────────────────────────────────────── # ── Red Team fields ─────────────────────────────────────────────
red_summary = Column(Text, nullable=True) red_summary = Column(Text, nullable=True)

View File

@@ -1,9 +1,7 @@
"""TestTemplate model — predefined test catalog entries.""" """TestTemplate model — predefined test catalog entries."""
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, Text, Boolean, DateTime, Index, func
from sqlalchemy import Column, String, Text, Boolean, DateTime, Index
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from app.database import Base from app.database import Base
@@ -36,7 +34,7 @@ class TestTemplate(Base):
atomic_test_id = Column(String, nullable=True) # ID in Atomic Red Team repo atomic_test_id = Column(String, nullable=True) # ID in Atomic Red Team repo
suggested_remediation = Column(Text, nullable=True) suggested_remediation = Column(Text, nullable=True)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
__table_args__ = ( __table_args__ = (
Index('ix_test_templates_mitre_technique_id', 'mitre_technique_id'), Index('ix_test_templates_mitre_technique_id', 'mitre_technique_id'),

View File

@@ -5,11 +5,9 @@ techniques, imported from MITRE CTI (STIX 2.0).
""" """
import uuid import uuid
from datetime import datetime
from sqlalchemy import ( from sqlalchemy import (
Column, String, Text, Boolean, DateTime, Column, String, Text, Boolean, DateTime,
ForeignKey, Index, UniqueConstraint, ForeignKey, Index, UniqueConstraint, func,
) )
from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -40,7 +38,7 @@ class ThreatActor(Base):
references = Column(JSONB, nullable=True, default=[]) # [{"url": "...", "description": "..."}] references = Column(JSONB, nullable=True, default=[]) # [{"url": "...", "description": "..."}]
mitre_url = Column(String, nullable=True) mitre_url = Column(String, nullable=True)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships # Relationships
techniques = relationship( techniques = relationship(

View File

@@ -1,7 +1,5 @@
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, Boolean, DateTime, func
from sqlalchemy import Column, String, Boolean, DateTime
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from app.database import Base from app.database import Base
@@ -28,5 +26,5 @@ class User(Base):
role = Column(String, nullable=False, default="viewer") role = Column(String, nullable=False, default="viewer")
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
must_change_password = Column(Boolean, default=True) must_change_password = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
last_login = Column(DateTime, nullable=True) last_login = Column(DateTime, nullable=True)

View File

@@ -1,9 +1,7 @@
"""Worklog model — immutable internal time-tracking records.""" """Worklog model — immutable internal time-tracking records."""
import uuid import uuid
from datetime import datetime from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Text, Index, func
from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Text, Index
from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -32,7 +30,7 @@ class Worklog(Base):
tempo_synced = Column(DateTime) tempo_synced = Column(DateTime)
tempo_worklog_id = Column(String(100)) tempo_worklog_id = Column(String(100))
integrity_hash = Column(String(64)) integrity_hash = Column(String(64))
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), server_default=func.now())
extra_metadata = Column("metadata", JSONB, default={}) extra_metadata = Column("metadata", JSONB, default={})
user = relationship("User", foreign_keys=[user_id]) user = relationship("User", foreign_keys=[user_id])