Skip to content
Go back

TypeScript for Pythonistas: A Guide to Building Red Team Tools

TypeScript for Pythonistas

Table of Contents

Open Table of Contents

Introduction

If you’re a Python developer wondering “Why should I learn TypeScript?”, this article is for you.

As a cybersecurity professional, I faced a problem: I wanted to build an MCP (Model Context Protocol) server so AI agents (like Claude, GPT-4, or local models) could execute security tools like nuclei and nmap.

MCP Layer

While Python is the king of scripting and AI research, TypeScript is becoming the standard for AI engineering and modern tooling. The official MCP SDK is in TypeScript, and frameworks like LangChain.js or Vercel AI SDK are pushing the boundaries of what’s possible in the browser and edge.

This article documents my journey learning TypeScript from a Python perspective, with security-focused examples.

Why TypeScript for Red Team & AI Engineering?

  1. Web Pentesting & Automation: Playwright and Puppeteer are native to Node.js. Writing complex browser automation for scraping or vulnerability scanning is often more robust in TypeScript than Selenium/Python.
  2. AI Agent Development: The JavaScript ecosystem for AI (LangChain.js, Vercel AI SDK) is huge. Building agents that run in the browser or on the edge (Cloudflare Workers) requires TS.
  3. MCP SDK: The standard protocol for LLMs to interact with external tools has first-class TypeScript support.
  4. Burp Extensions: Beyond Java, you can use TypeScript with modern frameworks.
  5. Electron Apps: Many modern pentesting tools use web technologies.
  6. Zod: Runtime data validation that outperforms Pydantic in speed and developer experience.

Beyond Browser


Basic Types: The Foundation

Python vs TypeScript: Basic Syntax

The most important difference: Python has optional type hints, TypeScript enforces them at compile time.

# Python - Type hints (optional, not enforced at runtime)
target: str = "https://example.com"
port: int = 443
is_vulnerable: bool = False

# This does NOT raise an error in Python, just a linter warning
port = "not a number"  # 😱 Runtime accepts this
// TypeScript - Required types, verified at compile time
let target: string = "https://example.com";
let port: number = 443;
let isVulnerable: boolean = false;

// This causes a COMPILE ERROR - code won't run
port = "not a number";  // ❌ Error: Type 'string' is not assignable to type 'number'

Arrays

# Python
open_ports: list[int] = [22, 80, 443]
hosts: list[str] = ["10.0.0.1", "10.0.0.2"]
// TypeScript - two equivalent forms
let openPorts: number[] = [22, 80, 443];
let hosts: Array<string> = ["10.0.0.1", "10.0.0.2"];

Interfaces: Defining Structures

In Python we use TypedDict or @dataclass. In TypeScript, we use interfaces.

Modeling a Vulnerability

# Python - with TypedDict
from typing import TypedDict, Optional, Literal

class Vulnerability(TypedDict):
    id: str
    severity: Literal["critical", "high", "medium", "low"]
    target: str
    description: str
    remediation: Optional[str]  # Optional field

# Using the definition
vuln: Vulnerability = {
    "id": "CVE-2024-1234",
    "severity": "high",
    "target": "https://example.com/login",
    "description": "SQL Injection in login form",
    # remediation is optional
}
// TypeScript - with interface
interface Vulnerability {
  id: string;
  severity: "critical" | "high" | "medium" | "low";  // Union type
  target: string;
  description: string;
  remediation?: string;  // The ? indicates optional
}

// Using the definition
const vuln: Vulnerability = {
  id: "CVE-2024-1234",
  severity: "high",
  target: "https://example.com/login",
  description: "SQL Injection in login form",
  // remediation is optional
};

Nested Interfaces for Nuclei Results

// Types for nuclei results
type Severity = "info" | "low" | "medium" | "high" | "critical";

interface TemplateInfo {
  name: string;
  author: string;
  severity: Severity;
  tags: string[];
  reference?: string[];
}

interface NucleiMatch {
  template: string;
  templateInfo: TemplateInfo;  // Nested interface
  host: string;
  matched: string;
  timestamp: Date;
  extractedResults?: string[];  // Optional array
}

// Example usage
const result: NucleiMatch = {
  template: "cves/2024/CVE-2024-1234.yaml",
  templateInfo: {
    name: "CVE-2024-1234 - SQL Injection",
    author: "security-researcher",
    severity: "high",
    tags: ["sqli", "cve", "cve2024"],
  },
  host: "https://vulnerable-site.com",
  matched: "/api/login",
  timestamp: new Date(),
};

Classes: OOP in TypeScript

Access Modifiers

Python uses convention (prefix _). TypeScript has real modifiers enforced at compilation.

# Python - Everything is public by convention
class SecurityTool:
    def __init__(self, name: str, version: str):
        self.name = name           # Public
        self._version = version    # "Private" by convention
        self.__api_key = None      # Name mangling (not truly private)
    
    def get_info(self) -> str:
        return f"{self.name} v{self._version}"
// TypeScript - Real modifiers
class SecurityTool {
  public name: string;           // Accessible from anywhere
  protected version: string;     // Only class and subclasses
  private apiKey?: string;       // Only this class

  constructor(name: string, version: string) {
    this.name = name;
    this.version = version;
  }

  getInfo(): string {
    return `${this.name} v${this.version}`;
  }
}

const tool = new SecurityTool("nuclei", "3.0.0");
console.log(tool.name);      // ✅ Works
console.log(tool.version);   // ❌ Error: 'version' is protected
console.log(tool.apiKey);    // ❌ Error: 'apiKey' is private

Constructor Shorthand (Very Useful!)

TypeScript lets you declare AND assign properties in one line:

// Long form
class Tool {
  name: string;
  version: string;
  
  constructor(name: string, version: string) {
    this.name = name;
    this.version = version;
  }
}

// Short form - Much better!
class Tool {
  constructor(
    public name: string,
    public version: string,
    private binary: string
  ) {
    // Properties are created automatically!
  }
}

Abstract Classes

# Python - ABC
from abc import ABC, abstractmethod

class OffensiveTool(ABC):
    @property
    @abstractmethod
    def tool_name(self) -> str:
        pass
    
    @abstractmethod
    async def execute(self, target: str) -> str:
        pass
    
    def validate_target(self, target: str) -> bool:
        """Concrete method shared by all"""
        import re
        url_pattern = r'^https?://[^\s]+$'
        return bool(re.match(url_pattern, target))
// TypeScript - abstract class
abstract class OffensiveTool {
  // Abstract properties
  abstract readonly toolName: string;
  abstract readonly description: string;

  // Abstract method
  abstract execute(target: string): Promise<string>;

  // Concrete method shared by all
  validateTarget(target: string): boolean {
    const urlPattern = /^https?:\/\/[^\s]+$/;
    return urlPattern.test(target);
  }
}

// Implementation
class NucleiScanner extends OffensiveTool {
  readonly toolName = "nuclei";
  readonly description = "Fast vulnerability scanner";

  async execute(target: string): Promise<string> {
    if (!this.validateTarget(target)) {
      throw new Error(`Invalid target: ${target}`);
    }
    // Simulate scan
    return JSON.stringify({ target, vulnerabilities: 3 });
  }
}

Async/Await: The Most Important Pattern

The syntax is almost identical to Python. This is one reason why the transition is so natural.

Basic Comparison

# Python
import asyncio

async def scan_target(target: str) -> str:
    print(f"Scanning {target}...")
    await asyncio.sleep(1)  # Simulate async operation
    return f"Scan complete for {target}"

# Execute
async def main():
    result = await scan_target("https://example.com")
    print(result)

asyncio.run(main())
// TypeScript
async function scanTarget(target: string): Promise<string> {
  console.log(`Scanning ${target}...`);
  await new Promise(resolve => setTimeout(resolve, 1000));
  return `Scan complete for ${target}`;
}

// Execute
async function main(): Promise<void> {
  const result = await scanTarget("https://example.com");
  console.log(result);
}

main();

Running CLI Commands (Critical for Red Team!)

# Python - subprocess
import asyncio
import subprocess

