Generated banner for Security Detection Rule Management
Generated logo for Security Detection Rule Management

Security Detection Rule Management

ElasticOfficialCreated by elastic

  • Skill
  • Tool integration
  • Security
  • Apache-2.0
  • Low risk
  • Has scripts

From the author

Create, tune, and manage Elastic Security detection rules (SIEM and Endpoint). Use for false positives, exceptions, new coverage, noisy rules, or rule management via Kibana API.
— the description by elastic

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured approach to managing Elastic Security detection rules, including SIEM and Endpoint behavior rules. It enables users to identify noisy rules, investigate false positives, and apply tuning strategies such as adding exceptions, patching queries, or adjusting thresholds. The skill utilizes the Kibana Detection Engine API to perform operations, ensuring consistent rule management and coverage maintenance.

What it does

  • Finds and lists existing detection rules using KQL filters.
  • Analyzes noisy rules to identify false positive patterns.
  • Creates new detection rules with custom queries and MITRE ATT&CK mappings.
  • Patches existing rules to update queries, severity, or risk scores.
  • Adds scoped exceptions to SIEM and Endpoint behavior rules.
  • Validates query syntax locally before applying changes to the cluster.

When to use it

  • When you need to reduce alert fatigue by tuning noisy detection rules.
  • When you need to create new detection rules for emerging threats or coverage gaps.
  • When you need to add specific exceptions for known-legitimate processes or users.
  • When you need to manage rule lifecycle operations like enabling, disabling, or deleting rules via API.

Advantages

  • Automates repetitive rule management tasks via the Kibana API.
  • Provides a consistent workflow for investigating and tuning false positives.
  • Includes validation tools to prevent invalid query syntax in production.
  • Supports both standard SIEM rules and specialized Endpoint behavior exceptions.

Usage examples

“Find the noisiest detection rules from the last 7 days and help me tune one.”
The agent will list the top noisy rules by alert volume and guide you through investigating the alerts to determine the best tuning strategy.
“Add an exception to exclude SCCM from the suspicious PowerShell rule.”
The agent will identify the rule, verify the exception criteria, and apply the exception to the rule to suppress future false positives.
“Create a new detection rule for certutil URL download or decode.”
The agent will validate the provided query and create a new detection rule with the specified severity, risk score, and MITRE tags.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/elastic/security-detection-rule-management/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip security-detection-rule-management.zip -d .agents/skills

Documentation by elastic

Detection Rule Management

Create new detection rules for emerging threats and coverage gaps, and tune existing rules to reduce false positives. All operations use the Kibana Detection Engine API via rule-manager.js.

Execution rules

  • Start executing tools immediately — do not read SKILL.md, browse the workspace, or list files first.

  • Report tool output faithfully. Copy rule IDs, names, alert counts, exception IDs, and error messages exactly as returned by the API. Do not abbreviate rule UUIDs, invent rule names, or round alert counts.

  • When a tool returns an error (rule not found, API failure), report the exact error — do not guess at alternatives.

Prerequisites

Install dependencies before first use from the skills/security directory:

cd skills/security && npm install

Set the required environment variables (or add them to a .env file in the workspace root):

export ELASTICSEARCH_URL="https://your-cluster.es.cloud.example.com:443"
export ELASTICSEARCH_API_KEY="your-api-key"
export KIBANA_URL="https://your-cluster.kb.cloud.example.com:443"
export KIBANA_API_KEY="your-kibana-api-key"

Common multi-step workflows

TaskTools to call (in order)
Tune noisy SIEM rulerule_manager find/noisy-rules → run_query (investigate FPs) → rule_manager patch or add-exception
Add endpoint behavior exceptionfetch_endpoint_rule (get rule definition from GitHub) → add_endpoint_exception (scoped to rule.id)
Create new detection rulerun_query (test query against data) → rule_manager create
Investigate rule alert volumerule_manager get → run_query (query alerts index)

For endpoint behavior rules, always fetch the rule definition first to understand query logic and existing exclusions before adding an exception. For SIEM rules, always investigate alert patterns with run_query before tuning.

Critical: For endpoint behavior rules, always use fetch_endpoint_rule (not shell or direct script calls) to get the rule definition, then use add_endpoint_exception to add the exception. These are dedicated tools — do not invoke the underlying scripts manually.

Workflow: Tune a rule for false positives

Steps 1–2: Identify noisy rules and analyze false positives

Find noisy rules with noisy-rules or find, then get the rule definition and investigate alerts:

