Skip to content

Commit

Permalink
Add binaries and optional GUI
Browse files Browse the repository at this point in the history
Move source files to ./Source/
Add binaries for Windows and Linux
Add an optional GUI for Windows
  • Loading branch information
ddbnl committed Apr 10, 2022
1 parent 8179466 commit 5ad70a0
Show file tree
Hide file tree
Showing 23 changed files with 903 additions and 31 deletions.
Binary file added Linux/AuditLogCollector
Binary file not shown.
Binary file added Linux/AuditLogSubscriber
Binary file not shown.
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
# Office365 API audit log collector

Collect Office365 and Azure audit logs through their respective APIs. No prior knowledge of APIs is required,
onboarding and script usage is described below. Currently supports the following outputs:
onboarding and script usage is described below. There is a GUI for Windows. Currently supports the following outputs:
- Azure Analytics Workspace (OMS)
- Graylog (or any other source that accepts a simple socket connection)
- File

Simply download the executable(s) you need from the Windows or Linux folder.:
- Windows:
- GUI - Office Audit Log Collector.exe
- GUI for collecting audit logs AND subscribing to audit log feeds (see onboarding instructions below)
- Office Audit Log Collector.exe
- Command line tool for collecting audit logs (see syntax below)
- Office Audit Log Subscriber.exe
- Command line tool for subscribing to audit logs feeds (see onboarding instructions below)
- Linux:
- OfficeAuditLogCollector
- Command line tool for collecting audit logs (see syntax below)
- OfficeAuditLogSubscriber
- Command line tool for subscribing to audit logs (see onboarding instructions below)

For a full audit trail schedule to run the script on a regular basis (preferably at least once every day). The last
run time is recorded automatically, so that when the script runs again it starts to retrieve audit logs from when it last ran.
Feel free to contribute other outputs if you happen to build any.
See the following link for more info on the management APIs: https://msdn.microsoft.com/en-us/office-365/office-365-management-activity-api-reference.

## Roadmap:

- Add an optional GUI
- Automate onboarding as much as possible to make it easier to use
- Make a container that runs this script
- Create a tutorial for automatic onboarding + docker container for the easiest way to run this

