Skip to content

Commit 3c4adf6

Browse files
authored
x-pack/filebeat/input/cel: add envvar support (elastic#40779)
1 parent 8b93e1c commit 3c4adf6

File tree

5 files changed

+101
-3
lines changed

5 files changed

+101
-3
lines changed

CHANGELOG.next.asciidoc

+1
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff]
304304
- Add `use_kubeadm` config option for filebeat (both filbeat.input and autodiscovery) in order to toggle kubeadm-config api requests {pull}40301[40301]
305305
- Make HTTP library function inclusion non-conditional in CEL input. {pull}40912[40912]
306306
- Add support for Crowdstrike streaming API to the streaming input. {issue}40264[40264] {pull}40838[40838]
307+
- Add support to CEL for reading host environment variables. {issue}40762[40762] {pull}40779[40779]
307308

308309
*Auditbeat*
309310

x-pack/filebeat/docs/inputs/input-cel.asciidoc

+29
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ As noted above the `cel` input provides functions, macros, and global variables
245245

246246
In addition to the extensions provided in the packages listed above, a global variable `useragent` is also provided which gives the user CEL program access to the {beatname_lc} user-agent string. By default, this value is assigned to all requests' user-agent headers unless the CEL program has already set the user-agent header value. Programs wishing to not provide a user-agent, should set this header to the empty string, `""`.
247247

248+
Host environment variables are made available via the global map `env`. Only environment variables that have been allow listed via the `allowed_environment` configuration list are visible to the CEL program.
249+
248250
The CEL environment enables the https://pkg.go.dev/github.com/google/cel-go/cel#OptionalTypes[optional types] library using the version defined {mito_docs}/lib#OptionalTypesVersion[here].
249251

250252
Additionally, it supports authentication via Basic Authentication, Digest Authentication or OAuth2.
@@ -357,6 +359,33 @@ filebeat.inputs:
357359
})
358360
----
359361

362+
[[environ-cel]]
363+
[float]
364+
=== `allowed_environment`
365+
366+
A list of host environment variable that will be made visible to the CEL execution environment. By default, no environment variables are visible.
367+
368+
["source","yaml",subs="attributes"]
369+
----
370+
filebeat.inputs:
371+
# Publish the list of files in $PATH every minute.
372+
- type: cel
373+
interval: 1m
374+
resource.url: ""
375+
allowed_environment:
376+
- PATH
377+
program: |
378+
{
379+
"events": {
380+
"message": env.?PATH.orValue("").split(":")
381+
.map(p, try(dir(p)))
382+
.filter(d, type(d) != type(""))
383+
.flatten()
384+
.collate("name")
385+
}
386+
}
387+
----
388+
360389
[[regexp-cel]]
361390
[float]
362391
==== `regexp`

x-pack/filebeat/input/cel/config.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ type config struct {
4747
// Redact is the debug log state redaction configuration.
4848
Redact *redact `config:"redact"`
4949

50+
// AllowedEnvironment is the set of env vars made
51+
// visible to an executing CEL evaluation.
52+
AllowedEnvironment []string `config:"allowed_environment"`
53+
5054
// Auth is the authentication config for connection to an HTTP
5155
// API endpoint.
5256
Auth authConfig `config:"auth"`
@@ -85,7 +89,7 @@ func (c config) Validate() error {
8589
if len(c.Regexps) != 0 {
8690
patterns = map[string]*regexp.Regexp{".": nil}
8791
}
88-
_, _, err = newProgram(context.Background(), c.Program, root, &http.Client{}, nil, nil, patterns, c.XSDs, logp.L().Named("input.cel"), nil)
92+
_, _, err = newProgram(context.Background(), c.Program, root, nil, &http.Client{}, nil, nil, patterns, c.XSDs, logp.L().Named("input.cel"), nil)
8993
if err != nil {
9094
return fmt.Errorf("failed to check program: %w", err)
9195
}

x-pack/filebeat/input/cel/input.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"path/filepath"
2222
"reflect"
2323
"regexp"
24+
"slices"
2425
"strconv"
2526
"strings"
2627
"time"
@@ -165,7 +166,7 @@ func (i input) run(env v2.Context, src *source, cursor map[string]interface{}, p
165166
Password: cfg.Auth.Basic.Password,
166167
}
167168
}
168-
prg, ast, err := newProgram(ctx, cfg.Program, root, client, limiter, auth, patterns, cfg.XSDs, log, trace)
169+
prg, ast, err := newProgram(ctx, cfg.Program, root, getEnv(cfg.AllowedEnvironment), client, limiter, auth, patterns, cfg.XSDs, log, trace)
169170
if err != nil {
170171
return err
171172
}
@@ -991,7 +992,19 @@ var (
991992
}
992993
)
993994

