-
Notifications
You must be signed in to change notification settings - Fork 334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[apps] adding G Suite app for all activity audit reports #426
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
c563805
[apps] adding 'auth' property helper to config
ryandeivert f4146e9
[apps] making the `required_auth_info` method a classmethod
ryandeivert 30c5f4c
[cli] user input restrictions now support a validation function
ryandeivert 363ec4b
[apps] adding gsuite app for admin/login/token/drive reports
ryandeivert c752b55
[schemas] adding gsuite reports schema and validation event
ryandeivert a27a5cc
[tests][apps] updating previous tests to use new `auth` property on c…
ryandeivert 1bda2b6
[tests][apps] adding unit tests for gsuite app(s)
ryandeivert 1448066
[apps][gsuite] calendar, groups, gplus, mobile, rules, and saml apps
ryandeivert File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
""" | ||
Copyright 2017-present, Airbnb Inc. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
""" | ||
import json | ||
|
||
from apiclient import discovery, errors | ||
from oauth2client.service_account import ServiceAccountCredentials | ||
|
||
from app_integrations import LOGGER | ||
from app_integrations.apps.app_base import app, AppIntegration | ||
|
||
|
||
class GSuiteReportsApp(AppIntegration): | ||
"""G Suite Reports base app integration. This is subclassed for various endpoints""" | ||
_SCOPES = ['https://www.googleapis.com/auth/admin.reports.audit.readonly'] | ||
|
||
def __init__(self, config): | ||
super(GSuiteReportsApp, self).__init__(config) | ||
self._activities_service = None | ||
self._next_page_token = None | ||
|
||
@classmethod | ||
def _type(cls): | ||
raise NotImplementedError('Subclasses should implement the _type method') | ||
|
||
@classmethod | ||
def service(cls): | ||
return 'gsuite' | ||
|
||
@classmethod | ||
def date_formatter(cls): | ||
"""Return a format string for a date, ie: 2010-10-28T10:26:35.000Z""" | ||
return '%Y-%m-%dT%H:%M:%SZ' | ||
|
||
@classmethod | ||
def _load_credentials(cls, keydata): | ||
"""Load ServiceAccountCredentials from Google service account JSON keyfile | ||
|
||
Args: | ||
keydata (dict): The loaded keyfile data from a Google service account | ||
JSON file | ||
""" | ||
try: | ||
creds = ServiceAccountCredentials.from_json_keyfile_dict( | ||
keydata, scopes=cls._SCOPES) | ||
except (ValueError, KeyError): | ||
# This has the potential to raise errors. See: https://tinyurl.com/y8q5e9rm | ||
LOGGER.exception('Could not generate credentials from keyfile for %s', | ||
cls.type()) | ||
return False | ||
|
||
return creds | ||
|
||
def _create_service(self): | ||
"""GSuite requests must be signed with the keyfile | ||
|
||
Returns: | ||
bool: True if the Google API discovery service was successfully established or False | ||
if any errors occurred during the creation of the Google discovery service, | ||
""" | ||
if self._activities_service: | ||
LOGGER.debug('Service already instantiated for %s', self.type()) | ||
return True | ||
|
||
creds = self._load_credentials(self._config.auth['keyfile']) | ||
if not creds: | ||
return False | ||
|
||
try: | ||
resource = discovery.build('admin', 'reports_v1', credentials=creds) | ||
except errors.Error: | ||
LOGGER.exception('Failed to build discovery service for %s', self.type()) | ||
return False | ||
|
||
# The google discovery service 'Resource' class that is returned by | ||
# 'discovery.build' dynamically loads methods/attributes, so pylint will complain | ||
# about no 'activities' member existing without the below pylint comment | ||
self._activities_service = resource.activities() # pylint: disable=no-member | ||
|
||
return True | ||
|
||
def _gather_logs(self): | ||
"""Gather the G Suite Admin Report logs for this application type | ||
|
||
Returns: | ||
bool or list: If the execution fails for some reason, return False. | ||
Otherwise, return a list of activies for this application type. | ||
""" | ||
if not self._create_service(): | ||
return False | ||
|
||
activities_list = self._activities_service.list( | ||
userKey='all', | ||
applicationName=self._type(), | ||
startTime=self._last_timestamp, | ||
pageToken=self._next_page_token if self._next_page_token else None | ||
) | ||
|
||
try: | ||
results = activities_list.execute() | ||
except errors.HttpError: | ||
LOGGER.exception('Failed to execute activities listing') | ||
return False | ||
|
||
if not results: | ||
LOGGER.error('No results received from the G Suite API request for %s', self.type()) | ||
return False | ||
|
||
self._next_page_token = results.get('nextPageToken') | ||
self._more_to_poll = bool(self._next_page_token) | ||
|
||
activities = results.get('items', []) | ||
if not activities: | ||
LOGGER.info('No logs in response from G Suite API request for %s', self.type()) | ||
return False | ||
|
||
self._last_timestamp = activities[-1]['id']['time'] | ||
|
||
return activities | ||
|
||
@classmethod | ||
def required_auth_info(cls): | ||
# Use a validation function to ensure the file the user provides is valid | ||
def keyfile_validator(keyfile): | ||
"""A JSON formatted (not p12) Google service account private key file key""" | ||
try: | ||
with open(keyfile.strip(), 'r') as json_keyfile: | ||
keydata = json.load(json_keyfile) | ||
except (IOError, ValueError): | ||
return False | ||
|
||
if not cls._load_credentials(keydata): | ||
return False | ||
|
||
return keydata | ||
|
||
return { | ||
'keyfile': | ||
{ | ||
'description': ('the path on disk to the JSON formatted Google ' | ||
'service account private key file'), | ||
'format': keyfile_validator | ||
} | ||
} | ||
|
||
def _sleep_seconds(self): | ||
"""Return the number of seconds this polling function should sleep for | ||
between requests to avoid failed requests. The Google Admin API allows for | ||
5 queries per second. Since it is very unlikely we will hit that, and since | ||
we are using Google's api client for requests, this can default to 0. | ||
|
||
Resource(s): | ||
https://developers.google.com/admin-sdk/reports/v1/limits | ||
|
||
Returns: | ||
int: Number of seconds that this function should sleep for between requests | ||
""" | ||
return 0 | ||
|
||
|
||
@app | ||
class GSuiteAdminReports(GSuiteReportsApp): | ||
"""G Suite Admin Activity Report app integration""" | ||
|
||
@classmethod | ||
def _type(cls): | ||
return 'admin' | ||
|
||
|
||
@app | ||
class GSuiteCalendarReports(GSuiteReportsApp): | ||
"""G Suite Calendar Activity Report app integration""" | ||
|
||
@classmethod | ||
def _type(cls): | ||
return 'calendar' | ||
|
||
|
||
@app | ||
class GSuiteDriveReports(GSuiteReportsApp): | ||
"""G Suite Drive Activity Report app integration""" | ||
|
||
@classmethod | ||
def _type(cls): | ||
return 'drive' | ||
|
||
|
||
@app | ||
class GSuiteGroupsReports(GSuiteReportsApp): | ||
"""G Suite Groups Activity Report app integration""" | ||
|
||
@classmethod | ||
def _type(cls): | ||
return 'groups' | ||
|
||
|
||
@app | ||
class GSuiteGPlusReports(GSuiteReportsApp): | ||
"""G Suite Google Plus Activity Report app integration""" | ||
|
||
@classmethod | ||
def _type(cls): | ||
return 'gplus' | ||
|
||
|
||
@app | ||
class GSuiteLoginReports(GSuiteReportsApp): | ||
"""G Suite Login Activity Report app integration""" | ||
|
||
@classmethod | ||
def _type(cls): | ||
return 'login' | ||
|
||
|
||
@app | ||
class GSuiteMobileReports(GSuiteReportsApp): | ||
"""G Suite Mobile Activity Report app integration""" | ||
|
||
@classmethod | ||
def _type(cls): | ||
return 'mobile' | ||
|
||
|
||
@app | ||
class GSuiteRulesReports(GSuiteReportsApp): | ||
"""G Suite Rules Activity Report app integration""" | ||
|
||
@classmethod | ||
def _type(cls): | ||
return 'rules' | ||
|
||
|
||
@app | ||
class GSuiteSAMLReports(GSuiteReportsApp): | ||
"""G Suite SAML Activity Report app integration""" | ||
|
||
@classmethod | ||
def _type(cls): | ||
return 'saml' | ||
|
||
|
||
@app | ||
class GSuiteTokenReports(GSuiteReportsApp): | ||
"""G Suite Token Activity Report app integration""" | ||
|
||
@classmethod | ||
def _type(cls): | ||
return 'token' |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there some reason that
self._next_page_token
could be an empty string? If so, you could do simply:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The case of it potentially being an empty string is exactly why I added this, so good catch :). I don't know for sure if passing
''
topageToken
would have a different effect than passingNone
so I just wanted to guard against it.I added this because I'm not sure what the default value (if any) is within the response for
nextPageToken
that gets returned. It's unclear if 1) the key exists at all if there is no value, and 2) what the value would be if it does exist but is empty (is it an empty string or a json null)?The code where this is set is here, using a
.get
:streamalert/app_integrations/apps/gsuite.py
Line 121 in 1448066
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a second set of eyes on the docs would help unearth something I'm not seeing ;)