diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 544969a..b369b5d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,9 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "vue": "^3.5.39" + "chart.js": "^4.5.1", + "vue": "^3.5.39", + "vue-chartjs": "^5.3.4" }, "devDependencies": { "@tailwindcss/vite": "^4.3.3", @@ -149,6 +151,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -952,6 +960,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1652,6 +1672,16 @@ } } }, + "node_modules/vue-chartjs": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.4.tgz", + "integrity": "sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, "node_modules/vue-router": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 65f9009..5c08693 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,9 @@ "preview": "vite preview" }, "dependencies": { - "vue": "^3.5.39" + "chart.js": "^4.5.1", + "vue": "^3.5.39", + "vue-chartjs": "^5.3.4" }, "devDependencies": { "@tailwindcss/vite": "^4.3.3", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 0e5894a..a8dfc21 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -7,7 +7,8 @@

Nucleus

diff --git a/frontend/src/components/Dashboard.vue b/frontend/src/components/Dashboard.vue new file mode 100644 index 0000000..b7d9c32 --- /dev/null +++ b/frontend/src/components/Dashboard.vue @@ -0,0 +1,144 @@ + + + diff --git a/frontend/src/router.js b/frontend/src/router.js index 052cab1..f07cab7 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -3,10 +3,12 @@ import TargetManager from './components/TargetManager.vue' import ScansHistory from './components/ScansHistory.vue' import FindingsInspector from './components/FindingsInspector.vue' import Login from './components/Login.vue' +import Dashboard from './components/Dashboard.vue' const routes = [ { path: '/login', component: Login }, - { path: '/', component: TargetManager, meta: { requiresAuth: true } }, + { path: '/', component: Dashboard, meta: { requiresAuth: true } }, + { path: '/targets', component: TargetManager, meta: { requiresAuth: true } }, { path: '/scans', component: ScansHistory, meta: { requiresAuth: true } }, { path: '/scans/:id', component: FindingsInspector, props: true, meta: { requiresAuth: true } } ] diff --git a/internal/api/handlers.go b/internal/api/handlers.go index d71a0c9..a5f4590 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -15,6 +15,8 @@ func RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("POST /api/auth/login", LoginHandler) mux.HandleFunc("POST /api/auth/logout", LogoutHandler) mux.HandleFunc("GET /api/auth/status", StatusHandler) + + mux.HandleFunc("GET /api/dashboard/stats", AuthMiddleware(getDashboardStats)) mux.HandleFunc("GET /api/targets", AuthMiddleware(getTargets)) mux.HandleFunc("POST /api/targets", AuthMiddleware(createTarget)) @@ -195,3 +197,59 @@ func getFindings(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(findings) } + +func getDashboardStats(w http.ResponseWriter, r *http.Request) { + var stats models.DashboardStats + stats.Severity = map[string]int{"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} + stats.TopHosts = []models.HostCount{} + stats.RecentTrends = []models.TrendPoint{} + + // Total Targets + db.DB.QueryRow("SELECT COUNT(*) FROM targets").Scan(&stats.TotalTargets) + + // Total Scans + db.DB.QueryRow("SELECT COUNT(*) FROM scans").Scan(&stats.TotalScans) + + // Severity Counts + rows, err := db.DB.Query("SELECT severity, COUNT(*) FROM findings GROUP BY severity") + if err == nil { + for rows.Next() { + var sev string + var count int + rows.Scan(&sev, &count) + stats.Severity[sev] = count + } + rows.Close() + } + + // Top Hosts + hRows, err := db.DB.Query("SELECT host, COUNT(*) as c FROM findings GROUP BY host ORDER BY c DESC LIMIT 5") + if err == nil { + for hRows.Next() { + var h models.HostCount + hRows.Scan(&h.Host, &h.Count) + stats.TopHosts = append(stats.TopHosts, h) + } + hRows.Close() + } + + // Recent Trends (Last 7 days) + tRows, err := db.DB.Query(` + SELECT DATE(detected_at) as d, COUNT(*) + FROM findings + WHERE detected_at >= date('now', '-7 days') + GROUP BY d + ORDER BY d ASC + `) + if err == nil { + for tRows.Next() { + var t models.TrendPoint + tRows.Scan(&t.Date, &t.Count) + stats.RecentTrends = append(stats.RecentTrends, t) + } + tRows.Close() + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(stats) +} diff --git a/internal/models/models.go b/internal/models/models.go index fcfe808..199da59 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -45,3 +45,21 @@ type NucleiFinding struct { MatchedAt string `json:"matched-at"` Timestamp string `json:"timestamp"` } + +type TrendPoint struct { + Date string `json:"date"` + Count int `json:"count"` +} + +type HostCount struct { + Host string `json:"host"` + Count int `json:"count"` +} + +type DashboardStats struct { + TotalTargets int `json:"total_targets"` + TotalScans int `json:"total_scans"` + Severity map[string]int `json:"severity"` + TopHosts []HostCount `json:"top_hosts"` + RecentTrends []TrendPoint `json:"recent_trends"` +}