|
| 1 | +import AppKit |
| 2 | + |
| 3 | +struct ScanOptions { |
| 4 | + var claudeCode = true |
| 5 | + var shellHistory = true |
| 6 | + var gitRepos = true |
| 7 | + var downloads = true |
| 8 | + var clawdbot = true |
| 9 | + var browser = true |
| 10 | + var system = true |
| 11 | + |
| 12 | + /// CLI flags to pass to scan-logs.mjs |
| 13 | + var skipArgs: [String] { |
| 14 | + var args: [String] = [] |
| 15 | + if !claudeCode { args.append("--skip-claude") } |
| 16 | + if !shellHistory { args.append("--skip-shell") } |
| 17 | + if !gitRepos { args.append("--skip-git") } |
| 18 | + if !downloads { args.append("--skip-downloads") } |
| 19 | + if !clawdbot { args.append("--skip-clawdbot") } |
| 20 | + if !browser { args.append("--skip-browser") } |
| 21 | + if !system { args.append("--skip-system") } |
| 22 | + return args |
| 23 | + } |
| 24 | + |
| 25 | + static func fromDefaults() -> ScanOptions { |
| 26 | + var opts = ScanOptions() |
| 27 | + let d = UserDefaults.standard |
| 28 | + func load(_ key: String, into value: inout Bool) { |
| 29 | + if d.object(forKey: key) != nil { value = d.bool(forKey: key) } |
| 30 | + } |
| 31 | + load("scanOpt_claudeCode", into: &opts.claudeCode) |
| 32 | + load("scanOpt_shellHistory", into: &opts.shellHistory) |
| 33 | + load("scanOpt_gitRepos", into: &opts.gitRepos) |
| 34 | + load("scanOpt_downloads", into: &opts.downloads) |
| 35 | + load("scanOpt_clawdbot", into: &opts.clawdbot) |
| 36 | + load("scanOpt_browser", into: &opts.browser) |
| 37 | + load("scanOpt_system", into: &opts.system) |
| 38 | + return opts |
| 39 | + } |
| 40 | + |
| 41 | + func saveToDefaults() { |
| 42 | + let d = UserDefaults.standard |
| 43 | + d.set(claudeCode, forKey: "scanOpt_claudeCode") |
| 44 | + d.set(shellHistory, forKey: "scanOpt_shellHistory") |
| 45 | + d.set(gitRepos, forKey: "scanOpt_gitRepos") |
| 46 | + d.set(downloads, forKey: "scanOpt_downloads") |
| 47 | + d.set(clawdbot, forKey: "scanOpt_clawdbot") |
| 48 | + d.set(browser, forKey: "scanOpt_browser") |
| 49 | + d.set(system, forKey: "scanOpt_system") |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +enum ScanPermissionPanel { |
| 54 | + |
| 55 | + // Returns nil if user cancelled, ScanOptions if confirmed. |
| 56 | + static func present() -> ScanOptions? { |
| 57 | + var opts = ScanOptions.fromDefaults() |
| 58 | + let firstTime = !UserDefaults.standard.bool(forKey: "scanPermissionsExplained") |
| 59 | + |
| 60 | + let alert = NSAlert() |
| 61 | + alert.messageText = "What vibe-sec will scan" |
| 62 | + alert.informativeText = firstTime |
| 63 | + ? "vibe-sec reads the following on your Mac only.\nUncheck anything you want to skip. Nothing leaves your machine." |
| 64 | + : "Choose what to include in this scan.\nNothing leaves your machine." |
| 65 | + |
| 66 | + // Build checkbox list |
| 67 | + let items: [(String, WritableKeyPath<ScanOptions, Bool>)] = [ |
| 68 | + ("Claude Code — session logs, settings, MCP config", \.claudeCode), |
| 69 | + ("Shell — command history (~/.zsh_history)", \.shellHistory), |
| 70 | + ("Git repos — .env files in ~/Documents/GitHub/", \.gitRepos), |
| 71 | + ("Downloads — API key files, service account JSON", \.downloads), |
| 72 | + ("clawdbot — Telegram bot token (~/.clawdbot/)", \.clawdbot), |
| 73 | + ("Safari/Chrome — visited cloud & financial services", \.browser), |
| 74 | + ("System — open ports, firewall, screen lock", \.system), |
| 75 | + ] |
| 76 | + |
| 77 | + let stack = NSStackView() |
| 78 | + stack.orientation = .vertical |
| 79 | + stack.alignment = .left |
| 80 | + stack.spacing = 6 |
| 81 | + |
| 82 | + var buttons: [(NSButton, WritableKeyPath<ScanOptions, Bool>)] = [] |
| 83 | + for (label, keyPath) in items { |
| 84 | + let btn = NSButton(checkboxWithTitle: label, target: nil, action: nil) |
| 85 | + btn.state = opts[keyPath: keyPath] ? .on : .off |
| 86 | + btn.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize + 1) |
| 87 | + stack.addArrangedSubview(btn) |
| 88 | + buttons.append((btn, keyPath)) |
| 89 | + } |
| 90 | + |
| 91 | + let fitting = stack.fittingSize |
| 92 | + stack.frame = NSRect(x: 0, y: 0, width: max(fitting.width, 380), height: fitting.height) |
| 93 | + alert.accessoryView = stack |
| 94 | + |
| 95 | + alert.addButton(withTitle: "Scan Now") |
| 96 | + alert.addButton(withTitle: "Cancel") |
| 97 | + alert.alertStyle = .informational |
| 98 | + |
| 99 | + let response = alert.runModal() |
| 100 | + guard response == .alertFirstButtonReturn else { return nil } |
| 101 | + |
| 102 | + // Read back checkbox states |
| 103 | + for (btn, keyPath) in buttons { |
| 104 | + opts[keyPath: keyPath] = btn.state == .on |
| 105 | + } |
| 106 | + opts.saveToDefaults() |
| 107 | + |
| 108 | + if firstTime { |
| 109 | + UserDefaults.standard.set(true, forKey: "scanPermissionsExplained") |
| 110 | + } |
| 111 | + |
| 112 | + return opts |
| 113 | + } |
| 114 | +} |
0 commit comments