305 lines
5.9 KiB
Go
305 lines
5.9 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
ioc.go
|
|
|
|
Indicator of Compromise extraction from honeypot events
|
|
|
|
Parses event service data to extract actionable IOCs: IP addresses,
|
|
URLs from download commands, domains from HTTP requests, and
|
|
credential pairs. Each IOC is tagged with its source context and
|
|
assigned a confidence score based on the extraction method.
|
|
*/
|
|
|
|
package intel
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/CarterPerez-dev/hive/pkg/types"
|
|
)
|
|
|
|
var urlPattern = regexp.MustCompile(
|
|
`https?://[^\s'"` + "`" + `<>]+`,
|
|
)
|
|
|
|
var domainPattern = regexp.MustCompile(
|
|
`https?://([^/:]+)`,
|
|
)
|
|
|
|
type Extractor struct{}
|
|
|
|
func NewExtractor() *Extractor {
|
|
return &Extractor{}
|
|
}
|
|
|
|
func (e *Extractor) Extract(
|
|
ev *types.Event,
|
|
) []*types.IOC {
|
|
var iocs []*types.IOC
|
|
|
|
iocs = append(iocs, e.extractSourceIP(ev)...)
|
|
iocs = append(iocs, e.extractFromData(ev)...)
|
|
|
|
return iocs
|
|
}
|
|
|
|
func (e *Extractor) extractSourceIP(
|
|
ev *types.Event,
|
|
) []*types.IOC {
|
|
if ev.SourceIP == "" {
|
|
return nil
|
|
}
|
|
|
|
ip := net.ParseIP(ev.SourceIP)
|
|
if ip == nil {
|
|
return nil
|
|
}
|
|
|
|
if ip.IsLoopback() || ip.IsPrivate() {
|
|
return nil
|
|
}
|
|
|
|
iocType := types.IOCIPv4
|
|
if ip.To4() == nil {
|
|
iocType = types.IOCIPv6
|
|
}
|
|
|
|
return []*types.IOC{{
|
|
Type: iocType,
|
|
Value: ev.SourceIP,
|
|
FirstSeen: ev.Timestamp,
|
|
LastSeen: ev.Timestamp,
|
|
SightCount: 1,
|
|
Confidence: confidenceForEvent(ev),
|
|
Source: "honeypot:" + ev.ServiceType.String(),
|
|
Tags: eventTags(ev),
|
|
}}
|
|
}
|
|
|
|
func (e *Extractor) extractFromData(
|
|
ev *types.Event,
|
|
) []*types.IOC {
|
|
if len(ev.ServiceData) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var parsed map[string]interface{}
|
|
if json.Unmarshal(ev.ServiceData, &parsed) != nil {
|
|
return nil
|
|
}
|
|
|
|
var iocs []*types.IOC
|
|
|
|
iocs = append(iocs, extractURLs(parsed, ev)...)
|
|
iocs = append(
|
|
iocs, extractUserAgents(parsed, ev)...,
|
|
)
|
|
|
|
return iocs
|
|
}
|
|
|
|
func extractURLs(
|
|
data map[string]interface{},
|
|
ev *types.Event,
|
|
) []*types.IOC {
|
|
var iocs []*types.IOC
|
|
|
|
for _, key := range []string{
|
|
"command", "raw", "query", "body", "path",
|
|
} {
|
|
val, ok := data[key].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
urls := urlPattern.FindAllString(val, -1)
|
|
for _, u := range urls {
|
|
iocs = append(iocs, &types.IOC{
|
|
Type: types.IOCURL,
|
|
Value: u,
|
|
FirstSeen: ev.Timestamp,
|
|
LastSeen: ev.Timestamp,
|
|
SightCount: 1,
|
|
Confidence: 80,
|
|
Source: "honeypot:" + ev.ServiceType.String(),
|
|
Tags: []string{"extracted-url"},
|
|
})
|
|
|
|
matches := domainPattern.FindStringSubmatch(u)
|
|
if len(matches) > 1 {
|
|
domain := matches[1]
|
|
if net.ParseIP(domain) == nil {
|
|
iocs = append(iocs, &types.IOC{
|
|
Type: types.IOCDomain,
|
|
Value: domain,
|
|
FirstSeen: ev.Timestamp,
|
|
LastSeen: ev.Timestamp,
|
|
SightCount: 1,
|
|
Confidence: 70,
|
|
Source: "honeypot:" + ev.ServiceType.String(),
|
|
Tags: []string{"extracted-domain"},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return iocs
|
|
}
|
|
|
|
func extractUserAgents(
|
|
data map[string]interface{},
|
|
ev *types.Event,
|
|
) []*types.IOC {
|
|
ua, ok := data["user_agent"].(string)
|
|
if !ok || ua == "" {
|
|
return nil
|
|
}
|
|
|
|
family := ClassifyHTTPClient(ua)
|
|
if family == "" || !IsKnownAttackTool(family) {
|
|
return nil
|
|
}
|
|
|
|
return []*types.IOC{{
|
|
Type: types.IOCUserAgent,
|
|
Value: ua,
|
|
FirstSeen: ev.Timestamp,
|
|
LastSeen: ev.Timestamp,
|
|
SightCount: 1,
|
|
Confidence: 90,
|
|
Source: "honeypot:" + ev.ServiceType.String(),
|
|
Tags: []string{"attack-tool", "tool:" + family},
|
|
}}
|
|
}
|
|
|
|
func confidenceForEvent(ev *types.Event) int {
|
|
switch ev.EventType {
|
|
case types.EventExploit:
|
|
return 95
|
|
case types.EventCommand:
|
|
return 85
|
|
case types.EventLoginSuccess, types.EventLoginFailed:
|
|
return 80
|
|
case types.EventScan:
|
|
return 70
|
|
case types.EventConnect:
|
|
return 50
|
|
default:
|
|
return 60
|
|
}
|
|
}
|
|
|
|
func eventTags(ev *types.Event) []string {
|
|
tags := []string{
|
|
"service:" + ev.ServiceType.String(),
|
|
}
|
|
if ev.EventType == types.EventExploit {
|
|
tags = append(tags, "exploit-attempt")
|
|
}
|
|
return tags
|
|
}
|
|
|
|
func ExtractBulk(
|
|
events []*types.Event,
|
|
) []*types.IOC {
|
|
ext := NewExtractor()
|
|
seen := make(map[string]*types.IOC)
|
|
|
|
for _, ev := range events {
|
|
iocs := ext.Extract(ev)
|
|
for _, ioc := range iocs {
|
|
key := ioc.Type.String() + ":" + ioc.Value
|
|
if existing, ok := seen[key]; ok {
|
|
existing.SightCount++
|
|
if ioc.LastSeen.After(existing.LastSeen) {
|
|
existing.LastSeen = ioc.LastSeen
|
|
}
|
|
if ioc.FirstSeen.Before(existing.FirstSeen) {
|
|
existing.FirstSeen = ioc.FirstSeen
|
|
}
|
|
if ioc.Confidence > existing.Confidence {
|
|
existing.Confidence = ioc.Confidence
|
|
}
|
|
existing.Tags = mergeUnique(
|
|
existing.Tags, ioc.Tags,
|
|
)
|
|
} else {
|
|
seen[key] = ioc
|
|
}
|
|
}
|
|
}
|
|
|
|
result := make([]*types.IOC, 0, len(seen))
|
|
for _, ioc := range seen {
|
|
result = append(result, ioc)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func mergeUnique(a, b []string) []string {
|
|
set := make(map[string]bool, len(a)+len(b))
|
|
for _, s := range a {
|
|
set[s] = true
|
|
}
|
|
for _, s := range b {
|
|
set[s] = true
|
|
}
|
|
result := make([]string, 0, len(set))
|
|
for s := range set {
|
|
result = append(result, s)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func FilterIPs(
|
|
iocs []*types.IOC,
|
|
) []*types.IOC {
|
|
var ips []*types.IOC
|
|
for _, ioc := range iocs {
|
|
if ioc.Type == types.IOCIPv4 ||
|
|
ioc.Type == types.IOCIPv6 {
|
|
ips = append(ips, ioc)
|
|
}
|
|
}
|
|
return ips
|
|
}
|
|
|
|
func FilterByMinConfidence(
|
|
iocs []*types.IOC, minConf int,
|
|
) []*types.IOC {
|
|
var filtered []*types.IOC
|
|
for _, ioc := range iocs {
|
|
if ioc.Confidence >= minConf {
|
|
filtered = append(filtered, ioc)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func FilterByAge(
|
|
iocs []*types.IOC, maxAge time.Duration,
|
|
) []*types.IOC {
|
|
cutoff := time.Now().UTC().Add(-maxAge)
|
|
var filtered []*types.IOC
|
|
for _, ioc := range iocs {
|
|
if ioc.LastSeen.After(cutoff) {
|
|
filtered = append(filtered, ioc)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func HasTag(ioc *types.IOC, tag string) bool {
|
|
for _, t := range ioc.Tags {
|
|
if strings.EqualFold(t, tag) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|