Skip to content

Commit

Permalink
Merge pull request #1212 from jahamilto/AzureSignInRetriever
Browse files Browse the repository at this point in the history
Azure sign in retriever
  • Loading branch information
nusantara-self authored Oct 18, 2024
2 parents 447d625 + 4d3e1f2 commit a548b13
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 0 deletions.
59 changes: 59 additions & 0 deletions analyzers/GetAzureSignIns/GetAzureSignIns.json
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
}

]

}
160 changes: 160 additions & 0 deletions analyzers/GetAzureSignIns/GetAzureSignIns.py
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()
49 changes: 49 additions & 0 deletions analyzers/GetAzureSignIns/README.md
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.
3 changes: 3 additions & 0 deletions analyzers/GetAzureSignIns/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
cortexutils
requests
datetime
104 changes: 104 additions & 0 deletions thehive-templates/GetAzureSignIns_1_0/long.html
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>
3 changes: 3 additions & 0 deletions thehive-templates/GetAzureSignIns_1_0/short.html
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>

0 comments on commit a548b13

Please sign in to comment.