Files
brov-macbook/NotchBuddy/Sources/App/DangerCheck.swift
T

95 lines
7.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
// MARK: - Destructive command warning
//
// Looks at a permission request (tool + tool_input) and returns short Russian reasons
// when it can destroy data or history, e.g. ["удаляет файлы рекурсивно"]. The approval
// card shows them in red and hides "Всегда" so a dangerous rule is never saved by one click.
// Heuristics only: an empty result does not mean the command is safe.
enum DangerCheck {
struct Rule {
let pattern: String
let reason: String
}
/// Shell command rules (case-insensitive regex, matched against the whole command).
static let shellRules: [Rule] = [
Rule(pattern: #"\brm\s+(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r|-r\b.*-f\b|-f\b.*-r\b|--recursive|-r\b|-R\b)"#,
reason: "удаляет файлы рекурсивно"),
Rule(pattern: #"\brm\s+.*(\s|^)(/|~|\$HOME|\*)(\s|$)"#, reason: "удаляет корень, домашнюю папку или всё подряд"),
Rule(pattern: #"\bgit\s+push\b.*(--force\b|--force-with-lease\b|\s-f\b|\s\+\S)"#, reason: "перезаписывает историю на сервере (force push)"),
Rule(pattern: #"\bgit\s+reset\s+--hard\b"#, reason: "выбрасывает несохранённые изменения (reset --hard)"),
Rule(pattern: #"\bgit\s+clean\s+-[a-z]*f"#, reason: "удаляет неотслеживаемые файлы (git clean)"),
Rule(pattern: #"\bgit\s+(checkout|restore)\s+(--\s+)?\.(\s|$)"#, reason: "откатывает все изменения в рабочей папке"),
Rule(pattern: #"\bgit\s+branch\s+-D\b"#, reason: "удаляет ветку без проверки слияния"),
Rule(pattern: #"\bgit\s+(rebase|filter-branch|filter-repo)\b"#, reason: "переписывает историю git"),
Rule(pattern: #"\bdrop\s+(table|database|schema|index|view)\b"#, reason: "удаляет таблицу или базу (DROP)"),
Rule(pattern: #"\btruncate\s+(table\s+)?\w"#, reason: "очищает таблицу (TRUNCATE)"),
Rule(pattern: #"\bdelete\s+from\s+\w+(\s*;|\s*$|\s*"|\s*')"#, reason: "удаляет все строки (DELETE без WHERE)"),
Rule(pattern: #"\bupdate\s+\w+\s+set\b(?!.*\bwhere\b)"#, reason: "меняет все строки (UPDATE без WHERE)"),
Rule(pattern: #"\b(mkfs|diskutil\s+(erase|zero|reformat|partition)|fdisk|newfs)"#, reason: "форматирует диск"),
Rule(pattern: #"\bdd\s+.*\bof="#, reason: "пишет напрямую на устройство (dd)"),
Rule(pattern: #"\bchmod\s+(-R\s+)?(0?777|a\+rwx)"#, reason: "открывает файлы всем на запись"),
Rule(pattern: #"\b(chmod|chown)\s+-R\b.*(\s/|\s~)"#, reason: "меняет права рекурсивно в системной или домашней папке"),
Rule(pattern: #"\bsudo\b"#, reason: "выполняется с правами администратора (sudo)"),
Rule(pattern: #"(curl|wget)\b[^|]*\|\s*(sudo\s+)?(sh|bash|zsh|python3?)\b"#, reason: "скачивает и сразу запускает скрипт из интернета"),
Rule(pattern: #":\(\)\s*\{\s*:\|:&\s*\};:"#, reason: "fork-бомба"),
Rule(pattern: #"\b(kill|pkill|killall)\s+(-9\s+)?(-1\b|1\b|Finder|Dock|WindowServer|loginwindow)"#, reason: "убивает системные процессы"),
Rule(pattern: #"\b(shutdown|reboot|halt)\b"#, reason: "выключает или перезагружает компьютер"),
Rule(pattern: #"\bdocker\s+(system\s+prune|volume\s+(rm|prune)|rm\s+-f)"#, reason: "удаляет контейнеры или тома Docker"),
Rule(pattern: #"\bkubectl\s+delete\b"#, reason: "удаляет ресурсы в Kubernetes"),
Rule(pattern: #"\bterraform\s+(destroy|apply\s+.*-auto-approve)"#, reason: "меняет или уничтожает инфраструктуру"),
Rule(pattern: #"\b(npm|pnpm|yarn)\s+publish\b|\bgh\s+release\s+create\b"#, reason: "публикует наружу"),
Rule(pattern: #"\bgh\s+repo\s+(delete|edit\s+.*--visibility\s+public)"#, reason: "удаляет репозиторий или делает его публичным"),
Rule(pattern: #">\s*(/etc/|/dev/sd|/dev/disk|~/\.ssh/|~/\.zshrc|~/\.bashrc)"#, reason: "перезаписывает системный или личный конфиг"),
Rule(pattern: #"\bfind\b.*\s-delete\b|\bfind\b.*-exec\s+rm\b"#, reason: "удаляет найденные файлы пачкой"),
Rule(pattern: #"\b(security\s+delete-|defaults\s+delete\b|launchctl\s+(unload|remove|bootout))"#, reason: "меняет системные настройки macOS"),
]
/// Paths that are dangerous to write or edit.
static let sensitivePaths: [(pattern: String, reason: String)] = [
(#"(^|/)\.env(\.|$)"#, "правит секреты (.env)"),
(#"/\.ssh/"#, "правит SSH-ключи или конфиг"),
(#"^/(etc|System|Library|usr|bin|sbin)/"#, "пишет в системную папку"),
(#"/\.claude/settings(\.local)?\.json$"#, "меняет настройки Claude Code"),
(#"/\.(zshrc|bashrc|zprofile|bash_profile|gitconfig)$"#, "правит конфиг оболочки или git"),
(#"/\.git/"#, "правит внутренности репозитория (.git)"),
(#"(id_rsa|id_ed25519|\.pem|\.key|credentials|secrets?)(\.|$)"#, "трогает ключи или секреты"),
]
static func reasons(tool: String, input: [String: Any]) -> [String] {
var out: [String] = []
if let command = input["command"] as? String {
for rule in shellRules where matches(rule.pattern, command) && !out.contains(rule.reason) {
out.append(rule.reason)
}
}
let fileTools: Set<String> = ["Write", "Edit", "MultiEdit", "NotebookEdit"]
if fileTools.contains(tool) {
let path = (input["file_path"] as? String ?? input["notebook_path"] as? String ?? "")
let expanded = (path as NSString).expandingTildeInPath
for (pattern, reason) in sensitivePaths where matches(pattern, expanded) && !out.contains(reason) {
out.append(reason)
}
}
return out
}
static func matches(_ pattern: String, _ text: String) -> Bool {
text.range(of: pattern, options: [.regularExpression, .caseInsensitive]) != nil
}
/// What the approval card shows for non-shell tools: "Write · ~/proj/.env".
static func describe(tool: String, input: [String: Any]) -> String {
if let command = input["command"] as? String { return command }
if let path = (input["file_path"] ?? input["notebook_path"] ?? input["path"]) as? String {
let home = FileManager.default.homeDirectoryForCurrentUser.path
let short = path.hasPrefix(home) ? "~" + path.dropFirst(home.count) : path
return "\(tool) · \(short)"
}
if let url = input["url"] as? String { return "\(tool) · \(url)" }
return tool
}
}