Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

harnessx

GitHub Workflow Status Latest version Codecov GoDoc reference

harnessx is a concurrent, dependency-aware check orchestration engine for Go. It lets you define a graph of checks with explicit dependencies, run them in parallel waves, and collect structured observations — without writing any scheduling or concurrency boilerplate. Common use cases include security scanning, compliance checks, health checks, and quality gates.


Features

Feature Description
DAG scheduling Checks are topologically sorted and executed in parallel waves
Two execution scopes ScopeGlobal runs once per target; ScopePerResource fans out over discovered resources
Resource discovery Global checks can emit Resource objects consumed by downstream per-resource checks
Conditional execution Skip checks based on prior results using composable Condition predicates
Skip decisions Skip a whole check or individual resources at runtime via SkipAlways / SkipWhen / SkipResourceWhen
Bounded concurrency Separate semaphores for level-wide and per-resource parallelism
Panic recovery A panicking check is recorded as failed; the scan continues uninterrupted
Context cancellation Full context.Context propagation with per-check timeouts
Reporter hooks Real-time OnCheckStart / OnCheckComplete / OnScanComplete callbacks
Structured observations Checks emit typed observations with title, description, evidence, and free-form metadata
Scenarios Run named check subsets (REST scan, GraphQL scan) without executing all registered checks
Zero dependencies Pure Go standard library — no external runtime dependencies

Installation

go get github.com/cerberauth/harnessx

Requires Go 1.22+.


Quick Start

package main

import (
    "context"
    "fmt"
    "net/http"

    "github.com/cerberauth/harnessx"
)

func main() {
    // 1. Describe the target.
    target := harnessx.Target{URL: "https://example.com", Host: "example.com"}

    // 2. Define checks.
    tlsCheck := harnessx.Check{
        ID:    "tls",
        Name:  "TLS configuration",
        Scope: harnessx.ScopeGlobal,
        Run: func(ctx context.Context, t harnessx.Target, _ harnessx.ResultStore) (harnessx.Result, error) {
            resp, err := http.Get(t.URL)
            if err != nil || !resp.TLS.HandshakeComplete {
                return harnessx.Result{
                    Observations: []harnessx.Observation{{
                        Title: "TLS not negotiated",
                    }},
                }, nil
            }
            return harnessx.Result{}, nil
        },
    }

    headerCheck := harnessx.Check{
        ID:        "headers",
        Name:      "Security headers",
        Scope:     harnessx.ScopeGlobal,
        DependsOn: []harnessx.CheckID{"tls"},
        // Only runs if TLS passed cleanly.
        Conditions: []harnessx.Condition{harnessx.IfCheckPassed("tls")},
        Run: func(ctx context.Context, t harnessx.Target, _ harnessx.ResultStore) (harnessx.Result, error) {
            resp, err := http.Get(t.URL)
            if err != nil {
                return harnessx.Result{}, err
            }
            var observations []harnessx.Observation
            if resp.Header.Get("Strict-Transport-Security") == "" {
                observations = append(observations, harnessx.Observation{
                    Title: "Missing HSTS header",
                })
            }
            return harnessx.Result{Observations: observations}, nil
        },
    }

    // 3. Create the engine and register checks.
    engine := harnessx.New(
        harnessx.WithMaxConcurrency(4),
        harnessx.WithChecks(tlsCheck, headerCheck),
    )

    // 4. Run the scan.
    summary, err := engine.Run(context.Background(), target)
    if err != nil {
        panic(err)
    }

    fmt.Printf("Executed: %d  Skipped: %d  Failed: %d\n",
        summary.Executed, summary.Skipped, summary.Failed)
    for _, o := range summary.Observations {
        fmt.Printf("%s: %s\n", o.Title, o.Description)
    }
}

Concepts

Checks

A Check is the fundamental unit of work. Each check has a unique ID, an execution Scope, and either a Run or RunResource function.

type Check struct {
    ID          CheckID
    Name        string
    Description string
    Tags        []string

    // Dependency graph
    DependsOn  []CheckID
    Conditions []Condition // AND-evaluated; any false → skip

    // Skip control
    Skip SkipDecision // static/dynamic skip, optionally per-resource

    // Execution
    Scope       CheckScope     // ScopeGlobal or ScopePerResource
    Run         CheckFunc      // used when Scope == ScopeGlobal
    RunResource ResourceCheckFunc // used when Scope == ScopePerResource

    Timeout     time.Duration  // 0 → engine default (30s)
    Concurrency int            // per-resource parallelism; 0 → engine default
}

Scopes

Scope Runs Function
ScopeGlobal Once per scan Run(ctx, target, store) (Result, error)
ScopePerResource Once per resource discovered so far RunResource(ctx, target, resource, store) (Result, error)

Resource Discovery

A ScopeGlobal check can return a Resources slice in its result. Those resources are accumulated in the engine's store and made available to all subsequent ScopePerResource checks.

crawl := harnessx.Check{
    ID:    "crawl",
    Scope: harnessx.ScopeGlobal,
    Run: func(ctx context.Context, t harnessx.Target, _ harnessx.ResultStore) (harnessx.Result, error) {
        endpoints := discover(ctx, t.URL) // returns []Resource
        return harnessx.Result{Resources: endpoints}, nil
    },
}

probe := harnessx.Check{
    ID:          "probe",
    Scope:       harnessx.ScopePerResource,
    DependsOn:   []harnessx.CheckID{"crawl"},
    RunResource: func(ctx context.Context, t harnessx.Target, r harnessx.Resource, _ harnessx.ResultStore) (harnessx.Result, error) {
        // called once for each Resource returned by "crawl"
        return test(ctx, r), nil
    },
}

Conditions

Conditions gate whether a check runs. All conditions in a check's Conditions slice must pass (AND semantics). Built-in predicates:

Predicate Description
IfCheckPassed(id) Prior check completed with no observations and no error
IfCheckObserved(id) Prior check produced at least one observation
IfCheckSkipped(id) Prior check was skipped
All(c1, c2, ...) All conditions must hold
Any(c1, c2, ...) At least one condition must hold
Not(c) Negates a condition
// Run "deep-probe" only if "detect" produced any observations.
deepProbe := harnessx.Check{
    ID:         "deep-probe",
    Scope:      harnessx.ScopeGlobal,
    DependsOn:  []harnessx.CheckID{"detect"},
    Conditions: []harnessx.Condition{
        harnessx.IfCheckObserved("detect"),
    },
    Run: ...,
}

Skip Decisions

Skip gates whether a check (or, for ScopePerResource checks, an individual resource) runs at all — evaluated before Conditions and before the check function. A non-empty reason skips and records Result.SkipReason; reporters still receive OnCheckComplete for it.

type SkipDecision struct { /* built via SkipAlways / SkipWhen / SkipResourceWhen */ }

func SkipAlways(reason string) SkipDecision
func SkipWhen(fn func(ctx context.Context, target Target, store ResultStore) string) SkipDecision
func SkipResourceWhen(fn func(ctx context.Context, target Target, resource Resource, store ResultStore) string) SkipDecision
  • SkipAlways / SkipWhen are check-wide: for a ScopePerResource check, a non-empty reason skips the entire check once, before it fans out over resources.
  • SkipResourceWhen is evaluated once per resource, so different resources on the same check can be skipped for different reasons (or not at all).
  • If a check's Skip has no per-resource decision, per-resource evaluation falls back to the check-wide one.
// Skip the whole check if the target isn't HTTPS.
tlsOnly := harnessx.Check{
    ID:   "hsts-header",
    Skip: harnessx.SkipWhen(func(ctx context.Context, t harnessx.Target, _ harnessx.ResultStore) string {
        if !strings.HasPrefix(t.URL, "https://") {
            return "target is not HTTPS"
        }
        return ""
    }),
    Run: ...,
}

// Skip only resources that opted out via metadata.
endpointAuth := harnessx.Check{
    ID:    "endpoint-auth",
    Scope: harnessx.ScopePerResource,
    Skip: harnessx.SkipResourceWhen(func(ctx context.Context, t harnessx.Target, r harnessx.Resource, _ harnessx.ResultStore) string {
        if r.Metadata["auth"] == "none" {
            return "endpoint declares no auth"
        }
        return ""
    }),
    RunResource: ...,
}

