Skip to content
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
193 changes: 151 additions & 42 deletions build/common/installer/scripts/tomlparser-geneva-config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,124 @@
@disable_linux = false

GENEVA_SUPPORTED_ENVIRONMENTS = ["Test", "Stage", "DiagnosticsProd", "FirstpartyProd", "BillingProd", "ExternalProd", "CaMooncake", "CaFairfax", "CaBlackforest", "Bleu"]

# Everything under [integrations.geneva_logs] comes from the container-azm-ms-agentconfig config
# map, which is tenant writable, so it is untrusted input. On linux the generated
# geneva_config_env_var file is appended to ~/.bashrc and sourced by main.sh while the agent runs
# as root, so an unquoted value would be executed as shell. Every value is therefore validated
# against an anchored allowlist below and single quoted when written.
# The anchors must be \A and \z (not ^ and $) so that a value such as "prod\n<injected command>"
# cannot pass validation by matching only its first line.

# Geneva environment, account and namespace identifiers must start with a letter and then may
# contain letters, digits, underscores, hyphens or periods.
GENEVA_NAME_REGEX = /\A[A-Za-z][A-Za-z0-9_\-\.]{0,63}\z/
# GCS region must start with a letter and contain only letters or digits.
GENEVA_REGION_REGEX = /\A[A-Za-z][A-Za-z0-9]{0,63}\z/
# Agent xml config version, for example "1.0" or "Ver2v0".
GENEVA_CONFIG_VERSION_REGEX = /\A[A-Za-z0-9][A-Za-z0-9._-]{0,63}\z/
# MUST be <identifier>#<value>, for example client_id#<guid> or mi_res_id#<identity resource id>.
# The windows agent relies on the same shape when it splits the value on "#".
GENEVA_AUTH_ID_REGEX = /\A(?:client_id|object_id|mi_res_id)#[A-Za-z0-9._\-\/()]{1,512}\z/i
# Kubernetes namespaces are RFC 1123 labels. Keep these constants Geneva-specific because the
# unit test driver loads all common parser tests into one Ruby process.
GENEVA_KUBERNETES_NAMESPACE_REGEX = /\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/
GENEVA_KUBERNETES_NAMESPACE_MAX_LENGTH = 63
# Infra namespaces may carry a trailing wildcard, which main.sh strips before using the value as
# part of a generated fluent-bit config file name.
GENEVA_INFRA_NAMESPACE_WILDCARD_SUFFIX = "-*"
GENEVA_DEFAULT_CONFIG_VERSION = "1.0"

# Renders a value as a single quoted shell word. Every character inside single quotes is literal
# to the shell, and an embedded single quote is emitted as '\'' so that the value cannot terminate
# its own quoting. Values are validated before they reach this point; this is the last line of
# defense that keeps config map content out of the shell parser.
# The block form of gsub is required: with a replacement string ruby would treat the \' as the
# post-match backreference instead of a literal backslash and quote.
def toShellSingleQuoted(value)
return "'" + value.to_s.gsub("'") { "'\\''" } + "'"
end

def isValidGenevaName(value)
return value.kind_of?(String) && !(value =~ GENEVA_NAME_REGEX).nil?
end

def isValidGenevaRegion(region)
return region.kind_of?(String) && !(region =~ GENEVA_REGION_REGEX).nil?
end

def isValidGenevaAuthId(authid)
return authid.kind_of?(String) && !(authid =~ GENEVA_AUTH_ID_REGEX).nil?
end

def isValidGenevaConfigVersion(configVersion)
return configVersion.kind_of?(String) && !(configVersion =~ GENEVA_CONFIG_VERSION_REGEX).nil?
end

# The environment identifiers of the airgap clouds are not known to the agent, which is why the
# allowlist below is a warning rather than a rejection. The character allowlist above is what
# makes the value safe to write into the environment file.
def isValidGenevaEnvironment(environment)
if !isValidGenevaName(environment)
return false
end
if !GENEVA_SUPPORTED_ENVIRONMENTS.map(&:downcase).include?(environment.downcase)
puts "config::geneva_logs::warn:geneva environment is not one of the known geneva environments"
end
return true
end

def isValidKubernetesNamespace(namespace)
return namespace.kind_of?(String) &&
namespace.length <= GENEVA_KUBERNETES_NAMESPACE_MAX_LENGTH &&
!(namespace =~ GENEVA_KUBERNETES_NAMESPACE_REGEX).nil?
end

def isValidGenevaInfraNamespace(namespace)
if !namespace.kind_of?(String)
return false
end
if namespace.end_with?(GENEVA_INFRA_NAMESPACE_WILDCARD_SUFFIX)
return isValidKubernetesNamespace(namespace[0...-GENEVA_INFRA_NAMESPACE_WILDCARD_SUFFIX.length])
end
return isValidKubernetesNamespace(namespace)
end

