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

[Ingest Manager] Refuse invalid stream values in configuration #19587

Merged
1 change: 1 addition & 0 deletions x-pack/elastic-agent/CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,4 @@
- Rename input.type logs to logfile {pull}19360[19360]
- Agent now installs/uninstalls Elastic Endpoint {pull}19248[19248]
- Agent now downloads Elastic Endpoint {pull}19503[19503]
- Refuse invalid stream values in configuration {pull}19587[19587]
4 changes: 4 additions & 0 deletions x-pack/elastic-agent/_meta/config/common.p2.yml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ outputs:

inputs:
- type: system/metrics

# The only two requirement are that it has only characters allowed in an Elasticsearch index name
# and does NOT contain a `-`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to be explicit on what is allowed even if it is conservative in nature. That way its less likely for them to make a mistake and its clear directly from the comment.

How about:

Must be all lowercase, no spaces, only characters a-z or numbers 0-9 (no special characters), and no more than 255 characters.

I think the character length might be an issue being its concat-ed to make the index name.

dataset.namespace: default
use_output: default
streams:
- metricset: cpu
# Same requirements as for the namespace apply.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to be explicit here as well, what ever is picked above, should just repeat verbatim here.

dataset.name: system.cpu
- metricset: memory
dataset.name: system.memory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ outputs:

inputs:
- type: system/metrics

# The only two requirement are that it has only characters allowed in an Elasticsearch index name
# and does NOT contain a `-`.
dataset.namespace: default
use_output: default
streams:
- metricset: cpu
# Same requirements as for the namespace apply.
dataset.name: system.cpu
- metricset: memory
dataset.name: system.memory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ outputs:

inputs:
- type: system/metrics

# The only two requirement are that it has only characters allowed in an Elasticsearch index name
# and does NOT contain a `-`.
dataset.namespace: default
use_output: default
streams:
- metricset: cpu
# Same requirements as for the namespace apply.
dataset.name: system.cpu
- metricset: memory
dataset.name: system.memory
Expand Down
4 changes: 4 additions & 0 deletions x-pack/elastic-agent/_meta/elastic-agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ outputs:

inputs:
- type: system/metrics

# The only two requirement are that it has only characters allowed in an Elasticsearch index name
# and does NOT contain a `-`.
dataset.namespace: default
use_output: default
streams:
- metricset: cpu
# Same requirements as for the namespace apply.
dataset.name: system.cpu
- metricset: memory
dataset.name: system.memory
Expand Down
4 changes: 4 additions & 0 deletions x-pack/elastic-agent/elastic-agent.docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ outputs:

inputs:
- type: system/metrics

# The only two requirement are that it has only characters allowed in an Elasticsearch index name
# and does NOT contain a `-`.
dataset.namespace: default
use_output: default
streams:
- metricset: cpu
# Same requirements as for the namespace apply.
dataset.name: system.cpu
- metricset: memory
dataset.name: system.memory
Expand Down
4 changes: 4 additions & 0 deletions x-pack/elastic-agent/elastic-agent.reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ outputs:

inputs:
- type: system/metrics

# The only two requirement are that it has only characters allowed in an Elasticsearch index name
# and does NOT contain a `-`.
dataset.namespace: default
use_output: default
streams:
- metricset: cpu
# Same requirements as for the namespace apply.
dataset.name: system.cpu
- metricset: memory
dataset.name: system.memory
Expand Down
4 changes: 4 additions & 0 deletions x-pack/elastic-agent/elastic-agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ outputs:

inputs:
- type: system/metrics

# The only two requirement are that it has only characters allowed in an Elasticsearch index name
# and does NOT contain a `-`.
dataset.namespace: default
use_output: default
streams:
- metricset: cpu
# Same requirements as for the namespace apply.
dataset.name: system.cpu
- metricset: memory
dataset.name: system.memory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ func TestEvaluation(t *testing.T) {
}

testCases := []testCase{
testCase{"simple version", "validate_version(%{[agent.version]}, '" + release.Version() + "')", true},
testCase{"~ version release", "validate_version(%{[agent.version]}, '~" + release.Version() + "')", true},
testCase{"^ version release", "validate_version(%{[agent.version]}, '^" + release.Version() + "')", true},
testCase{"range to release", "validate_version(%{[agent.version]}, '1.0.0 - " + release.Version() + "')", true},
testCase{"range lower", "validate_version(%{[agent.version]}, '1.0.0 - 5.0.0')", false},
testCase{"range include", "validate_version(%{[agent.version]}, '1.0.0 - 100.0.0')", true},
testCase{"family should equal", "%{[os.family]} == '" + runtime.GOOS + "'", true},
testCase{"family should not equal", "%{[os.family]} != '" + runtime.GOOS + "'", false},
{"simple version", "validate_version(%{[agent.version]}, '" + release.Version() + "')", true},
{"~ version release", "validate_version(%{[agent.version]}, '~" + release.Version() + "')", true},
{"^ version release", "validate_version(%{[agent.version]}, '^" + release.Version() + "')", true},
{"range to release", "validate_version(%{[agent.version]}, '1.0.0 - " + release.Version() + "')", true},
{"range lower", "validate_version(%{[agent.version]}, '1.0.0 - 5.0.0')", false},
{"range include", "validate_version(%{[agent.version]}, '1.0.0 - 100.0.0')", true},
{"family should equal", "%{[os.family]} == '" + runtime.GOOS + "'", true},
{"family should not equal", "%{[os.family]} != '" + runtime.GOOS + "'", false},
}

