ACRE Coverage Calculator

Calculate your organization’s ACRE (ATT&CK Coverage Ratio Evaluation) score by comparing your custom detection rules against the detectable ATT&CK technique baseline. Produces a weighted coverage score per OS platform, tracks history for trend analysis, and identifies coverage gaps with specific open-source rule recommendations.

Prerequisites

Before running this skill, verify:

  • platform_coverage.json exists in the working directory (generated by the platform-coverage skill)
  • Open-source detection repos are present at the paths below (cloned during the platform-coverage skill):
    • ~/Security-Detections-MCP/detections/sigma/
    • ~/Security-Detections-MCP/detections/security_content/
    • ~/Security-Detections-MCP/detections/detection-rules/
  • Your detection export file is ready (CSV or JSON — see format below)

If platform_coverage.json is missing, run the platform-coverage skill first, then return here.


Input Format

Your detection export must contain one row/object per rule with these fields. Column names are flexible; accepted variants are listed below.

FieldRequiredAccepted namesNotes
Rule identifierYesrule_id, id, name, rule_name, titleUsed in gap output only
Technique IDsYestechnique_ids, techniques, mitre_techniques, attack_techniquesComma-separated string or JSON array; e.g. T1059.001,T1059.003
Platform / OSYesplatform, os, operating_systemMust be Windows, Linux, or macOS (case-insensitive). If your export does not include this field, ask a GenAI tool to infer it from your data_source or log_source column before running this skill.
Creation dateYescreated, created_date, created_at, date_createdISO 8601 preferred (YYYY-MM-DD); also accepts MM/DD/YYYY and YYYY/MM/DD

CSV example:

rule_id,technique_ids,platform,created
det-001,T1059.001,Windows,2025-01-15
det-002,"T1055,T1055.001",Windows,2025-03-20
det-003,T1003.007,Linux,2025-06-01

JSON example:

[
  { "rule_id": "det-001", "technique_ids": ["T1059.001"], "platform": "Windows", "created": "2025-01-15" },
  { "rule_id": "det-002", "technique_ids": ["T1055", "T1055.001"], "platform": "Windows", "created": "2025-03-20" },
  { "rule_id": "det-003", "technique_ids": ["T1003.007"], "platform": "Linux", "created": "2025-06-01" }
]

Step 1: Load Platform Coverage Data

Read platform_coverage.json. This file contains one record per technique × platform pair produced by the platform-coverage skill.

Expected record structure:

{
  "technique_id": "T1059.004",
  "technique_name": "Unix Shell",
  "tactics": ["execution"],
  "platform": "Linux",
  "covered": true,
  "coverage": {
    "Sigma": 12,
    "Splunk": 8,
    "Elasticsearch": 6
  }
}

The tactics values are lowercase ATT&CK phase names (e.g., "persistence", "defense-evasion"). The coverage counts are per-library rule counts for that technique × platform pair.

If the file is absent or malformed, stop and tell the user to run the platform-coverage skill first.


Step 2: Build the Detectable Baseline (Denominator)

For each record in platform_coverage.json, apply the minimum threshold and tactic weight:

Minimum threshold: A technique × platform pair enters the denominator only if:

coverage.Sigma + coverage.Splunk + coverage.Elasticsearch >= 5

This filters out one-offs and ensures the community has meaningfully validated detectability. Techniques below this threshold are excluded entirely (neither covered nor uncovered in ACRE).

Tactic weight: Once a technique × platform pair passes the threshold, assign its weight:

  • If any of the technique’s tactics are in the weighted set → weight = 1.5
  • Otherwise → weight = 1.0

Weighted tactic set (the middle of the attack, where defenders can most disrupt adversaries):

Tactic IDPhase name in ATT&CK data
TA0003persistence
TA0004privilege-escalation
TA0005defense-evasion
TA0006credential-access
TA0007discovery
TA0008lateral-movement
TA0009collection
WEIGHTED_TACTICS = {
    "persistence", "privilege-escalation", "defense-evasion",
    "credential-access", "discovery", "lateral-movement", "collection"
}
 
def get_weight(tactics: list[str]) -> float:
    return 1.5 if any(t in WEIGHTED_TACTICS for t in tactics) else 1.0

The weighted denominator for each platform is the sum of weights across all technique × platform pairs that pass the threshold.


Step 3: Parse User Detections

Write and run a Python script parse_detections.py that accepts the detection file path as a command-line argument.

