usp_threatlocker

package
v1.32.32 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

ThreatLocker Adapter

Pulls events from the ThreatLocker Portal API into LimaCharlie. Events are forwarded in their original ThreatLocker JSON form — the adapter does not reshape payloads.

The adapter is intentionally generic. The ThreatLocker Portal API is uniform: every queryable resource exposes a <Resource>GetByParameters endpoint that takes a POST with a JSON body describing the filter, sort order and pagination. The adapter models each such endpoint as a feed, so adding a new event type is purely a configuration change — no code change required.

Out of the box

With no feeds configured, the adapter polls three feeds that together cover ThreatLocker's primary telemetry surfaces:

Default feed ThreatLocker endpoint What it carries
approval_request ApprovalRequest/ApprovalRequestGetByParameters (statusId = 1) Pending Application Control whitelist requests — one event per new request, shipped exactly once. Well suited to triggering automated investigations in LimaCharlie.
unified_audit ActionLog/ActionLogGetByParametersV2 The Unified Audit — ThreatLocker's combined event stream of execute / install / network / registry / read / write / move / delete / baseline / powershell / elevate / web activity across every module. Polled on a 5-minute rolling window.
system_audit SystemAudit/SystemAuditGetByParameters Portal / administrator activity — logins, policy edits, approval decisions, organization changes. Polled on a 5-minute rolling window.

unified_audit and system_audit both require a startDate/endDate filter on every request; the adapter rewrites those fields automatically (see the window feed setting below). At-least-once delivery is preserved by the per-feed deduper.

Each default feed can be turned off individually with collect_approval_requests / collect_unified_audit / collect_system_audit (all default true) without having to re-declare the others. Override feeds in your config to add custom feeds (e.g. denied approval requests, child-organization-scoped queries) or to replace the defaults entirely.

Authentication

Create an API token under Portal → Administration → API Users in the ThreatLocker portal. The token is sent verbatim in the Authorization header.

The API root is https://portalapi.<instance>.threatlocker.com/portalapi, where <instance> is your ThreatLocker instance. Provide it via instance, or set base_url to override the whole root.

Finding your instance

ThreatLocker hosts each tenant on one of several lettered instances (b, c, d, …) and API tokens are scoped to the instance that minted them. To find yours, open the ThreatLocker Portal, click the Help button in the top-right corner of any page, and read the letter in parentheses next to ThreatLocker Access (e.g. ThreatLocker Access (C) → instance: c).

⚠️ A token from one instance returns 403 TOKEN_REVOKED on every other instance — the API does not distinguish "wrong instance" from a genuinely revoked token. If you are confident the token is active and still see TOKEN_REVOKED, double-check the instance before assuming the token was revoked.

Parent-organization tokens

ThreatLocker scopes every query to the currently managed organization — the organization that minted the token (or the one named by the managed_organization_id header). By default the feeds ask only for that organization's own records (showChildOrganizations / viewChildOrganizations are false).

This is correct for a token scoped to a single (leaf) organization. It is not what you want for a token scoped to a parent/master organization: a parent has no endpoints of its own, so with the child-org flags off you will see

  • no approval_request events (the parent has no pending approvals of its own),
  • system_audit events that are mostly the adapter's own API polling, and
  • an HTTP 500 from the unified_audit (ActionLog) endpoint.

For a parent/master token, set include_child_organizations: true. The default feeds then flip their child-org flags on and return the parent's children (and grandchildren) too. Alternatively, set managed_organization_id to a specific child's GUID to scope every request down to that one child.

Configuration

