synapse 2.169.0__py311-none-any.whl → 2.170.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 (39) hide show
  1. synapse/cortex.py +88 -2
  2. synapse/datamodel.py +5 -0
  3. synapse/lib/ast.py +70 -12
  4. synapse/lib/cell.py +77 -7
  5. synapse/lib/layer.py +75 -6
  6. synapse/lib/node.py +7 -0
  7. synapse/lib/snap.py +22 -4
  8. synapse/lib/storm.py +1 -1
  9. synapse/lib/stormlib/cortex.py +1 -1
  10. synapse/lib/stormlib/model.py +339 -40
  11. synapse/lib/stormtypes.py +58 -1
  12. synapse/lib/types.py +35 -0
  13. synapse/lib/version.py +2 -2
  14. synapse/lib/view.py +87 -14
  15. synapse/models/files.py +40 -0
  16. synapse/models/inet.py +8 -4
  17. synapse/models/infotech.py +355 -17
  18. synapse/tests/files/cpedata.json +525034 -0
  19. synapse/tests/test_cortex.py +99 -0
  20. synapse/tests/test_lib_ast.py +66 -0
  21. synapse/tests/test_lib_cell.py +112 -0
  22. synapse/tests/test_lib_layer.py +52 -1
  23. synapse/tests/test_lib_scrape.py +72 -71
  24. synapse/tests/test_lib_snap.py +16 -1
  25. synapse/tests/test_lib_storm.py +118 -0
  26. synapse/tests/test_lib_stormlib_cortex.py +15 -0
  27. synapse/tests/test_lib_stormlib_model.py +427 -0
  28. synapse/tests/test_lib_stormtypes.py +135 -14
  29. synapse/tests/test_lib_types.py +20 -0
  30. synapse/tests/test_lib_view.py +77 -0
  31. synapse/tests/test_model_files.py +51 -0
  32. synapse/tests/test_model_inet.py +63 -1
  33. synapse/tests/test_model_infotech.py +187 -26
  34. synapse/tests/utils.py +12 -0
  35. {synapse-2.169.0.dist-info → synapse-2.170.0.dist-info}/METADATA +1 -1
  36. {synapse-2.169.0.dist-info → synapse-2.170.0.dist-info}/RECORD +39 -38
  37. {synapse-2.169.0.dist-info → synapse-2.170.0.dist-info}/LICENSE +0 -0
  38. {synapse-2.169.0.dist-info → synapse-2.170.0.dist-info}/WHEEL +0 -0
  39. {synapse-2.169.0.dist-info → synapse-2.170.0.dist-info}/top_level.txt +0 -0
synapse/lib/view.py CHANGED
@@ -196,10 +196,6 @@ class View(s_nexus.Pusher): # type: ignore
196
196
  async def _setMergeRequest(self, mergeinfo):
197
197
  self.reqParentQuorum()
198
198
 
199
- if self.hasKids():
200
- mesg = 'Cannot add a merge request to a view with children.'
201
- raise s_exc.BadState(mesg=mesg)
202
-
203
199
  s_schemas.reqValidMerge(mergeinfo)
204
200
  lkey = self.bidn + b'merge:req'
205
201
  self.core.slab.put(lkey, s_msgpack.en(mergeinfo), db='view:meta')
@@ -450,8 +446,7 @@ class View(s_nexus.Pusher): # type: ignore
450
446
  await self.core.feedBeholder('view:merge:fini', {'view': self.iden, 'merge': merge, 'merge': merge, 'votes': votes})
451
447
 
452
448
  # remove the view and top layer
453
- await self.core.delView(self.iden)
454
- await self.core.delLayer(self.layers[0].iden)
449
+ await self.core.delViewWithLayer(self.iden)
455
450
 
456
451
  except Exception as e: # pragma: no cover
457
452
  logger.exception(f'Error while merging view: {self.iden}')
@@ -1112,10 +1107,6 @@ class View(s_nexus.Pusher): # type: ignore
1112
1107
  mesg = 'Circular dependency of view parents is not supported.'
1113
1108
  raise s_exc.BadArg(mesg=mesg)
1114
1109
 
1115
- if parent.getMergeRequest() is not None:
1116
- mesg = 'You may not set the parent to a view with a pending merge request.'
1117
- raise s_exc.BadState(mesg=mesg)
1118
-
1119
1110
  if self.parent is not None:
1120
1111
  if self.parent.iden == parent.iden:
1121
1112
  return valu
@@ -1254,6 +1245,92 @@ class View(s_nexus.Pusher): # type: ignore
1254
1245
 
1255
1246
  todo.append(child)
1256
1247
 
