September 14, 2021 | 13 min read
Tomer Almog

Here at Cycode, we provide our customers with many types of product alerts. When developing the alerts feature, one of our challenges was to find a solution that assures each customer only receives the information they care about, to reduce noise. In other words, we needed to give our customers the ability to choose which alerts they wished to filter out. This blog post will detail how we evaluated four tools: Roslyn CSharpScript, DynamicExpresso, Flee, DynamicLinq, why we chose string-based conditions using DynamicLinq and how we ensured security.

But first, let’s understand what our requirements were.

Alert Management Feature Requirements

The feature requirement was to allow our customers (who are cybersecurity and engineering professionals) to write complex conditions that will filter out any unwanted alerts. So we set out to find the best solution.

The main requirements for developing this feature were:

1. Flexibility and Scalability

 Each alert that we provide comes with its own specific fields. Therefore, the condition customers will be setting needs to be dynamic enough to support this. In addition, we’re always developing new capabilities. So, the condition should not only support the current alerts fields, but it should also be able to easily support future fields and new alerts that will be added. 

Additionally, the solution should be able to support multiple different operators (AND/OR) and string functions (Contains, StartsWith, etc).

2. Security

By enabling our customers to configure the product, we’re potentially letting external actors execute logic in our system. As a cybersecurity company, we’re aware of the huge risk we’re taking and we know it should be done meticulously and carefully. Therefore, we need to make sure that it is impossible to exploit this mechanism.

3. Performance 

We need the logic to be executed as seamlessly as possible with minimum overhead. Since customers will probably have multiple conditions, we need to make sure that each condition evaluation takes no more than a few milliseconds at most.

Our Solution: String Evaluation

After brainstorming and research, we reached the conclusion that the perfect solution would be to accept the condition as string input, and have an engine that evaluates that condition transparently in the backend.

One of the biggest capabilities we needed for this direction was the ability to accept a string and execute it as a standard C# LINQ expression. This was convenient and valuable for us because the capability corresponds with the other expressions in our code, as well as allowing the ability to execute the statement on the database directly.

String Evaluation Tools

We figured that there were already some existing solutions in the market that we could use to evaluate a string as a C# lambda expression, which would save us the time and effort of building our own. So we evaluated a few of them to find the one most suitable for our needs.

The test we ran for each of them was similar:

  • We provided the same input, which is a lambda as string format (e.g.: `alert => alert.Name == “test”`).
  • We let the tool evaluate the lambda against a static `alert` class.
  • We checked the performance of each tool.

We looked at four tools:

Roslyn CSharpScript

One of the native extensions offered by Microsoft, Roslyn CSharpScript supports the standard C# LINQ expressions, which means it’s relatively easy to implement. It is offered under the MIT license and the evaluation time took 65 milliseconds.

DynamicExpresso

DynamicExpresso is an open-source library that provides the required capabilities under the MIT license. Similarly, it supports LINQ expressions. Its lambda evaluation took 0.1ms.

Flee

Flee is another open-source library licensed under the LGPL. Flee supports its own query language, which makes it relatively less favorable to implement. Its execution time was 2ms.

DynamicLinq

Another native extension package, DynamicLinq supports LINQ expressions, has an active user base and enjoys frequent updates. Its execution time was around 1ms and it can be used under the Apache 2.0 license.

Roslyn CSharpScript vs. DynamicExpresso vs. Flee vs. DynamicLinq Comparison Table

Name License Run time LINQ-based?
CSharpScript MIT 65ms Yes
DynamicExpresso MIT 0.1ms Yes
Flee LGPL 2ms No
DynamicLinq Apache 2.0 1ms Yes

 

The Chosen Library: DynamicLinq

We finally decided to go with DynamicLinq due to its fast execution times and its dedicated development community. LINQ provides the flexibility we need. So we found the optimal solution for our flexibility, scalability and performance concerns. But what about security? 

Tackling The Security Concerns

To ensure security, we closed off the backend API endpoint to end users and enabled only the BFF (backend for frontend) server to add the conditions. This drastically reduces the risk of unwanted content being evaluated by our backend engine.

How can users make changes? The frontend would receive the input coming from the end user. Then, the BFF (backend for frontend) would “translate” that input to the lambda string the backend expects to receive via the API. The frontend holds a mapping of each alert and its fields to assist with translation.

Cycode Alerts

In the above screenshot, you can see the rules that were already added to the system. Under “Excluded Asset”, you see the human-readable condition that the user chose. In the blue tooltip, you can see part of the “translation” of that condition, that is evaluated by the backend against the alerts.

The flow:

  1. The user chooses a field – for example “Name”. 
  2. The user chooses the relevant operator (equals, not equals, contains, etc)
  3. The user picks a value to filter by, for example “TestRepository”.
  4. The BFF generates a lambda query based on the logic provided by the user in string format and sends it to the backend.  
  5. We can run a validation to the input string against a mock alert before returning a 200 OK response to the BFF.

Cycode Alerts

In the above screenshot, you can see the interface through which the user creates a condition.

Reducing the Risk of Data Loss

In order to explain how we reduced the risk of data loss, I will detail the condition’s use cases, and how we managed to avoid executing the expression directly on the database. 

The first use case was to block future alerts that may be created. To achieve that, we would need to execute the expression against each potential new alert. No need for the database, but this flow’s performance should be monitored over time.

The second use case was to filter out older alerts that match the criteria. To achieve this, we would need to go over all the existing alerts retrospectively and filter out the matching ones. To avoid executing external logic directly on the database and risking the data, we can extract the relevant alerts from the database in a paged manner and try to execute the lambda against these entries. In case the evaluation fails for any reason, we stop the process immediately and investigate the lambda manually offline to understand the problem.  

Conclusion

Evaluating string-based conditions using DynamicLinq has proven to be extremely helpful in different random situations that we encountered and helped us easily accommodate specific customer requests. This feature allows us great flexibility to tackle varying ongoing challenges with good performance. To learn more about what we do, check out our engineering blog.