@@ -56,6 +56,7 @@ func main() {
|
||||
if errors.Is(err, deviceagent.ErrRestartRequired) {
|
||||
os.Exit(restartExitCode)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "probo-agent: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -79,6 +80,7 @@ func newRootCmd() *cobra.Command {
|
||||
root.AddCommand(newStatusCmd())
|
||||
root.AddCommand(newCollectCmd())
|
||||
root.AddCommand(newUpdateCmd())
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
@@ -145,10 +147,12 @@ func newInstallCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
dir := resolveDir(cmd)
|
||||
|
||||
ctx, cancel := context.WithTimeout(cmd.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
agent := deviceagent.New(dir, version, newAgentLogger())
|
||||
|
||||
resp, err := agent.EnrollNewDevice(ctx, strings.TrimRight(serverURL, "/"), enrollmentToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("enrollment failed: %w", err)
|
||||
@@ -161,6 +165,7 @@ func newInstallCmd() *cobra.Command {
|
||||
if err := persistAutoUpdate(dir, false); err != nil {
|
||||
return fmt.Errorf("cannot persist auto-update preference: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Auto-update disabled.")
|
||||
}
|
||||
|
||||
@@ -184,6 +189,7 @@ func newInstallCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
fmt.Println("Service installed and started.")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -205,6 +211,7 @@ func persistAutoUpdate(dir string, enabled bool) error {
|
||||
}
|
||||
|
||||
cfg.UpdatesDisabled = !enabled
|
||||
|
||||
return deviceagent.SaveConfig(dir, cfg)
|
||||
}
|
||||
|
||||
@@ -228,6 +235,7 @@ func newUninstallCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
_ = os.Remove(deviceagent.ConfigPath(dir))
|
||||
|
||||
fmt.Println("Uninstalled.")
|
||||
|
||||
return nil
|
||||
@@ -248,6 +256,7 @@ func newRunCmd() *cobra.Command {
|
||||
logger := newAgentLogger()
|
||||
agent := deviceagent.New(dir, version, logger)
|
||||
agent.Updater = newUpdater(logger, dir)
|
||||
|
||||
err := agent.Run(ctx)
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
@@ -264,6 +273,7 @@ func newStatusCmd() *cobra.Command {
|
||||
Short: "Print the agent's local state",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dir := resolveDir(cmd)
|
||||
|
||||
cfg, err := deviceagent.LoadConfig(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -294,6 +304,7 @@ func newCollectCmd() *cobra.Command {
|
||||
asJSON bool
|
||||
printDir bool
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "collect",
|
||||
Short: "Run the posture check set once and print results (no server push)",
|
||||
@@ -329,12 +340,14 @@ func newCollectCmd() *cobra.Command {
|
||||
|
||||
func newUpdateCmd() *cobra.Command {
|
||||
var checkOnly bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Check GitHub for a newer agent release and install it in place",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dir := resolveDir(cmd)
|
||||
logger := newAgentLogger()
|
||||
|
||||
updater := newUpdater(logger, dir)
|
||||
if updater == nil {
|
||||
return errors.New("cannot resolve current executable path")
|
||||
@@ -349,10 +362,12 @@ func newUpdateCmd() *cobra.Command {
|
||||
fmt.Printf("probo-agent is up to date (version %s).\n", version)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot check for updates: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Update available: %s -> %s\n", version, rel.Version)
|
||||
|
||||
if checkOnly {
|
||||
return nil
|
||||
}
|
||||
@@ -362,6 +377,7 @@ func newUpdateCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
fmt.Printf("Installed probo-agent %s. Restart the service to use it.\n", rel.Version)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ func New(dir, version string, logger *log.Logger) *Agent {
|
||||
if logger == nil {
|
||||
logger = log.NewLogger(log.WithName("device-agent"))
|
||||
}
|
||||
|
||||
return &Agent{
|
||||
Dir: dir,
|
||||
Version: version,
|
||||
@@ -95,6 +96,7 @@ func (a *Agent) EnrollNewDevice(
|
||||
if serverURL == "" {
|
||||
return nil, errors.New("server URL is required")
|
||||
}
|
||||
|
||||
if enrollmentToken == "" {
|
||||
return nil, errors.New("enrollment token is required")
|
||||
}
|
||||
@@ -127,15 +129,18 @@ func (a *Agent) EnrollNewDevice(
|
||||
if err := SaveConfig(a.Dir, cfg); err != nil {
|
||||
return nil, fmt.Errorf("cannot save config: %w", err)
|
||||
}
|
||||
|
||||
if err := SaveAPIKey(a.Dir, resp.APIKey); err != nil {
|
||||
return nil, fmt.Errorf("cannot save api key: %w", err)
|
||||
}
|
||||
|
||||
if err := clearPendingPostureBatches(a.Dir); err != nil {
|
||||
a.Logger.Warn("cannot clear pending posture queue after enrollment", log.Error(err))
|
||||
}
|
||||
|
||||
a.cfg = cfg
|
||||
a.client = NewClient(serverURL, resp.APIKey, a.UserAgent)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -145,15 +150,19 @@ func (a *Agent) LoadLocalState() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := LoadAPIKey(a.Dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.ServerURL == "" {
|
||||
return errors.New("config has no server URL")
|
||||
}
|
||||
|
||||
a.cfg = cfg
|
||||
a.client = NewClient(cfg.ServerURL, key, a.UserAgent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -185,6 +194,7 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
|
||||
heartbeatTicker := time.NewTicker(a.cfg.HeartbeatInterval)
|
||||
defer heartbeatTicker.Stop()
|
||||
|
||||
postureTicker := time.NewTicker(a.cfg.PostureInterval)
|
||||
defer postureTicker.Stop()
|
||||
|
||||
@@ -203,6 +213,7 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
if heartbeatIntervalChanged {
|
||||
heartbeatTicker.Reset(a.cfg.HeartbeatInterval)
|
||||
}
|
||||
|
||||
if postureIntervalChanged {
|
||||
postureTicker.Reset(a.cfg.PostureInterval)
|
||||
}
|
||||
@@ -223,9 +234,11 @@ func (a *Agent) autoUpdateEnabled() bool {
|
||||
if a.cfg == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.cfg.UpdatesDisabled {
|
||||
return false
|
||||
}
|
||||
|
||||
return a.Updater != nil
|
||||
}
|
||||
|
||||
@@ -238,6 +251,7 @@ func (a *Agent) newUpdateTicker() (*time.Ticker, <-chan time.Time) {
|
||||
}
|
||||
|
||||
t := time.NewTicker(a.cfg.UpdateInterval)
|
||||
|
||||
return t, t.C
|
||||
}
|
||||
|
||||
@@ -258,7 +272,9 @@ func (a *Agent) tryAutoUpdate(parent context.Context) bool {
|
||||
a.Logger.DebugCtx(ctx, "no agent update available")
|
||||
return false
|
||||
}
|
||||
|
||||
a.Logger.WarnCtx(ctx, "agent update check failed", log.Error(err))
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -281,23 +297,30 @@ func (a *Agent) tryAutoUpdate(parent context.Context) bool {
|
||||
func (a *Agent) CollectOnce(ctx context.Context) []checks.Result {
|
||||
now := time.Now()
|
||||
results := make([]checks.Result, 0)
|
||||
|
||||
for _, c := range checks.All() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return results
|
||||
default:
|
||||
}
|
||||
|
||||
checkCtx, cancel := context.WithTimeout(ctx, perCheckTimeout)
|
||||
r := c.Run(checkCtx)
|
||||
|
||||
cancel()
|
||||
|
||||
if r.ObservedAt.IsZero() {
|
||||
r.ObservedAt = now
|
||||
}
|
||||
|
||||
if r.CheckKey == "" {
|
||||
r.CheckKey = c.Key()
|
||||
}
|
||||
|
||||
results = append(results, r)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -308,6 +331,7 @@ func (a *Agent) Unenroll(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.client.Unenroll(ctx); err != nil {
|
||||
a.Logger.WarnCtx(
|
||||
ctx,
|
||||
@@ -315,12 +339,15 @@ func (a *Agent) Unenroll(ctx context.Context) error {
|
||||
log.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
if err := DeleteAPIKey(a.Dir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := clearPendingPostureBatches(a.Dir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -333,6 +360,7 @@ func (a *Agent) doHeartbeat(ctx context.Context) (bool, bool) {
|
||||
oldPostureInterval := a.cfg.PostureInterval
|
||||
|
||||
host := a.currentHostInfo(time.Now())
|
||||
|
||||
resp, err := a.client.Heartbeat(
|
||||
ctx,
|
||||
HeartbeatRequest{
|
||||
@@ -343,9 +371,11 @@ func (a *Agent) doHeartbeat(ctx context.Context) (bool, bool) {
|
||||
)
|
||||
if err != nil {
|
||||
a.Logger.ErrorCtx(ctx, "heartbeat failed", log.Error(err))
|
||||
|
||||
if IsUnauthorized(err) {
|
||||
a.handleUnauthorized()
|
||||
}
|
||||
|
||||
return false, false
|
||||
}
|
||||
|
||||
@@ -355,15 +385,18 @@ func (a *Agent) doHeartbeat(ctx context.Context) (bool, bool) {
|
||||
a.cfg.HeartbeatInterval = next
|
||||
}
|
||||
}
|
||||
|
||||
if resp.PostureSeconds > 0 {
|
||||
next := normalizePostureInterval(time.Duration(resp.PostureSeconds) * time.Second)
|
||||
if next != a.cfg.PostureInterval {
|
||||
a.cfg.PostureInterval = next
|
||||
}
|
||||
}
|
||||
|
||||
a.flushQueuedPostures(ctx)
|
||||
|
||||
heartbeatChanged := a.cfg.HeartbeatInterval != oldHeartbeatInterval
|
||||
|
||||
postureChanged := a.cfg.PostureInterval != oldPostureInterval
|
||||
if heartbeatChanged || postureChanged {
|
||||
if err := SaveConfig(a.Dir, a.cfg); err != nil {
|
||||
@@ -384,16 +417,19 @@ func (a *Agent) doPostures(ctx context.Context) {
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
results := a.CollectOnce(ctx)
|
||||
if len(results) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
passCount int
|
||||
failCount int
|
||||
unknownCount int
|
||||
notApplicableCount int
|
||||
)
|
||||
|
||||
for _, r := range results {
|
||||
switch r.Status {
|
||||
case checks.StatusPass:
|
||||
@@ -406,6 +442,7 @@ func (a *Agent) doPostures(ctx context.Context) {
|
||||
notApplicableCount++
|
||||
}
|
||||
}
|
||||
|
||||
a.Logger.InfoCtx(
|
||||
ctx,
|
||||
"posture checks completed",
|
||||
@@ -430,21 +467,27 @@ func (a *Agent) doPostures(ctx context.Context) {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
a.flushQueuedPostures(ctx)
|
||||
|
||||
if a.revoked {
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.client.PushPostures(ctx, payload); err != nil {
|
||||
a.Logger.ErrorCtx(ctx, "posture push failed", log.Error(err))
|
||||
|
||||
if IsUnauthorized(err) {
|
||||
a.handleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
dropped, enqueueErr := enqueuePendingPostureBatch(a.Dir, payload, a.currentTime())
|
||||
if enqueueErr != nil {
|
||||
a.Logger.ErrorCtx(ctx, "cannot queue posture batch after failed push", log.Error(enqueueErr))
|
||||
return
|
||||
}
|
||||
|
||||
a.Logger.WarnCtx(
|
||||
ctx,
|
||||
"queued posture batch for retry",
|
||||
@@ -458,6 +501,7 @@ func (a *Agent) flushQueuedPostures(ctx context.Context) {
|
||||
if a.revoked || a.client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
now := a.currentTime()
|
||||
if !a.pendingFlushRetryAt.IsZero() && now.Before(a.pendingFlushRetryAt) {
|
||||
return
|
||||
@@ -468,6 +512,7 @@ func (a *Agent) flushQueuedPostures(ctx context.Context) {
|
||||
a.Logger.WarnCtx(ctx, "cannot load pending posture batches", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(batches) == 0 {
|
||||
a.resetPendingFlushRetry()
|
||||
return
|
||||
@@ -479,9 +524,11 @@ func (a *Agent) flushQueuedPostures(ctx context.Context) {
|
||||
a.handleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if saveErr := savePendingPostureBatches(a.Dir, batches[i:]); saveErr != nil {
|
||||
a.Logger.ErrorCtx(ctx, "cannot persist pending posture batches", log.Error(saveErr))
|
||||
}
|
||||
|
||||
retryIn := a.schedulePendingFlushRetry(now)
|
||||
a.Logger.WarnCtx(
|
||||
ctx,
|
||||
@@ -490,6 +537,7 @@ func (a *Agent) flushQueuedPostures(ctx context.Context) {
|
||||
log.Int("remaining_batches", len(batches)-i),
|
||||
log.Duration("retry_in", retryIn),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -498,6 +546,7 @@ func (a *Agent) flushQueuedPostures(ctx context.Context) {
|
||||
a.Logger.ErrorCtx(ctx, "cannot clear pending posture batches", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
a.resetPendingFlushRetry()
|
||||
a.Logger.InfoCtx(ctx, "flushed pending posture batches", log.Int("batches", len(batches)))
|
||||
}
|
||||
@@ -508,9 +557,11 @@ func (a *Agent) currentHostInfo(now time.Time) HostInfo {
|
||||
if collector == nil {
|
||||
collector = CollectHostInfo
|
||||
}
|
||||
|
||||
a.hostInfo = collector()
|
||||
a.hostInfoCollectedAt = now
|
||||
}
|
||||
|
||||
return a.hostInfo
|
||||
}
|
||||
|
||||
@@ -518,6 +569,7 @@ func (a *Agent) currentTime() time.Time {
|
||||
if a.now != nil {
|
||||
return a.now()
|
||||
}
|
||||
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
@@ -525,9 +577,11 @@ func (a *Agent) randomInt63n(n int64) int64 {
|
||||
if n <= 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if a.randInt63n != nil {
|
||||
return a.randInt63n(n)
|
||||
}
|
||||
|
||||
return rand.Int63n(n)
|
||||
}
|
||||
|
||||
@@ -541,9 +595,11 @@ func (a *Agent) schedulePendingFlushRetry(now time.Time) time.Duration {
|
||||
nextBase = pendingFlushBackoffMax
|
||||
}
|
||||
}
|
||||
|
||||
a.pendingFlushBackoff = nextBase
|
||||
|
||||
jitterRange := nextBase / 5
|
||||
|
||||
jitter := time.Duration(0)
|
||||
if jitterRange > 0 {
|
||||
jitter = time.Duration(a.randomInt63n(int64(jitterRange)*2+1)) - jitterRange
|
||||
@@ -551,6 +607,7 @@ func (a *Agent) schedulePendingFlushRetry(now time.Time) time.Duration {
|
||||
|
||||
retryIn := max(nextBase+jitter, time.Second)
|
||||
a.pendingFlushRetryAt = now.Add(retryIn)
|
||||
|
||||
return retryIn
|
||||
}
|
||||
|
||||
@@ -564,17 +621,21 @@ func (a *Agent) handleUnauthorized() {
|
||||
if a.revoked {
|
||||
return
|
||||
}
|
||||
|
||||
a.revoked = true
|
||||
if a.client != nil {
|
||||
a.client.APIKey = ""
|
||||
}
|
||||
|
||||
a.Logger.Warn("agent API returned 401, wiping local key and requiring re-enrollment")
|
||||
|
||||
if err := DeleteAPIKey(a.Dir); err != nil {
|
||||
a.Logger.Error("cannot delete local key after 401", log.Error(err))
|
||||
}
|
||||
|
||||
if err := clearPendingPostureBatches(a.Dir); err != nil {
|
||||
a.Logger.Error("cannot delete pending posture queue after 401", log.Error(err))
|
||||
}
|
||||
|
||||
a.resetPendingFlushRetry()
|
||||
}
|
||||
|
||||
@@ -43,11 +43,14 @@ func darwinDiskEncryption(ctx context.Context) Result {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
on := strings.Contains(strings.ToLower(out.Stdout), "filevault is on")
|
||||
|
||||
ev := map[string]any{"raw": out.Stdout}
|
||||
if on {
|
||||
return pass(ev)
|
||||
}
|
||||
|
||||
return fail(ev)
|
||||
}
|
||||
|
||||
@@ -61,40 +64,51 @@ func darwinScreenLock(ctx context.Context) Result {
|
||||
"raw_stdout": status.Stdout,
|
||||
"raw_stderr": status.Stderr,
|
||||
}
|
||||
|
||||
mode, seconds, ok := darwinScreenLockMode(rawCombined)
|
||||
if ok {
|
||||
ev["mode"] = mode
|
||||
if mode == "seconds" && seconds >= 0 {
|
||||
ev["seconds"] = seconds
|
||||
}
|
||||
|
||||
if mode == "immediate" {
|
||||
return pass(ev)
|
||||
}
|
||||
|
||||
return fail(ev)
|
||||
}
|
||||
|
||||
if status.Err != nil {
|
||||
ev["error"] = status.Err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
ask, askSource := darwinReadScreenSaverDefault(ctx, "askForPassword")
|
||||
|
||||
ev := map[string]any{}
|
||||
if askSource != "" {
|
||||
ev["source"] = askSource
|
||||
}
|
||||
|
||||
if ask.Err != nil {
|
||||
if darwinDefaultsMissing(ask) {
|
||||
ev["ask_for_password"] = "0"
|
||||
ev["note"] = "askForPassword is unset or unavailable"
|
||||
|
||||
return fail(ev)
|
||||
}
|
||||
|
||||
ev["error"] = ask.Err.Error()
|
||||
ev["stderr"] = ask.Stderr
|
||||
|
||||
return unknown(ev)
|
||||
}
|
||||
|
||||
enabled := strings.TrimSpace(ask.Stdout) == "1"
|
||||
|
||||
delayCmd, delaySource := darwinReadScreenSaverDefault(ctx, "askForPasswordDelay")
|
||||
|
||||
ev["ask_for_password"] = ask.Stdout
|
||||
if delayCmd.Err == nil {
|
||||
ev["ask_for_password_delay"] = delayCmd.Stdout
|
||||
@@ -102,9 +116,11 @@ func darwinScreenLock(ctx context.Context) Result {
|
||||
ev["delay_source"] = delaySource
|
||||
}
|
||||
}
|
||||
|
||||
if enabled {
|
||||
return pass(ev)
|
||||
}
|
||||
|
||||
return fail(ev)
|
||||
}
|
||||
|
||||
@@ -113,20 +129,25 @@ func darwinScreenLockMode(raw string) (string, int, bool) {
|
||||
if strings.Contains(lower, "immediate") {
|
||||
return "immediate", 0, true
|
||||
}
|
||||
|
||||
if strings.Contains(lower, "off") {
|
||||
return "off", -1, true
|
||||
}
|
||||
|
||||
if before, _, ok := strings.Cut(lower, "seconds"); ok {
|
||||
prefix := strings.Fields(before)
|
||||
if len(prefix) == 0 {
|
||||
return "seconds", -1, true
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(prefix[len(prefix)-1])
|
||||
if err != nil {
|
||||
return "seconds", -1, true
|
||||
}
|
||||
|
||||
return "seconds", n, true
|
||||
}
|
||||
|
||||
return "", 0, false
|
||||
}
|
||||
|
||||
@@ -140,14 +161,17 @@ func darwinFirewall(ctx context.Context) Result {
|
||||
)
|
||||
if out.Err == nil {
|
||||
state := strings.TrimSpace(out.Stdout)
|
||||
|
||||
ev := map[string]any{"backend": "defaults", "global_state": state}
|
||||
if state == "1" || state == "2" {
|
||||
return pass(ev)
|
||||
}
|
||||
|
||||
return fail(ev)
|
||||
}
|
||||
|
||||
fallback := RunCommand(ctx, "/usr/libexec/ApplicationFirewall/socketfilterfw", "--getglobalstate")
|
||||
|
||||
ev := map[string]any{
|
||||
"backend": "socketfilterfw",
|
||||
"raw": fallback.Stdout,
|
||||
@@ -157,14 +181,18 @@ func darwinFirewall(ctx context.Context) Result {
|
||||
if fallback.Err != nil {
|
||||
ev["error"] = fallback.Err.Error()
|
||||
ev["stderr"] = fallback.Stderr
|
||||
|
||||
return unknown(ev)
|
||||
}
|
||||
|
||||
if darwinStateIndicatesEnabled(fallback.Stdout) {
|
||||
return pass(ev)
|
||||
}
|
||||
|
||||
if darwinStateIndicatesDisabled(fallback.Stdout) {
|
||||
return fail(ev)
|
||||
}
|
||||
|
||||
return unknown(ev)
|
||||
}
|
||||
|
||||
@@ -173,6 +201,7 @@ func darwinReadScreenSaverDefault(ctx context.Context, key string) (CmdResult, s
|
||||
consoleUser := darwinConsoleUser(ctx)
|
||||
if os.Geteuid() == 0 && consoleUser != "" {
|
||||
var consoleMissing CmdResult
|
||||
|
||||
consoleMissingSource := ""
|
||||
|
||||
if CommandExists("sudo") {
|
||||
@@ -190,9 +219,11 @@ func darwinReadScreenSaverDefault(ctx context.Context, key string) (CmdResult, s
|
||||
if consoleUserCurrentHost.Err == nil {
|
||||
return consoleUserCurrentHost, "console_user_current_host:" + consoleUser
|
||||
}
|
||||
|
||||
if !darwinDefaultsMissing(consoleUserCurrentHost) {
|
||||
return consoleUserCurrentHost, "console_user_current_host:" + consoleUser
|
||||
}
|
||||
|
||||
if consoleMissingSource == "" {
|
||||
consoleMissing = consoleUserCurrentHost
|
||||
consoleMissingSource = "console_user_current_host:" + consoleUser
|
||||
@@ -211,9 +242,11 @@ func darwinReadScreenSaverDefault(ctx context.Context, key string) (CmdResult, s
|
||||
if consoleUserDomain.Err == nil {
|
||||
return consoleUserDomain, "console_user:" + consoleUser
|
||||
}
|
||||
|
||||
if !darwinDefaultsMissing(consoleUserDomain) {
|
||||
return consoleUserDomain, "console_user:" + consoleUser
|
||||
}
|
||||
|
||||
if consoleMissingSource == "" {
|
||||
consoleMissing = consoleUserDomain
|
||||
consoleMissingSource = "console_user:" + consoleUser
|
||||
@@ -221,17 +254,21 @@ func darwinReadScreenSaverDefault(ctx context.Context, key string) (CmdResult, s
|
||||
}
|
||||
|
||||
plistPath := "/Users/" + consoleUser + "/Library/Preferences/com.apple.screensaver.plist"
|
||||
|
||||
consoleUserOut := RunCommand(ctx, "defaults", "read", plistPath, key)
|
||||
if consoleUserOut.Err == nil {
|
||||
return consoleUserOut, "console_user_plist:" + consoleUser
|
||||
}
|
||||
|
||||
if !darwinDefaultsMissing(consoleUserOut) {
|
||||
return consoleUserOut, "console_user_plist:" + consoleUser
|
||||
}
|
||||
|
||||
if consoleMissingSource == "" {
|
||||
consoleMissing = consoleUserOut
|
||||
consoleMissingSource = "console_user_plist:" + consoleUser
|
||||
}
|
||||
|
||||
if consoleMissingSource != "" {
|
||||
return consoleMissing, consoleMissingSource
|
||||
}
|
||||
@@ -250,14 +287,17 @@ func darwinReadScreenSaverDefault(ctx context.Context, key string) (CmdResult, s
|
||||
if !darwinDefaultsMissing(currentUser) {
|
||||
return currentUser, "current_user"
|
||||
}
|
||||
|
||||
if !darwinDefaultsMissing(currentHost) {
|
||||
return currentHost, "current_user_current_host"
|
||||
}
|
||||
|
||||
return currentUser, "current_user"
|
||||
}
|
||||
|
||||
func darwinDefaultsMissing(out CmdResult) bool {
|
||||
lower := strings.ToLower(out.Stderr + "\n" + out.Stdout)
|
||||
|
||||
return strings.Contains(lower, "does not exist") ||
|
||||
strings.Contains(lower, "could not find") ||
|
||||
strings.Contains(lower, "does not exist in domain")
|
||||
@@ -267,19 +307,23 @@ func darwinConsoleUser(ctx context.Context) string {
|
||||
if sudoUser := strings.TrimSpace(os.Getenv("SUDO_USER")); sudoUser != "" && sudoUser != "root" {
|
||||
return sudoUser
|
||||
}
|
||||
|
||||
out := RunCommand(ctx, "stat", "-f", "%Su", "/dev/console")
|
||||
if out.Err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
user := strings.TrimSpace(out.Stdout)
|
||||
if user == "" || user == "root" || user == "loginwindow" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
func darwinStateIndicatesEnabled(raw string) bool {
|
||||
lower := strings.ToLower(raw)
|
||||
|
||||
return strings.Contains(lower, "enabled") ||
|
||||
strings.Contains(lower, "state = 1") ||
|
||||
strings.Contains(lower, "state = 2")
|
||||
@@ -300,11 +344,14 @@ func darwinTimeSync(ctx context.Context) Result {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
on := strings.Contains(strings.ToLower(out.Stdout), "on")
|
||||
|
||||
ev := map[string]any{"raw": out.Stdout}
|
||||
if on {
|
||||
return pass(ev)
|
||||
}
|
||||
|
||||
return fail(ev)
|
||||
}
|
||||
|
||||
@@ -313,11 +360,13 @@ func darwinOSVersion(ctx context.Context) Result {
|
||||
if out.Err != nil || out.Stdout == "" {
|
||||
return unknown(map[string]any{"error": "sw_vers failed"})
|
||||
}
|
||||
|
||||
build := RunCommand(ctx, "sw_vers", "-buildVersion")
|
||||
ev := map[string]any{
|
||||
"product_version": out.Stdout,
|
||||
"build_version": build.Stdout,
|
||||
}
|
||||
|
||||
return pass(ev)
|
||||
}
|
||||
|
||||
@@ -337,10 +386,12 @@ func darwinAutoUpdate(ctx context.Context) Result {
|
||||
if strings.TrimSpace(primary.Stdout) == "1" {
|
||||
return pass(ev)
|
||||
}
|
||||
|
||||
return fail(ev)
|
||||
}
|
||||
|
||||
fallback := RunCommand(ctx, "softwareupdate", "--schedule")
|
||||
|
||||
ev := map[string]any{
|
||||
"backend": "softwareupdate",
|
||||
"raw": fallback.Stdout,
|
||||
@@ -352,6 +403,7 @@ func darwinAutoUpdate(ctx context.Context) Result {
|
||||
needsAdmin(fallback.Stderr) {
|
||||
ev["error"] = errString(fallback.Err)
|
||||
ev["stderr"] = fallback.Stderr
|
||||
|
||||
return unknown(ev)
|
||||
}
|
||||
|
||||
@@ -378,11 +430,14 @@ func darwinPasswordPolicy(ctx context.Context) Result {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
lower := strings.ToLower(out.Stdout)
|
||||
|
||||
ev := map[string]any{"raw_truncated": truncate(out.Stdout, 400)}
|
||||
if strings.Contains(lower, "no account policies") || lower == "" {
|
||||
return fail(ev)
|
||||
}
|
||||
|
||||
return pass(ev)
|
||||
}
|
||||
|
||||
@@ -396,11 +451,14 @@ func darwinRemoteLogin(ctx context.Context) Result {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
off := strings.Contains(strings.ToLower(out.Stdout), "off")
|
||||
|
||||
ev := map[string]any{"raw": out.Stdout}
|
||||
if off {
|
||||
return pass(ev)
|
||||
}
|
||||
|
||||
return fail(ev)
|
||||
}
|
||||
|
||||
@@ -413,7 +471,9 @@ func darwinMalwareProtection(ctx context.Context) Result {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ev := map[string]any{"engine": "XProtect", "plist": path}
|
||||
|
||||
version := RunCommand(
|
||||
ctx,
|
||||
"defaults",
|
||||
@@ -424,8 +484,10 @@ func darwinMalwareProtection(ctx context.Context) Result {
|
||||
if version.Err == nil {
|
||||
ev["version"] = version.Stdout
|
||||
}
|
||||
|
||||
return pass(ev)
|
||||
}
|
||||
|
||||
return fail(
|
||||
map[string]any{
|
||||
"engine": "XProtect",
|
||||
|
||||
@@ -29,6 +29,7 @@ var (
|
||||
func Register(key string, run func(context.Context) Result) {
|
||||
registryMu.Lock()
|
||||
defer registryMu.Unlock()
|
||||
|
||||
registry = append(
|
||||
registry,
|
||||
funcCheck{
|
||||
@@ -51,5 +52,6 @@ func All() []Check {
|
||||
return out[i].Key() < out[j].Key()
|
||||
},
|
||||
)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -51,10 +51,13 @@ func RunCommand(ctx context.Context, name string, args ...string) CmdResult {
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(cmdCtx, resolved, args...)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
|
||||
return CmdResult{
|
||||
Stdout: strings.TrimSpace(stdout.String()),
|
||||
Stderr: strings.TrimSpace(stderr.String()),
|
||||
@@ -93,6 +96,7 @@ func isExecutableFile(path string) bool {
|
||||
if err != nil || info.IsDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -44,9 +44,11 @@ func (c funcCheck) Run(ctx context.Context) Result {
|
||||
if r.CheckKey == "" {
|
||||
r.CheckKey = c.key
|
||||
}
|
||||
|
||||
if r.ObservedAt.IsZero() {
|
||||
r.ObservedAt = time.Now().UTC()
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -71,6 +73,7 @@ func truncate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
|
||||
return s[:n] + "…"
|
||||
}
|
||||
|
||||
|
||||
@@ -173,6 +173,7 @@ func IsUnauthorized(err error) bool {
|
||||
if !errors.As(err, &herr) {
|
||||
return false
|
||||
}
|
||||
|
||||
return herr.StatusCode == http.StatusUnauthorized
|
||||
}
|
||||
|
||||
@@ -186,11 +187,13 @@ func (c *Client) do(
|
||||
url := c.ServerURL + path
|
||||
|
||||
var body io.Reader
|
||||
|
||||
if in != nil {
|
||||
buf, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot marshal request: %w", err)
|
||||
}
|
||||
|
||||
body = bytes.NewReader(buf)
|
||||
}
|
||||
|
||||
@@ -198,6 +201,7 @@ func (c *Client) do(
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", c.UserAgent)
|
||||
@@ -214,6 +218,7 @@ func (c *Client) do(
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot perform request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
|
||||
@@ -64,6 +64,7 @@ func ConfigPath(dir string) string {
|
||||
if dir == "" {
|
||||
dir = DefaultConfigDir()
|
||||
}
|
||||
|
||||
return filepath.Join(dir, ConfigFileName)
|
||||
}
|
||||
|
||||
@@ -73,11 +74,14 @@ func LoadConfig(dir string) (*Config, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read config: %w", err)
|
||||
}
|
||||
|
||||
cfg := &Config{}
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, fmt.Errorf("cannot decode config: %w", err)
|
||||
}
|
||||
|
||||
cfg.applyDefaults()
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -96,12 +100,14 @@ func SaveConfig(dir string, cfg *Config) error {
|
||||
}
|
||||
|
||||
cfg.applyDefaults()
|
||||
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot encode config: %w", err)
|
||||
}
|
||||
|
||||
path := ConfigPath(dir)
|
||||
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0o600); err != nil {
|
||||
return fmt.Errorf("cannot write config: %w", err)
|
||||
|
||||
@@ -50,6 +50,7 @@ func CollectHostInfo() HostInfo {
|
||||
}
|
||||
|
||||
info.OSVersion = collectOSVersion()
|
||||
|
||||
info.HardwareUUID = collectHardwareUUID()
|
||||
if sn := collectSerialNumber(); sn != "" {
|
||||
info.SerialNumber = &sn
|
||||
@@ -66,6 +67,7 @@ func hashFallbackUUID() string {
|
||||
h.Write([]byte(hostname))
|
||||
h.Write([]byte{0})
|
||||
h.Write([]byte(mac))
|
||||
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
@@ -94,5 +96,6 @@ func firstStableMAC() string {
|
||||
func runQuiet(ctx context.Context, name string, args ...string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
out, err := cmd.Output()
|
||||
|
||||
return strings.TrimSpace(string(out)), err
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ func collectOSVersion() string {
|
||||
}
|
||||
|
||||
out, _ = runQuiet(ctx, "uname", "-sr")
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -54,6 +55,7 @@ func collectSerialNumber() string {
|
||||
defer cancel()
|
||||
|
||||
out, _ := runQuiet(ctx, "/usr/sbin/ioreg", "-d2", "-c", "IOPlatformExpertDevice")
|
||||
|
||||
return extractValue(out, "IOPlatformSerialNumber")
|
||||
}
|
||||
|
||||
@@ -65,6 +67,7 @@ func extractValue(s, key string) string {
|
||||
}
|
||||
|
||||
rest := s[idx:]
|
||||
|
||||
eq := strings.Index(rest, "=")
|
||||
if eq < 0 {
|
||||
return ""
|
||||
@@ -73,8 +76,10 @@ func extractValue(s, key string) string {
|
||||
rest = strings.TrimSpace(rest[eq+1:])
|
||||
rest = strings.TrimPrefix(rest, "<")
|
||||
rest = strings.TrimPrefix(rest, ">")
|
||||
|
||||
if strings.HasPrefix(rest, "\"") {
|
||||
rest = rest[1:]
|
||||
|
||||
before, _, ok := strings.Cut(rest, "\"")
|
||||
if !ok {
|
||||
return ""
|
||||
|
||||
@@ -61,6 +61,7 @@ func loadPendingPostureBatches(dir string) ([]pendingPostureBatch, error) {
|
||||
if len(batch.Results) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
filtered = append(filtered, batch)
|
||||
}
|
||||
|
||||
@@ -77,6 +78,7 @@ func savePendingPostureBatches(dir string, batches []pendingPostureBatch) error
|
||||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("cannot delete pending postures: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -46,10 +46,12 @@ func TestPendingPostureQueue_EnqueueTrimsOldestBatches(t *testing.T) {
|
||||
time.Unix(int64(i), 0),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
if i < maxPendingPostureBatches {
|
||||
assert.Equal(t, 0, dropped)
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, dropped)
|
||||
}
|
||||
|
||||
@@ -83,6 +85,7 @@ func TestAgent_flushQueuedPostures(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var calls atomic.Int32
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/agent/v1/postures", r.URL.Path)
|
||||
calls.Add(1)
|
||||
@@ -121,13 +124,16 @@ func TestAgent_flushQueuedPostures(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var calls atomic.Int32
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/agent/v1/postures", r.URL.Path)
|
||||
|
||||
call := calls.Add(1)
|
||||
if call == 2 {
|
||||
http.Error(w, "temporary error", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
@@ -158,6 +164,7 @@ func TestAgent_flushQueuedPostures(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var calls atomic.Int32
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/agent/v1/postures", r.URL.Path)
|
||||
calls.Add(1)
|
||||
@@ -181,6 +188,7 @@ func TestAgent_flushQueuedPostures(t *testing.T) {
|
||||
assert.Equal(t, int32(1), calls.Load())
|
||||
|
||||
now = firstRetryAt.Add(time.Second)
|
||||
|
||||
a.flushQueuedPostures(context.Background())
|
||||
assert.Equal(t, int32(2), calls.Load())
|
||||
assert.Equal(t, pendingFlushBackoffMin*2, a.pendingFlushBackoff)
|
||||
@@ -201,13 +209,16 @@ func TestAgent_flushQueuedPostures(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var calls atomic.Int32
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/agent/v1/postures", r.URL.Path)
|
||||
|
||||
call := calls.Add(1)
|
||||
if call == 1 {
|
||||
http.Error(w, "temporary error", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
@@ -224,6 +235,7 @@ func TestAgent_flushQueuedPostures(t *testing.T) {
|
||||
require.True(t, retryAt.After(now))
|
||||
|
||||
now = retryAt.Add(time.Second)
|
||||
|
||||
a.flushQueuedPostures(context.Background())
|
||||
assert.Equal(t, int32(2), calls.Load())
|
||||
assert.Zero(t, a.pendingFlushBackoff)
|
||||
|
||||
@@ -93,6 +93,7 @@ func Install(cfg Config) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write plist (need root?): %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
if err := tmpl.Execute(f, cfg); err != nil {
|
||||
|
||||
@@ -59,12 +59,14 @@ func extractTarGzFile(archivePath, wantPath, dest string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open archive: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read gzip: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = gz.Close() }()
|
||||
|
||||
tr := tar.NewReader(gz)
|
||||
@@ -82,7 +84,7 @@ func extractTarGzFile(archivePath, wantPath, dest string) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeRegA {
|
||||
if hdr.Typeflag != tar.TypeReg {
|
||||
return fmt.Errorf("update: %s is not a regular file", wantPath)
|
||||
}
|
||||
|
||||
@@ -97,6 +99,7 @@ func extractZipFile(archivePath, wantPath, dest string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open zip: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = r.Close() }()
|
||||
|
||||
for _, f := range r.File {
|
||||
|
||||
@@ -54,6 +54,7 @@ func LayoutFor(goos, goarch string) (AssetLayout, error) {
|
||||
binary := "probo-agent"
|
||||
isZip := false
|
||||
ext := "tar.gz"
|
||||
|
||||
if goos == "windows" {
|
||||
binary += ".exe"
|
||||
isZip = true
|
||||
|
||||
@@ -26,6 +26,7 @@ func copyFile(src, dst string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open %s: %w", src, err)
|
||||
}
|
||||
|
||||
defer func() { _ = in.Close() }()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
@@ -40,11 +41,14 @@ func copyFile(src, dst string) error {
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
_ = out.Close()
|
||||
_ = os.Remove(dst)
|
||||
|
||||
return fmt.Errorf("cannot copy to %s: %w", dst, err)
|
||||
}
|
||||
|
||||
if err := out.Sync(); err != nil {
|
||||
_ = out.Close()
|
||||
_ = os.Remove(dst)
|
||||
|
||||
return fmt.Errorf("cannot fsync %s: %w", dst, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ func replaceBinary(dst, src string) error {
|
||||
if err := copyFile(src, staging); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Chmod(staging, 0o755); err != nil {
|
||||
_ = os.Remove(staging)
|
||||
return fmt.Errorf("cannot chmod staged binary: %w", err)
|
||||
|
||||
@@ -135,6 +135,7 @@ func (j *jsonTimestamp) UnmarshalJSON(b []byte) error {
|
||||
}
|
||||
|
||||
*j = jsonTimestamp(t)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -191,6 +192,7 @@ func (u *Updater) CheckLatest(ctx context.Context) (*Release, error) {
|
||||
current := normalizeSemver(u.CurrentVersion)
|
||||
|
||||
var best *Release
|
||||
|
||||
for i := range releases {
|
||||
rel := &releases[i]
|
||||
if rel.Draft || rel.Prerelease {
|
||||
@@ -217,10 +219,12 @@ func (u *Updater) CheckLatest(ctx context.Context) (*Release, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
checksumURL, ok := findAssetURL(rel.Assets, checksumFileName)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
bundleURL, ok := findAssetURL(rel.Assets, checksumBundleFileName)
|
||||
if !ok {
|
||||
// Releases without a Sigstore bundle predate the
|
||||
@@ -281,6 +285,7 @@ func (u *Updater) Apply(ctx context.Context, rel *Release) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create update workdir: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = os.RemoveAll(workDir) }()
|
||||
|
||||
archivePath := filepath.Join(workDir, layout.ArchiveName)
|
||||
@@ -353,6 +358,7 @@ func (u *Updater) resolveVerifier() (Verifier, error) {
|
||||
}
|
||||
|
||||
u.Verifier = v
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
@@ -381,6 +387,7 @@ func (u *Updater) listReleases(ctx context.Context) ([]githubRelease, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build releases request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("User-Agent", u.userAgent())
|
||||
|
||||
@@ -388,6 +395,7 @@ func (u *Updater) listReleases(ctx context.Context) ([]githubRelease, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot fetch releases: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
@@ -408,6 +416,7 @@ func (u *Updater) downloadFile(ctx context.Context, src, dst string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build download request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/octet-stream")
|
||||
req.Header.Set("User-Agent", u.userAgent())
|
||||
|
||||
@@ -415,6 +424,7 @@ func (u *Updater) downloadFile(ctx context.Context, src, dst string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot fetch %s: %w", src, err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
@@ -423,6 +433,7 @@ func (u *Updater) downloadFile(ctx context.Context, src, dst string) error {
|
||||
}
|
||||
|
||||
tmp := dst + ".part"
|
||||
|
||||
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create %s: %w", tmp, err)
|
||||
@@ -432,6 +443,7 @@ func (u *Updater) downloadFile(ctx context.Context, src, dst string) error {
|
||||
_ = f.Close()
|
||||
return fmt.Errorf("cannot stream %s: %w", src, err)
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return fmt.Errorf("cannot close %s: %w", tmp, err)
|
||||
}
|
||||
@@ -440,6 +452,7 @@ func (u *Updater) downloadFile(ctx context.Context, src, dst string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot stat %s: %w", tmp, err)
|
||||
}
|
||||
|
||||
if stat.Size() > defaultDownloadLimit {
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("download %s exceeds %d bytes", src, defaultDownloadLimit)
|
||||
@@ -537,6 +550,7 @@ func verifyChecksum(archivePath, checksumPath, archiveName string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open archive: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
h := sha256.New()
|
||||
@@ -568,6 +582,7 @@ func readChecksum(path, archiveName string) (string, error) {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// `sha256sum` output is `<hex> <name>`; the GNU tool also
|
||||
// supports a single-space separator and a leading `*` flag
|
||||
// for binary mode. Handle both.
|
||||
|
||||
@@ -163,6 +163,7 @@ func newFakeReleaseServer(t *testing.T, tag, version string, layout AssetLayout,
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/repos/getprobo/probo/releases", func(w http.ResponseWriter, r *http.Request) {
|
||||
base := "http://" + r.Host
|
||||
|
||||
assets := []map[string]any{
|
||||
{
|
||||
"name": layout.ArchiveName,
|
||||
@@ -179,6 +180,7 @@ func newFakeReleaseServer(t *testing.T, tag, version string, layout AssetLayout,
|
||||
"browser_download_url": base + "/download/" + checksumBundleFileName,
|
||||
})
|
||||
}
|
||||
|
||||
body := []map[string]any{
|
||||
{
|
||||
"tag_name": frs.tag,
|
||||
@@ -187,6 +189,7 @@ func newFakeReleaseServer(t *testing.T, tag, version string, layout AssetLayout,
|
||||
"assets": assets,
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
_ = version
|
||||
@@ -206,6 +209,7 @@ func newFakeReleaseServer(t *testing.T, tag, version string, layout AssetLayout,
|
||||
|
||||
frs.server = httptest.NewServer(mux)
|
||||
t.Cleanup(frs.server.Close)
|
||||
|
||||
return frs
|
||||
}
|
||||
|
||||
@@ -213,9 +217,11 @@ func (f *fakeReleaseServer) URL() string { return f.server.URL }
|
||||
|
||||
func buildArchive(t *testing.T, layout AssetLayout, binary []byte) []byte {
|
||||
t.Helper()
|
||||
|
||||
if layout.IsZip {
|
||||
return buildZip(t, layout, binary)
|
||||
}
|
||||
|
||||
return buildTarGz(t, layout, binary)
|
||||
}
|
||||
|
||||
@@ -246,6 +252,7 @@ func buildTarGz(t *testing.T, layout AssetLayout, binary []byte) []byte {
|
||||
|
||||
data, err := os.ReadFile(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -268,6 +275,7 @@ func buildZip(t *testing.T, layout AssetLayout, binary []byte) []byte {
|
||||
|
||||
data, err := os.ReadFile(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -106,12 +106,15 @@ func NewCosignVerifier(cfg CosignVerifierConfig) (*CosignVerifier, error) {
|
||||
if cfg.Repo == "" {
|
||||
return nil, fmt.Errorf("update: cosign verifier requires Repo")
|
||||
}
|
||||
|
||||
if cfg.WorkflowPath == "" {
|
||||
cfg.WorkflowPath = expectedWorkflowPath
|
||||
}
|
||||
|
||||
if cfg.TagPrefix == "" {
|
||||
cfg.TagPrefix = DefaultTagPrefix
|
||||
}
|
||||
|
||||
if cfg.CacheDir == "" {
|
||||
return nil, fmt.Errorf("update: cosign verifier requires CacheDir")
|
||||
}
|
||||
@@ -169,6 +172,7 @@ func (v *CosignVerifier) Verify(_ context.Context, artifactPath, bundlePath stri
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open artifact for verification: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = artifact.Close() }()
|
||||
|
||||
policy := verify.NewPolicy(
|
||||
|
||||
Reference in New Issue
Block a user