async def run_command(command: str) -> dict:
    process = await asyncio.create_subprocess_shell(
        command,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    stdout, stderr = await process.communicate()
    
    return {
        "stdout": stdout.decode(),
        "stderr": stderr.decode(),
        "exit_code": process.returncode
    }

# Example: nuclei
result = await run_command("nuclei -u https://example.com -json")
// TypeScript - child_process
import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

interface CommandResult {
  stdout: string;
  stderr: string;
  exitCode: number;
}

async function runCommand(command: string): Promise<CommandResult> {
  try {
    const { stdout, stderr } = await execAsync(command, {
      timeout: 30000,
      maxBuffer: 1024 * 1024,
    });
    return { stdout, stderr, exitCode: 0 };
  } catch (error: unknown) {
    const execError = error as { stdout?: string; stderr?: string; code?: number };
    return {
      stdout: execError.stdout || "",
      stderr: execError.stderr || String(error),
      exitCode: execError.code || 1,
    };
  }
}

// Example: nuclei
const result = await runCommand("nuclei -u https://example.com -json");

Parallel vs Sequential Execution

# Python - asyncio.gather for parallelism
async def parallel_scans():
    results = await asyncio.gather(
        scan_target("target1.com"),
        scan_target("target2.com"),
        scan_target("target3.com"),
    )
    return results
// TypeScript - Promise.all for parallelism
async function parallelScans(): Promise<string[]> {
  const results = await Promise.all([
    scanTarget("target1.com"),
    scanTarget("target2.com"),
    scanTarget("target3.com"),
  ]);
  return results;
}

// Promise.allSettled - continues even if some fail
const results = await Promise.allSettled([
  scanTarget("target1.com"),  // Success
  scanTarget("target2.com"),  // Fails
  scanTarget("target3.com"),  // Success
]);

results.forEach((result, i) => {
  if (result.status === "fulfilled") {
    console.log(`Scan ${i}: ✅ ${result.value}`);
  } else {
    console.log(`Scan ${i}: ❌ ${result.reason}`);
  }
});

Error Handling

Custom Error Classes

# Python
class SecurityToolError(Exception):
    def __init__(self, message: str, tool_name: str, exit_code: int = None):
        super().__init__(message)
        self.tool_name = tool_name
        self.exit_code = exit_code

class TargetValidationError(Exception):
    def __init__(self, target: str, reason: str):
        super().__init__(f'Invalid target "{target}": {reason}')
        self.target = target
        self.reason = reason
// TypeScript
class SecurityToolError extends Error {
  constructor(
    message: string,
    public toolName: string,
    public exitCode?: number
  ) {
    super(message);
    this.name = "SecurityToolError";
  }
}

class TargetValidationError extends Error {
  constructor(
    public target: string,
    public reason: string
  ) {
    super(`Invalid target "${target}": ${reason}`);
    this.name = "TargetValidationError";
  }
}

The Result Pattern (Alternative to Exceptions)

This pattern, inspired by Rust, makes errors explicit in the return type.

// Define Result type
type Result<T, E = Error> = 
  | { success: true; data: T }
  | { success: false; error: E };

// Helpers
function ok<T>(data: T): Result<T, never> {
  return { success: true, data };
}

function err<E>(error: E): Result<never, E> {
  return { success: false, error };
}

// Usage in target validation
function validateTarget(target: string): Result<string, TargetValidationError> {
  const allowedDomains = ["testsite.local", "lab.internal"];
  
  try {
    const url = new URL(target);
    const isAllowed = allowedDomains.some(d => 
      url.hostname === d || url.hostname.endsWith(`.${d}`)
    );
    
    if (!isAllowed) {
      return err(new TargetValidationError(target, "Not in allowed list"));
    }
    return ok(target);
  } catch {
    return err(new TargetValidationError(target, "Invalid URL format"));
  }
}

// Caller MUST handle both cases
const result = validateTarget("https://evil.com");
if (result.success) {
  console.log(`Valid: ${result.data}`);
} else {
  console.log(`Invalid: ${result.error.reason}`);
}

Zod: Runtime Validation (Like Pydantic)

Zod is the most popular validation library in TypeScript. It’s similar to Pydantic in Python.

Why Zod

Why Do We Need Runtime Validation?

TypeScript types disappear after compilation. If you receive data from an API, CLI, or user, TypeScript can’t protect you.

import { z } from "zod";

// Define schema (like Pydantic models)
const NucleiOptionsSchema = z.object({
  target: z.string()
    .url("Must be a valid URL"),
  
  severity: z.array(
    z.enum(["info", "low", "medium", "high", "critical"])
  ).optional(),
  
  rateLimit: z.number()
    .min(1)
    .max(1000)
    .default(150),
  
  timeout: z.number()
    .positive()
    .default(30),
});

// Extract TypeScript type from schema 🎉
type NucleiOptions = z.infer<typeof NucleiOptionsSchema>;

// Validate data at runtime
const rawInput = { target: "https://example.com", severity: ["high"] };
const result = NucleiOptionsSchema.safeParse(rawInput);

if (result.success) {
  // result.data has type NucleiOptions
  console.log(`Scanning ${result.data.target} with rate limit ${result.data.rateLimit}`);
} else {
  console.log("Validation failed:", result.error.errors);
}

Zod vs Pydantic

# Python - Pydantic
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum

class Severity(str, Enum):
    info = "info"
    low = "low"
    medium = "medium"
    high = "high"
    critical = "critical"

class NucleiOptions(BaseModel):
    target: str = Field(..., pattern=r'^https?://')
    severity: Optional[list[Severity]] = None
    rate_limit: int = Field(default=150, ge=1, le=1000)
    timeout: int = Field(default=30, gt=0)

# Validate
options = NucleiOptions(target="https://example.com", severity=["high"])
// TypeScript - Zod
import { z } from "zod";

const NucleiOptionsSchema = z.object({
  target: z.string().url(),
  severity: z.array(z.enum(["info", "low", "medium", "high", "critical"])).optional(),
  rateLimit: z.number().min(1).max(1000).default(150),
  timeout: z.number().positive().default(30),
});

type NucleiOptions = z.infer<typeof NucleiOptionsSchema>;

// Validate
const options = NucleiOptionsSchema.parse({
  target: "https://example.com",
  severity: ["high"],
});

Module System

Python vs TypeScript

# Python - __init__.py and imports
# src/types/__init__.py
from .vulnerability import Vulnerability, Severity
from .scan_result import ScanResult

# src/tools/__init__.py  
from .nuclei import scan_with_nuclei
from .nmap import scan_with_nmap

# main.py
from src.types import Vulnerability, ScanResult
from src.tools import scan_with_nuclei, scan_with_nmap
// TypeScript - index.ts (barrel exports)
// src/types/index.ts
export { Vulnerability, Severity } from "./vulnerability.js";
export { ScanResult } from "./scanResult.js";

// src/tools/index.ts
export { scanWithNuclei } from "./nuclei.js";
export { scanWithNmap } from "./nmap.js";

// main.ts
import { Vulnerability, ScanResult } from "./types/index.js";
import { scanWithNuclei, scanWithNmap } from "./tools/index.js";

Advanced Patterns for Security Tools

Rate Limiter

class RateLimiter {
  private requests: number[] = [];

  constructor(
    private maxRequests: number,
    private windowMs: number
  ) {}

  canMakeRequest(): boolean {
    const now = Date.now();
    this.requests = this.requests.filter(t => t > now - this.windowMs);
    return this.requests.length < this.maxRequests;
  }

  recordRequest(): boolean {
    if (!this.canMakeRequest()) return false;
    this.requests.push(Date.now());
    return true;
  }
}

// Usage
const limiter = new RateLimiter(100, 60000); // 100 req/min
if (limiter.recordRequest()) {
  await runScan(target);
} else {
  console.log("Rate limit exceeded");
}

Audit Logger

interface AuditEntry {
  id: string;
  timestamp: Date;
  action: "tool_invoked" | "scan_started" | "scan_completed";
  tool?: string;
  target?: string;
  user?: string;
  result?: "success" | "failure";
}

class AuditLogger {
  private entries: AuditEntry[] = [];

  log(action: AuditEntry["action"], details: Partial<AuditEntry>): void {
    const entry: AuditEntry = {
      id: `audit-${Date.now()}`,
      timestamp: new Date(),
      action,
      ...details,
    };
    this.entries.push(entry);
    console.error("[AUDIT]", JSON.stringify(entry));
  }
}

// Usage
const audit = new AuditLogger();
audit.log("scan_started", { tool: "nuclei", target: "https://test.local", user: "claude" });

Best Practices

1. Use strict: true in tsconfig.json

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

2. Avoid any - Use unknown

// ❌ Bad
function process(data: any): void {
  console.log(data.name); // No verification
}

// ✅ Good
function process(data: unknown): void {
  if (typeof data === "object" && data !== null && "name" in data) {
    console.log((data as { name: string }).name);
  }
}

3. Use Nullish Coalescing and Optional Chaining

// Nullish coalescing (??) - only uses default if null/undefined
const port = userPort ?? 443;

// Optional chaining (?.) - safe access to nested properties
const userName = response?.data?.user?.name ?? "Anonymous";

4. Literal Types with as const

const SEVERITIES = ["info", "low", "medium", "high", "critical"] as const;
type Severity = typeof SEVERITIES[number]; // "info" | "low" | ...

Essential Libraries

LibraryUse CasePython Equivalent
zodRuntime validationpydantic
winstonStructured logginglogging + structlog
chalkTerminal colorscolorama + rich
commanderCLI parsingargparse + click
execaExecute commandssubprocess
p-limitLimited concurrencyasyncio.Semaphore
nanoidUnique IDsuuid + secrets

Conclusion

TypeScript is an excellent choice for Python developers who want to:

  1. Develop for JavaScript/Node ecosystems (MCP, Electron, extensions)
  2. Stricter types than Python’s optional type hints
  3. Dual validation (compilation + runtime with Zod)
  4. V8 native performance for CLI tools

The learning curve is gentle if you already know:

Next step: build an MCP server that lets any LLM execute security tools. Stay tuned!


Resources


About this series: This article is part of a learning project building an MCP server for red team tools. Future posts will cover the complete redteam-mcp implementation with nuclei and nmap.


Share this post on:

Previous Post
Engineering Security ML with Elastic – Part 1: From Logs to Features
Next Post
Model Context Protocol (MCP): Bridging the Gap Between AI and External Systems