994-
func newProgram(ctx context.Context, src, root string, client *http.Client, limiter *rate.Limiter, auth *lib.BasicAuth, patterns map[string]*regexp.Regexp, xsd map[string]string, log *logp.Logger, trace *httplog.LoggingRoundTripper) (cel.Program, *cel.Ast, error) {
995+
func getEnv(allowed []string) map[string]string {
996+
env := make(map[string]string)
997+
for _, kv := range os.Environ() {
998+
k, v, ok := strings.Cut(kv, "=")
999+
if !ok || !slices.Contains(allowed, k) {
1000+
continue
1001+
}
1002+
env[k] = v
1003+
}
1004+
return env
1005+
}
1006+
1007+
func newProgram(ctx context.Context, src, root string, vars map[string]string, client *http.Client, limiter *rate.Limiter, auth *lib.BasicAuth, patterns map[string]*regexp.Regexp, xsd map[string]string, log *logp.Logger, trace *httplog.LoggingRoundTripper) (cel.Program, *cel.Ast, error) {
9951008
xml, err := lib.XML(nil, xsd)
9961009
if err != nil {
9971010
return nil, nil, fmt.Errorf("failed to build xml type hints: %w", err)
@@ -1013,6 +1026,7 @@ func newProgram(ctx context.Context, src, root string, client *http.Client, limi
10131026
lib.Limit(limitPolicies),
10141027
lib.Globals(map[string]interface{}{
10151028
"useragent": userAgent,
1029+
"env": vars,
10161030
}),
10171031
}
10181032
if len(patterns) != 0 {

x-pack/filebeat/input/cel/input_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,52 @@ var inputTests = []struct {
365365
{"message": "Hello, Void!"},
366366
},
367367
},
368+
{
369+
name: "env_var_static",
370+
config: map[string]interface{}{
371+
"interval": 1,
372+
"allowed_environment": []string{
373+
"CELTESTENVVAR",
374+
"NONCELTESTENVVAR",
375+
},
376+
"program": `{"events":[
377+
{"message":env.?CELTESTENVVAR.orValue("not present")},
378+
{"message":env.?NONCELTESTENVVAR.orValue("not present")},
379+
{"message":env.?DISALLOWEDCELTESTENVVAR.orValue("not present")},
380+
]}`,
381+
"state": nil,
382+
"resource": map[string]interface{}{
383+
"url": "",
384+
},
385+
},
386+
want: []map[string]interface{}{
387+
{"message": "TESTVALUE"},
388+
{"message": "not present"},
389+
{"message": "not present"},
390+
},
391+
},
392+
{
393+
name: "env_var_dynamic",
394+
config: map[string]interface{}{
395+
"interval": 1,
396+
"allowed_environment": []string{
397+
"CELTESTENVVAR",
398+
"NONCELTESTENVVAR",
399+
},
400+
"program": `{"events": ["CELTESTENVVAR","NONCELTESTENVVAR","DISALLOWEDCELTESTENVVAR"].map(k,
401+
{"message":env[?k].orValue("not present")}
402+
)}`,
403+
"state": nil,
404+
"resource": map[string]interface{}{
405+
"url": "",
406+
},
407+
},
408+
want: []map[string]interface{}{
409+
{"message": "TESTVALUE"},
410+
{"message": "not present"},
411+
{"message": "not present"},
412+
},
413+
},
368414

369415
// FS-based tests.
370416
{
@@ -1645,6 +1691,10 @@ func TestInput(t *testing.T) {
16451691
"ndjson_log_file_simple_file_scheme": "Path handling on Windows is incompatible with url.Parse/url.URL.String. See go.dev/issue/6027.",
16461692
}
16471693

1694+
// Set a var that is available to test env look-up.
1695+
os.Setenv("CELTESTENVVAR", "TESTVALUE")
1696+
os.Setenv("DISALLOWEDCELTESTENVVAR", "DISALLOWEDTESTVALUE")
1697+
16481698
logp.TestingSetup()
16491699
for _, test := range inputTests {
16501700
t.Run(test.name, func(t *testing.T) {

0 commit comments

Comments
 (0)