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