DEV Community

Cover image for Building a Sigma Rule Engine in TypeScript: Detection-as-Code for Log Analysis
Polliog
Polliog

Posted on

Building a Sigma Rule Engine in TypeScript: Detection-as-Code for Log Analysis

When I started building LogWard, I wanted more than just a log viewer. I wanted developers to have the same security detection capabilities that SOC teams use in enterprise SIEMs like Splunk or QRadar.

But how do you implement security detection rules without reinventing the wheel?

Enter Sigma: the open standard for detection rules that works across different log management platforms.

What is Sigma? (And Why You Should Care)

Sigma is like "Snort rules for logs." It's a generic signature format that describes suspicious activity in a platform-agnostic way.

Instead of writing vendor-specific queries, you write a single YAML rule that can be translated to:

  • Splunk SPL
  • Elastic Query DSL
  • Microsoft Sentinel KQL
  • ...and now, SQL for LogWard

Here's a real example from SigmaHQ's repository:

title: Suspicious PowerShell Encoded Command
status: test
description: Detects suspicious encoded PowerShell commands
logsource:
    category: process_creation
    product: windows
detection:
    selection:
        CommandLine|contains:
            - 'powershell'
            - '-enc'
            - '-encodedcommand'
    condition: selection
level: medium
Enter fullscreen mode Exit fullscreen mode

This rule detects when PowerShell is executed with an encoded command (a common malware/ransomware technique).

The Problem: Making Sigma Work with PostgreSQL

Most Sigma tooling (like sigmac or pySigma) focuses on translating rules to Elasticsearch or Splunk. But LogWard uses PostgreSQL + TimescaleDB.

I needed to:

  1. Parse Sigma YAML rules
  2. Translate the detection logic to SQL WHERE clauses
  3. Run these queries efficiently against millions of log rows
  4. Trigger alerts when matches are found

The Implementation: A TypeScript Sigma Engine

I built a Sigma rule engine directly into LogWard's backend (Fastify + TypeScript). Here's the architecture:

1. Parsing Sigma Rules

First, I parse the YAML using js-yaml:

import yaml from 'js-yaml';
import fs from 'fs';

interface SigmaRule {
  title: string;
  description: string;
  logsource: {
    category?: string;
    product?: string;
    service?: string;
  };
  detection: {
    [key: string]: any;
    condition: string;
  };
  level: 'low' | 'medium' | 'high' | 'critical';
  falsepositives?: string[];
}

function loadSigmaRule(filePath: string): SigmaRule {
  const content = fs.readFileSync(filePath, 'utf8');
  return yaml.load(content) as SigmaRule;
}
Enter fullscreen mode Exit fullscreen mode

2. Translating Detection Logic to SQL

This is the tricky part. Sigma's detection field can be complex. Let me show you a simplified version:

class SigmaToSQL {
  translateDetection(detection: any, condition: string): string {
    // Parse the condition (e.g., "selection" or "selection and not filter")
    const clauses: string[] = [];

    for (const [key, value] of Object.entries(detection)) {
      if (key === 'condition') continue;

      // Handle different field modifiers (contains, startswith, endswith, etc.)
      const sqlClause = this.buildClause(key, value);
      clauses.push(sqlClause);
    }

    // Combine clauses based on the condition
    return this.combineConditions(clauses, condition);
  }

  private buildClause(detectionName: string, fields: any): string {
    const conditions: string[] = [];

    for (const [field, values] of Object.entries(fields)) {
      // Handle modifiers like "contains", "startswith", etc.
      const [fieldName, modifier] = this.parseFieldModifier(field);

      if (Array.isArray(values)) {
        // Multiple values = OR condition
        const orConditions = values.map(v => 
          this.buildSingleCondition(fieldName, modifier, v)
        );
        conditions.push(`(${orConditions.join(' OR ')})`);
      } else {
        conditions.push(this.buildSingleCondition(fieldName, modifier, values));
      }
    }

    return `(${conditions.join(' AND ')})`;
  }

  private buildSingleCondition(field: string, modifier: string, value: any): string {
    // Map log fields to database columns
    const dbColumn = this.mapFieldToColumn(field);

    switch (modifier) {
      case 'contains':
        return `${dbColumn} ILIKE '%${this.escape(value)}%'`;
      case 'startswith':
        return `${dbColumn} ILIKE '${this.escape(value)}%'`;
      case 'endswith':
        return `${dbColumn} ILIKE '%${this.escape(value)}'`;
      case 'exact':
      default:
        return `${dbColumn} = '${this.escape(value)}'`;
    }
  }

