post inicial
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"sysagent/internal/policy"
|
||||
)
|
||||
|
||||
// transientRule describe un error transitorio que justifica reintentar (§54).
|
||||
//
|
||||
// Solo se incluyen errores que garantizan que el comando NO llegó a hacer
|
||||
// su trabajo (no obtuvo el bloqueo, no resolvió el nombre), para que
|
||||
// reintentar sea seguro incluso con comandos que modifican el sistema.
|
||||
type transientRule struct {
|
||||
re *regexp.Regexp
|
||||
what string
|
||||
backoff []time.Duration // una espera por reintento
|
||||
}
|
||||
|
||||
var transientRules = []transientRule{
|
||||
{
|
||||
re: regexp.MustCompile(`(?i)could not get lock /var/lib/(dpkg|apt)|unable to acquire the dpkg frontend lock|unable to lock (the )?(administration|download) directory|waiting for cache lock|is another process using it\?`),
|
||||
what: "package manager lock held by another process",
|
||||
backoff: []time.Duration{5 * time.Second, 15 * time.Second, 30 * time.Second},
|
||||
},
|
||||
{
|
||||
re: regexp.MustCompile(`(?i)temporary failure in name resolution|temporary failure resolving|could not resolve host|name or service not known`),
|
||||
what: "temporary DNS failure",
|
||||
backoff: []time.Duration{2 * time.Second, 5 * time.Second},
|
||||
},
|
||||
}
|
||||
|
||||
// retryScale permite acortar las esperas en tests.
|
||||
var retryScale = time.Duration(1)
|
||||
|
||||
// runWithRetry ejecuta spec y reintenta con backoff ante errores
|
||||
// transitorios conocidos. Nunca reintenta indefinidamente: cada regla tiene
|
||||
// un número máximo de reintentos. Añade también la pista de escalada de
|
||||
// privilegios cuando falla por permisos (§46).
|
||||
func runWithRetry(ctx context.Context, env *Env, spec CommandSpec, display, cwd string, risks []policy.Risk) Result {
|
||||
var notes []string
|
||||
r := RunCommand(ctx, spec)
|
||||
attempts := map[int]int{}
|
||||
for {
|
||||
if r.ExitCode == 0 || r.TimedOut || ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
rule := -1
|
||||
out := r.Stderr + "\n" + r.Stdout
|
||||
for i, tr := range transientRules {
|
||||
if tr.re.MatchString(out) {
|
||||
rule = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if rule < 0 || attempts[rule] >= len(transientRules[rule].backoff) {
|
||||
break
|
||||
}
|
||||
base := transientRules[rule].backoff[attempts[rule]]
|
||||
wait := base * retryScale
|
||||
attempts[rule]++
|
||||
notes = append(notes, fmt.Sprintf("retry %d/%d after %s: %s", attempts[rule], len(transientRules[rule].backoff), base, transientRules[rule].what))
|
||||
env.Log.Infof("%s: %s; retrying in %s", display, transientRules[rule].what, base)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(wait):
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
r = RunCommand(ctx, spec)
|
||||
}
|
||||
|
||||
res := commandResult(display, cwd, r)
|
||||
if len(notes) > 0 {
|
||||
res.Output += "\n--- retries ---\n" + strings.Join(notes, "\n")
|
||||
res.Audit = append(res.Audit, "retries", fmt.Sprint(len(notes)))
|
||||
}
|
||||
if r.ExitCode != 0 && permissionDeniedRe.MatchString(r.Stderr+r.Stdout) {
|
||||
res.Output += "\n" + escalationHint()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
var permissionDeniedRe = regexp.MustCompile(`(?i)permission denied|operation not permitted|must be (run as )?root|are you root|requires (superuser|root) privileges|superuser privileges required|only root can`)
|
||||
|
||||
var (
|
||||
escalationOnce sync.Once
|
||||
escalationText string
|
||||
)
|
||||
|
||||
// escalationHint explica al modelo si puede elevar privilegios (§46): no se
|
||||
// asume root porque el proyecto lo pida; se comprueba qué hay disponible y
|
||||
// si funciona sin contraseña (sudo -n / doas -n no preguntan nunca).
|
||||
func escalationHint() string {
|
||||
if os.Geteuid() == 0 {
|
||||
return "hint: the agent already runs as root (euid=0); the denial comes from something else (read-only filesystem, SELinux/AppArmor, immutable attribute, capabilities or a container restriction)."
|
||||
}
|
||||
escalationOnce.Do(func() {
|
||||
var parts []string
|
||||
for _, tool := range []string{"sudo", "doas"} {
|
||||
if _, err := execLookPath(tool); err != nil {
|
||||
continue
|
||||
}
|
||||
r := RunCommand(context.Background(), CommandSpec{Name: tool, Args: []string{"-n", "true"}, Timeout: 5 * time.Second})
|
||||
parts = append(parts, fmt.Sprintf("%s (passwordless: %t)", tool, r.ExitCode == 0))
|
||||
}
|
||||
if _, err := execLookPath("su"); err == nil {
|
||||
parts = append(parts, "su (needs the target user's password)")
|
||||
}
|
||||
avail := "none available"
|
||||
if len(parts) > 0 {
|
||||
avail = strings.Join(parts, ", ")
|
||||
}
|
||||
escalationText = fmt.Sprintf("hint: permission denied and the agent is not running as root (uid=%d euid=%d). Privilege escalation: %s. Use it only if the policy and the task justify it; if a password is required, ask the operator with ask_secret (CLI only).", os.Getuid(), os.Geteuid(), avail)
|
||||
})
|
||||
return escalationText
|
||||
}
|
||||
Reference in New Issue
Block a user