internal/honeypot/honeypot.go

package honeypot

import (
	"context"
	"fmt"
	"honeypot/internal/database"
	"honeypot/internal/types"
	"log"
	"log/slog"
	"strings"
	"time"
)

// Honeypot is the interface that all honeypot implementations must satisfy.
type Honeypot interface {
	// Name returns the name of the honeypot (e.g., "ssh", "http").
	Name() types.HoneypotType

	// Label returns the label of the honeypot used in the UI (e.g., "SSH", "HTTP").
	Label() string

	// Start starts the honeypot server. It should run until the context is cancelled.
	Start(ctx context.Context, logger *slog.Logger) error

	// GetScores returns the scored ip addresses for the honeypot.
	GetScores(db *database.Database, interval string) ScoreMap

	// Ports returns the ports the honeypot is listening on.
	Ports() []uint16
}

type Score struct {
	Score uint        `json:"score"`
	Tags  []types.Tag `json:"tags"`
}

type ScoreMap map[string]Score

type ScoreCache struct {
	Scores      ScoreMap
	LastUpdated time.Time
}

func MergeScores(maps ...ScoreMap) ScoreMap {
	result := ScoreMap{}

	for _, m := range maps {
		for ip, score := range m {
			existing, ok := result[ip]
			if !ok {
				// first time we see this IP
				result[ip] = score
				continue
			}

			// add score
			existing.Score += score.Score

			// merge tags (deduplicated)
			existing.Tags = MergeTags(existing.Tags, score.Tags)

			result[ip] = existing
		}
	}

	return result
}

func MergeTags(a, b []types.Tag) []types.Tag {
	seen := make(map[types.Tag]struct{}, len(a)+len(b))
	out := make([]types.Tag, 0, len(a)+len(b))

	for _, t := range a {
		if _, ok := seen[t]; !ok {
			seen[t] = struct{}{}
			out = append(out, t)
		}
	}
	for _, t := range b {
		if _, ok := seen[t]; !ok {
			seen[t] = struct{}{}
			out = append(out, t)
		}
	}
	return out
}

func GetAuthAttemptScores(db *database.Database, interval string) ScoreMap {
	rows, err := db.DB.Query(fmt.Sprintf(`
		SELECT remote_addr, COUNT(*) AS score
		FROM honeypot_events
		WHERE event = ?
		AND time >= now() - INTERVAL %s
		GROUP BY remote_addr ORDER BY score DESC;
	`, interval), types.EventAuthAttempt)
	if err != nil {
		log.Println("error querying auth attempt scores:", err)
		return ScoreMap{}
	}
	defer rows.Close()

	scores := ScoreMap{}
	for rows.Next() {
		var ip string
		var score uint
		err := rows.Scan(&ip, &score)
		if err != nil {
			log.Println("error getting auth attempt scores:", err)
			return ScoreMap{}
		}
		scores[ip] = Score{Score: score * 100, Tags: []types.Tag{types.TagAuthAttempt}}
	}
	return scores
}

func UpdateBlocklist(db *database.Database, scores ScoreMap) {
	blockCounts, err := db.GetBlockCounts()
	if err != nil {
		log.Println("error getting block counts:", err)
		return
	}

	blockedAddresses, err := db.GetBlockedAddresses()
	if err != nil {
		log.Println("error getting blocked addresses:", err)
		return
	}

	blockedMap := make(map[string]bool)
	for _, entry := range blockedAddresses {
		blockedMap[entry.Address] = true
	}

	for address, score := range scores {
		if blockedMap[address] {
			continue
		}

		blockCount, ok := blockCounts[address]
		if !ok {
			blockCount = 1
		}

		reason := ""

		for _, tag := range score.Tags {
			reason += string(tag) + ","
		}
		reason = strings.TrimSuffix(reason, ",")

		db.InsertBlocklist(address, reason, time.Duration(blockCount)*time.Hour)
	}
}