Import upstream version 20200803
Kali Janitor
3 years ago
0 | FROM golang:alpine | |
1 | ||
2 | RUN apk add --update git make | |
3 | ||
4 | WORKDIR /src/certgraph | |
5 | ADD . . | |
6 | ||
7 | ENV CGO_ENABLED=0 | |
8 | RUN make install | |
9 | ||
10 | ENTRYPOINT [ "certgraph" ]⏎ |
0 | 0 | GIT_DATE := $(shell git log -1 --date=short --pretty='%cd' | tr -d -) |
1 | 1 | GIT_HASH := $(shell git rev-parse HEAD) |
2 | 2 | |
3 | BUILD_FLAGS := -ldflags "-X main.gitDate=$(GIT_DATE) -X main.gitHash=$(GIT_HASH)" | |
3 | BUILD_FLAGS := -trimpath -ldflags "-w -s -X main.gitDate=$(GIT_DATE) -X main.gitHash=$(GIT_HASH)" | |
4 | 4 | |
5 | 5 | PLATFORMS := linux/amd64 linux/386 linux/arm darwin/amd64 windows/amd64 windows/386 openbsd/amd64 |
6 | 6 | SOURCES := $(shell find . -maxdepth 1 -type f -name "*.go") |
7 | ALL_SOURCES = $(shell find . -type f -name '*.go') | |
7 | ALL_SOURCES = $(shell find . -type f -name '*.go') go.mod web/index_html.go | |
8 | 8 | |
9 | 9 | temp = $(subst /, ,$@) |
10 | 10 | os = $(word 1, $(temp)) |
11 | 11 | arch = $(word 2, $(temp)) |
12 | 12 | ext = $(shell if [ "$(os)" = "windows" ]; then echo ".exe"; fi) |
13 | 13 | |
14 | .PHONY: all release fmt clean serv $(PLATFORMS) | |
14 | .PHONY: all release fmt clean serv $(PLATFORMS) docker check | |
15 | 15 | |
16 | 16 | all: certgraph |
17 | 17 | |
25 | 25 | CGO_ENABLED=0 GOOS=$(os) GOARCH=$(arch) go build $(BUILD_FLAGS) -o 'build/bin/$(os)/$(arch)/certgraph$(ext)' $(SOURCES) |
26 | 26 | mkdir -p build/$(GIT_DATE)/; cd build/bin/$(os)/$(arch)/; zip -r ../../../$(GIT_DATE)/certgraph-$(os)-$(arch)-$(GIT_DATE).zip .; cd ../../../ |
27 | 27 | |
28 | web/index_html.go: docs/index.html | |
29 | go generate -x ./... | |
30 | ||
31 | docker: Dockerfile $(ALL_SOURCES) | |
32 | docker build -t lanrat/certgraph . | |
33 | ||
28 | 34 | fmt: |
29 | 35 | gofmt -s -w -l . |
30 | 36 | |
32 | 38 | go install $(BUILD_FLAGS) |
33 | 39 | |
34 | 40 | clean: |
35 | rm -r certgraph build/ | |
41 | rm -rf certgraph build/ web/index_html.go | |
36 | 42 | |
37 | serv: | |
38 | (cd docs; python -m SimpleHTTPServer) | |
43 | check: | lint check1 check2 | |
44 | ||
45 | check1: | |
46 | golangci-lint run | |
47 | ||
48 | check2: | |
49 | staticcheck -f stylish -checks all ./... | |
50 | ||
51 | lint: | |
52 | golint ./... | |
53 | ||
54 | serv: certgraph | |
55 | ./certgraph --serve 127.0.0.1:8080 | |
56 | ||
57 | updateMod: | |
58 | go get -u |
0 | 0 | # CertGraph |
1 | ### A tool to crawl the graph of certificate Alternate Names | |
1 | ||
2 | ## A tool to crawl the graph of certificate Alternate Names | |
2 | 3 | |
3 | 4 | CertGraph crawls SSL certificates creating a directed graph where each domain is a node and the certificate alternative names for that domain's certificate are the edges to other domain nodes. New domains are printed as they are found. In Detailed mode upon completion the Graph's adjacency list is printed. |
4 | 5 | |
9 | 10 | [Blog post with more information](https://lanrat.com/certgraph/) |
10 | 11 | |
11 | 12 | ## Usage |
12 | ``` | |
13 | ||
14 | ```console | |
13 | 15 | Usage of ./certgraph: [OPTION]... HOST... |
14 | https://github.com/lanrat/certgraph | |
16 | https://github.com/lanrat/certgraph | |
15 | 17 | OPTIONS: |
18 | -apex | |
19 | for every domain found, add the apex domain of the domain's parent | |
16 | 20 | -cdn |
17 | include certificates from CDNs | |
21 | include certificates from CDNs | |
18 | 22 | -ct-expired |
19 | include expired certificates in certificate transparency search | |
23 | include expired certificates in certificate transparency search | |
20 | 24 | -ct-subdomains |
21 | include sub-domains in certificate transparency search | |
25 | include sub-domains in certificate transparency search | |
22 | 26 | -depth uint |
23 | maximum BFS depth to go (default 5) | |
27 | maximum BFS depth to go (default 5) | |
24 | 28 | -details |
25 | print details about the domains crawled | |
29 | print details about the domains crawled | |
30 | -dns | |
31 | check for DNS records to determine if domain is registered | |
26 | 32 | -driver string |
27 | driver to use [crtsh, google, http, smtp] (default "http") | |
33 | driver to use [crtsh, google, http, smtp] (default "http") | |
28 | 34 | -json |
29 | print the graph as json, can be used for graph in web UI | |
30 | -ns | |
31 | check for NS records to determine if domain is registered | |
35 | print the graph as json, can be used for graph in web UI | |
32 | 36 | -parallel uint |
33 | number of certificates to retrieve in parallel (default 10) | |
37 | number of certificates to retrieve in parallel (default 10) | |
34 | 38 | -sanscap int |
35 | maximum number of uniq TLD+1 domains in certificate to include, 0 has no limit (default 80) | |
39 | maximum number of uniq apex domains in certificate to include, 0 has no limit (default 80) | |
36 | 40 | -save string |
37 | save certs to folder in PEM format | |
41 | save certs to folder in PEM format | |
42 | -serve string | |
43 | address:port to serve html UI on | |
38 | 44 | -timeout uint |
39 | tcp timeout in seconds (default 10) | |
40 | -tldplus1 | |
41 | for every domain found, add tldPlus1 of the domain's parent | |
45 | tcp timeout in seconds (default 10) | |
46 | -updatepsl | |
47 | Update the default Public Suffix List | |
42 | 48 | -verbose |
43 | verbose logging | |
49 | verbose logging | |
44 | 50 | -version |
45 | print version and exit | |
51 | print version and exit | |
46 | 52 | ``` |
47 | 53 | |
48 | 54 | ## Drivers |
49 | 55 | |
50 | 56 | CertGraph has multiple options for querying SSL certificates. The driver is responsible for retrieving the certificates for a given domain. Currently there are the following drivers: |
51 | 57 | |
52 | * **http** this is the default driver which works by connecting to the hosts over HTTPS and retrieving the certificates from the SSL connection | |
53 | ||
54 | * **smtp** like the *http* driver, but connects over port 25 and issues the *starttls* command to retrieve the certificates from the SSL connection | |
58 | * **http** this is the default driver which works by connecting to the hosts over HTTPS and retrieving the certificates from the SSL connection | |
55 | 59 | |
56 | * **crtsh** this driver searches Certificate Transparency logs via [crt.sh](https://crt.sh/). No packets are sent to any of the domains when using this driver | |
60 | * **smtp** like the *http* driver, but connects over port 25 and issues the *starttls* command to retrieve the certificates from the SSL connection | |
57 | 61 | |
58 | * **google** this is another Certificate Transparency driver that behaves like *crtsh* but uses the [Google Certificate Transparency Lookup Tool](https://transparencyreport.google.com/https/certificates) | |
62 | * **crtsh** this driver searches Certificate Transparency logs via [crt.sh](https://crt.sh/). No packets are sent to any of the domains when using this driver | |
59 | 63 | |
64 | * **google** this is another Certificate Transparency driver that behaves like *crtsh* but uses the [Google Certificate Transparency Lookup Tool](https://transparencyreport.google.com/https/certificates) | |
60 | 65 | |
61 | 66 | ## Example |
62 | ``` | |
67 | ||
68 | ```console | |
63 | 69 | $ ./certgraph -details eff.org |
64 | 70 | eff.org 0 Good 42E3E4605D8BB4608EB64936E2176A98B97EBF2E0F8F93A64A6640713C7D4325 |
65 | 71 | maps.eff.org 1 Good 42E3E4605D8BB4608EB64936E2176A98B97EBF2E0F8F93A64A6640713C7D4325 |
68 | 74 | atlas.eff.org 1 Good 42E3E4605D8BB4608EB64936E2176A98B97EBF2E0F8F93A64A6640713C7D4325 |
69 | 75 | kittens.eff.org 1 Good 42E3E4605D8BB4608EB64936E2176A98B97EBF2E0F8F93A64A6640713C7D4325 |
70 | 76 | ``` |
77 | ||
71 | 78 | The above output represents the adjacency list for the graph for the root domain `eff.org`. The adjacency list is in the form: |
72 | 79 | `Node Depth Status Cert-Fingerprint` |
73 | 80 | |
74 | 81 | ## [Releases](https://github.com/lanrat/certgraph/releases) |
75 | 82 | |
76 | Precompiled releases will occasionally be uploaded to the [releases github page](https://github.com/lanrat/certgraph/releases). https://github.com/lanrat/certgraph/releases | |
83 | Pre-compiled releases will occasionally be uploaded to the [releases github page](https://github.com/lanrat/certgraph/releases). [https://github.com/lanrat/certgraph/releases](https://github.com/lanrat/certgraph/releases) | |
77 | 84 | |
78 | Also available in [BlackArch](https://blackarch.org). | |
85 | ### [Docker](https://hub.docker.com/r/lanrat/certgraph/) | |
86 | ||
87 | CertGraph is an automated build on the Docker Hub! | |
88 | ||
89 | ```console | |
90 | $ docker run --rm -it lanrat/certgraph example.com | |
91 | example.com | |
92 | www.example.net | |
93 | www.example.org | |
94 | www.example.com | |
95 | example.org | |
96 | example.net | |
97 | example.edu | |
98 | www.example.edu | |
99 | ``` | |
100 | ||
101 | ### Linux Distributions | |
102 | ||
103 | * [BlackArch](https://blackarch.org) | |
104 | * [Kali Linux](https://www.kali.org/) | |
79 | 105 | |
80 | 106 | ## Compiling |
81 | 107 | |
82 | To compile certgraph you must have a working go 1.11 or newer compiler on your system, as certgraph makes use of go's modules for dependencies. | |
108 | To compile certgraph you must have a working go 1.13 or newer compiler on your system, as certgraph makes use of go's modules for dependencies. | |
83 | 109 | To compile for the running system compilation is as easy as running make |
84 | ``` | |
110 | ||
111 | ```console | |
85 | 112 | certgraph$ make |
86 | 113 | go build -o certgraph certgraph.go |
87 | 114 | ``` |
88 | 115 | |
89 | 116 | Alternatively you can use `go get` to install with this one-liner: |
90 | ``` | |
117 | ||
118 | ```console | |
91 | 119 | go get -u github.com/lanrat/certgraph |
92 | 120 | ``` |
93 | 121 | |
94 | 122 | ## [Web UI](https://lanrat.github.io/certgraph/) |
95 | 123 | |
96 | A web UI is provided in the docs folder and is accessible at the github pages url [https://lanrat.github.io/certgraph/](https://lanrat.github.io/certgraph/). | |
124 | A web UI is provided in the docs folder and is accessible at the github pages url [https://lanrat.github.io/certgraph/](https://lanrat.github.io/certgraph/), or can be run from the embedded web server by calling `certgraph --serve 127.0.0.1:8080`. | |
97 | 125 | |
98 | 126 | The web UI takes the output provided with the `-json` flag. |
99 | 127 | The JSON graph can be sent to the web interface as an uploaded file, remote URL, or as the query string using the data variable. |
110 | 138 | |
111 | 139 | [![whitehouse.gov graph](https://cloud.githubusercontent.com/assets/164192/20861407/4775ff26-b944-11e6-888c-4d93e3333494.png)](https://lanrat.github.io/certgraph/?data=https://gist.githubusercontent.com/lanrat/96c47dfee0faaaad633cc830b7e3b997/raw/3c79fed837cb3202e220de21d2a8eb128f4bbd9f/whitehouse.json) |
112 | 140 | |
141 | ## BygoneSSL detection | |
142 | ||
143 | ### Self Detection | |
144 | ||
145 | CertGraph can be used to detect [BygoneSSL](https://insecure.design) DoS with the following options. CT-DRIVER can be any Certificate Transparency capable driver. | |
146 | Provide all known input domains you own. If any domains you do not own are printed, then you are vulnerable. | |
147 | ||
148 | ```console | |
149 | certgraph -depth 1 -driver CT-DRIVER -ct-subdomains -cdn -apex [DOMAIN]... | |
150 | ``` | |
151 | ||
152 | ### Bug Bounty | |
153 | ||
154 | If you want to find a vulnerable site that has a bug bounty, certgraph can be used with the following options and any driver. But you will have better luck with a non Certificate Transparency driver to ensure that the certificates in question are actually in use | |
155 | ||
156 | ```console | |
157 | certgraph -cdn -dns -apex [DOMAIN]... | |
158 | ``` | |
159 | ||
160 | And domains that print `* Missing DNS for` have vulnerable certificates that should be rotated. |
16 | 16 | "github.com/lanrat/certgraph/driver/http" |
17 | 17 | "github.com/lanrat/certgraph/driver/smtp" |
18 | 18 | "github.com/lanrat/certgraph/graph" |
19 | "github.com/lanrat/certgraph/web" | |
19 | 20 | ) |
20 | 21 | |
21 | 22 | var ( |
27 | 28 | var certDriver driver.Driver |
28 | 29 | |
29 | 30 | // config & flags |
31 | // TODO move driver options to own struct | |
30 | 32 | var config struct { |
31 | 33 | timeout time.Duration |
32 | 34 | verbose bool |
40 | 42 | includeCTExpired bool |
41 | 43 | cdn bool |
42 | 44 | maxSANsSize int |
43 | tldPlus1 bool | |
45 | apex bool | |
44 | 46 | updatePSL bool |
45 | 47 | checkDNS bool |
46 | 48 | printVersion bool |
49 | serve string | |
47 | 50 | } |
48 | 51 | |
49 | 52 | func init() { |
54 | 57 | flag.StringVar(&config.driver, "driver", "http", fmt.Sprintf("driver to use [%s]", strings.Join(driver.Drivers, ", "))) |
55 | 58 | flag.BoolVar(&config.includeCTSubdomains, "ct-subdomains", false, "include sub-domains in certificate transparency search") |
56 | 59 | flag.BoolVar(&config.includeCTExpired, "ct-expired", false, "include expired certificates in certificate transparency search") |
57 | flag.IntVar(&config.maxSANsSize, "sanscap", 80, "maximum number of uniq TLD+1 domains in certificate to include, 0 has no limit") | |
60 | flag.IntVar(&config.maxSANsSize, "sanscap", 80, "maximum number of uniq apex domains in certificate to include, 0 has no limit") | |
58 | 61 | flag.BoolVar(&config.cdn, "cdn", false, "include certificates from CDNs") |
59 | 62 | flag.BoolVar(&config.checkDNS, "dns", false, "check for DNS records to determine if domain is registered") |
60 | flag.BoolVar(&config.tldPlus1, "tldplus1", false, "for every domain found, add tldPlus1 of the domain's parent") | |
63 | flag.BoolVar(&config.apex, "apex", false, "for every domain found, add the apex domain of the domain's parent") | |
61 | 64 | flag.BoolVar(&config.updatePSL, "updatepsl", false, "Update the default Public Suffix List") |
62 | 65 | flag.UintVar(&config.maxDepth, "depth", 5, "maximum BFS depth to go") |
63 | 66 | flag.UintVar(&config.parallel, "parallel", 10, "number of certificates to retrieve in parallel") |
64 | 67 | flag.BoolVar(&config.details, "details", false, "print details about the domains crawled") |
65 | 68 | flag.BoolVar(&config.printJSON, "json", false, "print the graph as json, can be used for graph in web UI") |
66 | 69 | flag.StringVar(&config.savePath, "save", "", "save certs to folder in PEM format") |
70 | flag.StringVar(&config.serve, "serve", "", "address:port to serve html UI on") | |
71 | ||
67 | 72 | flag.Usage = func() { |
68 | 73 | fmt.Fprintf(os.Stderr, "Usage of %s: [OPTION]... HOST...\n\thttps://github.com/lanrat/certgraph\nOPTIONS:\n", os.Args[0]) |
69 | 74 | flag.PrintDefaults() |
77 | 82 | if config.printVersion { |
78 | 83 | fmt.Println(version()) |
79 | 84 | return |
85 | } | |
86 | ||
87 | if len(config.serve) > 0 { | |
88 | err := web.Serve(config.serve) | |
89 | e(err) | |
80 | 90 | } |
81 | 91 | |
82 | 92 | // print usage if no domain passed |
107 | 117 | d := strings.ToLower(domain) |
108 | 118 | if len(d) > 0 { |
109 | 119 | startDomains = append(startDomains, cleanInput(d)) |
110 | if config.tldPlus1 { | |
111 | tldPlus1, err := dns.TLDPlus1(domain) | |
120 | if config.apex { | |
121 | apexDomain, err := dns.ApexDomain(domain) | |
112 | 122 | if err != nil { |
113 | 123 | continue |
114 | 124 | } |
115 | startDomains = append(startDomains, tldPlus1) | |
125 | startDomains = append(startDomains, apexDomain) | |
116 | 126 | } |
117 | 127 | } |
118 | 128 | } |
147 | 157 | |
148 | 158 | // setDriver sets the driver variable for the provided driver string and does any necessary driver prep work |
149 | 159 | // TODO make config generic and move this to driver module |
160 | // TODO support multi-driver | |
150 | 161 | func setDriver(driver string) error { |
151 | 162 | var err error |
152 | 163 | switch driver { |
159 | 170 | case "smtp": |
160 | 171 | certDriver, err = smtp.Driver(config.timeout, config.savePath) |
161 | 172 | default: |
162 | return fmt.Errorf("Unknown driver name: %s", config.driver) | |
173 | return fmt.Errorf("unknown driver name: %s", config.driver) | |
163 | 174 | } |
164 | 175 | return err |
165 | 176 | } |
243 | 254 | for _, neighbor := range certGraph.GetDomainNeighbors(domainNode.Domain, config.cdn, config.maxSANsSize) { |
244 | 255 | wg.Add(1) |
245 | 256 | domainNodeInputChan <- graph.NewDomainNode(neighbor, domainNode.Depth+1) |
246 | if config.tldPlus1 { | |
247 | tldPlus1, err := dns.TLDPlus1(neighbor) | |
257 | if config.apex { | |
258 | apexDomain, err := dns.ApexDomain(neighbor) | |
248 | 259 | if err != nil { |
249 | 260 | continue |
250 | 261 | } |
251 | 262 | wg.Add(1) |
252 | domainNodeInputChan <- graph.NewDomainNode(tldPlus1, domainNode.Depth+1) | |
263 | domainNodeInputChan <- graph.NewDomainNode(apexDomain, domainNode.Depth+1) | |
253 | 264 | } |
254 | 265 | } |
255 | 266 | }(domainNode) |
340 | 351 | domainNode.AddCertFingerprint(certNode.Fingerprint, certDriver.GetName()) |
341 | 352 | } |
342 | 353 | |
343 | // we dont process any other certificates returned, they will be collected | |
354 | // we don't process any other certificates returned, they will be collected | |
344 | 355 | // when we process the related domains |
345 | 356 | } |
346 | 357 | |
353 | 364 | if config.checkDNS && !domainNode.HasDNS { |
354 | 365 | // TODO print this in a better way |
355 | 366 | // TODO for debugging |
356 | realDomain, _ := dns.TLDPlus1(domainNode.Domain) | |
367 | realDomain, _ := dns.ApexDomain(domainNode.Domain) | |
357 | 368 | fmt.Fprintf(os.Stdout, "* Missing DNS for: %s\n", realDomain) |
358 | 369 | |
359 | 370 | } |
0 | // Package dns adds utility functions for performing dns queries | |
0 | 1 | package dns |
1 | 2 | |
2 | 3 | import ( |
25 | 26 | } |
26 | 27 | |
27 | 28 | // HasRecords does NS, CNAME, A, and AAAA lookups with a timeout |
28 | // returns error when no NS found, does not use TLDPlus1 | |
29 | // returns error when no NS found, does not use alexDomain | |
29 | 30 | func HasRecords(domain string, timeout time.Duration) (bool, error) { |
30 | 31 | ctx, cancel := context.WithTimeout(context.Background(), timeout) |
31 | 32 | defer cancel() |
67 | 68 | return false, nil |
68 | 69 | } |
69 | 70 | |
70 | // HasRecordsCache returns true if the domain has no DNS records (at the tldplus1 level) | |
71 | // HasRecordsCache returns true if the domain has no DNS records (at the apex domain level) | |
71 | 72 | // uses a cache to store results to prevent lots of DNS lookups |
72 | 73 | func HasRecordsCache(domain string, timeout time.Duration) (bool, error) { |
73 | domain, err := TLDPlus1(domain) | |
74 | domain, err := ApexDomain(domain) | |
74 | 75 | if err != nil { |
75 | 76 | return false, err |
76 | 77 | } |
13 | 13 | } |
14 | 14 | suffixListURL = "https://publicsuffix.org/list/public_suffix_list.dat" |
15 | 15 | suffixList = publicsuffix.DefaultList |
16 | nsCache = make(map[string]bool) | |
17 | 16 | ) |
18 | 17 | |
19 | 18 | // UpdatePublicSuffixList gets a new copy of the public suffix list from the internat and updates the built in copy with the new rules |
30 | 29 | } |
31 | 30 | defer resp.Body.Close() |
32 | 31 | newSuffixList := publicsuffix.NewList() |
33 | newSuffixList.Load(resp.Body, suffixListParseOptions) | |
32 | _, err = newSuffixList.Load(resp.Body, suffixListParseOptions) | |
34 | 33 | suffixList = newSuffixList |
35 | 34 | return err |
36 | 35 | } |
37 | 36 | |
38 | // TLDPlus1 returns TLD+1 of domain | |
39 | func TLDPlus1(domain string) (string, error) { | |
37 | // ApexDomain returns TLD+1 of domain | |
38 | func ApexDomain(domain string) (string, error) { | |
40 | 39 | return publicsuffix.DomainFromListWithOptions(suffixList, domain, suffixListFindOptions) |
41 | 40 | } |
16 | 16 | stroke: #333; |
17 | 17 | stroke-width: 1.5px; |
18 | 18 | } |
19 | ||
20 | 19 | .upload-drop-zone { |
21 | 20 | height: 200px; |
22 | 21 | border-width: 2px; |
0 | // Package crtsh implements an unofficial API client for Comodo's | |
1 | // Certificate Transparency search | |
2 | // https://crt.sh/ | |
3 | // | |
4 | // As the API is unofficial and has been reverse engineered it may stop working | |
5 | // at any time and comes with no guarantees. | |
6 | // | |
0 | 7 | package crtsh |
1 | ||
2 | /* | |
3 | * This file implements an unofficial API client for Comodo's | |
4 | * Certificate Transparency search | |
5 | * https://crt.sh/ | |
6 | * | |
7 | * As the API is unofficial and has been reverse engineered it may stop working | |
8 | * at any time and comes with no guarantees. | |
9 | */ | |
10 | 8 | |
11 | 9 | // TODO running in verbose gives error: pq: unnamed prepared statement does not exist |
12 | 10 | |
75 | 73 | } |
76 | 74 | |
77 | 75 | d.db, err = sql.Open("postgres", connStr) |
78 | ||
79 | d.setSQLTimeout(d.timeout.Seconds()) | |
76 | if err != nil { | |
77 | return nil, err | |
78 | } | |
79 | ||
80 | err = d.setSQLTimeout(d.timeout.Seconds()) | |
80 | 81 | |
81 | 82 | return d, err |
82 | 83 | } |
198 | 199 | |
199 | 200 | for rows.Next() { |
200 | 201 | var domain string |
201 | rows.Scan(&domain) | |
202 | err = rows.Scan(&domain) | |
203 | if err != nil { | |
204 | return nil, err | |
205 | } | |
202 | 206 | certNode.Domains = append(certNode.Domains, domain) |
203 | 207 | } |
204 | 208 | |
213 | 217 | return certNode, err |
214 | 218 | } |
215 | 219 | |
216 | driver.RawCertToPEMFile(rawCert, path.Join(d.savePath, fp.HexString())+".pem") | |
220 | err = driver.RawCertToPEMFile(rawCert, path.Join(d.savePath, fp.HexString())+".pem") | |
221 | if err != nil { | |
222 | return certNode, err | |
223 | } | |
217 | 224 | } |
218 | 225 | |
219 | 226 | return certNode, nil |
0 | // Package driver exposes interfaces and types certgraph drivers must implement | |
0 | 1 | package driver |
1 | 2 | |
2 | 3 | import ( |
7 | 8 | "github.com/lanrat/certgraph/fingerprint" |
8 | 9 | "github.com/lanrat/certgraph/status" |
9 | 10 | ) |
11 | ||
12 | // TODO add context instead of timeout on all requests | |
10 | 13 | |
11 | 14 | // Drivers contains all the drivers that have been registered |
12 | 15 | var Drivers []string |
0 | // Package google file implements an unofficial API client for Google's | |
1 | // Certificate Transparency search | |
2 | // https://transparencyreport.google.com/https/certificates | |
3 | // | |
4 | // As the API is unofficial and has been reverse engineered it may stop working | |
5 | // at any time and comes with no guarantees. | |
6 | // | |
0 | 7 | package google |
1 | ||
2 | /* | |
3 | * This file implements an unofficial API client for Google's | |
4 | * Certificate Transparency search | |
5 | * https://transparencyreport.google.com/https/certificates | |
6 | * | |
7 | * As the API is unofficial and has been reverse engineered it may stop working | |
8 | * at any time and comes with no guarantees. | |
9 | */ | |
10 | 8 | |
11 | 9 | import ( |
12 | 10 | "encoding/json" |
29 | 27 | } |
30 | 28 | |
31 | 29 | // Base URLs for Google's CT API |
32 | const searchURL1 = "https://transparencyreport.google.com/transparencyreport/api/v3/httpsreport/ct/certsearch?include_expired=false&include_subdomains=false&domain=example.com" | |
33 | const searchURL2 = "https://transparencyreport.google.com/transparencyreport/api/v3/httpsreport/ct/certsearch/page?p=DEADBEEF" | |
34 | const certURL = "https://transparencyreport.google.com/transparencyreport/api/v3/httpsreport/ct/certbyhash?hash=DEADBEEF" | |
30 | const ( | |
31 | searchURL1 = "https://transparencyreport.google.com/transparencyreport/api/v3/httpsreport/ct/certsearch?include_expired=false&include_subdomains=false&domain=example.com" | |
32 | searchURL2 = "https://transparencyreport.google.com/transparencyreport/api/v3/httpsreport/ct/certsearch/page?p=DEADBEEF" | |
33 | certURL = "https://transparencyreport.google.com/transparencyreport/api/v3/httpsreport/ct/certbyhash?hash=DEADBEEF" | |
34 | //summaryURL is not currently used | |
35 | //summaryURL = "https://transparencyreport.google.com/transparencyreport/api/v3/httpsreport/ct/summary" | |
36 | ) | |
35 | 37 | |
36 | 38 | type googleCT struct { |
37 | 39 | maxPages float64 // this is a float because that is the type automatically decoded from the JSON response |
0 | // Package http implements a certgraph driver for obtaining SSL certificates over https | |
0 | 1 | package http |
1 | 2 | |
2 | 3 | import ( |
52 | 53 | if found { |
53 | 54 | return cert, nil |
54 | 55 | } |
55 | return nil, fmt.Errorf("Certificate with Fingerprint %s not found", fp.HexString()) | |
56 | return nil, fmt.Errorf("certificate with Fingerprint %s not found", fp.HexString()) | |
56 | 57 | } |
57 | 58 | |
58 | 59 | // Driver creates a new SSL driver for HTTP Connections |
150 | 151 | |
151 | 152 | // save |
152 | 153 | if c.parent.save && len(connState.PeerCertificates) > 0 { |
153 | driver.CertsToPEMFile(connState.PeerCertificates, path.Join(c.parent.savePath, certResult.Fingerprint.HexString())+".pem") | |
154 | err = driver.CertsToPEMFile(connState.PeerCertificates, path.Join(c.parent.savePath, certResult.Fingerprint.HexString())+".pem") | |
154 | 155 | } |
155 | 156 | |
156 | 157 | return conn, err |
16 | 16 | } |
17 | 17 | defer f.Close() |
18 | 18 | for _, cert := range certs { |
19 | pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) | |
19 | err = pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) | |
20 | if err != nil { | |
21 | return err | |
22 | } | |
20 | 23 | } |
21 | 24 | return nil |
22 | 25 | } |
31 | 34 | return err |
32 | 35 | } |
33 | 36 | defer f.Close() |
34 | pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: cert}) | |
35 | return nil | |
37 | err = pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: cert}) | |
38 | return err | |
36 | 39 | } |
37 | 40 | |
38 | 41 | func fileExists(f string) bool { |
0 | // Package smtp implements a certgraph driver for obtaining SSL certificates over smtp with STARTTLS | |
0 | 1 | package smtp |
1 | 2 | |
2 | 3 | import ( |
54 | 55 | if found { |
55 | 56 | return cert, nil |
56 | 57 | } |
57 | return nil, fmt.Errorf("Certificate with Fingerprint %s not found", fp.HexString()) | |
58 | return nil, fmt.Errorf("certificate with Fingerprint %s not found", fp.HexString()) | |
58 | 59 | } |
59 | 60 | |
60 | 61 | // Driver creates a new SSL driver for SMTP Connections |
133 | 134 | |
134 | 135 | // save |
135 | 136 | if d.save && len(certs) > 0 { |
136 | driver.CertsToPEMFile(certs, path.Join(d.savePath, certResult.Fingerprint.HexString())+".pem") | |
137 | err = driver.CertsToPEMFile(certs, path.Join(d.savePath, certResult.Fingerprint.HexString())+".pem") | |
137 | 138 | } |
138 | 139 | |
139 | return results, nil | |
140 | return results, err | |
140 | 141 | } |
141 | 142 | |
142 | 143 | // getMX returns the MX records for the provided domain |
0 | // Package fingerprint defines types to define a certificate fingerprint for certgraph | |
0 | 1 | package fingerprint |
1 | 2 | |
2 | 3 | import ( |
16 | 17 | // FromHashBytes returns a Fingerprint generated by the first len(Fingerprint) bytes |
17 | 18 | func FromHashBytes(data []byte) Fingerprint { |
18 | 19 | var fp Fingerprint |
19 | /*if len(data) != sha256.Size { | |
20 | v("Data is not correct SHA256 size", data) | |
21 | }*/ | |
22 | 20 | for i := 0; i < len(data) && i < len(fp); i++ { |
23 | 21 | fp[i] = data[i] |
24 | 22 | } |
27 | 25 | |
28 | 26 | // FromBytes returns a Fingerprint generated by the provided bytes |
29 | 27 | func FromBytes(data []byte) Fingerprint { |
30 | var fp Fingerprint | |
31 | fp = sha256.Sum256(data) | |
28 | fp := sha256.Sum256(data) | |
32 | 29 | return fp |
33 | 30 | } |
34 | 31 | |
35 | 32 | // FromB64 returns a Fingerprint from a base64 encoded hash string |
36 | 33 | func FromB64(hash string) Fingerprint { |
37 | 34 | data, _ := base64.StdEncoding.DecodeString(hash) |
38 | /*if err != nil { | |
39 | fmt.Println(err) | |
40 | }*/ | |
41 | 35 | return FromHashBytes(data) |
42 | 36 | } |
43 | 37 |
0 | 0 | module github.com/lanrat/certgraph |
1 | 1 | |
2 | 2 | require ( |
3 | github.com/lib/pq v1.0.0 | |
4 | github.com/weppos/publicsuffix-go v0.4.0 | |
5 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd // indirect | |
6 | golang.org/x/text v0.3.0 // indirect | |
3 | github.com/lib/pq v1.8.0 | |
4 | github.com/weppos/publicsuffix-go v0.13.0 | |
5 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect | |
7 | 6 | ) |
7 | ||
8 | go 1.13 |
0 | github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= | |
1 | github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= | |
2 | github.com/weppos/publicsuffix-go v0.13.0 h1:0Tu1uzLBd1jPn4k6OnMmOPZH/l/9bj9kUOMMkoRs6Gg= | |
3 | github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= | |
4 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | |
5 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | |
6 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | |
7 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= | |
8 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | |
9 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |
10 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
11 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
12 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= | |
13 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
56 | 56 | return false |
57 | 57 | } |
58 | 58 | |
59 | // TLDPlus1Count the number of tld+1 domains in the certificate | |
60 | func (c *CertNode) TLDPlus1Count() int { | |
61 | tldPlus1Domains := make(map[string]bool) | |
59 | // ApexCount the number of tld+1 domains in the certificate | |
60 | func (c *CertNode) ApexCount() int { | |
61 | apexDomains := make(map[string]bool) | |
62 | 62 | for _, domain := range c.Domains { |
63 | tldPlus1, err := dns.TLDPlus1(domain) | |
63 | apexDomain, err := dns.ApexDomain(domain) | |
64 | 64 | if err != nil { |
65 | 65 | continue |
66 | 66 | } |
67 | tldPlus1Domains[tldPlus1] = true | |
67 | apexDomains[apexDomain] = true | |
68 | 68 | } |
69 | return len(tldPlus1Domains) | |
69 | return len(apexDomains) | |
70 | 70 | } |
71 | 71 | |
72 | 72 | // ToMap returns a map of the CertNode's fields (weak serialization) |
43 | 43 | } |
44 | 44 | } |
45 | 45 | |
46 | // CheckForDNS checks for the existence of DNS records for the domain's tld+1 | |
46 | // CheckForDNS checks for the existence of DNS records for the domain's apex | |
47 | 47 | // sets the value to the node and returns the result as well |
48 | 48 | func (d *DomainNode) CheckForDNS(timeout time.Duration) (bool, error) { |
49 | 49 | hasDNS, err := dns.HasRecordsCache(d.Domain, timeout) |
74 | 74 | return fingerprints |
75 | 75 | } |
76 | 76 | |
77 | // get the string representation of a node | |
77 | // String returns the string representation of a node | |
78 | 78 | func (d *DomainNode) String() string { |
79 | 79 | certString := "" |
80 | 80 | // Certs |
0 | // Package graph implements the graph data structures used by certgraph to build the certificate graph | |
0 | 1 | package graph |
1 | 2 | |
2 | 3 | import ( |
18 | 19 | func NewCertGraph() *CertGraph { |
19 | 20 | graph := new(CertGraph) |
20 | 21 | return graph |
21 | } | |
22 | ||
23 | // LoadOrStoreCert will return the CertNode in the graph with the provided node's fingerprint, or store the node if it did not already exist | |
24 | // returned bool is true if the CertNode was found, false if stored | |
25 | func (graph *CertGraph) LoadOrStoreCert(certNode *CertNode) (*CertNode, bool) { | |
26 | foundCertNode, ok := graph.certs.LoadOrStore(certNode.Fingerprint, certNode) | |
27 | return foundCertNode.(*CertNode), ok | |
28 | 22 | } |
29 | 23 | |
30 | 24 | // AddCert add a CertNode to the graph |
96 | 90 | certNode := node.(*CertNode) |
97 | 91 | if !cdn && certNode.CDNCert() { |
98 | 92 | //v(domain, "-> CDN CERT") |
99 | } else if maxSANsSize > 0 && certNode.TLDPlus1Count() > maxSANsSize { | |
93 | } else if maxSANsSize > 0 && certNode.ApexCount() > maxSANsSize { | |
100 | 94 | //v(domain, "-> Large CERT") |
101 | 95 | } else { |
102 | 96 | for _, neighbor := range certNode.Domains { |
0 | // Package status defines the various status certgraph discovered hosts/certificates may have | |
0 | 1 | package status |
1 | 2 | |
2 | 3 | import ( |
63 | 64 | CT = iota |
64 | 65 | ) |
65 | 66 | |
66 | // return domain status for printing | |
67 | // String returns the domain status for printing | |
67 | 68 | func (status DomainStatus) String() string { |
68 | 69 | switch status { |
69 | 70 | case UNKNOWN: |
0 | #!/bin/sh | |
1 | set -e | |
2 | HTMLFILE="../docs/index.html" | |
3 | HTML="$(cat $HTMLFILE)" | |
4 | DATE="$(date)" | |
5 | ||
6 | cat > index_html.go <<EOL | |
7 | package web | |
8 | ||
9 | // Code generated on "$DATE" DO NOT EDIT. | |
10 | ||
11 | const indexSource = \`$HTML\` | |
12 | ||
13 | EOL⏎ |
0 | package web | |
1 | ||
2 | // Code generated on "Mon Aug 3 14:01:58 PDT 2020" DO NOT EDIT. | |
3 | ||
4 | const indexSource = `<!DOCTYPE html> | |
5 | <html> | |
6 | <head> | |
7 | <meta charset="utf-8"> | |
8 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
9 | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
10 | <title>CertGraph</title> | |
11 | <link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/cerulean/bootstrap.min.css" rel="stylesheet" integrity="sha384-zF4BRsG/fLiTGfR9QL82DrilZxrwgY/+du4p/c7J72zZj+FLYq4zY00RylP9ZjiT" crossorigin="anonymous"> | |
12 | <script src="https://d3js.org/d3.v4.min.js"></script> | |
13 | <style type="text/css"> | |
14 | .links line { | |
15 | stroke-opacity: 0.6; | |
16 | stroke-width: 1px; | |
17 | fill: none; | |
18 | } | |
19 | .nodes circle { | |
20 | stroke: #333; | |
21 | stroke-width: 1.5px; | |
22 | } | |
23 | .upload-drop-zone { | |
24 | height: 200px; | |
25 | border-width: 2px; | |
26 | margin-bottom: 20px; | |
27 | color: #ccc; | |
28 | border-style: dashed; | |
29 | border-color: #ccc; | |
30 | line-height: 200px; | |
31 | text-align: center | |
32 | } | |
33 | .upload-drop-zone.drop { | |
34 | color: #222; | |
35 | border-color: #222; | |
36 | } | |
37 | </style> | |
38 | </head> | |
39 | <body> | |
40 | <div class="container"> | |
41 | ||
42 | <nav class="navbar navbar-inverse"> | |
43 | <div class="container-fluid"> | |
44 | <div class="navbar-header"> | |
45 | <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> | |
46 | <span class="sr-only">Toggle navigation</span> | |
47 | <span class="icon-bar"></span> | |
48 | <span class="icon-bar"></span> | |
49 | <span class="icon-bar"></span> | |
50 | </button> | |
51 | <a class="navbar-brand" href="#">CertGraph</a> | |
52 | </div> | |
53 | <div id="navbar" class="navbar-collapse collapse"> | |
54 | <ul class="nav navbar-nav"> | |
55 | <!-- <li><a href="#">Graph</a></li> --> | |
56 | </ul> | |
57 | <ul class="nav navbar-nav navbar-right"> | |
58 | <li class="dropdown"> | |
59 | <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Data <span class="caret"></span></a> | |
60 | <ul class="dropdown-menu" role="menu"> | |
61 | <li><a href="#" data-toggle="modal" data-target="#URLmodal">URL</a></li> | |
62 | <li><a href="#" data-toggle="modal" data-target="#Pastemodal">Paste</a></li> | |
63 | <li><a href="#" data-toggle="modal" data-target="#Filemodal">File Upload</a></li> | |
64 | </ul> | |
65 | </li> | |
66 | </ul> | |
67 | </div> | |
68 | </div> | |
69 | </nav> | |
70 | ||
71 | <div class="panel panel-info"> | |
72 | <div class="panel-heading"> | |
73 | <h3 class="panel-title pull-left">Graph</h3> | |
74 | <div class="pull-right"><a href="#" class="btn btn-primary btn-xs" id="generate">Download SVG</a></div> | |
75 | <div class="clearfix"></div> | |
76 | </div> | |
77 | <svg id="graph" width="100%" height="500"></svg> | |
78 | </div> | |
79 | ||
80 | <div class="panel panel-info"> | |
81 | <div class="panel-heading">Info</div> | |
82 | <div class="panel-body" id="node-info"> | |
83 | </div> | |
84 | </div> | |
85 | ||
86 | <ul class="nav nav-tabs"> | |
87 | <li class="active"><a href="#domains" data-toggle="tab" aria-expanded="false">Domains</a></li> | |
88 | <li class=""><a href="#certificates" data-toggle="tab" aria-expanded="true">Certificates</a></li> | |
89 | </ul> | |
90 | <div id="myTabContent" class="tab-content panel-body"> | |
91 | <div class="tab-pane fade active in" id="domains"> | |
92 | ||
93 | <table class="table table-striped table-hover "> | |
94 | <thead> | |
95 | <tr> | |
96 | <th>#</th> | |
97 | <th>Domain</th> | |
98 | <th>Status</th> | |
99 | <th>Lookup</th> | |
100 | </tr> | |
101 | </thead> | |
102 | <tbody id="domain-list"> | |
103 | </tbody> | |
104 | </table> | |
105 | ||
106 | </div> | |
107 | <div class="tab-pane fade" id="certificates"> | |
108 | ||
109 | <table class="table table-striped table-hover "> | |
110 | <thead> | |
111 | <tr> | |
112 | <th>#</th> | |
113 | <th>Hash</th> | |
114 | <th>Lookup</th> | |
115 | </tr> | |
116 | </thead> | |
117 | <tbody id="cert-list"> | |
118 | </tbody> | |
119 | </table> | |
120 | ||
121 | </div> | |
122 | ||
123 | </div> | |
124 | ||
125 | <footer> | |
126 | <hr> | |
127 | <div class="row"> | |
128 | <div class="col-xs-10"><a href="https://github.com/lanrat/certgraph">CertGraph</a></div> | |
129 | </div> | |
130 | </footer> | |
131 | ||
132 | <!-- URL Modal --> | |
133 | <div class="modal fade" id="URLmodal" role="dialog"> | |
134 | <div class="modal-dialog"> | |
135 | <!-- Modal content--> | |
136 | <div class="modal-content"> | |
137 | <div class="modal-header"> | |
138 | <button type="button" class="close" data-dismiss="modal">×</button> | |
139 | <h4 class="modal-title">Data URL</h4> | |
140 | </div> | |
141 | <div class="modal-body"> | |
142 | <label for="inputURL" class="col-lg-2 control-label">URL</label> | |
143 | <div class="col-lg-10"> | |
144 | <input type="text" class="form-control" id="inputURL" placeholder="https://domain.com/data.json"> | |
145 | </div> | |
146 | </div> | |
147 | <div class="modal-footer"> | |
148 | <button type="button" class="btn btn-primary" data-dismiss="modal" id="loadURL">Load</button> | |
149 | <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> | |
150 | </div> | |
151 | </div> | |
152 | </div> | |
153 | </div> <!-- /URL Modal --> | |
154 | ||
155 | <!-- Paste Modal --> | |
156 | <div class="modal fade" id="Pastemodal" role="dialog"> | |
157 | <div class="modal-dialog"> | |
158 | <!-- Modal content--> | |
159 | <div class="modal-content"> | |
160 | <div class="modal-header"> | |
161 | <button type="button" class="close" data-dismiss="modal">×</button> | |
162 | <h4 class="modal-title">JSON Data</h4> | |
163 | </div> | |
164 | <div class="modal-body"> | |
165 | <label for="inputPaste" class="col-lg-2 control-label">URL</label> | |
166 | <div class="col-lg-10"> | |
167 | <textarea class="form-control" rows="10" id="inputPaste"></textarea> | |
168 | </div> | |
169 | </div> | |
170 | <div class="modal-footer"> | |
171 | <button type="button" class="btn btn-primary" data-dismiss="modal" id="loadPaste">Load</button> | |
172 | <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> | |
173 | </div> | |
174 | </div> | |
175 | </div> | |
176 | </div> <!-- /Paste Modal --> | |
177 | ||
178 | <!-- File Modal --> | |
179 | <div class="modal fade" id="Filemodal" role="dialog"> | |
180 | <div class="modal-dialog"> | |
181 | <!-- Modal content--> | |
182 | <div class="modal-content"> | |
183 | <div class="modal-header"> | |
184 | <button type="button" class="close" data-dismiss="modal">×</button> | |
185 | <h4 class="modal-title">JSON File</h4> | |
186 | </div> | |
187 | <div class="modal-body"> | |
188 | <label for="inputFile" class="col-lg-2 control-label">File</label> | |
189 | <div class="col-lg-10"> | |
190 | <input type="file" class="form-control file" id="inputFile"> | |
191 | </div> | |
192 | <br/> | |
193 | <label for="drop-zone" class="col-lg-2 control-label">Or drag and drop a file below</label> | |
194 | <div class="upload-drop-zone" id="drop-zone"> | |
195 | Just drag and drop a JSON file here | |
196 | </div> | |
197 | </div> | |
198 | <div class="modal-footer"> | |
199 | <button type="button" class="btn btn-primary" data-dismiss="modal" id="loadFile">Load</button> | |
200 | <button type="button" class="btn btn-default" data-dismiss="modal" id="fileClose">Close</button> | |
201 | </div> | |
202 | </div> | |
203 | </div> | |
204 | </div> <!-- /File Modal --> | |
205 | ||
206 | </div> <!-- /container--> | |
207 | <script src="//code.jquery.com/jquery-1.11.3.min.js"></script> | |
208 | <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script> | |
209 | <script src="//cdn.rawgit.com/eligrey/FileSaver.js/e9d941381475b5df8b7d7691013401e171014e89/FileSaver.min.js"></script> | |
210 | <script> | |
211 | var svg = d3.select("svg"); | |
212 | var width = window.innerWidth-100; | |
213 | //var width = svg.attr("width"); | |
214 | var height = svg.attr("height"); | |
215 | ||
216 | /*var svgElem = document.getElementById("graph"); | |
217 | var width = svgElem.width.animVal.value, | |
218 | height = svgElem.height.animVal.value; | |
219 | console.log(width, height);*/ | |
220 | ||
221 | // TODO THIS | |
222 | // http://www.coppelia.io/2014/07/an-a-to-z-of-extra-features-for-the-d3-force-layout/ | |
223 | ||
224 | var color = d3.scaleOrdinal(d3.schemeCategory10); | |
225 | var simulation; | |
226 | ||
227 | svg = svg.call(d3.zoom().on("zoom", zoomed)).append("g"); | |
228 | ||
229 | svg.append("defs").append("marker") | |
230 | .attr("id", "arrow") | |
231 | .attr("viewBox", "0 -5 10 10") | |
232 | .attr("refX", 20) | |
233 | .attr("refY", 0) | |
234 | .attr("markerWidth", 8) | |
235 | .attr("markerHeight", 8) | |
236 | .attr("orient", "auto") | |
237 | //.attr("stroke", function(d) { return color(d.type); }) | |
238 | .append("svg:path") | |
239 | .attr("d", "M0,-5L10,0L0,5"); | |
240 | ||
241 | function resetGraph() { | |
242 | d3.select("g").selectAll("*").remove(); | |
243 | createTables(); | |
244 | ||
245 | // reset info | |
246 | var el = document.getElementById("node-info"); | |
247 | el.innerText = "Click on a node in the graph to view details."; | |
248 | ||
249 | // redo layout | |
250 | simulation = d3.forceSimulation() | |
251 | .force("link", d3.forceLink().id(function(d) { return d.id; })) | |
252 | .force("charge", d3.forceManyBody().strength(-100)) | |
253 | .force("center", d3.forceCenter(width / 2, height / 2)); | |
254 | } | |
255 | ||
256 | function createGraph (error, graph) { | |
257 | if (error) throw error; | |
258 | ||
259 | var link = svg.append("g") | |
260 | .attr("class", "links") | |
261 | .selectAll("line") | |
262 | .data(graph.links) | |
263 | .enter().append("line") | |
264 | .attr("stroke", function(d) { return color(d.type); }) | |
265 | .attr("marker-end", "url(#arrow)"); | |
266 | ||
267 | var text = svg.append("g").attr("class", "labels").selectAll("g") | |
268 | .data(graph.nodes) | |
269 | .enter().append("g"); | |
270 | ||
271 | text.append("text") | |
272 | .attr("x", 14) | |
273 | .attr("y", ".31em") | |
274 | .style("font-family", "sans-serif") | |
275 | .style("font-size", "0.7em") | |
276 | .text(function(d) { if (d.type == "domain") {return d.id; } return d.id.substring(0,8); }); | |
277 | ||
278 | var node = svg.append("g") | |
279 | .attr("class", "nodes") | |
280 | .selectAll("circle") | |
281 | .data(graph.nodes) | |
282 | .enter().append("circle") | |
283 | .attr("r", 10) | |
284 | .attr("fill", function(d) { if (d.root == "true") return color(d.root); return color(d.type); }) | |
285 | .call(d3.drag() | |
286 | .on("start", dragstarted) | |
287 | .on("drag", dragged) | |
288 | .on("end", dragended)); | |
289 | ||
290 | node.on("click",function(d){ | |
291 | // console.log("clicked", d.id); | |
292 | // console.log(d); | |
293 | updateInfoBox(d); | |
294 | }); | |
295 | ||
296 | node.append("title") | |
297 | .text(function(d) { return d.id; }); | |
298 | ||
299 | simulation | |
300 | .nodes(graph.nodes) | |
301 | .on("tick", ticked); | |
302 | ||
303 | simulation.force("link") | |
304 | .links(graph.links); | |
305 | ||
306 | function ticked() { | |
307 | link | |
308 | .attr("x1", function(d) { return d.source.x; }) | |
309 | .attr("y1", function(d) { return d.source.y; }) | |
310 | .attr("x2", function(d) { return d.target.x; }) | |
311 | .attr("y2", function(d) { return d.target.y; }); | |
312 | ||
313 | node | |
314 | .attr("cx", function(d) { return d.x; }) | |
315 | .attr("cy", function(d) { return d.y; }); | |
316 | text | |
317 | .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"}); | |
318 | } | |
319 | createTables(); | |
320 | } | |
321 | ||
322 | ||
323 | function dragstarted(d) { | |
324 | if (!d3.event.active) simulation.alphaTarget(0.3).restart(); | |
325 | d.fx = d.x; | |
326 | d.fy = d.y; | |
327 | } | |
328 | ||
329 | function dragged(d) { | |
330 | d.fx = d3.event.x; | |
331 | d.fy = d3.event.y; | |
332 | } | |
333 | ||
334 | function dragended(d) { | |
335 | if (!d3.event.active) simulation.alphaTarget(0); | |
336 | d.fx = null; | |
337 | d.fy = null; | |
338 | } | |
339 | ||
340 | function zoomed() { | |
341 | svg.attr("transform", "translate(" + d3.event.transform.x + "," + d3.event.transform.y + ")" + " scale(" + d3.event.transform.k + ")"); | |
342 | } | |
343 | ||
344 | d3.select("#generate").on("click", writeDownloadLink); | |
345 | function writeDownloadLink(){ | |
346 | try { | |
347 | var isFileSaverSupported = !!new Blob(); | |
348 | } catch (e) { | |
349 | alert("blob not supported"); | |
350 | } | |
351 | ||
352 | var html = d3.select("svg") | |
353 | .attr("title", "graph") //TODO | |
354 | .attr("version", 1.1) | |
355 | .attr("xmlns", "http://www.w3.org/2000/svg") | |
356 | .node().outerHTML; | |
357 | ||
358 | var blob = new Blob([html], {type: "image/svg+xml"}); | |
359 | saveAs(blob, "certificate_graph.svg"); //TODO root node name | |
360 | }; | |
361 | ||
362 | function updateInfoBox(d) { | |
363 | if (d) { | |
364 | var el = document.getElementById("node-info"); | |
365 | var s = "Type: "+d.type+"</br>"; | |
366 | if (d.type == "domain") { | |
367 | s = s + "Domain: "+linkifyDomain(d)+"</br>"; | |
368 | s = s + "Status: "+d.status+"</br>"; | |
369 | }else if (d.type = "certificate") { | |
370 | s = s + "Hash: "+linkifyCert(d)+"</br>"; | |
371 | } | |
372 | el.innerHTML = s; | |
373 | } | |
374 | } | |
375 | ||
376 | function createTables() { | |
377 | // TODO: redo this in native d3 | |
378 | domainEl = document.getElementById("domain-list"); | |
379 | domain_tbody2 = document.createElement('tbody'); | |
380 | domain_tbody2.id="domain-list"; | |
381 | domainEl.parentNode.replaceChild(domain_tbody2, domainEl); | |
382 | ||
383 | certEl = document.getElementById("cert-list"); | |
384 | cert_tbody2 = document.createElement('tbody'); | |
385 | cert_tbody2.id="cert-list"; | |
386 | certEl.parentNode.replaceChild(cert_tbody2, certEl); | |
387 | ||
388 | var domainCount = 0; | |
389 | function addTableDomain(d) { | |
390 | //console.log("domain", d); | |
391 | var c = ""; | |
392 | if (d.root == "true") { | |
393 | c = "info"; | |
394 | } | |
395 | $('#domain-list').append('<tr class="'+c+'"><td>'+ ++domainCount +'</td><td>'+linkifyDomain(d)+'</td><td>'+d.status+'</td><td>'+linkifyAny(d)+'</td></tr>'); | |
396 | } | |
397 | ||
398 | var certCount = 0; | |
399 | function addTableCert(d) { | |
400 | //console.log("cert", d); | |
401 | $('#cert-list').append('<tr><td>'+ ++certCount + '</td><td>'+linkifyCert(d)+'</td><td>'+linkifyAny(d)+'</td></tr>'); | |
402 | } | |
403 | ||
404 | ||
405 | d3.selectAll('circle').each(function(d){ | |
406 | if (d.type == "domain") { | |
407 | addTableDomain(d); | |
408 | }else if (d.type == "certificate") { | |
409 | addTableCert(d); | |
410 | } else { | |
411 | console.log("Unknown Type: ", d.type); | |
412 | } | |
413 | }) | |
414 | } | |
415 | ||
416 | function linkifyCert(d) { | |
417 | return '<a target="_blank" href="https://crt.sh/?sha256='+d.id+'">'+d.id+'</a>'; | |
418 | } | |
419 | function linkifyDomain(d) { | |
420 | return '<a target="_blank" href="https://'+d.id+'">'+d.id+'</a>'; | |
421 | } | |
422 | function linkifyAny(d) { | |
423 | return '<a target="_blank" href="https://crt.sh/?q='+d.id+'">🔎</a>'; | |
424 | } | |
425 | ||
426 | function getQueryVariable(variable){ | |
427 | var query = window.location.search.substring(1); | |
428 | var vars = query.split("&"); | |
429 | for (var i=0;i<vars.length;i++) { | |
430 | var pair = vars[i].split("="); | |
431 | if(pair[0] == variable){return pair[1];} | |
432 | } | |
433 | return ""; | |
434 | } | |
435 | ||
436 | // new data from url | |
437 | d3.select("#loadURL").on("click", loadURL); | |
438 | function loadURL(){ | |
439 | var url = document.getElementById("inputURL").value; | |
440 | history.pushState('', 'CertGraph', "?data="+url); | |
441 | resetGraph(); | |
442 | d3.json(url, createGraph); | |
443 | } | |
444 | ||
445 | // new data from paste | |
446 | d3.select("#loadPaste").on("click", loadPaste); | |
447 | function loadPaste(){ | |
448 | var dataStr = document.getElementById("inputPaste").value; | |
449 | history.pushState('', 'CertGraph', "?"); | |
450 | resetGraph(); | |
451 | var data = JSON.parse(dataStr); | |
452 | createGraph(null, data); | |
453 | } | |
454 | ||
455 | // new data from paste | |
456 | d3.select("#loadFile").on("click", loadFile); | |
457 | function loadFile(){ | |
458 | var file = document.getElementById("inputFile").files[0]; | |
459 | history.pushState('', 'CertGraph', "?"); | |
460 | resetGraph(); | |
461 | var reader = new FileReader(); | |
462 | reader.onload = function(e) { | |
463 | var dataStr = reader.result; | |
464 | var data = JSON.parse(dataStr); | |
465 | createGraph(null, data); | |
466 | } | |
467 | reader.readAsText(file); | |
468 | } | |
469 | ||
470 | var dropbox = document.getElementById('drop-zone'); | |
471 | function dragenter(e) { | |
472 | e.stopPropagation(); | |
473 | e.preventDefault(); | |
474 | dropbox.className = 'upload-drop-zone drop'; | |
475 | // console.log("enter"); | |
476 | return false; | |
477 | } | |
478 | function dragover(e) { | |
479 | e.stopPropagation(); | |
480 | e.preventDefault(); | |
481 | // console.log("over"); | |
482 | } | |
483 | function dragleave(e) { | |
484 | e.stopPropagation(); | |
485 | e.preventDefault(); | |
486 | dropbox.className = 'upload-drop-zone'; | |
487 | // console.log("leave"); | |
488 | return false; | |
489 | } | |
490 | function drop(e) { | |
491 | // console.log("drop"); | |
492 | e.stopPropagation(); | |
493 | e.preventDefault(); | |
494 | dropbox.className = 'upload-drop-zone'; | |
495 | ||
496 | var dt = e.dataTransfer; | |
497 | var files = dt.files; | |
498 | ||
499 | var reader = new FileReader(); | |
500 | reader.onload = function(e) { | |
501 | var dataStr = reader.result; | |
502 | var data = JSON.parse(dataStr); | |
503 | resetGraph(); | |
504 | createGraph(null, data); | |
505 | } | |
506 | reader.readAsText(files[0]); | |
507 | $('#fileClose').click(); | |
508 | return false; | |
509 | } | |
510 | dropbox.addEventListener("dragenter", dragenter, false); | |
511 | dropbox.addEventListener("dragover", dragover, false); | |
512 | dropbox.addEventListener("drop", drop, false); | |
513 | dropbox.addEventListener("dragleave",dragleave, false); | |
514 | ||
515 | // load initial graph data | |
516 | var dataURL = getQueryVariable("data"); | |
517 | if (dataURL == "") { | |
518 | // default graph | |
519 | dataURL = "https://gist.githubusercontent.com/lanrat/8187d01793bf3e578d76495182654206/raw/c49741b5206d81935febdf563452cc4346381e52/eff.json"; | |
520 | } | |
521 | resetGraph(); | |
522 | d3.json(dataURL, createGraph); | |
523 | </script> | |
524 | </body> | |
525 | </html>` | |
526 |
0 | // Package web defines a minimal web server for serving the web UI | |
1 | package web | |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | "net/http" | |
6 | ) | |
7 | ||
8 | //go:generate ./generate.sh | |
9 | ||
10 | // Serve starts a very basic webserver serving the embed web UI | |
11 | func Serve(addr string) error { | |
12 | http.HandleFunc("/", indexHandler) | |
13 | return http.ListenAndServe(addr, nil) | |
14 | } | |
15 | ||
16 | func indexHandler(w http.ResponseWriter, r *http.Request) { | |
17 | fmt.Fprintf(w, "%s", indexSource) | |
18 | } |