-
Notifications
You must be signed in to change notification settings - Fork 381
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1212 from jahamilto/AzureSignInRetriever
Azure sign in retriever
- Loading branch information
Showing
6 changed files
with
378 additions
and
0 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
{ | ||
"name": "GetAzureSignIns", | ||
"version": "1.0", | ||
"author": "@jahamilto", | ||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers", | ||
"license": "AGPL-V3", | ||
"description": "Pull all Azure sign ins for a user within the specified amount of time.", | ||
"dataTypeList": ["mail"], | ||
"command": "GetAzureSignIns/GetAzureSignIns.py", | ||
"baseConfig": "GetAzureSignIns", | ||
"configurationItems": [ | ||
{"name": "tenant_id", | ||
"description": "Azure Directory/Tenant ID", | ||
"type": "string", | ||
"multi": false, | ||
"required": true | ||
}, | ||
{"name": "client_id", | ||
"description": "Client ID/Application ID of Azure AD Registered App", | ||
"type": "string", | ||
"multi": false, | ||
"required": true | ||
}, | ||
{"name": "client_secret", | ||
"description": "Secret for Azure AD Registered Application", | ||
"type": "string", | ||
"multi": false, | ||
"required": true | ||
}, | ||
{"name": "lookup_range", | ||
"description": "Check for sign ins in the last X days. Should be between 1 and 31 days.", | ||
"type": "number", | ||
"multi": false, | ||
"required": false, | ||
"defaultValue": 7 | ||
}, | ||
{"name": "lookup_limit", | ||
"description": "Display no more than this many sign ins.", | ||
"type": "number", | ||
"multi": false, | ||
"required": false, | ||
"defaultValue": 12 | ||
}, | ||
{"name": "state", | ||
"description": "Expected sign in state (used as a taxonomy when sign ins appear outside of this area).", | ||
"type": "number", | ||
"multi": false, | ||
"required": false | ||
}, | ||
{"name": "country", | ||
"description": "Expected sign in country or region (used as a taxonomy when sign ins appear outside of this area).", | ||
"type": "number", | ||
"multi": false, | ||
"required": false | ||
} | ||
|
||
] | ||
|
||
} |
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,160 @@ | ||
#!/usr/bin/env python3 | ||
# encoding: utf-8 | ||
# Author: @jahamilto | ||
import requests | ||
import traceback | ||
from datetime import datetime, timedelta | ||
from cortexutils.analyzer import Analyzer | ||
|
||
# Initialize Azure Class | ||
class GetAzureSignIns(Analyzer): | ||
def __init__(self): | ||
Analyzer.__init__(self) | ||
self.client_id = self.get_param('config.client_id', None, 'Azure AD Application ID/Client ID Missing') | ||
self.client_secret = self.get_param('config.client_secret', None, 'Azure AD Registered Application Client Secret Missing') | ||
self.tenant_id = self.get_param('config.tenant_id', None, 'Azure AD Tenant ID Mising') | ||
self.time_range = self.get_param('config.lookup_range', 7) | ||
self.lookup_limit = self.get_param('config.lookup_limit', 12) | ||
self.state = self.get_param('config.state', None) | ||
self.country = self.get_param('config.country', None) | ||
|
||
|
||
def run(self): | ||
Analyzer.run(self) | ||
|
||
if self.data_type == 'mail': | ||
try: | ||
self.user = self.get_data() | ||
if not self.user: | ||
self.error("No user supplied") | ||
|
||
|
||
token_data = { | ||
"grant_type": "client_credentials", | ||
'client_id': self.client_id, | ||
'client_secret': self.client_secret, | ||
'resource': 'https://graph.microsoft.com', | ||
'scope': 'https://graph.microsoft.com' | ||
} | ||
|
||
filter_time = datetime.utcnow() - timedelta(days=self.time_range) | ||
format_time = str("{}T00:00:00Z".format(filter_time.strftime("%Y-%m-%d"))) | ||
|
||
|
||
|
||
#Authenticate to the graph api | ||
|
||
redirect_uri = "https://login.microsoftonline.com/{}/oauth2/token".format(self.tenant_id) | ||
token_r = requests.post(redirect_uri, data=token_data) | ||
token = token_r.json().get('access_token') | ||
|
||
if token_r.status_code != 200: | ||
self.error('Failure to obtain azure access token: {}'.format(token_r.content)) | ||
|
||
# Set headers for future requests | ||
headers = { | ||
'Authorization': 'Bearer {}'.format(token) | ||
} | ||
|
||
base_url = 'https://graph.microsoft.com/v1.0/' | ||
|
||
r = requests.get(base_url + "auditLogs/signIns?$filter=startsWith(userPrincipalName,'{}') and createdDateTime ge {}&$top={}".format(self.user, format_time, self.lookup_limit), headers=headers) | ||
|
||
# Check API results | ||
if r.status_code != 200: | ||
self.error('Failure to pull sign ins of user {}: {}'.format(self.user, r.content)) | ||
else: | ||
full_json = r.json()['value'] | ||
|
||
new_json = { | ||
"filterParameters": None, | ||
"signIns": [] | ||
} | ||
|
||
# Summary statistics | ||
risks = ex_state = ex_country = 0 | ||
|
||
for signin in full_json: | ||
|
||
success = False | ||
|
||
details = {} | ||
details["signInTime"] = signin["createdDateTime"] | ||
details["ip"] = signin["ipAddress"] | ||
details["appName"] = signin["appDisplayName"] | ||
details["clientApp"] = signin["clientAppUsed"] | ||
details["resourceName"] = signin["resourceDisplayName"] | ||
# Check how to format status result | ||
if signin["status"]["errorCode"] == 0: | ||
details["result"] = "Success" | ||
success = True | ||
else: | ||
details["result"] = "Failure: " + signin["status"]["failureReason"] | ||
details["riskLevel"] = signin["riskLevelDuringSignIn"] | ||
#Increase risk counter | ||
if details["riskLevel"] != 'none' and success: risks += 1 | ||
|
||
device = {} | ||
device_info = signin["deviceDetail"] | ||
device["id"] = "Not Available" if device_info["deviceId"] == "" else device_info["deviceId"] | ||
device["deviceName"] = "Not Available" if device_info["displayName"] == "" else device_info["displayName"] | ||
device["operatingSystem"] = device_info["operatingSystem"] | ||
|
||
location = {} | ||
location_info = signin["location"] | ||
location["city"] = location_info["city"] | ||
location["state"] = location_info["state"] | ||
if self.state and location["state"] != self.state and success: ex_state += 1 | ||
location["countryOrRegion"] = location_info["countryOrRegion"] | ||
if self.country and location["countryOrRegion"] != self.country and success: ex_country += 1 | ||
|
||
|
||
cAC = "None" | ||
for policies in signin["appliedConditionalAccessPolicies"]: | ||
if policies["result"] == "success": | ||
if cAC == 'None': | ||
cAC = policies["displayName"] | ||
else: | ||
cAC += (", " + policies["displayName"]) | ||
|
||
|
||
new_json["signIns"].append({ | ||
"id": signin["id"], | ||
"basicDetails": dict(details), | ||
"deviceDetails": dict(device), | ||
"locationDetails": dict(location), | ||
"appliedConditionalAccessPolicies": cAC | ||
}) | ||
|
||
new_json["sum_stats"] = {"riskySignIns": risks, "externalStateSignIns": ex_state, "foreignSignIns": ex_country} | ||
new_json["filterParameters"] = "Top {} signins from the last {} days. Displaying {} signins.".format(self.lookup_limit, self.time_range, len(new_json["signIns"])) | ||
|
||
# Build report to return to Cortex | ||
self.report(new_json) | ||
|
||
except Exception as ex: | ||
self.error(traceback.format_exc()) | ||
|
||
else: | ||
self.error('Incorrect dataType. "mail" expected.') | ||
|
||
|
||
def summary(self, raw): | ||
taxonomies = [] | ||
|
||
if len(raw['signIns']) == 0: | ||
taxonomies.append(self.build_taxonomy('info', 'AzureSignins', 'SignIns', 'None')) | ||
else: | ||
taxonomies.append(self.build_taxonomy('safe', 'AzureSignins', 'Count', len(raw['signIns']))) | ||
|
||
# If the summary stats are present, then add them. If not, don't. | ||
stats = raw["sum_stats"] | ||
if stats["riskySignIns"] != 0: taxonomies.append(self.build_taxonomy('suspicious', 'AzureSignins', 'Risky', stats[0])) | ||
if stats["externalStateSignIns"] != 0: taxonomies.append(self.build_taxonomy('suspicious', 'AzureSignins', 'OutOfState', stats[1])) | ||
if stats["foreignSignIns"] != 0: taxonomies.append(self.build_taxonomy('malicious', 'AzureSignins', 'ForeignSignIns', stats[2])) | ||
|
||
|
||
return {'taxonomies': taxonomies} | ||
|
||
if __name__ == '__main__': | ||
GetAzureSignIns().run() |
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,49 @@ | ||
## Azure Sign In Retreiver | ||
|
||
This responder allows you to revoke the session tokens for an Azure AD user. Requires the UPN of the account in question, which should be entered as a "mail" oberservable in TheHive. | ||
|
||
### Config | ||
|
||
To enable the responder, you *need* three values: | ||
1. Azure Tenant ID | ||
2. Application ID | ||
3. Application Secret | ||
|
||
The first two values can be found at any time in the application's Overview page in the Azure portal. The secret must be generated and then stored in a safe place, as it is only fully visible when you first make it. | ||
|
||
You can also specify the limits for how far back the analyzer requests sign ins. You can specify time and count for how many sign ins get returned. | ||
|
||
Finally, you can specify a state and country/region. These are used as taxonomies. If you run a query on a particular user and they return a few out-of-state sign ins, a taxonomy label will be added to the observable to reflect that. Likewise for the country/region. By default, this analyzer does not support selecting multiple states or countries, so if you have more than one that users will be signing in to, feel free to leave them blank. If the value is not configured, then the analyzer will simply not use the taxonomies. | ||
|
||
## Setup | ||
|
||
### Prereqs | ||
User account with the Cloud Application Administrator role. | ||
User account with the Global Administrator Role (most of the steps can be done with only the Cloud App Administrator role, but the final authorization for its API permissions requires GA). | ||
|
||
### Steps | ||
|
||
#### Creation | ||
1. Navigate to the [Azure Portal](https://portal.azure.com) and sign in with the relevant administrator account. | ||
2. Navigate to App Registrations, and create a new registration. | ||
3. Provide a display name (this can be anything, and can be changed later). Click Register. | ||
|
||
#### Secret | ||
4. Navigate to Certificates and Secrets. | ||
5. Create a new client secret. Enter a relevant description and set a security-conscious expiration date. | ||
6. Copy the Value. **This will only be fully visible for a short time, so you should immediately copy it and store it in a safe place**. | ||
|
||
#### API Permissions | ||
7. Navigate to API permissions. | ||
8. Add the Directory.Read.All, AuditLog.Read.All, and Policy.Read.ConditionalAccess permissions (Microsoft Graph API, application permissions). | ||
9. Using a GA account, select the "Grant admin consent for *TENANTNAME*" button. | ||
|
||
10. Place the relevant values into the config within Cortex. | ||
|
||
## Customization | ||
|
||
It is possible to add a color coding system to the long report as viewed from TheHive. Specifically, you can color code the Sign Ins table so that certain ones stand out. | ||
|
||
### Example | ||
|
||
Let's say you are in an organization where almost all of your users will be signing in from a single state. You could color code the table so that out-of-state sign ins are highlighted yellow, and out-of-country sign ins are highlighted in red. To enable customization like this, you must modify this analyzer's long.html to check for values within the full JSON report using the ng-style tag in the *table body > table row* element. An example exists as a comment in the long.html file at line 34. |
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,3 @@ | ||
cortexutils | ||
requests | ||
datetime |
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,104 @@ | ||
<div class="panel panel-success" ng-if="success && content.signIns.length == 0"> | ||
<div class="panel-heading"> | ||
Azure Sign Ins | ||
</div> | ||
|
||
<div class="panel-body"> | ||
Analyzers searched for: {{content.filterParameters}} | ||
</div> | ||
</div> | ||
|
||
<div class="panel panel-primary" ng-if="success && content.signIns.length > 0"> | ||
<div class="panel-heading"> | ||
Azure Sign Ins | ||
</div> | ||
|
||
<div class="panel-body"> | ||
<table class="table table-striped"> | ||
<thead> | ||
<tr> | ||
<th>SignIn ID</th> | ||
<th>Time</th> | ||
<th>Status</th> | ||
<th>IP</th> | ||
<th>App Name</th> | ||
<th>Risk</th> | ||
<th>Device ID</th> | ||
<th>Device Name</th> | ||
<th>Device OS</th> | ||
<th>Cond'l Access</th> | ||
<th>Location</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<!-- <tr ng-repeat="r in content.signIns" ng-style="{ 'background-color': r.locationDetails.countryOrRegion == 'US' && r.locationDetails.state != 'STATE' ? '#ebc27c' : r.locationDetails.countryOrRegion != 'COUNTRY' ? '#eb807c' : 'inherit' }"> --> | ||
<tr ng-repeat="r in content.signIns"> | ||
<td title={{r.id}}>{{r.id | limitTo: 8}}</td> | ||
<td>{{r.basicDetails.signInTime}}</td> | ||
<td title={{r.basicDetails.result}}>{{r.basicDetails.result | limitTo: 7}}</td> | ||
<td title={{r.basicDetails.ip}} ng-if="r.basicDetails.ip.length > 15">IPv6</td> | ||
<td ng-if="r.basicDetails.ip.length <= 14">{{r.basicDetails.ip}}</td> | ||
<td>{{r.basicDetails.appName}}</td> | ||
<td>{{r.basicDetails.riskLevel}}</td> | ||
<td ng-if="r.deviceDetails.id == 'Not Available'">Not Available</td> | ||
<td ng-if="r.deviceDetails.id != 'Not Available'" title={{r.deviceDetails.id}}>{{r.deviceDetails.id | limitTo: 8}}</td> | ||
<td>{{r.deviceDetails.deviceName}}</td> | ||
<td>{{r.deviceDetails.operatingSystem}}</td> | ||
<td ng-if="r.appliedConditionalAccessPolicies == 'None'">No</td> | ||
<td ng-if="r.appliedConditionalAccessPolicies != 'None'">Yes</td> | ||
<td>{{r.locationDetails.city}}, {{r.locationDetails.state}}, {{r.locationDetails.countryOrRegion}}</td> | ||
|
||
</tr> | ||
</tbody> | ||
</table> | ||
</div> | ||
</div> | ||
<div class="panel panel-primary" ng-if="success && content.signIns.length > 0"> | ||
<div class="panel-heading"> | ||
Expanded Information | ||
</div> | ||
|
||
<div class="panel-body"> | ||
<table class="table table-striped"> | ||
<thead> | ||
<tr> | ||
<th>SignIn ID</th> | ||
<th>IPv6</th> | ||
<th>App Name</th> | ||
<th>Client App</th> | ||
<th>Resource Name</th> | ||
<th>Applied CAPs</th> | ||
<th>Device ID</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr ng-repeat="r in content.signIns"> | ||
<td>{{r.id}}</td> | ||
<td ng-if="r.basicDetails.ip.length > 15">{{r.basicDetails.ip}}</td> | ||
<td ng-if="r.basicDetails.ip.length <= 14">IPv4</td> | ||
<td>{{r.basicDetails.appName}}</td> | ||
<td>{{r.basicDetails.clientApp}}</td> | ||
<td>{{r.basicDetails.resourceName}}</td> | ||
<td>{{r.appliedConditionalAccessPolicies}}</td> | ||
<td>{{r.deviceDetails.id}}</td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</div> | ||
|
||
</div> | ||
|
||
|
||
|
||
<!-- General error --> | ||
<div class="panel panel-danger" ng-if="!success"> | ||
<div class="panel-heading"> | ||
<strong>{{(artifact.data || artifact.attachment.name) | fang}}</strong> | ||
</div> | ||
<div class="panel-body"> | ||
<dl class="dl-horizontal" ng-if="content.errorMessage"> | ||
<dt><i class="fa fa-warning"></i> GetAzureSignIns:</dt> | ||
<dd class="wrap">{{content.errorMessage}}</dd> | ||
</dl> | ||
</div> | ||
</div> |
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,3 @@ | ||
<span class="label" ng-repeat="t in content.taxonomies" ng-class="{'safe': 'label-success', 'malicious':'label-danger'}[t.level]"> | ||
{{t.namespace}}:{{t.predicate}}="{{t.value}}" | ||
</span> |