Parsing logic:

  1. Auto-detect format by file extension (.csv → CSV reader, .jsonjson.load)
  2. Normalize all column names: lowercase, strip whitespace
  3. Map column name variants to canonical names (first match wins per field):
    • rule_idrule_id | id | name | rule_name | title
    • technique_idstechnique_ids | techniques | mitre_techniques | attack_techniques
    • platformplatform | os | operating_system
    • createdcreated | created_date | created_at | date_created
  4. For each row:
    • Expand technique_ids into a list: if string, split on commas and strip whitespace; if array, use as-is. Uppercase all IDs (e.g., t1059.001T1059.001).
    • Normalize platform to title case (windowsWindows). Accept only Windows, Linux, macOS — warn and skip rows with other values.
    • Parse created to a date. Try formats %Y-%m-%d, %m/%d/%Y, %Y/%m/%d in order.
    • If any required field is missing or unparseable, print a warning identifying the row and skip it.
  5. Print a summary: total rows read, rows skipped with reasons.
  6. Write parsed records to parsed_detections.json:
[
  {
    "rule_id": "det-001",
    "technique_ids": ["T1059.001"],
    "platform": "Windows",
    "created": "2025-01-15"
  }
]

Step 4: Calculate ACRE Scores

Write and run a Python script calculate_acre.py that loads the baseline from Step 2 and the parsed detections from Step 3.

Per platform (Windows, Linux, macOS):

  1. Build a set of covered (technique_id, platform) pairs from user detections
  2. Iterate over all technique × platform pairs in the denominator for this platform:
    • Add weight to weighted_total
    • If (technique_id, platform) is in the covered set: add weight to weighted_covered, increment covered_count
  3. acre_score = weighted_covered / weighted_total (return 0.0 if weighted_total == 0)

Output structure (write to acre_current.json):

{
  "date": "2026-04-12",
  "scores": {
    "Windows": {
      "acre_score": 0.47,
      "covered_count": 98,
      "detectable_count": 215,
      "weighted_covered": 132.5,
      "weighted_total": 281.5
    },
    "Linux": {
      "acre_score": 0.31,
      "covered_count": 45,
      "detectable_count": 145,
      "weighted_covered": 55.5,
      "weighted_total": 179.0
    },
    "macOS": {
      "acre_score": 0.24,
      "covered_count": 26,
      "detectable_count": 110,
      "weighted_covered": 36.0,
      "weighted_total": 150.0
    }
  }
}

Step 5: Update History

Load acre_history.json if it exists; otherwise start with { "entries": [] }.

Append today’s scores as a new entry using the structure from acre_current.json. If an entry for today’s date already exists, overwrite it (re-running the skill on the same day updates rather than duplicates).

Save back to acre_history.json:

{
  "entries": [
    {
      "date": "2025-06-01",
      "scores": {
        "Windows": { "acre_score": 0.38, "covered_count": 79, "detectable_count": 215, "weighted_covered": 106.5, "weighted_total": 281.5 },
        "Linux":   { "acre_score": 0.22, "covered_count": 30, "detectable_count": 145, "weighted_covered": 39.5,  "weighted_total": 179.0 },
        "macOS":   { "acre_score": 0.18, "covered_count": 19, "detectable_count": 110, "weighted_covered": 27.0,  "weighted_total": 150.0 }
      }
    },
    {
      "date": "2026-04-12",
      "scores": { ... }
    }
  ]
}

Note: because the denominator can shift as open-source libraries add or remove rules, store detectable_count and weighted_total alongside each entry. This keeps historical scores interpretable even when the denominator changes between runs.


Step 6: Identify Gaps and Find Open-Source Rule Recommendations

For each technique × platform pair in the denominator that is not covered by any user detection, find the specific open-source rules that address it.

Write and run a Python script find_gap_rules.py.

For each gap:

Search each of the three repos using the same parsing logic as the platform-coverage skill. Match rules to the gap’s technique ID and platform.

Sigma (~/Security-Detections-MCP/detections/sigma/):

  • Scan .yml files under rules/ and rules-threat-hunting/ recursively
  • A rule matches if its tags[] contains the technique ID (e.g., attack.t1059.001, case-insensitive) AND its logsource maps to the target platform (use the platform mapping table from the platform-coverage skill)
  • Report: { "file": "<relative path>", "title": "<rule title field>" }

Splunk (~/Security-Detections-MCP/detections/security_content/):

  • Scan .yml files under detections/ recursively; skip deprecated/
  • A rule matches if tags.mitre_attack_id contains the technique ID AND the platform resolves to the target OS (via data_source first, then tags.asset_type)
  • Report: { "file": "<relative path>", "name": "<name field>" }

Elastic (~/Security-Detections-MCP/detections/detection-rules/):

  • Scan .toml files under rules/ recursively; skip rules/_deprecated/
  • A rule matches if any rule.threat.technique[].id (or subtechnique id) equals the technique ID AND the directory path / OS tags match the target platform
  • Report: { "file": "<relative path>", "name": "<rule.name field>" }

Output — write acre_gaps.json:

