Skip to content

Latest commit

 

History

History
445 lines (323 loc) · 16.8 KB

CONTRIBUTING.md

File metadata and controls

445 lines (323 loc) · 16.8 KB

Contributing

This document covers how to extend the plugin: adding threat actors, writing new abilities, adding exfiltration methods, and working with the development tooling. It assumes you have a working installation (see INSTALL.md).


Table of Contents

  1. Development Environment
  2. Codebase Navigation
  3. Adding a Threat Actor
  4. Adding an Ability
  5. Adding an Exfiltration Method
  6. Modifying the Kill Chain
  7. Extending the MCP Tool Server
  8. Writing Tests
  9. Sensitive Coupling Points
  10. Submitting Changes

1. Development Environment

Setup

# From the caldera/ root
pip install -r requirements.txt
pip install -r plugins/seed_and_vine/requirements.txt

cp plugins/seed_and_vine/.env.example plugins/seed_and_vine/.env
# Set CALDERA_API_KEY and at least one LLM provider key in .env

Running with verbose logging

python server.py --insecure -l DEBUG

Seed & Vine logs are written to plugins/seed_and_vine/data/logs/ (three rotating files) in addition to the console.

Running tests

python -m pytest plugins/seed_and_vine/tests/ -v

All ten test files run without a live CALDERA server or LLM API keys. Tests that require asyncio use pytest-asyncio.

Rebuilding the ChromaDB index

After adding or modifying ability YAML files, trigger a manual index rebuild:

curl -s -X POST http://localhost:8888/plugin/seed_and_vine/api/reindex

Or restart CALDERA, the index is rebuilt automatically from scratch on each startup if the integrity manifest is invalid.


2. Codebase Navigation

Before making any change, read these files in order:

File Why
app/constants.py Every UUID, route path, tool name, timeout, and risk matrix entry lives here. If a value appears in two places, one of them is wrong.
ARCHITECTURE.md Explains the three-layer FSM, data flows, and design decisions.
Plugin CLAUDE.md Comprehensive developer reference with file map, coupling table, and data-flow walkthrough.

The golden rule: change a constant in constants.py, not in the file that uses it. Route paths, MCP tool names, ability UUIDs, and fact trait names are all centralized there. Changing them in one file without updating constants.py creates silent divergence.


3. Adding a Threat Actor

A threat actor in Seed & Vine is a named profile that influences:

  • The TTP narrative injected into the AI system prompt
  • The default exfiltration method priority order
  • The GUI dropdown options

Step 1: Add the profile to context_builder.py

Open app/context_builder.py and find _get_threat_actor_context(). Add a new branch:

elif threat_actor == "fin7":
    return {
        "name": "FIN7 (Carbanak)",
        "mitre_id": "G0046",
        "attribution": "Russian criminal organization",
        "ttp_profile": (
            "FIN7 specializes in point-of-sale intrusions and financial theft. "
            "Known for CARBANAK backdoor, spear-phishing initial access, "
            "and persistence via scheduled tasks."
        ),
        "default_exfil_methods": ["c2_channel", "smtp", "dropbox"],
    }

The default_exfil_methods list is the ordered fallback queue used when no custom order is set by the operator.

Step 2: Update get_default_exfil_order()

In the same file, get_default_exfil_order(threat_actor) returns the default fallback queue by actor name. Add the new actor:

def get_default_exfil_order(threat_actor: str) -> list[str]:
    orders = {
        "apt41":   ["cloudflare_c2", "dns_tunnel", "onedrive"],
        "lazarus": ["c2_channel", "smtp", "dropbox"],
        "fin7":    ["c2_channel", "smtp", "dropbox"],  # Add here
    }
    return orders.get(threat_actor, ["c2_channel"])

Step 3: Add to the Vue frontend

Open gui/views/seed_and_vine.vue and find the threatActorOptions array. Add an entry:

{ value: 'fin7', label: 'FIN7 (Carbanak)', description: 'Russian criminal, PoS and financial intrusions' }

Step 4: Test

