| Crates.io | hel |
| lib.rs | hel |
| version | 0.2.0 |
| created_at | 2026-01-20 09:45:41.789893+00 |
| updated_at | 2026-01-21 11:53:07.485735+00 |
| description | HEL — Heuristic Expression Language: a deterministic, auditable expression language & parser, AST, builtin registry and evaluator. |
| homepage | https://github.com/Sing-Security/hel |
| repository | https://github.com/Sing-Security/hel |
| max_upload_size | |
| id | 2056246 |
| size | 238,986 |
Status: OPEN — Apache-2.0
SPDX-License-Identifier: Apache-2.0
HEL provides a simple, high-level API for expression validation and evaluation:
use hel::validate_expression;
// Validate syntax without evaluation
let expr = r#"binary.arch == "x86_64" AND security.nx == false"#;
validate_expression(expr)?; // Returns Ok(()) or detailed parse error
use hel::{evaluate, FactsEvalContext, Value};
// Create evaluation context with facts
let mut ctx = FactsEvalContext::new();
ctx.add_fact("binary.arch", Value::String("x86_64".into()));
ctx.add_fact("security.nx", Value::Bool(false));
// Evaluate expression
let expr = r#"binary.arch == "x86_64" AND security.nx == false"#;
let result = evaluate(expr, &ctx)?; // Returns true
HEL supports .hel script files with reusable let bindings:
use hel::{evaluate_script, FactsEvalContext, Value};
let mut ctx = FactsEvalContext::new();
ctx.add_fact("manifest.permissions", Value::List(vec![
Value::String("READ_SMS".into()),
Value::String("SEND_SMS".into()),
]));
ctx.add_fact("binary.entropy", Value::Number(8.0));
let script = r#"
# Define reusable sub-expressions
let has_sms_perms =
manifest.permissions CONTAINS "READ_SMS" AND
manifest.permissions CONTAINS "SEND_SMS"
let has_obfuscation = binary.entropy > 7.5
# Final boolean expression
has_sms_perms AND has_obfuscation
"#;
let result = evaluate_script(script, &ctx)?; // Returns true
validate_expression(expr: &str) -> Result<(), HelError> - validate syntax without evaluationparse_expression(expr: &str) -> Result<Expression, HelError> - parse into ASTparse_script(script: &str) -> Result<Script, HelError> - parse .hel files with let bindingsevaluate(expr: &str, context: &FactsEvalContext) -> Result<bool, HelError> - evaluate with factsevaluate_script(script: &str, context: &FactsEvalContext) -> Result<bool, HelError> - evaluate scripts with let bindingsevaluate_with_resolver() and evaluate_with_context()Null, Bool, String, Number, List, Mapparse_rule(condition: &str) -> AstNode - direct AST constructionAstNode variants: Bool, String, Number, Float, Identifier, Attribute, Comparison, And, Or, ListLiteral, MapLiteral, FunctionCall==, !=, >, >=, <, <=, CONTAINS, INBuiltinsProvider trait and BuiltinsRegistry for namespace-aware function dispatchBuiltinFn type: pure, deterministic functions that map argument Values to a Result<Value, EvalError>CoreBuiltinsProvider included with generic functions (core.len, core.contains, core.upper, core.lower)evaluate_with_trace(condition, resolver, Option<&BuiltinsRegistry>) -> Result<EvalTrace, EvalError>EvalTrace contains deterministic list of AtomTrace entries and sorted list of facts_used()Schema representation (FieldType, TypeDef, FieldDef)PackageManifest (hel-package.toml), SchemaPackage, and PackageRegistryHEL is designed to be embedded in rule engines and security analysis tools. Here's how to integrate HEL into your application:
use hel::{evaluate_script, FactsEvalContext, Value};
use std::fs;
struct MalwareRule {
name: String,
description: String,
script_path: String,
}
fn check_sample(sample: &BinarySample, rules: &[MalwareRule]) -> Vec<String> {
// Build facts from sample
let mut ctx = FactsEvalContext::new();
ctx.add_fact("binary.arch", Value::String(sample.arch.clone().into()));
ctx.add_fact("binary.entropy", Value::Number(sample.entropy));
ctx.add_fact("manifest.permissions", Value::List(
sample.permissions.iter()
.map(|p| Value::String(p.clone().into()))
.collect()
));
ctx.add_fact("strings.count", Value::Number(sample.string_count as f64));
// Evaluate all rules
let mut detections = Vec::new();
for rule in rules {
// Load and evaluate .hel script
let script = fs::read_to_string(&rule.script_path)
.expect("Failed to load rule");
match evaluate_script(&script, &ctx) {
Ok(true) => {
println!("✓ Rule matched: {}", rule.name);
detections.push(rule.name.clone());
}
Ok(false) => {
println!(" Rule did not match: {}", rule.name);
}
Err(e) => {
eprintln!("✗ Rule evaluation error in {}: {}", rule.name, e);
}
}
}
detections
}
struct BinarySample {
arch: String,
entropy: f64,
permissions: Vec<String>,
string_count: usize,
}
android-malware.hel# Check for suspicious SMS permissions
let has_sms_perms =
manifest.permissions CONTAINS "READ_SMS" AND
manifest.permissions CONTAINS "SEND_SMS"
# Check for code obfuscation indicators
let has_obfuscation =
binary.entropy > 7.5 OR
strings.count < 10
# Final detection logic
has_sms_perms AND has_obfuscation
Validation Before Deployment: Always validate rule scripts before loading them:
let script = fs::read_to_string("rule.hel")?;
validate_expression(&script)?; // Catch syntax errors early
Error Handling: Distinguish between parse errors (rule bugs) and evaluation errors (data issues):
match evaluate_script(&script, &ctx) {
Ok(result) => { /* process result */ }
Err(e) if matches!(e.kind, ErrorKind::ParseError) => {
eprintln!("Rule has syntax error: {}", e);
}
Err(e) => {
eprintln!("Evaluation error: {}", e);
}
}
Performance: Parse scripts once and reuse the AST:
let parsed = parse_script(&script)?;
// Store parsed.bindings and parsed.final_expr
// Reuse for multiple evaluations
use hel::parse_rule;
let ast = parse_rule("binary.format == \"elf\" AND security.nx_enabled == true");
// `ast` is an `AstNode` representing the parsed expression
use hel::{evaluate_with_resolver, HelResolver, Value};
struct MyResolver;
impl HelResolver for MyResolver {
fn resolve_attr(&self, object: &str, field: &str) -> Option<Value> {
match (object, field) {
("binary", "format") => Some(Value::String("elf".into())),
("security", "nx_enabled") => Some(Value::Bool(true)),
_ => None,
}
}
}
let resolver = MyResolver;
let result = evaluate_with_resolver(r#"binary.format == "elf""#, &resolver)?;
assert!(result);
use hel::{evaluate_with_trace, HelResolver, builtins::BuiltinsRegistry, builtins::CoreBuiltinsProvider};
let mut registry = BuiltinsRegistry::new();
registry.register(&CoreBuiltinsProvider)?;
struct MyResolver;
impl HelResolver for MyResolver {
fn resolve_attr(&self, object: &str, field: &str) -> Option<hel::Value> { /* ... */ unimplemented!() }
}
let trace = evaluate_with_trace("core.len([1,2,3]) == 3", &MyResolver, Some(®istry))?;
println!("{}", trace.pretty_print()); // deterministic, human-friendly audit trail
Design notes and important details
BTreeMap and lists are iterated stably to ensure deterministic behavior across runs.facts_used() are sorted to make audit logs stable.Result<..., EvalError>. EvalError covers parse errors, type mismatches, unknown attributes, and invalid operations.+, -, *, /) beyond numeric comparisons in the current implementation.BuiltinsRegistry in the evaluation context. Without it, invoking FunctionCall yields an InvalidOperation error.f64 for runtime numbers; integer literal parsing persists u64 in the AST then converts as needed to Value::Number(f64).Documentation and where to look next
src modules to get API-level details:
hel::schema — package manifest, SchemaPackage, schema parsing helpers.hel::builtins — provider/registry API and CoreBuiltinsProvider.hel::trace — trace capture and pretty-print helpers.hel::parse_rule and the AST in src/lib.rs.docs/USAGE.md and docs/SCHEMA.md (examples and schema/package format).src/* demonstrate intended semantics and edge-case behavior (NaN handling, builtins, trace order, package registry collision detection).Contributing
unsafe in public APIs unless strictly necessary and justified with clear documentation.License
BuiltinsProvider.