Skip to content
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -37,22 +37,37 @@ public static string FormatCommandArguments(string bootstrapFile)
return commandArguments.ToString();
}

public static string PrepareConfigurationFile(string workingDirectory, IVariables variables)
public static string PrepareConfigurationFile(string workingDirectory, IVariables variables, Script script)
{
var configurationFile = Path.Combine(workingDirectory, "Configure." + Guid.NewGuid().ToString().Substring(10) + ".sh");

var builder = new StringBuilder(BootstrapScriptTemplate);
var encryptedVariables = EncryptVariables(variables);

var variableString = GetEncryptedVariablesKvp(variables);
var featureEnabled = FeatureToggle.BashParametersArrayFeatureToggle.IsEnabled(variables);
builder.Replace("#### BashParametersArrayFeatureToggle ####", featureEnabled ? "true" : "false");

var encryptedVariables = EncryptVariables(variables);
builder.Replace("#### VariableDeclarations ####", string.Join(LinuxNewLine, GetVariableSwitchConditions(encryptedVariables)));

builder.Replace("#### BashParametersArrayFeatureToggle ####", FeatureToggle.BashParametersArrayFeatureToggle.IsEnabled(variables) ? "true" : "false");
if (FeatureToggle.BashParametersArrayFeatureToggle.IsEnabled(variables))
if (featureEnabled)
{
builder.Replace("#### VARIABLESTRING.IV ####", variableString.iv);
builder.Replace("#### VARIABLESTRING.ENCRYPTED ####", variableString.encrypted);
var scriptUsesOctopusParameters = ScriptUsesOctopusParameters(script, variables);
// If the script doesn't use octopus_parameters at all, we don't want to bloat the bootstrap script with unused values.
if (scriptUsesOctopusParameters)
{
var variableString = GetEncryptedVariablesKvp(variables);
builder.Replace("#### VARIABLESTRING.IV ####", variableString.iv);
builder.Replace("#### VARIABLESTRING.ENCRYPTED ####", variableString.encrypted);
builder.Replace("#### SCRIPT_USES_OCTOPUS_PARAMETERS ####", "true");
}
else
{
builder.Replace("#### SCRIPT_USES_OCTOPUS_PARAMETERS ####", "false");
}
}
else
{
// Feature toggle is off, so the placeholder won't be used but still needs to be replaced
builder.Replace("#### SCRIPT_USES_OCTOPUS_PARAMETERS ####", "false");
}

using (var file = new FileStream(configurationFile, FileMode.CreateNew, FileAccess.Write))
Expand All @@ -65,22 +80,39 @@ public static string PrepareConfigurationFile(string workingDirectory, IVariable
File.SetAttributes(configurationFile, FileAttributes.Hidden);
return configurationFile;
}
static string EncodeAsHex(string value)

static bool ScriptUsesOctopusParameters(Script script, IVariables variables)
{
var bytes = Encoding.UTF8.GetBytes(value);
return BitConverter.ToString(bytes).Replace("-", "");
// Check if script actually uses octopus_parameters array access
// Pattern matches: octopus_parameters[key], octopus_parameters[@], etc.
const string pattern = @"octopus_parameters\[";
var regex = new System.Text.RegularExpressions.Regex(pattern);

if (File.Exists(script.File) && regex.IsMatch(File.ReadAllText(script.File)))
return true;

foreach (var variableName in variables.GetNames().Where(ScriptVariables.IsLibraryScriptModule))
{
if (ScriptVariables.GetLibraryScriptModuleLanguage(variables, variableName) == ScriptSyntax.Bash)
{
var contents = variables.Get(variableName);
if (contents != null && regex.IsMatch(contents))
return true;
}
}

return false;
}

static (string encrypted, string iv) GetEncryptedVariablesKvp(IVariables variables)
{
var sb = new StringBuilder();
foreach (var variable in variables
.Where(v => !ScriptVariables.IsLibraryScriptModule(v.Key))
.Where(v => !ScriptVariables.IsBuildInformationVariable(v.Key)))
{
var value = variable.Value ?? "nul";
sb.Append($"{EncodeAsHex(variable.Key)}").Append("$").AppendLine(EncodeAsHex(value));
sb.Append(variable.Key).Append('\0');
sb.Append(variable.Value ?? "").Append('\0');
}

var encrypted = VariableEncryptor.Encrypt(sb.ToString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ protected override IEnumerable<ScriptExecution> PrepareExecution(Script script,
Dictionary<string, string>? environmentVars = null)
{
var workingDirectory = Path.GetDirectoryName(script.File);
var configurationFile = BashScriptBootstrapper.PrepareConfigurationFile(workingDirectory, variables);
var configurationFile = BashScriptBootstrapper.PrepareConfigurationFile(workingDirectory, variables, script);
var (bootstrapFile, otherTemporaryFiles) = BashScriptBootstrapper.PrepareBootstrapFile(script, configurationFile, workingDirectory, variables);

var invocation = new CommandLineInvocation(
Expand Down
62 changes: 20 additions & 42 deletions source/Calamari.Common/Features/Scripting/Bash/Bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -264,42 +264,27 @@ function decrypt_and_parse_variables {
local encrypted="$1"
local iv="$2"

local decrypted
decrypted=$(decrypt_variable "$encrypted" "$iv")
declare -gA octopus_parameters=()

local -a key_byte_lengths=()
local -a value_byte_lengths=()
local -a hex_parts=()
# Decrypt the inline base64-encoded ciphertext and read NUL-delimited
# key\0value\0 pairs via mapfile in one shot.
local -a _kv
mapfile -d "" _kv < <(echo "$encrypted" | openssl enc -aes-256-cbc -d -a -A -nosalt -K "$sensitiveVariableKey" -iv "$iv")

while IFS='$' read -r hex_key hex_value; do
hex_value="${hex_value//$'\n'/}"
key_byte_lengths+=( $(( ${#hex_key} / 2 )) )
value_byte_lengths+=( $(( ${#hex_value} / 2 )) )
hex_parts+=( "${hex_key}${hex_value}" )
done <<< "$decrypted"

local concatenated_hex
concatenated_hex=$(printf "%s" "${hex_parts[@]}")

exec 3< <(echo -n "$concatenated_hex" | xxd -r -p)

local idx
for idx in "${!key_byte_lengths[@]}"; do
local key_byte_len="${key_byte_lengths[idx]}"
local value_byte_len="${value_byte_lengths[idx]}"
local decoded_key decoded_value

LC_ALL=C read -r -N "$key_byte_len" decoded_key <&3
LC_ALL=C read -r -N "$value_byte_len" decoded_value <&3

[[ "$decoded_value" == "nul" ]] && decoded_value=""
if [[ -n "$decoded_key" ]]; then
octopus_parameters["$decoded_key"]="$decoded_value"
fi
local i
for (( i = 0; i < ${#_kv[@]}; i += 2 )); do
[[ -n "${_kv[i]}" ]] && octopus_parameters["${_kv[i]}"]="${_kv[i+1]:-}"
done
}

exec 3<&-
function _ensure_octopus_parameters_loaded {
# Load octopus_parameters if feature toggle is enabled and Bash version supports it
bashParametersArrayFeatureToggle=#### BashParametersArrayFeatureToggle ####
if [[ "$bashParametersArrayFeatureToggle" == "true" ]]; then
if (( ${BASH_VERSINFO[0]:-0} > 4 || (${BASH_VERSINFO[0]:-0} == 4 && ${BASH_VERSINFO[1]:-0} > 2) )); then
decrypt_and_parse_variables "#### VARIABLESTRING.ENCRYPTED ####" "#### VARIABLESTRING.IV ####"
fi
fi
}

# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -366,16 +351,9 @@ function report_kubernetes_manifest_file
report_kubernetes_manifest "$MANIFEST" "$NAMESPACE"
}

bashParametersArrayFeatureToggle=#### BashParametersArrayFeatureToggle ####
scriptUsesOctopusParameters=#### SCRIPT_USES_OCTOPUS_PARAMETERS ####

if [ "$bashParametersArrayFeatureToggle" = true ]; then
if (( ${BASH_VERSINFO[0]:-0} > 4 || (${BASH_VERSINFO[0]:-0} == 4 && ${BASH_VERSINFO[1]:-0} > 2) )); then
if command -v xxd > /dev/null; then
decrypt_and_parse_variables "#### VARIABLESTRING.ENCRYPTED ####" "#### VARIABLESTRING.IV ####"
else
echo "xxd is not installed, this is required to use octopus_parameters"
fi
else
echo "Bash version 4.2 or later is required to use octopus_parameters"
fi
# Eagerly load octopus_parameters only if the user script actually uses it
if [ "$scriptUsesOctopusParameters" = true ]; then
_ensure_octopus_parameters_loaded
fi
39 changes: 37 additions & 2 deletions source/Calamari.Tests/Fixtures/Bash/BashFixture.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
Expand Down Expand Up @@ -335,6 +335,7 @@ public void ShouldSupportStrictVariableUnset(FeatureToggle? featureToggle)

static string specialCharacters => "! \" # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \\ ] ^ _ ` { | } ~ \n\u00b1 \u00d7 \u00f7 \u2211 \u220f \u2202 \u221e \u222b \u2248 \u2260 \u2264 \u2265 \u221a \u221b \u2206 \u2207 \u221d \n$ \u00a2 \u00a3 \u00a5 \u20ac \u20b9 \u20a9 \u20b1 \u20aa \u20bf \n• ‣ … ′ ″ ‘ ’ “ ” ‽ ¡ ¿ – — ― \n( ) [ ] { } ⟨ ⟩ « » ‘ ’ “ ” \n\u2190 \u2191 \u2192 \u2193 \u2194 \u2195 \u2196 \u2197 \u2198 \u2199 \u2b05 \u2b06 \u2b07 \u27a1 \u27f3 \nα β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω \n\u00a9 \u00ae \u2122 § ¶ † ‡ µ #";

[TestCase(FeatureToggle.BashParametersArrayFeatureToggle)]
[RequiresBashDotExeIfOnWindows]
public void ShouldBeAbleToEnumerateVariableValues(FeatureToggle? featureToggle)
{
Expand All @@ -354,7 +355,29 @@ public void ShouldBeAbleToEnumerateVariableValues(FeatureToggle? featureToggle)
["VariableName \n 11"] = "Value \n 11",
["VariableName.prop.anotherprop 12"] = "Value.prop.12",
["VariableName`prop`anotherprop` 13"] = "Value`prop`13",
[specialCharacters] = specialCharacters
[specialCharacters] = specialCharacters,
// Emoji / 4-byte UTF-8 codepoints
["EmojiKey 🎉💡🔥"] = "EmojiValue 😀🌍🚀",
// CJK (Chinese / Japanese / Korean)
["CJK 中文 日本語 한국어"] = "中文值 你好世界",
// Arabic RTL text
["Arabic مفتاح"] = "قيمة عربية",
// Bash command-injection attempts in both key and value
["InjectionAttempt $(echo injected)"] = "$(echo injected) `echo injected` ${HOME}",
// Empty value
["EmptyValueKey"] = "",
// Leading and trailing whitespace in value
["LeadingTrailingSpaces"] = " value padded with spaces ",
// Multiple '=' signs in value (parsers that split on '=' can mis-handle this)
["MultipleEquals"] = "a=b=c=d",
// Zero-width space (U+200B) – invisible but load-bearing
["ZeroWidth\u200bKey"] = "zero\u200bwidth\u200bvalue",
// ANSI escape sequence – terminal control injection attempt
["AnsiEscapeKey"] = "\x1b[31mRed\x1b[0m",
// Supplementary-plane Unicode (mathematical script + musical symbols)
["SupplementaryPlane 𝒜𝄞"] = "value 𝐀𝁆",
// Combining diacritical mark (NFD 'é' vs NFC U+00E9)
["CombiningDiacritical Caf\u0301"] = "Caf\u00e9",
}.AddFeatureToggleToDictionary(new List<FeatureToggle?> { FeatureToggle.BashParametersArrayFeatureToggle }));

output.AssertSuccess();
Expand Down Expand Up @@ -383,6 +406,18 @@ public void ShouldBeAbleToEnumerateVariableValues(FeatureToggle? featureToggle)
output.AssertOutput("Key: VariableName.prop.anotherprop 12, Value: Value.prop.12");
output.AssertOutput("Key: VariableName`prop`anotherprop` 13, Value: Value`prop`13");
output.AssertOutput($"Key: {specialCharacters}, Value: {specialCharacters}");

output.AssertOutput("Key: EmojiKey 🎉💡🔥, Value: EmojiValue 😀🌍🚀");
output.AssertOutput("Key: CJK 中文 日本語 한국어, Value: 中文值 你好世界");
output.AssertOutput("Key: Arabic مفتاح, Value: قيمة عربية");
output.AssertOutput("Key: InjectionAttempt $(echo injected), Value: $(echo injected) `echo injected` ${HOME}");
output.AssertOutput("Key: EmptyValueKey, Value: ");
output.AssertOutput("Key: LeadingTrailingSpaces, Value: value padded with spaces ");
output.AssertOutput("Key: MultipleEquals, Value: a=b=c=d");
output.AssertOutput($"Key: ZeroWidth\u200bKey, Value: zero\u200bwidth\u200bvalue");
output.AssertOutput($"Key: AnsiEscapeKey, Value: \x1b[31mRed\x1b[0m");
output.AssertOutput($"Key: SupplementaryPlane 𝒜𝄞, Value: value 𝐀𝁆");
output.AssertOutput($"Key: CombiningDiacritical Caf\u0301, Value: Caf\u00e9");
}
}

Expand Down