Skip to content

Commit

Permalink
README, schema and setup.py fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
dirkjanm committed Apr 15, 2020
1 parent 1ce51f3 commit 5de0658
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 42 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
![Python 3 only](https://img.shields.io/badge/python-3.6+-blue.svg)
![License: MIT](https://img.shields.io/pypi/l/roadlib.svg)

![ROADtools logo](roadrecon/frontend/src/assets/rt_transparent.svg)
<img src="roadrecon/frontend/src/assets/rt_transparent.svg" width="300px" alt="ROADtools logo" />

ROADtools is a framework to interact with Azure AD. It currently consists of a library (roadlib) and the ROADrecon Azure AD exploration tool.

Expand All @@ -15,7 +15,7 @@ ROADlib is a library that can be used to authenticate with Azure AD or to build

## ROADrecon
![PyPI version](https://img.shields.io/pypi/v/roadrecon.svg)
[![Build Status](https://dev.azure.com/dirkjanm/adconnectdump/_apis/build/status/fox-it.adconnectdump?branchName=master)](https://dev.azure.com/dirkjanm/adconnectdump/_build/latest?definitionId=16&branchName=master)
[![Build Status](https://dev.azure.com/dirkjanm/ROADtools/_apis/build/status/dirkjanm.ROADtools?branchName=master)](https://dev.azure.com/dirkjanm/ROADtools/_build/latest?definitionId=19&branchName=master)

ROADrecon is a tool for exploring information in Azure AD from both a Red Team and Blue Team perspective. In short, this is what it does:
* Uses an automatically generated metadata model to create an SQLAlchemy backed database on disk.
Expand Down
18 changes: 12 additions & 6 deletions roadlib/roadtools/roadlib/dbgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sqlalchemy.types
from sqlalchemy import Column, Text, Boolean, BigInteger as Integer, Binary, create_engine, Table, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.types import TypeDecorator, TEXT
Base = declarative_base()
Expand Down Expand Up @@ -97,6 +97,10 @@ def init(create=False, dburl='sqlite:///roadrecon.db'):
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
return engine
def get_session(engine):
Session = sessionmaker(bind=engine)
return Session()
'''

coldef = ' %s = Column(%s)'
Expand Down Expand Up @@ -147,8 +151,10 @@ def gen_db_class(classdef, rels, rev_rels):
'group_member_contact': ('Group', 'Contact', 'memberContacts', 'memberOf'),
'group_member_device': ('Group', 'Device', 'memberDevices', 'memberOf'),
'device_owner': ('Device', 'User', 'owner', 'ownedDevices'),
'application_owner': ('Application', 'User', 'owner', 'ownedApplications'),
'serviceprincipal_owner': ('ServicePrincipal', 'User', 'owner', 'ownedServicePrincipals'),
'application_owner_user': ('Application', 'User', 'ownerUsers', 'ownedApplications'),
'application_owner_serviceprincipal': ('Application', 'User', 'ownerServicePrincipals', 'ownedApplications'),
'serviceprincipal_owner_user': ('ServicePrincipal', 'User', 'ownerUsers', 'ownedServicePrincipals'),
'serviceprincipal_owner_serviceprincipal': ('ServicePrincipal', 'ServicePrincipal', 'ownerServicePrincipals', 'ownedServicePrincipals'),
'role_member_user': ('DirectoryRole', 'User', 'memberUsers', 'memberOfRole'),
'role_member_serviceprincipal': ('DirectoryRole', 'ServicePrincipal', 'memberServicePrincipals', 'memberOfRole'),
}
Expand Down Expand Up @@ -192,10 +198,10 @@ def gen_link_fkey(link_name, ref_table, rel_name, rev_rel_name, ref_column, sec_
# Tables to generate and relationships with other tables are defined here
tables = [
# Table, relation, back_relation
(User, [], ['group_member_user', 'application_owner', 'serviceprincipal_owner', 'role_member_user', 'device_owner']),
(ServicePrincipal, ['serviceprincipal_owner'], ['role_member_serviceprincipal']),
(User, [], ['group_member_user', 'application_owner_user', 'serviceprincipal_owner_user', 'role_member_user', 'device_owner']),
(ServicePrincipal, ['serviceprincipal_owner_user', 'serviceprincipal_owner_serviceprincipal'], ['role_member_serviceprincipal', 'serviceprincipal_owner_serviceprincipal']),
(Group, ['group_member_group', 'group_member_user', 'group_member_contact', 'group_member_device'], ['group_member_group']),
(Application, ['application_owner'], []),
(Application, ['application_owner_user', 'application_owner_serviceprincipal'], []),
(Device, ['device_owner'], ['group_member_device']),
# (Domain, [], []),
(DirectoryRole, ['role_member_user', 'role_member_serviceprincipal'], []),
Expand Down
46 changes: 36 additions & 10 deletions roadlib/roadtools/roadlib/metadef/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,26 @@ def __repr__(self):
Column('User', Text, ForeignKey('Users.objectId'))
)

lnk_application_owner = Table('lnk_application_owner', Base.metadata,
lnk_application_owner_user = Table('lnk_application_owner_user', Base.metadata,
Column('Application', Text, ForeignKey('Applications.objectId')),
Column('User', Text, ForeignKey('Users.objectId'))
)

lnk_serviceprincipal_owner = Table('lnk_serviceprincipal_owner', Base.metadata,
lnk_application_owner_serviceprincipal = Table('lnk_application_owner_serviceprincipal', Base.metadata,
Column('Application', Text, ForeignKey('Applications.objectId')),
Column('User', Text, ForeignKey('Users.objectId'))
)

lnk_serviceprincipal_owner_user = Table('lnk_serviceprincipal_owner_user', Base.metadata,
Column('ServicePrincipal', Text, ForeignKey('ServicePrincipals.objectId')),
Column('User', Text, ForeignKey('Users.objectId'))
)

lnk_serviceprincipal_owner_serviceprincipal = Table('lnk_serviceprincipal_owner_serviceprincipal', Base.metadata,
Column('ServicePrincipal', Text, ForeignKey('ServicePrincipals.objectId')),
Column('childServicePrincipal', Text, ForeignKey('ServicePrincipals.objectId'))
)

lnk_role_member_user = Table('lnk_role_member_user', Base.metadata,
Column('DirectoryRole', Text, ForeignKey('DirectoryRoles.objectId')),
Column('User', Text, ForeignKey('Users.objectId'))
Expand Down Expand Up @@ -228,12 +238,12 @@ class User(Base, SerializeMixin):
back_populates="memberUsers")

ownedApplications = relationship("Application",
secondary=lnk_application_owner,
back_populates="owner")
secondary=lnk_application_owner_user,
back_populates="ownerUsers")

ownedServicePrincipals = relationship("ServicePrincipal",
secondary=lnk_serviceprincipal_owner,
back_populates="owner")
secondary=lnk_serviceprincipal_owner_user,
back_populates="ownerUsers")

memberOfRole = relationship("DirectoryRole",
secondary=lnk_role_member_user,
Expand Down Expand Up @@ -287,14 +297,26 @@ class ServicePrincipal(Base, SerializeMixin):
servicePrincipalType = Column(Text)
useCustomTokenSigningKey = Column(Boolean)
verifiedPublisher = Column(JSON)
owner = relationship("User",
secondary=lnk_serviceprincipal_owner,
ownerUsers = relationship("User",
secondary=lnk_serviceprincipal_owner_user,
back_populates="ownedServicePrincipals")

ownerServicePrincipals = relationship("ServicePrincipal",
secondary=lnk_serviceprincipal_owner_serviceprincipal,
primaryjoin=objectId==lnk_serviceprincipal_owner_serviceprincipal.c.ServicePrincipal,
secondaryjoin=objectId==lnk_serviceprincipal_owner_serviceprincipal.c.childServicePrincipal,
back_populates="ownedServicePrincipals")

memberOfRole = relationship("DirectoryRole",
secondary=lnk_role_member_serviceprincipal,
back_populates="memberServicePrincipals")

ownedServicePrincipals = relationship("ServicePrincipal",
secondary=lnk_serviceprincipal_owner_serviceprincipal,
primaryjoin=objectId==lnk_serviceprincipal_owner_serviceprincipal.c.childServicePrincipal,
secondaryjoin=objectId==lnk_serviceprincipal_owner_serviceprincipal.c.ServicePrincipal,
back_populates="ownerServicePrincipals")


class Group(Base, SerializeMixin):
__tablename__ = "Groups"
Expand Down Expand Up @@ -413,8 +435,12 @@ class Application(Base, SerializeMixin):
tokenEncryptionKeyId = Column(Text)
trustedCertificateSubjects = Column(JSON)
verifiedPublisher = Column(JSON)
owner = relationship("User",
secondary=lnk_application_owner,
ownerUsers = relationship("User",
secondary=lnk_application_owner_user,
back_populates="ownedApplications")

ownerServicePrincipals = relationship("User",
secondary=lnk_application_owner_serviceprincipal,
back_populates="ownedApplications")


Expand Down
9 changes: 9 additions & 0 deletions roadlib/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
author='Dirk-jan Mollema',
author_email='[email protected]',
url='https://github.com/dirkjanm/ROADtools/',
license='MIT',
classifiers=[
'Intended Audience :: Information Technology',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
],
packages=find_namespace_packages(include=['roadtools.*']),
install_requires=['adal', 'sqlalchemy'],
zip_safe=False
Expand Down
7 changes: 5 additions & 2 deletions roadrecon/frontend/src/app/appmain/aadobjects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ export interface ApplicationsItem {
publisherName: string;
oauth2Permissions: object[];
publisherDomain: boolean;
publicClient: boolean;
appMetadata: appMetadata;
owner: UsersItem[];
ownerUsers: UsersItem[];
ownerServicePrincipals: ServicePrincipalsItem[];
}

export interface UsersItem {
Expand Down Expand Up @@ -110,7 +112,8 @@ export interface ServicePrincipalsItem {
oauth2Permissions: object[];
passwordCredentials: object;
keyCredentials: object;
owner: UsersItem[];
ownerUsers: UsersItem[];
ownerServicePrincipals: ServicePrincipalsItem[];
memberOfRole: DirectoryRolesItem[];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
<td mat-cell *matCellDef="let row"><span *ngIf="row.oauth2Permissions.length > 0">{{row.oauth2Permissions.length}}</span></td>
</ng-container>

<ng-container matColumnDef="owner">
<ng-container matColumnDef="ownerUsers">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Custom owner</th>
<td mat-cell *matCellDef="let row"><mat-icon *ngIf="row.owner.length > 0" aria-hidden="false" aria-label="Yes">check</mat-icon></td>
<td mat-cell *matCellDef="let row"><mat-icon *ngIf="row.ownerUsers.length + row.ownerServicePrincipals > 0" aria-hidden="false" aria-label="Yes">check</mat-icon></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ <h1 mat-dialog-title>{{ application.displayName }}</h1>
</table>
</mat-card>
<mat-divider></mat-divider>
<mat-expansion-panel *ngIf="application.owner.length > 0" expanded>
<mat-expansion-panel *ngIf="application.ownerUsers.length > 0" expanded>
<mat-expansion-panel-header>
<mat-panel-title>
Owners ({{ application.owner.length }})
Owners ({{ application.ownerUsers.length }})
</mat-panel-title>
</mat-expansion-panel-header>

<table mat-table [dataSource]="application.owner">
<table mat-table [dataSource]="application.ownerUsers">
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef>Name</th>
<td mat-cell *matCellDef="let row"><a [routerLink]="['/users/', row.objectId]">{{row.displayName}}</a></td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@

<ng-container matColumnDef="owner">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Custom owner</th>
<td mat-cell *matCellDef="let row"><mat-icon *ngIf="row.owner.length > 0" aria-hidden="false" aria-label="Yes">check</mat-icon></td>
<td mat-cell *matCellDef="let row"><mat-icon *ngIf="row.ownerUsers.length + row.ownerServicePrincipals.length > 0" aria-hidden="false" aria-label="Yes">check</mat-icon></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ <h1 mat-dialog-title>{{ sp.displayName }}</h1>
</table>
</mat-card>
<mat-divider></mat-divider>
<mat-expansion-panel *ngIf="sp.owner.length > 0" expanded>
<mat-expansion-panel *ngIf="sp.ownerUsers.length > 0" expanded>
<mat-expansion-panel-header>
<mat-panel-title>
Owners ({{ sp.owner.length }})
Owners ({{ sp.ownerUsers.length }})
</mat-panel-title>
</mat-expansion-panel-header>

<table mat-table [dataSource]="sp.owner">
<table mat-table [dataSource]="sp.ownerUsers">
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef>Name</th>
<td mat-cell *matCellDef="let row"><a [routerLink]="['/users/', row.objectId]">{{row.displayName}}</a></td>
Expand Down
20 changes: 16 additions & 4 deletions roadrecon/roadtools/roadrecon/gather.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def mknext(url, prevurl):
# Absolute URL
return url + '&api-version=1.61-internal'
parts = prevurl.split('/')
if 'directoryObjects' in url:
return '/'.join(parts[:4]) + '/' + url + '&api-version=1.61-internal'
return '/'.join(parts[:-1]) + '/' + url + '&api-version=1.61-internal'

async def dumphelper(url, method=requests.get):
Expand Down Expand Up @@ -136,7 +138,6 @@ async def dump_l_to_db(self, url, method, mapping, linkname, childtbl, parent):
async for obj in dumphelper(url, method=method):
objectid, objclass = obj['url'].split('/')[-2:]
# If only one type exists, we don't need to use the mapping
# print(parent.objectId, obj)
if mapping is not None:
try:
childtbl, linkname = mapping[objclass]
Expand Down Expand Up @@ -237,7 +238,7 @@ async def dump_linked_objects(self, objecttype, linktype, parenttbl, linkobjectt
self.session.commit()


async def dump_object_expansion(self, objecttype, dbtype, expandprop, linkname, childtbl, method=None):
async def dump_object_expansion(self, objecttype, dbtype, expandprop, linkname, childtbl, mapping=None, method=None):
if method is None:
method = self.ahsession.get
url = 'https://graph.windows.net/%s/%s?api-version=%s&$expand=%s' % (self.tenantid, objecttype, self.api_version, expandprop)
Expand All @@ -249,6 +250,13 @@ async def dump_object_expansion(self, objecttype, dbtype, expandprop, linkname,
print('Non-existing parent found: %s' % obj['objectId'])
continue
for epdata in obj[expandprop]:
objclass = epdata['odata.type']
if mapping is not None:
try:
childtbl, linkname = mapping[objclass]
except KeyError:
print('Unsupported member type: %s' % objclass)
continue
child = self.session.query(childtbl).get(epdata['objectId'])
if not child:
print('Non-existing child found: %s' % epdata['objectId'])
Expand Down Expand Up @@ -333,6 +341,10 @@ async def run(args, dburl):
'Microsoft.DirectoryServices.Contact': (Contact, 'memberContacts'),
'Microsoft.DirectoryServices.Device': (Device, 'memberDevices'),
}
owner_mapping = {
'Microsoft.DirectoryServices.User': (User, 'ownerUsers'),
'Microsoft.DirectoryServices.ServicePrincipal': (ServicePrincipal, 'ownerServicePrincipals'),
}
tasks = []
dumper.session = dbsession
async with aiohttp.ClientSession() as ahsession:
Expand All @@ -346,9 +358,9 @@ async def run(args, dburl):
tasks.append(dumper.dump_links('directoryRoles', 'members', DirectoryRole, mapping=role_mapping))
tasks.append(dumper.dump_linked_objects('servicePrincipals', 'appRoleAssignedTo', ServicePrincipal, AppRoleAssignment, ignore_duplicates=True))
tasks.append(dumper.dump_linked_objects('servicePrincipals', 'appRoleAssignments', ServicePrincipal, AppRoleAssignment, ignore_duplicates=True))
tasks.append(dumper.dump_object_expansion('servicePrincipals', ServicePrincipal, 'owners', 'owner', User))
tasks.append(dumper.dump_object_expansion('servicePrincipals', ServicePrincipal, 'owners', 'owner', User, mapping=owner_mapping))
tasks.append(dumper.dump_object_expansion('devices', Device, 'registeredOwners', 'owner', User))
tasks.append(dumper.dump_object_expansion('applications', Application, 'owners', 'owner', User))
tasks.append(dumper.dump_object_expansion('applications', Application, 'owners', 'owner', User, mapping=owner_mapping))
tasks.append(dumper.dump_custom_role_members(RoleAssignment))
if args.mfa:
tasks.append(dumper.dump_mfa('users', User, method=ahsession.get))
Expand Down
Loading

0 comments on commit 5de0658

Please sign in to comment.