Plugin Guide

complyctl extends to arbitrary policy engines through plugins. Each plugin is a standalone executable that communicates with the CLI via gRPC using the hashicorp/go-plugin framework.

Discovery#

Scanning providers are discovered by scanning ~/.complytime/providers/ for executables matching the naming convention:

complyctl-provider-<evaluator-id>

The CLI strips the complyctl-provider- prefix to derive the evaluator ID used for routing Generate and Scan requests.

Example BinaryEvaluator ID
complyctl-provider-openscapopenscap
complyctl-provider-kuberneteskubernetes
complyctl-provider-testtest

No manifest files, no configuration files. The executable must be in the plugin directory and have execute permission.

gRPC Interface#

Plugins implement the Plugin interface (defined in pkg/plugin/manager.go):

type Plugin interface {
    Describe(ctx context.Context, req *DescribeRequest) (*DescribeResponse, error)
    Generate(ctx context.Context, req *GenerateRequest) (*GenerateResponse, error)
    Scan(ctx context.Context, req *ScanRequest) (*ScanResponse, error)
}

Describe#

Called during plugin discovery and complyctl doctor diagnostics. Returns plugin health, version, and declared variable requirements (RequiredGlobalVariables, RequiredTargetVariables). Plugins that return Healthy: false or fail the RPC are skipped during loading.

Generate#

Called by complyctl generate. Receives a three-tier variable model (R48):

TierFieldSource
1 — GlobalGlobalVariablescomplytime.yaml top-level variables
2 — TargetTargetVariablescomplytime.yaml targets[].variables (one target per call)
3 — TestConfiguration[].ParametersPer-requirement parameters from the assessment plan

The plugin prepares declarative policies in whatever format the underlying engine expects.

Scan#

Called by complyctl scan. Receives targets only — no requirement IDs are sent. The provider evaluates all requirements from Generate-time state (R47). Returns AssessmentLog entries — one per requirement evaluated — each containing steps with pass/fail/skip/error results and a ConfidenceLevel enum.

Protobuf Contract#

The canonical protobuf definition lives at api/plugin/plugin.proto. Key types:

TypePurpose
GenerateRequestGlobal variables, target variables, assessment configurations
AssessmentConfigurationPlan ID, requirement ID, parameters map
TargetTarget ID + plugin-defined variables
AssessmentLogRequirement ID, steps, message, confidence level
StepName, result, message
DescribeResponseHealth, version, required global/target variable names
ConfidenceLevelEnum: NOT_SET, UNDETERMINED, LOW, MEDIUM, HIGH
ResultEnum: UNSPECIFIED, PASSED, FAILED, SKIPPED, ERROR

Authoring A Plugin (go)#

Use plugin.Serve() to register and start the gRPC server. The handshake is handled automatically.

package main

import (
    "context"

    "github.com/complytime/complyctl/pkg/plugin"
)

var _ plugin.Plugin = (*myPlugin)(nil)

type myPlugin struct{}

func (p *myPlugin) Describe(_ context.Context, _ *plugin.DescribeRequest) (*plugin.DescribeResponse, error) {
    return &plugin.DescribeResponse{
        Healthy: true,
        Version: "1.0.0",
        RequiredGlobalVariables: []string{"output_dir"},
        RequiredTargetVariables: []string{"kubeconfig"},
    }, nil
}

func (p *myPlugin) Generate(_ context.Context, req *plugin.GenerateRequest) (*plugin.GenerateResponse, error) {
    _ = req.GlobalVariables
    _ = req.TargetVariables
    for _, cfg := range req.Configuration {
        _ = cfg.RequirementID
        _ = cfg.Parameters
    }
    return &plugin.GenerateResponse{Success: true}, nil
}

func (p *myPlugin) Scan(_ context.Context, req *plugin.ScanRequest) (*plugin.ScanResponse, error) {
    var assessments []plugin.AssessmentLog
    for _, target := range req.Targets {
        assessments = append(assessments, plugin.AssessmentLog{
            RequirementID: target.TargetID + "-check",
            Steps: []plugin.Step{{
                Name:    "my-check",
                Result:  plugin.ResultPassed,
                Message: "check passed",
            }},
            Message:    "evaluation complete",
            Confidence: plugin.ConfidenceLevelHigh,
        })
    }
    return &plugin.ScanResponse{Assessments: assessments}, nil
}

func main() {
    plugin.Serve(&myPlugin{})
}

Build and install:

go build -o complyctl-provider-myplugin ./cmd/myplugin
cp complyctl-provider-myplugin ~/.complytime/providers/

Routing#

The CLI routes requests based on evaluator ID extracted from the Gemara policy graph:

  1. Policy assessment configs include an evaluator_id field
  2. CLI groups configs by evaluator ID
  3. Each group is dispatched to the matching plugin
  4. If no match is found, the request is broadcast to all loaded plugins

Variables#

Plugins receive variables through a three-tier model (R48):

TierConfig LocationDelivered ViaScope
Globalcomplytime.yaml variablesGenerateRequest.GlobalVariablesWorkspace-wide
Targetcomplytime.yaml targets[].variablesGenerateRequest.TargetVariables (Generate) / Target.Variables (Scan)Per-target
TestAssessment plan parametersAssessmentConfiguration.ParametersPer-requirement

Plugins declare their required variable names via the Describe RPC (RequiredGlobalVariables, RequiredTargetVariables). The complyctl doctor command validates these exist in the workspace config (R51).

variables:
  output_dir: /tmp/scan-results

targets:
  - id: production-cluster
    policies:
      - nist-800-53-r5
    variables:
      kubeconfig: /path/to/kubeconfig
      namespace: default

Reference Implementation#

See cmd/test-plugin/main.go for a complete working example. Build with:

make build-test-plugin