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

Update process handler #4425

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e43eaa6
Update env expand logic
Sep 6, 2023
170ed65
Upd process handler args validation
Sep 8, 2023
2daebb5
Remove test ex
Sep 8, 2023
88def48
Merge branch 'master' into users/KonstantinTyukalov/Update_processHan…
KonstantinTyukalov Sep 8, 2023
459cec6
Merge branch 'master' into users/KonstantinTyukalov/Update_processHan…
KonstantinTyukalov Sep 19, 2023
4c034fd
Add copyright + cleanup imports
Sep 19, 2023
bd689cf
Merge branch 'users/KonstantinTyukalov/Update_processHandler' of http…
Sep 19, 2023
981ada6
Simplify telemetry type
Sep 19, 2023
32b209f
Simplify PH telemetry tests
Sep 19, 2023
e6811ea
Merge branch 'master' into users/KonstantinTyukalov/Update_processHan…
KonstantinTyukalov Sep 21, 2023
fc640df
Update debug logs
Sep 21, 2023
c6c6bcf
Exclude % from allowed symbols
Sep 22, 2023
b1f2317
Fix cmd expand env
Sep 22, 2023
280eb30
remove useless telemetry
Sep 22, 2023
4a3ec54
Add test traits
Sep 22, 2023
4568202
Fix env incorrect expanding
Sep 22, 2023
c2549b4
Add more test cases
Sep 22, 2023
4c8eb5e
Fix exception condition
Sep 22, 2023
1ad825f
Move validation args to public
Sep 22, 2023
b6fd90c
Update validation logic
Oct 3, 2023
8004fc6
Add % to test
Oct 3, 2023
a57bd0d
Merge branch 'master' into users/KonstantinTyukalov/Update_processHan…
KonstantinTyukalov Oct 3, 2023
c881b5b
Code cleanup
Oct 12, 2023
7334ec0
Merge branch 'master' into users/KonstantinTyukalov/Update_processHan…
KonstantinTyukalov Oct 12, 2023
cd38260
Hide new logic under knob
Oct 13, 2023
bdabbd9
Move constants to class
Oct 16, 2023
48d481c
Merge branch 'master' into users/KonstantinTyukalov/Update_processHan…
kirill-ivlev Oct 16, 2023
e9a4017
move const value back
Oct 16, 2023
039979a
Merge branch 'users/KonstantinTyukalov/Update_processHandler' of http…
Oct 16, 2023
7d8a879
Add PublishTelemetry overload
Oct 16, 2023
fc08d0b
Update throw logic
Oct 16, 2023
d66169c
Update invalid args exception
Oct 16, 2023
37edc1a
Update private const letter case
Oct 16, 2023
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
7 changes: 7 additions & 0 deletions src/Agent.Sdk/Knob/AgentKnobs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,13 @@ public class AgentKnobs
new EnvironmentKnobSource("AZP_75787_ENABLE_COLLECT"),
new BuiltInDefaultKnobSource("false"));

public static readonly Knob ProcessHandlerEnableNewLogic = new Knob(
nameof(ProcessHandlerEnableNewLogic),
"Enables new sanitization logic for process handler",
new RuntimeKnobSource("AZP_75787_ENABLE_NEW_PH_LOGIC"),
new EnvironmentKnobSource("AZP_75787_ENABLE_NEW_PH_LOGIC"),
new BuiltInDefaultKnobSource("false"));

public static readonly Knob DisableDrainQueuesAfterTask = new Knob(
nameof(DisableDrainQueuesAfterTask),
"Forces the agent to disable draining queues after each task",
Expand Down
10 changes: 10 additions & 0 deletions src/Agent.Worker/Handlers/Handler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,20 @@ protected void RemovePSModulePathFromEnvironment()
}
}

// This overload is to handle specific types some other way.
protected void PublishTelemetry<T>(
Dictionary<string, T> telemetryData,
string feature = "TaskHandler"
)
{
// JsonConvert.SerializeObject always converts to base object.
PublishTelemetry((object)telemetryData, feature);
}

