mirror of
https://github.com/r-smith/deceptifeed.git
synced 2025-11-01 20:53:54 +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()
|
|
}
|
|
}
|
|
}
|