From 1cbf6c5d9ede2a013f4b9d28d5c3ae63e68d5791 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Fri, 2 Jan 2026 11:49:08 +0000 Subject: [PATCH] fixes --- frontend/gohoarder.pid | 1 + frontend/package.json | 2 + frontend/pnpm-lock.yaml | 47 ++ frontend/src/components/PackageDetails.vue | 420 ++++++++++++++++++ frontend/src/components/PackageList.vue | 50 ++- .../src/components/VulnerabilityBadge.vue | 12 +- .../components/VulnerabilityDetailsModal.vue | 316 ------------- frontend/src/components/ui/table/Table.vue | 15 + .../src/components/ui/table/TableBody.vue | 13 + .../src/components/ui/table/TableCell.vue | 13 + .../src/components/ui/table/TableHead.vue | 13 + .../src/components/ui/table/TableHeader.vue | 13 + frontend/src/components/ui/table/TableRow.vue | 13 + frontend/src/components/ui/table/index.ts | 6 + frontend/src/router/index.ts | 11 +- frontend/src/stores/packages.ts | 2 +- frontend/tailwind.config.js | 5 +- gohoarder.pid | 2 +- pkg/app/app.go | 2 +- pkg/app/handlers.go | 10 +- pkg/app/handlers_vulnerabilities.go | 4 +- pkg/metadata/interface.go | 22 +- pkg/metadata/sqlite/sqlite.go | 20 +- pkg/scanner/osv/osv.go | 30 +- pkg/scanner/rescanner.go | 95 +++- pkg/scanner/scanner.go | 19 +- pkg/scanner/trivy/trivy.go | 7 +- 27 files changed, 779 insertions(+), 384 deletions(-) create mode 100644 frontend/gohoarder.pid create mode 100644 frontend/src/components/PackageDetails.vue delete mode 100644 frontend/src/components/VulnerabilityDetailsModal.vue create mode 100644 frontend/src/components/ui/table/Table.vue create mode 100644 frontend/src/components/ui/table/TableBody.vue create mode 100644 frontend/src/components/ui/table/TableCell.vue create mode 100644 frontend/src/components/ui/table/TableHead.vue create mode 100644 frontend/src/components/ui/table/TableHeader.vue create mode 100644 frontend/src/components/ui/table/TableRow.vue create mode 100644 frontend/src/components/ui/table/index.ts 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 @@ + + + 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,