Start CALDERA, create an operation with the new actor, and verify:

  • The actor name appears in the Config panel dropdown
  • The exfil method order in the GUI reflects the new default
  • The AI prompt includes the new TTP profile (visible in data/logs/seed_vine_ai_trace.log)

4. Adding an Ability

Step 1: Create the YAML file

Abilities live in data/abilities/{tactic}/. Create a new YAML file following the CALDERA schema:

- id: <UUID>          # Generate with: python -c "import uuid; print(uuid.uuid4())"
  name: My New Ability
  description: >-
    Brief description for ChromaDB indexing. Keep this informative; it is the
    primary text used for semantic search.
  tactic: defense_evasion
  technique_id: T1562.001
  technique_name: "Impair Defenses: Disable or Modify Tools"
  executors:
    - name: sh
      platform: linux
      command: >-
        systemctl stop <service> 2>/dev/null;
        echo "[SV] Service stopped"
      timeout: 60
    - name: psh
      platform: windows
      command: >-
        Stop-Service -Name "<service>" -ErrorAction SilentlyContinue;
        Write-Output "[SV] Service stopped"
      timeout: 60
  plugin: seed_and_vine

UUID: generate a fresh one, never reuse an existing UUID.

Tactic values: discovery, privilege_escalation (underscore), defense_evasion, credential_access, collection, exfiltration. These must match the CALDERA tactic namespace as used in KILL_CHAIN_PHASES.

Fact variables: if the command uses #{variable_name} placeholders, the fact must exist in the fact store at execution time. Document the required facts in a fact_variables list (see the DNS tunnel ability for an example).

Step 2: Verify it loads

python server.py --insecure
curl -s 'http://localhost:8888/api/v2/abilities?tactic=defense_evasion' \
  -H 'KEY: ADMIN123' | python -m json.tool | grep '"name"'

Your ability should appear.

Step 3: Rebuild the ChromaDB index

curl -s -X POST http://localhost:8888/plugin/seed_and_vine/api/reindex

Step 4: Verify it is searchable

curl -s 'http://localhost:8888/plugin/seed_and_vine/api/status' | python -m json.tool
# Then trigger a manual search via the MCP tool in a test run, or write a test
# using the AbilityIndex class directly:
from plugins.seed_and_vine.app.ability_index import AbilityIndex
index = AbilityIndex(services)
results = index.search("disable antivirus service", tactic="defense_evasion", n_results=5)
print(results)

5. Adding an Exfiltration Method

Adding a new exfil method involves four coordinated changes: the ability YAML, the constant mapping, the fact source, and the frontend.

Step 1: Write the ability YAML

Create data/abilities/exfiltration/sv-exfil-<name>.yml. Follow the same structure as the existing six exfil abilities. Key points:

  • Tactic must be exfiltration
  • Both sh (Linux) and psh (Windows) executors should be present where feasible
  • Use #{trait_name} for all secrets and endpoints, never hardcode them
  • Set timeout: 180 (exfil can be slow for large files)
  • Add [SV-EXFIL] prefix to all echo/Write-Output statements for log parsing

Step 2: Add the UUID to constants.py

# In app/constants.py, Ability UUIDs section:
ABILITY_MYADAPTER_EXFIL = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# In EXFIL_METHOD_ABILITY_MAP:
EXFIL_METHOD_ABILITY_MAP = {
    "c2_channel":    ABILITY_LAZARUS_C2,
    "cloudflare_c2": ABILITY_APT41_CLOUDFLARE,
    "dns_tunnel":    ABILITY_APT41_DNS_TUNNEL,
    "dropbox":       ABILITY_LAZARUS_DROPBOX,
    "onedrive":      ABILITY_APT41_ONEDRIVE,
    "smtp":          ABILITY_LAZARUS_SMTP,
    "myadapter":     ABILITY_MYADAPTER_EXFIL,   # Add here
}

# Also update EXFIL_METHOD_ABILITY_MAP_WITH_NAMES (same file).
# IMPORTANT: values are 2-tuples (uuid, display_name), NOT dicts.
# Code accesses them as map["key"][0] (uuid) and map["key"][1] (name).
EXFIL_METHOD_ABILITY_MAP_WITH_NAMES = {
    ...
    "myadapter": (ABILITY_MYADAPTER_EXFIL, "MyActor - MyAdapter Exfiltration"),
}

