Skip to content

Extending Harbinger Playbook and Output Templating

Matthijs Gielen edited this page Jul 15, 2025 · 2 revisions

Extending Harbinger Playbook and Output Templating

Related Pages

Related topics: Playbooks and Task Management, File and Output Parsing

Relevant source files

Extending Harbinger: Playbook and Output Templating

Harbinger's power lies in its ability to automate and structure red team operations through the use of playbooks, job templates, and output parsers. This guide details how to extend Harbinger by creating new playbook and job templates, as well as custom output and file parsers to handle new data formats.

Playbook and Job Templating

Playbooks in Harbinger are sequences of jobs that can be executed on C2 implants. These are defined using a templating system that allows for reusable and configurable operational workflows.

Job Template Schemas

Job templates are defined using Pydantic schemas, which specify the arguments and structure for a given job. The core Arguments schema in harbinger/src/harbinger/job_templates/schemas.py defines a wide range of possible parameters for C2 jobs.

# harbinger/src/harbinger/job_templates/schemas.py
class Arguments(BaseModel):
    command: str = ""
    folder: str = ""
    path: str = ""
    # ... other arguments

Sources: harbinger/src/harbinger/job_templates/schemas.py:27-56

When creating a new job template, you will typically define a new schema that inherits from BaseModel and includes the specific arguments required for that job.

Base Job Classes

Harbinger provides base classes for C2 and proxy jobs in harbinger/src/harbinger/job_templates/c2/base.py and harbinger/src/harbinger/job_templates/proxy/base.py.

The C2Job class is the base for all C2-specific jobs.

# harbinger/src/harbinger/job_templates/c2/base.py
class C2Job(BaseModel):
    name: str
    description: str
    job_type: Literal[schemas.C2JobType.C2Job] = schemas.C2JobType.C2Job
    arguments: Arguments | None = None
    command: str

Sources: harbinger/src/harbinger/job_templates/c2/base.py:11-17

To create a new C2 job template, you would create a new class that inherits from C2Job and defines the specific name, description, and command.

Registering New Templates

New job templates must be registered in the appropriate map to be accessible through the API. For C2 jobs, this is the C2_JOB_BASE_MAP in harbinger/src/harbinger/job_templates/c2/base.py.

# harbinger/src/harbinger/job_templates/c2/base.py
C2_JOB_BASE_MAP = {
    "powershell": C2Job(
        name="powershell",
        description="Run a powershell command",
        command="powershell",
        arguments=Arguments(powershell=""),
    ),
    # ... other registered jobs
}

Sources: harbinger/src/harbinger/job_templates/c2/base.py:19-45

To add a new job, you would add an entry to this dictionary with the job's name as the key and an instance of your new job class as the value.

API Endpoints for Templates

The Harbinger API exposes endpoints for retrieving job and playbook templates, which are defined in harbinger/src/harbinger/job_templates/router.py.

The /templates/{c2_type}/ endpoint returns a list of available job templates for a given C2 type.

# harbinger/src/harbinger/job_templates/router.py
@router.get(
    "/{c2_type}/",
    response_model=TemplateList,
    tags=["proxy_jobs", "crud"],
)
async def job_templates(
    c2_type: schemas.C2Type,
    # ...
):
    if c2_type == schemas.C2Type.c2:
        return dict(templates=[key for key in C2_JOB_BASE_MAP.keys()])
    if c2_type == schemas.C2Type.proxy:
        return dict(templates=[key for key in PROXY_JOB_BASE_MAP.keys()])

Sources: harbinger/src/harbinger/job_templates/router.py:65-72

Once a new template is registered in the appropriate map, it will automatically be available through this endpoint.

Output and File Parsing

Harbinger's parsing framework is designed to be extensible, allowing you to create custom parsers for new types of C2 output and file formats.

Output Parsers

Output parsers are designed to analyze the text output from C2 tasks and extract structured information. All output parsers must inherit from the OutputParser abstract base class in harbinger/src/harbinger/worker/output.py.

# harbinger/src/harbinger/worker/output.py
class OutputParser(abc.ABC):
    needle: list[str] = []
    labels: list[str] = []

    @abc.abstractmethod
    async def match(self, text: str) -> bool:
        raise NotImplementedError

    @abc.abstractmethod
    async def parse(self, text: str, **kwargs) -> None:
        raise NotImplementedError

Sources: harbinger/src/harbinger/worker/output.py:41-55

To create a new output parser:

  1. Create a new class that inherits from OutputParser.
  2. Implement the match method to determine if the parser should process a given text output. You can use the needle attribute for simple string matching.
  3. Implement the parse method to perform the actual parsing logic, extracting data and saving it to the database.

File Parsers

File parsers are used to process the content of uploaded files. All file parsers must inherit from the BaseFileParser abstract base class in harbinger/src/harbinger/worker/files/parsers.py.

# harbinger/src/harbinger/worker/files/parsers.py
class BaseFileParser(ABC):
    @abstractmethod
    async def parse(
        self,
        db: AsyncSession,
        graph_db: AsyncNeo4jSession,
        tmpdirname: str,
        tmpfilename: str,
        file: schemas.File,
    ) -> list[schemas.File]:
        return []

Sources: harbinger/src/harbinger/worker/files/parsers.py:27-38

To create a new file parser:

  1. Create a new class that inherits from BaseFileParser.
  2. Implement the parse method. This method receives the database sessions and information about the file to be parsed. The base_parse method in the base class can be used to handle downloading the file to a temporary location.
  3. Your parse method should open and read the file, extract the relevant information, and save it to the database.

Registering New Parsers

New parsers need to be registered with the system to be used in workflows. This is typically done by adding the new parser to a list or dictionary that is iterated over when processing new data.

For example, in harbinger/src/harbinger/worker/output.py, you would add your new output parser to the OUTPUT_PARSERS list.

# Example of registering a new output parser
# (This is a conceptual example, the actual implementation may vary)

# in harbinger/src/harbinger/worker/output.py
from .parsers import NewCustomOutputParser

OUTPUT_PARSERS = [
    # ... existing parsers
    NewCustomOutputParser,
]

Conclusion

Extending Harbinger with custom playbook templates and parsers is a powerful way to tailor the framework to your specific operational needs. By following the established patterns of using Pydantic schemas for templates and inheriting from the base parser classes, you can seamlessly integrate new capabilities into the Harbinger system. This modular approach ensures that Harbinger can evolve to support new C2 frameworks, tools, and data formats, making it a versatile platform for red team operations.

Clone this wiki locally