mirror of
https://github.com/r-smith/deceptifeed.git
synced 2025-10-23 08:22:21 +00:00
This change adds a configuration option to enable Proxy Protocol (not yet implemented). Planned for TCP and SSH honeypot types. Use `<useProxyProtocol>true</useProxyProtocol>` in the XML config.
296 lines
8.7 KiB
Go
296 lines
8.7 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
|
|
"github.com/r-smith/deceptifeed/internal/logmonitor"
|
|
"github.com/r-smith/deceptifeed/internal/logrotate"
|
|
)
|
|
|
|
// Version stores Deceptifeed's version number. This variable is set at build
|
|
// time using the `-X` option with `-ldflags` and is assigned the latest Git
|
|
// tag. Refer to the Makefile in the project root for details on how it's set.
|
|
var Version = "undefined"
|
|
|
|
// This block of constants defines the default application settings when no
|
|
// configuration file is provided.
|
|
const (
|
|
DefaultEnableHTTP = true
|
|
DefaultEnableHTTPS = true
|
|
DefaultEnableSSH = true
|
|
DefaultEnableThreatFeed = true
|
|
DefaultPortHTTP = "8080"
|
|
DefaultPortHTTPS = "8443"
|
|
DefaultPortSSH = "2222"
|
|
DefaultPortThreatFeed = "9000"
|
|
DefaultThreatExpiryHours = 336
|
|
DefaultThreatDatabasePath = "deceptifeed-database.csv"
|
|
DefaultThreatIncludePrivate = true
|
|
DefaultLogPath = "deceptifeed-log.txt"
|
|
DefaultHomePagePath = ""
|
|
DefaultCertPathHTTPS = "deceptifeed-https.crt"
|
|
DefaultKeyPathHTTPS = "deceptifeed-https.key"
|
|
DefaultKeyPathSSH = "deceptifeed-ssh.key"
|
|
DefaultBannerSSH = "SSH-2.0-OpenSSH_9.6"
|
|
)
|
|
|
|
// ServerType represents the different types of honeypot servers that can be
|
|
// deployed. Each type has its own specific handlers and behavior.
|
|
type ServerType int
|
|
|
|
const (
|
|
HTTP ServerType = iota
|
|
HTTPS
|
|
SSH
|
|
TCP
|
|
UDP
|
|
)
|
|
|
|
// String returns a string represenation of ServerType.
|
|
func (t ServerType) String() string {
|
|
return [...]string{"http", "https", "ssh", "tcp", "udp"}[t]
|
|
}
|
|
|
|
// UnmarshalXMLAttr unmarshals the XML 'type' attribute from 'server' elements
|
|
// into a ServerType.
|
|
//
|
|
// Example XML snippet:
|
|
// <server type="http"><enabled>true</enabled></server>
|
|
func (t *ServerType) UnmarshalXMLAttr(attr xml.Attr) error {
|
|
switch attr.Value {
|
|
case "http":
|
|
*t = HTTP
|
|
case "https":
|
|
*t = HTTPS
|
|
case "ssh":
|
|
*t = SSH
|
|
case "tcp":
|
|
*t = TCP
|
|
case "udp":
|
|
*t = UDP
|
|
default:
|
|
return fmt.Errorf("invalid server type: %s", attr.Value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Config holds the configuration settings for the application. It contains the
|
|
// logger, settings for managing a threat feed, and the collection of honeypot
|
|
// servers that are configured to run.
|
|
type Config struct {
|
|
LogPath string `xml:"defaultLogPath"`
|
|
Servers []Server `xml:"honeypotServers>server"`
|
|
ThreatFeed ThreatFeed `xml:"threatFeed"`
|
|
FilePath string `xml:"-"`
|
|
Monitor *logmonitor.Monitor `xml:"-"`
|
|
}
|
|
|
|
// Server represents a honeypot server with its relevant settings.
|
|
type Server struct {
|
|
Type ServerType `xml:"type,attr"`
|
|
Enabled bool `xml:"enabled"`
|
|
Port string `xml:"port"`
|
|
CertPath string `xml:"certPath"`
|
|
KeyPath string `xml:"keyPath"`
|
|
HomePagePath string `xml:"homePagePath"`
|
|
ErrorPagePath string `xml:"errorPagePath"`
|
|
Banner string `xml:"banner"`
|
|
Headers []string `xml:"headers>header"`
|
|
Prompts []Prompt `xml:"prompts>prompt"`
|
|
SendToThreatFeed bool `xml:"sendToThreatFeed"`
|
|
UseProxyProtocol bool `xml:"useProxyProtocol"`
|
|
Rules Rules `xml:"rules"`
|
|
SourceIPHeader string `xml:"sourceIpHeader"`
|
|
LogPath string `xml:"logPath"`
|
|
LogEnabled bool `xml:"logEnabled"`
|
|
LogFile *logrotate.File `xml:"-"`
|
|
Logger *slog.Logger `xml:"-"`
|
|
}
|
|
|
|
type Rules struct {
|
|
Include []Rule `xml:"include"`
|
|
Exclude []Rule `xml:"exclude"`
|
|
}
|
|
|
|
type Rule struct {
|
|
Target string `xml:"target,attr"`
|
|
Pattern string `xml:",chardata"`
|
|
Negate bool `xml:"negate,attr"`
|
|
}
|
|
|
|
// Prompt represents a text prompt that can be displayed to connecting clients
|
|
// when using the TCP-type honeypot server. Each prompt waits for input and
|
|
// logs the response. A Server can include multiple prompts which are displayed
|
|
// one at a time. The optional Log field gives a description when logging the
|
|
// response.
|
|
type Prompt struct {
|
|
Text string `xml:",chardata"`
|
|
Log string `xml:"log,attr"`
|
|
}
|
|
|
|
// ThreatFeed represents an optional HTTP server that serves a list of IP
|
|
// addresses observed interacting with your honeypot servers. This server
|
|
// outputs data in a format compatible with most enterprise firewalls, which
|
|
// can be configured to automatically block communication with IP addresses
|
|
// appearing in the threat feed.
|
|
type ThreatFeed struct {
|
|
Enabled bool `xml:"enabled"`
|
|
Port string `xml:"port"`
|
|
DatabasePath string `xml:"databasePath"`
|
|
ExpiryHours int `xml:"threatExpiryHours"`
|
|
IsPrivateIncluded bool `xml:"includePrivateIPs"`
|
|
ExcludeListPath string `xml:"excludeListPath"`
|
|
EnableTLS bool `xml:"enableTLS"`
|
|
CertPath string `xml:"certPath"`
|
|
KeyPath string `xml:"keyPath"`
|
|
}
|
|
|
|
// Load reads an optional XML configuration file and unmarshals its contents
|
|
// into a Config struct. Any errors encountered opening or decoding the file
|
|
// are returned. When decoding is successful, the populated Config struct is
|
|
// returned.
|
|
func Load(filename string) (*Config, error) {
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
var config Config
|
|
absPath, err := filepath.Abs(filename)
|
|
if err != nil {
|
|
config.FilePath = filename
|
|
} else {
|
|
config.FilePath = absPath
|
|
}
|
|
|
|
xmlBytes, _ := io.ReadAll(file)
|
|
err = xml.Unmarshal(xmlBytes, &config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range config.Servers {
|
|
// Use the global log path if the server log path is not specified.
|
|
if len(config.Servers[i].LogPath) == 0 {
|
|
config.Servers[i].LogPath = config.LogPath
|
|
}
|
|
|
|
// Validate regex rules.
|
|
if err := validateRegexRules(config.Servers[i].Rules); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use the default SSH banner if no banner is specified.
|
|
if config.Servers[i].Type == SSH && len(config.Servers[i].Banner) == 0 {
|
|
config.Servers[i].Banner = DefaultBannerSSH
|
|
}
|
|
|
|
// Explicitly disable threat feed for UDP honeypots.
|
|
if config.Servers[i].Type == UDP {
|
|
config.Servers[i].SendToThreatFeed = false
|
|
}
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
// validateRegexRules checks the validity of regex patterns in the rules.
|
|
func validateRegexRules(rules Rules) error {
|
|
for _, rule := range rules.Include {
|
|
if _, err := regexp.Compile(rule.Pattern); err != nil {
|
|
return fmt.Errorf("invalid regex pattern: %s", rule.Pattern)
|
|
}
|
|
}
|
|
for _, rule := range rules.Exclude {
|
|
if _, err := regexp.Compile(rule.Pattern); err != nil {
|
|
return fmt.Errorf("invalid regex pattern: %s", rule.Pattern)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// InitializeLoggers creates structured loggers for each server. It opens log
|
|
// files using the server's specified log path, defaulting to the global log
|
|
// path if none is provided.
|
|
func (c *Config) InitializeLoggers() error {
|
|
const maxSize = 50
|
|
c.Monitor = logmonitor.New()
|
|
|
|
openedLogFiles := make(map[string]*slog.Logger)
|
|
|
|
for i := range c.Servers {
|
|
if !c.Servers[i].Enabled {
|
|
continue
|
|
}
|
|
|
|
logPath := c.Servers[i].LogPath
|
|
|
|
// If no log path is specified or if logging is disabled, discard logs.
|
|
if len(logPath) == 0 || !c.Servers[i].LogEnabled {
|
|
c.Servers[i].Logger = slog.New(slog.DiscardHandler)
|
|
continue
|
|
}
|
|
|
|
// Check if this log path has already been opened. If so, reuse the
|
|
// logger.
|
|
if logger, exists := openedLogFiles[logPath]; exists {
|
|
c.Servers[i].Logger = logger
|
|
continue
|
|
}
|
|
|
|
// Open the specified log file.
|
|
file, err := logrotate.OpenFile(logPath, maxSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a JSON logger with two writers: one writes to disk using file
|
|
// rotation, the other writes to a channel for live monitoring.
|
|
logger := slog.New(
|
|
slog.NewJSONHandler(
|
|
io.MultiWriter(file, c.Monitor),
|
|
&slog.HandlerOptions{
|
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
|
switch a.Key {
|
|
case slog.MessageKey, slog.LevelKey:
|
|
// Remove default 'message' and 'log level' fields.
|
|
return slog.Attr{}
|
|
case "source_ip_error":
|
|
// Remove 'source_ip_error' field if it's empty.
|
|
if len(a.Value.String()) == 0 {
|
|
return slog.Attr{}
|
|
}
|
|
}
|
|
return a
|
|
},
|
|
},
|
|
),
|
|
)
|
|
|
|
c.Servers[i].Logger = logger
|
|
c.Servers[i].LogFile = file
|
|
|
|
// Store the logger for reuse.
|
|
openedLogFiles[logPath] = logger
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CloseLogFiles closes all open log file handles for the servers. This
|
|
// function should be called when the application is shutting down.
|
|
func (c *Config) CloseLogFiles() {
|
|
for i := range c.Servers {
|
|
if c.Servers[i].LogFile != nil {
|
|
_ = c.Servers[i].LogFile.Close()
|
|
}
|
|
}
|
|
}
|