harnessx

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 10 Imported by: 0

README

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
Variants Run one check definition as several attempt variants (e.g. alg: none casings) — sequential by default, or parallel
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
Baseline comparison Capture an expected response per resource and flag deviations, with pluggable comparison logic
Zero dependencies Core engine (root package) is pure Go standard library; optional subpackages (reporters, checkdef) pull in their own deps only when imported

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

    // Variants: run the same check definition once per variant instead
    // (mutually exclusive with Run/RunResource — see "Variants" below).
    Variants           []string
    VariantMode        VariantMode // VariantsSequential (default) or VariantsParallel
    RunVariant         VariantCheckFunc         // used when Scope == ScopeGlobal
    RunResourceVariant VariantResourceCheckFunc // used when Scope == ScopePerResource

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

Hand-assembling ID, Name, Description, Link, Tags, and DependsOn in Go gets repetitive for a reusable check package. The checkdef subpackage parses that metadata from an embedded YAML, TOML, or JSON file instead:

import "github.com/cerberauth/harnessx/checkdef"

type CheckDef = checkdef.CheckDef // ID, Name, Description, Link, Tags, DependsOn, CVSSVector, CVSSScore, CWEID, CAPECID, OWASP, Extra

func MustParseCheckDefYAML(pkg string, data []byte) CheckDef
func MustParseCheckDefTOML(pkg string, data []byte) CheckDef
func MustParseCheckDefJSON(pkg string, data []byte) CheckDef

Each MustParse* panics (prefixed with pkg) on malformed input — definitions are typically parsed once at init() time from an //go:embedded file, so a bad definition is a build-time bug, not a runtime error to recover from.

# check.yaml
id: alg_none
name: "Algorithm None"
description: "Tests if the server accepts tokens with the algorithm set to 'none'."
link: "https://example.com/vulnerabilities/jwt-alg-none"
tags:
  - algorithm
depends_on:
  - baseline
cvss_vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N"
cvss_score: 9.3
cwe_id: "CWE-345"
capec_id: "CAPEC-31"
owasp: "API2:2023"
extra:
  custom_field: custom_value # def-specific fields this package doesn't model yet

NewCheck / NewResourceCheck turn a CheckDef plus a run function into a harnessx.Check, so a check only supplies what makes it different — the metadata fields are wired for you:

func NewCheck(def CheckDef, run harnessx.CheckFunc, opts ...Option) harnessx.Check         // Scope: ScopeGlobal
func NewResourceCheck(def CheckDef, run harnessx.ResourceCheckFunc, opts ...Option) harnessx.Check // Scope: ScopePerResource

// Same wiring, but run is invoked once per WithVariants entry — see "Variants".
func NewVariantCheck(def CheckDef, run harnessx.VariantCheckFunc, opts ...Option) harnessx.Check                 // Scope: ScopeGlobal
func NewVariantResourceCheck(def CheckDef, run harnessx.VariantResourceCheckFunc, opts ...Option) harnessx.Check // Scope: ScopePerResource

func WithSkip(s harnessx.SkipDecision) Option
func WithConditions(c ...harnessx.Condition) Option
func WithTimeout(d time.Duration) Option
func WithConcurrency(n int) Option // NewResourceCheck / NewVariantResourceCheck only
func WithVariants(variants ...string) Option
func WithVariantMode(mode harnessx.VariantMode) Option // default VariantsSequential
//go:embed check.yaml
var checkYAML []byte

var def = checkdef.MustParseCheckDefYAML("algnone", checkYAML)

var Check = checkdef.NewCheck(def, run,
    checkdef.WithSkip(harnessx.SkipWhen(func(ctx context.Context, t harnessx.Target, _ harnessx.ResultStore) string {
        if offline {
            return "requires live server"
        }
        return ""
    })),
)

checkdef is a separate package from root harnessx so the core engine keeps its zero-dependency guarantee — the YAML/TOML parsers are only pulled in by code that imports checkdef.

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)
Variants

Some checks are one exploit with several equivalent forms — a JWT alg: none bypass tried as "none", "NONE", "None". Rather than registering three near-duplicate checks (same ID, Skip, Conditions, DependsOn), give a single check a Variants list and a variant-aware run function. The engine invokes it once per variant and merges the results back into one Result:

algNone := harnessx.Check{
    ID:       "alg-none",
    Scope:    harnessx.ScopeGlobal,
    Variants: []string{"none", "NONE", "None"},
    RunVariant: func(ctx context.Context, t harnessx.Target, variant string, _ harnessx.ResultStore) (harnessx.Result, error) {
        if accepted := tryAlgNone(ctx, t, variant); accepted {
            return harnessx.Result{
                Observations: []harnessx.Observation{{Title: "server accepted alg=" + variant}},
            }, nil
        }
        return harnessx.Result{}, nil
    },
}
  • VariantMode controls how variants run: VariantsSequential (the default, zero value) runs them one at a time in list order; VariantsParallel runs them concurrently.
  • Each variant's outcome is recorded as an Attempt on the merged Result.Attempts, so a check that "found nothing" but tried five variants is distinguishable from one that never ran.
  • Observation.Variant is auto-filled with the variant name when a variant's run function leaves it empty, so downstream reporters and the Result.Observations slice know which variant produced each finding.
  • A panic in one variant is recovered and recorded on that variant's Attempt.Err — it does not abort the other variants.
  • ScopePerResource checks use RunResourceVariant instead of RunVariant, and run every variant against every resource.
type VariantCheckFunc func(ctx context.Context, target Target, variant string, store ResultStore) (Result, error)
type VariantResourceCheckFunc func(ctx context.Context, target Target, resource Resource, variant string, store ResultStore) (Result, error)

type VariantMode int
const (
    VariantsSequential VariantMode = iota // default
    VariantsParallel
)

type Attempt struct {
    Variant      string
    Observations []Observation
    Resources    []Resource
    Duration     time.Duration
    Data         any
    Err          error
}

checkdef.NewVariantCheck / checkdef.NewVariantResourceCheck wire a CheckDef plus a variant run function the same way NewCheck / NewResourceCheck do — see Check Definitions above.

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}
Selecting checks for a run

WithOnly and WithExclude narrow a single Engine.Run call to a subset of the engine's registered checks, without touching the registration itself. Unlike Scenario, the dependency graph is still built from Register/WithChecks — dropping a check that another depends on fails with ErrUnknownDependency, same as RunScenario.

// Only run these checks this time.
summary, err := engine.Run(ctx, target, harnessx.WithOnly("auth-check", "schema-check"))

// Run everything except these.
summary, err := engine.Run(ctx, target, harnessx.WithExclude("slow-check"))

WithExclude wins over WithOnly when both are passed and an ID appears in each.

Selecting by security metadata

Checks carry CVSSVector, CVSSScore, CWEID, CAPECID, and OWASP fields (populated automatically when a check is built from a checkdef.CheckDef). The metadata RunOptions narrow a run by those fields instead of by ID:

// Critical findings only this run.
summary, err := engine.Run(ctx, target, harnessx.WithMinCVSSScore(9.0))

// Only checks mapped to specific OWASP API categories / CWEs.
summary, err := engine.Run(ctx, target,
    harnessx.WithOWASP("API2:2023", "API3:2023"),
    harnessx.WithCWEID("CWE-287"),
)

// Arbitrary predicate.
summary, err := engine.Run(ctx, target, harnessx.WithFilter(func(c harnessx.Check) bool {
    return strings.HasPrefix(c.CAPECID, "CAPEC-1")
}))

Available: WithFilter, WithMinCVSSScore, WithMaxCVSSScore, WithCVSSScoreRange, WithCVSSVector, WithCWEID, WithCAPECID, WithOWASP. They are AND-combined with each other and with WithOnly; WithCWEID / WithCAPECID / WithOWASP match case-insensitively against any of the values passed. Dependencies are never dropped by a metadata filter — when a selected check DependsOn a check that doesn't match, that dependency is pulled back in so the graph stays valid. Only WithExclude removes a dependency.

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.

For a check with Variants, OnCheckComplete still fires once with the merged Result — inspect Result.Attempts to see the outcome of each variant individually (the built-in OTelReporter records one span event per attempt, tagged with its variant).

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

Baseline comparison detects the case where an endpoint's response changes in a way that indicates a bug — the canonical example being an endpoint that normally answers 401 Unauthorized but, because of a broken authorization check, answers 200 OK instead.

A Baseline is just a Snapshot{StatusCode int, Header http.Header, Body []byte, Duration time.Duration, Data any} — by default only the status code is compared, but Header/Body/Duration carry the full response, and Data can carry anything else, for custom comparators to inspect.

type Snapshot struct {
    StatusCode int
    Header     http.Header
    Body       []byte
    Duration   time.Duration
    Data       any
}
type Baseline = Snapshot

type BaselineSource func(ctx context.Context, target Target, resource Resource, store ResultStore) (Baseline, bool)
type BaselineComparator func(baseline, current Snapshot) []Observation

A baseline is obtained one of two ways:

  • Baseline probe — a dedicated check captures the expected response at scan time and stores it via CaptureBaselineCheck; downstream checks read it back with BaselineFromCheck(id).
  • Manual — a fixed value via StaticBaseline(b), or a per-resource value attached to Resource.Data at discovery time and read back with BaselineFromResource().

NewBaselineCheck wires a BaselineSource, a Capture function, and an optional Compare (defaults to CompareStatusCode) into a normal Check:

// Baseline captured once, at runtime, per resource.
probeCheck := harnessx.CaptureBaselineCheck("baseline-probe", "Baseline Probe",
    func(ctx context.Context, t harnessx.Target, r harnessx.Resource, _ harnessx.ResultStore) (harnessx.Snapshot, error) {
        return captureUnauthenticated(ctx, r.URL) // your capture logic
    })

// Compared against a later, potentially malicious, attempt.
bypassCheck := harnessx.NewBaselineCheck(harnessx.BaselineCheckConfig{
    ID:        "auth-bypass",
    DependsOn: []harnessx.CheckID{"baseline-probe"},
    Baseline:  harnessx.BaselineFromCheck("baseline-probe"),
    Capture: func(ctx context.Context, t harnessx.Target, r harnessx.Resource, _ harnessx.ResultStore) (harnessx.Snapshot, error) {
        return captureWithForgedHeader(ctx, r.URL) // your capture logic
    },
    // Compare defaults to CompareStatusCode; override for custom semantics,
    // e.g. only flag a denied -> allowed transition:
    Compare: func(baseline, current harnessx.Snapshot) []harnessx.Observation {
        if baseline.StatusCode >= 400 && current.StatusCode < 300 {
            return []harnessx.Observation{{Title: "Authorization bypass"}}
        }
        return nil
    },
})

For targets with no per-resource dimension (a single token, a single endpoint), use the ScopeGlobal counterparts — NewGlobalBaselineCheck, CaptureGlobalBaselineCheck, StaticGlobalBaseline, BaselineFromGlobalCheck — same shape, minus the Resource param.

For checks that probe by swapping/omitting a credential (token, API key, session ID), harnessx.ProbeAndCompareBaseline takes a probe.RequestBuilder and skips the Capture/Compare wiring entirely:

_, vulnerable, err := harnessx.ProbeAndCompareBaseline(ctx, p,
    func(ctx context.Context) (*http.Request, error) {
        return probe.NewRequest(ctx, http.MethodGet, resource.URL, nil, probe.WithBearerToken(forgedToken))
    }, store, "baseline-probe")

It builds the request via probe.NewRequest plus a named credential mutator (WithBearerToken, WithBasicAuth, WithAPIKeyHeader, WithAPIKeyQuery, WithAuthCookie, WithFormCredential), sends it via probe.Do, and diffs the resulting Snapshot against the baseline stored under "baseline-probe".

See the Baseline Comparison guide and examples/baseline-scan for a full runnable scenario.


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.
  • Baseline Scan: Detects an authorization bypass by comparing live HTTP responses against a per-resource baseline — one captured at runtime, one defined manually.

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, optionally narrowed
// by WithOnly / WithExclude for this call only.
// Always calls Reporter.OnScanComplete before returning.
func (e *Engine) Run(ctx context.Context, target Target, opts ...RunOption) (ScanSummary, error)

// WithOnly restricts a Run call to the given check IDs.
func WithOnly(ids ...CheckID) RunOption

// WithExclude removes the given check IDs from a Run call.
func WithExclude(ids ...CheckID) RunOption

// 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
Baseline Comparison
type Snapshot struct { StatusCode int; Header http.Header; Body []byte; Duration time.Duration; Data any }
type Baseline = Snapshot

// BaselineSource resolves the baseline for a resource; ok=false skips the check.
type BaselineSource func(ctx context.Context, target Target, resource Resource, store ResultStore) (Baseline, bool)

func StaticBaseline(b Baseline) BaselineSource
func BaselineFromResource() BaselineSource
func BaselineFromCheck(id CheckID) BaselineSource

// BaselineComparator judges the baseline against a freshly captured snapshot.
type BaselineComparator func(baseline, current Snapshot) []Observation

// CompareStatusCode is the default BaselineComparator.
func CompareStatusCode(baseline, current Snapshot) []Observation

type BaselineCheckConfig struct {
    ID, Name, Description string
    DependsOn             []CheckID
    Baseline              BaselineSource
    Capture               func(ctx context.Context, target Target, resource Resource, store ResultStore) (Snapshot, error)
    Compare               BaselineComparator // nil -> CompareStatusCode
    Timeout               time.Duration
    Concurrency           int
}

// NewBaselineCheck builds a ScopePerResource Check from cfg.
func NewBaselineCheck(cfg BaselineCheckConfig) Check

// CaptureBaselineCheck builds a ScopePerResource Check that captures a
// Snapshot per resource and stores it as Result.Data — the "baseline probe".
func CaptureBaselineCheck(id CheckID, name string, capture func(ctx context.Context, target Target, resource Resource, store ResultStore) (Snapshot, error)) Check

// ScopeGlobal counterparts, for targets with no per-resource dimension.
type GlobalBaselineSource func(ctx context.Context, target Target, store ResultStore) (Baseline, bool)
type GlobalCapture func(ctx context.Context, target Target, store ResultStore) (Snapshot, error)

func StaticGlobalBaseline(b Baseline) GlobalBaselineSource
func BaselineFromGlobalCheck(id CheckID) GlobalBaselineSource

type GlobalBaselineCheckConfig struct {
    ID, Name, Description string
    DependsOn              []CheckID
    Baseline               GlobalBaselineSource
    Capture                GlobalCapture
    Compare                BaselineComparator // nil -> CompareStatusCode
    Timeout                time.Duration
}

func NewGlobalBaselineCheck(cfg GlobalBaselineCheckConfig) Check
func CaptureGlobalBaselineCheck(id CheckID, name string, capture GlobalCapture) Check

// probe package: build a request from method+URL, then apply small
// composable mutators to it.
type RequestMutator func(*http.Request) error
type RequestBuilder func(ctx context.Context) (*http.Request, error)

func NewRequest(ctx context.Context, method, target string, body io.Reader, mutators ...RequestMutator) (*http.Request, error)
func WithHeader(name, value string) RequestMutator
func WithCookie(cookie *http.Cookie) RequestMutator
func WithQuery(name, value string) RequestMutator

// Named credential mutators — one per auth use case.
func WithBearerToken(token string) RequestMutator
func WithBasicAuth(username, password string) RequestMutator
func WithAPIKeyHeader(name, value string) RequestMutator
func WithAPIKeyQuery(name, value string) RequestMutator
func WithAuthCookie(name, value string) RequestMutator
func WithFormCredential(name, value string) RequestMutator

// NewRequestFromResource builds a request for r, defaulting to GET when
// r.Method is empty.
func NewRequestFromResource(ctx context.Context, r Resource, mutators ...probe.RequestMutator) (*http.Request, error)

// ProbeAndCompareBaseline sends the request returned by build via probe.Do,
// and compares the resulting Snapshot against the baseline stored under baselineID.
func ProbeAndCompareBaseline(ctx context.Context, p *probe.Probe, build probe.RequestBuilder, store ResultStore, baselineID CheckID) (Snapshot, bool, error)
Variants
type VariantCheckFunc func(ctx context.Context, target Target, variant string, store ResultStore) (Result, error)
type VariantResourceCheckFunc func(ctx context.Context, target Target, resource Resource, variant string, store ResultStore) (Result, error)

type VariantMode int
const (
    VariantsSequential VariantMode = iota // default: variants run one at a time, in order
    VariantsParallel                      // variants run concurrently
)

// Attempt records the outcome of a single variant run.
type Attempt struct {
    Variant      string
    Observations []Observation
    Resources    []Resource
    Duration     time.Duration
    Data         any
    Err          error
}

// checkdef helpers, same wiring as NewCheck / NewResourceCheck.
func checkdef.NewVariantCheck(def CheckDef, run harnessx.VariantCheckFunc, opts ...checkdef.Option) harnessx.Check
func checkdef.NewVariantResourceCheck(def CheckDef, run harnessx.VariantResourceCheckFunc, opts ...checkdef.Option) harnessx.Check
func checkdef.WithVariants(variants ...string) checkdef.Option
func checkdef.WithVariantMode(mode harnessx.VariantMode) checkdef.Option
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.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrCycleDetected     = errors.New("harnessx: dependency cycle detected")
	ErrDuplicateCheckID  = errors.New("harnessx: duplicate check ID")
	ErrUnknownDependency = errors.New("harnessx: unknown dependency check ID")
	ErrNoChecks          = errors.New("harnessx: no checks registered")
)

Functions

func DataAs

func DataAs[T any](r Result) (T, bool)

func GetData

func GetData[T any](store ResultStore, id CheckID) (T, bool)

func NewRequestFromResource added in v0.3.0

func NewRequestFromResource(ctx context.Context, r Resource, mutators ...probe.RequestMutator) (*http.Request, error)

NewRequestFromResource builds a request for resource r via probe.NewRequest, defaulting to GET when r.Method is empty.

func ResourceDataAs added in v0.1.2

func ResourceDataAs[T any](resource Resource) (T, bool)

Types

type Attempt added in v0.3.0

type Attempt struct {
	Variant      string
	Observations []Observation
	Resources    []Resource
	Duration     time.Duration
	Data         any
	Err          error
}

type Baseline added in v0.3.0

type Baseline = Snapshot

Baseline is a Snapshot held up as the expected/reference response for a resource.

type BaselineCheckConfig added in v0.3.0

type BaselineCheckConfig struct {
	ID          CheckID
	Name        string
	Description string
	DependsOn   []CheckID

	// Baseline resolves the expected response for a resource.
	Baseline BaselineSource

	// Capture performs the live attempt and returns its snapshot.
	Capture func(ctx context.Context, target Target, resource Resource, store ResultStore) (Snapshot, error)

	// Compare judges the baseline against the captured snapshot.
	// Defaults to CompareStatusCode when nil.
	Compare BaselineComparator

	Timeout     time.Duration
	Concurrency int
}

BaselineCheckConfig configures a baseline-comparison check built by NewBaselineCheck.

type BaselineComparator added in v0.3.0

type BaselineComparator func(baseline, current Snapshot) []Observation

BaselineComparator compares a baseline against a freshly captured snapshot and returns any resulting Observations. A nil or empty result means no deviation was found.

type BaselineSource added in v0.3.0

type BaselineSource func(ctx context.Context, target Target, resource Resource, store ResultStore) (Baseline, bool)

BaselineSource resolves the baseline for a resource. It returns ok=false when no baseline is available, in which case the comparison check skips.

func BaselineFromCheck added in v0.3.0

func BaselineFromCheck(id CheckID) BaselineSource

BaselineFromCheck returns a BaselineSource that reads the baseline captured for a resource by a prior "baseline probe" check (see CaptureBaselineCheck), via that check's per-resource Result.Data.

func BaselineFromResource added in v0.3.0

func BaselineFromResource() BaselineSource

BaselineFromResource returns a BaselineSource that reads a baseline manually attached to Resource.Data at discovery time.

func StaticBaseline added in v0.3.0

func StaticBaseline(b Baseline) BaselineSource

StaticBaseline returns a BaselineSource that always resolves to the same fixed baseline, regardless of target or resource.

type Check

type Check struct {
	ID          CheckID
	Name        string
	Description string
	Link        string
	Tags        []string
	DependsOn   []CheckID
	Conditions  []Condition

	// Security metadata, mirrored from checkdef.CheckDef. Purely
	// descriptive — the engine only reads these for run-time check
	// selection (see WithMinCVSSScore, WithCWEID, WithOWASP, ...).
	CVSSVector string
	CVSSScore  float64
	CWEID      string
	CAPECID    string
	OWASP      string

	Skip SkipDecision

	Scope       CheckScope
	Run         CheckFunc
	RunResource ResourceCheckFunc

	Variants           []string
	VariantMode        VariantMode
	RunVariant         VariantCheckFunc
	RunResourceVariant VariantResourceCheckFunc

	Timeout     time.Duration
	Concurrency int
}

func CaptureBaselineCheck added in v0.3.0

func CaptureBaselineCheck(id CheckID, name string, capture func(ctx context.Context, target Target, resource Resource, store ResultStore) (Snapshot, error)) Check

CaptureBaselineCheck builds a ScopePerResource Check that captures a Snapshot per resource via capture and stores it as Result.Data — the "baseline probe". Pair it with BaselineFromCheck(id) and a DependsOn on this check's id in the comparison check.

func CaptureGlobalBaselineCheck added in v0.3.0

func CaptureGlobalBaselineCheck(id CheckID, name string, capture GlobalCapture) Check

CaptureGlobalBaselineCheck is the ScopeGlobal counterpart of CaptureBaselineCheck, for targets with no per-resource dimension.

func NewBaselineCheck added in v0.3.0

func NewBaselineCheck(cfg BaselineCheckConfig) Check

NewBaselineCheck builds a ScopePerResource Check that resolves cfg.Baseline, captures the current response via cfg.Capture, and compares them via cfg.Compare (CompareStatusCode by default). A resource with no resolvable baseline is skipped rather than compared.

func NewGlobalBaselineCheck added in v0.3.0

func NewGlobalBaselineCheck(cfg GlobalBaselineCheckConfig) Check

NewGlobalBaselineCheck is the ScopeGlobal counterpart of NewBaselineCheck, for targets with no per-resource dimension (see BaselineCheckConfig for the ScopePerResource variant).

type CheckFunc

type CheckFunc func(ctx context.Context, target Target, store ResultStore) (Result, error)

type CheckID

type CheckID string

type CheckScope

type CheckScope int
const (
	ScopeGlobal CheckScope = iota
	ScopePerResource
)

type Condition

type Condition func(store ResultStore) bool

func All

func All(conditions ...Condition) Condition

func Any

func Any(conditions ...Condition) Condition

func IfCheckObserved added in v0.1.1

func IfCheckObserved(id CheckID) Condition

func IfCheckPassed

func IfCheckPassed(id CheckID) Condition

func IfCheckSkipped

func IfCheckSkipped(id CheckID) Condition

func Not

func Not(c Condition) Condition

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

func New

func New(opts ...Option) *Engine

func (*Engine) Register

func (e *Engine) Register(checks ...Check) error

func (*Engine) Run

func (e *Engine) Run(ctx context.Context, target Target, opts ...RunOption) (ScanSummary, error)

func (*Engine) RunScenario added in v0.1.2

func (e *Engine) RunScenario(ctx context.Context, target Target, scenario Scenario) (ScanSummary, error)

RunScenario executes the checks in scenario against target using the engine's configured reporters, concurrency limits, and default timeout. It does not consult or modify the engine's registered check list — scenario.Checks is fully self-contained.

Reporter.OnScanComplete is always called before returning, even if scenario.Checks is empty.

type GlobalBaselineCheckConfig added in v0.3.0

type GlobalBaselineCheckConfig struct {
	ID          CheckID
	Name        string
	Description string
	DependsOn   []CheckID

	Baseline GlobalBaselineSource

	Capture GlobalCapture

	Compare BaselineComparator

	Timeout time.Duration
}

GlobalBaselineCheckConfig is the ScopeGlobal counterpart of BaselineCheckConfig.

type GlobalBaselineSource added in v0.3.0

type GlobalBaselineSource func(ctx context.Context, target Target, store ResultStore) (Baseline, bool)

GlobalBaselineSource is the ScopeGlobal counterpart of BaselineSource: it resolves the Baseline to compare against for a target with no per-resource dimension.

func BaselineFromGlobalCheck added in v0.3.0

func BaselineFromGlobalCheck(id CheckID) GlobalBaselineSource

BaselineFromGlobalCheck is the ScopeGlobal counterpart of BaselineFromCheck: it reads the baseline from the global (non-per-resource) result of a prior check (see CaptureGlobalBaselineCheck).

func StaticGlobalBaseline added in v0.3.0

func StaticGlobalBaseline(b Baseline) GlobalBaselineSource

StaticGlobalBaseline is the ScopeGlobal counterpart of StaticBaseline.

type GlobalCapture added in v0.3.0

type GlobalCapture func(ctx context.Context, target Target, store ResultStore) (Snapshot, error)

GlobalCapture is the ScopeGlobal counterpart of the Capture func used by BaselineCheckConfig: it captures the current Snapshot for a target with no per-resource dimension.

type Observation added in v0.1.1

type Observation struct {
	CheckID     CheckID
	ResourceID  string
	Variant     string
	Title       string
	Description string
	Evidence    string
	Metadata    map[string]string
}

func CompareStatusCode added in v0.3.0

func CompareStatusCode(baseline, current Snapshot) []Observation

CompareStatusCode is the default BaselineComparator: it flags any change in status code between the baseline and the current snapshot.

type Option

type Option func(*engineConfig)

func WithChecks

func WithChecks(checks ...Check) Option

func WithDefaultTimeout

func WithDefaultTimeout(d time.Duration) Option

func WithMaxConcurrency

func WithMaxConcurrency(n int) Option

func WithMaxResourceConcurrency

func WithMaxResourceConcurrency(n int) Option

func WithReporters

func WithReporters(reporters ...Reporter) Option

type Reporter

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

type Resource

type Resource struct {
	ID       string
	URL      string
	Method   string
	Metadata map[string]string
	Data     any
}

type ResourceCheckFunc

type ResourceCheckFunc func(ctx context.Context, target Target, resource Resource, store ResultStore) (Result, error)

type Result

type Result struct {
	CheckID      CheckID
	ResourceID   string
	Observations []Observation
	Resources    []Resource
	Skipped      bool
	SkipReason   string
	Duration     time.Duration
	Metadata     map[string]string
	Data         any
	Err          error
	Attempts     []Attempt
}

func DataResult

func DataResult(data any) Result

DataResult returns a Result carrying data without a CheckID. The engine always sets CheckID after the run, so callers inside a Run func do not need to supply the ID themselves.

func ResultData

func ResultData(id CheckID, data any) Result

func Skip

func Skip(id CheckID, reason string) Result

type ResultStore

type ResultStore interface {
	Get(id CheckID) (Result, bool)
	GetForResource(id CheckID, resourceID string) (Result, bool)
	Observations() []Observation
	Resources() []Resource
}

type RunOption added in v0.4.0

type RunOption func(*runConfig)

RunOption customizes which registered checks execute for a single Engine.Run call.

func WithCAPECID added in v0.4.0

func WithCAPECID(ids ...string) RunOption

WithCAPECID keeps only checks whose CAPECID matches one of the given IDs (case-insensitive, e.g. "CAPEC-31").

func WithCVSSScoreRange added in v0.4.0

func WithCVSSScoreRange(min, max float64) RunOption

WithCVSSScoreRange keeps only checks whose CVSSScore is within [min, max].

func WithCVSSVector added in v0.4.0

func WithCVSSVector(vectors ...string) RunOption

WithCVSSVector keeps only checks whose CVSSVector exactly matches one of the given vectors.

func WithCWEID added in v0.4.0

func WithCWEID(ids ...string) RunOption

WithCWEID keeps only checks whose CWEID matches one of the given IDs (case-insensitive, e.g. "CWE-345").

func WithExclude added in v0.4.0

func WithExclude(ids ...CheckID) RunOption

WithExclude removes the given check IDs from a run.

func WithFilter added in v0.4.0

func WithFilter(keep func(Check) bool) RunOption

WithFilter restricts a run to checks for which keep returns true. Multiple WithFilter options (and the WithCVSS*/WithCWEID/WithCAPECID/WithOWASP helpers, which are built on it) are AND-combined. A check that a selected check depends on is kept even when it fails the filter, so the dependency graph stays valid — only WithExclude drops a dependency.

func WithMaxCVSSScore added in v0.4.0

func WithMaxCVSSScore(max float64) RunOption

WithMaxCVSSScore keeps only checks whose CVSSScore is <= max.

func WithMinCVSSScore added in v0.4.0

func WithMinCVSSScore(min float64) RunOption

WithMinCVSSScore keeps only checks whose CVSSScore is >= min.

func WithOWASP added in v0.4.0

func WithOWASP(ids ...string) RunOption

WithOWASP keeps only checks whose OWASP identifier matches one of the given values (case-insensitive, e.g. "API2:2023" or "A01:2021").

func WithOnly added in v0.4.0

func WithOnly(ids ...CheckID) RunOption

WithOnly restricts a run to the given check IDs. Combine with WithExclude to further narrow the set; excluded IDs win over included ones.

type ScanError

type ScanError struct {
	CheckID CheckID
	Cause   error
}

func (*ScanError) Error

func (e *ScanError) Error() string

func (*ScanError) Unwrap

func (e *ScanError) Unwrap() error

type ScanSummary

type ScanSummary struct {
	Target       Target
	TotalChecks  int
	Executed     int
	Skipped      int
	Failed       int
	Observations []Observation
	Results      []Result
	Duration     time.Duration
	Err          error
}

type Scenario added in v0.1.2

type Scenario struct {
	ID          string
	Name        string
	Description string
	Tags        []string
	Checks      []Check
}

