"""ORM 模型定义。 涉及表: - ``scan_tasks`` 扫描任务(同步与异步任务共用一张表) - ``scan_findings`` 单条检测信号 - ``audit_logs`` 审计日志(带哈希链,防篡改) - ``console_users`` 控制台账号 - ``login_attempts`` 登录失败记录(用于锁定) - ``policy_versions`` 策略版本快照 - ``review_records`` 人工复核记录 """ from __future__ import annotations import uuid from datetime import datetime, timezone from sqlalchemy import ( JSON, Boolean, DateTime, Float, ForeignKey, Index, Integer, String, Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db import Base def utcnow() -> datetime: return datetime.now(timezone.utc) def new_id() -> str: return uuid.uuid4().hex class ScanTask(Base): __tablename__ = "scan_tasks" id: Mapped[str] = mapped_column(String(64), primary_key=True, default=new_id) channel: Mapped[str] = mapped_column(String(64), default="default", index=True) client_id: Mapped[str] = mapped_column(String(64), index=True) filename: Mapped[str] = mapped_column(String(512)) declared_ext: Mapped[str] = mapped_column(String(32), default="") size: Mapped[int] = mapped_column(Integer, default=0) sha256: Mapped[str] = mapped_column(String(64), index=True) md5: Mapped[str] = mapped_column(String(32), default="") real_type: Mapped[str] = mapped_column(String(256), default="") status: Mapped[str] = mapped_column(String(16), default="pending", index=True) mode: Mapped[str] = mapped_column(String(16), default="sync") verdict: Mapped[str] = mapped_column(String(24), default="pending") action: Mapped[str] = mapped_column(String(32), default="pending") score: Mapped[float] = mapped_column(Float, default=0.0) reason: Mapped[str] = mapped_column(Text, default="") policy_version: Mapped[str] = mapped_column(String(64), default="") detail: Mapped[dict] = mapped_column(JSON, default=dict) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) duration_ms: Mapped[int] = mapped_column(Integer, default=0) findings: Mapped[list["ScanFinding"]] = relationship( back_populates="task", cascade="all, delete-orphan", lazy="selectin" ) class ScanFinding(Base): __tablename__ = "scan_findings" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) task_id: Mapped[str] = mapped_column( String(64), ForeignKey("scan_tasks.id", ondelete="CASCADE"), index=True ) engine: Mapped[str] = mapped_column(String(64)) layer: Mapped[str] = mapped_column(String(8)) severity: Mapped[str] = mapped_column(String(16)) rule: Mapped[str] = mapped_column(String(256)) detail: Mapped[str] = mapped_column(Text, default="") extracted_path: Mapped[str] = mapped_column(String(1024), default="") weight: Mapped[float] = mapped_column(Float, default=0.0) task: Mapped[ScanTask] = relationship(back_populates="findings") class AuditLog(Base): """审计日志。 每条记录包含 ``prev_hash`` 与 ``row_hash``,形成哈希链。 任一条记录被篡改或删除,后续记录的链式校验都会失败,可被检测出来。 """ __tablename__ = "audit_logs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True) actor: Mapped[str] = mapped_column(String(128), default="system") actor_role: Mapped[str] = mapped_column(String(32), default="system") action: Mapped[str] = mapped_column(String(64), index=True) target: Mapped[str] = mapped_column(String(256), default="") channel: Mapped[str] = mapped_column(String(64), default="") client_ip: Mapped[str] = mapped_column(String(64), default="") outcome: Mapped[str] = mapped_column(String(24), default="success") detail: Mapped[dict] = mapped_column(JSON, default=dict) prev_hash: Mapped[str] = mapped_column(String(64), default="") row_hash: Mapped[str] = mapped_column(String(64), default="") __table_args__ = (Index("ix_audit_logs_action_ts", "action", "ts"),) class ConsoleUser(Base): __tablename__ = "console_users" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) username: Mapped[str] = mapped_column(String(64), unique=True, index=True) password_hash: Mapped[str] = mapped_column(String(256)) role: Mapped[str] = mapped_column(String(32), default="admin") is_active: Mapped[bool] = mapped_column(Boolean, default=True) must_change_password: Mapped[bool] = mapped_column(Boolean, default=True) failed_attempts: Mapped[int] = mapped_column(Integer, default=0) locked_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) password_changed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) class LoginAttempt(Base): __tablename__ = "login_attempts" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) username: Mapped[str] = mapped_column(String(64), index=True) client_ip: Mapped[str] = mapped_column(String(64), default="") success: Mapped[bool] = mapped_column(Boolean, default=False) ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True) class PolicyVersion(Base): __tablename__ = "policy_versions" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) version: Mapped[str] = mapped_column(String(64), unique=True, index=True) content: Mapped[dict] = mapped_column(JSON, default=dict) sha256: Mapped[str] = mapped_column(String(64), default="") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) class ReviewRecord(Base): __tablename__ = "review_records" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) task_id: Mapped[str] = mapped_column(String(64), ForeignKey("scan_tasks.id", ondelete="CASCADE"), index=True) reviewer: Mapped[str] = mapped_column(String(64)) decision: Mapped[str] = mapped_column(String(32)) comment: Mapped[str] = mapped_column(Text, default="") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) class IdempotencyRecord(Base): """幂等记录:同一文件哈希 + 同一策略版本只扫描一次。""" __tablename__ = "idempotency_records" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) sha256: Mapped[str] = mapped_column(String(64), index=True) policy_version: Mapped[str] = mapped_column(String(64), index=True) task_id: Mapped[str] = mapped_column(String(64)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) __table_args__ = (UniqueConstraint("sha256", "policy_version", name="uq_idem_hash_policy"),)