Skip to content

Commit 58ec611

Browse files
committed
Supply advice despite non-fatal errors
Return a 400 error for single invalid domain API requests Clarify why DKIM may be missing Add `s1` and `s2` to our known DKIM selectors Don't return an error for NXDOMAIN DNS requests, as these are expected from some requests (e.g. trying to find the correct DKIM selector)
1 parent 99f7f5b commit 58ec611

File tree

8 files changed

+47
-20
lines changed

8 files changed

+47
-20
lines changed

README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# Domain Security Scanner
2-
The Domain Security Scanner can be used to perform scans against domains for DKIM, DMARC, and SPF DNS records. You can also serve this functionality via an API, or a dedicated mailbox. A web application is also available if organizations would like to perform a single domain scan for DKIM, DMARC or SPF at [https://dmarcguide.globalcyberalliance.org](https://dmarcguide.globalcyberalliance.org).
2+
3+
The Domain Security Scanner can be used to perform scans against domains for DKIM, DMARC, and SPF DNS records. You can
4+
also serve this functionality via an API, or a dedicated mailbox. A web application is also available if organizations
5+
would like to perform a single domain scan for DKIM, DMARC or SPF
6+
at [https://dmarcguide.globalcyberalliance.org](https://dmarcguide.globalcyberalliance.org).
37

48
## Download
5-
You can download pre-compiled binaries for macOS, Linux and Windows from the [releases](https://github.com/GlobalCyberAlliance/domain-security-scanner/releases) page.
9+
10+
You can download pre-compiled binaries for macOS, Linux and Windows from
11+
the [releases](https://github.com/GlobalCyberAlliance/domain-security-scanner/releases) page.
612

713
Alternatively, you can run the binary from within our pre-built Docker image:
814

@@ -23,6 +29,7 @@ make
2329
This will output a binary called `dss`. You can then move it or use it by running `./bin/dss` (on Unix devices).
2430

2531
## Find a Specific Record From a Single Domain
32+
2633
To scan a domain for a specific type of record (A, AAAA, CNAME, DKIM, DMARC, MX, SPF, TXT), run:
2734

2835
`dss scan [domain] --type dmarc`
@@ -35,7 +42,8 @@ Example:
3542

3643
## Bulk Scan Domains
3744

38-
Scan any number of domains' DNS records. By default, this listens on `STDIN`, meaning you run the command via `dss scan` and then enter each domain one-by-one.
45+
Scan any number of domains' DNS records. By default, this listens on `STDIN`, meaning you run the command via `dss scan`
46+
and then enter each domain one-by-one.
3947

4048
Alternatively, you can specify multiple domains at runtime:
4149

@@ -49,13 +57,17 @@ See the [zonefile.example](zonefile.example) file in this repo.
4957

5058
## Serve REST API
5159

52-
You can also expose the domain scanning functionality via a REST API. By default, this is rate limited to 3 requests per 3 second interval from a single IP address. Serve the API by running the following:
60+
You can also expose the domain scanning functionality via a REST API. By default, this is rate limited to 3 requests per
61+
3 second interval from a single IP address. Serve the API by running the following:
5362

5463
`dss serve api --port 80`
5564

56-
You can reach the API docs by visiting `http://server-ip:port/api/v1/docs` and the OpenAPI schema at `http://server-ip:port/api/v1/docs.json` or `http://server-ip:port/api/v1/docs.yaml`. You can also test requests through this interface thanks to [Scalar](https://github.com/scalar/scalar).
65+
You can reach the API docs by visiting `http://server-ip:port/api/v1/docs` and the OpenAPI schema
66+
at `http://server-ip:port/api/v1/docs.json` or `http://server-ip:port/api/v1/docs.yaml`. You can also test requests
67+
through this interface thanks to [Scalar](https://github.com/scalar/scalar).
5768

58-
You can then get a single domain's results by submitting a GET request like this `http://server-ip:port/api/v1/scan/globalcyberalliance.org`, which will return a JSON response similar to this:
69+
You can then get a single domain's results by submitting a GET request like
70+
this `http://server-ip:port/api/v1/scan/globalcyberalliance.org`, which will return a JSON response similar to this:
5971

6072
```json
6173
{
@@ -98,7 +110,8 @@ You can then get a single domain's results by submitting a GET request like this
98110
}
99111
```
100112

101-
Alternatively, you can scan multiple domains by POSTing them to `http://server-ip:port/api/v1/scan` with a request body like this:
113+
Alternatively, you can scan multiple domains by POSTing them to `http://server-ip:port/api/v1/scan` with a request body
114+
like this:
102115

103116
```json
104117
{
@@ -199,6 +212,7 @@ dss serve mail --inboundHost "imap.gmail.com:993" --inboundPass "SomePassword" -
199212
You can then email this inbox from any address, and you'll receive an email back with your scan results.
200213

201214
### Global Flags
215+
202216
| Flag | Short | Description |
203217
|------------------|-------|-----------------------------------------------------------------------------------------------------------------|
204218
| `--advise` | `-a` | Provide suggestions for incorrect/missing mail security features |

cmd/dss/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ var (
2626
Use: "dss",
2727
Short: "Scan a domain's DNS records.",
2828
Long: "Scan a domain's DNS records.\nhttps://github.com/GlobalCyberAlliance/domain-security-scanner",
29-
Version: "3.0.5",
29+
Version: "3.0.6",
3030
PersistentPreRun: func(cmd *cobra.Command, args []string) {
3131
var logWriter io.Writer
3232

cmd/dss/scan.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ package main
22

33
import (
44
"bufio"
5-
"os"
6-
75
"github.com/GlobalCyberAlliance/domain-security-scanner/pkg/advisor"
86
"github.com/GlobalCyberAlliance/domain-security-scanner/pkg/model"
97
"github.com/GlobalCyberAlliance/domain-security-scanner/pkg/scanner"
108
"github.com/spf13/cobra"
9+
"os"
1110
)
1211

1312
func init() {
@@ -98,7 +97,7 @@ func printResult(result *scanner.Result, domainAdvisor *advisor.Advisor) {
9897
ScanResult: result,
9998
}
10099

101-
if result.Error == "" && advise {
100+
if advise && result.Error != scanner.ErrInvalidDomain {
102101
resultWithAdvice.Advice = domainAdvisor.CheckAll(result.Domain, result.BIMI, result.DKIM, result.DMARC, result.MX, result.SPF)
103102
}
104103

pkg/advisor/advisor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ func (a *Advisor) CheckBIMI(bimi string) (advice []string) {
189189

190190
func (a *Advisor) CheckDKIM(dkim string) (advice []string) {
191191
if dkim == "" {
192-
return []string{"We couldn't detect any active DKIM record for your domain. Please visit https://dmarcguide.globalcyberalliance.org to fix this."}
192+
return []string{"We couldn't detect any active DKIM record for your domain. Due to how DKIM works, we only lookup common/known DKIM selectors (such as x, selector1, google). Visit https://dmarcguide.globalcyberalliance.org for more info on how to configure DKIM for your domain."}
193193
}
194194

195195
if strings.Contains(dkim, ";") {

pkg/http/scan.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ package http
33
import (
44
"context"
55
"fmt"
6-
"net/http"
7-
86
"github.com/GlobalCyberAlliance/domain-security-scanner/pkg/model"
97
"github.com/GlobalCyberAlliance/domain-security-scanner/pkg/scanner"
108
"github.com/danielgtaylor/huma/v2"
9+
"net/http"
1110
)
1211

1312
func (s *Server) registerScanRoutes() {
@@ -44,11 +43,15 @@ func (s *Server) registerScanRoutes() {
4443
return nil, huma.Error500InternalServerError(fmt.Errorf("expected 1 result, got %d", len(results)).Error())
4544
}
4645

46+
if results[0].Error == scanner.ErrInvalidDomain {
47+
return nil, huma.Error400BadRequest(scanner.ErrInvalidDomain)
48+
}
49+
4750
result := model.ScanResultWithAdvice{
4851
ScanResult: results[0],
4952
}
5053

51-
if s.Advisor != nil && result.ScanResult.Error == "" {
54+
if s.Advisor != nil {
5255
result.Advice = s.Advisor.CheckAll(result.ScanResult.Domain, result.ScanResult.BIMI, result.ScanResult.DKIM, result.ScanResult.DMARC, result.ScanResult.MX, result.ScanResult.SPF)
5356
}
5457

@@ -93,7 +96,7 @@ func (s *Server) registerScanRoutes() {
9396
ScanResult: result,
9497
}
9598

96-
if s.Advisor != nil && result.Error == "" {
99+
if s.Advisor != nil && result.Error != scanner.ErrInvalidDomain {
97100
res.Advice = s.Advisor.CheckAll(result.Domain, result.BIMI, result.DKIM, result.DMARC, result.MX, result.SPF)
98101
}
99102

pkg/mail/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func (s *Server) handler() error {
107107
ScanResult: result,
108108
}
109109

110-
if s.advisor != nil || result.Error == "" {
110+
if s.advisor != nil || result.Error != scanner.ErrInvalidDomain {
111111
resultWithAdvice.Advice = s.advisor.CheckAll(result.Domain, result.BIMI, result.DKIM, result.DMARC, result.MX, result.SPF)
112112
}
113113

pkg/scanner/requests.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ var (
2626
"google", // Google
2727
"selector1", // Microsoft
2828
"selector2", // Microsoft
29+
"s1", // Generic
30+
"s2", // Generic
2931
"k1", // MailChimp
3032
"mandrill", // Mandrill
3133
"everlytickey1", // Everlytic
@@ -91,6 +93,11 @@ func (s *Scanner) getDNSAnswers(domain string, recordType uint16) ([]dns.RR, err
9193
}
9294

9395
if in.Rcode != dns.RcodeSuccess {
96+
// disregard NXDOMAIN errors
97+
if in.Rcode == dns.RcodeNameError {
98+
return nil, nil
99+
}
100+
94101
return nil, fmt.Errorf("DNS query failed with rcode %v", in.Rcode)
95102
}
96103

pkg/scanner/scanner.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import (
1717
"github.com/spf13/cast"
1818
)
1919

20+
const (
21+
ErrInvalidDomain = "invalid domain name"
22+
)
23+
2024
type (
2125
Scanner struct {
2226
// cache is a simple in-memory cache to reduce external requests from the scanner.
@@ -79,8 +83,8 @@ func New(logger zerolog.Logger, timeout time.Duration, opts ...Option) (*Scanner
7983
dnsClient.Timeout = timeout
8084

8185
scanner := &Scanner{
82-
dnsClient: dnsClient, // Initialize a new dns.Client
83-
dnsBuffer: 4096, // Set the dnsBuffer size to 1024 bytes
86+
dnsClient: dnsClient,
87+
dnsBuffer: 4096,
8488
logger: logger,
8589
nameservers: []string{"8.8.8.8:53", "8.8.4.4:53", "1.1.1.1:53"}, // Set the default nameservers to Google and Cloudflare
8690
poolSize: uint16(runtime.NumCPU()),
@@ -167,7 +171,7 @@ func (s *Scanner) Scan(domains ...string) ([]*Result, error) {
167171
// fill variable to satisfy deferred cache fill
168172
result = &Result{
169173
Domain: domainToScan,
170-
Error: "invalid domain name",
174+
Error: ErrInvalidDomain,
171175
}
172176

173177
mutex.Lock()

0 commit comments

Comments
 (0)