Skip to content

Commit 6d49fd0

Browse files
committed
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>
1 parent e4fc88b commit 6d49fd0

7 files changed

Lines changed: 106 additions & 23 deletions

File tree

frontend/src/components/ServerCard.vue

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -181,32 +181,34 @@
181181
Logout
182182
</button>
183183

184-
<div
185-
v-if="!server.enabled"
186-
class="tooltip tooltip-top"
187-
data-tip="Enable server first"
188-
>
189-
<button
184+
<template v-if="hasEnabledScanners()">
185+
<div
186+
v-if="!server.enabled"
187+
class="tooltip tooltip-top"
188+
data-tip="Enable server first"
189+
>
190+
<button
191+
class="btn btn-sm btn-outline btn-ghost"
192+
disabled
193+
>
194+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
195+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
196+
</svg>
197+
Scan
198+
</button>
199+
</div>
200+
<router-link
201+
v-else
202+
:to="`/servers/${server.name}?tab=security`"
190203
class="btn btn-sm btn-outline btn-ghost"
191-
disabled
204+
title="Security Scan"
192205
>
193206
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
194207
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
195208
</svg>
196209
Scan
197-
</button>
198-
</div>
199-
<router-link
200-
v-else
201-
:to="`/servers/${server.name}?tab=security`"
202-
class="btn btn-sm btn-outline btn-ghost"
203-
title="Security Scan"
204-
>
205-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
206-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
207-
</svg>
208-
Scan
209-
</router-link>
210+
</router-link>
211+
</template>
210212

211213
<router-link
212214
:to="`/servers/${server.name}`"
@@ -262,6 +264,7 @@ import { ref, computed } from 'vue'
262264
import type { Server } from '@/types'
263265
import { useServersStore } from '@/stores/servers'
264266
import { useSystemStore } from '@/stores/system'
267+
import { useSecurityScannerStatus } from '@/composables/useSecurityScannerStatus'
265268
266269
interface Props {
267270
server: Server
@@ -271,6 +274,7 @@ const props = defineProps<Props>()
271274
272275
const serversStore = useServersStore()
273276
const systemStore = useSystemStore()
277+
const { hasEnabledScanners } = useSecurityScannerStatus()
274278
const loading = ref(false)
275279
const showDeleteConfirmation = ref(false)
276280
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Reactive composable that exposes whether any security scanner is currently
2+
// enabled. Used by Servers.vue, ServerDetail.vue, and other places that show
3+
// scan-trigger buttons — buttons should be hidden entirely when no scanner is
4+
// active.
5+
//
6+
// The result is cached at module scope so multiple components share a single
7+
// network call. Call refreshSecurityScannerStatus() after the user
8+
// installs/removes a scanner to update the state.
9+
10+
import { ref } from 'vue'
11+
import api from '@/services/api'
12+
13+
const enabledCount = ref<number | null>(null)
14+
const dockerAvailable = ref<boolean>(true)
15+
const loaded = ref(false)
16+
let inflight: Promise<void> | null = null
17+
18+
export async function refreshSecurityScannerStatus(): Promise<void> {
19+
if (inflight) {
20+
return inflight
21+
}
22+
inflight = (async () => {
23+
try {
24+
const res = await api.getSecurityOverview()
25+
const data: any = res?.data ?? {}
26+
// Prefer the new scanners_enabled field; fall back to scanners_installed
27+
// for backwards compatibility with older mcpproxy builds.
28+
const enabled = data.scanners_enabled ?? data.scanners_installed ?? 0
29+
enabledCount.value = typeof enabled === 'number' ? enabled : 0
30+
dockerAvailable.value = data.docker_available !== false
31+
loaded.value = true
32+
} catch {
33+
// On error, keep current values; the UI will fall back to dockerAvailable
34+
// checks. We don't want a transient API blip to make all scan buttons
35+
// disappear.
36+
} finally {
37+
inflight = null
38+
}
39+
})()
40+
return inflight
41+
}
42+
43+
export function useSecurityScannerStatus() {
44+
if (!loaded.value && !inflight) {
45+
void refreshSecurityScannerStatus()
46+
}
47+
return {
48+
enabledCount,
49+
dockerAvailable,
50+
loaded,
51+
/** True when at least one scanner is enabled and docker is available. */
52+
hasEnabledScanners: () => (enabledCount.value ?? 0) > 0,
53+
refresh: refreshSecurityScannerStatus,
54+
}
55+
}

frontend/src/views/Security.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
<p class="text-base-content/70 mt-1">Configure security scanner plugins and review scan results</p>
88
</div>
99
<div class="flex gap-2">
10-
<div class="tooltip" :data-tip="!overview?.docker_available ? 'Docker is required to run security scanners' : ''">
10+
<div
11+
v-if="(overview?.scanners_enabled ?? overview?.scanners_installed ?? 0) > 0"
12+
class="tooltip"
13+
:data-tip="!overview?.docker_available ? 'Docker is required to run security scanners' : ''"
14+
>
1115
<button @click="startScanAll" :disabled="loading || scanAllRunning || !overview?.docker_available" class="btn btn-primary">
1216
<span v-if="scanAllRunning" class="loading loading-spinner loading-sm"></span>
1317
{{ scanAllRunning ? 'Scanning...' : 'Scan All Servers' }}
@@ -386,6 +390,7 @@
386390
<script setup lang="ts">
387391
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
388392
import api from '@/services/api'
393+
import { refreshSecurityScannerStatus } from '@/composables/useSecurityScannerStatus'
389394
390395
const loading = ref(false)
391396
const error = ref('')
@@ -512,6 +517,9 @@ async function toggleScanner(scanner: any) {
512517
await api.removeScanner(scanner.id)
513518
}
514519
await refresh()
520+
// Refresh shared scanner-status cache so other pages (Servers,
521+
// ServerDetail) update their scan-button visibility immediately.
522+
await refreshSecurityScannerStatus()
515523
} finally {
516524
installing.value = null
517525
}

