Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security report mailing and small issue description fix #3930

Merged
merged 6 commits into from
Mar 13, 2025
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@ tld = "0.13"
openai = "^1.66.3"
async-timeout = "^4.0.3"
python-dateutil = "^2.9.0.post0"
pyzipper = "^0.3.6"
tweepy = "^4.15.0"


30 changes: 30 additions & 0 deletions website/templates/email/security_report.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description"
content="Security Vulnerability Report for {{ clean_domain }} - Please review the attached report for details.">
<meta name="keywords"
content="security, vulnerability, report, website security, cyber threats">
<title>Security Vulnerability Report</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css"
rel="stylesheet">
</head>
<body class="font-sans text-gray-700 bg-gray-100 p-6">
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-8">
<h1 class="text-2xl font-bold text-gray-800 mb-4">Security Vulnerability Report for {{ clean_domain }}</h1>
<p class="text-base text-gray-600 mb-4">A security vulnerability has been reported for your domain.</p>
<p class="text-base text-gray-600 mb-4">
The attached zip file contains screenshots and details of the vulnerability.
<br>
For security reasons, the zip file is password-protected.
</p>
<p class="text-base text-gray-600 mb-4">
<strong class="text-gray-800">Password:</strong> {{ password }}
</p>
<p class="text-base text-gray-600 mb-4">Please review this report as soon as possible and take appropriate action.</p>
<p class="text-sm text-gray-500 italic">This is an automated message from our security reporting system.</p>
</div>
</body>
</html>
2 changes: 1 addition & 1 deletion website/templates/report.html
Original file line number Diff line number Diff line change
@@ -195,7 +195,7 @@ <h2 class="text-xl font-semibold text-gray-900 truncate">{% trans "Bug Descripti
</div>
</div>
<textarea id="markdownInput"
name="markdown-content"
name="markdown_description"
class="w-full placeholder:text-base h-[300px] p-4 text-gray-700 border border-gray-200 rounded-lg "
placeholder="Enter your markdown here..."></textarea>
<div id="preview-area"
26 changes: 26 additions & 0 deletions website/tests.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
import time

import chromedriver_autoinstaller
from django.core import mail
from django.core.files.storage import default_storage
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import Client, LiveServerTestCase, TestCase
@@ -184,6 +185,30 @@ def test_post_bug_domain_url(self):
body = self.selenium.find_element("tag name", "body")
self.assertIn("XSS Attack on Google", body.text)

@override_settings(DEBUG=True)
def test_mail_security_bug(self):
self.selenium.get(f"{self.live_server_url}/report/")
self.selenium.find_element("name", "url").send_keys("https://example.com")
self.selenium.find_element("id", "description").send_keys("Critical Bug")
self.selenium.find_element("id", "markdownInput").send_keys("Bug details here")
image_path = os.path.abspath(os.path.join(os.getcwd(), "website/static/img/background.jpg"))
self.selenium.find_element("name", "screenshots").send_keys(image_path)

# Select label 4 (Security)
self.selenium.find_element("id", "labelSelect").send_keys("4")

self.selenium.find_element("name", "captcha_1").send_keys("PASSED")
self.selenium.find_element("name", "reportbug_button").click()

self.assertFalse(Issue.objects.filter(description="Critical Bug").exists())
self.assertEqual(len(mail.outbox), 1)
self.assertIn("Security Vulnerability Report", mail.outbox[0].subject)
self.selenium.get(f"{self.live_server_url}/all_activity/")
WebDriverWait(self.selenium, 30).until(EC.presence_of_element_located((By.TAG_NAME, "body")))

body_text = self.selenium.find_element(By.TAG_NAME, "body").text
self.assertNotIn("Critical Bug", body_text)

def setUp(self):
super().setUp()
# Verify emails for all test users
@@ -207,6 +232,7 @@ def verify_user_emails(self):
EmailAddress.objects.create(user=user, email=user.email, verified=True, primary=True)



class HideImage(TestCase):
def setUp(self):
test_issue = Issue.objects.create(description="test", url="test.com")
114 changes: 113 additions & 1 deletion website/views/issue.py
Original file line number Diff line number Diff line change
@@ -944,7 +944,6 @@
"report.html",
{"form": self.get_form(), "captcha_form": CaptchaForm()},
)

