Split monolithic scan_updates into individual subsystems (updates/storage/system/docker). Scanners now run in parallel via goroutines - cuts scan time roughly in half, maybe more. Agent changes: - Orchestrator pattern for scanner management - New scanners: storage (disk metrics), system (cpu/mem/processes) - New commands: scan_storage, scan_system, scan_docker - Wrapped existing scanners (APT/DNF/Docker/Windows/Winget) with common interface - Version bump to 0.1.20 Server changes: - Migration 015: agent_subsystems table with trigger for auto-init - Subsystem CRUD: enable/disable, interval (5min-24hr), auto-run toggle - API routes: /api/v1/agents/:id/subsystems/* (9 endpoints) - Stats tracking per subsystem Web UI changes: - ChatTimeline shows subsystem-specific labels and icons - AgentScanners got interactive toggles, interval dropdowns, manual trigger buttons - TypeScript types added for subsystems Backward compatible with legacy scan_updates - for now. Bugs probably exist somewhere.
248 lines
6.6 KiB
Go
248 lines
6.6 KiB
Go
package queries
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
type AgentQueries struct {
|
|
db *sqlx.DB
|
|
}
|
|
|
|
func NewAgentQueries(db *sqlx.DB) *AgentQueries {
|
|
return &AgentQueries{db: db}
|
|
}
|
|
|
|
// CreateAgent inserts a new agent into the database
|
|
func (q *AgentQueries) CreateAgent(agent *models.Agent) error {
|
|
query := `
|
|
INSERT INTO agents (
|
|
id, hostname, os_type, os_version, os_architecture,
|
|
agent_version, last_seen, status, metadata
|
|
) VALUES (
|
|
:id, :hostname, :os_type, :os_version, :os_architecture,
|
|
:agent_version, :last_seen, :status, :metadata
|
|
)
|
|
`
|
|
_, err := q.db.NamedExec(query, agent)
|
|
return err
|
|
}
|
|
|
|
// GetAgentByID retrieves an agent by ID
|
|
func (q *AgentQueries) GetAgentByID(id uuid.UUID) (*models.Agent, error) {
|
|
var agent models.Agent
|
|
query := `SELECT * FROM agents WHERE id = $1`
|
|
err := q.db.Get(&agent, query, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &agent, nil
|
|
}
|
|
|
|
// UpdateAgentLastSeen updates the agent's last_seen timestamp
|
|
func (q *AgentQueries) UpdateAgentLastSeen(id uuid.UUID) error {
|
|
query := `UPDATE agents SET last_seen = $1, status = 'online' WHERE id = $2`
|
|
_, err := q.db.Exec(query, time.Now().UTC(), id)
|
|
return err
|
|
}
|
|
|
|
// UpdateAgent updates an agent's full record including metadata
|
|
func (q *AgentQueries) UpdateAgent(agent *models.Agent) error {
|
|
query := `
|
|
UPDATE agents SET
|
|
hostname = :hostname,
|
|
os_type = :os_type,
|
|
os_version = :os_version,
|
|
os_architecture = :os_architecture,
|
|
agent_version = :agent_version,
|
|
last_seen = :last_seen,
|
|
status = :status,
|
|
metadata = :metadata
|
|
WHERE id = :id
|
|
`
|
|
_, err := q.db.NamedExec(query, agent)
|
|
return err
|
|
}
|
|
|
|
// UpdateAgentMetadata updates only the metadata, last_seen, and status fields
|
|
// Used for metrics updates to avoid overwriting version tracking
|
|
func (q *AgentQueries) UpdateAgentMetadata(id uuid.UUID, metadata models.JSONB, status string, lastSeen time.Time) error {
|
|
query := `
|
|
UPDATE agents SET
|
|
last_seen = $1,
|
|
status = $2,
|
|
metadata = $3
|
|
WHERE id = $4
|
|
`
|
|
_, err := q.db.Exec(query, lastSeen, status, metadata, id)
|
|
return err
|
|
}
|
|
|
|
// ListAgents returns all agents with optional filtering
|
|
func (q *AgentQueries) ListAgents(status, osType string) ([]models.Agent, error) {
|
|
var agents []models.Agent
|
|
query := `SELECT * FROM agents WHERE 1=1`
|
|
args := []interface{}{}
|
|
argIdx := 1
|
|
|
|
if status != "" {
|
|
query += ` AND status = $` + string(rune(argIdx+'0'))
|
|
args = append(args, status)
|
|
argIdx++
|
|
}
|
|
if osType != "" {
|
|
query += ` AND os_type = $` + string(rune(argIdx+'0'))
|
|
args = append(args, osType)
|
|
}
|
|
|
|
query += ` ORDER BY last_seen DESC`
|
|
err := q.db.Select(&agents, query, args...)
|
|
return agents, err
|
|
}
|
|
|
|
// MarkOfflineAgents marks agents as offline if they haven't checked in recently
|
|
func (q *AgentQueries) MarkOfflineAgents(threshold time.Duration) error {
|
|
query := `
|
|
UPDATE agents
|
|
SET status = 'offline'
|
|
WHERE last_seen < $1 AND status = 'online'
|
|
`
|
|
_, err := q.db.Exec(query, time.Now().Add(-threshold))
|
|
return err
|
|
}
|
|
|
|
// GetAgentLastScan gets the last scan time from update events
|
|
func (q *AgentQueries) GetAgentLastScan(id uuid.UUID) (*time.Time, error) {
|
|
var lastScan time.Time
|
|
query := `SELECT MAX(created_at) FROM update_events WHERE agent_id = $1`
|
|
err := q.db.Get(&lastScan, query, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &lastScan, nil
|
|
}
|
|
|
|
// GetAgentWithLastScan gets agent information including last scan time
|
|
func (q *AgentQueries) GetAgentWithLastScan(id uuid.UUID) (*models.AgentWithLastScan, error) {
|
|
var agent models.AgentWithLastScan
|
|
query := `
|
|
SELECT
|
|
a.*,
|
|
(SELECT MAX(created_at) FROM update_events WHERE agent_id = a.id) as last_scan
|
|
FROM agents a
|
|
WHERE a.id = $1`
|
|
err := q.db.Get(&agent, query, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &agent, nil
|
|
}
|
|
|
|
// ListAgentsWithLastScan returns all agents with their last scan times
|
|
func (q *AgentQueries) ListAgentsWithLastScan(status, osType string) ([]models.AgentWithLastScan, error) {
|
|
var agents []models.AgentWithLastScan
|
|
query := `
|
|
SELECT
|
|
a.*,
|
|
(SELECT MAX(created_at) FROM update_events WHERE agent_id = a.id) as last_scan
|
|
FROM agents a
|
|
WHERE 1=1`
|
|
args := []interface{}{}
|
|
argIdx := 1
|
|
|
|
if status != "" {
|
|
query += ` AND a.status = $` + string(rune(argIdx+'0'))
|
|
args = append(args, status)
|
|
argIdx++
|
|
}
|
|
if osType != "" {
|
|
query += ` AND a.os_type = $` + string(rune(argIdx+'0'))
|
|
args = append(args, osType)
|
|
argIdx++
|
|
}
|
|
|
|
query += ` ORDER BY a.last_seen DESC`
|
|
err := q.db.Select(&agents, query, args...)
|
|
return agents, err
|
|
}
|
|
|
|
// UpdateAgentVersion updates the agent's version information and checks for updates
|
|
func (q *AgentQueries) UpdateAgentVersion(id uuid.UUID, currentVersion string) error {
|
|
query := `
|
|
UPDATE agents SET
|
|
current_version = $1,
|
|
last_version_check = $2
|
|
WHERE id = $3
|
|
`
|
|
_, err := q.db.Exec(query, currentVersion, time.Now().UTC(), id)
|
|
return err
|
|
}
|
|
|
|
// UpdateAgentUpdateAvailable sets whether an update is available for an agent
|
|
func (q *AgentQueries) UpdateAgentUpdateAvailable(id uuid.UUID, updateAvailable bool) error {
|
|
query := `
|
|
UPDATE agents SET
|
|
update_available = $1
|
|
WHERE id = $2
|
|
`
|
|
_, err := q.db.Exec(query, updateAvailable, id)
|
|
return err
|
|
}
|
|
|
|
// DeleteAgent removes an agent and all associated data
|
|
func (q *AgentQueries) DeleteAgent(id uuid.UUID) error {
|
|
// Start a transaction for atomic deletion
|
|
tx, err := q.db.Beginx()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Delete the agent (CASCADE will handle related records)
|
|
_, err = tx.Exec("DELETE FROM agents WHERE id = $1", id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Commit the transaction
|
|
return tx.Commit()
|
|
}
|
|
|
|
// GetActiveAgentCount returns the count of active (online) agents
|
|
func (q *AgentQueries) GetActiveAgentCount() (int, error) {
|
|
var count int
|
|
query := `SELECT COUNT(*) FROM agents WHERE status = 'online'`
|
|
err := q.db.Get(&count, query)
|
|
return count, err
|
|
}
|
|
|
|
// UpdateAgentRebootStatus updates the reboot status for an agent
|
|
func (q *AgentQueries) UpdateAgentRebootStatus(id uuid.UUID, required bool, reason string) error {
|
|
query := `
|
|
UPDATE agents
|
|
SET reboot_required = $1,
|
|
reboot_reason = $2,
|
|
updated_at = $3
|
|
WHERE id = $4
|
|
`
|
|
_, err := q.db.Exec(query, required, reason, time.Now(), id)
|
|
return err
|
|
}
|
|
|
|
// UpdateAgentLastReboot updates the last reboot timestamp for an agent
|
|
func (q *AgentQueries) UpdateAgentLastReboot(id uuid.UUID, rebootTime time.Time) error {
|
|
query := `
|
|
UPDATE agents
|
|
SET last_reboot_at = $1,
|
|
reboot_required = FALSE,
|
|
reboot_reason = '',
|
|
updated_at = $2
|
|
WHERE id = $3
|
|
`
|
|
_, err := q.db.Exec(query, rebootTime, time.Now(), id)
|
|
return err
|
|
}
|