930 lines
46 KiB
Swift
930 lines
46 KiB
Swift
import Foundation
|
|
|
|
nonisolated enum CommandRiskRules {
|
|
static let kubernetesGlobalValueOptions: Set<String> = [
|
|
"--as", "--as-group", "--cache-dir", "--certificate-authority", "--client-certificate", "--client-key",
|
|
"--cluster", "--context", "--kubeconfig", "--namespace", "-n", "--password", "--profile",
|
|
"--profile-output", "--request-timeout", "--server", "--tls-server-name", "--token", "--user",
|
|
"--username",
|
|
]
|
|
|
|
static func isKnown(_ name: String) -> Bool {
|
|
readOnlyCommands.contains(name) || cautionCommands.contains(name) || destructiveCommands.contains(name)
|
|
|| argumentSensitiveCommands.contains(name)
|
|
}
|
|
|
|
static func isStandaloneHelpOrVersionRequest(_ args: [String]) -> Bool {
|
|
args == ["--help"] || args == ["--version"]
|
|
}
|
|
|
|
static func classify(name: String, args: [String]) -> RiskAssessment {
|
|
if destructiveCommands.contains(name) { return destructive(name) }
|
|
if cautionCommands.contains(name) { return caution(name) }
|
|
if readOnlyCommands.contains(name) { return .safe }
|
|
|
|
switch name {
|
|
case "rm": return classifyRemove(args)
|
|
case "find": return classifyFind(args)
|
|
case "chmod", "chown", "chgrp": return classifyPermissions(args)
|
|
case "awk": return caution(name)
|
|
case "sort":
|
|
return hasFlag(args, ["-o", "--output", "--compress-program"]) ? caution(name) : .safe
|
|
case "tree", "diff": return hasFlag(args, ["-o", "--output"]) ? caution(name) : .safe
|
|
case "rg": return hasFlag(args, ["--pre"]) ? caution(name) : .safe
|
|
case "uniq": return operands(args).count > 1 ? caution(name) : .safe
|
|
case "blkid": return hasFlag(args, ["-w", "--garbage-collect"]) ? caution(name) : .safe
|
|
case "sed": return classifySed(args)
|
|
case "yq": return hasFlag(args, ["-i", "--inplace", "--in-place"]) ? caution(name) : .safe
|
|
case "xxd": return classifyXXD(args)
|
|
case "tar": return classifyTar(args)
|
|
case "date": return hasFlag(args, ["-s", "--set"]) ? caution(name) : .safe
|
|
case "hostname": return classifyHostname(args)
|
|
case "mount": return classifyMount(args)
|
|
case "dmesg": return classifyDmesg(args)
|
|
case "systemctl": return classifySystemctl(args)
|
|
case "service": return classifyService(args)
|
|
case "journalctl": return classifyJournalctl(args)
|
|
case "timedatectl", "hostnamectl", "localectl", "loginctl", "resolvectl", "networkctl":
|
|
return classifySystemUtility(name, args)
|
|
case "ip", "bridge", "tc": return classifyIPFamily(name, args)
|
|
case "ifconfig": return classifyIfconfig(args)
|
|
case "route": return classifyRoute(args)
|
|
case "arp": return classifyARP(args)
|
|
case "ethtool": return classifyEthtool(args)
|
|
case "nvidia-smi": return classifyNvidiaSMI(args)
|
|
case "sensors": return hasFlag(args, ["-s", "--set"]) ? caution(name) : .safe
|
|
case "sysctl": return classifySysctl(args)
|
|
case "swapon": return classifySwapon(args)
|
|
case "ulimit": return classifyUlimit(args)
|
|
case "fdisk", "parted", "gdisk": return classifyPartitionEditor(name, args)
|
|
case "zfs", "zpool": return classifyZFS(name, args)
|
|
case "diskutil": return classifyDiskutil(args)
|
|
case "defaults": return classifyDefaults(args)
|
|
case "launchctl": return classifyLaunchctl(args)
|
|
case "losetup": return classifyLosetup(args)
|
|
case "iptables", "ip6tables", "nft", "ufw", "firewall-cmd": return classifyFirewall(name, args)
|
|
case "kill", "pkill", "killall": return classifyKill(name, args)
|
|
case "rsync": return classifyRsync(args)
|
|
case "crontab": return classifyCrontab(args)
|
|
case "git": return classifyGit(args)
|
|
case "git-lfs": return classifyGitLFS(args)
|
|
case "gh", "glab": return classifyHostedGit(name, args)
|
|
case "docker", "podman", "nerdctl": return classifyContainerCLI(name, args)
|
|
case "docker-compose", "podman-compose": return classifyCompose(name, args)
|
|
case "kubectl", "oc": return classifyKubernetes(name, args)
|
|
case "helm": return classifyHelm(args)
|
|
case "terraform", "tofu": return classifyTerraform(name, args)
|
|
case "ansible", "ansible-playbook", "ansible-vault": return caution(name)
|
|
case "ansible-config", "ansible-doc", "ansible-inventory", "ansible-lint": return .safe
|
|
case "aws", "az", "gcloud": return classifyCloudCLI(name, args)
|
|
case "apt", "apt-get", "apt-cache", "dpkg", "rpm", "yum", "dnf", "pacman", "apk", "zypper",
|
|
"snap", "flatpak", "brew", "softwareupdate", "pkg":
|
|
return classifySystemPackageManager(name, args)
|
|
case "npm", "pnpm", "yarn", "bun", "pip", "pip3", "pipx", "gem", "cargo", "go":
|
|
return classifyDevelopmentTool(name, args)
|
|
case "psql", "mysql", "sqlite3", "mongosh": return classifyDatabaseClient(name, args)
|
|
case "pg_dump": return classifyDump(args, name: name, outputFlags: ["-f", "--file"])
|
|
case "mysqldump": return classifyDump(args, name: name, outputFlags: ["-r", "--result-file"])
|
|
case "mysqladmin": return classifyMySQLAdmin(args)
|
|
case "redis-cli": return classifyRedis(args)
|
|
case "supervisorctl", "pm2": return classifyProcessManager(name, args)
|
|
case "nginx", "apachectl", "apache2ctl", "httpd", "haproxy", "caddy":
|
|
return classifyServerBinary(name, args)
|
|
case "command", "builtin":
|
|
return args.contains("-v") || args.contains("-V") ? .safe : caution(name)
|
|
default:
|
|
return RiskAssessment(
|
|
risk: .caution,
|
|
reasons: [String(localized: "`\(name)` is not a recognised read-only command.")]
|
|
)
|
|
}
|
|
}
|
|
|
|
static func operands(_ args: [String], valueOptions: Set<String> = []) -> [String] {
|
|
var result: [String] = []
|
|
var index = 0
|
|
var optionsEnded = false
|
|
while index < args.count {
|
|
let argument = args[index]
|
|
if argument == "--" {
|
|
optionsEnded = true
|
|
index += 1
|
|
continue
|
|
}
|
|
if !optionsEnded, argument.hasPrefix("-"), argument != "-" {
|
|
let option = String(argument.split(separator: "=", maxSplits: 1).first ?? Substring(argument))
|
|
if valueOptions.contains(option), !argument.contains("=") { index += 2 } else { index += 1 }
|
|
continue
|
|
}
|
|
result.append(argument)
|
|
index += 1
|
|
}
|
|
return result
|
|
}
|
|
|
|
// MARK: - Core commands
|
|
|
|
private static func classifyRemove(_ args: [String]) -> RiskAssessment {
|
|
let targetsRoot = args.contains { $0 == "/" || $0.hasPrefix("/*") }
|
|
if targetsRoot {
|
|
return RiskAssessment(
|
|
risk: .destructive,
|
|
reasons: [String(localized: "Recursive delete targeting the filesystem root.")]
|
|
)
|
|
}
|
|
return RiskAssessment(
|
|
risk: .destructive,
|
|
reasons: [String(localized: "Deletes files permanently.")]
|
|
)
|
|
}
|
|
|
|
private static func classifyFind(_ args: [String]) -> RiskAssessment {
|
|
if args.contains("-delete") {
|
|
return RiskAssessment(
|
|
risk: .destructive,
|
|
reasons: [String(localized: "`find -delete` removes every match.")]
|
|
)
|
|
}
|
|
if args.contains(where: { ["-exec", "-execdir", "-ok", "-okdir"].contains($0) }) {
|
|
return RiskAssessment(
|
|
risk: .caution,
|
|
reasons: [String(localized: "`find -exec` runs a command per match.")]
|
|
)
|
|
}
|
|
if args.contains(where: { ["-fprint", "-fprint0", "-fprintf", "-fls"].contains($0) }) {
|
|
return caution("find")
|
|
}
|
|
return .safe
|
|
}
|
|
|
|
private static func classifyPermissions(_ args: [String]) -> RiskAssessment {
|
|
if shortFlags(args).contains("R") {
|
|
return RiskAssessment(
|
|
risk: .destructive,
|
|
reasons: [String(localized: "Recursive ownership or permission change.")]
|
|
)
|
|
}
|
|
return RiskAssessment(
|
|
risk: .caution,
|
|
reasons: [String(localized: "Changes file ownership or permissions.")]
|
|
)
|
|
}
|
|
|
|
private static func classifySed(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["-i", "--in-place"]) { return caution("sed") }
|
|
let scripts = operands(args)
|
|
let dangerousScript = scripts.contains { script in
|
|
let trimmed = script.trimmingCharacters(in: .whitespaces)
|
|
return trimmed.hasPrefix("e ") || trimmed.hasPrefix("w ") || trimmed.contains(";e ")
|
|
|| trimmed.contains(";w ") || trimmed.hasSuffix("/e") || trimmed.contains("/e;")
|
|
|| trimmed.range(
|
|
of: #"(^|[;{}[:space:]0-9,$])([ew])([[:space:]]|$)"#,
|
|
options: .regularExpression
|
|
) != nil
|
|
|| trimmed.range(of: #"/[gpimI0-9]*[ew]([;[:space:]]|$)"#, options: .regularExpression)
|
|
!= nil
|
|
}
|
|
return dangerousScript ? caution("sed") : .safe
|
|
}
|
|
|
|
private static func classifyXXD(_ args: [String]) -> RiskAssessment {
|
|
guard hasFlag(args, ["-r", "--revert"]) else { return .safe }
|
|
return operands(args).count > 1 ? caution("xxd") : .safe
|
|
}
|
|
|
|
private static func classifyTar(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["--delete"]) { return destructive("tar") }
|
|
if hasFlag(args, ["-I", "--use-compress-program"])
|
|
|| args.contains(where: { $0.hasPrefix("--checkpoint-action=exec") })
|
|
{
|
|
return caution("tar")
|
|
}
|
|
var flags = shortFlags(args)
|
|
if let oldStyle = args.first, !oldStyle.hasPrefix("-"), oldStyle.allSatisfy({ $0.isLetter }) {
|
|
flags.formUnion(oldStyle)
|
|
}
|
|
if flags.contains("t") || hasFlag(args, ["--list"]) { return .safe }
|
|
if flags.contains("x") || hasFlag(args, ["--extract", "--get"]) { return caution("tar") }
|
|
if !flags.isDisjoint(with: ["c", "r", "u", "A"])
|
|
|| hasFlag(
|
|
args, ["--create", "--append", "--update", "--concatenate"])
|
|
{
|
|
return caution("tar")
|
|
}
|
|
return caution("tar")
|
|
}
|
|
|
|
private static func classifyHostname(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["-F", "--file"]) { return caution("hostname") }
|
|
return operands(args).isEmpty ? .safe : caution("hostname")
|
|
}
|
|
|
|
private static func classifyMount(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["-a", "--all"]) { return caution("mount") }
|
|
if args.isEmpty || (operands(args).isEmpty && hasFlag(args, ["-l", "--show-labels"])) { return .safe }
|
|
return operands(args).isEmpty ? .safe : caution("mount")
|
|
}
|
|
|
|
private static func classifyDmesg(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["-C", "-c", "--clear", "--read-clear"]) { return destructive("dmesg") }
|
|
if hasFlag(args, ["-D", "-E", "-n", "--console-off", "--console-on", "--console-level"]) {
|
|
return caution("dmesg")
|
|
}
|
|
return .safe
|
|
}
|
|
|
|
// MARK: - Services, networking, and storage
|
|
|
|
private static func classifySystemctl(_ args: [String]) -> RiskAssessment {
|
|
let actions = operands(args, valueOptions: ["--host", "--machine", "--root", "--image", "--type", "--state"])
|
|
let action = actions.first ?? "list-units"
|
|
let safe: Set<String> = [
|
|
"list-units", "list-automounts", "list-paths", "list-sockets", "list-timers", "is-active",
|
|
"is-enabled", "is-failed", "status", "show", "cat", "help", "list-dependencies", "list-unit-files",
|
|
"is-system-running", "get-default", "show-environment", "list-jobs", "list-machines",
|
|
]
|
|
let destructiveActions: Set<String> = ["stop", "disable", "mask", "kill", "cancel", "revert"]
|
|
if safe.contains(action) { return .safe }
|
|
if destructiveActions.contains(action) {
|
|
return RiskAssessment(
|
|
risk: .destructive,
|
|
reasons: [String(localized: "Stops, disables, or removes service state.")]
|
|
)
|
|
}
|
|
return caution("systemctl")
|
|
}
|
|
|
|
private static func classifyService(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["--status-all", "-e"]) { return .safe }
|
|
let words = operands(args)
|
|
guard words.count > 1 else { return caution("service") }
|
|
let action = words[1]
|
|
if ["status", "check", "describe"].contains(action) { return .safe }
|
|
if ["stop", "disable", "delete", "remove"].contains(action) { return destructive("service") }
|
|
return caution("service")
|
|
}
|
|
|
|
private static func classifyJournalctl(_ args: [String]) -> RiskAssessment {
|
|
if args.contains(where: { $0.hasPrefix("--vacuum") }) { return destructive("journalctl") }
|
|
if hasFlag(args, ["--rotate", "--flush", "--sync", "--relinquish-var"]) { return caution("journalctl") }
|
|
return .safe
|
|
}
|
|
|
|
private static func classifySystemUtility(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
let words = operands(args)
|
|
let action = words.first ?? ""
|
|
let safeByName: [String: Set<String>] = [
|
|
"timedatectl": ["", "status", "show", "timesync-status", "show-timesync"],
|
|
"hostnamectl": ["", "status", "show"],
|
|
"localectl": [
|
|
"", "status", "list-locales", "list-keymaps", "list-x11-keymap-models", "list-x11-keymap-layouts",
|
|
],
|
|
"loginctl": [
|
|
"", "list-sessions", "session-status", "show-session", "list-users", "user-status", "show-user",
|
|
"list-seats", "seat-status", "show-seat",
|
|
],
|
|
"resolvectl": ["", "status", "query", "statistics", "show-cache"],
|
|
"networkctl": ["", "list", "status", "lldp", "label"],
|
|
]
|
|
let destructiveActions: Set<String> = [
|
|
"terminate-session", "terminate-user", "kill-session", "kill-user", "power-off", "reboot", "stop",
|
|
"down", "delete",
|
|
]
|
|
if safeByName[name]?.contains(action) == true { return .safe }
|
|
if destructiveActions.contains(action) { return destructive(name) }
|
|
return caution(name)
|
|
}
|
|
|
|
private static func classifyIPFamily(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
let words = operands(args, valueOptions: ["-b", "-rc", "-n", "-netns"])
|
|
if words.isEmpty { return .safe }
|
|
let destructiveActions: Set<String> = ["delete", "del", "flush", "destroy", "down"]
|
|
let cautionActions: Set<String> = ["add", "set", "change", "replace", "append", "prepend", "up"]
|
|
if words.contains(where: destructiveActions.contains) { return destructive(name) }
|
|
if words.contains(where: cautionActions.contains) {
|
|
if words.contains("show") || words.contains("list") || words.contains("get") { return .safe }
|
|
return caution(name)
|
|
}
|
|
return .safe
|
|
}
|
|
|
|
private static func classifyIfconfig(_ args: [String]) -> RiskAssessment {
|
|
let words = operands(args)
|
|
if words.count <= 1 { return .safe }
|
|
if words.contains(where: { ["down", "destroy", "delete", "-alias"].contains($0) }) {
|
|
return destructive("ifconfig")
|
|
}
|
|
return caution("ifconfig")
|
|
}
|
|
|
|
private static func classifyRoute(_ args: [String]) -> RiskAssessment {
|
|
let words = operands(args)
|
|
guard let action = words.first else { return .safe }
|
|
if ["show", "get", "monitor"].contains(action) { return .safe }
|
|
if ["delete", "del", "flush"].contains(action) { return destructive("route") }
|
|
return caution("route")
|
|
}
|
|
|
|
private static func classifyARP(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["-d", "--delete"]) { return destructive("arp") }
|
|
if hasFlag(args, ["-s", "--set", "-f", "--file"]) { return caution("arp") }
|
|
return .safe
|
|
}
|
|
|
|
private static func classifyEthtool(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["--reset"]) { return destructive("ethtool") }
|
|
let setters: Set<String> = [
|
|
"-s", "--change", "-A", "--pause", "-a", "-C", "--coalesce", "-E", "--change-eeprom", "-G",
|
|
"--set-ring", "-K", "--features", "-L", "--set-channels", "-N", "--config-nfc", "-P",
|
|
"--set-permaddr", "-Q", "--set-priv-flags", "-U", "--config-ntuple", "-X", "--set-rxfh-indir",
|
|
"-p", "--identify", "-r", "--negotiate", "-t", "--test",
|
|
]
|
|
return args.contains(where: setters.contains) ? caution("ethtool") : .safe
|
|
}
|
|
|
|
private static func classifyNvidiaSMI(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["-r", "--gpu-reset", "--reset-ecc-errors"]) { return destructive("nvidia-smi") }
|
|
let settingPrefixes = [
|
|
"-ac", "-c", "-lgc", "-pl", "-pm", "-rac", "-rgc", "--persistence-mode",
|
|
"--applications-clocks", "--power-limit", "--compute-mode", "--lock-", "--gom", "--ecc-config",
|
|
"--mig-config", "--gpu-target-temp", "--reset-gpu-clocks",
|
|
]
|
|
if args.contains(where: { argument in settingPrefixes.contains(where: argument.hasPrefix) }) {
|
|
return caution("nvidia-smi")
|
|
}
|
|
return .safe
|
|
}
|
|
|
|
private static func classifySysctl(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["-w", "--write"]) || args.contains(where: { !$0.hasPrefix("-") && $0.contains("=") }) {
|
|
return caution("sysctl")
|
|
}
|
|
return .safe
|
|
}
|
|
|
|
private static func classifySwapon(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["-s", "--show", "--summary"]) && operands(args).isEmpty { return .safe }
|
|
return caution("swapon")
|
|
}
|
|
|
|
private static func classifyUlimit(_ args: [String]) -> RiskAssessment {
|
|
if args.isEmpty || args == ["-a"] { return .safe }
|
|
let values = operands(args)
|
|
return values.isEmpty ? .safe : caution("ulimit")
|
|
}
|
|
|
|
private static func classifyPartitionEditor(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["-l", "--list", "--print", "-p"]) || args.contains("print") { return .safe }
|
|
return destructive(name)
|
|
}
|
|
|
|
private static func classifyZFS(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
let action = operands(args).first ?? ""
|
|
let safe: Set<String> = [
|
|
"list", "get", "status", "history", "events", "holds", "userspace", "groupspace", "version",
|
|
]
|
|
let destructiveActions: Set<String> = [
|
|
"destroy", "remove", "detach", "offline", "export", "clear", "release", "rollback",
|
|
]
|
|
if safe.contains(action) {
|
|
if name == "zpool", hasFlag(args, ["-c"]) { return caution(name) }
|
|
return .safe
|
|
}
|
|
if destructiveActions.contains(action) { return destructive(name) }
|
|
return caution(name)
|
|
}
|
|
|
|
private static func classifyDiskutil(_ args: [String]) -> RiskAssessment {
|
|
let action = operands(args).first ?? "list"
|
|
if ["list", "info", "activity", "listFilesystems", "apfs", "verifyDisk", "verifyVolume"].contains(action) {
|
|
if action != "apfs" || args.contains(where: { ["list", "listSnapshots"].contains($0) }) { return .safe }
|
|
}
|
|
let containsDestructiveOperation = args.contains {
|
|
let lowered = $0.lowercased()
|
|
return lowered.contains("erase") || lowered.contains("delete") || lowered.contains("remove")
|
|
}
|
|
if containsDestructiveOperation
|
|
|| ["partitionDisk", "zeroDisk", "randomDisk", "secureErase", "unmount", "unmountDisk"].contains(action)
|
|
{
|
|
return destructive("diskutil")
|
|
}
|
|
return caution("diskutil")
|
|
}
|
|
|
|
private static func classifyDefaults(_ args: [String]) -> RiskAssessment {
|
|
let action = operands(args).first ?? ""
|
|
if ["", "read", "find", "domains", "currentHost"].contains(action) { return .safe }
|
|
if action == "delete" { return destructive("defaults") }
|
|
return caution("defaults")
|
|
}
|
|
|
|
private static func classifyLaunchctl(_ args: [String]) -> RiskAssessment {
|
|
let action = operands(args).first ?? "list"
|
|
if ["list", "print", "print-disabled", "dumpstate", "procinfo", "blame", "help", "version"].contains(action) {
|
|
return .safe
|
|
}
|
|
if ["bootout", "unload", "remove", "stop", "kill", "disable"].contains(action) {
|
|
return destructive("launchctl")
|
|
}
|
|
return caution("launchctl")
|
|
}
|
|
|
|
private static func classifyLosetup(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["-d", "-D", "--detach", "--detach-all"]) { return destructive("losetup") }
|
|
if hasFlag(args, ["-a", "-l", "-j", "--all", "--list", "--associated"]) { return .safe }
|
|
return caution("losetup")
|
|
}
|
|
|
|
private static func classifyFirewall(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
if name == "nft", operands(args).first == "list" { return .safe }
|
|
if name == "ufw", ["status", "show", "version"].contains(operands(args).first ?? "status") { return .safe }
|
|
if name == "firewall-cmd",
|
|
args.contains(where: {
|
|
$0 == "--state" || $0.hasPrefix("--get-") || $0.hasPrefix("--list-") || $0.hasPrefix("--query-")
|
|
})
|
|
{
|
|
return .safe
|
|
}
|
|
if ["iptables", "ip6tables"].contains(name),
|
|
hasFlag(args, ["-L", "-S", "-C", "--list", "--list-rules", "--check"])
|
|
{
|
|
return .safe
|
|
}
|
|
return destructive(name)
|
|
}
|
|
|
|
private static func classifyKill(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
if shortFlags(args).contains("9") || args.contains("-KILL") || args.contains("--signal=KILL") {
|
|
return RiskAssessment(
|
|
risk: .destructive,
|
|
reasons: [String(localized: "SIGKILL gives the process no chance to clean up.")]
|
|
)
|
|
}
|
|
return caution(name)
|
|
}
|
|
|
|
private static func classifyRsync(_ args: [String]) -> RiskAssessment {
|
|
if args.contains(where: { $0 == "--delete" || $0.hasPrefix("--delete-") }) { return destructive("rsync") }
|
|
return caution("rsync")
|
|
}
|
|
|
|
private static func classifyCrontab(_ args: [String]) -> RiskAssessment {
|
|
if shortFlags(args).contains("r") { return destructive("crontab") }
|
|
if shortFlags(args).contains("l") { return .safe }
|
|
return caution("crontab")
|
|
}
|
|
|
|
// MARK: - Source control and hosted development
|
|
|
|
private static func classifyGit(_ args: [String]) -> RiskAssessment {
|
|
let words = operands(args, valueOptions: ["-C", "-c", "--git-dir", "--work-tree", "--namespace"])
|
|
guard let action = words.first else { return .safe }
|
|
let safe: Set<String> = [
|
|
"status", "log", "diff", "show", "blame", "grep", "rev-parse", "rev-list", "ls-files", "ls-tree",
|
|
"cat-file", "for-each-ref", "describe", "shortlog", "name-rev", "count-objects", "verify-commit",
|
|
"verify-tag", "show-ref", "merge-base", "range-diff", "version", "help",
|
|
]
|
|
if safe.contains(action) { return .safe }
|
|
if action == "branch" { return classifyGitBranch(args) }
|
|
if action == "config" {
|
|
if hasFlag(args, ["-l", "--list", "--get", "--get-all", "--get-regexp", "--show-origin", "--show-scope"]) {
|
|
return .safe
|
|
}
|
|
return caution("git config")
|
|
}
|
|
if action == "tag" {
|
|
if hasFlag(args, ["-d", "--delete"]) { return destructive("git tag") }
|
|
if words.count == 1 || hasFlag(args, ["-l", "--list", "--contains", "--merged", "--no-merged"]) {
|
|
return .safe
|
|
}
|
|
return caution("git tag")
|
|
}
|
|
if action == "remote" {
|
|
let operation = words.dropFirst().first
|
|
return operation == nil || ["show", "get-url"].contains(operation!) ? .safe : caution("git remote")
|
|
}
|
|
if action == "stash" {
|
|
let operation = words.dropFirst().first ?? "push"
|
|
if ["list", "show"].contains(operation) { return .safe }
|
|
if ["drop", "clear"].contains(operation) { return destructive("git stash") }
|
|
return caution("git stash")
|
|
}
|
|
if action == "push" {
|
|
if hasFlag(args, ["-f", "--force", "--force-with-lease", "--mirror", "--delete"]) {
|
|
return RiskAssessment(
|
|
risk: .destructive,
|
|
reasons: [String(localized: "Force push can overwrite remote history.")]
|
|
)
|
|
}
|
|
return caution("git push")
|
|
}
|
|
if action == "reset", hasFlag(args, ["--hard", "--merge", "--keep"]) { return destructive("git reset") }
|
|
if action == "clean" || action == "restore" || (action == "checkout" && args.contains("--")) {
|
|
return destructive("git \(action)")
|
|
}
|
|
if action == "reflog", words.dropFirst().contains(where: { ["delete", "expire"].contains($0) }) {
|
|
return destructive("git reflog")
|
|
}
|
|
if action == "worktree", words.dropFirst().contains(where: { ["remove", "prune"].contains($0) }) {
|
|
return destructive("git worktree")
|
|
}
|
|
return caution("git \(action)")
|
|
}
|
|
|
|
private static func classifyGitBranch(_ args: [String]) -> RiskAssessment {
|
|
if hasFlag(args, ["-d", "-D", "--delete"]) { return destructive("git branch") }
|
|
let flags = shortFlags(args)
|
|
let words = operands(args)
|
|
if words.count == 1 || flags.contains("a") || flags.contains("r") || flags.contains("l")
|
|
|| hasFlag(args, ["--list", "--show-current", "--contains", "--merged", "--no-merged"])
|
|
{
|
|
return .safe
|
|
}
|
|
return caution("git branch")
|
|
}
|
|
|
|
private static func classifyGitLFS(_ args: [String]) -> RiskAssessment {
|
|
let action = operands(args).first ?? "env"
|
|
if ["env", "ls-files", "status", "version", "help", "locks"].contains(action) { return .safe }
|
|
if ["prune", "uninstall"].contains(action) { return destructive("git-lfs") }
|
|
return caution("git-lfs")
|
|
}
|
|
|
|
private static func classifyHostedGit(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
let words = operands(args, valueOptions: ["-R", "--repo", "--hostname"])
|
|
let readActions: Set<String> = ["list", "view", "status", "diff", "checks", "watch", "browse", "search"]
|
|
let destructiveActions: Set<String> = ["delete", "remove", "purge"]
|
|
if words.contains(where: destructiveActions.contains) { return destructive(name) }
|
|
if words.contains(where: readActions.contains) { return .safe }
|
|
return caution(name)
|
|
}
|
|
|
|
// MARK: - Containers, clusters, and infrastructure
|
|
|
|
private static func classifyContainerCLI(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
let words = operands(
|
|
args,
|
|
valueOptions: [
|
|
"--config", "--context", "-H", "--host", "-l", "--log-level", "--tlscacert", "--tlscert",
|
|
"--tlskey",
|
|
])
|
|
guard let action = words.first else { return .safe }
|
|
let safe: Set<String> = [
|
|
"ps", "images", "logs", "inspect", "stats", "version", "info", "top", "search", "events", "port",
|
|
"diff", "history",
|
|
]
|
|
let destructiveActions: Set<String> = ["rm", "rmi", "prune", "kill", "stop", "remove"]
|
|
if safe.contains(action) { return .safe }
|
|
if destructiveActions.contains(action) { return destructive(name) }
|
|
if let objectAction = words.dropFirst().first {
|
|
if ["ls", "list", "inspect", "logs", "stats", "top", "history"].contains(objectAction) { return .safe }
|
|
if ["rm", "remove", "prune", "kill", "stop", "down"].contains(objectAction) {
|
|
return destructive(name)
|
|
}
|
|
}
|
|
return caution(name)
|
|
}
|
|
|
|
private static func classifyCompose(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
let action = operands(args, valueOptions: ["-f", "--file", "-p", "--project-name", "--profile"]).first ?? ""
|
|
if ["config", "images", "logs", "ls", "ps", "top", "version"].contains(action) { return .safe }
|
|
if ["down", "kill", "rm", "stop"].contains(action) { return destructive(name) }
|
|
return caution(name)
|
|
}
|
|
|
|
private static func classifyKubernetes(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
let words = operands(args, valueOptions: kubernetesGlobalValueOptions)
|
|
guard let action = words.first else { return .safe }
|
|
let safe: Set<String> = [
|
|
"get", "describe", "logs", "explain", "top", "version", "api-resources", "api-versions", "cluster-info",
|
|
"events", "diff",
|
|
]
|
|
if safe.contains(action) { return .safe }
|
|
if action == "auth", words.dropFirst().first == "can-i" { return .safe }
|
|
if action == "config" {
|
|
let operation = words.dropFirst().first ?? "view"
|
|
if ["view", "get-contexts", "current-context"].contains(operation) { return .safe }
|
|
if operation == "unset" { return destructive("\(name) config") }
|
|
return caution("\(name) config")
|
|
}
|
|
if action == "rollout" {
|
|
let operation = words.dropFirst().first ?? "status"
|
|
if ["status", "history"].contains(operation) { return .safe }
|
|
return caution("\(name) rollout")
|
|
}
|
|
if ["delete", "drain"].contains(action) { return destructive(name) }
|
|
return caution(name)
|
|
}
|
|
|
|
private static func classifyHelm(_ args: [String]) -> RiskAssessment {
|
|
let words = operands(
|
|
args,
|
|
valueOptions: [
|
|
"--kube-context", "--kubeconfig", "--namespace", "-n", "--registry-config", "--repository-cache",
|
|
"--repository-config",
|
|
])
|
|
let action = words.first ?? "help"
|
|
if ["list", "status", "history", "get", "show", "search", "template", "lint", "version", "env", "verify"]
|
|
.contains(action)
|
|
{
|
|
if hasFlag(args, ["--post-renderer"]) { return caution("helm") }
|
|
return .safe
|
|
}
|
|
if ["uninstall", "delete"].contains(action) { return destructive("helm") }
|
|
return caution("helm")
|
|
}
|
|
|
|
private static func classifyTerraform(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
let words = operands(args)
|
|
let action = words.first ?? "help"
|
|
if ["version", "show", "output", "providers", "graph", "validate", "modules", "metadata"].contains(action) {
|
|
return .safe
|
|
}
|
|
if action == "plan" { return hasFlag(args, ["-out", "--out"]) ? caution(name) : .safe }
|
|
if action == "fmt" { return hasFlag(args, ["-check", "--check"]) ? .safe : caution(name) }
|
|
if action == "state" {
|
|
let operation = words.dropFirst().first ?? "list"
|
|
if ["list", "show", "pull"].contains(operation) { return .safe }
|
|
if operation == "rm" { return destructive("\(name) state") }
|
|
return caution("\(name) state")
|
|
}
|
|
if action == "workspace" {
|
|
let operation = words.dropFirst().first ?? "show"
|
|
if ["list", "show"].contains(operation) { return .safe }
|
|
if operation == "delete" { return destructive("\(name) workspace") }
|
|
return caution("\(name) workspace")
|
|
}
|
|
if action == "destroy" || action == "force-unlock" || (action == "apply" && args.contains("-destroy")) {
|
|
return destructive(name)
|
|
}
|
|
return caution(name)
|
|
}
|
|
|
|
// MARK: - Packages, cloud CLIs, databases, and services
|
|
|
|
private static func classifySystemPackageManager(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
if name == "dpkg" {
|
|
if hasFlag(args, ["-l", "-s", "-L", "-S", "--list", "--status", "--listfiles", "--search", "--verify"]) {
|
|
return .safe
|
|
}
|
|
if hasFlag(args, ["-r", "-P", "--remove", "--purge"]) { return destructive(name) }
|
|
return caution(name)
|
|
}
|
|
if name == "rpm" {
|
|
if shortFlags(args).contains("q") || hasFlag(args, ["--query", "--verify", "-V"]) { return .safe }
|
|
if shortFlags(args).contains("e") || hasFlag(args, ["--erase"]) { return destructive(name) }
|
|
return caution(name)
|
|
}
|
|
if name == "softwareupdate" {
|
|
if hasFlag(args, ["-l", "--list", "--history"]) { return .safe }
|
|
return caution(name)
|
|
}
|
|
if name == "pacman", shortFlags(args).contains("Q") { return .safe }
|
|
let action = operands(args).first ?? ""
|
|
let safe: Set<String> = [
|
|
"list", "show", "search", "info", "query", "policy", "depends", "rdepends", "repolist", "check",
|
|
"check-update", "audit", "outdated", "why", "uses", "leaves", "config", "doctor",
|
|
]
|
|
let destructiveActions: Set<String> = ["remove", "uninstall", "delete", "purge", "autoremove", "erase"]
|
|
if name == "apt-cache", action != "gencaches" { return .safe }
|
|
if safe.contains(action) { return .safe }
|
|
if destructiveActions.contains(action) { return destructive(name) }
|
|
return caution(name)
|
|
}
|
|
|
|
private static func classifyDevelopmentTool(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
if name == "go", args.first == "env" {
|
|
return hasFlag(args, ["-w", "-u"]) ? caution(name) : .safe
|
|
}
|
|
let action = operands(args).first ?? ""
|
|
if action == "audit", operands(args).contains("fix") { return caution(name) }
|
|
let safe: Set<String> = [
|
|
"list", "ls", "show", "info", "view", "search", "outdated", "audit", "why", "explain", "tree",
|
|
"metadata", "freeze", "check", "index", "doc", "env", "version", "help",
|
|
]
|
|
let destructiveActions: Set<String> = ["uninstall", "remove", "rm", "erase", "purge"]
|
|
if safe.contains(action) { return .safe }
|
|
if destructiveActions.contains(action) { return destructive(name) }
|
|
return caution(name)
|
|
}
|
|
|
|
private static func classifyCloudCLI(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
let valueOptions: Set<String> = [
|
|
"--profile", "--region", "--endpoint-url", "--output", "--query", "--project", "--account",
|
|
"--subscription",
|
|
"--configuration",
|
|
]
|
|
let words = operands(args, valueOptions: valueOptions)
|
|
if words.isEmpty { return .safe }
|
|
let readVerbs: Set<String> = [
|
|
"list", "ls", "show", "describe", "get", "head", "query", "scan", "search", "inspect", "status",
|
|
"version", "info", "whoami", "get-value", "get-iam-policy", "test-iam-permissions",
|
|
]
|
|
let destructiveVerbs: Set<String> = [
|
|
"delete", "remove", "rm", "purge", "destroy", "terminate", "stop", "disable", "revoke", "deallocate",
|
|
]
|
|
func matches(_ word: String, verbs: Set<String>) -> Bool {
|
|
verbs.contains(word) || verbs.contains(where: { word.hasPrefix("\($0)-") })
|
|
}
|
|
if args.contains("--delete") || words.contains(where: { matches($0, verbs: destructiveVerbs) }) {
|
|
return destructive(name)
|
|
}
|
|
if words.contains(where: { matches($0, verbs: readVerbs) }) { return .safe }
|
|
return caution(name)
|
|
}
|
|
|
|
private static func classifyDatabaseClient(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
if name == "psql", hasFlag(args, ["-l", "--list"]), !hasFlag(args, ["-c", "--command", "-f", "--file"]) {
|
|
return .safe
|
|
}
|
|
if name == "sqlite3", args.contains("-readonly") {
|
|
let commands = operands(args).dropFirst()
|
|
if !commands.isEmpty,
|
|
commands.allSatisfy({
|
|
$0.hasPrefix(".schema") || $0.hasPrefix(".tables") || $0.hasPrefix(".indexes")
|
|
})
|
|
{
|
|
return .safe
|
|
}
|
|
}
|
|
if let statement = embeddedStatement(args), containsDestructiveSQL(statement) { return destructive(name) }
|
|
return caution(name)
|
|
}
|
|
|
|
private static func classifyDump(_ args: [String], name: String, outputFlags: Set<String>) -> RiskAssessment {
|
|
hasFlag(args, outputFlags) ? caution(name) : .safe
|
|
}
|
|
|
|
private static func classifyMySQLAdmin(_ args: [String]) -> RiskAssessment {
|
|
let action =
|
|
operands(args, valueOptions: ["-h", "--host", "-P", "--port", "-u", "--user", "-p", "--password"])
|
|
.first ?? "status"
|
|
if ["ping", "status", "version", "processlist", "variables", "extended-status"].contains(action) {
|
|
return .safe
|
|
}
|
|
if ["drop", "shutdown", "kill"].contains(action) { return destructive("mysqladmin") }
|
|
return caution("mysqladmin")
|
|
}
|
|
|
|
private static func classifyRedis(_ args: [String]) -> RiskAssessment {
|
|
let words = operands(
|
|
args,
|
|
valueOptions: [
|
|
"-h", "-p", "-s", "-a", "--user", "--pass", "-n", "--dbnum", "--cluster", "--latency-history",
|
|
])
|
|
let action = words.first?.uppercased() ?? "PING"
|
|
let operation = words.dropFirst().first?.uppercased()
|
|
let safe: Set<String> = [
|
|
"PING", "INFO", "DBSIZE", "GET", "MGET", "EXISTS", "TYPE", "TTL", "PTTL", "SCAN", "SSCAN", "HSCAN",
|
|
"ZSCAN", "KEYS", "RANDOMKEY", "STRLEN", "GETRANGE", "HGET", "HGETALL", "HLEN", "LRANGE", "LLEN",
|
|
"SMEMBERS", "SCARD", "ZRANGE", "ZCARD", "ROLE", "TIME", "CLIENT", "COMMAND",
|
|
]
|
|
let destructiveActions: Set<String> = [
|
|
"DEL", "UNLINK", "FLUSHALL", "FLUSHDB", "SHUTDOWN", "REPLICAOF", "SLAVEOF",
|
|
]
|
|
if action == "CLIENT" {
|
|
if operation.map({ ["KILL", "PAUSE", "UNBLOCK"].contains($0) }) == true {
|
|
return destructive("redis-cli")
|
|
}
|
|
if operation.map({ ["LIST", "INFO", "GETNAME", "ID", "TRACKINGINFO"].contains($0) }) == true {
|
|
return .safe
|
|
}
|
|
return caution("redis-cli")
|
|
}
|
|
if action == "CONFIG" { return operation == "GET" ? .safe : caution("redis-cli") }
|
|
if safe.contains(action) { return .safe }
|
|
if destructiveActions.contains(action) { return destructive("redis-cli") }
|
|
return caution("redis-cli")
|
|
}
|
|
|
|
private static func classifyProcessManager(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
let action = operands(args).first ?? "status"
|
|
let safe: Set<String> = [
|
|
"status", "list", "ls", "pid", "version", "describe", "info", "logs", "jlist", "prettylist", "monit",
|
|
]
|
|
let destructiveActions: Set<String> = ["stop", "delete", "remove", "kill", "shutdown", "uninstall"]
|
|
if safe.contains(action) { return .safe }
|
|
if destructiveActions.contains(action) { return destructive(name) }
|
|
return caution(name)
|
|
}
|
|
|
|
private static func classifyServerBinary(_ name: String, _ args: [String]) -> RiskAssessment {
|
|
if args.contains(where: { ["stop", "graceful-stop"].contains($0) }) { return destructive(name) }
|
|
if name == "nginx", args.contains("-s"), args.contains(where: { ["stop", "quit"].contains($0) }) {
|
|
return destructive(name)
|
|
}
|
|
if name == "nginx", hasFlag(args, ["-t", "-T", "-V", "-v"]) { return .safe }
|
|
if ["apachectl", "apache2ctl", "httpd"].contains(name),
|
|
args.contains(where: { ["configtest", "-t", "-S", "-V", "-v"].contains($0) })
|
|
{
|
|
return .safe
|
|
}
|
|
if name == "haproxy", hasFlag(args, ["-c", "-vv", "-v"]) { return .safe }
|
|
if name == "caddy",
|
|
["validate", "adapt", "version", "list-modules", "environ"].contains(operands(args).first ?? "")
|
|
{
|
|
return .safe
|
|
}
|
|
return caution(name)
|
|
}
|
|
|
|
// MARK: - Shared helpers
|
|
|
|
private static func caution(_ displayName: String) -> RiskAssessment {
|
|
RiskAssessment(
|
|
risk: .caution,
|
|
reasons: [String(localized: "`\(displayName)` changes system, file, or external state.")]
|
|
)
|
|
}
|
|
|
|
private static func destructive(_ displayName: String) -> RiskAssessment {
|
|
RiskAssessment(
|
|
risk: .destructive,
|
|
reasons: [String(localized: "`\(displayName)` can irreversibly destroy data or state.")]
|
|
)
|
|
}
|
|
|
|
private static func hasFlag(_ args: [String], _ names: Set<String>) -> Bool {
|
|
args.contains { argument in
|
|
names.contains(argument)
|
|
|| names.contains(String(argument.split(separator: "=", maxSplits: 1).first ?? Substring(argument)))
|
|
|| names.contains(where: { name in
|
|
name.count == 2 && name.hasPrefix("-") && !name.hasPrefix("--")
|
|
&& argument.hasPrefix("-") && !argument.hasPrefix("--")
|
|
&& argument.dropFirst().contains(name.last!)
|
|
})
|
|
}
|
|
}
|
|
|
|
private static func shortFlags(_ args: [String]) -> Set<Character> {
|
|
Set(
|
|
args.filter { $0.hasPrefix("-") && !$0.hasPrefix("--") }
|
|
.flatMap { $0.dropFirst().prefix { $0 != "=" } }
|
|
)
|
|
}
|
|
|
|
private static func embeddedStatement(_ args: [String]) -> String? {
|
|
let options: Set<String> = ["-c", "--command", "-e", "--execute", "--eval"]
|
|
for (index, argument) in args.enumerated() {
|
|
if let equals = argument.firstIndex(of: "="), options.contains(String(argument[..<equals])) {
|
|
return String(argument[argument.index(after: equals)...])
|
|
}
|
|
if options.contains(argument), args.indices.contains(index + 1) { return args[index + 1] }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func containsDestructiveSQL(_ statement: String) -> Bool {
|
|
let pattern = #"(?i)\b(DROP|TRUNCATE|DELETE|FLUSH|SHUTDOWN)\b"#
|
|
return statement.range(of: pattern, options: .regularExpression) != nil
|
|
}
|
|
|
|
private static let readOnlyCommands: Set<String> = [
|
|
"ls", "ll", "cat", "bat", "less", "more", "head", "tail", "df", "du", "ps", "top", "htop", "btop",
|
|
"free", "uptime", "uname", "whoami", "id", "pwd", "which", "whereis", "type", "file", "stat", "wc",
|
|
"cut", "tr", "column", "jq", "grep", "egrep", "fgrep", "ag", "printenv", "echo",
|
|
"printf", "lsof", "netstat", "ss", "ping", "traceroute", "dig", "nslookup", "host", "md5sum", "sha1sum",
|
|
"sha256sum", "cksum", "cmp", "basename", "dirname", "realpath", "readlink", "groups", "last", "w", "who",
|
|
"lsblk", "vmstat", "iostat", "sar", "nproc", "lscpu", "lsmem", "lspci", "lsusb", "true", "false", "test",
|
|
"[", "pgrep", "pidof", "pstree", "mpstat", "pidstat", "findmnt", "getconf", "locale", "getent",
|
|
"iptables-save", "ip6tables-save",
|
|
"sestatus", "getenforce", "lsattr", "lsns", "dpkg-query", "zcat", "zgrep", "zless", "bzcat", "tac", "nl",
|
|
"rev", "fold", "expand", "unexpand", "paste", "join", "comm", "seq", "numfmt", "base64", "strings", "od",
|
|
"hexdump", "arch", "pvs", "vgs", "lvs", "pvdisplay", "vgdisplay", "lvdisplay", "pg_isready", "mongostat",
|
|
"mongotop", "mysqlshow",
|
|
]
|
|
|
|
private static let cautionCommands: Set<String> = [
|
|
"mv", "cp", "ln", "mkdir", "touch", "install", "tee", "make", "cmake", "ninja", "ssh-keygen",
|
|
"ssh-copy-id", "scp", "sftp", "unzip", "gunzip", "gzip", "zip", "xz", "bzip2", "curl", "wget", "fetch",
|
|
"http", "https", "xh", "nano", "vim", "vi", "emacs", "export", "source", ".", "alias", "unalias",
|
|
"usermod", "groupadd", "systemd-run", "screen", "tmux", "at", "batch", "renice", "createdb", "pg_restore",
|
|
"mongoexport", "mongodump", "mongoimport", "mongorestore", "vgcreate", "lvcreate", "pvcreate", "vgextend",
|
|
"lvextend", "resize2fs",
|
|
]
|
|
|
|
private static let destructiveCommands: Set<String> = [
|
|
"dd", "mkfs", "mkfs.ext4", "mkfs.xfs", "mkfs.btrfs", "shred", "wipefs", "truncate", "userdel", "groupdel",
|
|
"deluser", "delgroup", "reboot", "shutdown", "halt", "poweroff", "init", "swapoff", "umount", "setenforce",
|
|
"chroot", "mkswap", "vgremove", "lvremove", "pvremove", "dropdb", "iptables-restore", "ip6tables-restore",
|
|
]
|
|
|
|
private static let argumentSensitiveCommands: Set<String> = [
|
|
"rm", "find", "chmod", "chown", "chgrp", "awk", "sort", "tree", "diff", "rg", "uniq", "blkid", "sed",
|
|
"yq", "xxd", "tar", "date", "hostname",
|
|
"mount", "dmesg", "systemctl", "service", "journalctl", "timedatectl", "hostnamectl", "localectl", "loginctl",
|
|
"resolvectl", "networkctl", "ip", "bridge", "tc", "ifconfig", "route", "arp", "ethtool", "nvidia-smi",
|
|
"sensors", "sysctl", "swapon", "ulimit", "fdisk", "parted", "gdisk", "zfs", "zpool", "diskutil", "defaults",
|
|
"launchctl", "losetup", "iptables", "ip6tables", "nft", "ufw", "firewall-cmd", "kill", "pkill", "killall",
|
|
"rsync", "crontab", "git", "git-lfs", "gh", "glab", "docker", "podman", "nerdctl", "docker-compose",
|
|
"podman-compose", "kubectl", "oc", "helm", "terraform", "tofu", "ansible", "ansible-playbook",
|
|
"ansible-vault", "ansible-config", "ansible-doc", "ansible-inventory", "ansible-lint", "aws", "az", "gcloud",
|
|
"apt", "apt-get", "apt-cache", "dpkg", "rpm", "yum", "dnf", "pacman", "apk", "zypper", "snap",
|
|
"flatpak", "brew", "softwareupdate", "pkg", "npm", "pnpm", "yarn", "bun", "pip", "pip3", "pipx", "gem",
|
|
"cargo", "go", "psql", "mysql", "sqlite3", "mongosh", "pg_dump", "mysqldump", "mysqladmin", "redis-cli",
|
|
"supervisorctl", "pm2", "nginx", "apachectl", "apache2ctl", "httpd", "haproxy", "caddy", "command", "builtin",
|
|
"sudo", "doas", "env", "exec", "nice", "ionice", "timeout", "stdbuf", "setsid", "nohup", "watch", "busybox",
|
|
"toybox", "xargs", "parallel", "eval", "sh", "bash", "zsh", "dash", "fish", "ksh", "ssh",
|
|
]
|
|
}
|