# Joins the namespaces that are safe to use into the comma separated list the agent expects.
# main.sh splits this list again and interpolates each entry into a file name and a sed
# expression, so an entry that is not a kubernetes namespace is dropped rather than passed on.
# The rejected entry is never echoed back since it is untrusted input.
def joinValidNamespaces(namespaces, settingName, isInfra)
validNamespaces = []
if namespaces.nil? || !namespaces.kind_of?(Array)
return ""
end
namespaces.each do |namespace|
namespace = namespace.to_s.strip
next if namespace.empty?
isValid = isInfra ? isValidGenevaInfraNamespace(namespace) : isValidKubernetesNamespace(namespace)
if !isValid
ConfigParseErrorLogger.logError("Skipping an entry in #{settingName} because it is not a valid kubernetes namespace")
next
end
validNamespaces.push(namespace)
end
return validNamespaces.join(",")
end

# Returns the config version to use, falling back to the default when the configured value is not
# usable. A typo in an optional setting should not take the integration down.
def resolveConfigVersion(configVersion, settingName)
if configVersion.nil? || configVersion.empty?
puts "Since #{settingName} not specified so using default config version : #{GENEVA_DEFAULT_CONFIG_VERSION}"
return GENEVA_DEFAULT_CONFIG_VERSION
end
if isValidGenevaConfigVersion(configVersion)
return configVersion
end
ConfigParseErrorLogger.logError("Invalid value specified for #{settingName}, using default config version : #{GENEVA_DEFAULT_CONFIG_VERSION}")
return GENEVA_DEFAULT_CONFIG_VERSION
end
@geneva_account_environment = "" # Supported values Test, Stage, DiagnosticsProd, FirstpartyProd, BillingProd, ExternalProd, CaMooncake, CaFairfax, CaBlackforest, Bleu
@geneva_account_name = ""
@geneva_account_namespace = ""
Expand Down Expand Up @@ -109,18 +227,10 @@ def populateSettingValuesFromConfigMap(parsedConfig)
if @multi_tenancy
# this is only applicable incase of multi-tenacy
infra_namespaces = parsedConfig[:integrations][:geneva_logs][:infra_namespaces]
puts "config::geneva_logs:infra_namespaces provided in the configmap: #{infra_namespaces}"
if !infra_namespaces.nil? && !infra_namespaces.empty? &&
infra_namespaces.kind_of?(Array) && infra_namespaces.length > 0 &&
infra_namespaces[0].kind_of?(String) # Checking only for the first element to be string because toml enforces the arrays to contain elements of same type
infra_namespaces.each do |namespace|
if @infra_namespaces.empty?
# To not append , for the first element
@infra_namespaces.concat(namespace)
else
@infra_namespaces.concat("," + namespace)
end
end
@infra_namespaces = joinValidNamespaces(infra_namespaces, "infra_namespaces", true)
end
enable_fbit_threading = parsedConfig[:integrations][:geneva_logs][:enable_threading].to_s
puts "config::geneva_logs:enable_threading provided in the configmap: #{enable_fbit_threading}"
Expand Down Expand Up @@ -168,16 +278,16 @@ def populateSettingValuesFromConfigMap(parsedConfig)
@geneva_gcs_authid = geneva_gcs_authid

if !geneva_logs_config_version.nil? && !geneva_logs_config_version.empty?
@geneva_logs_config_version = geneva_logs_config_version
@geneva_logs_config_version = resolveConfigVersion(geneva_logs_config_version, "configversion")
else
@geneva_logs_config_version = "1.0"
@geneva_logs_config_version = GENEVA_DEFAULT_CONFIG_VERSION
puts "Since config version not specified so using default config version : #{@geneva_logs_config_version}"
end

if !geneva_logs_config_version_windows.nil? && !geneva_logs_config_version_windows.empty?
@geneva_logs_config_version_windows = geneva_logs_config_version_windows
@geneva_logs_config_version_windows = resolveConfigVersion(geneva_logs_config_version_windows, "windowsconfigversion")
else
@geneva_logs_config_version_windows = "1.0"
@geneva_logs_config_version_windows = GENEVA_DEFAULT_CONFIG_VERSION
puts "Since config version for windows not specified so using default config version : #{@geneva_logs_config_version_windows}"
end
else
Expand All @@ -187,18 +297,10 @@ def populateSettingValuesFromConfigMap(parsedConfig)

if @multi_tenancy
tenant_namespaces = parsedConfig[:integrations][:geneva_logs][:tenant_namespaces]
puts "config::geneva_logs:tenant_namespaces provided in the configmap: #{tenant_namespaces}"
if !tenant_namespaces.nil? && !tenant_namespaces.empty? &&
tenant_namespaces.kind_of?(Array) && tenant_namespaces.length > 0 &&
tenant_namespaces[0].kind_of?(String) # Checking only for the first element to be string because toml enforces the arrays to contain elements of same type
tenant_namespaces.each do |namespace|
if @tenant_namespaces.empty?
# To not append , for the first element
@tenant_namespaces.concat(namespace)
else
@tenant_namespaces.concat("," + namespace)
end
end
@tenant_namespaces = joinValidNamespaces(tenant_namespaces, "tenant_namespaces", false)
end
end