Step 3: Add required facts to data/sources/sv-facts.yml

- trait: myadapter_endpoint
  value: https://your-adapter-endpoint.example.com

Step 4: Add to the Vue frontend

In gui/views/seed_and_vine.vue, find the exfil method options (typically in a exfilMethods array or similar data property) and add:

{ value: 'myadapter', label: 'MyAdapter - Custom Channel', actor: 'custom' }

Step 5: Update threat actor default order (optional)

If the new method should be in a threat actor's default fallback queue, update get_default_exfil_order() in context_builder.py.


6. Modifying the Kill Chain

The kill chain is the ordered list of ATT&CK phases the Governor reasons over. It is defined in app/constants.py:

KILL_CHAIN_PHASES = [
    "discovery",
    "privilege_escalation",
    "defense_evasion",
    "credential_access",
    "collection",
    "exfiltration",
]

Adding a phase

  1. Add the phase name to KILL_CHAIN_PHASES in the correct position.
  2. Add a tool mapping in PHASE_TOOL_MAPPING in the same file:
    "lateral_movement": [TOOL_SEARCH_LATERAL, TOOL_GET_ABILITY_DETAILS,
                         TOOL_EXECUTE_EXISTING, TOOL_PROPOSE_NEW] + COMMON_TOOLS,
  3. Add a corresponding search tool to the MCP server if the new phase needs a dedicated search tool (see Section 7 below).
  4. Add the new tactic to RISK_MATRIX with an appropriate level and auto_approve setting.
  5. Update PHASE_ORDER in constants.py if you maintain an explicit ordering dict.

Note: The Governor reasons adaptively and does not require every phase to be visited in order. It selects phases based on the AI's assessment of what best advances the current goal. Adding a phase makes it available to the Governor; whether it gets chosen depends on the mission context.

Removing a phase

Remove the phase from KILL_CHAIN_PHASES and PHASE_TOOL_MAPPING. Existing abilities in that tactic are still searchable (the tactic filter in ChromaDB will simply never match), but the Governor will never select a task for that phase.


7. Extending the MCP Tool Server

The MCP server is in app/seed_vine_server.py. New tools are registered as Python functions decorated with @mcp.tool().

Adding a search tool

@mcp.tool()
def search_lateral_movement_ability_library(keyword: str) -> str:
    """
    Search the ability library for lateral movement techniques.

    Args:
        keyword: Natural language description of the lateral movement goal.

    Returns:
        JSON list of matching abilities with id, name, description, tactic, technique_id.
    """
    return _search_by_tactic(keyword, tactic="lateral_movement")

After adding the tool:

  1. Add the tool name constant to constants.py:
    TOOL_SEARCH_LATERAL = "search_lateral_movement_ability_library"
  2. Add the constant to PHASE_TOOL_MAPPING["lateral_movement"] in constants.py.
  3. Update the few-shot demos in app/planner_demos.py to include examples of the new tool being called.

Adding an action tool

Action tools (tools that cause side effects) must use CalderaRequest for HTTP calls, not aiohttp, because the MCP subprocess runs synchronously:

@mcp.tool()
def my_action_tool(param: str) -> str:
    """..."""
    client = CalderaRequest()
    response = client.post(
        f"{CALDERA_BASE_URL}/plugin/seed_and_vine/api/my_endpoint",
        json={"param": param}
    )
    return json.dumps(response)

The corresponding async handler must be added to seed_vine_api.py and registered as a route in hook.py.


8. Writing Tests

Tests live in plugins/seed_and_vine/tests/. Use pytest with pytest-asyncio for async tests.

Test structure conventions

import pytest
from unittest.mock import AsyncMock, MagicMock, patch

@pytest.mark.asyncio
async def test_my_feature():
    # Arrange
    svc = MagicMock()
    svc.state = {"status": "IDLE", "fact_store": {}}

    # Act
    result = await my_function(svc, param="value")

    # Assert
    assert result["status"] == "expected"

