mirror of
				https://github.com/r-smith/deceptifeed.git
				synced 2025-11-03 21:53:39 +00:00 
			
		
		
		
	This change adds optional support for running the threat feed server over HTTPS. This is controlled via the configuration file. Depending on the confgiuration, the threat feed may operate over either HTTP or HTTPS, but not both. The following configuration options are added to the threat feed (the `<threatFeed>` section in the conffguration file): - `<enableTLS>` - If `true`, the threat feed uses TLS. If `false` or if this is missing, use HTTP. - `<certPath>` - Path to TLS cert in PEM format. - `<keyPath>` - Path to private key in PEM format. Default configuration files are updated to include the new settings. The TLS feature is off by default. Existing user configuration files only need to be updated if this feature is needed. Otherwise, existing configuration files start the threat feed using HTTP as before. When the threat feed server starts in TLS mode, it automatically generates a self-signed cert if the cert and key files aen't found.
		
			
				
	
	
		
			289 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			289 lines
		
	
	
		
			8.5 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"`
 | 
						|
	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 {
 | 
						|
						// Remove 'message' and 'log level' fields from output.
 | 
						|
						if a.Key == slog.MessageKey || a.Key == slog.LevelKey {
 | 
						|
							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()
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |