Fix code formatting and linting issues

- Auto-format all Python files with black and isort
- Remove unused imports with autoflake
- Fix flake8 issues (missing newlines, blank lines, etc.)
- Fix nonlocal/global scope issues in main.py
- Fix security.py import order (E402)
- Remove f-string without placeholders
- Add nosec comment for intentional exception handling
- Fix test imports to match refactored DMARCParser API

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-09 12:08:51 +00:00
parent f6908fe9ec
commit 6ae017b142
28 changed files with 999 additions and 956 deletions
+131 -96
View File
@@ -1,33 +1,41 @@
from typing import List, Optional, Dict, Any
from datetime import datetime, timedelta
from fastapi import APIRouter, HTTPException, status, Path, Query
from pydantic import BaseModel
from typing import Any, Dict, List, Optional
from app.services.report_store import ReportStore
from fastapi import APIRouter, HTTPException, Path, Query, status
from pydantic import BaseModel
router = APIRouter()
class DomainBase(BaseModel):
"""Base Domain schema"""
name: str
description: Optional[str] = None
policy: Optional[str] = None
class DomainResponse(DomainBase):
"""Domain response schema"""
reports_count: int = 0
emails_count: int = 0
compliance_rate: float = 0.0
class DomainStatsResponse(BaseModel):
"""Domain statistics for the domain details page"""
complianceRate: float
totalEmails: int
failedEmails: int
reportCount: int
class DNSRecordResponse(BaseModel):
"""DNS record information for a domain"""
dmarc: bool
dmarcRecord: Optional[str] = None
spf: bool
@@ -35,13 +43,17 @@ class DNSRecordResponse(BaseModel):
dkim: bool
dkimSelectors: Optional[str] = None
class TimelinePoint(BaseModel):
"""Data point for compliance timeline"""
date: str
compliance_rate: float
class ReportEntry(BaseModel):
"""Summary of a DMARC report"""
id: str
org_name: str
begin_date: int
@@ -50,8 +62,10 @@ class ReportEntry(BaseModel):
pass_rate: float
policy: str
class SourceEntry(BaseModel):
"""Summary of a sending source"""
ip: str
count: int
spf: str
@@ -59,23 +73,30 @@ class SourceEntry(BaseModel):
dmarc: str
disposition: str
class DomainReportsResponse(BaseModel):
"""Domain reports with compliance timeline"""
reports: List[ReportEntry]
compliance_timeline: List[TimelinePoint]
class DomainSourcesResponse(BaseModel):
"""Domain sending sources"""
sources: List[SourceEntry]
class DomainSummaryResponse(BaseModel):
"""Domain summary for dashboard"""
total_domains: int
total_emails: int
overall_pass_rate: float
reports_processed: int
domains: List[Dict[str, Any]]
@router.get("/summary", response_model=DomainSummaryResponse)
async def get_domains_summary():
"""
@@ -84,45 +105,48 @@ async def get_domains_summary():
store = ReportStore.get_instance()
domains = store.get_domains()
summaries = store.get_all_domain_summaries()
# Calculate overall statistics
total_domains = len(domains)
total_emails = 0
total_passed = 0
total_reports = 0
domains_list = []
for domain_name in domains:
summary = summaries.get(domain_name, {})
total_emails += summary.get("total_count", 0)
total_passed += summary.get("passed_count", 0)
total_reports += summary.get("reports_processed", 0)
# Format domain data for frontend
domains_list.append({
"id": domain_name, # Using the domain name as ID for now
"domain_name": domain_name,
"total_emails": summary.get("total_count", 0),
"passed_count": summary.get("passed_count", 0),
"failed_count": summary.get("failed_count", 0),
"pass_rate": summary.get("compliance_rate", 0),
"report_count": summary.get("reports_processed", 0)
})
domains_list.append(
{
"id": domain_name, # Using the domain name as ID for now
"domain_name": domain_name,
"total_emails": summary.get("total_count", 0),
"passed_count": summary.get("passed_count", 0),
"failed_count": summary.get("failed_count", 0),
"pass_rate": summary.get("compliance_rate", 0),
"report_count": summary.get("reports_processed", 0),
}
)
# Calculate overall pass rate
overall_pass_rate = 0
if total_emails > 0:
overall_pass_rate = round((total_passed / total_emails) * 100, 1)
return DomainSummaryResponse(
total_domains=total_domains,
total_emails=total_emails,
overall_pass_rate=overall_pass_rate,
reports_processed=total_reports,
domains=domains_list
domains=domains_list,
)
@router.get("/domains", response_model=List[DomainResponse])
async def read_domains():
"""
@@ -132,7 +156,7 @@ async def read_domains():
store = ReportStore.get_instance()
domains = store.get_domains()
summaries = store.get_all_domain_summaries()
result = []
for domain_name in domains:
summary = summaries.get(domain_name, {})
@@ -141,12 +165,13 @@ async def read_domains():
policy=summary.get("policy", "unknown"),
reports_count=summary.get("reports_processed", 0),
emails_count=summary.get("total_count", 0),
compliance_rate=summary.get("compliance_rate", 0.0)
compliance_rate=summary.get("compliance_rate", 0.0),
)
result.append(domain_response)
return result
@router.get("/domains/{domain_name}", response_model=DomainResponse)
async def read_domain(domain_name: str):
"""
@@ -154,25 +179,27 @@ async def read_domain(domain_name: str):
"""
store = ReportStore.get_instance()
domains = store.get_domains()
if domain_name not in domains:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Domain not found",
)
summary = store.get_domain_summary(domain_name)
return DomainResponse(
name=domain_name,
policy=summary.get("policy", "unknown"),
reports_count=summary.get("reports_processed", 0),
emails_count=summary.get("total_count", 0),
compliance_rate=summary.get("compliance_rate", 0.0)
compliance_rate=summary.get("compliance_rate", 0.0),
)
# New endpoints for domain details page
@router.get("/{domain_id}/stats", response_model=DomainStatsResponse)
async def get_domain_stats(domain_id: str = Path(..., title="The domain ID or name")):
"""
@@ -180,43 +207,44 @@ async def get_domain_stats(domain_id: str = Path(..., title="The domain ID or na
"""
store = ReportStore.get_instance()
domains = store.get_domains()
# For Milestone 1, domain_id is simply the domain name
if domain_id not in domains:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Domain not found",
)
summary = store.get_domain_summary(domain_id)
total_count = summary.get("total_count", 0)
passed_count = summary.get("passed_count", 0)
failed_count = total_count - passed_count
compliance_rate = summary.get("compliance_rate", 0.0)
reports_processed = summary.get("reports_processed", 0)
return DomainStatsResponse(
complianceRate=compliance_rate,
totalEmails=total_count,
failedEmails=failed_count,
reportCount=reports_processed
reportCount=reports_processed,
)
@router.get("/{domain_id}/dns", response_model=DNSRecordResponse)
async def get_domain_dns_records(domain_id: str = Path(..., title="The domain ID or name")):
"""
Get DNS records for a specific domain. For Milestone 1,
Get DNS records for a specific domain. For Milestone 1,
this returns mock data since DNS integration is part of a future milestone.
"""
store = ReportStore.get_instance()
domains = store.get_domains()
if domain_id not in domains:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Domain not found",
)
# For Milestone 1, return mock DNS record data
# In a future milestone, this will be replaced with actual DNS lookups
return DNSRecordResponse(
@@ -225,97 +253,101 @@ async def get_domain_dns_records(domain_id: str = Path(..., title="The domain ID
spf=True,
spfRecord="v=spf1 include:_spf.google.com include:spf.protection.outlook.com -all",
dkim=True,
dkimSelectors="selector1, selector2"
dkimSelectors="selector1, selector2",
)
@router.get("/{domain_id}/reports", response_model=DomainReportsResponse)
async def get_domain_reports(
domain_id: str = Path(..., title="The domain ID or name"),
limit: int = Query(10, title="Maximum number of reports to return")
limit: int = Query(10, title="Maximum number of reports to return"),
):
"""
Get recent DMARC reports for a specific domain, along with compliance timeline
"""
store = ReportStore.get_instance()
domains = store.get_domains()
if domain_id not in domains:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Domain not found",
)
# Get reports for this domain
reports = store.get_domain_reports(domain_id, limit=limit)
# Generate report entries
report_entries = []
for report in reports:
report_entries.append(ReportEntry(
id=report.get("report_id", "unknown"),
org_name=report.get("org_name", "Unknown Organization"),
begin_date=report.get("begin_date", 0),
end_date=report.get("end_date", 0),
total_emails=report.get("total_count", 0),
pass_rate=report.get("pass_rate", 0.0),
policy=report.get("policy", "none")
))
report_entries.append(
ReportEntry(
id=report.get("report_id", "unknown"),
org_name=report.get("org_name", "Unknown Organization"),
begin_date=report.get("begin_date", 0),
end_date=report.get("end_date", 0),
total_emails=report.get("total_count", 0),
pass_rate=report.get("pass_rate", 0.0),
policy=report.get("policy", "none"),
)
)
# Generate compliance timeline (last 30 days)
timeline = []
for i in range(30, 0, -1):
date = datetime.now() - timedelta(days=i)
date_str = date.strftime("%Y-%m-%d")
# For Milestone 1, generate some mock data with variation
# In future milestone, this will use actual historical data
import random
compliance_rate = random.uniform(80, 100)
timeline.append(TimelinePoint(
date=date_str,
compliance_rate=round(compliance_rate, 1)
))
return DomainReportsResponse(
reports=report_entries,
compliance_timeline=timeline
)
timeline.append(TimelinePoint(date=date_str, compliance_rate=round(compliance_rate, 1)))
return DomainReportsResponse(reports=report_entries, compliance_timeline=timeline)
@router.get("/{domain_id}/sources", response_model=DomainSourcesResponse)
async def get_domain_sources(
domain_id: str = Path(..., title="The domain ID or name"),
days: int = Query(30, title="Number of days to look back")
days: int = Query(30, title="Number of days to look back"),
):
"""
Get sending sources for a specific domain
"""
store = ReportStore.get_instance()
domains = store.get_domains()
if domain_id not in domains:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Domain not found",
)
# Get sending sources for this domain
sources = store.get_domain_sources(domain_id, days=days)
source_entries = []
for source in sources:
source_entries.append(SourceEntry(
ip=source.get("source_ip", "unknown"),
count=source.get("count", 0),
spf=source.get("spf_result", "unknown"),
dkim=source.get("dkim_result", "unknown"),
dmarc="pass" if source.get("spf_result") == "pass" or source.get("dkim_result") == "pass" else "fail",
disposition=source.get("disposition", "none")
))
return DomainSourcesResponse(
sources=source_entries
)
source_entries.append(
SourceEntry(
ip=source.get("source_ip", "unknown"),
count=source.get("count", 0),
spf=source.get("spf_result", "unknown"),
dkim=source.get("dkim_result", "unknown"),
dmarc=(
"pass"
if source.get("spf_result") == "pass" or source.get("dkim_result") == "pass"
else "fail"
),
disposition=source.get("disposition", "none"),
)
)
return DomainSourcesResponse(sources=source_entries)
@router.delete("/{domain_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_domain(domain_id: str = Path(..., title="The domain ID or name")):
@@ -325,36 +357,37 @@ async def delete_domain(domain_id: str = Path(..., title="The domain ID or name"
"""
store = ReportStore.get_instance()
domains = store.get_domains()
if domain_id not in domains:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Domain not found",
)
# Perform deletion with cleanup
deleted = store.delete_domain_with_cleanup(domain_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete domain",
)
# Return 204 No Content on success
return None
@router.get("/search", response_model=List[DomainResponse])
async def search_domains(
q: Optional[str] = Query(None, title="Search query for domain name or description"),
policy: Optional[str] = Query(None, title="Filter by DMARC policy"),
page: int = Query(1, title="Page number", ge=1),
limit: int = Query(10, title="Number of domains per page", ge=1, le=100)
limit: int = Query(10, title="Number of domains per page", ge=1, le=100),
):
"""
Search domains with filtering and pagination.
This supports searching by domain name/description and filtering by DMARC policy.
Args:
q: Optional search query for domain name or description
policy: Optional filter by DMARC policy (none, quarantine, reject)
@@ -364,33 +397,35 @@ async def search_domains(
store = ReportStore.get_instance()
domains = store.get_domains()
summaries = store.get_all_domain_summaries()
# Apply search filter if provided
filtered_domains = []
for domain_name in domains:
summary = summaries.get(domain_name, {})
# Skip domain if it doesn't match the search query
if q and q.lower() not in domain_name.lower():
continue
# Skip domain if it doesn't match the policy filter
if policy and summary.get("policy") != policy:
continue
# Domain passed all filters
filtered_domains.append({
"name": domain_name,
"description": "", # No description in in-memory store
"policy": summary.get("policy", "unknown"),
"reports_count": summary.get("reports_processed", 0),
"emails_count": summary.get("total_count", 0),
"compliance_rate": summary.get("compliance_rate", 0.0)
})
filtered_domains.append(
{
"name": domain_name,
"description": "", # No description in in-memory store
"policy": summary.get("policy", "unknown"),
"reports_count": summary.get("reports_processed", 0),
"emails_count": summary.get("total_count", 0),
"compliance_rate": summary.get("compliance_rate", 0.0),
}
)
# Apply pagination
start_idx = (page - 1) * limit
end_idx = start_idx + limit
paginated_domains = filtered_domains[start_idx:end_idx]
return [DomainResponse(**domain) for domain in paginated_domains]
return [DomainResponse(**domain) for domain in paginated_domains]