> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

OXO: Building Multi-Tool Security Workflows with Docker Agents

[ View on GitHub ]

OXO: Building Multi-Tool Security Workflows with Docker Agents

Hook

What if you could chain Nmap, Nuclei, and ZAP in a single workflow without writing glue code? OXO treats security scanners as composable agents that speak a common language through message passing.

Context

Security scanning has a fragmentation problem. Your team probably uses Nmap for network discovery, Nuclei for vulnerability detection, ZAP for web app testing, and MobSF for mobile analysis. Each tool has its own CLI, output format, and deployment requirements. Building automated security pipelines means writing custom scripts to parse JSON, XML, or plaintext outputs, then feeding them into the next tool in your chain. When a scanner updates its output schema, your integration breaks.

Traditional solutions fall into two camps: monolithic commercial platforms that lock you into proprietary agents, or vulnerability management systems like DefectDojo that focus on aggregating results after scans complete. Neither addresses the orchestration problem—coordinating multiple specialized tools in a workflow where one scanner's output becomes another's input. OXO emerged from this gap, treating security scanners as modular agents that communicate through standardized messages. Instead of writing parsers, you define what data each agent consumes and produces using YAML selectors, letting the framework handle routing.

Technical Insight

OXO's architecture centers on agents—Dockerized security tools wrapped with message-passing interfaces. Each agent declares what it consumes (like IP addresses or domain names) and what it produces (open ports, discovered vulnerabilities) using selector definitions. The orchestrator routes messages between agents based on these declarations, creating implicit data pipelines without hardcoded dependencies.

Here's how an agent definition looks in practice. The Nmap agent's YAML manifest declares its inputs and outputs:

kind: Agent
name: nmap
version: 0.4.0
description: Fast network scanner
in_selectors:
  - v3.asset.ip.v4
  - v3.asset.ip.v6
  - v3.asset.domain.name
out_selectors:
  - v3.report.vulnerability
  - v3.asset.ip.v4.port.service
image: ostorlab/agent_nmap:0.4.0

When you scan a target, OXO emits initial messages—say, a v3.asset.domain.name message containing "example.com". Any agent subscribed to that selector (DNS resolvers, subdomain enumerators) receives it. When Nmap discovers open ports, it emits v3.asset.ip.v4.port.service messages. Agents like Nuclei or Tsunami, which scan specific services, automatically receive these messages and begin their scans. You've built a multi-stage scanning workflow without writing integration code.

The CLI makes local scanning straightforward. To scan an Android APK with static and dynamic analysis:

oxo scan run \
  --agent agent/ostorlab/mobsf \
  --agent agent/ostorlab/dex_audit \
  --agent agent/ostorlab/manifest_extractor \
  --file app.apk

Under the hood, OXO starts three Docker containers. The manifest extractor agent reads AndroidManifest.xml and emits messages about declared permissions and components. The dex_audit agent receives these messages, analyzes bytecode for known vulnerabilities, and emits vulnerability reports. MobSF performs comprehensive static analysis, contributing additional findings. All results flow to a report aggregator agent that you can query via GraphQL or export as JSON.

The message-passing model shines in complex workflows. Consider scanning a web application: you might start with subdomain enumeration (Amass), resolve IPs, scan ports (Nmap), identify technologies (Wappalyzer), then run targeted scans (Nuclei for CVEs, ZAP for DAST). In traditional scripts, you'd manage state between each step. With OXO, you declare the agent chain:

oxo scan run \
  --agent agent/ostorlab/amass \
  --agent agent/ostorlab/nmap \
  --agent agent/ostorlab/nuclei \
  --agent agent/ostorlab/zap \
  --agent agent/ostorlab/asteroid \
  --asset domain example.com

The framework handles message routing automatically. When Amass finds "api.example.com", Nmap scans it. When Nmap discovers port 443 open, ZAP and Nuclei receive service notifications and begin targeted tests. This declarative approach means adding a new scanner to your workflow is a single --agent flag—no script modification required.

Extending OXO means building agents, and the SDK simplifies this significantly. Here's a minimal agent in Python:

from ostorlab.agent import agent, definitions
from ostorlab.agent.message import message as m

class CustomScannerAgent(agent.Agent):
    @definitions.agent_definition(
        in_selectors=['v3.asset.domain.name'],
        out_selectors=['v3.report.vulnerability']
    )
    def process(self, message: m.Message) -> None:
        domain = message.data.get('name')
        # Your scanning logic here
        results = run_custom_scanner(domain)
        
        for vuln in results:
            self.emit(
                selector='v3.report.vulnerability',
                data={
                    'title': vuln.title,
                    'risk_rating': vuln.severity,
                    'technical_detail': vuln.description
                }
            )

Package this with a Dockerfile, push to a registry, and it's available to anyone in your organization. The agent store operates like a package manager—you reference agents by name, and OXO pulls the appropriate Docker image. This standardizes deployment: a security engineer doesn't need to install Go, Python, and Node.js to run scanners written in different languages. They just need Docker.

The GraphQL API enables programmatic access, useful for integrating OXO into existing dashboards or ticketing systems. After a scan completes, you can query results:

query {
  scan(scanId: "abc123") {
    vulnerabilities {
      title
      severity
      cvssScore
      technicalDetail
    }
    agents {
      name
      status
      runtime
    }
  }
}

This architecture choice—message passing over direct agent coupling—trades some performance for flexibility. Agents don't call each other; they communicate through the orchestrator. This indirection adds latency but enables dynamic workflows. You can add agents mid-scan (though OXO doesn't currently support this), replay messages for debugging, or split scanning across multiple machines by routing messages to remote agents.

Gotcha

Docker is both OXO's strength and its operational burden. Every agent runs in a container, which ensures reproducibility but means you're managing Docker images, networks, and volumes at scale. In CI/CD environments with ephemeral runners, pulling multi-gigabyte images for scanners like ZAP or MobSF adds significant overhead to scan startup time. There's no built-in image caching strategy beyond Docker's layer cache, so repeated scans in fresh environments repeatedly download dependencies.

Agent maintenance is a lottery. The public agent store hosts community-contributed scanners, but there's no enforcement of maintenance commitments. An agent that wraps a rapidly-evolving tool like Nuclei might lag behind upstream releases, missing critical templates. Worse, if an agent's maintainer abandons it, you inherit the maintenance burden or lose that capability. Unlike package managers with deprecation warnings, OXO doesn't signal when agents are stale or unmaintained. You discover this when scans fail or miss known vulnerabilities.

The message-passing model lacks conditional logic within the orchestrator itself. Agents execute if they receive matching messages, but you can't express rules like "run ZAP only if Nmap finds HTTP services on non-standard ports" without writing custom agent logic. For complex workflows requiring branching, state persistence between scan phases, or human-in-the-loop approvals, you'd need external orchestration (like wrapping OXO in Airflow) or implementing state machines within agents themselves. The framework excels at linear or fan-out workflows but struggles with sophisticated decision trees.

Verdict

Use OXO if you're managing multiple security scanners across diverse asset types (networks, web apps, mobile applications) and want unified workflows without writing integration glue. It's especially valuable in organizations standardizing on Docker, where the agent packaging model simplifies deployment across teams, or in CI/CD pipelines where you need reproducible security scans composed from specialized tools. The agent store accelerates workflow assembly—you're combining proven scanners, not building from scratch. Skip it if you're running security scans in resource-constrained environments where Docker overhead is prohibitive, if you need a single security tool rather than orchestration (just run Nuclei or Nmap directly), or if your workflows require complex conditional logic and state management that the message-passing model doesn't support. Also reconsider if operational simplicity is paramount—managing agent versions, Docker images, and community-maintained code adds complexity that dedicated platforms or scripted solutions might avoid.