What to test

  • Unit tests for FSM transitions: create a KillChainGovernor or TaskState with a mock service and assert that state transitions are correct after calling transition methods.
  • JSON parsing robustness: AIDecision.from_raw_text() must handle malformed LLM output, test all four parsing strategies.
  • Fact resolution: resolve_facts_in_command() must raise ValueError on unresolved #{...} placeholders.
  • Exfil fallback ordering: when the primary exfil method fails, the next method in exfil_method_order must be attempted.

Mocking CALDERA

Most tests mock data_svc, CalderaClient, and AbilityExecutor rather than running against a live CALDERA instance. The caldera_client.py wrapper makes this straightforward, patch CalderaClient.get and CalderaClient.post directly.

Running a subset of tests

# Run one file
python -m pytest plugins/seed_and_vine/tests/test_adaptive_governor.py -v

# Run by keyword
python -m pytest plugins/seed_and_vine/tests/ -v -k "approval"

# With output capture disabled (useful for debugging)
python -m pytest plugins/seed_and_vine/tests/ -v -s

9. Sensitive Coupling Points

Some parts of the codebase are tightly coupled across multiple files. A change in one place silently breaks something in another if you are not careful. This table lists the most dangerous coupling points.

Target Where to change What breaks if mismatched
API route paths (ROUTE_*) constants.py only Vue frontend hardcodes these paths
MCP tool names (TOOL_*) constants.py + seed_vine_server.py @mcp.tool() decorator name ReAct agent cannot find renamed tools; planner_demos.py examples break
KILL_CHAIN_PHASES constants.py Governor, PHASE_TOOL_MAPPING, task creation, and tests all depend on this list
GovernorState enum values kill_chain_governor.py FSM transition tables in seed_vine_svc.py, Governor, and tests
Ability UUIDs (ABILITY_*) constants.py + YAML id: field EXFIL_METHOD_ABILITY_MAP, DISCOVERY_ABILITIES, and _SV_ABILITIES fallback
Exfil method keys constants.py EXFIL_METHOD_ABILITY_MAP Must match Vue exfilMethods[*].value
Fact trait names (FACT_*) constants.py + YAML #{variable} Unresolved #{...} placeholders raise ValueError at execution time
Few-shot demo field names planner_demos.py / factory_demos.py Must match current DSPy Signature field names, stale demos cause dspy.Example schema errors
Route registration order in hook.py hook.py Routes MUST be registered before await seed_vine_svc.initialize(). aiohttp freezes the router when the application starts listening, adding routes after that point raises RuntimeError: Cannot register a resource into frozen router. Never move route registration into a background task or into initialize().

10. Submitting Changes

Before opening a pull request

  1. Run the full test suite: python -m pytest plugins/seed_and_vine/tests/ -v, all tests must pass.
  2. If you added or modified ability YAML files, verify POST /api/reindex completes without errors and your ability appears in a search result.
  3. If you changed constants.py, grep for the old value across all Python and Vue files to confirm no stale references remain.
  4. Update CLAUDE.md (plugin developer reference) and the relevant documentation file if you:
    • Added or removed a file
    • Added or removed an API endpoint
    • Added or removed an MCP tool
    • Added or removed an ability
    • Changed a key constant

Code style

  • Python: follow PEP 8; no line-length enforcement, but keep lines readable.
  • Type hints on all new public functions and class methods.
  • No module-level docstrings, use CLAUDE.md and architecture docs for narrative explanation.
  • No inline comments unless the code is genuinely non-obvious. Code should be readable without them.

Security considerations

  • Never hardcode secrets in YAML, Python, or Vue files. All sensitive values must come from .env or sv-facts.yml.
  • New exfil abilities must include [SV-EXFIL] markers in their output for log parsing.
  • New action tools in the MCP server must go through the execute_ability_for_mcp bridge; do not add direct CALDERA writes inside the subprocess.
  • If a new feature introduces a new #{trait} variable in an ability command, resolve_facts_in_command() must be able to resolve it, add the corresponding fact to sv-facts.yml and document which exfil method requires it in CONFIGURATION.md.