Skip to content

Commit

Permalink
x-pack/filebeat/input/entityanalytics/provider/jamf: add jamf provider
Browse files Browse the repository at this point in the history
  • Loading branch information
efd6 committed Jun 24, 2024
1 parent be1d89b commit e0df903
Show file tree
Hide file tree
Showing 12 changed files with 1,625 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ https:/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff]
- Fix handling of infinite rate values in CEL rate limit handling logic. {pull}39940[39940]
- Allow elision of set and append failure logging. {issue}34544[34544] {pull}39929[39929]
- Add ability to remove request trace logs from CEL input. {pull}39969[39969]
- Add Jamf entity analytics provider. {pull}39996[39996]

*Auditbeat*

Expand Down
175 changes: 173 additions & 2 deletions x-pack/filebeat/docs/inputs/input-entity-analytics.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The following identity providers are supported:

- <<provider-activedirectory>>
- <<provider-azure-ad>>
- <<provider-jamf>>
- <<provider-okta>>

==== Configuration options
Expand Down Expand Up @@ -521,6 +522,176 @@ For Example, `http-request-trace-*.ndjson`.

Enabling this option compromises security and should only be used for debugging.

[id="provider-jamf"]
==== Jamf Computer Management (`jamf`)

The `jamf` provider allows the input to retrieve computer records from the
Jamf API.

[float]
==== How It Works

[float]
===== Overview

The Jamf provider periodically contacts the Jamf API, retrieving updates for
computers, updates its internal cache of managed computer metadata, and ships
updated metadata to Elasticsearch.

Fetching and shipping updates occurs in one of two processes: *full
synchronizations* and *incremental updates*. Full synchronizations will send
the entire list of computers in state, along with write markers to
indicate the start and end of the synchronization event. Incremental updates
will only send data for changed computers records during that event. Changes
on a user or device can come in many forms, whether it be a change to the
user's metadata, or a user was added or deleted.

[float]
===== API Interactions

The provider periodically retrieves changes to user/device metadata from the
Jamf computers-preview API. This is done through calls to:

- https://developer.jamf.com/jamf-pro/reference/get_preview-computers[/api/preview/computers]

Updates are tracked by the provider by retaining a record of the time of the last
noted update in the returned user list. During provider updates the Jamf provider
makes use of the Jamf API's query filtering to only request records updated at or
since the provider's recorded last update.

[float]
===== Sending Computer Metadata to Elasticsearch

During a full synchronization, all users/devices stored in state will be sent
to the output, while incremental updates will only send users and devices
that have been updated. Full synchronizations will be bounded on either side
by write marker documents, which will look something like this:

["source","json",subs="attributes"]
----
{
"@timestamp": "2022-11-04T09:57:19.786056-05:00",
"event": {
"action": "started",
"start": "2022-11-04T09:57:19.786056-05:00"
},
"labels": {
"identity_source": "jamf-1"
}
}
----

Documents will show the current state of the computer record.

Example document:

["source","json",subs="attributes"]
----
{
"device": {
"id": "5982CE36-4526-580B-B4B9-ECC6782535BC"
},
"event": {
"action": "device-discovered"
},
"jamf": {
"location": {
"username": "john.doe",
"position": "Unknown Developer"
},
"site": null,
"name": "acme-C07DM3AZQ6NV",
"udid": "5982CE36-4526-580B-B4B9-ECC6782535BC",
"serialNumber": "C07DM3AZQ6NV",
"operatingSystemVersion": "14.0",
"operatingSystemBuild": "23A344",
"operatingSystemSupplementalBuildVersion": null,
"operatingSystemRapidSecurityResponse": null,
"macAddress": "64:0B:D7:AA:E4:B2",
"assetTag": null,
"modelIdentifier": "Macmini9,1",
"mdmAccessRights": 0,
"lastContactDate": "2024-04-18T14:26:51.514Z",
"lastReportDate": "2024-06-19T15:54:37.692Z",
"lastEnrolledDate": "2023-02-22T10:46:17.199Z",
"ipAddress": null,
"managementId": "1a59c510-b3a9-41cb-8afa-3d4187ac60d0",
"isManaged": true
},
"labels": {
"identity_source": "jamf-1"
}
}
----

[float]
==== Configuration

Example configuration:

["source","yaml",subs="attributes"]
----
{beatname_lc}.inputs:
- type: entity-analytics
enabled: true
id: jamf-1
provider: jamf
dataset: "all"
sync_interval: "12h"
update_interval: "30m"
jamf_tenant: "JAMF_TENANT"
jamf_username: "JAMF_USERNAME"
jamf_password: "JAMF_PASSWORD"
----

The `jamf` provider supports the following configuration:

[float]
===== `jamf_tenant`

The Jamf tenant host. Field is required.

[float]
===== `jamf_username`

The Jamf username, used for authentication. Field is required.

[float]
===== `jamf_password`

The Jamf user password, used for authentication. Field is required.

[float]
===== `page_size`

The number of computer records to collect with each API request. Defaults to https://developer.jamf.com/jamf-pro/reference/get_preview-computers[API default].

[float]
===== `sync_interval`

The interval in which full synchronizations should occur. The interval must be
longer than the update interval (`update_interval`) Expressed as a duration
string (e.g., 1m, 3h, 24h). Defaults to `24h` (24 hours).

[float]
===== `update_interval`

The interval in which incremental updates should occur. The interval must be
shorter than the full synchronization interval (`sync_interval`). Expressed as a
duration string (e.g., 1m, 3h, 24h). Defaults to `15m` (15 minutes).

[float]
==== `tracer.filename`

It is possible to log HTTP requests and responses to the Jamf API to a local file-system for debugging configurations.
This option is enabled by setting the `tracer.filename` value. Additional options are available to
tune log rotation behavior.

To differentiate the trace files generated from different input instances, a placeholder `*` can be added to the filename and will be replaced with the input instance id.
For Example, `http-request-trace-*.ndjson`.

Enabling this option compromises security and should only be used for debugging.

[id="provider-okta"]
==== Okta User Identities (`okta`)

Expand Down Expand Up @@ -550,8 +721,8 @@ The Okta provider periodically contacts the Okta API, retrieving updates for
users and devices, updates its internal cache of user metadata, and ships
updated user/device metadata to Elasticsearch.

Fetching and shipping updates occurs in one of two processes: **full
synchronizations** and *incremental updates*. Full synchronizations will send
Fetching and shipping updates occurs in one of two processes: *full
synchronizations* and *incremental updates*. Full synchronizations will send
the entire list of users and devices in state, along with write markers to
indicate the start and end of the synchronization event. Incremental updates
will only send data for changed users and devices during that event. Changes
Expand Down
1 change: 1 addition & 0 deletions x-pack/filebeat/input/entityanalytics/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
// For provider registration.
_ "github.com/elastic/beats/v7/x-pack/filebeat/input/entityanalytics/provider/activedirectory"
_ "github.com/elastic/beats/v7/x-pack/filebeat/input/entityanalytics/provider/azuread"
_ "github.com/elastic/beats/v7/x-pack/filebeat/input/entityanalytics/provider/jamf"
_ "github.com/elastic/beats/v7/x-pack/filebeat/input/entityanalytics/provider/okta"
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.ndjson
182 changes: 182 additions & 0 deletions x-pack/filebeat/input/entityanalytics/provider/jamf/conf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package jamf

import (
"errors"
"time"

"gopkg.in/natefinch/lumberjack.v2"

"github.com/elastic/elastic-agent-libs/transport/httpcommon"
)

// defaultConfig returns a default configuration.
func defaultConfig() conf {
maxAttempts := 5
waitMin := time.Second
waitMax := time.Minute
transport := httpcommon.DefaultHTTPTransportSettings()
transport.Timeout = 30 * time.Second

return conf{
SyncInterval: 24 * time.Hour,
UpdateInterval: 15 * time.Minute,
TokenGrace: time.Minute,
Request: &requestConfig{
Retry: retryConfig{
MaxAttempts: &maxAttempts,
WaitMin: &waitMin,
WaitMax: &waitMax,
},
RedirectForwardHeaders: false,
RedirectMaxRedirects: 10,
Transport: transport,
},
}
}

// conf contains parameters needed to configure the input.
type conf struct {
JamfTenant string `config:"jamf_tenant" validate:"required"`
JamfUsername string `config:"jamf_username" validate:"required"`
JamfPassword string `config:"jamf_password" validate:"required"`

// PageSize is the number of entities to collect in each request.
PageSize int `config:"page_size"`

// TokenGrace is purely here to allow tuning the tolerance for
// token staling. It is intentionally not documented in the
// public documentation as it is not a user control.
TokenGrace time.Duration `config:"token_grace_period"`

// SyncInterval is the time between full
// synchronisation operations.
SyncInterval time.Duration `config:"sync_interval"`

// UpdateInterval is the time between
// incremental updated.
UpdateInterval time.Duration `config:"update_interval"`

// Request is the configuration for establishing
// HTTP requests to the API.
Request *requestConfig `config:"request"`

// Tracer allows configuration of request trace logging.
Tracer *lumberjack.Logger `config:"tracer"`
}

type requestConfig struct {
Retry retryConfig `config:"retry"`
RedirectForwardHeaders bool `config:"redirect.forward_headers"`
RedirectHeadersBanList []string `config:"redirect.headers_ban_list"`
RedirectMaxRedirects int `config:"redirect.max_redirects"`
KeepAlive keepAlive `config:"keep_alive"`

Transport httpcommon.HTTPTransportSettings `config:",inline"`
}

type retryConfig struct {
MaxAttempts *int `config:"max_attempts"`
WaitMin *time.Duration `config:"wait_min"`
WaitMax *time.Duration `config:"wait_max"`
}

func (c retryConfig) Validate() error {
switch {
case c.MaxAttempts != nil && *c.MaxAttempts <= 0:
return errors.New("max_attempts must be greater than zero")
case c.WaitMin != nil && *c.WaitMin <= 0:
return errors.New("wait_min must be greater than zero")
case c.WaitMax != nil && *c.WaitMax <= 0:
return errors.New("wait_max must be greater than zero")
}
return nil
}

func (c retryConfig) getMaxAttempts() int {
if c.MaxAttempts == nil {
return 0
}
return *c.MaxAttempts
}

func (c retryConfig) getWaitMin() time.Duration {
if c.WaitMin == nil {
return 0
}
return *c.WaitMin
}

func (c retryConfig) getWaitMax() time.Duration {
if c.WaitMax == nil {
return 0
}
return *c.WaitMax
}

type keepAlive struct {
Disable *bool `config:"disable"`
MaxIdleConns int `config:"max_idle_connections"`
MaxIdleConnsPerHost int `config:"max_idle_connections_per_host"` // If zero, http.DefaultMaxIdleConnsPerHost is the value used by http.Transport.
IdleConnTimeout time.Duration `config:"idle_connection_timeout"`
}

func (c keepAlive) Validate() error {
if c.Disable == nil || *c.Disable {
return nil
}
if c.MaxIdleConns < 0 {
return errors.New("max_idle_connections must not be negative")
}
if c.MaxIdleConnsPerHost < 0 {
return errors.New("max_idle_connections_per_host must not be negative")
}
if c.IdleConnTimeout < 0 {
return errors.New("idle_connection_timeout must not be negative")
}
return nil
}

func (c keepAlive) settings() httpcommon.WithKeepaliveSettings {
return httpcommon.WithKeepaliveSettings{
Disable: c.Disable == nil || *c.Disable,
MaxIdleConns: c.MaxIdleConns,
MaxIdleConnsPerHost: c.MaxIdleConnsPerHost,
IdleConnTimeout: c.IdleConnTimeout,
}
}

var (
errInvalidSyncInterval = errors.New("zero or negative sync_interval")
errInvalidUpdateInterval = errors.New("zero or negative update_interval")
errSyncBeforeUpdate = errors.New("sync_interval not longer than update_interval")
)

// Validate runs validation against the config.
func (c *conf) Validate() error {
switch {
case c.SyncInterval <= 0:
return errInvalidSyncInterval
case c.UpdateInterval <= 0:
return errInvalidUpdateInterval
case c.SyncInterval <= c.UpdateInterval:
return errSyncBeforeUpdate
}

if c.Tracer == nil {
return nil
}
if c.Tracer.Filename == "" {
return errors.New("request tracer must have a filename if used")
}
if c.Tracer.MaxSize == 0 {
// By default Lumberjack caps file sizes at 100MB which
// is excessive for a debugging logger, so default to 1MB
// which is the minimum.
c.Tracer.MaxSize = 1
}
return nil
}
Loading

0 comments on commit e0df903

Please sign in to comment.