Skip to content

Commit c73da20

Browse files
committed
feat: add Discord message API and enhance DNS scan commands to support direct subdomain targeting
1 parent f05d8c5 commit c73da20

11 files changed

Lines changed: 257 additions & 424 deletions

File tree

cmd/autoar/main.go

Lines changed: 36 additions & 360 deletions
Large diffs are not rendered by default.

internal/modules/cf1016/cf1016.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,22 @@ var cloudflareCIDRs = []string{
4444
"131.0.72.0/22",
4545
}
4646

47+
// cloudflareCIDRsV6 is the list of Cloudflare's published IPv6 CIDR ranges.
48+
// Source: https://www.cloudflare.com/ips-v6
49+
var cloudflareCIDRsV6 = []string{
50+
"2400:cb00::/32",
51+
"2606:4700::/32",
52+
"2803:f800::/32",
53+
"2405:b500::/32",
54+
"2405:8100::/32",
55+
"2a06:98c0::/29",
56+
"2c0f:f248::/32",
57+
}
58+
4759
var cfNets []*net.IPNet
4860

4961
func init() {
50-
for _, cidr := range cloudflareCIDRs {
62+
for _, cidr := range append(cloudflareCIDRs, cloudflareCIDRsV6...) {
5163
_, network, err := net.ParseCIDR(cidr)
5264
if err == nil {
5365
cfNets = append(cfNets, network)

internal/modules/domain/domain.go

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -582,17 +582,13 @@ func runDomainPhase(phaseKey string, step, total int, description, domain string
582582
}
583583
} else {
584584
log.Printf("[DEBUG] [DOMAIN] No files found for phase %s after retries", phaseKey)
585-
// Send "0 findings" message to webhook only when not under bot
586-
if os.Getenv("AUTOAR_CURRENT_SCAN_ID") == "" {
587-
utils.SendPhaseFiles(phaseKey, domain, []string{})
588-
}
585+
// Always send "0 findings" message, let the discord utility handle routing
586+
utils.SendPhaseFiles(phaseKey, domain, []string{})
589587
}
590588
} else {
591589
log.Printf("[DEBUG] [DOMAIN] No expected files for phase %s", phaseKey)
592-
// Send "0 findings" message to webhook only when not under bot
593-
if os.Getenv("AUTOAR_CURRENT_SCAN_ID") == "" {
594-
utils.SendPhaseFiles(phaseKey, domain, []string{})
595-
}
590+
// Always send "0 findings" message, let the discord utility handle routing
591+
utils.SendPhaseFiles(phaseKey, domain, []string{})
596592
}
597593
}
598594

@@ -615,16 +611,4 @@ func runWithTimeout(fn func() error, timeout time.Duration) error {
615611
}
616612
}
617613

618-
// formatDomainFileSize formats file size in human-readable format
619-
func formatDomainFileSize(size int64) string {
620-
const unit = 1024
621-
if size < unit {
622-
return fmt.Sprintf("%d B", size)
623-
}
624-
div, exp := int64(unit), 0
625-
for n := size / unit; n >= unit; n /= unit {
626-
div *= unit
627-
exp++
628-
}
629-
return fmt.Sprintf("%.2f %cB", float64(size)/float64(div), "KMGTPE"[exp])
630-
}
614+

