commit b94789993dfbd3f33a862a2ece34150b6a7813a4 Author: Dev Date: Sat Sep 13 01:48:31 2025 +0300 fuckoff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cf5184 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Binaries +/fo +*.exe +*.dll +*.so +*.dylib + +# Build +/build/ +/dist/ + +# Go stuff +vendor/ +*.test + +# IDEs +.vscode/ +.idea/ +*.iml \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eb4b687 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 iwasforcedtobehere + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3c9439e --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +BINARY=fo +PKG=git.gostacks.org/iwasforcedtobehere/fo/cmd/fo + +.PHONY: build tidy clean + +build: + go build -o $(BINARY) $(PKG) + +clean: + rm -f $(BINARY) + + tidy: + go mod tidy diff --git a/README.md b/README.md new file mode 100644 index 0000000..379c1e0 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# fo — (FUCK OFF) for ports + + +## What is this? +`fo` is a cross-platform command-line tool that: +- Detects which process is hogging a TCP port (e.g., `3000`). +- Tries to end it gracefully; if it plays tough, we go full send (`kill -9` / `taskkill /F`). +- Prompts for `sudo` if your mortal shell lacks the power. +- Prints a colorful report with PID, name, and how long it’s been loitering. +- Drops a few lighthearted quips, and yes, the occasional fuck. + +In short: `fo 3000` → Port 3000 is yours again. + +## Demo +``` +$ fo 3000 +Scanning for suspicious activity on port 3000... +Found PID 1234 (node) possibly hogging port 3000 for 2h 13m 05s. +Process survived the gentle nudge. Time for the big red button. +Killed process 1234 (node) which was partying on port 3000 for 2h 13m 05s. +Port 3000’s unwanted guest has been evicted with extreme prejudice! +``` + +## Installation +- Go 1.21+ +- Linux/macOS/Windows + +### go install +``` +go install git.gostacks.org/iwasforcedtobehere/fo/cmd/fo@latest +``` +This will give you a `fo` binary in your `$GOPATH/bin` or `$GOBIN`. + +### From source +``` +git clone https://git.gostacks.org/iwasforcedtobehere/fo +cd fo +make build # or go build ./cmd/fo +``` + +## Usage +``` +fo + +Examples: + fo 3000 # tell 3000 to fuck off + fo 8080 # 8080 is not a personality trait + fo --help # usage info +``` + +## How it works +- Input validation: accepts only ports 1–65535. +- Process detection: + - Linux/macOS: prefers `lsof`, falls back to `ss` or `netstat`. + - Windows: uses `netstat -ano` + process introspection. +- Duration: approximates how long the process has been up via `gopsutil` (process create time). If we can’t be sure, we try to be honest and say “unknown duration”. +- Killing strategy: + - Gentle: `kill -TERM` or `taskkill /PID`. + - Force: `kill -KILL` or `taskkill /F`. + - Permissions: if the OS says “nope”, we’ll prompt for `sudo` and try again. +- Output: colorful, readable, and a bit cheeky. + +## Philosophy (polite satire included) +- Ports are public spaces. If your process is double-parking on `:3000`, it can fuck off. +- We try kindness first, then consequences. It’s DevOps, not diplomacy. +- Tooling should be honest, helpful, and slightly entertaining when it fails. + +## Dev plan (tl;dr of the build) +- CLI parsing with `flag`. +- Cross-platform port → PID resolution. +- Uptime via `gopsutil`. +- Elevation fallback via `sudo` where needed. +- Colorful output via `fatih/color`. + +## Notes +- On Linux/macOS, you may need `lsof`, `ss`, or `netstat` installed. +- On Windows, `netstat` and `taskkill` are built-in. +- `sudo` prompts are interactive in your terminal. + +## Roadmap +- Add `--json` output for scripts and CI. +- Add `--udp` support. +- Add `--ask` mode to confirm before killing when multiple contenders exist. +- Homebrew/Scoop packaging for easy install. + +## License +MIT. Be kind. Don’t be reckless in production unless your SLAs can take a hit. + +## Credits +- Built in Go. Colored by `fatih/color`. Informed by `gopsutil`. +- Proudly hosted under: https://git.gostacks.org/iwasforcedtobehere/fo diff --git a/cmd/fo/main.go b/cmd/fo/main.go new file mode 100644 index 0000000..e37b4ca --- /dev/null +++ b/cmd/fo/main.go @@ -0,0 +1,387 @@ +package main + +import ( + "bufio" + "errors" + "flag" + "fmt" + "math/rand" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "time" + + "github.com/fatih/color" + "github.com/shirou/gopsutil/v3/process" +) + +type ProcInfo struct { + PID int32 + Name string + Started time.Time +} + +func main() { + var showHelp bool + flag.BoolVar(&showHelp, "help", false, "Show help") + flag.BoolVar(&showHelp, "h", false, "Show help") + flag.Parse() + + if showHelp || flag.NArg() != 1 { + usage() + if flag.NArg() != 1 { + os.Exit(2) + } + return + } + + portStr := flag.Arg(0) + port, err := strconv.Atoi(portStr) + if err != nil || port < 1 || port > 65535 { + color.Red("Invalid port: %s (must be 1-65535)", portStr) + os.Exit(2) + } + + color.Cyan("Scanning for suspicious activity on port %d...", port) + + pid, name, err := findPIDByPort(port) + if err != nil { + color.Red("No process found for port %d: %v", port, err) + os.Exit(1) + } + + info := ProcInfo{PID: int32(pid), Name: name} + if p, err := process.NewProcess(int32(pid)); err == nil { + if ct, err := p.CreateTime(); err == nil && ct > 0 { + info.Started = time.Unix(0, ct*int64(time.Millisecond)) + } + if n, err := p.Name(); err == nil && n != "" { + info.Name = n + } + } + + // Duration estimation + var durStr string + if !info.Started.IsZero() { + dur := time.Since(info.Started) + durStr = humanizeDuration(dur) + } else { + durStr = "unknown duration" + } + + color.Yellow("Found PID %d (%s) possibly hogging port %d for %s.", info.PID, info.Name, port, durStr) + + // Try graceful kill first + if err := tryKill(info.PID, false); err != nil { + if isPermissionError(err) { + color.Magenta("Insufficient permissions. Attempting elevated termination (we'll ask nicely, then not-so-nicely).") + if err := tryKillElevated(info.PID, false); err != nil { + if !isNoSuchProcessError(err) && processExists(info.PID) { + color.Red("Failed gentle kill even with elevation: %v", err) + } + } + } else if !isNoSuchProcessError(err) { + color.Red("Gentle kill error: %v", err) + } + } + + // Give the process a moment to exit after TERM + waitForExit(info.PID, 1500*time.Millisecond) + + // Check if process still exists + alive := processExists(info.PID) + if alive { + color.Yellow("Process survived the gentle nudge. Time for the big red button.") + if err := tryKill(info.PID, true); err != nil { + if isNoSuchProcessError(err) { + // It's gone — treat as success + } else if isPermissionError(err) { + if err := tryKillElevated(info.PID, true); err != nil && !isNoSuchProcessError(err) { + // Re-check after elevation attempt + waitForExit(info.PID, 800*time.Millisecond) + if processExists(info.PID) { + color.Red("Even the big red button failed: %v", err) + os.Exit(1) + } + } + } else { + // Transient errors may happen; wait and verify + waitForExit(info.PID, 800*time.Millisecond) + if processExists(info.PID) { + color.Red("Force kill failed: %v", err) + os.Exit(1) + } + } + } + + // After force, wait briefly and confirm + waitForExit(info.PID, 500*time.Millisecond) + } + + // Final confirmation + if processExists(info.PID) { + color.Red("The process refuses to die. Consider manual intervention.") + os.Exit(1) + } + + quips := []string{ + "That pesky process is toast!", + fmt.Sprintf("Off with its PID! Bye-bye %d.", info.PID), + fmt.Sprintf("Port %d’s unwanted guest has been evicted with extreme prejudice!", port), + fmt.Sprintf("It’s RIP time for PID %d—no more freeloading!", info.PID), + "We came, we saw, we `kill -9`'d.", + "Sometimes you just have to say: fuck it, and reclaim the port.", + } + rand.Seed(time.Now().UnixNano()) + msg := quips[rand.Intn(len(quips))] + + color.Green("Killed process %d (%s) which was partying on port %d for %s.", info.PID, info.Name, port, durStr) + color.HiBlue(msg) +} + +func usage() { + fmt.Printf("fo — ruthlessly liberate a busy port (fo stands for 'FUCK OFF')\n") + fmt.Printf("Usage: fo \n") + fmt.Printf("Example: fo 3000\n") +} + +func humanizeDuration(d time.Duration) string { + if d < time.Minute { + return fmt.Sprintf("%ds", int(d.Seconds())) + } + hours := int(d.Hours()) + minutes := int(d.Minutes()) % 60 + seconds := int(d.Seconds()) % 60 + if hours > 0 { + return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds) + } + return fmt.Sprintf("%dm %ds", minutes, seconds) +} + +func findPIDByPort(port int) (int, string, error) { + if runtime.GOOS == "windows" { + return findPIDByPortWindows(port) + } + // Prefer lsof, fallback to ss/netstat + if pid, name, err := findByLsof(port); err == nil { + return pid, name, nil + } + if pid, name, err := findBySS(port); err == nil { + return pid, name, nil + } + return findByNetstat(port) +} + +func findByLsof(port int) (int, string, error) { + cmd := exec.Command("bash", "-lc", fmt.Sprintf("lsof -nP -iTCP:%d -sTCP:LISTEN -Fp -a +c 15 | sed -n 's/^p//p' | head -n1", port)) + out, err := cmd.Output() + if err != nil || len(out) == 0 { + return 0, "", errors.New("lsof no match") + } + pidStr := strings.TrimSpace(string(out)) + pid, _ := strconv.Atoi(pidStr) + if pid == 0 { + return 0, "", errors.New("invalid pid from lsof") + } + name := procName(int32(pid)) + return pid, name, nil +} + +func findBySS(port int) (int, string, error) { + // ss -ltnp | grep :PORT + cmd := exec.Command("bash", "-lc", fmt.Sprintf("ss -ltnp | grep ':%d' || true", port)) + out, err := cmd.Output() + if err != nil || len(out) == 0 { + return 0, "", errors.New("ss no match") + } + line := string(out) + // look for pid=1234 + pid := parsePIDFromSS(line) + if pid == 0 { + return 0, "", errors.New("no pid in ss") + } + name := procName(int32(pid)) + return pid, name, nil +} + +func parsePIDFromSS(s string) int { + // typical: users:(('node',pid=1234,fd=23)) + idx := strings.Index(s, "pid=") + if idx == -1 { + return 0 + } + rest := s[idx+4:] + var b strings.Builder + for _, r := range rest { + if r >= '0' && r <= '9' { + b.WriteRune(r) + } else { + break + } + } + pid, _ := strconv.Atoi(b.String()) + return pid +} + +func findByNetstat(port int) (int, string, error) { + cmd := exec.Command("bash", "-lc", fmt.Sprintf("netstat -nlp 2>/dev/null | grep ':%d' || true", port)) + out, err := cmd.Output() + if err != nil || len(out) == 0 { + return 0, "", errors.New("netstat no match") + } + line := string(out) + // look for pid/program-name + pid := parsePIDFromNetstat(line) + if pid == 0 { + return 0, "", errors.New("no pid in netstat") + } + name := procName(int32(pid)) + return pid, name, nil +} + +func parsePIDFromNetstat(s string) int { + // typical: ... LISTEN 1234/node + // find field with slash + fields := strings.Fields(s) + for _, f := range fields { + if strings.Contains(f, "/") { + parts := strings.SplitN(f, "/", 2) + if len(parts) == 2 { + pid, _ := strconv.Atoi(parts[0]) + if pid > 0 { + return pid + } + } + } + } + return 0 +} + +func findPIDByPortWindows(port int) (int, string, error) { + // netstat -ano | findstr :PORT + cmd := exec.Command("cmd", "/C", fmt.Sprintf("netstat -ano | findstr :%d", port)) + out, err := cmd.Output() + if err != nil || len(out) == 0 { + return 0, "", errors.New("netstat no match") + } + scanner := bufio.NewScanner(strings.NewReader(string(out))) + var line string + for scanner.Scan() { + l := scanner.Text() + if strings.Contains(l, fmt.Sprintf(":%d", port)) && strings.Contains(l, "LISTENING") { + line = l + break + } + } + if line == "" { + return 0, "", errors.New("no listening line") + } + fields := strings.Fields(line) + if len(fields) < 5 { + return 0, "", errors.New("unexpected netstat format") + } + pid, _ := strconv.Atoi(fields[len(fields)-1]) + name := windowsProcessName(pid) + return pid, name, nil +} + +func windowsProcessName(pid int) string { + if p, err := process.NewProcess(int32(pid)); err == nil { + if n, err := p.Name(); err == nil { + return n + } + } + return "unknown" +} + +func procName(pid int32) string { + if p, err := process.NewProcess(pid); err == nil { + if n, err := p.Name(); err == nil && n != "" { + return n + } + if e, err := p.Exe(); err == nil && e != "" { + parts := strings.Split(e, "/") + return parts[len(parts)-1] + } + } + return "unknown" +} + +func tryKill(pid int32, force bool) error { + if runtime.GOOS == "windows" { + // taskkill /PID pid [/F] + args := []string{"/C", "taskkill", "/PID", fmt.Sprintf("%d", pid)} + if force { + args = append(args, "/F") + } + cmd := exec.Command("cmd", args...) + return cmd.Run() + } + sig := "-TERM" + if force { + sig = "-KILL" + } + cmd := exec.Command("kill", sig, fmt.Sprintf("%d", pid)) + return cmd.Run() +} + +func tryKillElevated(pid int32, force bool) error { + if runtime.GOOS == "windows" { + // No sudo on Windows; rely on current privileges + return errors.New("elevation not supported on windows") + } + sig := "-TERM" + if force { + sig = "-KILL" + } + // Use sudo to prompt interactively in the terminal + cmd := exec.Command("sudo", "kill", sig, fmt.Sprintf("%d", pid)) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func isPermissionError(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "operation not permitted") || strings.Contains(msg, "permission") || strings.Contains(msg, "access is denied") +} + +func isNoSuchProcessError(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "no such process") || strings.Contains(msg, "process not found") || strings.Contains(msg, "esrch") +} + +func processExists(pid int32) bool { + if pid <= 0 { + return false + } + if _, err := os.Stat(fmt.Sprintf("/proc/%d", pid)); err == nil { + return true + } + // Fallback via gopsutil (cross-platform) + if p, err := process.NewProcess(pid); err == nil { + if ok, err := p.IsRunning(); err == nil { + return ok + } + } + return false +} + +func waitForExit(pid int32, timeout time.Duration) { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if !processExists(pid) { + return + } + time.Sleep(100 * time.Millisecond) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cacfaea --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module git.gostacks.org/iwasforcedtobehere/fo + +go 1.21 + +require ( + github.com/fatih/color v1.18.0 + github.com/shirou/gopsutil/v3 v3.24.5 +) + +require ( + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/sys v0.25.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aa6d16f --- /dev/null +++ b/go.sum @@ -0,0 +1,45 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=