Defense Plugin Architecture: Integrating CrowdStrike, Jamf, and Baramundi in Under 30 Minutes
Every SOC platform claims an "extensive integration catalog." Most of them mean a fixed list of partner connectors maintained by the vendor's engineering team. Real extensibility means your engineers can write a new integration in an afternoon. Here is the plugin interface that makes that possible, plus the three illustrative integrations (CrowdStrike, Jamf, Baramundi) we use to prove it.

TL;DR. Stop measuring a security platform by the size of its integration catalog. What matters is how long it takes your team, not the vendor's, to add an integration the catalog does not contain. If the answer is "an afternoon", you can keep pace with new threats; if it is "open a partner ticket and wait two quarters", you fall behind them. The shape of the plugin interface decides which of those you get.
The integration-catalog page on every SOC vendor's website is the same length and the same content. EDR vendors, MDM, vuln scanners, ticketing, chat, cloud platforms: the marquee logos rotate, the underlying claim is identical: "we support everything you already use."
Two questions tell you whether the claim is real.
First: can your engineers write a new integration without filing a ticket with the vendor? Second: if the answer is yes, how long does it take? "An afternoon" is a different platform from "two weeks of reverse-engineering an undocumented interface." Both can technically be called extensible.
This post walks through the plugin architecture used in Krasper Suite's Defense module, the layer that talks to endpoint, identity, and device-management systems. The three illustrative integrations are CrowdStrike (EDR), Jamf (Apple MDM), and Baramundi (UEM). They are concrete enough to prove the pattern works against real vendor APIs with very different shapes; they are also stand-ins for the longer answer, which is that any of them could be replaced or added to by a customer engineer in well under an hour against the interface described below.
Contents
- The integration tax: what plugin architecture is actually for
- The Defense Plugin interface
- Metadata discovery and registration
- A custom plugin in roughly 50 lines
- The health-check pattern most plugins skip
- Plugin lifecycle: install, enable, version, retire
- Why three different vendors prove the pattern
1. The integration tax: what plugin architecture is actually for
Every SOC platform pays an integration tax. The form it takes depends on how the platform is built.
If integrations are first-party only, written, maintained, and shipped by the vendor, the tax shows up as latency. A new release of the EDR you depend on changes a field in its API; the SOC vendor notices three weeks later; the patched integration ships in the next quarterly release; your detection coverage has a quiet gap in the meantime.
If integrations are first-party plus a partner program, the tax shifts to coordination. New vendors must be onboarded through a partner agreement before they can be integrated. The integrations that exist are well-supported; the ones that do not exist take six to twelve months to add, regardless of how trivial the API is.
Plugin architecture is what lets the integration tax sit with the engineers closest to the problem: yours. A well-designed plugin interface means a new EDR, a new MDM, a new asset source can be integrated by your team, on your schedule, against your security requirements. The platform vendor maintains the interface; the integrations against that interface are work anyone competent can do.
2. The Defense Plugin interface
A Defense Plugin in Suite is anything that implements four contracts:
- Asset Discovery: given a tenant context, return the assets this system knows about, in the canonical asset model.
- Telemetry Ingest: subscribe to or poll for security events from the system, normalize them into the platform's event schema, and publish them onto the shared bus.
- Action Execution: accept action requests (isolate endpoint, enforce policy, retrieve file, run script) and execute them against the upstream API, with explicit failure semantics.
- Health: answer authoritatively whether the plugin is currently functional, with enough detail for an operator to fix it.
Three of these are obvious; the fourth (health) is the one most home-grown integrations skip, and the one that determines whether the plugin is operable at 3 AM by someone other than its author.
┌──────────────────────────────────────────────┐ │ DefensePlugin (interface) │ │ │ │ + describe() → PluginMetadata │ │ + discover_assets() → AssetRecord[] │ │ + ingest() → AsyncIterator[Event] │ │ + execute(action) → ActionResult │ │ + health() → HealthReport │ └──────────────────────────────────────────────┘
Each method has a typed return; each external call has bounded timeouts; each side effect routes through the same idempotency layer the rest of the platform uses. Plugin authors do not implement those guarantees; they inherit them from the base class.
3. Metadata discovery and registration
A new plugin does not require redeploying the platform. The discovery loop picks it up from the plugin registry, reads its metadata, and makes it available for tenant configuration. The metadata describes what the plugin claims to do, what credentials it needs, what actions it supports, and what version contract it guarantees.
┌──────────────────────────────────────────────┐
│ PluginMetadata │
│ │
│ name: "acme-edr-plugin" │
│ version: "1.2.0" │
│ vendor: "acme" │
│ capabilities: │
│ - asset_discovery │
│ - telemetry_ingest │
│ - action.isolate_endpoint │
│ - action.run_remediation_script │
│ credentials_schema: { ... JSON Schema ... } │
│ api_contract_version: "1" │
└──────────────────────────────────────────────┘
api_contract_version is the load-bearing field. It is the version of the platform's plugin interface the plugin was built against, not the vendor API version. The platform refuses to load plugins whose contract version it cannot satisfy, which prevents the failure mode where an old plugin silently misbehaves against a newer interface.
Capabilities are explicit and granular. A plugin that does asset-discovery only does not get asked to execute actions. The operator UI surfaces what the plugin can do based on declared capabilities, so there are no "we tried to use this integration but it does not support what we need" surprises three weeks into production.
4. A custom plugin in roughly 50 lines
The fastest way to demonstrate the interface is to walk through what a minimal plugin actually looks like. The example below is a sketch, illustrative rather than production-ready, of integrating a fictional EDR product called Acme into Suite.
from suite_plugins import (
DefensePlugin, PluginMetadata, AssetRecord,
Event, ActionResult, HealthReport,
)
from suite_plugins.errors import UpstreamError
class AcmeEdrPlugin(DefensePlugin):
def describe(self) -> PluginMetadata:
return PluginMetadata(
name="acme-edr-plugin",
version="1.0.0",
vendor="acme",
capabilities=[
"asset_discovery",
"telemetry_ingest",
"action.isolate_endpoint",
],
credentials_schema={
"type": "object",
"required": ["api_token", "base_url"],
"properties": {
"api_token": {"type": "string"},
"base_url": {"type": "string", "format": "uri"},
},
},
api_contract_version="1",
)
async def discover_assets(self):
async for raw in self.client.list_endpoints():
yield AssetRecord(
native_id=raw["device_id"],
resolution_hints={
"serial": raw.get("serial_number"),
"mac": raw.get("primary_mac"),
"hostname": raw.get("hostname"),
},
os=raw.get("os_platform"),
last_seen=raw.get("last_checkin_at"),
)
async def ingest(self):
async for raw in self.client.stream_alerts():
yield Event(
event_type="edr.alert.created",
native_id=raw["alert_id"],
native_asset_id=raw["device_id"],
severity=raw["severity"],
payload=raw,
)
async def execute(self, action):
if action.type == "isolate_endpoint":
try:
await self.client.isolate(action.target.native_id)
return ActionResult.success()
except UpstreamError as e:
return ActionResult.failure(reason=str(e))
return ActionResult.unsupported()
async def health(self) -> HealthReport:
try:
await self.client.ping()
return HealthReport.ok()
except UpstreamError as e:
return HealthReport.degraded(reason=str(e))
Five methods, no infrastructure code. The plugin does not manage its own database, does not implement its own auth layer, does not roll its own retry logic. The base class and the runtime handle those. The plugin author writes the vendor-specific code and nothing else.
The pattern that matters: every method maps cleanly to a contract the platform already understands. AssetRecord is the canonical asset shape the platform's resolver expects. Event is the shape the event bus subscribes to. ActionResult is the shape every playbook node understands. The plugin does not invent vocabulary; it maps the vendor's vocabulary into the platform's.
5. The health-check pattern most plugins skip
health() looks trivial: call upstream, return ok or degraded. What separates a real health check from a check-the-box one is specificity.
A health check that returns "ok" or "fail" is barely useful. The operator finds out the integration is broken; they do not find out why. The plugin author then gets paged regardless.
A useful health check returns enough structure to triage:
┌────────────────────────────────────────────┐ │ HealthReport │ │ │ │ status: ok | degraded | down │ │ upstream_reachable: bool │ │ auth_valid: bool │ │ rate_limit_remaining: int | null │ │ last_successful_call: timestamp │ │ message: human-readable detail │ └────────────────────────────────────────────┘
Now the operator sees, at a glance, that the integration is down because authentication failed three hours ago, not because the network is broken, not because the upstream is rate-limiting. They can rotate the credentials in the tenant configuration and the plugin recovers without anyone touching code.
The platform calls health() on every plugin at a regular cadence and exposes the result on the operations dashboard. The same call also runs as a readiness check before any playbook that depends on the plugin executes. A playbook never silently runs against a known-broken integration.
6. Plugin lifecycle: install, enable, version, retire
A plugin's life has four phases, each with explicit semantics.
At install, a new plugin is uploaded or pulled from the plugin registry. The platform validates its metadata, checks the contract version, and registers it as available for tenant configuration, though it is not yet active for anyone.
A tenant then enables it by configuring credentials. The platform runs those credentials through schema validation, calls health() once to confirm the configuration works, and only then marks the plugin active for that tenant. A failed health check here keeps it from ever going live in a broken state.
New versions install alongside the old one. Tenants migrate individually rather than all at once, so nothing forces a fleet-wide upgrade; a version with a breaking change bumps api_contract_version, and the previous version stays installable for tenants that have not moved yet.
Finally, a plugin can be retired. It is marked deprecated, tenants are notified, new tenants can no longer enable it, and existing ones get a migration deadline. After that deadline the plugin goes read-only: events already ingested stay queryable, but no new discovery, ingest, or action runs.
This lifecycle is unglamorous but load-bearing. Without it, integrations accumulate forever, no one is sure which ones are still maintained, and the operational surface area silently grows until the next incident reveals which integrations were quietly broken for months. The lifecycle makes integration health a visible, auditable property of the platform.
7. Why three different vendors prove the pattern
The three integrations we ship in Suite's Defense module (CrowdStrike, Jamf, and Baramundi) exist as the empirical proof that the plugin interface above generalizes across very different upstream shapes.
CrowdStrike exposes a high-throughput streaming alert feed and a REST API for actions; the plugin's ingest() subscribes to the stream, execute() calls the REST endpoints. Jamf is Apple-centric, MDM-shaped, with a different vocabulary around devices and configuration profiles; the plugin maps that vocabulary into the canonical asset and action shapes the rest of the platform speaks. Baramundi covers Windows-centric UEM with patching and software inventory; the plugin handles bulk asset enumeration and software-installed events.
These three vendors expose completely different API styles and domain models, yet all of them reach the platform through the same plugin interface, each implementable by a single engineer in well under a day. That is the point of the architecture. The integrations are almost incidental; what matters is that the interface renders them interchangeable.
If you currently use a fourth EDR, a different MDM, a UEM we have not built against, the same interface is what you implement. The platform does not need to know about the vendor in advance.
Closing
An integration catalog is a poor way to measure a security platform. What counts is how cheaply your own team can extend it when the catalog falls short, and a well-shaped plugin interface, one with typed contracts, metadata-driven discovery, real health checks, and an explicit lifecycle, keeps that cost predictable and low.
The next post in this series looks at one specific consequence of this architecture: how the platform handles partial-failure scenarios where one plugin in a multi-step playbook is unavailable. There is no magic to it, just a lot of explicit error routing, which, as earlier posts have argued, is what mature automation actually looks like.
Further reading
- Plugin Architectures in Production Systems, design patterns for extensible platforms
- NIST SP 800-128: Guide for Security-Focused Configuration Management
- MITRE D3FEND: Hardening and Isolation techniques referenced by EDR integrations
enterprise infrastructure?
Schedule a technical briefing. No sales pitch, just architects and your team.