chore(home): mirror from kalinamall (9883e6a) with papatramp URLs

This commit is contained in:
2026-07-14 20:43:52 +10:00
commit ed4e78f6c3
312 changed files with 42790 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
from app.models.agent_command import AgentCommand
from app.models.api_key import ApiKey
from app.models.event import Event
from app.models.host import Host
from app.models.notification_channel import NotificationChannel
from app.models.notification_cooldown import NotificationCooldown
from app.models.notification_policy import NotificationPolicy
from app.models.problem import Problem, ProblemEvent
from app.models.login_attempt import LoginAttempt
from app.models.ui_settings import UiSettings
from app.models.event_severity_override import EventSeverityOverride
from app.models.event_type_visibility import EventTypeVisibility
from app.models.user import User
from app.models.mobile_settings import MobileSettings
from app.models.mobile_enrollment_code import MobileEnrollmentCode
from app.models.mobile_device import MobileDevice
from app.models.mobile_refresh_token import MobileRefreshToken
__all__ = [
"ApiKey",
"AgentCommand",
"Event",
"Host",
"NotificationChannel",
"NotificationCooldown",
"NotificationPolicy",
"Problem",
"ProblemEvent",
"User",
"EventSeverityOverride",
"EventTypeVisibility",
"LoginAttempt",
"UiSettings",
"MobileSettings",
"MobileEnrollmentCode",
"MobileDevice",
"MobileRefreshToken",
]
+27
View File
@@ -0,0 +1,27 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class AgentCommand(Base):
__tablename__ = "agent_commands"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
command_uuid: Mapped[str] = mapped_column(String(36), unique=True, index=True)
host_id: Mapped[int] = mapped_column(ForeignKey("hosts.id", ondelete="CASCADE"), index=True)
event_id: Mapped[int | None] = mapped_column(ForeignKey("events.id", ondelete="SET NULL"), nullable=True)
command_type: Mapped[str] = mapped_column(String(32))
params: Mapped[dict | None] = mapped_column(JSONB, default=dict)
status: Mapped[str] = mapped_column(String(16), default="pending", index=True)
result_stdout: Mapped[str | None] = mapped_column(Text)
result_stderr: Mapped[str | None] = mapped_column(Text)
requested_by: Mapped[str | None] = mapped_column(String(128))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
host: Mapped["Host"] = relationship(back_populates="agent_commands")
event: Mapped["Event | None"] = relationship()
+17
View File
@@ -0,0 +1,17 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class ApiKey(Base):
__tablename__ = "api_keys"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(128))
key_prefix: Mapped[str] = mapped_column(String(16), index=True)
key_hash: Mapped[str] = mapped_column(String(128), unique=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+30
View File
@@ -0,0 +1,30 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Event(Base):
__tablename__ = "events"
__table_args__ = (UniqueConstraint("event_id", name="uq_events_event_id"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
event_id: Mapped[str] = mapped_column(String(36), index=True)
host_id: Mapped[int] = mapped_column(ForeignKey("hosts.id"), index=True)
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
received_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
category: Mapped[str] = mapped_column(String(64), index=True)
type: Mapped[str] = mapped_column(String(128), index=True)
severity: Mapped[str] = mapped_column(String(16), index=True)
title: Mapped[str] = mapped_column(String(256))
summary: Mapped[str] = mapped_column(Text)
details: Mapped[dict | None] = mapped_column(JSONB)
raw: Mapped[dict | None] = mapped_column(JSONB)
dedup_key: Mapped[str | None] = mapped_column(String(512), index=True)
correlation_id: Mapped[str | None] = mapped_column(String(36))
payload: Mapped[dict] = mapped_column(JSONB)
host: Mapped["Host"] = relationship(back_populates="events")
@@ -0,0 +1,19 @@
from datetime import datetime
from sqlalchemy import DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class EventSeverityOverride(Base):
__tablename__ = "event_severity_overrides"
event_type: Mapped[str] = mapped_column(String(128), primary_key=True)
severity: Mapped[str] = mapped_column(String(16), nullable=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
@@ -0,0 +1,21 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class EventTypeVisibility(Base):
"""Per event-type UI visibility (default: show in events lists)."""
__tablename__ = "event_type_visibility"
event_type: Mapped[str] = mapped_column(String(128), primary_key=True)
show_in_events: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
+45
View File
@@ -0,0 +1,45 @@
from datetime import datetime
from sqlalchemy import DateTime, String, Text, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Host(Base):
__tablename__ = "hosts"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
agent_instance_id: Mapped[str | None] = mapped_column(String(128), unique=True, index=True)
hostname: Mapped[str] = mapped_column(String(255), index=True)
display_name: Mapped[str | None] = mapped_column(String(255))
os_family: Mapped[str] = mapped_column(String(32))
os_version: Mapped[str | None] = mapped_column(String(128))
product: Mapped[str] = mapped_column(String(64), index=True)
product_version: Mapped[str | None] = mapped_column(String(64))
ipv4: Mapped[str | None] = mapped_column(String(45))
ipv6: Mapped[str | None] = mapped_column(String(45))
tags: Mapped[list | None] = mapped_column(JSONB, default=list)
inventory: Mapped[dict | None] = mapped_column(JSONB)
inventory_updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
use_sac_mode: Mapped[str | None] = mapped_column(String(32))
ssh_admin_ok: Mapped[bool | None] = mapped_column(nullable=True)
ssh_admin_checked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
# Optional override for SSH/WinRM (home PCs, unique local admin). Empty → global Settings.
mgmt_user: Mapped[str | None] = mapped_column(String(256))
mgmt_password: Mapped[str | None] = mapped_column(Text)
pending_agent_update: Mapped[bool] = mapped_column(default=False, server_default="false")
pending_update_requested_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
pending_update_target_version: Mapped[str | None] = mapped_column(String(64))
agent_config: Mapped[dict | None] = mapped_column(JSONB)
agent_config_revision: Mapped[int] = mapped_column(default=0, server_default="0")
agent_update_state: Mapped[str | None] = mapped_column(String(32))
agent_update_last_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
agent_update_last_error: Mapped[str | None] = mapped_column(Text)
remote_action: Mapped[dict | None] = mapped_column(JSONB)
last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
events: Mapped[list["Event"]] = relationship(back_populates="host")
agent_commands: Mapped[list["AgentCommand"]] = relationship(back_populates="host")
+21
View File
@@ -0,0 +1,21 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class LoginAttempt(Base):
__tablename__ = "login_attempts"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
ip_address: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
username: Mapped[str | None] = mapped_column(String(64), nullable=True)
success: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
index=True,
)
+25
View File
@@ -0,0 +1,25 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class MobileDevice(Base):
__tablename__ = "mobile_devices"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("sac_users.id", ondelete="CASCADE"), index=True)
device_uuid: Mapped[str] = mapped_column(String(64), unique=True, index=True)
display_name: Mapped[str] = mapped_column(String(128), default="")
platform: Mapped[str] = mapped_column(String(32), default="android")
app_version: Mapped[str | None] = mapped_column(String(32), nullable=True)
fcm_token: Mapped[str | None] = mapped_column(String(512), nullable=True)
fcm_token_updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
enrollment_code_id: Mapped[int | None] = mapped_column(
ForeignKey("mobile_enrollment_codes.id", ondelete="SET NULL"),
)
enrolled_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
@@ -0,0 +1,23 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class MobileEnrollmentCode(Base):
__tablename__ = "mobile_enrollment_codes"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
label: Mapped[str] = mapped_column(String(128), default="")
code_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
code_prefix: Mapped[str] = mapped_column(String(16))
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("sac_users.id", ondelete="SET NULL"))
target_user_id: Mapped[int | None] = mapped_column(ForeignKey("sac_users.id", ondelete="SET NULL"))
login_mode: Mapped[str] = mapped_column(String(16), default="password")
max_uses: Mapped[int] = mapped_column(Integer, default=1)
use_count: Mapped[int] = mapped_column(Integer, default=0)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
@@ -0,0 +1,17 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class MobileRefreshToken(Base):
__tablename__ = "mobile_refresh_tokens"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
device_id: Mapped[int] = mapped_column(ForeignKey("mobile_devices.id", ondelete="CASCADE"))
token_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
+21
View File
@@ -0,0 +1,21 @@
from sqlalchemy import Boolean, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
MOBILE_SETTINGS_ROW_ID = 1
LOGIN_MODE_PASSWORD = "password"
LOGIN_MODE_CODE_ONLY = "code_only"
LOGIN_MODES = frozenset({LOGIN_MODE_PASSWORD, LOGIN_MODE_CODE_ONLY})
class MobileSettings(Base):
"""Singleton: глобальные настройки мобильного клиента Seaca."""
__tablename__ = "mobile_settings"
id: Mapped[int] = mapped_column(primary_key=True)
devices_allowed: Mapped[bool] = mapped_column(Boolean, default=False)
max_devices_per_user: Mapped[int] = mapped_column(Integer, default=3)
min_app_version: Mapped[str | None] = mapped_column(String(32), nullable=True)
@@ -0,0 +1,30 @@
from datetime import datetime
from sqlalchemy import DateTime, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class NotificationChannel(Base):
__tablename__ = "notification_channels"
channel: Mapped[str] = mapped_column(String(32), primary_key=True)
enabled: Mapped[bool] = mapped_column(default=False)
min_severity: Mapped[str] = mapped_column(String(16), default="warning")
bot_token: Mapped[str | None] = mapped_column(Text, nullable=True)
chat_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
webhook_url: Mapped[str | None] = mapped_column(Text, nullable=True)
webhook_secret_header: Mapped[str | None] = mapped_column(String(64), nullable=True)
webhook_secret: Mapped[str | None] = mapped_column(Text, nullable=True)
smtp_host: Mapped[str | None] = mapped_column(Text, nullable=True)
smtp_port: Mapped[int | None] = mapped_column(nullable=True)
smtp_user: Mapped[str | None] = mapped_column(String(128), nullable=True)
smtp_password: Mapped[str | None] = mapped_column(Text, nullable=True)
mail_from: Mapped[str | None] = mapped_column(String(256), nullable=True)
mail_to: Mapped[str | None] = mapped_column(Text, nullable=True)
smtp_starttls: Mapped[bool] = mapped_column(default=True)
smtp_ssl: Mapped[bool] = mapped_column(default=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
@@ -0,0 +1,16 @@
from datetime import datetime
from sqlalchemy import DateTime, String
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class NotificationCooldown(Base):
"""Последняя отправка оповещения по ключу (dedup_key / fingerprint)."""
__tablename__ = "notification_cooldown"
cooldown_key: Mapped[str] = mapped_column(String(512), primary_key=True)
kind: Mapped[str] = mapped_column(String(16)) # event | problem
last_notified_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
+19
View File
@@ -0,0 +1,19 @@
from sqlalchemy import Boolean, String
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
POLICY_ROW_ID = 1
class NotificationPolicy(Base):
"""Singleton: глобальное правило severity → каналы (F-NOT-02 урезанно)."""
__tablename__ = "notification_policy"
id: Mapped[int] = mapped_column(primary_key=True)
min_severity: Mapped[str] = mapped_column(String(16), default="warning")
use_telegram: Mapped[bool] = mapped_column(Boolean, default=True)
use_webhook: Mapped[bool] = mapped_column(Boolean, default=False)
use_email: Mapped[bool] = mapped_column(Boolean, default=False)
use_mobile: Mapped[bool] = mapped_column(Boolean, default=False)
+39
View File
@@ -0,0 +1,39 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Problem(Base):
__tablename__ = "problems"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
host_id: Mapped[int | None] = mapped_column(ForeignKey("hosts.id"), index=True)
title: Mapped[str] = mapped_column(String(256))
summary: Mapped[str] = mapped_column(Text)
severity: Mapped[str] = mapped_column(String(16), index=True)
status: Mapped[str] = mapped_column(String(32), index=True, default="open")
resolved_by: Mapped[str | None] = mapped_column(String(16))
rule_id: Mapped[str | None] = mapped_column(String(64))
fingerprint: Mapped[str] = mapped_column(String(128), index=True)
event_count: Mapped[int] = mapped_column(Integer, default=1)
last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
host: Mapped["Host | None"] = relationship()
event_links: Mapped[list["ProblemEvent"]] = relationship(back_populates="problem")
class ProblemEvent(Base):
__tablename__ = "problem_events"
problem_id: Mapped[int] = mapped_column(ForeignKey("problems.id", ondelete="CASCADE"), primary_key=True)
event_id: Mapped[int] = mapped_column(ForeignKey("events.id", ondelete="CASCADE"), primary_key=True)
problem: Mapped["Problem"] = relationship(back_populates="event_links")
event: Mapped["Event"] = relationship()
+40
View File
@@ -0,0 +1,40 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
UI_SETTINGS_ROW_ID = 1
class UiSettings(Base):
"""Singleton: глобальные настройки интерфейса SAC."""
__tablename__ = "ui_settings"
id: Mapped[int] = mapped_column(primary_key=True)
show_sidebar_system_stats: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
win_admin_user: Mapped[str | None] = mapped_column(String(256), nullable=True)
win_admin_password: Mapped[str | None] = mapped_column(Text, nullable=True)
linux_admin_user: Mapped[str | None] = mapped_column(String(128), nullable=True)
linux_admin_password: Mapped[str | None] = mapped_column(Text, nullable=True)
agent_update_mode: Mapped[str] = mapped_column(String(16), default="gpo", nullable=False)
agent_update_fallback_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
agent_update_fallback_minutes: Mapped[int] = mapped_column(default=15, nullable=False)
agent_recommended_rdp_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
agent_recommended_ssh_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
agent_min_rdp_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
agent_min_ssh_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
win_agent_update_script: Mapped[str | None] = mapped_column(Text, nullable=True)
agent_rdp_git_repo_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
agent_ssh_git_repo_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
agent_git_branch: Mapped[str] = mapped_column(String(64), default="main", nullable=False)
agent_git_rdp_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
agent_git_ssh_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
agent_git_fetched_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
login_ip_whitelist: Mapped[str | None] = mapped_column(Text, nullable=True)
login_sync_fail2ban: Mapped[bool] = mapped_column(default=False, server_default="false", nullable=False)
auto_rdp_flap_disconnect: Mapped[bool] = mapped_column(
Boolean, default=False, server_default="false", nullable=False
)
+21
View File
@@ -0,0 +1,21 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
USER_ROLE_ADMIN = "admin"
USER_ROLE_MONITOR = "monitor"
USER_ROLES = frozenset({USER_ROLE_ADMIN, USER_ROLE_MONITOR})
class User(Base):
__tablename__ = "sac_users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
username: Mapped[str] = mapped_column(String(64), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(String(128))
role: Mapped[str] = mapped_column(String(16), default=USER_ROLE_MONITOR)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())