chore: remove unused SMTP module (#816)
The SMTP configure/reports feature was never wired into the UI and is not referenced by any active workflow. Drop the backend router, routes, schemas, and services, and remove the SMTP/SMTPInput models plus the corresponding User relationship. Also drop the now-unused matplotlib and reportlab dependencies — both were only imported by the SMTP PDF report generator. The smtp table itself is left in the database (no alembic migration); rows become orphaned but harmless. A future migration can drop it once downstream deployments are known to be off the old schema. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
taylor_socfortress committed
Apr 24, 2026 at 15:52 UTC
324c9e78f13a29131efe3939448dd9eec9fc3552
12 files changed
+1
-510
backend/app/auth/models/users.py
-26
@@ -69,7 +69,6 @@ class User(SQLModel, table=True):
69
created_at: datetime.datetime = datetime.datetime.now()
70
role_id: Optional[int] = Field(foreign_key="role.id")
71
72
- smtp: "SMTP" = Relationship(back_populates="user")
72
role: Optional["Role"] = Relationship(back_populates="user")
73
customer_access: List["UserCustomerAccess"] = Relationship(back_populates="user")
74
tag_access: List["UserTagAccess"] = Relationship(back_populates="user")
@@ -110,31 +109,6 @@ class UserLogin(SQLModel):
109
password: str
110
111
113
-class SMTP(SQLModel, table=True):
114
- id: Optional[int] = Field(primary_key=True)
115
- email: EmailStr
116
- smtp_password: str = Field(max_length=256)
117
- smtp_server: str = Field(max_length=256)
118
- smtp_port: int
119
- user_id: int = Field(foreign_key="user.id")
120
-
121
- user: "User" = Relationship(back_populates="smtp")
122
-
123
-
124
-class SMTPInput(SQLModel):
125
- email: EmailStr
126
- smtp_password: str = Field(max_length=256)
127
- smtp_password2: str = Field(max_length=256)
128
- smtp_server: str = Field(max_length=256)
129
- smtp_port: int
130
-
131
- @validator("smtp_password2")
132
- def password_match(cls, v, values, **kwargs):
133
- if "smtp_password" in values and v != values["smtp_password"]:
134
- raise ValueError("passwords don't match")
135
- return v
136
-
137
-
112
class Password(BaseModel):
113
length: int = Field(
114
default=12,
backend/app/routers/__init__.py
-1
@@ -9,7 +9,6 @@ from .graylog import router as graylog_router
9
from .healthcheck import router as healtcheck_router
10
from .logs import router as logs_router
11
from .shuffle import router as shuffle_router
12
-from .smtp import router as smtp_router
12
from .sublime import router as sublime_router
13
from .velociraptor import router as velociraptor_router
14
from .wazuh_indexer import router as wazuh_indexer_router
backend/app/routers/smtp.py
deleted
-11
@@ -1,11 +0,0 @@
1
-from fastapi import APIRouter
2
-
3
-from app.smtp.routes.configure import smtp_configure_router
4
-from app.smtp.routes.reports import smtp_reports_router
5
-
6
-# Instantiate the APIRouter
7
-router = APIRouter()
8
-
9
-# Include the SMTP related routes
10
-router.include_router(smtp_configure_router, prefix="/smtp", tags=["smtp"])
11
-router.include_router(smtp_reports_router, prefix="/smtp", tags=["smtp"])
backend/app/smtp/routes/configure.py
deleted
-143
@@ -1,143 +0,0 @@
1
-from fastapi import APIRouter
2
-from fastapi import HTTPException
3
-from loguru import logger
4
-
5
-from app.auth.models.users import SMTP
6
-from app.auth.models.users import SMTPInput
7
-from app.auth.services.universal import select_all_users
8
-from app.auth.utils import AuthHandler
9
-from app.db.db_session import session
10
-from app.smtp.schema.configure import SMTPResponse
11
-
12
-smtp_configure_router = APIRouter()
13
-auth_handler = AuthHandler()
14
-
15
-
16
-@smtp_configure_router.post(
17
- "/{user_id}/register",
18
- response_model=SMTPResponse,
19
- status_code=200,
20
- description="Register new SMTP for user",
21
-)
22
-async def register(user_id: int, smtp: SMTPInput):
23
- """
24
- Register a new SMTP configuration for a user.
25
-
26
- Args:
27
- user_id (int): The ID of the user.
28
- smtp (SMTPInput): The SMTP configuration input.
29
-
30
- Returns:
31
- dict: A dictionary containing the message and success status of the operation.
32
- """
33
- users = select_all_users()
34
- logger.info(users)
35
- if not any(x.id == user_id for x in users):
36
- raise HTTPException(status_code=400, detail="User not found")
37
- # Check if SMTP already exists for user
38
- smtp_found = session.query(SMTP).filter(SMTP.user_id == user_id).first()
39
- if smtp_found:
40
- raise HTTPException(status_code=400, detail="SMTP already exists for user")
41
- hashed_pwd = auth_handler.get_password_hash(smtp.smtp_password)
42
- u = SMTP(
43
- email=smtp.email,
44
- smtp_password=hashed_pwd,
45
- smtp_server=smtp.smtp_server,
46
- smtp_port=smtp.smtp_port,
47
- user_id=user_id,
48
- )
49
- session.add(u)
50
- session.commit()
51
- return {"message": "SMTP created successfully", "success": True}
52
-
53
-
54
-@smtp_configure_router.get(
55
- "/{user_id}",
56
- response_model=SMTP,
57
- status_code=200,
58
- description="Get SMTP for user",
59
-)
60
-async def get_smtp(user_id: int):
61
- """
62
- Get SMTP configuration for a specific user.
63
-
64
- Args:
65
- user_id (int): The ID of the user.
66
-
67
- Returns:
68
- SMTP: The SMTP configuration for the user.
69
-
70
- Raises:
71
- HTTPException: If the user is not found or if SMTP configuration is not found for the user.
72
- """
73
- users = select_all_users()
74
- if not any(x.id == user_id for x in users):
75
- raise HTTPException(status_code=400, detail="User not found")
76
- smtp_found = session.query(SMTP).filter(SMTP.user_id == user_id).first()
77
- if not smtp_found:
78
- raise HTTPException(status_code=400, detail="SMTP not found for user")
79
- return smtp_found
80
-
81
-
82
-@smtp_configure_router.put(
83
- "/{user_id}",
84
- response_model=SMTPResponse,
85
- status_code=200,
86
- description="Update SMTP for user",
87
-)
88
-async def update_smtp(user_id: int, smtp: SMTPInput):
89
- """
90
- Update SMTP settings for a user.
91
-
92
- Args:
93
- user_id (int): The ID of the user.
94
- smtp (SMTPInput): The SMTP settings to update.
95
-
96
- Raises:
97
- HTTPException: If the user is not found or SMTP settings are not found for the user.
98
-
99
- Returns:
100
- dict: A dictionary containing the message and success status of the update.
101
- """
102
- users = select_all_users()
103
- if not any(x.id == user_id for x in users):
104
- raise HTTPException(status_code=400, detail="User not found")
105
- smtp_found = session.query(SMTP).filter(SMTP.user_id == user_id).first()
106
- if not smtp_found:
107
- raise HTTPException(status_code=400, detail="SMTP not found for user")
108
- smtp_found.email = smtp.email
109
- smtp_found.smtp_server = smtp.smtp_server
110
- smtp_found.smtp_port = smtp.smtp_port
111
- smtp_found.smtp_password = auth_handler.get_password_hash(smtp.smtp_password)
112
- session.commit()
113
- return {"message": "SMTP updated successfully", "success": True}
114
-
115
-
116
-@smtp_configure_router.delete(
117
- "/{user_id}",
118
- response_model=SMTPResponse,
119
- status_code=200,
120
- description="Delete SMTP for user",
121
-)
122
-async def delete_smtp(user_id: int):
123
- """
124
- Delete SMTP configuration for a user.
125
-
126
- Args:
127
- user_id (int): The ID of the user.
128
-
129
- Raises:
130
- HTTPException: If the user is not found or if SMTP configuration is not found for the user.
131
-
132
- Returns:
133
- dict: A dictionary containing the success message.
134
- """
135
- users = select_all_users()
136
- if not any(x.id == user_id for x in users):
137
- raise HTTPException(status_code=400, detail="User not found")
138
- smtp_found = session.query(SMTP).filter(SMTP.user_id == user_id).first()
139
- if not smtp_found:
140
- raise HTTPException(status_code=400, detail="SMTP not found for user")
141
- session.delete(smtp_found)
142
- session.commit()
143
- return {"message": "SMTP deleted successfully", "success": True}
backend/app/smtp/routes/reports.py
deleted
-42
@@ -1,42 +0,0 @@
1
-from fastapi import APIRouter
2
-from fastapi import HTTPException
3
-from loguru import logger
4
-
5
-from app.auth.models.users import SMTP
6
-from app.auth.models.users import SMTPInput
7
-from app.auth.services.universal import select_all_users
8
-from app.auth.utils import AuthHandler
9
-from app.db.db_session import session
10
-from app.smtp.schema.configure import SMTPResponse
11
-
12
-smtp_reports_router = APIRouter()
13
-auth_handler = AuthHandler()
14
-
15
-
16
-# ! TODO: Add SMTP reporting all things. Example is in the services/reports.py and services/create_report.py file
17
-@smtp_reports_router.post(
18
- "/{user_id}/register",
19
- response_model=SMTPResponse,
20
- status_code=200,
21
- description="Register new SMTP for user",
22
-)
23
-async def register(user_id: int, smtp: SMTPInput):
24
- users = select_all_users()
25
- logger.info(users)
26
- if not any(x.id == user_id for x in users):
27
- raise HTTPException(status_code=400, detail="User not found")
28
- # Check if SMTP already exists for user
29
- smtp_found = session.query(SMTP).filter(SMTP.user_id == user_id).first()
30
- if smtp_found:
31
- raise HTTPException(status_code=400, detail="SMTP already exists for user")
32
- hashed_pwd = auth_handler.get_password_hash(smtp.smtp_password)
33
- u = SMTP(
34
- email=smtp.email,
35
- smtp_password=hashed_pwd,
36
- smtp_server=smtp.smtp_server,
37
- smtp_port=smtp.smtp_port,
38
- user_id=user_id,
39
- )
40
- session.add(u)
41
- session.commit()
42
- return {"message": "SMTP created successfully", "success": True}
backend/app/smtp/schema/configure.py
deleted
-6
@@ -1,6 +0,0 @@
1
-from pydantic import BaseModel
2
-
3
-
4
-class SMTPResponse(BaseModel):
5
- message: str
6
- success: bool
backend/app/smtp/services/create_report.py
deleted
-151
@@ -1,151 +0,0 @@
1
-import urllib.request
2
-from typing import List
3
-
4
-import matplotlib
5
-from loguru import logger
6
-from reportlab.lib.pagesizes import letter
7
-from reportlab.lib.styles import getSampleStyleSheet
8
-from reportlab.lib.units import inch
9
-
10
-# from reportlab.pdfgen import canvas
11
-from reportlab.platypus import Image
12
-from reportlab.platypus import Paragraph
13
-from reportlab.platypus import SimpleDocTemplate
14
-from reportlab.platypus import Spacer
15
-
16
-matplotlib.use(
17
- "Agg",
18
-) # set the backend to Agg which is a non-interactive backend suitable
19
-# for scripts and web servers. This should resolve the main thread is not
20
-# in main loop issue as it bypasses the need for tkinter.
21
-import matplotlib.pyplot as plt
22
-
23
-from app.services.wazuh_indexer.alerts import AlertsService
24
-
25
-# ! TODO: Just a template
26
-
27
-
28
-def fetch_alert_data(service, fetch_func):
29
- """
30
- Fetches alert data using the provided function.
31
-
32
- Args:
33
- service: An instance of the service to use for fetching data.
34
- fetch_func (function): The function to use to fetch the data.
35
-
36
- Returns:
37
- Returns the result of the fetch function.
38
- """
39
- alerts = fetch_func()
40
- logger.info(alerts)
41
- return alerts
42
-
43
-
44
-def create_bar_chart(alerts: dict, title: str, output_filename: str) -> None:
45
- """
46
- Creates a horizontal bar chart of alerts and saves it to a file.
47
-
48
- Args:
49
- alerts (dict): A dictionary containing alert data.
50
- title (str): The title for the chart.
51
- output_filename (str): The filename to save the chart to.
52
-
53
- Returns:
54
- None
55
- """
56
- entities = [alert["hostname"] for alert in alerts["alerts_by_host"]]
57
- num_alerts = [alert["number_of_alerts"] for alert in alerts["alerts_by_host"]]
58
-
59
- plt.figure(figsize=(10, 10))
60
- plt.barh(entities, num_alerts, color="blue")
61
- plt.xlabel("Number of Alerts")
62
- plt.ylabel("Hostnames")
63
- plt.title(title)
64
- plt.tight_layout()
65
- plt.savefig(output_filename)
66
-
67
-
68
-def create_pie_chart(alerts: dict, title: str, output_filename: str) -> None:
69
- """
70
- Creates a pie chart of alerts and saves it to a file.
71
-
72
- Args:
73
- alerts (dict): A dictionary containing alert data.
74
- title (str): The title for the chart.
75
- output_filename (str): The filename to save the chart to.
76
-
77
- Returns:
78
- None
79
- """
80
- entities = [alert["rule"] for alert in alerts["alerts_by_rule"]]
81
- num_alerts = [alert["number_of_alerts"] for alert in alerts["alerts_by_rule"]]
82
-
83
- plt.figure(figsize=(10, 6))
84
- plt.pie(num_alerts, labels=entities, autopct="%1.1f%%")
85
- plt.legend(
86
- entities,
87
- loc="lower right",
88
- bbox_to_anchor=(1.0, 1.0),
89
- ) # Add this line to include a legend
90
- plt.title(title)
91
- plt.tight_layout()
92
- plt.savefig(output_filename)
93
-
94
-
95
-def create_pdf(title: str, image_filenames: List[str], pdf_filename: str) -> None:
96
- """
97
- Creates a PDF containing images.
98
-
99
- Args:
100
- title (str): The title for the PDF.
101
- image_filenames (List[str]): A list of image filenames to include in the PDF.
102
- pdf_filename (str): The filename to save the PDF to.
103
-
104
- Returns:
105
- None
106
- """
107
- # Download the SOC Fortress logo
108
- logo_url = "https://socfortress-images.s3.amazonaws.com/socfortress_logo_orange.png"
109
- logo_filename = "socfortress_logo_orange.png"
110
- urllib.request.urlretrieve(logo_url, logo_filename)
111
-
112
- doc = SimpleDocTemplate(pdf_filename, pagesize=letter)
113
- styles = getSampleStyleSheet()
114
- Story = []
115
-
116
- # Add a cover page
117
- Story.append(Spacer(1, 2 * inch))
118
- Story.append(Image(logo_filename, 5 * inch, 5 * inch)) # Adjust size as needed
119
- Story.append(Spacer(1, 1 * inch))
120
- style = styles["Title"]
121
- Story.append(Paragraph(title, style))
122
- Story.append(Spacer(1, 2 * inch))
123
-
124
- # Add the images
125
- for i, image_filename in enumerate(image_filenames):
126
- Story.append(Image(image_filename, 6 * inch, 4 * inch)) # Adjust size as needed
127
- Story.append(Spacer(1, 0.2 * inch))
128
-
129
- doc.build(Story)
130
-
131
-
132
-def create_alerts_report_pdf() -> None:
133
- """
134
- Creates a PDF report of alerts including a bar chart and a pie chart.
135
-
136
- Returns:
137
- None
138
- """
139
- service = AlertsService()
140
-
141
- alerts_by_host = fetch_alert_data(service, service.collect_alerts_by_host)
142
- create_bar_chart(alerts_by_host, "Number of Alerts by Host", "alerts_by_host.png")
143
-
144
- alerts_by_rules = fetch_alert_data(service, service.collect_alerts_by_rule)
145
- create_pie_chart(alerts_by_rules, "Number of Alerts by Rule", "alerts_by_rule.png")
146
-
147
- create_pdf(
148
- "Test",
149
- ["alerts_by_host.png", "alerts_by_rule.png"],
150
- "alerts_report.pdf",
151
- )
backend/app/smtp/services/reports.py
deleted
-122
@@ -1,122 +0,0 @@
1
-import smtplib
2
-from email import encoders
3
-from email.mime.base import MIMEBase
4
-from email.mime.multipart import MIMEMultipart
5
-from email.mime.text import MIMEText
6
-from typing import List
7
-
8
-from app.services.smtp.create_report import create_alerts_report_pdf
9
-from app.services.smtp.universal import EmailTemplate
10
-from app.services.smtp.universal import UniversalEmailCredentials
11
-
12
-# ! SEND REPORT
13
-
14
-
15
-class EmailReportSender:
16
- """
17
- Class for sending an email report with PDF attachments.
18
- """
19
-
20
- def __init__(self, to_email: str):
21
- """
22
- Constructor for the EmailReportSender class.
23
-
24
- Args:
25
- to_email (str): The email address to send the report to.
26
- """
27
- self.to_email = to_email
28
-
29
- def _get_credentials(self) -> dict:
30
- """
31
- Fetches the email credentials.
32
-
33
- Returns:
34
- dict: A dictionary containing the email credentials. If no credentials are found,
35
- the dictionary contains an "error" key.
36
- """
37
- try:
38
- return UniversalEmailCredentials.read_all()["emails_configured"][0]
39
- except IndexError:
40
- return {"error": "No email credentials found"}
41
-
42
- def create_email_message(self, subject: str, body: str) -> MIMEMultipart:
43
- """
44
- Creates an email message with the provided subject and body.
45
-
46
- Args:
47
- subject (str): The subject of the email.
48
- body (str): The body of the email.
49
-
50
- Returns:
51
- MIMEMultipart: An email message object. If an error occurs while fetching credentials,
52
- the return value is a dictionary containing an "error" key.
53
- """
54
- msg = MIMEMultipart()
55
- credentials = self._get_credentials()
56
- if "error" in credentials:
57
- return credentials
58
- msg["From"] = credentials["email"]
59
- msg["To"] = self.to_email
60
- msg["Subject"] = subject
61
- msg.attach(MIMEText(body, "html"))
62
- return msg
63
-
64
- def attach_pdfs(self, msg: MIMEMultipart, filenames: List[str]) -> MIMEMultipart:
65
- """
66
- Attaches PDF files to an email message.
67
-
68
- Args:
69
- msg (MIMEMultipart): The email message to attach the PDFs to.
70
- filenames (List[str]): A list of filenames of the PDFs to attach.
71
-
72
- Returns:
73
- MIMEMultipart: The email message with the attached PDFs.
74
- """
75
- for filename in filenames:
76
- with open(filename, "rb") as attachment_file:
77
- part = MIMEBase("application", "octet-stream")
78
- part.set_payload(attachment_file.read())
79
- encoders.encode_base64(part)
80
- part.add_header(
81
- "Content-Disposition",
82
- f"attachment; filename= {filename}",
83
- )
84
- msg.attach(part)
85
- return msg
86
-
87
- def send_email_with_pdf(self):
88
- """
89
- Sends an email with a PDF report.
90
-
91
- Returns:
92
- dict: A dictionary containing a "message" key describing the result of the operation
93
- and a "success" key indicating whether the operation was successful.
94
- """
95
- # Generate the PDF report
96
- create_alerts_report_pdf()
97
-
98
- # Render the email body
99
- template = EmailTemplate("email_template")
100
- body = template.render_html_body(template_name="email_template")
101
-
102
- # Create the email message and attach the PDF
103
- msg = self.create_email_message("Test Report", body)
104
- if isinstance(msg, dict) and "error" in msg:
105
- return {"message": msg["error"], "success": False}
106
- msg = self.attach_pdfs(msg, ["alerts_report.pdf"])
107
-
108
- credentials = self._get_credentials()
109
- if "error" in credentials:
110
- return {"message": credentials["error"], "success": False}
111
-
112
- # Send the email
113
- with smtplib.SMTP(
114
- credentials["smtp_server"],
115
- credentials["smtp_port"],
116
- ) as server:
117
- server.starttls()
118
- server.login(credentials["email"], credentials["password"])
119
- text = msg.as_string()
120
- server.sendmail(credentials["email"], self.to_email, text)
121
-
122
- return {"message": "Report sent successfully", "success": True}
backend/copilot.py
-2
@@ -76,7 +76,6 @@ from app.routers import scheduler
76
from app.routers import scoutsuite
77
from app.routers import shuffle
78
from app.routers import siem
79
-from app.routers import smtp
79
from app.routers import stack_provisioning
80
from app.routers import sublime
81
from app.routers import talon
@@ -145,7 +144,6 @@ api_router.include_router(sublime.router)
144
api_router.include_router(microsoft_patch_tuesday.router)
145
api_router.include_router(customers.router)
146
api_router.include_router(healthcheck.router)
148
-api_router.include_router(smtp.router)
147
api_router.include_router(dnstwist.router)
148
api_router.include_router(logs.router)
149
api_router.include_router(influxdb.router)
backend/requirements.in
-2
@@ -15,7 +15,6 @@ influxdb-client[async]
15
libmagic
16
loguru
17
marshmallow-sqlalchemy
18
-matplotlib
18
mitreattack-python
19
openai
20
passlib
@@ -31,7 +30,6 @@ python-magic
30
python-multipart
31
pyvelociraptor~=0.1
32
regex
34
-reportlab
33
requests
34
sqlmodel
35
uvicorn
backend/requirements.txt
-2
@@ -82,7 +82,6 @@ markdown-it-py==3.0.0
82
MarkupSafe==2.1.3
83
marshmallow==3.20.1
84
marshmallow-sqlalchemy==0.29.0
85
-matplotlib==3.8.0
85
mdurl==0.1.2
86
miniopy-async==1.21.1
87
#mitreattack-python==2.0.14
@@ -139,7 +138,6 @@ qrcode==7.4.2
138
reactivex==4.0.4
139
redis==4.6.0
140
regex==2023.10.3
142
-reportlab==4.0.5
141
requests==2.33.0
142
requests-cache==1.1.0
143
rfc3339-validator==0.1.4
docs/architecture/DATABASE_SCHEMA.md
+1
-2
@@ -10,7 +10,7 @@ This document summarizes the **current schema** for AI-agent workflows, using **
10
For typical agent change work, these domains are most relevant:
11
12
- Connectors and integration metadata: `connectors*`, `available_*`, `customer_*_connectors*`, `customer_*integrations*`, `integration_*`, `network_connectors_*`, `custom_alert_creation_*`, `monitoring_alerts`, `sigma_queries`, `github_audit_*`
13
-- Auth / users / roles: `user`, `role`, `smtp`, `user_customer_access`, `user_tag_access`, `role_tag_access`
13
+- Auth / users / roles: `user`, `role`, `user_customer_access`, `user_tag_access`, `role_tag_access`
14
- Incidents (alerts/cases/tags/comments): all `incident_management_*` tables
15
- Scheduler/job metadata: `scheduled_job_metadata`, `schedulerjob`, `index_snapshot_schedules`
16
- Agent data store / artifacts / reports: `agent_datastore`, `incident_management_case_datastore`, `incident_management_case_report_template_datastore`, `vulnerability_reports`, `sca_reports`, `agent_vulnerabilities`
@@ -149,7 +149,6 @@ When tag access control is enabled, alert visibility is constrained by the tag I
149
| `customer_provisioning_default_settings` | `id` | `cluster_name`, `cluster_key`, `master_ip`, `grafana_url`, `wazuh_worker_hostname` | None | `backend/app/customer_provisioning/models/default_settings.py` |
150
| `user` | `id` | `username`, `password`, `email`, `created_at`, `role_id` | `role_id -> role.id` | `backend/app/auth/models/users.py` (`User`) |
151
| `role` | `id` | `name`, `description` | None | `backend/app/auth/models/users.py` (`Role`) |
152
-| `smtp` | `id` | `email`, `smtp_server`, `smtp_port`, `user_id` | `user_id -> user.id` | `backend/app/auth/models/users.py` (`SMTP`) |
152
| `user_customer_access` | `id` | `user_id`, `customer_code`, `created_at` | `user_id -> user.id`, `customer_code -> customers.customer_code` | `backend/app/auth/models/users.py` |
153
| `user_tag_access` | `id` | `user_id`, `tag_id`, `created_at` | `user_id -> user.id`, `tag_id -> incident_management_alerttag.id` | `backend/app/auth/models/users.py` |
154
| `role_tag_access` | `id` | `role_id`, `tag_id`, `created_at` | `role_id -> role.id`, `tag_id -> incident_management_alerttag.id` | `backend/app/auth/models/users.py` |