-
Notifications
You must be signed in to change notification settings - Fork 19
Delegates
The OAT Analyzer has 4 Custom Delegate extensibility points and each built in operator also has a delegate that can be overridden. These allow you to extend the functionality of the Analyzer to your own custom operations and arbitrary complex objects.
The property parsing delegate provides a way to parse object types that Logical Analyzer doesn't support natively. For example, parsing Dictionaries and Lists that are not string based.
public delegate (bool Processed, object? Result) PropertyExtractionDelegate(object? obj, string index);
As an example, in Attack Surface Analyzer we extend Logical Analyzer property parsing to support our usage of TpmAlgId in dictionaries. link
public static (bool, object?) ParseCustomAsaProperties(object? obj, string index)
{
switch (obj)
{
case Dictionary<(TpmAlgId, uint), byte[]> algDict:
var elements = Convert.ToString(index, CultureInfo.InvariantCulture)?
.Trim('(').Trim(')').Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (Enum.TryParse(typeof(TpmAlgId), elements.First(), out object? result) &&
result is TpmAlgId Algorithm && uint.TryParse(elements.Last(), out uint Index) &&
algDict.TryGetValue((Algorithm, Index), out byte[]? byteArray))
{
return (true, byteArray);
}
else
{
return (true, null);
}
}
return (false, null);
}
The value extraction property provides an extension mechanism to provide a way to turn an object into extracted strings for Logical Analyzer to compare.
public delegate (bool Processed,
IEnumerable<string> valsExtracted,
IEnumerable<KeyValuePair<string, string>> dictExtracted)
ObjectToValuesDelegate(object? obj);
In Attack Surface Analyzer we also extend Value extraction to support our usage of TpmAlgId in dictionaries. link
public static (bool Processed,
IEnumerable<string> valsExtracted,
IEnumerable<KeyValuePair<string, string>> dictExtracted)
ParseCustomAsaObjectValues(object? obj)
{
if (obj is Dictionary<(TpmAlgId, uint), byte[]> algDict)
{
return (true,Array.Empty<string>(), algDict.ToList()
.Select(x =>
new KeyValuePair<string, string>(x.Key.ToString(), Convert.ToBase64String(x.Value)))
.ToList());
}
return (false, Array.Empty<string>(), Array.Empty<KeyValuePair<string,string>>());
}
The Custom Operation Delegate allows you to extend Logical Analyzer with your own custom operation or operations. Custom Operations are also provided with the set of captures of previously evaluated clauses in the same rule.
To create a custom Operation you will extend the OatOperation
class.
We also specify the CustomOperation
label. You must use this label in your Clause
to trigger the Operation.
For example, lets make an operation that always returns true.
In the constructor we specify that this is a Custom Operation by providing Operation.Custom
to the base constructor.
We also pass ReturnTrue
as the CustomOperation Label for this Operation.
public ReturnTrueOperation(Analyzer analyzer) : base(Operation.Custom, analyzer)
{
CustomOperation = "ReturnTrue";
OperationDelegate = NoOperationOperationDelegate;
ValidationDelegate = NoOperationValidationDelegate;
}
This simple delegate just returns true.
internal OperationResult ReturnTrueOperationOperationDelegate(Clause clause, object? state1, object? state2,
IEnumerable<ClauseCapture>? captures)
{
return new OperationResult(true);
}
This delegate warns if you have provided extra data this operation doesn't need when specifying your Clause.
internal IEnumerable<Violation> ReturnTrueOperationValidationDelegate(Rule rule, Clause clause)
{
if (clause.Data?.Any() || clause.DictData?.Any()){
yield return new Violation("No Data is expected.", rule, clause);
}
}
Here is the final ReturnTrueOperation class.
public class ReturnTrueOperation : OatOperation
{
public ReturnTrueOperation(Analyzer analyzer) : base(Operation.Custom, analyzer)
{
CustomOperation = "ReturnTrue";
OperationDelegate = NoOperationOperationDelegate;
ValidationDelegate = NoOperationValidationDelegate;
}
internal OperationResult ReturnTrueOperationOperationDelegate(Clause clause, object? state1, object? state2,
IEnumerable<ClauseCapture>? captures)
{
return new OperationResult(true);
}
internal IEnumerable<Violation> ReturnTrueOperationValidationDelegate(Rule rule, Clause clause)
{
if (clause.Data?.Any() || clause.DictData?.Any()){
yield return new Violation("No Data is expected.", rule, clause);
}
}
This is how you would create a rule using it. Note setting the CustomOperation field appropriately.
var rule = new Rule("Return True Rule")
{
Clauses = new List<Clause>()
{
new Clause(Operation.Custom)
{
CustomOperation = "ReturnTrue"
}
}
};
This is an example of using it on some objects
var analyzer = new Analyzer();
analyzer.SetOperation(new ReturnTrueOperation(analyzer));
analyzer.Analyze(rule, true); // true
analyzer.Analyze(rule, false); // true
analyzer.Analyze(rule, null); // true
In addition to setting Custom Operations you can override the default operations if you would prefer different functionality.
For example, if you wanted to be able to identify which regular expression matched when a Clause matches you could either create a custom operation for this purpose or override the Regex operation.
Note that this example changes the capture return type for the Regex operation to TypedClauseCapture<List<(int,Match)>
.
/// <summary>
/// The overridden Regex With Indices operation operation
/// </summary>
public class RegexWithIndicesOperation : OatOperation
{
private readonly ConcurrentDictionary<(string, RegexOptions), Regex?> RegexCache =
new ConcurrentDictionary<(string, RegexOptions), Regex?>();
/// <summary>
/// Create a RegexWithIndicesOperation given an analyzer
/// </summary>
/// <param name="analyzer">The analyzer context to work with</param>
public RegexWithIndicesOperation(Analyzer analyzer) : base(Operation.Regex, analyzer)
{
OperationDelegate = RegexWithIndicesOperationDelegate;
ValidationDelegate = RegexWithIndicesValidationDelegate;
}
internal IEnumerable<Violation> RegexWithIndicesValidationDelegate(Rule rule, Clause clause)
{
if (clause.Data?.Count == null || clause.Data?.Count == 0)
{
yield return new Violation("No Data provided!", rule, clause);
}
else if (clause.Data is List<string> regexList)
{
foreach (var regex in regexList)
{
if (!Helpers.IsValidRegex(regex))
{
yield return new Violation("Invalid Regex", rule, clause);
}
}
}
if (clause.DictData != null && clause.DictData?.Count > 0)
{
yield return new Violation("Dictionary Based Data Input is not allowed", rule, clause);
}
}
internal OperationResult RegexWithIndicesOperationDelegate(Clause clause, object? state1, object? state2,
IEnumerable<ClauseCapture>? captures)
{
(var stateOneList, _) = Analyzer?.ObjectToValues(state1) ?? (new List<string>(), new List<KeyValuePair<string, string>>());
(var stateTwoList, _) = Analyzer?.ObjectToValues(state2) ?? (new List<string>(), new List<KeyValuePair<string, string>>());
if (clause.Data is List<string> RegexList && RegexList.Any())
{
var options = RegexOptions.Compiled;
if (clause.Arguments.Contains("i"))
{
options |= RegexOptions.IgnoreCase;
}
if (clause.Arguments.Contains("m"))
{
options |= RegexOptions.Multiline;
}
var outmatches = new List<(int RegexIndex, Match Match>();
for (int i = 0: i < regexList.Count; i++){
var regex = StringToRegex(regexList[i], options);
if (regex != null)
{
foreach (var state in stateOneList)
{
var matches = regex.Matches(state);
if (matches.Count > 0 || (matches.Count == 0 && clause.Invert))
{
foreach (var match in matches)
{
if (match is Match m)
{
outmatches.Add((i,m));
}
}
}
}
foreach (var state in stateTwoList)
{
var matches = regex.Matches(state);
if (matches.Count > 0 || (matches.Count == 0 && clause.Invert))
{
foreach (var match in matches)
{
if (match is Match m)
{
outmatches.Add((i,m));
}
}
}
}
}
}
if (outmatches.Any()){
return new OperationResult(true, !clause.Capture ? null :
new TypedClauseCapture<List<(int RegexIndex,Match Match)>>(clause, outmatches, state1, state2));
}
}
return new OperationResult(false, null);
}
/// <summary>
/// Converts a strings to a compiled regex.
/// Uses an internal cache.
/// </summary>
/// <param name="built">The regex to build</param>
/// <param name="regexOptions">The options to use.</param>
/// <returns>The built Regex</returns>
public Regex? StringToRegex(string built, RegexOptions regexOptions)
{
if (!RegexCache.ContainsKey((built, regexOptions)))
{
try
{
RegexCache.TryAdd((built, regexOptions), new Regex(built, regexOptions));
}
catch (ArgumentException)
{
Log.Warning("InvalidArgumentException when creating regex. Regex {0} is invalid and will be skipped.", built);
RegexCache.TryAdd((built, regexOptions), null);
}
}
return RegexCache[(built, regexOptions)];
}
}