Key Required Description
client_options yes Standard USP adapter options (see the repo README).
api_key yes ThreatLocker API token.
instance yes* ThreatLocker instance identifier (e.g. g). *Required unless base_url is set.
base_url no Full API root override, e.g. https://portalapi.g.threatlocker.com/portalapi.
managed_organization_id no Scopes every request to that organization via the managedOrganizationId header.
include_child_organizations no When true, the default feeds include child (and grandchild) organizations in their results. Set this when the token is scoped to a parent/master organization — see Parent-organization tokens. Default false. Only affects the default feeds; with custom feeds, set the flags in each feed's parameters yourself.
collect_approval_requests no Whether to run the approval_request default feed. Default true.
collect_unified_audit no Whether to run the unified_audit default feed. Default true.
collect_system_audit no Whether to run the system_audit default feed. Default true.
feeds no List of feeds to poll. Defaults to the three feeds listed under Out of the box. When set, it replaces the defaults entirely and the collect_* / include_child_organizations options no longer apply.
page_size no Records per page. Default 100 (max 1000).
poll_interval no Wait between polls, as a Go duration in nanoseconds. Default 60000000000 (1 minute).
dedupe_ttl no How long a record id is remembered to suppress re-shipping. Default 7 days.
retry_base_delay / max_retry_delay / max_retry_attempts no Transient-failure retry tuning.
Feed fields
Key Required Description
name yes Labels the feed and becomes the EventType of every shipped event.
url yes API path of the *GetByParameters endpoint, relative to the API root.
parameters no JSON object merged into the request body (resource-specific filters).
order_by no Sort field. Default dateTime.
items_path no Key holding the records array when the response is an object envelope. Auto-detected (data, pageItems, ...) when empty.
timestamp_field no Path to the record's event time (supports /-separated nested paths). Default dateTime.
id_field no Path to the record's stable identifier, used for deduplication. Falls back to common id fields, then a content hash.
max_pages no Caps pages fetched per poll. Default 100.
window no When set (e.g. 5m), rewrites startDate / endDate on every poll to a rolling [now-window-poll_interval, now] range. Required for endpoints that mandate a date range (ActionLog, SystemAudit). The overlap with the previous poll is absorbed by the deduper.
start_date_field / end_date_field no Override the request-body field names used by window. Defaults: startDate / endDate.

How polling works

On every poll the adapter walks a feed's pages (pageNumber/pageSize) until the result set is exhausted (a short or empty page) or the feed's max_pages cap is reached. An in-memory deduper, keyed per feed, guarantees each record is shipped to LimaCharlie exactly once even though pages are re-fetched on every poll. Transient API failures (HTTP 5xx, 429, network errors) are retried with exponential backoff.

Errors coming back from the ThreatLocker API — including persistent 5xx, a malformed response, or an authentication failure (401/403) — are logged as warnings and never stop the adapter. The failing feed simply skips that poll and tries again on the next interval, while the other feeds keep running. This is deliberate: a problem on ThreatLocker's side cannot be fixed by restarting the collector, so the adapter does not treat it as fatal (which, in the hosted cloud-adapter environment, would tear the whole adapter down and eventually disable it). A token or endpoint that is fixed on the ThreatLocker side therefore recovers on its own, with no operator action. Only a failure delivering events to LimaCharlie is fatal, because there a restart re-establishes the connection. If a feed is known to be permanently broken for your tenant, turn it off with the matching collect_* option rather than leaving it to warn each poll.

The adapter deliberately re-walks every page rather than stopping early at the first page of already-seen records: the API paginates by offset over a live, mutable list, so a record can shift across a page boundary between two page fetches and an early stop could skip it permanently. Re-walking costs more requests but is correct.

For a large or high-churn feed, bound each poll's work with the feed's parameters (e.g. a date-range filter) and a max_pages that comfortably exceeds the feed's expected size. Querying newest-first (isAscending = false, the default) keeps the most recent records when max_pages truncates.

Examples

Default (pending approval requests), via the CLI:

./general threatlocker \
  client_options.identity.oid=$OID \
  client_options.identity.installation_key=$INSTALLATION_KEY \
  client_options.platform=json \
  client_options.sensor_seed_key=threatlocker \
  api_key=$THREATLOCKER_API_TOKEN \
  instance=g

Adding a custom feed (e.g. ship denied approval requests too) on top of the defaults, via a YAML config file:

threatlocker:
  client_options:
    identity:
      oid: $OID
      installation_key: $INSTALLATION_KEY
    platform: json
    sensor_seed_key: threatlocker
  api_key: $THREATLOCKER_API_TOKEN
  instance: g
  feeds:
    # Re-declare the three defaults explicitly so this list fully replaces them,
    # then append the custom feed.
    - name: approval_request
      url: ApprovalRequest/ApprovalRequestGetByParameters
      parameters:
        statusId: 1            # pending
        showChildOrganizations: false
      id_field: approvalRequestId
    - name: unified_audit
      url: ActionLog/ActionLogGetByParametersV2
      window: 5m
      parameters:
        paramsFieldsDto: []
        groupBys: []
        exportMode: false
        showTotalCount: false
        showChildOrganizations: false
        onlyTrueDenies: false
        simulateDeny: false
      id_field: actionLogId
    - name: system_audit
      url: SystemAudit/SystemAuditGetByParameters
      window: 5m
      parameters:
        viewChildOrganizations: false
      id_field: systemAuditId
    - name: approval_request_denied
      url: ApprovalRequest/ApprovalRequestGetByParameters
      parameters:
        statusId: 3            # denied
        showChildOrganizations: false
      id_field: approvalRequestId

Documentation

Overview

Package usp_threatlocker implements a generic USP adapter for the ThreatLocker Portal API (https://portalapi.<instance>.threatlocker.com/portalapi).

The ThreatLocker Portal API is uniform: every queryable resource exposes a "<Resource>GetByParameters" endpoint that takes a POST with a JSON body describing the filter, sort order and pagination, and returns the matching records. The adapter models a "feed" as one such endpoint plus its request parameters, so supporting a new ThreatLocker event type is purely a matter of configuration -- no code change required.

Events are forwarded to LimaCharlie in their original ThreatLocker JSON form; the adapter does not reshape payloads.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type HTTPError

type HTTPError struct {
	StatusCode int
	URL        string
	Body       string
}

HTTPError represents a non-2xx response from the ThreatLocker API. It carries the status code so callers can classify the error (retry vs. give up) without having to parse error strings.

func (*HTTPError) Error

func (e *HTTPError) Error() string

type ThreatLockerAdapter

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

ThreatLockerAdapter polls one or more ThreatLocker feeds and ships their records to LimaCharlie.

func NewThreatLockerAdapter

func NewThreatLockerAdapter(ctx context.Context, conf ThreatLockerConfig) (*ThreatLockerAdapter, chan struct{}, error)

NewThreatLockerAdapter creates a ThreatLocker adapter wired to LimaCharlie.

func (*ThreatLockerAdapter) Close

func (a *ThreatLockerAdapter) Close() error

Close stops the adapter. It is idempotent: repeated calls are no-ops and return the result of the first call.

type ThreatLockerClient

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

ThreatLockerClient is a thin wrapper around the ThreatLocker Portal API.

The Portal API is uniform: every queryable resource exposes a "<Resource>GetByParameters" endpoint that accepts a POST with a JSON body describing the filter, sort and pagination. This client therefore only needs a single generic POST helper, which keeps the adapter trivial to extend to new resources.

func NewThreatLockerClient

func NewThreatLockerClient(baseURL, apiKey, managedOrgID string) *ThreatLockerClient

NewThreatLockerClient builds a client. baseURL is the API root, e.g. "https://portalapi.<instance>.threatlocker.com/portalapi".

func (*ThreatLockerClient) Close

func (c *ThreatLockerClient) Close()

Close releases idle connections held by the underlying transport.

func (*ThreatLockerClient) Post

func (c *ThreatLockerClient) Post(ctx context.Context, path string, body interface{}) ([]byte, error)

Post issues a POST to the given API path with a JSON body and returns the raw response body. A non-200 response is returned as an *HTTPError.

type ThreatLockerConfig

type ThreatLockerConfig struct {
	ClientOptions uspclient.ClientOptions `json:"client_options" yaml:"client_options"`

	// APIKey is a ThreatLocker API token (Portal > API Users).
	APIKey string `json:"api_key" yaml:"api_key"`

	// Instance is the ThreatLocker instance identifier used to build the API
	// root: https://portalapi.<instance>.threatlocker.com/portalapi
	Instance string `json:"instance" yaml:"instance"`

	// BaseURL fully overrides the API root. When set, Instance is ignored.
	BaseURL string `json:"base_url" yaml:"base_url"`

	// ManagedOrganizationID, when set, scopes every request to that
	// organization via the managedOrganizationId header (useful for parent
	// organizations querying a specific child).
	ManagedOrganizationID string `json:"managed_organization_id" yaml:"managed_organization_id"`

	// IncludeChildOrganizations, when true, makes the default feeds include
	// child (and grandchild) organizations in their results by flipping on the
	// per-endpoint child-org flags (showChildOrganizations / viewChildOrganizations).
	//
	// Set this when the API token is scoped to a parent/master organization and
	// you want to collect the children's data: a parent has no endpoints of its
	// own, so with this off the approval-request feed is empty, the system-audit
	// feed carries only the adapter's own API activity, and the unified-audit
	// (ActionLog) feed can fail with HTTP 500. This only affects the default
	// feeds; when you supply your own `feeds`, set the flags in each feed's
	// `parameters` yourself.
	IncludeChildOrganizations bool `json:"include_child_organizations" yaml:"include_child_organizations"`

	// CollectApprovalRequests / CollectUnifiedAudit / CollectSystemAudit select
	// which of the three default feeds run. A nil (absent) value means enabled,
	// so the zero-config default collects all three. Set one to false to drop
	// that feed. These are ignored when a custom `feeds` list is supplied (in
	// that case the list itself is authoritative). At least one default feed
	// must remain enabled.
	CollectApprovalRequests *bool `json:"collect_approval_requests" yaml:"collect_approval_requests"`
	CollectUnifiedAudit     *bool `json:"collect_unified_audit" yaml:"collect_unified_audit"`
	CollectSystemAudit      *bool `json:"collect_system_audit" yaml:"collect_system_audit"`

	// Feeds is the set of ThreatLocker endpoints to poll. When empty, the
	// adapter defaults to a single feed of pending Application Control
	// approval requests.
	Feeds []ThreatLockerFeed `json:"feeds" yaml:"feeds"`

	// PageSize is the number of records requested per page. Default 100.
	PageSize int `json:"page_size" yaml:"page_size"`

	// PollInterval is the wait between polls of a feed. Default 1 minute.
	PollInterval time.Duration `json:"poll_interval" yaml:"poll_interval"`

	// DedupeTTL is how long a record's identifier is remembered to suppress
	// re-shipping it on subsequent polls. Default 7 days.
	DedupeTTL time.Duration `json:"dedupe_ttl" yaml:"dedupe_ttl"`

	// Retry tuning for transient API failures.
	RetryBaseDelay   time.Duration `json:"retry_base_delay" yaml:"retry_base_delay"`
	MaxRetryDelay    time.Duration `json:"max_retry_delay" yaml:"max_retry_delay"`
	MaxRetryAttempts int           `json:"max_retry_attempts" yaml:"max_retry_attempts"`

	// Deduper, when set, replaces the built-in in-memory deduper. It is not
	// settable through a config file; it exists as a seam for tests and for
	// embedders that want to supply a shared deduper.
	Deduper utils.Deduper `json:"-" yaml:"-"`
}

ThreatLockerConfig is the adapter configuration.

func (*ThreatLockerConfig) Validate

func (c *ThreatLockerConfig) Validate() error

type ThreatLockerFeed

type ThreatLockerFeed struct {
	// Name labels the feed and becomes the EventType of every shipped event.
	Name string `json:"name" yaml:"name"`

	// URL is the API path (relative to the API root) of the GetByParameters
	// endpoint, e.g. "ApprovalRequest/ApprovalRequestGetByParameters".
	URL string `json:"url" yaml:"url"`

	// Parameters are merged into the request body sent to the endpoint. Use it
	// for resource-specific filters, e.g. {"statusId": 1} for pending approval
	// requests. The adapter always sets pageNumber/pageSize itself, and sets
	// orderBy/isAscending unless they are provided here.
	Parameters utils.Dict `json:"parameters" yaml:"parameters"`

	// OrderBy is the field the endpoint sorts on. The adapter relies on a
	// newest-first ordering (isAscending=false) for incremental polling.
	OrderBy string `json:"order_by" yaml:"order_by"`

	// ItemsPath is the key under which the records array lives when the
	// endpoint returns an object envelope. Empty means: response is a bare
	// array, or auto-detect a common key.
	ItemsPath string `json:"items_path" yaml:"items_path"`

	// TimestampField is the path to the record's event time (a "/" separated
	// path is supported for nested fields). Defaults to "dateTime".
	TimestampField string `json:"timestamp_field" yaml:"timestamp_field"`

	// IDField is the path to the record's stable identifier, used for
	// deduplication. Empty means: probe a set of common id fields, and fall
	// back to a content hash.
	IDField string `json:"id_field" yaml:"id_field"`

	// MaxPages caps how many pages are fetched per poll, bounding the work of
	// a first poll against a large historical data set. Defaults to 100.
	MaxPages int `json:"max_pages" yaml:"max_pages"`

	// Window, when > 0, enables rolling time-range filtering on each poll. On
	// each call the adapter sets StartDateField to now-Window-PollInterval and
	// EndDateField to now. The +PollInterval ensures consecutive polls overlap
	// (so a slipped or delayed poll cycle does not leave a gap); the deduper
	// suppresses re-shipping records that fall into the overlap. Endpoints
	// like ActionLog and SystemAudit reject requests that omit a date range.
	Window time.Duration `json:"window" yaml:"window"`

	// StartDateField / EndDateField name the request-body fields that carry
	// the rolling-window endpoints. Defaults: "startDate" / "endDate". Only
	// consulted when Window > 0.
	StartDateField string `json:"start_date_field" yaml:"start_date_field"`
	EndDateField   string `json:"end_date_field" yaml:"end_date_field"`
}

ThreatLockerFeed describes a single ThreatLocker "*GetByParameters" endpoint to poll. New event types are added by appending feeds -- no code change.

Jump to

Keyboard shortcuts

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