Skip to content
Open
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
25 changes: 22 additions & 3 deletions rules/policy/lib/helpers.rego
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,33 @@ all_used_processors contains p if {
some p in pipeline_processors(t)
}

# is_env_var reports whether a value contains a ${env:VAR} / ${ENV:VAR}
# reference anywhere — so composite values like "Bearer ${env:TOKEN}" are
# recognised too. Right for the hardcoded-secret rules; endpoint rules use
# env_host instead, which anchors the reference to the start of the value.
is_env_var(val) if {
startswith(val, "${env:")
endswith(val, "}")
is_string(val)
contains(val, "${env:")
}

is_env_var(val) if {
is_string(val)
contains(val, "${ENV:")
}

# env_host reports whether a value's *host* is an environment reference — the
# value starts with ${env:VAR} / ${ENV:VAR}. Endpoint rules (TLS, bind address,
# URL scheme) use this instead of is_env_var: "${env:MY_POD_IP}:4317" is
# genuinely env-sourced, but "collector.example.com:${env:PORT}" hardcodes the
# host and must still be inspected.
env_host(val) if {
is_string(val)
startswith(val, "${env:")
}

env_host(val) if {
is_string(val)
startswith(val, "${ENV:")
endswith(val, "}")
}

looks_like_secret(key) if {
Expand Down
6 changes: 3 additions & 3 deletions rules/policy/main/exporter.rego
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ warn contains msg if {
endpoint != ""
not startswith(endpoint, "http://")
not startswith(endpoint, "https://")
not lib.is_env_var(endpoint)
not lib.env_host(endpoint)
msg := sprintf("OTEL-046: OTLP HTTP exporter '%s' endpoint '%s' is missing URL scheme.", [name, endpoint])
}

Expand All @@ -57,7 +57,7 @@ warn contains msg if {

# OTEL-048: sending_queue explicitly disabled
warn contains msg if {
pull_based := {"debug", "logging", "prometheus", "prometheusremotewrite", "file"}
pull_based := {"debug", "logging", "prometheus", "prometheusremotewrite", "file", "nop"}
some name, exporter in input.exporters
not split(name, "/")[0] in pull_based
exporter.sending_queue.enabled == false
Expand Down Expand Up @@ -99,7 +99,7 @@ warn contains msg if {

# OTEL-052: compression disabled for network exporter
warn contains msg if {
pull_based := {"debug", "logging", "prometheus", "prometheusremotewrite", "file"}
pull_based := {"debug", "logging", "prometheus", "prometheusremotewrite", "file", "nop"}
some name, exporter in input.exporters
not split(name, "/")[0] in pull_based
exporter.compression == "none"
Expand Down
18 changes: 18 additions & 0 deletions rules/policy/main/exporter_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,24 @@ test_046_warn_http_endpoint_no_scheme if {
contains(msg, "OTEL-046")
}

# An env-sourced endpoint may expand to a full URL, scheme included — the rule
# cannot know, so it stays quiet.
test_046_pass_env_host_endpoint if {
val := {"endpoint": "${env:OTLP_ENDPOINT}"}
cfg := json.patch(valid_config, [{"op": "add", "path": "/exporters/otlphttp", "value": val}])
msgs := main.warn with input as cfg
not_contains_rule(msgs, "OTEL-046")
}

# But a hardcoded host with only the port env-sourced definitely has no scheme.
test_046_warn_hardcoded_host_env_port if {
val := {"endpoint": "backend.example.com:${env:PORT}"}
cfg := json.patch(valid_config, [{"op": "add", "path": "/exporters/otlphttp", "value": val}])
msgs := main.warn with input as cfg
some msg in msgs
contains(msg, "OTEL-046")
}

test_047_warn_http_using_grpc_port if {
val := {"endpoint": "https://backend.example.com:4317"}
cfg := json.patch(valid_config, [{"op": "add", "path": "/exporters/otlphttp", "value": val}])
Expand Down
16 changes: 14 additions & 2 deletions rules/policy/main/main.rego
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ warn contains msg if {
}

warn contains msg if {
pull_based := {"debug", "logging", "prometheus", "prometheusremotewrite"}
pull_based := {"debug", "logging", "prometheus", "prometheusremotewrite", "nop"}
some name, exporter in input.exporters
base_type := split(name, "/")[0]
not base_type in pull_based
Expand All @@ -139,7 +139,7 @@ warn contains msg if {
not startswith(endpoint, "unix:")
not contains(endpoint, "localhost")
not contains(endpoint, "127.0.0.1")
not lib.is_env_var(endpoint)
not lib.env_host(endpoint)
msg := sprintf("OTEL-018: exporter '%s' has no TLS configured for non-local endpoint.", [name])
}

Expand Down Expand Up @@ -183,3 +183,15 @@ _exporter_has_alt_retry(base_type, exporter) if {
mode := exporter.s3uploader.retry_mode
mode != "nop"
}

# The loadbalancing exporter wraps an inner exporter, so its retry_on_failure
# and sending_queue live under protocol.otlp rather than at the top level.
_exporter_has_alt_retry(base_type, exporter) if {
base_type == "loadbalancing"
exporter.protocol.otlp.retry_on_failure
}

_exporter_has_alt_retry(base_type, exporter) if {
base_type == "loadbalancing"
exporter.protocol.otlp.sending_queue
}
44 changes: 44 additions & 0 deletions rules/policy/main/main_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,23 @@ test_018_pass_on_unresolved_env_var if {
not_contains_rule(msgs, "OTEL-018")
}

test_018_pass_on_env_var_with_port if {
val := {"endpoint": "${env:OTEL_HOST}:4317"}
cfg := json.patch(valid_config, [{"op": "replace", "path": "/exporters/otlp~1backend", "value": val}])
msgs := main.warn with input as cfg
not_contains_rule(msgs, "OTEL-018")
}

# A hardcoded remote host must still warn even when the *port* is an env
# reference — only an env-sourced host (env_host) clears the rule.
test_018_warn_hardcoded_host_env_port if {
val := {"endpoint": "collector.example.com:${env:PORT}"}
cfg := json.patch(valid_config, [{"op": "replace", "path": "/exporters/otlp~1backend", "value": val}])
msgs := main.warn with input as cfg
some msg in msgs
contains(msg, "OTEL-018")
}

test_013_warn_batch_not_last if {
val := ["batch", "memory_limiter"]
cfg := json.patch(valid_config, [{"op": "replace", "path": "/service/pipelines/traces/processors", "value": val}])
Expand Down Expand Up @@ -233,6 +250,33 @@ test_017_warn_awss3_without_retry if {
contains(msg, "awss3")
}

# The nop exporter discards data by design — retry/queue is meaningless.
test_017_pass_nop_exporter if {
cfg := json.patch(valid_config, [
{"op": "add", "path": "/exporters/nop", "value": {}},
{"op": "add", "path": "/service/pipelines/traces/exporters/-", "value": "nop"},
])
msgs := main.warn with input as cfg
not_contains_rule(msgs, "OTEL-017")
}

# loadbalancing nests retry/queue under protocol.otlp, not at the top level.
test_017_pass_loadbalancing_nested_resilience if {
lb := {
"protocol": {"otlp": {
"retry_on_failure": {"enabled": true},
"sending_queue": {"enabled": true},
}},
"resolver": {"static": {"hostnames": ["backend:4317"]}},
}
cfg := json.patch(valid_config, [
{"op": "add", "path": "/exporters/loadbalancing", "value": lb},
{"op": "add", "path": "/service/pipelines/traces/exporters/-", "value": "loadbalancing"},
])
msgs := main.warn with input as cfg
not_contains_rule(msgs, "OTEL-017")
}

test_020_warn_unused_receiver if {
cfg := json.patch(valid_config, [{"op": "add", "path": "/receivers/jaeger", "value": {"protocols": {"grpc": {}}}}])
msgs := main.warn with input as cfg
Expand Down
2 changes: 1 addition & 1 deletion rules/policy/main/security.rego
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ warn contains msg if {
endpoint != ""
not contains(endpoint, "localhost")
not contains(endpoint, "127.0.0.1")
not lib.is_env_var(endpoint)
not lib.env_host(endpoint)
object.get(proto_cfg, "transport", "") != "unix"
not proto_cfg.tls
msg := sprintf("OTEL-033: receiver '%s/%s' on non-localhost endpoint '%s' without TLS.", [name, proto, endpoint])
Expand Down
19 changes: 19 additions & 0 deletions rules/policy/main/security_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ test_033_pass_on_unresolved_env_var if {
not_contains_rule(msgs, "OTEL-033")
}

# The OTel Helm charts bind to "${env:MY_POD_IP}:4317" — an env reference with a
# port appended. is_env_var must recognise it even though it does not end in "}".
test_033_pass_on_env_var_with_port if {
val := {"protocols": {"grpc": {"endpoint": "${env:MY_POD_IP}:4317"}}}
cfg := json.patch(valid_config, [{"op": "replace", "path": "/receivers/otlp", "value": val}])
msgs := main.warn with input as cfg
not_contains_rule(msgs, "OTEL-033")
}

# A hardcoded remote host must still warn even when the *port* is an env
# reference — only an env-sourced host (env_host) clears the rule.
test_033_warn_hardcoded_host_env_port if {
val := {"protocols": {"grpc": {"endpoint": "collector.example.com:${env:PORT}"}}}
cfg := json.patch(valid_config, [{"op": "replace", "path": "/receivers/otlp", "value": val}])
msgs := main.warn with input as cfg
some msg in msgs
contains(msg, "OTEL-033")
}

test_034_deny_cors_wildcard if {
val := {"protocols": {"http": {"cors": {"allowed_origins": ["*"]}, "endpoint": "localhost:4318"}}}
cfg := json.patch(valid_config, [{"op": "replace", "path": "/receivers/otlp", "value": val}])
Expand Down