@cryptotaxi247 / CoPilot / commits / 324c9e78

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` |