Rework DNS results
This commit is contained in:
parent
eef6f4cf57
commit
8fbea49334
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'
|
||||
spamassassin:
|
||||
$ref: '#/components/schemas/SpamAssassinResult'
|
||||
dns_records:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DNSRecord'
|
||||
dns_results:
|
||||
$ref: '#/components/schemas/DNSResults'
|
||||
blacklists:
|
||||
type: object
|
||||
additionalProperties:
|
||||
|
|
@ -694,31 +692,168 @@ components:
|
|||
type: string
|
||||
description: Full SpamAssassin report
|
||||
|
||||
DNSRecord:
|
||||
DNSResults:
|
||||
type: object
|
||||
required:
|
||||
- domain
|
||||
- record_type
|
||||
- status
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
description: Domain name
|
||||
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
|
||||
enum: [MX, SPF, DKIM, DMARC, BIMI]
|
||||
description: DNS record type
|
||||
example: "SPF"
|
||||
status:
|
||||
description: MX hostname
|
||||
example: "mail.example.com"
|
||||
priority:
|
||||
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
|
||||
enum: [found, missing, invalid]
|
||||
description: Record status
|
||||
example: "found"
|
||||
value:
|
||||
description: Error message if validation failed
|
||||
example: "Failed to lookup MX records"
|
||||
|
||||
SPFRecord:
|
||||
type: object
|
||||
required:
|
||||
- valid
|
||||
properties:
|
||||
record:
|
||||
type: string
|
||||
description: Record value
|
||||
description: SPF record content
|
||||
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:
|
||||
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
|
||||
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
|
||||
domain := d.extractDomain(email)
|
||||
if domain == "" {
|
||||
return &DNSResults{
|
||||
Errors: []string{"Unable to extract domain from email"},
|
||||
return &api.DNSResults{
|
||||
Errors: &[]string{"Unable to extract domain from email"},
|
||||
}
|
||||
}
|
||||
|
||||
results := &DNSResults{
|
||||
results := &api.DNSResults{
|
||||
Domain: domain,
|
||||
}
|
||||
|
||||
// Check MX records
|
||||
results.MXRecords = d.checkMXRecords(domain)
|
||||
results.MxRecords = d.checkMXRecords(domain)
|
||||
|
||||
// Check SPF record
|
||||
results.SPFRecord = d.checkSPFRecord(domain)
|
||||
results.SpfRecord = d.checkSPFRecord(domain)
|
||||
|
||||
// Check DKIM records (from authentication results)
|
||||
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 {
|
||||
dkimRecord := d.checkDKIMRecord(*dkim.Domain, *dkim.Selector)
|
||||
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
|
||||
results.DMARCRecord = d.checkDMARCRecord(domain)
|
||||
results.DmarcRecord = d.checkDMARCRecord(domain)
|
||||
|
||||
// Check BIMI record (using default selector)
|
||||
results.BIMIRecord = d.checkBIMIRecord(domain, "default")
|
||||
results.BimiRecord = d.checkBIMIRecord(domain, "default")
|
||||
|
||||
return results
|
||||
}
|
||||
|
|
@ -158,51 +107,51 @@ func (d *DNSAnalyzer) extractDomain(email *EmailMessage) string {
|
|||
}
|
||||
|
||||
// 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)
|
||||
defer cancel()
|
||||
|
||||
mxRecords, err := d.resolver.LookupMX(ctx, domain)
|
||||
if err != nil {
|
||||
return []MXRecord{
|
||||
return &[]api.MXRecord{
|
||||
{
|
||||
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 {
|
||||
return []MXRecord{
|
||||
return &[]api.MXRecord{
|
||||
{
|
||||
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 {
|
||||
results = append(results, MXRecord{
|
||||
results = append(results, api.MXRecord{
|
||||
Host: mx.Host,
|
||||
Priority: mx.Pref,
|
||||
Valid: true,
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
return &results
|
||||
}
|
||||
|
||||
// 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)
|
||||
defer cancel()
|
||||
|
||||
txtRecords, err := d.resolver.LookupTXT(ctx, domain)
|
||||
if err != nil {
|
||||
return &SPFRecord{
|
||||
return &api.SPFRecord{
|
||||
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 {
|
||||
return &SPFRecord{
|
||||
return &api.SPFRecord{
|
||||
Valid: false,
|
||||
Error: "No SPF record found",
|
||||
Error: api.PtrTo("No SPF record found"),
|
||||
}
|
||||
}
|
||||
|
||||
if spfCount > 1 {
|
||||
return &SPFRecord{
|
||||
Record: spfRecord,
|
||||
return &api.SPFRecord{
|
||||
Record: &spfRecord,
|
||||
Valid: false,
|
||||
Error: "Multiple SPF records found (RFC violation)",
|
||||
Error: api.PtrTo("Multiple SPF records found (RFC violation)"),
|
||||
}
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if !d.validateSPF(spfRecord) {
|
||||
return &SPFRecord{
|
||||
Record: spfRecord,
|
||||
return &api.SPFRecord{
|
||||
Record: &spfRecord,
|
||||
Valid: false,
|
||||
Error: "SPF record appears malformed",
|
||||
Error: api.PtrTo("SPF record appears malformed"),
|
||||
}
|
||||
}
|
||||
|
||||
return &SPFRecord{
|
||||
Record: spfRecord,
|
||||
return &api.SPFRecord{
|
||||
Record: &spfRecord,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
|
@ -267,8 +216,8 @@ func (d *DNSAnalyzer) validateSPF(record string) bool {
|
|||
return hasValidEnding
|
||||
}
|
||||
|
||||
// checkDKIMRecord looks up and validates DKIM record for a domain and selector
|
||||
func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *DKIMRecord {
|
||||
// checkapi.DKIMRecord looks up and validates DKIM record for a domain and selector
|
||||
func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *api.DKIMRecord {
|
||||
// DKIM records are at: selector._domainkey.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)
|
||||
if err != nil {
|
||||
return &DKIMRecord{
|
||||
return &api.DKIMRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
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 {
|
||||
return &DKIMRecord{
|
||||
return &api.DKIMRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
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)
|
||||
if !d.validateDKIM(dkimRecord) {
|
||||
return &DKIMRecord{
|
||||
return &api.DKIMRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
Record: dkimRecord,
|
||||
Record: api.PtrTo(dkimRecord),
|
||||
Valid: false,
|
||||
Error: "DKIM record appears malformed",
|
||||
Error: api.PtrTo("DKIM record appears malformed"),
|
||||
}
|
||||
}
|
||||
|
||||
return &DKIMRecord{
|
||||
return &api.DKIMRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
Record: dkimRecord,
|
||||
Record: &dkimRecord,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
|
@ -332,8 +281,8 @@ func (d *DNSAnalyzer) validateDKIM(record string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// checkDMARCRecord looks up and validates DMARC record for a domain
|
||||
func (d *DNSAnalyzer) checkDMARCRecord(domain string) *DMARCRecord {
|
||||
// checkapi.DMARCRecord looks up and validates DMARC record for a domain
|
||||
func (d *DNSAnalyzer) checkDMARCRecord(domain string) *api.DMARCRecord {
|
||||
// DMARC records are at: _dmarc.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)
|
||||
if err != nil {
|
||||
return &DMARCRecord{
|
||||
return &api.DMARCRecord{
|
||||
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 == "" {
|
||||
return &DMARCRecord{
|
||||
return &api.DMARCRecord{
|
||||
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
|
||||
if !d.validateDMARC(dmarcRecord) {
|
||||
return &DMARCRecord{
|
||||
Record: dmarcRecord,
|
||||
Policy: policy,
|
||||
return &api.DMARCRecord{
|
||||
Record: &dmarcRecord,
|
||||
Policy: api.PtrTo(api.DMARCRecordPolicy(policy)),
|
||||
Valid: false,
|
||||
Error: "DMARC record appears malformed",
|
||||
Error: api.PtrTo("DMARC record appears malformed"),
|
||||
}
|
||||
}
|
||||
|
||||
return &DMARCRecord{
|
||||
Record: dmarcRecord,
|
||||
Policy: policy,
|
||||
return &api.DMARCRecord{
|
||||
Record: &dmarcRecord,
|
||||
Policy: api.PtrTo(api.DMARCRecordPolicy(policy)),
|
||||
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
|
||||
func (d *DNSAnalyzer) checkBIMIRecord(domain, selector string) *BIMIRecord {
|
||||
func (d *DNSAnalyzer) checkBIMIRecord(domain, selector string) *api.BIMIRecord {
|
||||
// BIMI records are at: selector._bimi.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)
|
||||
if err != nil {
|
||||
return &BIMIRecord{
|
||||
return &api.BIMIRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
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 {
|
||||
return &BIMIRecord{
|
||||
return &api.BIMIRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
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)
|
||||
if !d.validateBIMI(bimiRecord) {
|
||||
return &BIMIRecord{
|
||||
return &api.BIMIRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
Record: bimiRecord,
|
||||
LogoURL: logoURL,
|
||||
VMCURL: vmcURL,
|
||||
Record: &bimiRecord,
|
||||
LogoUrl: &logoURL,
|
||||
VmcUrl: &vmcURL,
|
||||
Valid: false,
|
||||
Error: "BIMI record appears malformed",
|
||||
Error: api.PtrTo("BIMI record appears malformed"),
|
||||
}
|
||||
}
|
||||
|
||||
return &BIMIRecord{
|
||||
return &api.BIMIRecord{
|
||||
Selector: selector,
|
||||
Domain: domain,
|
||||
Record: bimiRecord,
|
||||
LogoURL: logoURL,
|
||||
VMCURL: vmcURL,
|
||||
Record: &bimiRecord,
|
||||
LogoUrl: &logoURL,
|
||||
VmcUrl: &vmcURL,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ type AnalysisResults struct {
|
|||
Email *EmailMessage
|
||||
Authentication *api.AuthenticationResults
|
||||
Content *ContentResults
|
||||
DNS *DNSResults
|
||||
DNS *api.DNSResults
|
||||
Headers *api.HeaderAnalysis
|
||||
RBL *RBLResults
|
||||
SpamAssassin *SpamAssassinResult
|
||||
|
|
@ -141,10 +141,7 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
|
|||
|
||||
// Add DNS records
|
||||
if results.DNS != nil {
|
||||
dnsRecords := r.buildDNSRecords(results.DNS)
|
||||
if len(dnsRecords) > 0 {
|
||||
report.DnsRecords = &dnsRecords
|
||||
}
|
||||
report.DnsResults = results.DNS
|
||||
}
|
||||
|
||||
// Add headers results
|
||||
|
|
@ -204,118 +201,6 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
|
|||
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
|
||||
func (r *ReportGenerator) GenerateRawEmail(email *EmailMessage) string {
|
||||
if email == nil {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import type { DNSRecord } from "$lib/api/types.gen";
|
||||
import type { DNSResults } from "$lib/api/types.gen";
|
||||
|
||||
interface Props {
|
||||
dnsRecords: DNSRecord[];
|
||||
dnsResults?: DNSResults;
|
||||
}
|
||||
|
||||
let { dnsRecords }: Props = $props();
|
||||
let { dnsResults }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
|
|
@ -16,31 +16,217 @@
|
|||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each dnsRecords as record}
|
||||
<tr>
|
||||
<td><code>{record.domain}</code></td>
|
||||
<td><span class="badge bg-secondary">{record.record_type}</span></td>
|
||||
<td>
|
||||
<span class="badge {record.status === 'found' ? 'bg-success' : record.status === 'missing' ? 'bg-danger' : 'bg-warning'}">
|
||||
{record.status}
|
||||
</span>
|
||||
</td>
|
||||
<td><small class="text-muted">{record.value || '-'}</small></td>
|
||||
</tr>
|
||||
{#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">
|
||||
<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}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -145,10 +145,10 @@
|
|||
</div>
|
||||
|
||||
<!-- DNS Records -->
|
||||
{#if report.dns_records && report.dns_records.length > 0}
|
||||
{#if report.dns_results}
|
||||
<div class="row mb-4" id="dns">
|
||||
<div class="col-12">
|
||||
<DnsRecordsCard dnsRecords={report.dns_records} />
|
||||
<DnsRecordsCard dnsResults={report.dns_results} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue