Skip to content

Commit 60c2eef

Browse files
Dumbrisclaude
andauthored
feat(039): Security Scanner Plugin System (#364)
* feat(039): Security Scanner Plugin System Implement pluggable security scanner system for analyzing quarantined MCP servers before approval. Docker-based scanners run in isolated containers, produce SARIF reports, and integrate with the existing quarantine workflow. ## Changes - Scanner plugin architecture: types, registry (4 bundled scanners), Docker runner, SARIF 2.1.0 parser, parallel scan engine - Storage: 4 new BBolt buckets (scanners, jobs, reports, baselines) - Security service: install/configure scanners, scan/approve/reject workflow, integrity verification, risk scoring - REST API: 13 endpoints for scanner management, scan operations, approval flow, and security overview - CLI: mcpproxy security command group with 12 subcommands - Web UI: Security dashboard with scanner marketplace, scan trigger, findings viewer, approve/reject actions - SSE events: scan lifecycle and integrity alert events - Documentation: feature guide, spec, plan, autonomous summary ## Testing - 500+ tests passing across scanner, storage, httpapi, config packages - Race detector clean on all new code - Frontend type-checks clean and builds successfully Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(039): regenerate OpenAPI spec for SecurityConfig Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): wire SecurityService into HTTP server, E2E fixes Critical fixes discovered during E2E testing: - Wire scanner.Service as SecurityController in server setup - Register security routes unconditionally (nil-guard in handlers) - Use background context for scan goroutines (prevent request context cancellation) - Sync registry from storage on startup (scanner state survives restart) - Add source_dir parameter to scan API and service - Fix Semgrep: mount source at /src (Docker image requirement), enable network - Disable read-only for scanner containers (they need cache writes) - Fix Trivy image name: aquasecurity/trivy (not aquasec/trivy) - Add GetSecurityOverview alias on Service for interface compliance Verified E2E: - Semgrep scan found subprocess shell=True (HIGH) and eval() (MEDIUM) - Full approve/reject/report CLI workflow tested against running server Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): auto-resolve source from Docker containers for zero-config scanning Users no longer need to provide --source-dir. When scanning, MCPProxy automatically: 1. Finds the running Docker container for the server (mcpproxy-<name>-*) 2. Extracts app source files via docker cp + docker diff analysis 3. Mounts extracted source for scanner containers 4. Cleans up temp files after scan completes Fallback chain: Docker container -> working_dir -> command args -> error. HTTP/SSE servers resolve to URL for mcp_connection scanners. Verified E2E: mcpproxy security scan perplexity works with zero config. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): store scanner API keys in OS keyring, not plaintext Scanner API keys configured via the API or CLI are now stored in the OS keyring (macOS Keychain / Linux Secret Service / Windows Credential Manager) using the existing mcpproxy secrets system. - ConfigureScanner stores values via keyring, keeps ${keyring:...} refs - Engine resolves keyring references at scan time before passing to containers - Secrets visible in `mcpproxy secrets list` alongside server secrets - Fallback to direct storage if keyring is unavailable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): wrap security API responses in {success, data} envelope The Web UI expected APIResponse format {success: true, data: ...} but security handlers were writing raw data. Changed all handlers to use writeSuccess() which wraps via contracts.NewSuccessResponse(). Updated all 23 httpapi security tests to parse the wrapped format. Rebuilt frontend dist for embedded Web UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): E2E scanner fixes — CLI unwrap, Trivy entrypoint, tmpfs size Bugs found and fixed during E2E testing with real scanners: 1. CLI commands failed to parse wrapped API responses ({success, data}) - Added unwrapAPIResponse() helper to extract data from envelope - Applied to all 8 json.Unmarshal points in security_cmd.go 2. Trivy scanner: "unknown command trivy for trivy" - Trivy Docker image has ENTRYPOINT=[trivy], so command was doubled - Fixed: command is now ["fs", "--format", "sarif", "/scan/source"] 3. Trivy scanner: "no space left on device" during DB download - Trivy downloads 89MB vuln DB to /tmp on first run - Tmpfs was 100MB with noexec — too small and restrictive - Fixed: increased to 500MB, removed noexec (scanners need it) 4. Trivy Docker image: aquasecurity/trivy not on Docker Hub - Changed to ghcr.io/aquasecurity/trivy:latest (GHCR registry) Verified E2E: Semgrep + Trivy both complete scans successfully on quarantined everything-server with real Docker containers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): enrich scan findings with CVE links, package info, CVSS scores Findings now include: - help_uri: clickable link to CVE advisory (e.g., https://avd.aquasec.com/nvd/CVE-xxx) - cvss_score: CVSS severity score (0-10) from SARIF properties - package_name, installed_version, fixed_version: extracted from Trivy message format CLI report shows: [HIGH] CVE-2025-66414 (trivy-mcp) Package: @modelcontextprotocol/sdk v0.6.0 -> fix: 1.24.0 Details: https://avd.aquasec.com/nvd/cve-2025-66414 Web UI findings table shows clickable CVE links, package column, fix version badge, and CVSS score. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): Security tab on ServerDetail, scan badge on cards, threat categories Backend: SecurityScanSummary on Server contract, threat classification, scan summary computation from stored reports. Frontend: Security tab with risk score, threat summary cards, grouped findings, scan button with polling. Scan badge on server cards. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(039): regenerate OpenAPI spec for SecurityScanSummary types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): wire scan summary into management service path The /api/v1/servers endpoint uses management.ListServers(), not the legacy GetAllServers(). Moved scan summary enrichment to the httpapi handler. Also re-classify legacy findings without threat_level on read. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): consistent threat counts in Security tab AggregateReports now classifies findings before summarizing. ReportSummary includes dangerous/warnings/info_level counts. Frontend reads summary from API response. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): threat-based risk scoring + scanner execution logs Risk score based on threat level instead of raw CVSS severity. Scanner stdout/stderr/exit_code stored on ScannerJobStatus. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): Configure button on all installed scanners + custom env vars Configure button shows for all installed/configured scanners. Dialog supports required, optional, and custom env vars with keyring integration. Users can add OPENAI_API_KEY etc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): expandable finding details + scanner execution logs Each finding is now an expandable card with full details. Scanner Execution Logs section shows stderr/stdout per scanner. Fixed TS types to match Go API field names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): scan context, file tree API, scan history with limits ScanContext on ScanJob with source method, path, isolation status, file list. File tree API with suspicious file markers. Auto-prune keeps last 20 scans. Frontend scan context banner + scanned files collapsible with lazy loading. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * perf(039): strip sarif_raw from report API (2.1MB -> 2.9KB) SARIF raw data can be 2MB+ (Semgrep includes all rule definitions). Web UI was stuck on "Loading scan report..." because of this. Now stripped by default; add ?include_sarif=true for CLI --output sarif. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): Scan All with worker pool, queue, progress tracking ScanQueue with 3 concurrent workers, progress tracking, cancel support. REST API: scan-all, queue, cancel-all endpoints. CLI: --all flag and cancel-all subcommand. Web UI: Scan All button with progress card. Disabled servers auto-skipped with hint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: clean stale frontend dist bundles Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): eliminate false safety when all scanners fail SECURITY: UI now shows red error instead of green "safe" when scanners fail. AggregatedReport tracks scan_complete, scanners_run/failed/total. Server card badge shows "Scan Failed" (red) for incomplete scans. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: regenerate OpenAPI spec Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): skip scanners with missing Docker images + persist cache Three fixes for scanner reliability: 1. resolveScanners() now checks ImageExists() before adding to run list. Scanners with non-existent Docker images (mcp-scan, cisco-mcp-scanner placeholders) are skipped with a warning log instead of failing at runtime with exit code 125. 2. Per-scanner cache directory persisted at ~/.mcpproxy/scanner-cache/<id>/ mounted at /root/.cache in containers. Trivy DB (90MB) is now downloaded once and reused across runs instead of re-downloading every scan. 3. Fixed test to use nil docker runner for resolution logic tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): Semgrep timeout + exclude dependency dirs from scan Semgrep timed out on ElevenLabs (14,953 files, 361MB including 6,979 Python files from site-packages). Two fixes: 1. Semgrep timeout increased from 5min to 10min for large source trees 2. Semgrep command now excludes site-packages, node_modules, dist-packages 3. Source resolver excludes dependency directories from docker diff extraction — these are third-party packages, not user code Before: ElevenLabs extracted 14,953 files (361MB) -> Semgrep timeout After: Only actual MCP server code extracted -> fast scan Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): extract UV git checkouts for uvx-from-git servers For servers installed via uvx --from pkg@git+URL, the actual source code lives at /root/.cache/uv/git-v0/checkouts/<hash>/<rev>/. Previously this was extracted as /root/.cache (too broad) or filtered out entirely. Changes: - extractAppRoot recognizes UV git checkout paths specifically - isSystemPath excludes UV archive-v0 (dependencies) but keeps git-v0 - npm node_modules exclusion now allows npx cache paths - Tests updated for UV git checkout extraction Note: Scanner requires the server container to be fully connected. If uvx is still downloading from GitHub when the scan runs, the source won't be available yet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): extract UV git checkout source for uvx-from-git servers For servers installed via uvx --from pkg@git+URL (like gcore-mcp-server), the actual source lives at /root/.cache/uv/git-v0/checkouts/<hash>/<rev>/. Fixes: - extractAppRoot excludes /root/.cache and /root/.local (too broad) - Fallback path uses docker exec find to locate UV git checkouts directly when docker diff doesn't show them - Removed /root from fallback dirs (was copying entire 10K+ file cache) Before: 10,536 files (174MB) including all pip/uv dependencies After: 26 files (422KB) — just the actual MCP server source code Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): two-pass scanning -- fast security + background supply chain Pass 1: Semgrep on source + Trivy on lockfile (fast, immediate results) Pass 2: Trivy full filesystem (background, auto-starts after Pass 1) Results merged in single report with pass1/pass2 completion tracking. 7 new unit tests. Frontend shows both passes with progress indicator. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * perf(039): cache scan summaries in memory (12s -> 0.8s servers API) In-memory cache for scan summaries, invalidated on scan start/complete. Cold cache: 3.0s. Warm cache: 0.8s. Was: 12.1s. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * perf(039): paginate scanned files API (37K files -> 100 per page) Pagination with limit/offset/suspicious_only. Progressive loading in frontend with "Load more" button. Suspicious files sorted first. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): integrate Cisco MCP Scanner with YARA rules (offline, no API key) Built Docker image for Cisco AI Defense MCP Scanner. Runs YARA + readiness analyzers offline. Detects tool poisoning, prompt injection, credential harvesting, data exfiltration in MCP tool descriptions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): show evidence text that triggered security findings Evidence field on ScanFinding captures the tool description that triggered Cisco YARA warnings. Shown in Web UI and CLI report. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): auto-connect quarantined servers for scanning + detect empty scans When scanning quarantined/disconnected servers, auto-connect them first so tool definitions can be exported for the Cisco scanner. Create a temp dir for tool-definitions-only scans when no source files are available. Detect empty scans (0 files, 0 tools exported) and show "No Files Scanned" instead of a misleading green "0/100" safe score. Add --novcs to Semgrep so it scans Docker-extracted dirs that aren't git repos. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): scanner QA audit — 6 bug fixes, Docker image upgrades, source resolver fix ## Scanner QA Fixes - Return 409 Conflict for concurrent scan attempts (was 500) - Deduplicate findings when merging Pass 1 + Pass 2 reports - Populate threat level counts (dangerous/warnings/info_level) in security overview - Add Cancel Scan button to server security tab - Add Retry button after scan errors - Hide Scanned Files section for HTTP/tool_definitions_only servers ## Docker Image Upgrades - Python: python:3.11 → ghcr.io/astral-sh/uv:python3.13-bookworm-slim (single image for all Python) - Node.js: node:20 → node:22 (LTS until Apr 2028) - Add python3.13 to runtime detection in isolation manager ## Source Resolver Fix - Fix uvx git checkout resolution: match repo by pyproject.toml/git config instead of picking newest subdirectory (was scanning wrong repo) ## QA Report - 42 bugs found across API, backend, and frontend - 72 API tests executed across all server types - False positive analysis for cisco-mcp-scanner (context7, ElevenLabs) - Full HTML report: scanner-qa-report.html Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): AI scanner integration — mount Claude credentials, enable network - Mount ~/.claude read-only into AI scanner container for Claude API auth - Add ExtraMounts field to ScannerRunConfig for additional volume mounts - Set NetworkReq=true for mcp-ai-scanner (needs Claude API access) Tested on 3 servers: - malicious-demo: 4 tool poisoning findings (correct) - context7: 0 findings (no false positives, unlike cisco-mcp-scanner) - demo-filesystem: 0 findings (clean) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): add ListScanHistory and GetScanReportByJobID service methods Add ScanJobSummary type for lightweight scan history listing and two new Service methods: ListScanHistory (lists all scan jobs enriched with findings count and risk score) and GetScanReportByJobID (gets aggregated report by job ID with Pass 1/Pass 2 merging). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): add GET /security/scans and /security/scans/{jobId}/report endpoints Add scan history HTTP handlers with sorting, filtering, and pagination support. Update SecurityController interface with ListScanHistory and GetScanReportByJobID methods. Update mock in tests to satisfy interface. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): show scan ID and timestamp in CLI security report Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): add scan history API methods and ScanJobSummary type Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): add ScanReport.vue page with route /security/scans/:jobId Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): include ScanContext in AggregatedReport for report page display Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): replace scan-server section with scan history table on Security page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): simplify ServerDetail security tab to summary + View Full Report link Replace the full inline security report (findings grouped by threat type, supply chain audit, scanner logs, scanned files) with a compact summary card showing risk score, finding counts, and a "View Full Report" link to /security/scans/:jobId. Remove unused computed properties and constants (groupedFindings, pass2Findings, pass2HasDangerous, pass2HasWarnings, pass1SupplyChainCount, FindingGroup, threatTypeLabels, dangerousTypes). Add job_id field to SecurityScanReport TypeScript type. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): fix scan ID field name in Security.vue, optimize ListScanHistory performance - Security.vue used scan.job_id but API returns scan.id — fixed - ListScanHistory: use ScannerStatuses.FindingsCount instead of loading all reports per job (was O(N*M) bucket scans, hung on 224 jobs) - Update frontend dist Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): add stale scan cleanup, update MCP AI Scanner description Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): sortable columns, pagination, remove scan ID column and dropdown Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): record security scans in activity log Add ActivityTypeSecurityScan constant and ActivitySourceInternal source, then wire EventTypeSecurityScan{Started,Completed,Failed} events to new handlers that persist scan lifecycle records to the activity log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): rebuild frontend dist with all corrections Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): replace approve/reject with server status + quarantine on scan report page Shows actual server admin_state (enabled/disabled/quarantined) and offers contextual actions: quarantine when dangerous findings on enabled server, unquarantine when server is quarantined. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): remove approve/reject from ServerDetail security tab, rename nav to Security Scanners Remove Approve and Reject buttons from the security scan summary in ServerDetail.vue since these actions are no longer needed there (the scan report page handles this). Also remove the associated approveServerSecurity/rejectServerSecurity functions and the securityActionLoading ref. Rename the left nav item from "Security" to "Security Scanners" for clarity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): clean up stale running/pending scan jobs on startup When the mcpproxy process is killed, scan jobs stuck in "running" or "pending" status remain in the database permanently. This adds a CleanupStaleJobs() method that marks all such jobs as failed with an "interrupted by server restart" message, called once during startup before serving requests. The previous stale cleanup in ListScanHistory (which only ran on API calls and required a 2-hour threshold) is removed in favor of this more reliable startup-based approach. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): make vendor links visible with link-primary style Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): disable scan buttons when Docker is unavailable, show warning Add docker_available field to SecurityOverview API response. Disable "Scan All Servers" and per-server "Scan Now" buttons when Docker is not running, with tooltip explaining the requirement. Show a warning alert on the Security page when Docker is unavailable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): rebuild frontend dist with Docker availability check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): improve scanner status UX — show enabled/disabled/failed, API key hints, error details - Status shows "enabled" (green), "disabled" (gray), "failed" (red) - Error details shown inline when scanner fails (e.g., Docker pull failure) - "API key required" warning + "Set API Key" button for unconfigured required env - "Retry" button for failed scanners Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): wire scanner EventEmitter to Runtime — security scans now appear in Activity Log SetEmitter(s.runtime) was never called, so the scanner service used NoopEmitter and all scan events were silently discarded. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): restore scanner execution logs on Scan Report page Added ScannerStatuses to AggregatedReport struct and populated from the ScanJob. ScanReport.vue now shows per-scanner stdout/stderr, status badges, exit codes, and error messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): detect empty scans for HTTP servers with no tool definitions Previously HTTP/URL servers were exempted from empty scan detection, so when tool export failed (server disconnected), the scan reported risk_score=0 and scan_complete=true — misleading "clean" result. Now empty scan detection checks only TotalFiles==0 && ToolsExported==0, regardless of source method. HTTP servers with successfully exported tools (ToolsExported > 0) still pass; those where export failed are correctly flagged as empty_scan=true. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): fix MCP AI Scanner OAuth credentials — generate .credentials.json for container The scanner image uses Claude Agent SDK which reads OAuth tokens from ~/.claude/.credentials.json. When CLAUDE_CODE_OAUTH_TOKEN is configured via the UI (stored in keyring), we now: 1. Create a temp .claude directory 2. Copy .claude.json from host (if exists) 3. Generate .credentials.json with the OAuth token 4. Mount the temp dir as /app/.claude:ro Previously, the host's ~/.claude was mounted directly (read-only) but had no .credentials.json, so the scanner fell back to pattern-only mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): abort scan when HTTP/URL server has no source files and tools export fails HTTP/SSE servers (SourceMethod="url") with 0 files and ToolsExported=0 were slipping past the abort check, causing the Cisco MCP Scanner to fail with "File not found: /scan/source/tools.json" because no tools.json was written. Also fix AggregateReportsWithJobStatus to not mark URL or tool_definitions_only scans as empty — these methods are inherently file-less and 0 files is expected when scanners analyze URL endpoints or tool descriptions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): logarithmic risk scoring with deduplication and disclaimer Replace linear additive risk score with logarithmic diminishing returns: score = weight * log2(1 + unique_count) Findings are now deduplicated by (rule_id + location) so duplicate detections from multiple scanners no longer triple-count. Four dangerous findings now score 58 instead of 90 (cap). Add disclaimer banner on the Scan Report page and a tooltip on the Risk column in the Security overview explaining that the score is an experimental heuristic. There is no industry standard for aggregating multi-scanner MCP security findings into a single number (verified via research on CVSS/EPSS, Snyk/Ramparts/Cisco MCP Scanner, and OWASP/FAIR risk frameworks). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): allow Docker image override for security scanners Add per-scanner Docker image override so users can pin a specific version, use a mirror registry, or substitute a custom build without editing the bundled registry. Backend: - ScannerPlugin.ImageOverride field (persisted via existing scanner storage) - EffectiveImage() helper returns override if set, else bundled DockerImage - Engine uses EffectiveImage() for container launch and existence checks - PUT /api/v1/security/scanners/{id}/config accepts optional docker_image alongside env map (either or both required) Frontend: - Add Docker Image field to Configure dialog (placeholder shows default) - Show Configure button for all scanner states, not just installed/configured, so users can edit settings on disabled scanners too Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): hide scan buttons when no security scanners are enabled Previously the Servers page, ServerDetail security tab, and Security "Scan All" button were always visible regardless of whether any scanners were actually installed. Clicking them just produced an error "no scanners configured to run", which was a confusing dead end for users who hadn't opted into security scanning. Now scan-trigger UI is gated on the new SecurityOverview.ScannersEnabled field — the count of scanners with status installed or configured (the same set the engine actually runs). Backend: - Add ScannersEnabled int field to SecurityOverview - Compute it in Service.GetOverview by filtering scanner status Frontend: - New composable frontend/src/composables/useSecurityScannerStatus.ts that fetches /api/v1/security/overview once at app load and exposes a reactive hasEnabledScanners() helper. Module-scoped cache so all components share a single network call. - Servers.vue: gate "Scan All" header button - ServerDetail.vue: gate the "Security" tab itself and the "Scan Now" button inside the tab (defense-in-depth in case of deep links) - ServerCard.vue: gate the per-server "Scan" button - Security.vue: gate the "Scan All Servers" button using the existing overview response (which now includes scanners_enabled). Also calls refreshSecurityScannerStatus() after install/remove so other pages update immediately without a full reload. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): restore readable text on Servers "Scan All" button The button used "btn btn-primary btn-outline" which in the current DaisyUI theme collapses the text color onto the background, making "Scan All" invisible except on hover (reported in both corporate and business themes). Match the styling of the other scan triggers (Security "Scan All Servers", ServerDetail "Scan Now") by dropping btn-outline so the button renders as a standard filled btn-primary with primary-content text — readable in all themes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): reorganize sidebar, add collapse + tooltips Sidebar restructure: - Group navigation into Workspace / System sections with subtle uppercase labels — Dashboard sits solo at the top as the landing. - Workspace section holds the four priority items (Servers, Secrets, Activity Log, Security Scanners) with Agent Tokens nested as a second-level child under Secrets. - System section holds Repositories and Configuration, rendered at reduced weight (text-[13px], text-base-content/70) to signal they are secondary. - Remove Search entry — the top header already exposes a tools/servers search input. - Move Feedback out of the main nav to an icon button in the footer row next to the Theme dropdown, sized appropriately (h-9, w-5 icon) and flex-centered so it aligns correctly in both expanded and collapsed states. - Add inline SVG icons for every personal-edition nav item so the collapsed rail is recognisable at a glance. Collapsible sidebar: - New sidebarCollapsed state + toggleSidebar action in the system store, persisted to localStorage under mcpproxy-sidebar-collapsed. - Sidebar animates between w-64 (expanded) and w-14 (icon rail). The main content padding in App.vue animates in lockstep. - Collapse chevron sits in the logo row when expanded; a separate expand chevron appears in the rail when collapsed. - Labels hide in collapsed mode; titles fall back to native tooltips for discoverability. ServerCard tooltip: - Wrap the security scan badge in a DaisyUI tooltip that explains the current state (clean / N warnings / dangerous / failed / scanning / not scanned). Every variant includes the disclaimer that the score is an experimental heuristic and findings should be verified manually — so users don't over-trust a "Clean" label. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(039): background scanner image pulls + failed-status UX Enabling a scanner no longer blocks while Docker pulls a multi-hundred-MB image. Install/Configure now mark the scanner "pulling", return instantly, and kick off the pull in a goroutine via a new pullManager. Success flips it to installed/configured; failure flips it to error with the real docker pull output in error_message. All transitions publish a new security.scanner_changed SSE event so the web UI updates live without polling. Stop silently dropping scanners whose image is missing. engine.resolveScanners now returns a resolvedScanner{plugin, prefail} and executeScan records any prefailed scanner as a failed entry in the scan job. A "Scan All" run that previously reported "1 of 1 scanners ran" when Snyk was silently skipped now correctly reports "1 of 2 failed" with a user-readable reason pointing at the missing image. Normalize every custom scanner image under ghcr.io/smart-mcp-proxy/scanner-* (vendor images for Semgrep and Trivy are kept upstream). Ship Dockerfiles for snyk, cisco, ramparts, and nova-proximity under docker/scanners/ plus a multi-arch GitHub Actions workflow that publishes them to GHCR on merges to main. See docs/features/scanner-images.md for the rationale on keeping the images in this repo instead of a separate one. Scanner listing is now sorted alphabetically by ID in Registry.List(), Service.ListScanners(), and Security.vue so the API, CLI, and web UI all agree on the same order. Frontend: new "pulling…" badge with spinner and image-name hint, dedicated Retry button for error state (the old Retry wired to toggleScanner called remove — wrong), Configure/Set-API-Key buttons disabled while pulling, and a window-event bridge so Security.vue reacts to SSE updates without re-fetching unrelated pages. Also race-safe the scanner service EventEmitter with atomic.Pointer — the old bare field assignment was latently racy and the new background pull goroutine made it observable. Tests: updated engine/service mocks for the new signature and the new EmitSecurityScannerChanged method; fixed two preexisting test-drift assertions in internal/httpapi (install returned "enabled"/"disabled", tests still expected "installed"/"removed"). go test -race passes on scanner, httpapi, and runtime packages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(039): scanner image builds, lint, Windows tests Snyk scanner: upstream package was renamed mcp-scan → snyk-agent-scan. The renamed CLI no longer accepts --version, so switch build sanity check to `-h` and update the entrypoint to call snyk-agent-scan. Cisco scanner: mcp-scanner CLI also rejects --version; use `-h`. Lint: remove the dead `buildFileTree` wrapper, unused mock types in engine_test.go and service_test.go, the unused `findNewestSubdir` helper, `shellescapeArgs`, and an empty else branch in server.go. Windows tests: `os.UserHomeDir()` reads USERPROFILE on Windows, so the npx/uvx resolver tests that only set HOME could not find the fake home. Set both env vars in Setenv so the tests run cross-platform. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
1 parent 4204b1e commit 60c2eef

124 files changed

Lines changed: 18268 additions & 322 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: scanner-images
2+
3+
# Build and publish the custom security scanner Docker images to
4+
# ghcr.io/smart-mcp-proxy/scanner-*. Vendor images (trivy, semgrep, the
5+
# AI scanner which lives in a separate repo) are NOT built here.
6+
#
7+
# Triggers:
8+
# - push to main that touches docker/scanners/** or this workflow file
9+
# - manual dispatch (for ad-hoc rebuilds)
10+
# - pull requests: builds without pushing (to catch Dockerfile drift)
11+
12+
on:
13+
push:
14+
branches: [main]
15+
paths:
16+
- 'docker/scanners/**'
17+
- '.github/workflows/scanner-images.yml'
18+
pull_request:
19+
paths:
20+
- 'docker/scanners/**'
21+
- '.github/workflows/scanner-images.yml'
22+
workflow_dispatch:
23+
inputs:
24+
scanner:
25+
description: 'Scanner id to rebuild (blank = all)'
26+
required: false
27+
default: ''
28+
29+
permissions:
30+
contents: read
31+
packages: write
32+
33+
jobs:
34+
build:
35+
runs-on: ubuntu-latest
36+
strategy:
37+
fail-fast: false
38+
matrix:
39+
include:
40+
- id: snyk
41+
image: ghcr.io/smart-mcp-proxy/scanner-snyk
42+
context: docker/scanners/snyk
43+
- id: cisco
44+
image: ghcr.io/smart-mcp-proxy/scanner-cisco
45+
context: docker/scanners/cisco
46+
- id: ramparts
47+
image: ghcr.io/smart-mcp-proxy/scanner-ramparts
48+
context: docker/scanners/ramparts
49+
- id: proximity
50+
image: ghcr.io/smart-mcp-proxy/scanner-proximity
51+
context: docker/scanners/proximity
52+
steps:
53+
- name: Checkout
54+
uses: actions/checkout@v4
55+
56+
- name: Skip if not selected
57+
if: ${{ github.event_name == 'workflow_dispatch' && inputs.scanner != '' && inputs.scanner != matrix.id }}
58+
run: echo "Skipping ${{ matrix.id }} (workflow_dispatch selected ${{ inputs.scanner }})" && exit 0
59+
60+
- name: Set up QEMU
61+
uses: docker/setup-qemu-action@v3
62+
63+
- name: Set up Buildx
64+
uses: docker/setup-buildx-action@v3
65+
66+
- name: Log in to GHCR
67+
if: ${{ github.event_name != 'pull_request' }}
68+
uses: docker/login-action@v3
69+
with:
70+
registry: ghcr.io
71+
username: ${{ github.actor }}
72+
password: ${{ secrets.GITHUB_TOKEN }}
73+
74+
- name: Derive tags
75+
id: tags
76+
run: |
77+
set -euo pipefail
78+
short_sha="${GITHUB_SHA:0:7}"
79+
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
80+
if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
81+
echo "tags=${{ matrix.image }}:latest,${{ matrix.image }}:$short_sha" >> "$GITHUB_OUTPUT"
82+
else
83+
echo "tags=${{ matrix.image }}:pr-${{ github.event.pull_request.number || 'dev' }}" >> "$GITHUB_OUTPUT"
84+
fi
85+
86+
- name: Build and push
87+
uses: docker/build-push-action@v6
88+
with:
89+
context: ${{ matrix.context }}
90+
platforms: linux/amd64,linux/arm64
91+
push: ${{ github.event_name != 'pull_request' }}
92+
tags: ${{ steps.tags.outputs.tags }}
93+
cache-from: type=gha,scope=scanner-${{ matrix.id }}
94+
cache-to: type=gha,scope=scanner-${{ matrix.id }},mode=max
95+
labels: |
96+
org.opencontainers.image.source=https://github.com/smart-mcp-proxy/mcpproxy-go
97+
org.opencontainers.image.revision=${{ github.sha }}

cmd/mcpproxy/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ func main() {
178178
// Add feedback command (Spec 036)
179179
feedbackCmd := GetFeedbackCommand()
180180

181+
// Add security command (Spec 039: Security scanner plugins)
182+
securityCmd := GetSecurityCommand()
183+
181184
// Add connect/disconnect commands
182185
connectCmd := GetConnectCommand()
183186
disconnectCmd := GetDisconnectCommand()
@@ -199,6 +202,7 @@ func main() {
199202
rootCmd.AddCommand(tokenCmd)
200203
rootCmd.AddCommand(telemetryCmd)
201204
rootCmd.AddCommand(feedbackCmd)
205+
rootCmd.AddCommand(securityCmd)
202206
rootCmd.AddCommand(connectCmd)
203207
rootCmd.AddCommand(disconnectCmd)
204208

0 commit comments

Comments
 (0)