package tools import ( "bufio" "context" "encoding/json" "fmt" "net" "os" "os/exec" "runtime" "strings" ) // systemInfoTool reúne datos básicos del sistema (§46, §49, §50, §51) leyendo // directamente /proc, /etc/os-release y el PATH: no ejecuta comandos, así // que es seguro en cualquier modo de ejecución. type systemInfoTool struct{} func (systemInfoTool) Name() string { return "system_info" } func (systemInfoTool) Description() string { return "Report OS, kernel, architecture, hostname, CPU, memory, uptime, privileges (uid/euid, sudo/doas), init system, package managers, mounted filesystems and network interfaces. Read-only." } func (systemInfoTool) Schema() map[string]any { return obj(nil, map[string]any{}) } func (systemInfoTool) Run(_ context.Context, _ *Env, raw json.RawMessage) Result { var none struct{} if err := decode(raw, &none); err != nil { return fail("system_info", err) } return Result{OK: true, ExitCode: 0, Summary: "system_info", Output: CollectSystemInfo()} } // CollectSystemInfo produce un informe textual. Los datos que no se pueden // leer aparecen como UNKNOWN en lugar de suponerse. func CollectSystemInfo() string { var b strings.Builder kv := func(k, v string) { if v == "" { v = "UNKNOWN" } fmt.Fprintf(&b, "%s: %s\n", k, v) } osr := readKeyValues("/etc/os-release") kv("os", osr["PRETTY_NAME"]) kv("os_id", strings.TrimSpace(osr["ID"]+" "+osr["ID_LIKE"])) kv("os_version", osr["VERSION_ID"]) kv("kernel", readTrim("/proc/sys/kernel/osrelease")) kv("architecture", runtime.GOARCH) h, _ := os.Hostname() kv("hostname", h) cpuModel, cpus := "", 0 if f, err := os.Open("/proc/cpuinfo"); err == nil { sc := bufio.NewScanner(f) for sc.Scan() { line := sc.Text() if strings.HasPrefix(line, "processor") { cpus++ } if cpuModel == "" && (strings.HasPrefix(line, "model name") || strings.HasPrefix(line, "Model")) { if _, v, ok := strings.Cut(line, ":"); ok { cpuModel = strings.TrimSpace(v) } } } f.Close() } kv("cpu_model", cpuModel) kv("cpu_count", fmt.Sprint(max(cpus, runtime.NumCPU()))) mem := readKeyValuesSep("/proc/meminfo", ":") kv("mem_total", mem["MemTotal"]) kv("mem_available", mem["MemAvailable"]) kv("swap_total", mem["SwapTotal"]) if up := strings.Fields(readTrim("/proc/uptime")); len(up) > 0 { kv("uptime_seconds", up[0]) } kv("uid", fmt.Sprint(os.Getuid())) kv("euid", fmt.Sprint(os.Geteuid())) kv("running_as_root", fmt.Sprint(os.Geteuid() == 0)) kv("escalation_tools", strings.Join(available("sudo", "doas", "su"), " ")) initSys := "UNKNOWN" if st, err := os.Stat("/run/systemd/system"); err == nil && st.IsDir() { initSys = "systemd" } else if comm := readTrim("/proc/1/comm"); comm != "" { initSys = comm } kv("init_system", initSys) kv("package_managers", strings.Join(available("apt", "apt-get", "dpkg", "dnf", "yum", "rpm", "pacman", "zypper", "apk", "snap", "flatpak"), " ")) b.WriteString("filesystems:\n") if f, err := os.Open("/proc/mounts"); err == nil { sc := bufio.NewScanner(f) for sc.Scan() { p := strings.Fields(sc.Text()) if len(p) < 3 || isPseudoFS(p[2]) { continue } fmt.Fprintf(&b, " %s on %s type %s\n", p[0], p[1], p[2]) } f.Close() } b.WriteString("network_interfaces:\n") if ifs, err := net.Interfaces(); err == nil { for _, it := range ifs { addrs, _ := it.Addrs() var as []string for _, a := range addrs { as = append(as, a.String()) } fmt.Fprintf(&b, " %s flags=%s mac=%s addrs=%s\n", it.Name, it.Flags, it.HardwareAddr, strings.Join(as, ",")) } } return b.String() } // execLookPath existe para que find_program y available compartan criterio. func execLookPath(name string) (string, error) { return exec.LookPath(name) } func available(names ...string) []string { var out []string for _, n := range names { if _, err := exec.LookPath(n); err == nil { out = append(out, n) } } return out } func isPseudoFS(t string) bool { switch t { case "proc", "sysfs", "devtmpfs", "devpts", "tmpfs", "cgroup", "cgroup2", "securityfs", "pstore", "bpf", "debugfs", "tracefs", "mqueue", "hugetlbfs", "configfs", "fusectl", "autofs", "binfmt_misc", "efivarfs", "rpc_pipefs", "nsfs", "ramfs", "selinuxfs": return true } return false } func readTrim(path string) string { b, err := os.ReadFile(path) if err != nil { return "" } return strings.TrimSpace(string(b)) } func readKeyValues(path string) map[string]string { m := map[string]string{} f, err := os.Open(path) if err != nil { return m } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { if k, v, ok := strings.Cut(sc.Text(), "="); ok { m[k] = strings.Trim(v, `"`) } } return m } func readKeyValuesSep(path, sep string) map[string]string { m := map[string]string{} f, err := os.Open(path) if err != nil { return m } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { if k, v, ok := strings.Cut(sc.Text(), sep); ok { m[strings.TrimSpace(k)] = strings.TrimSpace(v) } } return m }