Writing a plugin¶
Everything that talks to an outside system is a plugin: source control, sign-in, ticketing, vulnerability data, cloud
posture, SIEM, notifications, evidence storage, secrets. The built-in integrations are plugins too, registered the same
way yours will be. The worked example is examples/plugins/aap-plugin-jsonl,
which is tested in CI.
The contract¶
A plugin is a Python package that publishes a Plugin record through the aap.plugins entry-point group:
[project.entry-points."aap.plugins"]
servicenow = "aap_servicenow:PLUGIN" # one Plugin, or a list of them
from aap.plugins import Plugin, RunContext, RunResult
def run(ctx: RunContext) -> RunResult: # ctx: engine, bundle (governed config), secrets, blobs, payload
... # do the work through the platform's services
return RunResult(items=12, gaps={}, errors=[])
PLUGIN = Plugin(
key="servicenow", # [a-z][a-z0-9_]*, unique; also the name in connector_runs
name="ServiceNow",
kind="ticketing", # scm, identity, ticketing, vuln_intel, workbench, cloud_posture,
# siem, notification, evidence_store, secrets
purpose="Remediation tickets in ServiceNow",
env=("AAP_SERVICENOW_URL",), # settings that must be present (names only)
secrets=("servicenow-token",), # secret-store names that must be present
bootstrap="how an operator sets it up",
interval="servicenow.sync_interval", # path in config/integrations.yaml; None = runs on demand only
run=run,
)
The platform shows the plugin on the Integrations page as configured only when every env setting and secrets
entry is present; until then it never runs and its evidence types stay NO_EVIDENCE. A plugin that fails to import is
listed with its error and the platform keeps running.
Interfaces by kind¶
The client a plugin wraps should satisfy the protocol for its kind in
aap/plugins/interfaces.py: TicketSystem, CloudPosture,
SiemSink, VulnFeed, EvidenceBlobStore, SecretStore, GitHubClient. Then the platform's services do the rest:
| Kind | Hand your client to | Example |
|---|---|---|
| ticketing | services.ticketing.sync(conn, client, policy, url, now) |
connectors/issues.py (GitHub/GitLab Issues) |
| siem | services.siem_export.derive(...) then dispatch(conn, sink) |
connectors/siem_sinks.py, the example plugin |
| cloud_posture | services.cloud_sync |
connectors/cloud.py |
| vuln_intel | services.vuln_feeds (OSV-style matching) |
connectors/vuln_feeds.py |
Rules your plugin must keep¶
These are the platform's invariants (CONTRIBUTING); a plugin that breaks one cannot be listed:
- Fail closed. Anything you cannot read is a gap (
RunResult.gaps), never a default "ok". - Evidence comes from the worker, through
evidence_store.insert_evidence, with an honest confidence. A setting you read through an API is C1; something the external system executed itself may be C3; you never claim more than your source can prove. - Least privilege. Ask for read-only credentials; if you must write (tickets, messages), use a separate credential and send identifiers and links, not finding details.
- No telemetry, no network calls outside
run. Importing your module must not do I/O. - One SIEM sink at a time. SIEM plugins call
aap.worker.connectors.other_siem_sinks(key)and refuse to run when another sink is configured.
Testing¶
- Unit tests against recorded API responses (
httpx.MockTransport). - A discovery test:
aap.plugins.discover([entry_point])returns your plugin with no load errors (see the example). - A live test against the real system in a container where one exists, marked
service(opt-in). - Install into the platform for an end-to-end check:
uv run --with-editable path/to/plugin aap integrations status.
Publishing¶
Name it aap-plugin-<system>, licence it compatibly (Apache-2.0 recommended), document its settings, secrets and the
least-privilege set-up for the external system. Open an issue to have it listed.