From 52f43c6bc5003c9bc7a77c525b61760ef7177652 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 24 Oct 2025 15:21:54 +0700 Subject: [PATCH] Add x-align-form authentication test --- api/openapi.yaml | 3 + pkg/analyzer/authentication.go | 14 +- pkg/analyzer/authentication_test.go | 6 +- pkg/analyzer/authentication_x_aligned_from.go | 65 ++++++++ .../authentication_x_aligned_from_test.go | 144 ++++++++++++++++++ .../lib/components/AuthenticationCard.svelte | 24 +++ .../lib/components/HeaderAnalysisCard.svelte | 11 +- web/src/routes/test/[test]/+page.svelte | 1 + 8 files changed, 260 insertions(+), 8 deletions(-) create mode 100644 pkg/analyzer/authentication_x_aligned_from.go create mode 100644 pkg/analyzer/authentication_x_aligned_from_test.go diff --git a/api/openapi.yaml b/api/openapi.yaml index a178dc9..f5eb96a 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -683,6 +683,9 @@ components: x_google_dkim: $ref: '#/components/schemas/AuthResult' description: Google-specific DKIM authentication result (x-google-dkim) + x_aligned_from: + $ref: '#/components/schemas/AuthResult' + description: X-Aligned-From authentication result (checks address alignment) AuthResult: type: object diff --git a/pkg/analyzer/authentication.go b/pkg/analyzer/authentication.go index bc6ae38..02f8b28 100644 --- a/pkg/analyzer/authentication.go +++ b/pkg/analyzer/authentication.go @@ -138,6 +138,13 @@ func (a *AuthenticationAnalyzer) parseAuthenticationResultsHeader(header string, results.XGoogleDkim = a.parseXGoogleDKIMResult(part) } } + + // Parse x-aligned-from + if strings.HasPrefix(part, "x-aligned-from=") { + if results.XAlignedFrom == nil { + results.XAlignedFrom = a.parseXAlignedFromResult(part) + } + } } } @@ -156,12 +163,15 @@ func (a *AuthenticationAnalyzer) CalculateAuthenticationScore(results *api.Authe // SPF (25 points) score += 25 * a.calculateSPFScore(results) / 100 - // DKIM (25 points) - score += 25 * a.calculateDKIMScore(results) / 100 + // DKIM (23 points) + score += 23 * a.calculateDKIMScore(results) / 100 // X-Google-DKIM (optional) - penalty if failed score += 12 * a.calculateXGoogleDKIMScore(results) / 100 + // X-Aligned-From + score += 2 * a.calculateXAlignedFromScore(results) / 100 + // DMARC (25 points) score += 25 * a.calculateDMARCScore(results) / 100 diff --git a/pkg/analyzer/authentication_test.go b/pkg/analyzer/authentication_test.go index 63f9e2d..27901b5 100644 --- a/pkg/analyzer/authentication_test.go +++ b/pkg/analyzer/authentication_test.go @@ -46,7 +46,7 @@ func TestGetAuthenticationScore(t *testing.T) { Result: api.AuthResultResultPass, }, }, - expectedScore: 75, // SPF=25 + DKIM=25 + DMARC=25 + expectedScore: 73, // SPF=25 + DKIM=23 + DMARC=25 }, { name: "SPF and DKIM only", @@ -58,7 +58,7 @@ func TestGetAuthenticationScore(t *testing.T) { {Result: api.AuthResultResultPass}, }, }, - expectedScore: 50, // SPF=25 + DKIM=25 + expectedScore: 48, // SPF=25 + DKIM=23 }, { name: "SPF fail, DKIM pass", @@ -70,7 +70,7 @@ func TestGetAuthenticationScore(t *testing.T) { {Result: api.AuthResultResultPass}, }, }, - expectedScore: 25, // SPF=0 + DKIM=25 + expectedScore: 23, // SPF=0 + DKIM=23 }, { name: "SPF softfail", diff --git a/pkg/analyzer/authentication_x_aligned_from.go b/pkg/analyzer/authentication_x_aligned_from.go new file mode 100644 index 0000000..36da2b0 --- /dev/null +++ b/pkg/analyzer/authentication_x_aligned_from.go @@ -0,0 +1,65 @@ +// This file is part of the happyDeliver (R) project. +// Copyright (c) 2025 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package analyzer + +import ( + "regexp" + "strings" + + "git.happydns.org/happyDeliver/internal/api" +) + +// parseXAlignedFromResult parses X-Aligned-From result from Authentication-Results +// Example: x-aligned-from=pass (Address match) +func (a *AuthenticationAnalyzer) parseXAlignedFromResult(part string) *api.AuthResult { + result := &api.AuthResult{} + + // Extract result (pass, fail, etc.) + re := regexp.MustCompile(`x-aligned-from=([\w]+)`) + if matches := re.FindStringSubmatch(part); len(matches) > 1 { + resultStr := strings.ToLower(matches[1]) + result.Result = api.AuthResultResult(resultStr) + } + + // Extract details (everything after the result) + result.Details = api.PtrTo(strings.TrimPrefix(part, "x-aligned-from=")) + + return result +} + +func (a *AuthenticationAnalyzer) calculateXAlignedFromScore(results *api.AuthenticationResults) (score int) { + if results.XAlignedFrom != nil { + switch results.XAlignedFrom.Result { + case api.AuthResultResultPass: + // pass: positive contribution + return 100 + case api.AuthResultResultFail: + // fail: negative contribution + return 0 + default: + // neutral, none, etc.: no impact + return 0 + } + } + + return 0 +} diff --git a/pkg/analyzer/authentication_x_aligned_from_test.go b/pkg/analyzer/authentication_x_aligned_from_test.go new file mode 100644 index 0000000..220ac39 --- /dev/null +++ b/pkg/analyzer/authentication_x_aligned_from_test.go @@ -0,0 +1,144 @@ +// This file is part of the happyDeliver (R) project. +// Copyright (c) 2025 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package analyzer + +import ( + "testing" + + "git.happydns.org/happyDeliver/internal/api" +) + +func TestParseXAlignedFromResult(t *testing.T) { + tests := []struct { + name string + part string + expectedResult api.AuthResultResult + expectedDetail string + }{ + { + name: "x-aligned-from pass with details", + part: "x-aligned-from=pass (Address match)", + expectedResult: api.AuthResultResultPass, + expectedDetail: "pass (Address match)", + }, + { + name: "x-aligned-from fail with reason", + part: "x-aligned-from=fail (Address mismatch)", + expectedResult: api.AuthResultResultFail, + expectedDetail: "fail (Address mismatch)", + }, + { + name: "x-aligned-from pass minimal", + part: "x-aligned-from=pass", + expectedResult: api.AuthResultResultPass, + expectedDetail: "pass", + }, + { + name: "x-aligned-from neutral", + part: "x-aligned-from=neutral (No alignment check performed)", + expectedResult: api.AuthResultResultNeutral, + expectedDetail: "neutral (No alignment check performed)", + }, + { + name: "x-aligned-from none", + part: "x-aligned-from=none", + expectedResult: api.AuthResultResultNone, + expectedDetail: "none", + }, + } + + analyzer := NewAuthenticationAnalyzer() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := analyzer.parseXAlignedFromResult(tt.part) + + if result.Result != tt.expectedResult { + t.Errorf("Result = %v, want %v", result.Result, tt.expectedResult) + } + + if result.Details == nil { + t.Errorf("Details = nil, want %v", tt.expectedDetail) + } else if *result.Details != tt.expectedDetail { + t.Errorf("Details = %v, want %v", *result.Details, tt.expectedDetail) + } + }) + } +} + +func TestCalculateXAlignedFromScore(t *testing.T) { + tests := []struct { + name string + result *api.AuthResult + expectedScore int + }{ + { + name: "pass result gives positive score", + result: &api.AuthResult{ + Result: api.AuthResultResultPass, + }, + expectedScore: 100, + }, + { + name: "fail result gives zero score", + result: &api.AuthResult{ + Result: api.AuthResultResultFail, + }, + expectedScore: 0, + }, + { + name: "neutral result gives zero score", + result: &api.AuthResult{ + Result: api.AuthResultResultNeutral, + }, + expectedScore: 0, + }, + { + name: "none result gives zero score", + result: &api.AuthResult{ + Result: api.AuthResultResultNone, + }, + expectedScore: 0, + }, + { + name: "nil result gives zero score", + result: nil, + expectedScore: 0, + }, + } + + analyzer := NewAuthenticationAnalyzer() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results := &api.AuthenticationResults{ + XAlignedFrom: tt.result, + } + + score := analyzer.calculateXAlignedFromScore(results) + + if score != tt.expectedScore { + t.Errorf("Score = %v, want %v", score, tt.expectedScore) + } + }) + } +} diff --git a/web/src/lib/components/AuthenticationCard.svelte b/web/src/lib/components/AuthenticationCard.svelte index d30ff39..cf1b80f 100644 --- a/web/src/lib/components/AuthenticationCard.svelte +++ b/web/src/lib/components/AuthenticationCard.svelte @@ -213,6 +213,30 @@ {/if} + + {#if false && authentication.x_aligned_from} +
+
+ +
+ X-Aligned-From + + {authentication.x_aligned_from.result} + + {#if authentication.x_aligned_from.domain} +
+ Domain: + {authentication.x_aligned_from.domain} +
+ {/if} + {#if authentication.x_aligned_from.details} +
{authentication.x_aligned_from.details}
+ {/if} +
+
+
+ {/if} +
diff --git a/web/src/lib/components/HeaderAnalysisCard.svelte b/web/src/lib/components/HeaderAnalysisCard.svelte index ac5ad7a..54f6743 100644 --- a/web/src/lib/components/HeaderAnalysisCard.svelte +++ b/web/src/lib/components/HeaderAnalysisCard.svelte @@ -1,5 +1,5 @@
@@ -60,7 +61,11 @@
- + {#if xAlignedFrom} + + {:else} + + {/if} Domain Alignment
diff --git a/web/src/routes/test/[test]/+page.svelte b/web/src/routes/test/[test]/+page.svelte index 59697e2..98fb3a1 100644 --- a/web/src/routes/test/[test]/+page.svelte +++ b/web/src/routes/test/[test]/+page.svelte @@ -198,6 +198,7 @@ headerAnalysis={report.header_analysis} headerGrade={report.summary?.header_grade} headerScore={report.summary?.header_score} + xAlignedFrom={report.authentication.x_aligned_from} />