ti2dac 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ti2dac-0.1.0/LICENSE +21 -0
- ti2dac-0.1.0/PKG-INFO +180 -0
- ti2dac-0.1.0/README.md +117 -0
- ti2dac-0.1.0/pyproject.toml +86 -0
- ti2dac-0.1.0/setup.cfg +4 -0
- ti2dac-0.1.0/src/ti2dac/__init__.py +3 -0
- ti2dac-0.1.0/src/ti2dac/aistatus.py +169 -0
- ti2dac-0.1.0/src/ti2dac/attack.py +169 -0
- ti2dac-0.1.0/src/ti2dac/chains.py +211 -0
- ti2dac-0.1.0/src/ti2dac/cli.py +951 -0
- ti2dac-0.1.0/src/ti2dac/clients.py +149 -0
- ti2dac-0.1.0/src/ti2dac/condense.py +228 -0
- ti2dac-0.1.0/src/ti2dac/config.py +320 -0
- ti2dac-0.1.0/src/ti2dac/correlate.py +97 -0
- ti2dac-0.1.0/src/ti2dac/data/attack_index.json +1 -0
- ti2dac-0.1.0/src/ti2dac/data/detections-ci.yml +42 -0
- ti2dac-0.1.0/src/ti2dac/data/wazuh_parents.json +133 -0
- ti2dac-0.1.0/src/ti2dac/duplicates.py +126 -0
- ti2dac-0.1.0/src/ti2dac/feedback.py +369 -0
- ti2dac-0.1.0/src/ti2dac/health.py +47 -0
- ti2dac-0.1.0/src/ti2dac/ingest.py +325 -0
- ti2dac-0.1.0/src/ti2dac/iocs.py +293 -0
- ti2dac-0.1.0/src/ti2dac/llm.py +598 -0
- ti2dac-0.1.0/src/ti2dac/locks.py +34 -0
- ti2dac-0.1.0/src/ti2dac/modelchoice.py +162 -0
- ti2dac-0.1.0/src/ti2dac/models.py +422 -0
- ti2dac-0.1.0/src/ti2dac/obs.py +131 -0
- ti2dac-0.1.0/src/ti2dac/pipeline.py +1025 -0
- ti2dac-0.1.0/src/ti2dac/prompts.py +183 -0
- ti2dac-0.1.0/src/ti2dac/quality.py +79 -0
- ti2dac-0.1.0/src/ti2dac/redact.py +279 -0
- ti2dac-0.1.0/src/ti2dac/regexes.py +193 -0
- ti2dac-0.1.0/src/ti2dac/render.py +664 -0
- ti2dac-0.1.0/src/ti2dac/replay.py +302 -0
- ti2dac-0.1.0/src/ti2dac/review.py +328 -0
- ti2dac-0.1.0/src/ti2dac/safety.py +213 -0
- ti2dac-0.1.0/src/ti2dac/sigma_tools.py +586 -0
- ti2dac-0.1.0/src/ti2dac/targets.py +384 -0
- ti2dac-0.1.0/src/ti2dac/users.py +99 -0
- ti2dac-0.1.0/src/ti2dac/wazuh.py +301 -0
- ti2dac-0.1.0/src/ti2dac/web/__init__.py +0 -0
- ti2dac-0.1.0/src/ti2dac/web/app.py +793 -0
- ti2dac-0.1.0/src/ti2dac/web/jobs.py +358 -0
- ti2dac-0.1.0/src/ti2dac/web/static/app.css +631 -0
- ti2dac-0.1.0/src/ti2dac/web/static/app.js +276 -0
- ti2dac-0.1.0/src/ti2dac/web/static/chevron.svg +1 -0
- ti2dac-0.1.0/src/ti2dac/web/static/favicon.svg +1 -0
- ti2dac-0.1.0/src/ti2dac/web/templates/_ui.html +107 -0
- ti2dac-0.1.0/src/ti2dac/web/templates/ai.html +207 -0
- ti2dac-0.1.0/src/ti2dac/web/templates/api.html +93 -0
- ti2dac-0.1.0/src/ti2dac/web/templates/base.html +74 -0
- ti2dac-0.1.0/src/ti2dac/web/templates/feedback.html +100 -0
- ti2dac-0.1.0/src/ti2dac/web/templates/index.html +196 -0
- ti2dac-0.1.0/src/ti2dac/web/templates/job.html +51 -0
- ti2dac-0.1.0/src/ti2dac/web/templates/run.html +514 -0
- ti2dac-0.1.0/src/ti2dac.egg-info/PKG-INFO +180 -0
- ti2dac-0.1.0/src/ti2dac.egg-info/SOURCES.txt +74 -0
- ti2dac-0.1.0/src/ti2dac.egg-info/dependency_links.txt +1 -0
- ti2dac-0.1.0/src/ti2dac.egg-info/entry_points.txt +2 -0
- ti2dac-0.1.0/src/ti2dac.egg-info/requires.txt +40 -0
- ti2dac-0.1.0/src/ti2dac.egg-info/top_level.txt +1 -0
- ti2dac-0.1.0/tests/test_ai_limits.py +377 -0
- ti2dac-0.1.0/tests/test_ai_review.py +282 -0
- ti2dac-0.1.0/tests/test_deterministic.py +200 -0
- ti2dac-0.1.0/tests/test_edge_cases.py +286 -0
- ti2dac-0.1.0/tests/test_hardening_pipeline.py +471 -0
- ti2dac-0.1.0/tests/test_hardening_web.py +383 -0
- ti2dac-0.1.0/tests/test_incident.py +185 -0
- ti2dac-0.1.0/tests/test_job_workers.py +117 -0
- ti2dac-0.1.0/tests/test_model_choice.py +131 -0
- ti2dac-0.1.0/tests/test_observability.py +167 -0
- ti2dac-0.1.0/tests/test_pipeline.py +348 -0
- ti2dac-0.1.0/tests/test_replay_semantics.py +67 -0
- ti2dac-0.1.0/tests/test_review_2026_11_07.py +292 -0
- ti2dac-0.1.0/tests/test_siem_coverage.py +323 -0
- ti2dac-0.1.0/tests/test_web_keys.py +100 -0
ti2dac-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sazidul0
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ti2dac-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ti2dac
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Threat Intelligence to Detection-as-Code: TI report -> Sigma -> multi-SIEM queries + safe simulation guides
|
|
5
|
+
Author: Sazidul0
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Sazidul0/TI2DaC
|
|
8
|
+
Project-URL: Repository, https://github.com/Sazidul0/TI2DaC
|
|
9
|
+
Project-URL: Issues, https://github.com/Sazidul0/TI2DaC/issues
|
|
10
|
+
Keywords: threat-intelligence,sigma,detection-as-code,mitre-attack,siem,splunk,elastic,microsoft-sentinel,wazuh,blue-team,detection-engineering,llm,security
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: anthropic>=2.3
|
|
26
|
+
Requires-Dist: pydantic>=2.7
|
|
27
|
+
Requires-Dist: pysigma<0.12,>=0.11
|
|
28
|
+
Requires-Dist: pyparsing<3.2,>=3.1
|
|
29
|
+
Requires-Dist: sigma-cli==1.0.6
|
|
30
|
+
Requires-Dist: pySigma-backend-splunk==1.1.3
|
|
31
|
+
Requires-Dist: pySigma-backend-elasticsearch==1.1.6
|
|
32
|
+
Requires-Dist: pySigma-backend-kusto==0.4.5
|
|
33
|
+
Requires-Dist: pySigma-backend-crowdstrike==2.0.1
|
|
34
|
+
Requires-Dist: pySigma-backend-qradar-aql==0.3.2
|
|
35
|
+
Requires-Dist: pySigma-backend-opensearch==1.0.4
|
|
36
|
+
Requires-Dist: pySigma-backend-loki==0.12.4
|
|
37
|
+
Requires-Dist: pySigma-pipeline-sysmon==1.0.4
|
|
38
|
+
Requires-Dist: pySigma-pipeline-windows==1.2.0
|
|
39
|
+
Requires-Dist: pySigma-backend-sqlite==0.2.0
|
|
40
|
+
Requires-Dist: regex==2023.12.25
|
|
41
|
+
Requires-Dist: dateparser==1.2.1
|
|
42
|
+
Requires-Dist: trafilatura>=1.8
|
|
43
|
+
Requires-Dist: beautifulsoup4
|
|
44
|
+
Requires-Dist: lxml
|
|
45
|
+
Requires-Dist: pypdf>=4
|
|
46
|
+
Requires-Dist: python-docx>=1.1
|
|
47
|
+
Requires-Dist: pyyaml>=6
|
|
48
|
+
Requires-Dist: httpx>=0.27
|
|
49
|
+
Requires-Dist: typer>=0.12
|
|
50
|
+
Requires-Dist: fastapi>=0.110
|
|
51
|
+
Requires-Dist: uvicorn>=0.29
|
|
52
|
+
Requires-Dist: jinja2>=3.1
|
|
53
|
+
Requires-Dist: markdown-it-py>=3
|
|
54
|
+
Requires-Dist: python-multipart
|
|
55
|
+
Provides-Extra: dev
|
|
56
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
57
|
+
Provides-Extra: gemini
|
|
58
|
+
Requires-Dist: google-genai>=1.30; extra == "gemini"
|
|
59
|
+
Provides-Extra: evtx
|
|
60
|
+
Requires-Dist: python-evtx>=0.7; extra == "evtx"
|
|
61
|
+
Requires-Dist: defusedxml>=0.7; extra == "evtx"
|
|
62
|
+
Dynamic: license-file
|
|
63
|
+
|
|
64
|
+
<p align="center">
|
|
65
|
+
<img src="https://raw.githubusercontent.com/Sazidul0/TI2DaC/main/docs/ti2dac-banner.svg" alt="ti2dac: Threat Intelligence to Detection-as-Code" width="100%">
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
<p align="center">
|
|
69
|
+
<a href="https://pypi.org/project/ti2dac/"><img src="https://img.shields.io/pypi/v/ti2dac?label=PyPI&color=1f4e79" alt="PyPI version"></a>
|
|
70
|
+
<a href="https://pypi.org/project/ti2dac/"><img src="https://img.shields.io/pypi/pyversions/ti2dac" alt="Python versions"></a>
|
|
71
|
+
<a href="https://github.com/Sazidul0/TI2DaC/actions/workflows/ci.yml"><img src="https://github.com/Sazidul0/TI2DaC/actions/workflows/ci.yml/badge.svg" alt="Build status"></a>
|
|
72
|
+
<a href="https://github.com/Sazidul0/TI2DaC/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Sazidul0/TI2DaC" alt="License"></a>
|
|
73
|
+
<a href="https://github.com/Sazidul0/TI2DaC/stargazers"><img src="https://img.shields.io/github/stars/Sazidul0/TI2DaC?style=flat" alt="GitHub stars"></a>
|
|
74
|
+
<a href="https://github.com/Sazidul0/TI2DaC/issues"><img src="https://img.shields.io/github/issues/Sazidul0/TI2DaC" alt="GitHub issues"></a>
|
|
75
|
+
</p>
|
|
76
|
+
|
|
77
|
+
**Open-source Threat Intelligence to Detection-as-Code engine.** ti2dac turns threat reports and incident write-ups into evidence-checked Sigma rules, native queries for Splunk, Elastic, Sentinel, Defender, CrowdStrike, QRadar, OpenSearch, Loki and Wazuh, and safe simulation and test guides, so blue teams can go from a report to a reviewed detection package in minutes.
|
|
78
|
+
|
|
79
|
+
`sigma` · `detection-as-code` · `threat-intelligence` · `mitre-attack` · `siem` · `splunk` · `elastic` · `microsoft-sentinel` · `wazuh` · `blue-team` · `detection-engineering` · `llm`
|
|
80
|
+
|
|
81
|
+
# ti2dac: Threat Intelligence → Detection-as-Code
|
|
82
|
+
|
|
83
|
+
ti2dac turns a **threat-intelligence report or an internal incident report** into a reviewed, tested detection package:
|
|
84
|
+
|
|
85
|
+
- the attacker behaviours (TTPs) in the report, with evidence quotes checked against the source and verified MITRE ATT&CK mappings
|
|
86
|
+
- one vendor-neutral **Sigma** rule per behaviour (single-event or threshold/correlation), validated with pySigma
|
|
87
|
+
- the same rule as native queries for **Splunk, Elastic, Microsoft Sentinel, Defender XDR, CrowdStrike NG-SIEM, QRadar, OpenSearch and Grafana Loki**, with a per-SIEM field-mapping table
|
|
88
|
+
- a **simulation and test guide** per rule: a safe lab script and its cleanup, the matching Atomic Red Team test, expected events, and positive and false-positive test procedures
|
|
89
|
+
- **unit tests** for each rule, plus local replay of real lab or incident logs, so you know a rule fires *before* it reaches a SIEM
|
|
90
|
+
- **ADS** triage documentation, telemetry requirements, a validation matrix and an ATT&CK Navigator layer
|
|
91
|
+
- a **feedback and review workflow**: error taxonomy, versioned corrections, lessons fed back into the AI, four-eyes approval and an audit log
|
|
92
|
+
|
|
93
|
+
The AI does the reading and the detection writing. Everything that can be deterministic is deterministic: conversion,
|
|
94
|
+
validation, documentation, safety checks and redaction. This keeps token use low and the output reproducible.
|
|
95
|
+
|
|
96
|
+