frontend/src/views/ServerDetail.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@
214214
Configuration
215215
</button>
216216
<button
217+
v-if="hasEnabledScanners()"
217218
:class="['tab tab-lg', activeTab === 'security' ? 'tab-active' : '']"
218219
@click="activeTab = 'security'; loadScannerNames(); loadScanReport()"
219220
>
@@ -501,8 +502,9 @@
501502
<div class="space-y-6">
502503
<!-- Header: Scan button + Risk Score -->
503504
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
504-
<div class="tooltip" :data-tip="!dockerAvailable ? 'Docker is required to run security scanners' : ''">
505+
<div class="tooltip" :data-tip="!dockerAvailable ? 'Docker is required to run security scanners' : (!hasEnabledScanners() ? 'No scanners enabled — install one from Security Scanners' : '')">
505506
<button
507+
v-if="hasEnabledScanners()"
506508
@click="startSecurityScan"
507509
:disabled="scanLoading || !dockerAvailable"
508510
class="btn btn-primary"
@@ -750,6 +752,7 @@ import AnnotationBadges from '@/components/AnnotationBadges.vue'
750752
import type { Hint } from '@/components/CollapsibleHintsPanel.vue'
751753
import type { Server, Tool, ToolApproval, SecurityScanReport } from '@/types'
752754
import api from '@/services/api'
755+
import { useSecurityScannerStatus } from '@/composables/useSecurityScannerStatus'
753756
754757
interface Props {
755758
serverName: string
@@ -785,6 +788,7 @@ const quarantinedTools = computed(() => {
785788
786789
// Security scan (Spec 039)
787790
const dockerAvailable = ref(true) // optimistic default until overview loads
791+
const { hasEnabledScanners } = useSecurityScannerStatus()
788792
const scanReport = ref<SecurityScanReport | null>(null)
789793
const scanStatus = ref<any>(null)
790794
const scanLoading = ref(false)

frontend/src/views/Servers.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
{{ serversStore.loading.loading ? 'Refreshing...' : 'Refresh' }}
2020
</button>
2121
<button
22+
v-if="hasEnabledScanners()"
2223
@click="scanAllServers"
2324
:disabled="scanAllRunning"
2425
class="btn btn-primary btn-outline"
@@ -170,12 +171,14 @@ import api from '@/services/api'
170171
import ServerCard from '@/components/ServerCard.vue'
171172
import CollapsibleHintsPanel from '@/components/CollapsibleHintsPanel.vue'
172173
import type { Hint } from '@/components/CollapsibleHintsPanel.vue'
174+
import { useSecurityScannerStatus } from '@/composables/useSecurityScannerStatus'
173175
174176
const serversStore = useServersStore()
175177
const systemStore = useSystemStore()
176178
const filter = ref<'all' | 'connected' | 'enabled' | 'quarantined'>('all')
177179
const searchQuery = ref('')
178180
const scanAllRunning = ref(false)
181+
const { hasEnabledScanners } = useSecurityScannerStatus()
179182
180183
const filteredServers = computed(() => {
181184
let servers = serversStore.servers

internal/security/scanner/service.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1129,10 +1129,18 @@ func (s *Service) GetSecurityOverview(ctx context.Context) (*SecurityOverview, e
11291129
func (s *Service) GetOverview(ctx context.Context) (*SecurityOverview, error) {
11301130
overview := &SecurityOverview{}
11311131

1132-
// Count installed scanners
1132+
// Count installed scanners. ScannersInstalled is the total number of
1133+
// scanners persisted in storage; ScannersEnabled is the subset the engine
1134+
// will actually run (status installed or configured). UI uses
1135+
// ScannersEnabled to decide whether to show scan-trigger buttons.
11331136
scanners, err := s.storage.ListScanners()
11341137
if err == nil {
11351138
overview.ScannersInstalled = len(scanners)
1139+
for _, sc := range scanners {
1140+
if sc.Status == ScannerStatusInstalled || sc.Status == ScannerStatusConfigured {
1141+
overview.ScannersEnabled++
1142+
}
1143+
}
11361144
}
11371145

11381146
// Count scan jobs

internal/security/scanner/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ type SecurityOverview struct {
249249
ActiveScans int `json:"active_scans"`
250250
FindingsBySeverity ReportSummary `json:"findings_by_severity"`
251251
ScannersInstalled int `json:"scanners_installed"`
252+
ScannersEnabled int `json:"scanners_enabled"` // Subset of installed that the engine will run (status installed or configured)
252253
ServersScanned int `json:"servers_scanned"`
253254
LastScanAt time.Time `json:"last_scan_at,omitempty"`
254255
DockerAvailable bool `json:"docker_available"`

0 commit comments

Comments
 (0)