for _, tc := range testCases {
Expand Down
184 changes: 184 additions & 0 deletions x-pack/elastic-agent/pkg/agent/application/filters/stream_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// 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 filters

import (
"strings"

"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors"
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/transpiler"
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger"
)

// ErrInvalidNamespace is error returned when namespace value provided is invalid.
var ErrInvalidNamespace = errors.New("provided namespace is invalid", errors.TypeConfig)

// ErrInvalidDataset is error returned when dataset name value provided is invalid.
var ErrInvalidDataset = errors.New("provided dataset name is invalid", errors.TypeConfig)

// StreamChecker checks for invalid values in stream namespace and dataset.
func StreamChecker(log *logger.Logger, ast *transpiler.AST) error {
inputsNode, found := transpiler.Lookup(ast, "inputs")
if !found {
return nil
}

inputsNodeList, ok := inputsNode.Value().(*transpiler.List)
if !ok {
return nil
}

inputsNodeListCollection, ok := inputsNodeList.Value().([]transpiler.Node)
if !ok {
return errors.New("inputs is not a list", errors.TypeConfig)
}

for _, inputNode := range inputsNodeListCollection {
// fail only if dataset.namespace or dataset[namespace] is found and invalid
// not provided values are ok and will be fixed by rules
if nsNode, found := inputNode.Find("dataset.namespace"); found {
nsKey, ok := nsNode.(*transpiler.Key)
if ok {
if newNamespace := nsKey.Value().(transpiler.Node).String(); !isNamespaceValid(newNamespace) {
return ErrInvalidNamespace
}
}
} else {
dsNode, found := inputNode.Find("dataset")
if found {
// got a dataset
datasetMap, ok := dsNode.Value().(*transpiler.Dict)
if ok {
nsNode, found := datasetMap.Find("namespace")
if found {
nsKey, ok := nsNode.(*transpiler.Key)
if ok {
if newNamespace := nsKey.Value().(transpiler.Node).String(); !isNamespaceValid(newNamespace) {
return ErrInvalidNamespace
}
}
}
}
}
}

streamsNode, ok := inputNode.Find("streams")
if !ok {
continue
}

streamsList, ok := streamsNode.Value().(*transpiler.List)
if !ok {
continue
}

streamNodes, ok := streamsList.Value().([]transpiler.Node)
if !ok {
return errors.New("streams is not a list", errors.TypeConfig)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested with Endpoint Security enabled in Fleet? I think this will cause an issue, because it does come with streams: [] and something similar failed in another rule because of it.

https://github.com/elastic/beats/pull/19248/files#diff-68f0d6baed417771ae64bf9e5a7587a7R267

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point here

}

for _, streamNode := range streamNodes {
streamMap, ok := streamNode.(*transpiler.Dict)
if !ok {
continue
}

// fix this only if in compact form
if dsNameNode, found := streamMap.Find("dataset.name"); found {
dsKey, ok := dsNameNode.(*transpiler.Key)
if ok {
if newDataset := dsKey.Value().(transpiler.Node).String(); !isDatasetValid(newDataset) {
return ErrInvalidDataset
}
}
} else {
datasetNode, found := streamMap.Find("dataset")
if found {
datasetMap, ok := datasetNode.Value().(*transpiler.Dict)
if !ok {
continue
}

dsNameNode, found := datasetMap.Find("name")
if found {
dsKey, ok := dsNameNode.(*transpiler.Key)
if ok {
if newDataset := dsKey.Value().(transpiler.Node).String(); !isDatasetValid(newDataset) {
return ErrInvalidDataset
}
}
}
}
}
}
}

return nil
}

// The only two requirement are that it has only characters allowed in an Elasticsearch index name
// and does NOT contain a `-`.
// Index names must meet the following criteria:
// Lowercase only
// Cannot include \, /, *, ?, ", <, >, |, ` ` (space character), ,, #
// Cannot start with -, _, +
// Cannot be . or ..

func isNamespaceValid(namespace string) bool {
// Cannot be . or ..
if namespace == "." || namespace == ".." {
return false
}

if len(namespace) <= 0 || len(namespace) > 255 {
return false
}

// Lowercase only
if strings.ToLower(namespace) != namespace {
return false
}

// Cannot include \, /, *, ?, ", <, >, |, ` ` (space character), ,, #
if strings.ContainsAny(namespace, "\\/*?\"<>| ,#-") {
return false
}

// Cannot start with -, _, +
if strings.HasPrefix(namespace, "-") || strings.HasPrefix(namespace, "_") || strings.HasPrefix(namespace, "+") {
return false
}

return true
}

// The same requirements as for the namespace apply.
func isDatasetValid(dataset string) bool {
// Cannot be . or ..
if dataset == "." || dataset == ".." {
return false
}

if len(dataset) <= 0 || len(dataset) > 255 {
return false
}

// Lowercase only
if strings.ToLower(dataset) != dataset {
return false
}

// Cannot include \, /, *, ?, ", <, >, |, ` ` (space character), ,, #
if strings.ContainsAny(dataset, "\\/*?\"<>| ,#-") {
return false
}

// Cannot start with -, _, +
if strings.HasPrefix(dataset, "-") || strings.HasPrefix(dataset, "_") || strings.HasPrefix(dataset, "+") {
return false
}

return true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why duplicate the same function and just give it the same name? Could remove the second one and rename it to isValid. Or could do:

func isDatasetValid(dataset string) bool {
    return isNamespaceValid(dataset)
}

That way in the future if the validation of dataset does differ from namespace, then only that function body needs to be changed.

}
Loading