mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-07-08 04:24:27 +00:00
fixes
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
12921
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-vue-next": "^0.562.0",
|
"lucide-vue-next": "^0.562.0",
|
||||||
|
"marked": "^17.0.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"reka-ui": "^2.7.0",
|
"reka-ui": "^2.7.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/vue": "^8.1.0",
|
"@testing-library/vue": "^8.1.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
|
|||||||
Generated
+47
@@ -23,6 +23,9 @@ importers:
|
|||||||
lucide-vue-next:
|
lucide-vue-next:
|
||||||
specifier: ^0.562.0
|
specifier: ^0.562.0
|
||||||
version: 0.562.0(vue@3.5.26(typescript@5.9.3))
|
version: 0.562.0(vue@3.5.26(typescript@5.9.3))
|
||||||
|
marked:
|
||||||
|
specifier: ^17.0.1
|
||||||
|
version: 17.0.1
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
|
version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
|
||||||
@@ -45,6 +48,9 @@ importers:
|
|||||||
'@fortawesome/fontawesome-free':
|
'@fortawesome/fontawesome-free':
|
||||||
specifier: ^7.1.0
|
specifier: ^7.1.0
|
||||||
version: 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':
|
'@testing-library/jest-dom':
|
||||||
specifier: ^6.9.1
|
specifier: ^6.9.1
|
||||||
version: 6.9.1
|
version: 6.9.1
|
||||||
@@ -420,56 +426,67 @@ packages:
|
|||||||
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
|
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
|
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
|
||||||
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
|
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.54.0':
|
'@rollup/rollup-linux-arm64-gnu@4.54.0':
|
||||||
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
|
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.54.0':
|
'@rollup/rollup-linux-arm64-musl@4.54.0':
|
||||||
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
|
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-gnu@4.54.0':
|
'@rollup/rollup-linux-loong64-gnu@4.54.0':
|
||||||
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
|
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
|
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
|
||||||
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
|
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
|
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
|
||||||
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
|
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.54.0':
|
'@rollup/rollup-linux-riscv64-musl@4.54.0':
|
||||||
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
|
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.54.0':
|
'@rollup/rollup-linux-s390x-gnu@4.54.0':
|
||||||
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
|
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.54.0':
|
'@rollup/rollup-linux-x64-gnu@4.54.0':
|
||||||
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
|
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.54.0':
|
'@rollup/rollup-linux-x64-musl@4.54.0':
|
||||||
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
|
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-openharmony-arm64@4.54.0':
|
'@rollup/rollup-openharmony-arm64@4.54.0':
|
||||||
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
|
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
|
||||||
@@ -502,6 +519,11 @@ packages:
|
|||||||
'@swc/helpers@0.5.18':
|
'@swc/helpers@0.5.18':
|
||||||
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
|
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':
|
'@tanstack/virtual-core@3.13.14':
|
||||||
resolution: {integrity: sha512-b5Uvd8J2dc7ICeX9SRb/wkCxWk7pUwN214eEPAQsqrsktSKTCmyLxOQWSMgogBByXclZeAdgZ3k4o0fIYUIBqQ==}
|
resolution: {integrity: sha512-b5Uvd8J2dc7ICeX9SRb/wkCxWk7pUwN214eEPAQsqrsktSKTCmyLxOQWSMgogBByXclZeAdgZ3k4o0fIYUIBqQ==}
|
||||||
|
|
||||||
@@ -1255,24 +1277,28 @@ packages:
|
|||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.30.2:
|
lightningcss-linux-arm64-musl@1.30.2:
|
||||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.30.2:
|
lightningcss-linux-x64-gnu@1.30.2:
|
||||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.30.2:
|
lightningcss-linux-x64-musl@1.30.2:
|
||||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.30.2:
|
lightningcss-win32-arm64-msvc@1.30.2:
|
||||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||||
@@ -1316,6 +1342,11 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
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:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1508,6 +1539,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
postcss: ^8.2.14
|
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:
|
postcss-selector-parser@6.1.2:
|
||||||
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
|
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -2235,6 +2270,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
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/virtual-core@3.13.14': {}
|
||||||
|
|
||||||
'@tanstack/vue-virtual@3.13.14(vue@3.5.26(typescript@5.9.3))':
|
'@tanstack/vue-virtual@3.13.14(vue@3.5.26(typescript@5.9.3))':
|
||||||
@@ -3140,6 +3180,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
marked@17.0.1: {}
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
mdn-data@2.12.2: {}
|
mdn-data@2.12.2: {}
|
||||||
@@ -3281,6 +3323,11 @@ snapshots:
|
|||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-selector-parser: 6.1.2
|
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:
|
postcss-selector-parser@6.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
cssesc: 3.0.0
|
cssesc: 3.0.0
|
||||||
|
|||||||
@@ -0,0 +1,420 @@
|
|||||||
|
<template>
|
||||||
|
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||||
|
<!-- Header with Back Button -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<Button @click="goBack" variant="ghost" class="mb-4">
|
||||||
|
<i class="fas fa-arrow-left mr-2"></i>
|
||||||
|
Back to Packages
|
||||||
|
</Button>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<i class="fas fa-box text-3xl text-primary-600"></i>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">{{ packageName }}</h1>
|
||||||
|
<div class="flex items-center gap-2 mt-2">
|
||||||
|
<Badge :class="getRegistryBadgeClass(registry)">{{ registry }}</Badge>
|
||||||
|
<Badge variant="outline" class="font-mono">v{{ version }}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="text-center py-12">
|
||||||
|
<i class="fas fa-spinner fa-spin text-4xl text-primary-600"></i>
|
||||||
|
<p class="mt-4 text-gray-600">Loading vulnerability details...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<Alert v-else-if="error" variant="destructive" class="mb-4">
|
||||||
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||||
|
<AlertDescription>{{ error }}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<!-- Vulnerability Details -->
|
||||||
|
<div v-else-if="vulnerabilities" class="space-y-6">
|
||||||
|
<!-- Summary Card -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="flex items-center gap-2">
|
||||||
|
<i class="fas fa-shield-virus text-red-600"></i>
|
||||||
|
Security Scan Summary
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||||
|
<div class="text-center p-4 bg-red-50 rounded-lg border border-red-200">
|
||||||
|
<p class="text-3xl font-bold text-red-600">{{ severityCounts.critical }}</p>
|
||||||
|
<p class="text-sm text-gray-600 mt-1">Critical</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-orange-50 rounded-lg border border-orange-200">
|
||||||
|
<p class="text-3xl font-bold text-orange-600">{{ severityCounts.high }}</p>
|
||||||
|
<p class="text-sm text-gray-600 mt-1">High</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-yellow-50 rounded-lg border border-yellow-200">
|
||||||
|
<p class="text-3xl font-bold text-yellow-600">{{ severityCounts.moderate }}</p>
|
||||||
|
<p class="text-sm text-gray-600 mt-1">Moderate</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||||
|
<p class="text-3xl font-bold text-blue-600">{{ severityCounts.low }}</p>
|
||||||
|
<p class="text-sm text-gray-600 mt-1">Low</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator class="my-4" />
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-gray-600">
|
||||||
|
<i class="fas fa-search mr-1"></i>
|
||||||
|
Scanned: {{ formatDate(vulnerabilities.scanned_at) }}
|
||||||
|
</span>
|
||||||
|
<span class="text-gray-600">
|
||||||
|
<i class="fas fa-cog mr-1"></i>
|
||||||
|
Scanner: {{ vulnerabilities.scanner }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="bypassedCount > 0" class="flex items-center gap-2 text-green-600">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<span>{{ bypassedCount }} bypassed</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- No Vulnerabilities -->
|
||||||
|
<Card v-if="vulnerabilityList.length === 0">
|
||||||
|
<CardContent class="text-center py-12">
|
||||||
|
<i class="fas fa-check-circle text-6xl text-green-500 mb-4"></i>
|
||||||
|
<p class="text-xl font-semibold text-gray-900">No Vulnerabilities Found</p>
|
||||||
|
<p class="mt-2 text-gray-600">This package is clean and safe to use</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Vulnerability List -->
|
||||||
|
<Card v-else>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="flex items-center gap-2">
|
||||||
|
<i class="fas fa-list text-gray-600"></i>
|
||||||
|
Detected Vulnerabilities ({{ vulnerabilityList.length }})
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="p-0">
|
||||||
|
<div class="border-t">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow class="bg-gray-50 hover:bg-gray-50">
|
||||||
|
<TableHead class="w-[100px]">Severity</TableHead>
|
||||||
|
<TableHead class="w-[180px]">CVE ID</TableHead>
|
||||||
|
<TableHead>Description</TableHead>
|
||||||
|
<TableHead class="w-[120px]">Fix Version</TableHead>
|
||||||
|
<TableHead class="w-[100px] text-center">Details</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<template v-for="(vuln, index) in vulnerabilityList" :key="vuln.id">
|
||||||
|
<!-- Main Row -->
|
||||||
|
<TableRow
|
||||||
|
:class="getRowClass(index, vuln.severity)"
|
||||||
|
@click="toggleRow(index)"
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<Badge :class="getSeverityBadgeClass(vuln.severity)">
|
||||||
|
{{ formatSeverityName(vuln.severity) }}
|
||||||
|
</Badge>
|
||||||
|
<span
|
||||||
|
v-if="vuln.bypassed"
|
||||||
|
class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800"
|
||||||
|
title="Bypassed"
|
||||||
|
>
|
||||||
|
<i class="fas fa-unlock text-xs"></i>
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span class="font-mono text-sm font-medium">{{ vuln.id }}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<p class="text-sm text-gray-900 line-clamp-2">{{ vuln.title || vuln.description }}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span v-if="vuln.fixed_in" class="inline-flex items-center text-sm text-green-700">
|
||||||
|
<i class="fas fa-arrow-up text-xs mr-1"></i>
|
||||||
|
v{{ vuln.fixed_in }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-sm text-gray-400">-</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="text-center">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 w-8 p-0"
|
||||||
|
@click.stop="toggleRow(index)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="[
|
||||||
|
'fas transition-transform',
|
||||||
|
expandedRows.has(index) ? 'fa-chevron-up' : 'fa-chevron-down'
|
||||||
|
]"
|
||||||
|
></i>
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
<!-- Expanded Details Row -->
|
||||||
|
<TableRow v-if="expandedRows.has(index)" class="bg-gray-50 hover:bg-gray-50">
|
||||||
|
<TableCell colspan="5" class="p-0">
|
||||||
|
<div class="px-6 py-4 space-y-3">
|
||||||
|
<!-- Full Description -->
|
||||||
|
<div>
|
||||||
|
<h5 class="font-semibold text-gray-900 mb-2">Description</h5>
|
||||||
|
<div
|
||||||
|
class="text-sm text-gray-700 leading-relaxed prose prose-sm max-w-none"
|
||||||
|
v-html="renderMarkdown(vuln.description)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bypass Information -->
|
||||||
|
<div v-if="vuln.bypassed && vuln.bypass" class="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||||
|
<h5 class="font-semibold text-green-900 mb-2 flex items-center gap-2">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Bypass Active
|
||||||
|
</h5>
|
||||||
|
<div class="grid grid-cols-3 gap-4 text-sm text-green-800">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Reason:</span> {{ vuln.bypass.reason }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">By:</span> {{ vuln.bypass.created_by }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Expires:</span> {{ formatDate(vuln.bypass.expires_at) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fix Information -->
|
||||||
|
<div v-if="vuln.fixed_in" class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i class="fas fa-wrench text-blue-700"></i>
|
||||||
|
<span class="text-sm text-blue-900">
|
||||||
|
<span class="font-semibold">Fix Available:</span> Upgrade to version <strong>{{ vuln.fixed_in }}</strong> or later
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Primary Reference -->
|
||||||
|
<div v-if="vuln.references && vuln.references.length > 0" class="flex items-center gap-2 text-sm">
|
||||||
|
<i class="fas fa-link text-gray-500"></i>
|
||||||
|
<a
|
||||||
|
:href="vuln.references[0]"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-blue-600 hover:text-blue-800 hover:underline font-medium"
|
||||||
|
>
|
||||||
|
View Full Advisory
|
||||||
|
</a>
|
||||||
|
<span v-if="vuln.references.length > 1" class="text-gray-500">
|
||||||
|
(+{{ vuln.references.length - 1 }} more)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table'
|
||||||
|
|
||||||
|
// Configure marked
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: true,
|
||||||
|
gfm: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const registry = computed(() => route.params.registry as string)
|
||||||
|
const packageName = computed(() => {
|
||||||
|
// Handle package names with slashes (e.g., Go packages like github.com/user/repo)
|
||||||
|
const nameParam = route.params.name
|
||||||
|
if (Array.isArray(nameParam)) {
|
||||||
|
return nameParam.join('/')
|
||||||
|
}
|
||||||
|
return nameParam as string
|
||||||
|
})
|
||||||
|
const version = computed(() => route.params.version as string)
|
||||||
|
|
||||||
|
interface BypassInfo {
|
||||||
|
id: string
|
||||||
|
reason: string
|
||||||
|
created_by: string
|
||||||
|
expires_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Vulnerability {
|
||||||
|
id: string
|
||||||
|
severity: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
references: string[]
|
||||||
|
fixed_in: string
|
||||||
|
bypassed: boolean
|
||||||
|
bypass?: BypassInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VulnerabilityResponse {
|
||||||
|
scanned: boolean
|
||||||
|
scanner: string
|
||||||
|
scanned_at: string
|
||||||
|
status: string
|
||||||
|
vulnerabilities: Vulnerability[]
|
||||||
|
vulnerability_count: number
|
||||||
|
severity_counts: {
|
||||||
|
critical: number
|
||||||
|
high: number
|
||||||
|
moderate: number
|
||||||
|
low: number
|
||||||
|
}
|
||||||
|
bypassed_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
const vulnerabilities = ref<VulnerabilityResponse | null>(null)
|
||||||
|
const expandedRows = ref<Set<number>>(new Set())
|
||||||
|
|
||||||
|
// Severity order for sorting (higher values = more severe)
|
||||||
|
const severityOrder: Record<string, number> = {
|
||||||
|
CRITICAL: 4,
|
||||||
|
HIGH: 3,
|
||||||
|
MEDIUM: 2,
|
||||||
|
MODERATE: 2, // Treat MODERATE same as MEDIUM
|
||||||
|
LOW: 1,
|
||||||
|
UNKNOWN: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const vulnerabilityList = computed(() => {
|
||||||
|
const vulns = vulnerabilities.value?.vulnerabilities || []
|
||||||
|
// Sort by severity (most severe first)
|
||||||
|
return [...vulns].sort((a, b) => {
|
||||||
|
const severityA = severityOrder[a.severity.toUpperCase()] || 0
|
||||||
|
const severityB = severityOrder[b.severity.toUpperCase()] || 0
|
||||||
|
return severityB - severityA // Descending order
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const severityCounts = computed(() => vulnerabilities.value?.severity_counts || { critical: 0, high: 0, moderate: 0, low: 0 })
|
||||||
|
const bypassedCount = computed(() => vulnerabilities.value?.bypassed_count || 0)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchVulnerabilities()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchVulnerabilities() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
vulnerabilities.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`/api/packages/${registry.value}/${packageName.value}/${version.value}/vulnerabilities`
|
||||||
|
)
|
||||||
|
// API wraps response in {success: true, data: {...}}
|
||||||
|
vulnerabilities.value = response.data.data
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Failed to fetch vulnerabilities:', err)
|
||||||
|
error.value = err.response?.data?.error?.message || err.message || 'Failed to load vulnerability details'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.push('/packages')
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRow(index: number) {
|
||||||
|
if (expandedRows.value.has(index)) {
|
||||||
|
expandedRows.value.delete(index)
|
||||||
|
} else {
|
||||||
|
expandedRows.value.add(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowClass(index: number, severity: string): string {
|
||||||
|
const classes = ['cursor-pointer']
|
||||||
|
if (expandedRows.value.has(index)) {
|
||||||
|
classes.push('bg-gray-50')
|
||||||
|
}
|
||||||
|
classes.push(getVulnerabilityBorderClass(severity))
|
||||||
|
return classes.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeverityBadgeClass(severity: string): string {
|
||||||
|
const classes: Record<string, string> = {
|
||||||
|
CRITICAL: 'bg-red-600 text-white hover:bg-red-700 border-0',
|
||||||
|
HIGH: 'bg-orange-500 text-white hover:bg-orange-600 border-0',
|
||||||
|
MEDIUM: 'bg-yellow-500 text-white hover:bg-yellow-600 border-0',
|
||||||
|
LOW: 'bg-blue-500 text-white hover:bg-blue-600 border-0',
|
||||||
|
MODERATE: 'bg-yellow-500 text-white hover:bg-yellow-600 border-0',
|
||||||
|
}
|
||||||
|
return classes[severity.toUpperCase()] || 'bg-gray-500 text-white hover:bg-gray-600 border-0'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVulnerabilityBorderClass(severity: string): string {
|
||||||
|
const classes: Record<string, string> = {
|
||||||
|
CRITICAL: 'border-l-4 border-l-red-600',
|
||||||
|
HIGH: 'border-l-4 border-l-orange-500',
|
||||||
|
MEDIUM: 'border-l-4 border-l-yellow-500',
|
||||||
|
MODERATE: 'border-l-4 border-l-yellow-500',
|
||||||
|
LOW: 'border-l-4 border-l-blue-500',
|
||||||
|
}
|
||||||
|
return classes[severity.toUpperCase()] || 'border-l-4 border-l-gray-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRegistryBadgeClass(registry: string): string {
|
||||||
|
const classes: Record<string, string> = {
|
||||||
|
npm: 'bg-red-500 text-white border-0',
|
||||||
|
pypi: 'bg-blue-500 text-white border-0',
|
||||||
|
go: 'bg-cyan-500 text-white border-0',
|
||||||
|
}
|
||||||
|
return classes[registry] || 'bg-gray-500 text-white border-0'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date: string): string {
|
||||||
|
return new Date(date).toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarkdown(text: string): string {
|
||||||
|
if (!text) return ''
|
||||||
|
return marked.parse(text) as string
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSeverityName(severity: string): string {
|
||||||
|
// Convert severity to title case (e.g., "CRITICAL" -> "Critical", "MODERATE" -> "Moderate")
|
||||||
|
const normalized = severity.toUpperCase()
|
||||||
|
return normalized.charAt(0) + normalized.slice(1).toLowerCase()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -13,28 +13,28 @@
|
|||||||
<span class="text-sm font-medium text-gray-700">Filter by registry:</span>
|
<span class="text-sm font-medium text-gray-700">Filter by registry:</span>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
@click="selectedRegistry = 'all'"
|
@click="changeRegistryFilter('all')"
|
||||||
:variant="selectedRegistry === 'all' ? 'default' : 'outline'"
|
:variant="selectedRegistry === 'all' ? 'default' : 'outline'"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
All
|
All
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="selectedRegistry = 'npm'"
|
@click="changeRegistryFilter('npm')"
|
||||||
:variant="selectedRegistry === 'npm' ? 'default' : 'outline'"
|
:variant="selectedRegistry === 'npm' ? 'default' : 'outline'"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<i class="fab fa-npm mr-2"></i>NPM
|
<i class="fab fa-npm mr-2"></i>NPM
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="selectedRegistry = 'pypi'"
|
@click="changeRegistryFilter('pypi')"
|
||||||
:variant="selectedRegistry === 'pypi' ? 'default' : 'outline'"
|
:variant="selectedRegistry === 'pypi' ? 'default' : 'outline'"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<i class="fab fa-python mr-2"></i>PyPI
|
<i class="fab fa-python mr-2"></i>PyPI
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="selectedRegistry = 'go'"
|
@click="changeRegistryFilter('go')"
|
||||||
:variant="selectedRegistry === 'go' ? 'default' : 'outline'"
|
:variant="selectedRegistry === 'go' ? 'default' : 'outline'"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
@@ -230,20 +230,13 @@
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- Vulnerability Details Modal -->
|
|
||||||
<VulnerabilityDetailsModal
|
|
||||||
v-if="selectedPackage"
|
|
||||||
v-model:open="showVulnerabilityModal"
|
|
||||||
:registry="selectedPackage.registry"
|
|
||||||
:package-name="selectedPackage.name"
|
|
||||||
:version="selectedPackage.version"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, computed, watch } from 'vue'
|
import { onMounted, ref, computed, watch } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { usePackageStore, type Package } from '../stores/packages'
|
import { usePackageStore, type Package } from '../stores/packages'
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@@ -265,16 +258,20 @@ import { Card, CardContent } from '@/components/ui/card'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import VulnerabilityBadge from './VulnerabilityBadge.vue'
|
import VulnerabilityBadge from './VulnerabilityBadge.vue'
|
||||||
import VulnerabilityDetailsModal from './VulnerabilityDetailsModal.vue'
|
|
||||||
|
|
||||||
|
// Props from router
|
||||||
|
const props = defineProps<{
|
||||||
|
registry?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const store = usePackageStore()
|
const store = usePackageStore()
|
||||||
const { packages, loading, error } = storeToRefs(store)
|
const { packages, loading, error } = storeToRefs(store)
|
||||||
|
|
||||||
const showDeleteModal = ref(false)
|
const showDeleteModal = ref(false)
|
||||||
const packageToDelete = ref<Package | null>(null)
|
const packageToDelete = ref<Package | null>(null)
|
||||||
const showVulnerabilityModal = ref(false)
|
const selectedRegistry = ref<string>(props.registry || 'all')
|
||||||
const selectedPackage = ref<{ registry: string; name: string; version: string } | null>(null)
|
|
||||||
const selectedRegistry = ref<string>('all')
|
|
||||||
const searchTerm = ref<string>('')
|
const searchTerm = ref<string>('')
|
||||||
const currentPage = ref<number>(1)
|
const currentPage = ref<number>(1)
|
||||||
const itemsPerPage = ref<number>(10)
|
const itemsPerPage = ref<number>(10)
|
||||||
@@ -359,6 +356,11 @@ watch([selectedRegistry, searchTerm], () => {
|
|||||||
resetPagination()
|
resetPagination()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Watch for route parameter changes to update filter
|
||||||
|
watch(() => route.params.registry, (newRegistry) => {
|
||||||
|
selectedRegistry.value = (newRegistry as string) || 'all'
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.fetchPackages()
|
await store.fetchPackages()
|
||||||
})
|
})
|
||||||
@@ -406,7 +408,19 @@ function formatDate(date: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showVulnerabilityDetails(registry: string, name: string, version: string) {
|
function showVulnerabilityDetails(registry: string, name: string, version: string) {
|
||||||
selectedPackage.value = { registry, name, version }
|
// Navigate to the package details page
|
||||||
showVulnerabilityModal.value = true
|
router.push({
|
||||||
|
name: 'package-details',
|
||||||
|
params: {
|
||||||
|
registry,
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeRegistryFilter(registry: string) {
|
||||||
|
const path = registry === 'all' ? '/packages' : `/packages/${registry}`
|
||||||
|
router.push(path)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -22,15 +22,15 @@
|
|||||||
HIGH: {{ counts.high }}
|
HIGH: {{ counts.high }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Medium Vulnerabilities -->
|
<!-- Moderate Vulnerabilities -->
|
||||||
<button
|
<button
|
||||||
v-if="counts.medium > 0"
|
v-if="counts.moderate > 0"
|
||||||
@click="handleClick('medium')"
|
@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"
|
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.medium} medium severity vulnerabilities - click for details`"
|
:title="`${counts.moderate} moderate severity vulnerabilities - click for details`"
|
||||||
>
|
>
|
||||||
<i class="fas fa-exclamation-circle mr-1"></i>
|
<i class="fas fa-exclamation-circle mr-1"></i>
|
||||||
MEDIUM: {{ counts.medium }}
|
MODERATE: {{ counts.moderate }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Low Vulnerabilities -->
|
<!-- Low Vulnerabilities -->
|
||||||
@@ -101,7 +101,7 @@ const emit = defineEmits<{
|
|||||||
click: [severity: string]
|
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) {
|
function handleClick(severity: string) {
|
||||||
emit('click', severity)
|
emit('click', severity)
|
||||||
|
|||||||
@@ -1,316 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Dialog v-model:open="isOpen">
|
|
||||||
<DialogContent class="max-w-4xl max-h-[80vh] overflow-y-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle class="flex items-center gap-2">
|
|
||||||
<i class="fas fa-shield-virus text-red-600"></i>
|
|
||||||
Security Vulnerabilities
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{{ packageInfo.registry }}/{{ packageInfo.name }}@{{ packageInfo.version }}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div v-if="loading" class="text-center py-12">
|
|
||||||
<i class="fas fa-spinner fa-spin text-4xl text-primary-600"></i>
|
|
||||||
<p class="mt-4 text-gray-600">Loading vulnerability details...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error State -->
|
|
||||||
<Alert v-else-if="error" variant="destructive" class="mb-4">
|
|
||||||
<i class="fas fa-exclamation-circle mr-2"></i>
|
|
||||||
<AlertDescription>{{ error }}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<!-- Vulnerability Details -->
|
|
||||||
<div v-else-if="vulnerabilities" class="space-y-4">
|
|
||||||
<!-- Summary Card -->
|
|
||||||
<Card>
|
|
||||||
<CardContent class="p-4">
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-2xl font-bold text-red-600">{{ severityCounts.critical }}</p>
|
|
||||||
<p class="text-sm text-gray-600">Critical</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-2xl font-bold text-orange-600">{{ severityCounts.high }}</p>
|
|
||||||
<p class="text-sm text-gray-600">High</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-2xl font-bold text-yellow-600">{{ severityCounts.medium }}</p>
|
|
||||||
<p class="text-sm text-gray-600">Medium</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-2xl font-bold text-blue-600">{{ severityCounts.low }}</p>
|
|
||||||
<p class="text-sm text-gray-600">Low</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator class="my-3" />
|
|
||||||
<div class="flex items-center justify-between text-sm">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<span class="text-gray-600">
|
|
||||||
<i class="fas fa-search mr-1"></i>
|
|
||||||
Scanned: {{ formatDate(vulnerabilities.scanned_at) }}
|
|
||||||
</span>
|
|
||||||
<span class="text-gray-600">
|
|
||||||
<i class="fas fa-cog mr-1"></i>
|
|
||||||
Scanner: {{ vulnerabilities.scanner }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="bypassedCount > 0" class="flex items-center gap-2 text-green-600">
|
|
||||||
<i class="fas fa-check-circle"></i>
|
|
||||||
<span>{{ bypassedCount }} bypassed</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- No Vulnerabilities -->
|
|
||||||
<div v-if="vulnerabilityList.length === 0" class="text-center py-8 text-gray-500">
|
|
||||||
<i class="fas fa-check-circle text-6xl text-green-500 mb-4"></i>
|
|
||||||
<p class="text-xl font-semibold">No Vulnerabilities Found</p>
|
|
||||||
<p class="mt-2">This package is clean and safe to use</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Vulnerability List -->
|
|
||||||
<div v-else class="space-y-3">
|
|
||||||
<Accordion type="multiple" class="w-full">
|
|
||||||
<AccordionItem
|
|
||||||
v-for="(vuln, index) in vulnerabilityList"
|
|
||||||
:key="vuln.id"
|
|
||||||
:value="`vuln-${index}`"
|
|
||||||
:class="getVulnerabilityBorderClass(vuln.severity)"
|
|
||||||
>
|
|
||||||
<AccordionTrigger class="px-4 py-3 hover:bg-gray-50">
|
|
||||||
<div class="flex items-center justify-between w-full pr-4">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<Badge :class="getSeverityBadgeClass(vuln.severity)">
|
|
||||||
{{ vuln.severity }}
|
|
||||||
</Badge>
|
|
||||||
<div class="text-left">
|
|
||||||
<h4 class="font-semibold text-gray-900">{{ vuln.id }}</h4>
|
|
||||||
<p class="text-sm text-gray-600">{{ vuln.title }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
v-if="vuln.bypassed"
|
|
||||||
class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-800 border border-green-300"
|
|
||||||
title="This vulnerability is bypassed"
|
|
||||||
>
|
|
||||||
<i class="fas fa-unlock mr-1"></i>
|
|
||||||
BYPASSED
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="vuln.fixed_in"
|
|
||||||
class="text-xs text-gray-500"
|
|
||||||
:title="`Fixed in version ${vuln.fixed_in}`"
|
|
||||||
>
|
|
||||||
<i class="fas fa-wrench mr-1"></i>
|
|
||||||
Fix: v{{ vuln.fixed_in }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent class="px-4 pb-4">
|
|
||||||
<div class="space-y-3">
|
|
||||||
<!-- Description -->
|
|
||||||
<div>
|
|
||||||
<h5 class="font-medium text-gray-900 mb-1">Description</h5>
|
|
||||||
<p class="text-sm text-gray-700">{{ vuln.description }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bypass Information -->
|
|
||||||
<div v-if="vuln.bypassed && vuln.bypass" class="bg-green-50 border border-green-200 rounded-lg p-3">
|
|
||||||
<h5 class="font-medium text-green-900 mb-2 flex items-center gap-2">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
Bypass Information
|
|
||||||
</h5>
|
|
||||||
<div class="space-y-1 text-sm text-green-800">
|
|
||||||
<p><strong>Reason:</strong> {{ vuln.bypass.reason }}</p>
|
|
||||||
<p><strong>Created by:</strong> {{ vuln.bypass.created_by }}</p>
|
|
||||||
<p><strong>Expires:</strong> {{ formatDate(vuln.bypass.expires_at) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Fixed In -->
|
|
||||||
<div v-if="vuln.fixed_in">
|
|
||||||
<h5 class="font-medium text-gray-900 mb-1">Fix Available</h5>
|
|
||||||
<p class="text-sm text-gray-700">
|
|
||||||
<i class="fas fa-arrow-up text-green-600 mr-1"></i>
|
|
||||||
Upgrade to version <strong>{{ vuln.fixed_in }}</strong> or later
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- References -->
|
|
||||||
<div v-if="vuln.references && vuln.references.length > 0">
|
|
||||||
<h5 class="font-medium text-gray-900 mb-1">References</h5>
|
|
||||||
<ul class="space-y-1">
|
|
||||||
<li v-for="(ref, i) in vuln.references" :key="i">
|
|
||||||
<a
|
|
||||||
:href="ref"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="text-sm text-blue-600 hover:text-blue-800 hover:underline flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<i class="fas fa-external-link-alt text-xs"></i>
|
|
||||||
{{ ref }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button @click="isOpen = false" variant="outline">
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, watch } from 'vue'
|
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from '@/components/ui/accordion'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
open: boolean
|
|
||||||
registry: string
|
|
||||||
packageName: string
|
|
||||||
version: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:open': [value: boolean]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const isOpen = computed({
|
|
||||||
get: () => props.open,
|
|
||||||
set: (value) => emit('update:open', value),
|
|
||||||
})
|
|
||||||
|
|
||||||
interface BypassInfo {
|
|
||||||
id: string
|
|
||||||
reason: string
|
|
||||||
created_by: string
|
|
||||||
expires_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Vulnerability {
|
|
||||||
id: string
|
|
||||||
severity: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
references: string[]
|
|
||||||
fixed_in: string
|
|
||||||
bypassed: boolean
|
|
||||||
bypass?: BypassInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VulnerabilityResponse {
|
|
||||||
scanned: boolean
|
|
||||||
scanner: string
|
|
||||||
scanned_at: string
|
|
||||||
status: string
|
|
||||||
vulnerabilities: Vulnerability[]
|
|
||||||
vulnerability_count: number
|
|
||||||
severity_counts: {
|
|
||||||
critical: number
|
|
||||||
high: number
|
|
||||||
medium: number
|
|
||||||
low: number
|
|
||||||
}
|
|
||||||
bypassed_count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
const vulnerabilities = ref<VulnerabilityResponse | null>(null)
|
|
||||||
|
|
||||||
const packageInfo = computed(() => ({
|
|
||||||
registry: props.registry,
|
|
||||||
name: props.packageName,
|
|
||||||
version: props.version,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const vulnerabilityList = computed(() => vulnerabilities.value?.vulnerabilities || [])
|
|
||||||
const severityCounts = computed(() => vulnerabilities.value?.severity_counts || { critical: 0, high: 0, medium: 0, low: 0 })
|
|
||||||
const bypassedCount = computed(() => vulnerabilities.value?.bypassed_count || 0)
|
|
||||||
|
|
||||||
// Fetch vulnerabilities when modal opens
|
|
||||||
watch(() => props.open, async (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
await fetchVulnerabilities()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function fetchVulnerabilities() {
|
|
||||||
loading.value = true
|
|
||||||
error.value = null
|
|
||||||
vulnerabilities.value = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`/api/packages/${props.registry}/${props.packageName}/${props.version}/vulnerabilities`
|
|
||||||
)
|
|
||||||
// API wraps response in {success: true, data: {...}}
|
|
||||||
vulnerabilities.value = response.data.data
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Failed to fetch vulnerabilities:', err)
|
|
||||||
error.value = err.response?.data?.error?.message || err.message || 'Failed to load vulnerability details'
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSeverityBadgeClass(severity: string): string {
|
|
||||||
const classes: Record<string, string> = {
|
|
||||||
CRITICAL: 'bg-red-100 text-red-800 border-red-300',
|
|
||||||
HIGH: 'bg-orange-100 text-orange-800 border-orange-300',
|
|
||||||
MEDIUM: 'bg-yellow-100 text-yellow-800 border-yellow-300',
|
|
||||||
LOW: 'bg-blue-100 text-blue-800 border-blue-300',
|
|
||||||
}
|
|
||||||
return classes[severity.toUpperCase()] || 'bg-gray-100 text-gray-800 border-gray-300'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVulnerabilityBorderClass(severity: string): string {
|
|
||||||
const classes: Record<string, string> = {
|
|
||||||
CRITICAL: 'border-l-4 border-l-red-500',
|
|
||||||
HIGH: 'border-l-4 border-l-orange-500',
|
|
||||||
MEDIUM: 'border-l-4 border-l-yellow-500',
|
|
||||||
LOW: 'border-l-4 border-l-blue-500',
|
|
||||||
}
|
|
||||||
return classes[severity.toUpperCase()] || 'border-l-4 border-l-gray-500'
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(date: string): string {
|
|
||||||
return new Date(date).toLocaleString()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative w-full overflow-auto">
|
||||||
|
<table :class="cn('w-full caption-bottom text-sm', props.class)">
|
||||||
|
<slot />
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tbody :class="cn('[&_tr:last-child]:border-0', props.class)">
|
||||||
|
<slot />
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<td :class="cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', props.class)">
|
||||||
|
<slot />
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<th :class="cn('h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0', props.class)">
|
||||||
|
<slot />
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<thead :class="cn('[&_tr]:border-b', props.class)">
|
||||||
|
<slot />
|
||||||
|
</thead>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr :class="cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', props.class)">
|
||||||
|
<slot />
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
@@ -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'
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Dashboard from '../components/Dashboard.vue'
|
import Dashboard from '../components/Dashboard.vue'
|
||||||
import PackageList from '../components/PackageList.vue'
|
import PackageList from '../components/PackageList.vue'
|
||||||
|
import PackageDetails from '../components/PackageDetails.vue'
|
||||||
import Stats from '../components/Stats.vue'
|
import Stats from '../components/Stats.vue'
|
||||||
import BypassManagementPanel from '../components/BypassManagementPanel.vue'
|
import BypassManagementPanel from '../components/BypassManagementPanel.vue'
|
||||||
|
|
||||||
@@ -13,9 +14,17 @@ const router = createRouter({
|
|||||||
component: Dashboard,
|
component: Dashboard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/packages',
|
path: '/packages/:registry?',
|
||||||
name: 'packages',
|
name: 'packages',
|
||||||
component: PackageList,
|
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',
|
path: '/stats',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import axios from 'axios'
|
|||||||
export interface VulnerabilityCounts {
|
export interface VulnerabilityCounts {
|
||||||
critical: number
|
critical: number
|
||||||
high: number
|
high: number
|
||||||
medium: number
|
moderate: number
|
||||||
low: number
|
low: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,5 +84,8 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [
|
||||||
|
require("tailwindcss-animate"),
|
||||||
|
require("@tailwindcss/typography"),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
29682
|
20805
|
||||||
|
|||||||
+1
-1
@@ -148,7 +148,7 @@ func (a *App) initializeComponents() error {
|
|||||||
// Initialize rescan worker if enabled
|
// Initialize rescan worker if enabled
|
||||||
if a.config.Security.Enabled && a.config.Security.RescanInterval > 0 {
|
if a.config.Security.Enabled && a.config.Security.RescanInterval > 0 {
|
||||||
log.Info().Dur("interval", a.config.Security.RescanInterval).Msg("Initializing package rescan worker")
|
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
|
// Initialize analytics engine
|
||||||
|
|||||||
+5
-5
@@ -58,8 +58,8 @@ func (a *App) handleListPackages(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Filter, clean, and deduplicate packages
|
// Filter, clean, and deduplicate packages
|
||||||
seen := make(map[string]*metadata.Package)
|
seen := make(map[string]*metadata.Package)
|
||||||
for _, pkg := range allPackages {
|
for _, pkg := range allPackages {
|
||||||
// Skip metadata entries
|
// Skip metadata entries (npm metadata pages, pypi pages, etc.)
|
||||||
if pkg.Version == "list" || pkg.Version == "latest" {
|
if pkg.Version == "list" || pkg.Version == "latest" || pkg.Version == "metadata" || pkg.Version == "page" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ func (a *App) handleListPackages(w http.ResponseWriter, r *http.Request) {
|
|||||||
"counts": map[string]int{
|
"counts": map[string]int{
|
||||||
"critical": severityCounts["CRITICAL"],
|
"critical": severityCounts["CRITICAL"],
|
||||||
"high": severityCounts["HIGH"],
|
"high": severityCounts["HIGH"],
|
||||||
"medium": severityCounts["MEDIUM"],
|
"moderate": severityCounts["MODERATE"],
|
||||||
"low": severityCounts["LOW"],
|
"low": severityCounts["LOW"],
|
||||||
},
|
},
|
||||||
"total": scanResult.VulnerabilityCount,
|
"total": scanResult.VulnerabilityCount,
|
||||||
@@ -330,8 +330,8 @@ func (a *App) handleStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
registryStats := make(map[string]map[string]interface{})
|
registryStats := make(map[string]map[string]interface{})
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
// Skip metadata entries
|
// Skip metadata entries (npm metadata pages, pypi pages, etc.)
|
||||||
if pkg.Version == "list" || pkg.Version == "latest" {
|
if pkg.Version == "list" || pkg.Version == "latest" || pkg.Version == "metadata" || pkg.Version == "page" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
totalSize += pkg.Size
|
totalSize += pkg.Size
|
||||||
|
|||||||
@@ -150,10 +150,10 @@ func (a *App) handleVulnerabilities(w http.ResponseWriter, r *http.Request) {
|
|||||||
"severity_counts": map[string]int{
|
"severity_counts": map[string]int{
|
||||||
"critical": severityCounts["CRITICAL"],
|
"critical": severityCounts["CRITICAL"],
|
||||||
"high": severityCounts["HIGH"],
|
"high": severityCounts["HIGH"],
|
||||||
"medium": severityCounts["MEDIUM"],
|
"moderate": severityCounts["MODERATE"],
|
||||||
"low": severityCounts["LOW"],
|
"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)
|
errors.WriteJSONSimple(w, http.StatusOK, response)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package metadata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ type ScanResult struct {
|
|||||||
// Vulnerability represents a security vulnerability
|
// Vulnerability represents a security vulnerability
|
||||||
type Vulnerability struct {
|
type Vulnerability struct {
|
||||||
ID string `json:"id"` // CVE-xxx, GHSA-xxx, etc.
|
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"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
References []string `json:"references"`
|
References []string `json:"references"`
|
||||||
@@ -103,6 +104,25 @@ type Vulnerability struct {
|
|||||||
DetectedBy []string `json:"detected_by,omitempty"` // List of scanners that detected this vulnerability
|
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
|
// ScanStatus represents scan result status
|
||||||
type ScanStatus string
|
type ScanStatus string
|
||||||
|
|
||||||
|
|||||||
@@ -449,7 +449,25 @@ func (s *SQLiteStore) SaveScanResult(ctx context.Context, result *metadata.ScanR
|
|||||||
|
|
||||||
// Update package security_scanned flag
|
// Update package security_scanned flag
|
||||||
updateQuery := `UPDATE packages SET security_scanned = 1 WHERE registry = ? AND name = ? AND version = ?`
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-10
@@ -253,32 +253,42 @@ func (s *Scanner) convertOSVResult(osvResp *OSVResponse, registry, packageName,
|
|||||||
|
|
||||||
// determineSeverity extracts severity from OSV vulnerability
|
// determineSeverity extracts severity from OSV vulnerability
|
||||||
func (s *Scanner) determineSeverity(vuln *OSVVulnerability) string {
|
func (s *Scanner) determineSeverity(vuln *OSVVulnerability) string {
|
||||||
|
var rawSeverity string
|
||||||
|
|
||||||
// Try to get severity from CVSS
|
// Try to get severity from CVSS
|
||||||
for _, sev := range vuln.Severity {
|
for _, sev := range vuln.Severity {
|
||||||
if sev.Type == "CVSS_V3" || sev.Type == "CVSS_V2" {
|
if sev.Type == "CVSS_V3" || sev.Type == "CVSS_V2" {
|
||||||
// Parse CVSS score to severity
|
// Parse CVSS score to severity
|
||||||
score := sev.Score
|
score := sev.Score
|
||||||
if strings.Contains(strings.ToUpper(score), "CRITICAL") {
|
if strings.Contains(strings.ToUpper(score), "CRITICAL") {
|
||||||
return "CRITICAL"
|
rawSeverity = "CRITICAL"
|
||||||
} else if strings.Contains(strings.ToUpper(score), "HIGH") {
|
} else if strings.Contains(strings.ToUpper(score), "HIGH") {
|
||||||
return "HIGH"
|
rawSeverity = "HIGH"
|
||||||
} else if strings.Contains(strings.ToUpper(score), "MEDIUM") {
|
} else if strings.Contains(strings.ToUpper(score), "MEDIUM") || strings.Contains(strings.ToUpper(score), "MODERATE") {
|
||||||
return "MEDIUM"
|
rawSeverity = "MODERATE"
|
||||||
} else if strings.Contains(strings.ToUpper(score), "LOW") {
|
} else if strings.Contains(strings.ToUpper(score), "LOW") {
|
||||||
return "LOW"
|
rawSeverity = "LOW"
|
||||||
|
}
|
||||||
|
if rawSeverity != "" {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check database_specific for severity
|
// Check database_specific for severity if not found in CVSS
|
||||||
if vuln.DatabaseSpecific != nil {
|
if rawSeverity == "" && vuln.DatabaseSpecific != nil {
|
||||||
if sev, ok := vuln.DatabaseSpecific["severity"].(string); ok {
|
if sev, ok := vuln.DatabaseSpecific["severity"].(string); ok {
|
||||||
return strings.ToUpper(sev)
|
rawSeverity = sev
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to MEDIUM if unknown
|
// Default to MODERATE if unknown
|
||||||
return "MEDIUM"
|
if rawSeverity == "" {
|
||||||
|
rawSeverity = "MODERATE"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize to standard severity values
|
||||||
|
return metadata.NormalizeSeverity(rawSeverity)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findFixedVersion extracts the fixed version from OSV affected ranges
|
// findFixedVersion extracts the fixed version from OSV affected ranges
|
||||||
|
|||||||
+85
-10
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
|
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
|
||||||
|
"github.com/lukaszraczylo/gohoarder/pkg/storage"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,15 +13,17 @@ import (
|
|||||||
type RescanWorker struct {
|
type RescanWorker struct {
|
||||||
manager *Manager
|
manager *Manager
|
||||||
metadataStore metadata.MetadataStore
|
metadataStore metadata.MetadataStore
|
||||||
|
storage storage.StorageBackend
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRescanWorker creates a new rescan worker
|
// 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{
|
return &RescanWorker{
|
||||||
manager: manager,
|
manager: manager,
|
||||||
metadataStore: metadataStore,
|
metadataStore: metadataStore,
|
||||||
|
storage: storageBackend,
|
||||||
interval: interval,
|
interval: interval,
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
}
|
}
|
||||||
@@ -40,8 +43,12 @@ func (w *RescanWorker) Start(ctx context.Context) {
|
|||||||
ticker := time.NewTicker(w.interval)
|
ticker := time.NewTicker(w.interval)
|
||||||
defer ticker.Stop()
|
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)
|
w.rescanPackages(ctx)
|
||||||
|
log.Info().
|
||||||
|
Dur("next_scan", w.interval).
|
||||||
|
Msg("Initial scan complete, next scan scheduled")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -64,7 +71,7 @@ func (w *RescanWorker) Stop() {
|
|||||||
|
|
||||||
// rescanPackages re-scans packages that need updating
|
// rescanPackages re-scans packages that need updating
|
||||||
func (w *RescanWorker) rescanPackages(ctx context.Context) {
|
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
|
// Get all packages
|
||||||
packages, err := w.metadataStore.ListPackages(ctx, &metadata.ListOptions{})
|
packages, err := w.metadataStore.ListPackages(ctx, &metadata.ListOptions{})
|
||||||
@@ -78,6 +85,12 @@ func (w *RescanWorker) rescanPackages(ctx context.Context) {
|
|||||||
failed := 0
|
failed := 0
|
||||||
|
|
||||||
for _, pkg := range packages {
|
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
|
// Check if package needs rescanning
|
||||||
needsRescan, err := w.needsRescan(ctx, pkg)
|
needsRescan, err := w.needsRescan(ctx, pkg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -95,19 +108,57 @@ func (w *RescanWorker) rescanPackages(ctx context.Context) {
|
|||||||
continue
|
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().
|
log.Info().
|
||||||
Str("registry", pkg.Registry).
|
Str("registry", pkg.Registry).
|
||||||
Str("package", pkg.Name).
|
Str("package", pkg.Name).
|
||||||
Str("version", pkg.Version).
|
Str("version", pkg.Version).
|
||||||
Msg("Package needs rescanning")
|
Msg("Package needs rescanning")
|
||||||
|
|
||||||
// TODO: Implement actual rescanning by:
|
// Get file path from storage using the storage key from the package metadata
|
||||||
// 1. Retrieving package file from storage
|
if pkg.StorageKey == "" {
|
||||||
// 2. Scanning it
|
log.Warn().
|
||||||
// This would require access to storage backend
|
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++
|
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)
|
scanResult, err := w.metadataStore.GetScanResult(ctx, pkg.Registry, pkg.Name, pkg.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// No scan result - needs scanning
|
// 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
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,3 +201,14 @@ func (w *RescanWorker) needsRescan(ctx context.Context, pkg *metadata.Package) (
|
|||||||
|
|
||||||
return false, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
+11
-8
@@ -260,7 +260,8 @@ func (m *Manager) compareSeverity(s1, s2 string) int {
|
|||||||
severityOrder := map[string]int{
|
severityOrder := map[string]int{
|
||||||
"CRITICAL": 4,
|
"CRITICAL": 4,
|
||||||
"HIGH": 3,
|
"HIGH": 3,
|
||||||
"MEDIUM": 2,
|
"MODERATE": 2,
|
||||||
|
"MEDIUM": 2, // Support both for backwards compatibility
|
||||||
"LOW": 1,
|
"LOW": 1,
|
||||||
"UNKNOWN": 0,
|
"UNKNOWN": 0,
|
||||||
}
|
}
|
||||||
@@ -353,10 +354,11 @@ func (m *Manager) CheckVulnerabilities(ctx context.Context, registry, packageNam
|
|||||||
severityCounts["HIGH"], thresholds.High), nil
|
severityCounts["HIGH"], thresholds.High), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check medium
|
// Check moderate (medium)
|
||||||
if thresholds.Medium >= 0 && severityCounts["MEDIUM"] > thresholds.Medium {
|
moderateCount := severityCounts["MODERATE"] + severityCounts["MEDIUM"] // Support both for backwards compatibility
|
||||||
return true, fmt.Sprintf("Package has %d MEDIUM vulnerabilities (threshold: %d)",
|
if thresholds.Medium >= 0 && moderateCount > thresholds.Medium {
|
||||||
severityCounts["MEDIUM"], thresholds.Medium), nil
|
return true, fmt.Sprintf("Package has %d MODERATE vulnerabilities (threshold: %d)",
|
||||||
|
moderateCount, thresholds.Medium), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check low
|
// Check low
|
||||||
@@ -379,9 +381,10 @@ func (m *Manager) CheckVulnerabilities(ctx context.Context, registry, packageNam
|
|||||||
if severityCounts["CRITICAL"] > 0 || severityCounts["HIGH"] > 0 {
|
if severityCounts["CRITICAL"] > 0 || severityCounts["HIGH"] > 0 {
|
||||||
return true, fmt.Sprintf("Package has HIGH or CRITICAL vulnerabilities"), nil
|
return true, fmt.Sprintf("Package has HIGH or CRITICAL vulnerabilities"), nil
|
||||||
}
|
}
|
||||||
case "MEDIUM":
|
case "MODERATE", "MEDIUM":
|
||||||
if severityCounts["CRITICAL"] > 0 || severityCounts["HIGH"] > 0 || severityCounts["MEDIUM"] > 0 {
|
moderateCount := severityCounts["MODERATE"] + severityCounts["MEDIUM"]
|
||||||
return true, fmt.Sprintf("Package has MEDIUM, HIGH, or CRITICAL vulnerabilities"), nil
|
if severityCounts["CRITICAL"] > 0 || severityCounts["HIGH"] > 0 || moderateCount > 0 {
|
||||||
|
return true, fmt.Sprintf("Package has MODERATE, HIGH, or CRITICAL vulnerabilities"), nil
|
||||||
}
|
}
|
||||||
case "LOW":
|
case "LOW":
|
||||||
if len(result.Vulnerabilities) > 0 {
|
if len(result.Vulnerabilities) > 0 {
|
||||||
|
|||||||
@@ -187,13 +187,16 @@ func (s *Scanner) convertTrivyResult(trivyResult *TrivyResult, registry, package
|
|||||||
// Aggregate all vulnerabilities from all results
|
// Aggregate all vulnerabilities from all results
|
||||||
for _, result := range trivyResult.Results {
|
for _, result := range trivyResult.Results {
|
||||||
for _, vuln := range result.Vulnerabilities {
|
for _, vuln := range result.Vulnerabilities {
|
||||||
|
// Normalize severity to standard values (CRITICAL, HIGH, MODERATE, LOW)
|
||||||
|
normalizedSeverity := metadata.NormalizeSeverity(vuln.Severity)
|
||||||
|
|
||||||
// Count by severity
|
// Count by severity
|
||||||
severityCounts[strings.ToUpper(vuln.Severity)]++
|
severityCounts[normalizedSeverity]++
|
||||||
|
|
||||||
// Add to vulnerabilities list
|
// Add to vulnerabilities list
|
||||||
vulnerabilities = append(vulnerabilities, metadata.Vulnerability{
|
vulnerabilities = append(vulnerabilities, metadata.Vulnerability{
|
||||||
ID: vuln.VulnerabilityID,
|
ID: vuln.VulnerabilityID,
|
||||||
Severity: strings.ToUpper(vuln.Severity),
|
Severity: normalizedSeverity,
|
||||||
Title: vuln.Title,
|
Title: vuln.Title,
|
||||||
Description: vuln.Description,
|
Description: vuln.Description,
|
||||||
References: vuln.References,
|
References: vuln.References,
|
||||||
|
|||||||
Reference in New Issue
Block a user