diff --git a/api/openapi.yaml b/api/openapi.yaml index 25c1b90..92bf3e3 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -760,7 +760,7 @@ components: properties: result: type: string - enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined, domain_pass, orgdomain_pass] + enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined] description: Authentication result example: "pass" domain: diff --git a/pkg/analyzer/dns_spf.go b/pkg/analyzer/dns_spf.go index a6b74c1..fa819c1 100644 --- a/pkg/analyzer/dns_spf.go +++ b/pkg/analyzer/dns_spf.go @@ -33,12 +33,11 @@ import ( // checkSPFRecords looks up and validates SPF records for a domain, including resolving include: directives func (d *DNSAnalyzer) checkSPFRecords(domain string) *[]api.SPFRecord { visited := make(map[string]bool) - return d.resolveSPFRecords(domain, visited, 0, true) + return d.resolveSPFRecords(domain, visited, 0) } // resolveSPFRecords recursively resolves SPF records including include: directives -// isMainRecord indicates if this is the primary domain's record (not an included one) -func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, depth int, isMainRecord bool) *[]api.SPFRecord { +func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, depth int) *[]api.SPFRecord { const maxDepth = 10 // Prevent infinite recursion if depth > maxDepth { @@ -104,7 +103,7 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, } // Basic validation - validationErr := d.validateSPF(spfRecord, isMainRecord) + validationErr := d.validateSPF(spfRecord) // Extract the "all" mechanism qualifier var allQualifier *api.SPFRecordAllQualifier @@ -141,7 +140,7 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, if redirectDomain != "" { // redirect= replaces the current domain's policy entirely // Only follow if no other mechanisms matched (per RFC 7208) - redirectRecords := d.resolveSPFRecords(redirectDomain, visited, depth+1, false) + redirectRecords := d.resolveSPFRecords(redirectDomain, visited, depth+1) if redirectRecords != nil { results = append(results, *redirectRecords...) } @@ -151,7 +150,7 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, // Extract and resolve include: directives includes := d.extractSPFIncludes(spfRecord) for _, includeDomain := range includes { - includedRecords := d.resolveSPFRecords(includeDomain, visited, depth+1, false) + includedRecords := d.resolveSPFRecords(includeDomain, visited, depth+1) if includedRecords != nil { results = append(results, *includedRecords...) } @@ -237,8 +236,7 @@ func (d *DNSAnalyzer) isValidSPFMechanism(token string) error { } // validateSPF performs basic SPF record validation -// isMainRecord indicates if this is the primary domain's record (not an included one) -func (d *DNSAnalyzer) validateSPF(record string, isMainRecord bool) error { +func (d *DNSAnalyzer) validateSPF(record string) error { // Must start with v=spf1 if !strings.HasPrefix(record, "v=spf1") { return fmt.Errorf("SPF record must start with 'v=spf1'") @@ -271,22 +269,19 @@ func (d *DNSAnalyzer) validateSPF(record string, isMainRecord bool) error { return nil } - // Only check for 'all' mechanism on the main record, not on included records - if isMainRecord { - // Check for common syntax issues - // Should have a final mechanism (all, +all, -all, ~all, ?all) - validEndings := []string{" all", " +all", " -all", " ~all", " ?all"} - hasValidEnding := false - for _, ending := range validEndings { - if strings.HasSuffix(record, ending) { - hasValidEnding = true - break - } + // Check for common syntax issues + // Should have a final mechanism (all, +all, -all, ~all, ?all) + validEndings := []string{" all", " +all", " -all", " ~all", " ?all"} + hasValidEnding := false + for _, ending := range validEndings { + if strings.HasSuffix(record, ending) { + hasValidEnding = true + break } + } - if !hasValidEnding { - return fmt.Errorf("SPF record should end with an 'all' mechanism (e.g., '-all', '~all') or have a 'redirect=' modifier") - } + if !hasValidEnding { + return fmt.Errorf("SPF record should end with an 'all' mechanism (e.g., '-all', '~all') or have a 'redirect=' modifier") } return nil diff --git a/pkg/analyzer/dns_spf_test.go b/pkg/analyzer/dns_spf_test.go index b1195cb..bc51a6f 100644 --- a/pkg/analyzer/dns_spf_test.go +++ b/pkg/analyzer/dns_spf_test.go @@ -128,8 +128,7 @@ func TestValidateSPF(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Test as main record (isMainRecord = true) since these tests check overall SPF validity - err := analyzer.validateSPF(tt.record, true) + err := analyzer.validateSPF(tt.record) if tt.expectError { if err == nil { t.Errorf("validateSPF(%q) expected error but got nil", tt.record) @@ -145,74 +144,6 @@ func TestValidateSPF(t *testing.T) { } } -func TestValidateSPF_IncludedRecords(t *testing.T) { - tests := []struct { - name string - record string - isMainRecord bool - expectError bool - errorMsg string - }{ - { - name: "Main record without 'all' - should error", - record: "v=spf1 include:_spf.example.com", - isMainRecord: true, - expectError: true, - errorMsg: "should end with an 'all' mechanism", - }, - { - name: "Included record without 'all' - should NOT error", - record: "v=spf1 include:_spf.example.com", - isMainRecord: false, - expectError: false, - }, - { - name: "Included record with only mechanisms - should NOT error", - record: "v=spf1 ip4:192.0.2.0/24 mx", - isMainRecord: false, - expectError: false, - }, - { - name: "Main record with only mechanisms - should error", - record: "v=spf1 ip4:192.0.2.0/24 mx", - isMainRecord: true, - expectError: true, - errorMsg: "should end with an 'all' mechanism", - }, - { - name: "Included record with 'all' - valid", - record: "v=spf1 ip4:192.0.2.0/24 -all", - isMainRecord: false, - expectError: false, - }, - { - name: "Main record with 'all' - valid", - record: "v=spf1 ip4:192.0.2.0/24 -all", - isMainRecord: true, - expectError: false, - }, - } - - analyzer := NewDNSAnalyzer(5 * time.Second) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := analyzer.validateSPF(tt.record, tt.isMainRecord) - if tt.expectError { - if err == nil { - t.Errorf("validateSPF(%q, isMainRecord=%v) expected error but got nil", tt.record, tt.isMainRecord) - } else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) { - t.Errorf("validateSPF(%q, isMainRecord=%v) error = %q, want error containing %q", tt.record, tt.isMainRecord, err.Error(), tt.errorMsg) - } - } else { - if err != nil { - t.Errorf("validateSPF(%q, isMainRecord=%v) unexpected error: %v", tt.record, tt.isMainRecord, err) - } - } - }) - } -} - func TestExtractSPFRedirect(t *testing.T) { tests := []struct { name string diff --git a/web/src/lib/components/AuthenticationCard.svelte b/web/src/lib/components/AuthenticationCard.svelte index 8f22eac..0b36dd0 100644 --- a/web/src/lib/components/AuthenticationCard.svelte +++ b/web/src/lib/components/AuthenticationCard.svelte @@ -16,16 +16,10 @@ function getAuthResultClass(result: string, noneIsFail: boolean): string { switch (result) { case "pass": - case "domain_pass": - case "orgdomain_pass": return "text-success"; - case "error": case "fail": case "missing": case "invalid": - case "null": - case "null_smtp": - case "null_header": return "text-danger"; case "softfail": case "neutral": @@ -42,18 +36,12 @@ function getAuthResultIcon(result: string, noneIsFail: boolean): string { switch (result) { case "pass": - case "domain_pass": - case "orgdomain_pass": return "bi-check-circle-fill"; case "fail": return "bi-x-circle-fill"; case "softfail": case "neutral": case "invalid": - case "null": - case "error": - case "null_smtp": - case "null_header": return "bi-exclamation-circle-fill"; case "missing": return "bi-dash-circle-fill"; diff --git a/web/src/lib/components/HeaderAnalysisCard.svelte b/web/src/lib/components/HeaderAnalysisCard.svelte index 306260e..36e173b 100644 --- a/web/src/lib/components/HeaderAnalysisCard.svelte +++ b/web/src/lib/components/HeaderAnalysisCard.svelte @@ -9,6 +9,7 @@ headerAnalysis: HeaderAnalysis; headerGrade?: string; headerScore?: number; + xAlignedFrom?: AuthResult; } let { dmarcRecord, headerAnalysis, headerGrade, headerScore, xAlignedFrom }: Props = $props(); @@ -61,7 +62,11 @@