{
  "generated": "2026-04-12",
  "gaps": [
    {
      "technique_id": "T1003.001",
      "technique_name": "LSASS Memory",
      "platform": "Windows",
      "tactics": ["credential-access"],
      "weight": 1.5,
      "open_source_rules": {
        "Sigma": [
          { "file": "rules/windows/credential_access/cred_dump_lsass_access.yml", "title": "LSASS Memory Access" },
          { "file": "rules/windows/credential_access/lsass_dump_comsvcs.yml", "title": "LSASS Dump via Comsvcs DLL" }
        ],
        "Splunk": [
          { "file": "detections/endpoint/windows_lsass_memory_dump.yml", "name": "Windows LSASS Memory Dump" }
        ],
        "Elastic": [
          { "file": "rules/windows/credential_access_lsass_memdump_handle_access.toml", "name": "LSASS Memory Dump Handle Access" }
        ]
      }
    }
  ]
}

Sort gaps by:

  1. Weight descending (1.5× weighted-tactic gaps first)
  2. Total open-source rule count descending within each weight tier (most community coverage = highest-confidence, easiest-to-adopt rules first)

Step 7: Produce Output

Console Summary

====================================================
ACRE Score Report — 2026-04-12
====================================================

Windows
  ACRE Score:   0.47  (47%)
  Covered:       98 / 215 detectable techniques
  Weighted:     132.5 / 281.5

Linux
  ACRE Score:   0.31  (31%)
  Covered:       45 / 145 detectable techniques
  Weighted:      55.5 / 179.0

macOS
  ACRE Score:   0.24  (24%)
  Covered:       26 / 110 detectable techniques
  Weighted:      36.0 / 150.0

Detectable threshold: ≥5 open-source rules (Sigma + Splunk + Elastic combined)
Tactic weight (1.5×): persistence, privilege-escalation, defense-evasion,
  credential-access, discovery, lateral-movement, collection
====================================================

ASCII Trend Chart

If acre_history.json contains 2 or more entries, render a terminal line chart. Use W for Windows, L for Linux, M for macOS as data-point markers. For readability, scale the Y-axis to the actual data range rather than always 0.0–1.0. Label x-axis with entry dates (abbreviated YYYY-MM). Connect consecutive same-OS points with . characters when there are 3 or more data points.

Example shape (values are illustrative):

ACRE Score History (weighted)
  0.6 |                          W
  0.5 |               W . . . .
  0.4 |         W . .       L
  0.3 |   W . .   L . . . .   L
  0.2 | L       M . . . . . . . M
  0.1 | M
      +--+--------+--------+--------+--
       2025-06  2025-09  2026-01  2026-04
  W = Windows   L = Linux   M = macOS

If only one history entry exists (first run), skip the chart and print: Run this skill again after adding more detections to see your trend line.

Top Gaps Table

Print the top 20 gaps by priority (1.5× techniques first, then by total OSS rule count descending):

Top Coverage Gaps — Windows
──────────────────────────────────────────────────────────────────────────
Technique   Name                             Weight   Sigma  Splunk  Elastic
──────────────────────────────────────────────────────────────────────────
T1003.001   LSASS Memory                      1.5×      14      8       6
T1055.012   Process Hollowing                 1.5×      11      5       9
T1547.001   Registry Run Keys / Startup       1.5×       9      6       4
T1078       Valid Accounts                    1.5×       7     11       3
...
Full gap list with rule file paths: acre_gaps.json

Repeat this table for Linux and macOS.


Output Files

FileContents
acre_current.jsonScores from this run
acre_history.jsonPersistent dated history; append one entry per run
acre_gaps.jsonPrioritized gap list with open-source rule file paths
parsed_detections.jsonNormalized detection records (intermediate; safe to delete)

Notes and Caveats

Denominator drift: The detectable baseline changes as open-source libraries add or remove rules. When the denominator shifts, ACRE scores can move even without any detection work. acre_history.json stores the weighted denominator with every entry so trend lines remain interpretable.

One detection = covered: ACRE counts a technique × platform pair as covered if at least one user rule exists for it. Depth of coverage (how many distinct procedures are addressed) is outside ACRE’s current scope — see the Technique Research Report (TRR) for procedure-level measurement.

Subtechniques vs. parents: A rule tagged to a parent technique (e.g., T1059) covers the parent only — it does not automatically propagate to subtechniques (T1059.001, etc.). Tag rules to the most specific applicable technique for accurate scoring.

Platform is required: ACRE requires an explicit OS field on every detection. If your SIEM export omits it, ask a GenAI tool to infer platform from your data_source or log_source field before running this skill.

Importing historical data: To backfill scores from before you started running this skill, manually add entries to acre_history.json following the entry structure in Step 5. Dates must be in YYYY-MM-DD format and entries should be sorted oldest-first for correct chart rendering.