1248
+ async def insertParentFork(self, useriden, name=None):
1249
+ '''
1250
+ Insert a new View between a forked View and its parent.
1251
+
1252
+ Returns:
1253
+ New view definition with the same perms as the current fork.
1254
+ '''
1255
+ if not self.isafork():
1256
+ mesg = f'View ({self.iden}) is not a fork, cannot insert a new fork between it and parent.'
1257
+ raise s_exc.BadState(mesg=mesg)
1258
+
1259
+ ctime = s_common.now()
1260
+ layriden = s_common.guid()
1261
+
1262
+ ldef = {
1263
+ 'iden': layriden,
1264
+ 'created': ctime,
1265
+ 'creator': useriden,
1266
+ 'lockmemory': self.core.conf.get('layers:lockmemory'),
1267
+ 'logedits': self.core.conf.get('layers:logedits'),
1268
+ 'readonly': False
1269
+ }
1270
+
1271
+ vdef = {
1272
+ 'iden': s_common.guid(),
1273
+ 'created': ctime,
1274
+ 'creator': useriden,
1275
+ 'parent': self.parent.iden,
1276
+ 'layers': [layriden] + [lyr.iden for lyr in self.parent.layers]
1277
+ }
1278
+
1279
+ if name is not None:
1280
+ vdef['name'] = name
1281
+
1282
+ s_layer.reqValidLdef(ldef)
1283
+ s_schemas.reqValidView(vdef)
1284
+
1285
+ return await self._push('view:forkparent', ldef, vdef)
1286
+
1287
+ @s_nexus.Pusher.onPush('view:forkparent', passitem=True)
1288
+ async def _insertParentFork(self, ldef, vdef, nexsitem):
1289
+
1290
+ s_layer.reqValidLdef(ldef)
1291
+ s_schemas.reqValidView(vdef)
1292
+
1293
+ if self.getMergeRequest() is not None:
1294
+ await self._delMergeRequest()
1295
+
1296
+ await self.core._addLayer(ldef, nexsitem)
1297
+ await self.core._addView(vdef)
1298
+
1299
+ forkiden = vdef.get('iden')
1300
+ self.parent = self.core.reqView(forkiden)
1301
+ await self.info.set('parent', forkiden)
1302
+
1303
+ await self._calcForkLayers()
1304
+
1305
+ for view in self.core.views.values():
1306
+ if view.isForkOf(self.iden):
1307
+ await view._calcForkLayers()
1308
+
1309
+ self.core._calcViewsByLayer()
1310
+
1311
+ authgate = await self.core.getAuthGate(self.iden)
1312
+ if authgate is None: # pragma: no cover
1313
+ return await self.parent.pack()
1314
+
1315
+ for userinfo in authgate.get('users'):
1316
+ useriden = userinfo.get('iden')
1317
+ if (user := self.core.auth.user(useriden)) is None: # pragma: no cover
1318
+ logger.warning(f'View {self.iden} AuthGate refers to unknown user {useriden}')
1319
+ continue
1320
+
1321
+ await user.setRules(userinfo.get('rules'), gateiden=forkiden, nexs=False)
1322
+ await user.setAdmin(userinfo.get('admin'), gateiden=forkiden, logged=False)
1323
+
1324
+ for roleinfo in authgate.get('roles'):
1325
+ roleiden = roleinfo.get('iden')
1326
+ if (role := self.core.auth.role(roleiden)) is None: # pragma: no cover
1327
+ logger.warning(f'View {self.iden} AuthGate refers to unknown role {roleiden}')
1328
+ continue
1329
+
1330
+ await role.setRules(roleinfo.get('rules'), gateiden=forkiden, nexs=False)
1331
+
1332
+ return await self.parent.pack()
1333
+
1257
1334
  async def fork(self, ldef=None, vdef=None):
