Add probo-agent binary, installer, and CI

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
Ludovic Vielle
2026-07-14 20:40:18 +02:00
parent b442e1ed76
commit d0dd87c6c7
24 changed files with 1597 additions and 75 deletions

View File

@@ -331,32 +331,20 @@ func (a *Agent) CollectOnce(ctx context.Context) []checks.Result {
return results
}
// Unenroll revokes server state best-effort and clears local credentials.
// Unenroll asks the server to revoke this device. Local wipe is left to
// RemoveLocalState (called by uninstall after service/tray teardown).
// Missing credentials or server failures are logged and ignored so
// uninstall can continue.
func (a *Agent) Unenroll(ctx context.Context) error {
if a.cfg == nil || a.client == nil {
if err := a.LoadLocalState(); err != nil {
return err
a.Logger.WarnCtx(ctx, "cannot load local state for unenroll", log.Error(err))
return nil
}
}
if err := a.client.Unenroll(ctx); err != nil {
a.Logger.WarnCtx(
ctx,
"unenroll server-side revocation failed, continuing with local wipe",
log.Error(err),
)
}
if err := DeleteAPIKey(a.Dir); err != nil {
return err
}
if err := clearPendingPostureBatches(a.Dir); err != nil {
return err
}
if err := ClearEnrollmentMarker(EnrollmentRunDir(a.Dir)); err != nil {
return err
a.Logger.WarnCtx(ctx, "unenroll server-side revocation failed", log.Error(err))
}
return nil

View File

@@ -0,0 +1,111 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package deviceagent
import (
"errors"
"fmt"
"os"
"path/filepath"
)
const (
// SigstoreCacheDirName is the TUF metadata cache under the agent
// state directory.
SigstoreCacheDirName = "sigstore-cache"
// configFileTmpName is the atomic-write leftover from SaveConfig.
configFileTmpName = ConfigFileName + ".tmp"
)
// RemoveLocalState deletes known agent local state for an install: allowlisted
// files under the state dir and EnrollmentRunDir, plus the sigstore-cache
// subdirectory. It never recursively deletes either directory itself.
// Missing paths are ignored. Filesystem roots and empty paths are rejected.
//
// Every path is attempted even if an earlier removal fails.
//
// enrolling.lock is intentionally retained: flock binds to the inode,
// so unlinking the path while LoadOrExchangeAPIKey holds the lock would
// let a concurrent caller create a new inode and bypass serialization.
func RemoveLocalState(dir string) error {
if dir == "" {
return errors.New("state directory is empty")
}
abs, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("cannot resolve state directory: %w", err)
}
cleaned := filepath.Clean(abs)
if isFilesystemRoot(cleaned) {
return fmt.Errorf("refusing to remove filesystem root %q", cleaned)
}
// Use the original dir (not Abs/Clean) so custom relative --dir values
// resolve the same sibling run tree as MarkEnrolled / IsEnrolled.
runDir := EnrollmentRunDir(dir)
runDirAbs, err := filepath.Abs(runDir)
if err != nil {
return fmt.Errorf("cannot resolve enrollment run dir: %w", err)
}
runDir = filepath.Clean(runDirAbs)
if isFilesystemRoot(runDir) {
return fmt.Errorf("refusing to remove enrollment run dir %q", runDir)
}
knownPaths := []string{
filepath.Join(cleaned, ConfigFileName),
filepath.Join(cleaned, configFileTmpName),
filepath.Join(cleaned, KeyFileName),
filepath.Join(cleaned, KeyFileName+".tmp"),
filepath.Join(cleaned, KeyFileName+".old"),
filepath.Join(cleaned, pendingPosturesFileName),
filepath.Join(cleaned, pendingPosturesFileName+".tmp"),
filepath.Join(runDir, EnrollmentMarkerName),
}
var errs error
for _, path := range knownPaths {
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
errs = errors.Join(errs, fmt.Errorf("cannot remove %s: %w", path, err))
}
}
cacheDir := filepath.Join(cleaned, SigstoreCacheDirName)
if err := os.RemoveAll(cacheDir); err != nil {
errs = errors.Join(errs, fmt.Errorf("cannot remove %s: %w", SigstoreCacheDirName, err))
}
// Best-effort: remove the dirs only when empty. Leave them alone when
// foreign files remain or removal is otherwise refused.
_ = os.Remove(cleaned)
_ = os.Remove(runDir)
return errs
}
func isFilesystemRoot(path string) bool {
cleaned := filepath.Clean(path)
return cleaned != "" && cleaned == filepath.Dir(cleaned)
}