Skip to content

Commit 98e0ef1

Browse files
committed
added option for -dns and -tldplus1
1 parent 0e43bc4 commit 98e0ef1

File tree

8 files changed

+199
-70
lines changed

8 files changed

+199
-70
lines changed

certgraph.go

+44-24
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import (
1010
"sync"
1111
"time"
1212

13+
"github.com/lanrat/certgraph/dns"
1314
"github.com/lanrat/certgraph/driver"
1415
"github.com/lanrat/certgraph/driver/crtsh"
1516
"github.com/lanrat/certgraph/driver/google"
1617
"github.com/lanrat/certgraph/driver/http"
1718
"github.com/lanrat/certgraph/driver/smtp"
1819
"github.com/lanrat/certgraph/graph"
19-
"github.com/lanrat/certgraph/status"
2020
)
2121

2222
var (
@@ -42,7 +42,8 @@ var config struct {
4242
cdn bool
4343
maxSANsSize int
4444
tldPlus1 bool
45-
checkNS bool
45+
updatePSL bool
46+
checkDNS bool
4647
printVersion bool
4748
}
4849

@@ -56,8 +57,9 @@ func init() {
5657
flag.BoolVar(&config.includeCTExpired, "ct-expired", false, "include expired certificates in certificate transparency search")
5758
flag.IntVar(&config.maxSANsSize, "sanscap", 80, "maximum number of uniq TLD+1 domains in certificate to include, 0 has no limit")
5859
flag.BoolVar(&config.cdn, "cdn", false, "include certificates from CDNs")
59-
flag.BoolVar(&config.checkNS, "ns", false, "check for NS records to determine if domain is registered")
60+
flag.BoolVar(&config.checkDNS, "dns", false, "check for DNS records to determine if domain is registered")
6061
flag.BoolVar(&config.tldPlus1, "tldplus1", false, "for every domain found, add tldPlus1 of the domain's parent")
62+
flag.BoolVar(&config.updatePSL, "updatepsl", false, "Update the default Public Suffix List")
6163
flag.UintVar(&config.maxDepth, "depth", 5, "maximum BFS depth to go")
6264
flag.UintVar(&config.parallel, "parallel", 10, "number of certificates to retrieve in parallel")
6365
flag.BoolVar(&config.details, "details", false, "print details about the domains crawled")
@@ -91,14 +93,23 @@ func main() {
9193
return
9294
}
9395

96+
// update the public suffix list if required
97+
if config.updatePSL {
98+
err := dns.UpdatePublicSuffixList(config.timeout)
99+
if err != nil {
100+
e(err)
101+
return
102+
}
103+
}
104+
94105
// add domains passed to startDomains
95106
startDomains := make([]string, 0, 1)
96107
for _, domain := range flag.Args() {
97108
d := strings.ToLower(domain)
98109
if len(d) > 0 {
99110
startDomains = append(startDomains, cleanInput(d))
100111
if config.tldPlus1 {
101-
tldPlus1, err := status.TLDPlus1(domain)
112+
tldPlus1, err := dns.TLDPlus1(domain)
102113
if err != nil {
103114
continue
104115
}
@@ -162,7 +173,9 @@ func v(a ...interface{}) {
162173
}
163174

164175
func e(a ...interface{}) {
165-
fmt.Fprintln(os.Stderr, a...)
176+
if a != nil {
177+
fmt.Fprintln(os.Stderr, a...)
178+
}
166179
}
167180

168181
// prints the graph as a json object
@@ -232,7 +245,7 @@ func breathFirstSearch(roots []string) {
232245
wg.Add(1)
233246
domainNodeInputChan <- graph.NewDomainNode(neighbor, domainNode.Depth+1)
234247
if config.tldPlus1 {
235-
tldPlus1, err := status.TLDPlus1(neighbor)
248+
tldPlus1, err := dns.TLDPlus1(neighbor)
236249
if err != nil {
237250
continue
238251
}
@@ -254,23 +267,7 @@ func breathFirstSearch(roots []string) {
254267
domainNode, more := <-domainNodeOutputChan
255268
if more {
256269
if !config.printJSON {
257-
if config.details {
258-
fmt.Fprintln(os.Stdout, domainNode)
259-
} else {
260-
fmt.Fprintln(os.Stdout, domainNode.Domain)
261-
}
262-
if config.checkNS {
263-
// TODO these ns lookups are likely done a LOT for many subdomains of the same domain
264-
ns, err := status.HasNameservers(domainNode.Domain, config.timeout)
265-
if err != nil {
266-
v("NS check error:", domainNode.Domain, err)
267-
continue
268-
}
269-
if !ns {
270-
// TODO print tldplus1 in a good way
271-
fmt.Fprintf(os.Stdout, "Missing NS: %s\n", domainNode.Domain)
272-
}
273-
}
270+
printNode(domainNode)
274271
} else if config.details {
275272
fmt.Fprintln(os.Stderr, domainNode)
276273
}
@@ -286,8 +283,16 @@ func breathFirstSearch(roots []string) {
286283
<-done // wait for save to finish
287284
}
288285

289-
// visit visit each node and get and set its neighbors
286+
// visit visits each node and get and set its neighbors
290287
func visit(domainNode *graph.DomainNode) {
288+
// check NS if necessary
289+
if config.checkDNS {
290+
_, err := domainNode.CheckForDNS(config.timeout)
291+
if err != nil {
292+
v("CheckForNS", err)
293+
}
294+
}
295+
291296
// perform cert search
292297
// TODO do pagination in multiple threads to not block on long searches
293298
results, err := certDriver.QueryDomain(domainNode.Domain)
@@ -340,6 +345,21 @@ func visit(domainNode *graph.DomainNode) {
340345
// when we process the related domains
341346
}
342347

348+
func printNode(domainNode *graph.DomainNode) {
349+
if config.details {
350+
fmt.Fprintln(os.Stdout, domainNode)
351+
} else {
352+
fmt.Fprintln(os.Stdout, domainNode.Domain)
353+
}
354+
if config.checkDNS && !domainNode.HasDNS {
355+
// TODO print this in a better way
356+
// TODO for debugging
357+
realDomain, _ := dns.TLDPlus1(domainNode.Domain)
358+
fmt.Fprintf(os.Stdout, "* Missing DNS for: %s\n", realDomain)
359+
360+
}
361+
}
362+
343363
// certNodeFromCertResult convert certResult to certNode
344364
func certNodeFromCertResult(certResult *driver.CertResult) *graph.CertNode {
345365
certNode := &graph.CertNode{

dns/ns.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package dns
2+
3+
import (
4+
"context"
5+
"net"
6+
"time"
7+
)
8+
9+
var (
10+
dnsCache = make(map[string]bool)
11+
dnsResolver = &net.Resolver{}
12+
)
13+
14+
func init() {
15+
//dnsResolver.PreferGo = true
16+
dnsResolver.StrictErrors = false
17+
}
18+
19+
func noSuchHostDNSError(err error) bool {
20+
dnsErr, ok := err.(*net.DNSError)
21+
if !ok {
22+
// not a DNSError
23+
return false
24+
}
25+
return dnsErr.Err == "no such host"
26+
}
27+
28+
// HasRecords does NS, CNAME, A, and AAAA lookups with a timeout
29+
// returns error when no NS found, does not use TLDPlus1
30+
func HasRecords(domain string, timeout time.Duration) (bool, error) {
31+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
32+
defer cancel()
33+
34+
// first check for NS
35+
ns, err := dnsResolver.LookupNS(ctx, domain)
36+
if err != nil && !noSuchHostDNSError(err) {
37+
//fmt.Println("NS error ", err)
38+
return false, err
39+
}
40+
if len(ns) > 0 {
41+
//fmt.Printf("Found %d NS for %s\n", len(ns), domain)
42+
return true, nil
43+
}
44+
45+
// next check for CNAME
46+
cname, err := dnsResolver.LookupCNAME(ctx, domain)
47+
if err != nil && !noSuchHostDNSError(err) {
48+
//fmt.Println("cname error ", err)
49+
return false, err
50+
}
51+
if len(cname) > 2 {
52+
//fmt.Printf("found CNAME %s for %s\n", cname, domain)
53+
return true, nil
54+
}
55+
56+
// next check for IP
57+
addrs, err := dnsResolver.LookupHost(ctx, domain)
58+
if err != nil && !noSuchHostDNSError(err) {
59+
//fmt.Println("ip error ", err)
60+
return false, err
61+
}
62+
if len(addrs) > 0 {
63+
//fmt.Printf("Found %d IPs for %s\n", len(addrs), domain)
64+
return true, nil
65+
}
66+
67+
//fmt.Printf("Found no DNS records for %s\n", domain)
68+
return false, nil
69+
}
70+
71+
// HasRecordsCache returns true if the domain has no DNS records (at the tldplus1 level)
72+
// uses a cache to store results to prevent lots of DNS lookups
73+
func HasRecordsCache(domain string, timeout time.Duration) (bool, error) {
74+
domain, err := TLDPlus1(domain)
75+
if err != nil {
76+
return false, err
77+
}
78+
hasDNS, found := dnsCache[domain]
79+
if found {
80+
return hasDNS, nil
81+
}
82+
hasRecords, err := HasRecords(domain, timeout)
83+
if err != nil {
84+
dnsCache[domain] = hasRecords
85+
}
86+
return hasRecords, err
87+
}

dns/publicsuffix.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package dns
2+
3+
import (
4+
"net/http"
5+
"time"
6+
7+
"github.com/weppos/publicsuffix-go/publicsuffix"
8+
)
9+
10+
var (
11+
suffixListFindOptions = &publicsuffix.FindOptions{
12+
IgnorePrivate: true,
13+
DefaultRule: publicsuffix.DefaultRule,
14+
}
15+
suffixListURL = "https://publicsuffix.org/list/public_suffix_list.dat"
16+
suffixList = publicsuffix.DefaultList
17+
nsCache = make(map[string]bool)
18+
)
19+
20+
// UpdatePublicSuffixList gets a new copy of the public suffix list from the internat and updates the built in copy with the new rules
21+
func UpdatePublicSuffixList(timeout time.Duration) error {
22+
suffixListParseOptions := &publicsuffix.ParserOption{
23+
PrivateDomains: !suffixListFindOptions.IgnorePrivate,
24+
}
25+
client := http.Client{
26+
Timeout: timeout,
27+
}
28+
resp, err := client.Get(suffixListURL)
29+
if err != nil {
30+
return err
31+
}
32+
defer resp.Body.Close()
33+
newSuffixList := publicsuffix.NewList()
34+
newSuffixList.Load(resp.Body, suffixListParseOptions)
35+
suffixList = newSuffixList
36+
return err
37+
}
38+
39+
// TLDPlus1 returns TLD+1 of domain
40+
func TLDPlus1(domain string) (string, error) {
41+
return publicsuffix.DomainFromListWithOptions(suffixList, domain, suffixListFindOptions)
42+
}

driver/driver.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ type Result interface {
4747
// FingerprintMap stores a mapping of domains to Fingerprints returned from the driver
4848
// in the case where multiple domains where queries (redirects, related, etc..) the
4949
// matching certificates will be in this map
50-
// the fingerprints returned are guaranted to be a complete result for the domain's certs, but related domains may or may not be complete
50+
// the fingerprints returned are guaranteed to be a complete result for the domain's certs, but related domains may or may not be complete
5151
type FingerprintMap map[string][]fingerprint.Fingerprint
5252

5353
// Add adds a domain and fingerprint to the map

go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ module github.com/lanrat/certgraph
33
require (
44
github.com/lib/pq v1.0.0
55
github.com/weppos/publicsuffix-go v0.4.0
6-
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d
6+
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd // indirect
7+
golang.org/x/text v0.3.0 // indirect
78
)

graph/cert_node.go

+10-7
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ package graph
22

33
import (
44
"fmt"
5-
"regexp"
65
"strings"
76

7+
"github.com/lanrat/certgraph/dns"
88
"github.com/lanrat/certgraph/fingerprint"
9-
"github.com/lanrat/certgraph/status"
109
)
1110

1211
// CertNode graph node to store certificate information
@@ -38,18 +37,22 @@ func (c *CertNode) AddFound(driver string) {
3837
}
3938

4039
// CDNCert returns true if we think the certificate belongs to a CDN
41-
// very weak detection, only supports fastly & cloudflair
40+
// very weak detection, only supports fastly & cloudflare
4241
func (c *CertNode) CDNCert() bool {
4342
for _, domain := range c.Domains {
44-
// cloudflair
45-
matched, _ := regexp.MatchString("([0-9][a-z])*\\.cloudflaressl\\.com", domain)
46-
if matched {
43+
// cloudflare
44+
if strings.HasSuffix(domain, ".cloudflaressl.com") {
4745
return true
4846
}
4947
// fastly
5048
if strings.HasSuffix(domain, "fastly.net") {
5149
return true
5250
}
51+
// akamai
52+
if strings.HasSuffix(domain, ".akamai.net") {
53+
return true
54+
}
55+
5356
}
5457
return false
5558
}
@@ -58,7 +61,7 @@ func (c *CertNode) CDNCert() bool {
5861
func (c *CertNode) TLDPlus1Count() int {
5962
tldPlus1Domains := make(map[string]bool)
6063
for _, domain := range c.Domains {
61-
tldPlus1, err := status.TLDPlus1(domain)
64+
tldPlus1, err := dns.TLDPlus1(domain)
6265
if err != nil {
6366
continue
6467
}

graph/domain_node.go

+13
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"fmt"
55
"strconv"
66
"strings"
7+
"time"
78

9+
"github.com/lanrat/certgraph/dns"
810
"github.com/lanrat/certgraph/fingerprint"
911
"github.com/lanrat/certgraph/status"
1012
)
@@ -17,6 +19,7 @@ type DomainNode struct {
1719
RelatedDomains status.Map
1820
Status status.Status
1921
Root bool
22+
HasDNS bool
2023
}
2124

2225
// NewDomainNode constructor for DomainNode, converts domain to nonWildcard
@@ -41,6 +44,15 @@ func (d *DomainNode) AddRelatedDomains(domains []string) {
4144
}
4245
}
4346

47+
// CheckForDNS checks for the existence of DNS records for the domain's tld+1
48+
// sets the value to the node and returns the result as well
49+
func (d *DomainNode) CheckForDNS(timeout time.Duration) (bool, error) {
50+
hasDNS, err := dns.HasRecordsCache(d.Domain, timeout)
51+
52+
d.HasDNS = hasDNS
53+
return hasDNS, err
54+
}
55+
4456
// AddStatusMap adds the status' in the map to the DomainNode
4557
// also sets the Node's own status if it is in the Map
4658
// side effect: will delete its own status from the provided map
@@ -94,5 +106,6 @@ func (d *DomainNode) ToMap() map[string]string {
94106
m["root"] = strconv.FormatBool(d.Root)
95107
m["depth"] = strconv.FormatUint(uint64(d.Depth), 10)
96108
m["related"] = relatedString
109+
m["hasDNS"] = strconv.FormatBool(d.HasDNS)
97110
return m
98111
}

0 commit comments

Comments
 (0)