├── .gitignore ├── README.md ├── db_setup.sql └── spfmap.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.txt 3 | spfmap -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spfmap 2 | A mass domain scanner to check for SPF and DMARC records. -------------------------------------------------------------------------------- /db_setup.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE results ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | domain_name TEXT UNIQUE, 4 | spf_string TEXT, 5 | dmarc_string TEXT, 6 | spf_all TEXT, 7 | dmarc_p TEXT 8 | ); -------------------------------------------------------------------------------- /spfmap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "os" 4 | import "net" 5 | import "flag" 6 | import "regexp" 7 | import "bufio" 8 | import "strconv" 9 | import "github.com/mxk/go-sqlite/sqlite3" 10 | import "sync" 11 | import tm "github.com/buger/goterm" 12 | import "github.com/alexcesaro/log/stdlog" 13 | import "github.com/alexcesaro/log" 14 | 15 | var logger log.Logger 16 | 17 | type ScanResult struct { 18 | domain_name string 19 | spf_string string 20 | dmarc_string string 21 | } 22 | 23 | func createReport(dbName string) { 24 | c, _ := sqlite3.Open(dbName) 25 | 26 | q_numRecords := "SELECT count(*) FROM results;" 27 | q_numSpfRecords := "SELECT count(domain_name) FROM results WHERE spf_string NOT LIKE \"\"" 28 | q_numDmarcRecords := "SELECT count(domain_name) FROM results WHERE dmarc_string NOT LIKE \"\"" 29 | q_numSpfAll := "SELECT count(domain_name) FROM results WHERE (spf_all LIKE \"%all\")" 30 | q_numSpfMinusAll := "SELECT count(domain_name) FROM results WHERE (spf_all LIKE \"-all\")" 31 | q_numDmarcRejectQuarantine := "SELECT count(domain_name) FROM results WHERE (dmarc_p LIKE \"p=reject\" OR dmarc_p LIKE \"p=quarantine\")" 32 | q_numNotSpoofable := "SELECT count(domain_name) FROM results WHERE spf_all NOT LIKE \"\" AND (dmarc_p LIKE \"p=reject\" OR dmarc_p LIKE \"p=quarantine\")" 33 | 34 | s1, _ := c.Query(q_numRecords) 35 | var numRecords int64 36 | s1.Scan(&numRecords) 37 | s2, _ := c.Query(q_numSpfRecords) 38 | var numSpfRecords int64 39 | s2.Scan(&numSpfRecords) 40 | s3, _ := c.Query(q_numDmarcRecords) 41 | var numDmarcRecords int64 42 | s3.Scan(&numDmarcRecords) 43 | s4, _ := c.Query(q_numSpfAll) 44 | var numSpfAll int64 45 | s4.Scan(&numSpfAll) 46 | s5, _ := c.Query(q_numSpfMinusAll) 47 | var numSpfMinusAll int64 48 | s5.Scan(&numSpfMinusAll) 49 | s6, _ := c.Query(q_numDmarcRejectQuarantine) 50 | var numDmarcRejectQuarantine int64 51 | s6.Scan(&numDmarcRejectQuarantine) 52 | s7, _ := c.Query(q_numNotSpoofable) 53 | var numNotSpoofable int64 54 | s7.Scan(&numNotSpoofable) 55 | 56 | pctSpf := (float64(numSpfRecords)/float64(numRecords))*100 57 | pctDmarc := (float64(numDmarcRecords)/float64(numRecords))*100 58 | pctSpfAll := (float64(numSpfAll)/float64(numRecords))*100 59 | pctSpfMinusAll := (float64(numSpfMinusAll)/float64(numRecords))*100 60 | pctDmarcRejectQuarantine := (float64(numDmarcRejectQuarantine)/float64(numRecords))*100 61 | pctNotSpoofable := (float64(numNotSpoofable)/float64(numRecords))*100 62 | 63 | tm.Clear() 64 | 65 | tm.MoveCursor(1, 1) 66 | tm.Println(tm.Bold("SPF and DMARC Report")) 67 | tm.Println("====================") 68 | tm.Println("Number of records total:", numRecords) 69 | tm.Println("") 70 | tm.Println("Domains with SPF records:", numSpfRecords, "(", pctSpf, "%)") 71 | tm.Println("Domains with DMARC records:", numDmarcRecords, "(", pctDmarc, "%)") 72 | tm.Println("") 73 | tm.Println(tm.Bold("SPF Statistics")) 74 | tm.Println("--------------") 75 | tm.Println("Domains with ~all or -all:", numSpfAll, "(", pctSpfAll, "%)") 76 | tm.Println("Domains with -all:", numSpfMinusAll, "(", pctSpfMinusAll, "%)") 77 | tm.Println("") 78 | tm.Println(tm.Bold("DMARC Statistics")) 79 | tm.Println("----------------") 80 | tm.Println("Domains with Reject or Quarantine Policy:", numDmarcRejectQuarantine, "(", pctDmarcRejectQuarantine, "%)") 81 | tm.Println("") 82 | tm.Println("") 83 | tm.Println("---------------------------------------------------") 84 | tm.Println(tm.Bold("Domains with non-spoofable configuration:"), tm.Color(strconv.Itoa(int(numNotSpoofable)), tm.RED), "(", pctNotSpoofable, "%)") 85 | tm.Println("---------------------------------------------------") 86 | 87 | tm.Flush() 88 | } 89 | 90 | 91 | func LookupSPF(domain string) (string, error) { 92 | txtRecords, err := net.LookupTXT(domain) 93 | if err != nil { 94 | return "", err 95 | } 96 | 97 | for _, record := range txtRecords { 98 | match, _ := regexp.MatchString("^v=spf1.*", record) 99 | if match == true { 100 | return record, nil 101 | } 102 | } 103 | 104 | return "", nil 105 | } 106 | 107 | func LookupDMARC(domain string) (string, error) { 108 | dmarcDomain := "_dmarc." + domain 109 | txtRecords, err := net.LookupTXT(dmarcDomain) 110 | if err != nil { 111 | return "", err 112 | } 113 | 114 | for _, record := range txtRecords { 115 | match, _ := regexp.MatchString("^v=DMARC1.*", record) 116 | if match == true { 117 | return record, nil 118 | } 119 | } 120 | 121 | return "", nil 122 | } 123 | 124 | func ingestDomains(inFileName string, ingestQueue chan<- string) { 125 | file, err := os.Open(inFileName) 126 | if err != nil { 127 | logger.Debug(err) 128 | os.Exit(1) 129 | } 130 | 131 | defer file.Close() 132 | 133 | reader := bufio.NewReader(file) 134 | scanner := bufio.NewScanner(reader) 135 | 136 | logger.Debug("File Reader: Initialized") 137 | 138 | for scanner.Scan() { 139 | ingestQueue <- scanner.Text() 140 | logger.Debug("File Reader: Added " + scanner.Text()) 141 | } 142 | close(ingestQueue) 143 | } 144 | 145 | // Does the heavy lifting 146 | func ingestWorker(id int, ingestQueue <-chan string, resultsQueue chan<- ScanResult, wg *sync.WaitGroup, 147 | scanSpf bool, scanDmarc bool) { 148 | logger.Debug("Ingest Worker", id, "initialized") 149 | for domain := range ingestQueue { 150 | logger.Debug("Ingest Worker", id, "processing ", domain) 151 | result := ScanResult{domain_name: domain} 152 | result.spf_string, _ = LookupSPF(domain) 153 | result.dmarc_string, _ = LookupDMARC(domain) 154 | 155 | resultsQueue <- result 156 | } 157 | logger.Debug("Ingest Worker", id, "finished") 158 | wg.Done() 159 | } 160 | 161 | func closeResultsQueue(wg *sync.WaitGroup, resultsQueue chan ScanResult, numWorkers int) { 162 | wg.Wait() 163 | logger.Debug("Closing results queue") 164 | close(resultsQueue) 165 | } 166 | 167 | func resultsWorker(resultsQueue <-chan ScanResult, dbName string) { 168 | c, _ := sqlite3.Open(dbName) 169 | 170 | logger.Debug("Results Worker initialized") 171 | 172 | for result := range resultsQueue { 173 | logger.Debug("Results worker processing", result.domain_name) 174 | all_r, _ := regexp.Compile(".all") 175 | all_s := all_r.FindString(result.spf_string) 176 | 177 | p_r, _ := regexp.Compile("(p=.*?)($|;|\\s|\\z|\"|')") 178 | p_inter := p_r.FindAllStringSubmatch(result.dmarc_string, -1) 179 | 180 | p_s := "" 181 | if len(p_inter) > 0 { 182 | 183 | if len(p_inter[0]) > 1 { 184 | p_s = p_inter[0][1] 185 | } 186 | } 187 | 188 | args := sqlite3.NamedArgs{"$domain": result.domain_name, 189 | "$spf_string": result.spf_string, 190 | "$dmarc_string": result.dmarc_string, 191 | "$spf_all": all_s, 192 | "$dmarc_p": p_s, 193 | } 194 | err := c.Exec("INSERT INTO results(domain_name, spf_string, dmarc_string, spf_all, dmarc_p) VALUES($domain, $spf_string, $dmarc_string, $spf_all, $dmarc_p);", args) 195 | if err != nil { 196 | logger.Debug(err) 197 | } 198 | 199 | } 200 | } 201 | 202 | func main() { 203 | 204 | /* Set up command line arguments */ 205 | spfScan := flag.Bool("spf", false, "Scan targets for SPF") 206 | dmarcScan := flag.Bool("dmarc", false, "Scan targets for DMARC") 207 | scanAll := flag.Bool("all", false, "Scan targets for both SPF and DMARC") 208 | 209 | genReport := flag.Bool("report", true, "Generate a report for the database") 210 | 211 | inFileName := flag.String("infile", "", "File with target lists (one domain per line)") 212 | targetName := flag.String("target", "", "Domain to target") 213 | dbName := flag.String("db", "spfmap.db", "Name of the SQLite3 database to target") 214 | ingestWorkerNumber := flag.Int("workers", 5, "Number of workers to process the data") 215 | 216 | 217 | flag.Parse() 218 | 219 | logger = stdlog.GetFromFlags() 220 | 221 | 222 | 223 | if *scanAll || *spfScan || *dmarcScan { 224 | 225 | // Set up ingest and results queues 226 | 227 | ingestQueue := make(chan string, 100) 228 | resultsQueue := make(chan ScanResult, 100) 229 | 230 | var wg sync.WaitGroup 231 | 232 | logger.Debug("Made Channels") 233 | 234 | // Logic to ensure we scan the right stuff 235 | runSpfScan := false 236 | runDmarcScan := false 237 | if *spfScan == true || *scanAll == true { 238 | runSpfScan = true 239 | } 240 | if *dmarcScan == true || *scanAll == true { 241 | runDmarcScan = true 242 | } 243 | 244 | logger.Debug("Scanning DMARC: ", runDmarcScan) 245 | logger.Debug("Scanning SPF: ", runSpfScan) 246 | 247 | // Spin out a number of ingest workers based on user input 248 | wg.Add(*ingestWorkerNumber) 249 | for w := 1; w <= *ingestWorkerNumber; w++ { 250 | go ingestWorker(w, ingestQueue, resultsQueue, &wg, runSpfScan, runDmarcScan) 251 | } 252 | 253 | if *inFileName != "" { 254 | go ingestDomains(*inFileName, ingestQueue) 255 | } else if *targetName != "" { 256 | ingestQueue <- *targetName 257 | close(ingestQueue) 258 | } else { 259 | os.Exit(1) 260 | } 261 | 262 | go closeResultsQueue(&wg, resultsQueue, *ingestWorkerNumber) 263 | 264 | resultsWorker(resultsQueue, *dbName) 265 | } 266 | 267 | if *genReport { 268 | createReport(*dbName) 269 | } 270 | 271 | } --------------------------------------------------------------------------------