client

package
v0.33.0 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: GPL-3.0 Imports: 50 Imported by: 0

Documentation

Overview

Package client provides the OpenCTEM API client.

Stability: Stable (docs/STABILITY.md).

Index

Constants

View Source
const (
	ReasonForcedByConfig      = "forced_by_config"
	ReasonNotKeyBound         = "not_key_bound"
	ReasonPlatformWithoutV3   = "platform_without_v3"
	ReasonPlatformWithoutGRPC = "platform_without_grpc"
	ReasonCertificates        = "certificates_unavailable"
	ReasonGRPCUnreachable     = "grpc_unreachable"
	ReasonHandshakeFailed     = "handshake_failed"
	ReasonHTTP2Refused        = "http2_refused"
	ReasonStreamReset         = "stream_reset"
	ReasonGRPCUnimplemented   = "grpc_unimplemented"
	ReasonGRPCFailed          = "grpc_failed"
	ReasonHTTPSUnavailable    = "https_binding_unavailable"
	ReasonIdentityRefused     = "identity_refused"
	// ReasonPlatformCertificate: the gRPC endpoint's certificate does not
	// verify against the pinned platform CA bundle. Never a fallback.
	ReasonPlatformCertificate = "platform_certificate_refused"
	// ReasonPlatformUnreachable: the platform did not answer the
	// negotiation (restarting, a gateway 502, the network). It says nothing
	// about v3: a working binding is kept, and the negotiation is retried
	// soon.
	ReasonPlatformUnreachable = "platform_unreachable"
)

Fallback reasons reported on the heartbeat.

View Source
const (
	// ProtocolAuto is the default. It means protocol v2.
	ProtocolAuto = "auto"
	// ProtocolV2 is protocol v2.
	ProtocolV2 = "v2"
)

