synapse 2.169.0__py311-none-any.whl → 2.171.0__py311-none-any.whl

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 synapse might be problematic. Click here for more details.

Files changed (41) hide show
  1. synapse/cortex.py +99 -3
  2. synapse/datamodel.py +5 -0
  3. synapse/lib/ast.py +70 -12
  4. synapse/lib/cell.py +76 -7
  5. synapse/lib/layer.py +75 -6
  6. synapse/lib/lmdbslab.py +17 -0
  7. synapse/lib/node.py +7 -0
  8. synapse/lib/snap.py +22 -4
  9. synapse/lib/storm.py +1 -1
  10. synapse/lib/stormlib/cortex.py +1 -1
  11. synapse/lib/stormlib/model.py +339 -40
  12. synapse/lib/stormtypes.py +58 -1
  13. synapse/lib/types.py +36 -1
  14. synapse/lib/version.py +2 -2
  15. synapse/lib/view.py +94 -15
  16. synapse/models/files.py +40 -0
  17. synapse/models/inet.py +8 -4
  18. synapse/models/infotech.py +355 -17
  19. synapse/tests/files/cpedata.json +525034 -0
  20. synapse/tests/test_cortex.py +108 -0
  21. synapse/tests/test_lib_ast.py +66 -0
  22. synapse/tests/test_lib_cell.py +112 -0
  23. synapse/tests/test_lib_layer.py +52 -1
  24. synapse/tests/test_lib_lmdbslab.py +36 -0
  25. synapse/tests/test_lib_scrape.py +72 -71
  26. synapse/tests/test_lib_snap.py +16 -1
  27. synapse/tests/test_lib_storm.py +118 -0
  28. synapse/tests/test_lib_stormlib_cortex.py +15 -0
  29. synapse/tests/test_lib_stormlib_model.py +427 -0
  30. synapse/tests/test_lib_stormtypes.py +147 -15
  31. synapse/tests/test_lib_types.py +21 -0
  32. synapse/tests/test_lib_view.py +77 -0
  33. synapse/tests/test_model_files.py +52 -0
  34. synapse/tests/test_model_inet.py +63 -1
  35. synapse/tests/test_model_infotech.py +187 -26
  36. synapse/tests/utils.py +42 -9
  37. {synapse-2.169.0.dist-info → synapse-2.171.0.dist-info}/METADATA +1 -1
  38. {synapse-2.169.0.dist-info → synapse-2.171.0.dist-info}/RECORD +41 -40
  39. {synapse-2.169.0.dist-info → synapse-2.171.0.dist-info}/LICENSE +0 -0
  40. {synapse-2.169.0.dist-info → synapse-2.171.0.dist-info}/WHEEL +0 -0
  41. {synapse-2.169.0.dist-info → synapse-2.171.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,10 @@
1
+ import copy
2
+ import string
1
3
  import asyncio
2
4
  import logging
3
5
 
6
+ import regex
7
+
4
8
  import synapse.exc as s_exc
5
9
  import synapse.data as s_data
6
10
 
@@ -9,10 +13,53 @@ import synapse.common as s_common
9
13
  import synapse.lib.chop as s_chop
10
14
  import synapse.lib.types as s_types
11
15
  import synapse.lib.module as s_module
16
+ import synapse.lib.scrape as s_scrape
12
17
  import synapse.lib.version as s_version
13
18
 
14
19
  logger = logging.getLogger(__name__)
15
20
 
21
+ # This is the regular expression pattern for CPE2.2. It's kind of a hybrid
22
+ # between compatible binding and preferred binding. Differences are here:
23
+ # - Use only the list of percent encoded values specified by preferred binding.
24
+ # This is to ensure it converts properly to CPE2.3.
25
+ # - Add tilde (~) to the UNRESERVED list which removes the need to specify the
26
+ # PACKED encoding specifically.
27
+ ALPHA = '[A-Za-z]'
28
+ DIGIT = '[0-9]'
29
+ UNRESERVED = r'[A-Za-z0-9\-\.\_~]'
30
+ SPEC1 = '%01'
31
+ SPEC2 = '%02'
32
+ # This is defined in the ABNF but not actually referenced
33
+ # SPECIAL = f'(?:{SPEC1}|{SPEC2})'
34
+ SPEC_CHRS = f'(?:{SPEC1}+|{SPEC2})'
35
+ PCT_ENCODED = '%(?:21|22|23|24|25|26|27|28|28|29|2a|2b|2c|2f|3a|3b|3c|3d|3e|3f|40|5b|5c|5d|5e|60|7b|7c|7d|7e)'
36
+ STR_WO_SPECIAL = f'(?:{UNRESERVED}|{PCT_ENCODED})*'
37
+ STR_W_SPECIAL = f'{SPEC_CHRS}? (?:{UNRESERVED}|{PCT_ENCODED})+ {SPEC_CHRS}?'
38
+ STRING = f'(?:{STR_W_SPECIAL}|{STR_WO_SPECIAL})'
39
+ REGION = f'(?:{ALPHA}{{2}}|{DIGIT}{{3}})'
40
+ LANGTAG = rf'(?:{ALPHA}{{2,3}}(?:\-{REGION})?)'
41
+ PART = '[hoa]?'
42
+ VENDOR = STRING
43
+ PRODUCT = STRING
44
+ VERSION = STRING
45
+ UPDATE = STRING
46
+ EDITION = STRING
47
+ LANG = f'{LANGTAG}?'
48
+ COMPONENT_LIST = f'''
49
+ (?:
50
+ {PART}:{VENDOR}:{PRODUCT}:{VERSION}:{UPDATE}:{EDITION}:{LANG} |
51
+ {PART}:{VENDOR}:{PRODUCT}:{VERSION}:{UPDATE}:{EDITION} |
52
+ {PART}:{VENDOR}:{PRODUCT}:{VERSION}:{UPDATE} |
53
+ {PART}:{VENDOR}:{PRODUCT}:{VERSION} |
54
+ {PART}:{VENDOR}:{PRODUCT} |
55
+ {PART}:{VENDOR} |
56
+ {PART}
57
+ )
58
+ '''
59
+
60
+ cpe22_regex = regex.compile(f'cpe:/{COMPONENT_LIST}', regex.VERBOSE | regex.IGNORECASE)
61
+ cpe23_regex = regex.compile(s_scrape._cpe23_regex, regex.VERBOSE | regex.IGNORECASE)
62
+
16
63
  def cpesplit(text):
17
64
  part = ''
18
65
  parts = []
@@ -36,7 +83,160 @@ def cpesplit(text):
36
83
  except StopIteration:
37
84
  parts.append(part)
38
85
 
39
- return parts
86
+ return [part.strip() for part in parts]
87
+
88
+ # Formatted String Binding characters that need to be escaped
89
+ FSB_ESCAPE_CHARS = [
90
+ '!', '"', '#', '$', '%', '&', "'", '(', ')',
91
+ '+', ',', '/', ':', ';', '<', '=', '>', '@',
92
+ '[', ']', '^', '`', '{', '|', '}', '~',
93
+ '\\', '?', '*'
94
+ ]
95
+
96
+ FSB_VALID_CHARS = ['-', '.', '_']
97
+ FSB_VALID_CHARS.extend(string.ascii_letters)
98
+ FSB_VALID_CHARS.extend(string.digits)
99
+ FSB_VALID_CHARS.extend(FSB_ESCAPE_CHARS)
100
+
101
+ def fsb_escape(text):
102
+ ret = ''
103
+ if text in ('*', '-'):
104
+ return text
105
+
106
+ # Check validity of text first
107
+ if (invalid := [char for char in text if char not in FSB_VALID_CHARS]):
108
+ badchars = ', '.join(invalid)
109
+ mesg = f'Invalid CPE 2.3 character(s) ({badchars}) detected.'
110
+ raise s_exc.BadTypeValu(mesg=mesg, valu=text)
111
+
112
+ textlen = len(text)
113
+
114
+ for idx, char in enumerate(text):
115
+ if char not in FSB_ESCAPE_CHARS:
116
+ ret += char
117
+ continue
118
+
119
+ escchar = f'\\{char}'
120
+
121
+ # The only character in the string
122
+ if idx == 0 and idx == textlen - 1:
123
+ ret += escchar
124
+ continue
125
+
126
+ # Handle the backslash as a special case
127
+ if char == '\\':
128
+ if idx == 0:
129
+ # Its the first character and escaping another special character
130
+ if text[idx + 1] in FSB_ESCAPE_CHARS:
131
+ ret += char
132
+ else:
133
+ ret += escchar
134
+
135
+ continue
136
+
137
+ if idx == textlen - 1:
138
+ # Its the last character and being escaped
139
+ if text[idx - 1] == '\\':
140
+ ret += char
141
+ else:
142
+ ret += escchar
143
+
144
+ continue
145
+
146
+ # The backslash is in the middle somewhere
147
+
148
+ # It's already escaped or it's escaping a special char
149
+ if text[idx - 1] == '\\' or text[idx + 1] in FSB_ESCAPE_CHARS:
150
+ ret += char
151
+ continue
152
+
153
+ # Lone backslash, escape it and move on
154
+ ret += escchar
155
+ continue
156
+
157
+ # First char, no look behind
158
+ if idx == 0:
159
+ # Escape the first character and go around
160
+ ret += escchar
161
+ continue
162
+
163
+ escaped = text[idx - 1] == '\\'
164
+
165
+ if not escaped:
166
+ ret += escchar
167
+ continue
168
+
169
+ ret += char
170
+
171
+ return ret
172
+
173
+ def fsb_unescape(text):
174
+ ret = ''
175
+ textlen = len(text)
176
+
177
+ for idx, char in enumerate(text):
178
+ # The last character so we can't look ahead
179
+ if idx == textlen - 1:
180
+ ret += char
181
+ continue
182
+
183
+ if char == '\\' and text[idx + 1] in FSB_ESCAPE_CHARS:
184
+ continue
185
+
186
+ ret += char
187
+
188
+ return ret
189
+
190
+ # URI Binding characters that can be encoded in percent format
191
+ URI_PERCENT_CHARS = [
192
+ # Do the percent first so we don't double encode by accident
193
+ ('%25', '%'),
194
+ ('%21', '!'), ('%22', '"'), ('%23', '#'), ('%24', '$'), ('%26', '&'), ('%27', "'"),
195
+ ('%28', '('), ('%29', ')'), ('%2a', '*'), ('%2b', '+'), ('%2c', ','), ('%2f', '/'), ('%3a', ':'),
196
+ ('%3b', ';'), ('%3c', '<'), ('%3d', '='), ('%3e', '>'), ('%3f', '?'), ('%40', '@'), ('%5b', '['),
197
+ ('%5c', '\\'), ('%5d', ']'), ('%5e', '^'), ('%60', '`'), ('%7b', '{'), ('%7c', '|'), ('%7d', '}'),
198
+ ('%7e', '~'),
199
+ ]
200
+
201
+ def uri_quote(text):
202
+ ret = ''
203
+ for (pct, char) in URI_PERCENT_CHARS:
204
+ text = text.replace(char, pct)
205
+ return text
206
+
207
+ def uri_unquote(text):
208
+ # iterate backwards so we do the % last to avoid double unquoting
209
+ # example: "%2521" would turn into "%21" which would then replace into "!"
210
+ for (pct, char) in URI_PERCENT_CHARS[::-1]:
211
+ text = text.replace(pct, char)
212
+ return text
213
+
214
+ UNSPECIFIED = ('', '*')
215
+ def uri_pack(edition, sw_edition, target_sw, target_hw, other):
216
+ # If the four extended attributes are unspecified, only return the edition value
217
+ if (sw_edition in UNSPECIFIED and target_sw in UNSPECIFIED and target_hw in UNSPECIFIED and other in UNSPECIFIED):
218
+ return edition
219
+
220
+ ret = [edition, '', '', '', '']
221
+
222
+ if sw_edition not in UNSPECIFIED:
223
+ ret[1] = sw_edition
224
+
225
+ if target_sw not in UNSPECIFIED:
226
+ ret[2] = target_sw
227
+
228
+ if target_hw not in UNSPECIFIED:
229
+ ret[3] = target_hw
230
+
231
+ if other not in UNSPECIFIED:
232
+ ret[4] = other
233
+
234
+ return '~' + '~'.join(ret)
235
+
236
+ def uri_unpack(edition):
237
+ if edition.startswith('~') and edition.count('~') == 5:
238
+ return edition[1:].split('~', 5)
239
+ return None
40
240
 
41
241
  class Cpe22Str(s_types.Str):
42
242
  '''
@@ -60,7 +260,14 @@ class Cpe22Str(s_types.Str):
60
260
  mesg = 'CPE 2.2 string is expected to start with "cpe:/"'
61
261
  raise s_exc.BadTypeValu(valu=valu, mesg=mesg)
62
262
 
63
- return zipCpe22(parts), {}
263
+ v2_2 = zipCpe22(parts)
264
+
265
+ rgx = cpe22_regex.match(v2_2)
266
+ if rgx is None or rgx.group() != v2_2:
267
+ mesg = 'CPE 2.2 string appears to be invalid.'
268
+ raise s_exc.BadTypeValu(mesg=mesg, valu=valu)
269
+
270
+ return v2_2, {}
64
271
 
65
272
  def _normPyList(self, parts):
66
273
  return zipCpe22(parts), {}
@@ -77,7 +284,7 @@ def chopCpe22(text):
77
284
  CPE 2.2 Formatted String
78
285
  https://cpe.mitre.org/files/cpe-specification_2.2.pdf
79
286
  '''
80
- if not text.startswith('cpe:/'):
287
+ if not text.startswith('cpe:/'): # pragma: no cover
81
288
  mesg = 'CPE 2.2 string is expected to start with "cpe:/"'
82
289
  raise s_exc.BadTypeValu(valu=text, mesg=mesg)
83
290
 
@@ -89,6 +296,18 @@ def chopCpe22(text):
89
296
 
90
297
  return parts
91
298
 
299
+ PART_IDX_PART = 0
300
+ PART_IDX_VENDOR = 1
301
+ PART_IDX_PRODUCT = 2
302
+ PART_IDX_VERSION = 3
303
+ PART_IDX_UPDATE = 4
304
+ PART_IDX_EDITION = 5
305
+ PART_IDX_LANG = 6
306
+ PART_IDX_SW_EDITION = 7
307
+ PART_IDX_TARGET_SW = 8
308
+ PART_IDX_TARGET_HW = 9
309
+ PART_IDX_OTHER = 10
310
+
92
311
  class Cpe23Str(s_types.Str):
93
312
  '''
94
313
  CPE 2.3 Formatted String
@@ -119,31 +338,113 @@ class Cpe23Str(s_types.Str):
119
338
 
120
339
  extsize = 11 - len(parts)
121
340
  parts.extend(['*' for _ in range(extsize)])
341
+
342
+ v2_3 = 'cpe:2.3:' + ':'.join(parts)
343
+
344
+ v2_2 = copy.copy(parts)
345
+ for idx, part in enumerate(v2_2):
346
+ if part == '*':
347
+ v2_2[idx] = ''
348
+ continue
349
+
350
+ part = fsb_unescape(part)
351
+ v2_2[idx] = uri_quote(part)
352
+
353
+ v2_2[PART_IDX_EDITION] = uri_pack(
354
+ v2_2[PART_IDX_EDITION],
355
+ v2_2[PART_IDX_SW_EDITION],
356
+ v2_2[PART_IDX_TARGET_SW],
357
+ v2_2[PART_IDX_TARGET_HW],
358
+ v2_2[PART_IDX_OTHER]
359
+ )
360
+
361
+ v2_2 = v2_2[:7]
362
+
363
+ parts = [fsb_unescape(k) for k in parts]
364
+
122
365
  elif text.startswith('cpe:/'):
366
+
367
+ v2_2 = text
123
368
  # automatically normalize CPE 2.2 format to CPE 2.3
124
369
  parts = chopCpe22(text)
370
+
371
+ # Account for blank fields
372
+ for idx, part in enumerate(parts):
373
+ if not part:
374
+ parts[idx] = '*'
375
+
125
376
  extsize = 11 - len(parts)
126
377
  parts.extend(['*' for _ in range(extsize)])
378
+
379
+ # URI bindings can pack extended attributes into the
380
+ # edition field, handle that here.
381
+ unpacked = uri_unpack(parts[PART_IDX_EDITION])
382
+ if unpacked:
383
+ (edition, sw_edition, target_sw, target_hw, other) = unpacked
384
+
385
+ if edition:
386
+ parts[PART_IDX_EDITION] = edition
387
+ else:
388
+ parts[PART_IDX_EDITION] = '*'
389
+
390
+ if sw_edition:
391
+ parts[PART_IDX_SW_EDITION] = sw_edition
392
+
393
+ if target_sw:
394
+ parts[PART_IDX_TARGET_SW] = target_sw
395
+
396
+ if target_hw:
397
+ parts[PART_IDX_TARGET_HW] = target_hw
398
+
399
+ if other:
400
+ parts[PART_IDX_OTHER] = other
401
+
402
+ parts = [uri_unquote(part) for part in parts]
403
+
404
+ # This feels a little uninuitive to escape parts for "escaped" and
405
+ # unescape parts for "parts" but values in parts could be incorrectly
406
+ # escaped or incorrectly unescaped so just do both.
407
+ escaped = [fsb_escape(part) for part in parts]
408
+ parts = [fsb_unescape(part) for part in parts]
409
+
410
+ v2_3 = 'cpe:2.3:' + ':'.join(escaped)
411
+
127
412
  else:
128
413
  mesg = 'CPE 2.3 string is expected to start with "cpe:2.3:"'
129
414
  raise s_exc.BadTypeValu(valu=valu, mesg=mesg)
130
415
 
416
+ rgx = cpe23_regex.match(v2_3)
417
+ if rgx is None or rgx.group() != v2_3:
418
+ mesg = 'CPE 2.3 string appears to be invalid.'
419
+ raise s_exc.BadTypeValu(mesg=mesg, valu=valu)
420
+
421
+ if isinstance(v2_2, list):
422
+ cpe22 = zipCpe22(v2_2)
423
+ else:
424
+ cpe22 = v2_2
425
+
426
+ rgx = cpe22_regex.match(cpe22)
427
+ if rgx is None or rgx.group() != cpe22:
428
+ v2_2 = None
429
+
131
430
  subs = {
132
- 'v2_2': parts,
133
- 'part': parts[0],
134
- 'vendor': parts[1],
135
- 'product': parts[2],
136
- 'version': parts[3],
137
- 'update': parts[4],
138
- 'edition': parts[5],
139
- 'language': parts[6],
140
- 'sw_edition': parts[7],
141
- 'target_sw': parts[8],
142
- 'target_hw': parts[9],
143
- 'other': parts[10],
431
+ 'part': parts[PART_IDX_PART],
432
+ 'vendor': parts[PART_IDX_VENDOR],
433
+ 'product': parts[PART_IDX_PRODUCT],
434
+ 'version': parts[PART_IDX_VERSION],
435
+ 'update': parts[PART_IDX_UPDATE],
436
+ 'edition': parts[PART_IDX_EDITION],
437
+ 'language': parts[PART_IDX_LANG],
438
+ 'sw_edition': parts[PART_IDX_SW_EDITION],
439
+ 'target_sw': parts[PART_IDX_TARGET_SW],
440
+ 'target_hw': parts[PART_IDX_TARGET_HW],
441
+ 'other': parts[PART_IDX_OTHER],
144
442
  }
145
443
 
146
- return 'cpe:2.3:' + ':'.join(parts), {'subs': subs}
444
+ if v2_2 is not None:
445
+ subs['v2_2'] = v2_2
446
+
447
+ return v2_3, {'subs': subs}
147
448
 
148
449
  class SemVer(s_types.Int):
149
450
  '''
@@ -412,6 +713,13 @@ class ItModule(s_module.CoreModule):
412
713
  'doc': 'A MITRE ATT&CK Campaign ID.',
413
714
  'ex': 'C0028',
414
715
  }),
