roadrecon 1.3.0__py3-none-any.whl → 1.5.0__py3-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.
- {roadrecon-1.3.0.dist-info → roadrecon-1.5.0.dist-info}/METADATA +1 -1
- {roadrecon-1.3.0.dist-info → roadrecon-1.5.0.dist-info}/RECORD +12 -12
- {roadrecon-1.3.0.dist-info → roadrecon-1.5.0.dist-info}/WHEEL +1 -1
- roadtools/roadrecon/dist_gui/index.html +3 -3
- roadtools/roadrecon/dist_gui/{main.049916c38aa43f94.js → main.9eac9b8f6864a9cc.js} +1 -1
- roadtools/roadrecon/gather.py +1 -0
- roadtools/roadrecon/main.py +5 -0
- roadtools/roadrecon/plugins/policies.py +24 -7
- roadtools/roadrecon/plugins/road2timeline.py +2 -2
- roadtools/roadrecon/server.py +29 -5
- {roadrecon-1.3.0.dist-info → roadrecon-1.5.0.dist-info}/entry_points.txt +0 -0
- {roadrecon-1.3.0.dist-info → roadrecon-1.5.0.dist-info}/top_level.txt +0 -0
roadtools/roadrecon/gather.py
CHANGED
|
@@ -615,6 +615,7 @@ async def run(args):
|
|
|
615
615
|
|
|
616
616
|
tasks = []
|
|
617
617
|
dumper.session = dbsession
|
|
618
|
+
# pylint: disable=not-callable
|
|
618
619
|
totalgroups = dbsession.query(func.count(Group.objectId)).scalar()
|
|
619
620
|
totaldevices = dbsession.query(func.count(Device.objectId)).scalar()
|
|
620
621
|
if totalgroups > MAX_GROUPS:
|
roadtools/roadrecon/main.py
CHANGED
|
@@ -65,6 +65,11 @@ def main():
|
|
|
65
65
|
gui_parser.add_argument('--profile',
|
|
66
66
|
action='store_true',
|
|
67
67
|
help='Enable flask profiler')
|
|
68
|
+
gui_parser.add_argument('--port',
|
|
69
|
+
type=int,
|
|
70
|
+
action='store',
|
|
71
|
+
help='HTTP Server port (default=5000)',
|
|
72
|
+
default=5000)
|
|
68
73
|
|
|
69
74
|
# Construct plugins module options
|
|
70
75
|
plugin_parser = subparsers.add_parser('plugin', help='Run a ROADrecon plugin')
|
|
@@ -403,6 +403,16 @@ class AccessPoliciesPlugin():
|
|
|
403
403
|
ot += self._parse_appcrit(icrit)
|
|
404
404
|
return ot
|
|
405
405
|
|
|
406
|
+
def _parse_authflows(self, cond):
|
|
407
|
+
if not 'AuthFlows' in cond:
|
|
408
|
+
return ''
|
|
409
|
+
ucond = cond['AuthFlows']
|
|
410
|
+
ot = '<strong>Flows included</strong>: '
|
|
411
|
+
|
|
412
|
+
for icrit in ucond['Include']:
|
|
413
|
+
for _, clist in icrit.items():
|
|
414
|
+
ot += escape(', '.join(clist))
|
|
415
|
+
return ot
|
|
406
416
|
|
|
407
417
|
def _parse_associated_polcies(self,location_object,is_trusted_location,condition_policy_list):
|
|
408
418
|
found_pols = []
|
|
@@ -454,15 +464,19 @@ class AccessPoliciesPlugin():
|
|
|
454
464
|
ot = '<strong>Including</strong>: '
|
|
455
465
|
|
|
456
466
|
for icrit in ucond['Include']:
|
|
457
|
-
ot += ', '.join(
|
|
467
|
+
ot += ', '.join(list({escape(self._translate_clienttype(crit)) for crit in icrit['ClientTypes']}))
|
|
458
468
|
|
|
459
|
-
if 'Exclude' in ucond:
|
|
460
|
-
ot += '\n<br /><strong>Excluding</strong>: '
|
|
461
|
-
|
|
462
|
-
for icrit in ucond['Exclude']:
|
|
463
|
-
ot += ', '.join([escape(crit) for crit in icrit['ClientTypes']])
|
|
464
469
|
return ot
|
|
465
470
|
|
|
471
|
+
def _translate_clienttype(self, client):
|
|
472
|
+
if client in ['EasSupported', 'EasUnsupported']:
|
|
473
|
+
return 'Exchange ActiveSync'
|
|
474
|
+
if client in ['OtherLegacy', 'LegacySmtp', 'LegacyPop', 'LegacyImap', 'LegacyMapi', 'LegacyOffice']:
|
|
475
|
+
return 'Legacy Clients'
|
|
476
|
+
if client == 'Native':
|
|
477
|
+
return 'Mobile and Desktop clients'
|
|
478
|
+
return client
|
|
479
|
+
|
|
466
480
|
def _parse_sessioncontrols(self, cond):
|
|
467
481
|
if not 'SessionControls' in cond:
|
|
468
482
|
return ''
|
|
@@ -511,6 +525,7 @@ class AccessPoliciesPlugin():
|
|
|
511
525
|
continue
|
|
512
526
|
out['who'] = self._parse_who(conditions)
|
|
513
527
|
out['applications'] = self._parse_application(conditions)
|
|
528
|
+
out['authflows'] = self._parse_authflows(conditions)
|
|
514
529
|
out['platforms'] = self._parse_platform(conditions)
|
|
515
530
|
out['locations'] = self._parse_locations(conditions)
|
|
516
531
|
out['clients'] = self._parse_clients(conditions)
|
|
@@ -572,7 +587,7 @@ class AccessPoliciesPlugin():
|
|
|
572
587
|
loc['name'] = escape(detail.get("NetworkName"))
|
|
573
588
|
loc['trusted'] = ("trusted" in detail.get("Categories","") if detail.get("Categories") else False)
|
|
574
589
|
loc['appliestounknowncountry'] = escape(str(detail.get("ApplyToUnknownCountry"))) if detail.get("ApplyToUnknownCountry") is not None else False
|
|
575
|
-
loc['ipranges'] = "\n<br />".join(detail.get('CidrIpRanges'))
|
|
590
|
+
loc['ipranges'] = "\n<br />".join(detail.get('CidrIpRanges')) if detail.get("CidrIpRanges") else ""
|
|
576
591
|
loc['categories'] = escape(", ".join(detail.get("Categories"))) if detail.get("Categories") is not None else ""
|
|
577
592
|
loc['associated_policies'] = "\n<br />".join(self._parse_associated_polcies(detail.get('NetworkId'),loc['trusted'],condition_policy_list))
|
|
578
593
|
loc['country_codes'] = escape(", ".join(detail.get("CountryIsoCodes"))) if detail.get("CountryIsoCodes") else None
|
|
@@ -597,6 +612,8 @@ class AccessPoliciesPlugin():
|
|
|
597
612
|
table += '<tr><td>At locations</td><td>{0}</td></tr>'.format(out['locations'])
|
|
598
613
|
if out['signinrisks'] != '':
|
|
599
614
|
table += '<tr><td>Sign-in risks</td><td>{0}</td></tr>'.format(out['signinrisks'])
|
|
615
|
+
if out['authflows'] != '':
|
|
616
|
+
table += '<tr><td>Authentication flows</td><td>{0}</td></tr>'.format(out['authflows'])
|
|
600
617
|
if out['controls'] != '':
|
|
601
618
|
table += '<tr><td>Controls</td><td>{0}</td></tr>'.format(out['controls'])
|
|
602
619
|
if out['sessioncontrols'] != '':
|
|
@@ -198,8 +198,8 @@ def main(args: Optional[argparse.Namespace] = None) -> Path:
|
|
|
198
198
|
|
|
199
199
|
# Do some reflection to grab the tables
|
|
200
200
|
# and their respective column types
|
|
201
|
-
db_metadata = sqlalchemy.MetaData(
|
|
202
|
-
db_metadata.reflect()
|
|
201
|
+
db_metadata = sqlalchemy.MetaData(schema="main")
|
|
202
|
+
db_metadata.reflect(bind=engine)
|
|
203
203
|
|
|
204
204
|
if not HAS_RTT_MODULES:
|
|
205
205
|
print('Error importing required modules for road2timeline. Make sure pyyaml, numpy and pandas are installed.')
|
roadtools/roadrecon/server.py
CHANGED
|
@@ -7,7 +7,7 @@ from marshmallow import fields
|
|
|
7
7
|
from roadtools.roadlib.metadef.database import User, JSON, Group, DirectoryRole, ServicePrincipal, AppRoleAssignment, TenantDetail, Application, Device, OAuth2PermissionGrant, AuthorizationPolicy, DirectorySetting, AdministrativeUnit, RoleDefinition
|
|
8
8
|
import os
|
|
9
9
|
import argparse
|
|
10
|
-
from sqlalchemy import func
|
|
10
|
+
from sqlalchemy import func, and_, or_, select
|
|
11
11
|
import mimetypes
|
|
12
12
|
|
|
13
13
|
app = Flask(__name__)
|
|
@@ -157,6 +157,10 @@ class TenantDetailSchema(RTModelSchema):
|
|
|
157
157
|
class Meta(RTModelSchema.Meta):
|
|
158
158
|
model = TenantDetail
|
|
159
159
|
|
|
160
|
+
class DirectorySettingSchema(RTModelSchema):
|
|
161
|
+
class Meta(RTModelSchema.Meta):
|
|
162
|
+
model = DirectorySetting
|
|
163
|
+
|
|
160
164
|
class AuthorizationPolicySchema(RTModelSchema):
|
|
161
165
|
class Meta(RTModelSchema.Meta):
|
|
162
166
|
model = AuthorizationPolicy
|
|
@@ -167,6 +171,7 @@ device_schema = DeviceSchema()
|
|
|
167
171
|
group_schema = GroupSchema()
|
|
168
172
|
application_schema = ApplicationSchema()
|
|
169
173
|
td_schema = TenantDetailSchema()
|
|
174
|
+
ds_schema = DirectorySettingSchema()
|
|
170
175
|
serviceprincipal_schema = ServicePrincipalSchema()
|
|
171
176
|
administrativeunit_schema = AdministrativeUnitSchema()
|
|
172
177
|
authorizationpolicy_schema = AuthorizationPolicySchema(many=True)
|
|
@@ -288,10 +293,18 @@ def get_mfa():
|
|
|
288
293
|
# for approle in per_user:
|
|
289
294
|
# enabledusers.append(approle.principalId)
|
|
290
295
|
|
|
291
|
-
# Filter out mailbox users by default
|
|
292
|
-
all_mfa = db.session.
|
|
296
|
+
# Filter out mailbox-only users by default
|
|
297
|
+
all_mfa = db.session.execute(select(User).where(
|
|
298
|
+
or_(User.cloudMSExchRecipientDisplayType is None,
|
|
299
|
+
and_(
|
|
300
|
+
User.cloudMSExchRecipientDisplayType != 0,
|
|
301
|
+
User.cloudMSExchRecipientDisplayType != 7,
|
|
302
|
+
User.cloudMSExchRecipientDisplayType != 18
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
))
|
|
293
306
|
out = []
|
|
294
|
-
for user in all_mfa:
|
|
307
|
+
for user, in all_mfa: # pylint: disable=E1133
|
|
295
308
|
mfa_methods = len(user.strongAuthenticationDetail['methods'])
|
|
296
309
|
methods = [method['methodType'] for method in user.strongAuthenticationDetail['methods']]
|
|
297
310
|
has_app = 'PhoneAppOTP' in methods or 'PhoneAppNotification' in methods
|
|
@@ -521,6 +534,11 @@ def get_tenantdetails():
|
|
|
521
534
|
drs = db.session.query(TenantDetail).first()
|
|
522
535
|
return td_schema.jsonify(drs)
|
|
523
536
|
|
|
537
|
+
@app.route("/api/directorysettings", methods=["GET"])
|
|
538
|
+
def get_directorysettings():
|
|
539
|
+
drs = db.session.query(DirectorySetting).first()
|
|
540
|
+
return ds_schema.jsonify(drs)
|
|
541
|
+
|
|
524
542
|
@app.route("/api/authorizationpolicies", methods=["GET"])
|
|
525
543
|
def get_authpolicies():
|
|
526
544
|
drs = db.session.query(AuthorizationPolicy).all()
|
|
@@ -528,6 +546,7 @@ def get_authpolicies():
|
|
|
528
546
|
|
|
529
547
|
@app.route("/api/stats", methods=["GET"])
|
|
530
548
|
def get_stats():
|
|
549
|
+
# pylint: disable=not-callable
|
|
531
550
|
stats = {
|
|
532
551
|
'countUsers': db.session.query(func.count(User.objectId)).scalar(),
|
|
533
552
|
'countGroups': db.session.query(func.count(Group.objectId)).scalar(),
|
|
@@ -563,6 +582,11 @@ def main(args=None):
|
|
|
563
582
|
parser.add_argument('--profile',
|
|
564
583
|
action='store_true',
|
|
565
584
|
help='Enable flask profiler')
|
|
585
|
+
parser.add_argument('--port',
|
|
586
|
+
type=int,
|
|
587
|
+
action='store',
|
|
588
|
+
help='HTTP Server port (default=5000)',
|
|
589
|
+
default=5000)
|
|
566
590
|
args = parser.parse_args()
|
|
567
591
|
if not ':/' in args.database:
|
|
568
592
|
if args.database[0] != '/':
|
|
@@ -575,7 +599,7 @@ def main(args=None):
|
|
|
575
599
|
if args.profile:
|
|
576
600
|
from werkzeug.middleware.profiler import ProfilerMiddleware
|
|
577
601
|
app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[5])
|
|
578
|
-
app.run(debug=args.debug)
|
|
602
|
+
app.run(debug=args.debug, port=args.port)
|
|
579
603
|
|
|
580
604
|
if __name__ == '__main__':
|
|
581
605
|
main()
|
|
File without changes
|
|
File without changes
|