Protocol setting (Config.Protocol, SENSOR_PROTOCOL). Protocol v2 is the only sensor protocol: the platform retired protocol v1 (/api/v1/agent/*) in 2026-10. "auto" and "v2" are the same; "v1" is refused.

View Source
const ContentPath = "/api/v2/sensor/content"

ContentPath is the prefix of the platform's sensor content routes: desired state, packs by digest, a pack's file index and its files.

View Source
const DefaultCommandPollLimit = 10

DefaultCommandPollLimit is how many commands GetCommands asks for.

View Source
const DefaultControlTimeout = 15 * time.Second

DefaultControlTimeout bounds one control request (a heartbeat) when Config.ControlTimeout is not set. The platform marks a sensor stale after 90 seconds without one; a heartbeat that has not been answered in 15 is better replaced by the next.

View Source
const DefaultOutboxSyncWait = 30 * time.Second

DefaultOutboxSyncWait is OutboxConfig.SyncWait's default.

View Source
const SensorALPN = "openctem-sensor/3"

SensorALPN is the ALPN protocol id the gRPC binding's client offers next to h2. The platform's gateway routes a ClientHello that offers it to the mTLS listener at layer 4, so the binding shares the platform's own port with the web UI and the API (an IP address or one host name, no extra port). The listener negotiates h2. The id only routes: the client certificate is the identity, and the server certificate is verified against the pinned sensor CA as on any other endpoint.

Variables

View Source
var ErrCABundleChanged = errors.New("protocol v3: the platform sent another gRPC CA bundle over an unpinned channel; keeping the pinned one")

ErrCABundleChanged is a certificate answer with another gRPC CA bundle than the pinned one, received over a channel that is not pinned itself.

View Source
var ErrIdentityRefused = errors.New("protocol v3: the platform refused the sensor's identity")

ErrIdentityRefused is an identity error from the platform on v3: no weaker transport is tried.

View Source
var ErrLogsUnsupported = errors.New("the platform does not take command logs")

ErrLogsUnsupported: the platform does not take command logs (it does not list protov2.FeatureLogs). The sensor keeps its logs local.

View Source
var ErrPlatformCertificate = errors.New("protocol v3: the gRPC endpoint's certificate does not verify against the pinned platform CA; no other binding is tried")

ErrPlatformCertificate is a gRPC endpoint whose certificate does not verify against the pinned platform CA bundle (a TLS-inspecting proxy, a rogue endpoint): the client sends nothing until a later negotiation verifies it, and never falls back to the HTTPS binding or v2.

View Source
var ErrProtocolV1Retired = errors.New("sensor protocol v1 is retired: remove the v1 protocol setting " +
	"(SENSOR_PROTOCOL / -protocol); the sensor speaks protocol v2")

ErrProtocolV1Retired is returned for the protocol setting "v1".

View Source
var ErrV2NoTool = errors.New("protocol v2 requires report.tool.name")

ErrV2NoTool: v2 requires the report's tool (RFC-026 §3.3 step 8).

View Source
var ErrV2Unsupported = errors.New("the platform does not serve sensor protocol v2 " +
	"(upgrade the OpenCTEM API: protocol v1 is retired)")

ErrV2Unsupported: the platform does not serve sensor protocol v2 (or not the part of it a call needs). Protocol v1 is retired, so there is nothing to fall back to: the platform must be upgraded (an OpenCTEM API from 2026-10 on serves protocol v2 for the whole sensor surface).

Functions

func DefaultLegacyRetryQueueDir added in v0.8.0

func DefaultLegacyRetryQueueDir() string

DefaultLegacyRetryQueueDir is where pre-outbox SDKs kept their retry queue.

func IsAuthenticationError

func IsAuthenticationError(err error) bool

IsAuthenticationError checks if the error is a 401 authentication error.

func IsAuthorizationError

func IsAuthorizationError(err error) bool

IsAuthorizationError checks if the error is a 403 authorization error.

func IsClientError

func IsClientError(err error) bool

IsClientError checks if the error is a 4xx client error.

func IsCommandGone added in v0.9.0

func IsCommandGone(err error) bool

IsCommandGone reports whether err says the command can no longer be worked on by this sensor: another sensor claimed it, it was canceled or expired, it already finished, or it is not (or no longer) this sensor's. The poller drops such a command instead of retrying it.

func IsNotFoundError

func IsNotFoundError(err error) bool

IsNotFoundError checks if the error is a 404 not found error.

func IsRateLimitError

func IsRateLimitError(err error) bool

IsRateLimitError checks if the error is a 429 rate limit error.

func IsRetryable

func IsRetryable(err error) bool

IsRetryable checks if the error should be retried.

func IsServerError

func IsServerError(err error) bool

IsServerError checks if the error is a 5xx server error.

func ParseProtocol added in v0.8.0

func ParseProtocol(s string) (string, error)

ParseProtocol validates a protocol setting ("" is auto). "v1" is refused with ErrProtocolV1Retired.

Types

type Binding added in v0.19.0

type Binding string

Binding is how the client reaches the platform.

const (
	BindingGRPC  Binding = "grpc"
	BindingHTTPS Binding = "https"
	BindingV2    Binding = "v2"
)

Bindings.

type Client

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

Client is the OpenCTEM API client. It implements the core.Pusher interface.

func New

func New(cfg *Config) *Client

New creates a new OpenCTEM API client.

func NewWithOptions

func NewWithOptions(opts ...Option) *Client

NewWithOptions creates a new client using functional options. Example:

client := client.NewWithOptions(
    client.WithBaseURL("http://localhost:8080"),
    client.WithAPIKey("xxx"),
    client.WithSensorID("agent-1"),
    client.WithTimeout(30 * time.Second),
)

func (*Client) APIKeyHint added in v0.8.0

func (c *Client) APIKeyHint() string

APIKeyHint names the client's API key in log lines without revealing it (core.APIKeyHint: at most its first 8 characters).

func (*Client) AbandonReport added in v0.8.0

func (c *Client) AbandonReport(ctx context.Context, reportID string) error

AbandonReport deletes an uncommitted segmented report.

func (*Client) AcknowledgeCommand

func (c *Client) AcknowledgeCommand(ctx context.Context, cmdID string) error

AcknowledgeCommand claims a command (POST .../commands/{id}/claim).

func (*Client) BaselineDiff added in v0.4.0

func (c *Client) BaselineDiff(ctx context.Context, repository, baseBranch string, fingerprints []string) ([]string, error)

BaselineDiff returns the subset of fingerprints that are NEW relative to a PR's base/target branch (not already open there). Used to focus a PR gate / inline comments on findings the PR introduces, not pre-existing tech debt. On error or no PR context the caller should treat all findings as new (fail-open for visibility). Returns the new fingerprints.

func (*Client) BindKey added in v0.20.0

func (c *Client) BindKey(ctx context.Context, signer *sensorsig.Signer) (*protov2.KeyBindResponse, error)

BindKey binds signer's key to the sensor this client's API key authenticates. On a client that signs with signer's key it confirms an earlier bind (the platform answers the identity without a change). The error is a *V2Error for a refusal (protov2.ProblemKeyBindRefused, ProblemKeyBindApproval, 401, 404 on a platform without the resource).

func (*Client) CheckFingerprints

func (c *Client) CheckFingerprints(ctx context.Context, fingerprints []string) (*retry.FingerprintCheckResult, error)

CheckFingerprints checks which fingerprints already exist on the server. This is used by the retry mechanism to avoid re-uploading data that already exists. It also serves as a connectivity check before processing the retry queue. This method implements retry.FingerprintChecker interface.

func (*Client) ClaimCommand added in v0.18.0

func (c *Client) ClaimCommand(ctx context.Context, id string) (*core.Command, error)

ClaimCommand claims the command with id for this sensor and returns it, payload included (POST .../commands/{id}/claim answers the command): a sensor started to run one given job (sensorkit Kit.RunJob). The platform refuses a command of another tenant, one another sensor holds, and one that is no longer pending; a replay of this sensor's own claim answers the command again. The command keeps the answer's payload bytes as received and its signed job (api RFC-040 §5.6); the CommandPoller claims through it when it verifies job signatures (core.CommandClaimer).

func (*Client) Close

func (c *Client) Close() error

Close stops the outbox's delivery and releases its directory. Items not delivered yet stay on disk for the next process.

func (*Client) CompleteCommand

func (c *Client) CompleteCommand(ctx context.Context, cmdID string, result json.RawMessage) error

CompleteCommand marks a command as completed with optional result.

func (*Client) ContentFetcher added in v0.32.0

func (c *Client) ContentFetcher() *contentcache.HTTPFetcher

ContentFetcher is the sensor's content fetcher on this client's connection and credentials: content comes from the platform only, never from upstreams (research/139).

func (*Client) Deliver added in v0.8.0

func (c *Client) Deliver(ctx context.Context, d *outbox.Delivery) (any, error)

Deliver implements outbox.Deliverer: one attempt at one item, classified for the outbox (RFC-026 §3.8 retry table).

func (*Client) DisableRetryQueue deprecated

func (c *Client) DisableRetryQueue(_ context.Context) error

DisableRetryQueue stops and closes the outbox.

Deprecated: use Close.

func (*Client) EgressEcho added in v0.25.0

func (c *Client) EgressEcho(ctx context.Context) (netip.Addr, error)

EgressEcho asks the platform which address it sees this sensor's requests come from (GET /egress-echo, api RFC-089), for the network self-description (netctx.Echo). netctx.ErrEchoUnsupported when the platform does not list "network" on hello or does not serve the route. The answer is only an address; anything else is an error.

func (*Client) EnableOutbox added in v0.8.0

func (c *Client) EnableOutbox(cfg OutboxConfig) error

EnableOutbox opens the outbox and starts delivering from it in the background until Close. Reports left in an old retry-queue directory are imported first.

func (*Client) EnableRetryQueue deprecated

func (c *Client) EnableRetryQueue(_ context.Context, cfg *RetryQueueConfig) error

EnableRetryQueue enables the durable outbox in cfg.Dir/outbox (default ~/.openctem/outbox) and imports the reports an older SDK queued in cfg.Dir.

Deprecated: use EnableOutbox.

func (*Client) EnableTransportV3 added in v0.19.0

func (c *Client) EnableTransportV3(opts TransportOptions)

EnableTransportV3 installs protocol v3 under the client (key-bound clients only; a bearer-key client stays on v2). StartTransport negotiates.

func (*Client) EnrichFindings

func (c *Client) EnrichFindings(ctx context.Context, findings []ctis.Finding) ([]ctis.Finding, error)

EnrichFindings adds EPSS and KEV data to findings with CVE IDs.

func (*Client) FailCommand

func (c *Client) FailCommand(ctx context.Context, cmdID string, errorMsg string) error

FailCommand marks a command as failed with an error message.

func (*Client) FlushOutbox added in v0.8.0

func (c *Client) FlushOutbox(ctx context.Context) error

FlushOutbox delivers what can be delivered now and returns when nothing is left that could be sent right away (or ctx ends). A one-shot run calls it before exiting.

func (*Client) GetCommands

func (c *Client) GetCommands(ctx context.Context) (*core.GetCommandsResponse, error)

GetCommands retrieves pending commands from the server (implements core.CommandClient).

func (*Client) GetCommandsLimit added in v0.10.0

func (c *Client) GetCommandsLimit(ctx context.Context, limit int) (*core.GetCommandsResponse, error)

GetCommandsLimit retrieves at most limit pending commands (implements core.LimitedCommandClient). The CommandPoller passes its free slots, so the platform keeps the rest for sensors that can run them now. limit < 1 is 1.

func (*Client) GetEPSSScores

func (c *Client) GetEPSSScores(ctx context.Context, cveIDs []string) ([]EPSSScore, error)

GetEPSSScores fetches EPSS scores for the given CVE IDs.

func (*Client) GetKEVEntries

func (c *Client) GetKEVEntries(ctx context.Context, cveIDs []string) ([]KEVEntry, error)

GetKEVEntries fetches CISA KEV entries for the given CVE IDs.

func (*Client) GetManifestState added in v0.15.0

func (c *Client) GetManifestState(ctx context.Context) (*core.ManifestAck, error)

GetManifestState re-reads the platform's view of the registered manifest (GET /manifest, api RFC-033 §6.12): its digest, the policy as it stands now and the heartbeat form. core.ErrManifestNotRegistered when the platform has none, core.ErrManifestUnsupported when it does not serve manifests. It implements core.ManifestStateReader.

func (*Client) GetReportStatus added in v0.8.0

func (c *Client) GetReportStatus(ctx context.Context, reportID string) (*protov2.Status, error)

GetReportStatus reads a report's status resource.

func (*Client) GetRetryQueueStats deprecated

func (c *Client) GetRetryQueueStats(_ context.Context) (*retry.QueueStats, error)

GetRetryQueueStats returns the outbox's state in the old shape.

Deprecated: use OutboxStats.

func (*Client) GetRetryWorkerStats deprecated

func (c *Client) GetRetryWorkerStats() (*retry.WorkerStats, error)

GetRetryWorkerStats returns the outbox's delivery counters in the old shape.

Deprecated: use OutboxStats.

func (*Client) Hello added in v0.8.0

func (c *Client) Hello(ctx context.Context) (*protov2.Hello, error)

Hello fetches GET /api/v2/sensor/hello.

func (*Client) KeyBound added in v0.18.0

func (c *Client) KeyBound() bool

KeyBound reports whether the client signs its requests with a sensor key instead of sending a bearer key.

func (*Client) Outbox added in v0.8.0

func (c *Client) Outbox() *outbox.Outbox

Outbox returns the client's outbox, or nil when it is not enabled.

func (*Client) OutboxStats added in v0.8.0

func (c *Client) OutboxStats() (outbox.Stats, bool)

OutboxStats returns the outbox's state; ok is false without an outbox.

func (*Client) PlatformSupports added in v0.16.0

func (c *Client) PlatformSupports(ctx context.Context, feature string) bool

PlatformSupports reports whether the platform lists feature on its hello (GET /api/v2/sensor/hello): one of the protov2.Feature* names, or a name a newer platform announces that this SDK has no constant for. The answer is the cached negotiation, asked again when stale. A platform without protocol v2, or one that cannot be asked right now, supports nothing. This is how a sensor lights up an optional behavior for a newer platform without an SDK release, and keeps the old behavior against an older one (docs/STABILITY.md).

func (*Client) PollCommands

func (c *Client) PollCommands(ctx context.Context, limit int) ([]Command, error)

PollCommands retrieves pending commands for this sensor (GET /api/v2/sensor/commands).

func (*Client) ProcessRetryQueueNow deprecated

func (c *Client) ProcessRetryQueueNow(ctx context.Context) error

ProcessRetryQueueNow delivers what the outbox can deliver now.

Deprecated: use FlushOutbox.

func (*Client) ProtocolFeatures added in v0.9.0

func (c *Client) ProtocolFeatures() []string

ProtocolFeatures returns the protocol v2 features the platform listed, as last negotiated (nil: no protocol v2, or not asked yet).

func (*Client) PushAssets

func (c *Client) PushAssets(ctx context.Context, report *ctis.Report) (*core.PushResult, error)

PushAssets sends a report's assets (its findings are dropped). See PushFindings for the outbox behavior.

func (*Client) PushExposures

func (c *Client) PushExposures(ctx context.Context, events []ExposureEvent) (*PushExposuresResult, error)

PushExposures sends exposure events to OpenCTEM.

func (*Client) PushFindings

func (c *Client) PushFindings(ctx context.Context, report *ctis.Report) (*core.PushResult, error)

PushFindings sends a report's findings (and assets) to OpenCTEM over protocol v2 (PUT /api/v2/sensor/results/...). A platform without protocol v2 results answers ErrV2Unsupported.

With the outbox enabled (EnableOutbox) the report is first written to disk and this call waits up to OutboxConfig.SyncWait for its delivery: it returns the result when the platform accepted it, a *RefusedError when the platform refused it for good (dead letter), and Queued=true when the platform could not be reached yet; the outbox delivers it later, across restarts. Without the outbox the report is sent once (with the client's own retries) and an error means it was not delivered.

A context from core.WithCommandID binds the results to that command.

func (*Client) PushReport deprecated

func (c *Client) PushReport(ctx context.Context, report *ctis.Report) error

PushReport pushes a report directly (no outbox), findings and assets alike. It implemented the old retry worker's interface.

Deprecated: use PushFindings.

func (*Client) PushResultsV2 added in v0.8.0

func (c *Client) PushResultsV2(ctx context.Context, report *ctis.Report, opts *V2PushOptions) (*protov2.Status, error)

PushResultsV2 sends a report over protocol v2 and returns its status resource. It makes one pass: transient failures (network, 429, 5xx) are returned as errors for the caller to retry with the same report id and progress (the outbox does). It handles the protocol's own recoveries: a 413 re-splits into smaller segments, an expired report or a gone command restarts under a new report id (unbound), and a conflicting replay of a report the server already has is resolved by reading its status.

func (*Client) PutConfigReport added in v0.18.0

func (c *Client) PutConfigReport(ctx context.Context, r *core.ConfigReport) (*core.ConfigReportAck, error)

PutConfigReport sends the sensor's config report (api RFC-033, config report) when the platform lists "config_report" on hello; core.ErrConfigReportUnsupported otherwise. The report must be finalized (core.ConfigReport.Finalize): it carries no setting values and is at most protov2.MaxConfigReportBytes. It implements core.ConfigReportPusher.

func (*Client) PutManifest added in v0.14.0

func (c *Client) PutManifest(ctx context.Context, m *core.Manifest) (*core.ManifestAck, error)

PutManifest registers the sensor manifest (api RFC-033) when the platform lists "manifest" on hello; core.ErrManifestUnsupported otherwise. It implements core.ManifestPusher.

func (*Client) QueueCommandLogs added in v0.18.0

func (c *Client) QueueCommandLogs(ctx context.Context, commandID string, batch protov2.CommandLogsRequest) error

QueueCommandLogs hands one batch to the outbox, which delivers it before the command's result and keeps it across an outage. Without an outbox it sends the batch now. Logs are best effort: a batch the platform refuses for good, or a platform without the logs feature, drops the batch; it never fails the command.

func (*Client) ReleaseCommand added in v0.10.0

func (c *Client) ReleaseCommand(ctx context.Context, cmdID, reason string) error

ReleaseCommand hands a claimed command back to the platform, which returns it to pending (unpinned) so any sensor can run it at once (implements core.ReleasingCommandClient). On a platform from before the release transition the command is failed with "released: <reason>" instead, so it does not wait for a timeout.

func (*Client) ReportCommandProgress

func (c *Client) ReportCommandProgress(ctx context.Context, cmdID string, progress int, message string) error

ReportCommandProgress reports progress of command execution.

func (*Client) ReportCommandResult

func (c *Client) ReportCommandResult(ctx context.Context, cmdID string, result *core.CommandResult) error

ReportCommandResult reports the result of command execution (implements core.CommandClient). With the outbox enabled the result is stored and delivered after every report of the same command was accepted (or refused, which turns a "completed" result into "failed"); it returns once the result was delivered, or after OutboxConfig.SyncWait (or at once while the platform is unreachable) with the result safe on disk.

Waiting for the delivery matters for dispatch (api RFC-030): the platform counts the command as held by this sensor until its complete arrives, so a poller that freed the slot and polled first was refused (no free capacity) or, before the platform knew the sensor's capacity, handed one command too many.

func (*Client) ResultsProtocol added in v0.8.0

func (c *Client) ResultsProtocol(ctx context.Context) (string, error)

ResultsProtocol returns the protocol the client uses for results ("v2"), or ErrV2Unsupported when the platform does not offer protocol v2 results.

func (*Client) SendCommandLogs added in v0.18.0

func (c *Client) SendCommandLogs(ctx context.Context, commandID string, batch protov2.CommandLogsRequest) (*protov2.CommandLogsResponse, error)

SendCommandLogs sends one batch of a command's log lines now (POST /commands/{id}/logs). It is ErrLogsUnsupported against a platform without the logs feature.

func (*Client) SendHeartbeat

func (c *Client) SendHeartbeat(ctx context.Context, status *core.SensorStatus) error

SendHeartbeat sends a heartbeat to OpenCTEM. It does not announce the heartbeat doorbell and ignores the response body: a caller that acts on the platform's hints uses SendHeartbeatWithHints instead.

func (*Client) SendHeartbeatForCancels added in v0.18.0

func (c *Client) SendHeartbeatForCancels(ctx context.Context, status *core.SensorStatus) ([]string, error)

SendHeartbeatForCancels sends a heartbeat exactly like SendHeartbeat (no doorbell announced) and returns the platform's cancel_command_ids: the commands this sensor listed as running that it must stop. A disabled sensor's pause is an error, as in SendHeartbeat.

func (*Client) SendHeartbeatWithHints added in v0.7.1

func (c *Client) SendHeartbeatWithHints(ctx context.Context, status *core.SensorStatus) (*core.HeartbeatHints, error)

SendHeartbeatWithHints sends a heartbeat and returns the doorbell hints the platform answered with (protocol v2 always has the doorbell). The caller acts on them: a disabled sensor is answered 200 with the pause action, not an error.

func (*Client) SetAPIKey added in v0.5.0

func (c *Client) SetAPIKey(key string)

SetAPIKey atomically replaces the API key used by subsequent requests. Safe to call concurrently with in-flight pushes (sensor key auto-renewal).

func (*Client) SetControlEventHandler added in v0.19.0

func (c *Client) SetControlEventHandler(f func(*core.HeartbeatHints))

SetControlEventHandler receives the control stream's events (the doorbell, pushed); a sensor hands them to core.Doorbell.Handle.

func (*Client) SetVerbose

func (c *Client) SetVerbose(v bool)

SetVerbose sets verbose mode.

func (*Client) SourceCredential added in v0.30.0

func (c *Client) SourceCredential(ctx context.Context, cmdID string) (*source.Credential, error)

SourceCredential asks the platform for the credential of a code-scan command's repository source (api RFC-091): a short-lived, read-only token for that one repository, minted on each call for the sensor holding the command's lease. The credential is returned, never logged or stored.

func (*Client) StartCommand

func (c *Client) StartCommand(ctx context.Context, cmdID string) error

StartCommand marks a command as started.

func (*Client) StartRetryWorker deprecated

func (c *Client) StartRetryWorker(_ context.Context) error

StartRetryWorker is a no-op: the outbox delivers in the background from EnableOutbox on. It fails when no outbox is enabled.

Deprecated: use EnableOutbox.

func (*Client) StartTransport added in v0.19.0

func (c *Client) StartTransport(ctx context.Context) error

StartTransport negotiates the transport now and keeps it current until ctx ends: renews the certificate, re-probes gRPC, renegotiates after repeated transport failures and runs the control stream. It returns the first negotiation's error (an identity refusal); the client then sends nothing but hello and the heartbeat until the identity is accepted again (no downgrade).

func (*Client) StopRetryWorker deprecated

func (c *Client) StopRetryWorker(_ context.Context) error

StopRetryWorker is a no-op; Close stops the outbox.

Deprecated: use Close.

func (*Client) TestConnection

func (c *Client) TestConnection(ctx context.Context) error

TestConnection tests the API connection.

func (*Client) TransportStatus added in v0.19.0

func (c *Client) TransportStatus() TransportStatus

TransportStatus is the transport in use (v2 without protocol v3).

type Command

type Command struct {
	ID             string          `json:"id"`
	TenantID       string          `json:"tenant_id,omitempty"`
	SourceID       string          `json:"source_id,omitempty"`
	Type           string          `json:"type"`
	Priority       string          `json:"priority"`
	Payload        json.RawMessage `json:"payload,omitempty"`
	Status         string          `json:"status"`
	ErrorMessage   string          `json:"error_message,omitempty"`
	CreatedAt      time.Time       `json:"created_at"`
	ExpiresAt      *time.Time      `json:"expires_at,omitempty"`
	AcknowledgedAt *time.Time      `json:"acknowledged_at,omitempty"`
	StartedAt      *time.Time      `json:"started_at,omitempty"`
	CompletedAt    *time.Time      `json:"completed_at,omitempty"`
	Result         json.RawMessage `json:"result,omitempty"`
	// LeaseEpoch and LeaseExpiresAt are the command's lease (api RFC-035 D6)
	// as of this answer.
	LeaseEpoch     int        `json:"lease_epoch,omitempty"`
	LeaseExpiresAt *time.Time `json:"lease_expires_at,omitempty"`
	// SignedJob is the job signer's envelope for this delivery (claim-N
	// answers; api RFC-040 §5.6).
	SignedJob json.RawMessage `json:"signed_job,omitempty"`
}

Command represents a command from the server.

type Config

type Config struct {
	BaseURL  string        `yaml:"base_url" json:"base_url"`
	APIKey   string        `yaml:"api_key" json:"api_key"`
	SensorID string        `yaml:"sensor_id" json:"sensor_id"` // Registered sensor ID for audit trail; the pre-rename key agent_id is still read (config_compat.go)
	Timeout  time.Duration `yaml:"timeout" json:"timeout"`
	// ControlTimeout bounds one heartbeat, which goes on a connection pool
	// of its own and is not retried with its stale report (default
	// DefaultControlTimeout, never above Timeout; api RFC-035).
	ControlTimeout time.Duration `yaml:"control_timeout" json:"control_timeout"`
	MaxRetries     int           `yaml:"max_retries" json:"max_retries"`
	RetryDelay     time.Duration `yaml:"retry_delay" json:"retry_delay"`
	Verbose        bool          `yaml:"verbose" json:"verbose"`

	// UserAgent is this client's product token, "name/version" (for example
	// "openctemio-sensor/0.3.1"), sent before the SDK's own token:
	// "openctemio-sensor/0.3.1 openctem-sdk-go/0.7.4". Empty: the process-wide
	// product set with useragent.SetProduct, if any.
	UserAgent string `yaml:"user_agent" json:"user_agent"`

	// Compression configuration
	EnableCompression bool   `yaml:"enable_compression" json:"enable_compression"` // Enable request compression (default: true)
	CompressionAlgo   string `yaml:"compression_algo" json:"compression_algo"`     // "zstd" or "gzip" (default: "zstd")
	CompressionLevel  int    `yaml:"compression_level" json:"compression_level"`   // 1-9 (default: 3)

	// Protocol selects the results protocol: "auto" (default: v2 when the
	// platform offers it, else v1), "v1" or "v2".
	Protocol string `yaml:"protocol" json:"protocol"`

	// Signer makes the client key-bound (api RFC-052): every request is
	// signed with the sensor key (RFC 9421, pkg/sensorsig) and no bearer
	// key is sent; APIKey is ignored.
	Signer *sensorsig.Signer `yaml:"-" json:"-"`

	// OutboxDir enables the durable outbox (see EnableOutbox) in that
	// directory. New logs, and leaves the outbox off, when it cannot be
	// opened; call EnableOutbox yourself to handle the error.
	OutboxDir string `yaml:"outbox_dir" json:"outbox_dir"`
	// OutboxMaxBytes and OutboxMaxAge override the outbox caps.
	OutboxMaxBytes int64         `yaml:"outbox_max_bytes" json:"outbox_max_bytes"`
	OutboxMaxAge   time.Duration `yaml:"outbox_max_age" json:"outbox_max_age"`

	// Deprecated: the retry queue is replaced by the outbox. EnableRetryQueue
	// enables the outbox (in OutboxDir, else RetryQueueDir/outbox, else
	// ~/.openctem/outbox) and imports the reports an older SDK left in
	// RetryQueueDir. RetryInterval and RetryMaxAttempts are ignored: the
	// outbox retries with back-off until RetryTTL (OutboxMaxAge) evicts.
	EnableRetryQueue bool          `yaml:"enable_retry_queue" json:"enable_retry_queue"`
	RetryQueueDir    string        `yaml:"retry_queue_dir" json:"retry_queue_dir"`
	RetryInterval    time.Duration `yaml:"retry_interval" json:"retry_interval"`
	RetryMaxAttempts int           `yaml:"retry_max_attempts" json:"retry_max_attempts"`
	RetryTTL         time.Duration `yaml:"retry_ttl" json:"retry_ttl"`
}

Config holds client configuration.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns default client config.

func (*Config) UnmarshalJSON added in v0.7.0

func (c *Config) UnmarshalJSON(data []byte) error

UnmarshalJSON reads Config, accepting the pre-rename key "agent_id" for SensorID so configuration files written before the agent -> sensor rename keep working (see legacyv1.MergeSensorID).

func (*Config) UnmarshalYAML added in v0.7.0

func (c *Config) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML does the same for YAML. It uses the function-argument form both gopkg.in/yaml.v2 and gopkg.in/yaml.v3 accept, so the SDK does not depend on a YAML library.

type EPSSScore

type EPSSScore struct {
	CVEID      string    `json:"cve_id"`
	Score      float64   `json:"score"`      // 0.0 to 1.0
	Percentile float64   `json:"percentile"` // 0.0 to 100.0
	Date       time.Time `json:"date"`
}

EPSSScore represents an EPSS score for a CVE.

type ExposureEvent

type ExposureEvent struct {
	// Event type: new_asset, asset_removed, exposure_detected, exposure_resolved
	Type string `json:"type"`

	// Asset identifier
	AssetID   string `json:"asset_id,omitempty"`
	AssetType string `json:"asset_type,omitempty"`
	AssetName string `json:"asset_name,omitempty"`

	// Exposure details
	ExposureType string `json:"exposure_type,omitempty"` // port_open, service_exposed, etc.
	Protocol     string `json:"protocol,omitempty"`
	Port         int    `json:"port,omitempty"`
	Service      string `json:"service,omitempty"`

	// Detection info
	DetectedAt  time.Time `json:"detected_at"`
	DetectedBy  string    `json:"detected_by,omitempty"` // scan source
	Severity    string    `json:"severity,omitempty"`
	Description string    `json:"description,omitempty"`

	// Resolution
	ResolvedAt *time.Time `json:"resolved_at,omitempty"`

	// Metadata
	Tags       []string       `json:"tags,omitempty"`
	Properties map[string]any `json:"properties,omitempty"`
}

ExposureEvent represents an attack surface change event.

type HTTPError

type HTTPError struct {
	StatusCode int    `json:"status_code"`
	Body       string `json:"body"`
	RequestID  string `json:"request_id,omitempty"`
	// RetryAfter is the server's Retry-After (429/503), 0 when absent.
	RetryAfter time.Duration `json:"retry_after,omitempty"`
}

HTTPError represents an HTTP error response.

func IsHTTPError

func IsHTTPError(err error) (*HTTPError, bool)

IsHTTPError checks if err is an HTTPError and returns it.

func (*HTTPError) Error

func (e *HTTPError) Error() string

func (*HTTPError) HTTPStatusCode added in v0.8.0

func (e *HTTPError) HTTPStatusCode() int

HTTPStatusCode returns the response status. pkg/core uses it to tell a rejected API key (401/403) from other failures without importing this package.

type HeartbeatOutbox added in v0.8.0

type HeartbeatOutbox struct {
	PendingCount     int   `json:"pending_count"`
	PendingBytes     int64 `json:"pending_bytes"`
	OldestAgeSeconds int64 `json:"oldest_age_seconds"`
	DeadLetterCount  int   `json:"dead_letter_count"`
	EvictedCount     int64 `json:"evicted_count"`
}

HeartbeatOutbox is the outbox state a heartbeat reports (additive; servers that do not know it ignore it).

type HeartbeatRequest

type HeartbeatRequest struct {
	Name     string           `json:"name,omitempty"`
	Status   core.SensorState `json:"status"`
	Version  string           `json:"version,omitempty"`
	Hostname string           `json:"hostname,omitempty"`
	// InstanceID is this sensor process's random id
	// (core.ProcessInstanceID): a restart changes it once, a key running in
	// two places makes two ids alternate, which the platform flags (api
	// RFC-032 Phase 0). Platforms that do not know it ignore it.
	InstanceID string `json:"instance_id,omitempty"`
	// ManifestDigest is the registered manifest's digest (api RFC-033).
	ManifestDigest string `json:"manifest_digest,omitempty"`
	// Content is a slim heartbeat's content freshness (api RFC-033 §6.12).
	Content    []core.ToolContent `json:"content,omitzero"`
	Message    string             `json:"message,omitempty"`
	Scanners   []string           `json:"scanners,omitempty"`
	Collectors []string           `json:"collectors,omitempty"`
	Uptime     int64              `json:"uptime_seconds,omitempty"`
	TotalScans int64              `json:"total_scans,omitempty"`
	Errors     int64              `json:"errors,omitempty"`

	// System Metrics
	CPUPercent    float64 `json:"cpu_percent,omitempty"`
	MemoryPercent float64 `json:"memory_percent,omitempty"`
	ActiveJobs    int     `json:"active_jobs,omitempty"`
	Region        string  `json:"region,omitempty"`
	// ActiveJobsReported sends active_jobs even when it is 0 (the sensor
	// measured it). Without it a 0 is left out, as before.
	ActiveJobsReported bool `json:"-"`

	// Outbox is the durable outbox's state (absent without an outbox).
	Outbox *HeartbeatOutbox `json:"outbox,omitempty"`

	// The sensor-reported capabilities (core.CapabilityReport): absent when
	// the sensor reports nothing, and an empty list when it reports none.
	Tools             []core.ToolInfo `json:"tools,omitzero"`
	Capabilities      []string        `json:"capabilities,omitzero"`
	MaxConcurrentJobs int             `json:"max_concurrent_jobs,omitempty"`
	OS                string          `json:"os,omitempty"`
	Arch              string          `json:"arch,omitempty"`

	// The sensor's work now (core.SensorStatus; api RFC-030): absent when
	// not reported.
	Resources       *resource.HostResources `json:"resources,omitempty"`
	Capacity        *resource.Capacity      `json:"capacity,omitempty"`
	Queue           *core.QueueStats        `json:"queue,omitempty"`
	RunningCommands []string                `json:"running,omitzero"`
	// Progress is each held command's finished targets; sent only to a
	// platform that lists FeatureCommandProgress.
	Progress map[string]core.CommandProgress `json:"progress,omitempty"`
	// Control is the control channel's own stats (api RFC-035).
	Control *core.ControlStats `json:"control,omitempty"`

	// SDK and Sensor name the SDK and the sensor binary. Every heartbeat
	// sends both: what the status carries, else the SDK linked into this
	// binary and a sensor block built from the status's version.
	SDK    *core.SDKInfo     `json:"sdk,omitempty"`
	Sensor *core.SensorBuild `json:"sensor,omitempty"`

	// LocalPolicy is the sensor-local policy report (api RFC-040 §5.7),
	// sent only to a platform that lists the "local_policy" feature.
	LocalPolicy *core.LocalPolicyReport `json:"local_policy,omitempty"`

	// ConfigReport is the config report's digest, rollup and counts, sent
	// only to a platform that lists the "config_report" feature.
	ConfigReport *core.ConfigReportSummary `json:"config_report,omitempty"`
}

HeartbeatRequest is the heartbeat payload.

func (HeartbeatRequest) MarshalJSON added in v0.10.0

func (r HeartbeatRequest) MarshalJSON() ([]byte, error)

MarshalJSON encodes the heartbeat. active_jobs is sent even when it is 0 if ActiveJobsReported is set: an idle sensor then reports "0 running", which the platform's dispatch can trust, instead of leaving the field out.

type KEVEntry

type KEVEntry struct {
	CVEID                      string    `json:"cve_id"`
	VendorProject              string    `json:"vendor_project"`
	Product                    string    `json:"product"`
	VulnerabilityName          string    `json:"vulnerability_name"`
	DateAdded                  time.Time `json:"date_added"`
	ShortDescription           string    `json:"short_description"`
	RequiredAction             string    `json:"required_action"`
	DueDate                    time.Time `json:"due_date"`
	KnownRansomwareCampaignUse string    `json:"known_ransomware_campaign_use"`
}

KEVEntry represents a CISA Known Exploited Vulnerabilities entry.

type Option

type Option func(*Client)

Option is a function that configures the client.

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey sets the API key.

func WithBaseURL

func WithBaseURL(url string) Option

WithBaseURL sets the API base URL.

func WithCompression

func WithCompression(algorithm string, level int) Option

WithCompression enables request compression with the specified algorithm. Supported algorithms: "zstd" (recommended), "gzip"

func WithProtocol added in v0.8.0

func WithProtocol(p string) Option

WithProtocol sets the protocol: ProtocolAuto (default) or ProtocolV2, which are the same. An unknown or retired value ("v1") keeps auto.

func WithRetry

func WithRetry(maxRetries int, retryDelay time.Duration) Option

WithRetry sets retry configuration.

func WithSensorID added in v0.7.0

func WithSensorID(id string) Option

WithSensorID sets the sensor ID for tracking which sensor is pushing data.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the HTTP timeout.

func WithUserAgent added in v0.8.0

func WithUserAgent(product string) Option

WithUserAgent sets this client's product token, "name/version", sent before the SDK's own token in the User-Agent (see Config.UserAgent).

func WithVerbose

func WithVerbose(v bool) Option

WithVerbose enables verbose logging.

func WithoutClaimOnPoll added in v0.18.0

func WithoutClaimOnPoll() Option

WithoutClaimOnPoll makes polls only list commands, as before claim-N (api RFC-046 §11): the poller claims each one. Use it only for a caller that polls without running what it gets (a dry run, a dashboard): a claimed command it does not run would wait for its lease to expire.

func WithoutCompression

func WithoutCompression() Option

WithoutCompression disables request compression.

type OutboxConfig added in v0.8.0

type OutboxConfig struct {
	// Dir is the outbox directory (required). The sensor's default is
	// /var/lib/openctem/outbox.
	Dir string
	// KeyFile is the encryption key file (default <Dir>/outbox.key).
	KeyFile string
	// MaxBytes caps the bytes on disk (default 1 GiB, and never more than
	// half of the space the outbox could use).
	MaxBytes int64
	// MaxAge evicts items older than this (default 7 days).
	MaxAge time.Duration
	// Concurrency is 1 (default) or 2.
	Concurrency int
	// SyncWait is how long PushFindings/PushAssets wait for the first
	// delivery before answering "queued" (default 30s; negative: do not
	// wait).
	SyncWait time.Duration
	// LegacyRetryQueueDir is a pre-outbox retry-queue directory whose
	// reports are imported once (default ~/.openctem/retry-queue; "-" skips
	// the import).
	LegacyRetryQueueDir string
	// Logf receives the outbox's log lines (default: stderr with a
	// timestamp).
	Logf func(format string, args ...any)
	// OnEvict is told when the caps dropped results (turn it into an alert).
	OnEvict func(evicted []outbox.Meta, reason string)
}

OutboxConfig enables the durable outbox (pkg/outbox) on a client. With it, every report and command result is written to disk before the first send and deleted only after the platform acknowledged it.

type PushExposuresResult

type PushExposuresResult struct {
	EventsCreated int `json:"events_created"`
	EventsUpdated int `json:"events_updated"`
	EventsSkipped int `json:"events_skipped"`
}

PushExposuresResult is the result of pushing exposure events.

type RefusedError added in v0.8.0

type RefusedError struct {
	DeadLetter outbox.DeadLetter
}

RefusedError says the platform refused a report for good; it is in the outbox's dead-letter folder.

func (*RefusedError) Error added in v0.8.0

func (e *RefusedError) Error() string

type RetryQueueConfig deprecated

type RetryQueueConfig struct {
	Dir         string
	MaxSize     int
	Interval    time.Duration
	BatchSize   int
	MaxAttempts int
	TTL         time.Duration
	Backoff     *retry.BackoffConfig
	AutoStart   bool
}

RetryQueueConfig configures the retry queue.

Deprecated: use EnableOutbox with an OutboxConfig. Dir is where the old queue's files are imported from (the outbox lives in Dir/outbox); TTL becomes the outbox's MaxAge; the other fields are ignored.

func DefaultRetryQueueConfig deprecated

func DefaultRetryQueueConfig() *RetryQueueConfig

DefaultRetryQueueConfig returns a configuration with default values.

Deprecated: use EnableOutbox.

type TransportMode added in v0.19.0

type TransportMode string

TransportMode is SENSOR_TRANSPORT.

const (
	// TransportAuto: gRPC, then HTTPS on a transport failure, then v2.
	TransportAuto TransportMode = "auto"
	// TransportGRPC: gRPC only; a failure is an error.
	TransportGRPC TransportMode = "grpc"
	// TransportHTTPS: the HTTPS binding only (v2 if the platform has no v3).
	TransportHTTPS TransportMode = "https"
	// TransportV2: protocol v2 only.
	TransportV2 TransportMode = "v2"
)

Transport modes.

func ParseTransportMode added in v0.19.0

func ParseTransportMode(s string) (TransportMode, error)

ParseTransportMode reads SENSOR_TRANSPORT ("" is auto).

type TransportOptions added in v0.19.0

type TransportOptions struct {
	// Mode is SENSOR_TRANSPORT.
	Mode TransportMode
	// CertDir keeps the client certificate and the pinned CA (0600 files
	// in an existing 0700 directory, e.g. the identity directory). ""
	// keeps them in memory.
	CertDir string
	// ReprobeInterval is how often a sensor not on gRPC tries gRPC again
	// (auto mode). Zero is 30 minutes (jittered).
	ReprobeInterval time.Duration
	// FailureThreshold is how many consecutive transport failures of the
	// binding in use trigger a new negotiation. Zero is 3.
	FailureThreshold int
	// Logf receives one line per transport change (nil: none).
	Logf func(format string, args ...any)
}

TransportOptions configure protocol v3 (Client.EnableTransportV3).

type TransportStatus added in v0.19.0

type TransportStatus struct {
	Binding        Binding
	FallbackReason string
	Since          time.Time
	// CertificateNotAfter is the client certificate's expiry (gRPC).
	CertificateNotAfter time.Time
}

TransportStatus is the transport in use.

type V2Error added in v0.8.0

type V2Error struct {
	Status     int
	Problem    *protov2.Problem
	RetryAfter time.Duration
	// Body is the raw answer when it was not a problem document (truncated).
	Body      string
	RequestID string
}

V2Error is a non-2xx answer of a v2 resource.

func (*V2Error) Error added in v0.8.0

func (e *V2Error) Error() string

func (*V2Error) HTTPStatusCode added in v0.8.0

func (e *V2Error) HTTPStatusCode() int

HTTPStatusCode lets core.AuthFailureStatus classify the error.

func (*V2Error) ProblemName added in v0.8.0

func (e *V2Error) ProblemName() protov2.ProblemType

ProblemName is the problem type's short name, or "".

func (*V2Error) RetryAfterHint added in v0.25.0

func (e *V2Error) RetryAfterHint() time.Duration

RetryAfterHint is the server's Retry-After (core.RetryAfterHinter).

func (*V2Error) Transient added in v0.8.0

func (e *V2Error) Transient() bool

Transient reports whether retrying later can succeed (RFC-026 §3.8: 429, 500, 502, 503, 504).

type V2Progress added in v0.8.0

type V2Progress struct {
	// ReportID in use (it changes only when the report had to be restarted:
	// expired, or re-split after a 413).
	ReportID string `json:"report_id"`
	// Unbound: the command is gone; the report is sent unsolicited.
	Unbound bool `json:"unbound,omitempty"`
	// SegFindings and SegBytes cap the segments after a 413 (0: the
	// server's limits). Each 413 halves the segment that was refused.
	SegFindings int `json:"seg_findings,omitempty"`
	SegBytes    int `json:"seg_bytes,omitempty"`
	// Split is true once the report was split after a 413: it is then sent
	// as segments plus a commit even when one segment remains.
	Split bool `json:"split,omitempty"`
	// Segments is the planned segment count (0: not planned yet).
	Segments int `json:"segments,omitempty"`
	// Acked maps a segment number to the Content-Digest the server
	// acknowledged for it.
	Acked map[int]string `json:"acked,omitempty"`
	// Restarts counts report-id restarts (bounded).
	Restarts int `json:"restarts,omitempty"`
	// contains filtered or unexported fields
}

V2Progress is the resumable state of one v2 report delivery. The outbox persists it, so a restart neither resends acknowledged segments nor loses the report id.

type V2PushOptions added in v0.8.0

type V2PushOptions struct {
	// ReportID is the stable report id (lower-case UUID). Empty: a new
	// UUIDv7. Retries MUST reuse it.
	ReportID string
	// CommandID binds the report to a command this sensor claimed ("" for
	// unsolicited results).
	CommandID string
	// Progress resumes an earlier attempt (nil: start).
	Progress *V2Progress
	// OnProgress persists progress after every acknowledged segment and
	// every restart. An error stops the push.
	OnProgress func(V2Progress) error
	// Logf receives restarts and fall-backs. Nil: silent.
	Logf func(format string, args ...any)
	// MaxFindingsPerSegment lowers the server's per-segment finding limit
	// (0: the server's). For constrained links and conformance tests.
	MaxFindingsPerSegment int
}

V2PushOptions configures PushResultsV2.

Jump to

Keyboard shortcuts

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