|
|
97
|
+
|
|
98
|
+
*Steps 1 to 9 run automatically. Steps 10 to 12 are done by people: ti2dac never runs the simulation scripts and never connects to your SIEM.*
|
|
99
|
+
|
|
100
|
+
## Install
|
|
101
|
+
|
|
102
|
+
Requires Python 3.10 or newer.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pip install ti2dac # or: pipx install ti2dac
|
|
106
|
+
pip install "ti2dac[gemini]" # optional: Google Gemini as the AI provider
|
|
107
|
+
pip install "ti2dac[evtx]" # optional: replay Windows .evtx evidence
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Quick start
|
|
111
|
+
|
|
112
|
+
From a source checkout (`git clone https://github.com/Sazidul0/TI2DaC && cd TI2DaC`):
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
uv venv .venv && uv pip install --python .venv/bin/python -e ".[dev]"
|
|
116
|
+
cp .env.example .env # then add ANTHROPIC_API_KEY (see docs/getting-started.md)
|
|
117
|
+
cp ti2dac.example.yaml ti2dac.yaml # then set your domains, customer names, telemetry
|
|
118
|
+
|
|
119
|
+
ti2dac run path/to/report.html # public threat-intel report (file or URL)
|
|
120
|
+
ti2dac run IR-2026-0147.docx --incident --events host-sysmon.evtx # internal incident report + its evidence logs
|
|
121
|
+
ti2dac serve # web UI on http://127.0.0.1:8000
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The two-step flow lets an analyst choose behaviours before any rule-generation tokens are spent:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
ti2dac analyze report.html # 1 AI call: behaviours ranked as B1..Bn
|
|
128
|
+
ti2dac generate <run-id> --select B2,B5 # 1 AI call per chosen behaviour
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Without AI (`--no-ai` or `TI2DAC_NO_AI=1`), the deterministic parts still work: ingestion, IOC extraction,
|
|
132
|
+
cited ATT&CK IDs, and validation, conversion, unit tests and replay of hand-written rules.
|
|
133
|
+
|
|
134
|
+
## Web UI
|
|
135
|
+
|
|
136
|
+
`ti2dac serve` starts a local web UI (bound to `127.0.0.1`) for analysts and reviewers, with logins and roles.
|
|
137
|
+
|
|
138
|
+

