Plugin Development Guide
Source layout
backend/internal/plugins contains the stable plugin infrastructure: the Plugin interface, input/result models, registry, shared command helpers, and artifact/result contracts.
backend/internal/plugins/wrappers contains concrete tool integrations. Each wrapper converts one external tool into normalized assets, vulnerabilities, metadata, and optional artifacts. Contributors should add new CLI/API tool integrations here, one file per tool when practical.
Plugin interface
type Plugin interface {
Name() string
Type() PluginType
Version() string
Run(ctx context.Context, input PluginInput, config PluginConfig) (*PluginResult, error)
}
Input / output schemas
PluginInput
{
"organization_id": "uuid",
"scan_id": "uuid",
"scope": [
{ "type": "domain", "value": "example.com" }
],
"profile": "default",
"options": {
"rate_limit": 50,
"timeout_seconds": 300,
"active_scan": true,
"test_mode": false
}
}
PluginResult
{
"plugin": "subfinder",
"status": "success",
"started_at": "2026-04-29T10:00:00Z",
"finished_at": "2026-04-29T10:02:30Z",
"normalized_entities": [
{
"entity_type": "asset",
"asset_type": "subdomain",
"value": "api.example.com",
"source_plugin": "subfinder",
"confidence": 0.95,
"metadata": {}
}
]
}
entity_type is either "asset" or "vulnerability".
Plugin artifacts
Plugins that produce files return PluginResult.Artifacts. The worker owns upload to S3/MinIO through the Files service; plugin code should only create temporary files and describe them.
type PluginArtifact struct {
Path string
Name string
Type string // screenshot, raw_output, report, evidence, artifact, other
ContentType string
Metadata map[string]interface{}
AssetType string
AssetValue string
CleanupPath string
}
Use artifacts for screenshots, raw scan artifacts, exported evidence, proof files, or archived plugin outputs. Do not store binary content in PostgreSQL.
For vulnerabilities, metadata must contain:
{
"template_id": "CVE-…",
"title": "…",
"severity": "high",
"description": "…",
"remediation": "…",
"matched_url": "https://…"
}
Creating a new plugin
- Create
backend/internal/plugins/wrappers/myplugin.go:
package wrappers
import (
"context"
"time"
"github.com/easm-platform/backend/internal/assets"
"github.com/easm-platform/backend/internal/plugins"
)
type MyPlugin struct{}
func (p *MyPlugin) Name() string { return "myplugin" }
func (p *MyPlugin) Type() plugins.PluginType { return plugins.TypeRecon }
func (p *MyPlugin) Version() string { return "1.0.0" }
func (p *MyPlugin) Run(ctx context.Context, input plugins.PluginInput, config plugins.PluginConfig) (*plugins.PluginResult, error) {
result := &plugins.PluginResult{Plugin: p.Name(), StartedAt: time.Now()}
if plugins.IsTestMode(config) {
result.NormalizedEntities = []assets.NormalizedEntity{ /* mock */ }
return plugins.FinishTestResult(result, p.Name(), len(result.NormalizedEntities)), nil
}
// Real implementation: exec.CommandContext(ctx, "mybinary", args...)
// Preserve stdout/stderr in RawOutput/RawError and parse normalized entities.
result.Status = "success"
result.FinishedAt = time.Now()
return result, nil
}
- Register it in
backend/internal/plugins/wrappers/defaults.gousing the existing default registration pattern:
register(&MyPlugin{}, plugins.PluginConfig{
Enabled: true,
ExecutionMode: plugins.ModeCLI,
TimeoutSecs: 300,
Retry: 2,
})
-
If the plugin calls an external CLI, add toolinstaller configuration in
backend/configs/config.yaml. Keep tool versions in configuration, not Go code. -
Add the plugin ID to one or more
scan_profilesentries inbackend/configs/config.yamlwhen the tool should be part of a scan profile. Profile validation requires every referenced plugin to be registered. -
Return normalized assets/vulnerabilities, populate
RawOutputandRawError, usemetadata["technologies"]only for technology details, and add focused tests.
Plugin types
| Type constant | Purpose |
|---|---|
TypeRecon |
Subdomain / domain discovery |
TypeResolver |
DNS resolution |
TypeHTTPProbe |
Live HTTP service detection |
TypePortScan |
Port/service scanning |
TypeCrawler |
Web app crawling |
TypeVulnScan |
Vulnerability detection |
TypeTechDetect |
Technology fingerprinting |
TypeLeakCheck |
Credential/secret exposure |
TypePhishingCheck |
Phishing indicator analysis |
TypeCustom |
Any other purpose |
Execution modes
| Mode | When |
|---|---|
ModeCLI |
Binary is on $PATH; plugin uses exec.Command |
ModeAPI |
Binary exposes HTTP API; plugin makes HTTP calls |
ModeDocker |
Plugin pulls and runs a Docker image |
Error handling
- Always populate
PluginResult.RawOutputandPluginResult.RawErrorwith stdout/stderr from CLI tools, including failure cases. - For CLI command failures, return a
PluginResultwithStatus = "failed"orStatus = "timeout", setresult.Error, and return a non-nil Go error. The worker preserves the returned result output while marking the scan job failed. - Use
ctx.Err() == context.DeadlineExceededto detect timeouts and setresult.Status = "timeout". - Avoid parser retry loops that execute the same external command more than once unless explicit retry logic exists at the scan/job level.
Expanded EASM plugin catalog
| Plugin | Input assets | Output assets | Created relations | Mode | Notes |
|---|---|---|---|---|---|
subfinder |
domain | subdomain | domain contains subdomain |
passive | Existing plugin. |
amass |
domain | subdomain | domain contains subdomain |
passive/active | Runs amass enum -d <domain> and parses the default text stdout format for compatibility with Amass versions that do not support -json. |
alterx |
domain, subdomain | candidate subdomain | parent generated_candidate candidate |
passive | Experimental; disabled by default because candidates require confirmation. |
resolver |
domain, subdomain | ip | domain resolves_to ip |
passive | Existing Go resolver plugin. |
dnsx |
domain, subdomain | ip | domain resolves_to ip |
passive | ProjectDiscovery DNS resolver. |
shuffledns |
domain, subdomain | ip, subdomain | domain resolves_to ip |
passive | Experimental; disabled by default because resolver/wordlist configuration is required. |
asnmap |
asn, org_name | cidr, asn | org owns asn; asn announces cidr |
passive | Experimental; disabled by default. |
httpx |
domain, subdomain, ip, service | url | service serves url; degraded host serves url only when no service can be identified |
active-light | Emits host, ip, port, scheme where available; detected technologies are stored in URL metadata.technologies. |
httpx_screenshot |
url, domain, subdomain, service, ip | screenshot files | file metadata linked to scan/job/asset | active-light | Stores screenshots through Files/S3 artifact upload; requires Chromium in the worker image and uses httpx -system-chrome. |
naabu |
ip, cidr, domain, subdomain | service | ip exposes service |
active | Fast port discovery. |
nmap |
ip, cidr, domain, subdomain | ip, service | ip exposes service |
active | Existing plugin. |
tlsx |
url, domain, subdomain, service, ip | certificate | preferred source asset has_certificate certificate |
active-light | Certificate inventory; service is preferred when host/IP/port metadata is available. |
katana |
url | path, url | url has_path path; url discovered_url url |
active | Web crawler. |
nuclei |
url, domain, subdomain, ip | vulnerability | asset vulnerability storage | active | Existing plugin. |
uncover |
domain, subdomain, ip | service | ip exposes service |
passive/API | Experimental; disabled by default because provider API keys are usually required. |
Supported asset types: domain, subdomain, ip, cidr, asn, service, url, path, certificate. Technologies are stored in asset metadata and should not be emitted as standalone assets. Metadata keys tech and technology are legacy inputs; persistence normalizes them into metadata.technologies as a deduplicated string array.
Canonical graph order is domain -> subdomain -> ip -> service -> url -> vulnerability. Supported graph relations include: contains, resolves_to, exposes, serves, has_certificate, has_path, discovered_url, generated_candidate, owns, and announces. uses_technology is legacy/deprecated and new plugins should not create it.
Tool versions and scan profile composition are configured in backend/configs/config.yaml; plugin code does not hardcode tool release versions or profile membership. Experimental tools are registered but disabled by default in normal profiles and tool installer config.
Canonical graph relation rules
Wrappers should emit enough metadata for the worker to build this chain:
domain -> subdomain -> ip -> service -> url -> vulnerability
Required metadata by output type:
| Output asset | Required metadata | Why |
|---|---|---|
| subdomain | parent_domain |
Creates domain contains subdomain. |
| ip | domain or host, record_type when available |
Creates host resolves_to IP. |
| service | ip, port, protocol |
Creates IP exposes service. Service values normalize to ip:port/protocol. |
| url | host, ip, port, scheme when available |
Creates service serves URL. Direct host serves URL is only a fallback when service data is missing. |
| certificate | host, ip, port when available |
Creates has_certificate, preferring service, then URL, then host. |
| path/discovered URL | parent_url |
Creates URL has_path path or URL discovered_url URL. |
Do not create direct IP/subdomain/domain-to-URL edges when a service can be found or created. Do not create uses_technology edges.
Technology metadata model
Do not emit AssetTechnology normalized entities or create uses_technology edges. Detected technologies belong in metadata on the asset they describe:
- URL assets:
metadata.technologies = ["nginx", "React"] - Service assets:
metadata.technologies = ["OpenSSH 8.9"]plusproduct,version, andservicefields where available
The canonical key is metadata.technologies; legacy keys tech and technology are normalized away at asset upsert. This reduces graph noise and keeps technology details close to the reachable asset.