Skip to content
90 changes: 90 additions & 0 deletions Packs/SentinelOne/Integrations/SentinelOne-V2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1969,6 +1969,96 @@ Returns threat notes.
| SentinelOne.Notes.Text | string | The note text. |
| SentinelOne.Notes.UpdatedAt | string | The note updated time. |

### sentinelone-list-installed-singularity-marketplace-applications

***
Returns all installed singularity marketplace applications that match the specified filter values.

#### Base Command

`sentinelone-list-installed-singularity-marketplace-applications`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| account_ids | A comma-separated list of account IDs. | Optional |
| application_catalog_id | Filter results by application catalog id. | Optional |
| creator_contains | Free-text filter by application creator. | Optional |
| cursor | Cursor position returned by the last request. | Optional |
| limit | The maximum number of results to return (1-1000). | Optional |
| id | A comma-separated list of applications IDs. | Optional |
| name_contains | Free-text filter by application name | Optional |
| site_ids | A comma-separated list of site IDs. | Optional |

#### Context Output

| **Path** | **Type** | **Description** |
| --- | --- | --- |
| SentinelOne.InstalledApps.ID | string | The application ID. |
| SentinelOne.InstalledApps.Account | string | The account name. |
| SentinelOne.InstalledApps.AccountId | string | The account ID. |
| SentinelOne.InstalledApps.ApplicationCatalogId | string | The application Catalog ID. |
| SentinelOne.InstalledApps.applicationCatalogName | string | The application Catalog name. |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| SentinelOne.InstalledApps.applicationCatalogName | string | The application Catalog name. |
| SentinelOne.InstalledApps.ApplicationCatalogName | string | The application Catalog name. |

| SentinelOne.InstalledApps.AlertMessage | string | The alert message. |
| SentinelOne.InstalledApps.CreatedAt | date | Application created at. |
| SentinelOne.InstalledApps.Creator | string | Application creator. |
| SentinelOne.InstalledApps.CreatorId | string | Application creator ID. |
| SentinelOne.InstalledApps.DesiredStatus | string | Application desired status. |
| SentinelOne.InstalledApps.HasAlert | boolean | Application has alert. |
| SentinelOne.InstalledApps.LastEntityCreatedAt | date | Application last entity created at. |
| SentinelOne.InstalledApps.Modifier | string | Modifier. |
| SentinelOne.InstalledApps.ModifierId | string | Modifier ID. |
| SentinelOne.InstalledApps.ScopeId | string | The scope ID. |
| SentinelOne.InstalledApps.ScopeLevel | string | The scope level. |
| SentinelOne.InstalledApps.Status | string | Status of application. |
| SentinelOne.InstalledApps.UpdatedAt | string | Application updated at. |
| SentinelOne.InstalledApps.ApplicationInstanceName | string | Application instance name. |


### sentinelone-get-service-users

***
Returns all service users that match the specified filter values.

#### Base Command

`sentinelone-get-service-users`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| account_ids | A comma-separated list of account IDs. | Optional |
| role_ids | A comma-separated list of rbac roles to filter by. | Optional |
| cursor | Cursor position returned by the last request. | Optional |
| limit | The maximum number of results to return (1-1000). | Optional |
| ids | A comma-separated list of service user IDs to filter by. | Optional |
| site_ids | A comma-separated list of site IDs. | Optional |

#### Context Output

| **Path** | **Type** | **Description** |
| --- | --- | --- |
| SentinelOne.ServiceUsers.ID | string | The service user ID. |
| SentinelOne.ServiceUsers.ApiTokenCreatedAt | date | Api token created at. |
| SentinelOne.ServiceUsers.ApiTokenExpiresAt | date | Api token expires at. |
| SentinelOne.ServiceUsers.CreatedAt | date | Service user created at. |
| SentinelOne.ServiceUsers.CreatedById | string | The service user created by Id. |
| SentinelOne.ServiceUsers.CreatedByName | string | The service user created by name. |
| SentinelOne.ServiceUsers.Description | string | Service user description. |
| SentinelOne.ServiceUsers.LastActivation | date | Last activation date. |
| SentinelOne.ServiceUsers.Name | string | Service user name. |
| SentinelOne.ServiceUsers.Scope | string | Service user scope. |
| SentinelOne.ServiceUsers.UpdatedAt | date | Service user updated at. |
| SentinelOne.ServiceUsers.UpdatedById | string | Service user updated by Id. |
| SentinelOne.ServiceUsers.UpdatedByName | string | Service user updated by name. |
| SentinelOne.ServiceUsers.ScopeRolesRoleId | string | Scope roles role Id. |
| SentinelOne.ServiceUsers.ScopeRolesRoleName | string | Scope roles role name. |
| SentinelOne.ServiceUsers.ScopeRolesAccountName | string | Scope roles account name. |
| SentinelOne.ServiceUsers.ScopeRolesId | string | Scope roles Id. |


### Incident Mirroring

You can enable incident mirroring between Cortex XSOAR incidents and SentinelOne v2 corresponding events (available from Cortex XSOAR version 6.0.0).
Expand Down
148 changes: 148 additions & 0 deletions Packs/SentinelOne/Integrations/SentinelOne-V2/SentinelOne-V2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,14 @@ def get_remote_script_results_request(self, computer_names: list, task_ids: list
response = self._http_request(method='POST', url_suffix=endpoint_url, json_data=payload)
return response.get("data", {}).get("download_links", [])

def list_installed_applications_request(self, params: dict):
response = self._http_request(method='GET', url_suffix='singularity-marketplace/applications', params=params)
return response.get('data', []), response.get('pagination', {})

def get_service_users_request(self, params: dict):
response = self._http_request(method='GET', url_suffix='service-users', params=params)
return response.get('data', []), response.get('pagination', {})

def remove_empty_fields(self, json_payload):
"""
Removes empty fields from a JSON payload and returns a new JSON object with non-empty fields.
Expand Down Expand Up @@ -3433,6 +3441,144 @@ def get_power_query_results(client: Client, args: dict):
return poll_power_query_results(client=client, cmd="sentinelone-get-power-query-results", args=args)


def list_installed_singu_mark_apps_command(client: Client, args: dict) -> CommandResults:
"""
List all installed applications matching the input filter
"""
# Get arguments
query_params = assign_params(
accountIds=args.get('account_ids'),
applicationCatalogId=args.get('application_catalog_id'),
creator__contains=args.get('creator_contains'),
cursor=args.get('cursor'),
id=args.get('id'),
limit=int(args.get('limit', 1000)),
name__contains=args.get('name_contains'),
siteIds=args.get('site_ids')
)

# Make request and get raw response
installed_applications, pagination = client.list_installed_applications_request(query_params)

if pagination and pagination.get("nextCursor") is not None:
demisto.results("Use the below cursor value to get the next page installed applications \n {}".format(
pagination['nextCursor']))
all_scopes = []
if installed_applications:
for each_app in installed_applications:
scopes = each_app.get("scopes")
if scopes is not None and len(scopes) > 0:
for scope in scopes:
scope["applicationCatalogId"] = each_app["applicationCatalogId"]
scope["applicationCatalogName"] = each_app["name"]
all_scopes.append(scope)
meta = "Provides summary information and details for all the installed applications that matched your search criteria."
else:
meta = "The search filters provided are returning no results. Please review and adjust them accordingly."

context_entries = []
for each_scope in all_scopes:
entry = {
'ID': each_scope.get('id'),
'Account': each_scope.get('account'),
'AccountId': each_scope.get('accountId'),
'ApplicationCatalogId': each_scope.get('applicationCatalogId'),
'applicationCatalogName': each_scope.get('applicationCatalogName'),
'AlertMessage': each_scope.get('alertMessage'),
'CreatedAt': each_scope.get('createdAt'),
'Creator': each_scope.get('creator'),
'CreatorId': each_scope.get('creatorId'),
'DesiredStatus': each_scope.get('desiredStatus'),
'HasAlert': each_scope.get('hasAlert'),
'LastEntityCreatedAt': each_scope.get('lastEntityCreatedAt'),
'Modifier': each_scope.get('modifier'),
'ModifierId': each_scope.get('modifierId'),
'ScopeId': each_scope.get('scopeId'),
'ScopeLevel': each_scope.get('scopeLevel'),
'Status': each_scope.get('status'),
'UpdatedAt': each_scope.get('updatedAt'),
'ApplicationInstanceName': each_scope.get('applicationInstanceName'),
}
context_entries.append(entry)

return CommandResults(
readable_output=tableToMarkdown(
'SentinelOne - List of Installed Applications',
context_entries,
headerTransform=pascalToSpace,
removeNull=True,
metadata=meta
),
outputs_prefix='SentinelOne.InstalledApps',
outputs_key_field='ID',
outputs=context_entries,
raw_response=installed_applications)


def get_service_users_command(client: Client, args: dict) -> CommandResults:
"""
Get all service users matching the input filter
"""
# Get arguments
query_params = assign_params(
accountIds=args.get('account_ids'),
roleIds=args.get('role_ids'),
cursor=args.get('cursor'),
ids=args.get('ids'),
limit=int(args.get('limit', 1000)),
siteIds=args.get('site_ids')
)
# Make request and get raw response
service_users, pagination = client.get_service_users_request(query_params)

if pagination and pagination.get("nextCursor") is not None:
demisto.results("Use the below cursor value to get the next page service users \n {}".format(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we breaking here?
what happens to the page fetched already?
can we not get all the data in 1 GO? using pagination internally? instead of spitting out the page token

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in PROD - there won't be 1000s of apps or service users anyway ;)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in PROD there won't be 1000s of apps or service users. that's the reason command is making the API call once. and return the context data. In case any user has the more than 1000, in that case we are showing the cursor value in the war room, so that user can fetch the remaining 1000, and so on.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saurabh what do you think, please suggest here. thanks

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we not make further API calls for further pages and return everything - instead of sending the page token
users may not know how to use it?

XSOAR team might say the same ;)

we are returning 2 things from a command - list of users/applications + page token when its more than 1000
ideally command should result only the asked details

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. sure

pagination['nextCursor']))

context_entries = []
if service_users:
for each_service_user in service_users:
entry = {
'ID': each_service_user.get('id'),
'ApiTokenCreatedAt': each_service_user.get('apiToken', {}).get('createdAt'),
'ApiTokenExpiresAt': each_service_user.get('apiToken', {}).get('expiresAt'),
'CreatedAt': each_service_user.get('createdAt'),
'CreatedById': each_service_user.get('createdBy', {}).get('id'),
'CreatedByName': each_service_user.get('createdBy', {}).get('name'),
'Description': each_service_user.get('description'),
'LastActivation': each_service_user.get('lastActivation'),
'Name': each_service_user.get('name'),
'Scope': each_service_user.get('scope'),
'UpdatedAt': each_service_user.get('updatedAt'),
'UpdatedById': each_service_user.get('updatedBy', {}).get("id"),
'UpdatedByName': each_service_user.get('updatedBy', {}).get("name"),
}
if each_service_user.get('scopeRoles') and len(each_service_user.get('scopeRoles')) > 0:
scope_role_items = each_service_user['scopeRoles'][0]
if scope_role_items:
entry['ScopeRolesRoleId'] = scope_role_items.get('roleId')
entry['ScopeRolesRoleName'] = scope_role_items.get('roleName')
entry['ScopeRolesAccountName'] = scope_role_items.get('accountName')
entry['ScopeRolesId'] = scope_role_items.get('id')
context_entries.append(entry)
meta = "Provides summary information and details for all the service users that matched your search criteria."
else:
meta = "The search filters provided are returning no results. Please review and adjust them accordingly."

return CommandResults(
readable_output=tableToMarkdown(
'SentinelOne - Get Service Users',
context_entries,
headerTransform=pascalToSpace,
removeNull=True,
metadata=meta
),
outputs_prefix='SentinelOne.ServiceUsers',
outputs_key_field='ID',
outputs=context_entries,
raw_response=service_users)


def get_mapping_fields_command():
"""
Returns the list of fields to map in outgoing mirroring, for incidents.
Expand Down Expand Up @@ -3872,6 +4018,8 @@ def main():
'sentinelone-get-remote-script-task-results': get_remote_script_results,
'sentinelone-remote-script-automate-results': remote_script_automate_results,
'sentinelone-get-power-query-results': get_power_query_results,
'sentinelone-list-installed-singularity-marketplace-applications': list_installed_singu_mark_apps_command,
'sentinelone-get-service-users': get_service_users_command,
},
'commands_with_params': {
'get-remote-data': get_remote_data_command,
Expand Down
Loading
Loading