1258
1335
  '''
1259
1336
  Make a new view inheriting from this view with the same layers and a new write layer on top
@@ -1272,10 +1349,6 @@ class View(s_nexus.Pusher): # type: ignore
1272
1349
  if vdef is None:
1273
1350
  vdef = {}
1274
1351
 
1275
- if self.getMergeRequest() is not None:
1276
- mesg = 'Cannot fork a view which has a merge request.'
1277
- raise s_exc.BadState(mesg=mesg)
1278
-
1279
1352
  ldef = await self.core.addLayer(ldef)
1280
1353
  layriden = ldef.get('iden')
1281
1354
 
synapse/models/files.py CHANGED
@@ -408,6 +408,9 @@ class FileModule(s_module.CoreModule):
408
408
  'doc': 'A section inside a Mach-O binary denoting a named region of bytes inside a segment.',
409
409
  }),
410
410
 
411
+ ('file:mime:lnk', ('guid', {}), {
412
+ 'doc': 'The GUID of the metadata pulled from a Windows shortcut or LNK file.',
413
+ }),
411
414
  ),
412
415
 
413
416
  'forms': (
@@ -699,6 +702,43 @@ class FileModule(s_module.CoreModule):
699
702
  'doc': 'The file offset to the beginning of the section'}),
700
703
  )),
701
704
 
705
+ ('file:mime:lnk', {}, (
706
+ ('flags', ('int', {}), {
707
+ 'doc': 'The flags specified by the LNK header that control the structure of the LNK file.'}),
708
+ ('entry:primary', ('file:path', {}), {
709
+ 'doc': 'The primary file path contained within the FileEntry structure of the LNK file.'}),
710
+ ('entry:secondary', ('file:path', {}), {
711
+ 'doc': 'The secondary file path contained within the FileEntry structure of the LNK file.'}),
712
+ ('entry:extended', ('file:path', {}), {
713
+ 'doc': 'The extended file path contained within the extended FileEntry structure of the LNK file.'}),
714
+ ('entry:localized', ('file:path', {}), {
715
+ 'doc': 'The localized file path contained within the extended FileEntry structure of the LNK file.'}),
716
+ ('entry:icon', ('file:path', {}), {
717
+ 'doc': 'The icon file path contained within the StringData structure of the LNK file.'}),
718
+ ('environment:path', ('file:path', {}), {
719
+ 'doc': 'The target file path contained within the EnvironmentVariableDataBlock structure of the LNK file.'}),
720
+ ('environment:icon', ('file:path', {}), {
721
+ 'doc': 'The icon file path contained within the IconEnvironmentDataBlock structure of the LNK file.'}),
722
+ ('working', ('file:path', {}), {
723
+ 'doc': 'The working directory used when activating the link target.'}),
724
+ ('relative', ('str', {'strip': True}), {
725
+ 'doc': 'The relative target path string contained within the StringData structure of the LNK file.'}),
726
+ ('arguments', ('it:cmd', {}), {
727
+ 'doc': 'The command line arguments passed to the target file when the LNK file is activated.'}),
728
+ ('desc', ('str', {}), {
729
+ 'disp': {'hint': 'text'},
730
+ 'doc': 'The description of the LNK file contained within the StringData section of the LNK file.'}),
731
+ ('target:attrs', ('int', {}), {
732
+ 'doc': 'The attributes of the target file according to the LNK header.'}),
733
+ ('target:size', ('int', {}), {
734
+ 'doc': 'The size of the target file according to the LNK header. The LNK format specifies that this is only the lower 32 bits of the target file size.'}),
735
+ ('target:created', ('time', {}), {
736
+ 'doc': 'The creation time of the target file according to the LNK header.'}),
737
+ ('target:accessed', ('time', {}), {
738
+ 'doc': 'The access time of the target file according to the LNK header.'}),
739
+ ('target:written', ('time', {}), {
740
+ 'doc': 'The write time of the target file according to the LNK header.'}),
741
+ )),
702
742
  ),
703
743
 
704
744
  }
synapse/models/inet.py CHANGED
@@ -23,7 +23,7 @@ import synapse.lookup.iana as s_l_iana
23
23
  logger = logging.getLogger(__name__)
24
24
  drivre = regex.compile(r'^\w[:|]')
25
25
  fqdnre = regex.compile(r'^[\w._-]+$', regex.U)
26
- srv6re = regex.compile(r'^\[([a-f0-9\.:]+)\]:(\d+)$')
26
+ srv6re = regex.compile(r'^\[([a-f0-9\.:]+)\](?::(\d+))?$', regex.IGNORECASE)
27
27
 
28
28
  udots = regex.compile(r'[\u3002\uff0e\uff61]')
29
29
 
@@ -142,11 +142,15 @@ class Addr(s_types.Str):
142
142
  if v6v4addr is not None:
143
143
  subs['ipv4'] = v6v4addr
144
144
 
145
- port = self.modl.type('inet:port').norm(port)[0]
146
145
  subs['ipv6'] = ipv6
147
- subs['port'] = port
148
146
 
149
- return f'{proto}://[{ipv6}]:{port}', {'subs': subs}
147
+ portstr = ''
148
+ if port is not None:
149
+ port = self.modl.type('inet:port').norm(port)[0]
150
+ subs['port'] = port
151
+ portstr = f':{port}'
152
+
153
+ return f'{proto}://[{ipv6}]{portstr}', {'subs': subs}
150
154
 
151
155
  mesg = f'Invalid IPv6 w/port ({orig})'
152
156
  raise s_exc.BadTypeValu(valu=orig, name=self.name, mesg=mesg)
@@ -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}), {