Rework DNS results

This commit is contained in:
nemunaire 2025-10-21 17:00:15 +07:00
commit ec1ab7886e
5 changed files with 441 additions and 286 deletions

View file

@ -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 type: string
enum: [MX, SPF, DKIM, DMARC, BIMI] description: DNS lookup errors
description: DNS record type
example: "SPF" MXRecord:
status: type: object
required:
- host
- priority
- valid
properties:
host:
type: string type: string
enum: [found, missing, invalid] description: MX hostname
description: Record status example: "mail.example.com"
example: "found" priority:
value: 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
description: Record value description: Error message if validation failed
example: "Failed to lookup MX records"
SPFRecord:
type: object
required:
- valid
properties:
record:
type: string
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

View file

@ -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,
} }
} }

View file

@ -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 {

View file

@ -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">
{#if !dnsResults}
<p class="text-muted mb-0">No DNS results available</p>
{:else}
<div class="mb-3">
<strong>Domain:</strong> <code>{dnsResults.domain}</code>
</div>
{#if dnsResults.errors && dnsResults.errors.length > 0}
<div class="alert alert-warning mb-3">
<strong>Errors:</strong>
<ul class="mb-0">
{#each dnsResults.errors as error}
<li>{error}</li>
{/each}
</ul>
</div>
{/if}
<!-- MX Records -->
{#if dnsResults.mx_records && dnsResults.mx_records.length > 0}
<div class="mb-4">
<h5 class="text-muted mb-2">
<span class="badge bg-secondary">MX</span> Mail Exchange Records
</h5>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm"> <table class="table table-sm table-bordered">
<thead> <thead>
<tr> <tr>
<th>Domain</th> <th>Priority</th>
<th>Type</th> <th>Host</th>
<th>Status</th> <th>Status</th>
<th>Value</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each dnsRecords as record} {#each dnsResults.mx_records as mx}
<tr> <tr>
<td><code>{record.domain}</code></td> <td>{mx.priority}</td>
<td><span class="badge bg-secondary">{record.record_type}</span></td> <td><code>{mx.host}</code></td>
<td> <td>
<span class="badge {record.status === 'found' ? 'bg-success' : record.status === 'missing' ? 'bg-danger' : 'bg-warning'}"> {#if mx.valid}
{record.status} <span class="badge bg-success">Valid</span>
</span> {:else}
<span class="badge bg-danger">Invalid</span>
{#if mx.error}
<br><small class="text-danger">{mx.error}</small>
{/if}
{/if}
</td> </td>
<td><small class="text-muted">{record.value || '-'}</small></td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> </div>
</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}
</div>
{/if}
<!-- 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>

View file

@ -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}