├── aroadtools
├── __init__.py
├── roadlib
│ ├── __init__.py
│ ├── database
│ │ ├── __init__.py
│ │ ├── metadef
│ │ │ ├── __init__.py
│ │ │ ├── basetypes.py
│ │ │ ├── complextypes.py
│ │ │ ├── entitytypes.py
│ │ │ └── database.py
│ │ └── dbgen.py
│ ├── utils.py
│ ├── constants.py
│ └── deviceauth.py
├── roadrecon
│ ├── __init__.py
│ └── plugins
│ │ ├── __init__.py
│ │ ├── server
│ │ ├── __init__.py
│ │ └── schema.py
│ │ ├── xlsexport.py
│ │ └── policies.py
└── _version.py
├── Manifest.in
├── README.md
├── pyproject.toml
├── Makefile
├── setup.py
├── LICENSE
└── .gitignore
/aroadtools/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aroadtools/roadlib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aroadtools/roadrecon/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aroadtools/roadlib/database/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aroadtools/roadrecon/plugins/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aroadtools/roadlib/database/metadef/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aroadtools/roadrecon/plugins/server/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Manifest.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include README.md
3 |
--------------------------------------------------------------------------------
/aroadtools/roadlib/utils.py:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | async def printhook(msg):
5 | print(msg)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # aroadtools
2 | fully async implementation of Dirkjan's ROADTools
3 | Partial implementation.
4 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0.0"]
3 | build-backend = "setuptools.build_meta"
--------------------------------------------------------------------------------
/aroadtools/_version.py:
--------------------------------------------------------------------------------
1 |
2 | __version__ = "0.0.1"
3 | __banner__ = \
4 | """
5 | # aroadtools %s
6 | # Author: Tamas Jos @skelsec (info@skelsecprojects.com)
7 | """ % __version__
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | clean:
2 | rm -f -r build/
3 | rm -f -r dist/
4 | rm -f -r *.egg-info
5 | find . -name '*.pyc' -exec rm -f {} +
6 | find . -name '*.pyo' -exec rm -f {} +
7 | find . -name '*~' -exec rm -f {} +
8 |
9 | publish: clean
10 | python3 setup.py sdist bdist_wheel
11 | python3 -m twine upload dist/*
12 |
13 | rebuild: clean
14 | python3 setup.py install
15 |
16 | build:
17 | python3 setup.py install
18 |
19 | install:
20 | python3 setup.py install
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | import re
3 |
4 | VERSIONFILE="aroadtools/_version.py"
5 | verstrline = open(VERSIONFILE, "rt").read()
6 | VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
7 | mo = re.search(VSRE, verstrline, re.M)
8 | if mo:
9 | verstr = mo.group(1)
10 | else:
11 | raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,))
12 |
13 |
14 | setup(
15 | # Application name:
16 | name="aroadtools",
17 |
18 | # Version number (initial):
19 | version=verstr,
20 |
21 | # Application author details:
22 | author="Tamas Jos",
23 | author_email="info@skelsecprojects.com",
24 |
25 | # Packages
26 | packages=find_packages(),
27 |
28 | # Include additional files into the package
29 | include_package_data=True,
30 |
31 |
32 | # Details
33 | url="https://github.com/skelsec/aroadtools",
34 |
35 | zip_safe = True,
36 | #
37 | # license="LICENSE.txt",
38 | description="",
39 | long_description="",
40 |
41 | # long_description=open("README.txt").read(),
42 | python_requires='>=3.6',
43 | classifiers=[
44 | "Programming Language :: Python :: 3.6",
45 | "License :: OSI Approved :: MIT License",
46 | "Operating System :: OS Independent",
47 | ],
48 | install_requires=[
49 | 'asn1crypto',
50 | 'cryptography',
51 | 'sqlalchemy',
52 | 'jwt',
53 | 'httpx',
54 | ],
55 | entry_points={
56 | 'console_scripts': [
57 | 'aroadtools-auth = aroadtools.roadlib.auth:main',
58 | 'aroadtools-gather = aroadtools.roadrecon.gather:main',
59 | ],
60 | }
61 | )
62 |
--------------------------------------------------------------------------------
/aroadtools/roadlib/database/metadef/basetypes.py:
--------------------------------------------------------------------------------
1 | import sqlalchemy.types # import Integer, String, JSON, Binary
2 | from sqlalchemy import Table, Column, MetaData, ForeignKey
3 |
4 | class Edm(object):
5 | class String(object):
6 | DBTYPE = sqlalchemy.types.Text
7 |
8 | class Boolean(object):
9 | DBTYPE = sqlalchemy.types.Boolean
10 |
11 | class Stream(object):
12 | DBTYPE = sqlalchemy.types.LargeBinary
13 |
14 | class Int32(object):
15 | DBTYPE = sqlalchemy.types.Integer
16 |
17 | class Int16(object):
18 | DBTYPE = sqlalchemy.types.Integer
19 |
20 | class Single(object):
21 | DBTYPE = sqlalchemy.types.Integer
22 |
23 | class Double(object):
24 | DBTYPE = sqlalchemy.types.Float
25 |
26 | class Decimal(object):
27 | DBTYPE = sqlalchemy.types.Float
28 |
29 | class Int64(object):
30 | DBTYPE = sqlalchemy.types.Integer
31 |
32 | class Guid(object):
33 | DBTYPE = sqlalchemy.types.Text
34 |
35 | class Duration(object):
36 | DBTYPE = sqlalchemy.types.Text
37 |
38 | class DateTime(object):
39 | DBTYPE = sqlalchemy.types.DateTime
40 |
41 | class Date(object):
42 | DBTYPE = sqlalchemy.types.Date
43 |
44 | class TimeOfDay(object):
45 | DBTYPE = sqlalchemy.types.Time
46 |
47 | class DateTimeOffset(object):
48 | DBTYPE = sqlalchemy.types.DateTime
49 |
50 | class Binary(object):
51 | DBTYPE = sqlalchemy.types.LargeBinary
52 |
53 | class Byte(object):
54 | DBTYPE = sqlalchemy.types.LargeBinary
55 |
56 | class Collection(object):
57 | DBTYPE = sqlalchemy.types.JSON
58 |
59 | class ComplexType(object):
60 | DBTYPE = sqlalchemy.types.JSON
61 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 skelsec
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
24 | ==============================================================================
25 | This library is a slight modification of Dirkjan's ROADTools package.
26 | Original code is also MIT licensed.
27 |
28 | MIT License
29 |
30 | Copyright (c) 2023 Dirk-jan Mollema
31 |
32 | Permission is hereby granted, free of charge, to any person obtaining a copy
33 | of this software and associated documentation files (the "Software"), to deal
34 | in the Software without restriction, including without limitation the rights
35 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
36 | copies of the Software, and to permit persons to whom the Software is
37 | furnished to do so, subject to the following conditions:
38 |
39 | The above copyright notice and this permission notice shall be included in all
40 | copies or substantial portions of the Software.
41 |
42 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
43 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
44 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
45 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
46 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
47 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
48 | SOFTWARE.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
--------------------------------------------------------------------------------
/aroadtools/roadlib/constants.py:
--------------------------------------------------------------------------------
1 | """
2 | Constants for roadlib containing well-known resources, login templates (xml), etc
3 | """
4 |
5 | WELLKNOWN_RESOURCES = {
6 | "msgraph": "https://graph.microsoft.com/",
7 | "aadgraph": "https://graph.windows.net/",
8 | "devicereg": "urn:ms-drs:enterpriseregistration.windows.net",
9 | "drs": "urn:ms-drs:enterpriseregistration.windows.net",
10 | "azrm": "https://management.core.windows.net/",
11 | "azurerm": "https://management.core.windows.net/",
12 | }
13 |
14 | WELLKNOWN_CLIENTS = {
15 | "aadps": "1b730954-1685-4b74-9bfd-dac224a7b894",
16 | "azcli": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
17 | "teams": "1fec8e78-bce4-4aaf-ab1b-5451cc387264",
18 | "msteams": "1fec8e78-bce4-4aaf-ab1b-5451cc387264",
19 | "azps": "1950a258-227b-4e31-a9cf-717495945fc2",
20 | "msedge": "ecd6b820-32c2-49b6-98a6-444530e5a77a",
21 | "edge": "ecd6b820-32c2-49b6-98a6-444530e5a77a",
22 | "msbroker": "29d9ed98-a469-4536-ade2-f981bc1d605e",
23 | "broker": "29d9ed98-a469-4536-ade2-f981bc1d605e"
24 | }
25 |
26 | WELLKNOWN_USER_AGENTS = {
27 | "edge": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0",
28 | "edge_windows": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0",
29 | "edge_android": "Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.66 Mobile Safari/537.36 EdgA/118.0.2088.66",
30 | "chrome": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
31 | "chrome_windows": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
32 | "chrome_android": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.3",
33 | "chrome_macos": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
34 | "chrome_linux": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.3",
35 | "chrome_ios": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/119.0.6045.109 Mobile/15E148 Safari/604.",
36 | "firefox": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0",
37 | "firefox_windows": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0",
38 | "firefox_macos": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/119.0",
39 | "firefox_ubuntu": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
40 | "safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.1",
41 | "safari_macos": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.1",
42 | "safari_ios": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.",
43 | "empty": "No user agent header"
44 | }
45 |
46 | DSSO_BODY_KERBEROS = '''
47 |
56 |
57 | http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue
58 | https://autologon.microsoftazuread-sso.com/{tenant}/winauth/trust/2005/windowstransport?client-request-id=4190b9ab-205d-4024-8caa-aedb3f79988b
59 | urn:uuid:36a2e970-3107-4e3f-99dd-569a9588f02c
60 |
61 |
62 |
63 | http://schemas.xmlsoap.org/ws/2005/02/trust/Issue
64 |
65 |
66 | urn:federation:MicrosoftOnline
67 |
68 |
69 | http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey
70 |
71 |
72 |
73 | '''
74 |
75 | DSSO_BODY_USERPASS = '''
76 |
77 |
78 | http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue
79 | urn:uuid:a5c80716-16fe-473c-bb5d-be0602f9e7fd
80 |
81 | http://www.w3.org/2005/08/addressing/anonymous
82 |
83 | https://autologon.microsoftazuread-sso.com/{tenant}/winauth/trust/2005/usernamemixed?client-request-id=19ac39db-81d2-4713-8046-b0b7240592be
84 |
85 |
86 | 2020-11-23T14:50:44.068Z
87 | 2020-11-23T15:00:44.068Z
88 |
89 |
90 | {username}
91 | {password}
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | urn:federation:MicrosoftOnline
100 |
101 |
102 | http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey
103 | http://schemas.xmlsoap.org/ws/2005/02/trust/Issue
104 |
105 |
106 |
107 | '''
108 |
--------------------------------------------------------------------------------
/aroadtools/roadrecon/plugins/server/schema.py:
--------------------------------------------------------------------------------
1 | from marshmallow import Schema, fields
2 | from marshmallow_sqlalchemy import ModelConverter, SQLAlchemyAutoSchema
3 |
4 | from aroadtools.roadlib.database.metadef.database import User, JSON, Group, DirectoryRole, ServicePrincipal, AppRoleAssignment, TenantDetail, Application, Device, OAuth2PermissionGrant, AuthorizationPolicy, DirectorySetting, AdministrativeUnit, RoleDefinition
5 |
6 |
7 | # Model definitions that include a custom JSON type, which doesn't get converted
8 | class RTModelConverter(ModelConverter):
9 | SQLA_TYPE_MAPPING = dict(
10 | list(ModelConverter.SQLA_TYPE_MAPPING.items()) +
11 | [(JSON, fields.Raw)]
12 | )
13 |
14 | # Our custom model schema which uses the model converter from above
15 | class RTModelSchema(SQLAlchemyAutoSchema):
16 | class Meta:
17 | model_converter = RTModelConverter
18 |
19 | # Schemas for objects
20 | # For each object type there is an Schema and Schema
21 | # the plural version is for lists of objects (doesn't include all fields)
22 | # the regular version includes all possible fields based on the SQLAlchemy meta definition
23 | class UsersSchema(Schema):
24 | class Meta:
25 | model = User
26 | fields = ('objectId', 'objectType', 'userPrincipalName', 'displayName', 'mail', 'lastDirSyncTime', 'accountEnabled', 'department', 'lastPasswordChangeDateTime', 'jobTitle', 'mobile', 'dirSyncEnabled', 'strongAuthenticationDetail', 'userType')
27 |
28 | class DevicesSchema(Schema):
29 | class Meta:
30 | model = User
31 | fields = ('objectId', 'objectType', 'accountEnabled', 'displayName', 'deviceManufacturer', 'deviceModel', 'deviceOSType', 'deviceOSVersion', 'deviceTrustType', 'isCompliant', 'deviceId', 'isManaged', 'isRooted', 'dirSyncEnabled')
32 |
33 | class DirectoryRoleSchema(Schema):
34 | class Meta:
35 | model = DirectoryRole
36 | fields = ('displayName', 'description')
37 |
38 | class OAuth2PermissionGrantsSchema(SQLAlchemyAutoSchema):
39 | class Meta:
40 | model = OAuth2PermissionGrant
41 |
42 | class AppRoleAssignmentsSchema(SQLAlchemyAutoSchema):
43 | class Meta:
44 | model = AppRoleAssignment
45 |
46 | class GroupsSchema(Schema):
47 | class Meta:
48 | model = Group
49 | fields = ('displayName', 'description', 'createdDateTime', 'dirSyncEnabled', 'objectId', 'objectType', 'groupTypes', 'mail', 'isPublic', 'isAssignableToRole', 'membershipRule')
50 |
51 | class AdministrativeUnitsSchema(Schema):
52 | class Meta:
53 | model = AdministrativeUnit
54 | fields = ('displayName', 'description', 'createdDateTime', 'objectId', 'objectType', 'membershipType', 'membershipRule')
55 |
56 | class SimpleServicePrincipalsSchema(Schema):
57 | """
58 | Simple ServicePrincipalSchema to prevent looping relationships with serviceprincipals
59 | owning other serviceprincipals
60 | """
61 | class Meta:
62 | model = ServicePrincipal
63 | fields = ('objectId', 'objectType', 'displayName', 'servicePrincipalType')
64 |
65 | class ServicePrincipalsSchema(Schema):
66 | class Meta:
67 | model = ServicePrincipal
68 | fields = ('objectId', 'objectType', 'displayName', 'appDisplayName', 'appRoleAssignmentRequired', 'appId', 'appOwnerTenantId', 'publisherName', 'replyUrls', 'appRoles', 'microsoftFirstParty', 'isDirSyncEnabled', 'oauth2Permissions', 'passwordCredentials', 'keyCredentials', 'ownerUsers', 'ownerServicePrincipals', 'accountEnabled', 'servicePrincipalType')
69 | ownerUsers = fields.Nested(UsersSchema, many=True)
70 | ownerServicePrincipals = fields.Nested(SimpleServicePrincipalsSchema, many=True)
71 |
72 | class ApplicationsSchema(Schema):
73 | class Meta:
74 | model = Application
75 | fields = ('objectId', 'objectType', 'displayName', 'appId', 'appDisplayName', 'oauth2AllowIdTokenImplicitFlow', 'availableToOtherTenants', 'publisherDomain', 'replyUrls', 'appRoles', 'publicClient', 'oauth2AllowImplicitFlow', 'oauth2Permissions', 'homepage', 'passwordCredentials', 'keyCredentials', 'ownerUsers', 'ownerServicePrincipals')
76 | ownerUsers = fields.Nested(UsersSchema, many=True)
77 | ownerServicePrincipals = fields.Nested(SimpleServicePrincipalsSchema, many=True)
78 |
79 | class DirectoryRolesSchema(RTModelSchema):
80 | class Meta(RTModelSchema.Meta):
81 | model = DirectoryRole
82 | memberUsers = fields.Nested(UsersSchema, many=True)
83 | memberServicePrincipals = fields.Nested(ServicePrincipalsSchema, many=True)
84 | memberGroups = fields.Nested(GroupsSchema, many=True)
85 |
86 | class UserSchema(RTModelSchema):
87 | class Meta(RTModelSchema.Meta):
88 | model = User
89 | memberOf = fields.Nested(GroupsSchema, many=True)
90 | memberOfRole = fields.Nested(DirectoryRoleSchema, many=True)
91 | ownedDevices = fields.Nested(DevicesSchema, many=True)
92 | ownedServicePrincipals = fields.Nested(ServicePrincipalsSchema, many=True)
93 | ownedApplications = fields.Nested(ApplicationsSchema, many=True)
94 | ownedGroups = fields.Nested(GroupsSchema, many=True)
95 |
96 | class DeviceSchema(RTModelSchema):
97 | class Meta(RTModelSchema.Meta):
98 | model = Device
99 | memberOf = fields.Nested(GroupsSchema, many=True)
100 | owner = fields.Nested(UsersSchema, many=True)
101 |
102 | class GroupSchema(RTModelSchema):
103 | class Meta(RTModelSchema.Meta):
104 | model = Group
105 | memberOf = fields.Nested(GroupsSchema, many=True)
106 | memberGroups = fields.Nested(GroupsSchema, many=True)
107 | memberUsers = fields.Nested(UsersSchema, many=True)
108 | memberOfRole = fields.Nested(DirectoryRoleSchema, many=True)
109 | memberDevices = fields.Nested(DevicesSchema, many=True)
110 | memberServicePrincipals = fields.Nested(SimpleServicePrincipalsSchema, many=True)
111 | ownerUsers = fields.Nested(UsersSchema, many=True)
112 | ownerServicePrincipals = fields.Nested(SimpleServicePrincipalsSchema, many=True)
113 |
114 | class AdministrativeUnitSchema(RTModelSchema):
115 | class Meta(RTModelSchema.Meta):
116 | model = AdministrativeUnit
117 | memberGroups = fields.Nested(GroupsSchema, many=True)
118 | memberUsers = fields.Nested(UsersSchema, many=True)
119 | memberDevices = fields.Nested(DevicesSchema, many=True)
120 |
121 | class ServicePrincipalSchema(RTModelSchema):
122 | class Meta(RTModelSchema.Meta):
123 | model = ServicePrincipal
124 | ownerUsers = fields.Nested(UsersSchema, many=True)
125 | ownerServicePrincipals = fields.Nested(ServicePrincipalsSchema, many=True)
126 | memberOfRole = fields.Nested(DirectoryRoleSchema, many=True)
127 | memberOf = fields.Nested(GroupSchema, many=True)
128 | oauth2PermissionGrants = fields.Nested(OAuth2PermissionGrantsSchema, many=True)
129 | appRolesAssigned = fields.Nested(AppRoleAssignmentsSchema, many=True)
130 | appRolesAssignedTo = fields.Nested(AppRoleAssignmentsSchema, many=True)
131 |
132 | class ApplicationSchema(RTModelSchema):
133 | class Meta(RTModelSchema.Meta):
134 | model = Application
135 | ownerUsers = fields.Nested(UsersSchema, many=True)
136 | ownerServicePrincipals = fields.Nested(ServicePrincipalsSchema, many=True)
137 |
138 | class TenantDetailSchema(RTModelSchema):
139 | class Meta(RTModelSchema.Meta):
140 | model = TenantDetail
141 |
142 | class AuthorizationPolicySchema(RTModelSchema):
143 | class Meta(RTModelSchema.Meta):
144 | model = AuthorizationPolicy
145 |
146 | # Instantiate all schemas
147 | user_schema = UserSchema()
148 | device_schema = DeviceSchema()
149 | group_schema = GroupSchema()
150 | application_schema = ApplicationSchema()
151 | td_schema = TenantDetailSchema()
152 | serviceprincipal_schema = ServicePrincipalSchema()
153 | administrativeunit_schema = AdministrativeUnitSchema()
154 | authorizationpolicy_schema = AuthorizationPolicySchema(many=True)
155 | users_schema = UsersSchema(many=True)
156 | devices_schema = DevicesSchema(many=True)
157 | groups_schema = GroupsSchema(many=True)
158 | applications_schema = ApplicationsSchema(many=True)
159 | serviceprincipals_schema = ServicePrincipalsSchema(many=True)
160 | directoryroles_schema = DirectoryRolesSchema(many=True)
161 | administrativeunits_schema = AdministrativeUnitsSchema(many=True)
162 |
--------------------------------------------------------------------------------
/aroadtools/roadlib/database/dbgen.py:
--------------------------------------------------------------------------------
1 | from aroadtools.roadlib.database.metadef.entitytypes import *
2 |
3 | header = '''import os
4 | import json
5 | import datetime
6 | import sqlalchemy.types
7 | from sqlalchemy import Column, Text, Boolean, BigInteger as Integer, create_engine, Table, ForeignKey
8 | from sqlalchemy.orm import relationship, sessionmaker, foreign, declarative_base
9 | from sqlalchemy.types import TypeDecorator, TEXT
10 | Base = declarative_base()
11 |
12 |
13 | class JSON(TypeDecorator):
14 | impl = TEXT
15 | cache_ok = True
16 | def process_bind_param(self, value, dialect):
17 | if value is not None:
18 | value = json.dumps(value)
19 |
20 | return value
21 |
22 | def process_result_value(self, value, dialect):
23 | if value is not None:
24 | value = json.loads(value)
25 | return value
26 |
27 | class DateTime(TypeDecorator):
28 | impl = sqlalchemy.types.DateTime
29 | def process_bind_param(self, value, dialect):
30 | if value is not None and isinstance(value, str):
31 | # Sometimes it ends on a Z, sometimes it doesn't
32 | if value[-1] == 'Z':
33 | if '.' in value:
34 | try:
35 | value = datetime.datetime.strptime(value[:-2], '%Y-%m-%dT%H:%M:%S.%f')
36 | except ValueError:
37 | value = datetime.datetime.strptime(value[:-2], '%Y-%m-%dT%H:%M:%S.')
38 | else:
39 | value = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
40 | elif '.' in value:
41 | if '+' in value:
42 | value = datetime.datetime.strptime(value[:-7], '%Y-%m-%dT%H:%M:%S.%f')
43 | else:
44 | try:
45 | value = datetime.datetime.strptime(value[:-1], '%Y-%m-%dT%H:%M:%S.%f')
46 | except ValueError:
47 | value = datetime.datetime.strptime(value[:-1], '%Y-%m-%dT%H:%M:%S.')
48 | else:
49 | value = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S')
50 |
51 | return value
52 |
53 | class SerializeMixin():
54 | def as_dict(self, delete_empty=False):
55 | """
56 | Converts the object to a dict
57 | """
58 | result = {}
59 | for c in self.__table__.columns:
60 | attr = getattr(self, c.name)
61 | if delete_empty:
62 | if attr:
63 | result[c.name] = attr
64 | else:
65 | result[c.name] = attr
66 | return result
67 |
68 |
69 | def __repr__(self):
70 | return str(self.as_dict(True))
71 | '''
72 |
73 | dbdef = '''
74 | class %s(Base, SerializeMixin):
75 | __tablename__ = "%ss"
76 | %s
77 | %s
78 | '''
79 |
80 | footer = '''
81 | def parse_db_argument(dbarg):
82 | \'\'\'
83 | Parse DB string given as argument into full path required
84 | for SQLAlchemy
85 | \'\'\'
86 | if not ':/' in dbarg:
87 | if dbarg[0] != '/':
88 | return 'sqlite:///' + os.path.join(os.getcwd(), dbarg)
89 | else:
90 | return 'sqlite:///' + dbarg
91 | else:
92 | return dbarg
93 |
94 | def init(create=False, dburl='sqlite:///roadrecon.db'):
95 | if 'postgresql' in dburl:
96 | engine = create_engine(dburl,
97 | executemany_mode='values',
98 | executemany_values_page_size=1001)
99 | else:
100 | engine = create_engine(dburl)
101 |
102 | if create:
103 | Base.metadata.drop_all(engine)
104 | Base.metadata.create_all(engine)
105 | return engine
106 |
107 | def get_session(engine):
108 | Session = sessionmaker(bind=engine)
109 | return Session()
110 | '''
111 |
112 | # Custom joins for service principals since these are kinda weird
113 | custom_splinks = '''
114 | oauth2PermissionGrants = relationship("OAuth2PermissionGrant",
115 | primaryjoin=objectId == foreign(OAuth2PermissionGrant.clientId))
116 |
117 | appRolesAssigned = relationship("AppRoleAssignment",
118 | primaryjoin=objectId == foreign(AppRoleAssignment.resourceId))
119 |
120 | appRolesAssignedTo = relationship("AppRoleAssignment",
121 | primaryjoin=objectId == foreign(AppRoleAssignment.principalId))
122 | '''
123 |
124 | coldef = ' %s = Column(%s)'
125 | pcoldef = ' %s = Column(%s, primary_key=True)'
126 | fcoldef = ' %s = Column(%s, ForeignKey("%s"))'
127 |
128 | def gen_db_class(classdef, rels, rev_rels):
129 | classname = classdef.__name__
130 | props = {}
131 | for base in classdef.__bases__:
132 | try:
133 | props.update(base.props)
134 | except AttributeError:
135 | # No base, so no props
136 | pass
137 | props.update(classdef.props)
138 | cols = []
139 | for pname, pclass in props.items():
140 | try:
141 | dbtype = pclass.DBTYPE.__name__
142 | except AttributeError:
143 | # Complex type
144 | dbtype = 'JSON'
145 | if dbtype == 'Binary':
146 | dbtype = 'Text'
147 | if dbtype == 'LargeBinary':
148 | dbtype = 'Text'
149 | if pname == 'objectId' or (classname == 'Domain' and pname == 'name') or (classname in ['RoleAssignment', 'EligibleRoleAssignment', 'AuthorizationPolicy', 'DirectorySetting'] and pname == 'id') or (classname == 'ApplicationRef' and pname == 'appId'):
150 | cols.append(pcoldef % (pname, dbtype))
151 | elif pname == 'roleDefinitionId':
152 | cols.append(fcoldef % (pname, dbtype, 'RoleDefinitions.objectId'))
153 | else:
154 | cols.append(coldef % (pname, dbtype))
155 | outrels = []
156 | for rel in rels:
157 | reldata = relations[rel]
158 | if reldata[0] == reldata[1]:
159 | outrels.append(gen_link_fkey(rel, reldata[1], reldata[2], reldata[3], reldata[0], 'child'+reldata[0]))
160 | elif reldata[0] == 'RoleDefinition' or reldata[1] == 'RoleDefinition':
161 | outrels.append(gen_link_nolinktbl(reldata[1], reldata[2], reldata[3]))
162 | else:
163 | outrels.append(gen_link(rel, reldata[1], reldata[2], reldata[3]))
164 |
165 | for rel in rev_rels:
166 | reldata = relations[rel]
167 | if reldata[0] == reldata[1]:
168 | outrels.append(gen_link_fkey(rel, reldata[0], reldata[3], reldata[2], 'child'+reldata[0], reldata[0]))
169 | elif reldata[0] == 'RoleDefinition' or reldata[1] == 'RoleDefinition':
170 | outrels.append(gen_link_nolinktbl(reldata[0], reldata[3], reldata[2]))
171 | else:
172 | outrels.append(gen_link(rel, reldata[0], reldata[3], reldata[2]))
173 |
174 | if classname == 'ServicePrincipal':
175 | outrels.append(custom_splinks)
176 | return dbdef % (classname, classname, '\n'.join(cols), '\n'.join(outrels))
177 |
178 | # Relationships defined here
179 | relations = {
180 | # Relationship name: (LeftGroup, RightGroup, relation name, reverse relation name)
181 | 'group_member_user': ('Group', 'User', 'memberUsers', 'memberOf'),
182 | 'group_member_group': ('Group', 'Group', 'memberGroups', 'memberOf'),
183 | 'group_member_contact': ('Group', 'Contact', 'memberContacts', 'memberOf'),
184 | 'group_member_device': ('Group', 'Device', 'memberDevices', 'memberOf'),
185 | 'group_member_serviceprincipal': ('Group', 'ServicePrincipal', 'memberServicePrincipals', 'memberOf'),
186 | 'device_owner': ('Device', 'User', 'owner', 'ownedDevices'),
187 | 'application_owner_user': ('Application', 'User', 'ownerUsers', 'ownedApplications'),
188 | 'application_owner_serviceprincipal': ('Application', 'ServicePrincipal', 'ownerServicePrincipals', 'ownedApplications'),
189 | 'serviceprincipal_owner_user': ('ServicePrincipal', 'User', 'ownerUsers', 'ownedServicePrincipals'),
190 | 'serviceprincipal_owner_serviceprincipal': ('ServicePrincipal', 'ServicePrincipal', 'ownerServicePrincipals', 'ownedServicePrincipals'),
191 | 'role_member_user': ('DirectoryRole', 'User', 'memberUsers', 'memberOfRole'),
192 | 'role_member_serviceprincipal': ('DirectoryRole', 'ServicePrincipal', 'memberServicePrincipals', 'memberOfRole'),
193 | 'role_member_group': ('DirectoryRole', 'Group', 'memberGroups', 'memberOfRole'),
194 | 'group_owner_user': ('Group', 'User', 'ownerUsers', 'ownedGroups'),
195 | 'group_owner_serviceprincipal': ('Group', 'ServicePrincipal', 'ownerServicePrincipals', 'ownedGroups'),
196 | 'au_member_user': ('AdministrativeUnit', 'User', 'memberUsers', 'memberOfAu'),
197 | 'au_member_group': ('AdministrativeUnit', 'Group', 'memberGroups', 'memberOfAu'),
198 | 'au_member_device': ('AdministrativeUnit', 'Device', 'memberDevices', 'memberOfAu'),
199 | 'role_assignment_active': ('RoleDefinition', 'RoleAssignment', 'assignments', 'roleDefinition'),
200 | 'role_assignment_eligible': ('RoleDefinition', 'EligibleRoleAssignment', 'eligibleAssignments', 'roleDefinition'),
201 | }
202 |
203 | link_tbl_tpl = '''
204 | lnk_%s = Table('lnk_%s', Base.metadata,
205 | Column('%s', Text, ForeignKey('%ss.objectId')),
206 | Column('%s', Text, ForeignKey('%ss.objectId'))
207 | )
208 | '''
209 |
210 | def gen_link_table(linkname, left_tbl, right_tbl):
211 | # For links between same properties, use different names
212 | if left_tbl == right_tbl:
213 | right_tbl_name = 'child' + right_tbl
214 | else:
215 | right_tbl_name = right_tbl
216 | return link_tbl_tpl % (linkname, linkname, left_tbl, left_tbl, right_tbl_name, right_tbl)
217 |
218 | # Simple link template for many to many relationships with link table
219 | link_tpl = ''' %s = relationship("%s",
220 | secondary=lnk_%s,
221 | back_populates="%s")
222 | '''
223 |
224 | def gen_link(link_name, ref_table, rel_name, rev_rel_name):
225 | return link_tpl % (rel_name, ref_table, link_name, rev_rel_name)
226 |
227 | # Simple link template for one to many relationships
228 | link_tpl_nolinktbl = ''' %s = relationship("%s",
229 | back_populates="%s")
230 | '''
231 |
232 | def gen_link_nolinktbl(ref_table, rel_name, rev_rel_name):
233 | return link_tpl_nolinktbl % (rel_name, ref_table, rev_rel_name)
234 |
235 | # Link template with explicit foreign key
236 | # this voodoo is inspired by https://docs.sqlalchemy.org/en/13/orm/join_conditions.html#self-referential-many-to-many-relationship
237 | link_tpl_fkey = ''' {0} = relationship("{1}",
238 | secondary=lnk_{2},
239 | primaryjoin=objectId==lnk_{2}.c.{3},
240 | secondaryjoin=objectId==lnk_{2}.c.{4},
241 | back_populates="{5}")
242 | '''
243 |
244 | def gen_link_fkey(link_name, ref_table, rel_name, rev_rel_name, ref_column, sec_ref_column):
245 | return link_tpl_fkey.format(rel_name, ref_table, link_name, ref_column, sec_ref_column, rev_rel_name)
246 |
247 | # Tables to generate and relationships with other tables are defined here
248 | tables = [
249 | # Table, relation, back_relation
250 | # These come first since they are referenced from service principals
251 | (AppRoleAssignment, [], []),
252 | (OAuth2PermissionGrant, [], []),
253 | (User, [], ['group_member_user', 'application_owner_user', 'serviceprincipal_owner_user', 'role_member_user', 'device_owner', 'group_owner_user', 'au_member_user']),
254 | (ServicePrincipal, ['serviceprincipal_owner_user', 'serviceprincipal_owner_serviceprincipal'], ['role_member_serviceprincipal', 'serviceprincipal_owner_serviceprincipal', 'application_owner_serviceprincipal', 'group_member_serviceprincipal', 'group_owner_serviceprincipal']),
255 | (Group, ['group_member_group', 'group_member_user', 'group_member_contact', 'group_member_device', 'group_member_serviceprincipal', 'group_owner_user', 'group_owner_serviceprincipal'], ['group_member_group', 'role_member_group', 'au_member_group']),
256 | (Application, ['application_owner_user', 'application_owner_serviceprincipal'], []),
257 | (Device, ['device_owner'], ['group_member_device', 'au_member_device']),
258 | # (Domain, [], []),
259 | (DirectoryRole, ['role_member_user', 'role_member_serviceprincipal', 'role_member_group'], []),
260 | (TenantDetail, [], []),
261 | (ApplicationRef, [], []),
262 | (ExtensionProperty, [], []),
263 | (Contact, [], ['group_member_contact']),
264 | (Policy, [], []),
265 | (RoleDefinition, ['role_assignment_eligible', 'role_assignment_active'], []),
266 | (RoleAssignment, [], ['role_assignment_active']),
267 | (EligibleRoleAssignment, [], ['role_assignment_eligible']),
268 | (AuthorizationPolicy, [], []),
269 | (DirectorySetting, [], []),
270 | (AdministrativeUnit, ['au_member_group', 'au_member_user', 'au_member_device'], [])
271 | ]
272 | with open('metadef/database.py', 'w') as outf:
273 | outf.write(header)
274 | for relname, reldata in relations.items():
275 | if relname == 'role_assignment_active' or relname == 'role_assignment_eligible':
276 | continue
277 | outf.write(gen_link_table(relname, reldata[0], reldata[1]))
278 | for table, links, revlinks in tables:
279 | outf.write(gen_db_class(table, links, revlinks))
280 | outf.write(footer)
281 |
--------------------------------------------------------------------------------
/aroadtools/roadlib/database/metadef/complextypes.py:
--------------------------------------------------------------------------------
1 | from aroadtools.roadlib.database.metadef.basetypes import Edm, Collection
2 |
3 | class AccessPass(object):
4 | props = {
5 | 'creationTime': Edm.DateTime,
6 | 'startTime': Edm.DateTime,
7 | 'endTime': Edm.DateTime,
8 | 'passId': Edm.Guid,
9 | 'accessPassUsage': Edm.String,
10 | 'accessPassCode': Edm.String,
11 | }
12 |
13 |
14 | class AlternativeSecurityId(object):
15 | props = {
16 | 'type': Edm.Int32,
17 | 'identityProvider': Edm.String,
18 | 'key': Edm.Binary,
19 | }
20 |
21 |
22 | class AppAddress(object):
23 | props = {
24 | 'address': Edm.String,
25 | 'addressType': Edm.String,
26 | }
27 |
28 |
29 | class AppBranding(object):
30 | props = {
31 | 'fontColor': Edm.String,
32 | 'logoBackgroundColor': Edm.String,
33 | }
34 |
35 |
36 | class AppMetadataEntry(object):
37 | props = {
38 | 'key': Edm.String,
39 | 'value': Edm.Binary,
40 | }
41 |
42 |
43 | class AppRole(object):
44 | props = {
45 | 'allowedMemberTypes': Collection,
46 | 'description': Edm.String,
47 | 'displayName': Edm.String,
48 | 'id': Edm.Guid,
49 | 'isEnabled': Edm.Boolean,
50 | 'lang': Edm.String,
51 | 'origin': Edm.String,
52 | 'value': Edm.String,
53 | }
54 |
55 |
56 | class AssignedLabel(object):
57 | props = {
58 | 'labelId': Edm.String,
59 | 'displayName': Edm.String,
60 | }
61 |
62 |
63 | class AssignedLicense(object):
64 | props = {
65 | 'disabledPlans': Collection,
66 | 'skuId': Edm.Guid,
67 | }
68 |
69 |
70 | class AssignedPlan(object):
71 | props = {
72 | 'assignedTimestamp': Edm.DateTime,
73 | 'capabilityStatus': Edm.String,
74 | 'service': Edm.String,
75 | 'servicePlanId': Edm.Guid,
76 | }
77 |
78 |
79 | class BitLockerKey(object):
80 | props = {
81 | 'creationTime': Edm.DateTime,
82 | 'customKeyInformation': Edm.Binary,
83 | 'keyIdentifier': Edm.Guid,
84 | 'keyMaterial': Edm.Binary,
85 | }
86 |
87 |
88 | class CustomSecurityAttributeValue(object):
89 | props = {
90 | 'fake_3DA706FF54E945C09DF178B67EE299C2': Edm.String,
91 | }
92 |
93 |
94 | class Certification(object):
95 | props = {
96 | 'isPublisherAttested': Edm.Boolean,
97 | 'isCertifiedByMicrosoft': Edm.Boolean,
98 | 'lastCertificationDateTime': Edm.DateTime,
99 | 'certificationExpirationDateTime': Edm.DateTime,
100 | 'certificationDetailsUrl': Edm.String,
101 | }
102 |
103 |
104 | class CertificateAuthorityInformation(object):
105 | props = {
106 | 'authorityType': Edm.String,
107 | 'crlDistributionPoint': Edm.String,
108 | 'deltaCrlDistributionPoint': Edm.String,
109 | 'trustedCertificate': Edm.Binary,
110 | 'trustedIssuer': Edm.String,
111 | 'trustedIssuerSki': Edm.String,
112 | }
113 |
114 |
115 | class CloudMSRtcServiceAttributes(object):
116 | props = {
117 | 'applicationOptions': Edm.Int32,
118 | 'deploymentLocator': Edm.String,
119 | 'hideFromAddressLists': Edm.Boolean,
120 | 'optionFlags': Edm.Int32,
121 | }
122 |
123 |
124 | class CompliantApplication(object):
125 | props = {
126 | 'mamEnrollmentId': Edm.Guid,
127 | 'expirationTime': Edm.DateTime,
128 | 'applicationId': Edm.Guid,
129 | 'userId': Edm.Guid,
130 | }
131 |
132 |
133 | class CredentialList(object):
134 | props = {
135 | 'passwords': Collection,
136 | 'userName': Edm.String,
137 | }
138 |
139 |
140 | class DefaultUserRolePermissions(object):
141 | props = {
142 | 'allowedToCreateApps': Edm.Boolean,
143 | 'allowedToCreateSecurityGroups': Edm.Boolean,
144 | 'allowedToReadOtherUsers': Edm.Boolean,
145 | }
146 |
147 |
148 | class DeviceKey(object):
149 | props = {
150 | 'creationTime': Edm.DateTime,
151 | 'customKeyInformation': Edm.Binary,
152 | 'keyIdentifier': Edm.Guid,
153 | 'keyMaterial': Edm.Binary,
154 | 'usage': Edm.String,
155 | }
156 |
157 |
158 | class EmployeeOrgData(object):
159 | props = {
160 | 'costCenter': Edm.String,
161 | 'division': Edm.String,
162 | }
163 |
164 |
165 | class SecuredEncryptionKey(object):
166 | props = {
167 | 'partnerId': Edm.String,
168 | 'shardId': Edm.Int32,
169 | 'version': Edm.Int32,
170 | 'publicKey': Edm.Binary,
171 | }
172 |
173 |
174 | class GuestTenantDetail(object):
175 | props = {
176 | 'tenantId': Edm.String,
177 | 'country': Edm.String,
178 | 'countryCode': Edm.String,
179 | 'defaultDomain': Edm.String,
180 | 'displayName': Edm.String,
181 | 'domains': Collection,
182 | 'isHomeTenant': Edm.Boolean,
183 | 'tenantType': Edm.String,
184 | 'tenantBrandingLogoUrl': Edm.String,
185 | }
186 |
187 |
188 | class IdentityInfo(object):
189 | props = {
190 | 'objectId': Edm.Guid,
191 | 'displayName': Edm.String,
192 | 'userPrincipalName': Edm.String,
193 | }
194 |
195 |
196 | class KeyCredential(object):
197 | props = {
198 | 'customKeyIdentifier': Edm.Binary,
199 | 'endDate': Edm.DateTime,
200 | 'keyId': Edm.Guid,
201 | 'startDate': Edm.DateTime,
202 | 'type': Edm.String,
203 | 'usage': Edm.String,
204 | 'value': Edm.Binary,
205 | }
206 |
207 |
208 | class LicenseAssignment(object):
209 | props = {
210 | 'accountId': Edm.Guid,
211 | 'skuId': Edm.Guid,
212 | }
213 |
214 |
215 | class KeyValue(object):
216 | props = {
217 | 'key': Edm.String,
218 | 'value': Edm.String,
219 | }
220 |
221 |
222 | class InvitationTicket(object):
223 | props = {
224 | 'type': Edm.String,
225 | 'ticket': Edm.String,
226 | }
227 |
228 |
229 | class LicenseUnitsDetail(object):
230 | props = {
231 | 'enabled': Edm.Int32,
232 | 'suspended': Edm.Int32,
233 | 'warning': Edm.Int32,
234 | 'lockedOut': Edm.Int32,
235 | }
236 |
237 |
238 | class PrivacyProfile(object):
239 | props = {
240 | 'contactEmail': Edm.String,
241 | 'statementUrl': Edm.String,
242 | }
243 |
244 |
245 | class DirectorySyncStatus(object):
246 | props = {
247 | 'attributeSetName': Edm.String,
248 | 'state': Edm.String,
249 | 'version': Edm.Int32,
250 | }
251 |
252 |
253 | class SettingValue(object):
254 | props = {
255 | 'name': Edm.String,
256 | 'value': Edm.String,
257 | }
258 |
259 |
260 | class SettingTemplateValue(object):
261 | props = {
262 | 'name': Edm.String,
263 | 'type': Edm.String,
264 | 'defaultValue': Edm.String,
265 | 'description': Edm.String,
266 | }
267 |
268 |
269 | class SignInNamesInfo(object):
270 | props = {
271 | 'type': Edm.String,
272 | 'value': Edm.String,
273 | }
274 |
275 |
276 | class OAuth2Permission(object):
277 | props = {
278 | 'adminConsentDescription': Edm.String,
279 | 'adminConsentDisplayName': Edm.String,
280 | 'id': Edm.Guid,
281 | 'isEnabled': Edm.Boolean,
282 | 'lang': Edm.String,
283 | 'origin': Edm.String,
284 | 'type': Edm.String,
285 | 'userConsentDescription': Edm.String,
286 | 'userConsentDisplayName': Edm.String,
287 | 'value': Edm.String,
288 | }
289 |
290 |
291 | class PasswordCredential(object):
292 | props = {
293 | 'customKeyIdentifier': Edm.Binary,
294 | 'endDate': Edm.DateTime,
295 | 'keyId': Edm.Guid,
296 | 'startDate': Edm.DateTime,
297 | 'value': Edm.String,
298 | }
299 |
300 |
301 | class PasswordProfile(object):
302 | props = {
303 | 'password': Edm.String,
304 | 'forceChangePasswordNextLogin': Edm.Boolean,
305 | 'enforceChangePasswordPolicy': Edm.Boolean,
306 | }
307 |
308 |
309 | class ProvisionedPlan(object):
310 | props = {
311 | 'capabilityStatus': Edm.String,
312 | 'provisioningStatus': Edm.String,
313 | 'service': Edm.String,
314 | }
315 |
316 |
317 | class ProvisioningError(object):
318 | props = {
319 | 'errorDetail': Edm.String,
320 | 'resolved': Edm.Boolean,
321 | 'service': Edm.String,
322 | 'timestamp': Edm.DateTime,
323 | }
324 |
325 |
326 | class ResourceAccess(object):
327 | props = {
328 | 'id': Edm.Guid,
329 | 'type': Edm.String,
330 | }
331 |
332 |
333 | class ResourceAction(object):
334 | props = {
335 | 'allowedResourceActions': Collection,
336 | }
337 |
338 |
339 | class DynamicResourceAccess(object):
340 | props = {
341 | 'appIdentifier': Edm.String,
342 | 'scopes': Collection,
343 | 'appRoles': Collection,
344 | }
345 |
346 |
347 | class SearchableDeviceKey(object):
348 | props = {
349 | 'usage': Edm.String,
350 | 'keyIdentifier': Edm.String,
351 | 'keyMaterial': Edm.Binary,
352 | 'creationTime': Edm.DateTime,
353 | 'deviceId': Edm.Guid,
354 | 'customKeyInformation': Edm.Binary,
355 | 'fidoAaGuid': Edm.String,
356 | 'fidoAuthenticatorVersion': Edm.String,
357 | 'fidoAttestationCertificates': Collection,
358 | }
359 |
360 |
361 | class ServicePlanInfo(object):
362 | props = {
363 | 'servicePlanId': Edm.Guid,
364 | 'servicePlanName': Edm.String,
365 | 'provisioningStatus': Edm.String,
366 | 'appliesTo': Edm.String,
367 | }
368 |
369 |
370 | class ServiceOriginatedResource(object):
371 | props = {
372 | 'capability': Edm.String,
373 | 'isLicenseReconciliationNeeded': Edm.Boolean,
374 | 'serviceInstance': Edm.String,
375 | 'servicePlanId': Edm.Guid,
376 | }
377 |
378 |
379 | class SelfServePasswordResetData(object):
380 | props = {
381 | 'alternateAuthenticationPhoneRegisteredTime': Edm.DateTime,
382 | 'alternateEmailRegisteredTime': Edm.DateTime,
383 | 'authenticationEmailRegisteredTime': Edm.DateTime,
384 | 'authenticationPhoneRegisteredTime': Edm.DateTime,
385 | 'deferralCount': Edm.Int32,
386 | 'deferredTime': Edm.DateTime,
387 | 'lastRegisteredTime': Edm.DateTime,
388 | 'mobilePhoneRegisteredTime': Edm.DateTime,
389 | 'reinforceAfterTime': Edm.DateTime,
390 | 'securityAnswersRegisteredTime': Edm.DateTime,
391 | }
392 |
393 |
394 | class SelfServePasswordResetPolicy(object):
395 | props = {
396 | 'enforcedRegistrationEnablement': Edm.String,
397 | 'enforcedRegistrationIntervalInDays': Edm.Int32,
398 | }
399 |
400 |
401 | class ServicePrincipalAuthenticationPolicy(object):
402 | props = {
403 | 'defaultPolicy': Edm.String,
404 | 'allowedPolicies': Collection,
405 | }
406 |
407 |
408 | class SigningCertificateUpdateStatus(object):
409 | props = {
410 | 'result': Edm.Int32,
411 | 'lastRunAt': Edm.DateTime,
412 | }
413 |
414 |
415 | class SamlSingleSignOnSettings(object):
416 | props = {
417 | 'relayState': Edm.String,
418 | }
419 |
420 |
421 | class EncryptedSecretHash(object):
422 | props = {
423 | 'encryptedHashValue': Edm.Binary,
424 | 'version': Edm.Int32,
425 | 'hashAlgorithm': Edm.String,
426 | 'hashSalt': Edm.Binary,
427 | 'iterationCount': Edm.Int32,
428 | 'creationTime': Edm.DateTime,
429 | }
430 |
431 |
432 | class StrongAuthenticationMethod(object):
433 | props = {
434 | 'methodType': Edm.String,
435 | 'isDefault': Edm.Boolean,
436 | }
437 |
438 |
439 | class StrongAuthenticationRequirement(object):
440 | props = {
441 | 'relyingParty': Edm.String,
442 | 'state': Edm.String,
443 | 'rememberDevicesNotIssuedBefore': Edm.DateTime,
444 | }
445 |
446 |
447 | class StrongAuthenticationPhoneAppDetail(object):
448 | props = {
449 | 'authenticationType': Edm.String,
450 | 'authenticatorFlavor': Edm.String,
451 | 'deviceId': Edm.Guid,
452 | 'deviceToken': Edm.String,
453 | 'deviceName': Edm.String,
454 | 'deviceTag': Edm.String,
455 | 'hashFunction': Edm.String,
456 | 'id': Edm.Guid,
457 | 'lastAuthenticatedTimestamp': Edm.DateTime,
458 | 'oathSecretKey': Edm.String,
459 | 'oathTokenTimeDrift': Edm.Int32,
460 | 'tenantDeviceId': Edm.String,
461 | 'timeInterval': Edm.Int32,
462 | 'phoneAppVersion': Edm.String,
463 | 'notificationType': Edm.String,
464 | }
465 |
466 |
467 | class StrongAuthenticationUserDetail(object):
468 | props = {
469 | 'alternativePhoneNumber': Edm.String,
470 | 'email': Edm.String,
471 | 'voiceOnlyPhoneNumber': Edm.String,
472 | 'phoneNumber': Edm.String,
473 | }
474 |
475 |
476 | class CompanyStrongAuthenticationDetails(object):
477 | props = {
478 | 'availableMFAMethods': Collection,
479 | 'phoneFactorId': Edm.String,
480 | 'blockApplicationPassword': Edm.Boolean,
481 | 'rememberDevicesEnabled': Edm.Boolean,
482 | 'rememberDevicesDurationDays': Edm.Int32,
483 | 'rememberDevicesUpdateTimestamp': Edm.DateTime,
484 | 'pinEnabled': Edm.Boolean,
485 | 'pinMinimumLength': Edm.Int32,
486 | }
487 |
488 |
489 | class RelyingPartyStrongAuthenticationPolicy(object):
490 | props = {
491 | 'relyingParties': Collection,
492 | 'enabled': Edm.Boolean,
493 | }
494 |
495 |
496 | class VerifiedDomain(object):
497 | props = {
498 | 'capabilities': Edm.String,
499 | 'default': Edm.Boolean,
500 | 'id': Edm.String,
501 | 'initial': Edm.Boolean,
502 | 'name': Edm.String,
503 | 'type': Edm.String,
504 | }
505 |
506 |
507 | class TrustedCertificateSubject(object):
508 | props = {
509 | 'authorityId': Edm.Guid,
510 | 'subjectName': Edm.String,
511 | 'revokedCertificateIdentifiers': Collection,
512 | }
513 |
514 |
515 | class InformationalUrl(object):
516 | props = {
517 | 'termsOfService': Edm.String,
518 | 'support': Edm.String,
519 | 'privacy': Edm.String,
520 | 'marketing': Edm.String,
521 | }
522 |
523 |
524 | class OptionalClaim(object):
525 | props = {
526 | 'name': Edm.String,
527 | 'source': Edm.String,
528 | 'essential': Edm.Boolean,
529 | 'additionalProperties': Collection,
530 | }
531 |
532 |
533 | class ParentalControlSettings(object):
534 | props = {
535 | 'countriesBlockedForMinors': Collection,
536 | 'legalAgeGroupRule': Edm.String,
537 | }
538 |
539 |
540 | class AuthorizationAction(object):
541 | props = {
542 | 'id': Edm.String,
543 | }
544 |
545 |
546 | class AuthorizationDecision(object):
547 | props = {
548 | 'actionId': Edm.String,
549 | 'accessDecision': Edm.String,
550 | }
551 |
552 |
553 | class AuthorizationResource(object):
554 | props = {
555 | 'scope': Edm.String,
556 | }
557 |
558 |
559 | class AuthorizationSubject(object):
560 | props = {
561 | 'appId': Edm.String,
562 | 'authorizationFlow': Edm.String,
563 | 'userId': Edm.String,
564 | }
565 |
566 |
567 | class RoleAssignmentDetail(object):
568 | props = {
569 | 'isDirect': Edm.Boolean,
570 | 'principalType': Edm.String,
571 | 'principalId': Edm.String,
572 | 'principalDisplayName': Edm.String,
573 | 'roleAssignmentId': Edm.String,
574 | }
575 |
576 |
577 | class VerifiedPublisher(object):
578 | props = {
579 | 'displayName': Edm.String,
580 | 'verifiedPublisherId': Edm.String,
581 | 'addedDateTime': Edm.DateTime,
582 | }
583 |
584 |
585 | class CrossTenantSynchronizationInfo(object):
586 | props = {
587 | 'creationType': Edm.String,
588 | }
589 |
590 |
591 | class AddIn(object):
592 | props = {
593 | 'id': Edm.Guid,
594 | 'type': Edm.String,
595 | 'properties': Collection,
596 | }
597 |
598 |
599 | class AppMetadata(object):
600 | props = {
601 | 'version': Edm.Int32,
602 | 'data': Collection,
603 | }
604 |
605 |
606 | class RolePermission(object):
607 | props = {
608 | 'resourceActions': ResourceAction,
609 | 'condition': Edm.String,
610 | }
611 |
612 |
613 | class DomainFederationSettings(object):
614 | props = {
615 | 'activeLogOnUri': Edm.String,
616 | 'defaultInteractiveAuthenticationMethod': Edm.String,
617 | 'federationBrandName': Edm.String,
618 | 'issuerUri': Edm.String,
619 | 'isExternal': Edm.Boolean,
620 | 'logOffUri': Edm.String,
621 | 'metadataExchangeUri': Edm.String,
622 | 'nextSigningCertificate': Edm.String,
623 | 'openIdConnectDiscoveryEndpoint': Edm.String,
624 | 'passiveLogOnUri': Edm.String,
625 | 'passwordChangeUri': Edm.String,
626 | 'passwordResetUri': Edm.String,
627 | 'preferredAuthenticationProtocol': Edm.String,
628 | 'promptLoginBehavior': Edm.String,
629 | 'signingCertificate': Edm.String,
630 | 'signingCertificateUpdateStatus': SigningCertificateUpdateStatus,
631 | 'supportsMfa': Edm.Boolean,
632 | }
633 |
634 |
635 | class RequiredResourceAccess(object):
636 | props = {
637 | 'resourceAppId': Edm.String,
638 | 'resourceAccess': Collection,
639 | }
640 |
641 |
642 | class OathTokenMetadata(object):
643 | props = {
644 | 'id': Edm.Guid,
645 | 'enabled': Edm.Boolean,
646 | 'tokenType': Edm.String,
647 | 'manufacturer': Edm.String,
648 | 'manufacturerProperties': Collection,
649 | 'serialNumber': Edm.String,
650 | }
651 |
652 |
653 | class StrongAuthenticationDetail(object):
654 | props = {
655 | 'encryptedPinHash': EncryptedSecretHash,
656 | 'encryptedPinHashHistory': Edm.Binary,
657 | 'methods': Collection,
658 | 'oathTokenMetadata': Collection,
659 | 'requirements': Collection,
660 | 'phoneAppDetails': Collection,
661 | 'proofupTime': Edm.Int64,
662 | 'verificationDetail': StrongAuthenticationUserDetail,
663 | }
664 |
665 |
666 | class StrongAuthenticationPolicy(object):
667 | props = {
668 | 'relyingPartyStrongAuthenticationPolicy': Collection,
669 | }
670 |
671 |
672 | class OptionalClaims(object):
673 | props = {
674 | 'idToken': Collection,
675 | 'accessToken': Collection,
676 | 'saml2Token': Collection,
677 | }
678 |
679 |
680 | class ResourceAuthorizationDecision(object):
681 | props = {
682 | 'resourceScope': Edm.String,
683 | 'authorizationDecisions': Collection,
684 | }
685 |
686 |
687 | class CrossTenantSynchronizationResource(object):
688 | props = {
689 | 'originTenantId': Edm.String,
690 | 'originId': Edm.String,
691 | 'synchronizationInfo': CrossTenantSynchronizationInfo,
692 | }
693 |
694 |
--------------------------------------------------------------------------------
/aroadtools/roadrecon/plugins/xlsexport.py:
--------------------------------------------------------------------------------
1 | """
2 | Export to Excel file plugin
3 | Contributed by Bastien Cacace (XMCO)
4 |
5 | Copyright 2020 - MIT License
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 |
26 | import argparse
27 | import json
28 | from html import escape
29 | import os
30 | import pprint
31 | import types
32 | import asyncio
33 |
34 | from marshmallow import Schema, fields
35 | from marshmallow_sqlalchemy import ModelConverter, SQLAlchemyAutoSchema
36 | from openpyxl import Workbook
37 | from openpyxl.styles import Font
38 | from openpyxl.styles import PatternFill
39 |
40 | from aroadtools.roadlib.database.metadef.database import (
41 | User, JSON, Group, DirectoryRole, ServicePrincipal, AppRoleAssignment,
42 | RoleAssignment, TenantDetail, Application, Device, OAuth2PermissionGrant
43 | )
44 | from aroadtools.roadrecon.plugins.server.schema import (
45 | user_schema, device_schema, group_schema, application_schema,
46 | td_schema, serviceprincipal_schema, users_schema, devices_schema,
47 | groups_schema, applications_schema, serviceprincipals_schema
48 | )
49 | import aroadtools.roadlib.database.metadef.database as database
50 | from aroadtools.roadlib.utils import printhook
51 |
52 | # Required property - plugin description
53 | DESCRIPTION = "Export data to an Excel file"
54 |
55 | # Plugin properties
56 | SUPPORTED_EXTENSIONS = ['.xls', '.xlsx']
57 |
58 |
59 | class ExportToFilePlugin():
60 | """
61 | Export data to a file (Excel format).
62 | """
63 |
64 | def __init__(self, session, file, verbose=False, printhook=printhook):
65 | self.session = session
66 | self.file = file
67 | self.verbose = verbose
68 | self.print = printhook
69 |
70 | def _print_msg(self, msg):
71 | if self.verbose:
72 | # this is a disgusting solution but I'm lazy
73 | asyncio.create_task(self.print(msg))
74 |
75 | def _create_excel_headers(self, sheet, list_headers):
76 | i = 1
77 | for header in list_headers:
78 | sheet.cell(row=1, column=i).value = header
79 | i += 1
80 |
81 | def _apply_style_sheet(self, sheet, width):
82 | # Style
83 | i = ord('A')
84 | for header in sheet[1]:
85 | header.font = Font(bold=True, color="FFFEFB")
86 | header.fill = PatternFill("solid", fgColor="47402D")
87 | sheet.column_dimensions[chr(i)].width = width
88 | i += 1
89 |
90 | def _fill_sheet(self, sheet, assets, fields):
91 | i = 2
92 | for asset in assets:
93 | j = 1
94 | for field in fields:
95 | if isinstance(asset, dict):
96 | result = asset.get(field)
97 | else:
98 | result = getattr(asset, field)
99 |
100 | if isinstance(result, types.GeneratorType):
101 | result = sorted(set(result))
102 | result = "\n".join(result)
103 | if isinstance(result, (list,)) and len(result) == 0:
104 | result = ""
105 | if isinstance(result, (list,)):
106 | if isinstance(result[0], (Group,User,ServicePrincipal)):
107 | object_name = []
108 | for obj in result:
109 | object_name.append(obj.displayName)
110 | result = "\n".join(object_name)
111 | elif isinstance(result[0], (dict,)):
112 | result = len(result)
113 | else:
114 | result = sorted(set(result))
115 | result = " ".join(result)
116 | if isinstance(result, (dict,)):
117 | result = json.dumps(result)
118 |
119 | sheet.cell(row=i, column=j).value = result
120 | j += 1
121 | i += 1
122 |
123 | def _create_sheet(self, book, name):
124 | book.create_sheet(name)
125 | sheet = book[name]
126 | return sheet
127 |
128 | def get_users(self, book, column_width=40):
129 | sheet_name = "Users"
130 | self._print_msg('Export %s info' % sheet_name)
131 |
132 | sheet = self._create_sheet(book, sheet_name)
133 | self._create_excel_headers(sheet, users_schema.Meta().fields)
134 | self._apply_style_sheet(sheet, column_width)
135 | all_users = self.session.query(User).all()
136 | self._fill_sheet(sheet, all_users, users_schema.Meta().fields)
137 |
138 | def get_devices(self, book, column_width=40):
139 | sheet_name = "Devices"
140 | self._print_msg('Export %s info' % sheet_name)
141 |
142 | sheet = self._create_sheet(book, sheet_name)
143 | self._create_excel_headers(sheet, devices_schema.Meta().fields)
144 | self._apply_style_sheet(sheet, column_width)
145 | all_devices = self.session.query(Device).all()
146 | self._fill_sheet(sheet, all_devices, devices_schema.Meta().fields)
147 |
148 | def get_groups(self, book, column_width=40):
149 | sheet_name = "Groups"
150 | self._print_msg('Export %s info' % sheet_name)
151 |
152 | sheet = self._create_sheet(book, sheet_name)
153 | self._create_excel_headers(sheet, groups_schema.Meta().fields)
154 | self._apply_style_sheet(sheet, column_width)
155 | all_groups = self.session.query(Group).all()
156 | self._fill_sheet(sheet, all_groups, groups_schema.Meta().fields)
157 |
158 | def get_member_of(self, book, column_width=40):
159 | sheet_name = "MemberOf"
160 | self._print_msg('Export %s info' % sheet_name)
161 |
162 | members_of = dict()
163 | hearders = ['objectId', 'displayName', 'memberOf']
164 | sheet = self._create_sheet(book, "MemberOf")
165 | self._create_excel_headers(sheet, hearders)
166 | self._apply_style_sheet(sheet, column_width)
167 | all_users = self.session.query(User).all()
168 | self._fill_sheet(sheet, all_users, hearders)
169 |
170 | def get_directory_roles(self, book, column_width=40):
171 | sheet_name = "Directory roles"
172 | self._print_msg('Export %s info' % sheet_name)
173 |
174 | fields = (
175 | 'objectId', 'objectType', 'displayName', 'cloudSecurityIdentifier',
176 | 'description', 'isSystem', 'roleDisabled', 'member', 'memberType'
177 | )
178 | sheet = self._create_sheet(book, sheet_name)
179 | self._create_excel_headers(sheet, fields)
180 | self._apply_style_sheet(sheet, column_width)
181 | all_directory_roles = self.session.query(DirectoryRole).all()
182 | directory_roles_by_member = []
183 | for directory_role in all_directory_roles:
184 | members = directory_role.memberUsers if directory_role.memberUsers else directory_role.memberServicePrincipals
185 | for member in members:
186 | directory_roles_by_member.append({
187 | 'objectId': directory_role.objectId,
188 | 'objectType': directory_role.objectType,
189 | 'displayName': directory_role.displayName,
190 | 'cloudSecurityIdentifier': directory_role.cloudSecurityIdentifier,
191 | 'displayName': directory_role.displayName,
192 | 'description': directory_role.description,
193 | 'member': member.displayName,
194 | 'memberType': member.objectType,
195 | 'isSystem': directory_role.isSystem,
196 | 'roleDisabled': directory_role.roleDisabled
197 | })
198 |
199 | self._fill_sheet(sheet, directory_roles_by_member, fields)
200 |
201 | def get_applications(self, book, column_width=40):
202 | sheet_name = "Applications"
203 | self._print_msg('Export %s info' % sheet_name)
204 |
205 | fields = (
206 | 'objectId', 'objectType', 'displayName', 'appId',
207 | 'oauth2AllowIdTokenImplicitFlow', 'availableToOtherTenants',
208 | 'publisherDomain', 'replyUrls', 'appRoles', 'publicClient',
209 | 'oauth2AllowImplicitFlow', 'oauth2Permissions', 'homepage',
210 | 'passwordCredentials', 'keyCredentials', 'ownerUsers',
211 | 'ownerServicePrincipals'
212 | )
213 | sheet = self._create_sheet(book, sheet_name)
214 | self._create_excel_headers(sheet, fields)
215 | self._apply_style_sheet(sheet, column_width)
216 | all_applications = self.session.query(Application).all()
217 | self._fill_sheet(sheet, all_applications, fields)
218 |
219 | def get_service_principals(self, book, column_width=40):
220 | sheet_name = "Service principals"
221 | self._print_msg('Export %s info' % sheet_name)
222 |
223 | fields = (
224 | 'objectId', 'objectType', 'displayName', 'appDisplayName',
225 | 'appId', 'publisherName', 'replyUrls', 'appRoles',
226 | 'microsoftFirstParty', 'oauth2Permissions', 'passwordCredentials',
227 | 'keyCredentials', 'ownerUsers', 'ownerServicePrincipals',
228 | 'accountEnabled', 'servicePrincipalType'
229 | )
230 | sheet = self._create_sheet(book, sheet_name)
231 | self._create_excel_headers(sheet, fields)
232 | self._apply_style_sheet(sheet, column_width)
233 | all_service_principal = self.session.query(ServicePrincipal).all()
234 | self._fill_sheet(sheet, all_service_principal, fields)
235 |
236 | def get_app_roles(self, book, column_width=40):
237 | sheet_name = "Applications roles"
238 | self._print_msg('Export %s info' % sheet_name)
239 |
240 | sheet = self._create_sheet(book, sheet_name)
241 | fields = ('objid', 'ptype', 'pname', 'app', 'value', 'desc', 'spid')
242 | self._create_excel_headers(sheet, fields)
243 | self._apply_style_sheet(sheet, column_width)
244 | approles = []
245 | for ar in self.session.query(AppRoleAssignment).all():
246 | rsp = self.session.get(ServicePrincipal, ar.resourceId)
247 | if ar.principalType == 'ServicePrincipal':
248 | sp = self.session.get(ServicePrincipal, ar.principalId)
249 | if ar.principalType == 'User':
250 | sp = self.session.get(User, ar.principalId)
251 | if ar.principalType == 'Group':
252 | sp = self.session.get(Group, ar.principalId)
253 | if not sp:
254 | self._print_msg('Could not resolve service principal for approle {0}'.format(str(ar)))
255 | continue
256 | if ar.id == '00000000-0000-0000-0000-000000000000':
257 | approles.append({
258 | 'objid': sp.objectId,
259 | 'ptype': ar.principalType,
260 | 'pname': sp.displayName,
261 | 'app': ar.resourceDisplayName,
262 | 'value': 'Default',
263 | 'desc': 'Default Role',
264 | 'spid': ar.resourceId,
265 | })
266 | else:
267 | for approle in rsp.appRoles:
268 | if approle['id'] == ar.id:
269 | approles.append({
270 | 'objid': sp.objectId,
271 | 'ptype': ar.principalType,
272 | 'pname': sp.displayName,
273 | 'app': ar.resourceDisplayName,
274 | 'value': approle['value'],
275 | 'desc': approle['displayName'],
276 | 'spid': ar.resourceId,
277 | })
278 | self._fill_sheet(sheet, approles, fields)
279 |
280 | def get_oauth2_permissions(self, book, column_width=40):
281 | sheet_name = "Oauth2 permissions"
282 | self._print_msg('Export %s info' % sheet_name)
283 |
284 | sheet = self._create_sheet(book, sheet_name)
285 | oauth2permissions = []
286 | fields = (
287 | 'type', 'userid', 'userdisplayname', 'targetapplication', 'targetspobjectid',
288 | 'sourceapplication', 'sourcespobjectid', 'expiry', 'scope'
289 | )
290 | self._create_excel_headers(sheet, fields)
291 | self._apply_style_sheet(sheet, column_width)
292 | for permgrant in self.session.query(OAuth2PermissionGrant).all():
293 | grant = {}
294 | rsp = self.session.get(ServicePrincipal, permgrant.clientId)
295 | if permgrant.consentType == 'Principal':
296 | grant['type'] = 'user'
297 | user = self.session.get(User, permgrant.principalId)
298 | grant['userid'] = user.objectId
299 | grant['userdisplayname'] = user.displayName
300 | else:
301 | grant['type'] = 'all'
302 | grant['userid'] = None
303 | grant['userdisplayname'] = None
304 |
305 | targetapp = self.session.get(ServicePrincipal, permgrant.resourceId)
306 | grant['targetapplication'] = targetapp.displayName
307 | grant['targetspobjectid'] = targetapp.objectId
308 | grant['sourceapplication'] = rsp.displayName
309 | grant['sourcespobjectid'] = rsp.objectId
310 | grant['expiry'] = permgrant.expiryTime
311 | grant['scope'] = permgrant.scope
312 | oauth2permissions.append(grant)
313 |
314 | self._fill_sheet(sheet, oauth2permissions, fields)
315 |
316 | def get_mfa(self, book, column_width=40):
317 | sheet_name = "MFA"
318 | self._print_msg('Export %s info' % sheet_name)
319 |
320 | sheet = self._create_sheet(book, sheet_name)
321 | fields = (
322 | 'objectId', 'displayName', 'mfamethods', 'accountEnabled', 'has_app',
323 | 'has_phonenr', 'has_fido', 'encryptedPinHash', 'encryptedPinHashHistory',
324 | 'methods', 'oathTokenMetadata', 'requirements', 'phoneAppDetails',
325 | 'proofupTime', 'verificationDetail', 'requirements'
326 | )
327 | self._create_excel_headers(sheet, fields)
328 | self._apply_style_sheet(sheet, column_width)
329 | all_mfa = self.session.query(User).all()
330 | mfa = []
331 | for user in all_mfa:
332 | mfa_methods = len(user.strongAuthenticationDetail['methods'])
333 | methods = [method['methodType'] for method in user.strongAuthenticationDetail['methods']]
334 | has_app = 'PhoneAppOTP' in methods or 'PhoneAppNotification' in methods
335 | has_phonenr = 'OneWaySms' in methods or 'TwoWayVoiceMobile' in methods
336 | has_fido = 'FIDO' in [key['usage'] for key in user.searchableDeviceKey]
337 | mfa.append({
338 | 'objectId': user.objectId,
339 | 'displayName': user.displayName,
340 | 'mfamethods': mfa_methods,
341 | 'accountEnabled': user.accountEnabled,
342 | 'has_app': has_app,
343 | 'has_phonenr': has_phonenr,
344 | 'has_fido': has_fido,
345 | 'encryptedPinHash': user.strongAuthenticationDetail['encryptedPinHash'],
346 | 'encryptedPinHashHistory': user.strongAuthenticationDetail['encryptedPinHashHistory'],
347 | 'methods': methods,
348 | 'oathTokenMetadata': user.strongAuthenticationDetail['oathTokenMetadata'],
349 | 'requirements': user.strongAuthenticationDetail['requirements'],
350 | 'phoneAppDetails': user.strongAuthenticationDetail['phoneAppDetails'],
351 | 'proofupTime': user.strongAuthenticationDetail['proofupTime'],
352 | 'verificationDetail': user.strongAuthenticationDetail['verificationDetail'],
353 | 'requirements': user.strongAuthenticationDetail['requirements']
354 | })
355 |
356 | self._fill_sheet(sheet, mfa, fields)
357 |
358 | def main(self):
359 | wb = Workbook()
360 | wb.remove(wb.active)
361 |
362 | self.get_users(wb)
363 | self.get_devices(wb)
364 | self.get_groups(wb)
365 | self.get_member_of(wb)
366 | self.get_directory_roles(wb)
367 | self.get_applications(wb)
368 | self.get_service_principals(wb)
369 | self.get_app_roles(wb)
370 | self.get_oauth2_permissions(wb)
371 | self.get_mfa(wb)
372 | wb.save(self.file)
373 |
374 |
375 | def create_args_parser():
376 | parser = argparse.ArgumentParser(
377 | add_help=True,
378 | description=DESCRIPTION,
379 | formatter_class=argparse.RawDescriptionHelpFormatter
380 | )
381 | parser.add_argument(
382 | '-d', '--database',
383 | action='store',
384 | help='Database file. Can be the local database name for SQLite, or an SQLAlchemy '
385 | 'compatible URL such as postgresql+psycopg2://dirkjan@/roadtools',
386 | default='roadrecon.db'
387 | )
388 | add_args(parser)
389 | return parser
390 |
391 |
392 | def add_args(parser):
393 | parser.add_argument(
394 | '-f', '--file',
395 | action='store',
396 | help='Output excel file (default: data.xlsx)',
397 | default='data.xls'
398 | )
399 | parser.add_argument(
400 | '-v', '--verbose',
401 | action='store_true',
402 | help='Also print details to the console',
403 | required=False
404 | )
405 |
406 |
407 | def main(args=None):
408 | if args is None:
409 | parser = create_args_parser()
410 | args = parser.parse_args()
411 |
412 | db_url = database.parse_db_argument(args.database)
413 | session = database.get_session(database.init(dburl=db_url))
414 | filename, file_extension = os.path.splitext(args.file)
415 | if not file_extension:
416 | file_extension = '.xlsx'
417 | if file_extension not in SUPPORTED_EXTENSIONS:
418 | print("%s is not a supported extention. Only %s are supported" % (file_extension, ', '.join(SUPPORTED_EXTENSIONS)))
419 | return
420 |
421 | plugin = ExportToFilePlugin(session, filename + file_extension, verbose=args.verbose)
422 | plugin.main()
423 | print("Data have been exported to the %s%s file" % (filename, file_extension))
424 |
425 | if __name__ == '__main__':
426 | main()
427 |
--------------------------------------------------------------------------------
/aroadtools/roadrecon/plugins/policies.py:
--------------------------------------------------------------------------------
1 | '''
2 | Conditional Access Policies parsing plugin
3 | Contributed by Dirk-jan Mollema and Adrien Raulot (Fox-IT)
4 | Uses code from ldapdomaindump under MIT license
5 |
6 | The code here isn't very tidy, don't use it as a perfect example
7 |
8 | Copyright 2020 - MIT License
9 |
10 | Permission is hereby granted, free of charge, to any person obtaining a copy
11 | of this software and associated documentation files (the "Software"), to deal
12 | in the Software without restriction, including without limitation the rights
13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | copies of the Software, and to permit persons to whom the Software is
15 | furnished to do so, subject to the following conditions:
16 |
17 | The above copyright notice and this permission notice shall be included in all
18 | copies or substantial portions of the Software.
19 |
20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | SOFTWARE.
27 | '''
28 | import json
29 | import os
30 | import codecs
31 | import argparse
32 | import pprint
33 | import base64
34 | import zlib
35 | from html import escape
36 | from aroadtools.roadlib.database.metadef.database import ServicePrincipal, User, Policy, Application, Group, DirectoryRole
37 | import aroadtools.roadlib.database.metadef.database as database
38 | from aroadtools.roadlib.utils import printhook
39 |
40 | # Required property - plugin description
41 | DESCRIPTION = '''
42 | Parse Conditional Access policies and export those to a file called caps.html
43 | '''
44 |
45 | STYLE_CSS = '''
46 | tbody th {
47 | border: 1px solid #000;
48 | }
49 | tbody td {
50 | border: 1px solid #ababab;
51 | border-spacing: 0px;
52 | padding: 4px;
53 | border-collapse: collapse;
54 | }
55 | body {
56 | font-family: verdana;
57 | }
58 | table {
59 | font-size: 13px;
60 | border-collapse: collapse;
61 | width: 100%;
62 | }
63 | tbody tr:nth-child(odd) td {
64 | background-color: #eee;
65 | }
66 | tbody tr:hover td {
67 | background-color: lightblue;
68 | }
69 | thead td {
70 | font-size: 19px;
71 | font-weight: bold;
72 | padding: 10px 0px;
73 | }
74 | '''
75 |
76 | class AccessPoliciesPlugin():
77 | """
78 | Conditional Access Policies parsing plugin
79 | """
80 | def __init__(self, session, file, printhook=printhook):
81 | self.session = session
82 | self.file = file
83 | self.print = printhook
84 |
85 | def write_html(self, rel_outfile, body, genfunc=None, genargs=None, closeTable=True):
86 | outfile = os.path.join('.', rel_outfile)
87 | with codecs.open(outfile, 'w', 'utf8') as of:
88 | of.write('\n\n')
89 | #Include the style:
90 | of.write('')
93 | of.write('')
94 | #If the generator is not specified, we should write the HTML blob directly
95 | if genfunc is None:
96 | of.write(body)
97 | else:
98 | for tpart in genfunc(*genargs):
99 | of.write(tpart)
100 | #Does the body contain an open table?
101 | if closeTable:
102 | of.write('')
103 | of.write('')
104 |
105 | def _get_group(self, gid):
106 | if isinstance(gid, list):
107 | return self.session.query(Group).filter(Group.objectId.in_(gid)).all()
108 | return self.session.query(Group).filter(Group.objectId == gid).first()
109 |
110 | def _get_application(self, aid):
111 | if isinstance(aid, list):
112 | res = self.session.query(Application).filter(Application.appId.in_(aid)).all()
113 | # if no result, query the ServicePrincipals
114 | if len(res) != len(aid):
115 | return self.session.query(ServicePrincipal).filter(ServicePrincipal.appId.in_(aid)).all()
116 | else:
117 | return res
118 | else:
119 | res = self.session.query(Application).filter(Application.appId == aid).first()
120 | # if no result, query the ServicePrincipals
121 | if res is None or len(res) == 0:
122 | return self.session.query(ServicePrincipal).filter(ServicePrincipal.appId == aid).first()
123 |
124 | def _get_user(self, uid):
125 | if isinstance(uid, list):
126 | return self.session.query(User).filter(User.objectId.in_(uid)).all()
127 | return self.session.query(User).filter(User.objectId == uid).first()
128 |
129 | def _get_serviceprincipal(self, uid):
130 | if isinstance(uid, list):
131 | return self.session.query(ServicePrincipal).filter(ServicePrincipal.objectId.in_(uid)).all()
132 | return self.session.query(ServicePrincipal).filter(ServicePrincipal.objectId == uid).first()
133 |
134 | def _get_role(self, rid):
135 | if isinstance(rid, list):
136 | return self.session.query(DirectoryRole).filter(DirectoryRole.roleTemplateId.in_(rid)).all()
137 | return self.session.query(DirectoryRole).filter(DirectoryRole.roleTemplateId == rid).first()
138 |
139 | async def _print_object(self, obj):
140 | if obj is None:
141 | return
142 | if isinstance(obj, list):
143 | for objitem in obj:
144 | await self._print_object(objitem)
145 | else:
146 | await self.print('\t ')
147 | await self.print(obj.objectId
148 | + ': '
149 | + obj.displayName)
150 | try:
151 | await self.print(' (' + obj.appId + ')')
152 | await self.print(' (' + obj.objectType+ ')')
153 | except:
154 | pass
155 | await self.print()
156 |
157 | def _translate_guestsexternal(self, value):
158 | return [value['GuestOrExternalUserTypes'], ]
159 |
160 | def _translate_authstrength(self, authstrengthguid):
161 | built_in = {
162 | '00000000-0000-0000-0000-000000000002': 'Multi-factor authentication',
163 | '00000000-0000-0000-0000-000000000003': 'Passwordless MFA',
164 | '00000000-0000-0000-0000-000000000004': 'Phishing-resistant MFA'
165 | }
166 | try:
167 | return built_in[authstrengthguid]
168 | except KeyError:
169 | return f"Unknown authentication strengh policy: {authstrengthguid} (probably custom)"
170 |
171 | def _parse_ucrit(self, crit):
172 | funct = {
173 | 'Applications' : self._get_application,
174 | 'Users' : self._get_user,
175 | 'Groups' : self._get_group,
176 | 'Roles': self._get_role,
177 | 'ServicePrincipals': self._get_serviceprincipal,
178 | 'GuestsOrExternalUsers': self._translate_guestsexternal
179 | }
180 | ot = ''
181 | for ctype, clist in crit.items():
182 | if 'All' in clist:
183 | ot += 'All users'
184 | break
185 | if 'None' in clist:
186 | ot += 'Nobody'
187 | break
188 | if 'Guests' in clist:
189 | ot += 'Guest users'
190 | try:
191 | objects = funct[ctype](clist)
192 | except KeyError:
193 | raise Exception('Unsupported criterium type: {0}'.format(ctype))
194 | if len(objects) > 0:
195 | if ctype == 'Users':
196 | ot += 'Users: '
197 | ot += ', '.join([escape(uobj.displayName) for uobj in objects])
198 | elif ctype == 'ServicePrincipals':
199 | ot += 'Service Principals: '
200 | ot += ', '.join([escape(uobj.displayName) for uobj in objects])
201 | elif ctype == 'Groups':
202 | ot += 'Users in groups: '
203 | ot += ', '.join([escape(uobj.displayName) for uobj in objects])
204 | elif ctype == 'Roles':
205 | ot += 'Users in roles: '
206 | ot += ', '.join([escape(uobj.displayName) for uobj in objects])
207 | elif ctype == 'GuestsOrExternalUsers':
208 | ot += 'Guests or external user types: '
209 | ot += ', '.join([escape(uobj) for uobj in objects])
210 | else:
211 | raise Exception('Unsupported criterium type: {0}'.format(ctype))
212 | else:
213 | if not 'Guests' in clist:
214 | ot += 'Unknown object(s) {0}'.format(', '.join(clist))
215 | print('Warning: Not all object IDs could be resolved for this policy')
216 | return ot
217 |
218 | def _parse_appcrit(self, crit):
219 | ot = ''
220 | for ctype, clist in crit.items():
221 | if ctype == 'Acrs':
222 | ot += 'Action: '
223 | ot += ', '.join([escape(action) for action in clist])
224 | else:
225 | if 'All' in clist:
226 | ot += 'All applications'
227 | break
228 | if 'None' in clist:
229 | ot += 'None'
230 | break
231 | if 'Office365' in clist:
232 | ot += 'All Office 365 applications'
233 | objects = self._get_application(clist)
234 | if objects is not None:
235 | if len(objects) > 0:
236 | if ctype == 'Applications':
237 | ot += 'Applications: '
238 | ot += ', '.join([escape(uobj.displayName) for uobj in objects])
239 | return ot
240 |
241 | def _parse_platform(self, cond):
242 | try:
243 | pcond = cond['DevicePlatforms']
244 | except KeyError:
245 | return ''
246 | ot = 'Including: '
247 |
248 | for icrit in pcond['Include']:
249 | if 'All' in icrit['DevicePlatforms']:
250 | ot += 'All platforms'
251 | else:
252 | ot += ', '.join(icrit['DevicePlatforms'])
253 |
254 | if 'Exclude' in pcond:
255 | ot += '\n
Excluding: '
256 |
257 | for icrit in pcond['Exclude']:
258 | ot += ', '.join(icrit['DevicePlatforms'])
259 | return ot
260 |
261 | def _parse_devices(self, cond):
262 | try:
263 | pcond = cond['Devices']
264 | except KeyError:
265 | return ''
266 | ot = 'Including: '
267 |
268 | for icrit in pcond['Include']:
269 | if 'DeviceStates' in icrit.keys():
270 | ot += 'Device states: '
271 | if 'All' in icrit['DeviceStates']:
272 | ot += 'All'
273 | else:
274 | ot += ' '.join(icrit['DeviceStates'])
275 | if 'DeviceRule' in icrit.keys():
276 | ot += 'Device rule: '
277 | if 'All' in icrit['DeviceRule']:
278 | ot += 'All devices'
279 | else:
280 | ot += icrit['DeviceRule']
281 |
282 | if 'Exclude' in pcond:
283 | ot += '\n
Excluding: '
284 |
285 | for icrit in pcond['Exclude']:
286 | if 'DeviceStates' in icrit.keys():
287 | ot += 'Device states: '
288 | if 'All' in icrit['DeviceStates']:
289 | ot += 'All'
290 | else:
291 | ot += ' '.join(icrit['DeviceStates'])
292 | if 'DeviceRule' in icrit.keys():
293 | ot += 'Device rule: '
294 | if 'All' in icrit['DeviceRule']:
295 | ot += 'All devices'
296 | else:
297 | ot += icrit['DeviceRule']
298 | return ot
299 |
300 | def _parse_locations(self, cond):
301 | try:
302 | lcond = cond['Locations']
303 | except KeyError:
304 | return ''
305 | ot = 'Including: '
306 |
307 | for icrit in lcond['Include']:
308 | ot += self._parse_locationcrit(icrit)
309 |
310 | if 'Exclude' in lcond:
311 | ot += '\n
Excluding: '
312 |
313 | for icrit in lcond['Exclude']:
314 | ot += self._parse_locationcrit(icrit)
315 | return ot
316 |
317 | def _parse_signinrisks(self, cond):
318 | try:
319 | srcond = cond['SignInRisks']
320 | except KeyError:
321 | return ''
322 |
323 | ot = 'Including: '
324 | for icrit in srcond['Include']:
325 | ot += ', '.join([escape(crit) for crit in icrit['SignInRisks']])
326 |
327 | if 'Exclude' in srcond:
328 | ot += '\n
Excluding: '
329 | for icrit in srcond['Exclude']:
330 | ot += ', '.join([escape(crit) for crit in icrit['SignInRisks']])
331 |
332 | return ot
333 |
334 | def _parse_locationcrit(self, crit):
335 | ot = ''
336 | for ctype, clist in crit.items():
337 | if 'AllTrusted' in clist:
338 | ot += 'All trusted locations'
339 | break
340 | if 'All' in clist:
341 | ot += 'All locations'
342 | break
343 | objects = self._translate_locations(clist)
344 | ot += 'Locations: '
345 | ot += ', '.join([escape(uobj) for uobj in objects])
346 | return ot
347 |
348 | def _translate_locations(self, locs):
349 | policies = self.session.query(Policy).filter(Policy.policyType == 6).all()
350 | out = []
351 | # Not sure if there can be multiple
352 | for policy in policies:
353 | for pdetail in policy.policyDetail:
354 | detaildata = json.loads(pdetail)
355 | if 'KnownNetworkPolicies' in detaildata and detaildata['KnownNetworkPolicies']['NetworkId'] in locs:
356 | out.append(detaildata['KnownNetworkPolicies']['NetworkName'])
357 | # New format
358 | for loc in locs:
359 | policies = self.session.query(Policy).filter(Policy.policyType == 6, Policy.policyIdentifier == loc).all()
360 | for policy in policies:
361 | out.append(policy.displayName)
362 | return out
363 |
364 | def _parse_who(self, cond):
365 | ucond = cond['Users']
366 | ot = 'Including: '
367 |
368 | if len(ucond['Include']) == 1 and 'Nobody' in self._parse_ucrit(ucond['Include'][0]) and 'ServicePrincipals' in cond:
369 | # Service Principal policy
370 | spcond = cond['ServicePrincipals']
371 | for icrit in spcond['Include']:
372 | ot += self._parse_ucrit(icrit)
373 |
374 | if 'Exclude' in spcond:
375 | ot += '\n
Excluding: '
376 | otl = []
377 | for icrit in spcond['Exclude']:
378 | otl.append(self._parse_ucrit(icrit))
379 | ot += '
'.join(otl)
380 |
381 | else:
382 | for icrit in ucond['Include']:
383 | ot += self._parse_ucrit(icrit)
384 |
385 | if 'Exclude' in ucond:
386 | ot += '\n
Excluding: '
387 | otl = []
388 | for icrit in ucond['Exclude']:
389 | otl.append(self._parse_ucrit(icrit))
390 | ot += '
'.join(otl)
391 | return ot
392 |
393 | def _parse_application(self, cond):
394 | ucond = cond['Applications']
395 | ot = 'Including: '
396 |
397 | for icrit in ucond['Include']:
398 | ot += self._parse_appcrit(icrit)
399 |
400 | if 'Exclude' in ucond:
401 | ot += '\n
Excluding: '
402 |
403 | for icrit in ucond['Exclude']:
404 | ot += self._parse_appcrit(icrit)
405 | return ot
406 |
407 |
408 | def _parse_associated_polcies(self,location_object,is_trusted_location,condition_policy_list):
409 | found_pols = []
410 |
411 | for pol in condition_policy_list:
412 | if not pol.policyDetail:
413 | continue
414 | parsed = json.loads(pol.policyDetail[0])
415 | if not parsed.get('Conditions') or not parsed.get('Conditions').get('Locations'):
416 | continue
417 |
418 | cloc = parsed.get('Conditions').get('Locations')
419 | incl = cloc.get('Include') or []
420 | excl = cloc.get('Exclude') or []
421 | for i in incl:
422 | if location_object in i.get('Locations') or (is_trusted_location and "AllTrusted" in i.get('Locations')):
423 | found_pols.append(escape(pol.displayName))
424 |
425 | for i in excl:
426 | if location_object in i.get('Locations') or (is_trusted_location and "AllTrusted" in i.get('Locations')):
427 | found_pols.append(escape(pol.displayName))
428 |
429 |
430 |
431 | return found_pols
432 |
433 |
434 | def _parse_controls(self, controls):
435 | acontrols = []
436 | for c in controls:
437 | if 'Control' in c:
438 | acontrols.append(', '.join(c['Control']))
439 | if 'AuthStrengthIds' in c:
440 | acontrols.append(', '.join([self._translate_authstrength(authstrengthguid) for authstrengthguid in c['AuthStrengthIds']]))
441 | if 'Block' in acontrols:
442 | ot = 'Deny logon'
443 | return ot
444 | if len(controls) > 1:
445 | ot = 'Requirements (all): '
446 | else:
447 | ot = 'Requirements (any): '
448 | ot += ', '.join(acontrols)
449 | return ot
450 |
451 | def _parse_clients(self, cond):
452 | if not 'ClientTypes' in cond:
453 | return ''
454 | ucond = cond['ClientTypes']
455 | ot = 'Including: '
456 |
457 | for icrit in ucond['Include']:
458 | ot += ', '.join([escape(crit) for crit in icrit['ClientTypes']])
459 |
460 | if 'Exclude' in ucond:
461 | ot += '\n
Excluding: '
462 |
463 | for icrit in ucond['Exclude']:
464 | ot += ', '.join([escape(crit) for crit in icrit['ClientTypes']])
465 | return ot
466 |
467 | def _parse_sessioncontrols(self, cond):
468 | if not 'SessionControls' in cond:
469 | return ''
470 | ucond = cond['SessionControls']
471 | return ', '.join(ucond)
472 |
473 |
474 | def _parse_compressed_cidr(self,detail):
475 | if not 'CompressedCidrIpRanges' in detail:
476 | return ''
477 | compressed = detail['CompressedCidrIpRanges']
478 | b = base64.b64decode(compressed)
479 | cstr = zlib.decompress(b, -zlib.MAX_WBITS)
480 | decoded_cidrs = escape(cstr.decode()).split(",")
481 | return decoded_cidrs
482 |
483 |
484 | async def main(self, should_print=False):
485 | pp = pprint.PrettyPrinter(indent=4)
486 | ol = []
487 | oloc = []
488 | html = 'Policies
'
489 | condition_policy_list = self.session.query(Policy).filter(Policy.policyType == 18)
490 | for policy in self.session.query(Policy).filter(Policy.policyType == 18).order_by(Policy.displayName):
491 | out = {}
492 | out['name'] = escape(policy.displayName)
493 | if should_print:
494 | await self.print()
495 | await self.print('####################')
496 | await self.print(policy.displayName)
497 | await self.print(policy.objectId)
498 | detail = json.loads(policy.policyDetail[0])
499 | if detail['State'] == 'Reporting':
500 | out['name'] += ' (Report only)'
501 | elif detail['State'] != 'Enabled':
502 | out['name'] += ' (Disabled)'
503 | if should_print:
504 | await self.print(pp.pformat(detail))
505 | try:
506 | conditions = detail['Conditions']
507 | except KeyError:
508 | conditions = None
509 | if conditions is None:
510 | if should_print:
511 | await self.print('Invalid policy - no conditions')
512 | continue
513 | out['who'] = self._parse_who(conditions)
514 | out['applications'] = self._parse_application(conditions)
515 | out['platforms'] = self._parse_platform(conditions)
516 | out['locations'] = self._parse_locations(conditions)
517 | out['clients'] = self._parse_clients(conditions)
518 | out['signinrisks'] = self._parse_signinrisks(conditions)
519 | out['sessioncontrols'] = self._parse_sessioncontrols(detail)
520 | out['devices'] = self._parse_devices(conditions)
521 |
522 | try:
523 | out['controls'] = self._parse_controls(detail['Controls'])
524 | except KeyError:
525 | out['controls'] = ''
526 | ol.append(out)
527 | if should_print:
528 | await self.print('####################')
529 |
530 |
531 |
532 | for policy in self.session.query(Policy).filter(Policy.policyType == 6).order_by(Policy.displayName):
533 | loc = {}
534 | loc['name'] = escape(policy.displayName)
535 | if should_print:
536 | await self.print()
537 | await self.print('####################')
538 | await self.print(policy.displayName)
539 | await self.print(policy.objectId)
540 | detail = None
541 | oldpolicy = False
542 |
543 | for pdetail in policy.policyDetail:
544 | detaildata = json.loads(pdetail)
545 | if 'KnownNetworkPolicies' in detaildata:
546 | detail = detaildata['KnownNetworkPolicies']
547 | oldpolicy = True
548 |
549 | if not oldpolicy:
550 | # New format
551 | detail = json.loads(policy.policyDetail[0])
552 |
553 | if should_print:
554 | await self.print(pp.pformat(detail))
555 | if not detail:
556 | continue
557 |
558 | loc['trusted'] = ("trusted" in detail.get("Categories","") if detail.get("Categories") else False)
559 | loc['appliestounknowncountry'] = escape(str(detail.get("ApplyToUnknownCountry"))) if detail.get("ApplyToUnknownCountry") is not None else False
560 | loc['ipranges'] = "\n
".join(self._parse_compressed_cidr(detail))
561 | loc['categories'] = escape(", ".join(detail.get("Categories"))) if detail.get("Categories") is not None else ""
562 | loc['associated_policies'] = "\n
".join(self._parse_associated_polcies(policy.policyIdentifier,loc['trusted'],condition_policy_list))
563 | loc['country_codes'] = escape(", ".join(detail.get("CountryIsoCodes"))) if detail.get("CountryIsoCodes") else None
564 | if should_print:
565 | await self.print(self._parse_compressed_cidr(detail))
566 | else:
567 | # Old format
568 | if should_print:
569 | await self.print(pp.pformat(detail))
570 | if not detail:
571 | continue
572 |
573 | loc['name'] = escape(detail.get("NetworkName"))
574 | loc['trusted'] = ("trusted" in detail.get("Categories","") if detail.get("Categories") else False)
575 | loc['appliestounknowncountry'] = escape(str(detail.get("ApplyToUnknownCountry"))) if detail.get("ApplyToUnknownCountry") is not None else False
576 | loc['ipranges'] = "\n
".join(detail.get('CidrIpRanges'))
577 | loc['categories'] = escape(", ".join(detail.get("Categories"))) if detail.get("Categories") is not None else ""
578 | loc['associated_policies'] = "\n
".join(self._parse_associated_polcies(detail.get('NetworkId'),loc['trusted'],condition_policy_list))
579 | loc['country_codes'] = escape(", ".join(detail.get("CountryIsoCodes"))) if detail.get("CountryIsoCodes") else None
580 |
581 |
582 | oloc.append(loc)
583 |
584 |
585 |
586 |
587 | for out in ol:
588 | table = '| {0} |
'.format(out['name'])
589 | table += '| Applies to | {0} |
'.format(out['who'])
590 | table += '| Applications | {0} |
'.format(out['applications'])
591 | if out['platforms'] != '':
592 | table += '| On platforms | {0} |
'.format(out['platforms'])
593 | if out['devices'] != '':
594 | table += '| Device filter | {0} |
'.format(out['devices'])
595 | if out['clients'] != '':
596 | table += '| Using clients | {0} |
'.format(out['clients'])
597 | if out['locations'] != '':
598 | table += '| At locations | {0} |
'.format(out['locations'])
599 | if out['signinrisks'] != '':
600 | table += '| Sign-in risks | {0} |
'.format(out['signinrisks'])
601 | if out['controls'] != '':
602 | table += '| Controls | {0} |
'.format(out['controls'])
603 | if out['sessioncontrols'] != '':
604 | table += '| Session controls | {0} |
'.format(out['sessioncontrols'])
605 | table += ''
606 | html += table
607 | html += '
'
608 | if len(oloc) > 0:
609 | html += "Named Locations
"
610 | for loc in oloc:
611 | table = '| {0} |
'.format(loc['name'])
612 | table += '| Trusted | {0} |
'.format(str(loc['trusted']))
613 | table += '| Apply to unknown country | {0} |
'.format(loc['appliestounknowncountry'])
614 | table += '| IP ranges | {0} |
'.format(loc['ipranges'])
615 | if(loc['categories']):
616 | table += '| Categories | {0} |
'.format(loc['categories'])
617 |
618 | if(loc['associated_policies']):
619 | table += '| Associated Policies | {0} |
'.format(loc['associated_policies'])
620 |
621 | if(loc['country_codes']):
622 | table += '| Country ISO Codes | {0} |
'.format(loc['country_codes'])
623 |
624 | table += ""
625 |
626 | html += table
627 |
628 | html += "
"
629 | self.write_html(self.file, html)
630 | await self.print('Results written to {0}'.format(self.file))
631 |
632 | def add_args(parser):
633 | parser.add_argument('-f',
634 | '--file',
635 | action='store',
636 | help='Output file (default: caps.html)',
637 | default='caps.html')
638 | parser.add_argument('-p',
639 | '--print',
640 | action='store_true',
641 | help='Also print details to the console')
642 | async def amain(args=None):
643 | if args is None:
644 | parser = argparse.ArgumentParser(add_help=True, description='ROADrecon policies to HTML plugin', formatter_class=argparse.RawDescriptionHelpFormatter)
645 | parser.add_argument('-d',
646 | '--database',
647 | action='store',
648 | help='Database file. Can be the local database name for SQLite, or an SQLAlchemy compatible URL such as postgresql+psycopg2://dirkjan@/roadtools',
649 | default='roadrecon.db')
650 | add_args(parser)
651 | args = parser.parse_args()
652 | db_url = database.parse_db_argument(args.database)
653 | session = database.get_session(database.init(dburl=db_url))
654 | plugin = AccessPoliciesPlugin(session, args.file)
655 | await plugin.main(args.print)
656 |
657 | def main():
658 | import asyncio
659 | asyncio.run(amain())
660 |
661 | if __name__ == '__main__':
662 | main()
663 |
--------------------------------------------------------------------------------
/aroadtools/roadlib/deviceauth.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import binascii
3 | import struct
4 | import json
5 | import sys
6 | import pprint
7 | import random
8 | import string
9 | import codecs
10 | import jwt
11 | import ssl
12 | import os
13 | import time
14 | import warnings
15 | import datetime
16 | import uuid
17 | from cryptography.hazmat.primitives import serialization, padding, hashes
18 | from cryptography.hazmat.primitives.asymmetric import rsa
19 | from cryptography.hazmat.primitives.asymmetric import padding as apadding
20 | from cryptography.hazmat.primitives.keywrap import aes_key_unwrap
21 | from cryptography.hazmat.primitives.serialization import pkcs12
22 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
23 | from cryptography import x509
24 | from cryptography.x509.oid import NameOID
25 | from cryptography.utils import CryptographyDeprecationWarning
26 | from aroadtools.roadlib.auth import Authentication, get_data, AuthenticationException
27 | import httpx
28 |
29 | warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning)
30 |
31 |
32 | class DeviceAuthentication():
33 | """
34 | Device authentication for ROADtools. Handles device registration,
35 | PRT request/renew and token request using WAM emulation.
36 | """
37 | def __init__(self, httptransport=None, httpauth = None):
38 | self.httptransport = httptransport
39 | self.httpauth = httpauth
40 | # Cryptography certificate object
41 | self.certificate = None
42 | # Cryptography private key object
43 | self.privkey = None
44 | # PEM key data
45 | self.keydata = None
46 | # Separate transport key if applicable
47 | self.transportprivkey = None
48 | self.transportkeydata = None
49 |
50 | # PRT data
51 | self.prt = None
52 | self.session_key = None
53 |
54 | # Windows Hello key - Cryptography private key object
55 | self.hellokey = None
56 | # Hello key as PEM
57 | self.hellokeydata = None
58 |
59 | # Proxies
60 | self.proxies = {}
61 | # Verify TLS certs
62 | self.verify = True
63 |
64 | def loadcert(self, pemfile=None, privkeyfile=None, pfxfile=None, pfxpass=None, pfxbase64=None):
65 | """
66 | Load a device certificate from disk
67 | """
68 | if pemfile and privkeyfile:
69 | with open(pemfile, "rb") as certf:
70 | self.certificate = x509.load_pem_x509_certificate(certf.read())
71 | with open(privkeyfile, "rb") as keyf:
72 | self.transportkeydata = self.keydata = keyf.read()
73 | self.transportprivkey = self.privkey = serialization.load_pem_private_key(self.keydata, password=None)
74 | return True
75 | if pfxfile or pfxbase64:
76 | if pfxfile:
77 | with open(pfxfile, 'rb') as pfxf:
78 | pfxdata = pfxf.read()
79 | if pfxbase64:
80 | pfxdata = base64.b64decode(pfxbase64)
81 | if isinstance(pfxpass, str):
82 | pfxpass = pfxpass.encode()
83 | self.privkey, self.certificate, _ = pkcs12.load_key_and_certificates(pfxdata, pfxpass)
84 | self.transportprivkey = self.privkey
85 | # PyJWT needs the key as PEM data anyway, so encode it
86 | self.transportkeydata = self.keydata = self.privkey.private_bytes(
87 | encoding=serialization.Encoding.PEM,
88 | format=serialization.PrivateFormat.TraditionalOpenSSL,
89 | encryption_algorithm=serialization.NoEncryption(),
90 | )
91 | return True
92 | print('You must specify either a PEM certificate file and private key file or a pfx file with the device keypair.')
93 | return False
94 |
95 | def loadkey(self, privkeyfile=None, pfxfile=None, pfxpass=None, pfxbase64=None, transport_only=False):
96 | """
97 | Load private key only (to use as transport key)
98 | Optionally load it as transport key only and not as device key to support separate transport keys
99 | """
100 | if privkeyfile:
101 | with open(privkeyfile, "rb") as keyf:
102 | if transport_only:
103 | # Only load as transport key
104 | self.transportkeydata = keyf.read()
105 | self.transportprivkey = serialization.load_pem_private_key(self.keydata, password=None)
106 | else:
107 | self.transportkeydata = self.keydata = keyf.read()
108 | self.transportprivkey = self.privkey = serialization.load_pem_private_key(self.keydata, password=None)
109 | return True
110 | if pfxfile or pfxbase64:
111 | if pfxfile:
112 | with open(pfxfile, 'rb') as pfxf:
113 | pfxdata = pfxf.read()
114 | if pfxbase64:
115 | pfxdata = base64.b64decode(pfxbase64)
116 | if isinstance(pfxpass, str):
117 | pfxpass = pfxpass.encode()
118 | # Load cert anyway since it's in the same file
119 | if not transport_only:
120 | self.privkey, self.certificate, _ = pkcs12.load_key_and_certificates(pfxdata, pfxpass)
121 | self.transportprivkey = self.privkey
122 | # PyJWT needs the key as PEM data anyway, so encode it
123 | self.transportkeydata = self.keydata = self.privkey.private_bytes(
124 | encoding=serialization.Encoding.PEM,
125 | format=serialization.PrivateFormat.TraditionalOpenSSL,
126 | encryption_algorithm=serialization.NoEncryption(),
127 | )
128 | else:
129 | self.transportprivkey, _, _ = pkcs12.load_key_and_certificates(pfxdata, pfxpass)
130 | self.transportkeydata = self.privkey.private_bytes(
131 | encoding=serialization.Encoding.PEM,
132 | format=serialization.PrivateFormat.TraditionalOpenSSL,
133 | encryption_algorithm=serialization.NoEncryption(),
134 | )
135 | return True
136 | print('You must specify either a private key file or a pfx file with the device keypair.')
137 | return False
138 |
139 | def loadprt(self, prtfile):
140 | """
141 | Load PRT from disk
142 | """
143 | if not prtfile:
144 | return False
145 | try:
146 | with codecs.open(prtfile, 'r', 'utf-8') as prtf:
147 | prtdata = json.load(prtf)
148 | self.prt = Authentication.ensure_plain_prt(prtdata['refresh_token'])
149 | self.session_key = Authentication.ensure_binary_sessionkey(prtdata['session_key'])
150 | except FileNotFoundError:
151 | return False
152 | return True
153 |
154 | def setprt(self, prt, sessionkey):
155 | """
156 | Set PRT parameters in the correct internal format
157 | """
158 | self.prt = Authentication.ensure_plain_prt(prt)
159 | self.session_key = Authentication.ensure_binary_sessionkey(sessionkey)
160 |
161 | def saveprt(self, prtdata, prtfile):
162 | """
163 | Save PRT data to file
164 | """
165 | with codecs.open(prtfile, 'w', 'utf-8') as prtf:
166 | json.dump(prtdata, prtf, sort_keys=True, indent=4)
167 | print(f"Saved PRT to {prtfile}")
168 |
169 | def loadhellokey(self, privkeyfile):
170 | """
171 | Load Windows Hello key from file
172 | """
173 | if not privkeyfile:
174 | return False
175 | try:
176 | with open(privkeyfile, "rb") as keyf:
177 | self.hellokeydata = keyf.read()
178 | self.hellokey = serialization.load_pem_private_key(self.hellokeydata, password=None)
179 | except FileNotFoundError:
180 | return False
181 | return True
182 |
183 | def get_privkey_kid(self, key=None):
184 | """
185 | Get the kid (key ID) for the given key from a file
186 | """
187 | if not key:
188 | key = self.hellokey
189 |
190 | pubkeycngblob = self.create_pubkey_blob_from_key(key)
191 | digest = hashes.Hash(hashes.SHA256())
192 | digest.update(pubkeycngblob)
193 | kid = base64.b64encode(digest.finalize()).decode('utf-8')
194 | return kid
195 |
196 | def create_hello_key(self, privout=None):
197 | """
198 | Create a key for Windows Hello, saving it to a file
199 | """
200 | if not privout:
201 | privout = 'winhello.key'
202 |
203 | # Generate our key
204 | key = rsa.generate_private_key(
205 | public_exponent=65537,
206 | key_size=2048,
207 | )
208 | # Write device key to disk
209 | print(f'Saving private key to {privout}')
210 | with open(privout, "wb") as keyf:
211 | keyf.write(key.private_bytes(
212 | encoding=serialization.Encoding.PEM,
213 | format=serialization.PrivateFormat.TraditionalOpenSSL,
214 | encryption_algorithm=serialization.NoEncryption(),
215 | ))
216 |
217 | pubkeycngblob = base64.b64encode(self.create_pubkey_blob_from_key(key))
218 | return key, pubkeycngblob
219 |
220 | def create_hello_prt_assertion(self, username):
221 | now = int(time.time())
222 | payload = {
223 | "iss": username,
224 | # Should be tenant ID, but this is not verified
225 | "aud": "common",
226 | "iat": now-3600,
227 | "exp": now+3600,
228 | "scope": "openid aza ugs"
229 | }
230 | headers = {
231 | "kid": self.get_privkey_kid(),
232 | "use": "ngc"
233 | }
234 | reqjwt = jwt.encode(payload, algorithm='RS256', key=self.hellokeydata, headers=headers)
235 | return reqjwt
236 |
237 | async def get_prt_with_hello_key(self, username, assertion=None):
238 | authlib = Authentication()
239 | challenge = await authlib.get_srv_challenge()['Nonce']
240 | if not assertion:
241 | assertion = self.create_hello_prt_assertion(username)
242 | # Construct
243 | payload = {
244 | "client_id": "38aa3b87-a06d-4817-b275-7a316988d93b",
245 | "request_nonce": challenge,
246 | "scope": "openid aza ugs",
247 | # Not sure if these matter
248 | "group_sids": [],
249 | "win_ver": "10.0.19041.868",
250 | "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
251 | # Windows includes this, but it is not required or used
252 | # user is instead taken from JWT assertion
253 | "username": username,
254 | "assertion": assertion
255 | }
256 | return await self.request_token_with_devicecert_signed_payload(payload)
257 |
258 | async def register_winhello_key(self, pubkeycngblob, access_token):
259 | headers = {
260 | 'Authorization': f'Bearer {access_token}',
261 | 'Content-Type': 'application/json',
262 | 'User-Agent': 'Dsreg/10.0 (Windows 10.0.19044.1826)',
263 | 'Accept': 'application/json',
264 | }
265 | data = {
266 | "kngc": pubkeycngblob.decode('utf-8')
267 | }
268 |
269 | async with httpx.AsyncClient(transport=self.httptransport, auth = self.httpauth) as session:
270 | res = await session.post('https://enterpriseregistration.windows.net/EnrollmentServer/key/?api-version=1.0', headers=headers, json=data)
271 | resdata = res.json()
272 |
273 | return resdata
274 |
275 | def create_pubkey_blob_from_key(self, key):
276 | """
277 | Convert a key (or certificate) to RSA key blob
278 | https://docs.microsoft.com/en-us/windows/win32/api/bcrypt/ns-bcrypt-bcrypt_rsakey_blob
279 | """
280 | pubkey = key.public_key()
281 | pubnumbers = pubkey.public_numbers()
282 |
283 | # From python docs https://docs.python.org/3/library/stdtypes.html#int.to_bytes
284 | exponent_as_bytes = pubnumbers.e.to_bytes((pubnumbers.e.bit_length() + 7) // 8, byteorder='big')
285 | modulus_as_bytes = pubnumbers.n.to_bytes((pubnumbers.n.bit_length() + 7) // 8, byteorder='big')
286 |
287 | header = [
288 | b'RSA1',
289 | struct.pack('