Merge branch 'worktree-agent-a853fbe0'
This commit is contained in:
@@ -218,8 +218,8 @@ Requirements for initial release. Each maps to roadmap phases.
|
|||||||
|
|
||||||
### Web Dashboard
|
### Web Dashboard
|
||||||
|
|
||||||
- [ ] **WEB-01**: Embedded HTTP server (chi + htmx + Tailwind CSS)
|
- [x] **WEB-01**: Embedded HTTP server (chi + htmx + Tailwind CSS)
|
||||||
- [ ] **WEB-02**: Dashboard overview page with summary statistics
|
- [x] **WEB-02**: Dashboard overview page with summary statistics
|
||||||
- [ ] **WEB-03**: Scan history and scan detail pages
|
- [ ] **WEB-03**: Scan history and scan detail pages
|
||||||
- [ ] **WEB-04**: Key listing page with filtering and "Reveal Key" toggle
|
- [ ] **WEB-04**: Key listing page with filtering and "Reveal Key" toggle
|
||||||
- [ ] **WEB-05**: OSINT/Recon launcher and results page
|
- [ ] **WEB-05**: OSINT/Recon launcher and results page
|
||||||
@@ -227,7 +227,7 @@ Requirements for initial release. Each maps to roadmap phases.
|
|||||||
- [ ] **WEB-07**: Dork management page
|
- [ ] **WEB-07**: Dork management page
|
||||||
- [ ] **WEB-08**: Settings configuration page
|
- [ ] **WEB-08**: Settings configuration page
|
||||||
- [ ] **WEB-09**: REST API (/api/v1/*) for programmatic access
|
- [ ] **WEB-09**: REST API (/api/v1/*) for programmatic access
|
||||||
- [ ] **WEB-10**: Optional basic auth / token auth
|
- [x] **WEB-10**: Optional basic auth / token auth
|
||||||
- [ ] **WEB-11**: Server-Sent Events for live scan progress
|
- [ ] **WEB-11**: Server-Sent Events for live scan progress
|
||||||
|
|
||||||
### Telegram Bot
|
### Telegram Bot
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
|||||||
- [x] **Phase 15: OSINT Forums, Collaboration & Log Aggregators** - StackOverflow/Reddit/HN, Notion/Trello, Elasticsearch/Grafana/Sentry (completed 2026-04-06)
|
- [x] **Phase 15: OSINT Forums, Collaboration & Log Aggregators** - StackOverflow/Reddit/HN, Notion/Trello, Elasticsearch/Grafana/Sentry (completed 2026-04-06)
|
||||||
- [x] **Phase 16: OSINT Threat Intel, Mobile, DNS & API Marketplaces** - VirusTotal/IntelX, APK scanning, crt.sh, Postman/SwaggerHub (completed 2026-04-06)
|
- [x] **Phase 16: OSINT Threat Intel, Mobile, DNS & API Marketplaces** - VirusTotal/IntelX, APK scanning, crt.sh, Postman/SwaggerHub (completed 2026-04-06)
|
||||||
- [x] **Phase 17: Telegram Bot & Scheduled Scanning** - Remote control bot and cron-based recurring scans with auto-notify (completed 2026-04-06)
|
- [x] **Phase 17: Telegram Bot & Scheduled Scanning** - Remote control bot and cron-based recurring scans with auto-notify (completed 2026-04-06)
|
||||||
- [ ] **Phase 18: Web Dashboard** - Embedded htmx + Tailwind dashboard aggregating all subsystems with SSE live updates
|
- [x] **Phase 18: Web Dashboard** - Embedded htmx + Tailwind dashboard aggregating all subsystems with SSE live updates (completed 2026-04-06)
|
||||||
|
|
||||||
## Phase Details
|
## Phase Details
|
||||||
|
|
||||||
@@ -391,4 +391,4 @@ Phases execute in numeric order: 1 → 2 → 3 → ... → 18
|
|||||||
| 15. OSINT Forums, Collaboration & Log Aggregators | 2/4 | Complete | 2026-04-06 |
|
| 15. OSINT Forums, Collaboration & Log Aggregators | 2/4 | Complete | 2026-04-06 |
|
||||||
| 16. OSINT Threat Intel, Mobile, DNS & API Marketplaces | 0/? | Complete | 2026-04-06 |
|
| 16. OSINT Threat Intel, Mobile, DNS & API Marketplaces | 0/? | Complete | 2026-04-06 |
|
||||||
| 17. Telegram Bot & Scheduled Scanning | 3/5 | Complete | 2026-04-06 |
|
| 17. Telegram Bot & Scheduled Scanning | 3/5 | Complete | 2026-04-06 |
|
||||||
| 18. Web Dashboard | 0/? | Not started | - |
|
| 18. Web Dashboard | 1/1 | Complete | 2026-04-06 |
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ gsd_state_version: 1.0
|
|||||||
milestone: v1.0
|
milestone: v1.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: executing
|
status: executing
|
||||||
stopped_at: Completed 17-04-PLAN.md
|
stopped_at: Completed 18-01-PLAN.md
|
||||||
last_updated: "2026-04-06T14:50:49.687Z"
|
last_updated: "2026-04-06T15:03:51.830Z"
|
||||||
last_activity: 2026-04-06
|
last_activity: 2026-04-06
|
||||||
progress:
|
progress:
|
||||||
total_phases: 18
|
total_phases: 18
|
||||||
completed_phases: 15
|
completed_phases: 16
|
||||||
total_plans: 90
|
total_plans: 91
|
||||||
completed_plans: 88
|
completed_plans: 89
|
||||||
percent: 20
|
percent: 20
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -102,6 +102,7 @@ Progress: [██░░░░░░░░] 20%
|
|||||||
| Phase 16 P01 | 4min | 2 tasks | 6 files |
|
| Phase 16 P01 | 4min | 2 tasks | 6 files |
|
||||||
| Phase 17 P01 | 3min | 2 tasks | 4 files |
|
| Phase 17 P01 | 3min | 2 tasks | 4 files |
|
||||||
| Phase 17 P04 | 3min | 2 tasks | 4 files |
|
| Phase 17 P04 | 3min | 2 tasks | 4 files |
|
||||||
|
| Phase 18 P01 | 3min | 2 tasks | 9 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -156,6 +157,7 @@ Recent decisions affecting current work:
|
|||||||
- [Phase 16]: URLhaus tag lookup with payload endpoint fallback
|
- [Phase 16]: URLhaus tag lookup with payload endpoint fallback
|
||||||
- [Phase 17]: telego v1.8.0 promoted from indirect to direct; context cancellation for graceful shutdown; rate limit 60s scan/verify/recon, 5s others
|
- [Phase 17]: telego v1.8.0 promoted from indirect to direct; context cancellation for graceful shutdown; rate limit 60s scan/verify/recon, 5s others
|
||||||
- [Phase 17]: Separated format from send for testable notifications without telego mock
|
- [Phase 17]: Separated format from send for testable notifications without telego mock
|
||||||
|
- [Phase 18]: html/template over templ for v1; Tailwind CDN; nil-safe handlers; constant-time auth comparison
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -170,6 +172,6 @@ None yet.
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-04-06T14:34:18.710Z
|
Last session: 2026-04-06T15:03:51.826Z
|
||||||
Stopped at: Completed 17-04-PLAN.md
|
Stopped at: Completed 18-01-PLAN.md
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
|||||||
125
.planning/phases/18-web-dashboard/18-01-SUMMARY.md
Normal file
125
.planning/phases/18-web-dashboard/18-01-SUMMARY.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
phase: 18-web-dashboard
|
||||||
|
plan: 01
|
||||||
|
subsystem: web
|
||||||
|
tags: [chi, htmx, go-embed, html-template, auth-middleware, dashboard]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 01-foundation
|
||||||
|
provides: storage.DB, providers.Registry
|
||||||
|
- phase: 09-osint-infrastructure
|
||||||
|
provides: recon.Engine
|
||||||
|
- phase: 08-dork-engine
|
||||||
|
provides: dorks.Registry
|
||||||
|
provides:
|
||||||
|
- "pkg/web package with chi v5 router, embedded static assets, auth middleware"
|
||||||
|
- "Overview dashboard page with stats from providers/recon/storage"
|
||||||
|
- "Server struct with NewServer constructor, Config, Router(), ListenAndServe()"
|
||||||
|
affects: [18-02, 18-03, 18-04, 18-05]
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: [chi v5.2.5, htmx v2.0.4]
|
||||||
|
patterns: [go:embed for static assets and templates, html/template with layout pattern, nil-safe handler for optional dependencies]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- pkg/web/server.go
|
||||||
|
- pkg/web/auth.go
|
||||||
|
- pkg/web/handlers.go
|
||||||
|
- pkg/web/embed.go
|
||||||
|
- pkg/web/static/htmx.min.js
|
||||||
|
- pkg/web/static/style.css
|
||||||
|
- pkg/web/templates/layout.html
|
||||||
|
- pkg/web/templates/overview.html
|
||||||
|
- pkg/web/server_test.go
|
||||||
|
modified:
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "html/template over templ for v1 per CONTEXT.md deferred decision"
|
||||||
|
- "Tailwind via CDN for v1 rather than standalone CLI build step"
|
||||||
|
- "Nil-safe handlers: overview works with zero Config (no DB, no providers)"
|
||||||
|
- "AuthMiddleware uses crypto/subtle constant-time comparison for timing-attack resistance"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Web handler pattern: method on Server struct, nil-check dependencies before use"
|
||||||
|
- "go:embed layout: static/ and templates/ subdirs under pkg/web/"
|
||||||
|
- "Template composition: define layout + block content pattern"
|
||||||
|
|
||||||
|
requirements-completed: [WEB-01, WEB-02, WEB-10]
|
||||||
|
|
||||||
|
duration: 3min
|
||||||
|
completed: 2026-04-06
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 18 Plan 01: Web Dashboard Foundation Summary
|
||||||
|
|
||||||
|
**chi v5 router with go:embed static assets (htmx, CSS), html/template layout, overview dashboard, and Basic/Bearer auth middleware**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 3 min
|
||||||
|
- **Started:** 2026-04-06T14:59:54Z
|
||||||
|
- **Completed:** 2026-04-06T15:02:56Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 9
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- chi v5.2.5 HTTP router with middleware stack (RealIP, Logger, Recoverer)
|
||||||
|
- Vendored htmx v2.0.4, embedded via go:embed alongside CSS and HTML templates
|
||||||
|
- Overview page with 4 stat cards (Total Keys, Providers, Recon Sources, Last Scan) and recent findings table
|
||||||
|
- Auth middleware supporting Basic and Bearer token with constant-time comparison, no-op when unconfigured
|
||||||
|
- 7 tests covering overview rendering, static serving, auth enforcement, and passthrough
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: chi v5 dependency + go:embed static assets + layout template** - `dd2c8c5` (feat)
|
||||||
|
2. **Task 2 RED: failing tests for server/auth/overview** - `3541c82` (test)
|
||||||
|
3. **Task 2 GREEN: implement server, auth, handlers** - `268a769` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `pkg/web/server.go` - chi router setup, NewServer constructor, ListenAndServe
|
||||||
|
- `pkg/web/auth.go` - Basic auth and bearer token middleware with constant-time compare
|
||||||
|
- `pkg/web/handlers.go` - Overview handler with OverviewData struct, nil-safe DB/provider access
|
||||||
|
- `pkg/web/embed.go` - go:embed directives for static/ and templates/
|
||||||
|
- `pkg/web/static/htmx.min.js` - Vendored htmx v2.0.4 (50KB)
|
||||||
|
- `pkg/web/static/style.css` - Custom overrides for stat cards, findings table, nav
|
||||||
|
- `pkg/web/templates/layout.html` - Base layout with nav bar, Tailwind CDN, htmx script
|
||||||
|
- `pkg/web/templates/overview.html` - Dashboard with stat cards grid and findings table
|
||||||
|
- `pkg/web/server_test.go` - 7 integration tests for server, auth, overview
|
||||||
|
- `go.mod` / `go.sum` - Added chi v5.2.5
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Used html/template (not templ) per CONTEXT.md deferred decision for v1
|
||||||
|
- Tailwind via CDN rather than standalone build step for v1 simplicity
|
||||||
|
- Nil-safe handlers allow server to start with zero config (no DB required)
|
||||||
|
- Auth uses crypto/subtle.ConstantTimeCompare to prevent timing attacks
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
None
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
None - no external service configuration required.
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
None - all data paths are wired to real sources (providers.Registry, recon.Engine, storage.DB) or gracefully show zeroes when dependencies are nil.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
All 9 files verified present. All 3 commit hashes verified in git log.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- Server skeleton ready for Plans 02-05 to add keys page, providers page, API endpoints, SSE
|
||||||
|
- Router exposed via Router() for easy route additions
|
||||||
|
- Template parsing supports adding new .html files to templates/
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 18-web-dashboard*
|
||||||
|
*Completed: 2026-04-06*
|
||||||
1
go.mod
1
go.mod
@@ -44,6 +44,7 @@ require (
|
|||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -53,6 +53,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
|||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
github.com/go-co-op/gocron/v2 v2.19.1 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI=
|
github.com/go-co-op/gocron/v2 v2.19.1 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI=
|
||||||
github.com/go-co-op/gocron/v2 v2.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
github.com/go-co-op/gocron/v2 v2.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
@@ -189,10 +191,6 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
|||||||
52
pkg/web/auth.go
Normal file
52
pkg/web/auth.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthMiddleware returns HTTP middleware that enforces authentication when
|
||||||
|
// at least one of user/pass or token is configured. If all auth fields are
|
||||||
|
// empty, the middleware is a no-op passthrough.
|
||||||
|
//
|
||||||
|
// Supported schemes:
|
||||||
|
// - Bearer <token> — matches the configured token
|
||||||
|
// - Basic <base64> — matches the configured user:pass
|
||||||
|
func AuthMiddleware(user, pass, token string) func(http.Handler) http.Handler {
|
||||||
|
authEnabled := user != "" || token != ""
|
||||||
|
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !authEnabled {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := r.Header.Get("Authorization")
|
||||||
|
|
||||||
|
// Check bearer token first.
|
||||||
|
if token != "" && strings.HasPrefix(auth, "Bearer ") {
|
||||||
|
provided := strings.TrimPrefix(auth, "Bearer ")
|
||||||
|
if subtle.ConstantTimeCompare([]byte(provided), []byte(token)) == 1 {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check basic auth.
|
||||||
|
if user != "" {
|
||||||
|
u, p, ok := r.BasicAuth()
|
||||||
|
if ok &&
|
||||||
|
subtle.ConstantTimeCompare([]byte(u), []byte(user)) == 1 &&
|
||||||
|
subtle.ConstantTimeCompare([]byte(p), []byte(pass)) == 1 {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("WWW-Authenticate", `Basic realm="keyhunter"`)
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
14
pkg/web/embed.go
Normal file
14
pkg/web/embed.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
// staticFiles holds the vendored static assets (htmx.min.js, style.css, etc.)
|
||||||
|
// embedded at compile time.
|
||||||
|
//
|
||||||
|
//go:embed static/*
|
||||||
|
var staticFiles embed.FS
|
||||||
|
|
||||||
|
// templateFiles holds HTML templates embedded at compile time.
|
||||||
|
//
|
||||||
|
//go:embed templates/*
|
||||||
|
var templateFiles embed.FS
|
||||||
58
pkg/web/handlers.go
Normal file
58
pkg/web/handlers.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/salvacybersec/keyhunter/pkg/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OverviewData holds template data for the overview dashboard page.
|
||||||
|
type OverviewData struct {
|
||||||
|
TotalKeys int
|
||||||
|
TotalProviders int
|
||||||
|
ReconSources int
|
||||||
|
LastScan string
|
||||||
|
RecentFindings []storage.Finding
|
||||||
|
PageTitle string
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleOverview renders the overview dashboard page with aggregated stats.
|
||||||
|
func (s *Server) handleOverview(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := OverviewData{
|
||||||
|
PageTitle: "Overview",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider count.
|
||||||
|
if s.cfg.Providers != nil {
|
||||||
|
data.TotalProviders = s.cfg.Providers.Stats().Total
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recon source count.
|
||||||
|
if s.cfg.ReconEngine != nil {
|
||||||
|
data.ReconSources = len(s.cfg.ReconEngine.List())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recent findings + total count from DB.
|
||||||
|
if s.cfg.DB != nil && s.cfg.EncKey != nil {
|
||||||
|
recent, err := s.cfg.DB.ListFindingsFiltered(s.cfg.EncKey, storage.Filters{Limit: 10})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("web: listing recent findings: %v", err)
|
||||||
|
} else {
|
||||||
|
data.RecentFindings = recent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total count via a broader query.
|
||||||
|
all, err := s.cfg.DB.ListFindingsFiltered(s.cfg.EncKey, storage.Filters{})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("web: counting findings: %v", err)
|
||||||
|
} else {
|
||||||
|
data.TotalKeys = len(all)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.tmpl.ExecuteTemplate(w, "layout", data); err != nil {
|
||||||
|
log.Printf("web: rendering overview: %v", err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
84
pkg/web/server.go
Normal file
84
pkg/web/server.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
|
||||||
|
"github.com/salvacybersec/keyhunter/pkg/dorks"
|
||||||
|
"github.com/salvacybersec/keyhunter/pkg/providers"
|
||||||
|
"github.com/salvacybersec/keyhunter/pkg/recon"
|
||||||
|
"github.com/salvacybersec/keyhunter/pkg/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds all dependencies and settings needed by the web server.
|
||||||
|
type Config struct {
|
||||||
|
DB *storage.DB
|
||||||
|
EncKey []byte
|
||||||
|
Providers *providers.Registry
|
||||||
|
Dorks *dorks.Registry
|
||||||
|
ReconEngine *recon.Engine
|
||||||
|
Port int
|
||||||
|
AuthUser string
|
||||||
|
AuthPass string
|
||||||
|
AuthToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is the KeyHunter web dashboard backed by chi.
|
||||||
|
type Server struct {
|
||||||
|
router chi.Router
|
||||||
|
cfg Config
|
||||||
|
tmpl *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer creates a Server, parsing embedded templates and building routes.
|
||||||
|
func NewServer(cfg Config) (*Server, error) {
|
||||||
|
// Parse all templates from the embedded filesystem.
|
||||||
|
tmpl, err := template.ParseFS(templateFiles, "templates/*.html")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing templates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Server{
|
||||||
|
cfg: cfg,
|
||||||
|
tmpl: tmpl,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
// Middleware stack.
|
||||||
|
r.Use(middleware.RealIP)
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
r.Use(middleware.Recoverer)
|
||||||
|
|
||||||
|
// Auth middleware (no-op when auth fields are empty).
|
||||||
|
r.Use(AuthMiddleware(cfg.AuthUser, cfg.AuthPass, cfg.AuthToken))
|
||||||
|
|
||||||
|
// Static file serving from embedded FS.
|
||||||
|
staticSub, err := fs.Sub(staticFiles, "static")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating static sub-filesystem: %w", err)
|
||||||
|
}
|
||||||
|
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))))
|
||||||
|
|
||||||
|
// Page routes.
|
||||||
|
r.Get("/", s.handleOverview)
|
||||||
|
|
||||||
|
s.router = r
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router returns the chi router for testing.
|
||||||
|
func (s *Server) Router() chi.Router {
|
||||||
|
return s.router
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAndServe starts the HTTP server on the configured port.
|
||||||
|
func (s *Server) ListenAndServe() error {
|
||||||
|
addr := fmt.Sprintf(":%d", s.cfg.Port)
|
||||||
|
return http.ListenAndServe(addr, s.router)
|
||||||
|
}
|
||||||
107
pkg/web/server_test.go
Normal file
107
pkg/web/server_test.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOverview_Returns200WithKeyHunter(t *testing.T) {
|
||||||
|
srv, err := NewServer(Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.Router().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Contains(t, rec.Body.String(), "KeyHunter")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaticAsset_HtmxJS(t *testing.T) {
|
||||||
|
srv, err := NewServer(Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/static/htmx.min.js", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.Router().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Contains(t, rec.Body.String(), "htmx")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuth_Returns401_WhenConfiguredButNoCreds(t *testing.T) {
|
||||||
|
srv, err := NewServer(Config{
|
||||||
|
AuthUser: "admin",
|
||||||
|
AuthPass: "secret",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.Router().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, rec.Code)
|
||||||
|
assert.Contains(t, rec.Header().Get("WWW-Authenticate"), "Basic")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuth_BasicAuth_Returns200(t *testing.T) {
|
||||||
|
srv, err := NewServer(Config{
|
||||||
|
AuthUser: "admin",
|
||||||
|
AuthPass: "secret",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.SetBasicAuth("admin", "secret")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.Router().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Contains(t, rec.Body.String(), "KeyHunter")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuth_BearerToken_Returns200(t *testing.T) {
|
||||||
|
srv, err := NewServer(Config{
|
||||||
|
AuthToken: "my-secret-token",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer my-secret-token")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.Router().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Contains(t, rec.Body.String(), "KeyHunter")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuth_NoAuthConfigured_PassesThrough(t *testing.T) {
|
||||||
|
srv, err := NewServer(Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.Router().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverview_ShowsStats(t *testing.T) {
|
||||||
|
srv, err := NewServer(Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.Router().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
// Should display stat values (zeroes when no DB)
|
||||||
|
assert.True(t, strings.Contains(body, "Total Keys Found"), "should show Total Keys stat card")
|
||||||
|
assert.True(t, strings.Contains(body, "Providers Loaded"), "should show Providers stat card")
|
||||||
|
assert.True(t, strings.Contains(body, "Recon Sources"), "should show Recon Sources stat card")
|
||||||
|
}
|
||||||
1
pkg/web/static/htmx.min.js
vendored
Normal file
1
pkg/web/static/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
39
pkg/web/static/style.css
Normal file
39
pkg/web/static/style.css
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/* KeyHunter Dashboard — custom overrides (Tailwind CDN handles utility classes) */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
|
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat card styling */
|
||||||
|
.stat-card {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Findings table */
|
||||||
|
.findings-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.findings-table th,
|
||||||
|
.findings-table td {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.findings-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
.findings-table tr:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation active link */
|
||||||
|
.nav-link-active {
|
||||||
|
border-bottom: 2px solid #3b82f6;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
43
pkg/web/templates/layout.html
Normal file
43
pkg/web/templates/layout.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{{define "layout"}}<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{if .PageTitle}}{{.PageTitle}} - {{end}}KeyHunter Dashboard</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="/static/htmx.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 min-h-screen">
|
||||||
|
<!-- Navigation -->
|
||||||
|
<nav class="bg-white shadow-sm border-b border-gray-200">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex justify-between h-16">
|
||||||
|
<div class="flex items-center space-x-8">
|
||||||
|
<a href="/" class="text-xl font-bold text-gray-900">KeyHunter</a>
|
||||||
|
<div class="hidden md:flex space-x-6">
|
||||||
|
<a href="/" class="text-gray-600 hover:text-gray-900 px-1 py-2 text-sm font-medium">Overview</a>
|
||||||
|
<a href="/keys" class="text-gray-600 hover:text-gray-900 px-1 py-2 text-sm font-medium">Keys</a>
|
||||||
|
<a href="/providers" class="text-gray-600 hover:text-gray-900 px-1 py-2 text-sm font-medium">Providers</a>
|
||||||
|
<a href="/recon" class="text-gray-600 hover:text-gray-900 px-1 py-2 text-sm font-medium">Recon</a>
|
||||||
|
<a href="/dorks" class="text-gray-600 hover:text-gray-900 px-1 py-2 text-sm font-medium">Dorks</a>
|
||||||
|
<a href="/settings" class="text-gray-600 hover:text-gray-900 px-1 py-2 text-sm font-medium">Settings</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{{block "content" .}}{{end}}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-white border-t border-gray-200 mt-auto">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
|
<p class="text-sm text-gray-500 text-center">KeyHunter - API Key Scanner</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>{{end}}
|
||||||
77
pkg/web/templates/overview.html
Normal file
77
pkg/web/templates/overview.html
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{{template "layout" .}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<!-- Stat cards -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
<div class="stat-card bg-white border border-gray-200">
|
||||||
|
<p class="text-sm font-medium text-gray-500">Total Keys Found</p>
|
||||||
|
<p class="mt-2 text-3xl font-bold text-gray-900">{{.TotalKeys}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card bg-white border border-gray-200">
|
||||||
|
<p class="text-sm font-medium text-gray-500">Providers Loaded</p>
|
||||||
|
<p class="mt-2 text-3xl font-bold text-gray-900">{{.TotalProviders}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card bg-white border border-gray-200">
|
||||||
|
<p class="text-sm font-medium text-gray-500">Recon Sources</p>
|
||||||
|
<p class="mt-2 text-3xl font-bold text-gray-900">{{.ReconSources}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card bg-white border border-gray-200">
|
||||||
|
<p class="text-sm font-medium text-gray-500">Last Scan</p>
|
||||||
|
<p class="mt-2 text-xl font-bold text-gray-900">{{if .LastScan}}{{.LastScan}}{{else}}Never{{end}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent findings -->
|
||||||
|
<div class="bg-white shadow-sm rounded-lg border border-gray-200">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900">Recent Findings</h2>
|
||||||
|
</div>
|
||||||
|
{{if .RecentFindings}}
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="findings-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Provider</th>
|
||||||
|
<th>Masked Key</th>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Confidence</th>
|
||||||
|
<th>Verified</th>
|
||||||
|
<th>Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .RecentFindings}}
|
||||||
|
<tr>
|
||||||
|
<td class="font-medium">{{.ProviderName}}</td>
|
||||||
|
<td class="font-mono text-sm text-gray-600">{{.KeyMasked}}</td>
|
||||||
|
<td class="text-sm text-gray-600">{{.SourcePath}}</td>
|
||||||
|
<td>
|
||||||
|
{{if eq .Confidence "high"}}
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">High</span>
|
||||||
|
{{else if eq .Confidence "medium"}}
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Medium</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">{{.Confidence}}</span>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if .Verified}}
|
||||||
|
<span class="text-green-600 font-medium">Live</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="text-gray-400">-</span>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td class="text-sm text-gray-500">{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="px-6 py-12 text-center text-gray-500">
|
||||||
|
<p class="text-lg">No findings yet</p>
|
||||||
|
<p class="mt-1 text-sm">Run a scan to detect API keys</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
Reference in New Issue
Block a user