Watch
1
0
Fork
You've already forked RedFlag
0

swap uuid lib, windows installer pass, README/RAF copy

- google/uuid -> gofrs/uuid/v5 across server + agent
- windows.go: cross-platform binding cleanup
- linux install template: disable sudo lecture for TTY-less service user
- README: XZ/SolarWinds lede, stable-release note, single attack-surface block
This commit is contained in:
Fimeg 2026-06-03 15:39:49 -04:00
commit 5758b26875
110 changed files with 613 additions and 495 deletions

View file

@ -8,7 +8,7 @@
| Section | Description |
|---------|-------------|
| [[p0-priority-start-here]] | **START HERE** — priority roadmap, current blockers, next steps |
| [[OVERVIEW]] | **START HERE** — architecture overview: what RedFlag is, the two capability tiers, architectural boundaries |
| [[core]] | Core concepts, ETHOS principles, architectural decisions |
| [[components]] | Server, agent, web — component breakdowns |
| [[security]] | Trust boundaries, auth layers, machine binding, key management, supply chain gate |

View file

@ -5,17 +5,18 @@
`v0.2.3.1` — June 2026 · MIT License
> **You're early — nearly 600 of you cloned this before it was announced.**
> A stable release is coming soon, bringing Windows support back fully gated.
> If you want it to keep existing, [sponsor the work](#sponsorship--consulting).
---
The update manager is part of your attack surface. Most homelab tooling ignores this. RedFlag doesn't.
The software that patches your fleet runs as root on every box. XZ Utils came through a build pipeline. SolarWinds came through an update. The update manager is part of your attack surface — most homelab tooling ignores that. RedFlag treats it as the attack surface it is.
Every command the server issues is Ed25519-signed. Agents verify the signature, check the nonce, validate the timestamp, and reject anything they've seen before. The signing key never leaves your server. Hardware binding means a stolen agent config doesn't work on a different machine. You can read the security model in the code, not in marketing copy.
It also just manages your updates — across Linux and Windows, including Docker containers running on those hosts — from a single dashboard, with a human approval step before anything gets installed.
The supply-chain gate goes deeper: when an update is approved, the server resolves the full dependency closure, checks every transitive artifact against OSV.dev, and mints a signed capability token binding the exact artifact hashes. A network-less privileged executor verifies the signature and every hash before anything installs — it can't reach out and can't be redirected. A known vulnerability anywhere in the closure is a full stop: the operator must override with a documented reason, or the token is never minted. The signing and hash verification have no skip path.
The supply-chain gate goes deeper: when an update is approved, the server resolves the full dependency closure, checks every transitive artifact against OSV.dev, and mints a signed capability token binding the exact artifact hashes. A network-less privileged executor verifies the signature and every hash before anything installs. A known vulnerability anywhere in the closure is a full stop — the operator must override with a documented reason, or the token is never minted. The signing and hash verification have no skip path.
It also just manages your updates — across Linux and Windows, including Docker containers running on those hosts — from a single dashboard, with a human approval step before anything gets installed.
ConnectWise charges $50/agent/month. RedFlag doesn't.

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View file

@ -7,7 +7,7 @@ require (
github.com/denisbrodbeck/machineid v1.0.1
github.com/docker/docker v27.4.1+incompatible
github.com/go-ole/go-ole v1.3.0
github.com/google/uuid v1.6.0
github.com/gofrs/uuid/v5 v5.4.0
github.com/scjalliance/comshim v0.0.0-20250111221056-b2ef9d8d7e0f
golang.org/x/sys v0.45.0
)

View file

@ -32,6 +32,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=

View file

@ -9,7 +9,7 @@ import (
"github.com/Fimeg/RedFlag/agent/internal/client"
"github.com/Fimeg/RedFlag/agent/internal/constants"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// LocalCache stores scan results locally for offline viewing

View file

@ -5,7 +5,7 @@
//
// The canonical signed message and closure hash MUST stay byte-identical across
// this package, the server's mirror of it, and helper/src/main.rs. See
// RAF/SUPPLY_CHAIN_GATE_PLAN.md for the contract.
// RAF/security/05-supply-chain-gate.md for the contract.
package capability
import (

View file

@ -19,7 +19,7 @@ import (
"github.com/Fimeg/RedFlag/agent/internal/event"
"github.com/Fimeg/RedFlag/agent/internal/models"
"github.com/Fimeg/RedFlag/agent/internal/system"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// Auth sentinel errors. The polling loop branches on these via errors.Is rather
@ -108,7 +108,7 @@ func (c *Client) bufferEventInternal(eventType, eventSubtype, severity, componen
}
event := &models.SystemEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentIDPtr,
EventType: eventType,
EventSubtype: eventSubtype,

View file

@ -10,7 +10,7 @@ import (
"github.com/Fimeg/RedFlag/agent/internal/constants"
"github.com/Fimeg/RedFlag/agent/internal/version"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// MigrationState tracks migration completion status (used by migration package)

View file

@ -6,7 +6,7 @@ import (
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"sync"

View file

@ -8,6 +8,7 @@ import (
"github.com/Fimeg/RedFlag/agent/internal/client"
"github.com/Fimeg/RedFlag/agent/internal/config"
"github.com/Fimeg/RedFlag/agent/internal/installer"
"github.com/Fimeg/RedFlag/agent/internal/models"
)
// HandleDryRunUpdate runs a package-manager dry-run for the named update so
@ -166,10 +167,18 @@ func resolveClosureHashes(packageType, packageName string, dependencies []string
// HandleConfirmDependencies installs a package together with the dependencies
// the operator confirmed during the dry-run review. Empty dependency list is
// allowed — that path resolves to a simple UpdatePackage on the named target.
func HandleConfirmDependencies(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, params map[string]interface{}, commandID string) error {
func HandleConfirmDependencies(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, params map[string]interface{}, commandID string) (err error) {
packageType, _ := params["package_type"].(string)
packageName, _ := params["package_name"].(string)
// Every error return below surfaces on the History channel as a failed
// install event (ETHOS #1), inward via the client event buffer.
defer func() {
if err != nil {
emitInstallEvent(apiClient, models.SubtypeFailed, models.SeverityError, packageType, packageName, commandID, err.Error())
}
}()
if packageType == "" || packageName == "" {
return fmt.Errorf("package_type and package_name parameters are required")
}
@ -200,45 +209,25 @@ func HandleConfirmDependencies(apiClient *client.Client, cfg *config.Config, ack
var result *installer.InstallResult
var action string
// Non-gated ecosystems — type-assert to concrete type for mutation.
switch i := inst.(type) {
case *installer.WingetInstaller:
if len(dependencies) > 0 {
action = "install_with_dependencies"
log.Printf("[INFO] [agent] [installer] install_with_deps package=%s deps=%v", packageName, dependencies)
allPackages := append([]string{packageName}, dependencies...)
result, err = i.InstallMultiple(allPackages)
} else {
action = "update"
log.Printf("[INFO] [agent] [installer] update_package package=%s", packageName)
result, err = i.UpdatePackage(packageName)
}
case *installer.WindowsUpdateInstaller:
if len(dependencies) > 0 {
action = "install_with_dependencies"
log.Printf("[INFO] [agent] [installer] install_with_deps package=%s deps=%v", packageName, dependencies)
allPackages := append([]string{packageName}, dependencies...)
result, err = i.InstallMultiple(allPackages)
} else {
action = "update"
log.Printf("[INFO] [agent] [installer] update_package package=%s", packageName)
result, err = i.UpdatePackage(packageName)
}
case *installer.DockerInstaller:
if len(dependencies) > 0 {
action = "install_with_dependencies"
log.Printf("[INFO] [agent] [installer] install_with_deps package=%s deps=%v", packageName, dependencies)
allPackages := append([]string{packageName}, dependencies...)
result, err = i.InstallMultiple(allPackages)
} else {
action = "update"
log.Printf("[INFO] [agent] [installer] update_package package=%s", packageName)
result, err = i.UpdatePackage(packageName)
}
default:
// Non-gated ecosystems mutate directly. dnf/apt do not implement
// NonGatedInstaller, so the assertion fails for them — the gate boundary
// is structural here, not a packageType guard.
mut, ok := inst.(installer.NonGatedInstaller)
if !ok {
return fmt.Errorf("[ERROR] [agent] [installer] direct_confirm_not_supported type=%s", packageType)
}
if len(dependencies) > 0 {
action = "install_with_dependencies"
log.Printf("[INFO] [agent] [installer] install_with_deps package=%s deps=%v", packageName, dependencies)
allPackages := append([]string{packageName}, dependencies...)
result, err = mut.InstallMultiple(allPackages)
} else {
action = "update"
log.Printf("[INFO] [agent] [installer] update_package package=%s", packageName)
result, err = mut.UpdatePackage(packageName)
}
if err != nil {
stdout, stderr, exitCode, duration := "", err.Error(), 1, 0
if result != nil {
@ -285,6 +274,9 @@ func HandleConfirmDependencies(apiClient *client.Client, cfg *config.Config, ack
log.Printf("[WARNING] [agent] [installer] report_install_failed error=%v", reportErr)
}
emitInstallEvent(apiClient, models.SubtypeSuccess, models.SeverityInfo, packageType, packageName, commandID,
fmt.Sprintf("%s of %s completed in %ds", action, packageName, result.DurationSeconds))
log.Printf("[INFO] [agent] [installer] install_complete action=%s package=%s duration=%ds",
action, packageName, result.DurationSeconds)
return nil

View file

@ -10,8 +10,26 @@ import (
"github.com/Fimeg/RedFlag/agent/internal/client"
"github.com/Fimeg/RedFlag/agent/internal/config"
"github.com/Fimeg/RedFlag/agent/internal/installer"
"github.com/Fimeg/RedFlag/agent/internal/models"
)
// emitInstallEvent records a package-install outcome on the system-event
// channel so it reaches the History page (ETHOS #1: errors are history).
// It routes inward through the client's event buffer, which persists to disk
// and flushes to the server independently of the command-result path — a
// failed install is auditable even if the result report is lost.
func emitInstallEvent(apiClient *client.Client, subtype, severity, packageType, packageName, commandID, message string) {
apiClient.BufferEvent(
models.EventTypeAgentInstall, subtype, severity,
packageType+"_installer", message,
map[string]interface{}{
"package_type": packageType,
"package_name": packageName,
"command_id": commandID,
},
)
}
// ExpectedHashes maps package names to their expected SHA256 hashes
func ExpectedHashes(cfg *config.Config) map[string]string {
hashes := make(map[string]string)
@ -28,10 +46,19 @@ func FetchExpectedHash(apiClient *client.Client, cfg *config.Config, packageName
return apiClient.GetExpectedHash(packageType, packageName, cfg.AgentID)
}
func HandleInstallUpdates(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, params map[string]interface{}, commandID string) error {
func HandleInstallUpdates(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, params map[string]interface{}, commandID string) (err error) {
packageType, _ := params["package_type"].(string)
packageName, _ := params["package_name"].(string)
// Any error return below lands on the History channel as a failed install
// event — one inward funnel covering every failure path (factory, hash
// verification, the install itself).
defer func() {
if err != nil {
emitInstallEvent(apiClient, models.SubtypeFailed, models.SeverityError, packageType, packageName, commandID, err.Error())
}
}()
if packageType == "" {
return fmt.Errorf("package_type parameter is required")
}
@ -76,42 +103,23 @@ func HandleInstallUpdates(apiClient *client.Client, cfg *config.Config, ackTrack
return fmt.Errorf("[SECURITY] [agent] [installer] direct_mutation_refused type=%s — mutation must go through capability-token path", packageType)
}
// Non-gated ecosystems — type-assert to concrete type for mutation.
switch i := inst.(type) {
case *installer.WingetInstaller:
if packageName != "" {
action = "update"
log.Printf("[INFO] [agent] [installer] updating_package package=%s type=%s", packageName, packageType)
result, err = i.UpdatePackage(packageName)
} else {
action = "upgrade"
log.Printf("[INFO] [agent] [installer] upgrading_all type=%s", packageType)
result, err = i.Upgrade()
}
case *installer.WindowsUpdateInstaller:
if packageName != "" {
action = "update"
log.Printf("[INFO] [agent] [installer] updating_package package=%s type=%s", packageName, packageType)
result, err = i.UpdatePackage(packageName)
} else {
action = "upgrade"
log.Printf("[INFO] [agent] [installer] upgrading_all type=%s", packageType)
result, err = i.Upgrade()
}
case *installer.DockerInstaller:
if packageName != "" {
action = "update"
log.Printf("[INFO] [agent] [installer] updating_package package=%s type=%s", packageName, packageType)
result, err = i.UpdatePackage(packageName)
} else {
action = "upgrade"
log.Printf("[INFO] [agent] [installer] upgrading_all type=%s", packageType)
result, err = i.Upgrade()
}
default:
// Non-gated ecosystems mutate directly. dnf/apt are excluded above and
// do not implement NonGatedInstaller, so the assertion fails for them.
mut, ok := inst.(installer.NonGatedInstaller)
if !ok {
return fmt.Errorf("[ERROR] [agent] [installer] direct_mutation_not_supported type=%s", packageType)
}
if packageName != "" {
action = "update"
log.Printf("[INFO] [agent] [installer] updating_package package=%s type=%s", packageName, packageType)
result, err = mut.UpdatePackage(packageName)
} else {
action = "upgrade"
log.Printf("[INFO] [agent] [installer] upgrading_all type=%s", packageType)
result, err = mut.Upgrade()
}
duration := int(time.Since(startTime).Seconds())
if err != nil {
@ -162,6 +170,9 @@ func HandleInstallUpdates(apiClient *client.Client, cfg *config.Config, ackTrack
log.Printf("[WARNING] [agent] [installer] report_failed error=%v", reportErr)
}
emitInstallEvent(apiClient, models.SubtypeSuccess, models.SeverityInfo, packageType, packageName, commandID,
fmt.Sprintf("%s of %s completed in %ds", result.Action, packageName, duration))
log.Printf("[INFO] [agent] [installer] install_complete action=%s type=%s duration=%ds", action, packageType, duration)
return nil
}

View file

@ -13,6 +13,21 @@ type Installer interface {
VerifyHash(packageName, version, expectedSHA256 string) error
}
// NonGatedInstaller is the set of ecosystems still permitted to mutate
// directly from the agent (winget, docker, windows_update). dnf/apt
// deliberately do NOT implement these methods — their mutation flows only
// through the capability-token path (supplychain.Consumer → redflag-helper).
//
// This interface is the ledger of "what may still bypass the gate." As
// ecosystems move behind the capability gate, drop them from this set and
// the type assertions in the handlers begin failing for them automatically —
// the boundary is structural, not a guard someone has to remember to add.
type NonGatedInstaller interface {
UpdatePackage(packageName string) (*InstallResult, error)
Upgrade() (*InstallResult, error)
InstallMultiple(packageNames []string) (*InstallResult, error)
}
// InstallerFactory creates appropriate installer based on package type
func InstallerFactory(packageType string, serverURL string) (Installer, error) {
switch packageType {

View file

@ -3,13 +3,28 @@ package installer
import (
"fmt"
"log"
"os/exec"
"runtime"
"strings"
"time"
"github.com/Fimeg/RedFlag/agent/pkg/windowsupdate"
"github.com/go-ole/go-ole"
"github.com/scjalliance/comshim"
)
// WindowsUpdateInstaller handles Windows Update installation
// Windows Update Agent operation result codes.
// https://learn.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-operationresultcode
const (
orcSucceeded int32 = 2
orcSucceededWithErrors int32 = 3
)
// WindowsUpdateInstaller installs Windows updates through the Windows Update
// Agent COM API (Microsoft.Update.Session) via go-ole — the same binding the WUA
// scanner uses. No shelling out: wuauclt's /updatenow was removed on Windows 10,
// and Install-WindowsUpdate needs the third-party PSWindowsUpdate module. The COM
// binding compiles cross-platform (go-ole ships non-Windows stubs); IsAvailable()
// gates execution to Windows at runtime.
type WindowsUpdateInstaller struct{}
// NewWindowsUpdateInstaller creates a new Windows Update installer
@ -17,9 +32,8 @@ func NewWindowsUpdateInstaller() *WindowsUpdateInstaller {
return &WindowsUpdateInstaller{}
}
// IsAvailable checks if Windows Update installer is available on this system
// IsAvailable reports whether this installer can run on the current host.
func (i *WindowsUpdateInstaller) IsAvailable() bool {
// Only available on Windows
return runtime.GOOS == "windows"
}
@ -28,185 +42,251 @@ func (i *WindowsUpdateInstaller) GetPackageType() string {
return "windows_update"
}
// Install installs a specific Windows update
// Install installs a specific Windows update by title.
func (i *WindowsUpdateInstaller) Install(packageName string) (*InstallResult, error) {
return i.installUpdates([]string{packageName}, false)
}
// InstallMultiple installs multiple Windows updates
// InstallMultiple installs multiple Windows updates by title.
func (i *WindowsUpdateInstaller) InstallMultiple(packageNames []string) (*InstallResult, error) {
return i.installUpdates(packageNames, false)
}
// Upgrade installs all available Windows updates
// Upgrade installs every available Windows update.
func (i *WindowsUpdateInstaller) Upgrade() (*InstallResult, error) {
return i.installUpdates(nil, true) // nil means all updates
return i.installUpdates(nil, true)
}
// DryRun performs a dry run installation to check what would be installed
// UpdatePackage updates a specific Windows update (alias for Install).
func (i *WindowsUpdateInstaller) UpdatePackage(packageName string) (*InstallResult, error) {
return i.Install(packageName)
}
// DryRun reports which updates would be installed for the given title without
// installing anything.
func (i *WindowsUpdateInstaller) DryRun(packageName, version string) (*InstallResult, error) {
return i.installUpdates([]string{packageName}, true)
}
// installUpdates is the internal implementation for Windows update installation
// VerifyHash is a no-op for Windows Update: the WUA validates update payloads
// against Microsoft's signed catalog itself, and individual updates expose no
// addressable download URL to hash. Fail-open by design.
func (i *WindowsUpdateInstaller) VerifyHash(packageName, version, expectedSHA256 string) error {
log.Printf("[INFO] [agent] [installer] hash_verification_skipped package=%s reason=windows_update_uses_wua_verification", packageName)
return nil
}
// installUpdates searches the Windows Update Agent for the requested updates and
// runs the download -> accept-EULA -> install lifecycle through the COM API.
// packageNames are matched against update titles (the scanner reports Title as the
// package name); a nil/empty slice means "all available updates" (upgrade).
func (i *WindowsUpdateInstaller) installUpdates(packageNames []string, isDryRun bool) (*InstallResult, error) {
if !i.IsAvailable() {
return nil, fmt.Errorf("Windows Update installer is only available on Windows")
}
startTime := time.Now()
// Determine action type
action := "install"
if packageNames == nil {
action = "upgrade" // Upgrade all updates
if len(packageNames) == 0 {
action = "upgrade"
}
result := &InstallResult{
Success: false,
IsDryRun: isDryRun,
Action: action,
DurationSeconds: 0,
Success: false,
IsDryRun: isDryRun,
Action: action,
PackagesInstalled: []string{},
Dependencies: []string{},
Dependencies: []string{},
}
// Initialize COM (mirror the WUA scanner's pattern).
comshim.Add(1)
defer comshim.Done()
ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_SPEED_OVER_MEMORY)
defer ole.CoUninitialize()
session, err := windowsupdate.NewUpdateSession()
if err != nil {
return i.fail(result, startTime, fmt.Errorf("create Windows Update session: %w", err))
}
searcher, err := session.CreateUpdateSearcher()
if err != nil {
return i.fail(result, startTime, fmt.Errorf("create update searcher: %w", err))
}
searchResult, err := searcher.Search("IsInstalled=0 AND IsHidden=0")
if err != nil {
return i.fail(result, startTime, fmt.Errorf("search for updates: %w", err))
}
selected := selectUpdates(searchResult.Updates, packageNames)
if len(selected) == 0 {
if len(packageNames) == 0 {
// Nothing to upgrade — a clean no-op, not a failure.
result.Success = true
result.Stdout = "No applicable Windows updates available"
result.DurationSeconds = int(time.Since(startTime).Seconds())
return result, nil
}
return i.fail(result, startTime,
fmt.Errorf("requested update(s) not found among available Windows updates: %v", packageNames))
}
if isDryRun {
// For dry run, simulate what would be installed
result.Success = true
result.Stdout = i.formatDryRunOutput(packageNames)
result.Stdout = formatSelected(selected)
result.PackagesInstalled = updateTitles(selected) // what WOULD be installed
result.DurationSeconds = int(time.Since(startTime).Seconds())
return result, nil
}
// Method 1: Try PowerShell Windows Update module
var lastErr error
var lastStderr string
if updates, err := i.installViaPowerShell(packageNames); err == nil {
result.Success = true
result.Stdout = updates
result.PackagesInstalled = packageNames
} else {
lastErr = err
// Method 2: Try wuauclt (Windows Update client, deprecated since Win 10 1709)
if updates, err := i.installViaWuauclt(packageNames); err == nil {
result.Success = true
result.Stdout = updates
result.PackagesInstalled = packageNames
} else {
lastStderr = err.Error()
// Accept EULAs where required before download/install.
for _, u := range selected {
if !u.EulaAccepted {
if err := u.AcceptEula(); err != nil {
return i.fail(result, startTime, fmt.Errorf("accept EULA for %q: %w", u.Title, err))
}
}
}
// If neither method succeeded, report the error — do not fake success
if !result.Success {
result.DurationSeconds = int(time.Since(startTime).Seconds())
result.ErrorMessage = fmt.Sprintf(
"no working Windows update mechanism available: PowerShell failed (%v), wuauclt failed (%s)",
lastErr, lastStderr)
return result, fmt.Errorf("%s", result.ErrorMessage)
// Download any updates not already cached.
downloader, err := session.CreateUpdateDownloader()
if err != nil {
return i.fail(result, startTime, fmt.Errorf("create update downloader: %w", err))
}
dlResult, err := downloader.Download(selected)
if err != nil {
return i.fail(result, startTime, fmt.Errorf("download updates: %w", err))
}
if !wuaSucceeded(dlResult.ResultCode) {
return i.fail(result, startTime,
fmt.Errorf("Windows Update download failed: ResultCode=%d HResult=0x%08X", dlResult.ResultCode, uint32(dlResult.HResult)))
}
// Install.
inst, err := session.CreateUpdateInstaller()
if err != nil {
return i.fail(result, startTime, fmt.Errorf("create update installer: %w", err))
}
instResult, err := inst.Install(selected)
if err != nil {
return i.fail(result, startTime, fmt.Errorf("install updates: %w", err))
}
if !wuaSucceeded(instResult.ResultCode) {
return i.fail(result, startTime,
fmt.Errorf("Windows Update install failed: ResultCode=%d HResult=0x%08X", instResult.ResultCode, uint32(instResult.HResult)))
}
result.Success = true
result.PackagesInstalled = updateTitles(selected)
result.RebootRequired = instResult.RebootRequired
result.Stdout = fmt.Sprintf("Installed %d Windows update(s): %s", len(selected), strings.Join(updateTitles(selected), "; "))
result.DurationSeconds = int(time.Since(startTime).Seconds())
// F-C1-3 fix: post-install verification marker
// TODO(DEV-031): Wire RebootRequired into scanner-side filtering.
// Currently this flag is set and reported to the server but the
// scanner does not suppress recently-installed updates from results.
if result.Success && !isDryRun {
result.RebootRequired = true
log.Printf("[INFO] [agent] [installer] windows_update_installed packages=%v reboot_required=%v duration=%ds",
packageNames, result.RebootRequired, result.DurationSeconds)
}
log.Printf("[INFO] [agent] [installer] windows_update_installed packages=%v reboot_required=%v duration=%ds",
result.PackagesInstalled, result.RebootRequired, result.DurationSeconds)
return result, nil
}
// installViaPowerShell uses PowerShell to install Windows updates
func (i *WindowsUpdateInstaller) installViaPowerShell(packageNames []string) (string, error) {
// PowerShell command to install updates
for _, packageName := range packageNames {
cmd := exec.Command("powershell", "-Command",
fmt.Sprintf("Install-WindowsUpdate -Title '%s' -AcceptAll -AutoRestart", packageName))
output, err := cmd.CombinedOutput()
if err != nil {
return string(output), fmt.Errorf("PowerShell installation failed for %s: %w (output: %s)", packageName, err, string(output))
}
}
return "Windows Updates installed via PowerShell", nil
}
// UpdatePackage updates a specific Windows update (alias for Install method)
func (i *WindowsUpdateInstaller) UpdatePackage(packageName string) (*InstallResult, error) {
// Windows uses same logic for updating as installing
return i.Install(packageName)
}
// VerifyHash verifies the Windows update hash before installation (fail-open)
func (i *WindowsUpdateInstaller) VerifyHash(packageName, version, expectedSHA256 string) error {
if expectedSHA256 == "" {
return nil // No hash to verify
}
// Windows Update doesn't expose direct download URLs for individual updates
// Hash verification would require accessing WSUS or Windows Update API
// Fail open - Windows has its own update verification via Windows Update Agent
log.Printf("[INFO] [agent] [installer] hash_verification_skipped package=%s reason=windows_update_uses_wua_verification", packageName)
return nil
}
// installViaWuauclt uses traditional Windows Update client (deprecated since Win 10 1709)
func (i *WindowsUpdateInstaller) installViaWuauclt(packageNames []string) (string, error) {
// Force detection of updates
cmd := exec.Command("cmd", "/c", "wuauclt /detectnow")
if out, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("wuauclt detectnow failed: %w (output: %s)", err, string(out))
}
// Wait for detection
time.Sleep(3 * time.Second)
// Install updates
cmd = exec.Command("cmd", "/c", "wuauclt /updatenow")
output, err := cmd.CombinedOutput()
if err != nil {
return string(output), fmt.Errorf("wuauclt updatenow failed: %w (output: %s)", err, string(output))
}
// wuauclt returns exit code 0 even when it does nothing on modern Windows.
// Check stderr for meaningful failure signals.
stderr := string(output)
if strings.Contains(stderr, "not recognized") || strings.Contains(stderr, "failed") {
return stderr, fmt.Errorf("wuauclt reported failure: %s", stderr)
}
return "Windows Updates installation initiated via wuauclt", nil
}
// formatDryRunOutput creates formatted output for dry run operations
func (i *WindowsUpdateInstaller) formatDryRunOutput(packageNames []string) string {
var output []string
output = append(output, "Dry run - the following updates would be installed:")
output = append(output, "")
for _, name := range packageNames {
output = append(output, fmt.Sprintf("• %s", name))
output = append(output, fmt.Sprintf(" Method: Windows Update (PowerShell/wuauclt)"))
output = append(output, fmt.Sprintf(" Requires: Administrator privileges"))
output = append(output, "")
}
return strings.Join(output, "\n")
}
// GetPendingUpdates returns a list of pending Windows updates
// TODO: Replace with Microsoft.Update.Session COM API via go-ole (see CRITICAL-005)
// GetPendingUpdates returns the titles of updates the Windows Update Agent reports
// as applicable but not yet installed.
func (i *WindowsUpdateInstaller) GetPendingUpdates() ([]string, error) {
if !i.IsAvailable() {
return nil, fmt.Errorf("Windows Update installer is only available on Windows")
}
return nil, fmt.Errorf("GetPendingUpdates not implemented — requires Microsoft.Update.Session COM API via go-ole")
comshim.Add(1)
defer comshim.Done()
ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_SPEED_OVER_MEMORY)
defer ole.CoUninitialize()
session, err := windowsupdate.NewUpdateSession()
if err != nil {
return nil, fmt.Errorf("create Windows Update session: %w", err)
}
searcher, err := session.CreateUpdateSearcher()
if err != nil {
return nil, fmt.Errorf("create update searcher: %w", err)
}
searchResult, err := searcher.Search("IsInstalled=0 AND IsHidden=0")
if err != nil {
return nil, fmt.Errorf("search for updates: %w", err)
}
return updateTitles(searchResult.Updates), nil
}
// fail finalizes a failed InstallResult: records the error, stamps duration, and
// returns it alongside the error so the caller reports ground truth (no fake success).
func (i *WindowsUpdateInstaller) fail(result *InstallResult, start time.Time, err error) (*InstallResult, error) {
result.Success = false
result.ErrorMessage = err.Error()
result.Stderr = err.Error()
result.ExitCode = 1
result.DurationSeconds = int(time.Since(start).Seconds())
return result, err
}
// selectUpdates picks the updates to act on. An empty names slice selects every
// available update (upgrade-all). Otherwise an update is selected when a requested
// name matches its title (exact, then case-insensitive contains) or one of its KB
// article IDs.
func selectUpdates(available []*windowsupdate.IUpdate, names []string) []*windowsupdate.IUpdate {
if len(names) == 0 {
return available
}
var selected []*windowsupdate.IUpdate
for _, u := range available {
if updateMatchesAny(u, names) {
selected = append(selected, u)
}
}
return selected
}
func updateMatchesAny(u *windowsupdate.IUpdate, names []string) bool {
title := strings.TrimSpace(u.Title)
lowerTitle := strings.ToLower(title)
for _, name := range names {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if title == name || strings.Contains(lowerTitle, strings.ToLower(name)) {
return true
}
for _, kb := range u.KBArticleIDs {
// KB IDs come back without the "KB" prefix; match either form.
if strings.EqualFold(kb, name) || strings.EqualFold("KB"+kb, name) {
return true
}
}
}
return false
}
func updateTitles(updates []*windowsupdate.IUpdate) []string {
titles := make([]string, 0, len(updates))
for _, u := range updates {
titles = append(titles, u.Title)
}
return titles
}
func formatSelected(updates []*windowsupdate.IUpdate) string {
var b strings.Builder
b.WriteString("Dry run - the following Windows updates would be installed:\n")
for _, u := range updates {
fmt.Fprintf(&b, " - %s", u.Title)
if len(u.KBArticleIDs) > 0 {
fmt.Fprintf(&b, " (KB%s)", strings.Join(u.KBArticleIDs, ", KB"))
}
b.WriteString("\n")
}
return b.String()
}
func wuaSucceeded(resultCode int32) bool {
return resultCode == orcSucceeded || resultCode == orcSucceededWithErrors
}

View file

@ -10,7 +10,7 @@ import (
"github.com/Fimeg/RedFlag/agent/internal/common"
"github.com/Fimeg/RedFlag/agent/internal/event"
"github.com/Fimeg/RedFlag/agent/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// MigrationPlan represents a complete migration plan
@ -79,7 +79,7 @@ func (e *MigrationExecutor) bufferEvent(eventSubtype, severity, component, messa
}
event := &models.SystemEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentIDPtr,
EventType: "migration_failure",
EventSubtype: eventSubtype,

View file

@ -12,7 +12,7 @@ import (
"github.com/Fimeg/RedFlag/agent/internal/migration"
"github.com/Fimeg/RedFlag/agent/internal/migration/pathutils"
"github.com/Fimeg/RedFlag/agent/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// FileValidator handles comprehensive file validation for migration
@ -219,7 +219,7 @@ func (v *FileValidator) ValidateBackupLocation(backupPath string) error {
}
// Test write permission (create a temp file)
testFile := filepath.Join(parent, ".migration_test_"+uuid.New().String()[:8])
testFile := filepath.Join(parent, ".migration_test_"+uuid.Must(uuid.NewV4()).String()[:8])
if err := os.WriteFile(testFile, []byte("test"), 0600); err != nil {
return fmt.Errorf("backup directory not writable: %w", err)
}
@ -335,7 +335,7 @@ func (v *FileValidator) bufferEvent(eventSubtype, severity, component, message s
}
event := &models.SystemEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: &v.agentID,
EventType: models.EventTypeAgentMigration, // Using model constant
EventSubtype: eventSubtype,

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// StorageMetricReport represents storage metrics from an agent

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// SystemEvent represents a unified event log entry for all system events
@ -27,6 +27,7 @@ const (
EventTypeAgentCheckIn = "agent_checkin"
EventTypeAgentScan = "agent_scan"
EventTypeAgentUpdate = "agent_update"
EventTypeAgentInstall = "agent_install"
EventTypeAgentConfig = "agent_config"
EventTypeAgentMigration = "agent_migration"
EventTypeAgentShutdown = "agent_shutdown"

View file

@ -14,7 +14,7 @@ import (
"github.com/Fimeg/RedFlag/agent/internal/config"
"github.com/Fimeg/RedFlag/agent/internal/crypto"
"github.com/Fimeg/RedFlag/agent/internal/logging"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// ConfirmedTracker is a disk-persisted set of command IDs the server has confirmed

View file

@ -21,7 +21,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/agent/internal/capability"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// DefaultExecutorPath is where the privileged executor binary is expected. The

View file

@ -21,7 +21,7 @@ services:
context: .
dockerfile: ./server/Dockerfile
args:
BUILD_VERSION: ${BUILD_VERSION:-0.2.3.1}
BUILD_VERSION: ${BUILD_VERSION:-0.2.3.4}
container_name: redflag-server
volumes:
- server-config:/app/config

View file

@ -7,7 +7,7 @@
// on every error path. The signing authority lives off this host; this process
// only verifies and executes.
//
// Contract: see RAF/SUPPLY_CHAIN_GATE_PLAN.md. The canonical signed message and
// Contract: see RAF/security/05-supply-chain-gate.md. The canonical signed message and
// closure hash here MUST stay byte-identical to the Go signer/verifier.
use std::collections::BTreeSet;

View file

@ -14,7 +14,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/api/handlers"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/Fimeg/RedFlag/server/internal/api/middleware"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/Fimeg/RedFlag/server/internal/command"
"github.com/Fimeg/RedFlag/server/internal/config"
"github.com/Fimeg/RedFlag/server/internal/database"

View file

@ -5,10 +5,11 @@ go 1.25.0
require (
github.com/alexedwards/argon2id v1.0.0
github.com/docker/docker v25.0.6+incompatible
github.com/doug-martin/goqu/v9 v9.19.0
github.com/gin-gonic/gin v1.11.0
github.com/go-git/go-git/v5 v5.19.1
github.com/gofrs/uuid/v5 v5.4.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9
gopkg.in/natefinch/lumberjack.v2 v2.2.1
@ -28,7 +29,6 @@ require (
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/doug-martin/goqu/v9 v9.19.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect

View file

@ -4,6 +4,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@ -88,6 +89,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
@ -168,6 +171,7 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

View file

@ -8,7 +8,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
type AgentEventsHandler struct {
@ -23,7 +23,7 @@ func NewAgentEventsHandler(aq *queries.AgentQueries) *AgentEventsHandler {
// GET /api/v1/agents/:id/events?severity=error,critical,warning&limit=50
func (h *AgentEventsHandler) GetAgentEvents(c *gin.Context) {
agentIDStr := c.Param("id")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return

View file

@ -12,7 +12,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// AgentTrackedSoftwareHandler exposes the per-agent binding CRUD.
@ -36,7 +36,7 @@ func NewAgentTrackedSoftwareHandler(b *queries.AgentTrackedSoftwareQueries, u *q
// ListByAgent returns the bindings + joined tracked_software metadata for one agent.
// GET /admin/agents/:id/tracked-software
func (h *AgentTrackedSoftwareHandler) ListByAgent(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent id"})
return
@ -53,7 +53,7 @@ func (h *AgentTrackedSoftwareHandler) ListByAgent(c *gin.Context) {
// ListInstallations returns the agents that have the given tracked_software bound.
// GET /admin/upstream/:id/installations
func (h *AgentTrackedSoftwareHandler) ListInstallations(c *gin.Context) {
softwareID, err := uuid.Parse(c.Param("id"))
softwareID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tracked_software id"})
return
@ -70,7 +70,7 @@ func (h *AgentTrackedSoftwareHandler) ListInstallations(c *gin.Context) {
// Upsert creates or updates the binding for (agent, tracked_software).
// POST /admin/agents/:id/tracked-software
func (h *AgentTrackedSoftwareHandler) Upsert(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent id"})
return
@ -105,12 +105,12 @@ func (h *AgentTrackedSoftwareHandler) Upsert(c *gin.Context) {
// Delete removes a binding by id, scoped to the agent.
// DELETE /admin/agents/:id/tracked-software/:bindingID
func (h *AgentTrackedSoftwareHandler) Delete(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent id"})
return
}
bindingID, err := uuid.Parse(c.Param("bindingID"))
bindingID, err := uuid.FromString(c.Param("bindingID"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid binding id"})
return
@ -148,7 +148,7 @@ func (h *AgentTrackedSoftwareHandler) Delete(c *gin.Context) {
// POST /admin/agents/:id/tracked-software/create-update
// B5: Install script is generated to download from Gitea and run.
func (h *AgentTrackedSoftwareHandler) CreateUpdateFromDrift(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent id"})
return
@ -204,7 +204,7 @@ func (h *AgentTrackedSoftwareHandler) CreateUpdateFromDrift(c *gin.Context) {
// Build UpdateEvent and write to current_package_state
updateEvent := models.UpdateEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
PackageType: binding.Ecosystem,
PackageName: binding.Name,
@ -249,7 +249,7 @@ func (h *AgentTrackedSoftwareHandler) CreateUpdateFromDrift(c *gin.Context) {
// GenerateInstallScript returns a shell script that downloads the release
// from Gitea and runs it. B5: install via one-click download script.
func (h *AgentTrackedSoftwareHandler) GenerateInstallScript(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent id"})
return
@ -259,7 +259,7 @@ func (h *AgentTrackedSoftwareHandler) GenerateInstallScript(c *gin.Context) {
bindingIDStr := c.Param("bindingID")
var bindingID uuid.UUID
if bindingIDStr != "" {
bindingID, err = uuid.Parse(bindingIDStr)
bindingID, err = uuid.FromString(bindingIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid binding id"})
return

View file

@ -16,7 +16,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/services"
"github.com/Fimeg/RedFlag/server/internal/version"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// AgentUpdateHandler handles agent binary update operations
// DEPRECATED: This handler is being consolidated - will be replaced by unified update handling
@ -59,7 +59,7 @@ func (h *AgentUpdateHandler) mintAgentSelfToken(agentID uuid.UUID, version, chec
now := time.Now().UTC()
token := &capability.Token{
Version: capability.Version,
TokenID: uuid.New().String(),
TokenID: uuid.Must(uuid.NewV4()).String(),
AgentID: agentID.String(),
PackageType: "agent-self",
Operation: "upgrade",
@ -119,7 +119,7 @@ func (h *AgentUpdateHandler) UpdateAgent(c *gin.Context) {
log.Printf("[DEBUG] [UpdateAgent] Parsed update request - Version: %s, Platform: %s, Nonce: %s", req.Version, req.Platform, req.Nonce)
}
agentIDUUID, err := uuid.Parse(agentID)
agentIDUUID, err := uuid.FromString(agentID)
if err != nil {
log.Printf("[UPDATE] Agent ID format error for %s: %v", agentID, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
@ -252,7 +252,7 @@ func (h *AgentUpdateHandler) UpdateAgent(c *gin.Context) {
}
// Generate nonce for replay protection
nonceUUID := uuid.New()
nonceUUID := uuid.Must(uuid.NewV4())
nonceTimestamp := time.Now().UTC()
var nonceSignature string
if h.signingService != nil {
@ -306,7 +306,7 @@ func (h *AgentUpdateHandler) UpdateAgent(c *gin.Context) {
// Create the command in database
command := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentIDUUID,
CommandType: commandType,
Params: commandParams,
@ -325,7 +325,7 @@ func (h *AgentUpdateHandler) UpdateAgent(c *gin.Context) {
// Log agent update initiation to system_events table
event := &models.SystemEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: &agentIDUUID,
EventType: "agent_update",
EventSubtype: "initiated",
@ -422,7 +422,7 @@ func (h *AgentUpdateHandler) BulkUpdateAgents(c *gin.Context) {
}
// Generate nonce for replay protection
nonceUUID := uuid.New()
nonceUUID := uuid.Must(uuid.NewV4())
nonceTimestamp := time.Now().UTC()
var nonceSignature string
if h.signingService != nil {
@ -445,7 +445,7 @@ func (h *AgentUpdateHandler) BulkUpdateAgents(c *gin.Context) {
// Create update command
command := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: "update_agent",
Params: map[string]interface{}{
@ -485,7 +485,7 @@ func (h *AgentUpdateHandler) BulkUpdateAgents(c *gin.Context) {
// Log each bulk update initiation to system_events table
event := &models.SystemEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: &agentID,
EventType: "agent_update",
EventSubtype: "initiated",
@ -658,7 +658,7 @@ func (h *AgentUpdateHandler) GenerateUpdateNonce(c *gin.Context) {
}
// Parse agent ID as UUID
agentIDUUID, err := uuid.Parse(agentID)
agentIDUUID, err := uuid.FromString(agentID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
return
@ -697,7 +697,7 @@ func (h *AgentUpdateHandler) CheckForUpdateAvailable(c *gin.Context) {
agentID := c.Param("id")
// Parse agent ID
agentIDUUID, err := uuid.Parse(agentID)
agentIDUUID, err := uuid.FromString(agentID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
return
@ -753,7 +753,7 @@ func (h *AgentUpdateHandler) GetUpdateStatus(c *gin.Context) {
agentID := c.Param("id")
// Parse agent ID
agentIDUUID, err := uuid.Parse(agentID)
agentIDUUID, err := uuid.FromString(agentID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
return

View file

@ -14,7 +14,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/services"
"github.com/Fimeg/RedFlag/server/internal/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
type AgentHandler struct {
@ -169,7 +169,7 @@ func (h *AgentHandler) queueSystemHeartbeat(agentID uuid.UUID, durationMinutes i
return fmt.Errorf("signing service unavailable")
}
cmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: models.CommandTypeEnableHeartbeat,
Params: models.JSONB{
@ -349,7 +349,7 @@ func (h *AgentHandler) RegisterAgent(c *gin.Context) {
// Create new agent
agent := &models.Agent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
Hostname: req.Hostname,
OSType: req.OSType,
OSVersion: req.OSVersion,
@ -414,7 +414,7 @@ func (h *AgentHandler) RegisterAgent(c *gin.Context) {
}
refreshTokenExpiry := time.Now().UTC().Add(90 * 24 * time.Hour)
tokenHash = queries.HashRefreshToken(refreshToken)
refreshFamilyID := uuid.New() // root of this agent's rotation family (migration 045)
refreshFamilyID := uuid.Must(uuid.NewV4()) // root of this agent's rotation family (migration 045)
if _, err := tx.Exec("INSERT INTO refresh_tokens (agent_id, token_hash, expires_at, family_id) VALUES ($1, $2, $3, $4)",
agent.ID, tokenHash, refreshTokenExpiry, refreshFamilyID); err != nil {
log.Printf("[ERROR] [server] [registration] create_refresh_token_failed error=%q", err)
@ -905,7 +905,7 @@ func (h *AgentHandler) GetCommands(c *gin.Context) {
// Create audit command to show in history
now := time.Now().UTC()
auditCmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: models.CommandTypeDisableHeartbeat,
Params: models.JSONB{},
@ -1002,7 +1002,7 @@ func (h *AgentHandler) ListAgents(c *gin.Context) {
// GetAgent returns a single agent by ID with last scan information
func (h *AgentHandler) GetAgent(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1020,7 +1020,7 @@ func (h *AgentHandler) GetAgent(c *gin.Context) {
// TriggerHeartbeat creates a heartbeat toggle command for an agent
func (h *AgentHandler) TriggerHeartbeat(c *gin.Context) {
idStr := c.Param("id")
agentID, err := uuid.Parse(idStr)
agentID, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1044,7 +1044,7 @@ func (h *AgentHandler) TriggerHeartbeat(c *gin.Context) {
// Create heartbeat command with duration parameter (manual = user-initiated)
cmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: commandType,
Params: models.JSONB{
@ -1112,7 +1112,7 @@ func (h *AgentHandler) triggerSystemHeartbeat(agentID uuid.UUID, durationMinutes
// Create system heartbeat command
cmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: models.CommandTypeEnableHeartbeat,
Params: models.JSONB{
@ -1145,7 +1145,7 @@ func (h *AgentHandler) triggerSystemHeartbeat(agentID uuid.UUID, durationMinutes
// GetHeartbeatStatus returns the current heartbeat status for an agent
func (h *AgentHandler) GetHeartbeatStatus(c *gin.Context) {
idStr := c.Param("id")
agentID, err := uuid.Parse(idStr)
agentID, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1213,7 +1213,7 @@ func (h *AgentHandler) GetHeartbeatStatus(c *gin.Context) {
// TriggerUpdate creates an update command for an agent
func (h *AgentHandler) TriggerUpdate(c *gin.Context) {
idStr := c.Param("id")
agentID, err := uuid.Parse(idStr)
agentID, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1252,7 +1252,7 @@ func (h *AgentHandler) TriggerUpdate(c *gin.Context) {
// Create update command
cmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: models.CommandTypeInstallUpdate,
Params: params,
@ -1492,7 +1492,7 @@ func (h *AgentHandler) RenewToken(c *gin.Context) {
// to recover from the old "unknown-" fallback machine ID bug (F-D1-1).
func (h *AgentHandler) RebindMachineID(c *gin.Context) {
idStr := c.Param("id")
agentID, err := uuid.Parse(idStr)
agentID, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1552,7 +1552,7 @@ func (h *AgentHandler) RebindMachineID(c *gin.Context) {
// UnregisterAgent removes an agent from the system
func (h *AgentHandler) UnregisterAgent(c *gin.Context) {
idStr := c.Param("id")
agentID, err := uuid.Parse(idStr)
agentID, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1719,7 +1719,7 @@ func (h *AgentHandler) EnableRapidPollingMode(agentID uuid.UUID, durationMinutes
// Rate limiting is implemented at router level in cmd/server/main.go
func (h *AgentHandler) SetRapidPollingMode(c *gin.Context) {
idStr := c.Param("id")
agentID, err := uuid.Parse(idStr)
agentID, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1778,7 +1778,7 @@ func (h *AgentHandler) SetRapidPollingMode(c *gin.Context) {
// TriggerReboot triggers a system reboot for an agent
func (h *AgentHandler) TriggerReboot(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1808,7 +1808,7 @@ func (h *AgentHandler) TriggerReboot(c *gin.Context) {
// Create reboot command
cmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: models.CommandTypeReboot,
Params: models.JSONB{
@ -1841,7 +1841,7 @@ func (h *AgentHandler) TriggerReboot(c *gin.Context) {
// GET /api/v1/agents/:id/config
func (h *AgentHandler) GetAgentConfig(c *gin.Context) {
idStr := c.Param("id")
agentID, err := uuid.Parse(idStr)
agentID, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1923,7 +1923,7 @@ type CircuitBreakerReport struct {
// ReportCircuitBreakerStats receives circuit breaker health from agents
// [ISSUE-004] Enables monitoring and alerting for circuit breaker states
func (h *AgentHandler) ReportCircuitBreakerStats(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1975,7 +1975,7 @@ func (h *AgentHandler) ReportCircuitBreakerStats(c *gin.Context) {
// the existing Remove Agent endpoint.
func (h *AgentHandler) RevokeAgent(c *gin.Context) {
idParam := c.Param("id")
agentID, err := uuid.Parse(idParam)
agentID, err := uuid.FromString(idParam)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent id"})
return
@ -2004,7 +2004,7 @@ func (h *AgentHandler) RevokeAgent(c *gin.Context) {
}
event := &models.SystemEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: &agentID,
EventType: "agent_revoked",
EventSubtype: "by_operator",

View file

@ -8,7 +8,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
"github.com/doug-martin/goqu/v9"
)
@ -75,7 +75,7 @@ func (h *ClientErrorHandler) GetErrors(c *gin.Context) {
sd = sd.Where(goqu.Ex{"error_type": errorType})
}
if agentIDStr != "" {
if id, err := uuid.Parse(agentIDStr); err == nil {
if id, err := uuid.FromString(agentIDStr); err == nil {
sd = sd.Where(goqu.Ex{"agent_id": id})
}
}

View file

@ -12,7 +12,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/services"
"github.com/Fimeg/RedFlag/server/internal/logging"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
type DockerHandler struct {
@ -96,7 +96,7 @@ func (h *DockerHandler) GetContainers(c *gin.Context) {
}
if agentID != "" {
if parsedID, err := uuid.Parse(agentID); err == nil {
if parsedID, err := uuid.FromString(agentID); err == nil {
filter.AgentID = &parsedID
}
}
@ -223,7 +223,7 @@ func (h *DockerHandler) GetContainers(c *gin.Context) {
// GetAgentContainers returns Docker containers for a specific agent
func (h *DockerHandler) GetAgentContainers(c *gin.Context) {
agentIDStr := c.Param("agent_id")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -329,7 +329,7 @@ func (h *DockerHandler) ApproveUpdate(c *gin.Context) {
}
// Parse the update ID from container_id (they're the same in our implementation)
updateID, err := uuid.Parse(containerID)
updateID, err := uuid.FromString(containerID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid container ID"})
return
@ -359,7 +359,7 @@ func (h *DockerHandler) RejectUpdate(c *gin.Context) {
}
// Parse the update ID
updateID, err := uuid.Parse(containerID)
updateID, err := uuid.FromString(containerID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid container ID"})
return
@ -395,7 +395,7 @@ func (h *DockerHandler) InstallUpdate(c *gin.Context) {
}
// Parse the update ID
updateID, err := uuid.Parse(containerID)
updateID, err := uuid.FromString(containerID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid container ID"})
return
@ -411,7 +411,7 @@ func (h *DockerHandler) InstallUpdate(c *gin.Context) {
// Create a command for the agent to install the update
// This would trigger the agent to pull the new image
command := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: update.AgentID,
CommandType: models.CommandTypeInstallUpdate, // Install Docker image update
Params: models.JSONB{

View file

@ -10,7 +10,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// DockerReportsHandler handles Docker image reports from agents
@ -45,7 +45,7 @@ func (h *DockerReportsHandler) ReportDockerImages(c *gin.Context) {
}
// Validate command exists and belongs to agent
commandID, err := uuid.Parse(req.CommandID)
commandID, err := uuid.FromString(req.CommandID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid command ID format"})
return
@ -94,7 +94,7 @@ func (h *DockerReportsHandler) ReportDockerImages(c *gin.Context) {
}
event := models.StoredDockerImage{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
PackageType: item.PackageType,
PackageName: imageName + ":" + imageTag,
@ -143,7 +143,7 @@ func (h *DockerReportsHandler) ReportDockerImages(c *gin.Context) {
// GetAgentDockerImages retrieves Docker image updates for a specific agent
func (h *DockerReportsHandler) GetAgentDockerImages(c *gin.Context) {
agentIDStr := c.Param("agentId")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -204,7 +204,7 @@ func (h *DockerReportsHandler) GetAgentDockerImages(c *gin.Context) {
// GetAgentDockerInfo retrieves detailed Docker information for an agent
func (h *DockerReportsHandler) GetAgentDockerInfo(c *gin.Context) {
agentIDStr := c.Param("agentId")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return

View file

@ -19,7 +19,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/Fimeg/RedFlag/server/internal/services"
serverVersion "github.com/Fimeg/RedFlag/server/internal/version"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/gin-gonic/gin"
)
@ -488,7 +488,7 @@ func (h *DownloadHandler) DownloadUpdatePackage(c *gin.Context) {
return
}
parsedPackageID, err := uuid.Parse(packageID)
parsedPackageID, err := uuid.FromString(packageID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid package ID format"})
return
@ -596,7 +596,7 @@ func (h *DownloadHandler) InstallScript(c *gin.Context) {
func parseAgentID(c *gin.Context) string {
// 1. Header → Secure (preferred)
if agentID := c.GetHeader("X-Agent-ID"); agentID != "" {
if _, err := uuid.Parse(agentID); err == nil {
if _, err := uuid.FromString(agentID); err == nil {
log.Printf("[DEBUG] Parsed agent ID from header: %s", agentID)
return agentID
}
@ -605,7 +605,7 @@ func parseAgentID(c *gin.Context) string {
// 2. Path parameter → Legacy compatible
if agentID := c.Param("agent_id"); agentID != "" {
if _, err := uuid.Parse(agentID); err == nil {
if _, err := uuid.FromString(agentID); err == nil {
log.Printf("[DEBUG] Parsed agent ID from path: %s", agentID)
return agentID
}
@ -614,7 +614,7 @@ func parseAgentID(c *gin.Context) string {
// 3. Query parameter → Fallback
if agentID := c.Query("agent_id"); agentID != "" {
if _, err := uuid.Parse(agentID); err == nil {
if _, err := uuid.FromString(agentID); err == nil {
log.Printf("[DEBUG] Parsed agent ID from query: %s", agentID)
return agentID
}
@ -632,7 +632,7 @@ func (h *DownloadHandler) HandleConfigDownload(c *gin.Context) {
agentIDParam := c.Param("agent_id")
// Validate UUID format
parsedAgentID, err := uuid.Parse(agentIDParam)
parsedAgentID, err := uuid.FromString(agentIDParam)
if err != nil {
log.Printf("Invalid agent ID format for config download: %s, error: %v", agentIDParam, err)
c.JSON(http.StatusBadRequest, gin.H{

View file

@ -25,7 +25,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/api/middleware"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// ---------------------------------------------------------------------------
@ -142,7 +142,7 @@ func TestUpdatePackageDownloadRequiresAuth(t *testing.T) {
func makeTestAgentJWT(t *testing.T, secret string) string {
t.Helper()
claims := middleware.AgentClaims{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),

View file

@ -6,7 +6,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// ReportEventsRequest represents a batch of events from an agent
@ -36,7 +36,7 @@ func NewEventsHandler(aq *queries.AgentQueries) *EventsHandler {
// Accepts up to 100 events per request (TD-003)
func (h *EventsHandler) ReportEvents(c *gin.Context) {
agentIDStr := c.Param("id")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -68,7 +68,7 @@ func (h *EventsHandler) ReportEvents(c *gin.Context) {
for i, event := range req.Events {
// Ensure event has ID
if event.ID == uuid.Nil {
event.ID = uuid.New()
event.ID = uuid.Must(uuid.NewV4())
}
// Set agent ID from URL parameter

View file

@ -7,7 +7,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
type MaintenanceWindowHandler struct {
@ -31,7 +31,7 @@ func (h *MaintenanceWindowHandler) ListWindows(c *gin.Context) {
// GetWindow returns a single maintenance window by ID.
func (h *MaintenanceWindowHandler) GetWindow(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid window ID"})
return
@ -73,7 +73,7 @@ func (h *MaintenanceWindowHandler) CreateWindow(c *gin.Context) {
// UpdateWindow updates an existing maintenance window.
func (h *MaintenanceWindowHandler) UpdateWindow(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid window ID"})
return
@ -95,7 +95,7 @@ func (h *MaintenanceWindowHandler) UpdateWindow(c *gin.Context) {
// DeleteWindow removes a maintenance window.
func (h *MaintenanceWindowHandler) DeleteWindow(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid window ID"})
return

View file

@ -9,7 +9,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// MetricsHandler handles system and storage metrics
@ -44,7 +44,7 @@ func (h *MetricsHandler) ReportMetrics(c *gin.Context) {
}
// Validate command exists and belongs to agent
commandID, err := uuid.Parse(req.CommandID)
commandID, err := uuid.FromString(req.CommandID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid command ID format"})
return
@ -65,7 +65,7 @@ func (h *MetricsHandler) ReportMetrics(c *gin.Context) {
events := make([]models.StoredMetric, 0, len(req.Metrics))
for _, item := range req.Metrics {
event := models.StoredMetric{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
PackageType: item.PackageType,
PackageName: item.PackageName,
@ -114,7 +114,7 @@ func (h *MetricsHandler) ReportMetrics(c *gin.Context) {
// GetAgentMetrics retrieves metrics for a specific agent
func (h *MetricsHandler) GetAgentMetrics(c *gin.Context) {
agentIDStr := c.Param("agentId")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -164,7 +164,7 @@ func (h *MetricsHandler) GetAgentMetrics(c *gin.Context) {
// GetAgentStorageMetrics retrieves storage metrics for a specific agent
func (h *MetricsHandler) GetAgentStorageMetrics(c *gin.Context) {
agentIDStr := c.Param("agentId")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -211,7 +211,7 @@ func (h *MetricsHandler) GetAgentStorageMetrics(c *gin.Context) {
// GetAgentSystemMetrics retrieves system metrics for a specific agent
func (h *MetricsHandler) GetAgentSystemMetrics(c *gin.Context) {
agentIDStr := c.Param("agentId")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return

View file

@ -9,7 +9,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/config"
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
type RegistrationTokenHandler struct {
@ -220,7 +220,7 @@ func (h *RegistrationTokenHandler) GetActiveRegistrationTokens(c *gin.Context) {
func (h *RegistrationTokenHandler) GetAgentsBoundToToken(c *gin.Context) {
// :token in the route here is the registration_tokens.id UUID, not the
// secret token string. Mirrors the /registration-tokens/delete/:id pattern.
tokenID, err := uuid.Parse(c.Param("token"))
tokenID, err := uuid.FromString(c.Param("token"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid token id (expected UUID)"})
return
@ -274,7 +274,7 @@ func (h *RegistrationTokenHandler) DeleteRegistrationToken(c *gin.Context) {
}
// Parse UUID
id, err := uuid.Parse(tokenID)
id, err := uuid.FromString(tokenID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid token ID format"})
return

View file

@ -25,7 +25,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// simulateRetryCommand replicates the FIXED retry flow:
@ -36,7 +36,7 @@ import (
func simulateRetryCommand(original *models.AgentCommand) *models.AgentCommand {
// Build the new command (same as handler does)
newCmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: original.AgentID,
CommandType: original.CommandType,
Params: original.Params,
@ -89,8 +89,8 @@ func TestRetryCommandEndpointProducesUnsignedCommand(t *testing.T) {
now := time.Now()
original := &models.AgentCommand{
ID: uuid.New(),
AgentID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: uuid.Must(uuid.NewV4()),
CommandType: "install_updates",
Params: models.JSONB{"package": "nginx", "version": "1.24.0"},
Status: models.CommandStatusFailed,
@ -138,8 +138,8 @@ func TestRetryCommandEndpointMustProduceSignedCommand(t *testing.T) {
now := time.Now()
original := &models.AgentCommand{
ID: uuid.New(),
AgentID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: uuid.Must(uuid.NewV4()),
CommandType: "install_updates",
Params: models.JSONB{"package": "nginx"},
Status: models.CommandStatusFailed,

View file

@ -7,7 +7,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)
@ -80,7 +80,7 @@ func (h *ScannerConfigHandler) UpdateScannerTimeout(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
/*
event := &models.SystemEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
EventType: "scanner_config_change",
EventSubtype: "timeout_updated",
Severity: "info",

View file

@ -6,7 +6,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// SecuritySettingsHandler handles security settings API endpoints
@ -65,10 +65,10 @@ func (h *SecuritySettingsHandler) UpdateSecuritySetting(c *gin.Context) {
userIDStr := c.GetString("user_id")
// Parse user_id to UUID (the service expects uuid.UUID)
userID, err := uuid.Parse(userIDStr)
userID, err := uuid.FromString(userIDStr)
if err != nil {
// Fallback for numeric user IDs — use a deterministic UUID
userID = uuid.NewSHA1(uuid.NameSpaceURL, []byte("user:"+userIDStr))
userID = uuid.NewV5(uuid.NamespaceURL, "user:"+userIDStr)
}
if err := h.securitySettingsService.ValidateSetting(category, key, req.Value); err != nil {
@ -163,9 +163,9 @@ func (h *SecuritySettingsHandler) ApplySecuritySettings(c *gin.Context) {
}
userIDStr := c.GetString("user_id")
userID, err := uuid.Parse(userIDStr)
userID, err := uuid.FromString(userIDStr)
if err != nil {
userID = uuid.NewSHA1(uuid.NameSpaceURL, []byte("user:"+userIDStr))
userID = uuid.NewV5(uuid.NamespaceURL, "user:"+userIDStr)
}
// Validate all settings first

View file

@ -8,7 +8,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// StorageMetricsHandler handles storage metrics endpoints
@ -44,7 +44,7 @@ func (h *StorageMetricsHandler) ReportStorageMetrics(c *gin.Context) {
// Insert storage metrics with error isolation
for _, metric := range req.Metrics {
dbMetric := models.StorageMetric{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: req.AgentID,
Mountpoint: metric.Mountpoint,
Device: metric.Device,
@ -95,7 +95,7 @@ func (h *StorageMetricsHandler) ReportStorageMetrics(c *gin.Context) {
func (h *StorageMetricsHandler) GetStorageMetrics(c *gin.Context) {
// Get agent ID from URL parameter (this is a dashboard endpoint, not agent endpoint)
agentIDStr := c.Param("id")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
log.Printf("[ERROR] Invalid agent ID %s: %v\n", agentIDStr, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})

View file

@ -14,7 +14,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/services"
"github.com/Fimeg/RedFlag/server/internal/logging"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
type SubsystemHandler struct {
@ -49,7 +49,7 @@ func (h *SubsystemHandler) SetScheduler(s *scheduler.Scheduler) {
func (h *SubsystemHandler) signAndCreateCommand(cmd *models.AgentCommand) error {
// Generate ID if not set (prevents zero UUID issues)
if cmd.ID == uuid.Nil {
cmd.ID = uuid.New()
cmd.ID = uuid.Must(uuid.NewV4())
}
// Set timestamps if not set
@ -97,7 +97,7 @@ func (h *SubsystemHandler) signAndCreateCommand(cmd *models.AgentCommand) error
// GetSubsystems retrieves all subsystems for an agent
// GET /api/v1/agents/:id/subsystems
func (h *SubsystemHandler) GetSubsystems(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
@ -115,7 +115,7 @@ func (h *SubsystemHandler) GetSubsystems(c *gin.Context) {
// GetSubsystem retrieves a specific subsystem for an agent
// GET /api/v1/agents/:id/subsystems/:subsystem
func (h *SubsystemHandler) GetSubsystem(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
@ -144,7 +144,7 @@ func (h *SubsystemHandler) GetSubsystem(c *gin.Context) {
// UpdateSubsystem updates subsystem configuration
// PATCH /api/v1/agents/:id/subsystems/:subsystem
func (h *SubsystemHandler) UpdateSubsystem(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
@ -184,7 +184,7 @@ func (h *SubsystemHandler) UpdateSubsystem(c *gin.Context) {
// EnableSubsystem enables a subsystem
// POST /api/v1/agents/:id/subsystems/:subsystem/enable
func (h *SubsystemHandler) EnableSubsystem(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
@ -212,7 +212,7 @@ func (h *SubsystemHandler) EnableSubsystem(c *gin.Context) {
// DisableSubsystem disables a subsystem
// POST /api/v1/agents/:id/subsystems/:subsystem/disable
func (h *SubsystemHandler) DisableSubsystem(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
@ -299,7 +299,7 @@ func (h *SubsystemHandler) getScanCommandType(subsystem string, agentID uuid.UUI
// BUG-015 FIX: Supports platform-specific scanners (apt, dnf, windows, winget)
// Frontend maps 'updates' → platform scanner, this handler creates scan_<platform> commands
func (h *SubsystemHandler) TriggerSubsystem(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
@ -387,7 +387,7 @@ func (h *SubsystemHandler) TriggerSubsystem(c *gin.Context) {
// GetSubsystemStats retrieves statistics for a subsystem
// GET /api/v1/agents/:id/subsystems/:subsystem/stats
func (h *SubsystemHandler) GetSubsystemStats(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
@ -416,7 +416,7 @@ func (h *SubsystemHandler) GetSubsystemStats(c *gin.Context) {
// SetAutoRun enables or disables auto-run for a subsystem
// POST /api/v1/agents/:id/subsystems/:subsystem/auto-run
func (h *SubsystemHandler) SetAutoRun(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
@ -453,7 +453,7 @@ func (h *SubsystemHandler) SetAutoRun(c *gin.Context) {
// SetInterval sets the interval for a subsystem
// POST /api/v1/agents/:id/subsystems/:subsystem/interval
func (h *SubsystemHandler) SetInterval(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
agentID, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return

View file

@ -20,7 +20,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/Fimeg/RedFlag/server/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// isValidResult checks if the result value complies with the database constraint
@ -102,7 +102,7 @@ func (h *UpdateHandler) SetOrchestrator(o LifecycleAdvancer) {
// dry-run has been made.
func (h *UpdateHandler) EnqueueDryRun(update *models.UpdateState) error {
command := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: update.AgentID,
CommandType: models.CommandTypeDryRunUpdate,
Params: map[string]interface{}{
@ -119,7 +119,7 @@ func (h *UpdateHandler) EnqueueDryRun(update *models.UpdateState) error {
// Collapse the agent's poll interval during the active phase (best-effort).
if shouldEnable, err := h.shouldEnableHeartbeat(update.AgentID, 10); err == nil && shouldEnable {
heartbeatCmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: update.AgentID,
CommandType: models.CommandTypeEnableHeartbeat,
Params: models.JSONB{"duration_minutes": 10},
@ -185,7 +185,7 @@ func (h *UpdateHandler) ReportUpdates(c *gin.Context) {
events := make([]models.UpdateEvent, 0, len(req.Updates))
for _, item := range req.Updates {
event := models.UpdateEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
PackageType: item.PackageType,
PackageName: item.PackageName,
@ -318,7 +318,7 @@ func (h *UpdateHandler) ListUpdates(c *gin.Context) {
// Parse agent_id if provided
if agentIDStr := c.Query("agent_id"); agentIDStr != "" {
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err == nil {
filters.AgentID = agentID
}
@ -418,7 +418,7 @@ func (h *UpdateHandler) ListPackages(c *gin.Context) {
// GetUpdate retrieves a single update by ID
func (h *UpdateHandler) GetUpdate(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"})
return
@ -439,7 +439,7 @@ func (h *UpdateHandler) GetUpdate(c *gin.Context) {
// knowing the package coordinates.
func (h *UpdateHandler) GetPackageFleet(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"})
return
@ -470,7 +470,7 @@ func (h *UpdateHandler) GetPackageFleet(c *gin.Context) {
// an update id, for the detail pane's Version Timeline card.
func (h *UpdateHandler) GetPackageVersions(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"})
return
@ -544,7 +544,7 @@ func (h *UpdateHandler) recordSupplyChainOverride(update *models.UpdateState, ho
log.Printf("[SECURITY] [server] [supply_chain] gate_overridden id=%s pkg=%s version=%s cause=%q operator_reason=%q",
update.ID, update.PackageName, update.AvailableVersion, hold.reason, operatorReason)
event := &models.SystemEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: &update.AgentID,
EventType: "supply_chain_override",
EventSubtype: subtype,
@ -571,7 +571,7 @@ func (h *UpdateHandler) recordSupplyChainOverride(update *models.UpdateState, ho
// ApproveUpdate marks an update as approved, with OSV.dev supply chain check
func (h *UpdateHandler) ApproveUpdate(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"})
return
@ -868,7 +868,7 @@ func (h *UpdateHandler) ReportLog(c *gin.Context) {
// result-ack, by contrast, correctly clears on result-recorded — it only
// needs to know the server received the report, not that it finalized.)
if req.CommandID != "" {
commandID, err := uuid.Parse(req.CommandID)
commandID, err := uuid.FromString(req.CommandID)
if err == nil {
command, err := h.commandQueries.GetCommandByID(commandID)
if err == nil && command != nil {
@ -908,7 +908,7 @@ func (h *UpdateHandler) ReportLog(c *gin.Context) {
}
logEntry := &models.UpdateLog{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
Action: req.Action,
Subsystem: subsystem,
@ -936,7 +936,7 @@ func (h *UpdateHandler) ReportLog(c *gin.Context) {
// NEW: Update command status if command_id is provided
if req.CommandID != "" {
commandID, err := uuid.Parse(req.CommandID)
commandID, err := uuid.FromString(req.CommandID)
if err != nil {
// Log warning but don't fail the request
log.Printf("[WARNING] [server] [updates] invalid_command_id_format command_id=%s", req.CommandID)
@ -1045,7 +1045,7 @@ func (h *UpdateHandler) ReportLog(c *gin.Context) {
message := services.RenderUpdateLog(req.Action, validResult, req.Stderr)
event := &models.SystemEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: &agentID,
EventType: eventType,
EventSubtype: "failed",
@ -1072,7 +1072,7 @@ func (h *UpdateHandler) ReportLog(c *gin.Context) {
// GetPackageHistory returns version history for a specific package
func (h *UpdateHandler) GetPackageHistory(c *gin.Context) {
agentIDStr := c.Param("agent_id")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1105,7 +1105,7 @@ func (h *UpdateHandler) GetPackageHistory(c *gin.Context) {
// GetBatchStatus returns recent batch processing status for an agent
func (h *UpdateHandler) GetBatchStatus(c *gin.Context) {
agentIDStr := c.Param("agent_id")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1128,7 +1128,7 @@ func (h *UpdateHandler) GetBatchStatus(c *gin.Context) {
// UpdatePackageStatus updates the status of a package (for when updates are installed)
func (h *UpdateHandler) UpdatePackageStatus(c *gin.Context) {
agentIDStr := c.Param("agent_id")
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
@ -1190,7 +1190,7 @@ func (h *UpdateHandler) ApproveUpdates(c *gin.Context) {
gateMin, gateEnforcement := services.PackageAgeGateConfig()
for _, idStr := range req.UpdateIDs {
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID: " + idStr})
return
@ -1317,7 +1317,7 @@ func (h *UpdateHandler) ApproveUpdates(c *gin.Context) {
// RejectUpdate rejects a single update
func (h *UpdateHandler) RejectUpdate(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"})
return
@ -1335,7 +1335,7 @@ func (h *UpdateHandler) RejectUpdate(c *gin.Context) {
// InstallUpdate marks an update as ready for installation and creates a dry run command for the agent
func (h *UpdateHandler) InstallUpdate(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"})
return
@ -1394,7 +1394,7 @@ func (h *UpdateHandler) InstallUpdate(c *gin.Context) {
// GetUpdateLogs retrieves installation logs for a specific update
func (h *UpdateHandler) GetUpdateLogs(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"})
return
@ -1479,7 +1479,7 @@ func (h *UpdateHandler) ReportDependencies(c *gin.Context) {
// Automatically create installation command since no dependencies need approval
command := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: models.CommandTypeConfirmDependencies,
Params: map[string]interface{}{
@ -1496,7 +1496,7 @@ func (h *UpdateHandler) ReportDependencies(c *gin.Context) {
// Check if heartbeat should be enabled (avoid duplicates)
if shouldEnable, err := h.shouldEnableHeartbeat(agentID, 10); err == nil && shouldEnable {
heartbeatCmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: models.CommandTypeEnableHeartbeat,
Params: models.JSONB{
@ -1624,7 +1624,7 @@ func (h *UpdateHandler) pinReportedClosure(agentID uuid.UUID, req *models.Depend
return
}
updateID, err := uuid.Parse(req.UpdateID)
updateID, err := uuid.FromString(req.UpdateID)
if err != nil {
log.Printf("[WARNING] [server] [capability] closure_pin_bad_update_id agent_id=%s update_id=%q error=%v",
agentID, req.UpdateID, err)
@ -1745,7 +1745,7 @@ func (h *UpdateHandler) enableHeartbeatBeforeInstall(agentID uuid.UUID) {
return
}
heartbeatCmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: models.CommandTypeEnableHeartbeat,
Params: models.JSONB{"duration_minutes": 10},
@ -1803,7 +1803,7 @@ func (h *UpdateHandler) ConfirmDependenciesAuto(update *models.UpdateState) (boo
// ConfirmDependencies handles user confirmation to proceed with dependency installation
func (h *UpdateHandler) ConfirmDependencies(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"})
return
@ -1843,7 +1843,7 @@ func (h *UpdateHandler) ConfirmDependencies(c *gin.Context) {
// Legacy command path (docker/winget/Windows): send a confirm_dependencies
// command for the agent to execute directly.
command := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: update.AgentID,
CommandType: models.CommandTypeConfirmDependencies,
Params: map[string]interface{}{
@ -1887,7 +1887,7 @@ func (h *UpdateHandler) GetAllLogs(c *gin.Context) {
// Parse agent_id if provided
if agentIDStr := c.Query("agent_id"); agentIDStr != "" {
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err == nil {
filters.AgentID = agentID
}
@ -1957,7 +1957,7 @@ func (h *UpdateHandler) GetActiveOperations(c *gin.Context) {
// contract cannot drift between entry points.
func (h *UpdateHandler) buildRetryCommand(original *models.AgentCommand) (*models.AgentCommand, error) {
newCommand := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: original.AgentID,
CommandType: original.CommandType,
Params: original.Params,
@ -1975,7 +1975,7 @@ func (h *UpdateHandler) buildRetryCommand(original *models.AgentCommand) (*model
// RetryCommand retries a failed, timed_out, or cancelled command by command ID.
func (h *UpdateHandler) RetryCommand(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid command ID"})
return
@ -2013,7 +2013,7 @@ func (h *UpdateHandler) RetryCommand(c *gin.Context) {
// cache ceiling that constrains client-side matching.
func (h *UpdateHandler) RetryUpdate(c *gin.Context) {
idStr := c.Param("id")
updateID, err := uuid.Parse(idStr)
updateID, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"})
return
@ -2048,7 +2048,7 @@ func (h *UpdateHandler) RetryUpdate(c *gin.Context) {
// CancelCommand cancels a pending or sent command
func (h *UpdateHandler) CancelCommand(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid command ID"})
return
@ -2066,7 +2066,7 @@ func (h *UpdateHandler) CancelCommand(c *gin.Context) {
// command — journal it inward so it surfaces in the events API / history.
if command != nil {
event := &models.SystemEvent{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: &command.AgentID,
EventType: models.EventTypeCommandFailed,
EventSubtype: "cancelled",
@ -2188,7 +2188,7 @@ func (h *UpdateHandler) GetExpectedHash(c *gin.Context) {
return
}
agentID, err := uuid.Parse(agentIDStr)
agentID, err := uuid.FromString(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent_id"})
return
@ -2270,7 +2270,7 @@ func (h *UpdateHandler) ReportCapabilityResult(c *gin.Context) {
}
agentID := c.MustGet("agent_id").(uuid.UUID)
tokenID, err := uuid.Parse(c.Param("token_id"))
tokenID, err := uuid.FromString(c.Param("token_id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid token_id"})
return

View file

@ -7,7 +7,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/Fimeg/RedFlag/server/internal/services/upstream"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// UpstreamHandler exposes the tracked_software CRUD + sync-now action.
@ -73,7 +73,7 @@ func (h *UpstreamHandler) Create(c *gin.Context) {
}
func (h *UpstreamHandler) Delete(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
id, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
@ -88,7 +88,7 @@ func (h *UpstreamHandler) Delete(c *gin.Context) {
// SyncNow forces a sync for a single tracked_software row. Blocks on the
// fetch so the operator sees the result inline.
func (h *UpstreamHandler) SyncNow(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
id, err := uuid.FromString(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return

View file

@ -8,7 +8,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// AgentClaims represents JWT claims for agent authentication

View file

@ -14,7 +14,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// MachineBindingMiddleware validates machine ID matches database record

View file

@ -20,14 +20,14 @@ import (
"github.com/Fimeg/RedFlag/server/internal/api/middleware"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// makeAgentJWT creates a valid agent JWT for testing
func makeAgentJWT(t *testing.T, secret string) string {
t.Helper()
claims := middleware.AgentClaims{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
RegisteredClaims: jwt.RegisteredClaims{
Issuer: middleware.JWTIssuerAgent,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),

View file

@ -22,7 +22,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/api/middleware"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// makeWebJWT creates a valid web/admin JWT with correct issuer for testing
@ -119,7 +119,7 @@ func TestAgentTokenRejectedByWebAuthMiddleware(t *testing.T) {
// Agent JWT with issuer=redflag-agent
agentClaims := middleware.AgentClaims{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
RegisteredClaims: jwt.RegisteredClaims{
Issuer: middleware.JWTIssuerAgent,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),

View file

@ -5,7 +5,7 @@
//
// The canonical signed message and closure hash MUST stay byte-identical across
// this package, the agent's mirror of it, and helper/src/main.rs. See
// RAF/SUPPLY_CHAIN_GATE_PLAN.md for the contract.
// RAF/security/05-supply-chain-gate.md for the contract.
package capability
import (

View file

@ -6,7 +6,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// Factory creates validated AgentCommand instances
@ -26,7 +26,7 @@ func NewFactory(commandQueries *queries.CommandQueries) *Factory {
// Create generates a new validated AgentCommand with unique ID
func (f *Factory) Create(agentID uuid.UUID, commandType string, params map[string]interface{}) (*models.AgentCommand, error) {
cmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: commandType,
Status: "pending",
@ -57,7 +57,7 @@ func (f *Factory) CreateWithIdempotency(agentID uuid.UUID, commandType string, p
// If no existing command found, proceed with creation
if err.Error() == "sql: no rows in result set" || err.Error() == "command not found" {
cmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: agentID,
CommandType: commandType,
Status: "pending",

View file

@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/Fimeg/RedFlag/server/internal/models"
)

View file

@ -7,7 +7,7 @@ import (
"time"
"github.com/alexedwards/argon2id"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -6,7 +6,7 @@ import (
"fmt"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -6,7 +6,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -8,7 +8,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/doug-martin/goqu/v9"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)
@ -471,7 +471,7 @@ func (q *AgentQueries) SetAgentUpdating(agentID string, isUpdating bool, targetV
// This is used to allow old agents to check in and receive updates even if they're below minimum version
func (q *AgentQueries) HasPendingUpdateCommand(agentID string) (bool, error) {
// Check if agent_id is a valid UUID
agentUUID, err := uuid.Parse(agentID)
agentUUID, err := uuid.FromString(agentID)
if err != nil {
return false, fmt.Errorf("invalid agent ID: %w", err)
}

View file

@ -6,7 +6,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/capability"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)
@ -28,11 +28,11 @@ func (q *CapabilityTokenQueries) Insert(t *capability.Token, updateID uuid.UUID)
if err != nil {
return err
}
tokenID, err := uuid.Parse(t.TokenID)
tokenID, err := uuid.FromString(t.TokenID)
if err != nil {
return err
}
agentID, err := uuid.Parse(t.AgentID)
agentID, err := uuid.FromString(t.AgentID)
if err != nil {
return err
}

View file

@ -6,7 +6,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)
@ -196,7 +196,7 @@ func (q *CommandQueries) MarkCommandsReceivedTx(tx *sqlx.Tx, agentID uuid.UUID,
parsed := make([]uuid.UUID, 0, len(commandIDs))
for _, idStr := range commandIDs {
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
continue
}
@ -683,7 +683,7 @@ func (q *CommandQueries) VerifyResultsRecorded(commandIDs []string) ([]string, e
// Convert string IDs to UUIDs
uuidIDs := make([]uuid.UUID, 0, len(commandIDs))
for _, idStr := range commandIDs {
id, err := uuid.Parse(idStr)
id, err := uuid.FromString(idStr)
if err != nil {
// Skip invalid UUIDs
continue
@ -744,7 +744,7 @@ func (q *CommandQueries) HasPendingUpdateCommand(agentID string) (bool, error) {
AND status = 'pending'
`
agentUUID, err := uuid.Parse(agentID)
agentUUID, err := uuid.FromString(agentID)
if err != nil {
return false, fmt.Errorf("invalid agent ID: %w", err)
}

View file

@ -119,7 +119,7 @@ func TestRetryCommandQueryDoesNotCopySignature(t *testing.T) {
// (Struct construction from commands.go:202)
//
// newCommand := &models.AgentCommand{
// ID: uuid.New(),
// ID: uuid.Must(uuid.NewV4()),
// AgentID: original.AgentID,
// CommandType: original.CommandType,
// Params: original.Params, ← Params copied

View file

@ -6,7 +6,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/doug-martin/goqu/v9"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -5,7 +5,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -7,7 +7,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/doug-martin/goqu/v9"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -2,7 +2,7 @@ package queries
import (
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -7,7 +7,7 @@ import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -8,7 +8,7 @@ import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -7,7 +7,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)
@ -83,7 +83,7 @@ func (q *SecuritySettingsQueries) CreateSetting(category, key string, value inte
}
setting := &models.SecuritySetting{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
Category: category,
Key: key,
Value: string(valueJSON),
@ -200,7 +200,7 @@ func (q *SecuritySettingsQueries) DeleteSetting(category, key string) (*string,
// CreateAuditLog creates an audit log entry for setting changes
func (q *SecuritySettingsQueries) CreateAuditLog(settingID, userID uuid.UUID, action, oldValue, newValue, reason string) error {
audit := &models.SecuritySettingAudit{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
SettingID: settingID,
UserID: userID,
Action: action,

View file

@ -6,7 +6,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)
@ -78,7 +78,7 @@ func (q *SigningKeyQueries) InsertSigningKey(ctx context.Context, keyID, publicK
`
now := time.Now().UTC()
params := map[string]interface{}{
"id": uuid.New(),
"id": uuid.Must(uuid.NewV4()),
"key_id": keyID,
"public_key": publicKeyHex,
"algorithm": "ed25519",

View file

@ -6,7 +6,7 @@ import (
"fmt"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// StorageMetricsQueries handles storage metrics database operations

View file

@ -6,7 +6,7 @@ import (
"fmt"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -11,7 +11,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/capability"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/doug-martin/goqu/v9"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)
@ -676,7 +676,7 @@ func (q *UpdateQueries) CreateUpdateEventsBatch(events []models.UpdateEvent) err
// Create batch record
batch := &models.UpdateBatch{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: events[0].AgentID,
BatchSize: len(events),
Status: "processing",

View file

@ -5,7 +5,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -6,7 +6,7 @@ package logging
import (
"github.com/Fimeg/RedFlag/server/internal/config"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)

View file

@ -10,7 +10,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
"gopkg.in/natefinch/lumberjack.v2"
)

View file

@ -5,7 +5,7 @@ import (
"encoding/json"
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// Agent represents a registered update agent

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// AgentUpdatePackage represents a signed agent binary package

View file

@ -4,7 +4,7 @@ import (
"errors"
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// AgentCommand represents a command to be executed by an agent

View file

@ -2,7 +2,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// DockerPort represents a port mapping in a Docker container

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// MaintenanceWindow represents a recurring weekly maintenance window

View file

@ -2,7 +2,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// MetricsReportRequest is sent by agents when reporting system/storage metrics

View file

@ -5,7 +5,7 @@ import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// SecurityEvent represents a security-related event that occurred

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// SecuritySetting represents a user-configurable security setting

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// SigningKey represents a signing key record in the database

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// StorageMetric represents a storage metric from an agent

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// AgentSubsystem represents a subsystem configuration for an agent

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// SystemEvent represents a unified event log entry for all system events

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// TrackedSoftware is a single piece of software the operator wants the

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
type User struct {

View file

@ -14,7 +14,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// Store is the persistence surface the orchestrator drives. Satisfied by

View file

@ -5,7 +5,7 @@ import (
"sync"
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// Orchestrator advances the package update lifecycle on a server-side timer and

View file

@ -6,7 +6,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// --- fakes -----------------------------------------------------------------
@ -145,8 +145,8 @@ func (c fixedClock) Now() time.Time { return c.t }
func pkg(status models.PackageStatus, severity string, age time.Duration, now time.Time) models.UpdateState {
return models.UpdateState{
ID: uuid.New(),
AgentID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: uuid.Must(uuid.NewV4()),
PackageType: "dnf",
PackageName: "demo",
Severity: severity,

View file

@ -6,7 +6,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// sweepAutoApprove advances eligible pending packages without an operator. Policy

View file

@ -6,7 +6,7 @@ import (
"sync"
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// SubsystemJob represents a scheduled subsystem scan

View file

@ -5,7 +5,7 @@ import (
"testing"
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
func TestPriorityQueue_BasicOperations(t *testing.T) {
@ -25,7 +25,7 @@ func TestPriorityQueue_BasicOperations(t *testing.T) {
}
// Push a job
agent1 := uuid.New()
agent1 := uuid.Must(uuid.NewV4())
job1 := &SubsystemJob{
AgentID: agent1,
AgentHostname: "agent-01",
@ -71,17 +71,17 @@ func TestPriorityQueue_Ordering(t *testing.T) {
// Push jobs in random order
jobs := []*SubsystemJob{
{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
NextRunAt: now.Add(30 * time.Minute), // Third
},
{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "storage",
NextRunAt: now.Add(5 * time.Minute), // First
},
{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "docker",
NextRunAt: now.Add(15 * time.Minute), // Second
},
@ -110,7 +110,7 @@ func TestPriorityQueue_Ordering(t *testing.T) {
func TestPriorityQueue_UpdateExisting(t *testing.T) {
pq := NewPriorityQueue()
agentID := uuid.New()
agentID := uuid.Must(uuid.NewV4())
now := time.Now()
// Push initial job
@ -153,8 +153,8 @@ func TestPriorityQueue_UpdateExisting(t *testing.T) {
func TestPriorityQueue_Remove(t *testing.T) {
pq := NewPriorityQueue()
agent1 := uuid.New()
agent2 := uuid.New()
agent1 := uuid.Must(uuid.NewV4())
agent2 := uuid.Must(uuid.NewV4())
pq.Push(&SubsystemJob{
AgentID: agent1,
@ -193,7 +193,7 @@ func TestPriorityQueue_Remove(t *testing.T) {
func TestPriorityQueue_Get(t *testing.T) {
pq := NewPriorityQueue()
agentID := uuid.New()
agentID := uuid.Must(uuid.NewV4())
job := &SubsystemJob{
AgentID: agentID,
Subsystem: "updates",
@ -214,7 +214,7 @@ func TestPriorityQueue_Get(t *testing.T) {
}
// Get non-existent job
retrieved = pq.Get(uuid.New(), "storage")
retrieved = pq.Get(uuid.Must(uuid.NewV4()), "storage")
if retrieved != nil {
t.Fatal("Get should return nil for non-existent job")
}
@ -227,7 +227,7 @@ func TestPriorityQueue_PopBefore(t *testing.T) {
// Add jobs with different NextRunAt times
for i := 0; i < 5; i++ {
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
NextRunAt: now.Add(time.Duration(i*10) * time.Minute),
})
@ -265,7 +265,7 @@ func TestPriorityQueue_PopBeforeWithLimit(t *testing.T) {
// Add 5 jobs all due now
for i := 0; i < 5; i++ {
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
NextRunAt: now,
})
@ -288,12 +288,12 @@ func TestPriorityQueue_PeekBefore(t *testing.T) {
now := time.Now()
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
NextRunAt: now.Add(5 * time.Minute),
})
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "storage",
NextRunAt: now.Add(15 * time.Minute),
})
@ -317,7 +317,7 @@ func TestPriorityQueue_Clear(t *testing.T) {
// Add some jobs
for i := 0; i < 10; i++ {
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
NextRunAt: time.Now(),
})
@ -354,19 +354,19 @@ func TestPriorityQueue_GetStats(t *testing.T) {
// Add jobs
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
AgentHostname: "agent-01",
Subsystem: "updates",
NextRunAt: now.Add(5 * time.Minute),
IntervalMinutes: 15,
})
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "storage",
NextRunAt: now.Add(10 * time.Minute),
})
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
NextRunAt: now.Add(15 * time.Minute),
})
@ -408,7 +408,7 @@ func TestPriorityQueue_Concurrency(t *testing.T) {
go func(idx int) {
defer wg.Done()
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
NextRunAt: time.Now().Add(time.Duration(idx) * time.Second),
})
@ -460,7 +460,7 @@ func TestPriorityQueue_ConcurrentReadWrite(t *testing.T) {
go func() {
for i := 0; i < 1000; i++ {
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
NextRunAt: time.Now(),
})
@ -495,7 +495,7 @@ func BenchmarkPriorityQueue_Push(b *testing.B) {
for i := 0; i < b.N; i++ {
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
NextRunAt: time.Now().Add(time.Duration(i) * time.Second),
})
@ -508,7 +508,7 @@ func BenchmarkPriorityQueue_Pop(b *testing.B) {
// Pre-fill the queue
for i := 0; i < b.N; i++ {
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
NextRunAt: time.Now().Add(time.Duration(i) * time.Second),
})
@ -526,7 +526,7 @@ func BenchmarkPriorityQueue_Peek(b *testing.B) {
// Pre-fill with 10000 jobs
for i := 0; i < 10000; i++ {
pq.Push(&SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
NextRunAt: time.Now().Add(time.Duration(i) * time.Second),
})

View file

@ -11,7 +11,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/Fimeg/RedFlag/server/internal/services"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// Config holds scheduler configuration
@ -473,7 +473,7 @@ func (w *worker) processJob(job *SubsystemJob) error {
return nil
}
cmd := &models.AgentCommand{
ID: uuid.New(),
ID: uuid.Must(uuid.NewV4()),
AgentID: job.AgentID,
CommandType: commandType,
Params: models.JSONB{},

View file

@ -4,7 +4,7 @@ import (
"testing"
"time"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
func TestScheduler_NewScheduler(t *testing.T) {
@ -61,8 +61,8 @@ func TestScheduler_QueueIntegration(t *testing.T) {
s := NewScheduler(config, nil, nil, nil, nil)
// Add jobs to queue
agent1 := uuid.New()
agent2 := uuid.New()
agent1 := uuid.Must(uuid.NewV4())
agent2 := uuid.Must(uuid.NewV4())
job1 := &SubsystemJob{
AgentID: agent1,
@ -193,7 +193,7 @@ func TestScheduler_ProcessQueueWithJobs(t *testing.T) {
// Add jobs that are due now
for i := 0; i < 5; i++ {
job := &SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
AgentHostname: "test-agent",
Subsystem: "updates",
IntervalMinutes: 15,
@ -272,7 +272,7 @@ func TestScheduler_ConcurrentQueueAccess(t *testing.T) {
go func() {
for i := 0; i < 100; i++ {
job := &SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
IntervalMinutes: 15,
NextRunAt: time.Now(),
@ -308,7 +308,7 @@ func BenchmarkScheduler_ProcessQueue(b *testing.B) {
// Pre-fill queue with jobs
for i := 0; i < 1000; i++ {
job := &SubsystemJob{
AgentID: uuid.New(),
AgentID: uuid.Must(uuid.NewV4()),
Subsystem: "updates",
IntervalMinutes: 15,
NextRunAt: time.Now(),

View file

@ -10,7 +10,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/config"
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)
@ -180,7 +180,7 @@ func (s *AgentLifecycleService) createAgent(
) error {
machineID := cfg.MachineID
agent := &models.Agent{
ID: uuid.MustParse(cfg.AgentID),
ID: uuid.Must(uuid.FromString(cfg.AgentID)),
Hostname: cfg.Hostname,
OSType: cfg.Platform,
AgentVersion: cfg.Version,

View file

@ -8,7 +8,7 @@ import (
"github.com/Fimeg/RedFlag/server/internal/capability"
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/Fimeg/RedFlag/server/internal/models"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
)
// DefaultTokenTTL bounds how long a minted token stays valid. Short by design:
@ -72,7 +72,7 @@ func (m *CapabilityMinter) MintForUpdate(update *models.UpdateState, closure []c
now := time.Now().UTC()
token := &capability.Token{
Version: capability.Version,
TokenID: uuid.New().String(),
TokenID: uuid.Must(uuid.NewV4()).String(),
AgentID: update.AgentID.String(),
PackageType: update.PackageType,
Operation: operation,

View file

@ -9,7 +9,7 @@ import (
"time"
"github.com/Fimeg/RedFlag/server/internal/database/queries"
"github.com/google/uuid"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
)
@ -295,12 +295,12 @@ func (cb *ConfigBuilder) injectDeploymentValues(config map[string]interface{}, r
func (cb *ConfigBuilder) determineAgentID(providedAgentID string) string {
if providedAgentID != "" {
// Validate it's a proper UUID
if _, err := uuid.Parse(providedAgentID); err == nil {
if _, err := uuid.FromString(providedAgentID); err == nil {
return providedAgentID
}
}
// Generate new UUID if none provided or invalid
return uuid.New().String()
return uuid.Must(uuid.NewV4()).String()
}
// applyEnvironmentDefaults applies environment-specific configuration defaults

Some files were not shown because too many files have changed in this diff Show more