"""Flashpoint Vulnerability Feed Integration Unit Tests.""" import json import os from unittest.mock import patch import pytest import requests_mock as rm import FeedFlashpointVulnerability from CommonServerPython import DemistoException, CommandResults from FeedFlashpointVulnerability import ( DEFAULT_LIMIT, HTTP_ERRORS, MESSAGES, URL_SUFFIX, Client, create_indicator_from_vulnerability, demisto, fetch_indicators_command, flashpoint_vulnerability_get_indicators_command, main, remove_space_from_args, test_module as main_test_module, validate_fetch_indicators_params, validate_params, PAGE_SIZE, validate_cvss_score, validate_epss_score, remove_nulls_from_nested_dictionary, ) from datetime import UTC, datetime, timedelta """ CONSTANTS """ API_KEY = "test_api_key" MOCK_URL = "https://mock.flashpoint.io" BASIC_PARAMS = { "url": MOCK_URL, "credentials": {"password": API_KEY}, "feedReliability": "A - Completely reliable", } """ UTILITY FUNCTIONS AND FIXTURES """ def util_load_json(path: str) -> dict: """Load a json file to python dict.""" with open(path, encoding="utf-8") as f: return json.loads(f.read()) @pytest.fixture def mock_client(): """Create a mock client object for testing.""" client = Client( base_url=MOCK_URL, api_key=API_KEY, verify=False, proxy=False, ) client.platform_url = MOCK_URL return client @pytest.fixture def requests_mock(): """Create a requests mock fixture.""" with rm.Mocker() as m: yield m """ TEST CASES FOR HELPER FUNCTIONS """ class TestRemoveSpaceFromArgs: """Test cases for remove_space_from_args function.""" def test_remove_space_from_args_with_spaces(self): """ Test remove_space_from_args removes leading/trailing spaces. Given: - Dictionary with string values containing spaces. When: - Calling remove_space_from_args function. Then: - Returns dictionary with trimmed string values. """ args = {"key1": " value1 ", "key2": "value2", "key3": " value3"} result = remove_space_from_args(args) assert result["key1"] == "value1" assert result["key2"] == "value2" assert result["key3"] == "value3" def test_remove_space_from_args_with_non_string_values(self): """ Test remove_space_from_args handles non-string values. Given: - Dictionary with mixed value types. When: - Calling remove_space_from_args function. Then: - Returns dictionary with non-string values unchanged. """ args = {"key1": " value1 ", "key2": 123, "key3": None} result = remove_space_from_args(args) assert result["key1"] == "value1" assert result["key2"] == 123 assert result["key3"] is None class TestGetNestedValue: """Test cases for demisto.get function.""" def test_demisto_get_simple_path(self): """ Test demisto.get with simple path. Given: - Dictionary with nested structure. When: - Calling demisto.get with simple path. Then: - Returns the value at the path. """ data = {"key1": "value1"} result = demisto.get(data, "key1") assert result == "value1" def test_demisto_get_nested_path(self): """ Test demisto.get with nested path. Given: - Dictionary with nested structure. When: - Calling demisto.get with nested path. Then: - Returns the value at the nested path. """ data = {"parent": {"child": {"value": "nested_value"}}} result = demisto.get(data, "parent.child.value") assert result == "nested_value" def test_demisto_get_missing_path(self): """ Test demisto.get with missing path. Given: - Dictionary without the specified path. When: - Calling demisto.get with missing path. Then: - Returns None. """ data = {"key1": "value1"} result = demisto.get(data, "missing.path") assert result is None def test_demisto_get_empty_path(self): """ Test demisto.get with empty path. Given: - Dictionary with data. When: - Calling demisto.get with empty path. Then: - Returns None. """ data = {"key1": "value1"} result = demisto.get(data, "") assert result is None class TestValidateParams: """Test cases for validate_params function.""" def test_validate_params_success(self): """ Test validate_params with valid parameters. Given: - Valid parameters dictionary. When: - Calling validate_params function. Then: - No exception is raised. """ params = BASIC_PARAMS.copy() validate_params(params) # Should not raise def test_validate_params_missing_url(self): """ Test validate_params with missing URL. Given: - Parameters without URL. When: - Calling validate_params function. Then: - Raises DemistoException. """ params = {"credentials": {"password": API_KEY}, "feedReliability": "A - Completely reliable"} with pytest.raises(DemistoException) as err: validate_params(params) assert MESSAGES["NO_PARAM_PROVIDED"].format("Server URL") in str(err.value) def test_validate_params_missing_api_key(self): """ Test validate_params with missing API key. Given: - Parameters without API key. When: - Calling validate_params function. Then: - Raises DemistoException. """ params = {"url": MOCK_URL, "credentials": {}, "feedReliability": "A - Completely reliable"} with pytest.raises(DemistoException) as err: validate_params(params) assert MESSAGES["NO_PARAM_PROVIDED"].format("API Key") in str(err.value) def test_validate_params_empty_api_key(self): """ Test validate_params with empty API key. Given: - Parameters with empty API key. When: - Calling validate_params function. Then: - Raises DemistoException. """ params = {"url": MOCK_URL, "credentials": {"password": " "}, "feedReliability": "A - Completely reliable"} with pytest.raises(DemistoException) as err: validate_params(params) assert MESSAGES["NO_PARAM_PROVIDED"].format("API Key") in str(err.value) def test_validate_params_missing_reliability(self): """ Test validate_params with missing feed reliability. Given: - Parameters without feed reliability. When: - Calling validate_params function. Then: - Raises DemistoException. """ params = {"url": MOCK_URL, "credentials": {"password": API_KEY}} with pytest.raises(DemistoException) as err: validate_params(params) assert MESSAGES["NO_PARAM_PROVIDED"].format("Source Reliability") in str(err.value) class TestCreateIndicatorFromVulnerability: """Test cases for create_indicator_from_vulnerability function.""" def test_create_indicator_with_cve(self): """ Test create_indicator_from_vulnerability with CVE ID. Given: - Vulnerability data with CVE ID. When: - Calling create_indicator_from_vulnerability function. Then: - Returns indicator with CVE type and value. - CVSS fields are populated from Flashpoint source (priority). - CPE list is extracted from products. """ vulnerability = util_load_json("test_data/vulnerability_detail_200.json") remove_nulls_from_nested_dictionary(vulnerability) indicator = util_load_json("test_data/indicator_with_cve.json") result = create_indicator_from_vulnerability(vulnerability, {"createRelationship": "True"}, MOCK_URL) assert result == indicator def test_create_indicator_without_cve(self): """ Test create_indicator_from_vulnerability without CVE ID. Given: - Vulnerability data without CVE ID. When: - Calling create_indicator_from_vulnerability function. Then: - Returns indicator with custom type and FP ID. - CVSS fields are populated from available sources. """ vulnerability = util_load_json("test_data/vulnerability_without_cve_200.json") remove_nulls_from_nested_dictionary(vulnerability) indicator = util_load_json("test_data/indicator_without_cve.json") params: dict = {"feedTags": []} result = create_indicator_from_vulnerability(vulnerability, params, MOCK_URL) assert result == indicator def test_create_indicator_without_tlp_color(self): """ Test create_indicator_from_vulnerability without TLP color. Given: - Vulnerability data and params without TLP color. When: - Calling create_indicator_from_vulnerability function. Then: - Returns indicator without trafficlightprotocol field. """ vulnerability = util_load_json("test_data/vulnerability_detail_200.json") params: dict = {"feedTags": []} indicator = create_indicator_from_vulnerability(vulnerability, params, MOCK_URL) assert "trafficlightprotocol" not in indicator["fields"] def test_create_indicator_cvss_priority_flashpoint_source(self): """ Test create_indicator_from_vulnerability prioritizes Flashpoint CVSS source. Given: - Vulnerability data with multiple CVSS sources including Flashpoint. When: - Calling create_indicator_from_vulnerability function. Then: - CVSS fields use Flashpoint source values (9.2) over NVD (7.1). """ vulnerability = util_load_json("test_data/vulnerability_detail_200.json") params: dict = {} indicator = create_indicator_from_vulnerability(vulnerability, params, MOCK_URL) # Should prioritize Flashpoint source (9.2) over NVD (7.1) assert indicator["fields"]["cvss"] == 9.2 assert indicator["fields"]["cvssscore"] == 9.2 def test_create_indicator_cvss_fallback_to_v2(self): """ Test create_indicator_from_vulnerability falls back to CVSSv2 when v3 unavailable. Given: - Vulnerability data with only CVSSv2 scores. When: - Calling create_indicator_from_vulnerability function. Then: - CVSS fields use CVSSv2 values. """ vulnerability = { "id": 333333, "cve_ids": ["CVE-2024-TEST"], "title": "Test Vulnerability", "description": "Test description", "timelines": {"published_at": "2026-01-01T00:00:00Z", "last_modified_at": "2026-01-01T00:00:00Z"}, "tags": [], "cvss_v3s": [], "cvss_v2s": [{"score": 6.4, "version": "2.0", "vector_string": "AV:N/AC:L/Au:N/C:P/I:P/A:N", "source": "NVD"}], "cvss_v4s": [], "products": [], } params: dict = {} indicator = create_indicator_from_vulnerability(vulnerability, params, MOCK_URL) assert indicator["fields"]["cvss"] == 6.4 assert indicator["fields"]["cvssversion"] == "2.0" assert "AV:N" in indicator["fields"]["cvssvector"] def test_create_indicator_cvss_fallback_to_v3(self): """ Test create_indicator_from_vulnerability uses CVSSv3 when v4 unavailable. Given: - Vulnerability data with only CVSSv3 scores. When: - Calling create_indicator_from_vulnerability function. Then: - CVSS fields use CVSSv3 values. """ vulnerability = { "id": 444444, "cve_ids": ["CVE-2024-V4TEST"], "title": "Test Vulnerability V4", "description": "Test description", "timelines": {"published_at": "2026-01-01T00:00:00Z", "last_modified_at": "2026-01-01T00:00:00Z"}, "tags": [], "cvss_v2s": [], "cvss_v3s": [ { "score": 9.1, "version": "3.1", "vector_string": "CVSS:3.1/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", "source": "Flashpoint", } ], "cvss_v4s": [], "products": [], } params: dict = {} indicator = create_indicator_from_vulnerability(vulnerability, params, MOCK_URL) assert indicator["fields"]["cvss"] == 9.1 assert indicator["fields"]["cvssversion"] == "3.1" assert "CVSS:3.1" in indicator["fields"]["cvssvector"] def test_create_indicator_cpe_extraction_multiple_products(self): """ Test create_indicator_from_vulnerability extracts CPEs from multiple products. Given: - Vulnerability data with multiple products and versions. When: - Calling create_indicator_from_vulnerability function. Then: - All CPEs are extracted into vulnerableproducts field. """ vulnerability = { "id": 555555, "cve_ids": ["CVE-2024-CPE"], "title": "Multi-Product Vulnerability", "description": "Test description", "timelines": {"published_at": "2026-01-01T00:00:00Z", "last_modified_at": "2026-01-01T00:00:00Z"}, "tags": [], "cvss_v3s": [], "cvss_v2s": [], "cvss_v4s": [], "products": [ { "id": 1, "name": "Product A", "versions": [ {"version": "1.0", "cpes": [{"name": "cpe:2.3:a:vendor:producta:1.0:*:*:*:*:*:*:*"}]}, {"version": "2.0", "cpes": [{"name": "cpe:2.3:a:vendor:producta:2.0:*:*:*:*:*:*:*"}]}, ], }, { "id": 2, "name": "Product B", "versions": [ {"version": "3.0", "cpes": [{"name": "cpe:2.3:a:vendor:productb:3.0:*:*:*:*:*:*:*"}]}, ], }, ], } params: dict = {} indicator = create_indicator_from_vulnerability(vulnerability, params, MOCK_URL) assert len(indicator["fields"]["vulnerableproducts"]) == 3 cpe_names = [cpe["CPE"] for cpe in indicator["fields"]["vulnerableproducts"]] assert "cpe:2.3:a:vendor:producta:1.0:*:*:*:*:*:*:*" in cpe_names assert "cpe:2.3:a:vendor:producta:2.0:*:*:*:*:*:*:*" in cpe_names assert "cpe:2.3:a:vendor:productb:3.0:*:*:*:*:*:*:*" in cpe_names def test_create_indicator_empty_cvss_arrays(self): """ Test create_indicator_from_vulnerability handles empty CVSS arrays. Given: - Vulnerability data with empty CVSS arrays. When: - Calling create_indicator_from_vulnerability function. Then: - CVSS fields are empty strings. """ vulnerability = { "id": 666666, "cve_ids": ["CVE-2024-NOCVSS"], "title": "No CVSS Vulnerability", "description": "Test description", "timelines": {"published_at": "2026-01-01T00:00:00Z", "last_modified_at": "2026-01-01T00:00:00Z"}, "tags": [], "cvss_v3s": [], "cvss_v2s": [], "cvss_v4s": [], "products": [], } params: dict = {} indicator = create_indicator_from_vulnerability(vulnerability, params, MOCK_URL) assert indicator["value"] == "CVE-2024-NOCVSS" assert indicator["type"] == "CVE" class TestValidateEpssScore: """Test cases for validate_epss_score function.""" def test_validate_epss_score_with_valid_min_and_max(self): """ Test validate_epss_score with valid min and max values. Given: - Valid min_epss = "0.5" and max_epss = "0.8" When: - Calling validate_epss_score Then: - No exception is raised """ validate_epss_score("0.5", "Minimum EPSS Score", "0.8", "Maximum EPSS Score") def test_validate_epss_score_with_valid_min_only(self): """ Test validate_epss_score with valid min value only. Given: - Valid min_epss = "0.3" and max_epss = None When: - Calling validate_epss_score Then: - No exception is raised """ validate_epss_score("0.3", "Minimum EPSS Score", None, "Maximum EPSS Score") def test_validate_epss_score_with_valid_max_only(self): """ Test validate_epss_score with valid max value only. Given: - min_epss = None and valid max_epss = "0.9" When: - Calling validate_epss_score Then: - No exception is raised """ validate_epss_score(None, "Minimum EPSS Score", "0.9", "Maximum EPSS Score") def test_validate_epss_score_with_both_none(self): """ Test validate_epss_score with both values as None. Given: - min_epss = None and max_epss = None When: - Calling validate_epss_score Then: - No exception is raised """ validate_epss_score(None, "Minimum EPSS Score", None, "Maximum EPSS Score") def test_validate_epss_score_with_integer_values(self): """ Test validate_epss_score with integer values. Given: - min_epss = "0" and max_epss = "1" When: - Calling validate_epss_score Then: - No exception is raised (integers are valid floats) """ validate_epss_score("0", "Minimum EPSS Score", "1", "Maximum EPSS Score") def test_validate_epss_score_with_equal_min_and_max(self): """ Test validate_epss_score with equal min and max values. Given: - min_epss = "0.5" and max_epss = "0.5" When: - Calling validate_epss_score Then: - No exception is raised (equal values are valid) """ validate_epss_score("0.5", "Minimum EPSS Score", "0.5", "Maximum EPSS Score") def test_validate_epss_score_with_invalid_min_non_numeric(self): """ Test validate_epss_score with invalid non-numeric min value. Given: - Invalid min_epss = "abc" When: - Calling validate_epss_score Then: - Raises DemistoException with appropriate message """ with pytest.raises(DemistoException) as err: validate_epss_score("abc", "Minimum EPSS Score", "0.8", "Maximum EPSS Score") assert MESSAGES["INVALID_EPSS_SCORE"].format("Minimum EPSS Score") in str(err.value) def test_validate_epss_score_with_invalid_max_non_numeric(self): """ Test validate_epss_score with invalid non-numeric max value. Given: - Invalid max_epss = "xyz" When: - Calling validate_epss_score Then: - Raises DemistoException with appropriate message """ with pytest.raises(DemistoException) as err: validate_epss_score("0.3", "Minimum EPSS Score", "xyz", "Maximum EPSS Score") assert MESSAGES["INVALID_EPSS_SCORE"].format("Maximum EPSS Score") in str(err.value) def test_validate_epss_score_with_min_greater_than_max(self): """ Test validate_epss_score with min greater than max. Given: - min_epss = "0.9" and max_epss = "0.5" When: - Calling validate_epss_score Then: - Raises DemistoException with appropriate message """ with pytest.raises(DemistoException) as err: validate_epss_score("0.9", "Minimum EPSS Score", "0.5", "Maximum EPSS Score") assert MESSAGES["INVALID_SCORE_RANGE"].format("Minimum EPSS Score", "Maximum EPSS Score") in str(err.value) def test_validate_epss_score_with_empty_string_min(self): """ Test validate_epss_score with empty string for min. Given: - min_epss = "" (empty string) When: - Calling validate_epss_score Then: - No exception is raised (empty string is falsy) """ validate_epss_score("", "Minimum EPSS Score", "0.8", "Maximum EPSS Score") def test_validate_epss_score_with_empty_string_max(self): """ Test validate_epss_score with empty string for max. Given: - max_epss = "" (empty string) When: - Calling validate_epss_score Then: - No exception is raised (empty string is falsy) """ validate_epss_score("0.3", "Minimum EPSS Score", "", "Maximum EPSS Score") def test_validate_epss_score_with_special_characters(self): """ Test validate_epss_score with special characters. Given: - min_epss = "0.5@#" When: - Calling validate_epss_score Then: - Raises DemistoException """ with pytest.raises(DemistoException) as err: validate_epss_score("0.5@#", "Minimum EPSS Score", "0.8", "Maximum EPSS Score") assert MESSAGES["INVALID_EPSS_SCORE"].format("Minimum EPSS Score") in str(err.value) class TestValidateCvssScore: """Test cases for validate_cvss_score function.""" def test_validate_cvss_score_with_valid_min_and_max(self): """ Test validate_cvss_score with valid min and max values. Given: - Valid min_cvss = "5.0" and max_cvss = "8.5" When: - Calling validate_cvss_score Then: - No exception is raised """ validate_cvss_score("5.0", "Minimum CVSS Score", "8.5", "Maximum CVSS Score") def test_validate_cvss_score_with_valid_min_only(self): """ Test validate_cvss_score with valid min value only. Given: - Valid min_cvss = "3.0" and max_cvss = None When: - Calling validate_cvss_score Then: - No exception is raised """ validate_cvss_score("3.0", "Minimum CVSS Score", None, "Maximum CVSS Score") def test_validate_cvss_score_with_valid_max_only(self): """ Test validate_cvss_score with valid max value only. Given: - min_cvss = None and valid max_cvss = "9.0" When: - Calling validate_cvss_score Then: - No exception is raised """ validate_cvss_score(None, "Minimum CVSS Score", "9.0", "Maximum CVSS Score") def test_validate_cvss_score_with_both_none(self): """ Test validate_cvss_score with both values as None. Given: - min_cvss = None and max_cvss = None When: - Calling validate_cvss_score Then: - No exception is raised """ validate_cvss_score(None, "Minimum CVSS Score", None, "Maximum CVSS Score") def test_validate_cvss_score_with_integer_values(self): """ Test validate_cvss_score with integer values. Given: - min_cvss = "0" and max_cvss = "10" When: - Calling validate_cvss_score Then: - No exception is raised (integers are valid floats) """ validate_cvss_score("0", "Minimum CVSS Score", "10", "Maximum CVSS Score") def test_validate_cvss_score_with_equal_min_and_max(self): """ Test validate_cvss_score with equal min and max values. Given: - min_cvss = "7.5" and max_cvss = "7.5" When: - Calling validate_cvss_score Then: - No exception is raised (equal values are valid) """ validate_cvss_score("7.5", "Minimum CVSS Score", "7.5", "Maximum CVSS Score") def test_validate_cvss_score_with_boundary_values(self): """ Test validate_cvss_score with boundary values (0 and 10). Given: - min_cvss = "0.0" and max_cvss = "10.0" When: - Calling validate_cvss_score Then: - No exception is raised """ validate_cvss_score("0.0", "Minimum CVSS Score", "10.0", "Maximum CVSS Score") def test_validate_cvss_score_with_invalid_min_non_numeric(self): """ Test validate_cvss_score with invalid non-numeric min value. Given: - Invalid min_cvss = "invalid" When: - Calling validate_cvss_score Then: - Raises DemistoException with appropriate message """ with pytest.raises(DemistoException) as err: validate_cvss_score("invalid", "Minimum CVSS Score", "8.0", "Maximum CVSS Score") assert MESSAGES["INVALID_CVSS_SCORE"].format("Minimum CVSS Score") in str(err.value) def test_validate_cvss_score_with_invalid_max_non_numeric(self): """ Test validate_cvss_score with invalid non-numeric max value. Given: - Invalid max_cvss = "not_a_number" When: - Calling validate_cvss_score Then: - Raises DemistoException with appropriate message """ with pytest.raises(DemistoException) as err: validate_cvss_score("5.0", "Minimum CVSS Score", "not_a_number", "Maximum CVSS Score") assert MESSAGES["INVALID_CVSS_SCORE"].format("Maximum CVSS Score") in str(err.value) def test_validate_cvss_score_with_min_greater_than_max(self): """ Test validate_cvss_score with min greater than max. Given: - min_cvss = "9.0" and max_cvss = "5.0" When: - Calling validate_cvss_score Then: - Raises DemistoException with appropriate message """ with pytest.raises(DemistoException) as err: validate_cvss_score("9.0", "Minimum CVSS Score", "5.0", "Maximum CVSS Score") assert MESSAGES["INVALID_SCORE_RANGE"].format("Minimum CVSS Score", "Maximum CVSS Score") in str(err.value) def test_validate_cvss_score_with_empty_string_min(self): """ Test validate_cvss_score with empty string for min. Given: - min_cvss = "" (empty string) When: - Calling validate_cvss_score Then: - No exception is raised (empty string is falsy) """ validate_cvss_score("", "Minimum CVSS Score", "8.0", "Maximum CVSS Score") def test_validate_cvss_score_with_empty_string_max(self): """ Test validate_cvss_score with empty string for max. Given: - max_cvss = "" (empty string) When: - Calling validate_cvss_score Then: - No exception is raised (empty string is falsy) """ validate_cvss_score("3.0", "Minimum CVSS Score", "", "Maximum CVSS Score") def test_validate_cvss_score_with_negative_value(self): """ Test validate_cvss_score with negative value. Given: - min_cvss = "-1.0" When: - Calling validate_cvss_score Then: - No exception is raised (function doesn't validate range, only numeric) """ with pytest.raises(DemistoException) as err: validate_cvss_score("-1.0", "Minimum CVSS Score", "5.0", "Maximum CVSS Score") assert MESSAGES["INVALID_CVSS_SCORE"].format("Minimum CVSS Score") in str(err.value) def test_validate_cvss_score_with_value_above_10(self): """ Test validate_cvss_score with value above 10. Given: - max_cvss = "15.0" When: - Calling validate_cvss_score Then: - No exception is raised (function doesn't validate range, only numeric) """ with pytest.raises(DemistoException) as err: validate_cvss_score("5.0", "Minimum CVSS Score", "15.0", "Maximum CVSS Score") assert MESSAGES["INVALID_CVSS_SCORE"].format("Maximum CVSS Score") in str(err.value) def test_validate_cvss_score_with_special_characters(self): """ Test validate_cvss_score with special characters. Given: - min_cvss = "7.5!@" When: - Calling validate_cvss_score Then: - Raises DemistoException """ with pytest.raises(DemistoException) as err: validate_cvss_score("7.5!@", "Minimum CVSS Score", "8.0", "Maximum CVSS Score") assert MESSAGES["INVALID_CVSS_SCORE"].format("Minimum CVSS Score") in str(err.value) def test_validate_cvss_score_with_whitespace(self): """ Test validate_cvss_score with whitespace in value. Given: - min_cvss = " 5.0 " When: - Calling validate_cvss_score Then: - No exception is raised (float() handles whitespace) """ validate_cvss_score(" 5.0 ", "Minimum CVSS Score", " 8.0 ", "Maximum CVSS Score") """ TEST CASES FOR COMMAND FUNCTIONS """ class TestTestModule: """Test cases for test_module function.""" def test_test_module_success(self, mock_client, requests_mock): """ Test test_module with successful API response. Given: - Mock client and successful API response. When: - Calling test_module function. Then: - Returns 'ok'. """ response = util_load_json("test_data/vulnerability_list_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=response, status_code=200) result = main_test_module(client=mock_client) assert result == "ok" def test_test_module_with_feed_enabled(self, mock_client, requests_mock, mocker): """ Test test_module with feed enabled. Given: - Mock client with feed enabled. When: - Calling test_module function. Then: - Returns 'ok' after calling fetch_indicators_command. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params: dict = {**BASIC_PARAMS, "feed": True} mocker.patch.object(demisto, "params", return_value=params) result = main_test_module(client=mock_client) assert result == "ok" @pytest.mark.parametrize( "status_code, error_msg", [ (400, HTTP_ERRORS[400]), (401, HTTP_ERRORS[401]), (403, HTTP_ERRORS[403]), (404, HTTP_ERRORS[404]), (500, HTTP_ERRORS[500]), ], ) def test_test_module_http_errors(self, mock_client, requests_mock, status_code, error_msg): """ Test test_module with various HTTP error responses. Given: - Mock client and HTTP error response. When: - Calling test_module function. Then: - Raises DemistoException with appropriate error message. """ requests_mock.get( f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json={"message": "error", "type": "validation_error", "errors": [{"detail": "invalid param"}]}, status_code=status_code, ) with pytest.raises(DemistoException) as err: main_test_module(client=mock_client) assert error_msg.format("validation_error", "invalid param") in str(err.value) class TestFetchIndicatorsCommand: """Test cases for fetch_indicators_command function.""" def test_fetch_indicators_success(self, mock_client, requests_mock): """ Test fetch_indicators_command with successful response. Given: - Mock client and successful API response. When: - Calling fetch_indicators_command function. Then: - Returns list of indicators and next_run dict. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") indicators_response = util_load_json("test_data/indicator_with_cve.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params: dict = {**BASIC_PARAMS, "first_fetch": "3 days", "createRelationship": True} last_run: dict = {} indicators, next_run = fetch_indicators_command(client=mock_client, params=params, last_run=last_run) assert len(indicators) == 1 assert indicators[0]["value"] == "CVE-YYYY-XXXX" assert indicators[0]["type"] == "CVE" assert indicators[0] == indicators_response def test_fetch_indicators_empty_response(self, mock_client, requests_mock): """ Test fetch_indicators_command with empty response. Given: - Mock client and empty API response. When: - Calling fetch_indicators_command function. Then: - Returns empty list of indicators. """ empty_response = util_load_json("test_data/vulnerability_list_empty_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=empty_response, status_code=200) params: dict = {**BASIC_PARAMS, "first_fetch": "3 days"} last_run: dict = {} indicators, next_run = fetch_indicators_command(client=mock_client, params=params, last_run=last_run) assert len(indicators) == 0 assert "next_last_touched_after" in next_run def test_fetch_indicators_with_last_run(self, mock_client, requests_mock): """ Test fetch_indicators_command with existing last_run. Given: - Mock client and existing last_run data. When: - Calling fetch_indicators_command function. Then: - Uses last_run data for API request. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params: dict = {**BASIC_PARAMS, "first_fetch": "3 days"} last_run: dict = {"next_last_touched_after": "2026-01-01T00:00:00Z"} indicators, next_run = fetch_indicators_command(client=mock_client, params=params, last_run=last_run) assert len(indicators) == 1 assert "next_last_touched_after" in next_run def test_fetch_indicators_is_test_mode(self, mock_client, requests_mock): """ Test fetch_indicators_command in test mode. Given: - Mock client with is_test=True. When: - Calling fetch_indicators_command function. Then: - Returns empty indicators and next_run. """ list_response = util_load_json("test_data/vulnerability_list_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) params: dict = {**BASIC_PARAMS, "first_fetch": "3 days"} last_run: dict = {} indicators, next_run = fetch_indicators_command(client=mock_client, params=params, last_run=last_run, is_test=True) assert indicators == [] assert next_run == {} def test_fetch_indicators_pagination_when_page_size_reached(self, mock_client, requests_mock, mocker): """ Test fetch_indicators_command pagination logic when PAGE_SIZE vulnerabilities are returned. Given: - Mock client and API response with exactly PAGE_SIZE vulnerabilities. - Existing last_run with from=0. When: - Calling fetch_indicators_command function. Then: - Returns indicators and next_run with incremented 'from' value. - next_run should copy last_run and increment 'from' by PAGE_SIZE. - next_last_touched_after and next_last_touched_before should be set to last_touched_before. """ # Create a list response with exactly PAGE_SIZE vulnerabilities vulnerabilities_list = [{"id": i} for i in range(1, PAGE_SIZE + 1)] list_response = { "total": 500, "next": f"https://api.example.com/vulnerabilities?from={PAGE_SIZE}&size={PAGE_SIZE}", "previous": None, "size": PAGE_SIZE, "from": 0, "results": vulnerabilities_list, } requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) # Mock get_vulnerability for each ID for i in range(1, PAGE_SIZE + 1): detail_response = { "id": i, "cve_ids": [f"CVE-2024-{i:04d}"], "description": f"Test vulnerability {i}", "published_at": "2026-01-01T00:00:00Z", "last_modified_at": "2026-01-01T00:00:00Z", "cvssv3_score": 7.5, } requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/{i}", json=detail_response, status_code=200) params: dict = {**BASIC_PARAMS, "first_fetch": "3 days"} last_run: dict = {"from": 0, "next_last_touched_before": "2026-01-10T00:00:00Z"} # Mock arg_to_datetime to return a fixed time mocker.patch( "FeedFlashpointVulnerability.arg_to_datetime", return_value=mocker.MagicMock(strftime=lambda x: "2026-01-10T00:00:00Z"), ) indicators, next_run = fetch_indicators_command(client=mock_client, params=params, last_run=last_run) # Verify pagination logic (lines 564-568) assert len(indicators) == PAGE_SIZE assert next_run["from"] == 0 + PAGE_SIZE assert next_run["next_last_touched_after"] == "2026-01-10T00:00:00Z" assert next_run["next_last_touched_before"] == "2026-01-10T00:00:00Z" def test_fetch_indicators_pagination_with_existing_from_value(self, mock_client, requests_mock, mocker): """ Test fetch_indicators_command pagination when last_run already has a 'from' value. Given: - Mock client and API response with exactly PAGE_SIZE vulnerabilities. - Existing last_run with from=2000. When: - Calling fetch_indicators_command function. Then: - next_run['from'] should be incremented from 2000 to 2000 + PAGE_SIZE. """ # Create a list response with exactly PAGE_SIZE vulnerabilities vulnerabilities_list = [{"id": i} for i in range(2001, 2000 + PAGE_SIZE + 1)] list_response = { "total": 5000, "next": f"https://api.example.com/vulnerabilities?from={2000 + PAGE_SIZE}&size={PAGE_SIZE}", "previous": None, "size": PAGE_SIZE, "from": 2000, "results": vulnerabilities_list, } requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) # Mock get_vulnerability for each ID for i in range(2001, 2000 + PAGE_SIZE + 1): detail_response = { "id": i, "cve_ids": [f"CVE-2024-{i:04d}"], "description": f"Test vulnerability {i}", "published_at": "2026-01-01T00:00:00Z", "last_modified_at": "2026-01-01T00:00:00Z", "cvssv3_score": 7.5, } requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/{i}", json=detail_response, status_code=200) params: dict = {**BASIC_PARAMS, "first_fetch": "3 days"} last_run: dict = {"from": 2000, "next_last_touched_before": "2026-01-10T00:00:00Z"} # Mock arg_to_datetime to return a fixed time mocker.patch( "FeedFlashpointVulnerability.arg_to_datetime", return_value=mocker.MagicMock(strftime=lambda x: "2026-01-10T00:00:00Z"), ) indicators, next_run = fetch_indicators_command(client=mock_client, params=params, last_run=last_run) # Verify pagination logic - from should increment from 2000 to 2000 + PAGE_SIZE assert len(indicators) == PAGE_SIZE assert next_run["from"] == 2000 + PAGE_SIZE assert next_run["next_last_touched_after"] == "2026-01-10T00:00:00Z" assert next_run["next_last_touched_before"] == "2026-01-10T00:00:00Z" def test_fetch_indicators_no_pagination_when_less_than_page_size(self, mock_client, requests_mock): """ Test fetch_indicators_command when fewer than PAGE_SIZE vulnerabilities are returned. Given: - Mock client and API response with fewer than PAGE_SIZE vulnerabilities. When: - Calling fetch_indicators_command function. Then: - next_run should NOT include 'from' field (no pagination needed). - next_last_touched_after should be set to next_last_touched_before from last_run or current time. """ # Create a list response with only 10 vulnerabilities (less than PAGE_SIZE) vulnerabilities_list = [{"id": i} for i in range(1, 11)] list_response = { "total": 10, "next": None, "previous": None, "size": 10, "from": 0, "results": vulnerabilities_list, } requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) # Mock get_vulnerability for each ID for i in range(1, 11): detail_response = { "id": i, "cve_ids": [f"CVE-2024-{i:04d}"], "description": f"Test vulnerability {i}", "published_at": "2026-01-01T00:00:00Z", "last_modified_at": "2026-01-01T00:00:00Z", "cvssv3_score": 7.5, } requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/{i}", json=detail_response, status_code=200) params: dict = {**BASIC_PARAMS, "first_fetch": "3 days"} last_run: dict = {"next_last_touched_before": "2026-01-10T00:00:00Z"} indicators, next_run = fetch_indicators_command(client=mock_client, params=params, last_run=last_run) # Verify no pagination - 'from' should not be in next_run assert len(indicators) == 10 assert "from" not in next_run assert next_run["next_last_touched_after"] == "2026-01-10T00:00:00Z" def test_fetch_indicators_timeout_after_4_minutes(self, mock_client, requests_mock, mocker): """ Test fetch_indicators_command when 4-minute timeout is exceeded. Given: - Mock client and API response with 50 vulnerabilities. - Mock datetime.now(timezone.utc) to simulate timeout after processing 10 indicators. When: - Calling fetch_indicators_command function. Then: - Exits loop early after timeout. - Returns only indicators processed before timeout (10 out of 50). - Sets pagination in next_run since len(vulnerabilities_ids) != len(indicators). """ # Create a list response with 50 vulnerabilities vulnerabilities_list = [{"id": i} for i in range(1, 51)] list_response = { "total": 50, "next": None, "previous": None, "size": 50, "from": 0, "results": vulnerabilities_list, } requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) # Mock get_vulnerability for each ID for i in range(1, 51): detail_response = { "id": i, "cve_ids": [f"CVE-2024-{i:04d}"], "description": f"Test vulnerability {i}", "published_at": "2026-01-01T00:00:00Z", "last_modified_at": "2026-01-01T00:00:00Z", "cvssv3_score": 7.5, } requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/{i}", json=detail_response, status_code=200) params: dict = {**BASIC_PARAMS, "first_fetch": "3 days"} last_run: dict = { "from": 0, "next_last_touched_after": "2026-01-05T00:00:00Z", "next_last_touched_before": "2026-01-10T00:00:00Z", } # Mock datetime.now(timezone.utc) to simulate timeout after 10 iterations from datetime import datetime, timedelta # Create a fixed start time (timezone-aware) start_time = datetime(2026, 1, 10, 12, 0, 0, tzinfo=UTC) call_count = {"count": 0} def mock_datetime_now(tz=None): call_count["count"] += 1 if call_count["count"] <= 1: # First call: initial current_time at line 532 return start_time elif call_count["count"] <= 11: # Calls 2-11: within timeout (10 iterations in the loop at line 549) # Return time 60 seconds after start (within 240 second limit) return start_time + timedelta(seconds=60) else: # Call 12+: exceed timeout (> 240 seconds) # Return time 250 seconds after start (exceeds 240 second limit) return start_time + timedelta(seconds=250) # Patch datetime.now to use our mock function mock_datetime_class = mocker.patch("FeedFlashpointVulnerability.datetime") mock_datetime_class.now = mock_datetime_now # Mock arg_to_datetime for fetch_params mocker.patch( "FeedFlashpointVulnerability.arg_to_datetime", return_value=mocker.MagicMock(strftime=lambda x: "2026-01-10T00:00:00Z"), ) indicators, next_run = fetch_indicators_command(client=mock_client, params=params, last_run=last_run) # Verify timeout behavior # Should process only 10 indicators before timeout (not all 50) assert len(indicators) == 10 assert len(indicators) < 50 # Didn't process all vulnerabilities # Verify pagination is set because len(vulnerabilities_ids) != len(indicators) assert "from" in next_run assert next_run["from"] == 0 + len(indicators) # 0 + 10 = 10 assert next_run["next_last_touched_after"] == "2026-01-05T00:00:00Z" # from last_run assert next_run["next_last_touched_before"] == "2026-01-10T00:00:00Z" # from last_run class TestValidateFetchIndicatorsParams: """Test cases for validate_fetch_indicators_params function.""" def test_validate_fetch_indicators_params_with_filters(self): """ Test validate_fetch_indicators_params with filter parameters. Given: - Parameters with various filters. When: - Calling validate_fetch_indicators_params function. Then: - Returns dict with all filter parameters. """ params = { "first_fetch": "3 days", "tags": "kev,zero_day", "min_cvssv3_score": "7.0", "max_cvssv3_score": "10.0", "products": "product1,product2", "vendors": "vendor1", } last_run: dict = {} fetch_params = validate_fetch_indicators_params(params, last_run) assert "last_touched_after" in fetch_params assert "last_touched_before" in fetch_params assert fetch_params["tags"] == "kev,zero_day" assert fetch_params["min_cvssv3_score"] == "7.0" assert fetch_params["products"] == "product1,product2" def test_validate_fetch_indicators_params_with_last_run(self): """ Test validate_fetch_indicators_params with last_run data. Given: - Parameters and existing last_run. When: - Calling validate_fetch_indicators_params function. Then: - Uses last_run values for last_touched_after. """ params = {"first_fetch": "3 days"} last_run = {"next_last_touched_after": "2026-01-01T00:00:00Z", "from": 100} fetch_params = validate_fetch_indicators_params(params, last_run) assert fetch_params["last_touched_after"] == "2026-01-01T00:00:00Z" assert fetch_params["from"] == 100 def test_validate_fetch_indicators_params_with_valid_severities(self): """ Test validate_fetch_indicators_params with valid severity values. Given: - Parameters with valid severity values. When: - Calling validate_fetch_indicators_params function. Then: - Returns dict with severity parameter. """ params = { "first_fetch": "3 days", "severities": "critical,high,medium", } last_run: dict = {} fetch_params = validate_fetch_indicators_params(params, last_run) assert fetch_params["severity"] == "critical,high,medium" def test_validate_fetch_indicators_params_with_invalid_severities_in_test_mode(self): """ Test validate_fetch_indicators_params with invalid severity values in test mode. Given: - Parameters with invalid severity values and is_test=True. When: - Calling validate_fetch_indicators_params function. Then: - Raises DemistoException with appropriate error message. """ params = { "first_fetch": "3 days", "severities": "critical,invalid_severity", } last_run: dict = {} with pytest.raises(DemistoException) as err: validate_fetch_indicators_params(params, last_run, is_test=True) assert "invalid_severity" in str(err.value) assert "Severity" in str(err.value) def test_validate_fetch_indicators_params_with_valid_ref_types(self): """ Test validate_fetch_indicators_params with valid reference type values. Given: - Parameters with valid reference type values. When: - Calling validate_fetch_indicators_params function. Then: - Returns dict with ref_types parameter. """ params = { "first_fetch": "3 days", "ref_types": "cve id,exploit database,metasploit url", } last_run: dict = {} fetch_params = validate_fetch_indicators_params(params, last_run) assert fetch_params["ref_types"] == "cveid,exploitdb,metasploit" def test_validate_fetch_indicators_params_with_invalid_ref_types_in_test_mode(self): """ Test validate_fetch_indicators_params with invalid reference type values in test mode. Given: - Parameters with invalid reference type values and is_test=True. When: - Calling validate_fetch_indicators_params function. Then: - Raises DemistoException with appropriate error message. """ params = { "first_fetch": "3 days", "ref_types": "cveid,invalid_ref_type", } last_run: dict = {} with pytest.raises(DemistoException) as err: validate_fetch_indicators_params(params, last_run, is_test=True) assert "invalid_ref_type" in str(err.value) assert "Reference Types" in str(err.value) def test_validate_fetch_indicators_params_with_valid_cwe_ids(self): """ Test validate_fetch_indicators_params with valid CWE ID values. Given: - Parameters with valid CWE ID values. When: - Calling validate_fetch_indicators_params function. Then: - Returns dict with cwe_ids parameter. """ params = { "first_fetch": "3 days", "cwe_ids": "79,89,22", } last_run: dict = {} fetch_params = validate_fetch_indicators_params(params, last_run) assert fetch_params["cwe_ids"] == "79,89,22" def test_validate_fetch_indicators_params_with_invalid_cwe_ids_in_test_mode(self): """ Test validate_fetch_indicators_params with invalid CWE ID values in test mode. Given: - Parameters with non-integer CWE ID values and is_test=True. When: - Calling validate_fetch_indicators_params function. Then: - Raises DemistoException with appropriate error message. """ params = { "first_fetch": "3 days", "cwe_ids": "79,not_a_number,22", } last_run: dict = {} with pytest.raises(DemistoException) as err: validate_fetch_indicators_params(params, last_run, is_test=True) assert "not_a_number" in str(err.value) assert "CWE IDs" in str(err.value) def test_validate_fetch_indicators_params_with_valid_locations(self): """ Test validate_fetch_indicators_params with valid location values. Given: - Parameters with valid location values. When: - Calling validate_fetch_indicators_params function. Then: - Returns dict with location parameter mapped correctly. """ params = { "first_fetch": "3 days", "locations": "remote / network access,local access required", } last_run: dict = {} fetch_params = validate_fetch_indicators_params(params, last_run) assert "remote" in fetch_params["location"] assert "local" in fetch_params["location"] def test_validate_fetch_indicators_params_with_invalid_locations_in_test_mode(self): """ Test validate_fetch_indicators_params with invalid location values in test mode. Given: - Parameters with invalid location values and is_test=True. When: - Calling validate_fetch_indicators_params function. Then: - Raises DemistoException with appropriate error message. """ params = { "first_fetch": "3 days", "locations": "remote / network access,invalid_location", } last_run: dict = {} with pytest.raises(DemistoException) as err: validate_fetch_indicators_params(params, last_run, is_test=True) assert "invalid_location" in str(err.value) assert "Locations" in str(err.value) def test_validate_fetch_indicators_params_with_valid_ransomware_scores(self): """ Test validate_fetch_indicators_params with valid ransomware score values. Given: - Parameters with valid ransomware score values. When: - Calling validate_fetch_indicators_params function. Then: - Returns dict with ransomware_score parameter. """ params = { "first_fetch": "3 days", "ransomware_scores": "critical,high", } last_run: dict = {} fetch_params = validate_fetch_indicators_params(params, last_run) assert fetch_params["ransomware_score"] == "critical,high" def test_validate_fetch_indicators_params_with_invalid_ransomware_scores_in_test_mode(self): """ Test validate_fetch_indicators_params with invalid ransomware score values in test mode. Given: - Parameters with invalid ransomware score values and is_test=True. When: - Calling validate_fetch_indicators_params function. Then: - Raises DemistoException with appropriate error message. """ params = { "first_fetch": "3 days", "ransomware_scores": "critical,invalid_score", } last_run: dict = {} with pytest.raises(DemistoException) as err: validate_fetch_indicators_params(params, last_run, is_test=True) assert "invalid_score" in str(err.value) assert "Ransomware Scores" in str(err.value) def test_validate_fetch_indicators_params_with_valid_attack_types(self): """ Test validate_fetch_indicators_params with valid attack type values. Given: - Parameters with valid attack type values. When: - Calling validate_fetch_indicators_params function. Then: - Returns dict with attack_type parameter. """ params = { "first_fetch": "3 days", "attack_types": "cryptographic,man-in-the-middle (mitm),race condition", } last_run: dict = {} fetch_params = validate_fetch_indicators_params(params, last_run) assert fetch_params["attack_type"] == "crypt,mitm,race" def test_validate_fetch_indicators_params_with_invalid_attack_types_in_test_mode(self): """ Test validate_fetch_indicators_params with invalid attack type values in test mode. Given: - Parameters with invalid attack type values and is_test=True. When: - Calling validate_fetch_indicators_params function. Then: - Raises DemistoException with appropriate error message. """ params = { "first_fetch": "3 days", "attack_types": "crypt,invalid_attack", } last_run: dict = {} with pytest.raises(DemistoException) as err: validate_fetch_indicators_params(params, last_run, is_test=True) assert "invalid_attack" in str(err.value) assert "Attack Types" in str(err.value) def test_validate_fetch_indicators_params_with_multiple_invalid_params_in_test_mode(self): """ Test validate_fetch_indicators_params with multiple invalid parameters in test mode. Given: - Parameters with multiple invalid values and is_test=True. When: - Calling validate_fetch_indicators_params function. Then: - Raises DemistoException with all error messages. """ params = { "first_fetch": "3 days", "severities": "invalid_severity", "attack_types": "invalid_attack", "cwe_ids": "not_a_number", } last_run: dict = {} with pytest.raises(DemistoException) as err: validate_fetch_indicators_params(params, last_run, is_test=True) error_msg = str(err.value) assert "invalid_severity" in error_msg assert "invalid_attack" in error_msg assert "not_a_number" in error_msg def test_validate_fetch_indicators_params_ignores_invalid_values_when_not_test_mode(self): """ Test validate_fetch_indicators_params ignores invalid values when not in test mode. Given: - Parameters with invalid values and is_test=False. When: - Calling validate_fetch_indicators_params function. Then: - Returns params with only valid values, no exception raised. """ params = { "first_fetch": "3 days", "severities": "critical,invalid_severity", "cwe_ids": "79,not_a_number", } last_run: dict = {} fetch_params = validate_fetch_indicators_params(params, last_run, is_test=False) assert fetch_params["severity"] == "critical" assert fetch_params["cwe_ids"] == "79" def test_validate_fetch_indicators_params_with_score_filters(self): """ Test validate_fetch_indicators_params with CVSS and EPSS score filters. Given: - Parameters with various score filters. When: - Calling validate_fetch_indicators_params function. Then: - Returns dict with all score parameters. """ params = { "first_fetch": "3 days", "min_epss_score": "0.5", "max_epss_score": "1.0", "min_cvssv2_score": "5.0", "max_cvssv2_score": "10.0", "min_cvssv3_score": "7.0", "max_cvssv3_score": "10.0", "min_cvssv4_score": "6.0", "max_cvssv4_score": "9.0", } last_run: dict = {} fetch_params = validate_fetch_indicators_params(params, last_run) assert fetch_params["min_epss_score"] == "0.5" assert fetch_params["max_epss_score"] == "1.0" assert fetch_params["min_cvssv2_score"] == "5.0" assert fetch_params["max_cvssv2_score"] == "10.0" assert fetch_params["min_cvssv3_score"] == "7.0" assert fetch_params["max_cvssv3_score"] == "10.0" assert fetch_params["min_cvssv4_score"] == "6.0" assert fetch_params["max_cvssv4_score"] == "9.0" def test_validate_fetch_indicators_params_with_ref_values(self): """ Test validate_fetch_indicators_params with ref_values parameter. Given: - Parameters with ref_values. When: - Calling validate_fetch_indicators_params function. Then: - Returns dict with ref_values parameter. """ params = { "first_fetch": "3 days", "ref_values": "CVE-2024-1234,CVE-2024-5678", } last_run: dict = {} fetch_params = validate_fetch_indicators_params(params, last_run) assert fetch_params["ref_values"] == "CVE-2024-1234,CVE-2024-5678" """ TEST CASES FOR CLIENT ERROR HANDLING """ class TestClientErrorHandling: """Test cases for Client error handling.""" def test_handle_errors_400_with_json_response(self, mock_client, requests_mock): """ Test handle_errors with 400 status and JSON error response. Given: - API response with 400 status and JSON error details. When: - Calling API method. Then: - Raises DemistoException with type and detail from response. """ requests_mock.get( f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json={"type": "validation_error", "errors": [{"detail": "Invalid parameter value"}]}, status_code=400, ) with pytest.raises(DemistoException) as err: mock_client.list_vulnerabilities() assert "validation_error" in str(err.value) assert "Invalid parameter value" in str(err.value) def test_handle_errors_400_with_non_json_response(self, mock_client, requests_mock): """ Test handle_errors with 400 status and non-JSON error response. Given: - API response with 400 status and non-JSON content. When: - Calling API method. Then: - Raises DemistoException with raw text. """ requests_mock.get( f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", text="Bad Request", status_code=400, ) with pytest.raises(DemistoException) as err: mock_client.list_vulnerabilities() assert "Bad request" in str(err.value) def test_handle_errors_400_with_empty_errors_array(self, mock_client, requests_mock): """ Test handle_errors with 400 status and empty errors array. Given: - API response with 400 status and empty errors array. When: - Calling API method. Then: - Raises DemistoException without crashing. """ requests_mock.get( f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json={"type": "validation_error", "errors": [{}]}, status_code=400, ) with pytest.raises(DemistoException) as err: mock_client.list_vulnerabilities() assert "Bad request" in str(err.value) """ TEST CASES FOR MAIN FUNCTION """ class TestMain: """Test cases for main function.""" @patch("FeedFlashpointVulnerability.return_results") def test_main_test_module(self, mock_return, requests_mock, mocker): """ Test main function with test-module command. Given: - Mock environment with test-module command. When: - Calling main function. Then: - Returns 'ok' result. """ response = util_load_json("test_data/vulnerability_list_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=response, status_code=200) mocker.patch.object(demisto, "params", return_value=BASIC_PARAMS) mocker.patch.object(demisto, "command", return_value="test-module") main() assert mock_return.call_args.args[0] == "ok" @patch("FeedFlashpointVulnerability.return_results") def test_main_fetch_indicators(self, mock_return, requests_mock, mocker): """ Test main function with fetch-indicators command. Given: - Mock environment with fetch-indicators command. When: - Calling main function. Then: - Creates indicators and sets last run. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) mocker.patch.object(demisto, "params", return_value=BASIC_PARAMS) mocker.patch.object(demisto, "command", return_value="fetch-indicators") mocker.patch.object(demisto, "getLastRun", return_value={}) mock_set_last_run = mocker.patch.object(demisto, "setLastRun") mock_create_indicators = mocker.patch.object(demisto, "createIndicators") main() mock_set_last_run.assert_called_once() mock_create_indicators.assert_called_once() @pytest.mark.parametrize( "params, err_msg", [ ({}, MESSAGES["NO_PARAM_PROVIDED"].format("Server URL")), ({"url": MOCK_URL}, MESSAGES["NO_PARAM_PROVIDED"].format("API Key")), ({"url": MOCK_URL, "credentials": {"password": API_KEY}}, MESSAGES["NO_PARAM_PROVIDED"].format("Source Reliability")), ], ) def test_main_invalid_params(self, params, err_msg, mocker): """ Test main function with invalid parameters. Given: - Invalid parameters. When: - Calling main function. Then: - Returns error message. """ mocker.patch.object(demisto, "params", return_value=params) mocker.patch.object(demisto, "command", return_value="test-module") return_error = mocker.patch.object(FeedFlashpointVulnerability, "return_error") main() assert err_msg in return_error.call_args[0][0] def test_main_not_implemented_command(self, mocker): """ Test main function with not implemented command. Given: - Not implemented command. When: - Calling main function. Then: - Returns NotImplementedError message. """ mocker.patch.object(demisto, "params", return_value=BASIC_PARAMS) mocker.patch.object(demisto, "command", return_value="unknown-command") return_error = mocker.patch.object(FeedFlashpointVulnerability, "return_error") main() assert "not implemented" in return_error.call_args[0][0].lower() """ TEST CASES FOR CLIENT CLASS """ class TestClient: """Test cases for Client class.""" def test_client_initialization(self): """ Test Client class initialization. Given: - Valid client parameters. When: - Creating Client instance. Then: - Client is created with correct attributes. """ client = Client( base_url=MOCK_URL, api_key=API_KEY, verify=True, proxy=False, ) assert client._base_url == MOCK_URL assert "Authorization" in client._headers assert f"Bearer {API_KEY}" in client._headers["Authorization"] def test_client_list_vulnerabilities(self, mock_client, requests_mock): """ Test Client.list_vulnerabilities method. Given: - Mock client and API response. When: - Calling list_vulnerabilities method. Then: - Returns API response data. """ response = util_load_json("test_data/vulnerability_list_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=response, status_code=200) result = mock_client.list_vulnerabilities(params={"size": 10}) assert result["total"] == 123456 assert len(result["results"]) == 1 def test_client_get_vulnerability(self, mock_client, requests_mock): """ Test Client.get_vulnerability method. Given: - Mock client and API response. When: - Calling get_vulnerability method. Then: - Returns vulnerability detail data. """ response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=response, status_code=200) result = mock_client.get_vulnerability(id="111111") assert result["id"] == 111111 assert result["cve_ids"] == ["CVE-YYYY-XXXX", "CVE-YYYY-ZZZZ"] def test_client_handle_errors_known_status(self, mock_client, requests_mock): """ Test Client.handle_errors with known HTTP status code. Given: - API response with 401 status. When: - Calling API method. Then: - Raises DemistoException with appropriate message. """ requests_mock.get( f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json={"message": "Unauthorized"}, status_code=401, ) with pytest.raises(DemistoException) as err: mock_client.list_vulnerabilities() assert HTTP_ERRORS[401] in str(err.value) class TestGetIndicatorsCommand: """Test cases for flashpoint_vulnerability_get_indicators_command function.""" @patch("FeedFlashpointVulnerability.return_results") def test_get_indicators_command_success(self, mock_return, requests_mock, mocker): """ Test flashpoint_vulnerability_get_indicators_command with successful response. Given: - Mock client and successful API response. When: - Calling main function with flashpoint-vulnerability-get-indicators command. Then: - Returns CommandResults with indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") expected_outputs = util_load_json("test_data/get_indicators_output.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./test_data/get_indicators.md")) as file: hr_output = file.read() params = BASIC_PARAMS.copy() params.update({"createRelationship": "True"}) args = {"limit": "10", "first_fetch": "3 days"} mocker.patch.object(demisto, "params", return_value=params) mocker.patch.object(demisto, "command", return_value="flashpoint-vulnerability-get-indicators") mocker.patch.object(demisto, "args", return_value=args) main() result = mock_return.call_args.args[0] assert result.outputs_prefix == "FlashpointVulnerabilityFeed.Indicator" assert result.outputs_key_field == "value" assert result.outputs == expected_outputs assert result.readable_output == hr_output def test_get_indicators_command_with_filters(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with filter parameters. Given: - Mock client and filter arguments. When: - Calling flashpoint_vulnerability_get_indicators_command with filters. Then: - Returns CommandResults with filtered indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = { "limit": "10", "first_fetch": "3 days", "severities": "Critical,High", "min_cvssv3_score": "7.0", "max_cvssv3_score": "10.0", } result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 # type: ignore[arg-type] def test_get_indicators_command_no_indicators_found(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with no indicators. Given: - Mock client and empty API response. When: - Calling flashpoint_vulnerability_get_indicators_command. Then: - Returns CommandResults with no indicators message. """ empty_response = util_load_json("test_data/vulnerability_list_empty_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=empty_response, status_code=200) params = BASIC_PARAMS.copy() args = {"limit": "10", "first_fetch": "3 days"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert MESSAGES["NO_INDICATORS_FOUND"] in result.readable_output # type: ignore[operator] def test_get_indicators_command_with_limit(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with custom limit. Given: - Mock client and custom limit argument. When: - Calling flashpoint_vulnerability_get_indicators_command with limit=5. Then: - Returns CommandResults respecting the limit. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = {"limit": "5", "first_fetch": "3 days"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) <= 5 # type: ignore[arg-type] def test_get_indicators_command_with_ransomware_score(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with ransomware score filter. Given: - Mock client and ransomware score filter. When: - Calling flashpoint_vulnerability_get_indicators_command with ransomware_scores. Then: - Returns CommandResults with filtered indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = {"limit": "10", "first_fetch": "3 days", "ransomware_scores": "Critical,High"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 # type: ignore[arg-type] def test_get_indicators_command_with_attack_types(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with attack types filter. Given: - Mock client and attack types filter. When: - Calling flashpoint_vulnerability_get_indicators_command with attack_types. Then: - Returns CommandResults with filtered indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = {"limit": "10", "first_fetch": "3 days", "attack_types": "cryptographic,Man-In-The-Middle (Mitm)"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 # type: ignore[arg-type] def test_get_indicators_command_with_products_and_vendors(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with products and vendors filters. Given: - Mock client and products/vendors filters. When: - Calling flashpoint_vulnerability_get_indicators_command with products and vendors. Then: - Returns CommandResults with filtered indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = {"limit": "10", "first_fetch": "3 days", "products": "Windows,Linux", "vendors": "Microsoft,RedHat"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 # type: ignore[arg-type] def test_get_indicators_command_with_cwe_ids(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with CWE IDs filter. Given: - Mock client and CWE IDs filter. When: - Calling flashpoint_vulnerability_get_indicators_command with cwe_ids. Then: - Returns CommandResults with filtered indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = {"limit": "10", "first_fetch": "3 days", "cwe_ids": "79,89,119"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 # type: ignore[arg-type] def test_get_indicators_command_with_cvss_scores(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with CVSS score filters. Given: - Mock client and CVSS score filters. When: - Calling flashpoint_vulnerability_get_indicators_command with min/max CVSS scores. Then: - Returns CommandResults with filtered indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = { "limit": "10", "first_fetch": "3 days", "min_cvssv2_score": "5.0", "max_cvssv2_score": "10.0", "min_cvssv3_score": "7.0", "max_cvssv3_score": "10.0", "min_cvssv4_score": "6.0", "max_cvssv4_score": "10.0", } result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 # type: ignore[arg-type] def test_get_indicators_command_with_reference_types(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with reference types filter. Given: - Mock client and reference types filter. When: - Calling flashpoint_vulnerability_get_indicators_command with ref_types. Then: - Returns CommandResults with filtered indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = {"limit": "10", "first_fetch": "3 days", "ref_types": "CVE ID,Exploit Database,Metasploit URL"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 # type: ignore[arg-type] def test_get_indicators_command_with_locations(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with locations filter. Given: - Mock client and locations filter. When: - Calling flashpoint_vulnerability_get_indicators_command with locations. Then: - Returns CommandResults with filtered indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = {"limit": "10", "first_fetch": "3 days", "locations": "Remote / Network Access,Local Access Required"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 # type: ignore[arg-type] def test_get_indicators_command_with_epss_scores(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with EPSS score filters. Given: - Mock client and EPSS score filters. When: - Calling flashpoint_vulnerability_get_indicators_command with min/max EPSS scores. Then: - Returns CommandResults with filtered indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = {"limit": "10", "first_fetch": "3 days", "min_epss_score": "0.5", "max_epss_score": "1.0"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 # type: ignore[arg-type] def test_get_indicators_command_with_tags(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with tags filter. Given: - Mock client and tags filter. When: - Calling flashpoint_vulnerability_get_indicators_command with tags. Then: - Returns CommandResults with filtered indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = {"limit": "10", "first_fetch": "3 days", "tags": "exploit,rce,privilege-escalation"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 # type: ignore[arg-type] def test_get_indicators_command_timeout_warning(self, mock_client, requests_mock, mocker): """ Test flashpoint_vulnerability_get_indicators_command with timeout exceeded. Given: - Mock client and multiple vulnerabilities causing timeout. When: - Calling flashpoint_vulnerability_get_indicators_command and exceeding 4 minutes. Then: - Returns CommandResults with warning and partial indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) start_time = datetime.now(UTC) timeout_time = start_time + timedelta(seconds=241) mock_datetime = mocker.patch("FeedFlashpointVulnerability.datetime") mock_datetime.now.side_effect = [start_time, timeout_time] mocker.patch("FeedFlashpointVulnerability.return_warning") params = BASIC_PARAMS.copy() args = {"limit": "10", "first_fetch": "3 days"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) def test_get_indicators_command_with_all_filters(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command with all filter parameters. Given: - Mock client and all available filter arguments. When: - Calling flashpoint_vulnerability_get_indicators_command with all filters. Then: - Returns CommandResults with filtered indicators. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() args = { "limit": "10", "first_fetch": "3 days", "ransomware_scores": "Critical,High", "attack_types": "cryptographic,Man-In-The-Middle (Mitm)", "severities": "Critical,High", "products": "Windows", "vendors": "Microsoft", "cwe_ids": "79,89", "min_cvssv3_score": "7.0", "max_cvssv3_score": "10.0", "ref_types": "CVE ID,Exploit Database", "ref_values": "CVE-2024-1234", "locations": "Remote / Network Access", "min_epss_score": "0.5", "max_epss_score": "1.0", "tags": "exploit,rce", } result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 # type: ignore[arg-type] def test_get_indicators_command_outputs_structure(self, mock_client, requests_mock): """ Test flashpoint_vulnerability_get_indicators_command output structure. Given: - Mock client and successful API response. When: - Calling flashpoint_vulnerability_get_indicators_command. Then: - Returns CommandResults with correct output structure. """ list_response = util_load_json("test_data/vulnerability_list_200.json") detail_response = util_load_json("test_data/vulnerability_detail_200.json") expected_outputs = util_load_json("test_data/get_indicators_output.json") with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./test_data/get_indicators.md")) as file: hr_output = file.read() requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}", json=list_response, status_code=200) requests_mock.get(f"{MOCK_URL}{URL_SUFFIX['VULNERABILITIES']}/111111", json=detail_response, status_code=200) params = BASIC_PARAMS.copy() params.update({"createRelationship": "True"}) args = {"limit": "10", "first_fetch": "3 days"} result = flashpoint_vulnerability_get_indicators_command(client=mock_client, params=params, args=args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "FlashpointVulnerabilityFeed.Indicator" assert result.outputs_key_field == "value" assert result.outputs == expected_outputs assert result.readable_output == hr_output class TestValidationEdgeCases: """Test cases for validation edge cases to improve code coverage.""" def test_validate_epss_score_min_below_zero(self): """ Test validate_epss_score with minimum EPSS score below 0. Given: - Minimum EPSS score value of -0.1. When: - Calling validate_epss_score function. Then: - Raises DemistoException with appropriate error message. """ with pytest.raises(DemistoException) as err: validate_epss_score(min_epss="-0.1", min_epss_label="Min EPSS Score", max_epss=None, max_epss_label="Max EPSS Score") assert MESSAGES["INVALID_EPSS_SCORE"].format("Min EPSS Score") in str(err.value) def test_validate_epss_score_min_above_one(self): """ Test validate_epss_score with minimum EPSS score above 1. Given: - Minimum EPSS score value of 1.5. When: - Calling validate_epss_score function. Then: - Raises DemistoException with appropriate error message. """ with pytest.raises(DemistoException) as err: validate_epss_score(min_epss="1.5", min_epss_label="Min EPSS Score", max_epss=None, max_epss_label="Max EPSS Score") assert MESSAGES["INVALID_EPSS_SCORE"].format("Min EPSS Score") in str(err.value) def test_validate_epss_score_max_below_zero(self): """ Test validate_epss_score with maximum EPSS score below 0. Given: - Maximum EPSS score value of -0.5. When: - Calling validate_epss_score function. Then: - Raises DemistoException with appropriate error message. """ with pytest.raises(DemistoException) as err: validate_epss_score(min_epss=None, min_epss_label="Min EPSS Score", max_epss="-0.5", max_epss_label="Max EPSS Score") assert MESSAGES["INVALID_EPSS_SCORE"].format("Max EPSS Score") in str(err.value) def test_validate_epss_score_max_above_one(self): """ Test validate_epss_score with maximum EPSS score above 1. Given: - Maximum EPSS score value of 2.0. When: - Calling validate_epss_score function. Then: - Raises DemistoException with appropriate error message. """ with pytest.raises(DemistoException) as err: validate_epss_score(min_epss=None, min_epss_label="Min EPSS Score", max_epss="2.0", max_epss_label="Max EPSS Score") assert MESSAGES["INVALID_EPSS_SCORE"].format("Max EPSS Score") in str(err.value) def test_validate_fetch_indicators_params_invalid_limit_negative(self, mock_client): """ Test validate_fetch_indicators_params with negative limit value. Given: - Command arguments with negative limit value. When: - Calling validate_fetch_indicators_params with is_command=True. Then: - Raises DemistoException with appropriate error message. """ args = {"limit": "-5", "first_fetch": "3 days"} with pytest.raises(DemistoException) as err: validate_fetch_indicators_params(params=args, is_command=True) assert MESSAGES["INVALID_LIMIT_PROVIDED"].format(-5, DEFAULT_LIMIT) in str(err.value) def test_validate_fetch_indicators_params_invalid_limit_exceeds_max(self, mock_client): """ Test validate_fetch_indicators_params with limit exceeding maximum. Given: - Command arguments with limit value greater than DEFAULT_LIMIT. When: - Calling validate_fetch_indicators_params with is_command=True. Then: - Raises DemistoException with appropriate error message. """ args = {"limit": "100", "first_fetch": "3 days"} with pytest.raises(DemistoException) as err: validate_fetch_indicators_params(params=args, is_command=True) assert MESSAGES["INVALID_LIMIT_PROVIDED"].format(100, DEFAULT_LIMIT) in str(err.value) def test_validate_fetch_indicators_params_invalid_from_negative(self, mock_client): """ Test validate_fetch_indicators_params with negative from value. Given: - Command arguments with negative from value. When: - Calling validate_fetch_indicators_params with is_command=True. Then: - Raises DemistoException with appropriate error message. """ args = {"limit": "10", "from": "-10", "first_fetch": "3 days"} with pytest.raises(DemistoException) as err: validate_fetch_indicators_params(params=args, is_command=True) assert MESSAGES["INVALID_FROM_PROVIDED"].format(-10) in str(err.value) def test_validate_fetch_indicators_params_valid_limit_at_boundary(self, mock_client): """ Test validate_fetch_indicators_params with limit at maximum boundary. Given: - Command arguments with limit equal to DEFAULT_LIMIT. When: - Calling validate_fetch_indicators_params with is_command=True. Then: - Returns validated parameters without error. """ args = {"limit": str(DEFAULT_LIMIT), "first_fetch": "3 days"} result = validate_fetch_indicators_params(params=args, is_command=True) assert result["size"] == DEFAULT_LIMIT def test_validate_fetch_indicators_params_valid_from_zero(self, mock_client): """ Test validate_fetch_indicators_params with from value of 0. Given: - Command arguments with from value of 0. When: - Calling validate_fetch_indicators_params with is_command=True. Then: - Returns validated parameters without error. """ args = {"limit": "10", "from": "0", "first_fetch": "3 days"} result = validate_fetch_indicators_params(params=args, is_command=True) assert result["from"] == 0 class TestRemoveNullsFromDictionary: """Test cases for improved remove_nulls_from_nested_dictionary function.""" def test_remove_nulls_simple_dict(self): """ Test remove_nulls_from_nested_dictionary with simple dictionary. Given: - Dictionary with null and empty values. When: - Calling remove_nulls_from_nested_dictionary. Then: - Removes all null and empty values. """ from FeedFlashpointVulnerability import remove_nulls_from_nested_dictionary data: dict = {"key1": "value1", "key2": None, "key3": "", "key4": [], "key5": {}, "key6": (), "key7": "value7"} remove_nulls_from_nested_dictionary(data) assert data == {"key1": "value1", "key7": "value7"} def test_remove_nulls_nested_dict(self): """ Test remove_nulls_from_nested_dictionary with nested dictionary. Given: - Dictionary with nested dictionaries containing null values. When: - Calling remove_nulls_from_nested_dictionary. Then: - Recursively removes all null and empty values from nested dicts. """ from FeedFlashpointVulnerability import remove_nulls_from_nested_dictionary data = { "level1": {"level2": {"key1": "value1", "key2": None, "key3": ""}, "key4": "value4"}, "key5": None, "key6": "value6", } remove_nulls_from_nested_dictionary(data) assert data == {"level1": {"level2": {"key1": "value1"}, "key4": "value4"}, "key6": "value6"} def test_remove_nulls_nested_empty_dict(self): """ Test remove_nulls_from_nested_dictionary with nested empty dictionary. Given: - Dictionary with nested empty dictionaries. When: - Calling remove_nulls_from_nested_dictionary. Then: - Removes empty nested dictionaries after cleaning. """ from FeedFlashpointVulnerability import remove_nulls_from_nested_dictionary data = {"key1": "value1", "nested": {"inner": None}, "key2": "value2"} remove_nulls_from_nested_dictionary(data) assert data == {"key1": "value1", "key2": "value2"} def test_remove_nulls_list_in_dict(self): """ Test remove_nulls_from_nested_dictionary with list inside dictionary. Given: - Dictionary containing lists with null values. When: - Calling remove_nulls_from_nested_dictionary. Then: - Removes null values from lists and empty lists from dict. """ from FeedFlashpointVulnerability import remove_nulls_from_nested_dictionary data = {"key1": [1, 2, None, "", 3], "key2": [], "key3": "value3"} remove_nulls_from_nested_dictionary(data) assert data == {"key1": [1, 2, 3], "key3": "value3"} def test_remove_nulls_nested_list_with_dicts(self): """ Test remove_nulls_from_nested_dictionary with nested list containing dictionaries. Given: - Dictionary with lists containing dictionaries with null values. When: - Calling remove_nulls_from_nested_dictionary. Then: - Recursively cleans dictionaries inside lists. """ from FeedFlashpointVulnerability import remove_nulls_from_nested_dictionary data = {"items": [{"name": "item1", "value": None}, {"name": "item2", "value": "val2"}, {"empty": None}]} remove_nulls_from_nested_dictionary(data) assert data == {"items": [{"name": "item1"}, {"name": "item2", "value": "val2"}]} def test_remove_nulls_deeply_nested_structure(self): """ Test remove_nulls_from_nested_dictionary with deeply nested structure. Given: - Complex nested structure with multiple levels. When: - Calling remove_nulls_from_nested_dictionary. Then: - Recursively cleans all levels. """ from FeedFlashpointVulnerability import remove_nulls_from_nested_dictionary data = { "level1": { "level2": { "level3": {"key1": "value1", "key2": None, "list": [1, None, {"nested": "value", "empty": ""}]}, "empty_dict": {}, }, "key3": "", }, "top_level": "value", } remove_nulls_from_nested_dictionary(data) assert data == { "level1": {"level2": {"level3": {"key1": "value1", "list": [1, {"nested": "value"}]}}}, "top_level": "value", } def test_remove_nulls_list_with_empty_nested_structures(self): """ Test remove_nulls_from_nested_dictionary with list containing empty nested structures. Given: - List with empty dictionaries and lists. When: - Calling remove_nulls_from_nested_dictionary on a dict containing the list. Then: - Removes empty nested structures from the list. """ from FeedFlashpointVulnerability import remove_nulls_from_nested_dictionary data = {"items": [{"key": "value"}, {}, [], None, "", {"nested": {"inner": None}}]} remove_nulls_from_nested_dictionary(data) assert data == {"items": [{"key": "value"}]} def test_remove_nulls_preserves_valid_values(self): """ Test remove_nulls_from_nested_dictionary preserves valid values. Given: - Dictionary with valid values including 0, False, and non-empty strings. When: - Calling remove_nulls_from_nested_dictionary. Then: - Preserves all valid values including falsy but non-null values. """ from FeedFlashpointVulnerability import remove_nulls_from_nested_dictionary data = {"zero": 0, "false": False, "string": "text", "null": None, "empty": "", "list": [0, False, "text"]} remove_nulls_from_nested_dictionary(data) assert data == {"zero": 0, "false": False, "string": "text", "list": [0, False, "text"]} def test_remove_nulls_empty_dict_input(self): """ Test remove_nulls_from_nested_dictionary with empty dictionary. Given: - Empty dictionary. When: - Calling remove_nulls_from_nested_dictionary. Then: - Returns empty dictionary unchanged. """ from FeedFlashpointVulnerability import remove_nulls_from_nested_dictionary data: dict = {} remove_nulls_from_nested_dictionary(data) assert data == {} def test_remove_nulls_all_null_values(self): """ Test remove_nulls_from_nested_dictionary with all null values. Given: - Dictionary with only null and empty values. When: - Calling remove_nulls_from_nested_dictionary. Then: - Returns empty dictionary. """ from FeedFlashpointVulnerability import remove_nulls_from_nested_dictionary data: dict = {"key1": None, "key2": "", "key3": [], "key4": {}, "key5": ()} remove_nulls_from_nested_dictionary(data) assert data == {}