Scenario is a named, ordered subset of checks to execute against a target. Pass it to Engine.RunScenario to execute only those checks using the engine's configured reporters, concurrency limits, and default timeout.

Checks can be shared across scenarios by referencing the same CheckFunc variable from multiple Check values, each with its own Check.DependsOn wiring — so the same business logic can run at different points in different scenario dependency graphs.

type SkipDecision

type SkipDecision struct {
	// contains filtered or unexported fields
}

func SkipAlways

func SkipAlways(reason string) SkipDecision

func SkipResourceWhen added in v0.2.0

func SkipResourceWhen(fn func(ctx context.Context, target Target, resource Resource, store ResultStore) string) SkipDecision

SkipResourceWhen builds a skip decision evaluated once per resource for a ScopePerResource check, instead of once for the whole check. Use this when the decision depends on the resource itself (e.g. its security scheme) rather than on the target or environment.

func SkipWhen

func SkipWhen(fn func(ctx context.Context, target Target, store ResultStore) string) SkipDecision

func (SkipDecision) Eval added in v0.1.2

func (s SkipDecision) Eval(ctx context.Context, target Target, store ResultStore) string

Eval runs the skip decision and returns a non-empty skip reason if the check should be skipped, or "" if it should run.

func (SkipDecision) EvalResource added in v0.2.0

func (s SkipDecision) EvalResource(ctx context.Context, target Target, resource Resource, store ResultStore) string

EvalResource runs the per-resource skip decision, falling back to Eval (the check-wide decision) when no resource-specific decision was set.

type Snapshot added in v0.3.0

type Snapshot struct {
	StatusCode int
	Header     http.Header
	Body       []byte
	Duration   time.Duration
	Data       any
}

Snapshot is a captured response, reduced to whatever a comparator needs to judge it. StatusCode is the convenience field the default comparator uses; Header, Body, and Duration carry the full response for comparators that need more; Data carries anything else custom comparators want to inspect.

func ProbeAndCompareBaseline added in v0.3.0

func ProbeAndCompareBaseline(ctx context.Context, p *probe.Probe, build probe.RequestBuilder, store ResultStore, baselineID CheckID) (Snapshot, bool, error)

ProbeAndCompareBaseline sends the request returned by build via probe.Do, keeping the full response (headers, body, duration) in the Snapshot for comparators that need more than the status code, and compares the resulting snapshot against the baseline stored under baselineID. It returns the current snapshot and whether the response deviated from the baseline.

type StaticResultStore added in v0.1.2

type StaticResultStore struct {
	// contains filtered or unexported fields
}

StaticResultStore is a ResultStore fixture for tests: seed it with the Results a check's Skip decision or Conditions should see, without running the Engine.

func NewStaticResultStore added in v0.1.2

func NewStaticResultStore(results ...Result) *StaticResultStore

func (*StaticResultStore) Get added in v0.1.2

func (s *StaticResultStore) Get(id CheckID) (Result, bool)

func (*StaticResultStore) GetForResource added in v0.1.2

func (s *StaticResultStore) GetForResource(id CheckID, _ string) (Result, bool)

func (*StaticResultStore) Observations added in v0.1.2

func (s *StaticResultStore) Observations() []Observation

func (*StaticResultStore) Resources added in v0.1.2

func (s *StaticResultStore) Resources() []Resource

type Target

type Target struct {
	URL      string
	Host     string
	Metadata map[string]string
	Data     any
}

type VariantCheckFunc added in v0.3.0

type VariantCheckFunc func(ctx context.Context, target Target, variant string, store ResultStore) (Result, error)

type VariantMode added in v0.3.0

type VariantMode int
const (
	VariantsSequential VariantMode = iota
	VariantsParallel
)

type VariantResourceCheckFunc added in v0.3.0

type VariantResourceCheckFunc func(ctx context.Context, target Target, resource Resource, variant string, store ResultStore) (Result, error)

Directories

Path Synopsis
Package checkdef parses declarative check metadata (ID, name, description, dependencies, ...) out of an embedded definition file, so a reusable check package can keep that metadata in YAML/TOML/JSON instead of hand-assembling it in Go.
Package checkdef parses declarative check metadata (ID, name, description, dependencies, ...) out of an embedded definition file, so a reusable check package can keep that metadata in YAML/TOML/JSON instead of hand-assembling it in Go.
examples
advanced-scan command
baseline-scan command
multi-scenario command
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL