Phisher
KnowBE4 PhishER integration allows to pull events from PhishER system and do mutations.
Network Security · PhishER
Details
| ID | Phisher |
|---|---|
| Provider | Vista Equity Partners |
| Category | Network Security |
| From Version | 5.5.0 |
| Docker Image | demisto/python3:3.12.13.10404775 |
| Supported Modules | Agentix XSIAM |
README
KnowBe4 PhishER integration allows to pull events from PhishER system and do mutations.
This integration was integrated and tested with version 6.0.0 of XSOAR
Configure Phisher in Cortex
| Parameter | Description | Required |
|---|---|---|
| Your server URL | True | |
| API Key | True | |
| First Fetch Time | First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year) | False |
| Fetch incidents | False | |
| Fetch Limit | Maximum number of alerts per fetch. Default is 50, maximum is 100. | False |
| Incident type | False | |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
phisher-message-list
Command to get messages from PhishER
Base Command
phisher-message-list
Input
| Argument Name | Description | Required |
|---|---|---|
| limit | The maximum number of messages to fetch. Default is 50. | Optional |
| query | The Lucene query to search against. | Optional |
| id | ID of specific message to retrieve. If ID is given query will be ignored. | Optional |
| include_events | Whether to include all message events in the result. Possible values are: False, True. Default is False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Phisher.Message.actionStatus | String | Action Status |
| Phisher.Message.attachments | String | A collection of attachments associated with this message |
| Phisher.Message.category | String | The message’s category |
| Phisher.Message.comments | String | A collection of comments associated with this message. |
| Phisher.Message.events | String | A collection of events associated with this message. |
| Phisher.Message.from | String | Sender’s email |
| Phisher.Message.id | String | Unique identifier for the message. |
| Phisher.Message.links | String | A collection of links that were found in the message. |
| Phisher.Message.phishmlReport | String | The PhishML report associated with this message |
| Phisher.Message.pipelineStatus | String | Pipeline Status |
| Phisher.Message.reportedBy | String | The person who reported the message. |
| Phisher.Message.rawUrl | String | URL where to download the raw message |
| Phisher.Message.rules | String | A collection of rules associated with this message. |
| Phisher.Message.severity | String | The message’s severity |
| Phisher.Message.subject | String | Subject of the message. |
| Phisher.Message.tags | String | A collection of tags associated with this message. |
Command Example
!phisher-message-list id=00a43d65-5802-4df6-9c3c-f7d2024ddb0b
Context Example
{
"Phisher": {
"Message": {
"actionStatus": "IN_REVIEW",
"attachments": [],
"category": "CLEAN",
"comments": [
{
"body": "Folarin Balogun",
"createdAt": "2021-08-17T14:43:22Z"
},
{
"body": "Emile Smith Rowe 10",
"createdAt": "2021-08-17T14:21:17Z"
},
{
"body": "Emile Smith Rowe",
"createdAt": "2021-08-17T14:20:32Z"
},
{
"body": "Chupi & Toto",
"createdAt": "2021-08-16T12:39:15Z"
}
],
"created at": "2021-07-07T15:18:58+00:00",
"from": "ekatsenelson@example.com",
"id": "00a43d65-5802-4df6-9c3c-f7d2024ddb0b",
"links":[],
"phishmlReport": null,
"pipelineStatus": "PROCESSED",
"rawUrl": "https://phisher.example.com",
"reportedBy": "ekatsenelson@example.com",
"rules": [],
"severity": "MEDIUM",
"subject": "Fwd: Your next career opportunity is... Right Here!",
"tags": [
{
"name": "SIA",
"type": "STANDARD"
},
{
"name": "DAVY KLAASEN",
"type": "STANDARD"
},
{
"name": "DUSAN TADIC",
"type": "STANDARD"
},
{
"name": "LENO",
"type": "STANDARD"
},
{
"name": "BALOGUN",
"type": "STANDARD"
},
{
"name": "RYAN GRAVENBERGH",
"type": "STANDARD"
}
]
}
}
}
Human Readable Output
Messages
ID Status Category From Severity Created At 00a43d65-5802-4df6-9c3c-f7d2024ddb0b IN_REVIEW CLEAN ekatsenelson@example.com MEDIUM 2021-07-07T15:18:58+00:00
phisher-create-comment
Adds a comment to a PhishER message
Base Command
phisher-create-comment
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Message ID. | Required |
| comment | The comment to add. | Required |
Context Output
There is no context output for this command.
Command Example
!phisher-create-comment id=00a43d65-5802-4df6-9c3c-f7d2024ddb0b comment="Test Comment"
Human Readable Output
The comment was added successfully
phisher-update-message
Updates a PhishER message status. User must provide at least one argument.
Base Command
phisher-update-message
Input
| Argument Name | Description | Required |
|---|---|---|
| category | Message Category, can be: UNKNOWN,CLEAN,SPAM,THREAT . Possible values are: UNKNOWN, CLEAN, SPAM, THREAT. | Optional |
| status | Message Status, can be: RECEIVED,IN_REVIEW,RESOLVED. Possible values are: RECEIVED, IN_REVIEW, RESOLVED. | Optional |
| severity | Message Severity, can be: UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL. Possible values are: UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL. | Optional |
| id | Message ID. | Required |
Context Output
There is no context output for this command.
Command Example
!phisher-update-message id=00a43d65-5802-4df6-9c3c-f7d2024ddb0b category=THREAT severity=MEDIUM status=IN_REVIEW
Human Readable Output
The message was updated successfully
phisher-tags-create
Add tags to a given message
Base Command
phisher-tags-create
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Message ID. | Required |
| tags | Comma separated list of tags to add. | Required |
Context Output
There is no context output for this command.
Command Example
!phisher-tags-create id=00a43d65-5802-4df6-9c3c-f7d2024ddb0b tags="Tag1, Tag2"
Human Readable Output
The tags were updated successfully
phisher-tags-delete
Removes tags from a given message.
Base Command
phisher-tags-delete
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Message ID. | Required |
| tags | Comma separated list of tags to remove. | Required |
Context Output
There is no context output for this command.
Command Example
!phisher-tags-delete id=00a43d65-5802-4df6-9c3c-f7d2024ddb0b tags="Tag2"
Human Readable Output
The tags were deleted successfully
Configuration parameters
url— Your server URL (required)apikey—first_fetch— First Fetch TimeisFetch— Fetch incidentsmax_fetch— Fetch Limitlook_back— Advanced: Minutes to look back when fetchingincidentType— Incident typeincidentFetchInterval— Incidents Fetch Intervalinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (5)
-
phisher-create-commentAdds a comment to a PhishER message.
-
phisher-message-listCommand to get messages from PhishER.
-
phisher-tags-createAdd tags to a given message. If you have existing PhishER actions that would trigger for the tag that you're adding, you'll need to manually run the actions.
-
phisher-tags-deleteRemoves tags from a given message.
-
phisher-update-messageUpdates a PhishER message status. User must provide at least one argument.
import json import knowbe4Phisher as phisher import pytest from CommonServerPython import CommandResults from freezegun import freeze_time from test_data.mock_tests import ( create_request_test, events_example, expected_fetch, expected_time, pagination_response, response_fetch, ) def util_load_json(path): with open(path, encoding="utf-8") as f: return json.load(f) client = phisher.Client( base_url="https://eu.test.com/graphql", verify=False, headers={"Authorization": "Bearer + key", "Content-Type": "application/json"}, proxy=False, first_fetch_time="100 days", ) @pytest.mark.parametrize("test_input, expected", create_request_test) def test_create_request(test_input, expected): """ Given: - A human readable query for GQL When: - Creating GQL request Then: - Ensure that query created as expected """ res = phisher.create_gql_request(test_input) assert res == expected calculate_events = [ ('\\" reported_at:[2021-07-01T16:51:45Z TO *]\\"', "13", pagination_response[0]), ('\\" reported_at:[2021-07-01T16:51:45Z TO *]\\"', "31", pagination_response[1]), ] @pytest.mark.parametrize("query, expected, return_value", calculate_events) def test_caclulate_event(mocker, query, expected, return_value): """ Given: - A result of api response from PhishER that contains number of messages When: - When calculating number of events before fetch Then: - Ensure that the number of messages is returned as expected """ mocker.patch.object(client, "phisher_gql_request", return_value=return_value) result = phisher.calculate_number_of_events(client, query) assert result == expected assert "X-KB4-Integration" in client._headers assert client._headers["X-KB4-Integration"] == "Cortex XSOAR PhishER" test_fetch = [ ({"last_fetch": None}, "30 days", "50", expected_fetch[0], response_fetch[0]), ({"last_fetch": None}, "30 days", "50", expected_fetch[1], response_fetch[1]), ] @pytest.mark.parametrize("last_run, first_fetch, max_fetch, expected, respon", test_fetch) def test_fetch_incidents(mocker, last_run, first_fetch, max_fetch, expected, respon): """ Given: - Phisher Integration Parameters When: - Fetching incidents. Then: - Ensure that the incidents returned are as expected. """ mocker.patch.object(client, "phisher_gql_request", return_value=respon) _, result = phisher.fetch_incidents(client, last_run, first_fetch, max_fetch) assert result == expected # --- Lookback / EIR-14074 tests --- MSG_A = { "actionStatus": "RECEIVED", "category": "UNKNOWN", "comments": [], "events": [ {"causer": "null", "createdAt": "2024-01-01T10:00:00Z", "eventType": "CREATED", "id": "evt-a1", "triggerer": "null"}, ], "from": "a@example.com", "id": "msg-a", "phishmlReport": None, "pipelineStatus": "PROCESSED", "reportedAt": "2024-01-01T09:59:55Z", "severity": "UNKNOWN_SEVERITY", "subject": "Message A", "tags": [], } MSG_B = { "actionStatus": "RECEIVED", "category": "UNKNOWN", "comments": [], "events": [ {"causer": "null", "createdAt": "2024-01-01T10:05:00Z", "eventType": "CREATED", "id": "evt-b1", "triggerer": "null"}, ], "from": "b@example.com", "id": "msg-b", "phishmlReport": None, "pipelineStatus": "PROCESSED", "reportedAt": "2024-01-01T10:04:55Z", "severity": "UNKNOWN_SEVERITY", "subject": "Message B", "tags": [], } def _gql_response(messages): return { "data": { "phisherMessages": {"nodes": messages, "pagination": {"page": 1, "pages": 1, "per": 50, "totalCount": len(messages)}} } } @freeze_time("2024-01-01T11:00:00Z") def test_fetch_incidents_first_run(mocker): """ Given: - No prior run (empty last_run), first_fetch of 7 days, 2 messages returned from API When: - fetch_incidents is called Then: - Both incidents are emitted, next_run has 'time' and 'found_incident_ids' containing both message ids """ mocker.patch.object(client, "phisher_gql_request", return_value=_gql_response([MSG_A, MSG_B])) next_run, incidents = phisher.fetch_incidents(client, {}, "7 days", 50) assert len(incidents) == 2 assert "time" in next_run assert "found_incident_ids" in next_run assert "msg-a" in next_run["found_incident_ids"] assert "msg-b" in next_run["found_incident_ids"] @freeze_time("2024-01-01T11:00:00Z") def test_fetch_incidents_dedup_via_found_ids(mocker): """ Given: - last_run contains msg-a in found_incident_ids, API returns both msg-a and msg-b When: - fetch_incidents is called Then: - Only msg-b is emitted (msg-a is deduped), found_incident_ids still includes msg-b """ last_run = {"time": "2024-01-01T10:00:00Z", "found_incident_ids": {"msg-a": 1704067200}} mocker.patch.object(client, "phisher_gql_request", return_value=_gql_response([MSG_A, MSG_B])) next_run, incidents = phisher.fetch_incidents(client, last_run, "7 days", 50) assert len(incidents) == 1 assert incidents[0]["dbotMirrorId"] == "msg-b" assert "msg-b" in next_run["found_incident_ids"] @freeze_time("2024-01-01T11:00:00Z") def test_fetch_incidents_late_arrival_recovered(mocker): """ Given: - last_run time is T-30min (10:30Z), look_back=60 - API returns a message with created_at = 2024-01-01T10:05:00Z (T-55min, before last_run["time"]) - That message is NOT in found_incident_ids When: - fetch_incidents is called with look_back=60 Then: - The late-arriving message IS emitted (lookback expanded the start window to T-60min) """ last_run = {"time": "2024-01-01T10:30:00Z", "found_incident_ids": {}} late_msg = { "actionStatus": "RECEIVED", "category": "UNKNOWN", "comments": [], "events": [ { "causer": "null", "createdAt": "2024-01-01T10:05:00Z", "eventType": "CREATED", "id": "evt-late", "triggerer": "null", }, ], "from": "late@example.com", "id": "msg-late", "phishmlReport": None, "pipelineStatus": "PROCESSED", "severity": "UNKNOWN_SEVERITY", "subject": "Late message", "tags": [], } mocker.patch.object(client, "phisher_gql_request", return_value=_gql_response([late_msg])) next_run, incidents = phisher.fetch_incidents(client, last_run, "7 days", 50, look_back=60) assert len(incidents) == 1 assert incidents[0]["dbotMirrorId"] == "msg-late" @freeze_time("2024-01-01T11:00:00Z") def test_fetch_incidents_lookback_zero_no_overlap(mocker): """ Given: - last_run time is 10:30Z, look_back=0 - A spy captures the GQL payload sent to phisher_gql_request When: - fetch_incidents is called with look_back=0 Then: - The GQL payload contains 'reported_at:{2024-01-01T10:30:00Z TO' (no window expansion) """ last_run = {"time": "2024-01-01T10:30:00Z", "found_incident_ids": {}} spy = mocker.patch.object(client, "phisher_gql_request", return_value=_gql_response([])) phisher.fetch_incidents(client, last_run, "7 days", 50, look_back=0) call_arg = spy.call_args[0][0] assert "reported_at:{2024-01-01T10:30:00Z TO" in call_arg @freeze_time("2024-01-01T11:00:00Z") def test_fetch_incidents_max_fetch_truncates(mocker): """ Given: - API returns 5 messages, max_fetch=2 When: - fetch_incidents is called Then: - Only 2 incidents are emitted """ msgs = [ { **MSG_A, "id": f"msg-{i}", "subject": f"Msg {i}", "events": [ { "causer": "null", "createdAt": f"2024-01-01T10:0{i}:00Z", "eventType": "CREATED", "id": f"evt-{i}", "triggerer": "null", } ], } for i in range(5) ] mocker.patch.object(client, "phisher_gql_request", return_value=_gql_response(msgs)) _next_run, incidents = phisher.fetch_incidents(client, {}, "7 days", 2) assert len(incidents) == 2 @freeze_time("2024-01-01T11:00:00Z") def test_fetch_incidents_legacy_last_fetch_migration(mocker): """ Given: - last_run has legacy shape {"last_fetch": "2024-01-01T10:00:00Z"} (pre-lookback upgrade) - API returns msg-a When: - fetch_incidents is called Then: - msg-a is emitted (legacy time is used, not first_fetch fallback) - next_run has the new dict shape with 'time' and 'found_incident_ids' """ # legacy state gets migrated in fetch_incidents_command; fetch_incidents itself # receives the already-migrated dict, so we simulate that here legacy_migrated = {"time": "2024-01-01T10:00:00Z", "found_incident_ids": {}} mocker.patch.object(client, "phisher_gql_request", return_value=_gql_response([MSG_A])) next_run, incidents = phisher.fetch_incidents(client, legacy_migrated, "7 days", 50) assert len(incidents) == 1 assert "time" in next_run assert "found_incident_ids" in next_run @freeze_time("2024-01-01T11:00:00Z") def test_fetch_incidents_query_uses_window(mocker): """ Given: - last_run time is 10:30Z, look_back=0 - now is frozen at 11:00Z When: - fetch_incidents is called Then: - GQL payload uses a closed window 'reported_at:{10:30:00Z TO 11:00:00Z}', not open-ended 'TO *' """ last_run = {"time": "2024-01-01T10:30:00Z", "found_incident_ids": {}} spy = mocker.patch.object(client, "phisher_gql_request", return_value=_gql_response([])) phisher.fetch_incidents(client, last_run, "7 days", 50, look_back=0) call_arg = spy.call_args[0][0] assert "reported_at:{2024-01-01T10:30:00Z TO 2024-01-01T11:00:00Z}" in call_arg assert "TO *" not in call_arg @freeze_time("2024-01-01T11:00:00Z") def test_fetch_incidents_cursor_uses_reported_at(mocker): """ Given: - A message with reportedAt earlier than its CREATED event createdAt When: - fetch_incidents is called Then: - created_at_cursor is set to reportedAt, not createdAt, keeping the cursor coherent with the reported_at Lucene query field """ last_run = {"time": "2024-01-01T09:00:00Z", "found_incident_ids": {}} mocker.patch.object(client, "phisher_gql_request", return_value=_gql_response([MSG_A])) next_run, incidents = phisher.fetch_incidents(client, last_run, "7 days", 50) assert len(incidents) == 1 raw = json.loads(incidents[0]["rawJSON"]) assert raw["created_at_cursor"] == "2024-01-01T09:59:55Z" # reportedAt, not createdAt (10:00:00Z) @freeze_time("2024-01-01T11:00:00Z") def test_test_module_returns_ok_when_no_messages(mocker): """ Given: - A valid connection to PhishER that returns zero messages When: - test-module is called (e.g. from XSOAR integration config) Then: - "ok" is returned — empty inbox is not a failure """ mocker.patch.object(client, "phisher_gql_request", return_value=_gql_response([])) mocker.patch("knowbe4Phisher.demisto").getLastRun.return_value = {} mocker.patch.object(client, "max_fetch", 50) result = phisher.test_module(client) assert result == "ok" @freeze_time("2024-01-01T11:00:00Z") def test_test_module_returns_ok_when_messages_exist(mocker): """ Given: - A valid connection to PhishER that returns messages When: - test-module is called Then: - "ok" is returned """ mocker.patch.object(client, "phisher_gql_request", return_value=_gql_response([MSG_A])) mocker.patch("knowbe4Phisher.demisto").getLastRun.return_value = {} mocker.patch.object(client, "max_fetch", 50) result = phisher.test_module(client) assert result == "ok" def test_time_creation(): """ Given: - Events example from Phisher Response When: - when fetching messages from Phisher - fetch or list of all messages Then: - Ensure that the event time is extracted as expected """ result = phisher.get_created_time(events_example) assert result == expected_time mock_responses = util_load_json("test_data/test_responses.json") command_results = util_load_json("test_data/mock_responses.json") @pytest.mark.parametrize( "function_to_test, function_to_mock, args, key", [ (phisher.phisher_message_list_command, "phisher_gql_request", {}, "message_list_all"), ], ) def test_commands_with_results(mocker, function_to_test, function_to_mock, args, key): expected_res = mock_responses[key] mocker.patch.object(client, function_to_mock, return_value=command_results[key]) result: CommandResults = function_to_test(client, args) assert result.outputs == expected_res @pytest.mark.parametrize( "function_to_test, function_to_mock, args, key", [ ( phisher.phisher_create_comment_command, "phisher_gql_request", {"id": "cff35e34-aeb6-4263-b592-c68fc03ea7cb", "comment": "Infinity Test"}, "create_comment", ), ( phisher.phisher_update_message_command, "phisher_gql_request", {"id": "cff35e34-aeb6-4263-b592-c68fc03ea7cb", "category": "SPAM", "status": "RESOLVED", "severity": "HIGH"}, "update_message", ), ( phisher.phisher_create_tags_command, "phisher_gql_request", {"id": "cff35e34-aeb6-4263-b592-c68fc03ea7cb", "tags": "Test Tag"}, "create_tags", ), ( phisher.phisher_delete_tags_command, "phisher_gql_request", {"id": "cff35e34-aeb6-4263-b592-c68fc03ea7cb", "tags": "Test Tag"}, "delete_tags", ), ], ) def test_commands_no_results(mocker, function_to_test, function_to_mock, args, key): expected_res = mock_responses[key] mocker.patch.object(client, function_to_mock, return_value=command_results[key]) result = function_to_test(client, args) assert result == expected_res