Rework DNS results
This commit is contained in:
parent
d87b0cbcb0
commit
ec1ab7886e
5 changed files with 441 additions and 286 deletions
169
api/openapi.yaml
169
api/openapi.yaml
|
|
@ -267,10 +267,8 @@ components:
|
||||||
$ref: '#/components/schemas/AuthenticationResults'
|
$ref: '#/components/schemas/AuthenticationResults'
|
||||||
spamassassin:
|
spamassassin:
|
||||||
$ref: '#/components/schemas/SpamAssassinResult'
|
$ref: '#/components/schemas/SpamAssassinResult'
|
||||||
dns_records:
|
dns_results:
|
||||||
type: array
|
$ref: '#/components/schemas/DNSResults'
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/DNSRecord'
|
|
||||||
blacklists:
|
blacklists:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
|
|
@ -694,31 +692,168 @@ components:
|
||||||
type: string
|
type: string
|
||||||
description: Full SpamAssassin report
|
description: Full SpamAssassin report
|
||||||
|
|
||||||
DNSRecord:
|
DNSResults:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- domain
|
- domain
|
||||||
- record_type
|
|
||||||
- status
|
|
||||||
properties:
|
properties:
|
||||||
domain:
|
domain:
|
||||||
type: string
|
type: string
|
||||||
description: Domain name
|
description: Domain name
|
||||||
example: "example.com"
|
example: "example.com"
|
||||||
record_type:
|
mx_records:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/MXRecord'
|
||||||
|
description: MX records for the domain
|
||||||
|
spf_record:
|
||||||
|
$ref: '#/components/schemas/SPFRecord'
|
||||||
|
dkim_records:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/DKIMRecord'
|
||||||
|
description: DKIM records found
|
||||||
|
dmarc_record:
|
||||||
|
$ref: '#/components/schemas/DMARCRecord'
|
||||||
|
bimi_record:
|
||||||
|
$ref: '#/components/schemas/BIMIRecord'
|
||||||
|
errors:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: DNS lookup errors
|
||||||
|
|
||||||
|
MXRecord:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- host
|
||||||
|
- priority
|
||||||
|
- valid
|
||||||
|
properties:
|
||||||
|
host:
|
||||||
type: string
|
type: string
|
||||||
enum: [MX, SPF, DKIM, DMARC, BIMI]
|
description: MX hostname
|
||||||
description: DNS record type
|
example: "mail.example.com"
|
||||||
example: "SPF"
|
priority:
|
||||||
status:
|
type: integer
|
||||||
|
format: uint16
|
||||||
|
description: MX priority (lower is higher priority)
|
||||||
|
example: 10
|
||||||
|
valid:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the MX record is valid
|
||||||
|
example: true
|
||||||
|
error:
|
||||||
type: string
|
type: string
|
||||||
enum: [found, missing, invalid]
|
description: Error message if validation failed
|
||||||
description: Record status
|
example: "Failed to lookup MX records"
|
||||||
example: "found"
|
|
||||||
value:
|
SPFRecord:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- valid
|
||||||
|
properties:
|
||||||
|
record:
|
||||||
type: string
|
type: string
|
||||||
description: Record value
|
description: SPF record content
|
||||||
example: "v=spf1 include:_spf.example.com ~all"
|
example: "v=spf1 include:_spf.example.com ~all"
|
||||||
|
valid:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the SPF record is valid
|
||||||
|
example: true
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: Error message if validation failed
|
||||||
|
example: "No SPF record found"
|
||||||
|
|
||||||
|
DKIMRecord:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- selector
|
||||||
|
- domain
|
||||||
|
- valid
|
||||||
|
properties:
|
||||||
|
selector:
|
||||||
|
type: string
|
||||||
|
description: DKIM selector
|
||||||
|
example: "default"
|
||||||
|
domain:
|
||||||
|
type: string
|
||||||
|
description: Domain name
|
||||||
|
example: "example.com"
|
||||||
|
record:
|
||||||
|
type: string
|
||||||
|
description: DKIM record content
|
||||||
|
example: "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA..."
|
||||||
|
valid:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the DKIM record is valid
|
||||||
|
example: true
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: Error message if validation failed
|
||||||
|
example: "No DKIM record found"
|
||||||
|
|
||||||
|
DMARCRecord:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- valid
|
||||||
|
properties:
|
||||||
|
record:
|
||||||
|
type: string
|
||||||
|
description: DMARC record content
|
||||||
|
example: "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com"
|
||||||
|
policy:
|
||||||
|
type: string
|
||||||
|
enum: [none, quarantine, reject, unknown]
|
||||||
|
description: DMARC policy
|
||||||
|
example: "quarantine"
|
||||||
|
valid:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the DMARC record is valid
|
||||||
|
example: true
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: Error message if validation failed
|
||||||
|
example: "No DMARC record found"
|
||||||
|
|
||||||
|
BIMIRecord:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- selector
|
||||||
|
- domain
|
||||||
|
- valid
|
||||||
|
properties:
|
||||||
|
selector:
|
||||||
|
type: string
|
||||||
|
description: BIMI selector
|
||||||
|
example: "default"
|
||||||
|
domain:
|
||||||
|
type: string
|
||||||
|
description: Domain name
|
||||||
|
example: "example.com"
|
||||||
|
record:
|
||||||
|
type: string
|
||||||
|
description: BIMI record content
|
||||||
|
example: "v=BIMI1; l=https://example.com/logo.svg"
|
||||||
|
logo_url:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
description: URL to the brand logo (SVG)
|
||||||
|
example: "https://example.com/logo.svg"
|
||||||
|
vmc_url:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
description: URL to Verified Mark Certificate (optional)
|
||||||
|
example: "https://example.com/vmc.pem"
|
||||||
|
valid:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the BIMI record is valid
|
||||||
|
example: true
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: Error message if validation failed
|
||||||
|
example: "No BIMI record found"
|
||||||
|
|
||||||
BlacklistCheck:
|
BlacklistCheck:
|
||||||
type: object
|
type: object
|
||||||
|
|
|
||||||
|
|
@ -51,79 +51,25 @@ func NewDNSAnalyzer(timeout time.Duration) *DNSAnalyzer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNSResults represents DNS validation results for an email
|
|
||||||
type DNSResults struct {
|
|
||||||
Domain string
|
|
||||||
MXRecords []MXRecord
|
|
||||||
SPFRecord *SPFRecord
|
|
||||||
DKIMRecords []DKIMRecord
|
|
||||||
DMARCRecord *DMARCRecord
|
|
||||||
BIMIRecord *BIMIRecord
|
|
||||||
Errors []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// MXRecord represents an MX record
|
|
||||||
type MXRecord struct {
|
|
||||||
Host string
|
|
||||||
Priority uint16
|
|
||||||
Valid bool
|
|
||||||
Error string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SPFRecord represents an SPF record
|
|
||||||
type SPFRecord struct {
|
|
||||||
Record string
|
|
||||||
Valid bool
|
|
||||||
Error string
|
|
||||||
}
|
|
||||||
|
|
||||||
// DKIMRecord represents a DKIM record
|
|
||||||
type DKIMRecord struct {
|
|
||||||
Selector string
|
|
||||||
Domain string
|
|
||||||
Record string
|
|
||||||
Valid bool
|
|
||||||
Error string
|
|
||||||
}
|
|
||||||
|
|
||||||
// DMARCRecord represents a DMARC record
|
|
||||||
type DMARCRecord struct {
|
|
||||||
Record string
|
|
||||||
Policy string // none, quarantine, reject
|
|
||||||
Valid bool
|
|
||||||
Error string
|
|
||||||
}
|
|
||||||
|
|
||||||
// BIMIRecord represents a BIMI record
|
|
||||||
type BIMIRecord struct {
|
|
||||||
Selector string
|
|
||||||
Domain string
|
|
||||||
Record string
|
|
||||||
LogoURL string // URL to the brand logo (SVG)
|
|
||||||
VMCURL string // URL to Verified Mark Certificate (optional)
|
|
||||||
Valid bool
|
|
||||||
Error string
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnalyzeDNS performs DNS validation for the email's domain
|
// AnalyzeDNS performs DNS validation for the email's domain
|
||||||
func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, authResults *api.AuthenticationResults) *DNSResults {
|
func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, authResults *api.AuthenticationResults) *api.DNSResults {
|
||||||
// Extract domain from From address
|
// Extract domain from From address
|
||||||
domain := d.extractDomain(email)
|
domain := d.extractDomain(email)
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
return &DNSResults{
|
return &api.DNSResults{
|
||||||
Errors: []string{"Unable to extract domain from email"},
|
Errors: &[]string{"Unable to extract domain from email"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results := &DNSResults{
|
results := &api.DNSResults{
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check MX records
|
// Check MX records
|
||||||
results.MXRecords = d.checkMXRecords(domain)
|
results.MxRecords = d.checkMXRecords(domain)
|
||||||
|
|
||||||
// Check SPF record
|
// Check SPF record
|
||||||
results.SPFRecord = d.checkSPFRecord(domain)
|
results.SpfRecord = d.checkSPFRecord(domain)
|
||||||
|
|
||||||
// Check DKIM records (from authentication results)
|
// Check DKIM records (from authentication results)
|
||||||
if authResults != nil && authResults.Dkim != nil {
|
if authResults != nil && authResults.Dkim != nil {
|
||||||
|
|
@ -131,17 +77,20 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, authResults *api.Authentic
|
||||||
if dkim.Domain != nil && dkim.Selector != nil {
|
if dkim.Domain != nil && dkim.Selector != nil {
|
||||||
dkimRecord := d.checkDKIMRecord(*dkim.Domain, *dkim.Selector)
|
dkimRecord := d.checkDKIMRecord(*dkim.Domain, *dkim.Selector)
|
||||||
if dkimRecord != nil {
|
if dkimRecord != nil {
|
||||||
results.DKIMRecords = append(results.DKIMRecords, *dkimRecord)
|
if results.DkimRecords == nil {
|
||||||
|
results.DkimRecords = new([]api.DKIMRecord)
|
||||||
|
}
|
||||||
|
*results.DkimRecords = append(*results.DkimRecords, *dkimRecord)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check DMARC record
|
// Check DMARC record
|
||||||
results.DMARCRecord = d.checkDMARCRecord(domain)
|
results.DmarcRecord = d.checkDMARCRecord(domain)
|
||||||
|
|
||||||
// Check BIMI record (using default selector)
|
// Check BIMI record (using default selector)
|
||||||
results.BIMIRecord = d.checkBIMIRecord(domain, "default")
|
results.BimiRecord = d.checkBIMIRecord(domain, "default")
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
@ -158,51 +107,51 @@ func (d *DNSAnalyzer) extractDomain(email *EmailMessage) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkMXRecords looks up MX records for a domain
|
// checkMXRecords looks up MX records for a domain
|
||||||
func (d *DNSAnalyzer) checkMXRecords(domain string) []MXRecord {
|
func (d *DNSAnalyzer) checkMXRecords(domain string) *[]api.MXRecord {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), d.Timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), d.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
mxRecords, err := d.resolver.LookupMX(ctx, domain)
|
mxRecords, err := d.resolver.LookupMX(ctx, domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []MXRecord{
|
return &[]api.MXRecord{
|
||||||
{
|
{
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: fmt.Sprintf("Failed to lookup MX records: %v", err),
|
Error: api.PtrTo(fmt.Sprintf("Failed to lookup MX records: %v", err)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(mxRecords) == 0 {
|
if len(mxRecords) == 0 {
|
||||||
return []MXRecord{
|
return &[]api.MXRecord{
|
||||||
{
|
{
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: "No MX records found",
|
Error: api.PtrTo("No MX records found"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []MXRecord
|
var results []api.MXRecord
|
||||||
for _, mx := range mxRecords {
|
for _, mx := range mxRecords {
|
||||||
results = append(results, MXRecord{
|
results = append(results, api.MXRecord{
|
||||||
Host: mx.Host,
|
Host: mx.Host,
|
||||||
Priority: mx.Pref,
|
Priority: mx.Pref,
|
||||||
Valid: true,
|
Valid: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return &results
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkSPFRecord looks up and validates SPF record for a domain
|
// checkSPFRecord looks up and validates SPF record for a domain
|
||||||
func (d *DNSAnalyzer) checkSPFRecord(domain string) *SPFRecord {
|
func (d *DNSAnalyzer) checkSPFRecord(domain string) *api.SPFRecord {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), d.Timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), d.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
txtRecords, err := d.resolver.LookupTXT(ctx, domain)
|
txtRecords, err := d.resolver.LookupTXT(ctx, domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &SPFRecord{
|
return &api.SPFRecord{
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: fmt.Sprintf("Failed to lookup TXT records: %v", err),
|
Error: api.PtrTo(fmt.Sprintf("Failed to lookup TXT records: %v", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,31 +166,31 @@ func (d *DNSAnalyzer) checkSPFRecord(domain string) *SPFRecord {
|
||||||
}
|
}
|
||||||
|
|
||||||
if spfCount == 0 {
|
if spfCount == 0 {
|
||||||
return &SPFRecord{
|
return &api.SPFRecord{
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: "No SPF record found",
|
Error: api.PtrTo("No SPF record found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if spfCount > 1 {
|
if spfCount > 1 {
|
||||||
return &SPFRecord{
|
return &api.SPFRecord{
|
||||||
Record: spfRecord,
|
Record: &spfRecord,
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: "Multiple SPF records found (RFC violation)",
|
Error: api.PtrTo("Multiple SPF records found (RFC violation)"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
if !d.validateSPF(spfRecord) {
|
if !d.validateSPF(spfRecord) {
|
||||||
return &SPFRecord{
|
return &api.SPFRecord{
|
||||||
Record: spfRecord,
|
Record: &spfRecord,
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: "SPF record appears malformed",
|
Error: api.PtrTo("SPF record appears malformed"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SPFRecord{
|
return &api.SPFRecord{
|
||||||
Record: spfRecord,
|
Record: &spfRecord,
|
||||||
Valid: true,
|
Valid: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -267,8 +216,8 @@ func (d *DNSAnalyzer) validateSPF(record string) bool {
|
||||||
return hasValidEnding
|
return hasValidEnding
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDKIMRecord looks up and validates DKIM record for a domain and selector
|
// checkapi.DKIMRecord looks up and validates DKIM record for a domain and selector
|
||||||
func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *DKIMRecord {
|
func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *api.DKIMRecord {
|
||||||
// DKIM records are at: selector._domainkey.domain
|
// DKIM records are at: selector._domainkey.domain
|
||||||
dkimDomain := fmt.Sprintf("%s._domainkey.%s", selector, domain)
|
dkimDomain := fmt.Sprintf("%s._domainkey.%s", selector, domain)
|
||||||
|
|
||||||
|
|
@ -277,20 +226,20 @@ func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *DKIMRecord {
|
||||||
|
|
||||||
txtRecords, err := d.resolver.LookupTXT(ctx, dkimDomain)
|
txtRecords, err := d.resolver.LookupTXT(ctx, dkimDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &DKIMRecord{
|
return &api.DKIMRecord{
|
||||||
Selector: selector,
|
Selector: selector,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: fmt.Sprintf("Failed to lookup DKIM record: %v", err),
|
Error: api.PtrTo(fmt.Sprintf("Failed to lookup DKIM record: %v", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(txtRecords) == 0 {
|
if len(txtRecords) == 0 {
|
||||||
return &DKIMRecord{
|
return &api.DKIMRecord{
|
||||||
Selector: selector,
|
Selector: selector,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: "No DKIM record found",
|
Error: api.PtrTo("No DKIM record found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -299,19 +248,19 @@ func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *DKIMRecord {
|
||||||
|
|
||||||
// Basic validation - should contain "v=DKIM1" and "p=" (public key)
|
// Basic validation - should contain "v=DKIM1" and "p=" (public key)
|
||||||
if !d.validateDKIM(dkimRecord) {
|
if !d.validateDKIM(dkimRecord) {
|
||||||
return &DKIMRecord{
|
return &api.DKIMRecord{
|
||||||
Selector: selector,
|
Selector: selector,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Record: dkimRecord,
|
Record: api.PtrTo(dkimRecord),
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: "DKIM record appears malformed",
|
Error: api.PtrTo("DKIM record appears malformed"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DKIMRecord{
|
return &api.DKIMRecord{
|
||||||
Selector: selector,
|
Selector: selector,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Record: dkimRecord,
|
Record: &dkimRecord,
|
||||||
Valid: true,
|
Valid: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -332,8 +281,8 @@ func (d *DNSAnalyzer) validateDKIM(record string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDMARCRecord looks up and validates DMARC record for a domain
|
// checkapi.DMARCRecord looks up and validates DMARC record for a domain
|
||||||
func (d *DNSAnalyzer) checkDMARCRecord(domain string) *DMARCRecord {
|
func (d *DNSAnalyzer) checkDMARCRecord(domain string) *api.DMARCRecord {
|
||||||
// DMARC records are at: _dmarc.domain
|
// DMARC records are at: _dmarc.domain
|
||||||
dmarcDomain := fmt.Sprintf("_dmarc.%s", domain)
|
dmarcDomain := fmt.Sprintf("_dmarc.%s", domain)
|
||||||
|
|
||||||
|
|
@ -342,9 +291,9 @@ func (d *DNSAnalyzer) checkDMARCRecord(domain string) *DMARCRecord {
|
||||||
|
|
||||||
txtRecords, err := d.resolver.LookupTXT(ctx, dmarcDomain)
|
txtRecords, err := d.resolver.LookupTXT(ctx, dmarcDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &DMARCRecord{
|
return &api.DMARCRecord{
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: fmt.Sprintf("Failed to lookup DMARC record: %v", err),
|
Error: api.PtrTo(fmt.Sprintf("Failed to lookup DMARC record: %v", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,9 +307,9 @@ func (d *DNSAnalyzer) checkDMARCRecord(domain string) *DMARCRecord {
|
||||||
}
|
}
|
||||||
|
|
||||||
if dmarcRecord == "" {
|
if dmarcRecord == "" {
|
||||||
return &DMARCRecord{
|
return &api.DMARCRecord{
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: "No DMARC record found",
|
Error: api.PtrTo("No DMARC record found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -369,17 +318,17 @@ func (d *DNSAnalyzer) checkDMARCRecord(domain string) *DMARCRecord {
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
if !d.validateDMARC(dmarcRecord) {
|
if !d.validateDMARC(dmarcRecord) {
|
||||||
return &DMARCRecord{
|
return &api.DMARCRecord{
|
||||||
Record: dmarcRecord,
|
Record: &dmarcRecord,
|
||||||
Policy: policy,
|
Policy: api.PtrTo(api.DMARCRecordPolicy(policy)),
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: "DMARC record appears malformed",
|
Error: api.PtrTo("DMARC record appears malformed"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DMARCRecord{
|
return &api.DMARCRecord{
|
||||||
Record: dmarcRecord,
|
Record: &dmarcRecord,
|
||||||
Policy: policy,
|
Policy: api.PtrTo(api.DMARCRecordPolicy(policy)),
|
||||||
Valid: true,
|
Valid: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -411,7 +360,7 @@ func (d *DNSAnalyzer) validateDMARC(record string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkBIMIRecord looks up and validates BIMI record for a domain and selector
|
// checkBIMIRecord looks up and validates BIMI record for a domain and selector
|
||||||
func (d *DNSAnalyzer) checkBIMIRecord(domain, selector string) *BIMIRecord {
|
func (d *DNSAnalyzer) checkBIMIRecord(domain, selector string) *api.BIMIRecord {
|
||||||
// BIMI records are at: selector._bimi.domain
|
// BIMI records are at: selector._bimi.domain
|
||||||
bimiDomain := fmt.Sprintf("%s._bimi.%s", selector, domain)
|
bimiDomain := fmt.Sprintf("%s._bimi.%s", selector, domain)
|
||||||
|
|
||||||
|
|
@ -420,20 +369,20 @@ func (d *DNSAnalyzer) checkBIMIRecord(domain, selector string) *BIMIRecord {
|
||||||
|
|
||||||
txtRecords, err := d.resolver.LookupTXT(ctx, bimiDomain)
|
txtRecords, err := d.resolver.LookupTXT(ctx, bimiDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &BIMIRecord{
|
return &api.BIMIRecord{
|
||||||
Selector: selector,
|
Selector: selector,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: fmt.Sprintf("Failed to lookup BIMI record: %v", err),
|
Error: api.PtrTo(fmt.Sprintf("Failed to lookup BIMI record: %v", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(txtRecords) == 0 {
|
if len(txtRecords) == 0 {
|
||||||
return &BIMIRecord{
|
return &api.BIMIRecord{
|
||||||
Selector: selector,
|
Selector: selector,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: "No BIMI record found",
|
Error: api.PtrTo("No BIMI record found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,23 +395,23 @@ func (d *DNSAnalyzer) checkBIMIRecord(domain, selector string) *BIMIRecord {
|
||||||
|
|
||||||
// Basic validation - should contain "v=BIMI1" and "l=" (logo URL)
|
// Basic validation - should contain "v=BIMI1" and "l=" (logo URL)
|
||||||
if !d.validateBIMI(bimiRecord) {
|
if !d.validateBIMI(bimiRecord) {
|
||||||
return &BIMIRecord{
|
return &api.BIMIRecord{
|
||||||
Selector: selector,
|
Selector: selector,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Record: bimiRecord,
|
Record: &bimiRecord,
|
||||||
LogoURL: logoURL,
|
LogoUrl: &logoURL,
|
||||||
VMCURL: vmcURL,
|
VmcUrl: &vmcURL,
|
||||||
Valid: false,
|
Valid: false,
|
||||||
Error: "BIMI record appears malformed",
|
Error: api.PtrTo("BIMI record appears malformed"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &BIMIRecord{
|
return &api.BIMIRecord{
|
||||||
Selector: selector,
|
Selector: selector,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Record: bimiRecord,
|
Record: &bimiRecord,
|
||||||
LogoURL: logoURL,
|
LogoUrl: &logoURL,
|
||||||
VMCURL: vmcURL,
|
VmcUrl: &vmcURL,
|
||||||
Valid: true,
|
Valid: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ type AnalysisResults struct {
|
||||||
Email *EmailMessage
|
Email *EmailMessage
|
||||||
Authentication *api.AuthenticationResults
|
Authentication *api.AuthenticationResults
|
||||||
Content *ContentResults
|
Content *ContentResults
|
||||||
DNS *DNSResults
|
DNS *api.DNSResults
|
||||||
Headers *api.HeaderAnalysis
|
Headers *api.HeaderAnalysis
|
||||||
RBL *RBLResults
|
RBL *RBLResults
|
||||||
SpamAssassin *SpamAssassinResult
|
SpamAssassin *SpamAssassinResult
|
||||||
|
|
@ -141,10 +141,7 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
|
||||||
|
|
||||||
// Add DNS records
|
// Add DNS records
|
||||||
if results.DNS != nil {
|
if results.DNS != nil {
|
||||||
dnsRecords := r.buildDNSRecords(results.DNS)
|
report.DnsResults = results.DNS
|
||||||
if len(dnsRecords) > 0 {
|
|
||||||
report.DnsRecords = &dnsRecords
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add headers results
|
// Add headers results
|
||||||
|
|
@ -204,118 +201,6 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
|
||||||
return report
|
return report
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildDNSRecords converts DNS analysis results to API DNS records
|
|
||||||
func (r *ReportGenerator) buildDNSRecords(dns *DNSResults) []api.DNSRecord {
|
|
||||||
records := []api.DNSRecord{}
|
|
||||||
|
|
||||||
if dns == nil {
|
|
||||||
return records
|
|
||||||
}
|
|
||||||
|
|
||||||
// MX records
|
|
||||||
if len(dns.MXRecords) > 0 {
|
|
||||||
for _, mx := range dns.MXRecords {
|
|
||||||
status := api.Found
|
|
||||||
if !mx.Valid {
|
|
||||||
if mx.Error != "" {
|
|
||||||
status = api.Missing
|
|
||||||
} else {
|
|
||||||
status = api.Invalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record := api.DNSRecord{
|
|
||||||
Domain: dns.Domain,
|
|
||||||
RecordType: api.MX,
|
|
||||||
Status: status,
|
|
||||||
}
|
|
||||||
|
|
||||||
if mx.Host != "" {
|
|
||||||
value := mx.Host
|
|
||||||
record.Value = &value
|
|
||||||
}
|
|
||||||
|
|
||||||
records = append(records, record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SPF record
|
|
||||||
if dns.SPFRecord != nil {
|
|
||||||
status := api.Found
|
|
||||||
if !dns.SPFRecord.Valid {
|
|
||||||
if dns.SPFRecord.Record == "" {
|
|
||||||
status = api.Missing
|
|
||||||
} else {
|
|
||||||
status = api.Invalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record := api.DNSRecord{
|
|
||||||
Domain: dns.Domain,
|
|
||||||
RecordType: api.SPF,
|
|
||||||
Status: status,
|
|
||||||
}
|
|
||||||
|
|
||||||
if dns.SPFRecord.Record != "" {
|
|
||||||
record.Value = &dns.SPFRecord.Record
|
|
||||||
}
|
|
||||||
|
|
||||||
records = append(records, record)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DKIM records
|
|
||||||
for _, dkim := range dns.DKIMRecords {
|
|
||||||
status := api.Found
|
|
||||||
if !dkim.Valid {
|
|
||||||
if dkim.Record == "" {
|
|
||||||
status = api.Missing
|
|
||||||
} else {
|
|
||||||
status = api.Invalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record := api.DNSRecord{
|
|
||||||
Domain: dkim.Domain,
|
|
||||||
RecordType: api.DKIM,
|
|
||||||
Status: status,
|
|
||||||
}
|
|
||||||
|
|
||||||
if dkim.Record != "" {
|
|
||||||
// Include selector in value for clarity
|
|
||||||
value := dkim.Record
|
|
||||||
record.Value = &value
|
|
||||||
}
|
|
||||||
|
|
||||||
records = append(records, record)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DMARC record
|
|
||||||
if dns.DMARCRecord != nil {
|
|
||||||
status := api.Found
|
|
||||||
if !dns.DMARCRecord.Valid {
|
|
||||||
if dns.DMARCRecord.Record == "" {
|
|
||||||
status = api.Missing
|
|
||||||
} else {
|
|
||||||
status = api.Invalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record := api.DNSRecord{
|
|
||||||
Domain: dns.Domain,
|
|
||||||
RecordType: api.DMARC,
|
|
||||||
Status: status,
|
|
||||||
}
|
|
||||||
|
|
||||||
if dns.DMARCRecord.Record != "" {
|
|
||||||
record.Value = &dns.DMARCRecord.Record
|
|
||||||
}
|
|
||||||
|
|
||||||
records = append(records, record)
|
|
||||||
}
|
|
||||||
|
|
||||||
return records
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateRawEmail returns the raw email message as a string
|
// GenerateRawEmail returns the raw email message as a string
|
||||||
func (r *ReportGenerator) GenerateRawEmail(email *EmailMessage) string {
|
func (r *ReportGenerator) GenerateRawEmail(email *EmailMessage) string {
|
||||||
if email == nil {
|
if email == nil {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { DNSRecord } from "$lib/api/types.gen";
|
import type { DNSResults } from "$lib/api/types.gen";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dnsRecords: DNSRecord[];
|
dnsResults?: DNSResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { dnsRecords }: Props = $props();
|
let { dnsResults }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
|
|
@ -16,31 +16,217 @@
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
{#if !dnsResults}
|
||||||
<table class="table table-sm">
|
<p class="text-muted mb-0">No DNS results available</p>
|
||||||
<thead>
|
{:else}
|
||||||
<tr>
|
<div class="mb-3">
|
||||||
<th>Domain</th>
|
<strong>Domain:</strong> <code>{dnsResults.domain}</code>
|
||||||
<th>Type</th>
|
</div>
|
||||||
<th>Status</th>
|
|
||||||
<th>Value</th>
|
{#if dnsResults.errors && dnsResults.errors.length > 0}
|
||||||
</tr>
|
<div class="alert alert-warning mb-3">
|
||||||
</thead>
|
<strong>Errors:</strong>
|
||||||
<tbody>
|
<ul class="mb-0">
|
||||||
{#each dnsRecords as record}
|
{#each dnsResults.errors as error}
|
||||||
<tr>
|
<li>{error}</li>
|
||||||
<td><code>{record.domain}</code></td>
|
{/each}
|
||||||
<td><span class="badge bg-secondary">{record.record_type}</span></td>
|
</ul>
|
||||||
<td>
|
</div>
|
||||||
<span class="badge {record.status === 'found' ? 'bg-success' : record.status === 'missing' ? 'bg-danger' : 'bg-warning'}">
|
{/if}
|
||||||
{record.status}
|
|
||||||
</span>
|
<!-- MX Records -->
|
||||||
</td>
|
{#if dnsResults.mx_records && dnsResults.mx_records.length > 0}
|
||||||
<td><small class="text-muted">{record.value || '-'}</small></td>
|
<div class="mb-4">
|
||||||
</tr>
|
<h5 class="text-muted mb-2">
|
||||||
|
<span class="badge bg-secondary">MX</span> Mail Exchange Records
|
||||||
|
</h5>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Priority</th>
|
||||||
|
<th>Host</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each dnsResults.mx_records as mx}
|
||||||
|
<tr>
|
||||||
|
<td>{mx.priority}</td>
|
||||||
|
<td><code>{mx.host}</code></td>
|
||||||
|
<td>
|
||||||
|
{#if mx.valid}
|
||||||
|
<span class="badge bg-success">Valid</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge bg-danger">Invalid</span>
|
||||||
|
{#if mx.error}
|
||||||
|
<br><small class="text-danger">{mx.error}</small>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- SPF Record -->
|
||||||
|
{#if dnsResults.spf_record}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="text-muted mb-2">
|
||||||
|
<span class="badge bg-secondary">SPF</span> Sender Policy Framework
|
||||||
|
</h5>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
{#if dnsResults.spf_record.valid}
|
||||||
|
<span class="badge bg-success">Valid</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge bg-danger">Invalid</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if dnsResults.spf_record.record}
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Record:</strong><br>
|
||||||
|
<code class="d-block mt-1 text-break">{dnsResults.spf_record.record}</code>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if dnsResults.spf_record.error}
|
||||||
|
<div class="text-danger">
|
||||||
|
<strong>Error:</strong> {dnsResults.spf_record.error}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- DKIM Records -->
|
||||||
|
{#if dnsResults.dkim_records && dnsResults.dkim_records.length > 0}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="text-muted mb-2">
|
||||||
|
<span class="badge bg-secondary">DKIM</span> DomainKeys Identified Mail
|
||||||
|
</h5>
|
||||||
|
{#each dnsResults.dkim_records as dkim}
|
||||||
|
<div class="card mb-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Selector:</strong> <code>{dkim.selector}</code>
|
||||||
|
<strong class="ms-3">Domain:</strong> <code>{dkim.domain}</code>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
{#if dkim.valid}
|
||||||
|
<span class="badge bg-success">Valid</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge bg-danger">Invalid</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if dkim.record}
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Record:</strong><br>
|
||||||
|
<code class="d-block mt-1 text-break small">{dkim.record}</code>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if dkim.error}
|
||||||
|
<div class="text-danger">
|
||||||
|
<strong>Error:</strong> {dkim.error}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
{/if}
|
||||||
</div>
|
|
||||||
|
<!-- DMARC Record -->
|
||||||
|
{#if dnsResults.dmarc_record}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="text-muted mb-2">
|
||||||
|
<span class="badge bg-secondary">DMARC</span> Domain-based Message Authentication
|
||||||
|
</h5>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
{#if dnsResults.dmarc_record.valid}
|
||||||
|
<span class="badge bg-success">Valid</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge bg-danger">Invalid</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if dnsResults.dmarc_record.policy}
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Policy:</strong>
|
||||||
|
<span class="badge {dnsResults.dmarc_record.policy === 'reject' ? 'bg-success' : dnsResults.dmarc_record.policy === 'quarantine' ? 'bg-warning' : 'bg-secondary'}">
|
||||||
|
{dnsResults.dmarc_record.policy}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if dnsResults.dmarc_record.record}
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Record:</strong><br>
|
||||||
|
<code class="d-block mt-1 text-break">{dnsResults.dmarc_record.record}</code>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if dnsResults.dmarc_record.error}
|
||||||
|
<div class="text-danger">
|
||||||
|
<strong>Error:</strong> {dnsResults.dmarc_record.error}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- BIMI Record -->
|
||||||
|
{#if dnsResults.bimi_record}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="text-muted mb-2">
|
||||||
|
<span class="badge bg-secondary">BIMI</span> Brand Indicators for Message Identification
|
||||||
|
</h5>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Selector:</strong> <code>{dnsResults.bimi_record.selector}</code>
|
||||||
|
<strong class="ms-3">Domain:</strong> <code>{dnsResults.bimi_record.domain}</code>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
{#if dnsResults.bimi_record.valid}
|
||||||
|
<span class="badge bg-success">Valid</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge bg-danger">Invalid</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if dnsResults.bimi_record.logo_url}
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Logo URL:</strong> <a href={dnsResults.bimi_record.logo_url} target="_blank" rel="noopener noreferrer">{dnsResults.bimi_record.logo_url}</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if dnsResults.bimi_record.vmc_url}
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>VMC URL:</strong> <a href={dnsResults.bimi_record.vmc_url} target="_blank" rel="noopener noreferrer">{dnsResults.bimi_record.vmc_url}</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if dnsResults.bimi_record.record}
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Record:</strong><br>
|
||||||
|
<code class="d-block mt-1 text-break">{dnsResults.bimi_record.record}</code>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if dnsResults.bimi_record.error}
|
||||||
|
<div class="text-danger">
|
||||||
|
<strong>Error:</strong> {dnsResults.bimi_record.error}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -145,10 +145,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DNS Records -->
|
<!-- DNS Records -->
|
||||||
{#if report.dns_records && report.dns_records.length > 0}
|
{#if report.dns_results}
|
||||||
<div class="row mb-4" id="dns">
|
<div class="row mb-4" id="dns">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<DnsRecordsCard dnsRecords={report.dns_records} />
|
<DnsRecordsCard dnsResults={report.dns_results} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue