Skip to content

Commit 3301471

Browse files
refactor: extract Wazuh DockerListener setup into dedicated package
- Moved DockerListener installation logic from cmd/create to pkg/wazuh/dockerlistener for better code organization - Added sync command support for --wazuh --docker integration to enable container event monitoring - Fixed Authentik blueprint export to capture stdout directly instead of using non-existent --output flag
1 parent ec9137b commit 3301471

10 files changed

Lines changed: 842 additions & 128 deletions

File tree

cmd/create/wazuh_docker_listener.go

Lines changed: 9 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,95 +2,28 @@
22
package create
33

44
import (
5-
"os"
6-
"strings"
7-
5+
eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
86
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
9-
"github.com/CodeMonkeyCybersecurity/eos/pkg/execute"
10-
"github.com/CodeMonkeyCybersecurity/eos/pkg/shared"
7+
"github.com/CodeMonkeyCybersecurity/eos/pkg/wazuh/dockerlistener"
8+
"github.com/spf13/cobra"
119
"github.com/uptrace/opentelemetry-go-extra/otelzap"
1210
"go.uber.org/zap"
13-
14-
eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
15-
"github.com/spf13/cobra"
1611
)
1712

1813
var DockerListenerCmd = &cobra.Command{
1914
Use: "docker-listener",
2015
Short: "Installs and configures the Wazuh DockerListener for Wazuh",
2116
Long: "Sets up a Python virtual environment and configures Wazuh's DockerListener integration.",
2217
RunE: eos.Wrap(func(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
23-
otelzap.Ctx(rc.Ctx).Info(" Setting up Wazuh DockerListener...")
24-
25-
steps := []struct {
26-
desc string
27-
fn func() error
28-
}{
29-
{" apt update", func() error { return execute.RunSimple(rc.Ctx, "apt", "update") }},
30-
{" install python3-venv + pip", func() error {
31-
return execute.RunSimple(rc.Ctx, "apt", "install", "-y", "python3-venv", "python3-pip")
32-
}},
33-
{" create venv dir", func() error {
34-
return execute.RunSimple(rc.Ctx, "mkdir", "-p", shared.VenvPath)
35-
}},
36-
{"🐍 create venv", func() error {
37-
return execute.RunSimple(rc.Ctx, "python3", "-m", "venv", shared.VenvPath)
38-
}},
39-
{" pip install requirements", func() error {
40-
return execute.RunSimple(rc.Ctx, shared.VenvPath+"/bin/pip", "install",
41-
"docker==7.1.0", "urllib3==1.26.20", "requests==2.32.2")
42-
}},
43-
{" patch DockerListener", func() error {
44-
return patchDockerListener(rc)
45-
}},
46-
{" restart wazuh-agent", func() error {
47-
return execute.RunSimple(rc.Ctx, "systemctl", "restart", "wazuh-agent")
48-
}},
49-
}
18+
logger := otelzap.Ctx(rc.Ctx)
19+
logger.Info(" Setting up Wazuh DockerListener...")
5020

51-
for _, step := range steps {
52-
otelzap.Ctx(rc.Ctx).Info(step.desc)
53-
if err := step.fn(); err != nil {
54-
otelzap.Ctx(rc.Ctx).Error(" Failed: "+step.desc, zap.Error(err))
55-
return err
56-
}
21+
if err := dockerlistener.Setup(rc); err != nil {
22+
logger.Error(" DockerListener setup failed", zap.Error(err))
23+
return err
5724
}
5825

59-
otelzap.Ctx(rc.Ctx).Info(" DockerListener setup complete.")
26+
logger.Info(" DockerListener setup complete.")
6027
return nil
6128
}),
6229
}
63-
64-
// TODO move to pkg/ to DRY up this code base but putting it with other similar functions
65-
func patchDockerListener(rc *eos_io.RuntimeContext) error {
66-
path := shared.DockerListener
67-
if _, err := os.Stat(path); os.IsNotExist(err) {
68-
otelzap.Ctx(rc.Ctx).Warn("DockerListener script not found", zap.String("path", path))
69-
return nil
70-
}
71-
72-
backup := path + ".bak"
73-
if err := execute.RunSimple(rc.Ctx, "cp", path, backup); err != nil {
74-
otelzap.Ctx(rc.Ctx).Warn("Failed to backup DockerListener", zap.Error(err))
75-
}
76-
77-
content, err := os.ReadFile(path)
78-
if err != nil {
79-
return err
80-
}
81-
82-
lines := strings.Split(string(content), "\n")
83-
if len(lines) < 2 {
84-
return nil // malformed or empty script
85-
}
86-
87-
shebang := "#!" + shared.VenvPath + "/bin/python3"
88-
newContent := shebang + "\n" + strings.Join(lines[1:], "\n")
89-
90-
if err := os.WriteFile(path, []byte(newContent), shared.DirPermStandard); err != nil {
91-
return err
92-
}
93-
94-
otelzap.Ctx(rc.Ctx).Info(" DockerListener script patched", zap.String("path", path))
95-
return nil
96-
}

cmd/sync/sync.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ var (
2828
syncTailscale bool
2929
syncAuthentik bool
3030
syncWazuh bool
31+
syncDocker bool
3132
)
3233

3334
func init() {
3435
// Register all available connectors
3536
sync.RegisterConnector(connectors.NewConsulVaultConnector())
3637
sync.RegisterConnector(connectors.NewConsulTailscaleAutoConnector())
3738
sync.RegisterConnector(connectors.NewAuthentikWazuhConnector())
39+
sync.RegisterConnector(connectors.NewWazuhDockerConnector())
3840
}
3941

4042
// SyncCmd is the root command for service synchronization
@@ -52,6 +54,7 @@ Currently supported service pairs:
5254
register Vault in Consul service catalog (Pattern 3: Raft + Secrets Engine)
5355
- --consul --tailscale: Configure local Consul to bind to Tailscale IP
5456
- --authentik --wazuh: Configure Wazuh SSO integration with Authentik
57+
- --wazuh --docker: Configure Wazuh DockerListener for container event monitoring
5558
5659
For joining Consul nodes into a cluster:
5760
- eos sync consul --nodes vhost7 vhost11 # Join multiple Consul nodes together
@@ -74,6 +77,9 @@ Examples:
7477
# Configure Wazuh SSO with Authentik
7578
eos sync --authentik --wazuh
7679
80+
# Enable Wazuh Docker container monitoring
81+
eos sync --wazuh --docker
82+
7783
# Preview changes without applying (dry-run)
7884
eos sync --consul --vault --dry-run
7985
@@ -100,6 +106,8 @@ func init() {
100106
"Sync Authentik service")
101107
SyncCmd.Flags().BoolVar(&syncWazuh, "wazuh", false,
102108
"Sync Wazuh service")
109+
SyncCmd.Flags().BoolVar(&syncDocker, "docker", false,
110+
"Sync Docker container monitoring with Wazuh")
103111

104112
// Operation flags
105113
SyncCmd.Flags().BoolVar(&syncDryRun, "dry-run", false,
@@ -132,12 +140,15 @@ func runSync(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error
132140
if syncWazuh {
133141
selectedServices = append(selectedServices, "wazuh")
134142
}
143+
if syncDocker {
144+
selectedServices = append(selectedServices, "docker")
145+
}
135146

136147
// Validate exactly 2 services selected
137148
if len(selectedServices) == 0 {
138149
return eos_err.NewUserError(
139150
"No services specified. Please specify exactly 2 services to sync.\n\n" +
140-
"Available services: --consul, --vault, --tailscale, --authentik, --wazuh\n\n" +
151+
"Available services: --consul, --vault, --tailscale, --authentik, --wazuh, --docker\n\n" +
141152
"Examples:\n" +
142153
" eos sync --consul --vault\n" +
143154
" eos sync --authentik --wazuh\n" +
@@ -178,7 +189,8 @@ func runSync(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error
178189
"Service pair not supported: %s ↔ %s\n\n"+
179190
"Currently supported pairs:\n"+
180191
" - consul ↔ vault\n"+
181-
" - consul ↔ tailscale (auto-discovers and joins Consul nodes)\n\n"+
192+
" - consul ↔ tailscale (auto-discovers and joins Consul nodes)\n"+
193+
" - docker ↔ wazuh (configures Wazuh DockerListener)\n\n"+
182194
"For explicit node targeting:\n"+
183195
" - eos sync consul --vhost7 --vhost11\n\n"+
184196
"Error: %v",

go.mod

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ require (
2828
github.com/hashicorp/go-multierror v1.1.1
2929
github.com/hashicorp/go-version v1.7.0
3030
github.com/hashicorp/hcl/v2 v2.24.0
31-
github.com/hashicorp/nomad/api v0.0.0-20251103214437-68b5cfb5c6b9
31+
github.com/hashicorp/nomad/api v0.0.0-20251104073108-6235838dbf30
3232
github.com/hashicorp/terraform-exec v0.24.0
3333
github.com/hashicorp/vault/api v1.22.0
3434
github.com/hashicorp/vault/api/auth/approle v0.11.0
@@ -67,6 +67,14 @@ require (
6767
tailscale.com v1.90.6
6868
)
6969

70+
exclude (
71+
github.com/armon/go-metrics v0.5.0
72+
github.com/armon/go-metrics v0.5.1
73+
github.com/armon/go-metrics v0.5.2
74+
github.com/armon/go-metrics v0.5.3
75+
github.com/armon/go-metrics v0.5.4
76+
)
77+
7078
require (
7179
cuelabs.dev/go/oci/ociregistry v0.0.0-20250722084951-074d06050084 // indirect
7280
dario.cat/mergo v1.0.2 // indirect
@@ -109,7 +117,7 @@ require (
109117
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
110118
github.com/distribution/reference v0.6.0 // indirect
111119
github.com/docker/go-units v0.5.0 // indirect
112-
github.com/ebitengine/purego v0.9.0 // indirect
120+
github.com/ebitengine/purego v0.9.1 // indirect
113121
github.com/emicklei/proto v1.14.2 // indirect
114122
github.com/emirpasic/gods v1.18.1 // indirect
115123
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
163163
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
164164
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
165165
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
166+
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
167+
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
166168
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
167169
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
168170
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
@@ -349,6 +351,8 @@ github.com/hashicorp/nomad/api v0.0.0-20251029164822-3a20db342673 h1:aq5EIY1F520
349351
github.com/hashicorp/nomad/api v0.0.0-20251029164822-3a20db342673/go.mod h1:sldFTIgs+FsUeKU3LwVjviAIuksxD8TzDOn02MYwslE=
350352
github.com/hashicorp/nomad/api v0.0.0-20251103214437-68b5cfb5c6b9 h1:PURgS42XSZh1yIUA5gOzrkSKAGzP72QFZMASZdhdfMY=
351353
github.com/hashicorp/nomad/api v0.0.0-20251103214437-68b5cfb5c6b9/go.mod h1:sldFTIgs+FsUeKU3LwVjviAIuksxD8TzDOn02MYwslE=
354+
github.com/hashicorp/nomad/api v0.0.0-20251104073108-6235838dbf30 h1:ZAebOeSxrdDmmaD9dK/4MRe6gTI0Ts25D6eKCMbnNNQ=
355+
github.com/hashicorp/nomad/api v0.0.0-20251104073108-6235838dbf30/go.mod h1:sldFTIgs+FsUeKU3LwVjviAIuksxD8TzDOn02MYwslE=
352356
github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc=
353357
github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY=
354358
github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE=

pkg/authentik/blueprints.go

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,56 +20,65 @@ import (
2020
// ExportBlueprint exports Authentik configuration as Blueprint YAML
2121
// VENDOR APPROACH: Uses `ak export_blueprint` command in worker container
2222
// BENEFITS: Automatic UUID handling, dependency resolution, official support
23+
// FIXED: ak export_blueprint outputs to stdout (no --output flag exists)
24+
// EVIDENCE: https://docs.goauthentik.io/customize/blueprints/export/
2325
func (c *Client) ExportBlueprint(ctx context.Context, outputPath string) error {
24-
// Run ak export_blueprint command in worker container
25-
// NOTE: Exports flows, stages, policies, providers as YAML
26+
// CORRECTED: ak export_blueprint outputs to stdout, redirect to file
27+
// BEFORE (WRONG): "ak", "export_blueprint", "--output", "/tmp/blueprint.yaml"
28+
// AFTER (CORRECT): "ak", "export_blueprint" > file
2629
cmd := exec.CommandContext(ctx,
2730
"docker", "exec",
2831
"hecate-server-1", // Authentik server container
2932
"ak", "export_blueprint",
30-
"--output", "/tmp/blueprint.yaml",
3133
)
3234

33-
output, err := cmd.CombinedOutput()
35+
// Capture stdout (blueprint YAML)
36+
output, err := cmd.Output()
3437
if err != nil {
35-
return fmt.Errorf("blueprint export failed: %w (output: %s)", err, string(output))
38+
// Include stderr for diagnostics
39+
if exitErr, ok := err.(*exec.ExitError); ok {
40+
return fmt.Errorf("blueprint export failed: %w\nStderr: %s", err, string(exitErr.Stderr))
41+
}
42+
return fmt.Errorf("blueprint export failed: %w", err)
3643
}
3744

38-
// Copy blueprint from container to host
39-
copyCmd := exec.CommandContext(ctx,
40-
"docker", "cp",
41-
"hecate-server-1:/tmp/blueprint.yaml",
42-
outputPath,
43-
)
45+
// Write blueprint output directly to host file
46+
if err := os.WriteFile(outputPath, output, 0600); err != nil {
47+
return fmt.Errorf("failed to write blueprint to %s: %w", outputPath, err)
48+
}
4449

45-
if err := copyCmd.Run(); err != nil {
46-
return fmt.Errorf("failed to copy blueprint from container: %w", err)
50+
// Verify file has content
51+
if len(output) == 0 {
52+
return fmt.Errorf("blueprint export produced empty output")
4753
}
4854

4955
return nil
5056
}
5157

5258
// ExportBlueprintToDirectory exports Blueprint and saves to specified directory
5359
// CONVENIENCE: Wrapper around ExportBlueprint with timestamped filename
60+
// FIXED: ak export_blueprint outputs to stdout (no --output flag exists)
61+
// EVIDENCE: https://docs.goauthentik.io/customize/blueprints/export/
5462
func ExportBlueprintToDirectory(rc *eos_io.RuntimeContext, outputDir string) (string, error) {
5563
logger := otelzap.Ctx(rc.Ctx)
5664

5765
// Create Blueprint filename
5866
blueprintPath := filepath.Join(outputDir, "23_authentik_blueprint.yaml")
5967

60-
// Use unified client to export
61-
// NOTE: For now, use exec directly until Client consolidation complete
68+
// CORRECTED: ak export_blueprint outputs to stdout, redirect to file
69+
// BEFORE (WRONG): "ak", "export_blueprint", "--output", "/tmp/blueprint.yaml"
70+
// AFTER (CORRECT): "ak", "export_blueprint" > file
6271
cmd := exec.CommandContext(rc.Ctx,
6372
"docker", "exec",
6473
"hecate-server-1",
6574
"ak", "export_blueprint",
66-
"--output", "/tmp/blueprint.yaml",
6775
)
6876

6977
logger.Info("Exporting Authentik Blueprint via ak command",
7078
zap.String("container", "hecate-server-1"))
7179

72-
output, err := cmd.CombinedOutput()
80+
// Capture stdout (blueprint YAML)
81+
output, err := cmd.Output()
7382
if err != nil {
7483
// Check if container exists
7584
checkCmd := exec.CommandContext(rc.Ctx, "docker", "ps", "-a", "--filter", "name=hecate-server-1", "--format", "{{.Names}}")
@@ -78,18 +87,16 @@ func ExportBlueprintToDirectory(rc *eos_io.RuntimeContext, outputDir string) (st
7887
return "", fmt.Errorf("Authentik server container not found (hecate-server-1) - is docker-compose running?")
7988
}
8089

81-
return "", fmt.Errorf("blueprint export failed: %w (output: %s)", err, string(output))
90+
// Include stderr for diagnostics
91+
if exitErr, ok := err.(*exec.ExitError); ok {
92+
return "", fmt.Errorf("blueprint export failed: %w\nStderr: %s", err, string(exitErr.Stderr))
93+
}
94+
return "", fmt.Errorf("blueprint export failed: %w", err)
8295
}
8396

84-
// Copy blueprint from container to host
85-
copyCmd := exec.CommandContext(rc.Ctx,
86-
"docker", "cp",
87-
"hecate-server-1:/tmp/blueprint.yaml",
88-
blueprintPath,
89-
)
90-
91-
if err := copyCmd.Run(); err != nil {
92-
return "", fmt.Errorf("failed to copy blueprint from container: %w", err)
97+
// Write blueprint output directly to host file
98+
if err := os.WriteFile(blueprintPath, output, 0600); err != nil {
99+
return "", fmt.Errorf("failed to write blueprint to %s: %w", blueprintPath, err)
93100
}
94101

95102
// Verify file was created and has content
@@ -98,6 +105,10 @@ func ExportBlueprintToDirectory(rc *eos_io.RuntimeContext, outputDir string) (st
98105
return "", fmt.Errorf("blueprint file not created: %w", err)
99106
}
100107

108+
if info.Size() == 0 {
109+
return "", fmt.Errorf("blueprint file is empty - export may have failed silently")
110+
}
111+
101112
logger.Info("✓ Exported Authentik Blueprint",
102113
zap.String("file", "23_authentik_blueprint.yaml"),
103114
zap.Int64("size_bytes", info.Size()),

pkg/docker/client.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package docker
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/docker/docker/api/types"
8+
"github.com/docker/docker/api/types/container"
9+
"github.com/docker/docker/client"
10+
)
11+
12+
const defaultTimeout = 5 * time.Second
13+
14+
// New establishes a Docker client using environment configuration with API version negotiation enabled.
15+
func New(ctx context.Context) (*client.Client, error) {
16+
return client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
17+
}
18+
19+
// Ping validates connectivity with the Docker daemon within a short timeout window.
20+
func Ping(ctx context.Context, cli *client.Client) error {
21+
pingCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
22+
defer cancel()
23+
24+
_, err := cli.Ping(pingCtx)
25+
return err
26+
}
27+
28+
// ListContainers performs a lightweight container listing to confirm API access without retrieving the full dataset.
29+
func ListContainers(ctx context.Context, cli *client.Client, limit int) ([]types.Container, error) {
30+
listCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
31+
defer cancel()
32+
33+
return cli.ContainerList(listCtx, container.ListOptions{Limit: limit})
34+
}

0 commit comments

Comments
 (0)