DeHashed
This integration allows you to check if your personal information such as your email, username, or password is being compromised.
Utilities · DeHashed
Details
| ID | DeHashed |
|---|---|
| Provider | DeHashed |
| Category | Utilities |
| From Version | 5.5.0 |
| Docker Image | demisto/fastapi:0.125.0.10158186 |
| Supported Modules | Agentix XSIAM |
README
This integration allows you to check if your personal information such as your email, username, or password is being compromised.
Configure DeHashed in Cortex
| Parameter | Description | Required |
|---|---|---|
| API Key | For generating an API Key, see https://www.dehashed.com. | True |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False | |
| Email Severity: The DBot reputation for compromised emails (SUSPICIOUS or MALICIOUS) | False | |
| Source Reliability | Reliability of the source providing the intelligence data. | 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.
dehashed-search
Performs a search to check if information is compromised.
Base Command
dehashed-search
Input
| Argument Name | Description | Required |
|---|---|---|
| asset_type | If you select the “all fields” option, the search is performed on all fields with the specified value entered in the “value” argument, and you don’t have to pass the “operation” argument. Possible values are: email, ip_address, username, hashed_password, name, vin, address, phone, all_fields. | Required |
| value | The searched value. | Required |
| operation | The search operator. Possible values are: is, regex. | Required |
| page | The number of page to return. Each page contains up to 5,000 results. | Optional |
| results_from | Starting result number to display. Default is 1. | Optional |
| results_to | Ending result number to display. Default is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| DeHashed.Search.Id | String | ID of the object. |
| DeHashed.Search.Email | String | Email address of the object. |
| DeHashed.Search.Username | String | Username of the object. |
| DeHashed.Search.Password | String | Password of the object. |
| DeHashed.Search.HashedPassword | String | Hashed password of the object. |
| DeHashed.Search.Name | String | Name of the object. |
| DeHashed.Search.Address | String | Address of the object. |
| DeHashed.Search.IpAddress | String | IP address of the object. |
| DeHashed.Search.Phone | Number | Phone number of the object. |
| DeHashed.Search.Dob | String | Date of birth. |
| DeHashed.Search.LicensePlate | String | License plate. |
| DeHashed.Search.Company | String | Company name. |
| DeHashed.Search.Url | String | Associated URL. |
| DeHashed.Search.Social | String | Social media handle. |
| DeHashed.Search.CryptocurrencyAddress | String | Cryptocurrency address. |
| DeHashed.Search.DatabaseName | String | Source database/breach name (drives DBot score). |
| Dehashed.LastQuery.ResultsFrom | Number | The value of the “results_from” argument that was passed in the last query. |
| Dehashed.LastQuery.ResultsTo | Unknown | The value of the “results_to” argument that was passed in the last query. |
| Dehashed.LastQuery.TotalResults | Number | The total number of entries returned from the last query. |
| Dehashed.LastQuery.DisplayedResults | Number | The number of entries that were displayed in Cortex XSOAR from the last query. |
Command Example
!dehashed-search asset_type=all_fields operation=contains value=or-gal@gmail.com results_to=4 results_from=1
!dehashed-search asset_type=email operation=is value=or-gal@gmail.com page=1
!dehashed-search asset_type=name operation=contains value=gal,gil,test1 results_from=2 results_to=30 page=3
!dehashed-search asset_type=name operation=regex value=joh?n(ath[oa]n)
Human Readable Output
Checks if an email address was compromised.
Base Command
email
Input
| Argument Name | Description | Required |
|---|---|---|
| The email address to check. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| DeHashed.Search.Id | String | ID of the object. |
| DeHashed.Search.Email | String | Email address of the object. |
| DeHashed.Search.Username | String | Username of the object. |
| DeHashed.Search.Password | String | Password of the object. |
| DeHashed.Search.HashedPassword | String | Hashed password of the object. |
| DeHashed.Search.Name | String | Name of the object. |
| DeHashed.Search.Address | String | Address of the object. |
| DeHashed.Search.IpAddress | String | IP address of the object. |
| DeHashed.Search.Phone | Number | Phone number of the object. |
| DeHashed.Search.Dob | String | Date of birth. |
| DeHashed.Search.LicensePlate | String | License plate. |
| DeHashed.Search.Company | String | Company name. |
| DeHashed.Search.Url | String | Associated URL. |
| DeHashed.Search.Social | String | Social media handle. |
| DeHashed.Search.CryptocurrencyAddress | String | Cryptocurrency address. |
| DeHashed.Search.DatabaseName | String | Source database/breach name (drives DBot score). |
| DBotScore.Indicator | String | The indicator that was tested. |
| DBotScore.Type | String | The indicator type. |
| DBotScore.Vendor | String | The vendor used to calculate the score. |
| DBotScore.Score | Number | The actual score. |
| DBotScore.Reliability | String | Reliability of the source providing the intelligence data. |
Command Example
!email email=or-gal@gmail.com
Human Readable Output
Configuration parameters
credentials— (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsemail_dbot_score— Email Severity: The DBot reputation for compromised emails (SUSPICIOUS or MALICIOUS)integration_reliability— Source Reliability
Commands (2)
-
dehashed-searchPerforms a search to check if information is compromised.
-
emailChecks if an email address was compromised.
import demistomock as demisto import pytest from pytest_mock import MockerFixture import json from pathlib import Path from typing import Any, TYPE_CHECKING if TYPE_CHECKING: from DeHashed import DehashedClient def load_mock_response(file_path: str) -> dict | list: """ Helper function to load mock response data from a JSON file. Args: file_path (str): The relative path under the test_data directory to the JSON file (e.g. "dehashed-search/response.json"). Returns: dict | list: The parsed JSON content. """ with (Path(__file__).parent / "test_data" / file_path).open() as f: return json.load(f) @pytest.fixture(autouse=True) def mock_support_multithreading(mocker: MockerFixture) -> None: """Mock support_multithreading to prevent demistomock attribute errors. This fixture automatically runs before each test to mock the support_multithreading function which is called during ContentClient initialization. Without this mock, tests fail with: AttributeError: module 'demistomock' has no attribute '_Demisto__do' """ mocker.patch("ContentClientApiModule.support_multithreading") @pytest.fixture def client() -> "DehashedClient": """ Pytest fixture that initializes and returns a DehashedClient instance for testing. Returns: DehashedClient: An instance of the DeHashed API client. """ from pydantic import SecretStr from DeHashed import DehashedClient, DehashedParams, Credentials params = DehashedParams( credentials=Credentials( password=SecretStr("test-key"), ), ) # type: ignore[call-arg] return DehashedClient(params) # region helpers def test_build_search_query_raises_on_empty_value() -> None: """ Given: - An empty `value` string. When: - Calling _build_search_query. Then: - Raises DemistoException with the 'must get "value" as an argument' message. """ from DeHashed import _build_search_query, DemistoException with pytest.raises(DemistoException, match='This command must get "value" as an argument.'): _build_search_query("email", "", "is") @pytest.mark.parametrize( "asset_type, value, operation, expected_query", [ pytest.param("email", "a@b.co", "is", 'email:"a@b.co"', id="is_single_email"), pytest.param("all_fields", "testgamil.co", "is", '"testgamil.co"', id="is_single_all_fields"), pytest.param("all_fields", "joh?n(ath[oa]n)", "regex", "joh?n(ath[oa]n)", id="regex_single_all_fields"), pytest.param("vin", "abc", "regex", "vin:abc", id="regex_single_vin"), ], ) def test_build_search_query_parametrized( asset_type: str, value: str, operation: str, expected_query: str, ) -> None: """ Given: - Various combinations of `asset_type`, `value`, and `operation` covering the `is` and `regex` operators (the only supported `Operation` Literal values) with a single string `value`. When: - Calling _build_search_query. Then: - Returns the query string per the documented rules: - `is` -> wraps the value in double quotes. - `regex` -> uses the raw value (regex relies on the client `regex=True` flag). - `all_fields` -> omits the leading `"<asset_type>:"` prefix. """ from DeHashed import _build_search_query assert _build_search_query(asset_type, value, operation) == expected_query def test_filter_results_explicit_in_range() -> None: """ Given: - A 2-entry list and `results_from=1`, `results_to=2`. When: - Calling _filter_results. Then: - Returns the full list with resolved `(1, 2)`. """ from DeHashed import _filter_results entries = [{"id": "id_1"}, {"id": "id_2"}] sliced, results_from, results_to = _filter_results(entries, 1, 2) assert sliced == entries assert results_from == 1 assert results_to == 2 def test_filter_results_to_beyond_len() -> None: """ Given: - A 2-entry list and `results_to=50` (beyond list length). When: - Calling _filter_results. Then: - Returns the full list (Python slicing tolerates `stop > len`); the resolved `results_to` is not clamped — it is returned verbatim. """ from DeHashed import _filter_results entries = [{"id": "id_1"}, {"id": "id_2"}] sliced, results_from, results_to = _filter_results(entries, 1, 50) assert sliced == entries assert results_from == 1 assert results_to == 2 def test_filter_results_explicit_range() -> None: """ Given: - A 2-entry list and `results_from=1`, `results_to=1`. When: - Calling _filter_results. Then: - Returns only the first entry; resolved range is `(1, 1)`. """ from DeHashed import _filter_results entries = [{"id": "id_1"}, {"id": "id_2"}] sliced, results_from, results_to = _filter_results(entries, 1, 1) assert sliced == entries[0:1] assert results_from == 1 assert results_to == 1 def test_transform_entry_flattens_lists() -> None: """ Given: - An entry with list-typed identity fields (`email`, `phone`). When: - Calling _transform_entry. Then: - List fields are flattened into comma-separated strings; keys are CamelCase. """ from DeHashed import _transform_entry out = _transform_entry({"email": ["a@b.co"], "phone": ["1", "2"]}) assert out == {"Email": "a@b.co", "Phone": "1, 2"} def test_transform_entry_passthrough_scalars() -> None: """ Given: - An entry with scalar string fields. When: - Calling _transform_entry. Then: - Scalar fields are passed through unchanged with CamelCase keys. """ from DeHashed import _transform_entry out = _transform_entry({"id": "x", "database_name": "DB"}) assert out == {"Id": "x", "DatabaseName": "DB"} def test_transform_entry_omits_empty_and_none() -> None: """ Given: - An entry containing `[]`, `[""]`, `None`, and `""` values mixed with valid ones. When: - Calling _transform_entry. Then: - Empty / None-valued keys are omitted from the output entirely. """ from DeHashed import _transform_entry out = _transform_entry( { "id": "x", "empty_list": [], "list_of_empty": [""], "none_value": None, "empty_string": "", "email": ["a@b.co"], } ) assert out == {"Id": "x", "Email": "a@b.co"} assert "EmptyList" not in out assert "ListOfEmpty" not in out assert "NoneValue" not in out assert "EmptyString" not in out def test_transform_entry_passthrough_dict() -> None: """ Given: - An entry with a nested dict value (`raw_record`). When: - Calling _transform_entry. Then: - The dict is passed through unchanged under a CamelCase key. """ from DeHashed import _transform_entry out = _transform_entry({"raw_record": {"k": "v"}}) assert out == {"RawRecord": {"k": "v"}} def test_transform_entry_passthrough_dict_with_bool_values() -> None: """ Given: - An entry whose `raw_record` dict contains bool values (mirrors the canonical api_response_example.json shape). When: - Calling _transform_entry. Then: - The dict (including bool values) is passed through unchanged. """ from DeHashed import _transform_entry out = _transform_entry({"raw_record": {"le_only": True, "unstructured": True}}) assert out == {"RawRecord": {"le_only": True, "unstructured": True}} def test_transform_entry_camel_case_keys() -> None: """ Given: - An entry with snake_case keys requiring multi-word CamelCase conversion. When: - Calling _transform_entry. Then: - Keys are converted to CamelCase (e.g. `hashed_password` -> `HashedPassword`). """ from DeHashed import _transform_entry out = _transform_entry({"hashed_password": ["abc"], "ip_address": ["1.1.1.1"]}) assert "HashedPassword" in out assert "IpAddress" in out assert out["HashedPassword"] == "abc" assert out["IpAddress"] == "1.1.1.1" def test_compute_score_no_sources_returns_none() -> None: """ Given: - An empty entries list (or entries lacking `database_name`). When: - Calling compute_score. Then: - Returns Common.DBotScore.NONE (0). """ from CommonServerPython import Common from DeHashed import compute_score assert compute_score([], "SUSPICIOUS") == Common.DBotScore.NONE assert compute_score([{"id": "x"}], "SUSPICIOUS") == Common.DBotScore.NONE def test_compute_score_suspicious_label() -> None: """ Given: - Entries containing `database_name` and the configured label "SUSPICIOUS". When: - Calling compute_score. Then: - Returns Common.DBotScore.SUSPICIOUS (numeric 2). """ from CommonServerPython import Common from DeHashed import compute_score score = compute_score([{"database_name": "DB-A"}], "SUSPICIOUS") assert score == Common.DBotScore.SUSPICIOUS assert score == 2 def test_compute_score_malicious_label() -> None: """ Given: - Entries containing `database_name` and the configured label "MALICIOUS". When: - Calling compute_score. Then: - Returns Common.DBotScore.BAD (numeric 3, labeled "MALICIOUS" in the UI). """ from CommonServerPython import Common from DeHashed import compute_score score = compute_score([{"database_name": "DB-A"}], "MALICIOUS") assert score == Common.DBotScore.BAD assert score == 3 def test_compute_score_unknown_label_defaults_to_bad() -> None: """ Given: - Entries with `database_name` and an unknown label string ("UNKNOWN"). When: - Calling compute_score. Then: - Defaults to Common.DBotScore.BAD (the `else` branch). """ from CommonServerPython import Common from DeHashed import compute_score assert compute_score([{"database_name": "DB-A"}], "UNKNOWN") == Common.DBotScore.BAD # endregion # region parameters class TestCredentials: """Tests for the Credentials pydantic model.""" @pytest.mark.parametrize( "password", [ pytest.param("simple-key", id="simple"), pytest.param("P@ssw0rd!#$%", id="complex"), pytest.param("a" * 64, id="long"), ], ) def test_password_stored_as_secret_str(self, password: str) -> None: """ Given: - A plain string password. When: - Constructing a Credentials model. Then: - The password field is stored as SecretStr and accessible via get_secret_value(). """ from pydantic import SecretStr from DeHashed import Credentials creds = Credentials(password=password) # type: ignore[arg-type] assert isinstance(creds.password, SecretStr) assert creds.password.get_secret_value() == password def test_credentials_missing_password_raises(self) -> None: """ Given: - No password provided. When: - Constructing Credentials. Then: - Raises DemistoException mentioning `password`. """ from CommonServerPython import DemistoException from DeHashed import Credentials with pytest.raises(DemistoException, match="password"): Credentials() # type: ignore[call-arg] class TestDehashedParams: """Tests for the DehashedParams pydantic model.""" def test_api_key_property_returns_credentials_password(self) -> None: """ Given: - DehashedParams constructed with credentials. When: - Accessing the `api_key` property. Then: - Returns the SecretStr password from the credentials. """ from pydantic import SecretStr from DeHashed import DehashedParams, Credentials params = DehashedParams( credentials=Credentials(password=SecretStr("my-secret")), ) # type: ignore[call-arg] assert isinstance(params.api_key, SecretStr) assert params.api_key.get_secret_value() == "my-secret" def test_email_dbot_score_default(self) -> None: """ Given: - No explicit `email_dbot_score`. When: - Constructing DehashedParams. Then: - The `email_dbot_score` defaults to "SUSPICIOUS". """ from pydantic import SecretStr from DeHashed import DehashedParams, Credentials params = DehashedParams( credentials=Credentials(password=SecretStr("my-secret")), ) # type: ignore[call-arg] assert params.email_dbot_score == "SUSPICIOUS" def test_email_dbot_score_explicit_malicious(self) -> None: """ Given: - An explicit `email_dbot_score="MALICIOUS"`. When: - Constructing DehashedParams. Then: - The value is stored as "MALICIOUS". """ from pydantic import SecretStr from DeHashed import DehashedParams, Credentials params = DehashedParams( credentials=Credentials(password=SecretStr("my-secret")), email_dbot_score="MALICIOUS", ) # type: ignore[call-arg] assert params.email_dbot_score == "MALICIOUS" def test_integration_reliability_default(self) -> None: """ Given: - No `integration_reliability` passed. When: - Constructing DehashedParams. Then: - `integration_reliability` defaults to "B - Usually reliable" (matches the integration YAML default — see DehashedParams field default). """ from pydantic import SecretStr from DeHashed import DehashedParams, Credentials params = DehashedParams( credentials=Credentials(password=SecretStr("my-secret")), ) # type: ignore[call-arg] assert params.integration_reliability == "B - Usually reliable" def test_params_missing_credentials_raises(self) -> None: """ Given: - No credentials provided. When: - Constructing DehashedParams. Then: - Raises DemistoException mentioning `credentials`. """ from CommonServerPython import DemistoException from DeHashed import DehashedParams with pytest.raises(DemistoException, match="credentials"): DehashedParams() # type: ignore[call-arg] class TestDehashedSearchArgs: """Tests for the DehashedSearchArgs pydantic model.""" def test_required_fields_only(self) -> None: """ Given: - Only `asset_type`, `value`, `operation`. When: - Constructing DehashedSearchArgs. Then: - Required fields are set; optional `page`, `results_from`, `results_to` fall back to their integer field defaults (`1`, `1`, `50`). """ from DeHashed import DehashedSearchArgs args = DehashedSearchArgs(asset_type="email", value="a@b.co", operation="is") # type: ignore[call-arg] assert args.asset_type == "email" assert args.value == "a@b.co" assert args.operation == "is" assert args.page == 1 assert args.results_from == 1 assert args.results_to == 50 def test_value_passthrough_string(self) -> None: """ Given: - A plain string `value="a@b.co"`. When: - Constructing DehashedSearchArgs. Then: - `value` is stored verbatim as a string (no list-splitting / no quoting — quoting is applied later in `_build_search_query` based on `operation`). """ from DeHashed import DehashedSearchArgs args = DehashedSearchArgs(asset_type="email", value="a@b.co", operation="is") # type: ignore[call-arg] assert args.value == "a@b.co" def test_value_with_commas_preserved(self) -> None: """ Given: - A string containing commas `value="a,b,c"`. When: - Constructing DehashedSearchArgs. Then: - `value` is preserved as the literal string `"a,b,c"` — there is no CSV-splitting / multi-value handling in the new query construction logic. """ from DeHashed import DehashedSearchArgs args = DehashedSearchArgs(asset_type="email", value="a,b,c", operation="is") # type: ignore[call-arg] assert args.value == "a,b,c" @pytest.mark.parametrize( "page_input, expected_page", [ pytest.param(3, 3, id="int"), pytest.param("4", 4, id="string"), ], ) def test_page_coercion_parametrized(self, page_input: int | str, expected_page: int) -> None: """ Given: - Page inputs of various supported types (int, string-int). When: - Constructing DehashedSearchArgs. Then: - `page` is coerced to int via `arg_to_number`. """ from DeHashed import DehashedSearchArgs args = DehashedSearchArgs( asset_type="email", value="a@b.co", operation="is", page=page_input, ) # type: ignore[call-arg] assert args.page == expected_page @pytest.mark.parametrize( "page_input", [ pytest.param(0, id="zero"), pytest.param(-1, id="negative"), ], ) def test_page_zero_or_negative_raises(self, page_input: int) -> None: """ Given: - `page=0` or `page=-1`. When: - Constructing DehashedSearchArgs. Then: - Raises DemistoException (validator wraps ValueError) — `page` must be > 0. """ from CommonServerPython import DemistoException from DeHashed import DehashedSearchArgs with pytest.raises(DemistoException, match="page"): DehashedSearchArgs(asset_type="email", value="a@b.co", operation="is", page=page_input) # type: ignore[call-arg] @pytest.mark.parametrize( "results_from_input", [ pytest.param(0, id="zero"), pytest.param(-1, id="negative"), ], ) def test_results_from_validator(self, results_from_input: int) -> None: """ Given: - `results_from=0` or `results_from=-1`. When: - Constructing DehashedSearchArgs. Then: - Raises DemistoException mentioning `results_from` — must be > 0. """ from CommonServerPython import DemistoException from DeHashed import DehashedSearchArgs with pytest.raises(DemistoException, match="results_from"): DehashedSearchArgs(asset_type="email", value="a@b.co", operation="is", results_from=results_from_input) # type: ignore[call-arg] @pytest.mark.parametrize( "results_to_input", [ pytest.param(0, id="zero"), pytest.param(-1, id="negative"), ], ) def test_results_to_validator(self, results_to_input: int) -> None: """ Given: - `results_to=0` or `results_to=-1`. When: - Constructing DehashedSearchArgs. Then: - Raises DemistoException mentioning `results_to` — must be > 0. """ from CommonServerPython import DemistoException from DeHashed import DehashedSearchArgs with pytest.raises(DemistoException, match="results_to"): DehashedSearchArgs(asset_type="email", value="a@b.co", operation="is", results_to=results_to_input) # type: ignore[call-arg] def test_invalid_asset_type_raises(self) -> None: """ Given: - An `asset_type` not in the AssetType Literal. When: - Constructing DehashedSearchArgs. Then: - Raises DemistoException (Literal violation). """ from CommonServerPython import DemistoException from DeHashed import DehashedSearchArgs with pytest.raises(DemistoException, match="asset_type"): DehashedSearchArgs(asset_type="bogus", value="x", operation="is") # type: ignore[call-arg] def test_invalid_operation_raises(self) -> None: """ Given: - An `operation` not in the Operation Literal. When: - Constructing DehashedSearchArgs. Then: - Raises DemistoException (Literal violation). """ from CommonServerPython import DemistoException from DeHashed import DehashedSearchArgs with pytest.raises(DemistoException, match="operation"): DehashedSearchArgs(asset_type="email", value="x", operation="bogus") # type: ignore[call-arg] class TestEmailArgs: """Tests for the EmailArgs pydantic model.""" def test_email_csv_split(self) -> None: """ Given: - A CSV string `email="a@b.co,c@d.co"`. When: - Constructing EmailArgs. Then: - `email` is split into `["a@b.co", "c@d.co"]` via argToList. """ from DeHashed import EmailArgs args = EmailArgs(email="a@b.co,c@d.co") # type: ignore[arg-type] assert args.email == ["a@b.co", "c@d.co"] def test_email_list_passthrough(self) -> None: """ Given: - A pre-built list `email=["a@b.co"]`. When: - Constructing EmailArgs. Then: - `email` is unchanged. """ from DeHashed import EmailArgs args = EmailArgs(email=["a@b.co"]) assert args.email == ["a@b.co"] def test_email_missing_raises(self) -> None: """ Given: - No `email` provided. When: - Constructing EmailArgs. Then: - Raises DemistoException mentioning `email`. """ from CommonServerPython import DemistoException from DeHashed import EmailArgs with pytest.raises(DemistoException, match="email"): EmailArgs() # type: ignore[call-arg] class TestDehashedExecutionConfig: """Tests for the DehashedExecutionConfig holder. `BaseExecutionConfig.__init__` reads from `demisto.command()`, `demisto.params()`, and `demisto.args()`. We patch all three via `mocker.patch.object(demisto, ...)` to construct the config in tests, per the resolved §7 question 5. """ def test_params_property(self, mocker: MockerFixture) -> None: """ Given: - `demisto.params()` returns a raw params dict with credentials. When: - Accessing `config.params`. Then: - Returns a `DehashedParams` instance with the credentials populated. """ from DeHashed import DehashedExecutionConfig, DehashedParams mocker.patch.object(demisto, "command", return_value="dehashed-search") mocker.patch.object( demisto, "params", return_value={ "credentials": {"password": "test-key"}, "email_dbot_score": "MALICIOUS", }, ) mocker.patch.object(demisto, "args", return_value={}) config = DehashedExecutionConfig() params = config.params assert isinstance(params, DehashedParams) assert params.email_dbot_score == "MALICIOUS" def test_dehashed_search_args_property(self, mocker: MockerFixture) -> None: """ Given: - `demisto.args()` returns raw dehashed-search args. When: - Accessing `config.dehashed_search_args`. Then: - Returns a `DehashedSearchArgs` instance with values parsed/coerced. """ from DeHashed import DehashedExecutionConfig, DehashedSearchArgs mocker.patch.object(demisto, "command", return_value="dehashed-search") mocker.patch.object( demisto, "params", return_value={"credentials": {"password": "test-key"}}, ) mocker.patch.object( demisto, "args", return_value={"asset_type": "email", "value": "a@b.co", "operation": "is"}, ) config = DehashedExecutionConfig() args = config.dehashed_search_args assert isinstance(args, DehashedSearchArgs) assert args.asset_type == "email" assert args.value == "a@b.co" assert args.operation == "is" def test_email_args_property(self, mocker: MockerFixture) -> None: """ Given: - `demisto.args()` returns raw email args. When: - Accessing `config.email_args`. Then: - Returns an `EmailArgs` instance with the email value parsed. """ from DeHashed import DehashedExecutionConfig, EmailArgs mocker.patch.object(demisto, "command", return_value="email") mocker.patch.object( demisto, "params", return_value={"credentials": {"password": "test-key"}}, ) mocker.patch.object(demisto, "args", return_value={"email": "a@b.co"}) config = DehashedExecutionConfig() args = config.email_args assert isinstance(args, EmailArgs) assert args.email == ["a@b.co"] # endregion # region auth def test_auth_handler_sets_dehashed_api_key_header() -> None: """ Given: - A SecretStr API key. When: - Constructing DehashedAuthHandler. Then: - The handler's `header_name` is "Dehashed-Api-Key" and `key` is the unwrapped secret value (verified directly via the inherited `APIKeyAuthHandler` attributes — see resolved §7 question 3). """ from pydantic import SecretStr from DeHashed import DehashedAuthHandler handler = DehashedAuthHandler(SecretStr("test-key")) assert handler.header_name == "Dehashed-Api-Key" assert handler.key == "test-key" assert handler.query_param is None # endregion # region client def test_general_search_posts_to_search_with_query(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - A DehashedClient with `client.post` patched to return an empty envelope. When: - Calling `client.general_search(query="email:x")`. Then: - `client.post` is called once with `url_suffix="/search"`, `json_data={"query": "email:x"}`, and `resp_type="json"`. """ post_mock = mocker.patch.object(client, "post", return_value={"entries": [], "total": 0}) client.general_search(query="email:x") post_mock.assert_called_once_with( url_suffix="/search", json_data={"query": "email:x"}, resp_type="json", ) def test_general_search_assigns_only_provided_params(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - `query`, `page=1`, `regex=True`; other params left as None. When: - Calling `client.general_search`. Then: - The forwarded `json_data` contains only the non-None fields (no `size`, `wildcard`, or `de_dupe` keys). """ post_mock = mocker.patch.object(client, "post", return_value={"entries": [], "total": 0}) client.general_search(query="email:x", page=1, regex=True) post_mock.assert_called_once_with( url_suffix="/search", json_data={"query": "email:x", "page": 1, "regex": True}, resp_type="json", ) def test_general_search_returns_post_payload(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - `client.post` returns a fixture-loaded empty envelope. When: - Calling `client.general_search`. Then: - The same payload is returned to the caller verbatim. """ expected = load_mock_response("dehashed-search/response_empty.json") mocker.patch.object(client, "post", return_value=expected) result = client.general_search(query="email:x") assert result == expected # endregion # region test-module def test_module_ok(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - A reachable DeHashed API (client.general_search returns a valid envelope). When: - Running test_module. Then: - Returns "ok" and client.general_search was called with the canary email query. """ from DeHashed import test_module mock_response = load_mock_response("test-module/response_empty.json") mocker.patch.object(client, "general_search", return_value=mock_response) result = test_module(client) assert result == "ok" client.general_search.assert_called_once_with(query="email:example@example.com", page=1, size=1) # type: ignore[attr-defined] def test_module_authentication_error(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - `client.general_search` raises an Exception (e.g. unauthorized). When: - Running test_module. Then: - Returns a string starting with "AuthenticationError: Connection failed." and containing the original error message (loose-match per §7 q4). - `demisto.error` is patched to keep stdout clean. """ from DeHashed import test_module mocker.patch.object(client, "general_search", side_effect=Exception("Unauthorized")) mocker.patch.object(demisto, "error") result = test_module(client) assert result.startswith("AuthenticationError: Connection failed.") assert "Unauthorized" in result # endregion # region dehashed-search def test_dehashed_search_returns_two_command_results(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - A standard 2-entry response from the DeHashed API. When: - Calling dehashed_search_command with `asset_type="all_fields"`, `value=["testgamil.co"]`, `operation="is"`. Then: - Returns a list of two CommandResults: the first for `DeHashed.LastQuery`, the second for `DeHashed.Search` with `outputs_key_field == "Id"` (mirrors the v1 `(val.Id==obj.Id)` context path). """ from DeHashed import dehashed_search_command, DehashedSearchArgs mock_response = load_mock_response("dehashed-search/response_two_entries_with_db.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = DehashedSearchArgs(asset_type="all_fields", value="testgamil.co", operation="is") # type: ignore[call-arg] results = dehashed_search_command(client, args) assert isinstance(results, list) assert len(results) == 2 assert results[0].outputs_prefix == "DeHashed.LastQuery(true)" assert results[1].outputs_prefix == "DeHashed.Search" assert results[1].outputs_key_field == "Id" def test_dehashed_search_last_query_outputs(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - A standard 2-entry response (`total=2`) and no explicit results-range args. When: - Calling dehashed_search_command. Then: - The first CommandResults `outputs` matches the v1 `LastQuery` block: `{"ResultsFrom": 1, "ResultsTo": 2, "DisplayedResults": 2, "TotalResults": 2, "PageNumber": 1}` (note: the new code defaults `results_to` to 50 when unset, but `DisplayedResults` reflects the actual slice length and `TotalResults` reflects the API-reported total). """ from DeHashed import dehashed_search_command, DehashedSearchArgs mock_response = load_mock_response("dehashed-search/response_two_entries_with_db_simple.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = DehashedSearchArgs(asset_type="all_fields", value="testgamil.co", operation="is") # type: ignore[call-arg] results = dehashed_search_command(client, args) last_query: dict[str, Any] = results[0].outputs # type: ignore[assignment, index] assert last_query["ResultsFrom"] == 1 assert last_query["DisplayedResults"] == 2 assert last_query["TotalResults"] == 2 assert last_query["PageNumber"] == 1 def test_dehashed_search_transformed_entries_outputs(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - The standard fixture (2 entries with mixed list / scalar fields). When: - Calling dehashed_search_command. Then: - `results[1].outputs` is a list of CamelCase-keyed dicts equivalent to `_transform_entry` applied to each raw entry. """ from DeHashed import dehashed_search_command, DehashedSearchArgs, _transform_entry mock_response = load_mock_response("dehashed-search/response.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = DehashedSearchArgs(asset_type="all_fields", value="testgamil.co", operation="is") # type: ignore[call-arg] results = dehashed_search_command(client, args) transformed: list[dict[str, Any]] = results[1].outputs # type: ignore[assignment, index] expected = [_transform_entry(e) for e in mock_response["entries"]] # type: ignore[arg-type, call-overload] assert transformed == expected def test_dehashed_search_total_results_propagated(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - A response with `total=2`. When: - Calling dehashed_search_command. Then: - `LastQuery.TotalResults` equals the API-reported `total` (preserves the v1 `TotalResults` assertion). """ from DeHashed import dehashed_search_command, DehashedSearchArgs mock_response = load_mock_response("dehashed-search/response_two_entries_no_db.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = DehashedSearchArgs(asset_type="all_fields", value="testgamil.co", operation="is") # type: ignore[call-arg] results = dehashed_search_command(client, args) last_query: dict[str, Any] = results[0].outputs # type: ignore[assignment, index] assert last_query["TotalResults"] == 2 @pytest.mark.parametrize( "asset_type, value, operation, expected_query, expected_regex", [ pytest.param("all_fields", "testgamil.co", "is", '"testgamil.co"', None, id="is_single_all_fields"), pytest.param("all_fields", "joh?n(ath[oa]n)", "regex", "joh?n(ath[oa]n)", True, id="regex_single_all_fields"), pytest.param("email", "a@b.co", "is", 'email:"a@b.co"', None, id="is_single_email"), pytest.param("name", "test1", "is", 'name:"test1"', None, id="is_single_name"), pytest.param("vin", "joh?n(ath[oa]n)", "regex", "vin:joh?n(ath[oa]n)", True, id="regex_single_vin"), ], ) def test_dehashed_search_query_construction_per_operator( mocker: MockerFixture, client: "DehashedClient", asset_type: str, value: str, operation: str, expected_query: str, expected_regex: bool | None, ) -> None: """ Given: - Combinations of `asset_type`, `value`, and `operation` covering the `is`, `contains`, and `regex` operators with a single string `value`. When: - Calling dehashed_search_command (with default `page=1`). Then: - `client.general_search` is invoked with the constructed query string, `page=1` (the field default), `size=REQUEST_PAGE_SIZE` (1000 — the dehashed-search command always fetches a full page from the API), and `regex=True` only when `operation == "regex"` (else `None`). """ from DeHashed import dehashed_search_command, DehashedSearchArgs, REQUEST_PAGE_SIZE mock_response = load_mock_response("dehashed-search/response_two_entries_no_db.json") general_search_mock = mocker.patch.object(client, "general_search", return_value=mock_response) args = DehashedSearchArgs(asset_type=asset_type, value=value, operation=operation) # type: ignore[call-arg] dehashed_search_command(client, args) general_search_mock.assert_called_once_with( query=expected_query, page=1, size=REQUEST_PAGE_SIZE, wildcard=None, regex=expected_regex, de_dupe=None, ) def test_dehashed_search_results_range_slices_output(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - A 2-entry response and `results_from=1`, `results_to=1` (request only the first entry). When: - Calling dehashed_search_command. Then: - `LastQuery.ResultsFrom == 1`, `ResultsTo == 1`, `DisplayedResults == 1`, `TotalResults == 2` (full total preserved); `Search.outputs` length is 1. - Direct port of the v1 `regex_operator_with_filter_and_change_result_range` test. """ from DeHashed import dehashed_search_command, DehashedSearchArgs mock_response = load_mock_response("dehashed-search/response_two_entries_no_db.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = DehashedSearchArgs( asset_type="vin", value="joh?n(ath[oa]n)", operation="regex", results_from=1, results_to=1, ) # type: ignore[call-arg] results = dehashed_search_command(client, args) last_query: dict[str, Any] = results[0].outputs # type: ignore[assignment, index] assert last_query["ResultsFrom"] == 1 assert last_query["ResultsTo"] == 1 assert last_query["DisplayedResults"] == 1 assert last_query["TotalResults"] == 2 search_outputs: list[dict[str, Any]] = results[1].outputs # type: ignore[assignment, index] assert len(search_outputs) == 1 def test_dehashed_search_no_entries_returns_no_results_message(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - `client.general_search` returns an empty envelope (no entries). When: - Calling dehashed_search_command. Then: - Returns a single CommandResults with `readable_output == "No matching results found"` and no outputs. """ from CommonServerPython import CommandResults from DeHashed import dehashed_search_command, DehashedSearchArgs mock_response = load_mock_response("dehashed-search/response_empty.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = DehashedSearchArgs(asset_type="email", value="nobody@example.com", operation="is") # type: ignore[call-arg] result = dehashed_search_command(client, args) assert isinstance(result, CommandResults) assert result.readable_output == "No matching results found" assert result.outputs is None def test_dehashed_search_unexpected_response_raises(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - `client.general_search` returns a non-dict (e.g. a list). When: - Calling dehashed_search_command. Then: - Raises DemistoException containing "Got unexpected output from api". """ from DeHashed import dehashed_search_command, DehashedSearchArgs, DemistoException mocker.patch.object(client, "general_search", return_value=["not", "a", "dict"]) args = DehashedSearchArgs(asset_type="email", value="a@b.co", operation="is") # type: ignore[call-arg] with pytest.raises(DemistoException, match="Got unexpected output from api"): dehashed_search_command(client, args) def test_dehashed_search_passes_page_to_client(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - `DehashedSearchArgs(page=3)`. When: - Calling dehashed_search_command. Then: - `client.general_search` is called with `page=3`, `size=REQUEST_PAGE_SIZE` (1000 — the dehashed-search command always fetches a full page from the API), and the `LastQuery.PageNumber` reflects the requested page. """ from DeHashed import dehashed_search_command, DehashedSearchArgs, REQUEST_PAGE_SIZE mock_response = load_mock_response("dehashed-search/response_one_entry.json") general_search_mock = mocker.patch.object(client, "general_search", return_value=mock_response) args = DehashedSearchArgs(asset_type="email", value="a@b.co", operation="is", page=3) # type: ignore[call-arg] results = dehashed_search_command(client, args) general_search_mock.assert_called_once_with( query='email:"a@b.co"', page=3, size=REQUEST_PAGE_SIZE, wildcard=None, regex=None, de_dupe=None, ) last_query: dict[str, Any] = results[0].outputs # type: ignore[assignment, index] assert last_query["PageNumber"] == 3 # endregion # region email def test_email_command_returns_search_outputs(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - A response with breach entries and `email_dbot_score="SUSPICIOUS"`. When: - Calling email_command for `target@example.com`. Then: - Returns a `list[CommandResults]` with one entry whose `outputs_prefix=="DeHashed.Search"`, `outputs_key_field=="Id"`, `outputs` populated from `_transform_entry`, and an attached `Common.EMAIL` indicator. """ from DeHashed import email_command, EmailArgs mock_response = load_mock_response("email/response.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = EmailArgs(email=["target@example.com"]) results = email_command(client, args, email_dbot_score="SUSPICIOUS", reliability="B - Usually reliable") assert isinstance(results, list) assert len(results) == 1 response = results[0] assert response.outputs_prefix == "DeHashed.Search" assert response.outputs_key_field == "Id" assert response.outputs is not None assert len(response.outputs) == 3 # type: ignore[arg-type] assert response.indicator is not None def test_email_command_dbot_score_suspicious(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - A response with entries carrying `database_name` and `email_dbot_score="SUSPICIOUS"`. When: - Calling email_command for `testgamil.com`. Then: - The returned CommandResults exposes a `Common.EMAIL` indicator whose DBotScore equals `Common.DBotScore.SUSPICIOUS` (numeric 2). Outputs target `DeHashed.Search` with `Id` as key field. - Direct port of the v1 `test_email_command_suspicious_dbot_score`. """ from CommonServerPython import Common from DeHashed import email_command, EmailArgs mock_response = load_mock_response("email/response_two_entries_with_db.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = EmailArgs(email=["testgamil.com"]) results = email_command(client, args, email_dbot_score="SUSPICIOUS", reliability="B - Usually reliable") assert isinstance(results, list) assert len(results) == 1 response = results[0] assert response.outputs_prefix == "DeHashed.Search" assert response.outputs_key_field == "Id" assert response.indicator is not None assert response.indicator.dbot_score.score == Common.DBotScore.SUSPICIOUS # type: ignore[attr-defined] assert response.indicator.dbot_score.score == 2 # type: ignore[attr-defined] def test_email_command_dbot_score_malicious(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - A response with entries carrying `database_name` and `email_dbot_score="MALICIOUS"`. When: - Calling email_command for `testgamil.com`. Then: - The returned CommandResults exposes a `Common.EMAIL` indicator whose DBotScore equals `Common.DBotScore.BAD` (numeric 3, labeled "MALICIOUS" in the UI). - Direct port of the v1 `test_email_command_malicious_dbot_score`. """ from CommonServerPython import Common from DeHashed import email_command, EmailArgs mock_response = load_mock_response("email/response_two_entries_with_db.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = EmailArgs(email=["testgamil.com"]) results = email_command(client, args, email_dbot_score="MALICIOUS", reliability="B - Usually reliable") assert isinstance(results, list) assert len(results) == 1 response = results[0] assert response.indicator is not None assert response.indicator.dbot_score.score == Common.DBotScore.BAD # type: ignore[attr-defined] assert response.indicator.dbot_score.score == 3 # type: ignore[attr-defined] @pytest.mark.parametrize( "fixture_path", [ pytest.param("email/response_empty.json", id="no_entries"), pytest.param("email/response_no_breaches.json", id="entries_without_db_name"), ], ) def test_email_command_dbot_score_none_when_no_breaches( mocker: MockerFixture, client: "DehashedClient", fixture_path: str ) -> None: """ Given: - A response with either no entries or entries missing `database_name`. When: - Calling email_command for `testgamil.com`. Then: - The returned CommandResults still exposes a `Common.EMAIL` indicator with `DBotScore == NONE` (0) and the readable output equals the "No matching results found" message (when no transformed entries). - Direct port of the v1 `test_email_command_no_entries_returned`. """ from CommonServerPython import Common from DeHashed import email_command, EmailArgs mock_response = load_mock_response(fixture_path) mocker.patch.object(client, "general_search", return_value=mock_response) args = EmailArgs(email=["testgamil.com"]) results = email_command(client, args, email_dbot_score="SUSPICIOUS", reliability="B - Usually reliable") assert isinstance(results, list) assert len(results) == 1 response = results[0] assert response.indicator is not None assert response.indicator.dbot_score.score == Common.DBotScore.NONE # type: ignore[attr-defined] assert response.indicator.dbot_score.score == 0 # type: ignore[attr-defined] @pytest.mark.parametrize( "reliability", [ pytest.param("A+ - 3rd party enrichment", id="a_plus"), pytest.param("A - Completely reliable", id="a"), pytest.param("B - Usually reliable", id="b"), pytest.param("C - Fairly reliable", id="c"), pytest.param("D - Not usually reliable", id="d"), pytest.param("E - Unreliable", id="e"), pytest.param("F - Reliability cannot be judged", id="f"), ], ) def test_email_command_reliability_propagated_parametrized( mocker: MockerFixture, client: "DehashedClient", reliability: str, ) -> None: """ Given: - A response with breach entries and a configured source reliability string. When: - Calling email_command across all 7 reliability values. Then: - The configured reliability is propagated onto the `Common.EMAIL` indicator's DBotScore. - Direct port of the v1 `test_email_different_reliability` parametrize. """ from CommonServerPython import DBotScoreReliability from DeHashed import email_command, EmailArgs mock_response = load_mock_response("email/response_one_entry_with_db.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = EmailArgs(email=["testgamil.com"]) results = email_command(client, args, email_dbot_score="SUSPICIOUS", reliability=reliability) expected_reliability = DBotScoreReliability.get_dbot_score_reliability_from_str(reliability) assert isinstance(results, list) assert len(results) == 1 response = results[0] assert response.indicator is not None assert response.indicator.dbot_score.reliability == expected_reliability # type: ignore[attr-defined] def test_email_command_reliability_none_omits_field(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - `reliability=None`. When: - Calling email_command. Then: - The indicator's DBotScore has no reliability set (falsy). """ from DeHashed import email_command, EmailArgs mock_response = load_mock_response("email/response_one_entry_with_db.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = EmailArgs(email=["testgamil.com"]) results = email_command(client, args, email_dbot_score="SUSPICIOUS", reliability=None) assert isinstance(results, list) assert len(results) == 1 response = results[0] assert response.indicator is not None # When reliability=None the integration code does not set the kwarg, # so the indicator's DBotScore.reliability should not be a valid reliability string. assert not getattr(response.indicator.dbot_score, "reliability", None) # type: ignore[attr-defined] def test_email_command_breach_description_includes_unique_sources(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - Entries with duplicate `database_name` values (`["TestBreach-A", "TestBreach-A", "TestBreach-B"]`) and a SUSPICIOUS-or-higher score. When: - Calling email_command. Then: - The malicious_description on the DBotScore reads `"Found in 2 breach(es): TestBreach-A, TestBreach-B"` — duplicates collapsed, sources sorted, count reflects unique sources. """ from DeHashed import email_command, EmailArgs mock_response = load_mock_response("email/response.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = EmailArgs(email=["target@example.com"]) results = email_command(client, args, email_dbot_score="SUSPICIOUS", reliability="B - Usually reliable") assert isinstance(results, list) assert len(results) == 1 response = results[0] assert response.indicator is not None description = response.indicator.dbot_score.malicious_description # type: ignore[attr-defined] assert description == "Found in 2 breach(es): TestBreach-A, TestBreach-B" def test_email_command_email_domain_extraction(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - `EmailArgs(email=["user@example.com"])`. When: - Calling email_command. Then: - The attached `Common.EMAIL` indicator's `domain == "example.com"`. """ from DeHashed import email_command, EmailArgs mock_response = load_mock_response("email/response_user_at_example.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = EmailArgs(email=["user@example.com"]) results = email_command(client, args, email_dbot_score="SUSPICIOUS", reliability=None) assert isinstance(results, list) assert len(results) == 1 response = results[0] assert response.indicator is not None assert response.indicator.domain == "example.com" # type: ignore[attr-defined] def test_email_command_email_without_at_sets_no_domain(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - An email-shaped value without "@" (`["nodomain"]`) — exercises the defensive `if "@" in indicator_value` guard. When: - Calling email_command. Then: - The indicator's `domain is None`. """ from DeHashed import email_command, EmailArgs mock_response = load_mock_response("email/response_empty.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = EmailArgs(email=["nodomain"]) results = email_command(client, args, email_dbot_score="SUSPICIOUS", reliability=None) assert isinstance(results, list) assert len(results) == 1 response = results[0] assert response.indicator is not None assert response.indicator.domain is None # type: ignore[attr-defined] def test_email_command_query_construction(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - `EmailArgs(email=["a@b.co"])`. When: - Calling email_command. Then: - `client.general_search` is called with `query='email:"a@b.co"'` (the email value is wrapped in double quotes via `_build_search_query` with `operation="is"`), `regex=None`, and all other optional params set to None. """ from DeHashed import email_command, EmailArgs, MAX_REQUEST_PAGE_SIZE mock_response = load_mock_response("email/response_one_entry_a_at_b.json") general_search_mock = mocker.patch.object(client, "general_search", return_value=mock_response) args = EmailArgs(email=["a@b.co"]) email_command(client, args, email_dbot_score="SUSPICIOUS", reliability=None) general_search_mock.assert_called_once_with( query='email:"a@b.co"', page=None, size=MAX_REQUEST_PAGE_SIZE, wildcard=None, regex=None, de_dupe=None, ) def test_email_command_unexpected_response_raises(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - `client.general_search` returns a non-dict (e.g. a list). When: - Calling email_command. Then: - Raises DemistoException containing "Got unexpected output from api". """ from DeHashed import email_command, EmailArgs, DemistoException mocker.patch.object(client, "general_search", return_value=["bad"]) args = EmailArgs(email=["a@b.co"]) with pytest.raises(DemistoException, match="Got unexpected output from api"): email_command(client, args, email_dbot_score="SUSPICIOUS", reliability=None) def test_email_command_dbot_score_none_when_no_breaches_readable_output(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - A response with no entries. When: - Calling email_command. Then: - The readable_output is "No matching results found" and the DBotScore is NONE. - Direct port of the v1 `test_email_command_no_entries_returned` (readable-output assertion). """ from CommonServerPython import Common from DeHashed import email_command, EmailArgs mock_response = load_mock_response("email/response_empty.json") mocker.patch.object(client, "general_search", return_value=mock_response) args = EmailArgs(email=["testgamil.com"]) results = email_command(client, args, email_dbot_score="SUSPICIOUS", reliability="B - Usually reliable") assert isinstance(results, list) assert len(results) == 1 response = results[0] assert response.indicator is not None assert response.indicator.dbot_score.score == Common.DBotScore.NONE # type: ignore[attr-defined] assert response.readable_output == "No matching results found for testgamil.com" def test_email_command_multiple_emails_returns_per_email_results(mocker: MockerFixture, client: "DehashedClient") -> None: """ Given: - `EmailArgs(email=["found@example.com", "missing@example.com"])` - one email has breach entries, the other has no entries. When: - Calling email_command. Then: - A list of two `CommandResults` is returned, one per email. - The first email has its own `Common.EMAIL` indicator with SUSPICIOUS DBotScore and outputs populated from `_transform_entry`. - The second email has its own `Common.EMAIL` indicator with NONE DBotScore and a per-email "No matching results found for missing@example.com" readable output. - `client.general_search` is called once per email with a per-email scoped query. """ from CommonServerPython import Common from DeHashed import email_command, EmailArgs found_response = load_mock_response("email/response_found_multi.json") missing_response = load_mock_response("email/response_missing_multi.json") general_search_mock = mocker.patch.object(client, "general_search", side_effect=[found_response, missing_response]) args = EmailArgs(email=["found@example.com", "missing@example.com"]) results = email_command(client, args, email_dbot_score="SUSPICIOUS", reliability="B - Usually reliable") # Two CommandResults — one per email. assert isinstance(results, list) assert len(results) == 2 # First email: breaches found. found = results[0] assert found.outputs_prefix == "DeHashed.Search" assert found.outputs_key_field == "Id" assert found.outputs is not None assert len(found.outputs) == 2 # type: ignore[arg-type] assert found.indicator is not None assert found.indicator.address == "found@example.com" # type: ignore[attr-defined] assert found.indicator.dbot_score.indicator == "found@example.com" # type: ignore[attr-defined] assert found.indicator.dbot_score.score == Common.DBotScore.SUSPICIOUS # type: ignore[attr-defined] # Second email: no entries — indicator surfaced with NONE score. missing = results[1] assert missing.indicator is not None assert missing.indicator.address == "missing@example.com" # type: ignore[attr-defined] assert missing.indicator.dbot_score.indicator == "missing@example.com" # type: ignore[attr-defined] assert missing.indicator.dbot_score.score == Common.DBotScore.NONE # type: ignore[attr-defined] assert missing.readable_output == "No matching results found for missing@example.com" # Each email queried separately. assert general_search_mock.call_count == 2 first_call_kwargs = general_search_mock.call_args_list[0].kwargs second_call_kwargs = general_search_mock.call_args_list[1].kwargs assert first_call_kwargs["query"] == 'email:"found@example.com"' assert second_call_kwargs["query"] == 'email:"missing@example.com"' # endregion