Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add QRadar exporter #1866

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions exporter/qradar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# QRadaar Exporter

The QRadar Exporter is designed for forwarding logs to a QRadar instance using its Syslog endpoint. This exporter supports customization of data export types and various configuration options to tailor the connection and data handling to specific needs.

## Minimum Agent Versions

- Introduced: [v1.61.0](https://github.com/observIQ/bindplane-agent/releases/tag/v1.61.0)

## Supported Pipelines

- Logs

## Configuration

| Field | Type | Default Value | Required | Description |
| -------------------- | ------ | ----------------- | -------- | ------------------------------------------------- |
| raw_log_field | string | | `false` | The field name to send raw logs to QRadar. |
| syslog.endpoint | string | `127.0.0.1:10514` | `false` | The QRadar endpoint. |
| syslog.transport | string | `tcp` | `false` | The network protocol to use (e.g., `tcp`, `udp`). |
| syslog.tls.key_file | string | | `false` | Configure the receiver to use TLS. |
| syslog.tls.cert_file | string | | `false` | Configure the receiver to use TLS. |

## Raw Log Field

The raw log field is the field name that the exporter will use to send raw logs to QRadar. It is an OTTL expression that can be used to reference any field in the log record. If the field is not present in the log record, the exporter will not send the log to QRadar. The log record context can be viewed here: [Log Record Context](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/ottl/contexts/ottllog/README.md).

## Example Configurations

### Syslog Configuration Example

```yaml
qradar:
raw_log_field: body
syslog:
endpoint: "syslog.example.com:10514"
network: "tcp"
```

75 changes: 75 additions & 0 deletions exporter/qradar/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright observIQ, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package qradar

import (
"errors"
"fmt"

"github.com/observiq/bindplane-agent/expr"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/confignet"
"go.opentelemetry.io/collector/config/configretry"
"go.opentelemetry.io/collector/config/configtls"
"go.opentelemetry.io/collector/exporter/exporterhelper"
"go.uber.org/zap"
)

// Config defines configuration for the QRadar exporter.
type Config struct {
exporterhelper.TimeoutSettings `mapstructure:",squash"`
exporterhelper.QueueSettings `mapstructure:"sending_queue"`
configretry.BackOffConfig `mapstructure:"retry_on_failure"`

// Syslog is the configuration for the connection to QRadar.
Syslog SyslogConfig `mapstructure:"syslog"`

// RawLogField is the field name that will be used to send raw logs to QRadar.
RawLogField string `mapstructure:"raw_log_field"`
}

// SyslogConfig defines configuration for QRadar connection.
type SyslogConfig struct {
confignet.AddrConfig `mapstructure:",squash"`

// TLSSetting struct exposes TLS client configuration.
TLSSetting *configtls.ClientConfig `mapstructure:"tls"`
}

// validate validates the Syslog configuration.
func (s *SyslogConfig) validate() error {
if s.AddrConfig.Endpoint == "" {
return errors.New("incomplete syslog configuration: endpoint is required")
}
return nil
}

// Validate validates the QRadar exporter configuration.
func (cfg *Config) Validate() error {

if err := cfg.Syslog.validate(); err != nil {
return err
}

if cfg.RawLogField != "" {
_, err := expr.NewOTTLLogRecordExpression(cfg.RawLogField, component.TelemetrySettings{
Logger: zap.NewNop(),
})
if err != nil {
return fmt.Errorf("raw_log_field is invalid: %s", err)
}
}
return nil
}
66 changes: 66 additions & 0 deletions exporter/qradar/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright observIQ, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package qradar

import (
"testing"

"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/config/confignet"
)

func TestConfig_Validate(t *testing.T) {
tests := []struct {
name string
cfg Config
wantErr bool
}{
{
name: "Valid syslog config",
cfg: Config{
Syslog: SyslogConfig{
AddrConfig: confignet.AddrConfig{
Endpoint: "localhost:514",
Transport: "tcp",
},
},
},
wantErr: false,
},
{
name: "Invalid syslog config - missing host",
cfg: Config{
Syslog: SyslogConfig{
AddrConfig: confignet.AddrConfig{
Endpoint: "",
Transport: "tcp",
},
},
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cfg.Validate()
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
18 changes: 18 additions & 0 deletions exporter/qradar/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright observIQ, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:generate mdatagen metadata.yaml

// Package qradar exports OpenTelemetry data to an endpoint or file.
package qradar // import "github.com/observiq/bindplane-agent/exporter/qradar"
139 changes: 139 additions & 0 deletions exporter/qradar/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright observIQ, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package qradar

import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"os"
"path/filepath"
"strings"

"go.opentelemetry.io/collector/consumer"
"go.opentelemetry.io/collector/exporter"
"go.opentelemetry.io/collector/pdata/plog"
"go.uber.org/zap"
)

type qradarExporter struct {
cfg *Config
logger *zap.Logger
marshaler logMarshaler
endpoint string
qradarClient
}

// qradarClient is a client for creating connections to IBM QRadar. (created for overriding in tests)
//
//go:generate mockery --name qradarClient --output ./internal/mocks --with-expecter --filename qradar_client.go --structname MockForwarderClient
type qradarClient interface {
Dial(network string, address string) (net.Conn, error)
DialWithTLS(network string, addr string, config *tls.Config) (*tls.Conn, error)
OpenFile(name string) (*os.File, error)
}

type defaultClient struct {
}

func (fc *defaultClient) Dial(network string, address string) (net.Conn, error) {
return net.Dial(network, address)
}

func (fc *defaultClient) DialWithTLS(network string, addr string, config *tls.Config) (*tls.Conn, error) {
return tls.Dial(network, addr, config)
}

func (fc *defaultClient) OpenFile(name string) (*os.File, error) {
cleanPath := filepath.Clean(name)
return os.OpenFile(cleanPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
}

func newExporter(cfg *Config, params exporter.Settings) (*qradarExporter, error) {
return &qradarExporter{
cfg: cfg,
logger: params.Logger,
marshaler: newMarshaler(*cfg, params.TelemetrySettings),
qradarClient: &defaultClient{},
}, nil
}

func (ce *qradarExporter) Capabilities() consumer.Capabilities {
return consumer.Capabilities{MutatesData: false}
}

func (ce *qradarExporter) logsDataPusher(ctx context.Context, ld plog.Logs) error {
// Open connection or file before sending each payload
writer, err := ce.openWriter(ctx)
if err != nil {
return fmt.Errorf("open writer: %w", err)
}
defer writer.Close()

payloads, err := ce.marshaler.MarshalRawLogs(ctx, ld)
if err != nil {
return fmt.Errorf("marshal logs: %w", err)
}

for _, payload := range payloads {
if err := ce.send(payload, writer); err != nil {
return fmt.Errorf("upload to QRadar: %w", err)
}
}

return nil
}

func (ce *qradarExporter) openWriter(ctx context.Context) (io.WriteCloser, error) {
return ce.openSyslogWriter(ctx)
}

func (ce *qradarExporter) openSyslogWriter(ctx context.Context) (io.WriteCloser, error) {
var conn net.Conn
var err error
transportStr := string(ce.cfg.Syslog.AddrConfig.Transport)

if ce.cfg.Syslog.TLSSetting != nil {
var tlsConfig *tls.Config
tlsConfig, err = ce.cfg.Syslog.TLSSetting.LoadTLSConfig(ctx)
if err != nil {
return nil, fmt.Errorf("load TLS config: %w", err)
}
conn, err = ce.DialWithTLS(transportStr, ce.cfg.Syslog.AddrConfig.Endpoint, tlsConfig)

if err != nil {
return nil, fmt.Errorf("dial with tls: %w", err)
}
} else {
conn, err = ce.Dial(transportStr, ce.cfg.Syslog.AddrConfig.Endpoint)

if err != nil {
return nil, fmt.Errorf("dial: %w", err)
}
}

return conn, nil
}

func (ce *qradarExporter) send(msg string, writer io.WriteCloser) error {
if !strings.HasSuffix(msg, "\n") {
msg = fmt.Sprintf("%s%s", msg, "\n")
}

_, err := io.WriteString(writer, msg)
return err
}
Loading
Loading