← Back to Blog

Pipelines for Penetration Testing

January 15, 2026 nullsecpipelinespentesting

Penetration testing is inherently pipeline-shaped: reconnaissance flows into enumeration, enumeration into exploitation, exploitation into persistence. NullSec Linux uses Lateralus pipelines to chain security tools into automated, repeatable workflows.

◉ The Problem with Manual Testing

A typical recon workflow:

  1. Run subdomain enumeration (amass, subfinder)
  2. Resolve DNS for discovered hosts
  3. Port scan live hosts (nmap, masscan)
  4. Screenshot web services
  5. Run vulnerability scanners (nuclei)
  6. Compile findings into report

Each step requires manual intervention, format conversion, and context switching. Miss a step or misconfigure a tool, and you miss findings.

◉ Pipeline Approach

Express the same workflow as data flow:

import security.{amass, subfinder, nmap, gowitness, nuclei}

fn full_recon(target: str) -> Report {
    target
        |> discover_subdomains()
        |> resolve_dns()
        |> scan_ports()
        |> identify_web_services()
        |> screenshot_all()
        |> scan_vulnerabilities()
        |> generate_report()
}

Benefits:

◉ Tool Wrappers

NullSec includes Lateralus wrappers for 250+ tools. Each wrapper:

// security.nmap wrapper
pub fn scan(target: str, opts: ScanOptions = default()) -> [Host] {
    let args = build_args(target, opts)
    let xml = run_nmap(args)
    parse_nmap_xml(xml)
}

pub struct Host {
    ip: str
    hostname: Option[str]
    ports: [Port]
    os: Option[str]
}

pub struct Port {
    number: u16
    protocol: str
    state: str
    service: str
    version: Option[str]
}

◉ Reconnaissance Pipeline

import security.{amass, subfinder, httpx, nmap}

fn recon(domain: str) -> ReconReport {
    // Parallel subdomain enumeration
    let subs = [
        amass.enum(domain, passive: true),
        subfinder.scan(domain),
    ] |> flatten() |> unique()

    // DNS resolution and HTTP probe
    let live = subs
        |> httpx.probe(ports: [80, 443, 8080, 8443])
        |> filter(_.is_live)

    // Port scanning (only live hosts)
    let hosts = live
        |> map(_.ip)
        |> unique()
        |> nmap.scan_batch(ports: "1-10000", timing: 4)

    ReconReport {
        domain: domain,
        subdomains: subs,
        live_hosts: live,
        port_data: hosts,
        timestamp: now()
    }
}

◉ Web Application Pipeline

import security.{burp, sqlmap, nuclei, ffuf}

fn webapp_scan(url: str) -> WebAppReport {
    // Directory enumeration
    let dirs = ffuf.fuzz(url, wordlist: "/usr/share/wordlists/common.txt")

    // Parameter discovery
    let params = burp.crawl(url, depth: 3)
        |> extract_params()

    // SQL injection testing
    let sqli = params
        |> filter(_.injectable_looking)
        |> map(fn(p) { sqlmap.test(p.url, p.param, level: 2) })
        |> filter(_.vulnerable)

    // Template-based scanning
    let vulns = nuclei.scan(url, templates: ["cves", "exposures", "misconfigs"])

    WebAppReport {
        url: url,
        directories: dirs,
        parameters: params,
        sqli_findings: sqli,
        vulnerabilities: vulns
    }
}

◉ Credential Attacks Pipeline

import security.{hydra, hashcat, secretsdump}
import data.wordlists

fn credential_attack(target: str, users: [str]) -> CredReport {
    // Password spraying (careful with lockouts)
    let spray_results = users
        |> hydra.spray(target,
            passwords: ["Summer2024!", "Welcome1"],
            service: "smb",
            threads: 1,  // Slow to avoid lockout
            delay: 30
        )

    // Dump hashes from successful logins
    let hashes = spray_results
        |> filter(_.success)
        |> map(fn(cred) {
            secretsdump.dump(target, cred.user, cred.pass)
        })
        |> flatten()

    // Crack recovered hashes
    let cracked = hashes
        |> hashcat.crack(
            mode: 1000,  // NTLM
            wordlist: wordlists.rockyou,
            rules: "best64"
        )

    CredReport {
        sprayed: spray_results,
        hashes: hashes,
        cracked: cracked
    }
}

◉ Reporting Pipeline

import report.{markdown, html, pdf}

fn generate_report(findings: [Finding]) -> Report {
    let sorted = findings
        |> sort_by(_.severity, descending: true)
        |> group_by(_.category)

    let sections = sorted
        |> map(fn((cat, items)) {
            Section {
                title: cat,
                findings: items |> map(format_finding),
                remediation: get_remediation(cat)
            }
        })

    let report = Report {
        title: "Penetration Test Report",
        date: now(),
        scope: findings |> map(_.target) |> unique(),
        executive_summary: summarize(sorted),
        sections: sections,
        appendices: generate_appendices(findings)
    }

    // Output in multiple formats
    report |> markdown.render() |> write("report.md")
    report |> html.render() |> write("report.html")
    report |> pdf.render() |> write("report.pdf")

    report
}

◉ Running Pipelines

NullSec includes CLI integration:

# Run recon pipeline
nullsec-pipeline recon example.com

# Run with custom output directory
nullsec-pipeline webapp https://target.com --output ./results

# Dry run (show what would execute)
nullsec-pipeline full-engagement target.com --dry-run

# Generate pipeline template
nullsec-pipeline new internal-pentest > engagement.ltl

◉ Scheduling and Monitoring

// Run continuous monitoring
fn continuous_monitor(targets: [str]) {
    loop {
        for target in targets {
            let report = quick_recon(target)

            // Compare to previous
            let prev = db.get_latest(target)
            let diff = compare_reports(prev, report)

            if diff.has_changes {
                alert.send("Changes detected on " + target, diff)
            }

            db.save(target, report)
        }

        sleep(hours(24))
    }
}

◉ Pipeline Library

NullSec ships with ready-to-use pipelines:

All pipelines at nullsec-pipelines. Tool wrappers at security.ltl.