184 lines
4.9 KiB
Go
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
|
|
}
|