internal/modules/gobot/api.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ func setupAPI() *gin.Engine {
165165
internal := r.Group("/internal")
166166
{
167167
internal.POST("/send-file", sendFileToDiscord)
168+
internal.POST("/send-message", sendMessageToDiscord)
168169
}
169170

170171
// List all scans
@@ -1748,4 +1749,78 @@ func sendFileToDiscord(c *gin.Context) {
17481749
c.JSON(http.StatusOK, gin.H{"message": "file sent successfully"})
17491750
}
17501751

1752+
// sendMessageToDiscord handles sending text messages from modules
1753+
func sendMessageToDiscord(c *gin.Context) {
1754+
log.Printf("[API] [sendMessageToDiscord] Received message send request")
1755+
1756+
var req struct {
1757+
ScanID string `json:"scan_id"`
1758+
Message string `json:"message" binding:"required"`
1759+
ChannelID string `json:"channel_id"`
1760+
}
1761+
1762+
if err := c.ShouldBindJSON(&req); err != nil {
1763+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1764+
return
1765+
}
1766+
1767+
var channelID string
1768+
if req.ChannelID != "" {
1769+
channelID = req.ChannelID
1770+
} else if req.ScanID != "" {
1771+
channelID = getChannelID(req.ScanID)
1772+
}
1773+
if channelID == "" {
1774+
channelID = os.Getenv("AUTOAR_CURRENT_CHANNEL_ID")
1775+
}
1776+
if channelID == "" {
1777+
channelID = os.Getenv("DISCORD_DEFAULT_CHANNEL_ID")
1778+
}
1779+
if channelID == "" {
1780+
c.JSON(http.StatusBadRequest, gin.H{"error": "no channel ID found"})
1781+
return
1782+
}
1783+
1784+
threadID := ""
1785+
if req.ScanID != "" {
1786+
scansMutex.RLock()
1787+
if scan, ok := activeScans[req.ScanID]; ok && scan.ThreadID != "" {
1788+
threadID = scan.ThreadID
1789+
}
1790+
scansMutex.RUnlock()
1791+
}
1792+
if threadID == "" && channelID != "" {
1793+
scansMutex.RLock()
1794+
for _, scan := range activeScans {
1795+
if scan.ChannelID == channelID && scan.ThreadID != "" {
1796+
threadID = scan.ThreadID
1797+
break
1798+
}
1799+
}
1800+
scansMutex.RUnlock()
1801+
}
1802+
1803+
targetID := channelID
1804+
if threadID != "" {
1805+
targetID = threadID
1806+
}
1807+
1808+
discordSessionMutex.RLock()
1809+
session := globalDiscordSession
1810+
discordSessionMutex.RUnlock()
1811+
1812+
if session == nil {
1813+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Discord bot not available"})
1814+
return
1815+
}
1816+
1817+
_, err := session.ChannelMessageSend(targetID, req.Message)
1818+
if err != nil {
1819+
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to send message: %v", err)})
1820+
return
1821+
}
1822+
1823+
c.JSON(http.StatusOK, gin.H{"message": "message sent successfully"})
1824+
}
1825+
17511826
// getEnv is defined in main.go

internal/modules/gobot/commands2.go

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,59 +15,106 @@ import (
1515
func handleDNS(s *discordgo.Session, i *discordgo.InteractionCreate) {
1616
options := i.ApplicationCommandData().Options
1717
domain := ""
18+
subdomain := ""
1819
scanType := "takeover" // Default to takeover
1920

2021
for _, opt := range options {
2122
switch opt.Name {
2223
case "domain":
2324
domain = opt.StringValue()
25+
case "subdomain":
26+
subdomain = opt.StringValue()
2427
case "type":
2528
scanType = opt.StringValue()
2629
}
2730
}
2831

29-
if domain == "" {
30-
respond(s, i, "Domain is required", false)
32+
// Strip URL scheme prefixes so users can paste URLs directly
33+
domain = strings.TrimPrefix(strings.TrimPrefix(domain, "https://"), "http://")
34+
domain = strings.TrimSuffix(domain, "/")
35+
subdomain = strings.TrimPrefix(strings.TrimPrefix(subdomain, "https://"), "http://")
36+
subdomain = strings.TrimSuffix(subdomain, "/")
37+
38+
if domain == "" && subdomain == "" {
39+
respond(s, i, "❌ Either **domain** or **subdomain** is required.", false)
3140
return
3241
}
3342

34-
// Map scan type to CLI command
43+
// Determine target label for Discord embed
44+
target := domain
45+
if subdomain != "" {
46+
target = subdomain
47+
}
48+
49+
// Build CLI command — subdomain mode passes -s instead of -d (skips enumeration)
3550
var command []string
3651
var scanName string
3752
switch scanType {
3853
case "cname":
39-
command = []string{autoarScript, "dns", "cname", "-d", domain}
4054
scanName = "DNS CNAME"
55+
if subdomain != "" {
56+
command = []string{autoarScript, "dns", "cname", "-d", subdomain}
57+
} else {
58+
command = []string{autoarScript, "dns", "cname", "-d", domain}
59+
}
4160
case "ns":
42-
command = []string{autoarScript, "dns", "ns", "-d", domain}
4361
scanName = "DNS NS"
62+
if subdomain != "" {
63+
command = []string{autoarScript, "dns", "ns", "-d", subdomain}
64+
} else {
65+
command = []string{autoarScript, "dns", "ns", "-d", domain}
66+
}
4467
case "azure-aws":
45-
command = []string{autoarScript, "dns", "azure-aws", "-d", domain}
4668
scanName = "DNS Azure/AWS"
69+
if subdomain != "" {
70+
command = []string{autoarScript, "dns", "azure-aws", "-d", subdomain}
71+
} else {
72+
command = []string{autoarScript, "dns", "azure-aws", "-d", domain}
73+
}
4774
case "dnsreaper":
48-
command = []string{autoarScript, "dns", "dnsreaper", "-d", domain}
4975
scanName = "DNSReaper"
76+
if subdomain != "" {
77+
command = []string{autoarScript, "dns", "dnsreaper", "-d", subdomain}
78+
} else {
79+
command = []string{autoarScript, "dns", "dnsreaper", "-d", domain}
80+
}
5081
case "dangling-ip":
51-
command = []string{autoarScript, "dns", "dangling-ip", "-d", domain}
5282
scanName = "DNS Dangling IP"
83+
if subdomain != "" {
84+
command = []string{autoarScript, "dns", "dangling-ip", "-d", subdomain}
85+
} else {
86+
command = []string{autoarScript, "dns", "dangling-ip", "-d", domain}
87+
}
5388
case "cf1016":
54-
command = []string{autoarScript, "dns", "cf1016", "-d", domain}
5589
scanName = "Cloudflare 1016 Dangling DNS"
90+
if subdomain != "" {
91+
// -s mode: scan the single subdomain directly, no live-subs.txt needed
92+
command = []string{autoarScript, "dns", "cf1016", "-s", subdomain}
93+
if domain != "" {
94+
command = append(command, "-d", domain)
95+
}
96+
} else {
97+
command = []string{autoarScript, "dns", "cf1016", "-d", domain}
98+
}
5699
default: // takeover
57-
command = []string{autoarScript, "dns", "takeover", "-d", domain}
58100
scanName = "DNS Takeover"
101+
if subdomain != "" {
102+
command = []string{autoarScript, "dns", "takeover", "-d", subdomain}
103+
} else {
104+
command = []string{autoarScript, "dns", "takeover", "-d", domain}
105+
}
59106
}
60107

61108
scanID := fmt.Sprintf("dns_%s_%d", scanType, time.Now().Unix())
62-
embed := createScanEmbed(scanName, domain, "running")
109+
embed := createScanEmbed(scanName, target, "running")
63110
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
64111
Type: discordgo.InteractionResponseChannelMessageWithSource,
65112
Data: &discordgo.InteractionResponseData{
66113
Embeds: []*discordgo.MessageEmbed{embed},
67114
},
68115
})
69116

70-
go runScanBackground(scanID, fmt.Sprintf("dns_%s", scanType), domain, command, s, i)
117+
go runScanBackground(scanID, fmt.Sprintf("dns_%s", scanType), target, command, s, i)
71118
}
72119

73120
// S3 Commands

internal/modules/gobot/commands_registration.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ func registerAllCommands(s *discordgo.Session) {
277277
Name: "dns",
278278
Description: "Run DNS takeover scan",
279279
Options: []*discordgo.ApplicationCommandOption{
280-
{Type: discordgo.ApplicationCommandOptionString, Name: "domain", Description: "The domain", Required: true},
280+
{Type: discordgo.ApplicationCommandOptionString, Name: "domain", Description: "Root domain (enumerates subdomains first)", Required: false},
281+
{Type: discordgo.ApplicationCommandOptionString, Name: "subdomain", Description: "Single subdomain to scan directly (skips enumeration, ideal for cf1016)", Required: false},
281282
{Type: discordgo.ApplicationCommandOptionString, Name: "type", Description: "Scan type: takeover (all), cname, ns, azure-aws, dnsreaper, dangling-ip, cf1016", Required: false, Choices: []*discordgo.ApplicationCommandOptionChoice{
282283
{Name: "Takeover (All)", Value: "takeover"},
283284
{Name: "CNAME", Value: "cname"},

internal/modules/lite/lite.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -381,17 +381,13 @@ func runPhase(phaseKey string, step, total int, description, domain string, time
381381
}
382382
} else {
383383
log.Printf("[DEBUG] [LITE] No files found for phase %s after retries", phaseKey)
384-
// Send "0 findings" message to webhook only when not under bot
385-
if os.Getenv("AUTOAR_CURRENT_SCAN_ID") == "" {
386-
utils.SendPhaseFiles(phaseKey, domain, []string{})
387-
}
384+
// Always send "0 findings" message, let the discord utility handle routing
385+
utils.SendPhaseFiles(phaseKey, domain, []string{})
388386
}
389387
} else {
390388
log.Printf("[DEBUG] [LITE] No expected files for phase %s", phaseKey)
391-
// Send "0 findings" message to webhook only when not under bot
392-
if os.Getenv("AUTOAR_CURRENT_SCAN_ID") == "" {
393-
utils.SendPhaseFiles(phaseKey, domain, []string{})
394-
}
389+
// Always send "0 findings" message, let the discord utility handle routing
390+
utils.SendPhaseFiles(phaseKey, domain, []string{})
395391
}
396392
}
397393

internal/modules/subdomain/subdomain.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -536,19 +536,15 @@ func runSubdomainPhase(phaseKey string, step, total int, description, subdomain
536536
if err := utils.SendPhaseFiles(phaseKey, subdomainClean, existingFiles); err != nil {
537537
log.Printf("[DEBUG] [SUBDOMAIN] Failed to send files for phase %s: %v", phaseKey, err)
538538
}
539-
} else {
540-
log.Printf("[DEBUG] [SUBDOMAIN] No files found for phase %s after retries", phaseKey)
541-
// Send "0 findings" message to webhook only when not under bot
542-
if os.Getenv("AUTOAR_CURRENT_SCAN_ID") == "" {
539+
} else {
540+
log.Printf("[DEBUG] [SUBDOMAIN] No files found for phase %s after retries", phaseKey)
541+
// Always send "0 findings" message, let the discord utility handle routing
543542
utils.SendPhaseFiles(phaseKey, subdomainClean, []string{})
544-
}
545-
}
543+
}
546544
} else {
547545
log.Printf("[DEBUG] [SUBDOMAIN] No expected files for phase %s", phaseKey)
548-
// Send "0 findings" message to webhook only when not under bot
549-
if os.Getenv("AUTOAR_CURRENT_SCAN_ID") == "" {
550-
utils.SendPhaseFiles(phaseKey, subdomainClean, []string{})
551-
}
546+
// Always send "0 findings" message, let the discord utility handle routing
547+
utils.SendPhaseFiles(phaseKey, subdomainClean, []string{})
552548
}
553549
}
554550

0 commit comments

Comments
 (0)