FluentFlow is matching engine which lets you easily define 'followed by'-relations in a flow of json objects. Rules can be written in javascript as plain functions or using a fluent API.
$ sudo npm install -g fluentflow
Usage: fluentflow.js [OPTIONS] rulesFile
rulesFile : path to the rules file
OPTIONS:
-j JSONPath : JSONPath expression
-t : test if rules are valid
-h : print this help
Configure rules.js:
[
// Check if somebody forked this repository after submitting an issue
// Reverse order because the github api displays events in this order
$.match(function(currentObject) {
return currentObject.type == "ForkEvent"
}).followedBy.match(function(currentObject, lastObject){
return currentObject.type == "IssuesEvent"
&& currentObject.actor.login == lastObject.actor.login;
}).then(function(fork, issue){
console.log('User: ' + fork.actor.login + ' forked after writing issue: ' + issue.id);
})
];
Start FluentFlow:
$ curl -s https://api.github.com/repos/t-moe/FluentFlow/events | fluentflow rules.js -j '*'
- Note: -j '*' because github responds with an array of json objects which we should split before processing
$ npm install --save fluentflow
const FluentFlow = require("fluentflow");
const Fluent = FluentFlow.Fluent;
const $ = Fluent.Matcher().starter;
// Build the rules
const builder = new FluentFlow.Matcher.Builder();
builder.append(
$.match(function(obj){return obj.bar===5;}).then(console.log).end(),
$.match(function(obj){return !!obj.fooo;}).then(console.log).end()
);
// Add rules to matcher
const matcher = new FluentFlow.Matcher();
matcher.addRules(builder.rules);
// Match objects
const objects = [
{"bar":2},
{"bar":5},
{"bar":7},
{"fooo":42}
];
objects.forEach(function(obj) {
matcher.matchNext(obj);
});
const FluentFlow = require("fluentflow");
const Fluent = FluentFlow.Fluent;
const $ = Fluent.Matcher().starter;
const objectFluent = Fluent.Object({
// register fields
'bar' : []
});
const currentObject = objectFluent.currentObject;
const lastObject = objectFluent.lastObject;
// Build the rules
const builder = new FluentFlow.Matcher.Builder();
builder.append(
$.match(currentObject.fieldNamed("bar").equals(5)).then(console.log).end(),
$.match(currentObject.fieldNamed("fooo").exists).then(console.log).end()
);
// Add rules to matcher
const matcher = new FluentFlow.Matcher();
matcher.addRules(builder.rules);
// Match objects
const objects = [
{"bar":2},
{"bar":5},
{"bar":7},
{"fooo":42}
];
objects.forEach(function(obj) {
matcher.matchNext(obj);
});
const FluentFlow = require("./modules");
const rules = '[ \
$.match(function(obj){return obj.bar===5;}).then(console.log).end(), \
$.match(function(obj){return !!obj.fooo;}).then(console.log).end() \
]' ;
const matchbox = new FluentFlow.Matchbox(rules);
const objects = [
{"bar":2},
{"bar":5},
{"bar":7},
{"fooo":42}
];
objects.forEach(function(obj) {
matchbox.matchNext(obj);
});
In the simplest case, you register a single matcher function (with match
) which will be executed to check every object. With then
you can specify a callback which will be executed if the rule matched.
$.match(function(object) { // add object checking callback
// do some checks here
return object.tcp && object.tcp.dstport==80; //return true on match
}).then(function(object) {
console.log("Match!",object);
});
Followed by relations can be described using the followedBy
keyword. The callbacks registered with match
or then
will have access to previously matched objects.
$.match(function(object) {
// do some checks here
return object.tcp && object.tcp.dstport==80; //return true on match
}).followedBy.match(function(object, lastobject){
//Do some checks here on object OR lastobject struct
return object.http && object.ip.src==lastobject.ip.dst; //return true on match
}).then(function(obj2, obj1) {
console.log("Match!", obj1, obj2);
});
Used to start phrasing a rule. No options, no special props.
Available Members: match, oneOf
Registers one or multiple functions which will be called to determine if the current "rule" matches. The functions must return true
if the rule "matched". The first passed function will be called first, and the second function will only be called if the first returned true
(and so on).
If a function takes only one parameter, it will receive the current object as argument. If a function takes multiple parameters then the function receives the previous object (and all objects before the previous) as argument as well. In the latter case the function will be called for every combination of the current/last objects.
Performance Hint: If you need access to the last Object (by adding a second parameter), add a function before it (which uses only one parameter) to filter out some of the objects.
A matcher function can also return undefined
and submit the result async by invoking this.next
with a boolean.
Available Members: followedBy, then
Takes one or multiple subchains and only contains with the following "rules" (followedBy
), when one of the passed chain matched.
Example:
$.oneOf( $.match(f1),
$.match(f2).followedBy.match(f3)
).then(cb)
The final callback cb
will only be called if either the first matching function f1
matched, or f2
matched followed by a object matching f3
.
Available Members: followedBy, then
Registers one or multiple function which will be called after the current "rule" has matched.
If the functions take only one parameter, then they will only be called once, with the current object as argument. If the function takes multiple parameters then the function receives all previous objects as well, and will be called for every combination.
Example see below.
Available Members: followedBy, then
Starts describing a new rule, which can only match once the previous rule has matched. The functions registered with match
of the newly created rule will receive the objects that matched in the last rule as 2nd, 3rd, ... parameter.
Example:
$.match(f1).followedBy.match(f2).then(cb)
The final callback cb
will only be called if f1
matched followed by a object matching f2
.
Available Members: match, oneOf
TODO: replace packet/lastPacket stuff in this section with something more generic
Take a look at pdml2flow if you want to use fluentflow with wireshark
Instead of using a callback function in match
you can also use the fluent API to automatically build a such function.
FluentApi | Javascript |
---|---|
packet.fieldNamed("tcp.dstport").equals(80) |
function (packet){return (parseInt(packet.tcp.dstport)==80);} |
packet.fieldNamed("tcp.dstport").exists.and.equals(80) |
function (packet){return (packet && packet.tcp&& typeof(packet.tcp.dstport) != "undefined")&&(parseInt(packet.tcp.dstport)==80);} |
packet.fieldNamed("tcp.dstport").equals(80).or.equals(443) |
`function (packet){return (parseInt(packet.tcp.dstport)==80) |
packet.fieldNamed("tcp.dstport").equals(lastPacket) |
function (packet,lastpacket){return (packet.tcp.dstport==lastpacket.tcp.dstport);} |
packet.fieldNamed("udp.src").exists.and.equals(lastPacket.fieldNamed("tcp.src")) |
function (packet,lastpacket){return (packet && packet.udp&& typeof(packet.udp.src) != "undefined")&&(packet.udp.src==lastpacket.tcp.src);} |
packet.fieldNamed("tcp.dstport").not.equals(lastPacket).or.equals(0) |
`function (packet,lastpacket){return !(packet.tcp.dstport==lastpacket.tcp.dstport) |
packet.fieldNamed("tcp.dstport").between(0,1024) |
function (packet){return (parseInt(packet.tcp.dstport)>0&&parseInt(packet.tcp.dstport)<1024);} |
packet.fieldNamed("tcp.dstport").between(0,lastPacket) |
function (packet,lastpacket){return (parseInt(packet.tcp.dstport)>0&&parseInt(packet.tcp.dstport)<parseInt(lastpacket.tcp.dstport));} |
packet.fieldNamed("http.host").contains("foo") |
function (packet){return (packet.http.host.indexOf("foo")>=0);} |
packet.fieldNamed("http.host").not.matches(/abc\d+/).and.matches(/.*\.ch/) |
function (packet){return !(/abc\d+/.test(packet.http.host))&&(/.*\.ch/.test(packet.http.host));} |
Instead of using fieldNamed("tcp.dstport")
you can also use field.tcp.dstport
. This only works for properties which have been registered (TODO: explain).
$ npm test