Recovery: Anti-Nuke¶
길드의 비정상 이벤트 패턴을 감지하고 대응하는 sub-context. 4가지 감지 패턴(mass role/channel delete, mass kick, permission escalation)을 rolling window 로 추적하며, 이상 감지 시 자동 스냅샷 + 알림을 수행한다. Enterprise 는 자동 권한 박탈 opt-in.
Sub-context position¶
- Parent — Recovery Core domain
- Location —
engine/recovery/subdomain/antinuke/ - Sibling sub-contexts — Snapshot (긴급 스냅샷 트리거), Restore (auto-action 트리거), Sync (이벤트 소스)
Why this sub-context exists¶
Umbra 의 가치 제안 3기둥 중 Protect 를 담당한다. Snapshot + Restore 만 있으면 "사고 난 뒤 되돌림" 이 가능하지만, 사용자 입장에서는 "사고 발생 자체" 가 스트레스다. AntiNuke 는 사고 시작을 감지하여 다음을 수행한다.
- 증거 보존 — 공격 진행 전에 상태 스냅샷
- 즉시 알림 — Owner 가 사고를 모른 채 지나가지 않도록
- 능동 대응 (Enterprise) — 공격자 권한 박탈로 피해 최소화
이 기능은 Pro 플랜의 차별화 포인트이자 MVP 에 포함되었다 (ADR-0026). "백업만 하는 봇" 과 "실시간으로 길드를 지키는 봇" 의 정체성 차이다.
Domain model¶
AntiNukeIncident¶
감지 이벤트 기록.
AntiNukeIncident
├─ id UUID v7 PK
├─ guild_id UUID → guild.guilds.id
├─ pattern TEXT 'mass_role_delete' | 'mass_channel_delete' | 'mass_kick' | 'permission_escalation'
├─ detected_at TIMESTAMPTZ
├─ window_start TIMESTAMPTZ
├─ window_end TIMESTAMPTZ
├─ events_count INTEGER 감지된 이벤트 수
├─ threshold INTEGER 적용된 임계값
├─ suspects JSONB [{ discord_user_id, audit_log_entries, confidence }]
├─ actions_taken JSONB ['snapshot_created', 'owner_notified', 'roles_revoked_from_X']
├─ snapshot_id UUID → recovery.snapshots.id (nullable, 실패 시 NULL)
├─ false_positive BOOLEAN 사용자 피드백 (nullable)
├─ false_positive_note TEXT
├─ created_at TIMESTAMPTZ
└─ updated_at TIMESTAMPTZ
DetectionPattern (enum)¶
DetectionPattern
mass_role_delete 기본: 5분 내 5개 이상 역할 삭제
mass_channel_delete 기본: 5분 내 3개 이상 채널 삭제
mass_kick 기본: 5분 내 10명 이상 추방
permission_escalation 기존 멤버가 Administrator 권한 획득
각 패턴은 threshold 와 window_seconds 를 가지며 Guild 별 커스터마이징 가능 (Enterprise).
RollingWindow (workflow-internal state)¶
Temporal Workflow 가 메모리에 유지하는 rolling window. 패턴별 이벤트 타임스탬프 큐.
RollingWindow
├─ pattern DetectionPattern
├─ events deque[EventOccurrence] 시간순 정렬
├─ threshold int
└─ window_dur time.Duration
Aggregates¶
- AntiNukeIncident — 독립 aggregate
Invariants¶
- Workflow 1:1 with Guild — Guild 당 AntiNuke workflow 하나 (Bot 설치 시 시작, 강퇴 시 종료)
- False positive 기록 가능 — 사용자가 "오탐" 피드백 가능, 임계값 튜닝 근거
- Snapshot 생성 실패 무시 — AntiNuke 알림은 스냅샷 실패해도 발송 (최소한 Owner 는 알아야 함)
- Auto-action 은 Enterprise 전용 + opt-in — 기본값 false
- AntiNuke 비활성 시 workflow 는 idle — 이벤트 무시, incident 기록 없음
State machine¶
AntiNuke workflow (per guild)¶
stateDiagram-v2
[*] --> Idle : Workflow started
Idle --> Watching : AntiNuke enabled (license)
Watching --> Idle : AntiNuke disabled
Watching --> Reacting : Pattern detected
Reacting --> Watching : Response complete
Watching --> [*] : Bot kicked / guild deleted
Idle --> [*] : Bot kicked / guild deleted
Incident lifecycle¶
stateDiagram-v2
[*] --> Detected : Pattern matched
Detected --> Responding : Actions in progress
Responding --> Completed : All actions done
Completed --> Reviewed : User provides feedback (optional)
Domain events¶
Published¶
| Event | Trigger | Payload | Subscribers |
|---|---|---|---|
AntiNukeTriggered | 패턴 감지 | guild_id, pattern, events_count, suspects | Notification (긴급, preference override), Audit |
AntiNukeActioned | 대응 완료 | guild_id, actions_taken | Notification, Audit |
Consumed¶
| Source | Channel | Action |
|---|---|---|
| Discord Gateway (role/channel delete, member remove, role update) | Bot 프로세스 → Temporal signal | Rolling window 에 이벤트 추가 |
| License 상태 변화 | Workflow signal | AntiNuke enable/disable 반영 |
Discord 이벤트 수신 경로는 Sync 와 별개:
- Sync: 길드 상태를 DB 에 반영
- AntiNuke: 이벤트 타임스탬프를 rolling window 에 적재
두 경로가 이벤트를 중복 수신 하지만 목적이 다르다.
AntiNuke Workflow (Temporal)¶
Temporal Workflow 로 장기 실행. 길드당 하나.
func AntiNukeWorkflow(ctx workflow.Context, guildID uuid.UUID) error {
windows := newRollingWindows(defaultThresholds)
antinukeEnabled := true
for {
selector := workflow.NewSelector(ctx)
// 이벤트 signal
selector.AddReceive(workflow.GetSignalChannel(ctx, "event"), func(c, more) {
var ev EventSignal
c.Receive(ctx, &ev)
if !antinukeEnabled { return }
window := windows[ev.Pattern]
window.Push(ev)
window.EvictOld(workflow.Now(ctx))
if window.Len() >= window.Threshold() {
respondToDetection(ctx, guildID, ev.Pattern, window)
window.Clear() // 같은 패턴의 연속 트리거 방지
}
})
// 설정 업데이트 signal
selector.AddReceive(workflow.GetSignalChannel(ctx, "config_update"), func(c, more) {
var cfg ConfigUpdate
c.Receive(ctx, &cfg)
antinukeEnabled = cfg.Enabled
windows.UpdateThresholds(cfg.Thresholds)
})
// 종료 signal
selector.AddReceive(workflow.GetSignalChannel(ctx, "shutdown"), func(c, more) {
c.Receive(ctx, nil)
return
})
// 정기 evict (1분마다)
timer := workflow.NewTimer(ctx, time.Minute)
selector.AddFuture(timer, func(f) {
now := workflow.Now(ctx)
for _, w := range windows { w.EvictOld(now) }
})
selector.Select(ctx)
}
}
func respondToDetection(ctx, guildID, pattern, window) {
// 1. Audit log fetch → suspect 추정
suspects := executeActivity(IdentifySuspects, guildID, pattern, window.Range())
// 2. 긴급 스냅샷 (실패해도 계속)
snapID, _ := executeActivity(CreateAntiNukeSnapshot, guildID, pattern)
// 3. Incident 기록
incidentID := executeActivity(RecordIncident, guildID, pattern, window, suspects, snapID)
// 4. AntiNukeTriggered 이벤트 발행 (Notification 긴급 알림)
executeActivity(PublishAntiNukeTriggered, incidentID, guildID, pattern, suspects)
// 5. Enterprise auto-action
if isEnterpriseAutoActionEnabled(guildID) {
for _, suspect := range suspects {
if suspect.Confidence > 0.8 {
executeActivity(RevokeMemberRoles, guildID, suspect.DiscordUserID)
}
}
executeActivity(PublishAntiNukeActioned, incidentID, actionsTaken)
}
}
Detection patterns¶
mass_role_delete¶
- Trigger event — Discord
GUILD_ROLE_DELETE - Default threshold — 5 roles
- Default window — 5 minutes
- Rationale — 일반 운영은 역할 몇 개 재구성도 5분 안에 5개 이상 삭제 드뭄
mass_channel_delete¶
- Trigger event — Discord
CHANNEL_DELETE - Default threshold — 3 channels
- Default window — 5 minutes
- Rationale — 채널 삭제는 역할보다 더 파괴적 영향. 더 낮은 임계값.
mass_kick¶
- Trigger event — Discord
GUILD_MEMBER_REMOVE - Default threshold — 10 members
- Default window — 5 minutes
- Rationale — 자연스러운 멤버 이탈과 구분하기 위해 비교적 높은 임계값. Discord Audit Log 에서 추방/밴 타입 구분하여 false positive 완화.
permission_escalation¶
- Trigger event — Discord
GUILD_ROLE_UPDATE또는GUILD_MEMBER_UPDATE로 Administrator 권한 획득 - Threshold — 1 (즉시 트리거)
- Window — N/A
- Rationale — Administrator 권한 획득은 매우 드물며 대부분 악의적. 가이드는 사전에 역할 관리 정책 권장.
Suspect identification¶
Discord Audit Log 를 활용하여 가해자 추정.
IdentifySuspects Activity:
1. Fetch audit_log entries for guild (최근 10분)
2. Filter by action_type 관련 패턴:
- mass_role_delete → MEMBER_ROLE_DELETE (executor_id)
- mass_channel_delete → CHANNEL_DELETE
- mass_kick → MEMBER_KICK
- permission_escalation → MEMBER_ROLE_UPDATE with admin
3. Aggregate by executor_id
4. Confidence = min(1.0, action_count / threshold)
5. Return top suspects (up to 3)
Ports¶
Inbound¶
// engine/recovery/subdomain/antinuke/port/service.go
type AntiNukeService interface {
StartWorkflow(ctx, guildID) error // Bot install 시
StopWorkflow(ctx, guildID) error // Bot kick 시
UpdateConfig(ctx, guildID, enabled bool, autoAction bool, thresholds map[string]Threshold) error
// Bot 프로세스가 이벤트 수신 시 호출
SignalEvent(ctx, guildID, event AntiNukeEvent) error
// 조회
ListIncidents(ctx, guildID, opts) ([]*AntiNukeIncident, error)
MarkFalsePositive(ctx, incidentID, note string) error
}
type AntiNukeEvent struct {
Pattern DetectionPattern
DiscordAggregateID string // role_id / channel_id / user_id 등
Timestamp time.Time
}
Outbound¶
type AntiNukeIncidentRepository interface {
Insert(ctx, incident) error
Update(ctx, incident) error
ListByGuild(ctx, guildID, opts) ([]*AntiNukeIncident, error)
GetByID(ctx, id) (*AntiNukeIncident, error)
}
type DiscordClient interface {
FetchAuditLog(ctx, guildID, actionType int, before time.Time) ([]AuditLogEntry, error)
RevokeMemberRoles(ctx, guildID, userID string) error
}
type SnapshotService interface {
Create(ctx, guildID, trigger SnapshotTrigger, opts) (*Snapshot, error)
}
type RestoreService interface {
Start(ctx, input) (*RestoreJob, error) // Enterprise auto-restore (옵션)
}
type TemporalClient interface {
StartAntiNukeWorkflow(ctx, guildID) (workflowID, error)
SignalWorkflow(ctx, workflowID, signal, payload) error
CancelWorkflow(ctx, workflowID, reason) error
}
type GuildConfigReader interface {
GetConfig(ctx, guildID) (*GuildConfig, error) // antinuke_enabled, thresholds 등
}
type LicensingReader interface {
Can(ctx, guildID, feature) (bool, error)
GetPlanCode(ctx, guildID) (PlanCode, error)
}
type EventPublisher interface {
Publish(ctx, event DomainEvent) error
}
Adapters¶
- Persistence —
engine/recovery/subdomain/antinuke/adapter/persistence/ - Discord —
engine/recovery/subdomain/antinuke/adapter/discord/— audit log fetch + role revoke - Temporal —
engine/recovery/subdomain/antinuke/adapter/temporal/— workflow/activities - Event —
engine/recovery/adapter/event/
Permission model¶
- 감지/알림 — Pro 이상 (Feature
ANTINUKE_DETECT) - 자동 대응 — Enterprise (Feature
ANTINUKE_AUTO_ACTION), opt-in 필요 (guild_configs.antinuke_auto_action) - 설정 변경 — Guild
MANAGE_GUILD권한 - Incident 조회 — 대시보드 (Pro 이상)
- False positive 마킹 — Guild
MANAGE_GUILD
Config customization¶
Pro 는 기본 임계값 사용. Enterprise 는 커스터마이징:
{
"mass_role_delete": { "count": 3, "window_seconds": 180 },
"mass_channel_delete": { "count": 2, "window_seconds": 180 },
"mass_kick": { "count": 5, "window_seconds": 180 },
"permission_escalation": { "count": 1, "window_seconds": 0 }
}
Enterprise 커스텀 설정은 guild.guild_configs.antinuke_thresholds JSONB 에 저장. 변경 시 workflow 에 config_update signal 전송.
False positive handling¶
- 사용자가 대시보드에서 incident 조회 시 "이건 정상 운영이었습니다" 마킹 가능
false_positive = true설정 +note기록- 통계: 오탐률 계산으로 임계값 튜닝 근거
- Phase 2: false positive 누적 시 해당 길드의 임계값 자동 조정 제안
Auto-action safeguards (Enterprise)¶
자동 권한 박탈은 잘못하면 Owner 신뢰 타격 → 안전장치:
- Owner 면책 — Owner 의 권한은 절대 박탈 안 함
- Confidence threshold —
suspect.Confidence > 0.8만 대상 - Action log — 모든 자동 대응은
actions_taken에 상세 기록 + Discord DM 으로 Owner 에게 즉시 알림 - Reversibility — 박탈된 역할은 DM 의 "취소" 버튼으로 즉시 복원 가능 (10분 TTL)
- Manual opt-in —
guild_configs.antinuke_auto_action = true는 Owner 가 대시보드에서 명시 체크해야
Failure modes¶
- Audit log fetch 실패 — Suspect 추정 없이 알림만 발송 (
suspects = []) - Snapshot 생성 실패 — 로그 남기고 알림은 계속 (Owner 가 모르는 게 더 위험)
- Role revoke 실패 (Enterprise) — 실패한 action 은
actions_taken.failed_revokes에 기록, Owner 알림 - Workflow 프로세스 장애 — Temporal 이 재시작 시 이어서 실행. rolling window 는 메모리 state 라 재시작 시 손실되지만 이벤트 누수는 signal 재전송으로 복원되지 않음 (이 한계는 감수 — 중요 패턴은 공격 지속 시 다시 감지됨)
- Enterprise 오탐 자동 박탈 — 사용자 피드백으로 false_positive 마킹 + Action log 에서 reverse
- License downgrade 중 workflow — config_update signal 로 즉시 disable
See also¶
domain/recovery/overview.md— Recovery 전체domain/recovery/snapshot.md— 긴급 스냅샷domain/recovery/restore.md— auto-action (Enterprise)flows/antinuke-trigger.md— 감지 시나리오data/recovery-schema.md— DB 스키마adr/0014-recovery-temporal-workflow.md— Temporal 채택adr/0026-antinuke-mvp-inclusion.md— MVP 포함 결정adr/0027-message-content-exclusion.md— 메시지 없이 감지