-
Notifications
You must be signed in to change notification settings - Fork 331
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][onelogin] Adding new integration app for OneLogin Events #355
Changes from 4 commits
a2fac20
71f7e9a
f78b963
623154e
52cf350
167b59e
c02c975
749e090
694f45e
2ebae36
f5e866d
d7585ad
7bfe013
278a02e
ba14c63
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
""" | ||
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. | ||
""" | ||
from datetime import datetime | ||
import re | ||
import requests | ||
|
||
from app_integrations import LOGGER | ||
from app_integrations.apps.app_base import AppIntegration | ||
|
||
|
||
class OneLoginApp(AppIntegration): | ||
"""OneLogin StreamAlert App""" | ||
_ONELOGIN_EVENTS_URL = 'https://api.us.onelogin.com/api/1/events' | ||
_ONELOGIN_TOKEN_URL = 'https://api.us.onelogin.com/auth/oauth2/v2/token' | ||
# OneLogin API returns 50 events per page | ||
_MAX_EVENTS_LIMIT = 50 | ||
|
||
# Define our authorization headers variable | ||
def __init__(self, config): | ||
super(OneLoginApp, self).__init__(config) | ||
self._auth_headers = None | ||
self._next_page_url = None | ||
|
||
@classmethod | ||
def _type(cls): | ||
return 'events' | ||
|
||
@classmethod | ||
def _endpoint(cls): | ||
"""Class method to return the OneLogin events endpoint | ||
|
||
Returns: | ||
str: Path of the events endpoint to query | ||
""" | ||
return cls._ONELOGIN_EVENTS_URL | ||
|
||
@classmethod | ||
def service(cls): | ||
return 'onelogin' | ||
|
||
def _generate_headers(self, token_url, client_secret, client_id): | ||
"""Each request will request a new token to call the resources APIs. | ||
|
||
More details to be found here: | ||
https://developers.onelogin.com/api-docs/1/oauth20-tokens/generate-tokens-2 | ||
|
||
Returns: | ||
str: Bearer token to be used to call the OneLogin resource APIs | ||
""" | ||
authorization = 'client_id: %s, client_secret: %s' % (client_id, client_secret) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We tend to use
Same for the |
||
headers_token = {'Authorization': authorization, | ||
'Content-Type': 'application/json'} | ||
|
||
response = requests.post(token_url, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I anticipate potentially needing If this is too much right now, this can happen at a later date - just a thought! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should I also refactor the duo application to use the new There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes please! :) |
||
json={'grant_type':'client_credentials'}, | ||
headers=headers_token) | ||
|
||
if not self._check_http_response(response): | ||
return False | ||
|
||
bearer = 'bearer:%s' % (response.json()['access_token']) | ||
self._auth_headers = {'Authorization': bearer} | ||
|
||
def _gather_logs(self): | ||
"""Gather the authentication log events.""" | ||
|
||
if not self._auth_headers: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ++ nice! |
||
self._auth_headers = self._generate_headers(self._ONELOGIN_TOKEN_URL, | ||
self._config['auth']['client_secret'], | ||
self._config['auth']['client_id']) | ||
|
||
self._next_page_url = self._ONELOGIN_EVENTS_URL | ||
return self._get_onelogin_events() | ||
|
||
def _get_onelogin_events(self): | ||
"""Get all events from the endpoint for this timeframe | ||
|
||
Returns: | ||
[ | ||
{ | ||
'id': <int:id>, | ||
'created_at': <str:created_at>, | ||
'account_id': <int:account_id>, | ||
'user_id': <int:user_id>, | ||
'event_type_id': <int:event_type_id>, | ||
'notes': <str:notes>, | ||
'ipaddr': <str:ipaddr>, | ||
'actor_user_id': <int:actor_user_id>, | ||
'assuming_acting_user_id': null, | ||
'role_id': <int:role_id>, | ||
'app_id': <int:app_id>, | ||
'group_id': <int:group_id>, | ||
'otp_device_id': <int:otp_device_id>, | ||
'policy_id': <int:policy_id>, | ||
'actor_system': <str:actor_system>, | ||
'custom_message': <str:custom_message>, | ||
'role_name': <str:role_name>, | ||
'app_name': <str:app_name>, | ||
'group_name': <str:group_name>, | ||
'actor_user_name': <str:actor_user_name>, | ||
'user_name': <str:user_name>, | ||
'policy_name': <str:policy_name>, | ||
'otp_device_name': <str:otp_device_name>, | ||
'operation_name': <str:operation_name>, | ||
'directory_sync_run_id': <int:directory_sync_run_id>, | ||
'directory_id': <int:directory_id>, | ||
'resolution': <str:resolution>, | ||
'client_id': <int:client_id>, | ||
'resource_type_id': <int:resource_type_id>, | ||
'error_description': <str:error_description> | ||
} | ||
] | ||
""" | ||
# OneLogin API expects the ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ | ||
formatted_date = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') | ||
params = {'since': formatted_date} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is going to actually only get events since the current time. This will likely result in events for only a few seconds or less (the time between setting this date and actually querying their API for the events). You'll want to use the |
||
|
||
# Make sure we have authentication headers | ||
if not self._auth_headers: | ||
return False | ||
|
||
events = self._get_onelogin_paginated_events(params) | ||
|
||
while self._more_to_poll and self._next_page_url: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually think we should avoid doing the pagination directly within the subclass and just let the The main thought process here is that this while loop could loop for a good amount of time, and the subclassed apps don't have a concept of when the lambda function is getting close to timing out. By letting the looping happen on the base class, where there is some checks in place to look for nearing the time out, we can operate with a little more safety. |
||
LOGGER.debug('More events to retrieve for \'%s\': %s', self.type(), self._more_to_poll) | ||
pagination = self._get_onelogin_paginated_events(None) | ||
|
||
# Add the events to our results, this is equivalent to using events.extend() | ||
events += pagination | ||
|
||
# Return the list of logs to the caller so they can be send to the batcher | ||
return events | ||
|
||
def _get_onelogin_paginated_events(self, params): | ||
"""Get events from the API pagination url | ||
|
||
Returns: | ||
The same as the method _get_onelogin_events() | ||
""" | ||
response = requests.get(self._next_page_url, headers=self._auth_headers, params=params) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can the base class' |
||
if not self._check_http_response(response): | ||
return False | ||
|
||
# Extract events from response | ||
events = response.json()['data'] | ||
|
||
# Do we have a pagination link to follow? | ||
self._more_to_poll = len(events) >= self._MAX_EVENTS_LIMIT | ||
|
||
# If we are already retrieved all the events, then set the value to prevent more requests | ||
self._next_page_url = response.json()['pagination']['next_link'] | ||
|
||
return events | ||
|
||
def required_auth_info(self): | ||
return { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As we spoke about offline, please add the localization (us/eu) to these so they can be user-provided. |
||
'client_secret': | ||
{ | ||
'description': ('the client secret for the OneLogin API. This ' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice work with these! |
||
'should a string of 57 alphanumeric characters'), | ||
'format': re.compile(r'^[a-zA-Z0-9]{57}$') | ||
}, | ||
'client_id': | ||
{ | ||
'description': ('the client id for the OneLogin API. This ' | ||
'should a string of 57 alphanumeric characters'), | ||
'format': re.compile(r'^[a-zA-Z0-9]{57}$') | ||
} | ||
} | ||
|
||
def _sleep_seconds(self): | ||
"""Return the number of seconds this polling function should sleep for | ||
between requests to avoid failed requests. OneLogin tokens allows for 5000 requests | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to confirm: 5000/hour = ~83/min = ~1.3/sec - there's no way we'll accidentally exceed this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Spoke offline, Javier will intentionally hit the limit locally so he can appropriately catch the exception and throw a useful error if it occurs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need to do some testing in the actual API to see if we can just generate a new token once we exceed the maximum number of requests per hour (5000) per token. Although I doubt they would allow such a simple workaround to their rate limit. In any case I can add the code to have a safe sleep of 2 seconds per request. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed that additional testing should be done to best determine how this should be handled. |
||
every hour, so returning 0 for now. | ||
|
||
Returns: | ||
int: Number of seconds that this function should sleep for between requests | ||
""" | ||
return 0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,11 @@ | |
"logs": [ | ||
"duo" | ||
] | ||
}, | ||
"prefix_cluster_onelogin-events-app-name_app": { | ||
"logs": [ | ||
"onelogin" | ||
] | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
"""Alert on the OneLogin event that a user has assumed the role of someone else.""" | ||
from stream_alert.rule_processor.rules_engine import StreamRules | ||
|
||
rule = StreamRules.rule | ||
|
||
@rule(logs=['onelogin:events'], | ||
outputs=['aws-s3:sample-bucket', | ||
'pagerduty:sample-integration']) | ||
def onelogin_events_assumed_role(rec): | ||
""" | ||
author: airbnb_csirt | ||
description: Alert on OneLogin users assuming a different role. | ||
reference: https://developers.onelogin.com/api-docs/1/events/event-types | ||
playbook: N/A | ||
""" | ||
return rec['event_type_id'] == 3 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
{ | ||
"records": [ | ||
{ | ||
"data": { | ||
"id": 123, | ||
"created_at": "2017-10-05T18:11:32Z", | ||
"account_id": 1234, | ||
"user_id": 123456789, | ||
"event_type_id": 3, | ||
"notes": "Notes", | ||
"ipaddr": "0.0.0.0", | ||
"actor_user_id": 987, | ||
"assuming_acting_user_id": 654, | ||
"role_id": 456, | ||
"app_id": 123456, | ||
"group_id": 98765, | ||
"otp_device_id": 11111, | ||
"policy_id": 22222, | ||
"actor_system": "System", | ||
"custom_message": "Message", | ||
"role_name": "Role", | ||
"app_name": "App Name", | ||
"group_name": "Group Name", | ||
"actor_user_name": "", | ||
"user_name": "username", | ||
"policy_name": "Policy Name", | ||
"otp_device_name": "OTP Device Name", | ||
"operation_name": "Operation Name", | ||
"directory_sync_run_id": 7777, | ||
"directory_id": 6666, | ||
"resolution": "Resolved", | ||
"client_id": 11223344, | ||
"resource_type_id": 44332211, | ||
"error_description": "ERROR ERROR" | ||
}, | ||
"description": "OneLogin generated event when a user assumed a different role, it should alert", | ||
"service": "stream_alert_app", | ||
"source": "prefix_cluster_onelogin-events-app-name_app", | ||
"trigger": true | ||
}, | ||
{ | ||
"data": { | ||
"id": 123, | ||
"created_at": "2017-10-05T18:11:32Z", | ||
"account_id": 1234, | ||
"user_id": 123456789, | ||
"event_type_id": 10, | ||
"notes": "Notes", | ||
"ipaddr": "0.0.0.0", | ||
"actor_user_id": 987, | ||
"assuming_acting_user_id": 654, | ||
"role_id": 456, | ||
"app_id": 123456, | ||
"group_id": 98765, | ||
"otp_device_id": 11111, | ||
"policy_id": 22222, | ||
"actor_system": "System", | ||
"custom_message": "Message", | ||
"role_name": "Role", | ||
"app_name": "App Name", | ||
"group_name": "Group Name", | ||
"actor_user_name": "", | ||
"user_name": "username", | ||
"policy_name": "Policy Name", | ||
"otp_device_name": "OTP Device Name", | ||
"operation_name": "Operation Name", | ||
"directory_sync_run_id": 7777, | ||
"directory_id": 6666, | ||
"resolution": "Resolved", | ||
"client_id": 11223344, | ||
"resource_type_id": 44332211, | ||
"error_description": "ERROR ERROR" | ||
}, | ||
"description": "OneLogin generated event when a user do any other action, it should not alert", | ||
"service": "stream_alert_app", | ||
"source": "prefix_cluster_onelogin-events-app-name_app", | ||
"trigger": false | ||
} | ||
] | ||
} |
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.
As we spoke about offline, switch to using a url that can be formatted with a region/localization (us/eu) for this link and the token link.