private void PublishTelemetry(
object telemetryData,
KonstantinTyukalov marked this conversation as resolved.
Show resolved Hide resolved
string feature = "TaskHandler"
)
{
ArgUtil.NotNull(Task, nameof(Task));

Expand Down
96 changes: 96 additions & 0 deletions src/Agent.Worker/Handlers/Helpers/CmdArgsSanitizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.Services.Agent.Util;

namespace Agent.Worker.Handlers.Helpers
{
public static class CmdArgsSanitizer
{
private const string _removedSymbolSign = "_#removed#_";
private const string _argsSplitSymbols = "^^";
private static readonly Regex _sanitizeRegExp = new("(?<!\\^)([^a-zA-Z0-9\\\\` _''\"\\-=\\/:\\.*,+~?^%])");

public static (string, CmdArgsSanitizingTelemetry) SanitizeArguments(string inputArgs)
{
if (inputArgs == null)
{
return (null, null);
}

var argsChunks = inputArgs.Split(_argsSplitSymbols);
var matchesChunks = new List<MatchCollection>();

for (int i = 0; i < argsChunks.Length; i++)
{
var matches = _sanitizeRegExp.Matches(argsChunks[i]);
if (matches.Count > 0)
{
matchesChunks.Add(matches);
argsChunks[i] = _sanitizeRegExp.Replace(argsChunks[i], _removedSymbolSign);
}
}

var resultArgs = string.Join(_argsSplitSymbols, argsChunks);

CmdArgsSanitizingTelemetry telemetry = null;

if (resultArgs != inputArgs)
{
var symbolsCount = matchesChunks
.Select(chunk => chunk.Count)
.Aggregate(0, (acc, mc) => acc + mc);
telemetry = new CmdArgsSanitizingTelemetry
(
RemovedSymbols: CmdArgsSanitizingTelemetry.ToSymbolsDictionary(matchesChunks),
RemovedSymbolsCount: symbolsCount
);
}

return (resultArgs, telemetry);
}
}

public record CmdArgsSanitizingTelemetry
(
Dictionary<string, int> RemovedSymbols,
int RemovedSymbolsCount
)
{
public static Dictionary<string, int> ToSymbolsDictionary(List<MatchCollection> matches)
{
ArgUtil.NotNull(matches, nameof(matches));

var symbolsDict = new Dictionary<string, int>();
foreach (var mc in matches)
{
foreach (var m in mc.Cast<Match>())
{
var symbol = m.Value;
if (symbolsDict.TryGetValue(symbol, out _))
{
symbolsDict[symbol] += 1;
}
else
{
symbolsDict[symbol] = 1;
}
}
}

return symbolsDict;
}

public Dictionary<string, object> ToDictionary()
{
return new()
{
["removedSymbols"] = RemovedSymbols,
["removedSymbolsCount"] = RemovedSymbolsCount,
};
}
}
}
176 changes: 96 additions & 80 deletions src/Agent.Worker/Handlers/Helpers/ProcessHandlerHelper.cs
Original file line number Diff line number Diff line change
@@ -1,66 +1,46 @@
using System;
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Text;
using Agent.Sdk.Knob;
using Microsoft.VisualStudio.Services.Agent.Util;
using Microsoft.VisualStudio.Services.Agent.Worker;
using Microsoft.VisualStudio.Services.Common;

namespace Agent.Worker.Handlers.Helpers
{
public static class ProcessHandlerHelper
{
public static (string, CmdTelemetry) ProcessInputArguments(string inputArgs)
private const char _escapingSymbol = '^';
private const string _envPrefix = "%";
private const string _envPostfix = "%";

public static (string, CmdTelemetry) ExpandCmdEnv(string inputArgs, Dictionary<string, string> environment)
{
const char quote = '"';
const char escapingSymbol = '^';
const string envPrefix = "%";
const string envPostfix = "%";
ArgUtil.NotNull(inputArgs, nameof(inputArgs));
ArgUtil.NotNull(environment, nameof(environment));

string result = inputArgs;
int startIndex = 0;
var telemetry = new CmdTelemetry();

while (true)
{
int prefixIndex = result.IndexOf(envPrefix, startIndex);
int prefixIndex = result.IndexOf(_envPrefix, startIndex);
if (prefixIndex < 0)
{
break;
}

telemetry.FoundPrefixes++;

if (prefixIndex > 0 && result[prefixIndex - 1] == escapingSymbol)
if (prefixIndex > 0 && result[prefixIndex - 1] == _escapingSymbol)
{
if (result[prefixIndex - 2] == 0 || result[prefixIndex - 2] != escapingSymbol)
{
startIndex++;
result = result[..(prefixIndex - 1)] + result[prefixIndex..];

telemetry.EscapedVariables++;

continue;
}

telemetry.EscapedEscapingSymbols++;
telemetry.EscapingSymbolBeforeVar++;
}

// We possibly should simplify that part -> if just no close quote, then break
int quoteIndex = result.IndexOf(quote, startIndex);
if (quoteIndex >= 0 && prefixIndex > quoteIndex)
{
int nextQuoteIndex = result.IndexOf(quote, quoteIndex + 1);
if (nextQuoteIndex < 0)
{
telemetry.QuotesNotEnclosed = 1;
break;
}

startIndex = nextQuoteIndex + 1;

telemetry.QuottedBlocks++;

continue;
}

int envStartIndex = prefixIndex + envPrefix.Length;
int envStartIndex = prefixIndex + _envPrefix.Length;
int envEndIndex = FindEnclosingIndex(result, prefixIndex);
if (envEndIndex == 0)
{
Expand All @@ -69,32 +49,29 @@ public static (string, CmdTelemetry) ProcessInputArguments(string inputArgs)
}

string envName = result[envStartIndex..envEndIndex];

telemetry.BracedVariables++;

if (envName.StartsWith(escapingSymbol))
if (envName.StartsWith(_escapingSymbol))
{
var sanitizedEnvName = envPrefix + envName[1..] + envPostfix;

result = result[..prefixIndex] + sanitizedEnvName + result[(envEndIndex + envPostfix.Length)..];
startIndex = prefixIndex + sanitizedEnvName.Length;

telemetry.VariablesStartsFromES++;

continue;
}

var head = result[..prefixIndex];
if (envName.Contains(escapingSymbol))
if (envName.Contains(_escapingSymbol, StringComparison.Ordinal))
{
head += envName.Split(escapingSymbol)[1];
envName = envName.Split(escapingSymbol)[0];

telemetry.VariablesWithESInside++;
}

var envValue = System.Environment.GetEnvironmentVariable(envName) ?? "";
var tail = result[(envEndIndex + envPostfix.Length)..];
// Since Windows have case-insensetive environment, and Process handler is windows-specific, we should allign this behavior.
var windowsEnvironment = new Dictionary<string, string>(environment, StringComparer.OrdinalIgnoreCase);

// In case we don't have such variable, we just leave it as is
if (!windowsEnvironment.TryGetValue(envName, out string envValue) || string.IsNullOrEmpty(envValue))
{
telemetry.NotExistingEnv++;
startIndex = prefixIndex + 1;
continue;
}

var tail = result[(envEndIndex + _envPostfix.Length)..];

result = head + envValue + tail;
startIndex = prefixIndex + envValue.Length;
Expand All @@ -119,49 +96,88 @@ private static int FindEnclosingIndex(string input, int targetIndex)

return 0;
}

public static (bool, Dictionary<string, object>) ValidateInputArguments(
string inputArgs,
Dictionary<string, string> environment,
IExecutionContext context)
{
var enableValidation = AgentKnobs.ProcessHandlerSecureArguments.GetValue(context).AsBoolean();
context.Debug($"Enable args validation: '{enableValidation}'");
var enableAudit = AgentKnobs.ProcessHandlerSecureArgumentsAudit.GetValue(context).AsBoolean();
context.Debug($"Enable args validation audit: '{enableAudit}'");
var enableTelemetry = AgentKnobs.ProcessHandlerTelemetry.GetValue(context).AsBoolean();
context.Debug($"Enable telemetry: '{enableTelemetry}'");

if (enableValidation || enableAudit || enableTelemetry)
{
context.Debug("Starting args env expansion");
var (expandedArgs, envExpandTelemetry) = ExpandCmdEnv(inputArgs, environment);
context.Debug($"Expanded args={expandedArgs}");

context.Debug("Starting args sanitization");
var (sanitizedArgs, sanitizeTelemetry) = CmdArgsSanitizer.SanitizeArguments(expandedArgs);

Dictionary<string, object> telemetry = null;
if (sanitizedArgs != inputArgs)
{
if (enableTelemetry)
{
telemetry = envExpandTelemetry.ToDictionary();
if (sanitizeTelemetry != null)
{
telemetry.AddRange(sanitizeTelemetry.ToDictionary());
}
}
if (sanitizedArgs != expandedArgs)
KonstantinTyukalov marked this conversation as resolved.
Show resolved Hide resolved
{
if (enableAudit && !enableValidation)
{
context.Warning(StringUtil.Loc("ProcessHandlerInvalidScriptArgs"));
}
if (enableValidation)
{
return (false, telemetry);
}

return (true, telemetry);
}
}

return (true, null);
}
else
{
context.Debug("Args sanitization skipped.");
return (true, null);
}
}
}

public class CmdTelemetry
{
public int FoundPrefixes { get; set; } = 0;
public int QuottedBlocks { get; set; } = 0;
public int VariablesExpanded { get; set; } = 0;
public int EscapedVariables { get; set; } = 0;
public int EscapedEscapingSymbols { get; set; } = 0;
public int EscapingSymbolBeforeVar { get; set; } = 0;
public int VariablesStartsFromES { get; set; } = 0;
public int BraceSyntaxEntries { get; set; } = 0;
public int BracedVariables { get; set; } = 0;
public int VariablesWithESInside { get; set; } = 0;
public int QuotesNotEnclosed { get; set; } = 0;
public int NotClosedEnvSyntaxPosition { get; set; } = 0;
public int NotExistingEnv { get; set; } = 0;

public Dictionary<string, int> ToDictionary()
public Dictionary<string, object> ToDictionary()
{
return new Dictionary<string, int>
return new Dictionary<string, object>
{
["foundPrefixes"] = FoundPrefixes,
["quottedBlocks"] = QuottedBlocks,
["variablesExpanded"] = VariablesExpanded,
["escapedVariables"] = EscapedVariables,
["escapedEscapingSymbols"] = EscapedEscapingSymbols,
["escapedVariables"] = EscapingSymbolBeforeVar,
["variablesStartsFromES"] = VariablesStartsFromES,
["braceSyntaxEntries"] = BraceSyntaxEntries,
["bracedVariables"] = BracedVariables,
["bariablesWithESInside"] = VariablesWithESInside,
["quotesNotEnclosed"] = QuotesNotEnclosed,
["notClosedBraceSyntaxPosition"] = NotClosedEnvSyntaxPosition
["notClosedBraceSyntaxPosition"] = NotClosedEnvSyntaxPosition,
["notExistingEnv"] = NotExistingEnv
};
}

public Dictionary<string, string> ToStringsDictionary()
{
var dict = ToDictionary();
var result = new Dictionary<string, string>();
foreach (var key in dict.Keys)
{
result.Add(key, dict[key].ToString());
}
return result;
}
};
}
1 change: 0 additions & 1 deletion src/Agent.Worker/Handlers/NodeHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ public async Task RunAsync()
{
Trace.Info("AZP_AGENT_IGNORE_VSTSTASKLIB enabled, ignoring fix");
}


StepHost.OutputDataReceived += OnDataReceived;
StepHost.ErrorDataReceived += OnDataReceived;
Expand Down
Loading