Skip to content

Delegates

Gabe Stocco edited this page Sep 9, 2020 · 28 revisions

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.

Property Parsing

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);
}

Value Extraction

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>>());
}

Custom Operation

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.

OatOperation Constructor

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;
}

Operation Delegate

This simple delegate just returns true.

internal OperationResult ReturnTrueOperationOperationDelegate(Clause clause, object? state1, object? state2, 
    IEnumerable<ClauseCapture>? captures)
{
    return new OperationResult(true);
}

Validation Delegate

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);
    }
}

Final Product

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

Overriding Built-In Operations

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)];
    }
}