716
+ ('it:mitre:attack:datasource', ('str', {'regex': r'^DS[0-9]{4}$'}), {
717
+ 'doc': 'A MITRE ATT&CK Datasource ID.',
718
+ 'ex': 'DS0026',
719
+ }),
720
+ ('it:mitre:attack:data:component', ('guid', {}), {
721
+ 'doc': 'A MITRE ATT&CK data component.',
722
+ }),
415
723
  ('it:mitre:attack:flow', ('guid', {}), {
416
724
  'doc': 'A MITRE ATT&CK Flow diagram.',
417
725
  }),
@@ -1216,6 +1524,10 @@ class ItModule(s_module.CoreModule):
1216
1524
  'uniq': True, 'sorted': True, 'split': ','}), {
1217
1525
  'doc': 'An array of ATT&CK tactics that include this technique.',
1218
1526
  }),
1527
+ ('data:components', ('array', {'type': 'it:mitre:attack:data:component',
1528
+ 'uniq': True, 'sorted': True}), {
1529
+ 'doc': 'An array of MITRE ATT&CK data components that detect the ATT&CK technique.',
1530
+ }),
1219
1531
  )),
1220
1532
  ('it:mitre:attack:software', {}, (
1221
1533
  ('software', ('it:prod:soft', {}), {
@@ -1335,6 +1647,27 @@ class ItModule(s_module.CoreModule):
1335
1647
  ('author:contact', ('ps:contact', {}), {
1336
1648
  'doc': 'The contact information for the author of the ATT&CK Flow diagram.'}),
1337
1649
  )),
1650
+ ('it:mitre:attack:datasource', {}, (
1651
+ ('name', ('str', {'lower': True, 'onespace': True}), {
1652
+ 'doc': 'The name of the datasource.'}),
1653
+ ('description', ('str', {}), {
1654
+ 'disp': {'hint': 'text'},
1655
+ 'doc': 'A description of the datasource.'}),
1656
+ ('references', ('array', {'type': 'inet:url', 'uniq': True, 'sorted': True}), {
1657
+ 'doc': 'An array of URLs that document the datasource.',
1658
+ }),
1659
+ )),
1660
+ ('it:mitre:attack:data:component', {}, (
1661
+ ('name', ('str', {'lower': True, 'onespace': True}), {
1662
+ 'ro': True,
1663
+ 'doc': 'The name of the data component.'}),
1664
+ ('description', ('str', {}), {
1665
+ 'disp': {'hint': 'text'},
1666
+ 'doc': 'A description of the data component.'}),
1667
+ ('datasource', ('it:mitre:attack:datasource', {}), {
1668
+ 'ro': True,
1669
+ 'doc': 'The datasource this data component belongs to.'}),
1670
+ )),
1338
1671
  ('it:dev:int', {}, ()),
1339
1672
  ('it:dev:pipe', {}, ()),
1340
1673
  ('it:dev:mutex', {}, ()),
@@ -1573,8 +1906,13 @@ class ItModule(s_module.CoreModule):
1573
1906
  'doc': 'A brief description of the hardware.'}),
1574
1907
  ('cpe', ('it:sec:cpe', {}), {
1575
1908
  'doc': 'The NIST CPE 2.3 string specifying this hardware.'}),
1909
+ ('manufacturer', ('ou:org', {}), {
1910
+ 'doc': 'The organization that manufactures this hardware.'}),
1911
+ ('manufacturer:name', ('ou:name', {}), {
1912
+ 'doc': 'The name of the organization that manufactures this hardware.'}),
1576
1913
  ('make', ('ou:name', {}), {
1577
- 'doc': 'The name of the organization which manufactures this hardware.'}),
1914
+ 'deprecated': True,
1915
+ 'doc': 'Deprecated. Please use :manufacturer:name.'}),
1578
1916
  ('model', ('str', {'lower': True, 'onespace': True}), {
1579
1917
  'doc': 'The model name or number for this hardware specification.'}),
1580
1918
  ('version', ('str', {'lower': True, 'onespace': True}), {