Files
gohoarder/frontend/src/components/VulnerabilityBadge.vue
T
lukaszraczylo c0061b99e3 chore(schema): migrate to GORM V2 with multi-database support
- [x] Implement GORM V2 metadata store with SQLite, PostgreSQL, and MySQL support
- [x] Add database migration system using gormigrate for schema versioning
- [x] Create migration CLI tool with support for migrate, rollback, and status commands
- [x] Add Docker support for migration container (Dockerfile.migrate)
- [x] Implement automatic partition management for PostgreSQL time-series tables
- [x] Add background aggregation worker for download statistics
- [x] Support connection pooling configuration (max_open_conns, max_idle_conns, conn_max_lifetime)
- [x] Add blocking mechanism based on vulnerability thresholds in stats and handlers
- [x] Update Helm charts with migration init containers and multi-database configuration
- [x] Replace deprecated SQLite store with optimized GORM implementation
- [x] Add comprehensive integration tests for MySQL and PostgreSQL
- [x] Update frontend to display blocked packages and storage utilization
- [x] Add goreleaser configuration for migrate binary and container image
- [x] Update configuration examples with database backend options and recommendations
2026-01-03 20:44:23 +00:00

142 lines
5.0 KiB
Vue

<template>
<div class="flex items-center gap-2">
<!-- Blocked Icon (if package exceeds thresholds) -->
<span
v-if="isBlocked"
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-red-600 text-white border border-red-700"
title="Download blocked - exceeds vulnerability thresholds"
>
<i class="fas fa-hand mr-1"></i>
BLOCKED
</span>
<!-- Critical Vulnerabilities -->
<button
v-if="counts.critical > 0"
@click="handleClick('critical')"
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-red-100 text-red-800 hover:bg-red-200 border border-red-300 transition-colors cursor-pointer"
:title="`${counts.critical} critical vulnerabilities - click for details`"
>
<i class="fas fa-shield-virus mr-1"></i>
CRITICAL: {{ counts.critical }}
</button>
<!-- High Vulnerabilities -->
<button
v-if="counts.high > 0"
@click="handleClick('high')"
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-orange-100 text-orange-800 hover:bg-orange-200 border border-orange-300 transition-colors cursor-pointer"
:title="`${counts.high} high severity vulnerabilities - click for details`"
>
<i class="fas fa-exclamation-triangle mr-1"></i>
HIGH: {{ counts.high }}
</button>
<!-- Moderate Vulnerabilities -->
<button
v-if="counts.moderate > 0"
@click="handleClick('moderate')"
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-yellow-100 text-yellow-800 hover:bg-yellow-200 border border-yellow-300 transition-colors cursor-pointer"
:title="`${counts.moderate} moderate severity vulnerabilities - click for details`"
>
<i class="fas fa-exclamation-circle mr-1"></i>
MODERATE: {{ counts.moderate }}
</button>
<!-- Low Vulnerabilities -->
<button
v-if="counts.low > 0"
@click="handleClick('low')"
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-800 hover:bg-blue-200 border border-blue-300 transition-colors cursor-pointer"
:title="`${counts.low} low severity vulnerabilities - click for details`"
>
<i class="fas fa-info-circle mr-1"></i>
LOW: {{ counts.low }}
</button>
<!-- Clean Badge (no vulnerabilities) -->
<span
v-if="status === 'clean' && total === 0"
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-green-100 text-green-800 border border-green-300"
:title="scannedAt ? `No vulnerabilities found - Scanned ${formatTimestamp(scannedAt)}` : 'No vulnerabilities found'"
>
<i class="fas fa-check-circle mr-1"></i>
CLEAN
<span v-if="scannedAt" class="ml-1 text-[10px] opacity-70">
({{ formatRelativeTime(scannedAt) }})
</span>
</span>
<!-- Pending Badge -->
<span
v-if="status === 'pending'"
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-gray-100 text-gray-700 border border-gray-300"
title="Security scan in progress"
>
<i class="fas fa-spinner fa-spin mr-1"></i>
SCANNING...
</span>
<!-- Not Scanned Badge -->
<span
v-if="status === 'not_scanned' || !scanned"
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-gray-100 text-gray-600 border border-gray-300"
title="Not yet scanned for vulnerabilities"
>
<i class="fas fa-question-circle mr-1"></i>
NOT SCANNED
</span>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { VulnerabilityCounts } from '../stores/packages'
interface Props {
scanned?: boolean
status?: 'clean' | 'vulnerable' | 'pending' | 'not_scanned'
counts?: VulnerabilityCounts
total?: number
scannedAt?: string // ISO 8601 timestamp
isBlocked?: boolean // Whether download is blocked due to vulnerabilities
}
const props = withDefaults(defineProps<Props>(), {
scanned: false,
status: 'not_scanned',
total: 0,
isBlocked: false,
})
const emit = defineEmits<{
click: [severity: string]
}>()
const counts = computed(() => props.counts || { critical: 0, high: 0, moderate: 0, low: 0 })
function handleClick(severity: string) {
emit('click', severity)
}
function formatTimestamp(timestamp: string): string {
const date = new Date(timestamp)
return date.toLocaleString()
}
function formatRelativeTime(timestamp: string): string {
const date = new Date(timestamp)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
if (diffMins < 1) return 'just now'
if (diffMins < 60) return `${diffMins}m ago`
if (diffHours < 24) return `${diffHours}h ago`
if (diffDays < 7) return `${diffDays}d ago`
return formatTimestamp(timestamp).split(',')[0] // Just the date part
}
</script>