node skills/security/detection-rule-management/scripts/rule-manager.js noisy-rules --days 7 --top 20
node skills/security/detection-rule-management/scripts/rule-manager.js find --filter "alert.attributes.name:*Suspicious*" --brief
node skills/security/detection-rule-management/scripts/rule-manager.js get --id <rule_uuid>
node skills/security/alert-triage/scripts/run-query.js "kibana.alert.rule.name:\"<rule_name>\"" --index ".alerts-security.alerts-*" --days 7 --full

Look for patterns: same process/user/host → exception candidate; broad pattern → tighten query; legitimate software → exception; too broad → rewrite or adjust threshold.

Step 3: Choose a tuning strategy

In order of preference:

  1. Add exception — Best for specific known-good processes, users, or hosts. Does not modify the rule query. Use when the rule is correct in general but fires on known-legitimate activity.

  2. Tighten the query — Patch the rule's query to exclude the FP pattern. Best when the false positives stem from the query being too broad.

  3. Adjust threshold / alert suppression — For threshold rules, increase the threshold value. For any rule type, enable alert suppression to reduce duplicate alerts on the same entity.

  4. Reduce risk score / severity — Downgrade the rule's priority if it generates many low-value alerts but still has some detection value.

  5. Disable the rule — Last resort. Only if the rule provides no value or is completely redundant with another rule.

Steps 4–5: Apply tuning, verify, and document

Add exception (single/multi-condition, wildcard via matches):

node skills/security/detection-rule-management/scripts/rule-manager.js add-exception \
  --rule-uuid <rule_uuid> \
  --entries "process.executable:is:C:\\Program Files\\SCCM\\CcmExec.exe" "process.parent.name:is:CcmExec.exe" \
  --name "Exclude SCCM" --comment "FP: SCCM deployment" --tags "tuning:fp" "source:soc" --yes

Patch query, threshold, severity, or disable:

node skills/security/detection-rule-management/scripts/rule-manager.js patch --id <rule_uuid> --query "process.name:powershell.exe AND NOT process.parent.name:CcmExec.exe" --yes
node skills/security/detection-rule-management/scripts/rule-manager.js patch --id <rule_uuid> --max-signals 50 --yes
node skills/security/detection-rule-management/scripts/rule-manager.js patch --id <rule_uuid> --severity low --risk-score 21 --yes
node skills/security/detection-rule-management/scripts/rule-manager.js disable --id <rule_uuid> --yes

Write operations (patch, enable, disable, delete, add-exception, bulk-action) prompt for confirmation by default. Pass --yes to skip the prompt (required when called by an agent).

Verify with rule-manager.js get --id <rule_uuid>. Update triage cases via the case-management skill.


Workflow: Create new detection rule

Steps 1–2: Define the threat, data sources, and fields

Specify MITRE ATT&CK technique(s), required data sources (Endpoint, Network, Cloud), and malicious vs legitimate behavior. Common indexes: logs-endpoint.events.process-*, logs-endpoint.events.network-*, .alerts-security.alerts-*, logs-windows.*, logs-aws.*. Key fields: process.name, process.command_line, process.parent.name, destination.ip, winlog.event_id, event.action. Verify data with run-query.js:

node skills/security/alert-triage/scripts/run-query.js "process.name:certutil.exe" --index "logs-endpoint.events.process-*" --days 30 --size 5
Step 3: Write and test the query

Rule types: query (KQL field matching), eql (event sequences), esql (aggregations), threshold (volume-based), threat_match (IOC correlation), new_terms (first-seen). Test against Elasticsearch before creating:

node skills/security/alert-triage/scripts/run-query.js "process.name:certutil.exe AND process.command_line:(*urlcache* OR *decode*)" \
  --index "logs-endpoint.events.process-*" --days 30

For EQL, use --query-file to avoid shell escaping issues.

Validate query syntax before creating or patching a rule. The validate-query command catches common errors locally — escaped backslashes, mismatched parentheses, unbalanced quotes, and duplicate boolean operators:

node skills/security/detection-rule-management/scripts/rule-manager.js validate-query \
  --query "process.name:taskkill.exe AND process.command_line:(*chrome.exe* OR *msedge.exe*)" --language kuery

The create and patch commands also run validation automatically and reject invalid queries. Pass --skip-validation only if you are certain the query is correct despite triggering a check.

Common KQL syntax mistakes:

  • Escaped forward-slashes — KQL wildcards use plain text. Write */IM chrome.exe*, not *\/IM chrome.exe*.

  • Mismatched parentheses — every ( must have a matching ).

  • Unbalanced quotes — every " must be paired.

  • Duplicate operators — AND AND or OR OR is always an error.

