Files
Fimeg 38894f64d3 feat: add config sync endpoint and security UI updates
- Add GET /api/v1/agents/:id/config endpoint for server configuration
- Agent fetches config during check-in and applies updates
- Add version tracking to prevent unnecessary config applications
- Clean separation: config sync independent of commands
- Fix agent UI subsystem settings to actually control agent behavior
- Update Security Health UI with frosted glass styling and tooltips
2025-11-03 22:36:26 -05:00

352 lines
9.3 KiB
Go

package queries
import (
"database/sql"
"fmt"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/google/uuid"
)
// DockerQueries handles database operations for Docker images
type DockerQueries struct {
db *sql.DB
}
func NewDockerQueries(db *sql.DB) *DockerQueries {
return &DockerQueries{db: db}
}
// CreateDockerEventsBatch creates multiple Docker image events in a single transaction
func (q *DockerQueries) CreateDockerEventsBatch(events []models.StoredDockerImage) error {
if len(events) == 0 {
return nil
}
tx, err := q.db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
// Prepare the insert statement
stmt, err := tx.Prepare(`
INSERT INTO docker_images (
id, agent_id, package_type, package_name, current_version, available_version,
severity, repository_source, metadata, event_type, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (agent_id, package_name, package_type, created_at) DO NOTHING
`)
if err != nil {
return fmt.Errorf("failed to prepare statement: %w", err)
}
defer stmt.Close()
// Insert each event with error isolation
for _, event := range events {
_, err := stmt.Exec(
event.ID,
event.AgentID,
event.PackageType,
event.PackageName,
event.CurrentVersion,
event.AvailableVersion,
event.Severity,
event.RepositorySource,
event.Metadata,
event.EventType,
event.CreatedAt,
)
if err != nil {
// Log error but continue with other events
fmt.Printf("Warning: Failed to insert docker image event %s: %v\n", event.ID, err)
continue
}
}
return tx.Commit()
}
// GetDockerImages retrieves Docker images based on filter criteria
func (q *DockerQueries) GetDockerImages(filter *models.DockerFilter) (*models.DockerResult, error) {
query := `
SELECT id, agent_id, package_type, package_name, current_version, available_version,
severity, repository_source, metadata, event_type, created_at
FROM docker_images
WHERE 1=1
`
args := []interface{}{}
argIndex := 1
// Build WHERE clause
if filter.AgentID != nil {
query += fmt.Sprintf(" AND agent_id = $%d", argIndex)
args = append(args, *filter.AgentID)
argIndex++
}
if filter.ImageName != nil {
query += fmt.Sprintf(" AND package_name ILIKE $%d", argIndex)
args = append(args, "%"+*filter.ImageName+"%")
argIndex++
}
if filter.Registry != nil {
query += fmt.Sprintf(" AND repository_source ILIKE $%d", argIndex)
args = append(args, "%"+*filter.Registry+"%")
argIndex++
}
if filter.Severity != nil {
query += fmt.Sprintf(" AND severity = $%d", argIndex)
args = append(args, *filter.Severity)
argIndex++
}
if filter.HasUpdates != nil {
if *filter.HasUpdates {
query += fmt.Sprintf(" AND current_version != available_version", argIndex)
} else {
query += fmt.Sprintf(" AND current_version = available_version", argIndex)
}
argIndex++
}
// Add ordering and pagination
query += " ORDER BY created_at DESC"
if filter.Limit != nil {
query += fmt.Sprintf(" LIMIT $%d", argIndex)
args = append(args, *filter.Limit)
argIndex++
}
if filter.Offset != nil {
query += fmt.Sprintf(" OFFSET $%d", argIndex)
args = append(args, *filter.Offset)
argIndex++
}
rows, err := q.db.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query docker images: %w", err)
}
defer rows.Close()
var images []models.StoredDockerImage
for rows.Next() {
var image models.StoredDockerImage
err := rows.Scan(
&image.ID,
&image.AgentID,
&image.PackageType,
&image.PackageName,
&image.CurrentVersion,
&image.AvailableVersion,
&image.Severity,
&image.RepositorySource,
&image.Metadata,
&image.EventType,
&image.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan docker image: %w", err)
}
images = append(images, image)
}
// Get total count
countQuery := `SELECT COUNT(*) FROM docker_images WHERE 1=1`
countArgs := []interface{}{}
countIndex := 1
if filter.AgentID != nil {
countQuery += fmt.Sprintf(" AND agent_id = $%d", countIndex)
countArgs = append(countArgs, *filter.AgentID)
countIndex++
}
if filter.ImageName != nil {
countQuery += fmt.Sprintf(" AND package_name ILIKE $%d", countIndex)
countArgs = append(countArgs, "%"+*filter.ImageName+"%")
countIndex++
}
if filter.Registry != nil {
countQuery += fmt.Sprintf(" AND repository_source ILIKE $%d", countIndex)
countArgs = append(countArgs, "%"+*filter.Registry+"%")
countIndex++
}
if filter.Severity != nil {
countQuery += fmt.Sprintf(" AND severity = $%d", countIndex)
countArgs = append(countArgs, *filter.Severity)
countIndex++
}
if filter.HasUpdates != nil {
if *filter.HasUpdates {
countQuery += fmt.Sprintf(" AND current_version != available_version", countIndex)
} else {
countQuery += fmt.Sprintf(" AND current_version = available_version", countIndex)
}
countIndex++
}
var total int
err = q.db.QueryRow(countQuery, countArgs...).Scan(&total)
if err != nil {
return nil, fmt.Errorf("failed to count docker images: %w", err)
}
// Calculate pagination
page := 1
perPage := 50
if filter.Offset != nil && filter.Limit != nil {
page = (*filter.Offset / *filter.Limit) + 1
perPage = *filter.Limit
}
return &models.DockerResult{
Images: images,
Total: total,
Page: page,
PerPage: perPage,
}, nil
}
// GetDockerImagesByAgentID retrieves Docker images for a specific agent
func (q *DockerQueries) GetDockerImagesByAgentID(agentID uuid.UUID, limit int) ([]models.StoredDockerImage, error) {
query := `
SELECT id, agent_id, package_type, package_name, current_version, available_version,
severity, repository_source, metadata, event_type, created_at
FROM docker_images
WHERE agent_id = $1
ORDER BY created_at DESC
LIMIT $2
`
rows, err := q.db.Query(query, agentID, limit)
if err != nil {
return nil, fmt.Errorf("failed to query docker images by agent: %w", err)
}
defer rows.Close()
var images []models.StoredDockerImage
for rows.Next() {
var image models.StoredDockerImage
err := rows.Scan(
&image.ID,
&image.AgentID,
&image.PackageType,
&image.PackageName,
&image.CurrentVersion,
&image.AvailableVersion,
&image.Severity,
&image.RepositorySource,
&image.Metadata,
&image.EventType,
&image.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan docker image: %w", err)
}
images = append(images, image)
}
return images, nil
}
// GetDockerImagesWithUpdates retrieves Docker images that have available updates
func (q *DockerQueries) GetDockerImagesWithUpdates(limit int) ([]models.StoredDockerImage, error) {
query := `
SELECT id, agent_id, package_type, package_name, current_version, available_version,
severity, repository_source, metadata, event_type, created_at
FROM docker_images
WHERE current_version != available_version
ORDER BY created_at DESC
LIMIT $1
`
rows, err := q.db.Query(query, limit)
if err != nil {
return nil, fmt.Errorf("failed to query docker images with updates: %w", err)
}
defer rows.Close()
var images []models.StoredDockerImage
for rows.Next() {
var image models.StoredDockerImage
err := rows.Scan(
&image.ID,
&image.AgentID,
&image.PackageType,
&image.PackageName,
&image.CurrentVersion,
&image.AvailableVersion,
&image.Severity,
&image.RepositorySource,
&image.Metadata,
&image.EventType,
&image.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan docker image: %w", err)
}
images = append(images, image)
}
return images, nil
}
// DeleteOldDockerImages deletes Docker images older than the specified number of days
func (q *DockerQueries) DeleteOldDockerImages(days int) error {
query := `DELETE FROM docker_images WHERE created_at < NOW() - INTERVAL '1 day' * $1`
result, err := q.db.Exec(query, days)
if err != nil {
return fmt.Errorf("failed to delete old docker images: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected > 0 {
fmt.Printf("Deleted %d old docker image records\n", rowsAffected)
}
return nil
}
// GetDockerStats returns statistics about Docker images across all agents
func (q *DockerQueries) GetDockerStats() (*models.DockerStats, error) {
var stats models.DockerStats
// Get total images
err := q.db.QueryRow("SELECT COUNT(*) FROM docker_images").Scan(&stats.TotalImages)
if err != nil {
return nil, fmt.Errorf("failed to get total docker images: %w", err)
}
// Get images with updates
err = q.db.QueryRow("SELECT COUNT(*) FROM docker_images WHERE current_version != available_version").Scan(&stats.UpdatesAvailable)
if err != nil {
return nil, fmt.Errorf("failed to get docker images with updates: %w", err)
}
// Get critical updates
err = q.db.QueryRow("SELECT COUNT(*) FROM docker_images WHERE severity = 'critical' AND current_version != available_version").Scan(&stats.CriticalUpdates)
if err != nil {
return nil, fmt.Errorf("failed to get critical docker updates: %w", err)
}
// Get agents with Docker images
err = q.db.QueryRow("SELECT COUNT(DISTINCT agent_id) FROM docker_images").Scan(&stats.AgentsWithContainers)
if err != nil {
return nil, fmt.Errorf("failed to get agents with docker images: %w", err)
}
return &stats, nil
}