Documentation
¶
Overview ¶
Package client provides the OpenCTEM API client.
Stability: Stable (docs/STABILITY.md).
Index ¶
- Constants
- Variables
- func DefaultLegacyRetryQueueDir() string
- func IsAuthenticationError(err error) bool
- func IsAuthorizationError(err error) bool
- func IsClientError(err error) bool
- func IsCommandGone(err error) bool
- func IsNotFoundError(err error) bool
- func IsRateLimitError(err error) bool
- func IsRetryable(err error) bool
- func IsServerError(err error) bool
- func ParseProtocol(s string) (string, error)
- type Binding
- type Client
- func (c *Client) APIKeyHint() string
- func (c *Client) AbandonReport(ctx context.Context, reportID string) error
- func (c *Client) AcknowledgeCommand(ctx context.Context, cmdID string) error
- func (c *Client) BaselineDiff(ctx context.Context, repository, baseBranch string, fingerprints []string) ([]string, error)
- func (c *Client) BindKey(ctx context.Context, signer *sensorsig.Signer) (*protov2.KeyBindResponse, error)
- func (c *Client) CheckFingerprints(ctx context.Context, fingerprints []string) (*retry.FingerprintCheckResult, error)
- func (c *Client) ClaimCommand(ctx context.Context, id string) (*core.Command, error)
- func (c *Client) Close() error
- func (c *Client) CompleteCommand(ctx context.Context, cmdID string, result json.RawMessage) error
- func (c *Client) ContentFetcher() *contentcache.HTTPFetcher
- func (c *Client) Deliver(ctx context.Context, d *outbox.Delivery) (any, error)
- func (c *Client) DisableRetryQueue(_ context.Context) errordeprecated
- func (c *Client) EgressEcho(ctx context.Context) (netip.Addr, error)
- func (c *Client) EnableOutbox(cfg OutboxConfig) error
- func (c *Client) EnableRetryQueue(_ context.Context, cfg *RetryQueueConfig) errordeprecated
- func (c *Client) EnableTransportV3(opts TransportOptions)
- func (c *Client) EnrichFindings(ctx context.Context, findings []ctis.Finding) ([]ctis.Finding, error)
- func (c *Client) FailCommand(ctx context.Context, cmdID string, errorMsg string) error
- func (c *Client) FlushOutbox(ctx context.Context) error
- func (c *Client) GetCommands(ctx context.Context) (*core.GetCommandsResponse, error)
- func (c *Client) GetCommandsLimit(ctx context.Context, limit int) (*core.GetCommandsResponse, error)
- func (c *Client) GetEPSSScores(ctx context.Context, cveIDs []string) ([]EPSSScore, error)
- func (c *Client) GetKEVEntries(ctx context.Context, cveIDs []string) ([]KEVEntry, error)
- func (c *Client) GetManifestState(ctx context.Context) (*core.ManifestAck, error)
- func (c *Client) GetReportStatus(ctx context.Context, reportID string) (*protov2.Status, error)
- func (c *Client) GetRetryQueueStats(_ context.Context) (*retry.QueueStats, error)deprecated
- func (c *Client) GetRetryWorkerStats() (*retry.WorkerStats, error)deprecated
- func (c *Client) Hello(ctx context.Context) (*protov2.Hello, error)
- func (c *Client) KeyBound() bool
- func (c *Client) Outbox() *outbox.Outbox
- func (c *Client) OutboxStats() (outbox.Stats, bool)
- func (c *Client) PlatformSupports(ctx context.Context, feature string) bool
- func (c *Client) PollCommands(ctx context.Context, limit int) ([]Command, error)
- func (c *Client) ProcessRetryQueueNow(ctx context.Context) errordeprecated
- func (c *Client) ProtocolFeatures() []string
- func (c *Client) PushAssets(ctx context.Context, report *ctis.Report) (*core.PushResult, error)
- func (c *Client) PushExposures(ctx context.Context, events []ExposureEvent) (*PushExposuresResult, error)
- func (c *Client) PushFindings(ctx context.Context, report *ctis.Report) (*core.PushResult, error)
- func (c *Client) PushReport(ctx context.Context, report *ctis.Report) errordeprecated
- func (c *Client) PushResultsV2(ctx context.Context, report *ctis.Report, opts *V2PushOptions) (*protov2.Status, error)
- func (c *Client) PutConfigReport(ctx context.Context, r *core.ConfigReport) (*core.ConfigReportAck, error)
- func (c *Client) PutManifest(ctx context.Context, m *core.Manifest) (*core.ManifestAck, error)
- func (c *Client) QueueCommandLogs(ctx context.Context, commandID string, batch protov2.CommandLogsRequest) error
- func (c *Client) ReleaseCommand(ctx context.Context, cmdID, reason string) error
- func (c *Client) ReportCommandProgress(ctx context.Context, cmdID string, progress int, message string) error
- func (c *Client) ReportCommandResult(ctx context.Context, cmdID string, result *core.CommandResult) error
- func (c *Client) ResultsProtocol(ctx context.Context) (string, error)
- func (c *Client) SendCommandLogs(ctx context.Context, commandID string, batch protov2.CommandLogsRequest) (*protov2.CommandLogsResponse, error)
- func (c *Client) SendHeartbeat(ctx context.Context, status *core.SensorStatus) error
- func (c *Client) SendHeartbeatForCancels(ctx context.Context, status *core.SensorStatus) ([]string, error)
- func (c *Client) SendHeartbeatWithHints(ctx context.Context, status *core.SensorStatus) (*core.HeartbeatHints, error)
- func (c *Client) SetAPIKey(key string)
- func (c *Client) SetControlEventHandler(f func(*core.HeartbeatHints))
- func (c *Client) SetVerbose(v bool)
- func (c *Client) SourceCredential(ctx context.Context, cmdID string) (*source.Credential, error)
- func (c *Client) StartCommand(ctx context.Context, cmdID string) error
- func (c *Client) StartRetryWorker(_ context.Context) errordeprecated
- func (c *Client) StartTransport(ctx context.Context) error
- func (c *Client) StopRetryWorker(_ context.Context) errordeprecated
- func (c *Client) TestConnection(ctx context.Context) error
- func (c *Client) TransportStatus() TransportStatus
- type Command
- type Config
- type EPSSScore
- type ExposureEvent
- type HTTPError
- type HeartbeatOutbox
- type HeartbeatRequest
- type KEVEntry
- type Option
- func WithAPIKey(key string) Option
- func WithBaseURL(url string) Option
- func WithCompression(algorithm string, level int) Option
- func WithProtocol(p string) Option
- func WithRetry(maxRetries int, retryDelay time.Duration) Option
- func WithSensorID(id string) Option
- func WithTimeout(d time.Duration) Option
- func WithUserAgent(product string) Option
- func WithVerbose(v bool) Option
- func WithoutClaimOnPoll() Option
- func WithoutCompression() Option
- type OutboxConfig
- type PushExposuresResult
- type RefusedError
- type RetryQueueConfigdeprecated
- type TransportMode
- type TransportOptions
- type TransportStatus
- type V2Error
- type V2Progress
- type V2PushOptions
Constants ¶
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" 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.
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.
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.
const DefaultCommandPollLimit = 10
DefaultCommandPollLimit is how many commands GetCommands asks for.
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.
const DefaultOutboxSyncWait = 30 * time.Second
DefaultOutboxSyncWait is OutboxConfig.SyncWait's default.
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 ¶
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.
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.
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.
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.
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".
var ErrV2NoTool = errors.New("protocol v2 requires report.tool.name")
ErrV2NoTool: v2 requires the report's tool (RFC-026 §3.3 step 8).
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 ¶
IsAuthenticationError checks if the error is a 401 authentication error.
func IsAuthorizationError ¶
IsAuthorizationError checks if the error is a 403 authorization error.
func IsClientError ¶
IsClientError checks if the error is a 4xx client error.
func IsCommandGone ¶ added in v0.9.0
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 ¶
IsNotFoundError checks if the error is a 404 not found error.
func IsRateLimitError ¶
IsRateLimitError checks if the error is a 429 rate limit error.
func IsRetryable ¶
IsRetryable checks if the error should be retried.
func IsServerError ¶
IsServerError checks if the error is a 5xx server error.
func ParseProtocol ¶ added in v0.8.0
ParseProtocol validates a protocol setting ("" is auto). "v1" is refused with ErrProtocolV1Retired.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is the OpenCTEM API client. It implements the core.Pusher interface.
func NewWithOptions ¶
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
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
AbandonReport deletes an uncommitted segmented report.
func (*Client) AcknowledgeCommand ¶
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
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 ¶
Close stops the outbox's delivery and releases its directory. Items not delivered yet stay on disk for the next process.
func (*Client) CompleteCommand ¶
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
Deliver implements outbox.Deliverer: one attempt at one item, classified for the outbox (RFC-026 §3.8 retry table).
func (*Client) DisableRetryQueue
deprecated
func (*Client) EgressEcho ¶ added in v0.25.0
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 ¶
FailCommand marks a command as failed with an error message.
func (*Client) FlushOutbox ¶ added in v0.8.0
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 ¶
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 ¶
GetEPSSScores fetches EPSS scores for the given CVE IDs.
func (*Client) GetKEVEntries ¶
GetKEVEntries fetches CISA KEV entries for the given CVE IDs.
func (*Client) GetManifestState ¶ added in v0.15.0
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
GetReportStatus reads a report's status resource.
func (*Client) GetRetryQueueStats
deprecated
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) KeyBound ¶ added in v0.18.0
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
Outbox returns the client's outbox, or nil when it is not enabled.
func (*Client) OutboxStats ¶ added in v0.8.0
OutboxStats returns the outbox's state; ok is false without an outbox.
func (*Client) PlatformSupports ¶ added in v0.16.0
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 ¶
PollCommands retrieves pending commands for this sensor (GET /api/v2/sensor/commands).
func (*Client) ProcessRetryQueueNow
deprecated
func (*Client) ProtocolFeatures ¶ added in v0.9.0
ProtocolFeatures returns the protocol v2 features the platform listed, as last negotiated (nil: no protocol v2, or not asked yet).
func (*Client) PushAssets ¶
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 ¶
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) 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
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
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
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 ¶
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
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) SourceCredential ¶ added in v0.30.0
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 ¶
StartCommand marks a command as started.
func (*Client) StartRetryWorker
deprecated
func (*Client) StartTransport ¶ added in v0.19.0
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 (*Client) TestConnection ¶
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 (*Config) UnmarshalJSON ¶ added in v0.7.0
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).
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 ¶
IsHTTPError checks if err is an HTTPError and returns it.
func (*HTTPError) HTTPStatusCode ¶ added in v0.8.0
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 WithCompression ¶
WithCompression enables request compression with the specified algorithm. Supported algorithms: "zstd" (recommended), "gzip"
func WithProtocol ¶ added in v0.8.0
WithProtocol sets the protocol: ProtocolAuto (default) or ProtocolV2, which are the same. An unknown or retired value ("v1") keeps auto.
func WithSensorID ¶ added in v0.7.0
WithSensorID sets the sensor ID for tracking which sensor is pushing data.
func WithUserAgent ¶ added in v0.8.0
WithUserAgent sets this client's product token, "name/version", sent before the SDK's own token in the User-Agent (see Config.UserAgent).
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) HTTPStatusCode ¶ added in v0.8.0
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
RetryAfterHint is the server's Retry-After (core.RetryAfterHinter).
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.