## Latest changes:
- Added a GUI for Windows
- Added executables for Windows and Linux
- Added Azure Log Analytics Workspace OMS output
- Added parameter to resume from last run time (use to not miss any logs when script hasn't run for a while)
- Added parameter for amount of hours or days to go back and look for content
Expand Down Expand Up @@ -51,9 +66,9 @@ See the following link for more info on the management APIs: https://msdn.micros

## Instructions:

### Creating an application in Azure:
- Create the app registration:
- Create app registration itself under Azure AD (own tenant only works fine for single tenant)
### Onboarding:
- Create an app registration:
- Create the app registration itself under Azure AD (own tenant only works fine for single tenant)
- Create app secret (only shown once upon creation, store it somewhere safe)
- Grant your new app permissions to read the Office API's:
- Graph: AuditLog.Read.All
Expand Down
File renamed without changes.
42 changes: 27 additions & 15 deletions AuditLogCollector.py → Source/AuditLogCollector.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Standard libs
import collections
import os
import sys
import json
import logging
import datetime
Expand All @@ -16,7 +16,7 @@

class AuditLogCollector(ApiConnection.ApiConnection):

def __init__(self, content_types, *args, resume=True, fallback_time=None,
def __init__(self, *args, content_types=None, resume=True, fallback_time=None,
file_output=False, output_path=None,
graylog_output=False, graylog_address=None, graylog_port=None,
azure_oms_output=False, azure_oms_workspace_id=None, azure_oms_shared_key=None,
Expand All @@ -41,38 +41,46 @@ def __init__(self, content_types, *args, resume=True, fallback_time=None,
self.graylog_output = graylog_output
self.azure_oms_output = azure_oms_output
self.output_path = output_path
self.content_types = content_types
self.content_types = content_types or collections.deque()
self._last_run_times = {}
self.resume = resume
if resume:
self.get_last_run_times()
self._fallback_time = fallback_time or datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=1)
self._known_content = {}
if self.azure_oms_output:
self._azure_oms_interface = AzureOMSInterface.AzureOMSInterface(workspace_id=azure_oms_workspace_id,
shared_key=azure_oms_shared_key)
if self.graylog_output:
self._graylog_interface = GraylogInterface.GraylogInterface(graylog_address=graylog_address,
graylog_port=graylog_port)
self._azure_oms_interface = AzureOMSInterface.AzureOMSInterface(workspace_id=azure_oms_workspace_id,
shared_key=azure_oms_shared_key)
self._graylog_interface = GraylogInterface.GraylogInterface(graylog_address=graylog_address,
graylog_port=graylog_port)
self.blobs_to_collect = collections.defaultdict(collections.deque)
self.monitor_thread = threading.Thread()
self.retrieve_available_content_threads = collections.deque()
self.retrieve_content_threads = collections.deque()
self.run_started = None
self.logs_retrieved = 0

def run_once(self, start_time=None):
"""
Check available content and retrieve it, then exit.
"""
run_started = datetime.datetime.now()
self._known_content.clear()
self.logs_retrieved = 0
self._graylog_interface.successfully_sent = 0
self._graylog_interface.unsuccessfully_sent = 0
self._azure_oms_interface.successfully_sent = 0
self._azure_oms_interface.unsuccessfully_sent = 0
self.run_started = datetime.datetime.now()
self._clean_known_content()
if self.resume:
self.get_last_run_times()
self.start_monitoring()
self.get_all_available_content(start_time=start_time)
self.monitor_thread.join()
if self._last_run_times:
if self.resume and self._last_run_times:
with open('last_run_times', 'w') as ofile:
json.dump(fp=ofile, obj=self._last_run_times)
logging.info("Finished. Total logs retrieved: {}. Run time: {}.".format(
self.logs_retrieved, datetime.datetime.now() - run_started))
self.logs_retrieved, datetime.datetime.now() - self.run_started))
if self.azure_oms_output:
logging.info("Azure OMS output report: {} successfully sent, {} errors".format(
self._azure_oms_interface.successfully_sent, self._azure_oms_interface.unsuccessfully_sent))
Expand Down Expand Up @@ -127,7 +135,7 @@ def get_all_available_content(self, start_time=None):
"""
for content_type in self.content_types.copy():
if not start_time:
if content_type in self._last_run_times.keys():
if self.resume and content_type in self._last_run_times.keys():
start_time = self._last_run_times[content_type]
else:
start_time = self._fallback_time
Expand Down Expand Up @@ -336,8 +344,12 @@ def known_content(self):
elif argsdict['time_hours']:
fallback_time = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=argsdict['time_hours'])

logging.basicConfig(filemode='w', filename=argsdict['log_path'],
level=logging.INFO if not argsdict['debug_logging'] else logging.DEBUG)
logger = logging.getLogger()
fileHandler = logging.FileHandler(argsdict['log_path'], mode='w')
streamHandler = logging.StreamHandler(sys.stdout)
logger.addHandler(streamHandler)
logger.addHandler(fileHandler)
logger.setLevel(logging.INFO if not argsdict['debug_logging'] else logging.DEBUG)
logging.log(level=logging.INFO, msg='Starting run @ {0}'.format(datetime.datetime.now()))

collector = AuditLogCollector(
Expand Down
30 changes: 20 additions & 10 deletions AuditLogSubscriber.py → Source/AuditLogSubscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,33 @@ def get_sub_status(self):
status = self.make_api_request(url='subscriptions/list', append_url=True)
return status.json()

def set_sub_status(self, ctype_stat):
def set_sub_status(self, ctype_stat=None, content_type=None, action=None):
"""
Args:
ctype_stat (tuple): content type, status (enabled | disabled)
Returns:
dict
"""
if ctype_stat[1] == 'enabled':
action = 'stop'
elif ctype_stat[1] == 'disabled':
action = 'start'
else:
return
status = self.make_api_request(url='subscriptions/{0}?contentType={1}'.format(action, ctype_stat[0]),
content_type = content_type or ctype_stat[0]
if not action:
if ctype_stat[1] == 'enabled':
action = 'stop'
elif ctype_stat[1] == 'disabled':
action = 'start'
else:
return
status = self.make_api_request(url='subscriptions/{0}?contentType={1}'.format(action, content_type),
append_url=True, get=False)
logging.info("Set sub status response: {}".format(status))
logging.debug("Set sub status response: {}".format(status))
try:
logging.debug("Set sub status json: {}".format(status.json()))
except Exception as e:
pass
if 200 <= status.status_code <= 299:
logging.info('Successfully set sub status: {} > {}'.format(content_type, action))
else:
raise RuntimeError("Unable to set sub status: {} > {}".format(content_type, action))
status.close()

def interactive(self):

Expand Down
7 changes: 6 additions & 1 deletion AzureOMSInterface.py → Source/AzureOMSInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,12 @@ def post_data(self, body, log_type, time_generated):
'time-generated-field': time_generated
}
response = self.session.post(uri, data=body, headers=headers)
status_code, json_output = response.status_code, response.json
status_code = response.status_code
try:
json_output = response.json()
except:
json_output = ''

response.close()
if 200 <= status_code <= 299:
logging.info('Accepted payload:' + body)
Expand Down
Loading

0 comments on commit 5ad70a0

Please sign in to comment.