Files
MeshSate/app/models.py
zhaolei b6a004bdb7 fix: 六引擎误报根因修复 — 解包递归、YARA 恒真规则、签名分级、real_type 溢出
检测范围与证据链修复,不改裁决口径(口径改动在下一条提交)。

- extractor:zip/tar 成员在超限分支外漏加回扫描队列,解包后内容永远不参与检测;
  zip 目录条目被当成内嵌对象;tar 超限成员先落盘不删。递归真正生效后深扫耗时可测。
- clamav:_HIGH/_MEDIUM/_UNWANTED 关键词是小写,而签名名是 "Win.Trojan..." 大小写混排,
  分级判定恒不命中全走默认档;改统一大写匹配。超过 clamd MaxFileSize 的裸奔区间
  (1G~1.5G) 新增 ClamAVSkippedSizeLimit(30) — 按"不可扫描不放行"进复核。
- yara:pe.yar C1 系列改用导入表判定,不再靠裸子串命中字符串表;
  linux.yar C7 多条规则条件恒真(裸 nc/fetch 之类)已收紧;C8/C3 相应收窄。
- structure:DangerousExecutableExtension 是载体信号而非恶意行为,critical 权重 45→35,
  不再作为把中低置信度信号一起顶过 block_score 的底座。
- scanner/documents:real_type 拼进 libmagic 对 MSI/OLE 的元数据串实测 334+ 字符,
  超列宽导致整条 scan_tasks INSERT 连 findings 一起丢(被拒文件不留痕)→
  列宽 256→512,migrate.sql 补 ALTER。
2026-11-02 20:26:56 +08:00

182 lines
7.5 KiB
Python

"""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(512), 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"
)
__table_args__ = (
# 控制台高频查询: WHERE channel = X ORDER BY created_at DESC LIMIT N
Index("ix_scan_tasks_channel_created", "channel", "created_at", postgresql_using="btree"),
)
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"),)