Compare commits
2 commits
master
...
internal_r
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d1dce038f | |||
| 6ae2c2463d |
8 changed files with 375 additions and 16 deletions
|
|
@ -942,6 +942,10 @@ components:
|
|||
$ref: '#/components/schemas/DMARCRecord'
|
||||
bimi_record:
|
||||
$ref: '#/components/schemas/BIMIRecord'
|
||||
dnssec_enabled:
|
||||
type: boolean
|
||||
description: Whether the From domain has DNSSEC enabled with valid chain of trust
|
||||
example: true
|
||||
ptr_records:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -47,6 +47,7 @@ require (
|
|||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/miekg/dns v1.1.4 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
|
|
@ -55,6 +56,7 @@ require (
|
|||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/peterzen/goresolver v1.0.2 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.56.0 // indirect
|
||||
github.com/redis/go-redis/v9 v9.16.0 // indirect
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -120,6 +120,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0=
|
||||
github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
|
@ -154,6 +156,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
|||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/peterzen/goresolver v1.0.2 h1:UxRxk835Onz7Go4oPUsOptSmBlIvN/yJ2kv3Srr3hw4=
|
||||
github.com/peterzen/goresolver v1.0.2/go.mod h1:LrWRiOeCYApgvR2OhpipNOeaE1yGfI+QQjpF0riJC8M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
|
|
@ -198,6 +202,7 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
|||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
|
@ -207,6 +212,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
|
|
@ -216,12 +222,14 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
|||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222171317-cd391775e71e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
|
|||
|
|
@ -127,6 +127,12 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, authResults *api.Authentic
|
|||
// Check BIMI record (for From domain - branding is based on visible sender)
|
||||
results.BimiRecord = d.checkBIMIRecord(fromDomain, "default")
|
||||
|
||||
// Check DNSSEC status (for From domain)
|
||||
dnssecEnabled, err := d.resolver.IsDNSSECEnabled(nil, fromDomain)
|
||||
if err == nil {
|
||||
results.DnssecEnabled = &dnssecEnabled
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +155,12 @@ func (d *DNSAnalyzer) AnalyzeDomainOnly(domain string) *api.DNSResults {
|
|||
// Check BIMI record with default selector
|
||||
results.BimiRecord = d.checkBIMIRecord(domain, "default")
|
||||
|
||||
// Check DNSSEC status
|
||||
dnssecEnabled, err := d.resolver.IsDNSSECEnabled(nil, domain)
|
||||
if err == nil {
|
||||
results.DnssecEnabled = &dnssecEnabled
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
|
|
@ -204,11 +216,16 @@ func (d *DNSAnalyzer) CalculateDNSScore(results *api.DNSResults, senderIP string
|
|||
|
||||
score := 0
|
||||
|
||||
// DNSSEC: 10 points
|
||||
if results.DnssecEnabled != nil && *results.DnssecEnabled {
|
||||
score += 10
|
||||
}
|
||||
|
||||
// PTR and Forward DNS: 20 points
|
||||
score += 20 * d.calculatePTRScore(results, senderIP) / 100
|
||||
|
||||
// MX Records: 20 points (10 for From domain, 10 for Return-Path domain)
|
||||
score += 20 * d.calculateMXScore(results) / 100
|
||||
// MX Records: 10 points (5 for From domain, 5 for Return-Path domain)
|
||||
score += 10 * d.calculateMXScore(results) / 100
|
||||
|
||||
// SPF Records: 20 points
|
||||
score += 20 * d.calculateSPFScore(results) / 100
|
||||
|
|
|
|||
|
|
@ -23,7 +23,12 @@ package analyzer
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/peterzen/goresolver"
|
||||
)
|
||||
|
||||
// DNSResolver defines the interface for DNS resolution operations.
|
||||
|
|
@ -43,38 +48,190 @@ type DNSResolver interface {
|
|||
// LookupHost looks up the given hostname using the local resolver.
|
||||
// It returns a slice of that host's addresses (IPv4 and IPv6).
|
||||
LookupHost(ctx context.Context, host string) ([]string, error)
|
||||
|
||||
// IsDNSSECEnabled checks if the given domain has DNSSEC enabled by querying for DNSKEY records.
|
||||
// Returns true if the domain has DNSSEC configured and the chain of trust is valid.
|
||||
IsDNSSECEnabled(ctx context.Context, domain string) (bool, error)
|
||||
}
|
||||
|
||||
// StandardDNSResolver is the default DNS resolver implementation that uses net.Resolver.
|
||||
// StandardDNSResolver is the default DNS resolver implementation that uses goresolver with DNSSEC validation.
|
||||
type StandardDNSResolver struct {
|
||||
resolver *net.Resolver
|
||||
resolver *goresolver.Resolver
|
||||
}
|
||||
|
||||
// NewStandardDNSResolver creates a new StandardDNSResolver with default settings.
|
||||
// NewStandardDNSResolver creates a new StandardDNSResolver with DNSSEC validation support.
|
||||
func NewStandardDNSResolver() DNSResolver {
|
||||
// Pass /etc/resolv.conf to load default DNS configuration
|
||||
resolver, err := goresolver.NewResolver("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to initialize goresolver: %v", err))
|
||||
}
|
||||
|
||||
return &StandardDNSResolver{
|
||||
resolver: &net.Resolver{
|
||||
PreferGo: true,
|
||||
},
|
||||
resolver: resolver,
|
||||
}
|
||||
}
|
||||
|
||||
// LookupMX implements DNSResolver.LookupMX using net.Resolver.
|
||||
// LookupMX implements DNSResolver.LookupMX using goresolver with DNSSEC validation.
|
||||
func (r *StandardDNSResolver) LookupMX(ctx context.Context, name string) ([]*net.MX, error) {
|
||||
return r.resolver.LookupMX(ctx, name)
|
||||
// Ensure the name ends with a dot for DNS queries
|
||||
queryName := name
|
||||
if !strings.HasSuffix(queryName, ".") {
|
||||
queryName = queryName + "."
|
||||
}
|
||||
|
||||
rrs, err := r.resolver.StrictNSQuery(queryName, dns.TypeMX)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mxRecords := make([]*net.MX, 0, len(rrs))
|
||||
for _, rr := range rrs {
|
||||
if mx, ok := rr.(*dns.MX); ok {
|
||||
mxRecords = append(mxRecords, &net.MX{
|
||||
Host: strings.TrimSuffix(mx.Mx, "."),
|
||||
Pref: mx.Preference,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(mxRecords) == 0 {
|
||||
return nil, fmt.Errorf("no MX records found for %s", name)
|
||||
}
|
||||
|
||||
return mxRecords, nil
|
||||
}
|
||||
|
||||
// LookupTXT implements DNSResolver.LookupTXT using net.Resolver.
|
||||
// LookupTXT implements DNSResolver.LookupTXT using goresolver with DNSSEC validation.
|
||||
func (r *StandardDNSResolver) LookupTXT(ctx context.Context, name string) ([]string, error) {
|
||||
return r.resolver.LookupTXT(ctx, name)
|
||||
// Ensure the name ends with a dot for DNS queries
|
||||
queryName := name
|
||||
if !strings.HasSuffix(queryName, ".") {
|
||||
queryName = queryName + "."
|
||||
}
|
||||
|
||||
rrs, err := r.resolver.StrictNSQuery(queryName, dns.TypeTXT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txtRecords := make([]string, 0, len(rrs))
|
||||
for _, rr := range rrs {
|
||||
if txt, ok := rr.(*dns.TXT); ok {
|
||||
// Join all TXT strings (a single TXT record can have multiple strings)
|
||||
txtRecords = append(txtRecords, strings.Join(txt.Txt, ""))
|
||||
}
|
||||
}
|
||||
|
||||
if len(txtRecords) == 0 {
|
||||
return nil, fmt.Errorf("no TXT records found for %s", name)
|
||||
}
|
||||
|
||||
return txtRecords, nil
|
||||
}
|
||||
|
||||
// LookupAddr implements DNSResolver.LookupAddr using net.Resolver.
|
||||
// LookupAddr implements DNSResolver.LookupAddr using goresolver with DNSSEC validation.
|
||||
func (r *StandardDNSResolver) LookupAddr(ctx context.Context, addr string) ([]string, error) {
|
||||
return r.resolver.LookupAddr(ctx, addr)
|
||||
// Convert IP address to reverse DNS name (e.g., 1.0.0.127.in-addr.arpa.)
|
||||
arpa, err := dns.ReverseAddr(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid IP address: %w", err)
|
||||
}
|
||||
|
||||
rrs, err := r.resolver.StrictNSQuery(arpa, dns.TypePTR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ptrRecords := make([]string, 0, len(rrs))
|
||||
for _, rr := range rrs {
|
||||
if ptr, ok := rr.(*dns.PTR); ok {
|
||||
ptrRecords = append(ptrRecords, strings.TrimSuffix(ptr.Ptr, "."))
|
||||
}
|
||||
}
|
||||
|
||||
if len(ptrRecords) == 0 {
|
||||
return nil, fmt.Errorf("no PTR records found for %s", addr)
|
||||
}
|
||||
|
||||
return ptrRecords, nil
|
||||
}
|
||||
|
||||
// LookupHost implements DNSResolver.LookupHost using net.Resolver.
|
||||
// LookupHost implements DNSResolver.LookupHost using goresolver with DNSSEC validation.
|
||||
func (r *StandardDNSResolver) LookupHost(ctx context.Context, host string) ([]string, error) {
|
||||
return r.resolver.LookupHost(ctx, host)
|
||||
// Ensure the host ends with a dot for DNS queries
|
||||
queryName := host
|
||||
if !strings.HasSuffix(queryName, ".") {
|
||||
queryName = queryName + "."
|
||||
}
|
||||
|
||||
var allAddrs []string
|
||||
|
||||
// Query A records (IPv4)
|
||||
rrsA, errA := r.resolver.StrictNSQuery(queryName, dns.TypeA)
|
||||
if errA == nil {
|
||||
for _, rr := range rrsA {
|
||||
if a, ok := rr.(*dns.A); ok {
|
||||
allAddrs = append(allAddrs, a.A.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Query AAAA records (IPv6)
|
||||
rrsAAAA, errAAAA := r.resolver.StrictNSQuery(queryName, dns.TypeAAAA)
|
||||
if errAAAA == nil {
|
||||
for _, rr := range rrsAAAA {
|
||||
if aaaa, ok := rr.(*dns.AAAA); ok {
|
||||
allAddrs = append(allAddrs, aaaa.AAAA.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return error only if both queries failed
|
||||
if errA != nil && errAAAA != nil {
|
||||
return nil, fmt.Errorf("failed to resolve host: IPv4 error: %v, IPv6 error: %v", errA, errAAAA)
|
||||
}
|
||||
|
||||
if len(allAddrs) == 0 {
|
||||
return nil, fmt.Errorf("no A or AAAA records found for %s", host)
|
||||
}
|
||||
|
||||
return allAddrs, nil
|
||||
}
|
||||
|
||||
// IsDNSSECEnabled checks if the given domain has DNSSEC enabled by querying for DNSKEY records.
|
||||
// It uses DNSSEC validation to ensure the chain of trust is valid.
|
||||
// Returns true if DNSSEC is properly configured and validated, false otherwise.
|
||||
func (r *StandardDNSResolver) IsDNSSECEnabled(ctx context.Context, domain string) (bool, error) {
|
||||
// Ensure the domain ends with a dot for DNS queries
|
||||
queryName := domain
|
||||
if !strings.HasSuffix(queryName, ".") {
|
||||
queryName = queryName + "."
|
||||
}
|
||||
|
||||
// Query for DNSKEY records with DNSSEC validation
|
||||
// If this succeeds, it means:
|
||||
// 1. The domain has DNSKEY records (DNSSEC is configured)
|
||||
// 2. The DNSSEC chain of trust is valid (validated by StrictNSQuery)
|
||||
rrs, err := r.resolver.StrictNSQuery(queryName, dns.TypeDNSKEY)
|
||||
if err != nil {
|
||||
// DNSSEC is not enabled or validation failed
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check if we got any DNSKEY records
|
||||
if len(rrs) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Verify we actually have DNSKEY records (not just any RR type)
|
||||
hasDNSKEY := false
|
||||
for _, rr := range rrs {
|
||||
if _, ok := rr.(*dns.DNSKEY); ok {
|
||||
hasDNSKEY = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return hasDNSKEY, nil
|
||||
}
|
||||
|
|
|
|||
111
pkg/analyzer/dns_resolver_test.go
Normal file
111
pkg/analyzer/dns_resolver_test.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// This file is part of the happyDeliver (R) project.
|
||||
// Copyright (c) 2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsDNSSECEnabled(t *testing.T) {
|
||||
resolver := NewStandardDNSResolver()
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
expectDNSSEC bool
|
||||
}{
|
||||
{
|
||||
name: "ietf.org has DNSSEC",
|
||||
domain: "ietf.org",
|
||||
expectDNSSEC: true,
|
||||
},
|
||||
{
|
||||
name: "google.com doesn't have DNSSEC",
|
||||
domain: "google.com",
|
||||
expectDNSSEC: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
enabled, err := resolver.IsDNSSECEnabled(ctx, tt.domain)
|
||||
if err != nil {
|
||||
t.Errorf("IsDNSSECEnabled() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if enabled != tt.expectDNSSEC {
|
||||
t.Errorf("IsDNSSECEnabled() for %s = %v, want %v", tt.domain, enabled, tt.expectDNSSEC)
|
||||
} else {
|
||||
// Log the result even if we're not validating
|
||||
if enabled {
|
||||
t.Logf("%s: DNSSEC is enabled ✅", tt.domain)
|
||||
} else {
|
||||
t.Logf("%s: DNSSEC is NOT enabled ⚠️", tt.domain)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDNSSECEnabled_NonExistentDomain(t *testing.T) {
|
||||
resolver := NewStandardDNSResolver()
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with a domain that doesn't exist
|
||||
enabled, err := resolver.IsDNSSECEnabled(ctx, "this-domain-definitely-does-not-exist-12345.com")
|
||||
if err != nil {
|
||||
// Error is acceptable for non-existent domains
|
||||
t.Logf("Non-existent domain returned error (expected): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If no error, DNSSEC should be disabled
|
||||
if enabled {
|
||||
t.Error("IsDNSSECEnabled() for non-existent domain should return false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDNSSECEnabled_WithTrailingDot(t *testing.T) {
|
||||
resolver := NewStandardDNSResolver()
|
||||
ctx := context.Background()
|
||||
|
||||
// Test that both formats work
|
||||
domain1 := "cloudflare.com"
|
||||
domain2 := "cloudflare.com."
|
||||
|
||||
enabled1, err1 := resolver.IsDNSSECEnabled(ctx, domain1)
|
||||
if err1 != nil {
|
||||
t.Errorf("IsDNSSECEnabled() without trailing dot error = %v", err1)
|
||||
}
|
||||
|
||||
enabled2, err2 := resolver.IsDNSSECEnabled(ctx, domain2)
|
||||
if err2 != nil {
|
||||
t.Errorf("IsDNSSECEnabled() with trailing dot error = %v", err2)
|
||||
}
|
||||
|
||||
if enabled1 != enabled2 {
|
||||
t.Errorf("IsDNSSECEnabled() results differ: without dot = %v, with dot = %v", enabled1, enabled2)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
import BimiRecordDisplay from "./BimiRecordDisplay.svelte";
|
||||
import PtrRecordsDisplay from "./PtrRecordsDisplay.svelte";
|
||||
import PtrForwardRecordsDisplay from "./PtrForwardRecordsDisplay.svelte";
|
||||
import DnssecDisplay from "./DnssecDisplay.svelte";
|
||||
|
||||
interface Props {
|
||||
domainAlignment?: DomainAlignment;
|
||||
|
|
@ -150,6 +151,9 @@
|
|||
|
||||
<!-- BIMI Record -->
|
||||
<BimiRecordDisplay bimiRecord={dnsResults.bimi_record} />
|
||||
|
||||
<!-- DNSSEC -->
|
||||
<DnssecDisplay dnssecEnabled={dnsResults.dnssec_enabled} domain={dnsResults.from_domain} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
56
web/src/lib/components/DnssecDisplay.svelte
Normal file
56
web/src/lib/components/DnssecDisplay.svelte
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
dnssecEnabled?: boolean;
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
let { dnssecEnabled, domain }: Props = $props();
|
||||
|
||||
// DNSSEC is valid if it's explicitly enabled
|
||||
const dnssecIsValid = $derived(dnssecEnabled === true);
|
||||
</script>
|
||||
|
||||
{#if dnssecEnabled !== undefined}
|
||||
<div class="card mb-4" id="dns-dnssec">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="text-muted mb-0">
|
||||
<i
|
||||
class="bi"
|
||||
class:bi-shield-check={dnssecIsValid}
|
||||
class:text-success={dnssecIsValid}
|
||||
class:bi-shield-x={!dnssecIsValid}
|
||||
class:text-warning={!dnssecIsValid}
|
||||
></i>
|
||||
DNSSEC
|
||||
</h5>
|
||||
<span class="badge bg-secondary">Security</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text small text-muted mb-3">
|
||||
DNSSEC (DNS Security Extensions) adds cryptographic signatures to DNS records to verify
|
||||
their authenticity and integrity. It protects against DNS spoofing and cache poisoning
|
||||
attacks, ensuring that DNS responses haven't been tampered with.
|
||||
</p>
|
||||
{#if domain}
|
||||
<div class="mb-2">
|
||||
<strong>Domain:</strong> <code>{domain}</code>
|
||||
</div>
|
||||
{/if}
|
||||
{#if dnssecIsValid}
|
||||
<div class="alert alert-success mb-0">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
<strong>Enabled:</strong> DNSSEC is properly configured with a valid chain of trust.
|
||||
This provides additional security and authenticity for your domain's DNS records.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="alert alert-warning mb-0">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Not Enabled:</strong> DNSSEC is not configured for this domain. While not
|
||||
required for email delivery, enabling DNSSEC provides additional security by protecting
|
||||
against DNS-based attacks. Consider enabling DNSSEC through your domain registrar or
|
||||
DNS provider.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Loading…
Add table
Add a link
Reference in a new issue