parse_ioc 0.4.0__tar.gz → 0.6.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.
@@ -0,0 +1,244 @@
1
+ Metadata-Version: 2.4
2
+ Name: parse_ioc
3
+ Version: 0.6.0
4
+ Summary: Parse a list of indicators into a dictionary or JSON structure for programmatic use.
5
+ Project-URL: Homepage, https://github.com/bonifield/parse_ioc
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ # ParseIOC
12
+
13
+ Parse a list of indicators into a dictionary, JSON structure, or Sqlite database for programmatic use.
14
+
15
+ Handles:
16
+ - emails and their domains
17
+ - URL domains, and username+password and ports if present
18
+ - ipv4 and ipv6 addresses and networks
19
+ - md5, sha1, sha256, and sha512 hashes
20
+
21
+ ...with the goal of **mapping this dictionary to fields in a SIEM** or similar database.
22
+
23
+ This enables programatic use of an IOC file to quickly query specific values in a SIEM, such as `source.ip` or `file.hash.sha256`.
24
+
25
+ This tool will support more formats (imphash, ssdeep, ja3+) "eventually".
26
+
27
+ **Otherwise-unidentifiable strings are categorized as "file".**
28
+
29
+ IOCs are categorized as one of these types:
30
+ - `domain`
31
+ - `email`
32
+ - `file_path_linux`
33
+ - `file_path_windows`
34
+ - `ipv4`
35
+ - `ipv4_network`
36
+ - `ipv6`
37
+ - `ipv6_network`
38
+ - `md5`
39
+ - `sha1`
40
+ - `sha256`
41
+ - `sha512`
42
+ - extra: `credentials`
43
+ - extra: `port`
44
+
45
+ ## Setup
46
+
47
+ ```
48
+ uv add parse_ioc
49
+ # or
50
+ python3 -m pip install parse_ioc
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ ### Imports
56
+
57
+ ```
58
+ from parse_ioc import ParseIOC, parse_multi, map_fields, to_sqlite
59
+ ```
60
+ - `ParseIOC`: class that parses each indicator
61
+ - `parse_multi`: function that wraps `ParseIOC` to create one large dictionary, or a list of dictionaries, containing the parsed indicators
62
+ - `map_fields`: function that maps parsed indicators by type to fields used in a SIEM, specified a TOML config
63
+ - `to_sqlite`: writes parsed indicators to a simple Sqlite3 database
64
+
65
+ ### Categorize a single indicator using `ParseIOC()`
66
+ ```
67
+ >>> i = ParseIOC("https://192.168.20.20/bad")
68
+ >>> i.to_dict
69
+ {'ioc': '192.168.20.20', 'ioc_type': 'ipv4'}
70
+ >>> i.to_json
71
+ '{"ioc": "192.168.20.20", "ioc_type": "ipv4"}'
72
+ ```
73
+
74
+ "extra" details are added when appropriate, such as credentials in a URL, ports, or domains in email addresses
75
+ ```
76
+ >>> e = ParseIOC("alice@email.local")
77
+ >>> print(json.dumps(e.to_dict, indent=4))
78
+ {
79
+ "ioc": "alice@email.local",
80
+ "ioc_type": "email",
81
+ "extra": [
82
+ {
83
+ "ioc": "email.local",
84
+ "ioc_type": "domain"
85
+ ...
86
+ ```
87
+
88
+ ### Categorize a **list** or **file** of indicators using `parse_multi()`
89
+
90
+ using `mode="combined"` produces one massive dictionary of categorized indicators
91
+ ```
92
+ >>> p = parse_multi(ioc_list, mode="combined")
93
+ # or
94
+ >>> p = parse_multi("ioc_examples.txt", mode="combined")
95
+ >>> import json
96
+ >>> print(json.dumps(p, indent=4))
97
+ {
98
+ "email": [
99
+ "xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai",
100
+ "alice@email.local",
101
+ ...
102
+ ```
103
+
104
+ using `mode="single"` produces a **list of dictionaries**, similar to calling `ParseIOC(ioc)` by itself in a loop and putting the results in a list
105
+ ```
106
+ >>> p = parse_multi("tests/ioc_examples.txt", mode="single")
107
+ >>> print(json.dumps(p, indent=4))
108
+ [
109
+ {
110
+ "ioc": "website.local",
111
+ "ioc_type": "domain"
112
+ },
113
+ {
114
+ "ioc": "anotherwebsite.local",
115
+ "ioc_type": "domain",
116
+ "extra": [
117
+ {
118
+ "ioc": 9443,
119
+ "ioc_type": "port"
120
+ }
121
+ ]
122
+ },
123
+ {
124
+ "ioc": "bob@email.local",
125
+ "ioc_type": "email",
126
+ "extra": [
127
+ {
128
+ "ioc": "email.local",
129
+ "ioc_type": "domain"
130
+ }
131
+ ```
132
+
133
+ ### Map a **list** or **file** of indicators to SIEM fields using `map_fields()`
134
+
135
+ - use the `map_ecs.toml` file as a reference or template
136
+
137
+ example shows parsing a file into [Elastic Common Schema (ECS)](https://www.elastic.co/docs/reference/ecs/ecs-field-reference) fields
138
+ ```
139
+ >>> m = map_fields("tests/ioc_examples.txt", "map_ecs.toml")
140
+ >>> print(json.dumps(m, indent=4))
141
+ {
142
+ "email.from.address": [
143
+ "xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai",
144
+ "alice@email.local",
145
+ "bob@emailsubdomain.bad.local.xn--p1ai",
146
+ "bob@email.local",
147
+ ...
148
+ ```
149
+
150
+ specify `keep_types=True` in `map_fields()` to nest the SIEM values under the original type categorizations
151
+ ```
152
+ >>> m = map_fields("tests/ioc_examples.txt", "map_ecs.toml", keep_types=True)
153
+ >>> print(json.dumps(m, indent=4))
154
+ {
155
+ "email": {
156
+ "email.from.address": [
157
+ "xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai",
158
+ "alice@email.local",
159
+ ...
160
+ ```
161
+
162
+ ### Create Sqlite database of parsed indicators using `to_sqlite()`
163
+ - table name: `indicators`
164
+ - columns: `ioc` (`TEXT`, primary key) and `type` (`TEXT`)
165
+ - uses `parse_multi(input_object, mode="combined")`
166
+ - `extra` data has its own row, not linked to its parent data
167
+ - ex. a port won't have any reference to the original URL it came from
168
+ ```
169
+ >>> to_sqlite("tests/ioc_examples.txt", "iocs.db")
170
+ exported 41 indicators to iocs.db
171
+ ```
172
+
173
+ example output using `sqlite3`
174
+ ```
175
+ $ sqlite3 iocs.db "SELECT ioc,type FROM indicators;"
176
+ xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai|email
177
+ alice@email.local|email
178
+ bob@emailsubdomain.bad.local.xn--p1ai|email
179
+ bob@email.local|email
180
+ 192.168.1.1|ipv4
181
+ ...
182
+ 9443|port
183
+ email.local|domain
184
+ ...
185
+ ```
186
+
187
+ ## Gotchas
188
+ - Domain names must have a period (`.`) otherwise the string will be considered a file
189
+ - This regex in the domain checker, determines if the parsed domain name drops further down to the `file` checker or not:
190
+ ```
191
+ file_extensions = re.compile(r"\.(?:exe|dll|msi|bat|cmd|elf|scr|cpl|ps1|vbs|pdf|docx|xlsx|pptx|doc|xls|ppt|rtf|csv|txt|log|xml|zip|rar|7z|tar|gz|iso|img|dmg|cab|png|jpg|jpeg|gif|ico|bmp|svg|mp3|mp4|wav|avi|mov|js|php|css|html|htm|sql|conf|ini|yaml|yml|json)$", re.IGNORECASE)
192
+ ```
193
+
194
+ ## Expected output from running `tests/parse_ioc.py` using example data
195
+ ```
196
+ $ uv run tests/parseonly.py -f tests/ioc_examples.txt -h
197
+ usage: parseonly.py [-h] -f FILENAME [-m] [-j] [-k]
198
+
199
+ parse indicators
200
+
201
+ options:
202
+ -h, --help show this help message and exit
203
+ -f, --file FILENAME input filename
204
+ -m, --map show mapped format
205
+ -j, --json display indented JSON
206
+ -k, --keep-types keep field types in output dict
207
+ ```
208
+
209
+ ```
210
+ $ uv run tests/parseonly.py -f tests/ioc_examples.txt -mjk
211
+ {
212
+ "email": {
213
+ "email.from.address": [
214
+ "xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai",
215
+ "bob@emailsubdomain.bad.local",
216
+ "alice@email.local",
217
+ "bob@emailsubdomain.bad.local.xn--p1ai",
218
+ "bob@email.local"
219
+ ],
220
+ "email.sender.address": [
221
+ "xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai",
222
+ "bob@emailsubdomain.bad.local",
223
+ ...
224
+ "ipv4": {
225
+ "source.ip": [
226
+ "192.168.20.20",
227
+ "8.8.8.8",
228
+ "1.2.3.4",
229
+ "192.168.1.1"
230
+ ],
231
+ ...
232
+ "ipv4_network": {
233
+ "source.ip": [
234
+ "192.168.1.0/24",
235
+ "8.8.8.0/24"
236
+ ...
237
+ ```
238
+
239
+ ## TODO
240
+
241
+ - add optional type conversions to match TheHive5 default observable types
242
+ - change the list of categorizations in README to a markdown table, showing the converted types
243
+ - need to streamline and optimize code
244
+ - remove the filepath regex statements created by AI
@@ -0,0 +1,234 @@
1
+ # ParseIOC
2
+
3
+ Parse a list of indicators into a dictionary, JSON structure, or Sqlite database for programmatic use.
4
+
5
+ Handles:
6
+ - emails and their domains
7
+ - URL domains, and username+password and ports if present
8
+ - ipv4 and ipv6 addresses and networks
9
+ - md5, sha1, sha256, and sha512 hashes
10
+
11
+ ...with the goal of **mapping this dictionary to fields in a SIEM** or similar database.
12
+
13
+ This enables programatic use of an IOC file to quickly query specific values in a SIEM, such as `source.ip` or `file.hash.sha256`.
14
+
15
+ This tool will support more formats (imphash, ssdeep, ja3+) "eventually".
16
+
17
+ **Otherwise-unidentifiable strings are categorized as "file".**
18
+
19
+ IOCs are categorized as one of these types:
20
+ - `domain`
21
+ - `email`
22
+ - `file_path_linux`
23
+ - `file_path_windows`
24
+ - `ipv4`
25
+ - `ipv4_network`
26
+ - `ipv6`
27
+ - `ipv6_network`
28
+ - `md5`
29
+ - `sha1`
30
+ - `sha256`
31
+ - `sha512`
32
+ - extra: `credentials`
33
+ - extra: `port`
34
+
35
+ ## Setup
36
+
37
+ ```
38
+ uv add parse_ioc
39
+ # or
40
+ python3 -m pip install parse_ioc
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ### Imports
46
+
47
+ ```
48
+ from parse_ioc import ParseIOC, parse_multi, map_fields, to_sqlite
49
+ ```
50
+ - `ParseIOC`: class that parses each indicator
51
+ - `parse_multi`: function that wraps `ParseIOC` to create one large dictionary, or a list of dictionaries, containing the parsed indicators
52
+ - `map_fields`: function that maps parsed indicators by type to fields used in a SIEM, specified a TOML config
53
+ - `to_sqlite`: writes parsed indicators to a simple Sqlite3 database
54
+
55
+ ### Categorize a single indicator using `ParseIOC()`
56
+ ```
57
+ >>> i = ParseIOC("https://192.168.20.20/bad")
58
+ >>> i.to_dict
59
+ {'ioc': '192.168.20.20', 'ioc_type': 'ipv4'}
60
+ >>> i.to_json
61
+ '{"ioc": "192.168.20.20", "ioc_type": "ipv4"}'
62
+ ```
63
+
64
+ "extra" details are added when appropriate, such as credentials in a URL, ports, or domains in email addresses
65
+ ```
66
+ >>> e = ParseIOC("alice@email.local")
67
+ >>> print(json.dumps(e.to_dict, indent=4))
68
+ {
69
+ "ioc": "alice@email.local",
70
+ "ioc_type": "email",
71
+ "extra": [
72
+ {
73
+ "ioc": "email.local",
74
+ "ioc_type": "domain"
75
+ ...
76
+ ```
77
+
78
+ ### Categorize a **list** or **file** of indicators using `parse_multi()`
79
+
80
+ using `mode="combined"` produces one massive dictionary of categorized indicators
81
+ ```
82
+ >>> p = parse_multi(ioc_list, mode="combined")
83
+ # or
84
+ >>> p = parse_multi("ioc_examples.txt", mode="combined")
85
+ >>> import json
86
+ >>> print(json.dumps(p, indent=4))
87
+ {
88
+ "email": [
89
+ "xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai",
90
+ "alice@email.local",
91
+ ...
92
+ ```
93
+
94
+ using `mode="single"` produces a **list of dictionaries**, similar to calling `ParseIOC(ioc)` by itself in a loop and putting the results in a list
95
+ ```
96
+ >>> p = parse_multi("tests/ioc_examples.txt", mode="single")
97
+ >>> print(json.dumps(p, indent=4))
98
+ [
99
+ {
100
+ "ioc": "website.local",
101
+ "ioc_type": "domain"
102
+ },
103
+ {
104
+ "ioc": "anotherwebsite.local",
105
+ "ioc_type": "domain",
106
+ "extra": [
107
+ {
108
+ "ioc": 9443,
109
+ "ioc_type": "port"
110
+ }
111
+ ]
112
+ },
113
+ {
114
+ "ioc": "bob@email.local",
115
+ "ioc_type": "email",
116
+ "extra": [
117
+ {
118
+ "ioc": "email.local",
119
+ "ioc_type": "domain"
120
+ }
121
+ ```
122
+
123
+ ### Map a **list** or **file** of indicators to SIEM fields using `map_fields()`
124
+
125
+ - use the `map_ecs.toml` file as a reference or template
126
+
127
+ example shows parsing a file into [Elastic Common Schema (ECS)](https://www.elastic.co/docs/reference/ecs/ecs-field-reference) fields
128
+ ```
129
+ >>> m = map_fields("tests/ioc_examples.txt", "map_ecs.toml")
130
+ >>> print(json.dumps(m, indent=4))
131
+ {
132
+ "email.from.address": [
133
+ "xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai",
134
+ "alice@email.local",
135
+ "bob@emailsubdomain.bad.local.xn--p1ai",
136
+ "bob@email.local",
137
+ ...
138
+ ```
139
+
140
+ specify `keep_types=True` in `map_fields()` to nest the SIEM values under the original type categorizations
141
+ ```
142
+ >>> m = map_fields("tests/ioc_examples.txt", "map_ecs.toml", keep_types=True)
143
+ >>> print(json.dumps(m, indent=4))
144
+ {
145
+ "email": {
146
+ "email.from.address": [
147
+ "xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai",
148
+ "alice@email.local",
149
+ ...
150
+ ```
151
+
152
+ ### Create Sqlite database of parsed indicators using `to_sqlite()`
153
+ - table name: `indicators`
154
+ - columns: `ioc` (`TEXT`, primary key) and `type` (`TEXT`)
155
+ - uses `parse_multi(input_object, mode="combined")`
156
+ - `extra` data has its own row, not linked to its parent data
157
+ - ex. a port won't have any reference to the original URL it came from
158
+ ```
159
+ >>> to_sqlite("tests/ioc_examples.txt", "iocs.db")
160
+ exported 41 indicators to iocs.db
161
+ ```
162
+
163
+ example output using `sqlite3`
164
+ ```
165
+ $ sqlite3 iocs.db "SELECT ioc,type FROM indicators;"
166
+ xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai|email
167
+ alice@email.local|email
168
+ bob@emailsubdomain.bad.local.xn--p1ai|email
169
+ bob@email.local|email
170
+ 192.168.1.1|ipv4
171
+ ...
172
+ 9443|port
173
+ email.local|domain
174
+ ...
175
+ ```
176
+
177
+ ## Gotchas
178
+ - Domain names must have a period (`.`) otherwise the string will be considered a file
179
+ - This regex in the domain checker, determines if the parsed domain name drops further down to the `file` checker or not:
180
+ ```
181
+ file_extensions = re.compile(r"\.(?:exe|dll|msi|bat|cmd|elf|scr|cpl|ps1|vbs|pdf|docx|xlsx|pptx|doc|xls|ppt|rtf|csv|txt|log|xml|zip|rar|7z|tar|gz|iso|img|dmg|cab|png|jpg|jpeg|gif|ico|bmp|svg|mp3|mp4|wav|avi|mov|js|php|css|html|htm|sql|conf|ini|yaml|yml|json)$", re.IGNORECASE)
182
+ ```
183
+
184
+ ## Expected output from running `tests/parse_ioc.py` using example data
185
+ ```
186
+ $ uv run tests/parseonly.py -f tests/ioc_examples.txt -h
187
+ usage: parseonly.py [-h] -f FILENAME [-m] [-j] [-k]
188
+
189
+ parse indicators
190
+
191
+ options:
192
+ -h, --help show this help message and exit
193
+ -f, --file FILENAME input filename
194
+ -m, --map show mapped format
195
+ -j, --json display indented JSON
196
+ -k, --keep-types keep field types in output dict
197
+ ```
198
+
199
+ ```
200
+ $ uv run tests/parseonly.py -f tests/ioc_examples.txt -mjk
201
+ {
202
+ "email": {
203
+ "email.from.address": [
204
+ "xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai",
205
+ "bob@emailsubdomain.bad.local",
206
+ "alice@email.local",
207
+ "bob@emailsubdomain.bad.local.xn--p1ai",
208
+ "bob@email.local"
209
+ ],
210
+ "email.sender.address": [
211
+ "xn--bob--o4d3g5c@xn--c1a3a5a.xn--p1ai",
212
+ "bob@emailsubdomain.bad.local",
213
+ ...
214
+ "ipv4": {
215
+ "source.ip": [
216
+ "192.168.20.20",
217
+ "8.8.8.8",
218
+ "1.2.3.4",
219
+ "192.168.1.1"
220
+ ],
221
+ ...
222
+ "ipv4_network": {
223
+ "source.ip": [
224
+ "192.168.1.0/24",
225
+ "8.8.8.0/24"
226
+ ...
227
+ ```
228
+
229
+ ## TODO
230
+
231
+ - add optional type conversions to match TheHive5 default observable types
232
+ - change the list of categorizations in README to a markdown table, showing the converted types
233
+ - need to streamline and optimize code
234
+ - remove the filepath regex statements created by AI