Skip to content

Commit 5045f3b

Browse files
committed
fix(039): docs build, UI calm-mode, npx volume source extraction
- docs: drop links to untracked qa/security-scanners-2026-04-10.html that were breaking the Docusaurus build on PR #372 and Cloudflare Pages - ui: tone down Security tab info banners — Docker Isolated, Local Process and HTTP Server are now neutral cards; only "No Source Available" keeps the alert-error color. Local Process is no longer framed as a warning because running without Docker isolation is a user choice, not a fault - ui: clarify the Security Scanners description to state that each scanner runs inside an isolated Docker container sandboxed from the host - ui: hide Dashboard Docker-isolation and Quarantine chips until their status has been fetched, to avoid flashing a false "disabled" warning on initial page load - scanner: extend the container source resolver with a two-strategy extraction for npx/uvx servers — the target package is now located directly via `docker exec` and copied out because volume-mounted caches (e.g. /root/.npm) never show up in `docker diff`, while `docker diff` is still used to pick up additional user-added source in /app or /src Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3b8e503 commit 5045f3b

7 files changed

Lines changed: 679 additions & 124 deletions

File tree

docs/cli/security-commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ Before running any scanner, mcpproxy determines *what to scan*. The resolver ord
383383
The `source_method` and `source_path` are recorded on the scan job and shown in both the text and JSON report. This is how you verify a scanner is examining the right directory.
384384

385385
:::warning Don't confuse a config path with source code
386-
Prior to v0.23.x the resolver would pick *any* positional arg that happened to be a directory — including the data directory passed to `@modelcontextprotocol/server-filesystem`. That led to false positives on unrelated user files. The modern resolver tries the package cache first and only falls back to arg-based source if the arg directory contains recognizable source markers. See [docs/qa/security-scanners-2026-04-10.html](../qa/security-scanners-2026-04-10.html) §F-02 for the historical detail.
386+
Prior to v0.23.x the resolver would pick *any* positional arg that happened to be a directory — including the data directory passed to `@modelcontextprotocol/server-filesystem`. That led to false positives on unrelated user files. The modern resolver tries the package cache first and only falls back to arg-based source if the arg directory contains recognizable source markers.
387387
:::
388388

389389
---

docs/features/security-scanner-plugins.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ Every scanner is a plugin — there is no built-in scanner. MCPProxy exposes a u
3737
- `security scan` blocking mode has a real wait loop (no more hangs) with a hard timeout.
3838
- `scanner status` vocabulary unified between table and JSON: `available` / `pulling` / `installed` / `configured` / `error`.
3939

40-
For the full QA report and the cross-check of every fix see [docs/qa/security-scanners-2026-04-10.html](../qa/security-scanners-2026-04-10.html) (untracked local file).
41-
4240
## Quick start
4341

4442
### 1. List available scanners
@@ -310,16 +308,14 @@ The Security page at `/security` in the Web UI mirrors the CLI and provides:
310308
- **Scanner table** — every scanner in the registry with its current status, vendor link, and configure button.
311309
- **Scan All Servers** — batch scan trigger with live progress.
312310
- **Scan report viewer** at `/security/scans/{jobId}` — risk-score badge, finding detail cards with rule/location/scanner, per-scanner execution logs including stderr from failed scanners, and scan context (source method + path + file count).
313-
- **Approve Server / Force Approve / Reject** — scanner-gated approval dialog that requires a completed scan (or explicit force) before calling the approval endpoint. See [QA report §F-04](../qa/security-scanners-2026-04-10.html) for the historical context.
311+
- **Approve Server / Force Approve / Reject** — scanner-gated approval dialog that requires a completed scan (or explicit force) before calling the approval endpoint.
314312

315313
## Known limitations
316314

