diff --git a/app/services.go b/app/services.go index cf19eec..a6e4b0c 100644 --- a/app/services.go +++ b/app/services.go @@ -10,6 +10,7 @@ import ( "log" "net" "net/http" + "path/filepath" "strconv" "strings" "sync" @@ -26,11 +27,19 @@ type App struct { cancel context.CancelFunc wg sync.WaitGroup // keep references to servers for graceful shutdown - httpSrv *http.Server + httpSrvs []*http.Server sshSigner ssh.Signer // track TCP listeners for graceful shutdown mu sync.Mutex listeners []net.Listener + conns map[net.Conn]struct{} +} + +// addHTTPServer registers an HTTP server for graceful shutdown +func (a *App) addHTTPServer(s *http.Server) { + a.mu.Lock() + a.httpSrvs = append(a.httpSrvs, s) + a.mu.Unlock() } // addListener registers a listener for later shutdown @@ -123,32 +132,40 @@ func trimQuotes(s string) string { } func NewApp(cfg Config) (*App, error) { - l, err := NewLogger(cfg) - if err != nil { - return nil, err - } - ctx, cancel := context.WithCancel(context.Background()) - a := &App{cfg: cfg, logger: l, ctx: ctx, cancel: cancel} - return a, nil + l, err := NewLogger(cfg) + if err != nil { + return nil, err + } + // Initialize threat intelligence system + threatIntelPath := "threat_intel.json" + if cfg.LogPath != "" { + threatIntelPath = filepath.Join(filepath.Dir(cfg.LogPath), "threat_intel.json") + } + ti := NewThreatIntel(threatIntelPath, true) + + // Root context for the App used for shutdown signalling + ctx, cancel := context.WithCancel(context.Background()) + a := &App{cfg: cfg, logger: l, threatIntel: ti, ctx: ctx, cancel: cancel, conns: make(map[net.Conn]struct{})} + return a, nil } func (a *App) Run(ctx context.Context) error { - // start services according to cfg - if a.cfg.Services.HTTP { - a.wg.Add(1) - go func() { - defer a.wg.Done() - a.startHTTP(a.cfg.Ports.HTTP) - }() - } - if a.cfg.Services.SSH { - a.wg.Add(1) - go func() { - defer a.wg.Done() - a.startTCPService("ssh", a.cfg.Ports.SSH, a.sshHandler) - }() - } - if a.cfg.Services.FTP { + // start services according to cfg + if a.cfg.Services.HTTP { + a.wg.Add(1) + go func() { + defer a.wg.Done() + a.startHTTP(a.cfg.Ports.HTTP) + }() + } + if a.cfg.Services.SSH { + a.wg.Add(1) + go func() { + defer a.wg.Done() + a.startTCPService("ssh", a.cfg.Ports.SSH, a.sshHandler) + }() + } + if a.cfg.Services.FTP { a.wg.Add(1) go func() { defer a.wg.Done() @@ -285,15 +302,27 @@ func (a *App) Run(ctx context.Context) error { func (a *App) Shutdown() { a.cancel() - // attempt to close http server if running - if a.httpSrv != nil { + // attempt to close all http servers if running + a.mu.Lock() + srvs := a.httpSrvs + a.httpSrvs = nil + a.mu.Unlock() + for _, s := range srvs { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - _ = a.httpSrv.Shutdown(ctx) + _ = s.Shutdown(ctx) + cancel() } // close all TCP listeners to unblock Accept loops a.closeAllListeners() + // close all tracked connections to unblock handlers + a.closeAllConns() a.wg.Wait() + + // Save threat intelligence data before shutdown + if a.threatIntel != nil { + _ = a.threatIntel.Save() + } + _ = a.logger.Close() } @@ -308,12 +337,47 @@ func (a *App) closeAllListeners() { } } +// addConn tracks an active connection for shutdown +func (a *App) addConn(c net.Conn) { + a.mu.Lock() + if a.conns == nil { + a.conns = make(map[net.Conn]struct{}) + } + a.conns[c] = struct{}{} + a.mu.Unlock() +} + +// removeConn removes a connection from tracking +func (a *App) removeConn(c net.Conn) { + a.mu.Lock() + delete(a.conns, c) + a.mu.Unlock() +} + +// closeAllConns closes all tracked connections to unblock handlers +func (a *App) closeAllConns() { + a.mu.Lock() + conns := make([]net.Conn, 0, len(a.conns)) + for c := range a.conns { + conns = append(conns, c) + } + a.conns = make(map[net.Conn]struct{}) + a.mu.Unlock() + for _, c := range conns { + _ = c.Close() + } +} + // helpers for logging func (a *App) logEvent(r Record) { - if a.logger == nil { - return + if a.logger != nil { + _ = a.logger.Log(r) + } + + // Record threat intelligence if IP is not private + if a.threatIntel != nil && r.RemoteAddr != "" && !IsPrivateIP(r.RemoteAddr) { + a.threatIntel.RecordActivity(r) } - _ = a.logger.Log(r) } // HTTP honeypot @@ -344,7 +408,7 @@ func (a *App) startHTTP(port int) { }) srv := &http.Server{Addr: addr, Handler: mux} - a.httpSrv = srv + a.addHTTPServer(srv) log.Printf("HTTP listening on %s", addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Printf("http server error: %v", err) @@ -399,7 +463,12 @@ func (a *App) startTCPService(name string, port int, handler func(net.Conn)) { return case conn := <-acceptCh: go func(c net.Conn) { - defer c.Close() + // track and ensure cleanup + a.addConn(c) + defer func() { + a.removeConn(c) + _ = c.Close() + }() // Check if context is cancelled before processing select { case <-a.ctx.Done(): diff --git a/app/web.go b/app/web.go index b09110d..0a2e07e 100644 --- a/app/web.go +++ b/app/web.go @@ -14,88 +14,132 @@ import ( var tpl = template.Must(template.New("base").Parse(` -Honeypot Dashboard + + + Honeypot Dashboard + + -

Honeypot Dashboard

-

Logs

+

🍯 Honeypot Dashboard

+ {{ .Body }} `)) - func (a *App) startWeb() { - bind := a.cfg.Web.Bind - port := a.cfg.Web.Port - addr := fmt.Sprintf("%s:%d", bind, port) - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - tpl.Execute(w, map[string]interface{}{"Body": template.HTML("

View logs

")}) - }) - mux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) { - // display last 200 logs - var rows []Record - if a.logger != nil && a.logger.mode == "sqlite" && a.logger.db != nil { - // query sqlite - q := `SELECT timestamp, remote_addr, remote_port, service, details, raw_payload FROM logs ORDER BY id DESC LIMIT 200` - rs, err := a.logger.db.Query(q) - if err != nil { - http.Error(w, "db query failed", 500) - return - } - defer rs.Close() - for rs.Next() { - var ts, ra, rp, svc, detailsS, raw string - if err := rs.Scan(&ts, &ra, &rp, &svc, &detailsS, &raw); err != nil { - continue - } - var det map[string]string - _ = json.Unmarshal([]byte(detailsS), &det) - rows = append(rows, Record{Timestamp: parseTime(ts), RemoteAddr: ra, RemotePort: rp, Service: svc, Details: det, RawPayload: raw}) - } - } else { - // try to read file based JSON-lines - path := a.cfg.LogPath - b, err := os.ReadFile(path) - if err == nil { - lines := strings.Split(string(b), "\n") - for i := len(lines) - 1; i >= 0 && len(rows) < 200; i-- { - line := strings.TrimSpace(lines[i]) - if line == "" { - continue - } - var rec Record - if err := json.Unmarshal([]byte(line), &rec); err != nil { - continue - } - rows = append(rows, rec) - } - } - } + bind := a.cfg.Web.Bind + port := a.cfg.Web.Port + addr := fmt.Sprintf("%s:%d", bind, port) + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + var sb strings.Builder + + // Get basic stats + if a.threatIntel != nil { + stats := a.threatIntel.GetStats() + sb.WriteString("
") + sb.WriteString(fmt.Sprintf("
%v
Total IPs
", stats["total_ips"])) + sb.WriteString(fmt.Sprintf("
%v
Blacklisted
", stats["blacklisted_ips"])) + sb.WriteString(fmt.Sprintf("
%v
Connections
", stats["total_connections"])) + sb.WriteString(fmt.Sprintf("
%v
Auth Attempts
", stats["total_auth_attempts"])) + sb.WriteString("
") + } + + sb.WriteString("

Quick Actions

") + sb.WriteString("

📋 View Recent Logs

") + sb.WriteString("

⚠️ View Top Threats

") + sb.WriteString("

🚫 Manage Blacklist

") + sb.WriteString("

📊 Detailed Statistics

") + tpl.Execute(w, map[string]interface{}{"Body": template.HTML(sb.String())}) + }) + mux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) { + // display last 200 logs + var rows []Record + if a.logger != nil && a.logger.mode == "sqlite" && a.logger.db != nil { + // query sqlite + q := `SELECT timestamp, remote_addr, remote_port, service, details, raw_payload FROM logs ORDER BY id DESC LIMIT 200` + rs, err := a.logger.db.Query(q) + if err != nil { + http.Error(w, "db query failed", 500) + return + } + defer rs.Close() + for rs.Next() { + var ts, ra, rp, svc, detailsS, raw string + if err := rs.Scan(&ts, &ra, &rp, &svc, &detailsS, &raw); err != nil { + continue + } + var det map[string]string + if err := json.Unmarshal([]byte(detailsS), &det); err != nil { + continue + } + rows = append(rows, Record{Timestamp: parseTime(ts), RemoteAddr: ra, RemotePort: rp, Service: svc, Details: det, RawPayload: raw}) + } + } else { + // try to read file based JSON-lines + path := a.cfg.LogPath + b, err := os.ReadFile(path) + if err == nil { + lines := strings.Split(string(b), "\n") + for i := len(lines) - 1; i >= 0 && len(rows) < 200; i-- { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + var rec Record + if err := json.Unmarshal([]byte(line), &rec); err != nil { + continue + } + rows = append(rows, rec) + } + } + } - // render simple table - var sb strings.Builder - sb.WriteString("") - for _, r := range rows { - detB, _ := json.Marshal(r.Details) - sb.WriteString(fmt.Sprintf("", r.Timestamp.Format("2006-01-02 15:04:05"), r.RemoteAddr, r.RemotePort, r.Service, template.HTMLEscapeString(string(detB)), template.HTMLEscapeString(r.RawPayload))) - } - sb.WriteString("
TimeRemoteServiceDetailsPayload
%s%s:%s%s%s
%s
") - tpl.Execute(w, map[string]interface{}{"Body": template.HTML(sb.String())}) - }) + // render simple table + var sb strings.Builder + sb.WriteString("") + for _, r := range rows { + detB, _ := json.Marshal(r.Details) + sb.WriteString(fmt.Sprintf("", r.Timestamp.Format("2006-01-02 15:04:05"), r.RemoteAddr, r.RemotePort, r.Service, template.HTMLEscapeString(string(detB)), template.HTMLEscapeString(r.RawPayload))) + } + sb.WriteString("
TimeRemoteServiceDetailsPayload
%s%s:%s%s%s
%s
") + tpl.Execute(w, map[string]interface{}{"Body": template.HTML(sb.String())}) + }) srv := &http.Server{Addr: addr, Handler: mux} - a.httpSrv = srv + a.addHTTPServer(srv) log.Printf("Dashboard listening on http://%s", addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Printf("dashboard error: %v", err) } } - func parseTime(s string) (t time.Time) { - t, _ = time.Parse(time.RFC3339Nano, s) - if t.IsZero() { - // fallback current time - t = time.Now() - } - return + t, _ = time.Parse(time.RFC3339Nano, s) + if t.IsZero() { + // fallback current time + t = time.Now() + } + return } diff --git a/honeypot.log b/honeypot.log index 0fd4a02..5086806 100644 --- a/honeypot.log +++ b/honeypot.log @@ -2,3 +2,4 @@ {"timestamp":"2025-09-27T21:34:21.343267479Z","remote_addr":"127.0.0.1","remote_port":"59260","service":"ssh","details":{"attempt":"1","client":"SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14","password":"asxcbvc","session_id":"1869413c69661475","username":"bob"},"raw_payload":"auth_attempt: map[attempt:1 client:SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14 password:asxcbvc session_id:1869413c69661475 username:bob]"} {"timestamp":"2025-09-27T21:34:24.503353375Z","remote_addr":"127.0.0.1","remote_port":"59260","service":"ssh","details":{"attempt":"2","client":"SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14","password":"ascojmnrhe[pom","session_id":"1869413c69661475","username":"bob"},"raw_payload":"auth_attempt: map[attempt:2 client:SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14 password:ascojmnrhe[pom session_id:1869413c69661475 username:bob]"} {"timestamp":"2025-09-27T21:34:30.534724871Z","remote_addr":"127.0.0.1","remote_port":"59260","service":"ssh","details":{"auth_attempts":"2","duration_sec":"13.35","error":"[ssh: no auth passed yet, permission denied, permission denied]","last_password":"ascojmnrhe[pom","last_username":"bob","session_id":"1869413c69661475"},"raw_payload":"session_end: map[auth_attempts:2 duration_sec:13.35 error:[ssh: no auth passed yet, permission denied, permission denied] last_password:ascojmnrhe[pom last_username:bob session_id:1869413c69661475]"} +{"timestamp":"2025-09-28T06:17:13.052200075Z","remote_addr":"127.0.0.1","remote_port":"59496","service":"ssh","details":{"auth_attempts":"0","duration_sec":"0.19","error":"read tcp 127.0.0.1:2222-\u003e127.0.0.1:59496: read: connection reset by peer","last_password":"","last_username":"","session_id":"18695dc5a178d6df"},"raw_payload":"session_end: map[auth_attempts:0 duration_sec:0.19 error:read tcp 127.0.0.1:2222-\u003e127.0.0.1:59496: read: connection reset by peer last_password: last_username: session_id:18695dc5a178d6df]"} diff --git a/threat_intel.json b/threat_intel.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/threat_intel.json @@ -0,0 +1 @@ +{} \ No newline at end of file