diff --git a/frontend/gohoarder.pid b/frontend/gohoarder.pid
new file mode 100644
index 0000000..e59d680
--- /dev/null
+++ b/frontend/gohoarder.pid
@@ -0,0 +1 @@
+12921
diff --git a/frontend/package.json b/frontend/package.json
index 69746d8..f033f24 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,6 +16,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.562.0",
+ "marked": "^17.0.1",
"pinia": "^3.0.4",
"reka-ui": "^2.7.0",
"tailwind-merge": "^3.4.0",
@@ -25,6 +26,7 @@
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^7.1.0",
+ "@tailwindcss/typography": "^0.5.19",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/vue": "^8.1.0",
"@vitejs/plugin-vue": "^6.0.3",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 1bcca4a..14e1eef 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -23,6 +23,9 @@ importers:
lucide-vue-next:
specifier: ^0.562.0
version: 0.562.0(vue@3.5.26(typescript@5.9.3))
+ marked:
+ specifier: ^17.0.1
+ version: 17.0.1
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
@@ -45,6 +48,9 @@ importers:
'@fortawesome/fontawesome-free':
specifier: ^7.1.0
version: 7.1.0
+ '@tailwindcss/typography':
+ specifier: ^0.5.19
+ version: 0.5.19(tailwindcss@3.4.19)
'@testing-library/jest-dom':
specifier: ^6.9.1
version: 6.9.1
@@ -420,56 +426,67 @@ packages:
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.54.0':
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.54.0':
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.54.0':
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.54.0':
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.54.0':
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.54.0':
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.54.0':
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-openharmony-arm64@4.54.0':
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
@@ -502,6 +519,11 @@ packages:
'@swc/helpers@0.5.18':
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
+ '@tailwindcss/typography@0.5.19':
+ resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
+ peerDependencies:
+ tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
+
'@tanstack/virtual-core@3.13.14':
resolution: {integrity: sha512-b5Uvd8J2dc7ICeX9SRb/wkCxWk7pUwN214eEPAQsqrsktSKTCmyLxOQWSMgogBByXclZeAdgZ3k4o0fIYUIBqQ==}
@@ -1255,24 +1277,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
@@ -1316,6 +1342,11 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+ marked@17.0.1:
+ resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==}
+ engines: {node: '>= 20'}
+ hasBin: true
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -1508,6 +1539,10 @@ packages:
peerDependencies:
postcss: ^8.2.14
+ postcss-selector-parser@6.0.10:
+ resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
+ engines: {node: '>=4'}
+
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
@@ -2235,6 +2270,11 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19)':
+ dependencies:
+ postcss-selector-parser: 6.0.10
+ tailwindcss: 3.4.19
+
'@tanstack/virtual-core@3.13.14': {}
'@tanstack/vue-virtual@3.13.14(vue@3.5.26(typescript@5.9.3))':
@@ -3140,6 +3180,8 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
+ marked@17.0.1: {}
+
math-intrinsics@1.1.0: {}
mdn-data@2.12.2: {}
@@ -3281,6 +3323,11 @@ snapshots:
postcss: 8.5.6
postcss-selector-parser: 6.1.2
+ postcss-selector-parser@6.0.10:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
diff --git a/frontend/src/components/PackageDetails.vue b/frontend/src/components/PackageDetails.vue
new file mode 100644
index 0000000..a56bbbe
--- /dev/null
+++ b/frontend/src/components/PackageDetails.vue
@@ -0,0 +1,420 @@
+
+
+
+
+
+
+
+
+
{{ packageName }}
+
+ {{ registry }}
+ v{{ version }}
+
+
+
+
+
+
+
+
+
Loading vulnerability details...
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+
+ Security Scan Summary
+
+
+
+
+
+
{{ severityCounts.critical }}
+
Critical
+
+
+
{{ severityCounts.high }}
+
High
+
+
+
{{ severityCounts.moderate }}
+
Moderate
+
+
+
{{ severityCounts.low }}
+
Low
+
+
+
+
+
+
+
+ Scanned: {{ formatDate(vulnerabilities.scanned_at) }}
+
+
+
+ Scanner: {{ vulnerabilities.scanner }}
+
+
+
+
+ {{ bypassedCount }} bypassed
+
+
+
+
+
+
+
+
+
+ No Vulnerabilities Found
+ This package is clean and safe to use
+
+
+
+
+
+
+
+
+ Detected Vulnerabilities ({{ vulnerabilityList.length }})
+
+
+
+
+
+
+
+ Severity
+ CVE ID
+ Description
+ Fix Version
+ Details
+
+
+
+
+
+
+
+
+ {{ formatSeverityName(vuln.severity) }}
+
+
+
+
+
+
+ {{ vuln.id }}
+
+
+ {{ vuln.title || vuln.description }}
+
+
+
+
+ v{{ vuln.fixed_in }}
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bypass Active
+
+
+
+ Reason: {{ vuln.bypass.reason }}
+
+
+ By: {{ vuln.bypass.created_by }}
+
+
+ Expires: {{ formatDate(vuln.bypass.expires_at) }}
+
+
+
+
+
+
+
+
+
+ Fix Available: Upgrade to version {{ vuln.fixed_in }} or later
+
+
+
+
+
+
+
+
+ View Full Advisory
+
+
+ (+{{ vuln.references.length - 1 }} more)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/PackageList.vue b/frontend/src/components/PackageList.vue
index 701c262..45c17bc 100644
--- a/frontend/src/components/PackageList.vue
+++ b/frontend/src/components/PackageList.vue
@@ -13,28 +13,28 @@
Filter by registry:
diff --git a/frontend/src/components/VulnerabilityBadge.vue b/frontend/src/components/VulnerabilityBadge.vue
index 7ce0e5c..085d9c8 100644
--- a/frontend/src/components/VulnerabilityBadge.vue
+++ b/frontend/src/components/VulnerabilityBadge.vue
@@ -22,15 +22,15 @@
HIGH: {{ counts.high }}
-
+
@@ -101,7 +101,7 @@ const emit = defineEmits<{
click: [severity: string]
}>()
-const counts = computed(() => props.counts || { critical: 0, high: 0, medium: 0, low: 0 })
+const counts = computed(() => props.counts || { critical: 0, high: 0, moderate: 0, low: 0 })
function handleClick(severity: string) {
emit('click', severity)
diff --git a/frontend/src/components/VulnerabilityDetailsModal.vue b/frontend/src/components/VulnerabilityDetailsModal.vue
deleted file mode 100644
index 36d482a..0000000
--- a/frontend/src/components/VulnerabilityDetailsModal.vue
+++ /dev/null
@@ -1,316 +0,0 @@
-
-
-
-
-
diff --git a/frontend/src/components/ui/table/Table.vue b/frontend/src/components/ui/table/Table.vue
new file mode 100644
index 0000000..efc0e91
--- /dev/null
+++ b/frontend/src/components/ui/table/Table.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/frontend/src/components/ui/table/TableBody.vue b/frontend/src/components/ui/table/TableBody.vue
new file mode 100644
index 0000000..e1d33ec
--- /dev/null
+++ b/frontend/src/components/ui/table/TableBody.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ui/table/TableCell.vue b/frontend/src/components/ui/table/TableCell.vue
new file mode 100644
index 0000000..ea7f34e
--- /dev/null
+++ b/frontend/src/components/ui/table/TableCell.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+ |
+
diff --git a/frontend/src/components/ui/table/TableHead.vue b/frontend/src/components/ui/table/TableHead.vue
new file mode 100644
index 0000000..d3f5ce0
--- /dev/null
+++ b/frontend/src/components/ui/table/TableHead.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+ |
+
diff --git a/frontend/src/components/ui/table/TableHeader.vue b/frontend/src/components/ui/table/TableHeader.vue
new file mode 100644
index 0000000..ff95b56
--- /dev/null
+++ b/frontend/src/components/ui/table/TableHeader.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ui/table/TableRow.vue b/frontend/src/components/ui/table/TableRow.vue
new file mode 100644
index 0000000..7b61bde
--- /dev/null
+++ b/frontend/src/components/ui/table/TableRow.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ui/table/index.ts b/frontend/src/components/ui/table/index.ts
new file mode 100644
index 0000000..79faa02
--- /dev/null
+++ b/frontend/src/components/ui/table/index.ts
@@ -0,0 +1,6 @@
+export { default as Table } from './Table.vue'
+export { default as TableHeader } from './TableHeader.vue'
+export { default as TableBody } from './TableBody.vue'
+export { default as TableRow } from './TableRow.vue'
+export { default as TableHead } from './TableHead.vue'
+export { default as TableCell } from './TableCell.vue'
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 80cecd4..9d2cd4a 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import Dashboard from '../components/Dashboard.vue'
import PackageList from '../components/PackageList.vue'
+import PackageDetails from '../components/PackageDetails.vue'
import Stats from '../components/Stats.vue'
import BypassManagementPanel from '../components/BypassManagementPanel.vue'
@@ -13,9 +14,17 @@ const router = createRouter({
component: Dashboard,
},
{
- path: '/packages',
+ path: '/packages/:registry?',
name: 'packages',
component: PackageList,
+ props: true,
+ },
+ {
+ // Separate route for package details - supports names with slashes (Go packages)
+ path: '/package/:registry/:name+/:version',
+ name: 'package-details',
+ component: PackageDetails,
+ props: true,
},
{
path: '/stats',
diff --git a/frontend/src/stores/packages.ts b/frontend/src/stores/packages.ts
index f1614a5..1020e2a 100644
--- a/frontend/src/stores/packages.ts
+++ b/frontend/src/stores/packages.ts
@@ -5,7 +5,7 @@ import axios from 'axios'
export interface VulnerabilityCounts {
critical: number
high: number
- medium: number
+ moderate: number
low: number
}
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
index 35dc817..82575c4 100644
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -84,5 +84,8 @@ export default {
}
}
},
- plugins: [require("tailwindcss-animate")],
+ plugins: [
+ require("tailwindcss-animate"),
+ require("@tailwindcss/typography"),
+ ],
}
diff --git a/gohoarder.pid b/gohoarder.pid
index 5446ef2..b2fcc59 100644
--- a/gohoarder.pid
+++ b/gohoarder.pid
@@ -1 +1 @@
-29682
+20805
diff --git a/pkg/app/app.go b/pkg/app/app.go
index 82ee9e2..526dc7b 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -148,7 +148,7 @@ func (a *App) initializeComponents() error {
// Initialize rescan worker if enabled
if a.config.Security.Enabled && a.config.Security.RescanInterval > 0 {
log.Info().Dur("interval", a.config.Security.RescanInterval).Msg("Initializing package rescan worker")
- a.rescanWorker = scanner.NewRescanWorker(a.scanManager, a.metadata, a.config.Security.RescanInterval)
+ a.rescanWorker = scanner.NewRescanWorker(a.scanManager, a.metadata, a.storage, a.config.Security.RescanInterval)
}
// Initialize analytics engine
diff --git a/pkg/app/handlers.go b/pkg/app/handlers.go
index 2a692a4..14136b5 100644
--- a/pkg/app/handlers.go
+++ b/pkg/app/handlers.go
@@ -58,8 +58,8 @@ func (a *App) handleListPackages(w http.ResponseWriter, r *http.Request) {
// Filter, clean, and deduplicate packages
seen := make(map[string]*metadata.Package)
for _, pkg := range allPackages {
- // Skip metadata entries
- if pkg.Version == "list" || pkg.Version == "latest" {
+ // Skip metadata entries (npm metadata pages, pypi pages, etc.)
+ if pkg.Version == "list" || pkg.Version == "latest" || pkg.Version == "metadata" || pkg.Version == "page" {
continue
}
@@ -121,7 +121,7 @@ func (a *App) handleListPackages(w http.ResponseWriter, r *http.Request) {
"counts": map[string]int{
"critical": severityCounts["CRITICAL"],
"high": severityCounts["HIGH"],
- "medium": severityCounts["MEDIUM"],
+ "moderate": severityCounts["MODERATE"],
"low": severityCounts["LOW"],
},
"total": scanResult.VulnerabilityCount,
@@ -330,8 +330,8 @@ func (a *App) handleStats(w http.ResponseWriter, r *http.Request) {
registryStats := make(map[string]map[string]interface{})
for _, pkg := range packages {
- // Skip metadata entries
- if pkg.Version == "list" || pkg.Version == "latest" {
+ // Skip metadata entries (npm metadata pages, pypi pages, etc.)
+ if pkg.Version == "list" || pkg.Version == "latest" || pkg.Version == "metadata" || pkg.Version == "page" {
continue
}
totalSize += pkg.Size
diff --git a/pkg/app/handlers_vulnerabilities.go b/pkg/app/handlers_vulnerabilities.go
index 9b2b939..108f914 100644
--- a/pkg/app/handlers_vulnerabilities.go
+++ b/pkg/app/handlers_vulnerabilities.go
@@ -150,10 +150,10 @@ func (a *App) handleVulnerabilities(w http.ResponseWriter, r *http.Request) {
"severity_counts": map[string]int{
"critical": severityCounts["CRITICAL"],
"high": severityCounts["HIGH"],
- "medium": severityCounts["MEDIUM"],
+ "moderate": severityCounts["MODERATE"],
"low": severityCounts["LOW"],
},
- "bypassed_count": len(scanResult.Vulnerabilities) - (severityCounts["CRITICAL"] + severityCounts["HIGH"] + severityCounts["MEDIUM"] + severityCounts["LOW"]),
+ "bypassed_count": len(scanResult.Vulnerabilities) - (severityCounts["CRITICAL"] + severityCounts["HIGH"] + severityCounts["MODERATE"] + severityCounts["LOW"]),
}
errors.WriteJSONSimple(w, http.StatusOK, response)
diff --git a/pkg/metadata/interface.go b/pkg/metadata/interface.go
index 4c9ec11..e2e84cf 100644
--- a/pkg/metadata/interface.go
+++ b/pkg/metadata/interface.go
@@ -2,6 +2,7 @@ package metadata
import (
"context"
+ "strings"
"time"
)
@@ -95,7 +96,7 @@ type ScanResult struct {
// Vulnerability represents a security vulnerability
type Vulnerability struct {
ID string `json:"id"` // CVE-xxx, GHSA-xxx, etc.
- Severity string `json:"severity"` // critical, high, medium, low
+ Severity string `json:"severity"` // critical, high, moderate, low
Title string `json:"title"`
Description string `json:"description"`
References []string `json:"references"`
@@ -103,6 +104,25 @@ type Vulnerability struct {
DetectedBy []string `json:"detected_by,omitempty"` // List of scanners that detected this vulnerability
}
+// NormalizeSeverity normalizes severity names to standard values
+// Ensures consistent naming: CRITICAL, HIGH, MODERATE, LOW
+func NormalizeSeverity(severity string) string {
+ normalized := strings.ToUpper(strings.TrimSpace(severity))
+
+ // Map MEDIUM to MODERATE for consistency
+ if normalized == "MEDIUM" {
+ return "MODERATE"
+ }
+
+ // Ensure we only return valid severity levels
+ switch normalized {
+ case "CRITICAL", "HIGH", "MODERATE", "LOW":
+ return normalized
+ default:
+ return "LOW" // Default unknown severities to LOW
+ }
+}
+
// ScanStatus represents scan result status
type ScanStatus string
diff --git a/pkg/metadata/sqlite/sqlite.go b/pkg/metadata/sqlite/sqlite.go
index 346a9fe..132f3ab 100644
--- a/pkg/metadata/sqlite/sqlite.go
+++ b/pkg/metadata/sqlite/sqlite.go
@@ -449,7 +449,25 @@ func (s *SQLiteStore) SaveScanResult(ctx context.Context, result *metadata.ScanR
// Update package security_scanned flag
updateQuery := `UPDATE packages SET security_scanned = 1 WHERE registry = ? AND name = ? AND version = ?`
- s.db.ExecContext(ctx, updateQuery, result.Registry, result.PackageName, result.PackageVersion)
+ updateResult, err := s.db.ExecContext(ctx, updateQuery, result.Registry, result.PackageName, result.PackageVersion)
+ if err != nil {
+ log.Warn().
+ Err(err).
+ Str("registry", result.Registry).
+ Str("package", result.PackageName).
+ Str("version", result.PackageVersion).
+ Msg("Failed to update security_scanned flag")
+ // Don't return error - scan result is already saved
+ } else {
+ rowsAffected, _ := updateResult.RowsAffected()
+ if rowsAffected == 0 {
+ log.Warn().
+ Str("registry", result.Registry).
+ Str("package", result.PackageName).
+ Str("version", result.PackageVersion).
+ Msg("Package not found when updating security_scanned flag - possibly name mismatch")
+ }
+ }
return nil
}
diff --git a/pkg/scanner/osv/osv.go b/pkg/scanner/osv/osv.go
index 39cf4fc..a300a74 100644
--- a/pkg/scanner/osv/osv.go
+++ b/pkg/scanner/osv/osv.go
@@ -253,32 +253,42 @@ func (s *Scanner) convertOSVResult(osvResp *OSVResponse, registry, packageName,
// determineSeverity extracts severity from OSV vulnerability
func (s *Scanner) determineSeverity(vuln *OSVVulnerability) string {
+ var rawSeverity string
+
// Try to get severity from CVSS
for _, sev := range vuln.Severity {
if sev.Type == "CVSS_V3" || sev.Type == "CVSS_V2" {
// Parse CVSS score to severity
score := sev.Score
if strings.Contains(strings.ToUpper(score), "CRITICAL") {
- return "CRITICAL"
+ rawSeverity = "CRITICAL"
} else if strings.Contains(strings.ToUpper(score), "HIGH") {
- return "HIGH"
- } else if strings.Contains(strings.ToUpper(score), "MEDIUM") {
- return "MEDIUM"
+ rawSeverity = "HIGH"
+ } else if strings.Contains(strings.ToUpper(score), "MEDIUM") || strings.Contains(strings.ToUpper(score), "MODERATE") {
+ rawSeverity = "MODERATE"
} else if strings.Contains(strings.ToUpper(score), "LOW") {
- return "LOW"
+ rawSeverity = "LOW"
+ }
+ if rawSeverity != "" {
+ break
}
}
}
- // Check database_specific for severity
- if vuln.DatabaseSpecific != nil {
+ // Check database_specific for severity if not found in CVSS
+ if rawSeverity == "" && vuln.DatabaseSpecific != nil {
if sev, ok := vuln.DatabaseSpecific["severity"].(string); ok {
- return strings.ToUpper(sev)
+ rawSeverity = sev
}
}
- // Default to MEDIUM if unknown
- return "MEDIUM"
+ // Default to MODERATE if unknown
+ if rawSeverity == "" {
+ rawSeverity = "MODERATE"
+ }
+
+ // Normalize to standard severity values
+ return metadata.NormalizeSeverity(rawSeverity)
}
// findFixedVersion extracts the fixed version from OSV affected ranges
diff --git a/pkg/scanner/rescanner.go b/pkg/scanner/rescanner.go
index 7438c59..316b677 100644
--- a/pkg/scanner/rescanner.go
+++ b/pkg/scanner/rescanner.go
@@ -5,6 +5,7 @@ import (
"time"
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
+ "github.com/lukaszraczylo/gohoarder/pkg/storage"
"github.com/rs/zerolog/log"
)
@@ -12,15 +13,17 @@ import (
type RescanWorker struct {
manager *Manager
metadataStore metadata.MetadataStore
+ storage storage.StorageBackend
interval time.Duration
stopCh chan struct{}
}
// NewRescanWorker creates a new rescan worker
-func NewRescanWorker(manager *Manager, metadataStore metadata.MetadataStore, interval time.Duration) *RescanWorker {
+func NewRescanWorker(manager *Manager, metadataStore metadata.MetadataStore, storageBackend storage.StorageBackend, interval time.Duration) *RescanWorker {
return &RescanWorker{
manager: manager,
metadataStore: metadataStore,
+ storage: storageBackend,
interval: interval,
stopCh: make(chan struct{}),
}
@@ -40,8 +43,12 @@ func (w *RescanWorker) Start(ctx context.Context) {
ticker := time.NewTicker(w.interval)
defer ticker.Stop()
- // Run initial scan immediately
+ // Run initial scan immediately on startup
+ log.Info().Msg("Running initial package scan on startup")
w.rescanPackages(ctx)
+ log.Info().
+ Dur("next_scan", w.interval).
+ Msg("Initial scan complete, next scan scheduled")
for {
select {
@@ -64,7 +71,7 @@ func (w *RescanWorker) Stop() {
// rescanPackages re-scans packages that need updating
func (w *RescanWorker) rescanPackages(ctx context.Context) {
- log.Info().Msg("Starting package rescan cycle")
+ log.Info().Msg("Starting package rescan cycle - checking all packages for scan status")
// Get all packages
packages, err := w.metadataStore.ListPackages(ctx, &metadata.ListOptions{})
@@ -78,6 +85,12 @@ func (w *RescanWorker) rescanPackages(ctx context.Context) {
failed := 0
for _, pkg := range packages {
+ // Skip metadata entries (npm metadata pages, pypi pages, etc.)
+ if pkg.Version == "list" || pkg.Version == "latest" || pkg.Version == "metadata" || pkg.Version == "page" {
+ skipped++
+ continue
+ }
+
// Check if package needs rescanning
needsRescan, err := w.needsRescan(ctx, pkg)
if err != nil {
@@ -95,19 +108,57 @@ func (w *RescanWorker) rescanPackages(ctx context.Context) {
continue
}
- // Rescan the package
- // Note: We need the file path - we'll need to reconstruct it or get it from storage
- // For now, we'll just log and skip actual rescanning
log.Info().
Str("registry", pkg.Registry).
Str("package", pkg.Name).
Str("version", pkg.Version).
Msg("Package needs rescanning")
- // TODO: Implement actual rescanning by:
- // 1. Retrieving package file from storage
- // 2. Scanning it
- // This would require access to storage backend
+ // Get file path from storage using the storage key from the package metadata
+ if pkg.StorageKey == "" {
+ log.Warn().
+ Str("registry", pkg.Registry).
+ Str("package", pkg.Name).
+ Str("version", pkg.Version).
+ Msg("Package has no storage key, skipping rescan")
+ failed++
+ continue
+ }
+
+ filePath, err := w.getPackageFilePath(ctx, pkg.StorageKey)
+ if err != nil {
+ log.Warn().
+ Err(err).
+ Str("registry", pkg.Registry).
+ Str("package", pkg.Name).
+ Str("version", pkg.Version).
+ Str("storage_key", pkg.StorageKey).
+ Msg("Failed to get package file path, skipping rescan")
+ failed++
+ continue
+ }
+
+ if filePath == "" {
+ log.Debug().
+ Str("registry", pkg.Registry).
+ Str("package", pkg.Name).
+ Str("version", pkg.Version).
+ Msg("No local file path available, skipping rescan")
+ skipped++
+ continue
+ }
+
+ // Perform the actual scan
+ if err := w.manager.ScanPackage(ctx, pkg.Registry, pkg.Name, pkg.Version, filePath); err != nil {
+ log.Error().
+ Err(err).
+ Str("registry", pkg.Registry).
+ Str("package", pkg.Name).
+ Str("version", pkg.Version).
+ Msg("Failed to rescan package")
+ failed++
+ continue
+ }
scanned++
}
@@ -126,6 +177,19 @@ func (w *RescanWorker) needsRescan(ctx context.Context, pkg *metadata.Package) (
scanResult, err := w.metadataStore.GetScanResult(ctx, pkg.Registry, pkg.Name, pkg.Version)
if err != nil {
// No scan result - needs scanning
+ log.Debug().
+ Str("package", pkg.Name).
+ Str("version", pkg.Version).
+ Msg("Package has no scan result, needs scanning")
+ return true, nil
+ }
+
+ // If package is not marked as scanned but has scan result, it's a stale state - rescan
+ if !pkg.SecurityScanned {
+ log.Info().
+ Str("package", pkg.Name).
+ Str("version", pkg.Version).
+ Msg("Package has scan result but security_scanned flag is false, needs update")
return true, nil
}
@@ -137,3 +201,14 @@ func (w *RescanWorker) needsRescan(ctx context.Context, pkg *metadata.Package) (
return false, nil
}
+
+// getPackageFilePath retrieves the local file path for a package from storage
+func (w *RescanWorker) getPackageFilePath(ctx context.Context, storageKey string) (string, error) {
+ // Check if storage backend supports local paths
+ if localProvider, ok := w.storage.(storage.LocalPathProvider); ok {
+ return localProvider.GetLocalPath(ctx, storageKey)
+ }
+
+ // If storage doesn't support local paths (S3, SMB), we can't rescan
+ return "", nil
+}
diff --git a/pkg/scanner/scanner.go b/pkg/scanner/scanner.go
index 9f73ac4..0c0b994 100644
--- a/pkg/scanner/scanner.go
+++ b/pkg/scanner/scanner.go
@@ -260,7 +260,8 @@ func (m *Manager) compareSeverity(s1, s2 string) int {
severityOrder := map[string]int{
"CRITICAL": 4,
"HIGH": 3,
- "MEDIUM": 2,
+ "MODERATE": 2,
+ "MEDIUM": 2, // Support both for backwards compatibility
"LOW": 1,
"UNKNOWN": 0,
}
@@ -353,10 +354,11 @@ func (m *Manager) CheckVulnerabilities(ctx context.Context, registry, packageNam
severityCounts["HIGH"], thresholds.High), nil
}
- // Check medium
- if thresholds.Medium >= 0 && severityCounts["MEDIUM"] > thresholds.Medium {
- return true, fmt.Sprintf("Package has %d MEDIUM vulnerabilities (threshold: %d)",
- severityCounts["MEDIUM"], thresholds.Medium), nil
+ // Check moderate (medium)
+ moderateCount := severityCounts["MODERATE"] + severityCounts["MEDIUM"] // Support both for backwards compatibility
+ if thresholds.Medium >= 0 && moderateCount > thresholds.Medium {
+ return true, fmt.Sprintf("Package has %d MODERATE vulnerabilities (threshold: %d)",
+ moderateCount, thresholds.Medium), nil
}
// Check low
@@ -379,9 +381,10 @@ func (m *Manager) CheckVulnerabilities(ctx context.Context, registry, packageNam
if severityCounts["CRITICAL"] > 0 || severityCounts["HIGH"] > 0 {
return true, fmt.Sprintf("Package has HIGH or CRITICAL vulnerabilities"), nil
}
- case "MEDIUM":
- if severityCounts["CRITICAL"] > 0 || severityCounts["HIGH"] > 0 || severityCounts["MEDIUM"] > 0 {
- return true, fmt.Sprintf("Package has MEDIUM, HIGH, or CRITICAL vulnerabilities"), nil
+ case "MODERATE", "MEDIUM":
+ moderateCount := severityCounts["MODERATE"] + severityCounts["MEDIUM"]
+ if severityCounts["CRITICAL"] > 0 || severityCounts["HIGH"] > 0 || moderateCount > 0 {
+ return true, fmt.Sprintf("Package has MODERATE, HIGH, or CRITICAL vulnerabilities"), nil
}
case "LOW":
if len(result.Vulnerabilities) > 0 {
diff --git a/pkg/scanner/trivy/trivy.go b/pkg/scanner/trivy/trivy.go
index 60d12e4..47c56e0 100644
--- a/pkg/scanner/trivy/trivy.go
+++ b/pkg/scanner/trivy/trivy.go
@@ -187,13 +187,16 @@ func (s *Scanner) convertTrivyResult(trivyResult *TrivyResult, registry, package
// Aggregate all vulnerabilities from all results
for _, result := range trivyResult.Results {
for _, vuln := range result.Vulnerabilities {
+ // Normalize severity to standard values (CRITICAL, HIGH, MODERATE, LOW)
+ normalizedSeverity := metadata.NormalizeSeverity(vuln.Severity)
+
// Count by severity
- severityCounts[strings.ToUpper(vuln.Severity)]++
+ severityCounts[normalizedSeverity]++
// Add to vulnerabilities list
vulnerabilities = append(vulnerabilities, metadata.Vulnerability{
ID: vuln.VulnerabilityID,
- Severity: strings.ToUpper(vuln.Severity),
+ Severity: normalizedSeverity,
Title: vuln.Title,
Description: vuln.Description,
References: vuln.References,