larxius commited on
Commit
8d2ab49
·
verified ·
1 Parent(s): 353996e

Update backend_structured/scanners_core.py

Browse files
Files changed (1) hide show
  1. backend_structured/scanners_core.py +443 -155
backend_structured/scanners_core.py CHANGED
@@ -1,11 +1,47 @@
1
  import sys
2
  import os
3
- sys.path.insert(0, os.path.abspath('backend'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Blueprint, request, jsonify, current_app, send_from_directory
 
 
 
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 functools import wraps
37
- from markupsafe import escape # always available with Flask
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 SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, Image, Flowable
 
 
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 Image object that strictly preserves original aspect ratio.
 
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 Image(img_source, width=max_width, height=max_height, kind='proportional', hAlign=hAlign)
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 Image(img_source, width=calc_w, height=calc_h, kind='proportional', hAlign=hAlign)
965
  except Exception:
966
- return Image(img_source, width=max_width, height=max_height, kind='proportional', hAlign=hAlign)
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
- org_logo = None
999
- if scan.org_id:
1000
- org = db.session.get(Organization, scan.org_id)
1001
- if org:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1002
  org_name = org.name
1003
- if org.report_logo_url:
 
 
 
 
1004
  try:
1005
- resp = requests.get(org.report_logo_url, timeout=5)
1006
- if resp.status_code == 200:
1007
- org_logo = io.BytesIO(resp.content)
1008
- except Exception:
1009
- pass
1010
-
1011
- logo_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'frontend', 'public', 'logoo.png'))
1012
- has_local_logo = os.path.exists(logo_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- if has_local_logo:
 
 
 
 
 
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
- if has_local_logo:
 
 
 
 
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=[150, 10, 300], hAlign='LEFT')
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:", normal))
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=[80, 80, 80, 80, 80], hAlign='LEFT')
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
- if not is_ssl:
1377
- elements.append(Paragraph("Performed tests", heading2))
1378
- elements.append(Paragraph("<bullet>&bull;</bullet>All set of applicable OWASP Top 10 Security Threats", bullet_style))
1379
- if is_full:
1380
- elements.append(Paragraph("<bullet>&bull;</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>&bull;</bullet>Penetration Testing Execution Standard", bullet_style))
1406
- elements.append(Paragraph("<bullet>&bull;</bullet>OWASP Top 10 Application Security Risks - 2017", bullet_style))
1407
- elements.append(Paragraph("<bullet>&bull;</bullet>OWASP Testing Guide", bullet_style))
1408
- elements.append(Paragraph("<bullet>&bull;</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
- ssl_info = getattr(scan, 'ssl_info', None) or get_ssl_info(scan.target_url)
1416
- if ssl_info:
1417
- elements.append(Paragraph(f"<b>Issuer:</b> {html.escape(str(ssl_info.get('issuer', 'Unknown') or 'Unknown'))}", normal))
1418
- elements.append(Paragraph(f"<b>Subject:</b> {html.escape(str(ssl_info.get('subject', 'Unknown') or 'Unknown'))}", normal))
1419
- elements.append(Paragraph(f"<b>Expiry:</b> {html.escape(str(ssl_info.get('expiry', 'Unknown') or 'Unknown'))}", normal))
1420
- elements.append(Paragraph(f"<b>TLS Version:</b> {html.escape(str(ssl_info.get('version', 'Unknown') or 'Unknown'))}", normal))
1421
- else:
1422
- elements.append(Paragraph("Could not retrieve SSL certificate details.", normal))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Analysis\nHTTP/1.1 200 OK\nServer: nginx\nContent-Type: text/html\n... [snip] ...\n\n[Detection] {title}\nMissing or misconfigured attribute in server response."
1481
- if cat == 'SSL/TLS':
1482
- return f"# TLS Handshake Probe\nopenssl s_client -connect {dom}:443 -tls1_2\n\n# Protocol Analysis\nCONNECTED(00000003)\n[Detection] {title}\nCertificate or protocol weakness verified during handshake negotiation."
1483
- if 'SQL' in title or cat == 'Injection':
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
- if org_logo:
1587
- org_logo.seek(0)
1588
  try:
1589
- canvas_obj.drawImage(ImageReader(org_logo), letter[0] - 160, letter[1] - 55, width=120, height=40, preserveAspectRatio=True, mask='auto')
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("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>&bull;</bullet>All set of applicable OWASP Top 10 Security Threats", bullet_style))
1497
+ elements.append(Paragraph("<bullet>&bull;</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>&bull;</bullet>Penetration Testing Execution Standard", bullet_style))
1527
+ elements.append(Paragraph("<bullet>&bull;</bullet>OWASP Top 10 Application Security Risks - 2017", bullet_style))
1528
+ elements.append(Paragraph("<bullet>&bull;</bullet>OWASP Testing Guide", bullet_style))
1529
+ elements.append(Paragraph("<bullet>&bull;</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/>&bull; {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 &mdash; 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