Step 4: Create the rule
node skills/security/detection-rule-management/scripts/rule-manager.js create \
  --name "Certutil URL Download or Decode" \
  --description "Detects certutil.exe used to download files or decode Base64 payloads, a common LOLBin technique." \
  --type query \
  --query "process.name:certutil.exe AND process.command_line:(*urlcache* OR *decode*)" \
  --index "logs-endpoint.events.process-*" \
  --severity medium --risk-score 47 \
  --tags "OS:Windows" "Tactic:Defense Evasion" "Tactic:Command and Control" \
  --false-positives "IT administrators using certutil for legitimate certificate operations" \
  --references "https://attack.mitre.org/techniques/T1140/" \
  --interval 5m --disabled

For complex rules (EQL sequences, MITRE mappings, alert suppression), use create --from-file rule_definition.json and --threat-file. See references/detection-api-reference.md for schema.

Step 5: Monitor and iterate

Monitor alert volume with noisy-rules --days 3 --top 10 and tune false positives as needed.


Workflow: Endpoint behavior rules tuning

Tune Elastic Endpoint behavior rules by adding Endpoint exceptions scoped to specific rules. Endpoint exceptions live in Security → Exceptions → Endpoint Security Exception List, not under individual SIEM rules.

Key principles: Always fetch the rule definition from protections-artifacts first. Always scope exceptions to the rule (rule.id or rule.name). Use full paths over process names. Run the mandatory entity cross-check (Step 4b) before any exception. Simulate impact (Step 5b) and aim for ≥60% noise reduction.

Scripts: fetch-endpoint-rule-from-github.js (get rule TOML by id), add-endpoint-exception.js (add to Endpoint Exception List; rule.id/rule.name required), check-exclusion-best-practices.js.

For the full step-by-step workflow (Steps 1–6), queries, and simulation templates, see references/endpoint-behavior-tuning-workflow.md. For exclusion best practices, see references/endpoint-rule-exclusion-best-practices.md.


Tool reference

rule-manager.js

All commands are run from the workspace root. All output is JSON unless noted.

CommandDescription
findSearch/list rules with optional KQL filter
getGet a rule by --id or --rule-id
createCreate a rule (inline flags or --from-file)
patchPatch specific fields on a rule
enableEnable a rule
disableDisable a rule
deleteDelete a rule
exportExport rules as NDJSON
bulk-actionBulk enable/disable/delete/duplicate/edit
add-exceptionAdd an exception item to a rule
list-exceptionsList items on an exception list
create-shared-listCreate a shared exception list
noisy-rulesFind noisiest rules by alert volume
validate-queryCheck query syntax before create/patch

Endpoint behavior tuning: fetch-endpoint-rule-from-github.js (get rule TOML by id), add-endpoint-exception.js (add to Endpoint Exception List; rule.id/rule.name required), check-exclusion-best-practices.js.

Exception entry format

Pass entries as field:operator:value. Operators: is, is_not, is_one_of, is_not_one_of, exists, does_not_exist, matches, does_not_match. Example: process.name:is:svchost.exe, file.path:matches:C:\\Program Files\\*.

Additional resources

Examples

  • "Find the noisiest detection rules from the last 7 days and help me tune one"

  • "Add an exception to exclude SCCM from the suspicious PowerShell rule"

  • "Create a new detection rule for certutil URL download or decode"

Guidelines

  • Report only tool output. When summarizing results, quote or paraphrase only what the tools returned. Do not invent IDs, hostnames, IPs, scores, process trees, or other details not present in the tool response.

  • Preserve identifiers from the request. If the user provides specific hostnames, agent IDs, case IDs, or other values, use those exact values in tool calls and responses — do not substitute different identifiers.

  • Confirm actions concisely. After executing a tool, confirm what was done using the tool's return data. Do not fabricate internal IDs, metadata, or status details unless they appear in the tool response.

  • Distinguish facts from inference. If you draw conclusions beyond what the tools returned (e.g., suggesting a MITRE technique based on observed behavior), clearly label those as your assessment rather than presenting them as tool output.

  • Start executing tools immediately. Do not read SKILL.md, browse directories, or list files before acting.

  • Report tool output verbatim. Copy rule IDs, names, alert counts, and error messages exactly as returned. Do not abbreviate UUIDs or round numbers.

Production use

  • All write operations (create, patch, enable, disable, delete, add-exception, bulk-action, add-endpoint-exception) prompt for confirmation. Pass --yes or -y to skip when called by an agent.

  • Endpoint exceptions suppress detections globally. Always scope exceptions to a specific rule using rule.id or rule.name in the entries. A broad, unscoped exception can silently reduce detection coverage.

  • Verify environment variables point to the intended cluster before running any script.

  • Use --dry-run with bulk-action to preview impact before executing bulk changes.

