') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - ancat/equation: Equation exposes a minimal environment to allow safe execution of Ruby code represented via a custom expression language. · GitHub
Skip to content

Repository files navigation

Equation [WIP]

A rules engine for your Ruby app! Use a constrained and relatively safe language to express logic for your Ruby app, without having to write Ruby. This allows you to use text (e.g. in a configuration file or database) to store logic in a way that can be updated independently from your code faster than it takes for a deploy without opening extra security vulnerabilities.

Use cases include:

  • writing rules to describe HTTP traffic that then gets dropped, like a WAF
  • writing policies to express authorization logic
  • etc

Modeled loosely after Symfony Expression Language.

Demos

Take a look at the examples directory for some demonstrations of Equation's capabilities. The main example is a web application firewall for Rails; rules can be managed from any ActiveStorage compatible backend (e.g. filesystem, Amazon AWS, etc) and updated independently of application code, allowing for faster and more expressive blocking logic without having to wait for a deploy to go through.

Example

In this example, we'll use a rule to determine whether a request should be dropped or not. While the rule here is hardcoded into the program, it could just as easily be pulled from a database, some redis cache, etc instead. Rules can also be cached, saving you an extra parsing step.

require'equation'# set up the execution environment and give it access to the rails request objectengine=EquationEngine.new(default: {age: 12,username: "OMAR",request: request})suspicious_request=engine.eval(rule: '$request.path == "/api/login" && $request.remote_ip == "1.2.3.4" && $username == "OMAR"')ifsuspicious_request# log some things, notify some peopleend

Language Features

Because Equation is modeled after Symfony Expression Language, it supports a lot of the same features. For a more exhaustive list, check out the tests.

Literals

  • Strings: double quotes, e.g. "hello world"
  • Numbers: all treated as floats, e.g. 0, -10, 0.5
  • Arrays: square brackets, e.g. [403, 404] or ["yes", "no", "maybe"]; can be mixed types
  • Booleans: true, false
  • Null: nil

Variables

Variables are only made available to the engine at initialization. For example, given this setup code:

engine=EquationEngine.new(default: {name: "OMAR",age: 12})

These variables and all their properties are accessible from within rules:

$name == "OMAR" # true
$name.length # 4
$name.reverse # RAMO

Methods

Like variables, methods are only made available to the engine at initialization. They can take any number and type of arguments, including variables or return values from other methods.

engine=EquationEngine.new(default: {age: 12},methods: {is_even: ->(n){n%2==0}})

is_even can now be called as follows:

is_even(5) # false
is_even($age) # true

Comparisons

$name == "Dumpling" && $age >= 12
$name in ["Dumpling", "Meatball"] || $age == 12

Development

  • To work on the expression language itself, take a look at lib/equation_grammar.treetop. Equation is built using treetop.
  • Run bundle exec rake build_grammar to generate the corresponding parser Ruby code.
  • Run bundle exec rake spec to run tests.

About

Equation exposes a minimal environment to allow safe execution of Ruby code represented via a custom expression language.

Topics

Resources

Stars

9 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages