diff --git a/CLAUDE.md b/CLAUDE.md index cb332f7..4a295e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,16 @@ Container Census is a multi-host Docker monitoring system written in Go. It cons 2. **Agent** (`cmd/agent`): Lightweight agent for remote Docker hosts 3. **Telemetry Collector** (`cmd/telemetry-collector`): Analytics aggregation service with PostgreSQL backend +## Build Instructions + +**IMPORTANT**: When building binaries during development, ALWAYS build to `/tmp/container-census`: + +```bash +CGO_ENABLED=1 go build -o /tmp/container-census ./cmd/server +``` + +This ensures a consistent location for testing and prevents confusion with multiple build locations. + ## Build and Development Commands ### Prerequisites diff --git a/internal/api/vulnerabilities.go b/internal/api/vulnerabilities.go index 43955ab..6738ddc 100644 --- a/internal/api/vulnerabilities.go +++ b/internal/api/vulnerabilities.go @@ -15,7 +15,7 @@ import ( // VulnerabilityScanner interface for the vulnerability scanner type VulnerabilityScanner interface { GetCachedScan(imageID string) (*vulnerability.VulnerabilityScan, error) - ScanImage(ctx context.Context, imageRef string) (*vulnerability.VulnerabilityScanResult, error) + ScanImage(ctx context.Context, imageID string, imageName string) (*vulnerability.VulnerabilityScanResult, error) UpdateTrivyDB(ctx context.Context) error GetConfig() *vulnerability.Config SetConfig(config *vulnerability.Config) diff --git a/internal/vulnerability/scanner.go b/internal/vulnerability/scanner.go index a570123..fc1295d 100644 --- a/internal/vulnerability/scanner.go +++ b/internal/vulnerability/scanner.go @@ -31,26 +31,27 @@ func NewScanner(config *Config, storage VulnerabilityStorage) *Scanner { } // ScanImage scans an image for vulnerabilities using Trivy -func (s *Scanner) ScanImage(ctx context.Context, imageRef string) (*VulnerabilityScanResult, error) { +// imageID should be the SHA256 image ID, imageName should be the image reference +func (s *Scanner) ScanImage(ctx context.Context, imageID string, imageName string) (*VulnerabilityScanResult, error) { if !s.config.GetEnabled() { return nil, fmt.Errorf("vulnerability scanning is disabled") } startTime := time.Now() - log.Printf("Starting vulnerability scan for image: %s", imageRef) + log.Printf("Starting vulnerability scan for image: %s (ID: %s)", imageName, imageID) // Create context with timeout scanCtx, cancel := context.WithTimeout(ctx, s.config.GetScanTimeout()) defer cancel() - // Run Trivy scan - trivyResult, err := s.runTrivy(scanCtx, imageRef) + // Run Trivy scan using the image name + trivyResult, err := s.runTrivy(scanCtx, imageName) if err != nil { scanDuration := time.Since(startTime).Milliseconds() - // Save failed scan + // Save failed scan with the actual image ID failedScan := &VulnerabilityScan{ - ImageID: extractImageID(imageRef), - ImageName: imageRef, + ImageID: imageID, + ImageName: imageName, ScannedAt: time.Now(), ScanDurationMs: scanDuration, Success: false, @@ -61,14 +62,14 @@ func (s *Scanner) ScanImage(ctx context.Context, imageRef string) (*Vulnerabilit } // Parse results - vulnerabilities := s.parseTrivyResult(trivyResult, imageRef) + vulnerabilities := s.parseTrivyResult(trivyResult, imageID) severityCounts := CalculateSeverityCounts(vulnerabilities) scanDuration := time.Since(startTime).Milliseconds() - // Create scan record + // Create scan record with the actual image ID scan := &VulnerabilityScan{ - ImageID: extractImageID(imageRef), - ImageName: imageRef, + ImageID: imageID, + ImageName: imageName, ScannedAt: time.Now(), ScanDurationMs: scanDuration, Success: true, @@ -84,7 +85,7 @@ func (s *Scanner) ScanImage(ctx context.Context, imageRef string) (*Vulnerabilit } log.Printf("Vulnerability scan completed for %s: %d vulnerabilities found (%d critical, %d high) in %dms", - imageRef, severityCounts.GetTotal(), severityCounts.Critical, severityCounts.High, scanDuration) + imageName, severityCounts.GetTotal(), severityCounts.Critical, severityCounts.High, scanDuration) return &VulnerabilityScanResult{ Scan: *scan, @@ -149,9 +150,8 @@ func (s *Scanner) runTrivy(ctx context.Context, imageRef string) (*TrivyResult, } // parseTrivyResult converts Trivy output to our vulnerability format -func (s *Scanner) parseTrivyResult(trivyResult *TrivyResult, imageRef string) []Vulnerability { +func (s *Scanner) parseTrivyResult(trivyResult *TrivyResult, imageID string) []Vulnerability { vulnerabilities := make([]Vulnerability, 0) - imageID := extractImageID(imageRef) for _, result := range trivyResult.Results { if result.Vulnerabilities == nil { diff --git a/internal/vulnerability/scheduler.go b/internal/vulnerability/scheduler.go index 1d85111..16bc807 100644 --- a/internal/vulnerability/scheduler.go +++ b/internal/vulnerability/scheduler.go @@ -200,8 +200,8 @@ func (w *worker) processJob(job ScanJob) { atomic.AddInt32(&w.scheduler.inProgressCount, 1) defer atomic.AddInt32(&w.scheduler.inProgressCount, -1) - // Perform the scan - _, err := w.scheduler.scanner.ScanImage(w.scheduler.ctx, job.ImageName) + // Perform the scan with both image ID and name + _, err := w.scheduler.scanner.ScanImage(w.scheduler.ctx, job.ImageID, job.ImageName) if err != nil { // Only log unexpected errors (not "image not available") if err.Error() != "image not available for scanning" { diff --git a/scripts/run.local.sh b/scripts/run.local.sh index c37bc28..b7e2afa 100755 --- a/scripts/run.local.sh +++ b/scripts/run.local.sh @@ -1,2 +1,2 @@ #!/bin/bash -SERVER_PORT=3000 CONFIG_PATH=/opt/docker-compose/census-server/census/config/config.yaml AUTH_ENABLED=false DATABASE_PATH=/opt/docker-compose/census-server/census/server/census.db /tmp/census-1.3.25 \ No newline at end of file +SERVER_PORT=3000 CONFIG_PATH=/opt/docker-compose/census-server/census/config/config.yaml AUTH_ENABLED=false DATABASE_PATH=/opt/docker-compose/census-server/census/server/census.db /tmp/container-census \ No newline at end of file diff --git a/scripts/server-build.sh b/scripts/server-build.sh index 4ab04ea..801ab30 100755 --- a/scripts/server-build.sh +++ b/scripts/server-build.sh @@ -1,2 +1,4 @@ #!/bin/bash -CGO_ENABLED=1 go build -o container-census cmd/server/main.go \ No newline at end of file +# CGO_ENABLED=1 go build -o container-census cmd/server/main.go + +CGO_ENABLED=1 go build -o /tmp/container-census ./cmd/server && ls -lh /tmp/container-census diff --git a/web/app.js b/web/app.js index 9ccce90..108f7d2 100644 --- a/web/app.js +++ b/web/app.js @@ -16,6 +16,10 @@ let vulnerabilityCache = {}; // Cache vulnerability data by imageID let vulnerabilityScansMap = {}; // Pre-loaded map of all scans to avoid 404s let vulnerabilitySummary = null; // Cache overall summary +// Auth credentials (empty if auth is disabled) +let authUsername = ''; +let authPassword = ''; + // Initialize document.addEventListener('DOMContentLoaded', () => { setupEventListeners(); @@ -416,6 +420,7 @@ function setupEventListeners() { document.getElementById('vulnerabilitySettingsBtn')?.addEventListener('click', openVulnerabilitySettingsModal); document.getElementById('securitySearchInput')?.addEventListener('input', filterSecurityScans); document.getElementById('securitySeverityFilter')?.addEventListener('change', filterSecurityScans); + document.getElementById('securityStatusFilter')?.addEventListener('change', filterSecurityScans); // Vulnerability settings modal const vulnSettingsForm = document.getElementById('vulnerabilitySettingsForm'); @@ -4639,7 +4644,11 @@ async function getVulnerabilityScan(imageID) { // Pre-load all vulnerability scans to avoid 404 requests async function preloadVulnerabilityScans() { try { - const response = await fetch('/api/vulnerabilities/scans?limit=1000'); + const response = await fetch('/api/vulnerabilities/scans?limit=1000', { + headers: { + 'Authorization': 'Basic ' + btoa(authUsername + ':' + authPassword) + } + }); if (response.ok) { const scans = await response.json(); // Build a map of imageID -> scan data @@ -4733,7 +4742,7 @@ function getVulnerabilityBadgeHTML(scan) { // Add vulnerability badge to container card (called asynchronously) async function addVulnerabilityBadge(containerElement, imageID) { const scan = await getVulnerabilityScan(imageID); - const badgeHTML = getVulnerabilityBadgeHTML(scan); + const badgeHTML = getVulnerabilityBadgeHTML(scan, imageID); // Find the image row in the container card const imageRow = containerElement.querySelector('.detail-value.image-value'); @@ -4741,7 +4750,16 @@ async function addVulnerabilityBadge(containerElement, imageID) { // Add badge after the image name const badgeContainer = document.createElement('span'); badgeContainer.innerHTML = badgeHTML; - imageRow.parentElement.appendChild(badgeContainer.firstChild); + const badge = badgeContainer.firstChild; + + // Make badge clickable if it has vulnerabilities + if (scan && scan.scan && scan.scan.success) { + const imageName = imageRow.textContent.trim(); + badge.style.cursor = 'pointer'; + badge.onclick = () => viewVulnerabilityDetails(imageID, imageName); + } + + imageRow.parentElement.appendChild(badge); } } @@ -4795,13 +4813,13 @@ async function loadSecurityTab() { allVulnerabilityScans = scans || []; // Update summary cards - updateSecuritySummaryCards(summary); + updateSecuritySummaryCards(summary, allVulnerabilityScans); // Render security chart renderSecurityChart(summary); // Render vulnerability trends chart - renderVulnerabilityTrendsChart(); + renderVulnerabilityTrendsChart(allVulnerabilityScans); // Update queue status updateQueueStatus(summary?.queue_status); @@ -4864,7 +4882,7 @@ async function loadAllVulnerabilityScans() { } // Update security summary cards -function updateSecuritySummaryCards(summary) { +function updateSecuritySummaryCards(summary, scans) { if (!summary) { document.getElementById('totalScannedImages').textContent = '-'; document.getElementById('totalCriticalVulns').textContent = '-'; @@ -4875,7 +4893,12 @@ function updateSecuritySummaryCards(summary) { // Handle both wrapped (summary.summary) and direct summary objects const s = summary.summary || summary; - document.getElementById('totalScannedImages').textContent = s.total_images_scanned || 0; + const totalScans = scans ? scans.length : 0; + const uniqueImages = s.total_images_scanned || 0; + + // Show both unique images and total scans for clarity + const displayText = totalScans > 0 ? `${uniqueImages} (${totalScans} scans)` : `${uniqueImages}`; + document.getElementById('totalScannedImages').textContent = displayText; document.getElementById('totalCriticalVulns').textContent = s.severity_counts?.critical || 0; document.getElementById('totalHighVulns').textContent = s.severity_counts?.high || 0; document.getElementById('atRiskImages').textContent = s.images_with_vulnerabilities || 0; @@ -4941,22 +4964,17 @@ function renderSecurityChart(summary) { let trendsChart = null; // Render vulnerability trends chart -async function renderVulnerabilityTrendsChart() { +function renderVulnerabilityTrendsChart(scans) { const ctx = document.getElementById('vulnerabilityTrendsChart'); if (!ctx) return; try { - // Fetch all scans - const response = await fetch('/api/vulnerabilities/scans?limit=1000', { - headers: { 'Authorization': 'Basic ' + btoa(authUsername + ':' + authPassword) } - }); - - if (!response.ok) { - throw new Error('Failed to fetch scans'); + // Use provided scans data + if (!scans || scans.length === 0) { + console.log('No scan data available for trends chart'); + return; } - const scans = await response.json(); - // Group scans by date (last 30 days) and calculate aggregates const now = new Date(); const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); @@ -5168,6 +5186,7 @@ function updateQueueStatus(queueStatus) { function filterSecurityScans() { const searchTerm = document.getElementById('securitySearchInput')?.value.toLowerCase() || ''; const severityFilter = document.getElementById('securitySeverityFilter')?.value || ''; + const statusFilter = document.getElementById('securityStatusFilter')?.value || ''; const filtered = allVulnerabilityScans.filter(scan => { const matchesSearch = searchTerm === '' || @@ -5177,7 +5196,7 @@ function filterSecurityScans() { let matchesSeverity = true; if (severityFilter) { if (severityFilter === 'clean') { - matchesSeverity = scan.total_vulnerabilities === 0; + matchesSeverity = scan.total_vulnerabilities === 0 && scan.success; } else if (severityFilter === 'critical') { matchesSeverity = (scan.severity_counts?.critical || 0) > 0; } else if (severityFilter === 'high') { @@ -5189,7 +5208,19 @@ function filterSecurityScans() { } } - return matchesSearch && matchesSeverity; + let matchesStatus = true; + if (statusFilter) { + const error = scan.error || ''; + if (statusFilter === 'scanned') { + matchesStatus = scan.success; + } else if (statusFilter === 'remote') { + matchesStatus = !scan.success && (error.includes('image not available for scanning') || error.includes('not available')); + } else if (statusFilter === 'failed') { + matchesStatus = !scan.success && !(error.includes('image not available for scanning') || error.includes('not available')); + } + } + + return matchesSearch && matchesSeverity && matchesStatus; }); renderSecurityScansTable(filtered); @@ -5201,7 +5232,7 @@ function renderSecurityScansTable(scans) { if (!tbody) return; if (scans.length === 0) { - tbody.innerHTML = '
${escapeHtml(scan.image_name)}