Environment variables

VariableRequiredDescription
ELASTICSEARCH_URLYesElasticsearch URL (for noisy-rules aggregation)
ELASTICSEARCH_API_KEYYesElasticsearch API key
KIBANA_URLYesKibana URL (for rules API)
KIBANA_API_KEYYesKibana API key

Files

scripts/rule-manager.js

#!/usr/bin/env node
/**
 * Detection rule management CLI wrapping the Kibana Detection Engine REST API.
 * Supports listing, searching, creating, patching, enabling/disabling rules,
 * and managing rule exceptions for false-positive tuning.
 */

import { readFileSync } from "fs";
import { createInterface } from "readline";
import { kibanaDelete, kibanaFetch, kibanaGet, kibanaPatch, kibanaPost } from "./kibana-client.js";

const RULES_API = "/api/detection_engine/rules";
const EXCEPTIONS_API = "/api/exception_lists";

function promptConfirm(message) {
  const rl = createInterface({ input: process.stdin, output: process.stderr });
  return new Promise((resolve) => {
    rl.question(`${message} [y/N] `, (answer) => {
      rl.close();
      resolve(answer.trim().toLowerCase() === "y");
    });
  });
}

function parseArgs(argv) {
  const result = {};
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i];
    if (arg === "-y") {
      result.yes = true;
    } else if (arg.startsWith("--")) {
      const key = arg.slice(2).replace(/-/g, "_");
      const next = argv[i + 1];
      if (next !== undefined && !next.startsWith("--")) {
        const values = [];
        let j = i + 1;
        while (j < argv.length && !argv[j].startsWith("--")) {
          values.push(argv[j]);
          j++;
        }
        result[key] = values.length === 1 ? values[0] : values;
        i = j - 1;
      } else {
        result[key] = true;
      }
    }
  }
  return result;
}

function parseIntArg(val, def) {
  if (val === undefined || val === null) return def;
  const n = Number(val);
  return Number.isNaN(n) ? def : n;
}

function ensureArray(val) {
  if (val === undefined || val === null) return [];
  return Array.isArray(val) ? val : [val];
}

// ---------------------------------------------------------------------------
// Query syntax validation
// ---------------------------------------------------------------------------

const KQL_SYNTAX_CHECKS = [
  {
    pattern: /\\\//,
    id: "escaped-forward-slash",
    message: "KQL does not use backslash escaping. Use plain text (e.g. */IM chrome.exe* not *\\/IM chrome.exe*).",
  },
  {
    pattern: /(?:^|\s)AND\s+AND(?:\s|$)|(?:^|\s)OR\s+OR(?:\s|$)/,
    id: "duplicate-operator",
    message: "Duplicate boolean operator (AND AND or OR OR).",
  },
  {
    pattern: /(?:^|\s)AND\s+OR(?:\s|$)|(?:^|\s)OR\s+AND(?:\s|$)/,
    id: "conflicting-operators",
    message: "Conflicting adjacent boolean operators (AND OR or OR AND).",
  },
];

const EQL_SYNTAX_CHECKS = [
  {
    pattern: /\\\//,
    id: "escaped-forward-slash",
    message:
      "Unexpected escaped forward-slash. EQL uses double-backslash (\\\\) for literal backslashes in Windows paths.",
  },
  {
    pattern: /\bwhere\b.*\bwhere\b/i,
    id: "duplicate-where",
    message: "Duplicate 'where' clause in the same event filter.",
  },
];

const ESQL_SYNTAX_CHECKS = [
  {
    pattern: /(?:^|\s)AND\s+AND(?:\s|$)|(?:^|\s)OR\s+OR(?:\s|$)/,
    id: "duplicate-operator",
    message: "Duplicate boolean operator (AND AND or OR OR).",
  },
];

