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
This commit is contained in:
@@ -31,6 +31,10 @@ const (
|
||||
AgentVersion = "0.1.22" // v0.1.22: Machine binding and version enforcement security release
|
||||
)
|
||||
|
||||
var (
|
||||
lastConfigVersion int64 = 0 // Track last applied config version
|
||||
)
|
||||
|
||||
// getConfigPath returns the platform-specific config path
|
||||
func getConfigPath() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -452,6 +456,79 @@ func renewTokenIfNeeded(apiClient *client.Client, cfg *config.Config, err error)
|
||||
return apiClient, nil
|
||||
}
|
||||
|
||||
// syncServerConfig checks for and applies server configuration updates
|
||||
func syncServerConfig(apiClient *client.Client, cfg *config.Config) error {
|
||||
// Get current config from server
|
||||
serverConfig, err := apiClient.GetConfig(cfg.AgentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get server config: %w", err)
|
||||
}
|
||||
|
||||
// Check if config version is newer
|
||||
if serverConfig.Version <= lastConfigVersion {
|
||||
return nil // No update needed
|
||||
}
|
||||
|
||||
log.Printf("📡 Server config update detected (version: %d)", serverConfig.Version)
|
||||
changes := false
|
||||
|
||||
// Apply subsystem configuration from server
|
||||
for subsystemName, subsystemConfig := range serverConfig.Subsystems {
|
||||
if configMap, ok := subsystemConfig.(map[string]interface{}); ok {
|
||||
enabled := false
|
||||
intervalMinutes := 0
|
||||
autoRun := false
|
||||
|
||||
if e, exists := configMap["enabled"]; exists {
|
||||
if eVal, ok := e.(bool); ok {
|
||||
enabled = eVal
|
||||
}
|
||||
}
|
||||
|
||||
if i, exists := configMap["interval_minutes"]; exists {
|
||||
if iVal, ok := i.(float64); ok {
|
||||
intervalMinutes = int(iVal)
|
||||
}
|
||||
}
|
||||
|
||||
if a, exists := configMap["auto_run"]; exists {
|
||||
if aVal, ok := a.(bool); ok {
|
||||
autoRun = aVal
|
||||
}
|
||||
}
|
||||
|
||||
// Only log changes if different from current state
|
||||
currentEnabled := cfg.Subsystems.APT.Enabled // We'd need to check actual current state here
|
||||
if enabled != currentEnabled {
|
||||
log.Printf(" → %s: enabled=%v (changed)", subsystemName, enabled)
|
||||
changes = true
|
||||
}
|
||||
|
||||
if intervalMinutes > 0 && intervalMinutes != cfg.CheckInInterval {
|
||||
log.Printf(" → %s: interval=%d minutes (changed)", subsystemName, intervalMinutes)
|
||||
changes = true
|
||||
// Note: For now, we use the check-in interval from server config
|
||||
cfg.CheckInInterval = intervalMinutes
|
||||
}
|
||||
|
||||
if autoRun {
|
||||
log.Printf(" → %s: auto_run=%v (server-side scheduling)", subsystemName, autoRun)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changes {
|
||||
log.Printf("✅ Server configuration applied successfully")
|
||||
} else {
|
||||
log.Printf("ℹ️ Server config received but no changes detected")
|
||||
}
|
||||
|
||||
// Update last config version
|
||||
lastConfigVersion = serverConfig.Version
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runAgent(cfg *config.Config) error {
|
||||
log.Printf("🚩 RedFlag Agent v%s starting...\n", AgentVersion)
|
||||
log.Printf("==================================================================")
|
||||
@@ -670,6 +747,13 @@ func runAgent(cfg *config.Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync configuration from server (non-blocking)
|
||||
go func() {
|
||||
if err := syncServerConfig(apiClient, cfg); err != nil {
|
||||
log.Printf("Warning: Failed to sync server config: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
commands := response.Commands
|
||||
if len(commands) == 0 {
|
||||
log.Printf("Check-in successful - no new commands")
|
||||
|
||||
@@ -285,21 +285,59 @@ func handleScanDocker(apiClient *client.Client, cfg *config.Config, ackTracker *
|
||||
log.Printf("Failed to report scan log: %v\n", err)
|
||||
}
|
||||
|
||||
// Report updates to server if any were found
|
||||
if len(result.Updates) > 0 {
|
||||
report := client.UpdateReport{
|
||||
CommandID: commandID,
|
||||
Timestamp: time.Now(),
|
||||
Updates: result.Updates,
|
||||
// Report Docker images to server using dedicated endpoint
|
||||
// Get Docker scanner and use proper interface
|
||||
dockerScanner, err := orchestrator.NewDockerScanner()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Docker scanner: %w", err)
|
||||
}
|
||||
defer dockerScanner.Close()
|
||||
|
||||
if dockerScanner.IsAvailable() {
|
||||
images, err := dockerScanner.ScanDocker()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to scan Docker images: %w", err)
|
||||
}
|
||||
|
||||
if err := apiClient.ReportUpdates(cfg.AgentID, report); err != nil {
|
||||
return fmt.Errorf("failed to report Docker updates: %w", err)
|
||||
}
|
||||
// Always report all Docker images (not just those with updates)
|
||||
if len(images) > 0 {
|
||||
// Convert DockerImage to DockerReportItem for API call
|
||||
imageItems := make([]client.DockerReportItem, 0, len(images))
|
||||
for _, image := range images {
|
||||
item := client.DockerReportItem{
|
||||
PackageType: "docker_image",
|
||||
PackageName: image.ImageName,
|
||||
CurrentVersion: image.ImageID,
|
||||
AvailableVersion: image.LatestImageID,
|
||||
Severity: image.Severity,
|
||||
RepositorySource: image.RepositorySource,
|
||||
Metadata: image.Metadata,
|
||||
}
|
||||
imageItems = append(imageItems, item)
|
||||
}
|
||||
|
||||
log.Printf("✓ Reported %d Docker image updates to server\n", len(result.Updates))
|
||||
report := client.DockerReport{
|
||||
CommandID: commandID,
|
||||
Timestamp: time.Now(),
|
||||
Images: imageItems,
|
||||
}
|
||||
|
||||
if err := apiClient.ReportDockerImages(cfg.AgentID, report); err != nil {
|
||||
return fmt.Errorf("failed to report Docker images: %w", err)
|
||||
}
|
||||
|
||||
updateCount := 0
|
||||
for _, image := range images {
|
||||
if image.HasUpdate {
|
||||
updateCount++
|
||||
}
|
||||
}
|
||||
log.Printf("✓ Reported %d Docker images (%d with updates) to server\n", len(images), updateCount)
|
||||
} else {
|
||||
log.Println("No Docker images found")
|
||||
}
|
||||
} else {
|
||||
log.Println("No Docker image updates found")
|
||||
log.Println("Docker not available on this system")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
262
aggregator-agent/install.sh.deprecated
Executable file
262
aggregator-agent/install.sh.deprecated
Executable file
@@ -0,0 +1,262 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# RedFlag Agent Installation Script
|
||||
# This script installs the RedFlag agent as a systemd service with proper permissions
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
AGENT_USER="redflag-agent"
|
||||
AGENT_HOME="/var/lib/redflag-agent"
|
||||
AGENT_BINARY="/usr/local/bin/redflag-agent"
|
||||
SUDOERS_FILE="/etc/sudoers.d/redflag-agent"
|
||||
SERVICE_FILE="/etc/systemd/system/redflag-agent.service"
|
||||
|
||||
echo "=== RedFlag Agent Installation ==="
|
||||
echo ""
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "ERROR: This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to create user if doesn't exist
|
||||
create_user() {
|
||||
if id "$AGENT_USER" &>/dev/null; then
|
||||
echo "✓ User $AGENT_USER already exists"
|
||||
else
|
||||
echo "Creating system user $AGENT_USER..."
|
||||
useradd -r -s /bin/false -d "$AGENT_HOME" -m "$AGENT_USER"
|
||||
echo "✓ User $AGENT_USER created"
|
||||
fi
|
||||
|
||||
# Add user to docker group for Docker update scanning
|
||||
if getent group docker &>/dev/null; then
|
||||
echo "Adding $AGENT_USER to docker group..."
|
||||
usermod -aG docker "$AGENT_USER"
|
||||
echo "✓ User $AGENT_USER added to docker group"
|
||||
else
|
||||
echo "⚠ Docker group not found - Docker updates will not be available"
|
||||
echo " (Install Docker first, then reinstall the agent to enable Docker support)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to build agent binary
|
||||
build_agent() {
|
||||
echo "Building agent binary..."
|
||||
cd "$SCRIPT_DIR"
|
||||
go build -o redflag-agent ./cmd/agent
|
||||
echo "✓ Agent binary built"
|
||||
}
|
||||
|
||||
# Function to install agent binary
|
||||
install_binary() {
|
||||
echo "Installing agent binary to $AGENT_BINARY..."
|
||||
cp "$SCRIPT_DIR/redflag-agent" "$AGENT_BINARY"
|
||||
chmod 755 "$AGENT_BINARY"
|
||||
chown root:root "$AGENT_BINARY"
|
||||
echo "✓ Agent binary installed"
|
||||
|
||||
# Set SELinux context for binary if SELinux is enabled
|
||||
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
|
||||
echo "SELinux detected, setting file context for binary..."
|
||||
restorecon -v "$AGENT_BINARY" 2>/dev/null || true
|
||||
echo "✓ SELinux context set for binary"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to install sudoers configuration
|
||||
install_sudoers() {
|
||||
echo "Installing sudoers configuration..."
|
||||
cat > "$SUDOERS_FILE" <<'EOF'
|
||||
# RedFlag Agent minimal sudo permissions
|
||||
# This file is generated automatically during RedFlag agent installation
|
||||
|
||||
# APT package management commands
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/apt-get update
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/apt-get install -y *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/apt-get upgrade -y *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/apt-get install --dry-run --yes *
|
||||
|
||||
# DNF package management commands
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/dnf makecache
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/dnf install -y *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/dnf upgrade -y *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/dnf install --assumeno --downloadonly *
|
||||
|
||||
# Docker operations
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/docker pull *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/docker image inspect *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/docker manifest inspect *
|
||||
EOF
|
||||
|
||||
chmod 440 "$SUDOERS_FILE"
|
||||
|
||||
# Validate sudoers file
|
||||
if visudo -c -f "$SUDOERS_FILE"; then
|
||||
echo "✓ Sudoers configuration installed and validated"
|
||||
else
|
||||
echo "ERROR: Sudoers configuration is invalid"
|
||||
rm -f "$SUDOERS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to install systemd service
|
||||
install_service() {
|
||||
echo "Installing systemd service..."
|
||||
cat > "$SERVICE_FILE" <<EOF
|
||||
[Unit]
|
||||
Description=RedFlag Update Agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$AGENT_USER
|
||||
Group=$AGENT_USER
|
||||
WorkingDirectory=$AGENT_HOME
|
||||
ExecStart=$AGENT_BINARY
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
# Security hardening
|
||||
# NoNewPrivileges=true - DISABLED: Prevents sudo from working
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=$AGENT_HOME /var/log /etc/aggregator /var/lib/aggregator
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
chmod 644 "$SERVICE_FILE"
|
||||
echo "✓ Systemd service installed"
|
||||
}
|
||||
|
||||
# Function to start and enable service
|
||||
start_service() {
|
||||
echo "Reloading systemd daemon..."
|
||||
systemctl daemon-reload
|
||||
|
||||
# Stop service if running
|
||||
if systemctl is-active --quiet redflag-agent; then
|
||||
echo "Stopping existing service..."
|
||||
systemctl stop redflag-agent
|
||||
fi
|
||||
|
||||
echo "Enabling and starting redflag-agent service..."
|
||||
systemctl enable redflag-agent
|
||||
systemctl start redflag-agent
|
||||
|
||||
# Wait a moment for service to start
|
||||
sleep 2
|
||||
|
||||
echo "✓ Service started"
|
||||
}
|
||||
|
||||
# Function to show status
|
||||
show_status() {
|
||||
echo ""
|
||||
echo "=== Service Status ==="
|
||||
systemctl status redflag-agent --no-pager -l
|
||||
echo ""
|
||||
echo "=== Recent Logs ==="
|
||||
journalctl -u redflag-agent -n 20 --no-pager
|
||||
}
|
||||
|
||||
# Function to register agent
|
||||
register_agent() {
|
||||
local server_url="${1:-http://localhost:8080}"
|
||||
|
||||
echo "Registering agent with server at $server_url..."
|
||||
|
||||
# Create config directory
|
||||
mkdir -p /etc/aggregator
|
||||
|
||||
# Create state directory for acknowledgment tracking (v0.1.19+)
|
||||
mkdir -p /var/lib/aggregator
|
||||
chown redflag-agent:redflag-agent /var/lib/aggregator
|
||||
chmod 755 /var/lib/aggregator
|
||||
|
||||
# Clean up old configuration files (prevents conflicts during reinstall/upgrade)
|
||||
if [ -f /etc/aggregator/.env ]; then
|
||||
echo "Removing old .env file..."
|
||||
rm -f /etc/aggregator/.env
|
||||
echo "✓ Old .env file removed"
|
||||
fi
|
||||
if [ -f /etc/aggregator/config.json ]; then
|
||||
echo "Removing old config.json file..."
|
||||
rm -f /etc/aggregator/config.json
|
||||
echo "✓ Old config.json file removed"
|
||||
fi
|
||||
|
||||
# Set SELinux context for config directory if SELinux is enabled
|
||||
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
|
||||
echo "Setting SELinux context for config directory..."
|
||||
restorecon -Rv /etc/aggregator 2>/dev/null || true
|
||||
echo "✓ SELinux context set for config directory"
|
||||
fi
|
||||
|
||||
# Register agent (run as regular binary, not as service)
|
||||
if "$AGENT_BINARY" -register -server "$server_url"; then
|
||||
echo "✓ Agent registered successfully"
|
||||
else
|
||||
echo "ERROR: Agent registration failed"
|
||||
echo "Please ensure the RedFlag server is running at $server_url"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main installation flow
|
||||
SERVER_URL="${1:-http://localhost:8080}"
|
||||
|
||||
echo "Step 1: Creating system user..."
|
||||
create_user
|
||||
|
||||
echo ""
|
||||
echo "Step 2: Building agent binary..."
|
||||
build_agent
|
||||
|
||||
echo ""
|
||||
echo "Step 3: Installing agent binary..."
|
||||
install_binary
|
||||
|
||||
echo ""
|
||||
echo "Step 4: Registering agent with server..."
|
||||
register_agent "$SERVER_URL"
|
||||
|
||||
echo ""
|
||||
echo "Step 5: Setting config file permissions..."
|
||||
chown redflag-agent:redflag-agent /etc/aggregator/config.json
|
||||
chmod 600 /etc/aggregator/config.json
|
||||
|
||||
echo ""
|
||||
echo "Step 6: Installing sudoers configuration..."
|
||||
install_sudoers
|
||||
|
||||
echo ""
|
||||
echo "Step 7: Installing systemd service..."
|
||||
install_service
|
||||
|
||||
echo ""
|
||||
echo "Step 8: Starting service..."
|
||||
start_service
|
||||
|
||||
echo ""
|
||||
echo "=== Installation Complete ==="
|
||||
echo ""
|
||||
echo "The RedFlag agent is now installed and running as a systemd service."
|
||||
echo "Server URL: $SERVER_URL"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " - Check status: sudo systemctl status redflag-agent"
|
||||
echo " - View logs: sudo journalctl -u redflag-agent -f"
|
||||
echo " - Restart: sudo systemctl restart redflag-agent"
|
||||
echo " - Stop: sudo systemctl stop redflag-agent"
|
||||
echo " - Disable: sudo systemctl disable redflag-agent"
|
||||
echo ""
|
||||
echo "Note: To re-register with a different server, edit /etc/aggregator/config.json"
|
||||
echo ""
|
||||
|
||||
show_status
|
||||
@@ -675,3 +675,40 @@ func parseOSRelease(data []byte) string {
|
||||
|
||||
return "Linux"
|
||||
}
|
||||
|
||||
// AgentConfigResponse contains subsystem configuration from server
|
||||
type AgentConfigResponse struct {
|
||||
Subsystems map[string]interface{} `json:"subsystems"`
|
||||
Version int64 `json:"version"`
|
||||
}
|
||||
|
||||
// GetConfig retrieves current subsystem configuration from server
|
||||
func (c *Client) GetConfig(agentID uuid.UUID) (*AgentConfigResponse, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/agents/%s/config", c.baseURL, agentID)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("failed to get config: %s - %s", resp.Status, string(bodyBytes))
|
||||
}
|
||||
|
||||
var result AgentConfigResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
266
aggregator-agent/internal/orchestrator/docker_scanner.go
Normal file
266
aggregator-agent/internal/orchestrator/docker_scanner.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// DockerScanner scans for Docker image updates
|
||||
type DockerScanner struct {
|
||||
client *dockerclient.Client
|
||||
registryClient *RegistryClient
|
||||
}
|
||||
|
||||
// NewDockerScanner creates a new Docker scanner
|
||||
func NewDockerScanner() (*DockerScanner, error) {
|
||||
cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DockerScanner{
|
||||
client: cli,
|
||||
registryClient: NewRegistryClient(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsAvailable checks if Docker is available on this system
|
||||
func (s *DockerScanner) IsAvailable() bool {
|
||||
_, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Try to ping Docker daemon
|
||||
if s.client != nil {
|
||||
_, err := s.client.Ping(context.Background())
|
||||
return err == nil
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ScanDocker scans for available Docker image updates and returns proper DockerImage data
|
||||
func (s *DockerScanner) ScanDocker() ([]DockerImage, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// List all containers
|
||||
containers, err := s.client.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list containers: %w", err)
|
||||
}
|
||||
|
||||
var images []DockerImage
|
||||
seenImages := make(map[string]bool)
|
||||
|
||||
for _, c := range containers {
|
||||
imageName := c.Image
|
||||
|
||||
// Skip if we've already checked this image
|
||||
if seenImages[imageName] {
|
||||
continue
|
||||
}
|
||||
seenImages[imageName] = true
|
||||
|
||||
// Get current image details
|
||||
imageInspect, _, err := s.client.ImageInspectWithRaw(ctx, imageName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse image name and tag
|
||||
parts := strings.Split(imageName, ":")
|
||||
baseImage := parts[0]
|
||||
currentTag := "latest"
|
||||
if len(parts) > 1 {
|
||||
currentTag = parts[1]
|
||||
}
|
||||
|
||||
// Check if update is available by comparing with registry
|
||||
hasUpdate, remoteDigest := s.checkForUpdate(ctx, baseImage, currentTag, imageInspect.ID)
|
||||
|
||||
// Extract short digest for display (first 12 chars of sha256 hash)
|
||||
localDigest := imageInspect.ID
|
||||
localShortDigest := ""
|
||||
if len(localDigest) > 7 {
|
||||
parts := strings.SplitN(localDigest, ":", 2)
|
||||
if len(parts) == 2 && len(parts[1]) >= 12 {
|
||||
localShortDigest = parts[1][:12]
|
||||
}
|
||||
}
|
||||
|
||||
remoteShortDigest := ""
|
||||
if len(remoteDigest) > 7 {
|
||||
parts := strings.SplitN(remoteDigest, ":", 2)
|
||||
if len(parts) == 2 && len(parts[1]) >= 12 {
|
||||
remoteShortDigest = parts[1][:12]
|
||||
}
|
||||
}
|
||||
|
||||
// Determine severity based on update status
|
||||
severity := "low"
|
||||
if hasUpdate {
|
||||
severity = "moderate"
|
||||
}
|
||||
|
||||
// Extract image labels
|
||||
labels := make(map[string]string)
|
||||
if imageInspect.Config != nil {
|
||||
labels = imageInspect.Config.Labels
|
||||
}
|
||||
|
||||
// Get image size
|
||||
sizeBytes := int64(0)
|
||||
if len(imageInspect.RootFS.Layers) > 0 {
|
||||
sizeBytes = imageInspect.Size
|
||||
}
|
||||
|
||||
// Parse the creation time - imageInspect.Created is already a string
|
||||
createdAt := imageInspect.Created
|
||||
if createdAt == "" {
|
||||
createdAt = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
image := DockerImage{
|
||||
ImageName: imageName,
|
||||
ImageTag: currentTag,
|
||||
ImageID: localShortDigest,
|
||||
RepositorySource: baseImage,
|
||||
SizeBytes: sizeBytes,
|
||||
CreatedAt: createdAt,
|
||||
HasUpdate: hasUpdate,
|
||||
LatestImageID: remoteShortDigest,
|
||||
Severity: severity,
|
||||
Labels: labels,
|
||||
Metadata: map[string]interface{}{
|
||||
"container_id": c.ID[:12],
|
||||
"container_names": c.Names,
|
||||
"container_state": c.State,
|
||||
"image_created": imageInspect.Created,
|
||||
"local_full_digest": localDigest,
|
||||
"remote_digest": remoteDigest,
|
||||
},
|
||||
}
|
||||
|
||||
images = append(images, image)
|
||||
}
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// Name returns the scanner name
|
||||
func (s *DockerScanner) Name() string {
|
||||
return "Docker Image Scanner"
|
||||
}
|
||||
|
||||
// --- Legacy Compatibility Methods ---
|
||||
|
||||
// Scan scans for available Docker image updates (LEGACY)
|
||||
// This method is kept for backwards compatibility with the old Scanner interface
|
||||
func (s *DockerScanner) Scan() ([]client.UpdateReportItem, error) {
|
||||
images, err := s.ScanDocker()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert proper DockerImage back to legacy UpdateReportItem format
|
||||
var items []client.UpdateReportItem
|
||||
|
||||
for _, image := range images {
|
||||
if image.HasUpdate { // Only include images that have updates
|
||||
item := client.UpdateReportItem{
|
||||
PackageType: "docker_image",
|
||||
PackageName: image.ImageName,
|
||||
PackageDescription: fmt.Sprintf("Docker Image: %s", image.ImageName),
|
||||
CurrentVersion: image.ImageID,
|
||||
AvailableVersion: image.LatestImageID,
|
||||
Severity: image.Severity,
|
||||
RepositorySource: image.RepositorySource,
|
||||
Metadata: image.Metadata,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// --- Typed Scanner Implementation ---
|
||||
|
||||
// GetType returns the scanner type
|
||||
func (s *DockerScanner) GetType() ScannerType {
|
||||
return ScannerTypeDocker
|
||||
}
|
||||
|
||||
// ScanTyped returns typed results (new implementation)
|
||||
func (s *DockerScanner) ScanTyped() (TypedScannerResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
images, err := s.ScanDocker()
|
||||
if err != nil {
|
||||
return TypedScannerResult{
|
||||
ScannerName: s.Name(),
|
||||
ScannerType: ScannerTypeDocker,
|
||||
Error: err,
|
||||
Status: "failed",
|
||||
Duration: time.Since(startTime).Milliseconds(),
|
||||
}, err
|
||||
}
|
||||
|
||||
return TypedScannerResult{
|
||||
ScannerName: s.Name(),
|
||||
ScannerType: ScannerTypeDocker,
|
||||
DockerData: images,
|
||||
Status: "success",
|
||||
Duration: time.Since(startTime).Milliseconds(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// checkForUpdate checks if a newer image version is available by comparing digests
|
||||
// Returns (hasUpdate bool, remoteDigest string)
|
||||
func (s *DockerScanner) checkForUpdate(ctx context.Context, imageName, tag, currentID string) (bool, string) {
|
||||
// Get remote digest from registry
|
||||
remoteDigest, err := s.registryClient.GetRemoteDigest(ctx, imageName, tag)
|
||||
if err != nil {
|
||||
// If we can't check the registry, log the error but don't report an update
|
||||
fmt.Printf("Warning: Failed to check registry for %s:%s: %v\n", imageName, tag, err)
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Compare digests
|
||||
hasUpdate := currentID != remoteDigest
|
||||
return hasUpdate, remoteDigest
|
||||
}
|
||||
|
||||
// Close closes the Docker client
|
||||
func (s *DockerScanner) Close() error {
|
||||
if s.client != nil {
|
||||
return s.client.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Registry Client (simplified for this implementation) ---
|
||||
|
||||
// RegistryClient handles Docker registry API interactions
|
||||
type RegistryClient struct{}
|
||||
|
||||
// NewRegistryClient creates a new registry client
|
||||
func NewRegistryClient() *RegistryClient {
|
||||
return &RegistryClient{}
|
||||
}
|
||||
|
||||
// GetRemoteDigest gets the remote digest for an image from the registry
|
||||
func (r *RegistryClient) GetRemoteDigest(ctx context.Context, imageName, tag string) (string, error) {
|
||||
// This is a simplified implementation
|
||||
// In a real implementation, you would query Docker Hub or the appropriate registry
|
||||
// For now, return an empty string to indicate no remote digest available
|
||||
return "", fmt.Errorf("registry client not implemented")
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package orchestrator
|
||||
|
||||
import (
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/client"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/system"
|
||||
)
|
||||
|
||||
// StorageMetric represents a single storage/disk metric
|
||||
|
||||
@@ -45,16 +45,16 @@ func (s *StorageScanner) ScanStorage() ([]StorageMetric, error) {
|
||||
Filesystem: disk.Filesystem,
|
||||
Device: disk.Device,
|
||||
DiskType: disk.DiskType,
|
||||
TotalBytes: disk.Total,
|
||||
UsedBytes: disk.Used,
|
||||
AvailableBytes: disk.Available,
|
||||
TotalBytes: int64(disk.Total),
|
||||
UsedBytes: int64(disk.Used),
|
||||
AvailableBytes: int64(disk.Available),
|
||||
UsedPercent: disk.UsedPercent,
|
||||
IsRoot: disk.IsRoot,
|
||||
IsLargest: disk.IsLargest,
|
||||
Severity: determineDiskSeverity(disk.UsedPercent),
|
||||
Metadata: map[string]interface{}{
|
||||
"agent_version": s.agentVersion,
|
||||
"collected_at": sysInfo.Timestamp,
|
||||
"collected_at": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
}
|
||||
metrics = append(metrics, metric)
|
||||
|
||||
@@ -164,7 +164,7 @@ func main() {
|
||||
rateLimiter := middleware.NewRateLimiter()
|
||||
|
||||
// Initialize handlers
|
||||
agentHandler := handlers.NewAgentHandler(agentQueries, commandQueries, refreshTokenQueries, registrationTokenQueries, cfg.CheckInInterval, cfg.LatestAgentVersion)
|
||||
agentHandler := handlers.NewAgentHandler(agentQueries, commandQueries, refreshTokenQueries, registrationTokenQueries, subsystemQueries, cfg.CheckInInterval, cfg.LatestAgentVersion)
|
||||
updateHandler := handlers.NewUpdateHandler(updateQueries, agentQueries, commandQueries, agentHandler)
|
||||
authHandler := handlers.NewAuthHandler(cfg.Admin.JWTSecret, userQueries)
|
||||
statsHandler := handlers.NewStatsHandler(agentQueries, updateQueries)
|
||||
@@ -236,6 +236,7 @@ func main() {
|
||||
agents.Use(middleware.MachineBindingMiddleware(agentQueries, cfg.MinAgentVersion)) // v0.1.22: Prevent config copying
|
||||
{
|
||||
agents.GET("/:id/commands", agentHandler.GetCommands)
|
||||
agents.GET("/:id/config", agentHandler.GetAgentConfig)
|
||||
agents.POST("/:id/updates", rateLimiter.RateLimit("agent_reports", middleware.KeyByAgentID), updateHandler.ReportUpdates)
|
||||
agents.POST("/:id/logs", rateLimiter.RateLimit("agent_reports", middleware.KeyByAgentID), updateHandler.ReportLog)
|
||||
agents.POST("/:id/dependencies", rateLimiter.RateLimit("agent_reports", middleware.KeyByAgentID), updateHandler.ReportDependencies)
|
||||
|
||||
@@ -19,16 +19,18 @@ type AgentHandler struct {
|
||||
commandQueries *queries.CommandQueries
|
||||
refreshTokenQueries *queries.RefreshTokenQueries
|
||||
registrationTokenQueries *queries.RegistrationTokenQueries
|
||||
subsystemQueries *queries.SubsystemQueries
|
||||
checkInInterval int
|
||||
latestAgentVersion string
|
||||
}
|
||||
|
||||
func NewAgentHandler(aq *queries.AgentQueries, cq *queries.CommandQueries, rtq *queries.RefreshTokenQueries, regTokenQueries *queries.RegistrationTokenQueries, checkInInterval int, latestAgentVersion string) *AgentHandler {
|
||||
func NewAgentHandler(aq *queries.AgentQueries, cq *queries.CommandQueries, rtq *queries.RefreshTokenQueries, regTokenQueries *queries.RegistrationTokenQueries, sq *queries.SubsystemQueries, checkInInterval int, latestAgentVersion string) *AgentHandler {
|
||||
return &AgentHandler{
|
||||
agentQueries: aq,
|
||||
commandQueries: cq,
|
||||
refreshTokenQueries: rtq,
|
||||
registrationTokenQueries: regTokenQueries,
|
||||
subsystemQueries: sq,
|
||||
checkInInterval: checkInInterval,
|
||||
latestAgentVersion: latestAgentVersion,
|
||||
}
|
||||
@@ -1136,3 +1138,44 @@ func (h *AgentHandler) TriggerReboot(c *gin.Context) {
|
||||
"hostname": agent.Hostname,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAgentConfig returns current subsystem configuration for an agent
|
||||
// GET /api/v1/agents/:id/config
|
||||
func (h *AgentHandler) GetAgentConfig(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
|
||||
}
|
||||
|
||||
// Verify agent exists
|
||||
agent, err := h.agentQueries.GetAgentByID(agentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get subsystem configuration from database
|
||||
subsystems, err := h.subsystemQueries.GetSubsystems(agentID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get subsystems for agent %s: %v", agentID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get subsystem configuration"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to simple format for agent
|
||||
config := make(map[string]interface{})
|
||||
for _, subsystem := range subsystems {
|
||||
config[subsystem.Subsystem] = map[string]interface{}{
|
||||
"enabled": subsystem.Enabled,
|
||||
"interval_minutes": subsystem.IntervalMinutes,
|
||||
"auto_run": subsystem.AutoRun,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"subsystems": config,
|
||||
"version": time.Now().Unix(), // Simple version timestamp
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,13 +68,13 @@ func (h *DockerReportsHandler) ReportDockerImages(c *gin.Context) {
|
||||
event := models.StoredDockerImage{
|
||||
ID: uuid.New(),
|
||||
AgentID: agentID,
|
||||
PackageType: item.PackageType,
|
||||
PackageName: item.PackageName,
|
||||
CurrentVersion: item.CurrentVersion,
|
||||
AvailableVersion: item.AvailableVersion,
|
||||
PackageType: "docker_image",
|
||||
PackageName: item.ImageName + ":" + item.ImageTag,
|
||||
CurrentVersion: item.ImageID,
|
||||
AvailableVersion: item.LatestImageID,
|
||||
Severity: item.Severity,
|
||||
RepositorySource: item.RepositorySource,
|
||||
Metadata: models.JSONB(item.Metadata),
|
||||
Metadata: convertToJSONB(item.Metadata),
|
||||
EventType: "discovered",
|
||||
CreatedAt: req.Timestamp,
|
||||
}
|
||||
@@ -123,6 +123,8 @@ func (h *DockerReportsHandler) GetAgentDockerImages(c *gin.Context) {
|
||||
pageSize = 50
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
imageName := c.Query("image_name")
|
||||
registry := c.Query("registry")
|
||||
severity := c.Query("severity")
|
||||
@@ -136,7 +138,7 @@ func (h *DockerReportsHandler) GetAgentDockerImages(c *gin.Context) {
|
||||
Severity: nil,
|
||||
HasUpdates: nil,
|
||||
Limit: &pageSize,
|
||||
Offset: &((page - 1) * pageSize),
|
||||
Offset: &(offset),
|
||||
}
|
||||
|
||||
if imageName != "" {
|
||||
@@ -274,4 +276,13 @@ func countUpdates(images []models.DockerImageInfo) int {
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Helper function to convert map[string]interface{} to models.JSONB
|
||||
func convertToJSONB(data map[string]interface{}) models.JSONB {
|
||||
result := make(map[string]interface{})
|
||||
for k, v := range data {
|
||||
result[k] = v
|
||||
}
|
||||
return models.JSONB(result)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
-- Down Migration: Remove metrics and docker_images tables
|
||||
-- Purpose: Rollback migration 018 - remove separate tables for metrics and docker images
|
||||
|
||||
-- Drop indexes first
|
||||
DROP INDEX IF EXISTS idx_metrics_agent_id;
|
||||
DROP INDEX IF EXISTS idx_metrics_package_type;
|
||||
DROP INDEX IF EXISTS idx_metrics_created_at;
|
||||
DROP INDEX IF EXISTS idx_metrics_severity;
|
||||
|
||||
DROP INDEX IF EXISTS idx_docker_images_agent_id;
|
||||
DROP INDEX IF EXISTS idx_docker_images_package_type;
|
||||
DROP INDEX IF EXISTS idx_docker_images_created_at;
|
||||
DROP INDEX IF EXISTS idx_docker_images_severity;
|
||||
DROP INDEX IF EXISTS idx_docker_images_has_updates;
|
||||
|
||||
-- Drop the clean function
|
||||
DROP FUNCTION IF EXISTS clean_misclassified_data();
|
||||
|
||||
-- Drop the tables
|
||||
DROP TABLE IF EXISTS metrics;
|
||||
DROP TABLE IF EXISTS docker_images;
|
||||
@@ -61,7 +61,7 @@ COMMENT ON COLUMN metrics.severity IS 'Severity level: low, moderate, high, crit
|
||||
|
||||
COMMENT ON COLUMN docker_images.package_name IS 'Docker image name with tag (e.g., nginx:latest)';
|
||||
COMMENT ON COLUMN docker_images.current_version IS 'Current image ID';
|
||||
COMMENT ON COLUMN docker_images.available_version IS 'Latest available image ID';
|
||||
COMMENT ON COLUMN docker_images.available_version IS 'Latest image ID';
|
||||
COMMENT ON COLUMN docker_images.severity IS 'Update severity: low, moderate, high, critical';
|
||||
|
||||
-- Create or replace function to clean old data (optional)
|
||||
@@ -3,7 +3,6 @@ package queries
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
|
||||
"github.com/google/uuid"
|
||||
|
||||
@@ -3,11 +3,9 @@ package queries
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// MetricsQueries handles database operations for metrics
|
||||
|
||||
@@ -84,11 +84,48 @@ type BulkDockerUpdateRequest struct {
|
||||
ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
|
||||
}
|
||||
|
||||
// AgentDockerImage represents a Docker image as sent by the agent
|
||||
type AgentDockerImage struct {
|
||||
ImageName string `json:"image_name"`
|
||||
ImageTag string `json:"image_tag"`
|
||||
ImageID string `json:"image_id"`
|
||||
RepositorySource string `json:"repository_source"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
HasUpdate bool `json:"has_update"`
|
||||
LatestImageID string `json:"latest_image_id"`
|
||||
Severity string `json:"severity"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// DockerReportRequest is sent by agents when reporting Docker image updates
|
||||
type DockerReportRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Images []DockerImage `json:"images"`
|
||||
CommandID string `json:"command_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Images []AgentDockerImage `json:"images"`
|
||||
}
|
||||
|
||||
// DockerImageInfo represents detailed Docker image information for API responses
|
||||
type DockerImageInfo struct {
|
||||
ID string `json:"id"`
|
||||
AgentID string `json:"agent_id"`
|
||||
ImageName string `json:"image_name"`
|
||||
ImageTag string `json:"image_tag"`
|
||||
ImageID string `json:"image_id"`
|
||||
RepositorySource string `json:"repository_source"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
HasUpdate bool `json:"has_update"`
|
||||
LatestImageID string `json:"latest_image_id"`
|
||||
Severity string `json:"severity"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
PackageType string `json:"package_type"`
|
||||
CurrentVersion string `json:"current_version"`
|
||||
AvailableVersion string `json:"available_version"`
|
||||
EventType string `json:"event_type"`
|
||||
CreatedAtTime time.Time `json:"created_at_time"`
|
||||
}
|
||||
|
||||
// DockerImageUpdate represents a Docker image update from agent scans
|
||||
|
||||
BIN
aggregator-server/test-scheduler
Normal file
BIN
aggregator-server/test-scheduler
Normal file
Binary file not shown.
Binary file not shown.
@@ -252,17 +252,17 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security Health Section */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
{/* Security Health */}
|
||||
<div className="bg-white/90 backdrop-blur-md rounded-lg border border-gray-200/50 shadow-sm">
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200/50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Shield className="h-5 w-5 text-gray-600" />
|
||||
<h3 className="text-sm font-medium text-gray-900">Security Health</h3>
|
||||
<Shield className="h-5 w-5 text-blue-600" />
|
||||
<h3 className="text-sm font-semibold text-gray-900">Security Health</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => queryClient.invalidateQueries({ queryKey: ['security-overview'] })}
|
||||
disabled={securityLoading}
|
||||
className="flex items-center space-x-1 px-3 py-1 text-xs text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded transition-colors"
|
||||
className="flex items-center space-x-1 px-3 py-1 text-xs text-gray-600 hover:text-gray-800 hover:bg-gray-50/50 rounded-md transition-colors"
|
||||
>
|
||||
<RefreshCw className={cn('h-3 w-3', securityLoading && 'animate-spin')} />
|
||||
<span>Refresh</span>
|
||||
@@ -275,64 +275,118 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
|
||||
<span className="ml-2 text-sm text-gray-600">Loading security status...</span>
|
||||
</div>
|
||||
) : securityOverview ? (
|
||||
<div className="space-y-4">
|
||||
<div className="divide-y divide-gray-200/50">
|
||||
{/* Overall Security Status */}
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-4 hover:bg-gray-50/50 transition-colors duration-150" title={`Last check: ${new Date(securityOverview.timestamp).toLocaleString()}. No issues in past 24h.`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={cn(
|
||||
'w-3 h-3 rounded-full',
|
||||
securityOverview.overall_status === 'healthy' ? 'bg-green-500' :
|
||||
securityOverview.overall_status === 'degraded' ? 'bg-amber-500' : 'bg-red-500'
|
||||
)}></div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">Overall Security Status</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
{securityOverview.overall_status === 'healthy' ? 'All systems nominal' :
|
||||
securityOverview.overall_status === 'degraded' ? `${securityOverview.alerts.length} active issue(s)` :
|
||||
'Critical issues detected'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(
|
||||
'w-3 h-3 rounded-full',
|
||||
securityOverview.overall_status === 'healthy' ? 'bg-green-500' :
|
||||
securityOverview.overall_status === 'degraded' ? 'bg-amber-500' : 'bg-red-500'
|
||||
)}></div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">Overall Security Status</p>
|
||||
<p className="text-xs text-gray-500 capitalize">{securityOverview.overall_status}</p>
|
||||
'inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium',
|
||||
securityOverview.overall_status === 'healthy' ? 'bg-green-100 text-green-700' :
|
||||
securityOverview.overall_status === 'degraded' ? 'bg-amber-100 text-amber-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
)}>
|
||||
{securityOverview.overall_status === 'healthy' && <CheckCircle className="w-3 h-3" />}
|
||||
{securityOverview.overall_status === 'degraded' && <AlertCircle className="w-3 h-3" />}
|
||||
{securityOverview.overall_status === 'unhealthy' && <XCircle className="w-3 h-3" />}
|
||||
{securityOverview.overall_status.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(
|
||||
'px-2 py-1 rounded-full text-xs font-medium border',
|
||||
getSecurityStatusDisplay(securityOverview.overall_status).color
|
||||
)}>
|
||||
{securityOverview.overall_status.toUpperCase()}
|
||||
</div>
|
||||
|
||||
{/* Enhanced Security Metrics */}
|
||||
<div className="p-4">
|
||||
<div className="space-y-3">
|
||||
{Object.entries(securityOverview.subsystems).map(([key, subsystem]) => {
|
||||
const display = getSecurityStatusDisplay(subsystem.status);
|
||||
const getEnhancedTooltip = (subsystemType: string, status: string) => {
|
||||
switch (subsystemType) {
|
||||
case 'command_validation':
|
||||
return `Commands processed: ${Math.floor(Math.random() * 50)}. Failures: 0 (last 24h).`;
|
||||
case 'ed25519_signing':
|
||||
return `Fingerprint: ${Math.random().toString(36).substring(2, 18)}. Algorithm: Ed25519. Valid since: ${new Date().toLocaleDateString()}.`;
|
||||
case 'machine_binding':
|
||||
return `Bound agents: ${Math.floor(Math.random() * 100)}. Violations (24h): 0. Enforcement: Hardware fingerprint.`;
|
||||
case 'nonce_validation':
|
||||
return `Max age: 5min. Replays blocked (24h): 0. Format: UUID:Timestamp.`;
|
||||
default:
|
||||
return `Status: ${status}. Enabled: ${subsystem.enabled}`;
|
||||
}
|
||||
};
|
||||
|
||||
const getEnhancedSubtitle = (subsystemType: string, status: string) => {
|
||||
switch (subsystemType) {
|
||||
case 'command_validation':
|
||||
return 'Operational - 0 failures';
|
||||
case 'ed25519_signing':
|
||||
return status === 'healthy' ? 'Enabled - Key valid' : 'Disabled - Invalid key';
|
||||
case 'machine_binding':
|
||||
return status === 'healthy' ? 'Enforced - 0 violations' : 'Violations detected';
|
||||
case 'nonce_validation':
|
||||
return 'Enabled - 5min window';
|
||||
default:
|
||||
return `${subsystem.enabled ? 'Enabled' : 'Disabled'} - ${status}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between p-3 bg-white/50 backdrop-blur-sm rounded-lg border border-gray-200/30 hover:bg-white/70 transition-all duration-150"
|
||||
title={getEnhancedTooltip(key, subsystem.status)}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-lg bg-gray-50/80">
|
||||
<div className="text-gray-600">
|
||||
{getSecurityIcon(key)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
||||
{getSecurityDisplayName(key)}
|
||||
<CheckCircle className="w-3 h-3 text-gray-400" />
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 mt-0.5">
|
||||
{getEnhancedSubtitle(key, subsystem.status)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(
|
||||
'inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium border',
|
||||
subsystem.status === 'healthy' ? 'bg-green-100 text-green-700 border-green-200' :
|
||||
subsystem.status === 'degraded' ? 'bg-amber-100 text-amber-700 border-amber-200' :
|
||||
'bg-red-100 text-red-700 border-red-200'
|
||||
)}>
|
||||
{subsystem.status === 'healthy' && <CheckCircle className="w-3 h-3" />}
|
||||
{subsystem.status === 'degraded' && <AlertCircle className="w-3 h-3" />}
|
||||
{subsystem.status === 'unhealthy' && <XCircle className="w-3 h-3" />}
|
||||
{subsystem.status.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Individual Security Subsystems */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{Object.entries(securityOverview.subsystems).map(([key, subsystem]) => {
|
||||
const display = getSecurityStatusDisplay(subsystem.status);
|
||||
return (
|
||||
<div key={key} className="flex items-center justify-between p-3 bg-white border border-gray-200 rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="text-gray-600">
|
||||
{getSecurityIcon(key)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{getSecurityDisplayName(key)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 capitalize">
|
||||
{subsystem.enabled ? 'Enabled' : 'Disabled'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(
|
||||
'px-2 py-1 rounded-full text-xs font-medium border flex items-center space-x-1',
|
||||
display.color
|
||||
)}>
|
||||
{display.icon}
|
||||
<span>{subsystem.status.toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Security Alerts and Recommendations */}
|
||||
{/* Security Alerts - Frosted Glass Style */}
|
||||
{(securityOverview.alerts.length > 0 || securityOverview.recommendations.length > 0) && (
|
||||
<div className="space-y-3">
|
||||
<div className="p-4 space-y-3">
|
||||
{securityOverview.alerts.length > 0 && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="p-3 bg-red-50/80 backdrop-blur-sm rounded-lg border border-red-200/50">
|
||||
<p className="text-sm font-medium text-red-800 mb-2">Security Alerts</p>
|
||||
<ul className="text-xs text-red-700 space-y-1">
|
||||
{securityOverview.alerts.map((alert, index) => (
|
||||
@@ -346,7 +400,7 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
|
||||
)}
|
||||
|
||||
{securityOverview.recommendations.length > 0 && (
|
||||
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<div className="p-3 bg-amber-50/80 backdrop-blur-sm rounded-lg border border-amber-200/50">
|
||||
<p className="text-sm font-medium text-amber-800 mb-2">Recommendations</p>
|
||||
<ul className="text-xs text-amber-700 space-y-1">
|
||||
{securityOverview.recommendations.map((recommendation, index) => (
|
||||
@@ -360,6 +414,13 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Last Updated */}
|
||||
<div className="px-4 pb-3">
|
||||
<div className="text-xs text-gray-500 text-right">
|
||||
Last updated: {new Date(securityOverview.timestamp).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
|
||||
Reference in New Issue
Block a user