Lucene search
+L

KittyReport Generator

🗓️ 27 Apr 2026 10:51:54Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 1 Views

KittyReport Generator generates a professional PDF report of security assessment findings.

Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from kittysploit import *
from core.models.models import Host, Service, Vulnerability, Credential, Note, Loot
import os
from datetime import datetime

# ReportLab imports
try:
    from reportlab.lib.pagesizes import A4
    from reportlab.lib import colors
    from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
    from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, Image, HRFlowable
    from reportlab.lib.units import inch
    from reportlab.lib.enums import TA_CENTER, TA_LEFT
    REPORTLAB_AVAILABLE = True
except ImportError:
    REPORTLAB_AVAILABLE = False

def _c2_timeline_rows(framework, limit: int = 40):
    """Best-effort C2 ops timeline for the PDF report."""
    try:
        from lib.c2.ops_log import get_ops_log

        return get_ops_log(framework).timeline(limit=limit)
    except Exception:
        return []


class Module(Analysis):
    """KittyReport Generator - Professional PDF reporting engine."""
    
    __info__ = {
        'name': 'KittyReport Generator',
        'description': 'Generates a professional, high-design PDF report of all findings (Hosts, Vulns, Creds, Loot).',
        'author': ['KittySploit Team'],
        'license': 'MIT',
        'type': 'analysis',
        'dependencies': ['reportlab']
    }

    FILENAME = OptString("penetration_test_report.pdf", description="Output filename", required=True)
    WORKSPACE = OptString("default", description="Workspace to report", required=True)
    TITLE = OptString("Penetration Test Report", description="Report title", required=True)
    SUBTITLE = OptString("Security Assessment Findings", description="Report subtitle", required=True)
    AUTHOR = OptString("KittySploit Offensive Team", description="Report author", required=True)

    def run(self):
        if not REPORTLAB_AVAILABLE:
            print_error("ReportLab is not installed. Please install it with 'pip install reportlab'.")
            return False

        workspace_name = self.WORKSPACE
        filename = self.FILENAME
        
        if not filename.endswith('.pdf'):
            filename += '.pdf'
        
        output_dir = os.path.join(os.getcwd(), "output", "reports")
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)
        
        full_path = os.path.join(output_dir, filename)
        
        print_status(f"Generating professional report for workspace: {workspace_name}")
        
        try:
            with self.framework.db_manager.get_db_session(workspace_name) as s:
                # Query data
                hosts = s.query(Host).all()
                creds = s.query(Credential).all()
                vulns = s.query(Vulnerability).all()
                loots = s.query(Loot).all()
                notes = s.query(Note).all()

                # Start PDF Generation
                doc = SimpleDocTemplate(full_path, pagesize=A4, rightMargin=50, leftMargin=50, topMargin=50, bottomMargin=50)
                styles = getSampleStyleSheet()
                
                # Custom Styles
                title_style = ParagraphStyle('TitleStyle', parent=styles['Title'], fontSize=28, textColor=colors.HexColor('#2c3e50'), spaceAfter=10, alignment=TA_CENTER)
                subtitle_style = ParagraphStyle('SubtitleStyle', parent=styles['Normal'], fontSize=16, textColor=colors.HexColor('#7f8c8d'), spaceAfter=30, alignment=TA_CENTER)
                heading1_style = ParagraphStyle('Heading1Style', parent=styles['Heading1'], fontSize=20, textColor=colors.HexColor('#2980b9'), spaceBefore=20, spaceAfter=15, borderPadding=10)
                heading2_style = ParagraphStyle('Heading2Style', parent=styles['Heading2'], fontSize=14, textColor=colors.HexColor('#2c3e50'), spaceBefore=15, spaceAfter=10)
                normal_style = styles['Normal']
                
                elements = []

                # --- Cover Page ---
                logo_path = os.path.join(os.getcwd(), 'static', 'logo.jpg')
                if os.path.exists(logo_path):
                    elements.append(Image(logo_path, width=2*inch, height=2*inch))
                
                elements.append(Spacer(1, 1.5*inch))
                elements.append(Paragraph(self.TITLE, title_style))
                elements.append(Paragraph(self.SUBTITLE, subtitle_style))
                elements.append(Spacer(1, 1*inch))
                
                # Info Box
                info_data = [
                    ["Workspace:", workspace_name],
                    ["Date:", datetime.now().strftime('%B %d, %Y')],
                    ["Time:", datetime.now().strftime('%H:%M:%S')],
                    ["Assessed by:", self.AUTHOR]
                ]
                it = Table(info_data, colWidths=[1.5*inch, 3*inch])
                it.setStyle(TableStyle([
                    ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
                    ('FONTSIZE', (0, 0), (-1, -1), 12),
                    ('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#2980b9')),
                    ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
                    ('LINEBELOW', (0, 0), (-1, -1), 0.5, colors.HexColor('#ecf0f1')),
                    ('BOTTOMPADDING', (0, 0), (-1, -1), 10),
                    ('TOPPADDING', (0, 0), (-1, -1), 10),
                ]))
                elements.append(it)
                elements.append(PageBreak())

                # --- Executive Summary ---
                elements.append(Paragraph("Executive Summary", heading1_style))
                elements.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor('#2980b9'), spaceAfter=20))
                
                summary_text = f"This report outlines the security assessment findings for the <b>{workspace_name}</b> environment. " \
                               f"During the assessment, a total of <b>{len(hosts)}</b> hosts were identified, with <b>{len(vulns)}</b> security vulnerabilities discovered."
                elements.append(Paragraph(summary_text, normal_style))
                elements.append(Spacer(1, 0.3*inch))

                # Risk Distribution Table
                if vulns:
                    risk_counts = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0, 'unknown': 0}
                    for v in vulns:
                        level = v.risk_level.lower() if v.risk_level else 'unknown'
                        risk_counts[level] = risk_counts.get(level, 0) + 1
                    
                    risk_data = [
                        ["Critical", "High", "Medium", "Low"],
                        [str(risk_counts['critical']), str(risk_counts['high']), str(risk_counts['medium']), str(risk_counts['low'])]
                    ]
                    rt = Table(risk_data, colWidths=[1.2*inch]*4)
                    rt.setStyle(TableStyle([
                        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
                        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
                        ('FONTSIZE', (0, 0), (-1, -1), 14),
                        ('BACKGROUND', (0, 0), (0, 0), colors.HexColor('#c0392b')), # Critical
                        ('BACKGROUND', (1, 0), (1, 0), colors.HexColor('#e67e22')), # High
                        ('BACKGROUND', (2, 0), (2, 0), colors.HexColor('#f1c40f')), # Medium
                        ('BACKGROUND', (3, 0), (3, 0), colors.HexColor('#3498db')), # Low
                        ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
                        ('GRID', (0, 0), (-1, -1), 1, colors.white),
                        ('BOTTOMPADDING', (0, 0), (-1, -1), 10),
                        ('TOPPADDING', (0, 0), (-1, -1), 10),
                    ]))
                    elements.append(rt)
                
                elements.append(Spacer(1, 0.5*inch))

                # --- Section 1: Asset Inventory ---
                elements.append(Paragraph("1. Asset Inventory", heading1_style))
                if not hosts:
                    elements.append(Paragraph("No assets discovered in this workspace.", normal_style))
                else:
                    host_data = [["IP Address", "Hostname", "Operating System", "Status"]]
                    for h in hosts:
                        host_data.append([h.address, h.hostname or "-", h.os or "N/A", h.status.upper()])
                    
                    ht = Table(host_data, colWidths=[1.5*inch, 1.5*inch, 1.5*inch, 1*inch], repeatRows=1)
                    ht.setStyle(TableStyle([
                        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
                        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
                        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
                        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
                        ('FONTSIZE', (0, 0), (-1, -1), 10),
                        ('BACKGROUND', (0, 1), (-1, -1), colors.HexColor('#f9f9f9')),
                        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f2f2f2')]),
                        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#bdc3c7'))
                    ]))
                    elements.append(ht)
                
                elements.append(PageBreak())

                # --- Section 2: Security Findings ---
                elements.append(Paragraph("2. Security Findings", heading1_style))
                if not vulns:
                    elements.append(Paragraph("Great! No security vulnerabilities were recorded for this assessment.", normal_style))
                else:
                    for i, v in enumerate(vulns):
                        # Severity Badge
                        sev = v.risk_level.lower() if v.risk_level else 'unknown'
                        sev_color = colors.HexColor('#95a5a6')
                        if sev == 'critical': sev_color = colors.HexColor('#c0392b')
                        elif sev == 'high': sev_color = colors.HexColor('#e67e22')
                        elif sev == 'medium': sev_color = colors.HexColor('#f1c40f')
                        elif sev == 'low': sev_color = colors.HexColor('#3498db')

                        elements.append(Paragraph(f"Finding #{i+1}: {v.name}", heading2_style))
                        
                        # Info Table for the finding
                        finding_info = [
                            ["Severity:", Paragraph(f"<font color='{sev_color}'><b>{sev.upper()}</b></font>", normal_style)],
                            ["CVE:", v.cve or "N/A"],
                            ["CVSS:", v.cvss_score or "N/A"]
                        ]
                        fit = Table(finding_info, colWidths=[1.5*inch, 4*inch])
                        fit.setStyle(TableStyle([
                            ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
                            ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#ecf0f1')),
                        ]))
                        elements.append(fit)
                        elements.append(Spacer(1, 0.1*inch))
                        
                        elements.append(Paragraph("<b>Description:</b>", normal_style))
                        elements.append(Paragraph(v.description or "No description provided.", normal_style))
                        elements.append(Spacer(1, 0.1*inch))

                        if v.proof_of_concept:
                            elements.append(Paragraph("<b>Evidence / Proof of Concept:</b>", normal_style))
                            poc_text = str(v.proof_of_concept).replace("\n", "<br/>")
                            # Escape XML-sensitive chars for ReportLab Paragraph
                            poc_text = (
                                poc_text.replace("&", "&amp;")
                                .replace("<br/>", "___BR___")
                                .replace("<", "&lt;")
                                .replace(">", "&gt;")
                                .replace("___BR___", "<br/>")
                            )
                            elements.append(Paragraph(poc_text[:3500], normal_style))
                            elements.append(Spacer(1, 0.1*inch))
                        
                        if v.remediation:
                            elements.append(Paragraph("<b>Recommendation:</b>", normal_style))
                            elements.append(Paragraph(v.remediation, normal_style))
                        
                        elements.append(Spacer(1, 0.3*inch))
                        if (i + 1) % 2 == 0: elements.append(PageBreak()) # Break every 2 findings to keep it clean

                # --- Section 3: Compromised Credentials ---
                if elements[-1] != PageBreak(): elements.append(PageBreak())
                elements.append(Paragraph("3. Compromised Credentials", heading1_style))
                if not creds:
                    elements.append(Paragraph("No credentials were recovered during this assessment.", normal_style))
                else:
                    cred_data = [["Username", "Secret (Password/Hash)", "Type", "Source"]]
                    for c in creds:
                        pwd = c.password or c.password_hash or "********"
                        cred_data.append([c.username or "Unknown", pwd, c.hash_type or "plain", c.source or "N/A"])
                    
                    ct = Table(cred_data, colWidths=[1.5*inch, 2*inch, 1*inch, 1.5*inch], repeatRows=1)
                    ct.setStyle(TableStyle([
                        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
                        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
                        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
                        ('FONTSIZE', (0, 0), (-1, -1), 9),
                        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f2f2f2')]),
                        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#bdc3c7')),
                        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
                    ]))
                    elements.append(ct)

                # --- Section 4: Exfiltrated Data (Loot) ---
                elements.append(Spacer(1, 0.5*inch))
                elements.append(Paragraph("4. Exfiltrated Data (Loot)", heading1_style))
                if not loots:
                    elements.append(Paragraph("No loot items were collected.", normal_style))
                else:
                    loot_data = [["Item Name", "Loot Type", "Size", "Acquired Date"]]
                    for l in loots:
                        size = l.get_file_size_human() if hasattr(l, 'get_file_size_human') else "N/A"
                        loot_data.append([l.name, l.loot_type, size, l.created_at.strftime('%Y-%m-%d')])
                    
                    lt = Table(loot_data, colWidths=[1.8*inch, 1.4*inch, 0.8*inch, 2*inch], repeatRows=1)
                    lt.setStyle(TableStyle([
                        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
                        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
                        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
                        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#bdc3c7'))
                    ]))
                    elements.append(lt)

                    evidence_loots = [l for l in loots if str(l.loot_type or "").lower() == "evidence"]
                    if evidence_loots:
                        elements.append(Spacer(1, 0.3*inch))
                        elements.append(Paragraph("4.1 Scanner Evidence Details", heading2_style))
                        for el in evidence_loots[:25]:
                            elements.append(Paragraph(f"<b>{el.name}</b>", normal_style))
                            if el.file_path:
                                elements.append(Paragraph(f"Path: {el.file_path}", normal_style))
                            preview = ""
                            if el.content:
                                preview = str(el.content)[:1200]
                            if preview:
                                safe = (
                                    preview.replace("&", "&amp;")
                                    .replace("<", "&lt;")
                                    .replace(">", "&gt;")
                                    .replace("\n", "<br/>")
                                )
                                elements.append(Paragraph(safe, normal_style))
                            elements.append(Spacer(1, 0.15*inch))

                # --- Section 5: C2 Activity Timeline ---
                elements.append(Spacer(1, 0.5*inch))
                elements.append(Paragraph("5. C2 Activity Timeline", heading1_style))
                c2_rows = _c2_timeline_rows(self.framework, limit=40)
                if not c2_rows:
                    elements.append(Paragraph(
                        "No C2 implant tasks were recorded for this workspace "
                        "(HTTP polling ops log empty).",
                        normal_style,
                    ))
                else:
                    elements.append(Paragraph(
                        f"Recent implant task activity ({len(c2_rows)} entries).",
                        normal_style,
                    ))
                    elements.append(Spacer(1, 0.15*inch))
                    c2_data = [["When", "Status", "Implant", "Operator", "Command"]]
                    for r in c2_rows:
                        when = str(r.get("created_at") or "")[:19].replace("T", " ")
                        cmd = str(r.get("command") or "").replace("\n", " ")
                        if len(cmd) > 60:
                            cmd = cmd[:57] + "..."
                        implant = str(r.get("implant_id") or r.get("session_id") or "")
                        if len(implant) > 18:
                            implant = implant[:16] + ".."
                        c2_data.append([
                            when,
                            str(r.get("status") or ""),
                            implant,
                            str(r.get("operator") or ""),
                            cmd,
                        ])
                    c2t = Table(c2_data, colWidths=[1.3*inch, 0.8*inch, 1.2*inch, 0.9*inch, 2.0*inch], repeatRows=1)
                    c2t.setStyle(TableStyle([
                        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
                        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
                        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
                        ('FONTSIZE', (0, 0), (-1, -1), 8),
                        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f2f2f2')]),
                        ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#bdc3c7')),
                        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
                    ]))
                    elements.append(c2t)

                # Finalize
                doc.build(elements)
                print_success(f"High-design PDF report successfully generated: {full_path}")
                return True

        except Exception as e:
            print_error(f"Error during report generation: {str(e)}")
            import traceback
            print_debug(traceback.format_exc())
            return False

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

30 Jul 2026 05:02Current
5.8Medium risk
Vulners AI Score5.8
1