function validateQuerySyntax(query, language) {
  const errors = [];

  let checks;
  if (language === "eql") {
    checks = EQL_SYNTAX_CHECKS;
  } else if (language === "esql") {
    checks = ESQL_SYNTAX_CHECKS;
  } else {
    checks = KQL_SYNTAX_CHECKS;
  }

  for (const check of checks) {
    if (check.pattern.test(query)) {
      errors.push({ id: check.id, message: check.message });
    }
  }

  const openParens = (query.match(/\(/g) || []).length;
  const closeParens = (query.match(/\)/g) || []).length;
  if (openParens !== closeParens) {
    errors.push({
      id: "mismatched-parens",
      message: `Mismatched parentheses: ${openParens} open vs ${closeParens} close.`,
    });
  }

  const quoteCount = (query.match(/"/g) || []).length;
  if (quoteCount % 2 !== 0) {
    errors.push({ id: "unbalanced-quotes", message: `Unbalanced double-quotes: ${quoteCount} found (must be even).` });
  }

  return errors;
}

async function validateQuery(args) {
  const query = args.query;
  if (!query) {
    console.error("Error: --query is required");
    process.exit(1);
  }
  const language = args.language || "kuery";
  const errors = validateQuerySyntax(query, language);
  if (errors.length === 0) {
    console.log(JSON.stringify({ valid: true, query, language, errors: [] }, null, 2));
  } else {
    console.log(JSON.stringify({ valid: false, query, language, errors }, null, 2));
    process.exit(1);
  }
}

// ---------------------------------------------------------------------------
// Rules CRUD
// ---------------------------------------------------------------------------

async function findRules(args, space) {
  const params = {
    per_page: parseIntArg(args.per_page, 20),
    page: parseIntArg(args.page, 1),
    sort_field: args.sort_field || "name",
    sort_order: args.sort_order || "asc",
  };
  if (args.filter) params.filter = args.filter;
  if (args.fields) params.fields = args.fields;

  const result = await kibanaGet(`${RULES_API}/_find`, params, space);
  if (args.brief) {
    const rules = result.data || [];
    const brief = rules.map((r) => ({
      id: r.id,
      rule_id: r.rule_id,
      name: r.name,
      type: r.type,
      enabled: r.enabled,
      severity: r.severity,
      risk_score: r.risk_score,
      tags: r.tags || [],
      immutable: r.immutable,
    }));
    console.log(JSON.stringify({ total: result.total || 0, rules: brief }, null, 2));
  } else {
    console.log(JSON.stringify(result, null, 2));
  }
  return result;
}

async function getRule(args, space) {
  const params = {};
  if (args.id) params.id = args.id;
  else if (args.rule_id) params.rule_id = args.rule_id;
  else {
    console.error("Error: provide --id or --rule-id");
    process.exit(1);
  }

  const result = await kibanaGet(RULES_API, params, space);
  console.log(JSON.stringify(result, null, 2));
  return result;
}

async function createRule(args, space) {
  let body;
  if (args.from_file) {
    body = JSON.parse(readFileSync(args.from_file, "utf8"));
  } else {
    body = {
      name: args.name,
      description: args.description || "",
      type: args.type,
      risk_score: parseIntArg(args.risk_score, 50),
      severity: args.severity || "medium",
      enabled: !args.disabled,
    };
    if (args.query) body.query = args.query;
    if (args.language) body.language = args.language;
    else if (args.type === "query" || args.type === "saved_query") body.language = "kuery";
    else if (args.type === "eql") body.language = "eql";
    else if (args.type === "esql") body.language = "esql";
    if (args.index) body.index = ensureArray(args.index);
    if (args.interval) body.interval = args.interval;
    if (args.from_time) body.from = args.from_time;
    if (args.tags) body.tags = ensureArray(args.tags);
    if (args.threat_file) body.threat = JSON.parse(readFileSync(args.threat_file, "utf8"));
    if (args.false_positives) body.false_positives = ensureArray(args.false_positives);
    if (args.references) body.references = ensureArray(args.references);
    if (args.note) body.note = args.note;
    if (args.rule_id) body.rule_id = args.rule_id;
    if (args.max_signals !== undefined) body.max_signals = parseIntArg(args.max_signals, undefined);
    if (args.type === "threshold" && args.threshold_field) {
      body.threshold = {
        field: args.threshold_field,
        value: parseIntArg(args.threshold_value, 1),
      };
    }
  }

  if (body.query && !args.skip_validation) {
    const lang = body.language || "kuery";
    const syntaxErrors = validateQuerySyntax(body.query, lang);
    if (syntaxErrors.length > 0) {
      console.error("Query syntax validation failed:");
      for (const err of syntaxErrors) {
        console.error(`  - [${err.id}] ${err.message}`);
      }
      console.error(`\nQuery: ${body.query}`);
      console.error(`Language: ${lang}`);
      console.error("\nFix the query before creating the rule. Pass --skip-validation to bypass.");
      process.exit(1);
    }
  }

  const result = await kibanaPost(RULES_API, body, space);
  console.log(JSON.stringify(result, null, 2));
  return result;
}

async function patchRule(args, space) {
  const body = {};
  if (args.id) body.id = args.id;
  else if (args.rule_id) body.rule_id = args.rule_id;
  else {
    console.error("Error: provide --id or --rule-id");
    process.exit(1);
  }

  if (args.name) body.name = args.name;
  if (args.description) body.description = args.description;
  if (args.query) body.query = args.query;
  if (args.language) body.language = args.language;
  if (args.index) body.index = ensureArray(args.index);
  if (args.interval) body.interval = args.interval;
  if (args.from_time) body.from = args.from_time;
  if (args.tags) body.tags = ensureArray(args.tags);
  if (args.severity) body.severity = args.severity;
  if (args.risk_score !== undefined) body.risk_score = parseIntArg(args.risk_score, 50);
  if (args.false_positives) body.false_positives = ensureArray(args.false_positives);
  if (args.note) body.note = args.note;
  if (args.max_signals !== undefined) body.max_signals = parseIntArg(args.max_signals, undefined);
  if (args.enabled !== undefined) body.enabled = args.enabled === "true";

  if (body.query && !args.skip_validation) {
    const lang = body.language || args.language || "kuery";
    const syntaxErrors = validateQuerySyntax(body.query, lang);
    if (syntaxErrors.length > 0) {
      console.error("Query syntax validation failed:");
      for (const err of syntaxErrors) {
        console.error(`  - [${err.id}] ${err.message}`);
      }
      console.error(`\nQuery: ${body.query}`);
      console.error(`Language: ${lang}`);
      console.error("\nFix the query before patching the rule. Pass --skip-validation to bypass.");
      process.exit(1);
    }
  }

  const result = await kibanaPatch(RULES_API, body, space);
  console.log(JSON.stringify(result, null, 2));
  return result;
}

async function toggleRule(args, space, enable) {
  const body = { enabled: enable };
  if (args.id) body.id = args.id;
  else if (args.rule_id) body.rule_id = args.rule_id;
  else {
    console.error("Error: provide --id or --rule-id");
    process.exit(1);
  }

  const result = await kibanaPatch(RULES_API, body, space);
  const state = enable ? "enabled" : "disabled";
  console.log(`Rule ${result.name || "?"} is now ${state}`);
  return result;
}

async function deleteRule(args, space) {
  const params = {};
  if (args.id) params.id = args.id;
  else if (args.rule_id) params.rule_id = args.rule_id;
  else {
    console.error("Error: provide --id or --rule-id");
    process.exit(1);
  }

  const qs = new URLSearchParams(params).toString();
  const result = await kibanaDelete(`${RULES_API}?${qs}`, space);
  console.log(JSON.stringify(result, null, 2));
  return result;
}

async function exportRules(args, space) {
  const params = { exclude_export_details: "false" };
  if (args.file_name) params.file_name = args.file_name;

  let body = undefined;
  if (args.rule_ids) {
    const ids = Array.isArray(args.rule_ids) ? args.rule_ids : [args.rule_ids];
    body = { objects: ids.map((rid) => ({ rule_id: rid })) };
  }

  const result = await kibanaFetch(`${RULES_API}/_export`, {
    method: "POST",
    body: body ? JSON.stringify(body) : undefined,
    params,
    space,
  });

  if (!result.success) {
    throw new Error(result.error || `HTTP ${result.status}`);
  }
  console.log(result.data);
  return result.data;
}

async function bulkAction(args, space) {
  const body = { action: args.action };
  if (args.ids) body.ids = Array.isArray(args.ids) ? args.ids : [args.ids];
  else if (args.query) body.query = args.query;
  else body.query = "";

  if (args.action === "edit" && args.edit_file) {
    body.edit = JSON.parse(readFileSync(args.edit_file, "utf8"));
  }

  const path = args.dry_run ? `${RULES_API}/_bulk_action?dry_run=true` : `${RULES_API}/_bulk_action`;
  const result = await kibanaPost(path, body, space);
  console.log(JSON.stringify(result, null, 2));
  return result;
}

// ---------------------------------------------------------------------------
// Rule exceptions (false-positive tuning)
// ---------------------------------------------------------------------------

const OP_MAP = {
  is: ["match", "included"],
  is_not: ["match", "excluded"],
  is_one_of: ["match_any", "included"],
  is_not_one_of: ["match_any", "excluded"],
  exists: ["exists", "included"],
  does_not_exist: ["exists", "excluded"],
  matches: ["wildcard", "included"],
  does_not_match: ["wildcard", "excluded"],
};

function parseExceptionEntry(entryStr) {
  const parts = entryStr.split(":");
  let field, operator, value;
  if (parts.length >= 3) {
    field = parts[0];
    operator = parts[1];
    value = parts.slice(2).join(":");
  } else if (parts.length === 2) {
    field = parts[0];
    value = parts[1];
    operator = "is";
  } else {
    throw new Error(`Invalid entry format '${entryStr}'. Use field:operator:value or field:value`);
  }

  const mapped = OP_MAP[operator];
  if (!mapped) {
    throw new Error(`Unknown operator '${operator}'. Use: ${Object.keys(OP_MAP).join(", ")}`);
  }

  const [entryType, listOperator] = mapped;
  const entry = { field, type: entryType, operator: listOperator };
  if (entryType === "match") entry.value = value;
  else if (entryType === "match_any") entry.value = value.split(",").map((v) => v.trim());
  else if (entryType === "wildcard") entry.value = value;
  return entry;
}

async function addException(args, space) {
  const entries = [];
  const entryStrs = Array.isArray(args.entries) ? args.entries : [args.entries];
  for (const entryStr of entryStrs) {
    entries.push(parseExceptionEntry(entryStr));
  }

  const item = {
    type: "simple",
    name: args.name || `Exception ${crypto.randomUUID().slice(0, 8)}`,
    description: args.description || "",
    entries,
  };
  if (args.tags) item.tags = ensureArray(args.tags);
  if (args.expire) item.expire_time = args.expire;
  if (args.comment) item.comments = [{ comment: args.comment }];

  const body = { items: [item] };
  const result = await kibanaPost(`${RULES_API}/${args.rule_uuid}/exceptions`, body, space);
  console.log(JSON.stringify(result, null, 2));
  return result;
}

async function listExceptions(args, space) {
  const params = {
    list_id: args.list_id,
    namespace_type: args.namespace_type || "single",
    per_page: parseIntArg(args.per_page, 20),
    page: parseIntArg(args.page, 1),
  };
  const result = await kibanaGet(`${EXCEPTIONS_API}/items/_find`, params, space);
  console.log(JSON.stringify(result, null, 2));
  return result;
}

async function createSharedExceptionList(args, space) {
  const body = {
    name: args.name,
    description: args.description || "",
  };
  if (args.tags) body.tags = ensureArray(args.tags);

  const result = await kibanaPost("/api/exceptions/shared", body, space);
  console.log(JSON.stringify(result, null, 2));
  return result;
}

// ---------------------------------------------------------------------------
// Alert volume analysis (for tuning decisions)
// ---------------------------------------------------------------------------

async function noisyRules(args) {
  const { createClient } = await import("./es-client.js");
  const es = createClient();

  const days = parseIntArg(args.days, 7);
  const size = parseIntArg(args.top, 20);

  const query = {
    bool: {
      must: [{ range: { "@timestamp": { gte: `now-${days}d` } } }],
    },
  };
  const aggs = {
    by_rule: {
      terms: {
        field: "kibana.alert.rule.name",
        size,
        order: { _count: "desc" },
      },
      aggs: {
        rule_id: { terms: { field: "kibana.alert.rule.uuid", size: 1 } },
        severity: { terms: { field: "kibana.alert.severity", size: 1 } },
        unique_agents: { cardinality: { field: "agent.id" } },
      },
    },
  };

  const result = await es.search({
    index: ".alerts-security.alerts-*",
    query,
    aggs,
    size: 0,
  });

  const buckets = result.aggregations?.by_rule?.buckets || [];
  const output = buckets.map((b) => {
    const ruleIds = b.rule_id?.buckets || [];
    const sevs = b.severity?.buckets || [];
    return {
      rule_name: b.key,
      alert_count: b.doc_count,
      rule_uuid: ruleIds[0]?.key ?? null,
      severity: sevs[0]?.key ?? null,
      unique_agents: b.unique_agents?.value ?? 0,
    };
  });

  console.log(JSON.stringify(output, null, 2));
  return output;
}

// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------

async function main() {
  const cmd = process.argv[2];
  const rawArgs = parseArgs(process.argv.slice(3));
  const space = rawArgs.space;
  const yes = rawArgs.yes === true;

  const requireArg = (name, msg) => {
    const val = rawArgs[name];
    if (val === undefined || val === null || val === "") {
      console.error(`Error: ${msg}`);
      process.exit(1);
    }
    return val;
  };

  try {
    switch (cmd) {
      case "find":
        await findRules(rawArgs, space);
        break;
      case "get":
        if (!rawArgs.id && !rawArgs.rule_id) {
          console.error("Error: provide --id or --rule-id");
          process.exit(1);
        }
        await getRule(rawArgs, space);
        break;
      case "create":
        if (!yes) {
          const label = rawArgs.name || rawArgs.from_file || "new rule";
          const ok = await promptConfirm(`Create rule "${label}"?`);
          if (!ok) {
            console.log("Aborted.");
            process.exit(0);
          }
        }
        await createRule(rawArgs, space);
        break;
      case "patch":
        if (!rawArgs.id && !rawArgs.rule_id) {
          console.error("Error: provide --id or --rule-id");
          process.exit(1);
        }
        if (!yes) {
          const ok = await promptConfirm(`Patch rule ${rawArgs.id || rawArgs.rule_id}?`);
          if (!ok) {
            console.log("Aborted.");
            process.exit(0);
          }
        }
        await patchRule(rawArgs, space);
        break;
      case "enable":
        if (!rawArgs.id && !rawArgs.rule_id) {
          console.error("Error: provide --id or --rule-id");
          process.exit(1);
        }
        if (!yes) {
          const ok = await promptConfirm(`Enable rule ${rawArgs.id || rawArgs.rule_id}?`);
          if (!ok) {
            console.log("Aborted.");
            process.exit(0);
          }
        }
        await toggleRule(rawArgs, space, true);
        break;
      case "disable":
        if (!rawArgs.id && !rawArgs.rule_id) {
          console.error("Error: provide --id or --rule-id");
          process.exit(1);
        }
        if (!yes) {
          const ok = await promptConfirm(`Disable rule ${rawArgs.id || rawArgs.rule_id}?`);
          if (!ok) {
            console.log("Aborted.");
            process.exit(0);
          }
        }
        await toggleRule(rawArgs, space, false);
        break;
      case "delete":
        if (!rawArgs.id && !rawArgs.rule_id) {
          console.error("Error: provide --id or --rule-id");
          process.exit(1);
        }
        if (!yes) {
          const ok = await promptConfirm(`Delete rule ${rawArgs.id || rawArgs.rule_id}? This cannot be undone.`);
          if (!ok) {
            console.log("Aborted.");
            process.exit(0);
          }
        }
        await deleteRule(rawArgs, space);
        break;
      case "export":
        await exportRules(rawArgs, space);
        break;
      case "bulk-action":
        requireArg("action", "--action is required");
        if (!yes && !rawArgs.dry_run) {
          const ok = await promptConfirm(`Execute bulk ${rawArgs.action}?`);
          if (!ok) {
            console.log("Aborted.");
            process.exit(0);
          }
        }
        await bulkAction(rawArgs, space);
        break;
      case "add-exception":
        requireArg("rule_uuid", "--rule-uuid is required");
        requireArg("entries", "--entries is required");
        if (!yes) {
          const ok = await promptConfirm(`Add exception to rule ${rawArgs.rule_uuid}?`);
          if (!ok) {
            console.log("Aborted.");
            process.exit(0);
          }
        }
        await addException(rawArgs, space);
        break;
      case "list-exceptions":
        requireArg("list_id", "--list-id is required");
        await listExceptions(rawArgs, space);
        break;
      case "create-shared-list":
        requireArg("name", "--name is required");
        if (!yes) {
          const ok = await promptConfirm(`Create shared exception list "${rawArgs.name}"?`);
          if (!ok) {
            console.log("Aborted.");
            process.exit(0);
          }
        }
        await createSharedExceptionList(rawArgs, space);
        break;
      case "noisy-rules":
        await noisyRules(rawArgs);
        break;
      case "validate-query":
        await validateQuery(rawArgs);
        break;
      default:
        console.error(
          "Error: unknown command. Use: find, get, create, patch, enable, disable, delete, export, bulk-action, add-exception, list-exceptions, create-shared-list, noisy-rules, validate-query",
        );
        process.exit(1);
    }
  } catch (err) {
    console.error(`Error: ${err.message}`);
    process.exit(1);
  }
}

main().catch((err) => {
  console.error(`Error: ${err.message}`);
  process.exit(1);
});

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an API key, Needs Node.js

The author says: Requires Node.js 22+, network access to Kibana and Elasticsearch. Environment variables: KIBANA_URL plus KIBANA_API_KEY or KIBANA_USERNAME/KIBANA_PASSWORD; ELASTICSEARCH_URL or ELASTICSEARCH_CLOUD_ID plus ELASTICSEARCH_API_KEY or ELASTICSEARCH_USERNAME/ELASTICSEARCH_PASSWORD.

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
elastic
Publisher
Elastic (Official)
Source
elastic/agent-skills · skills/security/detection-rule-management · commit 09d879d
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced
Also distributed in
elastic/agent-skills

AutoGineer is not affiliated with or endorsed by elastic or Elastic.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download security-detection-rule-management.zip