tokenauth = False
obj = form.save(commit=False)
report_anonymous = self.request.POST.get("report_anonymous", "off") == "on"
@@ -979,6 +978,119 @@
domain = Domain.objects.create(name=clean_domain, url=clean_domain)
domain.save()

# Don't save issue if security vulnerability
if form.instance.label == "4" or form.instance.label == 4:
dest_email = getattr(domain, "email", None)
if not dest_email and domain.organization:
dest_email = getattr(domain.organization, "email", None)

if dest_email:
import secrets
import string
import tempfile
from pathlib import Path

import pyzipper
from django.core.mail import EmailMessage

with tempfile.TemporaryDirectory() as temp_dir:
password = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(11))
zip_path = os.path.join(temp_dir, "security_report.zip")

screenshot_paths = []

if self.request.FILES.getlist("screenshots"):
for idx, screenshot in enumerate(self.request.FILES.getlist("screenshots")):
file_path = os.path.join(temp_dir, f"screenshot_{idx+1}{Path(screenshot.name).suffix}")
with open(file_path, "wb+") as destination:
for chunk in screenshot.chunks():
destination.write(chunk)
screenshot_paths.append(file_path)

elif self.request.POST.get("screenshot-hash"):
screenshot_hash = self.request.POST.get("screenshot-hash")
orig_path = os.path.join(settings.MEDIA_ROOT, "uploads", f"{screenshot_hash}.png")
if os.path.exists(orig_path):
dest_path = os.path.join(temp_dir, "screenshot_1.png")
import shutil

shutil.copy(orig_path, dest_path)
screenshot_paths.append(dest_path)

details_md_path = os.path.join(temp_dir, "vulnerability_details.md")
with open(details_md_path, "w") as f:
f.write("# Security Vulnerability Report\n")
f.write("\n")

f.write(f"**URL:** {obj.url}\n")
f.write(f"**Domain:** {clean_domain}\n")

if obj.cve_id:
f.write(f"**CVE ID:** {obj.cve_id}\n")

f.write("\n")
f.write(f"**Description:**\n{obj.description}\n\n")

if obj.markdown_description:
f.write("## Detailed Description\n")
f.write(f"{obj.markdown_description}\n\n")

if (
self.request.user.is_authenticated
and self.request.POST.get("report_anonymous", "off") != "on"
):
username = self.request.user.username or "Unknown User"
email = self.request.user.email if self.request.user.email else "No email provided"

f.write("### Reported by:\n")
f.write(f"- **Name:** {username}\n")
f.write(f"- **Email:** {email}\n\n")

f.write(f"**Report Date:** {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}\n")

screenshot_paths.append(details_md_path)

with pyzipper.AESZipFile(
zip_path, "w", compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES
) as zipf:
zipf.setpassword(password.encode())
for file in screenshot_paths:
zipf.write(file, arcname=os.path.basename(file))

email_subject = f"Security Vulnerability Report for {clean_domain}"
html_body = render_to_string(
"email/security_report.html",
{
"clean_domain": clean_domain,
"password": password,
},
)

email = EmailMessage(
subject=email_subject,
body=html_body,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[dest_email],
)
email.content_subtype = "html"

with open(zip_path, "rb") as f:
email.attach("security_report.zip", f.read(), "application/zip")

email.send(fail_silently=False)

messages.success(
self.request,
"Security vulnerability report sent securely to the organization. Thank you for your report.",
)
else:
messages.warning(
self.request,
"Could not send security vulnerability report as no contact email is available for this domain.",
)

return HttpResponseRedirect("/")

hunt = self.request.POST.get("hunt", None)
if hunt is not None and hunt != "None":
hunt = Hunt.objects.filter(id=hunt).first()