|
|
139
|
+
|
|
140
|
+
*The Runs page: upload a report or paste a URL, choose the document type, and follow recent jobs and totals.*
|
|
141
|
+
|
|
142
|
+

|
|
143
|
+
|
|
144
|
+
*A run: behaviours extracted from the report, ranked, with checked evidence quotes and ATT&CK mappings. Tick any behaviour without a rule to generate more.*
|
|
145
|
+
|
|
146
|
+
## Safety at a glance
|
|
147
|
+
|
|
148
|
+
- **Your data stays home.** The AI sees only trimmed, redacted text. Names, hosts, internal IPs, emails and secrets are replaced with placeholders first. TLP:RED documents never go to an AI.
|
|
149
|
+
- **Nothing is executed.** Simulation scripts are linted for destructive or credential-stealing actions, and the tool never runs them. Nothing the model writes is run.
|
|
150
|
+
- **Nothing is trusted unchecked.** Evidence quotes, ATT&CK IDs, Sigma syntax and rule self-tests are verified by code before you see a rule.
|
|
151
|
+
- **People stay in charge.** The tool drafts. A second person (the approver) must approve what the author created, and only approved rules are exported.
|
|
152
|
+
|
|
153
|
+
Generated rules are drafts until you have tested them on real events. See [known limitations](docs/known-limitations.md).
|
|
154
|
+
|
|
155
|
+
## Documentation
|
|
156
|
+
|
|
157
|
+
| Guide | What is in it |
|
|
158
|
+
|---|---|
|
|
159
|
+
| [Getting started](docs/getting-started.md) | AI provider setup, a step-by-step walk-through, supported inputs |
|
|
160
|
+
| [How it works](docs/how-it-works.md) | Architecture in plain terms, the working process, how tokens are kept low |
|
|
161
|
+
| [Quality, safety and testing](docs/quality-and-testing.md) | Safety controls and the three levels of proving a rule works |
|
|
162
|
+
| [SIEM coverage](docs/siem-coverage.md) | The 14 targets, Wazuh, correlations and client profiles |
|
|
163
|
+
| [Review and configuration](docs/governance-and-config.md) | Feedback, four-eyes approval, Git hand-off, `ti2dac.yaml` |
|
|
164
|
+
| [Operations](docs/operations.md) | Monitoring, web UI security, output package layout |
|
|
165
|
+
| [CLI reference](docs/cli-reference.md) | Every command |
|
|
166
|
+
| [Known limitations](docs/known-limitations.md) | What the tool does not do yet |
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
.venv/bin/python -m pytest -q # 254 tests, no network or API key needed
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Dependency pins that matter: pySigma must stay `0.11.x`, because the QRadar backend breaks on 2.x and newer
|
|
175
|
+
SQLite-backend releases silently pull in pySigma 2. `pyparsing` must stay below 3.2, which pySigma 0.11's condition
|
|
176
|
+
parser needs. `pySigma-backend-sqlite` must be `0.2.0`. `regex` and `dateparser` are pinned together (see `pyproject.toml`).
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT. See [LICENSE](LICENSE).
|
ti2dac-0.1.0/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Sazidul0/TI2DaC/main/docs/ti2dac-banner.svg" alt="ti2dac: Threat Intelligence to Detection-as-Code" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://pypi.org/project/ti2dac/"><img src="https://img.shields.io/pypi/v/ti2dac?label=PyPI&color=1f4e79" alt="PyPI version"></a>
|
|
7
|
+
<a href="https://pypi.org/project/ti2dac/"><img src="https://img.shields.io/pypi/pyversions/ti2dac" alt="Python versions"></a>
|
|
8
|
+
<a href="https://github.com/Sazidul0/TI2DaC/actions/workflows/ci.yml"><img src="https://github.com/Sazidul0/TI2DaC/actions/workflows/ci.yml/badge.svg" alt="Build status"></a>
|
|
9
|
+
<a href="https://github.com/Sazidul0/TI2DaC/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Sazidul0/TI2DaC" alt="License"></a>
|
|
10
|
+
<a href="https://github.com/Sazidul0/TI2DaC/stargazers"><img src="https://img.shields.io/github/stars/Sazidul0/TI2DaC?style=flat" alt="GitHub stars"></a>
|
|
11
|
+
<a href="https://github.com/Sazidul0/TI2DaC/issues"><img src="https://img.shields.io/github/issues/Sazidul0/TI2DaC" alt="GitHub issues"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
**Open-source Threat Intelligence to Detection-as-Code engine.** ti2dac turns threat reports and incident write-ups into evidence-checked Sigma rules, native queries for Splunk, Elastic, Sentinel, Defender, CrowdStrike, QRadar, OpenSearch, Loki and Wazuh, and safe simulation and test guides, so blue teams can go from a report to a reviewed detection package in minutes.
|
|
15
|
+
|
|
16
|
+
`sigma` · `detection-as-code` · `threat-intelligence` · `mitre-attack` · `siem` · `splunk` · `elastic` · `microsoft-sentinel` · `wazuh` · `blue-team` · `detection-engineering` · `llm`
|
|
17
|
+
|
|
18
|
+
# ti2dac: Threat Intelligence → Detection-as-Code
|
|
19
|
+
|
|
20
|
+
ti2dac turns a **threat-intelligence report or an internal incident report** into a reviewed, tested detection package:
|
|
21
|
+
|
|
22
|
+
- the attacker behaviours (TTPs) in the report, with evidence quotes checked against the source and verified MITRE ATT&CK mappings
|
|
23
|
+
- one vendor-neutral **Sigma** rule per behaviour (single-event or threshold/correlation), validated with pySigma
|
|
24
|
+
- the same rule as native queries for **Splunk, Elastic, Microsoft Sentinel, Defender XDR, CrowdStrike NG-SIEM, QRadar, OpenSearch and Grafana Loki**, with a per-SIEM field-mapping table
|
|
25
|
+
- a **simulation and test guide** per rule: a safe lab script and its cleanup, the matching Atomic Red Team test, expected events, and positive and false-positive test procedures
|
|
26
|
+
- **unit tests** for each rule, plus local replay of real lab or incident logs, so you know a rule fires *before* it reaches a SIEM
|
|
27
|
+
- **ADS** triage documentation, telemetry requirements, a validation matrix and an ATT&CK Navigator layer
|
|
28
|
+
- a **feedback and review workflow**: error taxonomy, versioned corrections, lessons fed back into the AI, four-eyes approval and an audit log
|
|
29
|
+
|
|
30
|
+
The AI does the reading and the detection writing. Everything that can be deterministic is deterministic: conversion,
|
|
31
|
+
validation, documentation, safety checks and redaction. This keeps token use low and the output reproducible.
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
*Steps 1 to 9 run automatically. Steps 10 to 12 are done by people: ti2dac never runs the simulation scripts and never connects to your SIEM.*
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
Requires Python 3.10 or newer.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install ti2dac # or: pipx install ti2dac
|
|
43
|
+
pip install "ti2dac[gemini]" # optional: Google Gemini as the AI provider
|
|
44
|
+
pip install "ti2dac[evtx]" # optional: replay Windows .evtx evidence
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
From a source checkout (`git clone https://github.com/Sazidul0/TI2DaC && cd TI2DaC`):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv venv .venv && uv pip install --python .venv/bin/python -e ".[dev]"
|
|
53
|
+
cp .env.example .env # then add ANTHROPIC_API_KEY (see docs/getting-started.md)
|
|
54
|
+
cp ti2dac.example.yaml ti2dac.yaml # then set your domains, customer names, telemetry
|
|
55
|
+
|
|
56
|
+
ti2dac run path/to/report.html # public threat-intel report (file or URL)
|
|
57
|
+
ti2dac run IR-2026-0147.docx --incident --events host-sysmon.evtx # internal incident report + its evidence logs
|
|
58
|
+
ti2dac serve # web UI on http://127.0.0.1:8000
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The two-step flow lets an analyst choose behaviours before any rule-generation tokens are spent:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
ti2dac analyze report.html # 1 AI call: behaviours ranked as B1..Bn
|
|
65
|
+
ti2dac generate <run-id> --select B2,B5 # 1 AI call per chosen behaviour
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Without AI (`--no-ai` or `TI2DAC_NO_AI=1`), the deterministic parts still work: ingestion, IOC extraction,
|
|
69
|
+
cited ATT&CK IDs, and validation, conversion, unit tests and replay of hand-written rules.
|
|
70
|
+
|
|
71
|
+
## Web UI
|
|
72
|
+
|
|
73
|
+
`ti2dac serve` starts a local web UI (bound to `127.0.0.1`) for analysts and reviewers, with logins and roles.
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
|
|
77
|
+
*The Runs page: upload a report or paste a URL, choose the document type, and follow recent jobs and totals.*
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+
|
|
81
|
+
*A run: behaviours extracted from the report, ranked, with checked evidence quotes and ATT&CK mappings. Tick any behaviour without a rule to generate more.*
|
|
82
|
+
|
|
83
|
+
## Safety at a glance
|
|
84
|
+
|
|
85
|
+
- **Your data stays home.** The AI sees only trimmed, redacted text. Names, hosts, internal IPs, emails and secrets are replaced with placeholders first. TLP:RED documents never go to an AI.
|
|
86
|
+
- **Nothing is executed.** Simulation scripts are linted for destructive or credential-stealing actions, and the tool never runs them. Nothing the model writes is run.
|
|
87
|
+
- **Nothing is trusted unchecked.** Evidence quotes, ATT&CK IDs, Sigma syntax and rule self-tests are verified by code before you see a rule.
|
|
88
|
+
- **People stay in charge.** The tool drafts. A second person (the approver) must approve what the author created, and only approved rules are exported.
|
|
89
|
+
|
|
90
|
+
Generated rules are drafts until you have tested them on real events. See [known limitations](docs/known-limitations.md).
|
|
91
|
+
|
|
92
|
+
## Documentation
|
|
93
|
+
|
|
94
|
+
| Guide | What is in it |
|
|
95
|
+
|---|---|
|
|
96
|
+
| [Getting started](docs/getting-started.md) | AI provider setup, a step-by-step walk-through, supported inputs |
|
|
97
|
+
| [How it works](docs/how-it-works.md) | Architecture in plain terms, the working process, how tokens are kept low |
|
|
98
|
+
| [Quality, safety and testing](docs/quality-and-testing.md) | Safety controls and the three levels of proving a rule works |
|
|
99
|
+
| [SIEM coverage](docs/siem-coverage.md) | The 14 targets, Wazuh, correlations and client profiles |
|
|
100
|
+
| [Review and configuration](docs/governance-and-config.md) | Feedback, four-eyes approval, Git hand-off, `ti2dac.yaml` |
|
|
101
|
+
| [Operations](docs/operations.md) | Monitoring, web UI security, output package layout |
|
|
102
|
+
| [CLI reference](docs/cli-reference.md) | Every command |
|
|
103
|
+
| [Known limitations](docs/known-limitations.md) | What the tool does not do yet |
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
.venv/bin/python -m pytest -q # 254 tests, no network or API key needed
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Dependency pins that matter: pySigma must stay `0.11.x`, because the QRadar backend breaks on 2.x and newer
|
|
112
|
+
SQLite-backend releases silently pull in pySigma 2. `pyparsing` must stay below 3.2, which pySigma 0.11's condition
|
|
113
|
+
parser needs. `pySigma-backend-sqlite` must be `0.2.0`. `regex` and `dateparser` are pinned together (see `pyproject.toml`).
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ti2dac"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Threat Intelligence to Detection-as-Code: TI report -> Sigma -> multi-SIEM queries + safe simulation guides"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Sazidul0" }]
|
|
14
|
+
keywords = ["threat-intelligence", "sigma", "detection-as-code", "mitre-attack", "siem", "splunk", "elastic", "microsoft-sentinel", "wazuh", "blue-team", "detection-engineering", "llm", "security"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Information Technology",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Environment :: Web Environment",
|
|
21
|
+
"Topic :: Security",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"anthropic>=2.3",
|
|
30
|
+
"pydantic>=2.7",
|
|
31
|
+
"pysigma>=0.11,<0.12",
|
|
32
|
+
"pyparsing>=3.1,<3.2", # pySigma 0.11 condition parser breaks on pyparsing 3.2+
|
|
33
|
+
# Exact pins: the sigma backends are tested together at these versions.
|
|
34
|
+
# Unpinned, pip backtracks through dozens of releases during install.
|
|
35
|
+
"sigma-cli==1.0.6",
|
|
36
|
+
"pySigma-backend-splunk==1.1.3",
|
|
37
|
+
"pySigma-backend-elasticsearch==1.1.6",
|
|
38
|
+
"pySigma-backend-kusto==0.4.5",
|
|
39
|
+
"pySigma-backend-crowdstrike==2.0.1",
|
|
40
|
+
"pySigma-backend-qradar-aql==0.3.2",
|
|
41
|
+
"pySigma-backend-opensearch==1.0.4",
|
|
42
|
+
"pySigma-backend-loki==0.12.4",
|
|
43
|
+
"pySigma-pipeline-sysmon==1.0.4",
|
|
44
|
+
"pySigma-pipeline-windows==1.2.0",
|
|
45
|
+
"pySigma-backend-sqlite==0.2.0", # local rule evaluation (self-tests, replay); later versions need pySigma 2
|
|
46
|
+
# pySigma-backend-qradar-aql needs regex<2024; dateparser>=1.4 (pulled in via trafilatura) needs regex>=2024.
|
|
47
|
+
# These two pins are the combination the project is tested with.
|
|
48
|
+
"regex==2023.12.25",
|
|
49
|
+
"dateparser==1.2.1",
|
|
50
|
+
"trafilatura>=1.8",
|
|
51
|
+
"beautifulsoup4",
|
|
52
|
+
"lxml",
|
|
53
|
+
"pypdf>=4",
|
|
54
|
+
"python-docx>=1.1", # incident reports in Word format
|
|
55
|
+
"pyyaml>=6",
|
|
56
|
+
"httpx>=0.27",
|
|
57
|
+
"typer>=0.12",
|
|
58
|
+
"fastapi>=0.110",
|
|
59
|
+
"uvicorn>=0.29",
|
|
60
|
+
"jinja2>=3.1",
|
|
61
|
+
"markdown-it-py>=3",
|
|
62
|
+
"python-multipart",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[project.optional-dependencies]
|
|
66
|
+
dev = ["pytest>=8"]
|
|
67
|
+
gemini = ["google-genai>=1.30"]
|
|
68
|
+
evtx = ["python-evtx>=0.7", "defusedxml>=0.7"]
|
|
69
|
+
|
|
70
|
+
[project.urls]
|
|
71
|
+
Homepage = "https://github.com/Sazidul0/TI2DaC"
|
|
72
|
+
Repository = "https://github.com/Sazidul0/TI2DaC"
|
|
73
|
+
Issues = "https://github.com/Sazidul0/TI2DaC/issues"
|
|
74
|
+
|
|
75
|
+
[project.scripts]
|
|
76
|
+
ti2dac = "ti2dac.cli:app"
|
|
77
|
+
|
|
78
|
+
[tool.setuptools.packages.find]
|
|
79
|
+
where = ["src"]
|
|
80
|
+
|
|
81
|
+
[tool.setuptools.package-data]
|
|
82
|
+
ti2dac = ["data/*.json", "data/*.yaml", "data/*.yml", "web/templates/*.html", "web/static/*"]
|
|
83
|
+
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
testpaths = ["tests"]
|
|
86
|
+
filterwarnings = ["ignore::SyntaxWarning", "ignore::DeprecationWarning"]
|
ti2dac-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""AI provider health: limit detection, circuit breaker and request counting.
|
|
2
|
+
|
|
3
|
+
Shared by the CLI and the web UI through <state_dir>/ai_status.json, so that once a provider says
|
|
4
|
+
"quota exhausted" (or our own daily cap is reached) NO process sends further requests until the reset
|
|
5
|
+
time. Hammering a provider after it refused is wasteful and can look like abuse to the provider.
|
|
6
|
+
|
|
7
|
+
States per provider:
|
|
8
|
+
ok last call succeeded
|
|
9
|
+
rate_limited short-term limit (per-minute); calls resume after retry_at
|
|
10
|
+
quota_exhausted daily/monthly quota, credit or spend limit; calls blocked until retry_at (or manual reset)
|
|
11
|
+
daily_cap our own `ai.daily_request_limit` reached; resets at local midnight
|
|
12
|
+
auth_error key missing/invalid; blocked until reset
|
|
13
|
+
refused the model declined a request (informational; does not block)
|
|
14
|
+
unavailable provider down/overloaded after retries (informational; does not block)
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
from datetime import datetime, timedelta, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from .locks import file_lock
|
|
24
|
+
|
|
25
|
+
BLOCKING = {"rate_limited", "quota_exhausted", "daily_cap", "auth_error"}
|
|
26
|
+
DAILY_COUNTERS = ("requests_today", "input_tokens_today", "output_tokens_today", "cache_read_tokens_today", "cost_today_usd")
|
|
27
|
+
TOTAL_COUNTERS = ("input_tokens_total", "output_tokens_total", "cost_total_usd")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _now() -> datetime:
|
|
31
|
+
return datetime.now(timezone.utc)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _path(state_dir: str | Path) -> Path:
|
|
35
|
+
return Path(state_dir) / "ai_status.json"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _locked(state_dir: str | Path):
|
|
39
|
+
"""Read-modify-write of ai_status.json: the server, its job workers and the CLI all count requests."""
|
|
40
|
+
return file_lock(Path(state_dir) / "ai_status.lock")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load(state_dir: str | Path) -> dict:
|
|
44
|
+
try:
|
|
45
|
+
return json.loads(_path(state_dir).read_text())
|
|
46
|
+
except (OSError, ValueError):
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _save(state_dir: str | Path, data: dict) -> None:
|
|
51
|
+
p = _path(state_dir)
|
|
52
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
tmp = p.with_suffix(f".{os.getpid()}.tmp")
|
|
54
|
+
tmp.write_text(json.dumps(data, indent=1))
|
|
55
|
+
tmp.replace(p)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def provider_status(state_dir: str | Path, provider: str) -> dict:
|
|
59
|
+
s = load(state_dir).get(provider, {})
|
|
60
|
+
today = datetime.now().date().isoformat()
|
|
61
|
+
if s.get("day") != today:
|
|
62
|
+
s = {**s, "day": today, **{k: 0 for k in DAILY_COUNTERS}}
|
|
63
|
+
if s.get("state") == "daily_cap":
|
|
64
|
+
s["state"] = "ok"
|
|
65
|
+
return s
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def update(state_dir: str | Path, provider: str, **fields) -> dict:
|
|
69
|
+
with _locked(state_dir):
|
|
70
|
+
data = load(state_dir)
|
|
71
|
+
cur = provider_status(state_dir, provider) if provider in data else {
|
|
72
|
+
"day": datetime.now().date().isoformat(), "requests_today": 0}
|
|
73
|
+
cur.update(fields)
|
|
74
|
+
cur["updated_at"] = _now().isoformat(timespec="seconds")
|
|
75
|
+
data[provider] = cur
|
|
76
|
+
_save(state_dir, data)
|
|
77
|
+
return cur
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def count_request(state_dir: str | Path, provider: str) -> int:
|
|
81
|
+
with _locked(state_dir):
|
|
82
|
+
data = load(state_dir)
|
|
83
|
+
cur = provider_status(state_dir, provider)
|
|
84
|
+
cur["requests_today"] = int(cur.get("requests_today", 0)) + 1
|
|
85
|
+
data[provider] = cur
|
|
86
|
+
_save(state_dir, data)
|
|
87
|
+
return cur["requests_today"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def add_usage(state_dir: str | Path, provider: str, input_tokens: int, output_tokens: int,
|
|
91
|
+
cache_read_tokens: int, cost_usd: float) -> None:
|
|
92
|
+
"""Token accounting per provider: today (resets at local midnight) and since the state dir was created."""
|
|
93
|
+
with _locked(state_dir):
|
|
94
|
+
data = load(state_dir)
|
|
95
|
+
cur = provider_status(state_dir, provider)
|
|
96
|
+
for key, val in (("input_tokens", input_tokens), ("output_tokens", output_tokens)):
|
|
97
|
+
cur[f"{key}_today"] = int(cur.get(f"{key}_today", 0)) + val
|
|
98
|
+
cur[f"{key}_total"] = int(cur.get(f"{key}_total", 0)) + val
|
|
99
|
+
cur["cache_read_tokens_today"] = int(cur.get("cache_read_tokens_today", 0)) + cache_read_tokens
|
|
100
|
+
cur["cost_today_usd"] = round(float(cur.get("cost_today_usd", 0)) + cost_usd, 4)
|
|
101
|
+
cur["cost_total_usd"] = round(float(cur.get("cost_total_usd", 0)) + cost_usd, 4)
|
|
102
|
+
data[provider] = cur
|
|
103
|
+
_save(state_dir, data)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def record_ok(state_dir: str | Path, provider: str) -> None:
|
|
107
|
+
update(state_dir, provider, state="ok", message="", retry_at="", last_success=_now().isoformat(timespec="seconds"))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def record_block(state_dir: str | Path, provider: str, state: str, message: str, retry_after_s: float | None) -> None:
|
|
111
|
+
retry_at = (_now() + timedelta(seconds=retry_after_s)).isoformat(timespec="seconds") if retry_after_s else ""
|
|
112
|
+
update(state_dir, provider, state=state, message=message[:500], retry_at=retry_at)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def reset(state_dir: str | Path, provider: str | None = None) -> None:
|
|
116
|
+
with _locked(state_dir):
|
|
117
|
+
data = load(state_dir)
|
|
118
|
+
for name in ([provider] if provider else list(data)):
|
|
119
|
+
if name in data:
|
|
120
|
+
data[name].update(state="ok", message="", retry_at="")
|
|
121
|
+
_save(state_dir, data)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def block_reason(state_dir: str | Path, provider: str, daily_limit: int = 0) -> str:
|
|
125
|
+
"""Why calls to `provider` are blocked right now ('' = allowed). Never contacts the provider."""
|
|
126
|
+
s = provider_status(state_dir, provider)
|
|
127
|
+
state = s.get("state", "ok")
|
|
128
|
+
if daily_limit and int(s.get("requests_today", 0)) >= daily_limit:
|
|
129
|
+
return (f"{provider}: daily request cap reached ({s.get('requests_today')}/{daily_limit}, "
|
|
130
|
+
"ai.daily_request_limit). Resets at local midnight, or raise the cap.")
|
|
131
|
+
if state not in BLOCKING:
|
|
132
|
+
return ""
|
|
133
|
+
retry_at = s.get("retry_at", "")
|
|
134
|
+
if retry_at:
|
|
135
|
+
try:
|
|
136
|
+
if datetime.fromisoformat(retry_at) <= _now():
|
|
137
|
+
return "" # the provider's own reset time has passed
|
|
138
|
+
except ValueError:
|
|
139
|
+
pass
|
|
140
|
+
elif state == "rate_limited":
|
|
141
|
+
return ""
|
|
142
|
+
when = f" Retry after {retry_at} (UTC)." if retry_at else " Fix the cause, then reset with `ti2dac ai-status --reset` or the AI page."
|
|
143
|
+
return f"{provider}: {state.replace('_', ' ')}. {s.get('message', '')}".strip() + when
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def human_wait(retry_at: str) -> str:
|
|
147
|
+
try:
|
|
148
|
+
secs = (datetime.fromisoformat(retry_at) - _now()).total_seconds()
|
|
149
|
+
except (ValueError, TypeError):
|
|
150
|
+
return ""
|
|
151
|
+
if secs <= 0:
|
|
152
|
+
return "now"
|
|
153
|
+
h, m = int(secs // 3600), int(secs % 3600 // 60)
|
|
154
|
+
return f"{h}h {m}m" if h else f"{m}m"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def key_status(provider: str) -> str:
|
|
158
|
+
"""Where credentials would come from (never the key itself)."""
|
|
159
|
+
if provider == "gemini":
|
|
160
|
+
for var in ("GEMINI_API_KEY", "GOOGLE_API_KEY"):
|
|
161
|
+
if os.environ.get(var):
|
|
162
|
+
return f"{var} is set"
|
|
163
|
+
return "not configured"
|
|
164
|
+
for var in ("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"):
|
|
165
|
+
if os.environ.get(var):
|
|
166
|
+
return f"{var} is set"
|
|
167
|
+
if os.environ.get("ANTHROPIC_PROFILE") or (Path.home() / ".config" / "anthropic").is_dir():
|
|
168
|
+
return "`ant auth login` profile found"
|
|
169
|
+
return "not configured"
|