Files
iasysadmin/internal/tools/sysinfo.go
T
2026-09-27 16:04:02 -06:00

184 lines
4.9 KiB
Go

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
}