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.
- synapse/cortex.py +88 -2
- synapse/datamodel.py +5 -0
- synapse/lib/ast.py +70 -12
- synapse/lib/cell.py +77 -7
- synapse/lib/layer.py +75 -6
- synapse/lib/node.py +7 -0
- synapse/lib/snap.py +22 -4
- synapse/lib/storm.py +1 -1
- synapse/lib/stormlib/cortex.py +1 -1
- synapse/lib/stormlib/model.py +339 -40
- synapse/lib/stormtypes.py +58 -1
- synapse/lib/types.py +35 -0
- synapse/lib/version.py +2 -2
- synapse/lib/view.py +87 -14
- synapse/models/files.py +40 -0
- synapse/models/inet.py +8 -4
- synapse/models/infotech.py +355 -17
- synapse/tests/files/cpedata.json +525034 -0
- synapse/tests/test_cortex.py +99 -0
- synapse/tests/test_lib_ast.py +66 -0
- synapse/tests/test_lib_cell.py +112 -0
- synapse/tests/test_lib_layer.py +52 -1
- synapse/tests/test_lib_scrape.py +72 -71
- synapse/tests/test_lib_snap.py +16 -1
- synapse/tests/test_lib_storm.py +118 -0
- synapse/tests/test_lib_stormlib_cortex.py +15 -0
- synapse/tests/test_lib_stormlib_model.py +427 -0
- synapse/tests/test_lib_stormtypes.py +135 -14
- synapse/tests/test_lib_types.py +20 -0
- synapse/tests/test_lib_view.py +77 -0
- synapse/tests/test_model_files.py +51 -0
- synapse/tests/test_model_inet.py +63 -1
- synapse/tests/test_model_infotech.py +187 -26
- synapse/tests/utils.py +12 -0
- {synapse-2.169.0.dist-info → synapse-2.170.0.dist-info}/METADATA +1 -1
- {synapse-2.169.0.dist-info → synapse-2.170.0.dist-info}/RECORD +39 -38
- {synapse-2.169.0.dist-info → synapse-2.170.0.dist-info}/LICENSE +0 -0
- {synapse-2.169.0.dist-info → synapse-2.170.0.dist-info}/WHEEL +0 -0
- {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.
|
|
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\.:]+)\]
|
|
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
|
-
|
|
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)
|
synapse/models/infotech.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
'
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
'
|
|
136
|
-
'
|
|
137
|
-
'
|
|
138
|
-
'
|
|
139
|
-
'
|
|
140
|
-
'
|
|
141
|
-
'
|
|
142
|
-
'
|
|
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
|
-
|
|
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
|
-
'
|
|
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}), {
|