Expand Down Expand Up @@ -233,32 +335,37 @@ def populateSettingValuesFromConfigMap(parsedConfig)
def isValidGenevaConfig(environment, namespace, namespacewindows, account, authid, region)
isValid = false
begin
if environment.nil? || environment.empty?
# The rejected values are never echoed back into the logs since they are untrusted input.
if environment.nil? || environment.empty? || !isValidGenevaEnvironment(environment)
puts "config::geneva_logs::error:geneva environment MUST be valid"
return isValid
end

if namespace.nil? || namespace.empty?
if namespace.nil? || namespace.empty? || !isValidGenevaName(namespace)
puts "config::geneva_logs::error:geneva account namespace MUST be valid"
return isValid
end

if region.nil? || region.empty?
if region.nil? || region.empty? || !isValidGenevaRegion(region)
puts "config::geneva_logs::error:geneva GCS region MUST be valid"
return isValid
end

if authid.nil? || authid.empty?
if authid.nil? || authid.empty? || !isValidGenevaAuthId(authid)
puts "config::geneva_logs::error:geneva GCS AuthID MUST be valid"
return isValid
end
## namespacewindows is optional hence we dont need this validation
# if namespacewindows.nil? || namespacewindows.empty?
# puts "config::geneva_logs::error:geneva account namespace for windows MUST be valid"
# return isValid
# end
# TODO - add the validation once we figured out the environment for airgap clouds
# GENEVA_SUPPORTED_ENVIRONMENTS.map(&:downcase).include?(environment.downcase)

## account and namespacewindows are optional hence only the format is validated when provided
if !account.nil? && !account.empty? && !isValidGenevaName(account)
puts "config::geneva_logs::error:geneva account MUST be valid"
return isValid
end

if !namespacewindows.nil? && !namespacewindows.empty? && !isValidGenevaName(namespacewindows)
puts "config::geneva_logs::error:geneva account namespace for windows MUST be valid"
return isValid
end
isValid = true
rescue => errorStr
puts "config::geneva_logs::error:Exception while validating Geneva config - #{errorStr}"
Expand Down Expand Up @@ -316,16 +423,18 @@ def is_configure_geneva_env_vars()
end

if is_configure_geneva_env_vars()
file.write("export MONITORING_GCS_ENVIRONMENT=#{@geneva_account_environment}\n")
file.write("export MONITORING_GCS_NAMESPACE=#{@geneva_account_namespace}\n")
file.write("export MONITORING_GCS_ACCOUNT=#{@geneva_account_name}\n")
file.write("export MONITORING_GCS_REGION=#{@geneva_gcs_region}\n")
file.write("export MONITORING_CONFIG_VERSION=#{@geneva_logs_config_version}\n")
file.write("export MONITORING_GCS_AUTH_ID=#{@geneva_gcs_authid}\n")
file.write("export MONITORING_GCS_AUTH_ID_TYPE=AuthMSIToken")
# Config map derived values are single quoted so that the shell that sources this file
# treats them as literal data and never as commands.
file.write("export MONITORING_GCS_ENVIRONMENT=#{toShellSingleQuoted(@geneva_account_environment)}\n")
file.write("export MONITORING_GCS_NAMESPACE=#{toShellSingleQuoted(@geneva_account_namespace)}\n")
file.write("export MONITORING_GCS_ACCOUNT=#{toShellSingleQuoted(@geneva_account_name)}\n")
file.write("export MONITORING_GCS_REGION=#{toShellSingleQuoted(@geneva_gcs_region)}\n")
file.write("export MONITORING_CONFIG_VERSION=#{toShellSingleQuoted(@geneva_logs_config_version)}\n")
file.write("export MONITORING_GCS_AUTH_ID=#{toShellSingleQuoted(@geneva_gcs_authid)}\n")
file.write("export MONITORING_GCS_AUTH_ID_TYPE=AuthMSIToken\n")
end
file.write("export GENEVA_LOGS_INFRA_NAMESPACES=#{@infra_namespaces}\n")
file.write("export GENEVA_LOGS_TENANT_NAMESPACES=#{@tenant_namespaces}\n")
file.write("export GENEVA_LOGS_INFRA_NAMESPACES=#{toShellSingleQuoted(@infra_namespaces)}\n")
file.write("export GENEVA_LOGS_TENANT_NAMESPACES=#{toShellSingleQuoted(@tenant_namespaces)}\n")

# This required environment variable in geneva mode
file.write("export MDSD_MSGPACK_SORT_COLUMNS=1\n")
Expand Down
Loading
Loading