Execution Model

  1. Checks are validated and sorted into parallel levels via Kahn's topological sort.
  2. All checks within a level execute concurrently, bounded by WithMaxConcurrency.
  3. Each level receives a frozen snapshot of the result store taken before any check in that level starts — intra-level races are impossible by design.
  4. ScopePerResource checks within a level fan out over all currently known resources, bounded by WithMaxResourceConcurrency (or the check's own Concurrency field).

Scenarios

A Scenario groups a named set of checks to be executed together. Use RunScenario instead of Run to execute only that subset — the engine's registered checks are ignored.

restScenario := harnessx.Scenario{
    ID:   "rest-api",
    Name: "REST API Scan",
    Checks: []harnessx.Check{discoveryCheck, authCheck, schemaCheck},
}

summary, err := engine.RunScenario(ctx, target, restScenario)

To share business logic across scenarios while varying the dependency order, define the Run function as a variable and reference it in multiple Check values with different DependsOn fields:

var checkAuthFn harnessx.CheckFunc = func(...) (harnessx.Result, error) { ... }

// REST: auth after endpoint discovery
restAuth := harnessx.Check{ID: "rest-auth", DependsOn: []harnessx.CheckID{"rest-discovery"}, Run: checkAuthFn}

// GraphQL: same logic, wired after schema introspection
gqlAuth  := harnessx.Check{ID: "gql-auth",  DependsOn: []harnessx.CheckID{"gql-introspection"}, Run: checkAuthFn}

Reporter

Implement the Reporter interface to receive real-time events:

type Reporter interface {
    OnScanStart(target Target, totalChecks int)
    OnCheckStart(check Check, target Target, resource *Resource)
    OnCheckComplete(result Result)
    OnScanComplete(summary ScanSummary)
}

OnScanStart fires before any check runs with the total registered check count — use it to initialise a progress bar. OnScanComplete is always called — even after a context cancellation or early error.

engine := harnessx.New(
    harnessx.WithReporters(myReporter, otherReporter),
)

Examples

  • Advanced Scan: A comprehensive example demonstrating multi-level dependencies, resource discovery, custom conditions, and a pretty-printing reporter.
  • Multi-Scenario Scan: REST API and GraphQL API scenarios sharing business logic with different dependency graphs. Select a scenario at runtime via CLI argument.

API Reference

Engine

// New creates a new Engine with the given options.
func New(opts ...Option) *Engine

// Register adds checks to the engine. Returns ErrDuplicateCheckID if any ID conflicts.
func (e *Engine) Register(checks ...Check) error

// Run executes all registered checks against target.
// Always calls Reporter.OnScanComplete before returning.
func (e *Engine) Run(ctx context.Context, target Target) (ScanSummary, error)

// RunScenario executes only the checks in scenario against target.
// Ignores checks registered via Register or WithChecks.
// Always calls Reporter.OnScanComplete before returning.
func (e *Engine) RunScenario(ctx context.Context, target Target, scenario Scenario) (ScanSummary, error)

Skip Decisions

// SkipAlways always returns reason — the check (or resource) is always skipped.
func SkipAlways(reason string) SkipDecision

// SkipWhen evaluates fn once for the whole check.
func SkipWhen(fn func(ctx context.Context, target Target, store ResultStore) string) SkipDecision

// SkipResourceWhen evaluates fn once per resource, for ScopePerResource checks.
func SkipResourceWhen(fn func(ctx context.Context, target Target, resource Resource, store ResultStore) string) SkipDecision

Options

Option Default Description
WithMaxConcurrency(n) runtime.NumCPU() Maximum checks running concurrently within a level
WithMaxResourceConcurrency(n) runtime.NumCPU() Default maximum resource goroutines per check
WithDefaultTimeout(d) 30s Per-check timeout when Check.Timeout is zero
WithReporters(reporters...) NoopReporter Real-time event callbacks (multiple reporters supported)
WithChecks(checks...) Register checks at construction time

Errors

Error Meaning
ErrNoChecks Run was called with no checks registered
ErrDuplicateCheckID Two checks share the same CheckID
ErrUnknownDependency A DependsOn entry references a non-existent check
ErrCycleDetected The dependency graph contains a cycle
*ScanError A check's Run/RunResource returned an error or panicked

License

This repository is licensed under the MIT License @ CerberAuth.

About

harnessx is a concurrent, dependency-aware security check orchestration engine for Go. It lets you define a graph of security checks with explicit dependencies, run them in parallel waves, and collect structured findings — without writing any scheduling or concurrency boilerplate.

Resources

Code of conduct

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages