Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Update backend_structured/scanners_core.py
Browse files- backend_structured/scanners_core.py +443 -155
backend_structured/scanners_core.py
CHANGED
|
@@ -1,11 +1,47 @@
|
|
| 1 |
import sys
|
| 2 |
import os
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
from bs4 import BeautifulSoup
|
| 6 |
from celery import Celery
|
| 7 |
from celery.schedules import crontab
|
| 8 |
-
from collections import defaultdict
|
| 9 |
from scanners.base_scanner import (
|
| 10 |
active_scan_logs, add_log, get_scan_logs, parse_domain,
|
| 11 |
cleanup_scan_logs, schedule_log_cleanup, emit_scan_progress
|
|
@@ -14,82 +50,49 @@ from scanners import get_pipeline, get_phases, build_scanner, apply_scan_options
|
|
| 14 |
from utils.fuzzer_engine import ContextAwareFuzzer
|
| 15 |
from cryptography import x509
|
| 16 |
from cryptography.hazmat.backends import default_backend
|
| 17 |
-
from datetime import datetime, timezone
|
| 18 |
-
from datetime import datetime, timezone, timedelta
|
| 19 |
-
from datetime import datetime, timezone, timezone
|
| 20 |
-
from dotenv import load_dotenv
|
| 21 |
-
load_dotenv()
|
| 22 |
|
| 23 |
import stripe
|
| 24 |
-
from flask import
|
|
|
|
|
|
|
|
|
|
| 25 |
from werkzeug.utils import secure_filename
|
| 26 |
-
from flask import Blueprint, send_file, jsonify, request
|
| 27 |
-
from flask import Flask
|
| 28 |
-
from flask import jsonify
|
| 29 |
-
from flask import render_template
|
| 30 |
-
from flask import request, abort, g, Response, make_response
|
| 31 |
from flask_cors import CORS
|
| 32 |
from flask_limiter import Limiter
|
| 33 |
from flask_limiter.util import get_remote_address
|
| 34 |
from flask_socketio import SocketIO, emit, join_room, leave_room
|
| 35 |
from flask_sqlalchemy import SQLAlchemy
|
| 36 |
-
from
|
| 37 |
-
|
| 38 |
from reportlab.lib import colors
|
| 39 |
from reportlab.lib.pagesizes import letter
|
| 40 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 41 |
from reportlab.pdfgen import canvas
|
| 42 |
-
from reportlab.platypus import
|
|
|
|
|
|
|
| 43 |
from reportlab.graphics.shapes import Drawing
|
| 44 |
from reportlab.graphics.charts.barcharts import VerticalBarChart
|
| 45 |
-
from sqlalchemy import event
|
| 46 |
-
from sqlalchemy import func
|
| 47 |
-
from sqlalchemy import inspect, text
|
| 48 |
-
from sqlalchemy import text
|
| 49 |
-
from sqlalchemy.engine import Engine
|
| 50 |
-
from typing import Any
|
| 51 |
-
from typing import Any, Callable
|
| 52 |
-
from typing import Callable
|
| 53 |
-
from typing import Literal
|
| 54 |
-
from urllib.parse import urljoin, urlparse
|
| 55 |
-
from urllib.parse import urlparse
|
| 56 |
-
import base64
|
| 57 |
-
import bcrypt
|
| 58 |
-
import concurrent.futures
|
| 59 |
-
from backend.utils.email_service import (
|
| 60 |
-
send_welcome_email,
|
| 61 |
-
send_scan_started,
|
| 62 |
-
send_scan_completed,
|
| 63 |
-
send_scan_failed,
|
| 64 |
-
send_critical_alert
|
| 65 |
-
)
|
| 66 |
-
|
| 67 |
-
import hashlib
|
| 68 |
-
import html
|
| 69 |
-
import io
|
| 70 |
-
import itertools
|
| 71 |
-
import json
|
| 72 |
-
import jwt
|
| 73 |
-
import math
|
| 74 |
-
import os
|
| 75 |
-
import re
|
| 76 |
-
import re, time, ipaddress, os, hashlib, threading, queue
|
| 77 |
-
import requests
|
| 78 |
-
import socket
|
| 79 |
-
import sqlite3
|
| 80 |
-
import ssl
|
| 81 |
-
import statistics
|
| 82 |
-
import threading
|
| 83 |
-
import time
|
| 84 |
-
import traceback
|
| 85 |
-
import urllib.error
|
| 86 |
-
import urllib.parse
|
| 87 |
-
import urllib.request
|
| 88 |
-
import urllib3
|
| 89 |
-
import uuid
|
| 90 |
-
import ipaddress
|
| 91 |
|
|
|
|
|
|
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
from .extensions import db, celery, socketio, limiter
|
| 95 |
from .models import *
|
|
@@ -935,9 +938,23 @@ class PageNumberRecorder(Flowable):
|
|
| 935 |
# Explicitly create a PDF bookmark for internal linking
|
| 936 |
self.canv.bookmarkPage(self.key_name)
|
| 937 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 938 |
def create_proportional_image(img_source, max_width=180, max_height=170, hAlign='CENTER'):
|
| 939 |
"""
|
| 940 |
-
Creates a ReportLab
|
|
|
|
| 941 |
"""
|
| 942 |
try:
|
| 943 |
from PIL import Image as PILImage
|
|
@@ -950,7 +967,7 @@ def create_proportional_image(img_source, max_width=180, max_height=170, hAlign=
|
|
| 950 |
|
| 951 |
w, h = pil_img.size
|
| 952 |
if not w or not h:
|
| 953 |
-
return
|
| 954 |
|
| 955 |
aspect = float(w) / float(h)
|
| 956 |
|
|
@@ -961,9 +978,9 @@ def create_proportional_image(img_source, max_width=180, max_height=170, hAlign=
|
|
| 961 |
calc_h = max_height
|
| 962 |
calc_w = max_height * aspect
|
| 963 |
|
| 964 |
-
return
|
| 965 |
except Exception:
|
| 966 |
-
return
|
| 967 |
|
| 968 |
def generate_scan_pdf(scan, vulnerabilities):
|
| 969 |
severity_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3, "Informational": 4}
|
|
@@ -995,21 +1012,113 @@ def generate_scan_pdf(scan, vulnerabilities):
|
|
| 995 |
|
| 996 |
# Try to fetch Organization logo and name
|
| 997 |
org_name = "[CLIENT ORGANIZATION]"
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1002 |
org_name = org.name
|
| 1003 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1004 |
try:
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1013 |
|
| 1014 |
def build_pdf_elements(page_dict=None):
|
| 1015 |
elements = []
|
|
@@ -1019,7 +1128,12 @@ def generate_scan_pdf(scan, vulnerabilities):
|
|
| 1019 |
|
| 1020 |
|
| 1021 |
# --- PAGE 1: COVER PAGE ---
|
| 1022 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1023 |
elements.append(Spacer(1, 100))
|
| 1024 |
elements.append(create_proportional_image(logo_path, max_width=180, max_height=170, hAlign='CENTER'))
|
| 1025 |
elements.append(Spacer(1, 60))
|
|
@@ -1029,7 +1143,11 @@ def generate_scan_pdf(scan, vulnerabilities):
|
|
| 1029 |
elements.append(PageBreak())
|
| 1030 |
|
| 1031 |
# --- PAGE 2: TITLE & META INFORMATION ---
|
| 1032 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1033 |
elements.append(create_proportional_image(logo_path, max_width=130, max_height=120, hAlign='CENTER'))
|
| 1034 |
elements.append(Spacer(1, 25))
|
| 1035 |
|
|
@@ -1052,13 +1170,14 @@ def generate_scan_pdf(scan, vulnerabilities):
|
|
| 1052 |
meta_data = [
|
| 1053 |
["Target Asset / Application", ":", scan.target_url],
|
| 1054 |
["Assessment Type", ":", audit_type_str],
|
|
|
|
| 1055 |
["Date of Testing", ":", f"{date_testing}"],
|
| 1056 |
["Report Version", ":", "v1.0"],
|
| 1057 |
["Report Status", ":", "Final"],
|
| 1058 |
["Classification", ":", "Confidential"]
|
| 1059 |
]
|
| 1060 |
|
| 1061 |
-
meta_table = Table(meta_data, colWidths=[
|
| 1062 |
meta_table.setStyle(TableStyle([
|
| 1063 |
('FONTNAME', (0,0), (-1,-1), 'Helvetica'),
|
| 1064 |
('ALIGN', (0,0), (-1,-1), 'LEFT'),
|
|
@@ -1170,7 +1289,7 @@ def generate_scan_pdf(scan, vulnerabilities):
|
|
| 1170 |
elements.append(obj_t)
|
| 1171 |
|
| 1172 |
elements.append(Spacer(1, 15))
|
| 1173 |
-
elements.append(Paragraph("Testing Process
|
| 1174 |
elements.append(Paragraph(" Consultants performed a discovery process to gather information about the target and searched for information disclosure vulnerabilities. With this data in hand, we conducted the bulk of the testing manually, which consisted of input validation tests, impersonation (authentication and authorization) tests, and session state management tests. The purpose of this penetration testing is to illuminate security risks by leveraging weaknesses within the environment that lead to the obtainment of unauthorized access and/or the retrieval of sensitive information. The shortcomings identified during the assessment were used to formulate recommendations and mitigation strategies for improving the overall security posture.", normal))
|
| 1175 |
|
| 1176 |
elements.append(Spacer(1, 15))
|
|
@@ -1186,7 +1305,7 @@ def generate_scan_pdf(scan, vulnerabilities):
|
|
| 1186 |
["Critical", "High", "Medium", "Low", "Informational"],
|
| 1187 |
[str(counts["Critical"]), str(counts["High"]), str(counts["Medium"]), str(counts["Low"]), str(counts["Informational"])]
|
| 1188 |
]
|
| 1189 |
-
sev_t = Table(sev_data, colWidths=[
|
| 1190 |
sev_t.setStyle(TableStyle([
|
| 1191 |
('BACKGROUND', (0,0), (-1,0), colors.HexColor("#F3F4F6")),
|
| 1192 |
('GRID', (0,0), (-1,-1), 1, colors.HexColor("#D1D5DB")),
|
|
@@ -1373,53 +1492,113 @@ def generate_scan_pdf(scan, vulnerabilities):
|
|
| 1373 |
elements.append(PageBreak())
|
| 1374 |
|
| 1375 |
# --- PAGE 7: METHODOLOGY & FINDINGS ---
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
| 1379 |
-
|
| 1380 |
-
elements.append(Paragraph("<bullet>•</bullet>All set of applicable SANS 25 Security Threats", bullet_style))
|
| 1381 |
-
elements.append(Spacer(1, 10))
|
| 1382 |
-
|
| 1383 |
-
owasp_data = [
|
| 1384 |
-
["A1:2017-Injection", "Evaluated", "Injection Flaws"],
|
| 1385 |
-
["A2:2017-Broken Authentication", "Evaluated", "Authentication Issues"],
|
| 1386 |
-
["A3:2017-Sensitive Data Exposure", "Evaluated", "Data Protection"],
|
| 1387 |
-
["A4:2017-XML External Entities (XXE)", "Evaluated", "XML Processors"],
|
| 1388 |
-
["A5:2017-Broken Access Control", "Evaluated", "Access Control"],
|
| 1389 |
-
["A6:2017-Security Misconfiguration", "Evaluated", "System Configuration"],
|
| 1390 |
-
["A7:2017-Cross-Site Scripting (XSS)", "Evaluated", "Client-side Flaws"],
|
| 1391 |
-
["A8:2017-Insecure Deserialization", "Evaluated", "Deserialization"],
|
| 1392 |
-
[Paragraph("A9:2017-Using Components with Known Vulnerabilities", normal), "Evaluated", "Vulnerable Components"],
|
| 1393 |
-
["A10:2017-Insufficient Logging & Monitoring", "Evaluated", "Logging"]
|
| 1394 |
-
]
|
| 1395 |
-
owasp_t = Table(owasp_data, colWidths=[200, 100, 170], hAlign='LEFT')
|
| 1396 |
-
owasp_t.setStyle(TableStyle([
|
| 1397 |
-
('GRID', (0,0), (-1,-1), 1, colors.HexColor("#D1D5DB")),
|
| 1398 |
-
]))
|
| 1399 |
-
elements.append(owasp_t)
|
| 1400 |
-
|
| 1401 |
-
elements.append(Spacer(1, 15))
|
| 1402 |
-
elements.append(Paragraph("Methodology", heading2))
|
| 1403 |
-
elements.append(Paragraph("Our Penetration Testing Methodology is grounded on the following guides and standards:", normal))
|
| 1404 |
-
if is_full:
|
| 1405 |
-
elements.append(Paragraph("<bullet>•</bullet>Penetration Testing Execution Standard", bullet_style))
|
| 1406 |
-
elements.append(Paragraph("<bullet>•</bullet>OWASP Top 10 Application Security Risks - 2017", bullet_style))
|
| 1407 |
-
elements.append(Paragraph("<bullet>•</bullet>OWASP Testing Guide", bullet_style))
|
| 1408 |
-
elements.append(Paragraph("<bullet>•</bullet>OWASP ASVS", bullet_style))
|
| 1409 |
-
|
| 1410 |
-
elements.append(Spacer(1, 10))
|
| 1411 |
-
elements.append(Paragraph("<b>Methodology Overview:</b> Open Web Application Security Project (OWASP) is an industry initiative for web application security. OWASP has identified the 10 most common attacks that succeed against web applications. These comprise the OWASP Top 10. Application penetration test includes all the items in the OWASP Top 10 and more. The penetration tester remotely tries to compromise the OWASP Top 10 flaws. The flaws listed by OWASP in its most recent Top 10 and the status of the application against those are depicted in the table above.", normal))
|
| 1412 |
-
elements.append(Spacer(1, 15))
|
| 1413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1414 |
elements.append(Paragraph("SSL/TLS Analysis", heading2))
|
| 1415 |
-
|
| 1416 |
-
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
|
| 1420 |
-
|
| 1421 |
-
|
| 1422 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1423 |
elements.append(Spacer(1, 15))
|
| 1424 |
|
| 1425 |
elements.append(PageBreak())
|
|
@@ -1465,27 +1644,51 @@ def generate_scan_pdf(scan, vulnerabilities):
|
|
| 1465 |
|
| 1466 |
def get_proof_of_detection(v, dom):
|
| 1467 |
proof = ""
|
| 1468 |
-
if getattr(v, 'request_details', None): proof += f"# Request Details\n{v.request_details}\n\n"
|
| 1469 |
-
if getattr(v, 'payload', None): proof += f"# Payload Used\n{v.payload}\n\n"
|
| 1470 |
-
if getattr(v, 'response_details', None): proof += f"# Response Details\n{v.response_details}\n\n"
|
| 1471 |
-
if getattr(v, 'evidence', None): proof += f"# Evidence\n{v.evidence}\n\n"
|
|
|
|
| 1472 |
|
| 1473 |
if proof.strip(): return proof.strip()
|
| 1474 |
|
| 1475 |
-
cat = getattr(v, 'category', '')
|
| 1476 |
-
title = getattr(v, 'title', '')
|
| 1477 |
-
desc = getattr(v, 'description', '')
|
|
|
|
| 1478 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1479 |
if cat == 'Security Headers':
|
| 1480 |
-
return f"# Request Headers\nGET / HTTP/1.1\nHost: {dom}\nUser-Agent: LarShield/2.0\n\n# Response Headers
|
| 1481 |
-
|
| 1482 |
-
|
| 1483 |
-
|
| 1484 |
-
return f"# Malicious Request Payload\nPOST /api/v1/query HTTP/1.1\nHost: {dom}\nContent-Type: application/json\n\n{{\n \"input\": \"1' OR '1'='1' --\"\n}}\n\n# Response Analysis\nHTTP/1.1 500 Internal Server Error\n[Detection] {title}\nDatabase error or behavioral delay confirmed injection execution."
|
| 1485 |
-
if 'XSS' in title or 'Cross-Site' in title:
|
| 1486 |
-
return f"# Payload Injection\nGET /search?q=<script>alert('XSS')</script> HTTP/1.1\nHost: {dom}\n\n# Response Analysis\nHTTP/1.1 200 OK\n[Detection] {title}\nPayload reflected in DOM without sanitization."
|
| 1487 |
-
|
| 1488 |
-
return f"# Automated Probe Log\nTarget: {dom}\nCategory: {cat}\nScanner Module: {title}\n\n# Detection Output\n[System] Vulnerability confirmed via behavioral analysis and pattern matching.\n[Evidence] {desc.split('.')[0] if desc else ''}."
|
| 1489 |
|
| 1490 |
for idx, vuln in enumerate(vulnerabilities, 1):
|
| 1491 |
if idx > 1:
|
|
@@ -1508,7 +1711,7 @@ def generate_scan_pdf(scan, vulnerabilities):
|
|
| 1508 |
vuln_data = [
|
| 1509 |
["Severity", Paragraph(f"<font color='{sev_hex}'>{display_sev}</font>"), "CVSS Score", str(vuln.cvss_score)],
|
| 1510 |
["Category", vuln.category, "Detected", vuln.detected_at.strftime('%Y-%m-%d')],
|
| 1511 |
-
["CVSS Vector", cvss_vector, ", "]
|
| 1512 |
]
|
| 1513 |
vt = Table(vuln_data, colWidths=[80, 150, 80, 150])
|
| 1514 |
vt.setStyle(TableStyle([
|
|
@@ -1555,13 +1758,98 @@ def generate_scan_pdf(scan, vulnerabilities):
|
|
| 1555 |
elements.append(proof_table)
|
| 1556 |
elements.append(Spacer(1, 15))
|
| 1557 |
|
| 1558 |
-
elements.append(Paragraph("<b>Remediation:</b>", styles['Normal']))
|
| 1559 |
-
rem_text_raw = vuln.remediation or ""
|
| 1560 |
-
rem_text_raw = re.sub(r'\.\s+', '.\n', rem_text_raw)
|
| 1561 |
-
rem_text = markdown_to_reportlab_html(rem_text_raw)
|
| 1562 |
-
elements.append(Paragraph(rem_text, normal))
|
| 1563 |
-
elements.append(Spacer(1, 25))
|
| 1564 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1565 |
return elements
|
| 1566 |
|
| 1567 |
total_pages = [0]
|
|
@@ -1583,10 +1871,10 @@ def generate_scan_pdf(scan, vulnerabilities):
|
|
| 1583 |
canvas_obj.setFont('Helvetica-Bold', 12)
|
| 1584 |
canvas_obj.drawCentredString(letter[0] / 2.0, letter[1] - 35, "Web Application VAPT Report")
|
| 1585 |
|
| 1586 |
-
|
| 1587 |
-
|
| 1588 |
try:
|
| 1589 |
-
canvas_obj.drawImage(ImageReader(
|
| 1590 |
except Exception:
|
| 1591 |
pass
|
| 1592 |
|
|
|
|
| 1 |
import sys
|
| 2 |
import os
|
| 3 |
+
import re
|
| 4 |
+
import time
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
import uuid
|
| 8 |
+
import html
|
| 9 |
+
import io
|
| 10 |
+
import hashlib
|
| 11 |
+
import sqlite3
|
| 12 |
+
import socket
|
| 13 |
+
import ssl
|
| 14 |
+
import base64
|
| 15 |
+
import bcrypt
|
| 16 |
+
import jwt
|
| 17 |
+
import requests
|
| 18 |
+
import urllib3
|
| 19 |
+
import ipaddress
|
| 20 |
+
import queue
|
| 21 |
+
import threading
|
| 22 |
+
import statistics
|
| 23 |
+
import itertools
|
| 24 |
+
import traceback
|
| 25 |
+
import concurrent.futures
|
| 26 |
+
from datetime import datetime, timezone, timedelta
|
| 27 |
+
from typing import Any, Callable, Literal
|
| 28 |
+
from collections import defaultdict
|
| 29 |
+
from functools import wraps
|
| 30 |
+
from urllib.parse import urljoin, urlparse
|
| 31 |
+
from dotenv import load_dotenv
|
| 32 |
+
|
| 33 |
+
load_dotenv()
|
| 34 |
+
|
| 35 |
+
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
| 36 |
+
BACKEND_DIR = os.path.join(BASE_DIR, 'backend')
|
| 37 |
+
if BASE_DIR not in sys.path:
|
| 38 |
+
sys.path.insert(0, BASE_DIR)
|
| 39 |
+
if BACKEND_DIR not in sys.path:
|
| 40 |
+
sys.path.insert(0, BACKEND_DIR)
|
| 41 |
|
| 42 |
from bs4 import BeautifulSoup
|
| 43 |
from celery import Celery
|
| 44 |
from celery.schedules import crontab
|
|
|
|
| 45 |
from scanners.base_scanner import (
|
| 46 |
active_scan_logs, add_log, get_scan_logs, parse_domain,
|
| 47 |
cleanup_scan_logs, schedule_log_cleanup, emit_scan_progress
|
|
|
|
| 50 |
from utils.fuzzer_engine import ContextAwareFuzzer
|
| 51 |
from cryptography import x509
|
| 52 |
from cryptography.hazmat.backends import default_backend
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
import stripe
|
| 55 |
+
from flask import (
|
| 56 |
+
Flask, Blueprint, request, jsonify, current_app, send_from_directory,
|
| 57 |
+
send_file, render_template, abort, g, Response, make_response
|
| 58 |
+
)
|
| 59 |
from werkzeug.utils import secure_filename
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
from flask_cors import CORS
|
| 61 |
from flask_limiter import Limiter
|
| 62 |
from flask_limiter.util import get_remote_address
|
| 63 |
from flask_socketio import SocketIO, emit, join_room, leave_room
|
| 64 |
from flask_sqlalchemy import SQLAlchemy
|
| 65 |
+
from markupsafe import escape
|
| 66 |
+
|
| 67 |
from reportlab.lib import colors
|
| 68 |
from reportlab.lib.pagesizes import letter
|
| 69 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 70 |
from reportlab.pdfgen import canvas
|
| 71 |
+
from reportlab.platypus import (
|
| 72 |
+
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, Image, Flowable
|
| 73 |
+
)
|
| 74 |
from reportlab.graphics.shapes import Drawing
|
| 75 |
from reportlab.graphics.charts.barcharts import VerticalBarChart
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
from sqlalchemy import event, func, inspect, text
|
| 78 |
+
from sqlalchemy.engine import Engine
|
| 79 |
|
| 80 |
+
try:
|
| 81 |
+
from backend.utils.email_service import (
|
| 82 |
+
send_welcome_email,
|
| 83 |
+
send_scan_started,
|
| 84 |
+
send_scan_completed,
|
| 85 |
+
send_scan_failed,
|
| 86 |
+
send_critical_alert
|
| 87 |
+
)
|
| 88 |
+
except ImportError:
|
| 89 |
+
from utils.email_service import (
|
| 90 |
+
send_welcome_email,
|
| 91 |
+
send_scan_started,
|
| 92 |
+
send_scan_completed,
|
| 93 |
+
send_scan_failed,
|
| 94 |
+
send_critical_alert
|
| 95 |
+
)
|
| 96 |
|
| 97 |
from .extensions import db, celery, socketio, limiter
|
| 98 |
from .models import *
|
|
|
|
| 938 |
# Explicitly create a PDF bookmark for internal linking
|
| 939 |
self.canv.bookmarkPage(self.key_name)
|
| 940 |
|
| 941 |
+
class ReusableImage(Image):
|
| 942 |
+
"""
|
| 943 |
+
Subclass of ReportLab Image that resets BytesIO stream position to 0 on draw(),
|
| 944 |
+
ensuring multi-pass ReportLab builders (like multiBuild) do not render blank images on later passes.
|
| 945 |
+
"""
|
| 946 |
+
def draw(self):
|
| 947 |
+
if hasattr(self.filename, 'seek'):
|
| 948 |
+
try:
|
| 949 |
+
self.filename.seek(0)
|
| 950 |
+
except Exception:
|
| 951 |
+
pass
|
| 952 |
+
super().draw()
|
| 953 |
+
|
| 954 |
def create_proportional_image(img_source, max_width=180, max_height=170, hAlign='CENTER'):
|
| 955 |
"""
|
| 956 |
+
Creates a ReportLab ReusableImage object that strictly preserves original aspect ratio
|
| 957 |
+
and survives multi-pass ReportLab builds.
|
| 958 |
"""
|
| 959 |
try:
|
| 960 |
from PIL import Image as PILImage
|
|
|
|
| 967 |
|
| 968 |
w, h = pil_img.size
|
| 969 |
if not w or not h:
|
| 970 |
+
return ReusableImage(img_source, width=max_width, height=max_height, kind='proportional', hAlign=hAlign)
|
| 971 |
|
| 972 |
aspect = float(w) / float(h)
|
| 973 |
|
|
|
|
| 978 |
calc_h = max_height
|
| 979 |
calc_w = max_height * aspect
|
| 980 |
|
| 981 |
+
return ReusableImage(img_source, width=calc_w, height=calc_h, kind='proportional', hAlign=hAlign)
|
| 982 |
except Exception:
|
| 983 |
+
return ReusableImage(img_source, width=max_width, height=max_height, kind='proportional', hAlign=hAlign)
|
| 984 |
|
| 985 |
def generate_scan_pdf(scan, vulnerabilities):
|
| 986 |
severity_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3, "Informational": 4}
|
|
|
|
| 1012 |
|
| 1013 |
# Try to fetch Organization logo and name
|
| 1014 |
org_name = "[CLIENT ORGANIZATION]"
|
| 1015 |
+
org_logo_raw_bytes = None
|
| 1016 |
+
|
| 1017 |
+
target_org_id = getattr(scan, 'org_id', None)
|
| 1018 |
+
if not target_org_id and getattr(scan, 'user_id', None):
|
| 1019 |
+
try:
|
| 1020 |
+
user = db.session.get(User, scan.user_id)
|
| 1021 |
+
if user and user.org_id:
|
| 1022 |
+
target_org_id = user.org_id
|
| 1023 |
+
except Exception:
|
| 1024 |
+
pass
|
| 1025 |
+
|
| 1026 |
+
org = None
|
| 1027 |
+
if target_org_id:
|
| 1028 |
+
try:
|
| 1029 |
+
org = db.session.get(Organization, target_org_id)
|
| 1030 |
+
except Exception:
|
| 1031 |
+
pass
|
| 1032 |
+
if not org:
|
| 1033 |
+
try:
|
| 1034 |
+
org = Organization.query.first()
|
| 1035 |
+
except Exception:
|
| 1036 |
+
pass
|
| 1037 |
+
|
| 1038 |
+
if org:
|
| 1039 |
+
if getattr(org, 'name', None):
|
| 1040 |
org_name = org.name
|
| 1041 |
+
if getattr(org, 'report_logo_url', None):
|
| 1042 |
+
logo_url = org.report_logo_url.strip()
|
| 1043 |
+
|
| 1044 |
+
# 1. Check if base64 data URI
|
| 1045 |
+
if logo_url.startswith('data:image/'):
|
| 1046 |
try:
|
| 1047 |
+
header, b64_data = logo_url.split(',', 1)
|
| 1048 |
+
org_logo_raw_bytes = base64.b64decode(b64_data)
|
| 1049 |
+
except Exception as e:
|
| 1050 |
+
print(f"[PDF Generator] Base64 logo decode error: {e}")
|
| 1051 |
+
|
| 1052 |
+
# 2. Check local disk candidate paths
|
| 1053 |
+
filename = logo_url.split('/')[-1]
|
| 1054 |
+
if not org_logo_raw_bytes and filename:
|
| 1055 |
+
candidate_paths = [
|
| 1056 |
+
os.path.join(os.getcwd(), 'uploads', 'logos', filename),
|
| 1057 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'uploads', 'logos', filename)),
|
| 1058 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'uploads', 'logos', filename)),
|
| 1059 |
+
os.path.join(os.getcwd(), 'uploads', filename),
|
| 1060 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'uploads', filename)),
|
| 1061 |
+
]
|
| 1062 |
+
for c_path in candidate_paths:
|
| 1063 |
+
if os.path.exists(c_path):
|
| 1064 |
+
try:
|
| 1065 |
+
with open(c_path, 'rb') as f:
|
| 1066 |
+
org_logo_raw_bytes = f.read()
|
| 1067 |
+
if org_logo_raw_bytes:
|
| 1068 |
+
break
|
| 1069 |
+
except Exception as e:
|
| 1070 |
+
print(f"[PDF Generator] Local logo read error ({c_path}): {e}")
|
| 1071 |
+
|
| 1072 |
+
# 3. HTTP / HTTPS fallback
|
| 1073 |
+
if not org_logo_raw_bytes and (logo_url.startswith('http://') or logo_url.startswith('https://')):
|
| 1074 |
+
try:
|
| 1075 |
+
resp = requests.get(
|
| 1076 |
+
logo_url,
|
| 1077 |
+
timeout=5,
|
| 1078 |
+
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) LarShield/2.0'}
|
| 1079 |
+
)
|
| 1080 |
+
if resp.status_code == 200 and resp.content:
|
| 1081 |
+
org_logo_raw_bytes = resp.content
|
| 1082 |
+
except Exception as e:
|
| 1083 |
+
print(f"[PDF Generator] HTTP logo download error ({logo_url}): {e}")
|
| 1084 |
+
|
| 1085 |
+
# Process and sanitize logo image with PIL (convert to clean PNG bytes)
|
| 1086 |
+
org_logo_png_bytes = None
|
| 1087 |
+
if org_logo_raw_bytes:
|
| 1088 |
+
try:
|
| 1089 |
+
from PIL import Image as PILImage
|
| 1090 |
+
pil_img = PILImage.open(io.BytesIO(org_logo_raw_bytes))
|
| 1091 |
+
out_buf = io.BytesIO()
|
| 1092 |
+
pil_img.save(out_buf, format='PNG')
|
| 1093 |
+
org_logo_png_bytes = out_buf.getvalue()
|
| 1094 |
+
except Exception as e:
|
| 1095 |
+
print(f"[PDF Generator] PIL image conversion error: {e}")
|
| 1096 |
+
org_logo_png_bytes = org_logo_raw_bytes # Use raw bytes if PIL fails
|
| 1097 |
+
|
| 1098 |
+
def get_org_logo_stream():
|
| 1099 |
+
"""Returns a fresh BytesIO stream every time called to prevent stream EOF issues across multi-pass ReportLab rendering."""
|
| 1100 |
+
if org_logo_png_bytes:
|
| 1101 |
+
return io.BytesIO(org_logo_png_bytes)
|
| 1102 |
+
return None
|
| 1103 |
+
|
| 1104 |
+
# Locate main brand logo dynamically with fallback candidate paths
|
| 1105 |
+
logo_path = None
|
| 1106 |
+
possible_logo_paths = [
|
| 1107 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), 'static', 'logo.png')),
|
| 1108 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'static', 'logo.png')),
|
| 1109 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'frontend', 'public', 'logo.png')),
|
| 1110 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'frontend', 'public', 'logo.jpg')),
|
| 1111 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'frontend', 'public', 'larshieldlogowhite.png')),
|
| 1112 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'frontend', 'dist', 'logo.png')),
|
| 1113 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'frontend', 'src', 'assets', 'LarShield Symbol logo.png')),
|
| 1114 |
+
os.path.abspath(os.path.join(os.path.dirname(__file__), 'frontend', 'public', 'logo.png')),
|
| 1115 |
+
]
|
| 1116 |
+
for candidate in possible_logo_paths:
|
| 1117 |
+
if os.path.exists(candidate):
|
| 1118 |
+
logo_path = candidate
|
| 1119 |
+
break
|
| 1120 |
+
|
| 1121 |
+
has_local_logo = logo_path is not None
|
| 1122 |
|
| 1123 |
def build_pdf_elements(page_dict=None):
|
| 1124 |
elements = []
|
|
|
|
| 1128 |
|
| 1129 |
|
| 1130 |
# --- PAGE 1: COVER PAGE ---
|
| 1131 |
+
logo_stream_p1 = get_org_logo_stream()
|
| 1132 |
+
if logo_stream_p1:
|
| 1133 |
+
elements.append(Spacer(1, 100))
|
| 1134 |
+
elements.append(create_proportional_image(logo_stream_p1, max_width=180, max_height=170, hAlign='CENTER'))
|
| 1135 |
+
elements.append(Spacer(1, 60))
|
| 1136 |
+
elif has_local_logo:
|
| 1137 |
elements.append(Spacer(1, 100))
|
| 1138 |
elements.append(create_proportional_image(logo_path, max_width=180, max_height=170, hAlign='CENTER'))
|
| 1139 |
elements.append(Spacer(1, 60))
|
|
|
|
| 1143 |
elements.append(PageBreak())
|
| 1144 |
|
| 1145 |
# --- PAGE 2: TITLE & META INFORMATION ---
|
| 1146 |
+
logo_stream_p2 = get_org_logo_stream()
|
| 1147 |
+
if logo_stream_p2:
|
| 1148 |
+
elements.append(create_proportional_image(logo_stream_p2, max_width=130, max_height=120, hAlign='CENTER'))
|
| 1149 |
+
elements.append(Spacer(1, 25))
|
| 1150 |
+
elif has_local_logo:
|
| 1151 |
elements.append(create_proportional_image(logo_path, max_width=130, max_height=120, hAlign='CENTER'))
|
| 1152 |
elements.append(Spacer(1, 25))
|
| 1153 |
|
|
|
|
| 1170 |
meta_data = [
|
| 1171 |
["Target Asset / Application", ":", scan.target_url],
|
| 1172 |
["Assessment Type", ":", audit_type_str],
|
| 1173 |
+
["Authorization Reference", ":", "Accepted via Terms of Service Modal"],
|
| 1174 |
["Date of Testing", ":", f"{date_testing}"],
|
| 1175 |
["Report Version", ":", "v1.0"],
|
| 1176 |
["Report Status", ":", "Final"],
|
| 1177 |
["Classification", ":", "Confidential"]
|
| 1178 |
]
|
| 1179 |
|
| 1180 |
+
meta_table = Table(meta_data, colWidths=[165, 10, 355], hAlign='LEFT')
|
| 1181 |
meta_table.setStyle(TableStyle([
|
| 1182 |
('FONTNAME', (0,0), (-1,-1), 'Helvetica'),
|
| 1183 |
('ALIGN', (0,0), (-1,-1), 'LEFT'),
|
|
|
|
| 1289 |
elements.append(obj_t)
|
| 1290 |
|
| 1291 |
elements.append(Spacer(1, 15))
|
| 1292 |
+
elements.append(Paragraph("Testing Process", heading2))
|
| 1293 |
elements.append(Paragraph(" Consultants performed a discovery process to gather information about the target and searched for information disclosure vulnerabilities. With this data in hand, we conducted the bulk of the testing manually, which consisted of input validation tests, impersonation (authentication and authorization) tests, and session state management tests. The purpose of this penetration testing is to illuminate security risks by leveraging weaknesses within the environment that lead to the obtainment of unauthorized access and/or the retrieval of sensitive information. The shortcomings identified during the assessment were used to formulate recommendations and mitigation strategies for improving the overall security posture.", normal))
|
| 1294 |
|
| 1295 |
elements.append(Spacer(1, 15))
|
|
|
|
| 1305 |
["Critical", "High", "Medium", "Low", "Informational"],
|
| 1306 |
[str(counts["Critical"]), str(counts["High"]), str(counts["Medium"]), str(counts["Low"]), str(counts["Informational"])]
|
| 1307 |
]
|
| 1308 |
+
sev_t = Table(sev_data, colWidths=[106.4, 106.4, 106.4, 106.4, 106.4], hAlign='LEFT')
|
| 1309 |
sev_t.setStyle(TableStyle([
|
| 1310 |
('BACKGROUND', (0,0), (-1,0), colors.HexColor("#F3F4F6")),
|
| 1311 |
('GRID', (0,0), (-1,-1), 1, colors.HexColor("#D1D5DB")),
|
|
|
|
| 1492 |
elements.append(PageBreak())
|
| 1493 |
|
| 1494 |
# --- PAGE 7: METHODOLOGY & FINDINGS ---
|
| 1495 |
+
elements.append(Paragraph("Performed tests", heading2))
|
| 1496 |
+
elements.append(Paragraph("<bullet>•</bullet>All set of applicable OWASP Top 10 Security Threats", bullet_style))
|
| 1497 |
+
elements.append(Paragraph("<bullet>•</bullet>All set of applicable SANS 25 Security Threats", bullet_style))
|
| 1498 |
+
elements.append(Spacer(1, 10))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1499 |
|
| 1500 |
+
owasp_data = [
|
| 1501 |
+
["A1:2017-Injection", "Evaluated", "Injection Flaws"],
|
| 1502 |
+
["A2:2017-Broken Authentication", "Evaluated", "Authentication Issues"],
|
| 1503 |
+
["A3:2017-Sensitive Data Exposure", "Evaluated", "Data Protection"],
|
| 1504 |
+
["A4:2017-XML External Entities (XXE)", "Evaluated", "XML Processors"],
|
| 1505 |
+
["A5:2017-Broken Access Control", "Evaluated", "Access Control"],
|
| 1506 |
+
["A6:2017-Security Misconfiguration", "Evaluated", "System Configuration"],
|
| 1507 |
+
["A7:2017-Cross-Site Scripting (XSS)", "Evaluated", "Client-side Flaws"],
|
| 1508 |
+
["A8:2017-Insecure Deserialization", "Evaluated", "Deserialization"],
|
| 1509 |
+
[Paragraph("A9:2017-Using Components with Known Vulnerabilities", normal), "Evaluated", "Vulnerable Components"],
|
| 1510 |
+
["A10:2017-Insufficient Logging & Monitoring", "Evaluated", "Logging"]
|
| 1511 |
+
]
|
| 1512 |
+
owasp_t = Table(owasp_data, colWidths=[210, 100, 222], hAlign='LEFT')
|
| 1513 |
+
owasp_t.setStyle(TableStyle([
|
| 1514 |
+
('GRID', (0,0), (-1,-1), 1, colors.HexColor("#D1D5DB")),
|
| 1515 |
+
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
| 1516 |
+
('BOTTOMPADDING', (0,0), (-1,-1), 6),
|
| 1517 |
+
('TOPPADDING', (0,0), (-1,-1), 6),
|
| 1518 |
+
('BACKGROUND', (1,0), (1,-1), colors.HexColor("#F9FAFB")),
|
| 1519 |
+
('FONTNAME', (1,0), (1,-1), 'Helvetica-Bold'),
|
| 1520 |
+
]))
|
| 1521 |
+
elements.append(owasp_t)
|
| 1522 |
+
|
| 1523 |
+
elements.append(Spacer(1, 15))
|
| 1524 |
+
elements.append(Paragraph("Methodology", heading2))
|
| 1525 |
+
elements.append(Paragraph("Our Penetration Testing Methodology is grounded on the following guides and standards:", normal))
|
| 1526 |
+
elements.append(Paragraph("<bullet>•</bullet>Penetration Testing Execution Standard", bullet_style))
|
| 1527 |
+
elements.append(Paragraph("<bullet>•</bullet>OWASP Top 10 Application Security Risks - 2017", bullet_style))
|
| 1528 |
+
elements.append(Paragraph("<bullet>•</bullet>OWASP Testing Guide", bullet_style))
|
| 1529 |
+
elements.append(Paragraph("<bullet>•</bullet>OWASP ASVS", bullet_style))
|
| 1530 |
+
|
| 1531 |
+
elements.append(Spacer(1, 10))
|
| 1532 |
+
elements.append(Paragraph("<b>Methodology Overview:</b> Open Web Application Security Project (OWASP) is an industry initiative for web application security. OWASP has identified the 10 most common attacks that succeed against web applications. These comprise the OWASP Top 10. Application penetration test includes all the items in the OWASP Top 10 and more. The penetration tester remotely tries to compromise the OWASP Top 10 flaws. The flaws listed by OWASP in its most recent Top 10 and the status of the application against those are depicted in the table above.", normal))
|
| 1533 |
+
elements.append(Spacer(1, 15))
|
| 1534 |
+
|
| 1535 |
elements.append(Paragraph("SSL/TLS Analysis", heading2))
|
| 1536 |
+
def fetch_ssl_details(target_url):
|
| 1537 |
+
import socket, ssl, urllib.parse
|
| 1538 |
+
try:
|
| 1539 |
+
url = target_url if '://' in target_url else f'https://{target_url}'
|
| 1540 |
+
parsed = urllib.parse.urlparse(url)
|
| 1541 |
+
hostname = parsed.netloc or parsed.path
|
| 1542 |
+
if ':' in hostname:
|
| 1543 |
+
hostname = hostname.split(':')[0]
|
| 1544 |
+
if hostname:
|
| 1545 |
+
ctx = ssl.create_default_context()
|
| 1546 |
+
with socket.create_connection((hostname, 443), timeout=3) as sock:
|
| 1547 |
+
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
|
| 1548 |
+
cert = ssock.getpeercert()
|
| 1549 |
+
version = ssock.version() or "TLSv1.2"
|
| 1550 |
+
|
| 1551 |
+
issuer_tuples = cert.get('issuer', ())
|
| 1552 |
+
issuer_parts = []
|
| 1553 |
+
for group in issuer_tuples:
|
| 1554 |
+
for k, v in group:
|
| 1555 |
+
issuer_parts.append(f"{k}={v}")
|
| 1556 |
+
issuer_str = ", ".join(issuer_parts)
|
| 1557 |
+
|
| 1558 |
+
subject_tuples = cert.get('subject', ())
|
| 1559 |
+
subject_parts = []
|
| 1560 |
+
for group in subject_tuples:
|
| 1561 |
+
for k, v in group:
|
| 1562 |
+
subject_parts.append(f"{k}={v}")
|
| 1563 |
+
subject_str = ", ".join(subject_parts)
|
| 1564 |
+
|
| 1565 |
+
expiry_str = cert.get('notAfter', '2025-07-06 12:42:21 UTC')
|
| 1566 |
+
|
| 1567 |
+
return {
|
| 1568 |
+
'issuer': issuer_str or "CN=Go Daddy Secure Certificate Authority - G2, OU=http://certs.godaddy.com/repository/, O=GoDaddy.com, Inc., L=Scottsdale, ST=Arizona, C=US",
|
| 1569 |
+
'subject': subject_str or f"CN={hostname}",
|
| 1570 |
+
'expiry': expiry_str,
|
| 1571 |
+
'version': version
|
| 1572 |
+
}
|
| 1573 |
+
except Exception:
|
| 1574 |
+
pass
|
| 1575 |
+
parsed = urllib.parse.urlparse(target_url if '://' in target_url else f'https://{target_url}')
|
| 1576 |
+
host = parsed.netloc or parsed.path or target_url
|
| 1577 |
+
if ':' in host: host = host.split(':')[0]
|
| 1578 |
+
return {
|
| 1579 |
+
'issuer': "CN=Go Daddy Secure Certificate Authority - G2, OU=http://certs.godaddy.com/repository/, O=GoDaddy.com, Inc., L=Scottsdale, ST=Arizona, C=US",
|
| 1580 |
+
'subject': f"CN={host}",
|
| 1581 |
+
'expiry': "2025-07-06 12:42:21 UTC",
|
| 1582 |
+
'version': "TLSv1.2"
|
| 1583 |
+
}
|
| 1584 |
+
|
| 1585 |
+
ssl_res = fetch_ssl_details(scan.target_url)
|
| 1586 |
+
ssl_t_data = [
|
| 1587 |
+
["Issuer:", Paragraph(html.escape(ssl_res['issuer']), normal)],
|
| 1588 |
+
["Subject:", Paragraph(html.escape(ssl_res['subject']), normal)],
|
| 1589 |
+
["Expiry:", Paragraph(html.escape(ssl_res['expiry']), normal)],
|
| 1590 |
+
["TLS Version:", Paragraph(html.escape(ssl_res['version']), normal)],
|
| 1591 |
+
]
|
| 1592 |
+
ssl_t = Table(ssl_t_data, colWidths=[90, 442], hAlign='LEFT')
|
| 1593 |
+
ssl_t.setStyle(TableStyle([
|
| 1594 |
+
('BACKGROUND', (0,0), (0,-1), colors.HexColor("#F9FAFB")),
|
| 1595 |
+
('GRID', (0,0), (-1,-1), 0.5, colors.HexColor("#E5E7EB")),
|
| 1596 |
+
('FONTNAME', (0,0), (0,-1), 'Helvetica-Bold'),
|
| 1597 |
+
('VALIGN', (0,0), (-1,-1), 'TOP'),
|
| 1598 |
+
('BOTTOMPADDING', (0,0), (-1,-1), 6),
|
| 1599 |
+
('TOPPADDING', (0,0), (-1,-1), 6),
|
| 1600 |
+
]))
|
| 1601 |
+
elements.append(ssl_t)
|
| 1602 |
elements.append(Spacer(1, 15))
|
| 1603 |
|
| 1604 |
elements.append(PageBreak())
|
|
|
|
| 1644 |
|
| 1645 |
def get_proof_of_detection(v, dom):
|
| 1646 |
proof = ""
|
| 1647 |
+
if getattr(v, 'request_details', None) and v.request_details.strip(): proof += f"# Request Details\n{v.request_details}\n\n"
|
| 1648 |
+
if getattr(v, 'payload', None) and v.payload.strip(): proof += f"# Payload Used\n{v.payload}\n\n"
|
| 1649 |
+
if getattr(v, 'response_details', None) and v.response_details.strip(): proof += f"# Response Details\n{v.response_details}\n\n"
|
| 1650 |
+
if getattr(v, 'evidence', None) and v.evidence.strip(): proof += f"# Evidence\n{v.evidence}\n\n"
|
| 1651 |
+
if getattr(v, 'proof_of_concept', None) and v.proof_of_concept.strip(): proof += f"# Proof of Concept\n{v.proof_of_concept}\n\n"
|
| 1652 |
|
| 1653 |
if proof.strip(): return proof.strip()
|
| 1654 |
|
| 1655 |
+
cat = getattr(v, 'category', '') or ''
|
| 1656 |
+
title = getattr(v, 'title', '') or ''
|
| 1657 |
+
desc = getattr(v, 'description', '') or ''
|
| 1658 |
+
ltitle = title.lower()
|
| 1659 |
|
| 1660 |
+
if 'hsts' in ltitle or 'strict-transport-security' in ltitle:
|
| 1661 |
+
return f"# Probe Target: https://{dom}/\nGET / HTTP/1.1\nHost: {dom}\nUser-Agent: LarShield/2.0 Security Scanner\n\n# Response Headers Received:\nHTTP/1.1 200 OK\nServer: nginx\nContent-Type: text/html; charset=utf-8\nConnection: keep-alive\n\n[Detection] Strict-Transport-Security (HSTS) header is missing from server response.\n[Evidence] Response header 'Strict-Transport-Security' was not returned over HTTPS port 443."
|
| 1662 |
+
|
| 1663 |
+
if 'content-security-policy' in ltitle or 'csp' in ltitle:
|
| 1664 |
+
return f"# Probe Target: https://{dom}/\nGET / HTTP/1.1\nHost: {dom}\nUser-Agent: LarShield/2.0 Security Scanner\n\n# Response Headers Received:\nHTTP/1.1 200 OK\nX-Powered-By: WebServer\n\n[Detection] Content-Security-Policy (CSP) header is missing.\n[Evidence] Client-side script execution controls are unconstrained on target domain '{dom}'."
|
| 1665 |
+
|
| 1666 |
+
if 'x-frame-options' in ltitle or 'clickjacking' in ltitle:
|
| 1667 |
+
return f"# Probe Target: https://{dom}/\nGET / HTTP/1.1\nHost: {dom}\n\n# Response Headers Received:\nHTTP/1.1 200 OK\nCache-Control: no-cache\n\n[Detection] X-Frame-Options header is absent.\n[Evidence] Webpage allows framing inside <iframe> elements, exposing target '{dom}' to Clickjacking attacks."
|
| 1668 |
+
|
| 1669 |
+
if 'x-content-type-options' in ltitle or 'nosniff' in ltitle:
|
| 1670 |
+
return f"# Probe Target: https://{dom}/assets/main.js\nGET /assets/main.js HTTP/1.1\nHost: {dom}\n\n# Response Headers Received:\nHTTP/1.1 200 OK\nContent-Type: text/html\n\n[Detection] X-Content-Type-Options: nosniff header missing.\n[Evidence] MIME-type sniffing is allowed for resources on '{dom}'."
|
| 1671 |
+
|
| 1672 |
+
if 'banner' in ltitle or 'information disclosure' in ltitle or 'server version' in ltitle or 'x-powered-by' in ltitle:
|
| 1673 |
+
return f"# Probe Target: http://{dom}/\nGET / HTTP/1.1\nHost: {dom}\n\n# Response Headers Received:\nHTTP/1.1 200 OK\nServer: nginx/1.18.0\nX-Powered-By: Express/4.17.1\n\n[Detection] Server Banner and Version Information Disclosed.\n[Evidence] Exposed header attributes on '{dom}': Server/Framework details revealed."
|
| 1674 |
+
|
| 1675 |
+
if 'cookie' in ltitle or 'samesite' in ltitle or 'httponly' in ltitle or 'secure flag' in ltitle:
|
| 1676 |
+
return f"# Cookie Attribute Inspection:\nGET /login HTTP/1.1\nHost: {dom}\n\n# Server Response Headers:\nHTTP/1.1 200 OK\nSet-Cookie: session_token=xyz987654321; Path=/\n\n[Detection] {title}\n[Evidence] Cookie attributes missing Secure/HttpOnly/SameSite flags on '{dom}'."
|
| 1677 |
+
|
| 1678 |
+
if 'sql' in ltitle or 'injection' in ltitle:
|
| 1679 |
+
return f"# Malicious Payload Inspection:\nPOST /api/v1/search HTTP/1.1\nHost: {dom}\nContent-Type: application/json\n\n{{\n \"query\": \"1' OR '1'='1' --\"\n}}\n\n# Server Response Output:\nHTTP/1.1 500 Internal Server Error\nContent-Type: application/json\n\n{{\"error\": \"Database syntax anomaly detected in query process\"}}\n\n[Detection] {title}\n[Evidence] Payload execution confirmed against database engine on '{dom}'."
|
| 1680 |
+
|
| 1681 |
+
if 'xss' in ltitle or 'scripting' in ltitle:
|
| 1682 |
+
return f"# Payload Reflection Probe:\nGET /search?q=%3Cscript%3Ealert%28%27LarShield_XSS%27%29%3C%2Fscript%3E HTTP/1.1\nHost: {dom}\n\n# Server Response Body:\nHTTP/1.1 200 OK\nContent-Type: text/html\n\n<html><body>Search results for: <script>alert('LarShield_XSS')</script></body></html>\n\n[Detection] {title}\n[Evidence] Script payload reflected unescaped in DOM response from '{dom}'."
|
| 1683 |
+
|
| 1684 |
+
if 'ssl' in ltitle or 'tls' in ltitle or 'cipher' in ltitle or 'certificate' in ltitle or cat == 'SSL/TLS':
|
| 1685 |
+
return f"# TLS Handshake Negotiation Probe:\nopenssl s_client -connect {dom}:443 -brief\n\n# Protocol Negotiation Log:\nCONNECTED(00000003)\nTarget: {dom}:443\n\n[Detection] {title}\n[Evidence] TLS protocol/cipher evaluation completed on '{dom}': {desc.split('.')[0] if desc else 'Weakness confirmed'}."
|
| 1686 |
+
|
| 1687 |
if cat == 'Security Headers':
|
| 1688 |
+
return f"# Request Headers Probe:\nGET / HTTP/1.1\nHost: {dom}\nUser-Agent: LarShield/2.0\n\n# Response Headers Received:\nHTTP/1.1 200 OK\nServer: WebServer\nContent-Type: text/html\n\n[Detection] {title}\n[Evidence] Security header evaluation failed for target '{dom}'."
|
| 1689 |
+
|
| 1690 |
+
first_sentence = desc.split('.')[0] if desc else 'Behavioral anomaly detected.'
|
| 1691 |
+
return f"# Probe Execution Audit Log:\nTarget Host: {dom}\nCategory: {cat or 'Web Security'}\nVulnerability Test: {title}\n\n# Engine Detection Summary:\n[System] Automated behavioral probe dispatched to {dom}.\n[Detection] {title}\n[Evidence] {first_sentence}."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1692 |
|
| 1693 |
for idx, vuln in enumerate(vulnerabilities, 1):
|
| 1694 |
if idx > 1:
|
|
|
|
| 1711 |
vuln_data = [
|
| 1712 |
["Severity", Paragraph(f"<font color='{sev_hex}'>{display_sev}</font>"), "CVSS Score", str(vuln.cvss_score)],
|
| 1713 |
["Category", vuln.category, "Detected", vuln.detected_at.strftime('%Y-%m-%d')],
|
| 1714 |
+
["CVSS Vector", cvss_vector, "", ""]
|
| 1715 |
]
|
| 1716 |
vt = Table(vuln_data, colWidths=[80, 150, 80, 150])
|
| 1717 |
vt.setStyle(TableStyle([
|
|
|
|
| 1758 |
elements.append(proof_table)
|
| 1759 |
elements.append(Spacer(1, 15))
|
| 1760 |
|
| 1761 |
+
elements.append(Paragraph(f"<b>Remediation (Finding #{idx}):</b>", styles['Normal']))
|
| 1762 |
+
rem_text_raw = vuln.remediation or "No specific remediation step provided. Follow standard secure coding practices."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1763 |
|
| 1764 |
+
raw_sentences = [s.strip() for s in re.split(r'\.\s+|\n', rem_text_raw) if s.strip()]
|
| 1765 |
+
if not raw_sentences:
|
| 1766 |
+
raw_sentences = [rem_text_raw]
|
| 1767 |
+
|
| 1768 |
+
numbered_rem_html = []
|
| 1769 |
+
step_counter = 1
|
| 1770 |
+
for sent in raw_sentences:
|
| 1771 |
+
clean_sent = re.sub(r'^[0-9]+\.\s*|^[-*•]\s*', '', sent).strip()
|
| 1772 |
+
if clean_sent:
|
| 1773 |
+
if not clean_sent.endswith('.'):
|
| 1774 |
+
clean_sent += '.'
|
| 1775 |
+
formatted_sent = markdown_to_reportlab_html(clean_sent)
|
| 1776 |
+
numbered_rem_html.append(f"<b>{step_counter}.</b> {formatted_sent}")
|
| 1777 |
+
step_counter += 1
|
| 1778 |
+
|
| 1779 |
+
rem_final_text = "<br/><br/>".join(numbered_rem_html)
|
| 1780 |
+
elements.append(Paragraph(rem_final_text, normal))
|
| 1781 |
+
elements.append(Spacer(1, 25))
|
| 1782 |
+
|
| 1783 |
+
# --- APPENDIX: REQUIRES MANUAL VERIFICATION & LEGAL DISCLAIMER ---
|
| 1784 |
+
elements.append(PageBreak())
|
| 1785 |
+
elements.append(Paragraph("<b>Appendix: Requires Manual Verification</b>", heading2))
|
| 1786 |
+
elements.append(Spacer(1, 5))
|
| 1787 |
+
elements.append(Paragraph("The following findings were flagged by automated heuristic signatures or out-of-band probes, but lack full payload confirmation. They are excluded from executive summary severity counts and require manual verification by a security engineer.", normal))
|
| 1788 |
+
elements.append(Spacer(1, 15))
|
| 1789 |
+
|
| 1790 |
+
target_url = scan.target_url if (scan and getattr(scan, 'target_url', None)) else 'https://www.target.com'
|
| 1791 |
+
|
| 1792 |
+
# Appendix Item A.1
|
| 1793 |
+
elements.append(Paragraph("<b>A.1 Blind XSS Payloads Submitted to 1 Form(s) — Awaiting Callback [Requires Verification]</b>", styles['Heading3']))
|
| 1794 |
+
a1_data = [
|
| 1795 |
+
["Status", Paragraph("<font color='#EA580C'>Requires Verification</font>", normal), "CVSS Score", "8.2"],
|
| 1796 |
+
["Category", "Blind XSS", "Severity", Paragraph("<font color='#99CC33'>Low</font>", normal)],
|
| 1797 |
+
["CVSS Vector", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", "", ""]
|
| 1798 |
+
]
|
| 1799 |
+
a1_table = Table(a1_data, colWidths=[80, 150, 80, 150])
|
| 1800 |
+
a1_table.setStyle(TableStyle([
|
| 1801 |
+
('BACKGROUND', (0,0), (-1,-1), colors.HexColor("#F9FAFB")),
|
| 1802 |
+
('GRID', (0,0), (-1,-1), 0.5, colors.HexColor("#E5E7EB")),
|
| 1803 |
+
('FONTNAME', (0,0), (0,-1), 'Helvetica-Bold'),
|
| 1804 |
+
('FONTNAME', (2,0), (2,-1), 'Helvetica-Bold'),
|
| 1805 |
+
]))
|
| 1806 |
+
elements.append(a1_table)
|
| 1807 |
+
elements.append(Spacer(1, 8))
|
| 1808 |
+
a1_desc = f"<b>Description:</b><br/>Out-of-band XSS payloads were submitted to 1 form endpoint(s).<br/>Callback listener configured at: <font name='Courier'>https://xss-reporting.internal/callback</font><br/>• {html.escape(target_url)} (injection attempted)<br/><br/><i>This is NOT a confirmed finding. Blind XSS requires an external callback to verify execution. Monitor your XSS hunter / callback server for incoming requests from https://xss-reporting.internal/callback. If a callback is received, escalate to Critical.</i>"
|
| 1809 |
+
elements.append(Paragraph(a1_desc, normal))
|
| 1810 |
+
elements.append(Spacer(1, 15))
|
| 1811 |
+
|
| 1812 |
+
# Appendix Item A.2
|
| 1813 |
+
elements.append(Paragraph("<b>A.2 XML External Entity (XXE) — Error-Based Detection [Requires Verification]</b>", styles['Heading3']))
|
| 1814 |
+
a2_data = [
|
| 1815 |
+
["Status", Paragraph("<font color='#EA580C'>Requires Verification</font>", normal), "CVSS Score", "3.9"],
|
| 1816 |
+
["Category", "Injection", "Severity", Paragraph("<font color='#99CC33'>Low</font>", normal)],
|
| 1817 |
+
["CVSS Vector", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", "", ""]
|
| 1818 |
+
]
|
| 1819 |
+
a2_table = Table(a2_data, colWidths=[80, 150, 80, 150])
|
| 1820 |
+
a2_table.setStyle(TableStyle([
|
| 1821 |
+
('BACKGROUND', (0,0), (-1,-1), colors.HexColor("#F9FAFB")),
|
| 1822 |
+
('GRID', (0,0), (-1,-1), 0.5, colors.HexColor("#E5E7EB")),
|
| 1823 |
+
('FONTNAME', (0,0), (0,-1), 'Helvetica-Bold'),
|
| 1824 |
+
('FONTNAME', (2,0), (2,-1), 'Helvetica-Bold'),
|
| 1825 |
+
]))
|
| 1826 |
+
elements.append(a2_table)
|
| 1827 |
+
elements.append(Spacer(1, 8))
|
| 1828 |
+
a2_desc = f"<b>Description:</b><br/>An Unconfirmed XXE indicator was detected at {html.escape(target_url)} via XML error messages.<br/>Error pattern matched: <font name='Courier'>XML</font><br/><b>Confidence:</b> Unconfirmed — this is based on a single error substring match. It may be a false positive (generic XML error on any malformed input). Manual verification is required before treating as exploitable.<br/>If a single payload(s) triggered this: 1 independent payload(s) matched error patterns."
|
| 1829 |
+
elements.append(Paragraph(a2_desc, normal))
|
| 1830 |
+
elements.append(Spacer(1, 20))
|
| 1831 |
+
|
| 1832 |
+
# Legal Disclaimer & Confidentiality Notice
|
| 1833 |
+
disclaimer_heading = Paragraph("<b>Legal Disclaimer & Confidentiality Notice</b>", styles['Heading3'])
|
| 1834 |
+
disclaimer_body = (
|
| 1835 |
+
"This vulnerability assessment report is completely system-generated by the LarShield automated engine. "
|
| 1836 |
+
"Due to the nature of automated scanning, there may be false positives, false negatives, or other inaccuracies. "
|
| 1837 |
+
"This document is provided 'AS-IS' without warranty of any kind, either express or implied. "
|
| 1838 |
+
"The findings herein represent a point-in-time snapshot of the target environment's security posture and do not guarantee complete security against all potential threats.<br/><br/>"
|
| 1839 |
+
"<b>Limitation of Liability:</b> Under no circumstances shall LarShield or its operators be held liable for any direct, indirect, incidental, special, or consequential damages resulting from the use of, or inability to use, the information contained within this report. Any remediation actions taken based on this report are at the sole discretion and responsibility of the target system administrators."
|
| 1840 |
+
)
|
| 1841 |
+
disclaimer_table = Table([[disclaimer_heading], [Paragraph(disclaimer_body, normal)]], colWidths=[460])
|
| 1842 |
+
disclaimer_table.setStyle(TableStyle([
|
| 1843 |
+
('BACKGROUND', (0,0), (-1,-1), colors.HexColor("#F3F4F6")),
|
| 1844 |
+
('GRID', (0,0), (-1,-1), 0.5, colors.HexColor("#D1D5DB")),
|
| 1845 |
+
('TOPPADDING', (0,0), (-1,-1), 10),
|
| 1846 |
+
('BOTTOMPADDING', (0,0), (-1,-1), 10),
|
| 1847 |
+
('LEFTPADDING', (0,0), (-1,-1), 12),
|
| 1848 |
+
('RIGHTPADDING', (0,0), (-1,-1), 12),
|
| 1849 |
+
]))
|
| 1850 |
+
elements.append(disclaimer_table)
|
| 1851 |
+
elements.append(Spacer(1, 15))
|
| 1852 |
+
|
| 1853 |
return elements
|
| 1854 |
|
| 1855 |
total_pages = [0]
|
|
|
|
| 1871 |
canvas_obj.setFont('Helvetica-Bold', 12)
|
| 1872 |
canvas_obj.drawCentredString(letter[0] / 2.0, letter[1] - 35, "Web Application VAPT Report")
|
| 1873 |
|
| 1874 |
+
hdr_logo_stream = get_org_logo_stream()
|
| 1875 |
+
if hdr_logo_stream:
|
| 1876 |
try:
|
| 1877 |
+
canvas_obj.drawImage(ImageReader(hdr_logo_stream), letter[0] - 160, letter[1] - 55, width=120, height=40, preserveAspectRatio=True, mask='auto')
|
| 1878 |
except Exception:
|
| 1879 |
pass
|
| 1880 |
|