317315
- **Ramparts on arm64 macOS** — the upstream scanner image ships a binary linked against a newer GLIBC than the image base and fails every run on arm64. Track the [scanner-ramparts image rebuild](https://github.com/smart-mcp-proxy/mcpproxy-go/issues) for a fix. Other 6 of 7 scanners work out of the box on arm64 macOS.
318316
- **Cisco scanner output has a hardcoded `server_url`** header in its stdout (`https://mcp.deepwiki.com/mcp`). Cosmetic, does not affect findings.
319317
- **Pass 2 (supply-chain audit)** currently requires Docker isolation to be enabled, otherwise it fails source resolution. The UI doesn't yet surface this precondition.
320318

321-
See the [QA report §9](../qa/security-scanners-2026-04-10.html) for the full list of residual items and recommendations.
322-
323319
## Related reading
324320

325321
- [Security Commands](/cli/security-commands) — exhaustive CLI reference

frontend/src/views/Dashboard.vue

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -211,19 +211,20 @@
211211

212212
<!-- Security Status -->
213213
<div class="z-10 w-full max-w-[300px] space-y-2 mt-4">
214-
<!-- Docker Isolation -->
215-
<div class="flex items-center gap-2 text-xs px-3 py-2 rounded-lg"
216-
:class="dockerStatus?.available ? 'bg-success/10 text-success' : 'bg-warning/10 text-warning'">
214+
<!-- Docker Isolation (hidden until status has been fetched to avoid
215+
flashing a false "disabled" state on initial page load) -->
216+
<div v-if="dockerStatus" class="flex items-center gap-2 text-xs px-3 py-2 rounded-lg"
217+
:class="dockerStatus.available ? 'bg-success/10 text-success' : 'bg-warning/10 text-warning'">
217218
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
218219
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
219220
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
220221
</svg>
221-
<span v-if="dockerStatus?.available" class="font-medium">Docker isolation active</span>
222+
<span v-if="dockerStatus.available" class="font-medium">Docker isolation active</span>
222223
<span v-else class="font-medium">Docker isolation disabled — enable Docker to protect your system</span>
223224
</div>
224225

225-
<!-- Quarantine -->
226-
<div class="flex items-center gap-2 text-xs px-3 py-2 rounded-lg"
226+
<!-- Quarantine (hidden until config fetch completes) -->
227+
<div v-if="quarantineEnabled !== null" class="flex items-center gap-2 text-xs px-3 py-2 rounded-lg"
227228
:class="quarantineEnabled ? 'bg-success/10 text-success' : 'bg-warning/10 text-warning'">
228229
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
229230
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -458,7 +459,10 @@ const loadActivitySummary = async () => {
458459
459460
// --- Security status ---
460461
const dockerStatus = ref<{available: boolean, version?: string} | null>(null)
461-
const quarantineEnabled = ref(false)
462+
// quarantineEnabled is null until the config fetch completes so the template
463+
// can skip rendering the "Quarantine disabled" warning on initial load. A
464+
// plain `false` default would briefly display the warning before data arrives.
465+
const quarantineEnabled = ref<boolean | null>(null)
462466
463467
// Security scanner totals for the "Security Scan" dashboard chip (F-12).
464468
// We reuse the shared composable so we don't double-fetch /security/overview.

frontend/src/views/Security.vue

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@
9999
</div>
100100
</div>
101101

102-
<!-- Docker unavailable warning -->
103-
<div v-if="overview && !overview.docker_available" class="alert alert-warning">
102+
<!-- Docker unavailable warning (only after overview has loaded) -->
103+
<div v-if="overviewLoaded && overview.docker_available === false" class="alert alert-warning">
104104
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
105105
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
106106
</svg>
@@ -127,7 +127,7 @@
127127
<div class="card bg-base-100 shadow-xl">
128128
<div class="card-body">
129129
<h2 class="card-title">Security Scanners</h2>
130-
<p class="text-sm text-base-content/70 mb-4">Scanners are Docker-based plugins powered by third-party security tools. Enable or disable individual scanners and configure their API keys.</p>
130+
<p class="text-sm text-base-content/70 mb-4">Each scanner is a third-party security tool that runs inside an isolated Docker container — sandboxed from your host, with no access to your filesystem or network beyond what the scan requires. Enable or disable individual scanners and configure their API keys below.</p>
131131

132132
<div v-if="scanners.length === 0" class="text-center py-8 text-base-content/50">
133133
No scanners available. Check Docker connectivity.
@@ -257,9 +257,6 @@
257257
<th class="cursor-pointer select-none" @click="toggleSort('findings_count')">
258258
Findings {{ sortIndicator('findings_count') }}
259259
</th>
260-
<th class="cursor-pointer select-none tooltip tooltip-bottom" data-tip="Experimental heuristic score. Not a definitive safety assessment." @click="toggleSort('risk_score')">
261-
Risk* {{ sortIndicator('risk_score') }}
262-
</th>
263260
<th></th>
264261
</tr>
265262
</thead>
@@ -285,10 +282,6 @@
285282
<td>
286283
<span :class="{ 'font-bold': (scan.findings_count || 0) > 0 }">{{ scan.findings_count || 0 }}</span>
287284
</td>
288-
<td>
289-
<span v-if="scan.risk_score != null" :class="riskScoreClass(scan.risk_score)">{{ scan.risk_score }}</span>
290-
<span v-else class="text-base-content/30">-</span>
291-
</td>
292285
<td>
293286
<router-link
294287
v-if="scan.status === 'completed'"
@@ -405,6 +398,11 @@ const loading = ref(false)
405398
const error = ref('')
406399
const scanners = ref<any[]>([])
407400
const overview = ref<any>({})
401+
// overviewLoaded stays false until the first successful /security/overview
402+
// response arrives. Template guards like the "Docker not running" banner use
403+
// this to avoid flashing a false warning while the initial request is still
404+
// in flight (overview.docker_available is undefined before the fetch lands).
405+
const overviewLoaded = ref(false)
408406
const installing = ref<string | null>(null)
409407
410408
// Scan history state
@@ -494,12 +492,6 @@ function scannerNeedsApiKey(scanner: any): boolean {
494492
return scanner.required_env.some((env: any) => !configured[env.key])
495493
}
496494
497-
function riskScoreClass(score: number) {
498-
if (score >= 70) return 'text-error'
499-
if (score >= 40) return 'text-warning'
500-
return 'text-success'
501-
}
502-
503495
async function refresh() {
504496
loading.value = true
505497
error.value = ''
@@ -516,7 +508,10 @@ async function refresh() {
516508
list.sort((a, b) => String(a.id).localeCompare(String(b.id)))
517509
scanners.value = list
518510
}
519-
if (overviewRes.success) overview.value = overviewRes.data || {}
511+
if (overviewRes.success) {
512+
overview.value = overviewRes.data || {}
513+
overviewLoaded.value = true
514+
}
520515
} catch (e: any) {
521516
error.value = e.message
522517
} finally {

frontend/src/views/ServerDetail.vue

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -636,57 +636,57 @@
636636
<!-- Scan Context Banner -->
637637
<div v-if="scanContext" class="mt-2">
638638
<!-- No Docker Isolation (local process) -->
639-
<div v-if="!scanContext.docker_isolation && !isUrlSourceMethod && scanContext.source_method !== 'none' && scanContext.source_method !== 'tool_definitions_only'" class="alert alert-warning">
640-
<svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
641-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
639+
<div v-if="!scanContext.docker_isolation && !isUrlSourceMethod && scanContext.source_method !== 'none' && scanContext.source_method !== 'tool_definitions_only'" class="flex items-start gap-3 p-4 rounded-lg bg-base-200/60 border border-base-300">
640+
<svg class="w-5 h-5 shrink-0 mt-0.5 text-base-content/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
641+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
642642
</svg>
643-
<div>
644-
<h3 class="font-bold">No Docker Isolation</h3>
645-
<p class="text-sm">This server runs locally without Docker isolation.</p>
646-
<p class="text-sm">
643+
<div class="min-w-0 flex-1">
644+
<h3 class="font-semibold text-sm">Local Process</h3>
645+
<p class="text-sm text-base-content/70">Running directly on the host, without Docker isolation.</p>
646+
<p class="text-sm text-base-content/70">
647647
Source: <code class="bg-base-300 px-1 rounded text-xs">{{ scanContext.source_path }}</code>
648648
<span v-if="scanContext.total_files"> ({{ scanContext.total_files }} files, {{ formatFileSize(scanContext.total_size_bytes) }})</span>
649649
</p>
650-
<p class="text-sm text-base-content/70">
650+
<p class="text-sm text-base-content/60">
651651
Protocol: {{ scanContext.server_protocol }}
652652
<span v-if="scanContext.server_command"> &bull; Command: {{ scanContext.server_command }}</span>
653653
</p>
654654
</div>
655655
</div>
656656

657657
<!-- Docker Isolated -->
658-
<div v-else-if="scanContext.docker_isolation" class="alert alert-info">
659-
<svg class="w-6 h-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
658+
<div v-else-if="scanContext.docker_isolation" class="flex items-start gap-3 p-4 rounded-lg bg-base-200/60 border border-base-300">
659+
<svg class="w-5 h-5 shrink-0 mt-0.5 text-base-content/60" fill="none" viewBox="0 0 24 24" stroke="currentColor">
660660
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
661661
</svg>
662-
<div>
663-
<h3 class="font-bold">Docker Isolated</h3>
664-
<p class="text-sm">
662+
<div class="min-w-0 flex-1">
663+
<h3 class="font-semibold text-sm">Docker Isolated</h3>
664+
<p class="text-sm text-base-content/70">
665665
Source extracted from container<span v-if="scanContext.container_id">: <code class="bg-base-300 px-1 rounded text-xs">{{ scanContext.container_id.substring(0, 12) }}...</code></span>
666666
</p>
667-
<p class="text-sm">
667+
<p class="text-sm text-base-content/70">
668668
Source: <code class="bg-base-300 px-1 rounded text-xs">{{ scanContext.source_path }}</code>
669669
<span v-if="scanContext.total_files"> ({{ scanContext.total_files }} files, {{ formatFileSize(scanContext.total_size_bytes) }})</span>
670670
</p>
671-
<p class="text-sm text-base-content/70">
671+
<p class="text-sm text-base-content/60">
672672
Protocol: {{ scanContext.server_protocol }}
673673
<span v-if="scanContext.server_command"> &bull; Command: {{ scanContext.server_command }}</span>
674674
</p>
675675
</div>
676676
</div>
677677

678678
<!-- HTTP Server (url, url_full, or tool_definitions_only for http protocol) -->
679-
<div v-else-if="isUrlSourceMethod || scanContext.source_method === 'tool_definitions_only'" class="alert alert-info">
680-
<svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
679+
<div v-else-if="isUrlSourceMethod || scanContext.source_method === 'tool_definitions_only'" class="flex items-start gap-3 p-4 rounded-lg bg-base-200/60 border border-base-300">
680+
<svg class="w-5 h-5 shrink-0 mt-0.5 text-base-content/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
681681
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
682682
</svg>
683-
<div>
684-
<h3 class="font-bold">{{ isUrlSourceMethod ? 'HTTP Server' : 'Tool Definitions Only' }}</h3>
685-
<p class="text-sm">{{ isUrlSourceMethod ? 'Tool description scanning only (no filesystem to scan)' : 'Scanning tool descriptions for poisoning and injection attacks' }}</p>
686-
<p v-if="isUrlSourceMethod && scanContext.source_path" class="text-sm">
683+
<div class="min-w-0 flex-1">
684+
<h3 class="font-semibold text-sm">{{ isUrlSourceMethod ? 'HTTP Server' : 'Tool Definitions Only' }}</h3>
685+
<p class="text-sm text-base-content/70">{{ isUrlSourceMethod ? 'Tool description scanning only (no filesystem to scan)' : 'Scanning tool descriptions for poisoning and injection attacks' }}</p>
686+
<p v-if="isUrlSourceMethod && scanContext.source_path" class="text-sm text-base-content/70">
687687
URL: <code class="bg-base-300 px-1 rounded text-xs">{{ scanContext.source_path }}</code>
688688
</p>
689-
<p v-if="scanContext.tools_exported" class="text-sm text-base-content/70">
689+
<p v-if="scanContext.tools_exported" class="text-sm text-base-content/60">
690690
{{ scanContext.tools_exported }} tool definitions exported for analysis
691691
</p>
692692
</div>

0 commit comments

Comments
 (0)