mirror of
https://github.com/r-smith/deceptifeed.git
synced 2025-10-23 00:12:22 +00:00
Compare commits
3 Commits
dc06d64b5b
...
a9dcc759f7
Author | SHA1 | Date | |
---|---|---|---|
|
a9dcc759f7 | ||
|
f9d7b767bc | ||
|
375da6eeac |
6
go.mod
6
go.mod
@@ -3,8 +3,8 @@ module github.com/r-smith/deceptifeed
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/net v0.39.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/net v0.40.0
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.32.0 // indirect
|
||||
require golang.org/x/sys v0.33.0 // indirect
|
||||
|
16
go.sum
16
go.sum
@@ -1,8 +1,8 @@
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
|
@@ -11,6 +11,7 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -148,65 +149,100 @@ func listenHTTPS(cfg *config.Server, response *responseConfig) {
|
||||
// based on the honeypot configuration.
|
||||
func handleConnection(cfg *config.Server, customHeaders map[string]string, response *responseConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Log details of the incoming HTTP request.
|
||||
// Log connection details. The log fields and format differ based on
|
||||
// whether a custom source IP header is configured.
|
||||
dst_ip, dst_port := getLocalAddr(r)
|
||||
src_ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
username, password, isAuth := r.BasicAuth()
|
||||
if isAuth {
|
||||
cfg.Logger.LogAttrs(context.Background(), slog.LevelInfo, "",
|
||||
logData := []slog.Attr{}
|
||||
if len(cfg.SourceIPHeader) > 0 {
|
||||
// A custom source IP header is configured. Set rem_ip to the
|
||||
// original connecting IP and src_ip to the IP from the header. If
|
||||
// the header is missing, invalid, contains multiple IPs, or if
|
||||
// there a multiple headers with the same name, parsing will fail,
|
||||
// and src_ip will fallback to the original connecting IP.
|
||||
rem_ip := src_ip
|
||||
header := r.Header[cfg.SourceIPHeader]
|
||||
parsed := false
|
||||
errMsg := ""
|
||||
switch len(header) {
|
||||
case 0:
|
||||
errMsg = "Missing header " + cfg.SourceIPHeader
|
||||
case 1:
|
||||
v := header[0]
|
||||
if _, err := netip.ParseAddr(v); err != nil {
|
||||
if strings.Contains(v, ",") {
|
||||
errMsg = "Multiple values in header " + cfg.SourceIPHeader
|
||||
} else {
|
||||
errMsg = "Invalid IP in header " + cfg.SourceIPHeader
|
||||
}
|
||||
} else {
|
||||
parsed = true
|
||||
src_ip = v
|
||||
}
|
||||
default:
|
||||
errMsg = "Multiple instances of header " + cfg.SourceIPHeader
|
||||
}
|
||||
|
||||
logData = append(logData,
|
||||
slog.String("event_type", "http"),
|
||||
slog.String("remote_ip", rem_ip),
|
||||
slog.String("source_ip", src_ip),
|
||||
slog.Bool("source_ip_parsed", parsed),
|
||||
)
|
||||
if !parsed {
|
||||
logData = append(logData, slog.String("source_ip_error", errMsg))
|
||||
}
|
||||
logData = append(logData,
|
||||
slog.String("server_ip", dst_ip),
|
||||
slog.String("server_port", dst_port),
|
||||
slog.String("server_name", config.GetHostname()),
|
||||
slog.Group("event_details",
|
||||
slog.String("method", r.Method),
|
||||
slog.String("path", r.URL.Path),
|
||||
slog.String("query", r.URL.RawQuery),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("protocol", r.Proto),
|
||||
slog.String("host", r.Host),
|
||||
slog.Group("basic_auth",
|
||||
slog.String("username", username),
|
||||
slog.String("password", password),
|
||||
),
|
||||
slog.Any("headers", flattenHeaders(r.Header)),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
cfg.Logger.LogAttrs(context.Background(), slog.LevelInfo, "",
|
||||
// No custom source IP header is configured. Log the standard
|
||||
// connection details, keeping src_ip as the remote connecting IP.
|
||||
logData = append(logData,
|
||||
slog.String("event_type", "http"),
|
||||
slog.String("source_ip", src_ip),
|
||||
slog.String("server_ip", dst_ip),
|
||||
slog.String("server_port", dst_port),
|
||||
slog.String("server_name", config.GetHostname()),
|
||||
slog.Group("event_details",
|
||||
slog.String("method", r.Method),
|
||||
slog.String("path", r.URL.Path),
|
||||
slog.String("query", r.URL.RawQuery),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("protocol", r.Proto),
|
||||
slog.String("host", r.Host),
|
||||
slog.Any("headers", flattenHeaders(r.Header)),
|
||||
)
|
||||
}
|
||||
|
||||
// Log standard HTTP request information.
|
||||
eventDetails := []any{
|
||||
slog.String("method", r.Method),
|
||||
slog.String("path", r.URL.Path),
|
||||
slog.String("query", r.URL.RawQuery),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("protocol", r.Proto),
|
||||
slog.String("host", r.Host),
|
||||
slog.Any("headers", flattenHeaders(r.Header)),
|
||||
}
|
||||
|
||||
// If the request includes a "basic" Authorization header, decode and
|
||||
// log the credentials.
|
||||
if username, password, ok := r.BasicAuth(); ok {
|
||||
eventDetails = append(eventDetails,
|
||||
slog.Group("basic_auth",
|
||||
slog.String("username", username),
|
||||
slog.String("password", password),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Combine log data and write the final log entry.
|
||||
logData = append(logData, slog.Group("event_details", eventDetails...))
|
||||
cfg.Logger.LogAttrs(context.Background(), slog.LevelInfo, "", logData...)
|
||||
|
||||
// Print a simplified version of the request to the console.
|
||||
fmt.Printf("[HTTP] %s %s %s %s\n", src_ip, r.Method, r.URL.Path, r.URL.RawQuery)
|
||||
|
||||
// Update the threat feed with the source IP address from the request.
|
||||
// If the configuration specifies an HTTP header to be used for the
|
||||
// source IP, retrieve the header value and use it instead of the
|
||||
// connecting IP.
|
||||
// Update the threat feed using the source IP address (src_ip). If a
|
||||
// custom header is configured, src_ip contains the IP extracted from
|
||||
// the header. Otherwise, it contains the remote connecting IP.
|
||||
if shouldUpdateThreatFeed(cfg, r) {
|
||||
src := src_ip
|
||||
if len(cfg.SourceIPHeader) > 0 {
|
||||
if header := r.Header.Get(cfg.SourceIPHeader); len(header) > 0 {
|
||||
src = header
|
||||
}
|
||||
}
|
||||
threatfeed.Update(src)
|
||||
threatfeed.Update(src_ip)
|
||||
}
|
||||
|
||||
// Apply optional custom HTTP response headers.
|
||||
|
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -65,11 +65,8 @@ var (
|
||||
func Update(ip string) {
|
||||
// Check if the given IP string is a private address. The threat feed may
|
||||
// be configured to include or exclude private IPs.
|
||||
netIP := net.ParseIP(ip)
|
||||
if netIP == nil || netIP.IsLoopback() {
|
||||
return
|
||||
}
|
||||
if !cfg.ThreatFeed.IsPrivateIncluded && netIP.IsPrivate() {
|
||||
parsedIP, err := netip.ParseAddr(ip)
|
||||
if err != nil || parsedIP.IsLoopback() || (!cfg.ThreatFeed.IsPrivateIncluded && parsedIP.IsPrivate()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -2,10 +2,9 @@ package threatfeed
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"cmp"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -16,11 +15,11 @@ import (
|
||||
|
||||
// feedEntry represents an individual entry in the threat feed.
|
||||
type feedEntry struct {
|
||||
IP string `json:"ip"`
|
||||
IPBytes net.IP `json:"-"`
|
||||
Added time.Time `json:"added"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Observations int `json:"observations"`
|
||||
IP string `json:"ip"`
|
||||
IPBytes netip.Addr `json:"-"`
|
||||
Added time.Time `json:"added"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Observations int `json:"observations"`
|
||||
}
|
||||
|
||||
// feedEntries is a slice of feedEntry structs. It represents the threat feed
|
||||
@@ -86,13 +85,13 @@ loop:
|
||||
continue
|
||||
}
|
||||
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil || (parsedIP.IsPrivate() && !cfg.ThreatFeed.IsPrivateIncluded) {
|
||||
parsedIP, err := netip.ParseAddr(ip)
|
||||
if err != nil || (parsedIP.IsPrivate() && !cfg.ThreatFeed.IsPrivateIncluded) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ipnet := range excludedCIDR {
|
||||
if ipnet.Contains(parsedIP) {
|
||||
for _, prefix := range excludedCIDR {
|
||||
if prefix.Contains(parsedIP) {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
@@ -120,9 +119,9 @@ loop:
|
||||
// should contain an IP address or CIDR. It returns a map of the unique IPs and
|
||||
// a slice of the CIDR ranges found in the file. The file may include comments
|
||||
// using "#". The "#" symbol on a line and everything after is ignored.
|
||||
func parseExcludeList(filepath string) (map[string]struct{}, []*net.IPNet, error) {
|
||||
func parseExcludeList(filepath string) (map[string]struct{}, []netip.Prefix, error) {
|
||||
if len(filepath) == 0 {
|
||||
return map[string]struct{}{}, []*net.IPNet{}, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
f, err := os.Open(filepath)
|
||||
@@ -134,22 +133,24 @@ func parseExcludeList(filepath string) (map[string]struct{}, []*net.IPNet, error
|
||||
// `ips` stores individual IPs to exclude, and `cidr` stores CIDR networks
|
||||
// to exclude.
|
||||
ips := make(map[string]struct{})
|
||||
cidr := []*net.IPNet{}
|
||||
cidr := []netip.Prefix{}
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
line := scanner.Text()
|
||||
|
||||
// Remove comments from text.
|
||||
if i := strings.Index(line, "#"); i != -1 {
|
||||
line = strings.TrimSpace(line[:i])
|
||||
// Remove comments and trim.
|
||||
if i := strings.IndexByte(line, '#'); i != -1 {
|
||||
line = line[:i]
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(line) > 0 {
|
||||
if _, ipnet, err := net.ParseCIDR(line); err == nil {
|
||||
cidr = append(cidr, ipnet)
|
||||
} else {
|
||||
ips[line] = struct{}{}
|
||||
}
|
||||
if prefix, err := netip.ParsePrefix(line); err == nil {
|
||||
cidr = append(cidr, prefix)
|
||||
} else {
|
||||
ips[line] = struct{}{}
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
@@ -164,13 +165,13 @@ func (f feedEntries) applySort(method sortMethod, direction sortDirection) {
|
||||
switch method {
|
||||
case byIP:
|
||||
slices.SortFunc(f, func(a, b feedEntry) int {
|
||||
return bytes.Compare(a.IPBytes, b.IPBytes)
|
||||
return a.IPBytes.Compare(b.IPBytes)
|
||||
})
|
||||
case byLastSeen:
|
||||
slices.SortFunc(f, func(a, b feedEntry) int {
|
||||
t := a.LastSeen.Compare(b.LastSeen)
|
||||
if t == 0 {
|
||||
return bytes.Compare(a.IPBytes, b.IPBytes)
|
||||
return a.IPBytes.Compare(b.IPBytes)
|
||||
}
|
||||
return t
|
||||
})
|
||||
@@ -178,7 +179,7 @@ func (f feedEntries) applySort(method sortMethod, direction sortDirection) {
|
||||
slices.SortFunc(f, func(a, b feedEntry) int {
|
||||
t := a.Added.Compare(b.Added)
|
||||
if t == 0 {
|
||||
return bytes.Compare(a.IPBytes, b.IPBytes)
|
||||
return a.IPBytes.Compare(b.IPBytes)
|
||||
}
|
||||
return t
|
||||
})
|
||||
@@ -186,7 +187,7 @@ func (f feedEntries) applySort(method sortMethod, direction sortDirection) {
|
||||
slices.SortFunc(f, func(a, b feedEntry) int {
|
||||
t := cmp.Compare(a.Observations, b.Observations)
|
||||
if t == 0 {
|
||||
return bytes.Compare(a.IPBytes, b.IPBytes)
|
||||
return a.IPBytes.Compare(b.IPBytes)
|
||||
}
|
||||
return t
|
||||
})
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
@@ -65,7 +66,7 @@ func handleWebSocket(ws *websocket.Conn) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if netIP := net.ParseIP(ip); !netIP.IsPrivate() && !netIP.IsLoopback() {
|
||||
if parsedIP, err := netip.ParseAddr(ip); err != nil || (!parsedIP.IsPrivate() && !parsedIP.IsLoopback()) {
|
||||
return
|
||||
}
|
||||
fmt.Println("[Threat Feed]", ip, "established WebSocket connection")
|
||||
|
@@ -3,6 +3,7 @@ package threatfeed
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// enforcePrivateIP is a middleware that restricts access to the HTTP server
|
||||
@@ -12,11 +13,11 @@ func enforcePrivateIP(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not get IP", http.StatusInternalServerError)
|
||||
http.Error(w, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if netIP := net.ParseIP(ip); !netIP.IsPrivate() && !netIP.IsLoopback() {
|
||||
if parsedIP, err := netip.ParseAddr(ip); err != nil || (!parsedIP.IsPrivate() && !parsedIP.IsLoopback()) {
|
||||
http.Error(w, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
Reference in New Issue
Block a user