txt2detection 1.0.9__tar.gz → 1.0.10__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.
Potentially problematic release.
This version of txt2detection might be problematic. Click here for more details.
- {txt2detection-1.0.9 → txt2detection-1.0.10}/.github/workflows/run-tests.yml +4 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/PKG-INFO +1 -1
- {txt2detection-1.0.9 → txt2detection-1.0.10}/pyproject.toml +1 -1
- txt2detection-1.0.10/tests/src/conftest.py +18 -0
- txt2detection-1.0.10/tests/src/test_attack_flow.py +519 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/src/test_bundler.py +6 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/attack_flow.py +1 -3
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/bundler.py +18 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/.env.example +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/.env.markdown +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/.github/workflows/create-release.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/.gitignore +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/LICENSE +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/README.md +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/config/detection_languages.yaml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/docs/README.md +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/docs/txt2detection.png +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/requirements.txt +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/CVE-2024-56520.txt +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/EC2-exfil.txt +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/observables.txt +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-attack-enterprise.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-attack-flow.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-custom-tags.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-existing-related.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-master.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-no-author.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-no-date.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-no-description.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-no-level.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-no-license.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-no-status.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-no-tags.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-no-title.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-observables.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/files/sigma-rule-one-date.yml +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/manual-tests/README.md +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/src/__init__.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/src/requirements.txt +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/src/test_main.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/src/test_main_run_txt2detction.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/src/test_models.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/src/test_observables.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/tests/src/test_utils.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/__init__.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/__main__.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/ai_extractor/__init__.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/ai_extractor/anthropic.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/ai_extractor/base.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/ai_extractor/deepseek.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/ai_extractor/gemini.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/ai_extractor/models.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/ai_extractor/openai.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/ai_extractor/openrouter.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/ai_extractor/prompts.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/ai_extractor/utils.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/credential_checker.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/models.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/observables.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection/utils.py +0 -0
- {txt2detection-1.0.9 → txt2detection-1.0.10}/txt2detection.py +0 -0
|
@@ -33,6 +33,10 @@ jobs:
|
|
|
33
33
|
run: |
|
|
34
34
|
echo > .env
|
|
35
35
|
echo "INPUT_TOKEN_LIMIT=1000" >> .env
|
|
36
|
+
echo "CTIBUTLER_BASE_URL=https://api.ctibutler.com/" >> .env
|
|
37
|
+
echo "CTIBUTLER_API_KEY=${{ secrets.CTIBUTLER_API_KEY }}" >> .env
|
|
38
|
+
echo "VULMATCH_BASE_URL=https://api.vulmatch.com/" >> .env
|
|
39
|
+
echo "VULMATCH_API_KEY=${{ secrets.VULMATCH_API_KEY }}" >> .env
|
|
36
40
|
|
|
37
41
|
- name: Run Tests
|
|
38
42
|
id: run_tests
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: txt2detection
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.10
|
|
4
4
|
Summary: A command line tool that takes a txt file containing threat intelligence and turns it into a detection rule.
|
|
5
5
|
Project-URL: Homepage, https://github.com/muchdogesec/txt2detection
|
|
6
6
|
Project-URL: Issues, https://github.com/muchdogesec/txt2detection/issues
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from txt2detection.bundler import Bundler
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def bundler_instance():
|
|
10
|
+
return Bundler(
|
|
11
|
+
name="Test Report",
|
|
12
|
+
identity=None,
|
|
13
|
+
tlp_level="red",
|
|
14
|
+
description="This is a test report.",
|
|
15
|
+
labels=["tlp.red", "test.test-var"],
|
|
16
|
+
created=datetime(2025, 1, 1),
|
|
17
|
+
report_id="74e36652-00f5-4dca-bf10-9f02fc996dcc",
|
|
18
|
+
)
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
from txt2detection.ai_extractor.models import AttackFlowList
|
|
7
|
+
from txt2detection.attack_flow import (
|
|
8
|
+
create_navigator_layer,
|
|
9
|
+
get_techniques_from_extracted_objects,
|
|
10
|
+
parse_domain_flow,
|
|
11
|
+
parse_flow,
|
|
12
|
+
extract_attack_flow_and_navigator,
|
|
13
|
+
)
|
|
14
|
+
from stix2 import Report
|
|
15
|
+
|
|
16
|
+
from txt2detection.bundler import Bundler
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parse_flow(dummy_report, dummy_objects, dummy_flow):
|
|
20
|
+
tactics = get_all_tactics()
|
|
21
|
+
techniques = get_techniques_from_extracted_objects(dummy_objects, tactics)
|
|
22
|
+
flow = dummy_flow
|
|
23
|
+
report = dummy_report
|
|
24
|
+
expected_ids = {
|
|
25
|
+
"attack-flow--57213643-98f5-5ca2-a12d-fbe5a97f54a2",
|
|
26
|
+
"attack-action--62080947-214c-5c4b-8587-dc507659250d",
|
|
27
|
+
"x-mitre-tactic--5bc1d813-693e-4823-9961-abf9af4b0e92",
|
|
28
|
+
"relationship--8f4e8003-25dc-51ef-8c5f-ab022d5ad458",
|
|
29
|
+
"attack-action--57faf9fd-87e4-55a8-b1a8-9ba779ef16f7",
|
|
30
|
+
"report--bc14a07a-5189-5f64-85c3-33161b923627",
|
|
31
|
+
"x-mitre-tactic--ffd5bcee-6e16-4dd2-8eca-7b3beedf33ca",
|
|
32
|
+
"attack-pattern--3f886f2a-874f-4333-b794-aa6075009b1c",
|
|
33
|
+
"attack-pattern--1ecb2399-e8ba-4f6b-8ba7-5c27d49405cf",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
flow_objects = parse_flow(report, flow, techniques, tactics)
|
|
37
|
+
assert {obj["id"] for obj in flow_objects} == expected_ids
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_parse_flow__no_success(dummy_report):
|
|
41
|
+
flow_objects = parse_flow(
|
|
42
|
+
dummy_report,
|
|
43
|
+
AttackFlowList(
|
|
44
|
+
success=False,
|
|
45
|
+
matrix="enterprise",
|
|
46
|
+
items=[],
|
|
47
|
+
tactic_selection=[],
|
|
48
|
+
),
|
|
49
|
+
None,
|
|
50
|
+
None,
|
|
51
|
+
)
|
|
52
|
+
assert len(flow_objects) == 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@lru_cache
|
|
56
|
+
def get_all_tactics():
|
|
57
|
+
return Bundler.get_attack_tactics()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_parse_domain_flow(dummy_report, dummy_objects, dummy_flow):
|
|
61
|
+
tactics = get_all_tactics()
|
|
62
|
+
techniques = get_techniques_from_extracted_objects(dummy_objects, tactics)
|
|
63
|
+
flow = dummy_flow
|
|
64
|
+
report = dummy_report
|
|
65
|
+
flow_objects = parse_domain_flow(
|
|
66
|
+
report, flow, techniques, tactics, "enterprise-attack"
|
|
67
|
+
)
|
|
68
|
+
assert {obj["id"] for obj in flow_objects} == {
|
|
69
|
+
"attack-flow--57213643-98f5-5ca2-a12d-fbe5a97f54a2",
|
|
70
|
+
"attack-pattern--1ecb2399-e8ba-4f6b-8ba7-5c27d49405cf",
|
|
71
|
+
"x-mitre-tactic--5bc1d813-693e-4823-9961-abf9af4b0e92",
|
|
72
|
+
"attack-pattern--3f886f2a-874f-4333-b794-aa6075009b1c",
|
|
73
|
+
"x-mitre-tactic--ffd5bcee-6e16-4dd2-8eca-7b3beedf33ca",
|
|
74
|
+
"attack-action--62080947-214c-5c4b-8587-dc507659250d",
|
|
75
|
+
"attack-action--57faf9fd-87e4-55a8-b1a8-9ba779ef16f7",
|
|
76
|
+
"relationship--8f4e8003-25dc-51ef-8c5f-ab022d5ad458",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_get_techniques_from_extracted_objects(dummy_objects):
|
|
81
|
+
tactics = get_all_tactics()
|
|
82
|
+
techniques = get_techniques_from_extracted_objects(dummy_objects, tactics)
|
|
83
|
+
stix_objects = [v.pop("stix_obj") for v in techniques.values()]
|
|
84
|
+
assert dummy_objects == stix_objects
|
|
85
|
+
print(techniques)
|
|
86
|
+
assert techniques == {
|
|
87
|
+
"T1190": {
|
|
88
|
+
"domain": "enterprise-attack",
|
|
89
|
+
"name": "Exploit Public-Facing Application",
|
|
90
|
+
"possible_tactics": {"initial-access": "TA0001"},
|
|
91
|
+
"id": "T1190",
|
|
92
|
+
"platforms": [
|
|
93
|
+
"Containers",
|
|
94
|
+
"IaaS",
|
|
95
|
+
"Linux",
|
|
96
|
+
"Network Devices",
|
|
97
|
+
"Windows",
|
|
98
|
+
"macOS",
|
|
99
|
+
"ESXi",
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
"T1547": {
|
|
103
|
+
"domain": "enterprise-attack",
|
|
104
|
+
"name": "Boot or Logon Autostart Execution",
|
|
105
|
+
"possible_tactics": {
|
|
106
|
+
"persistence": "TA0003",
|
|
107
|
+
"privilege-escalation": "TA0004",
|
|
108
|
+
},
|
|
109
|
+
"id": "T1547",
|
|
110
|
+
"platforms": ["Linux", "macOS", "Windows", "Network Devices"],
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_extract_attack_flow_and_navigator(
|
|
116
|
+
bundler_instance, dummy_objects, dummy_report
|
|
117
|
+
):
|
|
118
|
+
bundler = MagicMock()
|
|
119
|
+
bundler.report = dummy_report
|
|
120
|
+
bundler.bundle.objects = dummy_objects
|
|
121
|
+
ai_extractor = MagicMock()
|
|
122
|
+
mock_extract_flow = ai_extractor.extract_attack_flow
|
|
123
|
+
text = "My awesome text"
|
|
124
|
+
|
|
125
|
+
tactics = get_all_tactics()
|
|
126
|
+
techniques = get_techniques_from_extracted_objects(bundler.bundle.objects, tactics)
|
|
127
|
+
bundler.get_attack_tactics.return_value = tactics
|
|
128
|
+
|
|
129
|
+
with (
|
|
130
|
+
patch("txt2detection.attack_flow.parse_flow") as mock_parse_flow,
|
|
131
|
+
patch(
|
|
132
|
+
"txt2detection.attack_flow.create_navigator_layer"
|
|
133
|
+
) as mock_create_navigator_layer,
|
|
134
|
+
):
|
|
135
|
+
# ================= Both flow and navigator ===================
|
|
136
|
+
flow, nav = extract_attack_flow_and_navigator(
|
|
137
|
+
bundler, text, True, True, ai_extractor
|
|
138
|
+
)
|
|
139
|
+
assert bundler.flow_objects == mock_parse_flow.return_value
|
|
140
|
+
assert (flow, nav) == (
|
|
141
|
+
mock_extract_flow.return_value,
|
|
142
|
+
mock_create_navigator_layer.return_value,
|
|
143
|
+
)
|
|
144
|
+
mock_parse_flow.assert_called_once_with(
|
|
145
|
+
bundler.report, mock_extract_flow.return_value, techniques, tactics
|
|
146
|
+
)
|
|
147
|
+
mock_extract_flow.assert_called_once_with(text, techniques)
|
|
148
|
+
|
|
149
|
+
mock_create_navigator_layer.assert_called_once_with(
|
|
150
|
+
bundler.report,
|
|
151
|
+
mock_extract_flow.return_value,
|
|
152
|
+
techniques,
|
|
153
|
+
tactics,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
### reset mocks
|
|
157
|
+
mock_parse_flow.reset_mock()
|
|
158
|
+
mock_create_navigator_layer.reset_mock()
|
|
159
|
+
mock_extract_flow.reset_mock()
|
|
160
|
+
|
|
161
|
+
# ================= only flow ===================
|
|
162
|
+
flow, nav = extract_attack_flow_and_navigator(
|
|
163
|
+
bundler, text, True, False, ai_extractor
|
|
164
|
+
)
|
|
165
|
+
assert bundler.flow_objects == mock_parse_flow.return_value
|
|
166
|
+
assert (flow, nav) == (mock_extract_flow.return_value, None)
|
|
167
|
+
mock_parse_flow.assert_called_once_with(
|
|
168
|
+
bundler.report, mock_extract_flow.return_value, techniques, tactics
|
|
169
|
+
)
|
|
170
|
+
mock_extract_flow.assert_called_once_with(text, techniques)
|
|
171
|
+
|
|
172
|
+
mock_create_navigator_layer.assert_not_called()
|
|
173
|
+
|
|
174
|
+
### reset mocks
|
|
175
|
+
mock_parse_flow.reset_mock()
|
|
176
|
+
mock_create_navigator_layer.reset_mock()
|
|
177
|
+
mock_extract_flow.reset_mock()
|
|
178
|
+
|
|
179
|
+
# ================= only navigator ===================
|
|
180
|
+
flow, nav = extract_attack_flow_and_navigator(
|
|
181
|
+
bundler, text, False, True, ai_extractor
|
|
182
|
+
)
|
|
183
|
+
assert bundler.flow_objects == mock_parse_flow.return_value
|
|
184
|
+
mock_extract_flow.assert_called_once_with(text, techniques)
|
|
185
|
+
assert (flow, nav) == (
|
|
186
|
+
mock_extract_flow.return_value,
|
|
187
|
+
mock_create_navigator_layer.return_value,
|
|
188
|
+
)
|
|
189
|
+
mock_parse_flow.assert_not_called()
|
|
190
|
+
|
|
191
|
+
mock_create_navigator_layer.assert_called_once_with(
|
|
192
|
+
bundler.report,
|
|
193
|
+
mock_extract_flow.return_value,
|
|
194
|
+
techniques,
|
|
195
|
+
tactics,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
### reset mocks
|
|
199
|
+
mock_parse_flow.reset_mock()
|
|
200
|
+
mock_create_navigator_layer.reset_mock()
|
|
201
|
+
mock_extract_flow.reset_mock()
|
|
202
|
+
# ============ no technique object ============
|
|
203
|
+
bundler.bundle.objects = []
|
|
204
|
+
flow, nav = extract_attack_flow_and_navigator(
|
|
205
|
+
bundler, text, True, True, ai_extractor
|
|
206
|
+
)
|
|
207
|
+
mock_extract_flow.assert_not_called()
|
|
208
|
+
assert (flow, nav) == (None, None)
|
|
209
|
+
mock_parse_flow.assert_not_called()
|
|
210
|
+
|
|
211
|
+
mock_create_navigator_layer.assert_not_called()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_create_navigator_layer(dummy_report):
|
|
215
|
+
summary = "this is a summary"
|
|
216
|
+
flow = MagicMock()
|
|
217
|
+
tactics_1 = {
|
|
218
|
+
"TA01": "initial-access",
|
|
219
|
+
"TA02": "lateral-movement",
|
|
220
|
+
"TA03": "command-and-control",
|
|
221
|
+
}
|
|
222
|
+
tactics_2 = {
|
|
223
|
+
"TA11": "initial-access",
|
|
224
|
+
"TA12": "lateral-movement",
|
|
225
|
+
"TA25": "command-and-control",
|
|
226
|
+
"TA123": "persistence",
|
|
227
|
+
"TA91": "exfiltration",
|
|
228
|
+
}
|
|
229
|
+
flow.items = [
|
|
230
|
+
SimpleNamespace(
|
|
231
|
+
attack_technique_id="T0001",
|
|
232
|
+
attack_tactic_id="TA01",
|
|
233
|
+
description="description 1",
|
|
234
|
+
),
|
|
235
|
+
SimpleNamespace(
|
|
236
|
+
attack_technique_id="T0003",
|
|
237
|
+
attack_tactic_id="TA03",
|
|
238
|
+
description="description 2",
|
|
239
|
+
),
|
|
240
|
+
SimpleNamespace(
|
|
241
|
+
attack_technique_id="T1001",
|
|
242
|
+
attack_tactic_id="TA11",
|
|
243
|
+
description="description 3",
|
|
244
|
+
),
|
|
245
|
+
SimpleNamespace(
|
|
246
|
+
attack_technique_id="T1002",
|
|
247
|
+
attack_tactic_id="TA12",
|
|
248
|
+
description="description 4",
|
|
249
|
+
),
|
|
250
|
+
SimpleNamespace(
|
|
251
|
+
attack_technique_id="T2001",
|
|
252
|
+
attack_tactic_id="TA11",
|
|
253
|
+
description="description 28jhsjhs",
|
|
254
|
+
),
|
|
255
|
+
SimpleNamespace(
|
|
256
|
+
attack_technique_id="T2003",
|
|
257
|
+
attack_tactic_id="TA91",
|
|
258
|
+
description="description sasa",
|
|
259
|
+
),
|
|
260
|
+
]
|
|
261
|
+
techniques = {
|
|
262
|
+
"T0001": dict(
|
|
263
|
+
id="T0001", domain="enterprise-attack", possible_tactics=tactics_1
|
|
264
|
+
),
|
|
265
|
+
"T0002": dict(
|
|
266
|
+
id="T0002", domain="enterprise-attack", possible_tactics=tactics_1
|
|
267
|
+
),
|
|
268
|
+
"T0003": dict(
|
|
269
|
+
id="T0003", domain="enterprise-attack", possible_tactics=tactics_1
|
|
270
|
+
),
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
retval = create_navigator_layer(
|
|
274
|
+
dummy_report,
|
|
275
|
+
flow,
|
|
276
|
+
techniques,
|
|
277
|
+
tactics={"version": "16.1"},
|
|
278
|
+
)
|
|
279
|
+
assert len(retval) == 1
|
|
280
|
+
print(retval)
|
|
281
|
+
assert retval == [
|
|
282
|
+
{
|
|
283
|
+
"versions": {"layer": "4.5", "attack": "16.1", "navigator": "5.1.0"},
|
|
284
|
+
"name": "fake python vulnerability report",
|
|
285
|
+
"domain": "enterprise-attack",
|
|
286
|
+
"techniques": [],
|
|
287
|
+
"gradient": {
|
|
288
|
+
"colors": ["#ffffff", "#ff6666"],
|
|
289
|
+
"minValue": 0,
|
|
290
|
+
"maxValue": 100,
|
|
291
|
+
},
|
|
292
|
+
"legendItems": [],
|
|
293
|
+
"metadata": [
|
|
294
|
+
{
|
|
295
|
+
"name": "report_id",
|
|
296
|
+
"value": "report--bc14a07a-5189-5f64-85c3-33161b923627",
|
|
297
|
+
}
|
|
298
|
+
],
|
|
299
|
+
"links": [
|
|
300
|
+
{
|
|
301
|
+
"label": "Generated using txt2detection",
|
|
302
|
+
"url": "https://github.com/muchdogesec/txt2detection/",
|
|
303
|
+
}
|
|
304
|
+
],
|
|
305
|
+
"layout": {"layout": "side"},
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_create_navigator_layer__real_flow(dummy_report, dummy_flow, dummy_objects):
|
|
311
|
+
tactics = get_all_tactics()
|
|
312
|
+
techniques = get_techniques_from_extracted_objects(dummy_objects, tactics)
|
|
313
|
+
tactics["version"] = "16.2"
|
|
314
|
+
retval = create_navigator_layer(dummy_report, dummy_flow, techniques, tactics)
|
|
315
|
+
assert len(retval) == 1
|
|
316
|
+
assert retval == [
|
|
317
|
+
{
|
|
318
|
+
"versions": {"layer": "4.5", "attack": "16.2", "navigator": "5.1.0"},
|
|
319
|
+
"name": "fake python vulnerability report",
|
|
320
|
+
"domain": "enterprise-attack",
|
|
321
|
+
"techniques": [
|
|
322
|
+
{
|
|
323
|
+
"techniqueID": "T1190",
|
|
324
|
+
"tactic": "initial-access",
|
|
325
|
+
"score": 100,
|
|
326
|
+
"showSubtechniques": True,
|
|
327
|
+
"comment": "The adversary exploited a vulnerability in a public-facing application, identified as CVE-2024-56520, to gain initial access to the system.",
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"techniqueID": "T1547",
|
|
331
|
+
"tactic": "persistence",
|
|
332
|
+
"score": 100,
|
|
333
|
+
"showSubtechniques": True,
|
|
334
|
+
"comment": "After gaining access, the adversary used boot or logon autostart execution techniques to maintain persistence on the compromised system.",
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
"gradient": {
|
|
338
|
+
"colors": ["#ffffff", "#ff6666"],
|
|
339
|
+
"minValue": 0,
|
|
340
|
+
"maxValue": 100,
|
|
341
|
+
},
|
|
342
|
+
"legendItems": [],
|
|
343
|
+
"metadata": [
|
|
344
|
+
{
|
|
345
|
+
"name": "report_id",
|
|
346
|
+
"value": "report--bc14a07a-5189-5f64-85c3-33161b923627",
|
|
347
|
+
}
|
|
348
|
+
],
|
|
349
|
+
"links": [
|
|
350
|
+
{
|
|
351
|
+
"label": "Generated using txt2detection",
|
|
352
|
+
"url": "https://github.com/muchdogesec/txt2detection/",
|
|
353
|
+
}
|
|
354
|
+
],
|
|
355
|
+
"layout": {"layout": "side"},
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@pytest.fixture
|
|
361
|
+
def dummy_objects():
|
|
362
|
+
return [
|
|
363
|
+
{
|
|
364
|
+
"created": "2018-04-18T17:59:24.739Z",
|
|
365
|
+
"created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
|
|
366
|
+
"description": "Adversaries may attempt to exploit a weakness in an Internet-facing host or system to initially access a network. The weakness in the system can be a software bug, a temporary glitch, or a misconfiguration.\n\nExploited applications are often websites/web servers, but can also include databases (like SQL), standard services (like SMB or SSH), network device administration and management protocols (like SNMP and Smart Install), and any other system with Internet-accessible open sockets.(Citation: NVD CVE-2016-6662)(Citation: CIS Multiple SMB Vulnerabilities)(Citation: US-CERT TA18-106A Network Infrastructure Devices 2018)(Citation: Cisco Blog Legacy Device Attacks)(Citation: NVD CVE-2014-7169) On ESXi infrastructure, adversaries may exploit exposed OpenSLP services; they may alternatively exploit exposed VMware vCenter servers.(Citation: Recorded Future ESXiArgs Ransomware 2023)(Citation: Ars Technica VMWare Code Execution Vulnerability 2021) Depending on the flaw being exploited, this may also involve [Exploitation for Defense Evasion](https://attack.mitre.org/techniques/T1211) or [Exploitation for Client Execution](https://attack.mitre.org/techniques/T1203).\n\nIf an application is hosted on cloud-based infrastructure and/or is containerized, then exploiting it may lead to compromise of the underlying instance or container. This can allow an adversary a path to access the cloud or container APIs (e.g., via the [Cloud Instance Metadata API](https://attack.mitre.org/techniques/T1552/005)), exploit container host access via [Escape to Host](https://attack.mitre.org/techniques/T1611), or take advantage of weak identity and access management policies.\n\nAdversaries may also exploit edge network infrastructure and related appliances, specifically targeting devices that do not support robust host-based defenses.(Citation: Mandiant Fortinet Zero Day)(Citation: Wired Russia Cyberwar)\n\nFor websites and databases, the OWASP top 10 and CWE top 25 highlight the most common web-based vulnerabilities.(Citation: OWASP Top 10)(Citation: CWE top 25)",
|
|
367
|
+
"external_references": [
|
|
368
|
+
{
|
|
369
|
+
"source_name": "mitre-attack",
|
|
370
|
+
"url": "https://attack.mitre.org/techniques/T1190",
|
|
371
|
+
"external_id": "T1190",
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
"id": "attack-pattern--3f886f2a-874f-4333-b794-aa6075009b1c",
|
|
375
|
+
"kill_chain_phases": [
|
|
376
|
+
{"kill_chain_name": "mitre-attack", "phase_name": "initial-access"}
|
|
377
|
+
],
|
|
378
|
+
"modified": "2025-04-15T19:58:25.266Z",
|
|
379
|
+
"name": "Exploit Public-Facing Application",
|
|
380
|
+
"object_marking_refs": [
|
|
381
|
+
"marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
|
|
382
|
+
],
|
|
383
|
+
"revoked": False,
|
|
384
|
+
"spec_version": "2.1",
|
|
385
|
+
"type": "attack-pattern",
|
|
386
|
+
"x_mitre_attack_spec_version": "3.2.0",
|
|
387
|
+
"x_mitre_contributors": [
|
|
388
|
+
"Yossi Weizman, Azure Defender Research Team",
|
|
389
|
+
"Praetorian",
|
|
390
|
+
],
|
|
391
|
+
"x_mitre_data_sources": [
|
|
392
|
+
"Network Traffic: Network Traffic Content",
|
|
393
|
+
"Application Log: Application Log Content",
|
|
394
|
+
],
|
|
395
|
+
"x_mitre_deprecated": False,
|
|
396
|
+
"x_mitre_detection": "Monitor application logs for abnormal behavior that may indicate attempted or successful exploitation. Use deep packet inspection to look for artifacts of common exploit traffic, such as SQL injection. Web Application Firewalls may detect improper inputs attempting exploitation.",
|
|
397
|
+
"x_mitre_domains": ["enterprise-attack"],
|
|
398
|
+
"x_mitre_is_subtechnique": False,
|
|
399
|
+
"x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
|
|
400
|
+
"x_mitre_platforms": [
|
|
401
|
+
"Containers",
|
|
402
|
+
"IaaS",
|
|
403
|
+
"Linux",
|
|
404
|
+
"Network Devices",
|
|
405
|
+
"Windows",
|
|
406
|
+
"macOS",
|
|
407
|
+
"ESXi",
|
|
408
|
+
],
|
|
409
|
+
"x_mitre_version": "2.7",
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"created": "2020-01-23T17:46:59.535Z",
|
|
413
|
+
"created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
|
|
414
|
+
"description": "Adversaries may configure system settings to automatically execute a program during system boot or logon to maintain persistence or gain higher-level privileges on compromised systems. Operating systems may have mechanisms for automatically running a program on system boot or account logon.(Citation: Microsoft Run Key)(Citation: MSDN Authentication Packages)(Citation: Microsoft TimeProvider)(Citation: Cylance Reg Persistence Sept 2013)(Citation: Linux Kernel Programming) These mechanisms may include automatically executing programs that are placed in specially designated directories or are referenced by repositories that store configuration information, such as the Windows Registry. An adversary may achieve the same goal by modifying or extending features of the kernel.\n\nSince some boot or logon autostart programs run with higher privileges, an adversary may leverage these to elevate privileges.",
|
|
415
|
+
"external_references": [
|
|
416
|
+
{
|
|
417
|
+
"source_name": "mitre-attack",
|
|
418
|
+
"url": "https://attack.mitre.org/techniques/T1547",
|
|
419
|
+
"external_id": "T1547",
|
|
420
|
+
}
|
|
421
|
+
],
|
|
422
|
+
"id": "attack-pattern--1ecb2399-e8ba-4f6b-8ba7-5c27d49405cf",
|
|
423
|
+
"kill_chain_phases": [
|
|
424
|
+
{"kill_chain_name": "mitre-attack", "phase_name": "persistence"},
|
|
425
|
+
{
|
|
426
|
+
"kill_chain_name": "mitre-attack",
|
|
427
|
+
"phase_name": "privilege-escalation",
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
"modified": "2025-04-15T19:58:12.270Z",
|
|
431
|
+
"name": "Boot or Logon Autostart Execution",
|
|
432
|
+
"object_marking_refs": [
|
|
433
|
+
"marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
|
|
434
|
+
],
|
|
435
|
+
"revoked": False,
|
|
436
|
+
"spec_version": "2.1",
|
|
437
|
+
"type": "attack-pattern",
|
|
438
|
+
"x_mitre_attack_spec_version": "3.2.0",
|
|
439
|
+
"x_mitre_data_sources": [
|
|
440
|
+
"Process: OS API Execution",
|
|
441
|
+
"Module: Module Load",
|
|
442
|
+
"Command: Command Execution",
|
|
443
|
+
"File: File Creation",
|
|
444
|
+
"Windows Registry: Windows Registry Key Creation",
|
|
445
|
+
"Windows Registry: Windows Registry Key Modification",
|
|
446
|
+
"File: File Modification",
|
|
447
|
+
"Kernel: Kernel Module Load",
|
|
448
|
+
"Process: Process Creation",
|
|
449
|
+
"Driver: Driver Load",
|
|
450
|
+
],
|
|
451
|
+
"x_mitre_deprecated": False,
|
|
452
|
+
"x_mitre_detection": "Monitor for additions or modifications of mechanisms that could be used to trigger autostart execution, such as relevant additions to the Registry. Look for changes that are not correlated with known updates, patches, or other planned administrative activity. Tools such as Sysinternals Autoruns may also be used to detect system autostart configuration changes that could be attempts at persistence.(Citation: TechNet Autoruns) Changes to some autostart configuration settings may happen under normal conditions when legitimate software is installed. \n\nSuspicious program execution as autostart programs may show up as outlier processes that have not been seen before when compared against historical data.To increase confidence of malicious activity, data and events should not be viewed in isolation, but as part of a chain of behavior that could lead to other activities, such as network connections made for Command and Control, learning details about the environment through Discovery, and Lateral Movement.\n\nMonitor DLL loads by processes, specifically looking for DLLs that are not recognized or not normally loaded into a process. Look for abnormal process behavior that may be due to a process loading a malicious DLL.\n\nMonitor for abnormal usage of utilities and command-line parameters involved in kernel modification or driver installation.",
|
|
453
|
+
"x_mitre_domains": ["enterprise-attack"],
|
|
454
|
+
"x_mitre_is_subtechnique": False,
|
|
455
|
+
"x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
|
|
456
|
+
"x_mitre_platforms": ["Linux", "macOS", "Windows", "Network Devices"],
|
|
457
|
+
"x_mitre_version": "1.3",
|
|
458
|
+
},
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@pytest.fixture
|
|
463
|
+
def dummy_report():
|
|
464
|
+
return Report(
|
|
465
|
+
**{
|
|
466
|
+
"type": "report",
|
|
467
|
+
"spec_version": "2.1",
|
|
468
|
+
"id": "report--bc14a07a-5189-5f64-85c3-33161b923627",
|
|
469
|
+
"created_by_ref": "identity--a4d70b75-6f4a-5d19-9137-da863edd33d7",
|
|
470
|
+
"created": "2024-05-01T08:53:31.000Z",
|
|
471
|
+
"modified": "2024-05-01T08:53:31.000Z",
|
|
472
|
+
"name": "fake python vulnerability report",
|
|
473
|
+
"description": "requirements.txt file that contains pypotr version 5.1.1. the version was uploaded by a state agent; Attack Technique used include T1547 and cve-2024-56520 and T1190; The attack takes control of dns 1.1.1.1 and domain-name google.com and goes to url https://datadome.net",
|
|
474
|
+
"published": "2024-05-01T08:53:31Z",
|
|
475
|
+
"object_refs": [
|
|
476
|
+
"indicator--f2e55328-d20a-4637-ba55-394fc56121a4",
|
|
477
|
+
"data-source--22684436-fab3-5847-b642-bcf23430e361",
|
|
478
|
+
],
|
|
479
|
+
"labels": ["x1.we", "pop.aaa1"],
|
|
480
|
+
"external_references": [
|
|
481
|
+
{
|
|
482
|
+
"source_name": "description_md5_hash",
|
|
483
|
+
"external_id": "4b2ca609c2ae27843b251a761efb8cdf",
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
"source_name": "url",
|
|
487
|
+
"external_id": "https://example.com/pypotr-compromised",
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
"object_marking_refs": [
|
|
491
|
+
"marking-definition--bab4a63c-aed9-4cf5-a766-dfca5abac2bb",
|
|
492
|
+
"marking-definition--a4d70b75-6f4a-5d19-9137-da863edd33d7",
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@pytest.fixture
|
|
499
|
+
def dummy_flow():
|
|
500
|
+
return AttackFlowList.model_validate(
|
|
501
|
+
{
|
|
502
|
+
"tactic_selection": [["T1190", "initial-access"], ["T1547", "persistence"]],
|
|
503
|
+
"items": [
|
|
504
|
+
{
|
|
505
|
+
"position": 0,
|
|
506
|
+
"attack_technique_id": "T1190",
|
|
507
|
+
"name": "Exploitation of Public-Facing Application",
|
|
508
|
+
"description": "The adversary exploited a vulnerability in a public-facing application, identified as CVE-2024-56520, to gain initial access to the system.",
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
"position": 1,
|
|
512
|
+
"attack_technique_id": "T1547",
|
|
513
|
+
"name": "Establishing Persistence via Boot or Logon Autostart",
|
|
514
|
+
"description": "After gaining access, the adversary used boot or logon autostart execution techniques to maintain persistence on the compromised system.",
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
"success": True,
|
|
518
|
+
}
|
|
519
|
+
)
|
|
@@ -283,3 +283,9 @@ def test_get_cve_objects(bundler_instance):
|
|
|
283
283
|
cves = ['CVE-2025-1234', 'CVE-2024-1234']
|
|
284
284
|
retval = bundler_instance.get_cve_objects(cves)
|
|
285
285
|
assert {r['name'] for r in retval} == set(cves)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_flow_objects__adds_extension_definition(bundler_instance):
|
|
289
|
+
bundler_instance.flow_objects = [{'id': 'some-other', 'type': ''}]
|
|
290
|
+
assert bundler_instance.all_objects.issuperset({'some-other', "extension-definition--fb9c968a-745b-4ade-9b25-c324172197f4"})
|
|
291
|
+
assert "extension-definition--fb9c968a-745b-4ade-9b25-c324172197f4" not in bundler_instance.report.object_refs
|
|
@@ -9,14 +9,12 @@ from .bundler import Bundler
|
|
|
9
9
|
from .ai_extractor.base import BaseAIExtractor
|
|
10
10
|
from .models import UUID_NAMESPACE
|
|
11
11
|
from stix2extensions.attack_action import AttackAction, AttackFlow
|
|
12
|
-
from stix2extensions._extensions import attack_flow_ExtensionDefinitionSMO
|
|
13
|
-
|
|
14
12
|
|
|
15
13
|
def parse_flow(report, flow: AttackFlowList, techniques, tactics):
|
|
16
14
|
logging.info(f"flow.success = {flow.success}")
|
|
17
15
|
if not flow.success:
|
|
18
16
|
return []
|
|
19
|
-
objects = [report
|
|
17
|
+
objects = [report]
|
|
20
18
|
for domain in ["enterprise-attack", "mobile-attack", "ics-attack"]:
|
|
21
19
|
flow_objects = parse_domain_flow(report, flow, techniques, tactics, domain)
|
|
22
20
|
objects.extend(flow_objects)
|
|
@@ -42,6 +42,7 @@ class Bundler:
|
|
|
42
42
|
uuid = None
|
|
43
43
|
id_map = dict()
|
|
44
44
|
data: DataContainer
|
|
45
|
+
ATTACK_FLOW_SMO_URL = "https://github.com/muchdogesec/stix2extensions/raw/refs/heads/main/remote-definitions/attack-flow.json"
|
|
45
46
|
# https://raw.githubusercontent.com/muchdogesec/stix4doge/refs/heads/main/objects/identity/txt2detection.json
|
|
46
47
|
default_identity = Identity(
|
|
47
48
|
**{
|
|
@@ -361,6 +362,23 @@ class Bundler:
|
|
|
361
362
|
for d in container.detections:
|
|
362
363
|
self.add_rule_indicator(d)
|
|
363
364
|
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def flow_objects(self):
|
|
368
|
+
return self._flow_objects
|
|
369
|
+
|
|
370
|
+
@flow_objects.setter
|
|
371
|
+
def flow_objects(self, objects):
|
|
372
|
+
smo_objects = requests.get(self.ATTACK_FLOW_SMO_URL).json()["objects"]
|
|
373
|
+
objects.extend(smo_objects)
|
|
374
|
+
for obj in objects:
|
|
375
|
+
if obj["id"] == self.report.id:
|
|
376
|
+
continue
|
|
377
|
+
is_report_object = obj["type"] not in ["extension-definition", "identity"]
|
|
378
|
+
self.add_ref(obj, append_report=is_report_object)
|
|
379
|
+
self._flow_objects = objects
|
|
380
|
+
|
|
381
|
+
|
|
364
382
|
|
|
365
383
|
def make_logsouce_string(source: dict):
|
|
366
384
|
d = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|