Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"system": false,
"systemAssociatedTypes": null,
"threshold": 72,
"type": "shortText",
"type": "date",
"unmapped": false,
"unsearchable": false,
"useAsKpi": false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"associatedToAll": true,
"associatedToAll": false,
"associatedTypes": null,
"breachScript": "",
"caseInsensitive": true,
Expand Down
53 changes: 37 additions & 16 deletions Packs/Code42/Integrations/Code42/Code42.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ def _create_alert_query(event_severity_filter, start_time):

def _get_all_high_risk_employees_from_page(page, risk_tags):
res = []
# Note: page is a `Py42Response` and has no `get()` method.
employees = page["items"]
employees = page.get("items") or []
for employee in employees:
if not risk_tags:
res.append(employee)
Expand Down Expand Up @@ -198,8 +197,8 @@ def get_all_departing_employees(self, results):
results = int(results) if results else None
pages = self._get_sdk().detectionlists.departing_employee.get_all()
for page in pages:
# Note: page is a `Py42Response` and has no `get()` method.
employees = page["items"]
page_json = json.loads(page.text)
employees = page_json.get("items") or []
for employee in employees:
res.append(employee)
if results and len(res) == results:
Expand Down Expand Up @@ -236,7 +235,8 @@ def get_all_high_risk_employees(self, risk_tags, results):
res = []
pages = self._get_sdk().detectionlists.high_risk_employee.get_all()
for page in pages:
employees = _get_all_high_risk_employees_from_page(page, risk_tags)
page_json = json.loads(page.text)
employees = _get_all_high_risk_employees_from_page(page_json, risk_tags)
for employee in employees:
res.append(employee)
if results and len(res) == results:
Expand All @@ -246,10 +246,11 @@ def get_all_high_risk_employees(self, risk_tags, results):
def fetch_alerts(self, start_time, event_severity_filter):
query = _create_alert_query(event_severity_filter, start_time)
res = self._get_sdk().alerts.search(query)
return res["alerts"]
return json.loads(res.text).get("alerts")

def get_alert_details(self, alert_id):
res = self._get_sdk().alerts.get_details(alert_id)["alerts"]
py42_res = self._get_sdk().alerts.get_details(alert_id)
res = json.loads(py42_res.text).get("alerts")
if not res:
raise Code42AlertNotFoundError(alert_id)
return res[0]
Expand All @@ -263,7 +264,8 @@ def get_current_user(self):
return res

def get_user(self, username):
res = self._get_sdk().users.get_by_username(username)["users"]
py42_res = self._get_sdk().users.get_by_username(username)
res = json.loads(py42_res.text).get("users")
if not res:
raise Code42UserNotFoundError(username)
return res[0]
Expand Down Expand Up @@ -320,15 +322,16 @@ def remove_user_from_legal_hold_matter(self, username, matter_name):
def get_org(self, org_name):
org_pages = self._get_sdk().orgs.get_all()
for org_page in org_pages:
orgs = org_page["orgs"]
page_json = json.loads(org_page.text)
orgs = page_json.get("orgs")
for org in orgs:
if org.get("orgName") == org_name:
return org
raise Code42OrgNotFoundError(org_name)

def search_file_events(self, payload):
res = self._get_sdk().securitydata.search_file_events(payload)
return res["fileEvents"]
py42_res = self._get_sdk().securitydata.search_file_events(payload)
return json.loads(py42_res.text).get("fileEvents")

def download_file(self, hash_arg):
security_module = self._get_sdk().securitydata
Expand All @@ -337,7 +340,7 @@ def download_file(self, hash_arg):
elif _hash_is_sha256(hash_arg):
return security_module.stream_file_by_sha256(hash_arg)
else:
raise Exception("Unsupported hash. Must be SHA256 or MD5.")
raise Code42UnsupportedHashError()

def _get_user_id(self, username):
user_id = self.get_user(username).get("userUid")
Expand Down Expand Up @@ -391,6 +394,20 @@ def __init__(self, org_name):
)


class Code42UnsupportedHashError(Exception):
def __init__(self):
super(Code42UnsupportedHashError, self).__init__(
"Unsupported hash. Must be SHA256 or MD5."
)


class Code42MissingSearchArgumentsError(Exception):
def __init__(self):
super(Code42MissingSearchArgumentsError, self).__init__(
"No query args provided for searching Code42 security events."
)


class Code42LegalHoldMatterNotFoundError(Exception):
def __init__(self, matter_name):
super(Code42LegalHoldMatterNotFoundError, self).__init__(
Expand All @@ -401,8 +418,9 @@ def __init__(self, matter_name):
class Code42InvalidLegalHoldMembershipError(Exception):
def __init__(self, username, matter_name):
super(Code42InvalidLegalHoldMembershipError, self).__init__(
"User '{0}' is not an active member of legal hold matter '{1}'".format(username,
matter_name)
"User '{0}' is not an active member of legal hold matter '{1}'".format(
username, matter_name
)
)


Expand Down Expand Up @@ -468,6 +486,9 @@ def build_query_payload(args):
username = args.get("username")
exposure = args.get("exposure")

if not _hash and not hostname and not username and not exposure:
raise Code42MissingSearchArgumentsError()

search_args = FileEventQueryFilters(pg_size)
search_args.append_result(_hash, _create_hash_filter)
search_args.append_result(hostname, OSHostname.eq)
Expand Down Expand Up @@ -1143,7 +1164,7 @@ def _fetch_alerts(self, start_query_time):
return self._client.fetch_alerts(start_query_time, self._event_severity_filter)

def _create_incident_from_alert(self, alert):
details = self._client.get_alert_details(alert["id"])
details = self._client.get_alert_details(alert.get("id"))
incident = _create_incident_from_alert_details(details)
if self._include_files:
details = self._relate_files_to_alert(details)
Expand All @@ -1161,7 +1182,7 @@ def _relate_files_to_alert(self, alert_details):
return alert_details

def _get_file_events_from_alert_details(self, observation, alert_details):
security_data_query = map_observation_to_security_query(observation, alert_details["actor"])
security_data_query = map_observation_to_security_query(observation, alert_details.get("actor"))
return self._client.search_file_events(security_data_query)


Expand Down
79 changes: 63 additions & 16 deletions Packs/Code42/Integrations/Code42/Code42_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from py42.response import Py42Response
from Code42 import (
Code42Client,
Code42AlertNotFoundError,
Code42UserNotFoundError,
Code42LegalHoldMatterNotFoundError,
Code42InvalidLegalHoldMembershipError,
build_query_payload,
Expand Down Expand Up @@ -34,6 +32,11 @@
legal_hold_remove_user_command,
download_file_command,
fetch_incidents,
Code42AlertNotFoundError,
Code42UserNotFoundError,
Code42OrgNotFoundError,
Code42UnsupportedHashError,
Code42MissingSearchArgumentsError,
)
import time

Expand Down Expand Up @@ -1293,8 +1296,13 @@ def assert_detection_list_outputs_match_response_items(outputs_list, response_it
"""TESTS"""


def test_client_lazily_inits_sdk(mocker):
mocker.patch("py42.sdk.from_local_account")
def test_client_lazily_inits_sdk(mocker, code42_sdk_mock):
sdk_factory_mock = mocker.patch("py42.sdk.from_local_account")
response_json_mock = """{"total": 1, "users": [{"username": "Test"}]}"""
code42_sdk_mock.users.get_by_username.return_value = create_mock_code42_sdk_response(
mocker, response_json_mock
)
sdk_factory_mock.return_value = code42_sdk_mock

# test that sdk does not init during ctor
client = Code42Client(sdk=None, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=False)
Expand All @@ -1305,23 +1313,28 @@ def test_client_lazily_inits_sdk(mocker):
assert client._sdk is not None


def test_client_when_no_alert_found_raises_alert_not_found(code42_sdk_mock):
code42_sdk_mock.alerts.get_details.return_value = (
json.loads('{"type$": "ALERT_DETAILS_RESPONSE", "alerts": []}')
def test_client_when_no_alert_found_raises_alert_not_found(mocker, code42_sdk_mock):
response_json = """{"alerts": []}"""
code42_sdk_mock.alerts.get_details.return_value = create_mock_code42_sdk_response(
mocker, response_json
)
client = create_client(code42_sdk_mock)
with pytest.raises(Code42AlertNotFoundError):
client.get_alert_details("mock-id")


def test_client_when_no_user_found_raises_user_not_found(code42_sdk_mock):
code42_sdk_mock.users.get_by_username.return_value = json.loads('{"totalCount":0, "users":[]}')
def test_client_when_no_user_found_raises_user_not_found(mocker, code42_sdk_mock):
response_json = """{"totalCount": 0, "users": []}"""
code42_sdk_mock.users.get_by_username.return_value = create_mock_code42_sdk_response(
mocker, response_json
)
client = create_client(code42_sdk_mock)
with pytest.raises(Code42UserNotFoundError):
client.get_user("test@example.com")


def test_client_add_to_matter_when_no_legal_hold_matter_found_raises_matter_not_found(code42_sdk_mock, mocker):

code42_sdk_mock.legalhold.get_all_matters.return_value = (
get_empty_legalhold_matters_response(mocker, MOCK_GET_ALL_MATTERS_RESPONSE)
)
Expand All @@ -1331,8 +1344,9 @@ def test_client_add_to_matter_when_no_legal_hold_matter_found_raises_matter_not_
client.add_user_to_legal_hold_matter("TESTUSERNAME", "TESTMATTERNAME")


def test_client_add_to_matter_when_no_user_found_raises_user_not_found(code42_sdk_mock):
code42_sdk_mock.users.get_by_username.return_value = json.loads('{"totalCount":0, "users":[]}')
def test_client_add_to_matter_when_no_user_found_raises_user_not_found(mocker, code42_sdk_mock):
response_json = '{"totalCount":0, "users":[]}'
code42_sdk_mock.users.get_by_username.return_value = create_mock_code42_sdk_response(mocker, response_json)
client = create_client(code42_sdk_mock)
with pytest.raises(Code42UserNotFoundError):
client.add_user_to_legal_hold_matter("TESTUSERNAME", "TESTMATTERNAME")
Expand All @@ -1348,8 +1362,9 @@ def test_client_remove_from_matter_when_no_legal_hold_matter_found_raises_except
client.remove_user_from_legal_hold_matter("TESTUSERNAME", "TESTMATTERNAME")


def test_client_remove_from_matter_when_no_user_found_raises_user_not_found(code42_sdk_mock):
code42_sdk_mock.users.get_by_username.return_value = json.loads('{"totalCount":0, "users":[]}')
def test_client_remove_from_matter_when_no_user_found_raises_user_not_found(mocker, code42_sdk_mock):
response_json = '{"totalCount":0, "users":[]}'
code42_sdk_mock.users.get_by_username.return_value = create_mock_code42_sdk_response(mocker, response_json)
client = create_client(code42_sdk_mock)
with pytest.raises(Code42UserNotFoundError):
client.remove_user_from_legal_hold_matter("TESTUSERNAME", "TESTMATTERNAME")
Expand Down Expand Up @@ -1725,6 +1740,23 @@ def test_user_create_command(code42_users_mock):
assert cmd_res.outputs["Email"] == "new.user@example.com"


def test_user_create_command_when_org_not_found_raises_org_not_found(mocker, code42_users_mock):
response_json = """{"total": 0, "orgs": []}"""
code42_users_mock.orgs.get_all.return_value = create_mock_code42_sdk_response_generator(
mocker, [response_json]
)
client = create_client(code42_users_mock)
with pytest.raises(Code42OrgNotFoundError):
user_create_command(
client,
{
"orgname": _TEST_ORG_NAME,
"username": "new.user@example.com",
"email": "new.user@example.com",
}
)


def test_user_block_command(code42_users_mock):
client = create_client(code42_users_mock)
cmd_res = user_block_command(client, {"username": "new.user@example.com"})
Expand Down Expand Up @@ -1798,6 +1830,12 @@ def test_security_data_search_command(code42_file_events_mock):
assert output_item == mapped_event


def test_securitydata_search_command_when_not_given_any_queryable_args_raises_error(code42_file_events_mock):
client = create_client(code42_file_events_mock)
with pytest.raises(Code42MissingSearchArgumentsError):
securitydata_search_command(client, {})


def test_download_file_command_when_given_md5(code42_sdk_mock, mocker):
fr = mocker.patch("Code42.fileResult")
client = create_client(code42_sdk_mock)
Expand All @@ -1817,6 +1855,15 @@ def test_download_file_command_when_given_sha256(code42_sdk_mock, mocker):
assert fr.call_count == 1


def test_download_file_when_given_other_hash_raises_unsupported_hash(code42_sdk_mock, mocker):
mocker.patch("Code42.fileResult")
_hash = "41966f10cc59ab466444add08974fde4cd37f88d79321d42da8e4c79b51c214941966f10cc59ab466444add08974fde4cd37" \
"f88d79321d42da8e4c79b51c2149"
client = create_client(code42_sdk_mock)
with pytest.raises(Code42UnsupportedHashError):
_ = download_file_command(client, {"hash": _hash})


def test_fetch_when_no_significant_file_categories_ignores_filter(
code42_fetch_incidents_mock, mocker
):
Expand All @@ -1840,8 +1887,8 @@ def test_fetch_when_no_significant_file_categories_ignores_filter(
assert "IMAGE" not in actual_query


def test_fetch_incidents_handles_single_severity(code42_sdk_mock):
client = create_client(code42_sdk_mock)
def test_fetch_incidents_handles_single_severity(code42_fetch_incidents_mock):
client = create_client(code42_fetch_incidents_mock)
fetch_incidents(
client=client,
last_run={"last_fetch": None},
Expand All @@ -1851,7 +1898,7 @@ def test_fetch_incidents_handles_single_severity(code42_sdk_mock):
include_files=True,
integration_context=None,
)
assert "HIGH" in str(code42_sdk_mock.alerts.search.call_args[0][0])
assert "HIGH" in str(code42_fetch_incidents_mock.alerts.search.call_args[0][0])


def test_fetch_incidents_handles_multi_severity(code42_fetch_incidents_mock):
Expand Down
1 change: 0 additions & 1 deletion Packs/Code42/Integrations/Code42/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
Use the Code42 integration to identify potential data exfiltration from insider threats while speeding investigation and response by providing fast access to file events and metadata across physical and cloud environments.
This integration was integrated and tested with version xx of Code42
## Configure Code42 on Cortex XSOAR

1. Navigate to **Settings** > **Integrations** > **Servers & Services**.
Expand Down
4 changes: 2 additions & 2 deletions Packs/Code42/Integrations/Code42/command_examples.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
!code42-departingemployee-add username="partner.demisto@example.com" departuredate="2020-010-28" note="Leaving for competitor"
!code42-departingemployee-remove username="partner.demisto@example.com"
!code42-departingemployee-get-all
!code42-highriskemployee-add username="partner.demisto@example.com" note="Risky activity"
!code42-highriskemployee-remove username="partner.demisto@example.com" note="Risky activity"
!code42-highriskemployee-add username="partner.demisto@example.com" note="Approved activity"
!code42-highriskemployee-remove username="partner.demisto@example.com" note="Approved activity"
!code42-highriskemployee-get-all
!code42-highriskemployee-add-risk-tags username="partner.demisto@example.com" note="PERFORMANCE_CONCERN"
!code42-highriskemployee-remove-risk-tags username="partner.demisto@example.com" risktags="PERFORMANCE_CONCERNS"
Expand Down
3 changes: 3 additions & 0 deletions Packs/Code42/ReleaseNotes/2_0_0.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

#### Integrations
##### Code42
## [Unreleased]
- Internal code improvements.
- Added new commands:
- **code42-departingemployee-get-all**
Expand All @@ -14,6 +15,8 @@
- **code42-user-block**
- **code42-user-unblock**
- **code42-user-create**
- **code42-legalhold-add-user**
- **code42-legalhold-remove-user**
- **code42-file-download**
- Improve error messages for all Commands to include exception detail.
- **Code42 Exfiltration Playbook** now downloads the file in replace of a manual step for retrieving file contents.
Expand Down