{
  "$comment": "detect.htora.dev framework data. One file drives both pieces: the site renders it, the repo CI validates against it. String fields are site copy.",
  "framework": {
    "id": "detect",
    "name": "Detection Lifecycle Framework",
    "version": "0.3.0",
    "date": "2026-09-12",
    "license": "CC BY 4.0",
    "audience": "Detection engineering leaders and practitioners",
    "site": "https://detect.htora.dev",
    "repo": "TBD"
  },
  "stages": [
    {
      "id": "intake",
      "order": 1,
      "name": "Intake",
      "purpose": "Write down a request for a new detection with enough context that it can be scored.",
      "inputs": [
        "a threat report",
        "a finding from an incident",
        "a red team or purple team result",
        "a compliance requirement",
        "a gap on the coverage chart",
        "a replacement for a retired rule"
      ],
      "exit_gate": {
        "statement": "The backlog entry has a number for each of the six scoring inputs, a one-line note saying where each number came from, and a score computed from them.",
        "count": "6 inputs, 6 evidence lines, 1 score"
      },
      "owner": "Detection engineering lead",
      "artifact": "score.yml with stage set to intake",
      "metrics": [
        "intake_to_score_days",
        "backlog_age_days"
      ],
      "common_failure": "Requests arrive as nothing more than an alert name, with no note about the threat behind it, so the team builds them in the order they showed up.",
      "sigma_status": null,
      "ci_jobs": [
        "score"
      ]
    },
    {
      "id": "hypothesis",
      "order": 2,
      "name": "Hypothesis",
      "purpose": "Write one sentence saying what the rule will catch, which log source it needs, and what it will miss.",
      "inputs": [
        "the scored backlog entry",
        "the ATT&CK technique or sub-technique",
        "the log source inventory"
      ],
      "exit_gate": {
        "statement": "The ADS document has a one-sentence hypothesis in the form 'When an attacker does X, log source Y records Z', names the data source, and names one thing the rule cannot see.",
        "count": "1 hypothesis, 1 data source, 1 blind spot"
      },
      "owner": "Detection engineer",
      "artifact": "The Goal, Categorization, Strategy Abstract, and Blind Spots and Assumptions sections of ads.md",
      "metrics": [
        "readiness_at_hypothesis"
      ],
      "common_failure": "The hypothesis describes one tool's fingerprint. When that tool changes, the rule goes quiet.",
      "sigma_status": null,
      "ci_jobs": []
    },
    {
      "id": "research",
      "order": 3,
      "name": "Research",
      "purpose": "Confirm the thing the rule looks for actually appears in your logs, then measure how often it appears during normal business.",
      "inputs": [
        "the hypothesis",
        "sample events from production",
        "a simulated attack (an Atomic Red Team test, or running the technique by hand)"
      ],
      "exit_gate": {
        "statement": "The behavior has been reproduced once in a test environment and the resulting log events saved as test fixtures, and the number of times it shows up during normal business has been counted over 7 days of production data.",
        "count": "1 reproduction, 1 fixture capture, 7-day baseline"
      },
      "owner": "Detection engineer",
      "artifact": "The Technical Context section of ads.md; tests/events.jsonl",
      "metrics": [
        "research_hours_per_rule"
      ],
      "common_failure": "The 7-day count is skipped, so the first week in production is spent tuning out noise the count would have shown in advance.",
      "sigma_status": null,
      "ci_jobs": []
    },
    {
      "id": "build",
      "order": 4,
      "name": "Build",
      "purpose": "Write the rule in Sigma and the tests that prove it.",
      "inputs": [
        "the test fixture events",
        "the Sigma specification",
        "the target registry (which query languages, and the field mapping each one uses)"
      ],
      "exit_gate": {
        "statement": "The rule file passes the lint checks, converts cleanly into every query language listed in the repository's target registry, and has at least one test event it must match and one it must ignore.",
        "count": "1 lint pass, N conversions, 2 fixtures"
      },
      "owner": "Detection engineer",
      "artifact": "rule.yml, tests/events.jsonl, tests/expected.yml",
      "metrics": [
        "percent_with_tests"
      ],
      "common_failure": "The rule is written straight in the SIEM's own query language. It then runs on one product only, its changes cannot be compared against an approved version, and it cannot move when the SIEM changes.",
      "sigma_status": "experimental",
      "ci_jobs": [
        "lint",
        "convert"
      ]
    },
    {
      "id": "validate",
      "order": 5,
      "name": "Validate",
      "purpose": "Prove the rule fires on the events it should catch, stays silent on the ones it should ignore, and that its converted query still matches the version a reviewer last approved.",
      "inputs": [
        "the converted queries",
        "a running instance of each engine (in memory for SQL, a container for the rest)",
        "the approved copies of the converted queries (golden files)"
      ],
      "exit_gate": {
        "statement": "The test events run through a real instance of every engine that has one, and each returns exactly the expected results; every converted query is identical to the approved copy, or the difference has been explained and accepted; and a second engineer has approved the change.",
        "count": "N engine passes, N golden diffs, 1 approval"
      },
      "owner": "Second detection engineer (reviewer)",
      "artifact": "Passing CI run; approved pull request",
      "metrics": [
        "first_pass_validation_rate",
        "review_turnaround_hours"
      ],
      "common_failure": "Whenever the conversion output changes, someone overwrites the approved copy to match it. The comparison then never fails, and never catches anything.",
      "sigma_status": "test",
      "ci_jobs": [
        "lint",
        "convert",
        "replay (one leg per engine)",
        "golden-diff",
        "review"
      ]
    },
    {
      "id": "deploy",
      "order": 6,
      "name": "Deploy",
      "purpose": "Put the rule into production with alerting switched off, run it over the last 7 days of real data, and only then switch alerting on.",
      "inputs": [
        "the approved pull request",
        "Splunk ACS credentials",
        "Sentinel or Defender API credentials",
        "the 7-day alert threshold (alert_volume_threshold_7d) from score.yml"
      ],
      "exit_gate": {
        "statement": "The rule is live with its alert action off; running it over the last 7 days returned no more alerts than the threshold recorded in score.yml; and the alert action has then been switched on.",
        "count": "1 seven-day replay, 1 threshold check, 1 enable"
      },
      "owner": "Detection engineering lead",
      "artifact": "A deploy record: version, date, SIEM, and the alert count from the 7-day run",
      "metrics": [
        "deploy_lead_time_days",
        "post_deploy_replay_failures"
      ],
      "common_failure": "Alerting is switched on before the 7-day check, and the SOC pays for the noise in triage time.",
      "sigma_status": "stable",
      "ci_jobs": [
        "deploy",
        "post-deploy-replay",
        "enable"
      ]
    },
    {
      "id": "tune",
      "order": 7,
      "name": "Tune",
      "purpose": "Keep the share of true alerts above the agreed floor as the environment changes.",
      "inputs": [
        "alert outcomes (true positive, benign true positive, false positive)",
        "analyst feedback",
        "notices of environment changes"
      ],
      "exit_gate": {
        "statement": "Over the last 30 days, the share of the rule's alerts that were true positives is at or above its floor, and every tuning change went through a pull request and the validate gate.",
        "count": "1 precision check per 30 days, 0 changes outside the repo"
      },
      "owner": "Detection engineer with the SOC lead",
      "artifact": "A tuning pull request that cites the alert ids that prompted it",
      "metrics": [
        "precision_30d",
        "triage_minutes_median",
        "minutes_per_true_positive"
      ],
      "common_failure": "Tuning is done by adding exclusions in the SIEM's own interface, so the rule in the repo no longer matches the rule running in production.",
      "sigma_status": "stable",
      "ci_jobs": [
        "precision-check (scheduled)"
      ]
    },
    {
      "id": "retire",
      "order": 8,
      "name": "Retire",
      "purpose": "Remove rules whose alerts cost more analyst time than they are worth.",
      "inputs": [
        "the 30-day true-positive share (precision_30d)",
        "true positives over the last 180 days",
        "whether the log source is still arriving",
        "whether the technique or the asset still applies"
      ],
      "exit_gate": {
        "statement": "The rule's status is set to deprecated, it is switched off in production, a retirement note records why, and any replacement has been entered at intake with a link to that note.",
        "count": "1 status change, 1 disable, 1 record, 0 or 1 replacement"
      },
      "owner": "Detection engineering lead",
      "artifact": "A retirement note in ads.md: reason, date, and the id of the replacement rule",
      "metrics": [
        "rules_retired_per_quarter",
        "rule_debt"
      ],
      "common_failure": "Nothing is ever retired, so analysts keep triaging alerts from rules that no one owns or understands anymore.",
      "sigma_status": "deprecated",
      "ci_jobs": [
        "disable"
      ]
    }
  ],
  "lines": [
    {
      "id": "backlog",
      "name": "Backlog line",
      "color": "#3A6EA5",
      "stages": [
        "intake",
        "hypothesis",
        "research"
      ]
    },
    {
      "id": "build",
      "name": "Build line",
      "color": "#D08A2E",
      "stages": [
        "build",
        "validate"
      ]
    },
    {
      "id": "live",
      "name": "Live line",
      "color": "#2F8F6B",
      "stages": [
        "deploy",
        "tune",
        "retire"
      ]
    }
  ],
  "loops": [
    {
      "from": "tune",
      "to": "validate",
      "rule": "Every tuning change is a pull request, so it goes back through validate before it reaches production."
    },
    {
      "from": "retire",
      "to": "intake",
      "rule": "When a rule is retired, its replacement enters intake with a link to the retirement note."
    }
  ],
  "detection_zero": {
    "id": "DET-0000",
    "name": "Log source silence",
    "hypothesis": "When a log source the rules depend on stops sending, the SIEM sees nothing from it for longer than the usual gap between its events.",
    "rule": "Alert when a source in the log inventory has sent nothing for three times its usual gap between events, and never sooner than 1 hour.",
    "why_first": "Every other rule depends on its data arriving. A source covered by this alert is one step closer to a readiness score of 5.",
    "stage_note": "Built once per program, before any scored rule."
  },
  "start_here": {
    "questions": [
      {
        "id": "q1_inventory",
        "text": "Do you have a log source inventory: a list of every source feeding the SIEM, with who owns it, how long it is kept, whether its fields are parsed, and whether an alert fires when it goes quiet?",
        "type": "yes_no"
      },
      {
        "id": "q2_history",
        "text": "Do you have the last 12 months of incidents, each tagged with the ATT&CK techniques the attacker used?",
        "type": "yes_no"
      },
      {
        "id": "q3_hours",
        "text": "How many analyst hours a week go to building and tuning detections?",
        "type": "number"
      }
    ],
    "paths": [
      {
        "id": "p0_no_inventory",
        "when": "q1_inventory = no",
        "steps": [
          "Build the inventory. Five columns are enough: source, owner, days kept, fields parsed yes or no, silence alert yes or no.",
          "Ship DET-0000, the silence alert, for every source in the inventory.",
          "Score the backlog. Expect most readiness scores to land at 3 or 4."
        ]
      },
      {
        "id": "p1_inventory_no_history",
        "when": "q1_inventory = yes and q2_history = no",
        "steps": [
          "Without your own incident history, threat relevance comes from public prevalence lists (MITRE CTID's Top ATT&CK Techniques) and reports about your sector; those are the 3 and 4 on the scale.",
          "Score every candidate whose readiness is 3 or higher.",
          "Ship the top 10 by score."
        ]
      },
      {
        "id": "p2_inventory_and_history",
        "when": "q1_inventory = yes and q2_history = yes",
        "steps": [
          "Techniques seen in your own incidents score 5 on threat relevance.",
          "Score the backlog.",
          "Ship the top 10 by score."
        ]
      }
    ],
    "capacity_rule": "Work on no more rules at once than your weekly hours divided by the hours one rule takes from build through validate. Use 8 hours per rule until you have measured your own number."
  },
  "score_model": {
    "version": "0.1.0",
    "inputs": [
      {
        "id": "threat_relevance",
        "symbol": "T",
        "scale": [
          1,
          5
        ],
        "question": "How recently, and how close to you, has this technique been used?",
        "anchors": {
          "1": "No public reporting in the last 24 months.",
          "2": "Public reporting in the last 24 months, but nothing tying it to your sector.",
          "3": "On MITRE CTID's Top ATT&CK Techniques list, or near the top of a vendor's annual threat report.",
          "4": "Named in reporting about your sector in the last 12 months (an ISAC, a regulator, or a peer).",
          "5": "Seen in your own incidents or red team findings in the last 12 months."
        },
        "evidence_examples": [
          "incident ticket id",
          "ISAC report and section",
          "CTID Top ATT&CK Techniques entry"
        ]
      },
      {
        "id": "impact",
        "symbol": "I",
        "scale": [
          1,
          5
        ],
        "question": "What does the attacker reach if the technique succeeds?",
        "anchors": {
          "1": "Reconnaissance, or a low-consequence action on a single host.",
          "2": "One host compromised, and existing controls contain it.",
          "3": "Data access or code execution on a business-critical system.",
          "4": "A step toward the crown jewels: lateral movement or privilege escalation heading for tier-0 identity (domain admin and the systems that control it), OT, or payment systems.",
          "5": "Direct action on tier-0 identity, an OT safety system, or a payment system."
        },
        "evidence_examples": [
          "attack path id",
          "crown-jewel register entry",
          "architecture diagram reference"
        ]
      },
      {
        "id": "robustness",
        "symbol": "B",
        "scale": [
          1,
          5
        ],
        "question": "How hard is it for an attacker to slip past this rule using the data you have? Score it with the five Summiting the Pyramid levels: 1 matches something the attacker can change in seconds, 5 matches something the technique cannot work without.",
        "anchors": {
          "1": "Matches a throwaway indicator: a file hash, IP address, domain, or filename the attacker can change at will.",
          "2": "Matches one tool the attacker brings along: its strings, its arguments, the files it drops. A different tool walks past.",
          "3": "Matches how a built-in system program is abused: the arguments or behavior of something that was already on the machine.",
          "4": "Matches something most ways of doing the technique share, such as an API call or a sequence of events.",
          "5": "Matches something every way of doing the technique must produce."
        },
        "evidence_examples": [
          "observable named in the hypothesis",
          "Summiting the Pyramid level and reasoning"
        ]
      },
      {
        "id": "telemetry_readiness",
        "symbol": "R",
        "scale": [
          0,
          5
        ],
        "question": "Is the data this rule needs already in the SIEM, and in good enough shape to use?",
        "anchors": {
          "0": "The log source is not collected at all.",
          "1": "Collected, but the event type or field the rule needs is missing.",
          "2": "Collected, but sampled, truncated, or kept for less than 30 days.",
          "3": "Collected in full and kept 30 days or more, but the fields the rule needs are not parsed out.",
          "4": "Collected and parsed, but not enriched with who the user is or what the asset is.",
          "5": "Collected, parsed, enriched, and covered by the silence alert (DET-0000)."
        },
        "evidence_examples": [
          "inventory row",
          "field extraction name",
          "DET-0000 coverage entry"
        ]
      },
      {
        "id": "build_effort",
        "symbol": "E",
        "scale": [
          1,
          5
        ],
        "question": "How many hours of work from writing the hypothesis to passing validation?",
        "anchors": {
          "1": "Under 4 hours: adapting a public rule that already comes with test events.",
          "2": "4 to 8 hours.",
          "3": "8 to 16 hours.",
          "4": "16 to 40 hours: needs a simulated attack or new field parsing.",
          "5": "Over 40 hours: needs a new log source brought into the SIEM."
        },
        "evidence_examples": [
          "estimate with the comparable rule id",
          "ticket estimate"
        ]
      },
      {
        "id": "run_cost",
        "symbol": "C",
        "scale": [
          1,
          5
        ],
        "question": "How many analyst minutes a day will this rule consume? Expected alerts per day multiplied by the typical minutes to triage one.",
        "anchors": {
          "1": "Under 5 analyst minutes a day.",
          "2": "5 to 15 minutes a day.",
          "3": "15 minutes to 1 hour a day.",
          "4": "1 to 3 hours a day.",
          "5": "Over 3 hours a day."
        },
        "evidence_examples": [
          "7-day baseline count from research",
          "triage minutes of the comparable rule"
        ]
      }
    ],
    "formula": {
      "value": "wT*T + wI*I + wB*B",
      "cost": "wE*E + wC*C",
      "score": "(value / cost) * (R / 5)^k",
      "gate": "A readiness of 0 sets the score to 0 and marks the rule blocked: nothing can be built on data that is not there.",
      "rounding": "2 decimals",
      "range_balanced": "0.06 to 7.50 with every weight at 1"
    },
    "presets": {
      "balanced": {
        "wT": 1,
        "wI": 1,
        "wB": 1,
        "wE": 1,
        "wC": 1,
        "k": 1,
        "use_when": "The default. One score for the whole backlog.",
        "failure_mode": "Only as good as the six numbers, so every number needs its evidence line."
      },
      "threat_led": {
        "wT": 2,
        "wI": 1,
        "wB": 1,
        "wE": 1,
        "wC": 1,
        "k": 1,
        "use_when": "You have your own incident history or strong reporting about your sector.",
        "failure_mode": "Ranks by what attackers do, so it can pull effort toward techniques your logs cannot see yet."
      },
      "asset_led": {
        "wT": 1,
        "wI": 2,
        "wB": 1,
        "wE": 1,
        "wC": 1,
        "k": 1,
        "use_when": "You know which systems matter most and how an attacker would reach them.",
        "failure_mode": "Only works with an asset inventory, which most teams lack."
      },
      "telemetry_led": {
        "wT": 1,
        "wI": 1,
        "wB": 1,
        "wE": 1,
        "wC": 1,
        "k": 2,
        "use_when": "A new program that needs early wins from the data already in the SIEM.",
        "failure_mode": "Favors what your logs already show, like searching under the streetlight, and can leave the biggest threats uncovered."
      }
    },
    "mandate": {
      "fields": [
        "framework",
        "requirement",
        "due"
      ],
      "rule": "A rule required by a regulation or an audit carries a mandate with the framework, the requirement number, and a due date. If the due date is within 90 days, it goes to the front of the queue, ordered by date. Everything else is ordered by score, highest first; ties go to the rule with the lower run cost.",
      "failure_mode": "Mandated rules come with budget and deadlines, and often catch auditors better than attackers."
    },
    "evidence_rule": "Every one of the six inputs comes with one line saying where the number came from: an incident id, a report section, a query, a ticket. CI rejects a record missing any of them.",
    "alert_volume_threshold_7d": {
      "default": "One and a half times the alerts expected in a week (1.5 x 7 x expected alerts per day), using the count measured during research.",
      "used_by": "deploy exit gate"
    },
    "precision_floor": {
      "program_cap_minutes_per_true_positive": 120,
      "derived": "A rule's floor is the typical minutes to triage one of its alerts divided by the most analyst minutes the program will spend per true positive (120 by default). A rule that takes 30 minutes to triage must be right at least 25% of the time; one that takes 6 minutes, 5%.",
      "override": "precision_floor field in score.yml"
    },
    "retire_triggers": [
      "Its true-positive share stayed below its floor for two 30-day periods in a row.",
      "It fired at least once in 180 days and was never right.",
      "The log source it needs has been missing for 30 days straight, so its readiness fell to 0.",
      "The technique or the system it protects is gone, for example because the platform was decommissioned."
    ],
    "review_trigger": "It has not fired at all in 365 days. Either the data stopped or the hypothesis was wrong; review it before retiring it."
  },
  "metrics": [
    {
      "id": "intake_to_score_days",
      "definition": "Days between a request entering the backlog and receiving its first score.",
      "unit": "days",
      "aggregation": "median",
      "stage": "intake"
    },
    {
      "id": "backlog_age_days",
      "definition": "How long scored requests have been waiting with no one starting the hypothesis.",
      "unit": "days",
      "aggregation": "median",
      "stage": "intake"
    },
    {
      "id": "readiness_at_hypothesis",
      "definition": "Of the requests that reach hypothesis, the share whose data is already in the SIEM in usable shape (readiness 3 or higher).",
      "unit": "percent",
      "aggregation": "ratio",
      "stage": "hypothesis"
    },
    {
      "id": "research_hours_per_rule",
      "definition": "Hours logged in research for one rule.",
      "unit": "hours",
      "aggregation": "median",
      "stage": "research"
    },
    {
      "id": "percent_with_tests",
      "definition": "Share of rules past the build stage that have both a test event to match and one to ignore.",
      "unit": "percent",
      "aggregation": "ratio",
      "stage": "build"
    },
    {
      "id": "first_pass_validation_rate",
      "definition": "Share of pull requests that pass every validation check on the first run.",
      "unit": "percent",
      "aggregation": "ratio",
      "stage": "validate"
    },
    {
      "id": "review_turnaround_hours",
      "definition": "Hours from a pull request being marked ready to a second engineer approving it.",
      "unit": "hours",
      "aggregation": "median",
      "stage": "validate"
    },
    {
      "id": "deploy_lead_time_days",
      "definition": "Days from a rule being merged to its alert action being switched on.",
      "unit": "days",
      "aggregation": "median",
      "stage": "deploy"
    },
    {
      "id": "post_deploy_replay_failures",
      "definition": "Deploys in the quarter where the 7-day production run produced more alerts than the threshold.",
      "unit": "count",
      "aggregation": "sum per quarter",
      "stage": "deploy"
    },
    {
      "id": "precision_30d",
      "definition": "Of a rule's alerts in the last 30 days, the share that were true positives.",
      "unit": "ratio",
      "aggregation": "per rule; program median",
      "stage": "tune"
    },
    {
      "id": "triage_minutes_median",
      "definition": "Typical analyst minutes spent on one alert from the rule.",
      "unit": "minutes",
      "aggregation": "median",
      "stage": "tune"
    },
    {
      "id": "minutes_per_true_positive",
      "definition": "Analyst minutes spent for each true positive the rule produces: triage minutes divided by precision.",
      "unit": "minutes",
      "aggregation": "per rule; program median",
      "stage": "tune"
    },
    {
      "id": "rule_debt",
      "definition": "Share of live rules that fired at least once in 180 days and were never right.",
      "unit": "percent",
      "aggregation": "ratio",
      "stage": "retire"
    },
    {
      "id": "rules_retired_per_quarter",
      "definition": "Rules set to deprecated in the quarter.",
      "unit": "count",
      "aggregation": "sum",
      "stage": "retire"
    },
    {
      "id": "robustness_weighted_coverage",
      "definition": "For each ATT&CK tactic, each live rule counts as its robustness score divided by 5 (a level-5 rule counts as one whole rule, a level-1 rule as one fifth), summed and shown beside the plain rule count. A tactic covered by many easily evaded rules scores low here.",
      "unit": "ratio",
      "aggregation": "per tactic",
      "stage": "program"
    }
  ],
  "sigma_status_map": {
    "build": "experimental",
    "validate": "test",
    "deploy": "stable",
    "tune": "stable",
    "retire": "deprecated",
    "rule": "A rule's stage can be read from the status field in its Sigma file. Intake, hypothesis, and research have no rule file yet."
  },
  "crosswalk": {
    "used": [
      {
        "id": "ads",
        "name": "Palantir Alerting and Detection Strategy Framework",
        "used_in": "ads.md section names per rule",
        "sections": [
          "Goal",
          "Categorization",
          "Strategy Abstract",
          "Technical Context",
          "Blind Spots and Assumptions",
          "False Positives",
          "Validation",
          "Priority",
          "Response",
          "Additional Resources"
        ]
      },
      {
        "id": "sigma_status",
        "name": "Sigma rule status field",
        "used_in": "stage marker on every rule file; the Sigma rule itself is the single source each query language is generated from",
        "values": [
          "experimental",
          "test",
          "stable",
          "deprecated",
          "unsupported"
        ]
      },
      {
        "id": "summiting_the_pyramid",
        "name": "MITRE CTID Summiting the Pyramid",
        "used_in": "robustness input, 5 levels (anchor wording paraphrased)"
      },
      {
        "id": "attack",
        "name": "MITRE ATT&CK",
        "used_in": "technique field in score.yml, validated by lint; tactic derived by metrics.py from the pinned map",
        "pin": {
          "enterprise": "19.2",
          "ics": "19.2",
          "file": "attack/technique-tactics.json (repo), regenerated by tools/attack_pin.py",
          "pinned_on": "2026-09-12"
        }
      },
      {
        "id": "ctid_top_attack_techniques",
        "name": "MITRE CTID Top ATT&CK Techniques",
        "used_in": "threat_relevance anchor 3"
      },
      {
        "id": "wsjf",
        "name": "SAFe Weighted Shortest Job First",
        "used_in": "formula shape: value over cost"
      }
    ],
    "pin_and_verify_before_release": [
      {
        "id": "attack",
        "todo": "re-run tools/attack_pin.py on each ATT&CK release and review the map diff; v19.2 renamed Defense Evasion to Stealth and added Defense Impairment, and ICS techniques now include T16xx ids"
      },
      {
        "id": "summiting_the_pyramid",
        "todo": "verify level wording and current version against the CTID primary source"
      },
      {
        "id": "ctid_top_attack_techniques",
        "todo": "verify current list and calculator inputs against the CTID primary source"
      },
      {
        "id": "sigma_status",
        "todo": "verify the status value list against the current Sigma specification"
      },
      {
        "id": "detection_engineering_maturity_matrix",
        "todo": "map stages to dimensions from the primary source; unmapped until verified"
      },
      {
        "id": "acsc_cisa_priority_logs_2025",
        "todo": "map the 14 source categories to the telemetry inventory template"
      }
    ]
  }
}
