Skip to content

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

  1. 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
}
  1. Register it in backend/internal/plugins/wrappers/defaults.go using the existing default registration pattern:
register(&MyPlugin{}, plugins.PluginConfig{
    Enabled:       true,
    ExecutionMode: plugins.ModeCLI,
    TimeoutSecs:   300,
    Retry:         2,
})
  1. If the plugin calls an external CLI, add toolinstaller configuration in backend/configs/config.yaml. Keep tool versions in configuration, not Go code.

  2. Add the plugin ID to one or more scan_profiles entries in backend/configs/config.yaml when the tool should be part of a scan profile. Profile validation requires every referenced plugin to be registered.

  3. Return normalized assets/vulnerabilities, populate RawOutput and RawError, use metadata["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.RawOutput and PluginResult.RawError with stdout/stderr from CLI tools, including failure cases.
  • For CLI command failures, return a PluginResult with Status = "failed" or Status = "timeout", set result.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.DeadlineExceeded to detect timeouts and set result.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"] plus product, version, and service fields 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.