Last updated at Thu, 30 Nov 2017 19:32:52 GMT
Here at Komand, we needed an intuitive way to filter data from a trigger step. When automating security operations and processes, sometimes you don’t want a workflow to start on every trigger. Splunk logs may be firing off millions of events, but running a workflow for each one may not really be what you need. If we were to set up a privilege escalation rule in Sysdig Falco and index it in Splunk, we would want to run a specific workflow for that rule, but a separate workflow for detecting SQL injection.
Additionally, automating decisions that need to be made when output from previous steps satisfies certain criteria would need to perform some sort of filtering, in order to decide which path to go down next. For example, if we find malware in email attachments, we may want to automatically navigate down a remediation path to ensure there was no compromise.
In this post, I will walk through how we incorporated a PEG.js parser and added typeahead support for the expression editor in our workflow builder. We will use a simpler parsing expression grammar (PEG) than the one at Komand, but it will serve our purposes for this post. It only deals with five operators: “=”, “!=”, “contains”, "starts_with", and "ends_with".
First, we create the grammar and parser using PEG.js. PEG.js has a great online editor that verifies your grammar as you create it, as well as a command line tool. Below is the grammar we will be using:
start
= expression
expression
= characters ws? (equality / relational)
equality
= equalityOp ws? characters
relational
= relationalOp ws? characters
characters
= [a-zA-Z0-9.]+
ws "whitespace"
= [\n\t ]+ { return ""; }
equalityOp "operator"
= "=" / "!="
relationalOp "operator"
= "contains" / “starts_with” / “ends_with”
We won’t go into too much detail about how we created this PEG, but if you are interested you can learn more about creating your own here. This grammar in particular allows us to write expressions such as “x = something”, “127.0.0.1 = 127.0.0.1”, or “someLog contains “drop all”” and is actually a subset of what we use here at Komand.
Once we have a grammar for our language, we can then generate a parser using PEG.js, and start to use it in React. You can either generate it on the fly via javascript (from documentation):
var peg = require("pegjs");
var parser = peg.generate([pass in grammar here]);
or by using the command line:
pegjs grammar.pegjs
We chose the latter because we wanted to know the code we were using in production and be able to version the grammar and parser.
Next, let’s create our expression editor:
import React from 'react';
import parser from './parser';
class ExpressionEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
ast: {},
query: '',
error: ''
};
}
handleChange = (e) => {
this.setState({ query: e.target.value });
}
render() {
const { query } = this.state;
return (
<div className="expression-editor">
<textarea
className="expression-editor__textarea"
onChange={this.handleChange}
value={query}
></textarea>
</div>
);
}
}
export default ExpressionEditor;
Nothing too crazy. We update a simple textarea with the expression as we enter it.
Now we can flesh out our handleChange method to incorporate our parser. It’s as simple as passing our query to parser.parse and catching any errors that are thrown from our generated parser. Then we update our state as normal.
handleChange = (e) => {
const query = e.target.value;
let ast = {}; let error = '';
try {
ast = parser.parse(query);
} catch (ex) {
error = ex.message;
}
this.setState({ ast, query, error });
}
At Komand, we have the concept of workflows and steps. Previous steps produce output that can be used by later steps in the workflow. Wouldn’t it be awesome if we had typeahead for this variable output and for the operators of our expression language so we don’t have to remember them all?
In comes react-mentions. Whether you are using @ or #, tons of social media platforms have mentions. They come usually in the form of a pre-populated list dropdown, from which you can select your BFF or a trending hashtag. Why not apply the same concept for our expression language?
React-mentions makes it super simple. You pass the character(s) on to trigger the Mention component as a “trigger” prop, as well as the list of items to populate when those characters are typed as a “data” prop. Let’s replace the textarea with a MentionsInput component. We will hard code the operators for now. Our render method then becomes:
render() {
const { query, error } = this.state;
const operators = [
{ id: '1', display: '=' },
{ id: '2', display: '!=' },
{ id: '3', display: 'contains' },
{ id: '4', display: 'starts_with' },
{ id: '5', display: 'ends_with' },
];
return (
<div className="expression-editor">
<MentionsInput
className="expression-editor__textarea"
value={query}
onChange={this.handleChange}
markup="#[__display__]"
>
<Mention
type="operators"
trigger="#"
data={operators}
/>
</MentionsInput>
<div className="expression-editor__error">
{error}
</div>
</div>
);
}
This library also gives you full flexibility on how you highlight those keywords, and how you display the list of suggestions. You can render different styles, elements, or entire React components if you need a more complex UI.
To change how the keywords are represented in the textarea itself, you can change the value displayed in the textarea by altering the “markup” prop or by supplying a “transformDisplay” function prop on the MentionsInput component to have more extensive customizability (e.g. based on the “type” prop on the Mention).
transformDisplay(id) {
return `<--${id}-->`;
}
To change the way the suggestions for typeahead are rendered, supply the Mention component with a renderSuggestion method like below as a prop:
renderSuggestion(suggestion, search, highlightedDisplay) {
return (
<li className="variable">
{ highlightedDisplay }
</li>
);
}
Our completed component can be found below (it does not have the above customizations for rendering suggestions and transforming the display):
import React from 'react';
import { MentionsInput, Mention } from 'react-mentions';
import parser from './parser';
class ExpressionEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
ast: {},
query: '',
error: '',
};
}
handleChange = (e) => {
const query = e.target.value;
let ast = {};
let error = '';
try {
ast = parser.parse(query);
} catch (ex) {
error = ex.message;
}
this.setState({ ast, query, error });
}
render() {
const { query, error } = this.state;
const operators = [
{ id: '1', display: '=' },
{ id: '2', display: '!=' },
{ id: '3', display: 'contains' },
{ id: '4', display: 'starts_with' },
{ id: '5', display: 'ends_with' },
];
return (
<div className="expression-editor">
<MentionsInput
className="expression-editor__textarea"
value={query}
onChange={this.handleChange}
markup="#[__display__]"
>
<Mention
type="operators"
trigger="#"
data={operators}
/>
</MentionsInput>
<div className="expression-editor__error">
{error}
</div>
</div>
);
}
}
export default ExpressionEditor;
And that’s it! When we type #, a list of operators will be displayed in a drop down suggestion list. At Komand, we used this same technique to populate variables from previous steps when the user begins to type “{{“. Now we can filter out the noise from our triggers and make automated decisions based on previous steps.
If you’re interested in trying out Komand and these features, sign up for our beta!