  private parseFieldModifier(field: string): [string, string] {
    if (field.includes('|')) {
      const [fieldName, modifier] = field.split('|');
      return [fieldName, modifier];
    }
    return [field, 'exact'];
  }

  private mapFieldToColumn(sigmaField: string): string {
    // Map Sigma standard fields to LogWard's schema
    const mapping: Record = {
      'CommandLine': 'attributes->\'CommandLine\'',
      'Image': 'attributes->\'Image\'',
      'User': 'attributes->\'User\'',
      // ... more mappings
    };
    return mapping[sigmaField] || `attributes->'${sigmaField}'`;
  }

  private escape(value: string): string {
    // SQL injection prevention
    return value.replace(/'/g, "''");
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Running Detection Queries

Now I can translate a Sigma rule to SQL and run it against the logs:

async function detectThreats(orgId: string, rule: SigmaRule) {
  const translator = new SigmaToSQL();
  const whereClause = translator.translateDetection(
    rule.detection, 
    rule.detection.condition
  );

  // Query logs that match the detection rule
  const query = `
    SELECT id, timestamp, message, attributes, level
    FROM logs
    WHERE org_id = $1
      AND timestamp > NOW() - INTERVAL '1 hour'
      AND (${whereClause})
    ORDER BY timestamp DESC
    LIMIT 100
  `;

  const results = await db.query(query, [orgId]);

  if (results.rows.length > 0) {
    // Trigger alert!
    await triggerAlert(orgId, rule, results.rows);
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Scheduled Detection

I use BullMQ to run Sigma rules on a schedule (every 5 minutes, hourly, etc.):

import { Queue, Worker } from 'bullmq';

const detectionQueue = new Queue('sigma-detection', {
  connection: redis
});

// Schedule rule execution
await detectionQueue.add(
  'run-sigma-rule',
  { ruleId: 'suspicious-powershell', orgId: 'org-123' },
  { repeat: { every: 300000 } } // Every 5 minutes
);

// Worker processes the job
const worker = new Worker('sigma-detection', async (job) => {
  const { ruleId, orgId } = job.data;
  const rule = await loadSigmaRule(ruleId);
  await detectThreats(orgId, rule);
});
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Detecting Crypto Mining

Let's use an actual Sigma rule from the community to detect cryptocurrency mining:

title: Cryptocurrency Mining Indicators
description: Detects common crypto mining processes
logsource:
    category: process_creation
    product: windows
detection:
    selection:
        Image|endswith:
            - '\xmrig.exe'
            - '\cpuminer.exe'
            - '\nheqminer.exe'
    condition: selection
level: high
Enter fullscreen mode Exit fullscreen mode

When translated, this becomes:

SELECT * FROM logs
WHERE org_id = 'org-123'
  AND timestamp > NOW() - INTERVAL '1 hour'
  AND (
    attributes->>'Image' ILIKE '%\xmrig.exe'
    OR attributes->>'Image' ILIKE '%\cpuminer.exe'
    OR attributes->>'Image' ILIKE '%\nheqminer.exe'
  )
Enter fullscreen mode Exit fullscreen mode

If any logs match, LogWard sends an alert via email/webhook.

The Benefits: Detection-as-Code

With Sigma integration, developers get:

  1. Community-Maintained Rules: Access to 4000+ detection rules maintained by the security community
  2. No Vendor Lock-In: Rules work across platforms
  3. Version Control: Store rules in Git alongside your infrastructure code
  4. Customization: Fork and adapt rules to your environment

What's Next for LogWard's Sigma Engine

The current implementation is a working MVP, but I'm planning:

  • Full pySigma Compatibility: Supporting advanced features like aggregation conditions
  • Rule Testing Framework: Simulate attacks against sample logs to validate rules
  • SigmaHQ Integration: One-click import of curated rule sets
  • Visual Rule Builder: GUI for creating Sigma rules without touching YAML

Try It Yourself

The Sigma engine is already in LogWard's codebase. You can:

  1. Clone the repo: git clone https://github.com/logward-dev/logward
  2. Add your Sigma rules to /sigma-rules/
  3. Configure detection schedules in the UI
  4. Get alerts when threats are detected

Or try the hosted version at logward.dev (currently in free alpha).


What do you think? Should every log management tool support Sigma out-of-the-box? Let me know in the comments.

💻 GitHub: https://github.com/logward-dev/logward

🔐 Sigma Rules: https://github.com/SigmaHQ/sigma

🚀 Try LogWard: https://logward.dev

Top comments (0)