SecurityScorecard

Provides scorecards for domains.

Vulnerability Management · SecurityScorecard

Details

IDSecurityScorecard
ProviderSecurityScorecard
CategoryVulnerability Management
From Version6.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Provides commands to access SecurityScorecard’s API.
This integration was integrated and tested with the latest version of SecurityScorecard’s API as of August 2024.

Configure SecurityScorecard in Cortex

Parameter Description Required
SecurityScorecard API Base URL   True
Username/Email The SecurityScorecard username/email. True
API Token   True
Fetch incidents   False
Incidents Fetch Interval SecurityScorecard is updated on a daily basis therefore there’s no need to modify this value. False
Fetch Limit Maximum number of alerts per fetch. The maximum is 50. False
First fetch First fetch query (<number> <time unit>, e.g., 12 hours, 7 days. SecurityScorecard provides a maximum of 7 days back. To ensure no alerts are missed, it’s recommended to use a value less than 2 days. False
Incident type   False
Trust any certificate (not secure)   False
Use system proxy settings   False
Portfolio ID   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.

securityscorecard-portfolios-list


List all Portfolios.

Base Command

securityscorecard-portfolios-list

Input

Argument Name Description Required
limit Limit the amount of Portfolios to return. Defaults to 50. Optional

Context Output

Path Type Description
SecurityScorecard.Portfolio.id String Portfolio ID.
SecurityScorecard.Portfolio.name String Portfolio name.
SecurityScorecard.Portfolio.description String Portfolio description.
SecurityScorecard.Portfolio.privacy String Portfolio privacy. Can be either private, shared or team.
SecurityScorecard.Portfolio.read_only Boolean Whether the portfolio is read only.

securityscorecard-portfolio-list-companies


Lists all companies in Portfolio.

Base Command

securityscorecard-portfolio-list-companies

Input

Argument Name Description Required
portfolio_id Portfolio ID. The Portfolio ID can be retrieved using the ‘securityscorecard-portfolios-list’ command. Required
grade Grade filter. To filter multiple grades, comma-separate them, e.g. A,B. Possible values are: A, B, C, D, E, F. Optional
industry Industry filter. To filter multiple industries, comma-separate them, e.g. education,financial_services. Possible values are: education, financial_services, food, government, healthcare, information_services, manufacturing, retail, technology. Optional
vulnerability Vulnerability filter. Optional
issue_type Comma-separated list of issue types. Possible values are: adware_installation_trail, adware_installation, alleged_breach_incident, chatter, anonymous_proxy, service_cassandra, service_couchdb, attack_detected, attack_feed, new_booter_shell, spa_browser, cdn_hosting, tlscert_expired, tlscert_revoked, tlscert_self_signed, tlscert_excessive_expiration, tlscert_weak_signature, tlscert_no_revocation, service_cloud_provider, csp_no_policy_v2, csp_unsafe_policy_v2, csp_too_broad_v2, marketing_site, cookie_missing_secure_attribute, short_term_lending_site, leaked_credentials, leaked_credentials_info, service_dns, new_defacement, ransomware_victim, domain_uses_hsts_preloading, service_elasticsearch, employee_satisfaction, service_end_of_life, service_end_of_service, exposed_personal_information, exposed_personal_information_info, admin_subdomain_v2, tlscert_extended_validation, service_ftp, patching_cadence_high, web_vuln_host_high, service_vuln_host_high, service_imap, iot_camera, industrial_control_device, insecure_https_redirect_pattern_v2, service_ldap, service_ldap_anonymous, social_network_issues, patching_cadence_low, web_vuln_host_low, service_vuln_host_low, spf_record_malformed, malware_controller, malware_1_day, malware_30_day, malware_365_day, malware_infection, malware_infection_trail, patching_cadence_medium, web_vuln_host_medium, service_vuln_host_medium, service_microsoft_sql, minecraft_server, service_mongodb, no_browser_policy, service_mysql, service_neo4j, service_networking, object_storage_bucket_with_risky_acl, open_resolver, exposed_ports, service_open_vpn, service_oracle_db, outdated_os, outdated_browser, non_malware_events_last_month, service_pop3, service_pptp, phishing, typosquat, service_postgresql, exploited_product, public_text_credit_cards, public_text_database_dump, public_text_hashes, public_text_mention, public_text_password_dump, service_pulse_vpn, service_rdp, ransomware_association, redirect_chain_contains_http_v2, service_redis, remote_access, service_smb, mail_server_unusual_port, service_soap, spf_record_wildcard, spf_record_softfail, spf_record_missing, ssh_weak_protocol, ssh_weak_cipher, ssh_weak_mac, tls_weak_protocol, github_information_leak_disclosure, google_information_leak_disclosure, cookie_missing_http_only, domain_missing_https_v2, suspicious_traffic, tls_ocsp_stapling, tls_weak_cipher, telephony, service_telnet, tor_node_events_last_month, upnp_accessible, unsafe_sri_v2, uce, service_vnc, dnssec_detected, waf_detected_v2, hsts_incorrect_v2, hosted_on_object_storage_v2, references_object_storage_v2, x_content_type_options_incorrect_v2, x_frame_options_incorrect_v2, x_xss_protection_incorrect_v2, service_rsync. Optional
had_breach_within_last_days Domains with breaches in the last X days. Possible values are numbers, e.g. 1000. Optional

Context Output

Path Type Description
SecurityScorecard.Portfolio.Company.domain String Company domain.
SecurityScorecard.Portfolio.Company.name String Company name.
SecurityScorecard.Portfolio.Company.score Number Company overall score in numeric form (55-100).
SecurityScorecard.Portfolio.Company.grade String Company overall score in letter grade.
SecurityScorecard.Portfolio.Company.grade_url String Company overall score URL to SVG asset.
SecurityScorecard.Portfolio.Company.last30days_score_change Number Company overall score numeric change (±) in the last month.
SecurityScorecard.Portfolio.Company.industry String Industry category of the domain.
SecurityScorecard.Portfolio.Company.size String Company size, e.g. ‘size_more_than_10000’.
SecurityScorecard.Portfolio.Company.is_custom_vendor Boolean Whether the company is a custom vendor.
SecurityScorecard.Portfolio.Company.total Number Total number of companies in Portfolio.

securityscorecard-company-score-get


Retrieve company overall score.

Base Command

securityscorecard-company-score-get

Input

Argument Name Description Required
domain Company domain, e.g. google.com. The company must first be added to a Portfolio in order to be able to get its score. Required

Context Output

Path Type Description
SecurityScorecard.Company.Score.domain String Company domain.
SecurityScorecard.Company.Score.name String Company name.
SecurityScorecard.Company.Score.score Number Company overall score in numeric form (55-100).
SecurityScorecard.Company.Score.grade String Company overall score in letter grade form (A-F).
SecurityScorecard.Company.Score.last30days_score_change Number Company overall score numeric change (±) in the last month.
SecurityScorecard.Company.Score.industry String ndustry category of the domain.
SecurityScorecard.Company.Score.size String Company size, e.g. ‘size_more_than_10000’.

securityscorecard-company-factor-score-get


Retrieve company factor score.

Base Command

securityscorecard-company-factor-score-get

Input

Argument Name Description Required
domain Company domain. Required
severity Issue severity filter. Comma-separated list of the following values: ‘positive’, ‘info’, ‘low’, ‘medium’, ‘high’. Optional

Context Output

Path Type Description
SecurityScorecard.Company.Factor.name String Factor name.
SecurityScorecard.Company.Factor.score Number Factor score in numeric form (55-100).
SecurityScorecard.Company.Factor.grade String Factor score in letter grade form (A-F).
SecurityScorecard.Company.Factor.Issue.type String Type of issue found.
SecurityScorecard.Company.Factor.Issue.count Number How many times the issue was found.
SecurityScorecard.Company.Factor.Issue.severity String Severity of the issue.
SecurityScorecard.Company.Factor.Issue.total_score_impact Number Contribution of issue on overall score.
SecurityScorecard.Company.Factor.Issue.detail_url String URL to the details of the issue.
SecurityScorecard.Company.Factor.total Number Number of factors returned.

securityscorecard-company-history-score-get


Retrieve company historical scores.

Base Command

securityscorecard-company-history-score-get

Input

Argument Name Description Required
domain Company domain, e.g. google.com. Required
from Initial date for historical data. Value should be in format YYYY-MM-DD. Optional
to End date for historical data. Value should be in format YYYY-MM-DD. Optional
timing Timing granularity. Possible values are: daily, weekly. Optional

Context Output

Path Type Description
SecurityScorecard.Company.ScoreHistory.domain String Company domain.
SecurityScorecard.Company.ScoreHistory.date Date Score date.
SecurityScorecard.Company.ScoreHistory.score Number Company historical security score in numeric form (55-100).

securityscorecard-company-history-factor-score-get


Retrieve company historical factor scores.

Base Command

securityscorecard-company-history-factor-score-get

Input

Argument Name Description Required
domain Company domain, e.g. google.com. Required
from Initial date for historical data. Value should be in format ‘YYYY-MM-DD’. Optional
to End date for historical data. Value should be in format ‘YYYY-MM-DD’. Optional
timing Timing granularity. or “monthly”. Possible values are: daily, weekly, monthly. Optional

Context Output

Path Type Description
SecurityScorecard.Company.FactorHistory.domain String Company domain.
SecurityScorecard.Company.FactorHistory.date Date Score date.
SecurityScorecard.Company.FactorHistory.Factor.name Number Factor name.
SecurityScorecard.Company.FactorHistory.score Number Company historical security score in numeric form (55-100).

securityscorecard-alert-grade-change-create


Create alert based on grade.

Base Command

securityscorecard-alert-grade-change-create

Input

Argument Name Description Required
change_direction Direction of change. Possible values are: rises, drops. Required
score_types Comma-separated list of risk factors to monitor. Possible values are ‘overall’, ‘any_factor_score’, ‘network_security’, ‘dns_health’, ‘patching_cadence’, ‘endpoint_security’, ‘ip_reputation’, ‘application_security’, ‘cubit_score’, ‘hacker_chatter’, ‘leaked_information’, ‘social_engineering’. Required
target What do you want to monitor with this alert. This argument is required if the portfolio argument is not specified. Possible values are: my_scorecard, any_followed_company. Optional
portfolio A portfolio_id to use as a target for the alert. This argument is required if the target argument is not specified. You can get a list of portfolios by running !securityscorecard-portfolios-list. Optional

Context Output

Path Type Description
SecurityScorecard.Alerts.GradeChangeAlert.id String Alert ID.

securityscorecard-alert-score-threshold-create


Create alert based threshold met.

Base Command

securityscorecard-alert-score-threshold-create

Input

Argument Name Description Required
change_direction Direction of change. Possible values are: rises_above, drops_below. Required
threshold The numeric score used as the threshold to trigger the alert. Required
score_types Comma separated list of risk factors to monitor. Possible values are ‘overall’, ‘any_factor_score’, ‘network_security’, ‘dns_health’, ‘patching_cadence’, ‘endpoint_security’, ‘ip_reputation’, ‘application_security’, ‘cubit_score’, ‘hacker_chatter’, ‘leaked_information’, ‘social_engineering’. For multiple factors, provide comma-separated list, i.e. leaked_information,social_engineering. Required
target What do you want to monitor with this alert. This argument is required if the portfolio argument is not specified. Possible values are: my_scorecard, any_followed_company. Optional
portfolio A portfolio_id to use as a target for the alert. This argument is required if the target argument is not specified. You can get a list of portfolios by running !securityscorecard-portfolios-list. Optional

Context Output

Path Type Description
SecurityScorecard.Alerts.ScoreThresholdAlert.id String Alert ID.

securityscorecard-alert-delete


Delete an alert.

Base Command

securityscorecard-alert-delete

Input

Argument Name Description Required
alert_id Alert ID. Required

Context Output

There is no context output for this command.

securityscorecard-alerts-list


List alerts triggered in the last week.

Base Command

securityscorecard-alerts-list

Input

Argument Name Description Required
portfolio_id Portfolio ID. Can be retrieved using !securityscorecard-portfolios-list. Optional

Context Output

Path Type Description
SecurityScorecard.Alerts.Alert.id String Alert ID.
SecurityScorecard.Alerts.Alert.email String Alert email recipient.
SecurityScorecard.Alerts.Alert.change_type String Alert change type configured (score or threshold).
SecurityScorecard.Alerts.Alert.domain String Alert domain.
SecurityScorecard.Alerts.Alert.company_name String Alert company name.
SecurityScorecard.Alerts.Alert.Portfolio.id array Alert Portfolio ID.
SecurityScorecard.Alerts.Alert.my_scorecard Boolean Whether the alert was triggered on private scorecard. This depends on whether ‘my_scorecard’ was added to the optional argument ‘target’ when creating alerts using the ‘securityscorecard-alert-score-threshold-create’ and ‘securityscorecard-alert-grade-change-create’ commands.
SecurityScorecard.Alerts.Alert.created_at Date Timestamp of when the alert was triggered.

securityscorecard-company-services-get


Retrieve the service providers of a domain.

Base Command

securityscorecard-company-services-get

Input

Argument Name Description Required
domain Company domain. Required

Context Output

Path Type Description
SecurityScorecard.Service.vendor_domain String Vendor domain, e.g. Google, Amazon.
SecurityScorecard.Service.client_domain String Client domain. This value is identical to the input of the domain argument.
SecurityScorecard.Service.categories array Vendor service provider, e.g. mail_provider, nameserver_provider.

securityscorecard-company-events-get


Retrieve a company’s historical events.

Base Command

securityscorecard-company-events-get

Input

Argument Name Description Required
domain Company domain, e.g. google.com. Required
date_from Initial date for historical data. Value should be in format 2020-01-30T00:00:00.000Z. Optional
date_to End date for historical data. Value should be in format 2020-01-30T00:00:00.000Z. Optional

Context Output

Path Type Description
SecurityScorecard.Events.Event.ssc_event_id string event id.
SecurityScorecard.Events.Event.date date event date.
SecurityScorecard.Events.Event.status unknown event status.
SecurityScorecard.Events.Event.issue_count number event issue count.
SecurityScorecard.Events.Event.score_impact number event score impact.
SecurityScorecard.Events.Event.issue_type string event issue type.
SecurityScorecard.Events.Event.severity string event severity.
SecurityScorecard.Events.Event.factor string event factor.
SecurityScorecard.Events.Event.ssc_detail_url string event detail url.

securityscorecard-company-findings-get


Retrieve an issue_type’s historical findings in a scorecard.

Base Command

securityscorecard-company-findings-get

Input

Argument Name Description Required
domain Retrieve the service providers of a domain. Required
date The effective_date for historical data. Value should be in format ‘YYYY-MM-DD’. Required
issue_type Key representing issue type, e.g. api_key_exposed. Required
status group_status filter. Comma-separated list of the following values: ‘active’, ‘inactive’, ‘all’. Optional

Context Output

There is no context output for this command.

securityscorecard-issue-metadata


Retrieve metadata for an issue type, including description and recommendation.

Base Command

securityscorecard-issue-metadata

Input

Argument Name Description Required
issue_type Key representing issue type, e.g. api_key_exposed. Required

Context Output

Path Type Description
SecurityScorecard.Metadata.Issues.key string Key representing issue type, e.g. api_key_exposed.
SecurityScorecard.Metadata.Issues.severity string issue severity.
SecurityScorecard.Metadata.Issues.factor string issue factor.
SecurityScorecard.Metadata.Issues.title string issue title.
SecurityScorecard.Metadata.Issues.short_description string issue short description.
SecurityScorecard.Metadata.Issues.long_description string issue long description.
SecurityScorecard.Metadata.Issues.recommendation string issue recommendation.

securityscorecard-alert-rules-list


List alert subscriptions for the user.

Base Command

securityscorecard-alert-rules-list

Input

This command does not require any arguments.

Context Output

Path Type Description
SecurityScorecard.AlertRules.Rule.id String Alert Rule ID.
SecurityScorecard.AlertRules.Rule.name String Alert Rule name.
SecurityScorecard.AlertRules.Rule.target String Target of the Rule.
SecurityScorecard.AlertRules.Rule.updated_at Date Timestamp when the alert rule was last updated.
SecurityScorecard.AlertRules.Rule.paused_at String Timestamp when the alert rule was paused.

securityscorecard-issue-details-get


Retrieve issue details for a specific issue type and domain.

Base Command

securityscorecard-issue-details-get

Input
Argument Name Description Required
domain The domain to get the issue details for. Required
issue_type The issue type to get the details for. Required

Context Output

Path Type Description
SecurityScorecard.IssueDetails.issue_id String Unique UUID for this measurement.
SecurityScorecard.IssueDetails.parent_domain String Parent domain aka vendor.
SecurityScorecard.IssueDetails.issue_type String issue_type of the findings.
SecurityScorecard.IssueDetails.count Number The number of findings.
SecurityScorecard.IssueDetails.group_status String If findings are active or not.
SecurityScorecard.IssueDetails.first_seen_time Date Epoch of observation in nanoseconds.
SecurityScorecard.IssueDetails.last_seen_time Date Epoch of observation in nanoseconds.
SecurityScorecard.IssueDetails.port Number Port number of the observation if applicable.
SecurityScorecard.IssueDetails.domain String Domain of the observation if applicable.
SecurityScorecard.IssueDetails.ip String IP address of the observation if applicable.
SecurityScorecard.IssueDetails.protocol String Protocol of the observation if applicable.
SecurityScorecard.IssueDetails.observations String Observation data in raw JSON format.

Configuration parameters

  • base_url — SecurityScorecard API Base URL (required)
  • username — Username/Email (required)
  • isFetch — Fetch incidents
  • incidentFetchInterval — Incidents Fetch Interval
  • max_fetch — Fetch Limit
  • first_fetch — First fetch
  • incidentType — Incident type
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • portfolio_id — Portfolio ID

Commands (16)

  • securityscorecard-alert-delete

    Delete an alert.

  • securityscorecard-alert-grade-change-create

    Create alert based on grade.

  • securityscorecard-alert-rules-list

    List alert subscriptions for the user.

  • securityscorecard-alert-score-threshold-create

    Create alert based threshold met.

  • securityscorecard-alerts-list

    List alerts triggered in the last week.

  • securityscorecard-company-events-get

    Retrieve a company's historical events.

  • securityscorecard-company-factor-score-get

    Retrieve company factor score.

  • securityscorecard-company-findings-get

    Retrieve an issue_type's historical findings in a scorecard.

  • securityscorecard-company-history-factor-score-get

    Retrieve company historical factor scores.

  • securityscorecard-company-history-score-get

    Retrieve company historical scores.

  • securityscorecard-company-score-get

    Retrieve company overall score.

  • securityscorecard-company-services-get

    Retrieve the service providers of a domain.

  • securityscorecard-issue-details-get

    Retrieve issue details for a specific issue type and domain.

  • securityscorecard-issue-metadata

    Retrieve metadata for an issue type, including description and recommendation.

  • securityscorecard-portfolio-list-companies

    Lists all companies in Portfolio.

  • securityscorecard-portfolios-list

    List all Portfolios.

import json
from unittest.mock import MagicMock

import pytest
from CommonServerPython import *
from SecurityScorecard import (
    SecurityScorecardClient,
    alert_grade_change_create_command,
    alert_rules_list_command,
    alert_score_threshold_create_command,
    company_event_findings_get_command,
    company_events_get_command,
    company_factor_score_get_command,
    company_history_factor_score_get_command,
    company_history_score_get_command,
    company_score_get_command,
    company_services_get_command,
    get_last_run,
    incidents_to_import,
    issue_details_get_command,
    issue_metadata_get_command,
    portfolio_list_companies_command,
    portfolios_list_command,
)

""" TEST CONSTANTS """


USERNAME = "user@domain.com"
PORTFOLIO_ID = "1"
PORTFOLIO_ID_NE = "2"
DOMAIN = "domain1.com"
DOMAIN_NE = "domain2.com"


""" Load test data """


def load_json(path):
    with open(path, encoding="utf-8") as f:
        return json.load(f)


test_data = load_json("./test_data/data.json")


""" Helper Functions Unit Tests"""

FROZEN_DATE = "2021-10-09T22:38:02.560Z"


@pytest.mark.freeze_time(FROZEN_DATE)
@pytest.mark.parametrize(
    "last_run, first_fetch", [(FROZEN_DATE, None), (None, "3 days"), (None, "7 days"), (FROZEN_DATE, "7 days")]
)
def test_get_last_run(last_run, first_fetch):
    """
    Given:
        - The last run time of the fetch mechanism
        - The first fetch interval

    When:
        - Case A: Last run date is defined and first fetch is undefined.
        - Case B: Last run date is undefined and first fetch is defined to 3 days.
        - Case C: Last run date is undefined and first fetch is defined to 7 days.
        - Case D: Last run date is defined and first fetch is defined to 7 days.

    Then:
        - Case A: Last run date Unix timestamp will be returned.
        - Case B: Last run date will be set to to now and Unix timestamp will be set to most recent alert.
        - Case C: Last run date will be set to to now and Unix timestamp will be set to most recent alert.
        - Case D: Last run date Unix timestamp will be returned.
    """

    last_run_dt = get_last_run(last_run=last_run, first_fetch=first_fetch)

    if last_run:
        assert last_run_dt.replace(microsecond=0, second=0, minute=0, hour=0) == arg_to_datetime(
            arg_name="last_run", arg=last_run
        ).replace(microsecond=0, second=0, minute=0, hour=0, tzinfo=None)
    else:
        # resetting microsecond as causes failure:
        # E             +datetime.datetime(2021, 10, 3, 14, 50, 3, 242595)
        # E             -datetime.datetime(2021, 10, 3, 14, 50, 3, 244008)
        assert last_run_dt.replace(microsecond=0, second=0, minute=0, hour=0, tzinfo=None) == arg_to_datetime(
            arg_name="first_fetch", arg=first_fetch
        ).replace(microsecond=0, second=0, minute=0, hour=0, tzinfo=None)


alerts_mock = test_data.get("alerts")
incidents_mock = test_data.get("incidents")
incidents_to_import_test_inputs = [
    ([]),
    (alerts_mock),
]


@pytest.mark.parametrize("alerts", incidents_to_import_test_inputs)
def test_incidents_to_import(alerts: list):
    """
    Given:
        - List of alerts

    When:
        - Case A: No alerts supplied
        - Case B: alerts supplied

    Then:
        - Case A : No alerts imported
        - Case B: alerts imported

    """

    # Need to remove tz info to deal with tz awareness with arg_to_datetime
    incidents = incidents_to_import(
        alerts=alerts,
        last_run=arg_to_datetime("2021-07-25T00:00:00.000Z").replace(tzinfo=None),  # type: ignore
    )
    if not alerts:
        assert not incidents
    else:
        assert incidents == incidents_mock


""" Command Unit Tests """


MOCK_URL = "mock://securityscorecard-mock-url"

client = SecurityScorecardClient(
    base_url=MOCK_URL, verify=False, proxy=False, headers={}, username=USERNAME, api_key="API_KEY", max_fetch=100
)


@pytest.mark.parametrize("args", [({}), ({"limit": "1"}), ({"limit": "60"})])
def test_portfolios_list(mocker, args):
    """
    Given:
        - A limit
        - 3 alerts total
    When:
        - Case A: limit undefined
        - Case B: limit defined as 1 (less than the total)
        - Case C: limit defined as 60

    Then:
        - Case A: All portfolios returned
        - Case B: 1 portfolio returned
        - Case C: All portfolios returned
    """

    portfolios_mock = test_data.get("portfolios")
    portfolio_entries = portfolios_mock.get("entries")

    mocker.patch.object(client, "get_portfolios", return_value=portfolios_mock)

    portfolios_cmd_res: CommandResults = portfolios_list_command(client=client, args=args)
    portfolios_returned = portfolios_cmd_res.outputs

    if not args:
        assert portfolio_entries == portfolios_returned


companies_list_test_inputs = [
    (
        {
            "portfolio_id": PORTFOLIO_ID,
            "grade": None,
            "industry": None,
            "vulnerability": None,
            "issue_type": None,
            "had_breach_within_last_days": None,
        }
    ),
    (
        {
            "portfolio_id": PORTFOLIO_ID,
            "grade": "A",
            "industry": None,
            "vulnerability": None,
            "issue_type": None,
            "had_breach_within_last_days": None,
        }
    ),
    (
        {
            "portfolio_id": PORTFOLIO_ID,
            "grade": "A",
            "industry": "food",
            "vulnerability": None,
            "issue_type": None,
            "had_breach_within_last_days": None,
        }
    ),
    (
        {
            "portfolio_id": PORTFOLIO_ID,
            "grade": None,
            "industry": None,
            "vulnerability": None,
            "issue_type": None,
            "had_breach_within_last_days": "7",
        }
    ),
]


@pytest.mark.parametrize("args", companies_list_test_inputs)
def test_portfolio_list_companies(mocker, args: Dict[str, Any]):
    """
    Given:
        - A portfolio ID
        - 3 portfolios in test data
    When:
        - Case A: no filters supplied
        - Case B: grade filter supplied
        - Case C: grade filter and an industry filter supplied
        - Case D: invalid had breach within last days filter supplied
    Then:
        - Case A: All 3 portfolios returned
        - Case B: 2 portfolios returned
        - Case C: 1 portfolio returned
        - Case D: All 3 portfolios returned
    """

    if args.get("grade"):
        if args.get("industry"):
            companies_mock: Dict[str, str] = test_data.get("companies_A_grade_food_industry")
        else:
            companies_mock = test_data.get("companies_A_grade")

    else:
        companies_mock = test_data.get("companies")

    companies_entries: List[Dict[str, Any]] = companies_mock.get("entries")  # type: ignore

    mocker.patch.object(client, "get_companies_in_portfolio", return_value=companies_mock)

    companies_cmd_res: CommandResults = portfolio_list_companies_command(client=client, args=args)

    companies = companies_cmd_res.outputs

    assert companies == companies_entries


def test_portfolio_list_companies_portfolio_not_found(mocker):
    """
    Given:
        - A portfolio ID

    When:
        - Portfolio ID doesn't exist

    Then:
        - Throw 404, 'portfolio not found'
    """

    portfolio_not_found = test_data.get("portfolio_not_exist")
    mocker.patch.object(client, "get_companies_in_portfolio", return_value=portfolio_not_found)

    with pytest.raises(Exception):
        portfolio_list_companies_command(
            client=client,
            portfolio_id=PORTFOLIO_ID_NE,
            grade=None,
            industry_arg=None,
            vulnerability=None,
            issue_type=None,
            had_breach_within_last_days=None,
        )


company_score_test_input = [
    ({"domain": DOMAIN}),
    ({"domain": "google.com"}),
    ({"domain": "GOOGLE.COM"}),
    ({"domain": "nonexistantdomain.com"}),
]


@pytest.mark.parametrize("args", company_score_test_input)
def test_get_company_score(mocker, args):
    """
    Given:
        - A domain

    When:
        - Case A: domain is domain1.com
        - Case B: domain is lowercase
        - Case C: domain is uppercase

    Then:
        - Case A: score returned for domain
        - Case B: score returned for domain
        - Case C: score returned for domain
    """

    score_mock = test_data.get("score")
    mocker.patch.object(client, "get_company_score", return_value=score_mock)

    response_cmd_res: CommandResults = company_score_get_command(client=client, args=args)

    score = response_cmd_res.outputs

    assert score == score_mock


def test_get_company_score_not_found(mocker):
    """
    Given:
        - A company domain

    When:
        - Company domain doesn't exist

    Then:
        - company not found: nonexistentdomain.com (404)
    """

    company_not_found = test_data.get("company_not_found")
    mocker.patch.object(client, "get_company_score", return_value=company_not_found)

    cmd_res: CommandResults = company_score_get_command(client=client, args={"domain": DOMAIN_NE})

    status = cmd_res.outputs.get("error").get("statusCode")
    message = cmd_res.outputs.get("error").get("message")

    assert status == 404
    assert message == f"company not found: {DOMAIN_NE}"


factor_score_test_inputs = [
    ({"domain": DOMAIN, "severity": None}),
    ({"domain": DOMAIN, "severity": "positive"}),
    ({"domain": DOMAIN, "severity": "high,low"}),
]


@pytest.mark.parametrize("args", factor_score_test_inputs)
def test_get_company_factor_score(mocker, args):
    """
    Given:
        - A domain
        - A severity filter
    When:
        - Case A: Domain is valid, severity unspecified
        - Case B: Domain is valid, severity is positive
        - Case C: Domain is valid, severity is low and high
    Then:
        - Case A: Results in all severity factor scores for domain
        - Case B: Results only in positive severity factor scores for domain
        - Case C: Results in high and low severity factor scores for domain
    """

    if args.get("severity") == "positive":
        factor_score_mock = test_data.get("factor_score_severity_positive")
    elif args.get("severity") == "high,low":
        factor_score_mock = test_data.get("factor_score_severity_low_high")
    else:
        factor_score_mock = test_data.get("factor_score")

    mocker.patch.object(client, "get_company_factor_score", return_value=factor_score_mock)

    response: CommandResults = company_factor_score_get_command(client=client, args=args)

    assert response.outputs == factor_score_mock.get("entries")


company_historical_scores_test_inputs = [
    ({"domain": DOMAIN, "from": None, "to": None, "timing": None}),
    ({"domain": DOMAIN, "from": "2021-07-01", "to": "2021-07-31", "timing": "daily"}),
    ({"domain": DOMAIN, "from": "2021-07-01", "to": "2021-07-31", "timing": None}),
    ({"domain": DOMAIN, "from": "2021-07-01", "to": "2021-07-31", "timing": "weekly"}),
]


@pytest.mark.parametrize("args", company_historical_scores_test_inputs)
def test_get_company_historical_scores(mocker, args):
    """
    Given:
        - A domain
        - Date range
        - Timing
    When:
        - Case A: no range, no resolution
        - Case B: range 2021-07-01 to 2021-07-04, daily
        - Case C: range 2021-07-01 to 2021-07-04, no resolution
        - Case D: range 2021-07-01 to 2021-07-31, weekly
    Then:
        - Case A: Return all scores
        - Case B: Return scores received for 2021-07-01 to 2021-07-04 on daily basis
        - Case C: Return scores received for 2021-07-01 to 2021-07-04 on daily basis
        - Case D: Score received for 2021-07-01 to 2021-07-31 on weekly basis
    """

    if args.get("daily"):
        historical_score_mock = test_data.get("historical_score_daily")
    elif args.get("weekly"):
        historical_score_mock = test_data.get("historical_scores_weekly")
    else:
        historical_score_mock = test_data.get("historical_score")

    mocker.patch.object(client, "get_company_historical_scores", return_value=historical_score_mock)

    response: CommandResults = company_history_score_get_command(client=client, args=args)

    cmd_output = response.outputs

    assert cmd_output == historical_score_mock.get("entries")


company_historical_factor_scores_test_inputs = [
    ({"domain": DOMAIN, "from": None, "to": None, "timing": None}),
    ({"domain": DOMAIN, "from": "2021-07-01", "to": "2021-07-31", "timing": "daily"}),
    ({"domain": DOMAIN, "from": "2021-07-01", "to": "2021-07-31", "timing": None}),
    ({"domain": DOMAIN, "from": "2021-07-01", "to": "2021-07-31", "timing": "weekly"}),
]


@pytest.mark.parametrize("args", company_historical_factor_scores_test_inputs)
def test_get_company_historical_factor_scores(mocker, args):
    """
    Given:
        - A domain
        - Date range
        - Timing
    When:
        - Case A: no range, no resolution
        - Case B: range 2021-07-01 to 2021-07-04, daily
        - Case C: range 2021-07-01 to 2021-07-04, no resolution
        - Case D: range 2021-07-01 to 2021-07-31, weekly
    Then:
        - Case A: Return all scores
        - Case B: Return scores received for 2021-07-01 to 2021-07-04 on daily basis
        - Case C: Return scores received for 2021-07-01 to 2021-07-04 on daily basis
        - Case D: Score received for 2021-07-01 to 2021-07-31 on weekly basis
    """

    if args.get("daily"):
        historical_factor_score_mock = test_data.get("historical_factor_score_daily")
    elif args.get("weekly"):
        historical_factor_score_mock = test_data.get("historical_factor_scores_weekly")
    else:
        historical_factor_score_mock = test_data.get("historical_factor_score")

    historical_factor_score_mock = test_data.get("historical_factor_score")
    mocker.patch.object(client, "get_company_historical_factor_scores", return_value=historical_factor_score_mock)

    cmd_res: CommandResults = company_history_factor_score_get_command(client=client, args=args)

    factor_scores = cmd_res.outputs
    assert factor_scores == historical_factor_score_mock.get("entries")


alert_creation_inputs = [
    ({"change_direction": "drops", "score_types": "overall", "target": "my_scorecard", "portfolio": None}),
    ({"change_direction": "drops", "score_types": ["application_security"], "target": None, "portfolio": "1"}),
    ({"change_direction": "rises", "score_types": "application_security", "target": None, "portfolio": "1"}),
    ({"change_direction": "rises", "score_types": ["overall"], "target": "any_followed_company", "portfolio": None}),
]


@pytest.mark.parametrize("args, expected_delivery", zip(alert_creation_inputs, test_data.get("delivery_objects").get("entries")))
def test_create_grade_alert_subscription_delivery_object(mocker, args, expected_delivery):
    """
    Test the creation of a grade alert subscription delivery object.
    This test mocks the `create_alert_subscription` method of the `client` object
    to return a predefined alert subscription. It then verifies that the delivery
    object created by the `alert_grade_change_create_command` function matches the
    expected delivery structure and that the result outputs the correct alert ID.
    Args:
        mocker (MockerFixture): The mocker fixture used to patch methods.
        args (dict): The arguments for the alert creation.
        expected_delivery (dict): The expected delivery object.
    Raises:
        AssertionError: If the delivery object or result outputs do not match the expected values.
    """

    create_grade_alert_mock = {"id": "test_alert_id"}
    mocker.patch.object(client, "create_alert_subscription", return_value=create_grade_alert_mock)

    result = alert_grade_change_create_command(client, args)

    actual_delivery = client.create_alert_subscription.call_args[1]["delivery"]

    assert actual_delivery == expected_delivery
    assert result.outputs == "test_alert_id"


# Define the test data for score threshold alert creation tests
threshold_alert_creation_inputs = [
    {
        "change_direction": "drops_below",
        "threshold": 85,
        "score_types": ["overall"],
        "target": "any_followed_company",
        "portfolio": None,
    },
    {
        "change_direction": "rises_above",
        "threshold": 70,
        "score_types": ["application_security", "test_factor"],
        "target": None,
        "portfolio": "1",
    },
]


@pytest.mark.parametrize(
    "args, expected_delivery", zip(threshold_alert_creation_inputs, test_data.get("threshold_delivery_objects").get("entries"))
)
def test_create_score_threshold_alert_subscription_delivery_object(mocker, args, expected_delivery):
    """
    Test the creation of a score threshold alert subscription delivery object.
    This test mocks the `create_alert_subscription` method of the `client` object
    to return a predefined alert subscription. It then verifies that the delivery
    object created by the `alert_score_threshold_create_command` function matches the
    expected delivery structure and that the result outputs the correct alert ID.
    Args:
        mocker (MockerFixture): The mocker fixture used to patch methods.
        args (dict): The arguments for the alert creation.
        expected_delivery (dict): The expected delivery object.
    Raises:
        AssertionError: If the delivery object or result outputs do not match the expected values.
    """

    create_score_threshold_alert_mock = {"id": "test_alert_id"}
    mocker.patch.object(client, "create_alert_subscription", return_value=create_score_threshold_alert_mock)

    result = alert_score_threshold_create_command(client, args)
    actual_delivery = client.create_alert_subscription.call_args[1]["delivery"]

    assert actual_delivery == expected_delivery
    assert result.outputs == "test_alert_id"


grade_alert_test_input = [
    ({"change_direction": "rises", "score_types": "overall", "target": None, "portfolio": PORTFOLIO_ID}),
    ({"change_direction": "rises", "score_types": "application_security", "target": "my_scorecard", "portfolio": "1"}),
    ({"change_direction": "rises", "score_types": "application_security", "target": None, "portfolio": None}),
]


@pytest.mark.parametrize("args", grade_alert_test_input)
def test_create_grade_change_alert(mocker, args):
    """
    Given:
        - Direction change
        - Score type(s)
        - Target or Portfolio
    When:
        - Case A: rising grade, overall score type, to portfolio
        - Case B: Both portfolio and target are specified
        - Case C: Neither portfolio and target are specified
    Then:
        - Case A: Alert created
        - Case D: DemistoException thrown
        - Case E: DemistoException thrown
    """

    create_grade_alert_mock = test_data.get("create_grade_alert")
    mocker.patch.object(client, "create_alert_subscription", return_value=create_grade_alert_mock)

    if args.get("target") and args.get("portfolio"):
        with pytest.raises(DemistoException) as exc:
            alert_grade_change_create_command(client=client, args=args)

        assert "Both 'portfolio' and 'target' argument have been set" in str(exc.value)
    elif not args.get("target") and not args.get("portfolio"):
        with pytest.raises(DemistoException) as exc:
            alert_grade_change_create_command(client=client, args=args)

        assert "Either 'portfolio' or 'target' argument must be given" in str(exc.value)
    else:
        cmd_res: CommandResults = alert_grade_change_create_command(client=client, args=args)

        assert cmd_res.outputs == create_grade_alert_mock.get("id")


score_alert_test_input = [
    ({"change_direction": "rises", "threshold": 90, "score_types": "overall", "target": None, "portfolio": PORTFOLIO_ID}),
    (
        {
            "change_direction": "rises",
            "threshold": 90,
            "score_types": "application_security",
            "target": "my_scorecard",
            "portfolio": "1",
        }
    ),
    ({"change_direction": "rises", "threshold": 90, "score_types": "application_security", "target": None, "portfolio": None}),
    ({"change_direction": "rises", "threshold": "A", "score_types": "application_security", "target": None, "portfolio": None}),
]


@pytest.mark.parametrize("args", score_alert_test_input)
def test_create_score_change_alert(mocker, args):
    """
    Given:
        - A username
        - Direction change
        - Score type(s)
        - A threshold
        - Target or Portfolio
    When:
        - Case A: Username is valid, rising grade, overall score type, to portfolio
        - Case B: Both portfolio and target are specified
        - Case C: Neither portfolio and target are specified
        - Case D: Threshold supplied is not a number
    Then:
        - Case A: Alert created
        - Case B: DemistoException thrown
        - Case C: DemistoException thrown
        - Case D: ValueError thrown
    """

    create_score_alert_mock = test_data.get("create_score_alert")
    mocker.patch.object(client, "create_alert_subscription", return_value=create_score_alert_mock)

    if not isinstance(args.get("threshold"), int):
        with pytest.raises(ValueError) as exc:
            alert_score_threshold_create_command(client=client, args=args)

        assert "is not a valid number" in str(exc.value)
    elif args.get("target") and args.get("portfolio"):
        with pytest.raises(DemistoException) as exc:
            alert_score_threshold_create_command(client=client, args=args)

        assert "Both 'portfolio' and 'target' argument have been set" in str(exc.value)
    elif not args.get("target") and not args.get("portfolio"):
        with pytest.raises(DemistoException) as exc:
            alert_score_threshold_create_command(client=client, args=args)

        assert "Either 'portfolio' or 'target' argument must be given" in str(exc.value)
    else:
        cmd_res: CommandResults = alert_score_threshold_create_command(client=client, args=args)

        assert cmd_res.outputs == create_score_alert_mock.get("id")


services_test_input = (({"domain": DOMAIN}), ({"domain": DOMAIN_NE}))


@pytest.mark.parametrize("args", services_test_input)
def test_get_domain_services(mocker, args):
    """
    Given:
        - A domain
    When:
        - Case A: Domain is valid
        - Case B: Domain is invalid
    Then:
        - Case A: List of services is returned
        - Case B: Bad request
    """

    if args.get("domain") == DOMAIN_NE:
        services_mock = test_data.get("company_not_found")
        mocker.patch.object(client, "get_domain_services", return_value=services_mock)

        cmd_res: CommandResults = company_services_get_command(client=client, args=args)

        error = cmd_res.readable_output

        assert f"Error returning services for domain '{args.get('domain')}'" == error

    else:
        services_mock = test_data.get("services")
        mocker.patch.object(client, "get_domain_services", return_value=services_mock)

        cmd_res: CommandResults = company_services_get_command(client=client, args=args)
        services = cmd_res.outputs

        assert services == services_mock.get("entries")


def test_incidents_to_import_empty_alerts_future_last_run():
    """
    Given:
        - An empty list of alerts
        - A last run date in the future
    When:
        - incidents_to_import is called
    Then:
        - An empty list of incidents is returned
    """
    alerts = []
    last_run = arg_to_datetime("2022-01-01T00:00:00.000Z").replace(tzinfo=None)
    incidents = incidents_to_import(alerts=alerts, last_run=last_run)
    assert isinstance(incidents, list)
    assert len(incidents) == 0


@pytest.mark.parametrize("limit", ["0"])
def test_portfolios_list_limit_zero(mocker, limit):
    """
    Given:
        - A limit of 0
    When:
        - portfolios_list is called
    Then:
        - An empty list of portfolios is returned
    """
    mocker.patch.object(client, "get_portfolios", return_value=[])
    portfolios = client.get_portfolios(limit=limit)
    assert isinstance(portfolios, list)
    assert len(portfolios) == 0


def test_incidents_to_import_alerts_last_run_in_past():
    """
    Given:
        - A list of alerts
        - A last run date in the past
    When:
        - incidents_to_import is called
    Then:
        - A list of incidents is returned
    """
    mock = [
        {
            "id": "27",
            "username": "user@domain.com",
            "change_type": "score_change",
            "domain": "domain1.co.il",
            "company_name": "Domain1",
            "portfolios": [{"id": "a", "name": "test_portfolio"}],
            "my_scorecard": False,  # corrected false to False
            "created_at": "2021-07-27T00:00:00.000Z",
            "change_data": [
                {"score": 77, "factor": "network_security", "direction": "drops", "grade_letter": "C", "score_impact": -3},
                {"score": 69, "factor": "ip_reputation", "direction": "drops", "grade_letter": "D", "score_impact": -3},
            ],
        }
    ]
    last_run = arg_to_datetime("2020-01-01T00:00:00.000Z").replace(tzinfo=None)
    incidents = incidents_to_import(alerts=mock, last_run=last_run)
    assert isinstance(incidents, list)
    assert len(incidents) > 0


@pytest.fixture
def mocked_security_scorecard_client():
    mocked_security_scorecard_client = SecurityScorecardClient(
        base_url="https://api.securityscorecard.io",
        verify=True,
        proxy=None,
        headers={"Content-Type": "application/json"},
        username="your_username",
        api_key="your_api_key",
    )
    mocked_security_scorecard_client.get_issue_metadata = MagicMock(
        return_value={
            "key": "issue_type",
            "severity": "high",
            "factor": "network_security",
            "title": "Issue Title",
            "short_description": "Short description",
            "long_description": "Long description",
            "recommendation": "Recommendation",
        }
    )
    mocked_security_scorecard_client.get_company_events = MagicMock(
        return_value={
            "entries": [
                {
                    "id": "1",
                }
            ]
        }
    )
    mocked_security_scorecard_client.get_company_event_findings = MagicMock(
        return_value={
            "entries": [
                {
                    "id": "1",
                }
            ]
        }
    )
    return mocked_security_scorecard_client


def test_issue_metadata_get_command_success(mocked_security_scorecard_client):
    # Call the issue_metadata_get_command function with a valid issue type
    args = {"issue_type": "issue_type"}
    result = issue_metadata_get_command(mocked_security_scorecard_client, args)

    # Assert that the result is a CommandResults object with the expected attributes
    assert isinstance(result, CommandResults)
    assert result.readable_output.startswith("### Issue Type issue_type")
    assert "key|severity|factor|title|short_description|long_description|recommendation" in result.readable_output
    assert (
        "| issue_type | high | network_security | Issue Title | Short description | Long description | Recommendation"
        in result.readable_output
    )  # noqa: E501
    assert result.outputs == {
        "key": "issue_type",
        "severity": "high",
        "factor": "network_security",
        "title": "Issue Title",
        "short_description": "Short description",
        "long_description": "Long description",
        "recommendation": "Recommendation",
    }
    assert result.raw_response == {
        "key": "issue_type",
        "severity": "high",
        "factor": "network_security",
        "title": "Issue Title",
        "short_description": "Short description",
        "long_description": "Long description",
        "recommendation": "Recommendation",
    }
    assert result.outputs_key_field == "key"

    # Assert that the client's get_issue_metadata method was called with the expected arguments
    mocked_security_scorecard_client.get_issue_metadata.assert_called_with(issue_type="issue_type")


def test_company_events_get_command_success(mocked_security_scorecard_client):
    # Call the company_events_get_command function with valid arguments
    args = {"domain": "example.com"}
    result = company_events_get_command(mocked_security_scorecard_client, args)

    # Assert that the result is a CommandResults object with the expected attributes
    assert isinstance(result, CommandResults)
    # assert result.outputs == output.COMPANY_EVENTS_GET_OUTPUTS
    # assert result.raw_response == output.COMPANY_EVENTS_GET_RAW_RESPONSE
    assert result.outputs_prefix == "SecurityScorecard.Company.Events"
    assert result.outputs_key_field == "ssc_event_id"

    # Assert that the mocked_security_scorecard_client's get_company_events method was called with the expected arguments
    mocked_security_scorecard_client.get_company_events.assert_called_with(domain="example.com", date_to=None, date_from=None)


def test_company_event_findings_get_command_success(mocked_security_scorecard_client):
    # Call the company_event_findings_get_command function with valid arguments
    args = {"domain": "example.com", "event_id": "example_event_id"}
    result = company_event_findings_get_command(mocked_security_scorecard_client, args)

    # Assert that the result is a CommandResults object with the expected attributes
    assert isinstance(result, CommandResults)
    assert result.outputs_prefix == "SecurityScorecard.Company.Findings"
    assert result.outputs_key_field == "issue_id"

    # Assert that the mocked_security_scorecard_client's get_company_event_findings method was called with the expected arguments
    mocked_security_scorecard_client.get_company_event_findings.assert_called()


def test_alert_rules_list_command(mocker):
    """
    Given:
        - No specific arguments
    When:
        - Retrieving alert rules
    Then:
        - Ensure the alert rules are returned correctly
    """

    alert_rules_mock = test_data.get("alert_rules")
    mocker.patch.object(client, "http_request_wrapper", return_value=alert_rules_mock)

    response_cmd_res: CommandResults = alert_rules_list_command(client=client, args={})

    alert_rules = response_cmd_res.raw_response.get("entries")

    assert alert_rules == alert_rules_mock.get("entries")
    assert response_cmd_res.outputs_prefix == "SecurityScorecard.AlertRules.Rule"
    assert response_cmd_res.outputs_key_field == "id"


def test_issue_details_get_command_success(mocker):
    """
    Given:
        - A domain and issue type
    When:
        - Retrieving issue details for the specified domain and issue type
    Then:
        - Ensure the issue details are returned correctly
    """
    mock_response = {
        "entries": [
            {
                "domain": "example.com",
                "issue_id": "1",
                "issue_type": "spf_record_missing",
                "count": "2",
                "status": "active",
                "first_seen_time": "2023-01-01T00:00:00Z",
                "last_seen_time": "2023-01-02T00:00:00Z",
                "description": "SPF record is missing",
                "recommendation": "Add SPF record",
            }
        ]
    }

    mocker.patch.object(client, "get_company_issue_findings", return_value=mock_response)

    args = {"domain": "example.com", "issue_type": "spf_record_missing"}
    result = issue_details_get_command(client, args)

    assert isinstance(result, CommandResults)
    assert result.outputs == mock_response["entries"]
    assert result.outputs_prefix == "SecurityScorecard.IssueDetails"
    assert result.outputs_key_field == "issue_id"
    assert result.readable_output.startswith("### Domain example.com -- Findings for spf_record_missing")


def test_issue_details_get_command_no_results(mocker):
    """
    Given:
        - A domain and issue type
    When:
        - No issue details are found for the specified domain and issue type
    Then:
        - Ensure an empty result is returned
    """
    mock_response = {"entries": []}

    mocker.patch.object(client, "get_company_issue_findings", return_value=mock_response)

    args = {"domain": "example.com", "issue_type": "spf_record_missing"}
    result = issue_details_get_command(client, args)

    assert isinstance(result, CommandResults)
    assert result.outputs == []
    assert result.outputs_prefix == "SecurityScorecard.IssueDetails"
    assert result.outputs_key_field == "issue_id"
    assert "No findings were found" in result.readable_output


def test_issue_details_get_command_invalid_domain(mocker):
    """
    Given:
        - An invalid domain and issue type
    When:
        - Retrieving issue details for the specified domain and issue type
    Then:
        - Ensure an error is raised
    """
    mocker.patch.object(client, "get_company_issue_findings", side_effect=DemistoException("Invalid domain"))

    args = {"domain": "invalid.com", "issue_type": "spf_record_missing"}
    with pytest.raises(DemistoException, match="Invalid domain"):
        issue_details_get_command(client, args)