Session 4 complete - RedFlag update management platform
🚩 Private development - version retention only ✅ Complete web dashboard (React + TypeScript + TailwindCSS) ✅ Production-ready server backend (Go + Gin + PostgreSQL) ✅ Linux agent with APT + Docker scanning + local CLI tools ✅ JWT authentication and REST API ✅ Update discovery and approval workflow 🚧 Status: Alpha software - active development 📦 Purpose: Version retention during development ⚠️ Not for public use or deployment
This commit is contained in:
178
aggregator-server/internal/api/handlers/agents.go
Normal file
178
aggregator-server/internal/api/handlers/agents.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/aggregator-project/aggregator-server/internal/api/middleware"
|
||||
"github.com/aggregator-project/aggregator-server/internal/database/queries"
|
||||
"github.com/aggregator-project/aggregator-server/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AgentHandler struct {
|
||||
agentQueries *queries.AgentQueries
|
||||
commandQueries *queries.CommandQueries
|
||||
checkInInterval int
|
||||
}
|
||||
|
||||
func NewAgentHandler(aq *queries.AgentQueries, cq *queries.CommandQueries, checkInInterval int) *AgentHandler {
|
||||
return &AgentHandler{
|
||||
agentQueries: aq,
|
||||
commandQueries: cq,
|
||||
checkInInterval: checkInInterval,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAgent handles agent registration
|
||||
func (h *AgentHandler) RegisterAgent(c *gin.Context) {
|
||||
var req models.AgentRegistrationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Create new agent
|
||||
agent := &models.Agent{
|
||||
ID: uuid.New(),
|
||||
Hostname: req.Hostname,
|
||||
OSType: req.OSType,
|
||||
OSVersion: req.OSVersion,
|
||||
OSArchitecture: req.OSArchitecture,
|
||||
AgentVersion: req.AgentVersion,
|
||||
LastSeen: time.Now(),
|
||||
Status: "online",
|
||||
Metadata: models.JSONB{},
|
||||
}
|
||||
|
||||
// Add metadata if provided
|
||||
if req.Metadata != nil {
|
||||
for k, v := range req.Metadata {
|
||||
agent.Metadata[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Save to database
|
||||
if err := h.agentQueries.CreateAgent(agent); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to register agent"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, err := middleware.GenerateAgentToken(agent.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
response := models.AgentRegistrationResponse{
|
||||
AgentID: agent.ID,
|
||||
Token: token,
|
||||
Config: map[string]interface{}{
|
||||
"check_in_interval": h.checkInInterval,
|
||||
"server_url": c.Request.Host,
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetCommands returns pending commands for an agent
|
||||
func (h *AgentHandler) GetCommands(c *gin.Context) {
|
||||
agentID := c.MustGet("agent_id").(uuid.UUID)
|
||||
|
||||
// Update last_seen
|
||||
if err := h.agentQueries.UpdateAgentLastSeen(agentID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update last seen"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get pending commands
|
||||
commands, err := h.commandQueries.GetPendingCommands(agentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve commands"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
commandItems := make([]models.CommandItem, 0, len(commands))
|
||||
for _, cmd := range commands {
|
||||
commandItems = append(commandItems, models.CommandItem{
|
||||
ID: cmd.ID.String(),
|
||||
Type: cmd.CommandType,
|
||||
Params: cmd.Params,
|
||||
})
|
||||
|
||||
// Mark as sent
|
||||
h.commandQueries.MarkCommandSent(cmd.ID)
|
||||
}
|
||||
|
||||
response := models.CommandsResponse{
|
||||
Commands: commandItems,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// ListAgents returns all agents
|
||||
func (h *AgentHandler) ListAgents(c *gin.Context) {
|
||||
status := c.Query("status")
|
||||
osType := c.Query("os_type")
|
||||
|
||||
agents, err := h.agentQueries.ListAgents(status, osType)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list agents"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"agents": agents,
|
||||
"total": len(agents),
|
||||
})
|
||||
}
|
||||
|
||||
// GetAgent returns a single agent by ID
|
||||
func (h *AgentHandler) GetAgent(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
|
||||
return
|
||||
}
|
||||
|
||||
agent, err := h.agentQueries.GetAgentByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, agent)
|
||||
}
|
||||
|
||||
// TriggerScan creates a scan command for an agent
|
||||
func (h *AgentHandler) TriggerScan(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
agentID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create scan command
|
||||
cmd := &models.AgentCommand{
|
||||
ID: uuid.New(),
|
||||
AgentID: agentID,
|
||||
CommandType: models.CommandTypeScanUpdates,
|
||||
Params: models.JSONB{},
|
||||
Status: models.CommandStatusPending,
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(cmd); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create command"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "scan triggered", "command_id": cmd.ID})
|
||||
}
|
||||
163
aggregator-server/internal/api/handlers/updates.go
Normal file
163
aggregator-server/internal/api/handlers/updates.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aggregator-project/aggregator-server/internal/database/queries"
|
||||
"github.com/aggregator-project/aggregator-server/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UpdateHandler struct {
|
||||
updateQueries *queries.UpdateQueries
|
||||
}
|
||||
|
||||
func NewUpdateHandler(uq *queries.UpdateQueries) *UpdateHandler {
|
||||
return &UpdateHandler{updateQueries: uq}
|
||||
}
|
||||
|
||||
// ReportUpdates handles update reports from agents
|
||||
func (h *UpdateHandler) ReportUpdates(c *gin.Context) {
|
||||
agentID := c.MustGet("agent_id").(uuid.UUID)
|
||||
|
||||
var req models.UpdateReportRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Process each update
|
||||
for _, item := range req.Updates {
|
||||
update := &models.UpdatePackage{
|
||||
ID: uuid.New(),
|
||||
AgentID: agentID,
|
||||
PackageType: item.PackageType,
|
||||
PackageName: item.PackageName,
|
||||
PackageDescription: item.PackageDescription,
|
||||
CurrentVersion: item.CurrentVersion,
|
||||
AvailableVersion: item.AvailableVersion,
|
||||
Severity: item.Severity,
|
||||
CVEList: models.StringArray(item.CVEList),
|
||||
KBID: item.KBID,
|
||||
RepositorySource: item.RepositorySource,
|
||||
SizeBytes: item.SizeBytes,
|
||||
Status: "pending",
|
||||
Metadata: item.Metadata,
|
||||
}
|
||||
|
||||
if err := h.updateQueries.UpsertUpdate(update); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save update"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "updates recorded",
|
||||
"count": len(req.Updates),
|
||||
})
|
||||
}
|
||||
|
||||
// ListUpdates retrieves updates with filtering
|
||||
func (h *UpdateHandler) ListUpdates(c *gin.Context) {
|
||||
filters := &models.UpdateFilters{
|
||||
Status: c.Query("status"),
|
||||
Severity: c.Query("severity"),
|
||||
PackageType: c.Query("package_type"),
|
||||
}
|
||||
|
||||
// Parse agent_id if provided
|
||||
if agentIDStr := c.Query("agent_id"); agentIDStr != "" {
|
||||
agentID, err := uuid.Parse(agentIDStr)
|
||||
if err == nil {
|
||||
filters.AgentID = &agentID
|
||||
}
|
||||
}
|
||||
|
||||
// Parse pagination
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
||||
filters.Page = page
|
||||
filters.PageSize = pageSize
|
||||
|
||||
updates, total, err := h.updateQueries.ListUpdates(filters)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list updates"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"updates": updates,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUpdate retrieves a single update by ID
|
||||
func (h *UpdateHandler) GetUpdate(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"})
|
||||
return
|
||||
}
|
||||
|
||||
update, err := h.updateQueries.GetUpdateByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "update not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, update)
|
||||
}
|
||||
|
||||
// ApproveUpdate marks an update as approved
|
||||
func (h *UpdateHandler) ApproveUpdate(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// For now, use "admin" as approver. Will integrate with proper auth later
|
||||
if err := h.updateQueries.ApproveUpdate(id, "admin"); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to approve update"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "update approved"})
|
||||
}
|
||||
|
||||
// ReportLog handles update execution logs from agents
|
||||
func (h *UpdateHandler) ReportLog(c *gin.Context) {
|
||||
agentID := c.MustGet("agent_id").(uuid.UUID)
|
||||
|
||||
var req models.UpdateLogRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
log := &models.UpdateLog{
|
||||
ID: uuid.New(),
|
||||
AgentID: agentID,
|
||||
Action: req.Action,
|
||||
Result: req.Result,
|
||||
Stdout: req.Stdout,
|
||||
Stderr: req.Stderr,
|
||||
ExitCode: req.ExitCode,
|
||||
DurationSeconds: req.DurationSeconds,
|
||||
ExecutedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.updateQueries.CreateUpdateLog(log); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save log"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "log recorded"})
|
||||
}
|
||||
71
aggregator-server/internal/api/middleware/auth.go
Normal file
71
aggregator-server/internal/api/middleware/auth.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AgentClaims represents JWT claims for agent authentication
|
||||
type AgentClaims struct {
|
||||
AgentID uuid.UUID `json:"agent_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// JWTSecret is set by the server at initialization
|
||||
var JWTSecret string
|
||||
|
||||
// GenerateAgentToken creates a new JWT token for an agent
|
||||
func GenerateAgentToken(agentID uuid.UUID) (string, error) {
|
||||
claims := AgentClaims{
|
||||
AgentID: agentID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(JWTSecret))
|
||||
}
|
||||
|
||||
// AuthMiddleware validates JWT tokens from agents
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
if tokenString == authHeader {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization format"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, &AgentClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(JWTSecret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*AgentClaims); ok {
|
||||
c.Set("agent_id", claims.AgentID)
|
||||
c.Next()
|
||||
} else {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token claims"})
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
41
aggregator-server/internal/config/config.go
Normal file
41
aggregator-server/internal/config/config.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// Config holds the application configuration
|
||||
type Config struct {
|
||||
ServerPort string
|
||||
DatabaseURL string
|
||||
JWTSecret string
|
||||
CheckInInterval int
|
||||
OfflineThreshold int
|
||||
}
|
||||
|
||||
// Load reads configuration from environment variables
|
||||
func Load() (*Config, error) {
|
||||
// Load .env file if it exists (for development)
|
||||
_ = godotenv.Load()
|
||||
|
||||
checkInInterval, _ := strconv.Atoi(getEnv("CHECK_IN_INTERVAL", "300"))
|
||||
offlineThreshold, _ := strconv.Atoi(getEnv("OFFLINE_THRESHOLD", "600"))
|
||||
|
||||
return &Config{
|
||||
ServerPort: getEnv("SERVER_PORT", "8080"),
|
||||
DatabaseURL: getEnv("DATABASE_URL", "postgres://aggregator:aggregator@localhost:5432/aggregator?sslmode=disable"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "change-me-in-production"),
|
||||
CheckInInterval: checkInInterval,
|
||||
OfflineThreshold: offlineThreshold,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
76
aggregator-server/internal/database/db.go
Normal file
76
aggregator-server/internal/database/db.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// DB wraps the database connection
|
||||
type DB struct {
|
||||
*sqlx.DB
|
||||
}
|
||||
|
||||
// Connect establishes a connection to the PostgreSQL database
|
||||
func Connect(databaseURL string) (*DB, error) {
|
||||
db, err := sqlx.Connect("postgres", databaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
// Configure connection pool
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(5)
|
||||
|
||||
// Test the connection
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
return &DB{db}, nil
|
||||
}
|
||||
|
||||
// Migrate runs database migrations
|
||||
func (db *DB) Migrate(migrationsPath string) error {
|
||||
// Read migration files
|
||||
files, err := os.ReadDir(migrationsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read migrations directory: %w", err)
|
||||
}
|
||||
|
||||
// Filter and sort .up.sql files
|
||||
var migrationFiles []string
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(file.Name(), ".up.sql") {
|
||||
migrationFiles = append(migrationFiles, file.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(migrationFiles)
|
||||
|
||||
// Execute migrations
|
||||
for _, filename := range migrationFiles {
|
||||
path := filepath.Join(migrationsPath, filename)
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read migration %s: %w", filename, err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(string(content)); err != nil {
|
||||
return fmt.Errorf("failed to execute migration %s: %w", filename, err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Executed migration: %s\n", filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (db *DB) Close() error {
|
||||
return db.DB.Close()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Drop tables in reverse order (respecting foreign key constraints)
|
||||
DROP TABLE IF EXISTS agent_commands;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS agent_tags;
|
||||
DROP TABLE IF EXISTS update_logs;
|
||||
DROP TABLE IF EXISTS update_packages;
|
||||
DROP TABLE IF EXISTS agent_specs;
|
||||
DROP TABLE IF EXISTS agents;
|
||||
|
||||
-- Drop extension
|
||||
DROP EXTENSION IF EXISTS "uuid-ossp";
|
||||
@@ -0,0 +1,127 @@
|
||||
-- Enable UUID extension
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Agents table
|
||||
CREATE TABLE agents (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
hostname VARCHAR(255) NOT NULL,
|
||||
os_type VARCHAR(50) NOT NULL CHECK (os_type IN ('windows', 'linux', 'macos')),
|
||||
os_version VARCHAR(100),
|
||||
os_architecture VARCHAR(20),
|
||||
agent_version VARCHAR(20) NOT NULL,
|
||||
last_seen TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
status VARCHAR(20) DEFAULT 'online' CHECK (status IN ('online', 'offline', 'error')),
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_agents_status ON agents(status);
|
||||
CREATE INDEX idx_agents_os_type ON agents(os_type);
|
||||
CREATE INDEX idx_agents_last_seen ON agents(last_seen);
|
||||
|
||||
-- Agent specs
|
||||
CREATE TABLE agent_specs (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
agent_id UUID REFERENCES agents(id) ON DELETE CASCADE,
|
||||
cpu_model VARCHAR(255),
|
||||
cpu_cores INTEGER,
|
||||
memory_total_mb INTEGER,
|
||||
disk_total_gb INTEGER,
|
||||
disk_free_gb INTEGER,
|
||||
network_interfaces JSONB,
|
||||
docker_installed BOOLEAN DEFAULT false,
|
||||
docker_version VARCHAR(50),
|
||||
package_managers TEXT[],
|
||||
collected_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_agent_specs_agent_id ON agent_specs(agent_id);
|
||||
|
||||
-- Update packages
|
||||
CREATE TABLE update_packages (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
agent_id UUID REFERENCES agents(id) ON DELETE CASCADE,
|
||||
package_type VARCHAR(50) NOT NULL,
|
||||
package_name VARCHAR(500) NOT NULL,
|
||||
package_description TEXT,
|
||||
current_version VARCHAR(100),
|
||||
available_version VARCHAR(100) NOT NULL,
|
||||
severity VARCHAR(20) CHECK (severity IN ('critical', 'important', 'moderate', 'low', 'none')),
|
||||
cve_list TEXT[],
|
||||
kb_id VARCHAR(50),
|
||||
repository_source VARCHAR(255),
|
||||
size_bytes BIGINT,
|
||||
status VARCHAR(30) DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'scheduled', 'installing', 'installed', 'failed', 'ignored')),
|
||||
discovered_at TIMESTAMP DEFAULT NOW(),
|
||||
approved_by VARCHAR(255),
|
||||
approved_at TIMESTAMP,
|
||||
scheduled_for TIMESTAMP,
|
||||
installed_at TIMESTAMP,
|
||||
error_message TEXT,
|
||||
metadata JSONB,
|
||||
UNIQUE(agent_id, package_type, package_name, available_version)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_updates_status ON update_packages(status);
|
||||
CREATE INDEX idx_updates_agent ON update_packages(agent_id);
|
||||
CREATE INDEX idx_updates_severity ON update_packages(severity);
|
||||
CREATE INDEX idx_updates_package_type ON update_packages(package_type);
|
||||
CREATE INDEX idx_updates_composite ON update_packages(status, severity, agent_id);
|
||||
|
||||
-- Update logs
|
||||
CREATE TABLE update_logs (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
agent_id UUID REFERENCES agents(id) ON DELETE CASCADE,
|
||||
update_package_id UUID REFERENCES update_packages(id) ON DELETE SET NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
result VARCHAR(20) NOT NULL CHECK (result IN ('success', 'failed', 'partial')),
|
||||
stdout TEXT,
|
||||
stderr TEXT,
|
||||
exit_code INTEGER,
|
||||
duration_seconds INTEGER,
|
||||
executed_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_logs_agent ON update_logs(agent_id);
|
||||
CREATE INDEX idx_logs_result ON update_logs(result);
|
||||
CREATE INDEX idx_logs_executed_at ON update_logs(executed_at DESC);
|
||||
|
||||
-- Agent tags
|
||||
CREATE TABLE agent_tags (
|
||||
agent_id UUID REFERENCES agents(id) ON DELETE CASCADE,
|
||||
tag VARCHAR(100) NOT NULL,
|
||||
PRIMARY KEY (agent_id, tag)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_agent_tags_tag ON agent_tags(tag);
|
||||
|
||||
-- Users (for authentication)
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) DEFAULT 'user' CHECK (role IN ('admin', 'user', 'readonly')),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
last_login TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
|
||||
-- Commands queue (for agent orchestration)
|
||||
CREATE TABLE agent_commands (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
agent_id UUID REFERENCES agents(id) ON DELETE CASCADE,
|
||||
command_type VARCHAR(50) NOT NULL,
|
||||
params JSONB,
|
||||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'completed', 'failed')),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
sent_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
result JSONB
|
||||
);
|
||||
|
||||
CREATE INDEX idx_commands_agent_status ON agent_commands(agent_id, status);
|
||||
CREATE INDEX idx_commands_created_at ON agent_commands(created_at DESC);
|
||||
83
aggregator-server/internal/database/queries/agents.go
Normal file
83
aggregator-server/internal/database/queries/agents.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/aggregator-project/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(), 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
|
||||
}
|
||||
79
aggregator-server/internal/database/queries/commands.go
Normal file
79
aggregator-server/internal/database/queries/commands.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/aggregator-project/aggregator-server/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type CommandQueries struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewCommandQueries(db *sqlx.DB) *CommandQueries {
|
||||
return &CommandQueries{db: db}
|
||||
}
|
||||
|
||||
// CreateCommand inserts a new command for an agent
|
||||
func (q *CommandQueries) CreateCommand(cmd *models.AgentCommand) error {
|
||||
query := `
|
||||
INSERT INTO agent_commands (
|
||||
id, agent_id, command_type, params, status
|
||||
) VALUES (
|
||||
:id, :agent_id, :command_type, :params, :status
|
||||
)
|
||||
`
|
||||
_, err := q.db.NamedExec(query, cmd)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPendingCommands retrieves pending commands for an agent
|
||||
func (q *CommandQueries) GetPendingCommands(agentID uuid.UUID) ([]models.AgentCommand, error) {
|
||||
var commands []models.AgentCommand
|
||||
query := `
|
||||
SELECT * FROM agent_commands
|
||||
WHERE agent_id = $1 AND status = 'pending'
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 10
|
||||
`
|
||||
err := q.db.Select(&commands, query, agentID)
|
||||
return commands, err
|
||||
}
|
||||
|
||||
// MarkCommandSent updates a command's status to sent
|
||||
func (q *CommandQueries) MarkCommandSent(id uuid.UUID) error {
|
||||
now := time.Now()
|
||||
query := `
|
||||
UPDATE agent_commands
|
||||
SET status = 'sent', sent_at = $1
|
||||
WHERE id = $2
|
||||
`
|
||||
_, err := q.db.Exec(query, now, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// MarkCommandCompleted updates a command's status to completed
|
||||
func (q *CommandQueries) MarkCommandCompleted(id uuid.UUID, result models.JSONB) error {
|
||||
now := time.Now()
|
||||
query := `
|
||||
UPDATE agent_commands
|
||||
SET status = 'completed', completed_at = $1, result = $2
|
||||
WHERE id = $3
|
||||
`
|
||||
_, err := q.db.Exec(query, now, result, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// MarkCommandFailed updates a command's status to failed
|
||||
func (q *CommandQueries) MarkCommandFailed(id uuid.UUID, result models.JSONB) error {
|
||||
now := time.Now()
|
||||
query := `
|
||||
UPDATE agent_commands
|
||||
SET status = 'failed', completed_at = $1, result = $2
|
||||
WHERE id = $3
|
||||
`
|
||||
_, err := q.db.Exec(query, now, result, id)
|
||||
return err
|
||||
}
|
||||
141
aggregator-server/internal/database/queries/updates.go
Normal file
141
aggregator-server/internal/database/queries/updates.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aggregator-project/aggregator-server/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type UpdateQueries struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewUpdateQueries(db *sqlx.DB) *UpdateQueries {
|
||||
return &UpdateQueries{db: db}
|
||||
}
|
||||
|
||||
// UpsertUpdate inserts or updates an update package
|
||||
func (q *UpdateQueries) UpsertUpdate(update *models.UpdatePackage) error {
|
||||
query := `
|
||||
INSERT INTO update_packages (
|
||||
id, agent_id, package_type, package_name, package_description,
|
||||
current_version, available_version, severity, cve_list, kb_id,
|
||||
repository_source, size_bytes, status, metadata
|
||||
) VALUES (
|
||||
:id, :agent_id, :package_type, :package_name, :package_description,
|
||||
:current_version, :available_version, :severity, :cve_list, :kb_id,
|
||||
:repository_source, :size_bytes, :status, :metadata
|
||||
)
|
||||
ON CONFLICT (agent_id, package_type, package_name, available_version)
|
||||
DO UPDATE SET
|
||||
package_description = EXCLUDED.package_description,
|
||||
current_version = EXCLUDED.current_version,
|
||||
severity = EXCLUDED.severity,
|
||||
cve_list = EXCLUDED.cve_list,
|
||||
kb_id = EXCLUDED.kb_id,
|
||||
repository_source = EXCLUDED.repository_source,
|
||||
size_bytes = EXCLUDED.size_bytes,
|
||||
metadata = EXCLUDED.metadata,
|
||||
discovered_at = NOW()
|
||||
`
|
||||
_, err := q.db.NamedExec(query, update)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListUpdates retrieves updates with filtering
|
||||
func (q *UpdateQueries) ListUpdates(filters *models.UpdateFilters) ([]models.UpdatePackage, int, error) {
|
||||
var updates []models.UpdatePackage
|
||||
whereClause := []string{"1=1"}
|
||||
args := []interface{}{}
|
||||
argIdx := 1
|
||||
|
||||
if filters.AgentID != nil {
|
||||
whereClause = append(whereClause, fmt.Sprintf("agent_id = $%d", argIdx))
|
||||
args = append(args, *filters.AgentID)
|
||||
argIdx++
|
||||
}
|
||||
if filters.Status != "" {
|
||||
whereClause = append(whereClause, fmt.Sprintf("status = $%d", argIdx))
|
||||
args = append(args, filters.Status)
|
||||
argIdx++
|
||||
}
|
||||
if filters.Severity != "" {
|
||||
whereClause = append(whereClause, fmt.Sprintf("severity = $%d", argIdx))
|
||||
args = append(args, filters.Severity)
|
||||
argIdx++
|
||||
}
|
||||
if filters.PackageType != "" {
|
||||
whereClause = append(whereClause, fmt.Sprintf("package_type = $%d", argIdx))
|
||||
args = append(args, filters.PackageType)
|
||||
argIdx++
|
||||
}
|
||||
|
||||
// Get total count
|
||||
countQuery := "SELECT COUNT(*) FROM update_packages WHERE " + strings.Join(whereClause, " AND ")
|
||||
var total int
|
||||
err := q.db.Get(&total, countQuery, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Get paginated results
|
||||
query := fmt.Sprintf(`
|
||||
SELECT * FROM update_packages
|
||||
WHERE %s
|
||||
ORDER BY discovered_at DESC
|
||||
LIMIT $%d OFFSET $%d
|
||||
`, strings.Join(whereClause, " AND "), argIdx, argIdx+1)
|
||||
|
||||
limit := filters.PageSize
|
||||
if limit == 0 {
|
||||
limit = 50
|
||||
}
|
||||
offset := (filters.Page - 1) * limit
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
args = append(args, limit, offset)
|
||||
err = q.db.Select(&updates, query, args...)
|
||||
return updates, total, err
|
||||
}
|
||||
|
||||
// GetUpdateByID retrieves a single update by ID
|
||||
func (q *UpdateQueries) GetUpdateByID(id uuid.UUID) (*models.UpdatePackage, error) {
|
||||
var update models.UpdatePackage
|
||||
query := `SELECT * FROM update_packages WHERE id = $1`
|
||||
err := q.db.Get(&update, query, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &update, nil
|
||||
}
|
||||
|
||||
// ApproveUpdate marks an update as approved
|
||||
func (q *UpdateQueries) ApproveUpdate(id uuid.UUID, approvedBy string) error {
|
||||
query := `
|
||||
UPDATE update_packages
|
||||
SET status = 'approved', approved_by = $1, approved_at = NOW()
|
||||
WHERE id = $2 AND status = 'pending'
|
||||
`
|
||||
_, err := q.db.Exec(query, approvedBy, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateUpdateLog inserts an update log entry
|
||||
func (q *UpdateQueries) CreateUpdateLog(log *models.UpdateLog) error {
|
||||
query := `
|
||||
INSERT INTO update_logs (
|
||||
id, agent_id, update_package_id, action, result,
|
||||
stdout, stderr, exit_code, duration_seconds
|
||||
) VALUES (
|
||||
:id, :agent_id, :update_package_id, :action, :result,
|
||||
:stdout, :stderr, :exit_code, :duration_seconds
|
||||
)
|
||||
`
|
||||
_, err := q.db.NamedExec(query, log)
|
||||
return err
|
||||
}
|
||||
105
aggregator-server/internal/models/agent.go
Normal file
105
aggregator-server/internal/models/agent.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Agent represents a registered update agent
|
||||
type Agent struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Hostname string `json:"hostname" db:"hostname"`
|
||||
OSType string `json:"os_type" db:"os_type"`
|
||||
OSVersion string `json:"os_version" db:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture" db:"os_architecture"`
|
||||
AgentVersion string `json:"agent_version" db:"agent_version"`
|
||||
LastSeen time.Time `json:"last_seen" db:"last_seen"`
|
||||
Status string `json:"status" db:"status"`
|
||||
Metadata JSONB `json:"metadata" db:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// AgentSpecs represents system specifications for an agent
|
||||
type AgentSpecs struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
|
||||
CPUModel string `json:"cpu_model" db:"cpu_model"`
|
||||
CPUCores int `json:"cpu_cores" db:"cpu_cores"`
|
||||
MemoryTotalMB int `json:"memory_total_mb" db:"memory_total_mb"`
|
||||
DiskTotalGB int `json:"disk_total_gb" db:"disk_total_gb"`
|
||||
DiskFreeGB int `json:"disk_free_gb" db:"disk_free_gb"`
|
||||
NetworkInterfaces JSONB `json:"network_interfaces" db:"network_interfaces"`
|
||||
DockerInstalled bool `json:"docker_installed" db:"docker_installed"`
|
||||
DockerVersion string `json:"docker_version" db:"docker_version"`
|
||||
PackageManagers StringArray `json:"package_managers" db:"package_managers"`
|
||||
CollectedAt time.Time `json:"collected_at" db:"collected_at"`
|
||||
}
|
||||
|
||||
// AgentRegistrationRequest is the payload for agent registration
|
||||
type AgentRegistrationRequest struct {
|
||||
Hostname string `json:"hostname" binding:"required"`
|
||||
OSType string `json:"os_type" binding:"required"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
AgentVersion string `json:"agent_version" binding:"required"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// AgentRegistrationResponse is returned after successful registration
|
||||
type AgentRegistrationResponse struct {
|
||||
AgentID uuid.UUID `json:"agent_id"`
|
||||
Token string `json:"token"`
|
||||
Config map[string]interface{} `json:"config"`
|
||||
}
|
||||
|
||||
// JSONB type for PostgreSQL JSONB columns
|
||||
type JSONB map[string]interface{}
|
||||
|
||||
// Value implements driver.Valuer for database storage
|
||||
func (j JSONB) Value() (driver.Value, error) {
|
||||
if j == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(j)
|
||||
}
|
||||
|
||||
// Scan implements sql.Scanner for database retrieval
|
||||
func (j *JSONB) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*j = nil
|
||||
return nil
|
||||
}
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(bytes, j)
|
||||
}
|
||||
|
||||
// StringArray type for PostgreSQL text[] columns
|
||||
type StringArray []string
|
||||
|
||||
// Value implements driver.Valuer
|
||||
func (s StringArray) Value() (driver.Value, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
// Scan implements sql.Scanner
|
||||
func (s *StringArray) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*s = nil
|
||||
return nil
|
||||
}
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(bytes, s)
|
||||
}
|
||||
49
aggregator-server/internal/models/command.go
Normal file
49
aggregator-server/internal/models/command.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AgentCommand represents a command to be executed by an agent
|
||||
type AgentCommand struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
|
||||
CommandType string `json:"command_type" db:"command_type"`
|
||||
Params JSONB `json:"params" db:"params"`
|
||||
Status string `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
SentAt *time.Time `json:"sent_at,omitempty" db:"sent_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" db:"completed_at"`
|
||||
Result JSONB `json:"result,omitempty" db:"result"`
|
||||
}
|
||||
|
||||
// CommandsResponse is returned when an agent checks in for commands
|
||||
type CommandsResponse struct {
|
||||
Commands []CommandItem `json:"commands"`
|
||||
}
|
||||
|
||||
// CommandItem represents a command in the response
|
||||
type CommandItem struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Params JSONB `json:"params"`
|
||||
}
|
||||
|
||||
// Command types
|
||||
const (
|
||||
CommandTypeScanUpdates = "scan_updates"
|
||||
CommandTypeCollectSpecs = "collect_specs"
|
||||
CommandTypeInstallUpdate = "install_updates"
|
||||
CommandTypeRollback = "rollback_update"
|
||||
CommandTypeUpdateAgent = "update_agent"
|
||||
)
|
||||
|
||||
// Command statuses
|
||||
const (
|
||||
CommandStatusPending = "pending"
|
||||
CommandStatusSent = "sent"
|
||||
CommandStatusCompleted = "completed"
|
||||
CommandStatusFailed = "failed"
|
||||
)
|
||||
88
aggregator-server/internal/models/update.go
Normal file
88
aggregator-server/internal/models/update.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UpdatePackage represents a single update available for installation
|
||||
type UpdatePackage struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
|
||||
PackageType string `json:"package_type" db:"package_type"`
|
||||
PackageName string `json:"package_name" db:"package_name"`
|
||||
PackageDescription string `json:"package_description" db:"package_description"`
|
||||
CurrentVersion string `json:"current_version" db:"current_version"`
|
||||
AvailableVersion string `json:"available_version" db:"available_version"`
|
||||
Severity string `json:"severity" db:"severity"`
|
||||
CVEList StringArray `json:"cve_list" db:"cve_list"`
|
||||
KBID string `json:"kb_id" db:"kb_id"`
|
||||
RepositorySource string `json:"repository_source" db:"repository_source"`
|
||||
SizeBytes int64 `json:"size_bytes" db:"size_bytes"`
|
||||
Status string `json:"status" db:"status"`
|
||||
DiscoveredAt time.Time `json:"discovered_at" db:"discovered_at"`
|
||||
ApprovedBy string `json:"approved_by,omitempty" db:"approved_by"`
|
||||
ApprovedAt *time.Time `json:"approved_at,omitempty" db:"approved_at"`
|
||||
ScheduledFor *time.Time `json:"scheduled_for,omitempty" db:"scheduled_for"`
|
||||
InstalledAt *time.Time `json:"installed_at,omitempty" db:"installed_at"`
|
||||
ErrorMessage string `json:"error_message,omitempty" db:"error_message"`
|
||||
Metadata JSONB `json:"metadata" db:"metadata"`
|
||||
}
|
||||
|
||||
// UpdateReportRequest is sent by agents when reporting discovered updates
|
||||
type UpdateReportRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Updates []UpdateReportItem `json:"updates"`
|
||||
}
|
||||
|
||||
// UpdateReportItem represents a single update discovered by an agent
|
||||
type UpdateReportItem struct {
|
||||
PackageType string `json:"package_type" binding:"required"`
|
||||
PackageName string `json:"package_name" binding:"required"`
|
||||
PackageDescription string `json:"package_description"`
|
||||
CurrentVersion string `json:"current_version"`
|
||||
AvailableVersion string `json:"available_version" binding:"required"`
|
||||
Severity string `json:"severity"`
|
||||
CVEList []string `json:"cve_list"`
|
||||
KBID string `json:"kb_id"`
|
||||
RepositorySource string `json:"repository_source"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
Metadata JSONB `json:"metadata"`
|
||||
}
|
||||
|
||||
// UpdateLog represents an execution log entry
|
||||
type UpdateLog struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
|
||||
UpdatePackageID *uuid.UUID `json:"update_package_id,omitempty" db:"update_package_id"`
|
||||
Action string `json:"action" db:"action"`
|
||||
Result string `json:"result" db:"result"`
|
||||
Stdout string `json:"stdout" db:"stdout"`
|
||||
Stderr string `json:"stderr" db:"stderr"`
|
||||
ExitCode int `json:"exit_code" db:"exit_code"`
|
||||
DurationSeconds int `json:"duration_seconds" db:"duration_seconds"`
|
||||
ExecutedAt time.Time `json:"executed_at" db:"executed_at"`
|
||||
}
|
||||
|
||||
// UpdateLogRequest is sent by agents when reporting execution results
|
||||
type UpdateLogRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Action string `json:"action" binding:"required"`
|
||||
Result string `json:"result" binding:"required"`
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
DurationSeconds int `json:"duration_seconds"`
|
||||
}
|
||||
|
||||
// UpdateFilters for querying updates
|
||||
type UpdateFilters struct {
|
||||
AgentID *uuid.UUID
|
||||
Status string
|
||||
Severity string
|
||||
PackageType string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
Reference in New Issue
Block a user