Commit 60c2eef
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
- .github/workflows
- cmd/mcpproxy
- docker/scanners
- cisco
- proximity
- ramparts
- snyk
- docs/features
- frontend/src
- components
- composables
- router
- services
- stores
- types
- views
- internal
- config
- contracts
- httpapi
- runtime
- security/scanner
- server
- storage
- upstream
- core
- managed
- types
- oas
- specs
- 039-scanner-qa-audit
- 039-security-scanner-plugins
- checklists
- web/frontend/dist
- assets
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
178 | 178 | | |
179 | 179 | | |
180 | 180 | | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
181 | 184 | | |
182 | 185 | | |
183 | 186 | | |
| |||
199 | 202 | | |
200 | 203 | | |
201 | 204 | | |
| 205 | + | |
202 | 206 | | |
203 | 207 | | |
204 | 208 | | |
| |||
0 commit comments