fix: nil logger on Windows scan path, rpmEVRAhead equal-version false positive, desktop-self replay token burned before install
- NewOrchestrator initialises a log-only TeeLogger (nil buffer) so the Windows service path never carries a nil logger into executeScan - rpmEVRAhead returns false when all epoch/version/release components compare equal; adds test cases for "0:2.0-1" vs "2.0-1" normalisation - processDesktopSelfToken splits replayCheckAndRecordAgent into replayCheckAgent (before install) + recordAgentTokenConsumed (after successful install) so a transient install failure does not permanently consume the token - Comments on helperSelfStagingPath (Go) and DEFAULT_HELPER_SELF_SOURCE (Rust) name each other as the cross-language counterpart
This commit is contained in:
parent
2da1e92fe9
commit
8b1884eadc
6 changed files with 63 additions and 8 deletions
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/Fimeg/RedFlag/agent/internal/circuitbreaker"
|
||||
"github.com/Fimeg/RedFlag/agent/internal/client"
|
||||
"github.com/Fimeg/RedFlag/agent/internal/event"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
// Scanner represents a generic update scanner
|
||||
|
|
@ -74,11 +75,14 @@ type Orchestrator struct {
|
|||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewOrchestrator creates a new scanner orchestrator
|
||||
// NewOrchestrator creates a new scanner orchestrator with a log-only TeeLogger
|
||||
// (nil buffer, no agent ID). Callers that have a TeeLogger should prefer
|
||||
// NewOrchestratorWithEvents to attach the agent event buffer.
|
||||
func NewOrchestrator() *Orchestrator {
|
||||
return &Orchestrator{
|
||||
scanners: make(map[string]*ScannerConfig),
|
||||
inventoryScanners: make(map[string]*InventoryScannerConfig),
|
||||
logger: event.NewTeeLogger(nil, uuid.Nil),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,12 @@ const (
|
|||
|
||||
selfUpdateOperation = "upgrade"
|
||||
|
||||
agentSelfStagingPath = "/var/lib/redflag/agent/pending-upgrade.bin"
|
||||
agentSelfStagingPath = "/var/lib/redflag/agent/pending-upgrade.bin"
|
||||
|
||||
// helperSelfStagingPath is a cross-language contract: the Go agent stages the
|
||||
// new helper binary here, and the Rust helper reads it on self-upgrade.
|
||||
// The Rust counterpart is DEFAULT_HELPER_SELF_SOURCE in helper/src/main.rs.
|
||||
// Both must be updated together if this path changes.
|
||||
helperSelfStagingPath = "/var/lib/redflag/agent/pending-helper.bin"
|
||||
|
||||
maxSelfUpdateBinarySize = 500 * 1024 * 1024
|
||||
|
|
@ -132,15 +137,29 @@ func (c *Consumer) processDesktopSelfToken(ctx context.Context, token *capabilit
|
|||
}
|
||||
defer os.Remove(staged)
|
||||
|
||||
if err := replayCheckAndRecordAgent(token.TokenID); err != nil {
|
||||
// Check replay before any side effects; record consumed only after a
|
||||
// successful install. The token is Ed25519-signed, hash-pinned, and
|
||||
// version-pinned, so re-executing the identical install after a transient
|
||||
// failure (disk full, backup fail) is idempotent — burning the token before
|
||||
// install would permanently prevent retry (ETHOS #3, #4).
|
||||
if err := replayCheckAgent(token.TokenID); err != nil {
|
||||
return selfPolicyResult(token, "denied", "token_already_consumed", policyExitReplay, 1, err), nil
|
||||
}
|
||||
|
||||
target, err := installDesktopBinary(staged)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [agent] [supplychain] desktop_self_install_failed token_id=%s error=%v", token.TokenID, err)
|
||||
return selfPolicyResult(token, "failed", "desktop_self_install_failed", policyExitExecFailed, 1, err), nil
|
||||
}
|
||||
|
||||
// Install succeeded — mark token consumed so it cannot be replayed.
|
||||
if err := recordAgentTokenConsumed(token.TokenID); err != nil {
|
||||
// Log but do not abort: the binary is already in place. A duplicate
|
||||
// replay would be caught by hash + version checks, and the helper
|
||||
// would refuse an identical re-install anyway.
|
||||
log.Printf("[WARNING] [agent] [supplychain] desktop_self_replay_record_failed token_id=%s error=%v", token.TokenID, err)
|
||||
}
|
||||
|
||||
if err := signalDesktopRestart(target); err != nil {
|
||||
log.Printf("[WARNING] [agent] [supplychain] desktop_restart_signal_failed token_id=%s error=%v", token.TokenID, err)
|
||||
}
|
||||
|
|
@ -284,7 +303,10 @@ func loadCapabilityPublicKey() ([]byte, error) {
|
|||
return nil, fmt.Errorf("invalid public key size at %s: raw=%d decoded=%d", keyPath, len(data), len(decoded))
|
||||
}
|
||||
|
||||
func replayCheckAndRecordAgent(tokenID string) error {
|
||||
// replayCheckAgent returns an error if tokenID has already been consumed.
|
||||
// It does NOT record the token; call recordAgentTokenConsumed after a
|
||||
// successful install to close the replay window.
|
||||
func replayCheckAgent(tokenID string) error {
|
||||
if _, err := safeTokenFilename(tokenID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -293,16 +315,25 @@ func replayCheckAndRecordAgent(tokenID string) error {
|
|||
}
|
||||
|
||||
statePath := filepath.Join(constants.GetAgentStateDir(), "consumed-self-tokens")
|
||||
if contents, err := os.ReadFile(statePath); err == nil {
|
||||
contents, err := os.ReadFile(statePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("read replay state: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
for _, line := range strings.Split(string(contents), "\n") {
|
||||
if strings.TrimSpace(line) == tokenID {
|
||||
return fmt.Errorf("token_id=%s", tokenID)
|
||||
}
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("read replay state: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordAgentTokenConsumed appends tokenID to the consumed-self-tokens file.
|
||||
// Call only after a successful install to avoid permanently burning the token
|
||||
// on a transient failure.
|
||||
func recordAgentTokenConsumed(tokenID string) error {
|
||||
statePath := filepath.Join(constants.GetAgentStateDir(), "consumed-self-tokens")
|
||||
if err := os.MkdirAll(filepath.Dir(statePath), 0o700); err != nil {
|
||||
return fmt.Errorf("create replay state dir: %w", err)
|
||||
}
|
||||
|
|
|
|||
4
helper/.cargo/config.toml
Normal file
4
helper/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Cross-linkers for CI cross-compile matrix (.gitea/workflows/ci.yml).
|
||||
# Cargo only consults these for the named targets; host builds are unaffected.
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
|
@ -65,6 +65,10 @@ const AGENT_SELF_PACKAGE_TYPE: &str = "agent-self";
|
|||
// then invokes the old helper with a helper-self token. The old helper verifies
|
||||
// the hash against the token's closure entry and replaces itself.
|
||||
const HELPER_SELF_PACKAGE_TYPE: &str = "helper-self";
|
||||
// DEFAULT_HELPER_SELF_SOURCE is a cross-language contract: the Go agent stages the
|
||||
// new helper binary here before invoking the old helper with a helper-self token.
|
||||
// The Go counterpart is helperSelfStagingPath in agent/internal/supplychain/binary_update.go.
|
||||
// Both must be updated together if this path changes.
|
||||
const DEFAULT_HELPER_SELF_SOURCE: &str = "/var/lib/redflag/agent/pending-helper.bin";
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
|
|
|
|||
|
|
@ -154,6 +154,15 @@ func TestGatedTargetAheadUsesPackageManagerVersionShape(t *testing.T) {
|
|||
if gatedTargetAhead("dnf", "2:99.0.0-1.fc43", "3:1.0.0-1.fc43") {
|
||||
t.Fatal("dnf lower epoch should not be treated as newer")
|
||||
}
|
||||
// Epoch normalisation: "0:2.0-1" and "2.0-1" are semantically identical because
|
||||
// splitRPMEVR defaults a missing epoch to 0. The string-equality guard is bypassed
|
||||
// by the textual difference, so the component-wise comparison must return false.
|
||||
if gatedTargetAhead("dnf", "0:2.0-1", "2.0-1") {
|
||||
t.Fatal("dnf explicit-epoch-0 vs implicit-epoch-0 should be treated as a no-op")
|
||||
}
|
||||
if gatedTargetAhead("dnf", "2.0-1", "0:2.0-1") {
|
||||
t.Fatal("dnf implicit-epoch-0 vs explicit-epoch-0 should be treated as a no-op")
|
||||
}
|
||||
if !gatedTargetAhead("apt", "1.2.3-1ubuntu1~22.04", "1.2.3-1ubuntu1") {
|
||||
t.Fatal("apt package versions should not be rejected by semver comparison")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1016,7 +1016,10 @@ func rpmEVRAhead(target, current string) bool {
|
|||
if cmp := rpmVersionCompare(targetRelease, currentRelease); cmp != 0 {
|
||||
return cmp > 0
|
||||
}
|
||||
return true
|
||||
// All components equal — target is not ahead of current.
|
||||
// This catches textually-different but semantically-equal strings such as
|
||||
// "0:2.0-1" vs "2.0-1" where splitRPMEVR normalises the missing epoch to 0.
|
||||
return false
|
||||
}
|
||||
|
||||
func splitRPMEVR(evr string) (epoch int, version string, release string) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue