Change RBL test to return map of ips

This commit is contained in:
nemunaire 2025-10-21 14:42:18 +07:00
commit 954a9d705e
4 changed files with 113 additions and 72 deletions

View file

@ -277,9 +277,18 @@ components:
items:
$ref: '#/components/schemas/DNSRecord'
blacklists:
type: array
items:
$ref: '#/components/schemas/BlacklistCheck'
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/BlacklistCheck'
description: Map of IP addresses to their blacklist check results (array of checks per IP)
example:
"192.0.2.1":
- rbl: "zen.spamhaus.org"
listed: false
- rbl: "bl.spamcop.net"
listed: false
raw_headers:
type: string
description: Raw email headers
@ -498,14 +507,9 @@ components:
BlacklistCheck:
type: object
required:
- ip
- rbl
- listed
properties:
ip:
type: string
description: IP address checked
example: "192.0.2.1"
rbl:
type: string
description: RBL/DNSBL name

View file

@ -68,14 +68,14 @@ func NewRBLChecker(timeout time.Duration, rbls []string) *RBLChecker {
// RBLResults represents the results of RBL checks
type RBLResults struct {
Checks []RBLCheck
Checks map[string][]RBLCheck // Map of IP -> list of RBL checks for that IP
IPsChecked []string
ListedCount int
}
// RBLCheck represents a single RBL check result
// Note: IP is not included here as it's used as the map key in the API
type RBLCheck struct {
IP string
RBL string
Listed bool
Response string
@ -84,7 +84,9 @@ type RBLCheck struct {
// CheckEmail checks all IPs found in the email headers against RBLs
func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults {
results := &RBLResults{}
results := &RBLResults{
Checks: make(map[string][]RBLCheck),
}
// Extract IPs from Received headers
ips := r.extractIPs(email)
@ -98,7 +100,7 @@ func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults {
for _, ip := range ips {
for _, rbl := range r.RBLs {
check := r.checkIP(ip, rbl)
results.Checks = append(results.Checks, check)
results.Checks[ip] = append(results.Checks[ip], check)
if check.Listed {
results.ListedCount++
}
@ -179,7 +181,6 @@ func (r *RBLChecker) isPublicIP(ipStr string) bool {
// checkIP checks a single IP against a single RBL
func (r *RBLChecker) checkIP(ip, rbl string) RBLCheck {
check := RBLCheck{
IP: ip,
RBL: rbl,
}
@ -285,14 +286,16 @@ func (r *RBLChecker) GenerateRBLChecks(results *RBLResults) []api.Check {
checks = append(checks, summaryCheck)
// Create individual checks for each listing and RBL errors
for _, check := range results.Checks {
if check.Listed {
detailCheck := r.generateListingCheck(&check)
checks = append(checks, detailCheck)
} else if check.Error != "" && strings.Contains(check.Error, "RBL operational issue") {
// Generate info check for RBL errors
detailCheck := r.generateRBLErrorCheck(&check)
checks = append(checks, detailCheck)
for ip, rblChecks := range results.Checks {
for _, check := range rblChecks {
if check.Listed {
detailCheck := r.generateListingCheck(ip, &check)
checks = append(checks, detailCheck)
} else if check.Error != "" && strings.Contains(check.Error, "RBL operational issue") {
// Generate info check for RBL errors
detailCheck := r.generateRBLErrorCheck(ip, &check)
checks = append(checks, detailCheck)
}
}
}
@ -310,7 +313,11 @@ func (r *RBLChecker) generateSummaryCheck(results *RBLResults) api.Check {
check.Score = score
check.Grade = ScoreToCheckGrade(score)
totalChecks := len(results.Checks)
// Calculate total checks across all IPs
totalChecks := 0
for _, rblChecks := range results.Checks {
totalChecks += len(rblChecks)
}
listedCount := results.ListedCount
if listedCount == 0 {
@ -345,7 +352,7 @@ func (r *RBLChecker) generateSummaryCheck(results *RBLResults) api.Check {
}
// generateListingCheck creates a check for a specific RBL listing
func (r *RBLChecker) generateListingCheck(rblCheck *RBLCheck) api.Check {
func (r *RBLChecker) generateListingCheck(ip string, rblCheck *RBLCheck) api.Check {
check := api.Check{
Category: api.Blacklist,
Name: fmt.Sprintf("RBL: %s", rblCheck.RBL),
@ -354,7 +361,7 @@ func (r *RBLChecker) generateListingCheck(rblCheck *RBLCheck) api.Check {
Grade: ScoreToCheckGrade(0),
}
check.Message = fmt.Sprintf("IP %s is listed on %s", rblCheck.IP, rblCheck.RBL)
check.Message = fmt.Sprintf("IP %s is listed on %s", ip, rblCheck.RBL)
// Determine severity based on which RBL
if strings.Contains(rblCheck.RBL, "spamhaus") {
@ -381,7 +388,7 @@ func (r *RBLChecker) generateListingCheck(rblCheck *RBLCheck) api.Check {
}
// generateRBLErrorCheck creates an info-level check for RBL operational errors
func (r *RBLChecker) generateRBLErrorCheck(rblCheck *RBLCheck) api.Check {
func (r *RBLChecker) generateRBLErrorCheck(ip string, rblCheck *RBLCheck) api.Check {
check := api.Check{
Category: api.Blacklist,
Name: fmt.Sprintf("RBL: %s", rblCheck.RBL),
@ -391,7 +398,7 @@ func (r *RBLChecker) generateRBLErrorCheck(rblCheck *RBLCheck) api.Check {
Severity: api.PtrTo(api.CheckSeverityInfo),
}
check.Message = fmt.Sprintf("RBL %s returned an error code for IP %s", rblCheck.RBL, rblCheck.IP)
check.Message = fmt.Sprintf("RBL %s returned an error code for IP %s", rblCheck.RBL, ip)
advice := fmt.Sprintf("The RBL %s is experiencing operational issues (error code: %s).", rblCheck.RBL, rblCheck.Response)
check.Advice = &advice
@ -406,13 +413,14 @@ func (r *RBLChecker) generateRBLErrorCheck(rblCheck *RBLCheck) api.Check {
// GetUniqueListedIPs returns a list of unique IPs that are listed on at least one RBL
func (r *RBLChecker) GetUniqueListedIPs(results *RBLResults) []string {
seenIPs := make(map[string]bool)
var listedIPs []string
for _, check := range results.Checks {
if check.Listed && !seenIPs[check.IP] {
listedIPs = append(listedIPs, check.IP)
seenIPs[check.IP] = true
for ip, rblChecks := range results.Checks {
for _, check := range rblChecks {
if check.Listed {
listedIPs = append(listedIPs, ip)
break // Only add the IP once
}
}
}
@ -423,9 +431,11 @@ func (r *RBLChecker) GetUniqueListedIPs(results *RBLResults) []string {
func (r *RBLChecker) GetRBLsForIP(results *RBLResults, ip string) []string {
var rbls []string
for _, check := range results.Checks {
if check.IP == ip && check.Listed {
rbls = append(rbls, check.RBL)
if rblChecks, exists := results.Checks[ip]; exists {
for _, check := range rblChecks {
if check.Listed {
rbls = append(rbls, check.RBL)
}
}
}

View file

@ -347,7 +347,9 @@ func TestGenerateSummaryCheck(t *testing.T) {
results: &RBLResults{
IPsChecked: []string{"198.51.100.1"},
ListedCount: 0,
Checks: make([]RBLCheck, 6), // 6 default RBLs
Checks: map[string][]RBLCheck{
"198.51.100.1": make([]RBLCheck, 6), // 6 default RBLs
},
},
expectedStatus: api.CheckStatusPass,
expectedScore: 200,
@ -357,7 +359,9 @@ func TestGenerateSummaryCheck(t *testing.T) {
results: &RBLResults{
IPsChecked: []string{"198.51.100.1"},
ListedCount: 1,
Checks: make([]RBLCheck, 6),
Checks: map[string][]RBLCheck{
"198.51.100.1": make([]RBLCheck, 6),
},
},
expectedStatus: api.CheckStatusWarn,
expectedScore: 100,
@ -367,7 +371,9 @@ func TestGenerateSummaryCheck(t *testing.T) {
results: &RBLResults{
IPsChecked: []string{"198.51.100.1"},
ListedCount: 2,
Checks: make([]RBLCheck, 6),
Checks: map[string][]RBLCheck{
"198.51.100.1": make([]RBLCheck, 6),
},
},
expectedStatus: api.CheckStatusWarn,
expectedScore: 50,
@ -377,7 +383,9 @@ func TestGenerateSummaryCheck(t *testing.T) {
results: &RBLResults{
IPsChecked: []string{"198.51.100.1"},
ListedCount: 4,
Checks: make([]RBLCheck, 6),
Checks: map[string][]RBLCheck{
"198.51.100.1": make([]RBLCheck, 6),
},
},
expectedStatus: api.CheckStatusFail,
expectedScore: 0,
@ -413,7 +421,6 @@ func TestGenerateListingCheck(t *testing.T) {
{
name: "Spamhaus listing",
rblCheck: &RBLCheck{
IP: "198.51.100.1",
RBL: "zen.spamhaus.org",
Listed: true,
Response: "127.0.0.2",
@ -424,7 +431,6 @@ func TestGenerateListingCheck(t *testing.T) {
{
name: "SpamCop listing",
rblCheck: &RBLCheck{
IP: "198.51.100.1",
RBL: "bl.spamcop.net",
Listed: true,
Response: "127.0.0.2",
@ -435,7 +441,6 @@ func TestGenerateListingCheck(t *testing.T) {
{
name: "Other RBL listing",
rblCheck: &RBLCheck{
IP: "198.51.100.1",
RBL: "dnsbl.sorbs.net",
Listed: true,
Response: "127.0.0.2",
@ -449,7 +454,7 @@ func TestGenerateListingCheck(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
check := checker.generateListingCheck(tt.rblCheck)
check := checker.generateListingCheck("198.51.100.1", tt.rblCheck)
if check.Status != tt.expectedStatus {
t.Errorf("Status = %v, want %v", check.Status, tt.expectedStatus)
@ -490,9 +495,11 @@ func TestGenerateRBLChecks(t *testing.T) {
results: &RBLResults{
IPsChecked: []string{"198.51.100.1"},
ListedCount: 0,
Checks: []RBLCheck{
{IP: "198.51.100.1", RBL: "zen.spamhaus.org", Listed: false},
{IP: "198.51.100.1", RBL: "bl.spamcop.net", Listed: false},
Checks: map[string][]RBLCheck{
"198.51.100.1": {
{RBL: "zen.spamhaus.org", Listed: false},
{RBL: "bl.spamcop.net", Listed: false},
},
},
},
minChecks: 1, // Summary check only
@ -502,10 +509,12 @@ func TestGenerateRBLChecks(t *testing.T) {
results: &RBLResults{
IPsChecked: []string{"198.51.100.1"},
ListedCount: 2,
Checks: []RBLCheck{
{IP: "198.51.100.1", RBL: "zen.spamhaus.org", Listed: true},
{IP: "198.51.100.1", RBL: "bl.spamcop.net", Listed: true},
{IP: "198.51.100.1", RBL: "dnsbl.sorbs.net", Listed: false},
Checks: map[string][]RBLCheck{
"198.51.100.1": {
{RBL: "zen.spamhaus.org", Listed: true},
{RBL: "bl.spamcop.net", Listed: true},
{RBL: "dnsbl.sorbs.net", Listed: false},
},
},
},
minChecks: 3, // Summary + 2 listing checks
@ -534,12 +543,18 @@ func TestGenerateRBLChecks(t *testing.T) {
func TestGetUniqueListedIPs(t *testing.T) {
results := &RBLResults{
Checks: []RBLCheck{
{IP: "198.51.100.1", RBL: "zen.spamhaus.org", Listed: true},
{IP: "198.51.100.1", RBL: "bl.spamcop.net", Listed: true},
{IP: "198.51.100.2", RBL: "zen.spamhaus.org", Listed: true},
{IP: "198.51.100.2", RBL: "bl.spamcop.net", Listed: false},
{IP: "198.51.100.3", RBL: "zen.spamhaus.org", Listed: false},
Checks: map[string][]RBLCheck{
"198.51.100.1": {
{RBL: "zen.spamhaus.org", Listed: true},
{RBL: "bl.spamcop.net", Listed: true},
},
"198.51.100.2": {
{RBL: "zen.spamhaus.org", Listed: true},
{RBL: "bl.spamcop.net", Listed: false},
},
"198.51.100.3": {
{RBL: "zen.spamhaus.org", Listed: false},
},
},
}
@ -556,11 +571,15 @@ func TestGetUniqueListedIPs(t *testing.T) {
func TestGetRBLsForIP(t *testing.T) {
results := &RBLResults{
Checks: []RBLCheck{
{IP: "198.51.100.1", RBL: "zen.spamhaus.org", Listed: true},
{IP: "198.51.100.1", RBL: "bl.spamcop.net", Listed: true},
{IP: "198.51.100.1", RBL: "dnsbl.sorbs.net", Listed: false},
{IP: "198.51.100.2", RBL: "zen.spamhaus.org", Listed: true},
Checks: map[string][]RBLCheck{
"198.51.100.1": {
{RBL: "zen.spamhaus.org", Listed: true},
{RBL: "bl.spamcop.net", Listed: true},
{RBL: "dnsbl.sorbs.net", Listed: false},
},
"198.51.100.2": {
{RBL: "zen.spamhaus.org", Listed: true},
},
},
}

View file

@ -190,21 +190,29 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
}
}
// Add blacklist checks
// Add blacklist checks as a map of IP -> array of BlacklistCheck
if results.RBL != nil && len(results.RBL.Checks) > 0 {
blacklistChecks := make([]api.BlacklistCheck, 0, len(results.RBL.Checks))
for _, check := range results.RBL.Checks {
blCheck := api.BlacklistCheck{
Ip: check.IP,
Rbl: check.RBL,
Listed: check.Listed,
blacklistMap := make(map[string][]api.BlacklistCheck)
// Convert internal RBL checks to API format
for ip, rblChecks := range results.RBL.Checks {
apiChecks := make([]api.BlacklistCheck, 0, len(rblChecks))
for _, check := range rblChecks {
blCheck := api.BlacklistCheck{
Rbl: check.RBL,
Listed: check.Listed,
}
if check.Response != "" {
blCheck.Response = &check.Response
}
apiChecks = append(apiChecks, blCheck)
}
if check.Response != "" {
blCheck.Response = &check.Response
}
blacklistChecks = append(blacklistChecks, blCheck)
blacklistMap[ip] = apiChecks
}
if len(blacklistMap) > 0 {
report.Blacklists = &blacklistMap
}
report.Blacklists = &blacklistChecks
}
// Add raw headers