Implementation of a Kafka REST Proxy logging plugin (#2534)
This PR implements the status/result logger functions necessary interface with a Kafka REST Proxy service. Specifically, this is compatible with the [Confluent KAFKA Rest Proxy Service ](https://docs.confluent.io/1.0/kafka-rest/docs/intro.html).
This commit is contained in:
@@ -875,7 +875,7 @@ Valid time units are `s`, `m`, `h`.
|
||||
|
||||
Which log output plugin should be used for osquery status logs received from clients.
|
||||
|
||||
Options are `filesystem`, `firehose`, `kinesis`, `lambda`, `pubsub`, and `stdout`.
|
||||
Options are `filesystem`, `firehose`, `kinesis`, `lambda`, `pubsub`, `kafkarest`, and `stdout`.
|
||||
|
||||
- Default value: `filesystem`
|
||||
- Environment variable: `FLEET_OSQUERY_STATUS_LOG_PLUGIN`
|
||||
@@ -890,7 +890,7 @@ Options are `filesystem`, `firehose`, `kinesis`, `lambda`, `pubsub`, and `stdout
|
||||
|
||||
Which log output plugin should be used for osquery result logs received from clients.
|
||||
|
||||
Options are `filesystem`, `firehose`, `kinesis`, `lambda`, `pubsub`, and `stdout`.
|
||||
Options are `filesystem`, `firehose`, `kinesis`, `lambda`, `pubsub`, `kafkarest`, and `stdout`.
|
||||
|
||||
- Default value: `filesystem`
|
||||
- Environment variable: `FLEET_OSQUERY_RESULT_LOG_PLUGIN`
|
||||
@@ -1425,6 +1425,68 @@ This feature is useful when combined with [subscription filters](https://cloud.g
|
||||
status_topic: osquery_status
|
||||
```
|
||||
|
||||
#### Kafka Rest Proxy Logging
|
||||
|
||||
##### kafkarest_proxyhost
|
||||
|
||||
This flag only has effect if `osquery_status_log_plugin` or `osquery_result_log_plugin` is set to `kafkarest`.
|
||||
|
||||
The URL of the host which to check for the topic existence and post messages to the specified topic.
|
||||
|
||||
- Default value: none
|
||||
- Environment variable: `FLEET_KAFKAREST_PROXYHOST`
|
||||
- Config file format:
|
||||
|
||||
```
|
||||
kafkarest:
|
||||
proxyhost: "https://localhost:8443"
|
||||
```
|
||||
|
||||
##### kafkarest_status_topic
|
||||
|
||||
This flag only has effect if `osquery_status_log_plugin` is set to `kafkarest`.
|
||||
|
||||
The identifier of the kafka topic that osquery status logs will be published to.
|
||||
|
||||
- Default value: none
|
||||
- Environment variable: `FLEET_KAFKAREST_STATUS_TOPIC`
|
||||
- Config file format:
|
||||
|
||||
```
|
||||
kafkarest:
|
||||
status_topic: osquery_status
|
||||
```
|
||||
|
||||
##### kafkarest_result_topic
|
||||
|
||||
This flag only has effect if `osquery_result_log_plugin` is set to `kafkarest`.
|
||||
|
||||
The identifier of the kafka topic that osquery status logs will be published to.
|
||||
|
||||
- Default value: none
|
||||
- Environment variable: `FLEET_KAFKAREST_RESULT_TOPIC`
|
||||
- Config file format:
|
||||
|
||||
```
|
||||
kafkarest:
|
||||
status_topic: osquery_result
|
||||
```
|
||||
|
||||
##### kafkarest_timeout
|
||||
|
||||
This flag only has effect if `osquery_status_log_plugin` or `osquery_result_log_plugin` is set to `kafkarest`.
|
||||
|
||||
The timeout value for the http post attempt. Value is in units of seconds.
|
||||
|
||||
- Default value: 5
|
||||
- Environment variable: `FLEET_KAFKAREST_TIMEOUT`
|
||||
- Config file format:
|
||||
|
||||
```
|
||||
kafkarest:
|
||||
timeout: 5
|
||||
```
|
||||
|
||||
#### S3 file carving backend
|
||||
|
||||
##### s3_bucket
|
||||
|
||||
+23
-3
@@ -33,7 +33,7 @@ type MysqlConfig struct {
|
||||
TLSKey string `yaml:"tls_key"`
|
||||
TLSCA string `yaml:"tls_ca"`
|
||||
TLSServerName string `yaml:"tls_server_name"`
|
||||
TLSConfig string `yaml:"tls_config"` //tls=customValue in DSN
|
||||
TLSConfig string `yaml:"tls_config"` // tls=customValue in DSN
|
||||
MaxOpenConns int `yaml:"max_open_conns"`
|
||||
MaxIdleConns int `yaml:"max_idle_conns"`
|
||||
ConnMaxLifetime int `yaml:"conn_max_lifetime"`
|
||||
@@ -183,6 +183,14 @@ type FilesystemConfig struct {
|
||||
EnableLogCompression bool `json:"enable_log_compression" yaml:"enable_log_compression"`
|
||||
}
|
||||
|
||||
// KafkaRESTConfig defines configs for the Kafka REST Proxy logging plugin.
|
||||
type KafkaRESTConfig struct {
|
||||
StatusTopic string `json:"status_topic" yaml:"status_topic"`
|
||||
ResultTopic string `json:"result_topic" yaml:"result_topic"`
|
||||
ProxyHost string `json:"proxyhost" yaml:"proxyhost"`
|
||||
Timeout int `json:"timeout" yaml:"timeout"`
|
||||
}
|
||||
|
||||
// LicenseConfig defines configs related to licensing Fleet.
|
||||
type LicenseConfig struct {
|
||||
Key string `yaml:"key"`
|
||||
@@ -218,6 +226,7 @@ type FleetConfig struct {
|
||||
S3 S3Config
|
||||
PubSub PubSubConfig
|
||||
Filesystem FilesystemConfig
|
||||
KafkaREST KafkaRESTConfig
|
||||
License LicenseConfig
|
||||
Vulnerabilities VulnerabilitiesConfig
|
||||
}
|
||||
@@ -454,6 +463,12 @@ func (man Manager) addConfigs() {
|
||||
man.addConfigBool("filesystem.enable_log_compression", false,
|
||||
"Enable compression for the rotated osquery log files")
|
||||
|
||||
// KafkaREST
|
||||
man.addConfigString("kafkarest.status_topic", "", "Kafka REST topic for status logs")
|
||||
man.addConfigString("kafkarest.result_topic", "", "Kafka REST topic for result logs")
|
||||
man.addConfigString("kafkarest.proxyhost", "", "Kafka REST proxy host url")
|
||||
man.addConfigInt("kafkarest.timeout", 5, "Kafka REST proxy json post timeout")
|
||||
|
||||
// License
|
||||
man.addConfigString("license.key", "", "Fleet license key (to enable Fleet Premium features)")
|
||||
|
||||
@@ -609,6 +624,12 @@ func (man Manager) LoadConfig() FleetConfig {
|
||||
EnableLogRotation: man.getConfigBool("filesystem.enable_log_rotation"),
|
||||
EnableLogCompression: man.getConfigBool("filesystem.enable_log_compression"),
|
||||
},
|
||||
KafkaREST: KafkaRESTConfig{
|
||||
StatusTopic: man.getConfigString("kafkarest.status_topic"),
|
||||
ResultTopic: man.getConfigString("kafkarest.result_topic"),
|
||||
ProxyHost: man.getConfigString("kafkarest.proxyhost"),
|
||||
Timeout: man.getConfigInt("kafkarest.timeout"),
|
||||
},
|
||||
License: LicenseConfig{
|
||||
Key: man.getConfigString("license.key"),
|
||||
},
|
||||
@@ -807,7 +828,6 @@ func (man Manager) loadConfigFile() {
|
||||
|
||||
man.viper.SetConfigFile(configFile)
|
||||
err := man.viper.ReadInConfig()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error loading config file:", err)
|
||||
os.Exit(1)
|
||||
@@ -819,7 +839,7 @@ func (man Manager) loadConfigFile() {
|
||||
// TestConfig returns a barebones configuration suitable for use in tests.
|
||||
// Individual tests may want to override some of the values provided.
|
||||
func TestConfig() FleetConfig {
|
||||
var testLogFile = "/dev/null"
|
||||
testLogFile := "/dev/null"
|
||||
if runtime.GOOS == "windows" {
|
||||
testLogFile = "NUL"
|
||||
}
|
||||
|
||||
@@ -383,3 +383,10 @@ type LambdaConfig struct {
|
||||
StatusFunction string `json:"status_function"`
|
||||
ResultFunction string `json:"result_function"`
|
||||
}
|
||||
|
||||
// KafkaRESTConfig shadows config.KafkaRESTConfig
|
||||
type KafkaRESTConfig struct {
|
||||
StatusTopic string `json:"status_topic"`
|
||||
ResultTopic string `json:"result_topic"`
|
||||
ProxyHost string `json:"proxyhost"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
krContentTypeValue = "application/vnd.kafka.json.v1+json"
|
||||
krContentTypeHeader = "Content-Type"
|
||||
krTimestampHeader = "TimeStamp"
|
||||
krPublishTopicURL = "%s/topics/%s"
|
||||
krCheckTopicURL = "%s/topics/?topic=%s"
|
||||
)
|
||||
|
||||
type KafkaRESTParams struct {
|
||||
KafkaProxyHost string
|
||||
KafkaTopic string
|
||||
KafkaTimeout int
|
||||
}
|
||||
|
||||
type kafkaRESTProducer struct {
|
||||
client *http.Client
|
||||
URL string
|
||||
CheckURL string
|
||||
}
|
||||
|
||||
type kafkaRecords struct {
|
||||
Records []kafkaValue `json:"records"`
|
||||
}
|
||||
|
||||
type kafkaValue struct {
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
|
||||
func NewKafkaRESTWriter(p *KafkaRESTParams) (*kafkaRESTProducer, error) {
|
||||
producer := &kafkaRESTProducer{
|
||||
URL: fmt.Sprintf(krPublishTopicURL, p.KafkaProxyHost, p.KafkaTopic),
|
||||
CheckURL: fmt.Sprintf(krCheckTopicURL, p.KafkaProxyHost, p.KafkaTopic),
|
||||
client: &http.Client{
|
||||
Timeout: time.Duration(p.KafkaTimeout) * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
return producer, producer.checkTopic()
|
||||
}
|
||||
|
||||
func (l *kafkaRESTProducer) Write(ctx context.Context, logs []json.RawMessage) error {
|
||||
data := kafkaRecords{
|
||||
Records: make([]kafkaValue, len(logs)),
|
||||
}
|
||||
|
||||
for i, log := range logs {
|
||||
data.Records[i] = kafkaValue{
|
||||
Value: log,
|
||||
}
|
||||
}
|
||||
|
||||
output, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "kafka rest marshal")
|
||||
}
|
||||
|
||||
resp, err := l.post(l.URL, bytes.NewBuffer(output))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "kafka rest post")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return checkResponse(resp)
|
||||
}
|
||||
|
||||
func checkResponse(resp *http.Response) (err error) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return errors.Errorf("Error: %d. %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *kafkaRESTProducer) checkTopic() (err error) {
|
||||
resp, err := l.client.Get(l.CheckURL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "kafka rest topic check")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return checkResponse(resp)
|
||||
}
|
||||
|
||||
func (l *kafkaRESTProducer) post(url string, buf *bytes.Buffer) (*http.Response, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, url, buf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "kafka rest new request")
|
||||
}
|
||||
|
||||
now := float64(time.Now().UnixNano()) / float64(time.Second)
|
||||
req.Header.Set(krContentTypeHeader, krContentTypeValue)
|
||||
req.Header.Set(krTimestampHeader, fmt.Sprintf("%f", now))
|
||||
|
||||
return l.client.Do(req)
|
||||
}
|
||||
@@ -87,6 +87,15 @@ func New(config config.FleetConfig, logger log.Logger) (*OsqueryLogger, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create stdout status logger")
|
||||
}
|
||||
case "kafkarest":
|
||||
status, err = NewKafkaRESTWriter(&KafkaRESTParams{
|
||||
KafkaProxyHost: config.KafkaREST.ProxyHost,
|
||||
KafkaTopic: config.KafkaREST.StatusTopic,
|
||||
KafkaTimeout: config.KafkaREST.Timeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create kafka rest status logger")
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf(
|
||||
"unknown status log plugin: %s", config.Osquery.StatusLogPlugin,
|
||||
@@ -161,6 +170,15 @@ func New(config config.FleetConfig, logger log.Logger) (*OsqueryLogger, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create stdout result logger")
|
||||
}
|
||||
case "kafkarest":
|
||||
result, err = NewKafkaRESTWriter(&KafkaRESTParams{
|
||||
KafkaProxyHost: config.KafkaREST.ProxyHost,
|
||||
KafkaTopic: config.KafkaREST.ResultTopic,
|
||||
KafkaTimeout: config.KafkaREST.Timeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create kafka rest result logger")
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf(
|
||||
"unknown result log plugin: %s", config.Osquery.StatusLogPlugin,
|
||||
|
||||
@@ -248,6 +248,14 @@ func (svc *Service) LoggingConfig(ctx context.Context) (*fleet.Logging, error) {
|
||||
}
|
||||
case "stdout":
|
||||
logging.Status = fleet.LoggingPlugin{Plugin: "stdout"}
|
||||
case "kafkarest":
|
||||
logging.Status = fleet.LoggingPlugin{
|
||||
Plugin: "kafkarest",
|
||||
Config: fleet.KafkaRESTConfig{
|
||||
StatusTopic: conf.KafkaREST.StatusTopic,
|
||||
ProxyHost: conf.KafkaREST.ProxyHost,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf("unrecognized logging plugin: %s", conf.Osquery.StatusLogPlugin)
|
||||
}
|
||||
@@ -294,6 +302,14 @@ func (svc *Service) LoggingConfig(ctx context.Context) (*fleet.Logging, error) {
|
||||
logging.Result = fleet.LoggingPlugin{
|
||||
Plugin: "stdout",
|
||||
}
|
||||
case "kafkarest":
|
||||
logging.Result = fleet.LoggingPlugin{
|
||||
Plugin: "kafkarest",
|
||||
Config: fleet.KafkaRESTConfig{
|
||||
ResultTopic: conf.KafkaREST.ResultTopic,
|
||||
ProxyHost: conf.KafkaREST.ProxyHost,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf("unrecognized logging plugin: %s", conf.Osquery.ResultLogPlugin)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user