BitSight for Security Performance Management

Use the "Bitsight for Security Performance Management" Integration to get company guid, details, and findings. This integration also allows to fetch the findings by using the fetch incidents capability.

Utilities · Bitsight

Details

IDBitSight for Security Performance Management
ProviderBitSight
CategoryUtilities
From Version6.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Use the “Bitsight for Security Performance Management” Integration to get company guid, details, and findings. This integration also allows to fetch the findings by using the fetch incidents capability.
This integration was integrated and tested with version 01 of Bitsight for Security Performance Management

Configure Bitsight for Security Performance Management in Cortex

Parameter Description Required
API Key   True
Company’s GUID Use “bitsight-companies-guid-get” command to retrieve the company’s GUID. False
First fetch time in days Enter the number in days. When the fetch incident runs for first time, incidents will be fetched for a given number of days. False
Mirroring Direction The mirroring direction in which to mirror the findings. You can mirror “Incoming” (from Bitsight to XSOAR), “Outgoing” (from XSOAR to Bitsight), or in both directions. False
Mirror Tag for Notes The tag value should be used to mirror XSOAR incident notes to Bitsight finding comments by adding the same tag in the notes.

Note: This parameter is required when the mirroring direction is set to ‘Outgoing’ or ‘Incoming And Outgoing’.
False
Bitsight User Email Address Provide the Bitsight user email address to be used for sending XSOAR incident notes as Bitsight finding comments.

Note: This parameter is required when the mirroring direction is set to ‘Outgoing’ or ‘Incoming And Outgoing’.
False
Bitsight Remediation Status for Incident Opening Remediation status to set in Bitsight when opening incidents in XSOAR. Default value is ‘Open’.

Note: This parameter is only used when the mirroring direction is set to ‘Outgoing’ or ‘Incoming And Outgoing’.
False
Reopen incident based on Bitsight Remediation Status If selected, closed incidents will be reopened in XSOAR when finding remediation status on Bitsight platform matches the configured ‘Remediation Status for Incident Opening’.

Note: This parameter is only used when the mirroring direction is set to ‘Incoming’ or ‘Incoming And Outgoing’.
False
Bitsight Remediation Status for Incident Closure Remediation status to set in Bitsight when closing incidents in XSOAR. Default value is ‘Resolved’.

Note: This parameter is only used when the mirroring direction is set to ‘Outgoing’ or ‘Incoming And Outgoing’.
False
Close incident based on Bitsight Remediation Status If selected, active incidents will be closed in XSOAR when finding remediation status on Bitsight platform matches the configured ‘Remediation Status for Incident Closure’.

Note: This parameter is only used when the mirroring direction is set to ‘Incoming’ or ‘Incoming And Outgoing’.
False
Max Fetch Maximum number of incidents to fetch. The maximum value is 200. False
Findings Affect Rating Reason Filter by the affect rating reason of the findings to fetch. False
Findings Minimum Severity Minimum severity of the findings to fetch. False
Findings Minimum Asset Category Filter by the asset category (critical, high, medium, low). False
Findings Grade Filter the result by the value of grade. False
Risk Vector (‘All’ has been selected by default) This parameter supports comma separated list of values. By default ‘All’ will be selected, if you need only particular values you can unselect ‘All’ and select the required values. False
Trust any certificate (not secure)   False
Use system proxy settings   False
Fetch incidents   False
Incident type   False
Incidents Fetch Interval   False

Notes for mirroring

  • This feature is compliant with XSOAR version 6.1.0 and above.
  • When mirroring incidents, you can make changes in Bitsight that will be reflected in Cortex XSOAR, or vice versa.
  • The mirroring direction can be set to “Incoming” (from Bitsight to XSOAR), “Outgoing” (from XSOAR to Bitsight), or “Incoming And Outgoing” for bidirectional synchronization.
  • New notes from the Cortex XSOAR incident will be created as comments in the Bitsight findings. Updates to existing notes in the Cortex XSOAR incident will not be reflected in the Bitsight findings.
  • New comments from the Bitsight findings will be created as notes in the Cortex XSOAR incident. Updates to existing comments in the Bitsight findings will create new notes in the Cortex XSOAR incident.
  • When outgoing mirroring is enabled, the remediation status in Bitsight will be updated based on the Remediation status parameter in XSOAR:
    • Opening incidents in XSOAR will set the remediation status in Bitsight according to the “Bitsight Remediation Status for Incident Opening” parameter (default: “Open”).
    • Closing incidents in XSOAR will set the remediation status in Bitsight according to the “Bitsight Remediation Status for Incident Closure” parameter (default: “Resolved”).
  • If an active Cortex XSOAR incident is tied to a specific BitSight finding, and the finding’s remediation status matches the “Bitsight Remediation Status for Incident Closure” parameter:
    • If the “Reopen incident based on BitSight Remediation Status” parameter is selected and “Incoming Mirroring” is enabled, the incident will be closed in XSOAR.
  • If a closed Cortex XSOAR incident is tied to a specific BitSight finding, and the finding’s remediation status matches the “Bitsight Remediation Status for Incident Opening” parameter:
    • If the “Reopen incident based on BitSight Remediation Status” parameter is selected and “Incoming Mirroring” is enabled, the incident will be reopened in XSOAR.
  • The mirroring settings apply only for incidents that are fetched after applying the settings.
  • The mirroring is strictly tied to Incident type “BitSight Findings” & Incoming mapper “BitSight - Incoming Mapper”. If you want to change or use your custom incident type/mapper then make sure changes related to these are present.
  • If you want to use the mirror mechanism and you’re using custom mappers, then the incoming mapper must contain the following fields: dbotMirrorDirection, dbotMirrorId, dbotMirrorInstance, and dbotMirrorTags.
  • Following new fields are introduced in the response of the incident to enable the mirroring:
    • mirror_direction: This field determines the mirroring direction for the incident. It is a required field for XSOAR to enable mirroring support.
    • mirror_tags: This field determines what would be the tag needed to mirror the Cortex XSOAR entry out to Bitsight. It is a required field for XSOAR to enable mirroring support.
    • mirror_instance: This field determines from which instance the XSOAR incident was created. It is a required field for XSOAR to enable mirroring support.

Troubleshooting

The following are tips for handling issues with mirroring incidents between Bitsight and Cortex XSOAR.

Issue Recommendation
Mirroring is not working. Open Context Data and search for dbot. Confirm the dbot fields are configured correctly either through the mapper for that specific incident type or using setIncident. Specifically, make sure the integration instance is configured correctly for the mirroring direction (incoming, outgoing, both) - dbotMirrorId, dbotMirrorDirection, dbotMirrorInstance, dbotMirrorTags.
Required fields are not getting sent or not visible in UI. This may be a mapping issue, specifically if you have used a custom mapper make sure you’ve covered all the out of box mapper fields.
Notes from Cortex XSOAR have not been mirrored in Bitsight Tag is required for mirroring notes from Cortex XSOAR to Bitsight. There might be a reason the note is not tagged as the tag needs to be added manually in Cortex XSOAR.
Click Actions > Tags and add the “note” tag (OR the specific tag name which was set up in the Instance Configuration).
Server experiencing high load due to mirroring. Consider increasing the mirroring interval to reduce server load. The default mirroring interval is 1 minute, which can be adjusted by updating the sync.mirror.job.delay field in the Integration Server Configuration to a higher value based on your server capacity and requirements. For more information, see Integration Server Configurations (XSOAR 6.x) or Server Configurations (XSOAR 8.x).
No new findings are being fetched after updating filter parameters (e.g., Findings Minimum Severity, Findings Minimum Asset Category, Findings Grade, Findings Affect Rating Reason). When you modify filter parameters in the integration configuration, the existing instance continues using the previous first fetch time and offset. This may not retrieve findings based on the new filters. To resolve this:
Option 1: Reset the instance’s last run. This will restart fetching from the beginning with the new filter settings.
Option 2: Create a new integration instance with the updated filter parameters. This ensures a fresh start without affecting the existing instance’s fetch history.

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.

bitsight-company-details-get


Bitsight command to get company details based on the provided GUID. The details include rating details, rating history, and grades for individual risk vectors.

Base Command

bitsight-company-details-get

Input

Argument Name Description Required
guid GUID of the company to fetch its details.

Note: Users can get the list of the GUID by executing the “bitsight-companies-guid-get” command.
Required

Context Output

Path Type Description
BitSight.Company.guid string The unique identifier of this company.
BitSight.Company.customId string The customizable ID assigned to this company.
BitSight.Company.name string The name of this company.
BitSight.Company.description string Details about this company, which typically includes its industry and location.
BitSight.Company.ipv4Count number The number of IP addresses attributed to this company.
BitSight.Company.peopleCount number The number of employees in this company.
BitSight.Company.shortname string The abbreviated name of this company.
BitSight.Company.industry string The industry of this company.
BitSight.Company.industrySlug string The industry slug name of this company.
BitSight.Company.subIndustry string The sub-industry of this company.
BitSight.Company.subIndustrySlug string The sub-industry slug name of this company.
BitSight.Company.homePage string The URL of this company’s primary external website.
BitSight.Company.primaryDomain string The name of this company’s primary domain.
BitSight.Company.type string The type of rating.
BitSight.Company.displayURL string The URL to this company’s overview page in the Bitsight platform.
BitSight.Company.ratingDetails.name string The name of this risk vector.
BitSight.Company.ratingDetails.rating number Internal rating of this risk vector.
BitSight.Company.ratingDetails.grade string The letter grade of this risk vector.
BitSight.Company.ratingDetails.percentile number This company’s performance on this risk vector against their peers.
BitSight.Company.ratingDetails.gradeColor string The hex code to display letter grade colors in HTML applications.
BitSight.Company.ratingDetails.category string The risk category of this risk vector.
BitSight.Company.ratingDetails.categoryOrder number Used to visually sort this risk category in the Bitsight platform.
BitSight.Company.ratingDetails.beta boolean A true value indicates this risk vector is in beta and does not affect this company’s security rating.
BitSight.Company.ratingDetails.order number Used to visually sort this risk vector in the Bitsight platform.
BitSight.Company.ratingDetails.displayUrl string The URL in the Bitsight platform that contains the details of this risk vector.
BitSight.Company.ratings.ratingDate date The date when this Bitsight Security Rating Report was generated.
BitSight.Company.ratings.rating number The Bitsight Security Rating of this company on this day.
BitSight.Company.ratings.range string The rating category of this company on this day.
BitSight.Company.ratings.ratingColor string The hex code to display rating category colors in HTML applications.
BitSight.Company.searchCount number The number of times this company has been listed in search results.
BitSight.Company.subscriptionType string The type of subscription used to monitor this company.
BitSight.Company.sparkline string The URL path to the security rating trend line of this company during the past one year.
BitSight.Company.subscriptionTypeKey string The slug name of the subscription used to monitor this company.
BitSight.Company.subscriptionEndDate date The date when the subscription to this company expires.
BitSight.Company.bulkEmailSenderStatus string A FULL value indicates this company provides bulk email sending services, which excludes this company from the Spam Propagation risk vector.
BitSight.Company.serviceProvider boolean A true value indicates this company is a service provider.
BitSight.Company.customerMonitoringCount number The number of companies that are monitoring this company.
BitSight.Company.availableUpgradeTypes string For internal Bitsight use.
BitSight.Company.hasCompanyTree boolean A true value indicates this company has a Ratings Tree.
BitSight.Company.hasPreferredContact boolean For internal Bitsight use.
BitSight.Company.isBundle boolean A true value indicates this company is part of a ratings bundle.
BitSight.Company.ratingIndustryMedian string Indicates this company’s position in the peer group distribution chart.
BitSight.Company.primaryCompany.guid string The unique identifier of this organization’s primary company.
BitSight.Company.primaryCompany.name string The name of this organization’s primary company.
BitSight.Company.permissions.canDownloadCompanyReport boolean A true value indicates you can view and download Bitsight Security Rating Reports (PDF).
BitSight.Company.permissions.canViewForensics boolean A true value indicates you have the Event Forensics add-on package.
BitSight.Company.permissions.canViewServiceProviders boolean A true value indicates you can access Bitsight for Fourth Party Risk Management.
BitSight.Company.permissions.canRequestSelfPublishedEntity boolean A true value indicates you can request the creation of a self-published rating.
BitSight.Company.permissions.canViewInfrastructure boolean A true value indicates you can view your infrastructure attribution.
BitSight.Company.permissions.canAnnotate boolean A true value indicates you can identify assets and segment your network with infrastructure tags.
BitSight.Company.permissions.canViewCompanyReports boolean A true value indicates you can view Bitsight Security Rating Reports.
BitSight.Company.permissions.canManagePrimaryCompany boolean A true value indicates you can highlight a primary for your organization.
BitSight.Company.permissions.hasControl boolean For internal Bitsight use.
BitSight.Company.permissions.canEnableVendorAccess boolean A true value indicates you can modify vendor access.
BitSight.Company.isPrimary boolean A true value indicates your company is the primary for your organization.
BitSight.Company.securityGrade string For internal Bitsight use.
BitSight.Company.inSpmPortfolio boolean A true value indicates this company is in your Security Performance Management portfolio (My Company, SPM Subsidiary, etc.).
BitSight.Company.isMycompMysubsBundle string For internal Bitsight use.
BitSight.Company.companyFeatures string For internal Bitsight use.

Command example

!bitsight-company-details-get guid=00000000-0000-0000-0000-000000000001

Context Example

{
    "BitSight": {
        "Company": {
            "bulkEmailSenderStatus": "NONE",
            "customerMonitoringCount": 228,
            "description": "Saperix Technologies LLC develops risk analysis software solutions.",
            "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/overview/",
            "guid": "00000000-0000-0000-0000-000000000001",
            "hasCompanyTree": true,
            "hasPreferredContact": true,
            "homepage": "http://www.saperix.com",
            "inSpmPortfolio": true,
            "industry": "Technology",
            "industrySlug": "technology",
            "ipv4Count": 4169,
            "isBundle": false,
            "isMycompMysubsBundle": false,
            "isPrimary": false,
            "name": "Saperix, Inc.",
            "peopleCount": 400,
            "permissions": {
                "canAnnotate": true,
                "canDownloadCompanyReport": true,
                "canManagePrimaryCompany": true,
                "canRequestSelfPublishedEntity": true,
                "canViewCompanyReports": true,
                "canViewForensics": true,
                "canViewInfrastructure": true,
                "canViewIpAttributions": true,
                "canViewServiceProviders": true,
                "hasControl": true
            },
            "primaryCompany": {
                "guid": "00000000-0000-0000-0000-000000000002",
                "name": "Saperix Corporate"
            },
            "primaryDomain": "saperix.com",
            "ratingDetails": [
                {
                    "beta": false,
                    "category": "Compromised Systems",
                    "categoryOrder": 0,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/compromised-systems/?filter=Botnet%20Infections",
                    "grade": "A",
                    "gradeColor": "#2c4d7f",
                    "name": "Botnet Infections",
                    "order": 0,
                    "percentile": 100,
                    "rating": 820
                },
                {
                    "beta": false,
                    "category": "Compromised Systems",
                    "categoryOrder": 0,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/compromised-systems/?filter=Spam%20Propagation",
                    "grade": "A",
                    "gradeColor": "#2c4d7f",
                    "name": "Spam Propagation",
                    "order": 1,
                    "percentile": 100,
                    "rating": 820
                },
                {
                    "beta": false,
                    "category": "Compromised Systems",
                    "categoryOrder": 0,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/compromised-systems/?filter=Malware%20Servers",
                    "grade": "A",
                    "gradeColor": "#2c4d7f",
                    "name": "Malware Servers",
                    "order": 2,
                    "percentile": 100,
                    "rating": 820
                },
                {
                    "beta": false,
                    "category": "Compromised Systems",
                    "categoryOrder": 0,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/compromised-systems/?filter=Unsolicited%20Communications",
                    "grade": "A",
                    "gradeColor": "#2c4d7f",
                    "name": "Unsolicited Communications",
                    "order": 3,
                    "percentile": 100,
                    "rating": 820
                },
                {
                    "beta": false,
                    "category": "Compromised Systems",
                    "categoryOrder": 0,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/compromised-systems/?filter=Potentially%20Exploited",
                    "grade": "B",
                    "gradeColor": "#526d96",
                    "name": "Potentially Exploited",
                    "order": 4,
                    "percentile": 77,
                    "rating": 760
                },
                {
                    "beta": false,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=spf",
                    "grade": "B",
                    "gradeColor": "#526d96",
                    "name": "SPF",
                    "order": 5,
                    "percentile": 87,
                    "rating": 780
                },
                {
                    "beta": false,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=dkim",
                    "grade": "C",
                    "gradeColor": "#ecb870",
                    "name": "DKIM",
                    "order": 6,
                    "percentile": 54,
                    "rating": 700
                },
                {
                    "beta": false,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=certificate",
                    "grade": "B",
                    "gradeColor": "#526d96",
                    "name": "SSL Certificates",
                    "order": 7,
                    "percentile": 86,
                    "rating": 780
                },
                {
                    "beta": false,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=ssl",
                    "grade": "C",
                    "gradeColor": "#ecb870",
                    "name": "SSL Configurations",
                    "order": 8,
                    "percentile": 55,
                    "rating": 700
                },
                {
                    "beta": false,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=open_port",
                    "grade": "A",
                    "gradeColor": "#2c4d7f",
                    "name": "Open Ports",
                    "order": 9,
                    "percentile": 90,
                    "rating": 790
                },
                {
                    "beta": false,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=http_headers",
                    "grade": "F",
                    "gradeColor": "#b24053",
                    "name": "Web Application Headers",
                    "order": 10,
                    "percentile": 8,
                    "rating": 490
                },
                {
                    "beta": false,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=pc",
                    "grade": "C",
                    "gradeColor": "#ecb870",
                    "name": "Patching Cadence",
                    "order": 11,
                    "percentile": 62,
                    "rating": 720
                },
                {
                    "beta": false,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=insecure_sys",
                    "grade": "C",
                    "gradeColor": "#ecb870",
                    "name": "Insecure Systems",
                    "order": 12,
                    "percentile": 61,
                    "rating": 700
                },
                {
                    "beta": false,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=server_software",
                    "grade": "A",
                    "gradeColor": "#2c4d7f",
                    "name": "Server Software",
                    "order": 13,
                    "percentile": 99,
                    "rating": 810
                },
                {
                    "beta": false,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=endpoint_pc",
                    "grade": "F",
                    "gradeColor": "#b24053",
                    "name": "Desktop Software",
                    "order": 14,
                    "percentile": 1,
                    "rating": 400
                },
                {
                    "beta": false,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=endpoint_mobile",
                    "grade": "F",
                    "gradeColor": "#b24053",
                    "name": "Mobile Software",
                    "order": 15,
                    "percentile": 9,
                    "rating": 500
                },
                {
                    "beta": true,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=dnssec",
                    "grade": "F",
                    "gradeColor": "#b24053",
                    "name": "DNSSEC",
                    "order": 16,
                    "percentile": 0,
                    "rating": 300
                },
                {
                    "beta": true,
                    "category": "Diligence",
                    "categoryOrder": 1,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=mobile_appsec",
                    "grade": "N/A",
                    "gradeColor": "#495057",
                    "name": "Mobile Application Security",
                    "order": 17,
                    "percentile": "N/A",
                    "rating": "N/A"
                },
                {
                    "beta": false,
                    "category": "User Behavior",
                    "categoryOrder": 2,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/user-behavior",
                    "grade": "B",
                    "gradeColor": "#526d96",
                    "name": "File Sharing",
                    "order": 18,
                    "percentile": 79,
                    "rating": 750
                },
                {
                    "beta": false,
                    "category": "Public Disclosures",
                    "categoryOrder": 3,
                    "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/rating-details/?vector=news",
                    "grade": "C",
                    "gradeColor": "#ecb870",
                    "name": "Security Incidents",
                    "order": 19,
                    "percentile": 47,
                    "rating": 790
                }
            ],
            "ratingIndustryMedian": "below",
            "ratings": [
                {
                    "range": "Basic",
                    "rating": 600,
                    "ratingColor": "#b24053",
                    "ratingDate": "2021-03-10"
                },
                {
                    "range": "Basic",
                    "rating": 600,
                    "ratingColor": "#b24053",
                    "ratingDate": "2021-03-09"
                },
                {
                    "range": "Basic",
                    "rating": 600,
                    "ratingColor": "#b24053",
                    "ratingDate": "2021-03-08"
                },
                {
                    "range": "Basic",
                    "rating": 600,
                    "ratingColor": "#b24053",
                    "ratingDate": "2021-03-07"
                },
                {
                    "range": "Basic",
                    "rating": 600,
                    "ratingColor": "#b24053",
                    "ratingDate": "2021-03-06"
                },
                {
                    "range": "Basic",
                    "rating": 600,
                    "ratingColor": "#b24053",
                    "ratingDate": "2021-03-05"
                },
                {
                    "range": "Basic",
                    "rating": 600,
                    "ratingColor": "#b24053",
                    "ratingDate": "2021-03-04"
                },
                {
                    "range": "Basic",
                    "rating": 600,
                    "ratingColor": "#b24053",
                    "ratingDate": "2021-03-03"
                },
                {
                    "range": "Basic",
                    "rating": 600,
                    "ratingColor": "#b24053",
                    "ratingDate": "2021-03-02"
                },
                {
                    "range": "Basic",
                    "rating": 600,
                    "ratingColor": "#b24053",
                    "ratingDate": "2021-03-01"
                }
            ],
            "searchCount": 8956,
            "serviceProvider": false,
            "shortname": "Saperix",
            "sparkline": "https://api.bitsighttech.com/ratings/v1/companies/00000000-0000-0000-0000-000000000001/sparkline?size=small",
            "subIndustry": "Computer & Network Security",
            "subIndustrySlug": "computer_network_security",
            "subscriptionType": "Total Risk Monitoring",
            "subscriptionTypeKey": "continuous_monitoring",
            "type": "CURATED"
        }
    }
}

Human Readable Output

Company Details

Company Info Ratings Rating Details
guid: 00000000-0000-0000-0000-000000000001
customId: null
name: Saperix, Inc.
description: Saperix Technologies LLC develops risk analysis software solutions.
ipv4Count: 4169
peopleCount: 400
shortName: Saperix
industry: Technology
industrySlug: technology
subIndustry: Computer & Network Security
subIndustrySlug: computer_network_security
homePage: http://www.saperix.com
primaryDomain: saperix.com
type: CURATED
displayURL: https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/overview/
{‘rating’: 600, ‘rating_date’: ‘2021-03-10’, ‘range’: ‘Basic’},
{‘rating’: 600, ‘rating_date’: ‘2021-03-09’, ‘range’: ‘Basic’},
{‘rating’: 600, ‘rating_date’: ‘2021-03-08’, ‘range’: ‘Basic’},
{‘rating’: 600, ‘rating_date’: ‘2021-03-07’, ‘range’: ‘Basic’},
{‘rating’: 600, ‘rating_date’: ‘2021-03-06’, ‘range’: ‘Basic’},
{‘rating’: 600, ‘rating_date’: ‘2021-03-05’, ‘range’: ‘Basic’},
{‘rating’: 600, ‘rating_date’: ‘2021-03-04’, ‘range’: ‘Basic’},
{‘rating’: 600, ‘rating_date’: ‘2021-03-03’, ‘range’: ‘Basic’},
{‘rating’: 600, ‘rating_date’: ‘2021-03-02’, ‘range’: ‘Basic’},
{‘rating’: 600, ‘rating_date’: ‘2021-03-01’, ‘range’: ‘Basic’}
{‘name’: ‘Botnet Infections’, ‘rating’: 820, ‘percentile’: 100, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/compromised-systems/?filter=Botnet%20Infections’},
{‘name’: ‘Spam Propagation’, ‘rating’: 820, ‘percentile’: 100, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/compromised-systems/?filter=Spam%20Propagation’},
{‘name’: ‘Malware Servers’, ‘rating’: 820, ‘percentile’: 100, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/compromised-systems/?filter=Malware%20Servers’},
{‘name’: ‘Unsolicited Communications’, ‘rating’: 820, ‘percentile’: 100, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/compromised-systems/?filter=Unsolicited%20Communications’},
{‘name’: ‘Potentially Exploited’, ‘rating’: 760, ‘percentile’: 77, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/compromised-systems/?filter=Potentially%20Exploited’},
{‘name’: ‘SPF’, ‘rating’: 780, ‘percentile’: 87, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=spf’},
{‘name’: ‘DKIM’, ‘rating’: 700, ‘percentile’: 54, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=dkim’},
{‘name’: ‘SSL Certificates’, ‘rating’: 780, ‘percentile’: 86, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=certificate’},
{‘name’: ‘SSL Configurations’, ‘rating’: 700, ‘percentile’: 55, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=ssl’},
{‘name’: ‘Open Ports’, ‘rating’: 790, ‘percentile’: 90, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=open_port’},
{‘name’: ‘Web Application Headers’, ‘rating’: 490, ‘percentile’: 8, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=http_headers’},
{‘name’: ‘Patching Cadence’, ‘rating’: 720, ‘percentile’: 62, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=pc’},
{‘name’: ‘Insecure Systems’, ‘rating’: 700, ‘percentile’: 61, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=insecure_sys’},
{‘name’: ‘Server Software’, ‘rating’: 810, ‘percentile’: 99, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=server_software’},
{‘name’: ‘Desktop Software’, ‘rating’: 400, ‘percentile’: 1, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=endpoint_pc’},
{‘name’: ‘Mobile Software’, ‘rating’: 500, ‘percentile’: 9, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=endpoint_mobile’},
{‘name’: ‘DNSSEC’, ‘rating’: 300, ‘percentile’: 0, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=dnssec’},
{‘name’: ‘Mobile Application Security’, ‘rating’: ‘N/A’, ‘percentile’: ‘N/A’, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/diligence-details/?filter=mobile_appsec’},
{‘name’: ‘File Sharing’, ‘rating’: 750, ‘percentile’: 79, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/user-behavior’},
{‘name’: ‘Security Incidents’, ‘rating’: 790, ‘percentile’: 47, ‘display_url’: ‘https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/rating-details/?vector=news’}

bitsight-company-findings-get


Bitsight command to get company findings.

Base Command

bitsight-company-findings-get

Input

Argument Name Description Required
guid GUID of the company.

Note: Users can get the list of the GUID by executing the “bitsight-companies-guid-get” command.
Required
first_seen Filter the findings that were seen on and after this date. Format accepted: YYYY-MM-DD, Example: 2021-01-01. Required
last_seen Filter the findings that were seen on and prior to this date. Format accepted: YYYY-MM-DD, Example: 2021-01-01. Required
severity Minimum Severity of the findings. Possible values are: minor, moderate, material, severe. Optional
grade Filter by the grade of the findings. Supports comma separated values. Select the values from the list of predefined values: good, fair, warn, bad, neutral and na. Optional
asset_category Minimum Asset Category of the findings.

Example: If low is selected from the options then low, medium, high, and critical will be considered in retrieving results. Possible values are: low, medium, high, critical.
Optional
risk_vector_label Risk category of the findings. Supports comma separated values. Select the values from the list of predefined values: Web Application Headers, Botnet Infections, Breaches, Desktop Software, DKIM, DNSSEC, File Sharing, Insecure Systems, Malware Servers, Mobile App Publications, Mobile Application Security, Mobile Software, Open Ports, Patching Cadence, Potentially Exploited, Server Software, Spam Propagation, SPF, SSL Certificates, SSL Configurations, Unsolicited Communications, Web Application Security, DMARC. Optional
limit Set the maximum number of results to be retrieved. The maximum value is 1000.

Note: If a negative value is provided then the default value of 100 will be used. Default is 100.
Optional
offset Set the starting point of the results to be returned. A 0 (zero) value starts the results from the first record in the result set. Default is 0. Optional

Context Output

Path Type Description
BitSight.Company.guid string The unique identifier of this company.
BitSight.Company.CompanyFinding.temporaryId string A temporary identifier for this finding.
BitSight.Company.CompanyFinding.affectsRating boolean Indicates if this finding has an impact on the letter grade.
BitSight.Company.CompanyFinding.assets.asset string The asset (IP address or domain) associated with this finding.
BitSight.Company.CompanyFinding.assets.identifier string Identifier associated with the asset.
BitSight.Company.CompanyFinding.assets.category string The Bitsight-calculated asset importance.
BitSight.Company.CompanyFinding.assets.importance number For internal Bitsight use.
BitSight.Company.CompanyFinding.assets.isIp boolean A true value indicates this asset is an IP address.
BitSight.Company.CompanyFinding.details string Details of this finding. The included keys vary, depending on the following risk types Compromised Systems, Diligence, File Sharing.
BitSight.Company.CompanyFinding.evidenceKey string The company’s asset (domain or IP address) that’s attributed to the finding.
BitSight.Company.CompanyFinding.firstSeen date The date of the first observation.
BitSight.Company.CompanyFinding.lastSeen date The date of the most recent observation.
BitSight.Company.CompanyFinding.relatedFindings string Details of related findings.
BitSight.Company.CompanyFinding.riskCategory string The risk category associated with this finding.
BitSight.Company.CompanyFinding.riskVector string The slug name of the risk vector associated with this finding.
BitSight.Company.CompanyFinding.riskVectorLabel string The name of the risk vector associated with this finding.
BitSight.Company.CompanyFinding.rolledupObservationId string A unique identifier for this observation.
BitSight.Company.CompanyFinding.severity number The severity of the finding, which is the measured risk that this finding introduces.
BitSight.Company.CompanyFinding.severityCategory string The slug name of the finding severity.
BitSight.Company.CompanyFinding.tags string Infrastructure tags that help identify this asset.
BitSight.Company.CompanyFinding.duration string For internal Bitsight use.
BitSight.Company.CompanyFinding.comments string A thread of finding comments.
BitSight.Company.CompanyFinding.remainingDecay number For internal Bitsight use.
BitSight.Company.CompanyFinding.remediationHistory.lastRequestedRefreshDate date The date when a record refresh that included this finding was last requested.
BitSight.Company.CompanyFinding.remediationHistory.lastRefreshStatusDate date The date when a refresh of the remediation status of this finding was last requested.
BitSight.Company.CompanyFinding.remediationHistory.lastRefreshStatusLabel string The current record refresh status of this finding.
BitSight.Company.CompanyFinding.remediationHistory.lastRefreshReasonCode string The reason code of the last refresh of this finding.
BitSight.Company.CompanyFinding.remediationHistory.lastRemediationStatusLabel string The current remediation status of this finding.
BitSight.Company.CompanyFinding.remediationHistory.lastRemediationStatusDate date The date when the remediation status of this finding was last changed.
BitSight.Company.CompanyFinding.remediationHistory.remediationAssignments unknown The users who are assigned to remediate this finding.
BitSight.Company.CompanyFinding.remediationHistory.lastRemediationStatusUpdatedBy string The name of the user who updated the remediation status of this finding.
BitSight.Company.CompanyFinding.assetOverrides.asset string The domain or IP address of the overridden asset.
BitSight.Company.CompanyFinding.assetOverrides.importance string The user-assigned asset importance.
BitSight.Company.CompanyFinding.assetOverrides.overrideImportance unknown For internal Bitsight use.
BitSight.Company.CompanyFinding.attributedCompanies.guid string The unique identifier of the company attributed to the finding.
BitSight.Company.CompanyFinding.attributedCompanies.name string The name of the company that is attributed to the finding.
BitSight.Page.name String Name of the command.
BitSight.Page.next String The URL to navigate to the next page of results.
BitSight.Page.previous String The URL to navigate to the previous page of results.
BitSight.Page.count Number The number of findings.

Command example

!bitsight-company-findings-get guid=00000000-0000-0000-0000-000000000001 first_seen=2021-01-01 last_seen=2022-03-01 limit=2

Context Example

{
    "BitSight": {
        "Company": {
            "CompanyFinding": [
                {
                    "affectsRating": false,
                    "assets": [
                        {
                            "asset": "X.X.X.1",
                            "category": "low",
                            "importance": 0,
                            "isIp": true
                        }
                    ],
                    "attributedCompanies": [
                        {
                            "guid": "00000000-0000-0000-0000-000000000001",
                            "name": "Saperix, Inc."
                        }
                    ],
                    "details": {
                        "checkPass": "",
                        "country": "United States",
                        "destPort": 22,
                        "diligenceAnnotations": {
                            "cPE": [
                                "a:openbsd:openssh:8.0"
                            ],
                            "close-seen": "2022-03-11 16:22:22",
                            "message": "Detected service: SSH {{(OpenSSH_8.0)}}",
                            "product": "OpenSSH",
                            "transport": "tcp",
                            "version": "8.0"
                        },
                        "geoIpLocation": "US",
                        "grade": "GOOD",
                        "remediations": [
                            {
                                "helpText": "This port was observed running SSH, which is used for sending and receiving secure communication.",
                                "message": "Detected service: SSH (OpenSSH_8.0)",
                                "remediationTip": ""
                            }
                        ],
                        "rollupEndDate": "2022-01-28",
                        "rollupStartDate": "2022-01-28",
                        "searchableDetails": "Detected service: SSH {{(OpenSSH_8.0)}},tcp,OpenSSH"
                    },
                    "evidenceKey": "X.X.X.1:22",
                    "firstSeen": "2022-01-28",
                    "lastSeen": "2022-01-28",
                    "riskCategory": "Diligence",
                    "riskVector": "open_ports",
                    "riskVectorLabel": "Open Ports",
                    "rolledupObservationId": "11A3==",
                    "severity": 1,
                    "severityCategory": "minor",
                    "temporaryId": "A9yq"
                },
                {
                    "affectsRating": true,
                    "assetOverrides": [
                        {
                            "asset": "X.X.X.2",
                            "importance": "high",
                            "overrideImportance": "high"
                        }
                    ],
                    "assets": [
                        {
                            "asset": "X.X.X.2",
                            "category": "critical",
                            "importance": 0.49,
                            "isIp": true
                        }
                    ],
                    "attributedCompanies": [
                        {
                            "guid": "00000000-0000-0000-0000-000000000002",
                            "name": "Saperix Lab"
                        },
                        {
                            "guid": "00000000-0000-0000-0000-000000000001",
                            "name": "Saperix, Inc."
                        }
                    ],
                    "details": {
                        "checkPass": "",
                        "country": "United States",
                        "destPort": 143,
                        "geoIpLocation": "US",
                        "grade": "GOOD",
                        "observedIps": [
                            "X.X.X.2:143"
                        ],
                        "rollupEndDate": "2022-03-01",
                        "rollupStartDate": "2021-01-04"
                    },
                    "evidenceKey": "X.X.X.2:143",
                    "firstSeen": "2021-01-04",
                    "lastSeen": "2022-03-01",
                    "remainingDecay": 32,
                    "riskCategory": "Diligence",
                    "riskVector": "ssl_configurations",
                    "riskVectorLabel": "SSL Configurations",
                    "rolledupObservationId": "10A==",
                    "severity": 1,
                    "severityCategory": "minor",
                    "temporaryId": "A9yq"
                }
            ],
            "guid": "00000000-0000-0000-0000-000000000001"
        },
        "Page": {
            "count": 2441,
            "name": "bitsight-company-findings-get",
            "next": "https://api.bitsighttech.com/v1/companies/00000000-0000-0000-0000-000000000001/findings?expand=attributed_companies&first_seen_gte=2021-01-01&last_seen_lte=2022-03-01&limit=2&offset=2&unsampled=true",
            "previous": null
        }
    }
}

Human Readable Output

Company findings

Total Findings: 2441

Evidence Key Risk Vector Label First Seen Last Seen ID Risk Category Severity Asset Category Finding Grade
X.X.X.1:22 Open Ports 2022-01-28 2022-01-28 A9Jq Diligence minor X.X.X.1: Low Good
X.X.X.2:143 SSL Configurations 2021-01-04 2022-03-01 A9yq Diligence minor X.X.X.2: Critical Good

bitsight-companies-guid-get


Bitsight command to get list of companies and GUID.

Base Command

bitsight-companies-guid-get

Input

There are no input arguments for this command.

Context Output

Path Type Description
BitSight.Company.name String Name of this company.
BitSight.Company.shortname String The abbreviated name of this company.
BitSight.Company.guid String The unique identifier of this company.
BitSight.Company.customId String The customizable ID assigned to this company.
BitSight.Company.networkSizeV4 Number The number of IPv4 addresses attributed to this company.
BitSight.Company.rating Number The most recent security rating of this company.
BitSight.Company.ratingDate Date The date when the rating report for this company was generated.
BitSight.Company.dateAdded Date The date when this company was added to your portfolio.
BitSight.Company.industry String The industry of this company.
BitSight.Company.industrySlug String The slug name of this company’s industry.
BitSight.Company.subIndustry String The sub-industry of this company.
BitSight.Company.subIndustrySlug String The slug name of this company’s sub-industry.
BitSight.Company.type String The rating type.
BitSight.Company.logo String The URL in the Bitsight platform to this company’s logo image.
BitSight.Company.sparkline String The URL in the Bitsight platform to this company’s historical ratings trend line.
BitSight.Company.externalId Number The external ID assigned to this company.
BitSight.Company.subscriptionType String The subscription type used to monitor this company.
BitSight.Company.subscriptionTypeKey String The slug name of the subscription type used to monitor this company.
BitSight.Company.primaryDomain String The primary domain of this company.
BitSight.Company.securityGrade String For internal Bitsight use.
BitSight.Company.gradeDate Date For internal Bitsight use.
BitSight.Company.displayURL String The URL in the Bitsight platform to this company’s overview page.
BitSight.Company.href String The URL in the Bitsight platform to this company’s page.
BitSight.MyCompany.guid String The unique identifier of my company.

Command example


#### Context Example

```json
{
    "BitSight": {
        "Company": [
            {
                "dateAdded": "2020-09-03",
                "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000001/overview/",
                "externalId": 14885770,
                "guid": "00000000-0000-0000-0000-000000000001",
                "href": "https://api.bitsighttech.com/v1/companies/00000000-0000-0000-0000-000000000001",
                "industry": "Technology",
                "industrySlug": "technology",
                "logo": "https://api.bitsighttech.com/ratings/v1/companies/00000000-0000-0000-0000-000000000001/logo-image",
                "name": "Saperix, Inc.",
                "networkSizeV4": 4169,
                "primaryDomain": "saperix.com",
                "rating": 640,
                "ratingDate": "2022-03-29",
                "shortname": "Saperix",
                "sparkline": "https://api.bitsighttech.com/ratings/v1/companies/00000000-0000-0000-0000-000000000001/sparkline?size=small",
                "subIndustry": "Computer & Network Security",
                "subIndustrySlug": "computer_network_security",
                "subscriptionType": "Total Risk Monitoring",
                "subscriptionTypeKey": "continuous_monitoring",
                "type": "CURATED"
            },
            {
                "dateAdded": "2021-11-23",
                "displayUrl": "https://service.bitsighttech.com/app/company/00000000-0000-0000-0000-000000000002/overview/",
                "externalId": 51818179,
                "guid": "00000000-0000-0000-0000-000000000002",
                "href": "https://api.bitsighttech.com/v1/companies/00000000-0000-0000-0000-000000000002",
                "industry": "Technology",
                "industrySlug": "technology",
                "logo": "https://api.bitsighttech.com/ratings/v1/companies/00000000-0000-0000-0000-000000000002/logo-image",
                "name": "Saperix Corporate",
                "networkSizeV4": 4032,
                "primaryDomain": "saperix.com",
                "rating": 730,
                "ratingDate": "2022-03-29",
                "shortname": "Saperix Corporate",
                "sparkline": "https://api.bitsighttech.com/ratings/v1/companies/00000000-0000-0000-0000-000000000002/sparkline?size=small",
                "subIndustry": "Computer & Network Security",
                "subIndustrySlug": "computer_network_security",
                "subscriptionType": "MySubsidiary",
                "subscriptionTypeKey": "my_subsidiary",
                "type": "CURATED,SELF-PUBLISHED"
            }
        ],
        "MyCompany": {
            "guid": "00000000-0000-0000-0000-000000000001"
        }
    }
}

Human Readable Output

Companies

My Company: 00000000-0000-0000-0000-000000000001

Company Name Company Short Name GUID Rating
Saperix, Inc. Saperix 00000000-0000-0000-0000-000000000001 640
Saperix Corporate Saperix Corporate 00000000-0000-0000-0000-000000000002 730

Configuration parameters

  • apikey — API Key (required)
  • guid — Company's GUID
  • first_fetch — First fetch time in days
  • mirror_direction — Mirroring Direction
  • note_tag — Mirror Tag for Notes
  • user_email — Bitsight User Email Address
  • open_status_of_bitsight — Bitsight Remediation Status for Incident Opening
  • reopen_closed_incident — Reopen incident based on Bitsight Remediation Status
  • close_status_of_bitsight — Bitsight Remediation Status for Incident Closure
  • close_active_incident — Close incident based on Bitsight Remediation Status
  • max_fetch — Max Fetch
  • findings_affect_rating_reason — Findings Affect Rating Reason
  • findings_min_severity — Findings Minimum Severity
  • findings_min_asset_category — Findings Minimum Asset Category
  • findings_grade — Findings Grade
  • risk_vector — Risk Vector
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval

Commands (3)

  • bitsight-companies-guid-get

    Bitsight command to get list of companies and GUID.

  • bitsight-company-details-get

    Bitsight command to get company details based on the provided GUID. The details include rating details, rating history, and grades for individual risk vectors.

  • bitsight-company-findings-get

    Bitsight command to get company findings.

"""Test File for BitSightForSecurityPerformanceManagement Integration."""

import json
import os
from datetime import datetime, timedelta
from unittest.mock import patch

import BitSightForSecurityPerformanceManagement as bitsight
import demistomock as demisto
import pytest
from CommonServerPython import BaseClient, DemistoException, EntryType

BASE_URL = "https://test.com"
DEFAULT_FINDINGS_GRADE = "WARN,GOOD"
RISK_VECTOR_INPUT = "SSL Certificates"


def util_load_json(path):
    """Load file in JSON format."""
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


def test_get_companies_guid_command(mocker):
    """Tests success for companies_guid_get_command."""
    # Positive Scenario
    client = bitsight.Client(base_url=BASE_URL)

    res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/companies_guid_get_response.json"))
    with open(
        os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/companies_guid_get_success.md"), encoding="utf-8"
    ) as f:
        hr = f.read()
    mocker.patch.object(BaseClient, "_http_request", return_value=res["raw_response"])

    companies_guid_get_command_results = bitsight.companies_guid_get_command(client)

    assert companies_guid_get_command_results.outputs == res["outputs"]
    assert companies_guid_get_command_results.readable_output == hr


def test_company_details_get_command(mocker):
    """Tests success for company_details_get_command."""
    inp_args = {"guid": "123"}
    client = bitsight.Client(base_url=BASE_URL)

    res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/company_details_get_response.json"))
    with open(
        os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/company_details_get_success.md"), encoding="utf-8"
    ) as f:
        hr = f.read()

    mocker.patch.object(BaseClient, "_http_request", return_value=res["raw_response"])

    company_details_get_command_results = bitsight.company_details_get_command(client, inp_args)

    assert company_details_get_command_results.outputs == res["outputs"]
    assert company_details_get_command_results.readable_output == hr


def test_company_details_get_command_when_invalid_arguments_are_provided(requests_mock):
    """Test failure for company_details_get_command."""
    inp_args = {"guid": "non-existing-guid"}
    client = bitsight.Client(base_url=BASE_URL)

    requests_mock.get(BASE_URL + "/v1/companies/non-existing-guid", json={"detail": "Not found."}, status_code=404)
    with pytest.raises(DemistoException) as e:
        bitsight.company_details_get_command(client, inp_args)

    assert str(e.value) == 'Error in API call [404] - None\n{"detail": "Not found."}'


def test_company_findings_get_command(mocker):
    """Tests success for company_findings_get_command."""
    inp_args = {
        "guid": "123",
        "first_seen": "2021-01-01",
        "last_seen": "2022-02-21",
        "risk_vector_label": "Open Ports",
        "severity": "minor",
        "grade": "warn,good,bad",
    }

    client = bitsight.Client(base_url=BASE_URL)

    res = util_load_json(
        os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/company_findings_get_response.json")
    )
    with open(
        os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/company_findings_get_success.md"), encoding="utf-8"
    ) as f:
        hr = f.read()
    mocker.patch.object(BaseClient, "_http_request", return_value=res["raw_response"])

    company_findings_get_command_results = bitsight.company_findings_get_command(client, inp_args)

    assert company_findings_get_command_results.outputs == res["outputs"]
    assert company_findings_get_command_results.readable_output == hr


company_findings_get_on_failure_params = [
    (
        "invalid-severity",
        None,
        None,
        None,
        None,
        bitsight.ERROR_MESSAGES["INVALID_SELECT"].format(
            "invalid-severity", "severity", ", ".join(bitsight.SEVERITY_MAPPING.keys())
        ),
    ),
    (
        None,
        "invalid-asset-category",
        "",
        None,
        None,
        bitsight.ERROR_MESSAGES["INVALID_SELECT"].format(
            "invalid-asset-category", "asset_category", ", ".join(bitsight.ASSET_CATEGORY_MAPPING.keys())
        ),
    ),
    (
        None,
        None,
        "invalid-risk-vector",
        None,
        None,
        bitsight.ERROR_MESSAGES["INVALID_SELECT"].format(
            "invalid-risk-vector", "risk_vector_label", ", ".join(bitsight.RISK_VECTOR_MAPPING.keys())
        ),
    ),
    (
        None,
        None,
        "breaches,invalid-risk-vector",
        None,
        None,
        bitsight.ERROR_MESSAGES["INVALID_SELECT"].format(
            "invalid-risk-vector", "risk_vector_label", ", ".join(bitsight.RISK_VECTOR_MAPPING.keys())
        ),
    ),
    (None, None, None, "abc", None, 'Invalid number: "limit"="abc"'),
    (None, None, None, None, "abc", 'Invalid number: "offset"="abc"'),
    (None, None, None, bitsight.MAX_LIMIT + 1, None, bitsight.ERROR_MESSAGES["LIMIT_GREATER_THAN_ALLOWED"]),
]


@pytest.mark.parametrize(
    "severity, asset_category, risk_vector_label, limit, offset, error", company_findings_get_on_failure_params
)
def test_company_findings_get_command_when_invalid_arguments_are_provided(
    severity, asset_category, risk_vector_label, limit, offset, error
):
    """Test failure for company_findings_get_command."""
    inp_args = {
        "guid": "123",
        "first_seen": "2021-01-01",
        "last_seen": "2022-02-21",
        "risk_vector_label": risk_vector_label,
        "severity": severity,
        "asset_category": asset_category,
        "limit": limit,
        "offset": offset,
    }
    client = bitsight.Client(base_url=BASE_URL)

    with pytest.raises(ValueError) as e:
        bitsight.company_findings_get_command(client, inp_args)

    assert str(e.value) == error


def test_fetch_incidents_success_without_last_run(mocker):
    """Tests success for fetch_incidents when called for the first time."""
    inp_args = {
        "guid": "123",
        "first_fetch": "2",
        "findings_min_severity": "severe",
        "findings_grade": DEFAULT_FINDINGS_GRADE,
        "findings_asset_category": "low",
        "risk_vector": RISK_VECTOR_INPUT,
        "findings_affect_rating_reason": " ",
    }
    client = bitsight.Client(base_url=BASE_URL)
    mocker.patch.object(demisto, "params", return_value=inp_args)

    res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_incidents_response.json"))
    mocker.patch.object(BaseClient, "_http_request", return_value=res["response"])

    last_run, events = bitsight.fetch_incidents(client=client, last_run={}, params=inp_args)

    curr_date = (datetime.now() - timedelta(days=int(inp_args["first_fetch"]))).strftime("%Y-%m-%d")
    assert curr_date == last_run["first_fetch"]
    assert res["response"]["count"] == last_run["offset"]
    assert events == res["incidents"]


def test_fetch_incidents_success_with_last_run(mocker):
    """Tests success for fetch_incidents when called with last run."""
    inp_args = {
        "guid": "123",
        "first_fetch": "2",
        "findings_min_severity": "severe",
        "findings_grade": DEFAULT_FINDINGS_GRADE,
        "findings_asset_category": "low",
        "risk_vector": RISK_VECTOR_INPUT,
        "findings_affect_rating_reason": "Yes,No: Grace Period,No: Incubation Period",
    }
    client = bitsight.Client(base_url=BASE_URL)
    mocker.patch.object(demisto, "params", return_value=inp_args)

    res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_incidents_response.json"))
    mocker.patch.object(BaseClient, "_http_request", return_value=res["response"])

    last_run, events = bitsight.fetch_incidents(
        client=client, last_run={"first_fetch": "2022-03-27", "offset": 2}, params=inp_args
    )

    assert res["response"]["count"] + 2 == last_run["offset"]
    assert last_run["first_fetch"] == "2022-03-27"
    assert events == res["incidents"]


def test_fetch_incidents_with_duplicate_findings(mocker):
    """Tests fetch_incidents when duplicate findings are encountered."""
    inp_args = {
        "guid": "123",
        "first_fetch": "2",
        "findings_min_severity": "severe",
        "findings_grade": DEFAULT_FINDINGS_GRADE,
        "findings_asset_category": "low",
        "risk_vector": RISK_VECTOR_INPUT,
    }
    client = bitsight.Client(base_url=BASE_URL)
    mocker.patch.object(demisto, "params", return_value=inp_args)

    # Mock response with duplicate findings (same temporary_id)
    res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_incidents_response.json"))
    mocker.patch.object(BaseClient, "_http_request", return_value=res["response"])

    # Test with last_run containing already_fetched_findings with one of the duplicate IDs
    last_run_with_duplicates = {
        "first_fetch": "2025-10-05",
        "offset": 1,
        "already_fetched_findings": [
            "dummy_rolledup_observation_id_1-#-2022-03-27"
        ],  # dummy_rolledup_observation_id_1 is already fetched
    }

    last_run, events = bitsight.fetch_incidents(client=client, last_run=last_run_with_duplicates, params=inp_args)

    # Assertions
    # Should only create 1 event for dummy_rolledup_observation_id_2
    assert len(events) == 1
    assert events[0]["name"] == "Bitsight Finding - SSL Certificates - dummy.com - 2022-03-27"

    # Verify last_run is updated correctly
    assert last_run["first_fetch"] == "2025-10-05"
    assert last_run["offset"] == 3
    assert "dummy_rolledup_observation_id_1-#-2022-03-27" in last_run["already_fetched_findings"]
    assert "dummy_rolledup_observation_id_2-#-2022-03-27" in last_run["already_fetched_findings"]
    assert len(last_run["already_fetched_findings"]) == 2


def test_fetch_incidents_when_empty_response(mocker):
    """Tests for fetch_incidents when empty response is returned."""
    inp_args = {
        "guid": "123",
        "first_fetch": "2",
        "findings_min_severity": "severe",
        "findings_grade": DEFAULT_FINDINGS_GRADE,
        "findings_asset_category": "low",
        "risk_vector": RISK_VECTOR_INPUT,
    }
    client = bitsight.Client(base_url=BASE_URL)
    mocker.patch.object(demisto, "params", return_value=inp_args)
    mocker.patch.object(BaseClient, "_http_request", return_value={"count": 3, "results": []})

    last_run, events = bitsight.fetch_incidents(
        client=client, last_run={"first_fetch": "2022-03-27", "offset": 3}, params=inp_args
    )

    assert last_run["offset"] == 3
    assert last_run["first_fetch"] == "2022-03-27"


def test_fetch_incidents_with_invalid_affect_rating_reason(mocker, capfd):
    """Tests for fetch_incidents when invalid findings_affect_rating_reason is provided."""
    inp_args = {
        "guid": "123",
        "first_fetch": "2",
        "findings_min_severity": "severe",
        "findings_grade": DEFAULT_FINDINGS_GRADE,
        "findings_asset_category": "low",
        "risk_vector": RISK_VECTOR_INPUT,
        "findings_affect_rating_reason": "Invalid Reason",
    }
    client = bitsight.Client(base_url=BASE_URL)
    mocker.patch.object(demisto, "params", return_value=inp_args)

    with capfd.disabled():
        with pytest.raises(ValueError) as e:
            bitsight.fetch_incidents(client=client, last_run={}, params=inp_args)

        assert str(e.value) == bitsight.ERROR_MESSAGES["INVALID_SELECT"].format(
            "Invalid Reason", "Findings Affect Rating Reason", ", ".join(bitsight.VALID_AFFECT_RATING_REASON)
        )


@patch("BitSightForSecurityPerformanceManagement.return_results")  # noqa: F821
def test_test_module(mock_return, mocker):
    """Tests success for test_module."""
    # Positive Scenario
    res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/companies_guid_get_response.json"))
    mocker.patch.object(BaseClient, "_http_request", return_value=res["raw_response"])
    mocker.patch.object(demisto, "params", return_value={"apikey": "123"})
    mocker.patch.object(demisto, "command", return_value="test-module")

    bitsight.main()

    assert mock_return.call_args.args[0] == "ok"


def test_get_modified_remote_command_successful_retrieval(mocker, requests_mock):
    """
    Given:
    - A client object.
    - Mock data for company findings.

    When:
    - Mocking the 'dateparser.parse' function to return a specific datetime.
    - Mocking the 'get_company_findings' function to return findings data.

    Then:
    - Calling the 'get_modified_remote_data_command' function with the provided client and arguments.
    - Verify the function returns the expected modified incident IDs.
    """
    # Mock dateparser and get_company_findings
    client = bitsight.Client(base_url=BASE_URL)
    mocker.patch("BitSightForSecurityPerformanceManagement.dateparser.parse", return_value=datetime(2025, 9, 14, 10))
    # Mock the HTTP request using requests_mock
    mock_response = {
        "results": [
            {"temporary_id": "A9jq", "rolledup_observation_id": "dummy_id_1", "first_seen": "2025-09-14"},
            {"temporary_id": "A9jr", "rolledup_observation_id": "dummy_id_2", "first_seen": "2025-09-14"},
        ]
    }
    requests_mock.get(
        f"{BASE_URL}/v1/companies/test-guid/findings?limit=5000&sort=-last_seen&last_remediation_status_date_gte=2025-09-14",
        json=mock_response,
        status_code=200,
    )

    args = {"lastUpdate": "2025-09-14T10:00:00+00:00", "guid": "test-guid"}
    result = bitsight.get_modified_remote_data_command(client, args)

    # Assertions
    # Verify the HTTP request was made correctly
    assert requests_mock.called

    bitsight.dateparser.parse.assert_called_with("2025-09-14T10:00:00+00:00", settings={"TIMEZONE": "UTC"})
    assert "dummy_id_1-#-2025-09-14" in result.modified_incident_ids
    assert "dummy_id_2-#-2025-09-14" in result.modified_incident_ids


def test_get_modified_remote_command_with_empty_results(mocker):
    """
    Given:
    - A client object.

    When:
    - Mocking the 'dateparser.parse' function to return a specific datetime.
    - Mocking the 'get_company_findings' function to return empty results.

    Then:
    - Calling the 'get_modified_remote_data_command' function with the provided client and arguments.
    - Verify the function returns an empty list of modified incident IDs.
    """
    # Mock dateparser and get_company_findings
    client = bitsight.Client(base_url=BASE_URL)
    mocker.patch("BitSightForSecurityPerformanceManagement.dateparser.parse", return_value=datetime(2025, 9, 14, 10))
    mocker.patch.object(client, "get_company_findings", return_value={"results": []})

    args = {"lastUpdate": "2025-09-14T10:00:00+00:00", "guid": "test-guid"}
    result = bitsight.get_modified_remote_data_command(client, args)

    # Assertions
    bitsight.dateparser.parse.assert_called_with("2025-09-14T10:00:00+00:00", settings={"TIMEZONE": "UTC"})
    client.get_company_findings.assert_called_once_with(
        guid="test-guid",
        first_seen=None,
        last_seen=None,
        optional_params={"limit": 5000, "sort": "-last_seen", "last_remediation_status_date_gte": "2025-09-14"},
    )
    assert result.modified_incident_ids == []


def test_get_modified_remote_data_command_with_duplicate_findings(mocker):
    """
    Given:
    - A client object.
    - Mock data for company findings with duplicate IDs.

    When:
    - Mocking the 'dateparser.parse' function to return a specific datetime.
    - Mocking the 'get_company_findings' function to return findings with duplicate IDs.

    Then:
    - Calling the 'get_modified_remote_data_command' function with the provided client and arguments.
    - Verify the function returns unique modified incident IDs (duplicates filtered out).
    """
    # Mock dateparser and get_company_findings
    client = bitsight.Client(base_url=BASE_URL)
    mocker.patch("BitSightForSecurityPerformanceManagement.dateparser.parse", return_value=datetime(2025, 9, 14, 10))
    mocker.patch.object(
        client,
        "get_company_findings",
        return_value={
            "results": [
                {"temporary_id": "A9jq", "rolledup_observation_id": "dummy_id_1", "first_seen": "2025-09-14"},
                {"temporary_id": "A9jq", "rolledup_observation_id": "dummy_id_1", "first_seen": "2025-09-14"},  # Duplicate
                {"temporary_id": "A9jr", "rolledup_observation_id": "dummy_id_2", "first_seen": "2025-09-14"},
            ]
        },
    )

    args = {"lastUpdate": "2025-09-14T10:00:00+00:00", "guid": "test-guid"}
    result = bitsight.get_modified_remote_data_command(client, args)

    # Assertions
    client.get_company_findings.assert_called_once_with(
        guid="test-guid",
        first_seen=None,
        last_seen=None,
        optional_params={"limit": 5000, "sort": "-last_seen", "last_remediation_status_date_gte": "2025-09-14"},
    )
    # Should have unique IDs only
    assert len(result.modified_incident_ids) == 2
    assert "dummy_id_1-#-2025-09-14" in result.modified_incident_ids
    assert "dummy_id_2-#-2025-09-14" in result.modified_incident_ids


def test_get_remote_data_command_successful_retrieval(requests_mock, mocker):
    """
    Given:
    - A client object.
    - Valid mirror ID and arguments.

    When:
    - Mocking the 'get_company_findings' function to return finding data.
    - Mocking the 'get_remediations' function to return remediation status.
    - Mocking the 'get_finding_comments' function to return comments.

    Then:
    - Calling the 'get_remote_data_command' function with the provided client and arguments.
    - Verify the function returns the expected finding data and new entries.
    """
    client = bitsight.Client(base_url=BASE_URL)
    inp_args = {
        "guid": "123",
        "close_status_of_bitsight": "Closed",
        "open_status_of_bitsight": "Work In Progress",
    }
    mocker.patch.object(demisto, "params", return_value=inp_args)

    # Mock finding data
    finding_data = {
        "temporary_id": "A9jq",
        "affects_rating": True,
        "rolledup_observation_id": "dummy_id_1",
        "evidence_key": "test.com",
        "risk_vector": "ssl_certificates",
        "severity": 5.0,
        "first_seen": "2025-09-14",
    }

    # Mock HTTP requests using requests_mock
    # Mock get_company_findings request
    requests_mock.get(
        f"{BASE_URL}/v1/companies/test-guid/findings?rolledup_observation_id=dummy_id_1&sort=-last_seen&first_seen=2025-09-14",
        json={"results": [finding_data]},
        status_code=200,
    )

    # Mock get_remediations request
    remediations_url = (
        f"{BASE_URL}/ratings/v1/remediations?"
        "company_guid=test-guid&rolledup_observation_id=dummy_id_1&evidence_key=test.com&risk_vector=ssl_certificates"
    )
    requests_mock.get(
        remediations_url,
        json={"results": [{"status": {"value": "Open"}}]},
        status_code=200,
    )

    mock_comments_response = {
        "results": [
            {
                "guid": "comment-1",
                "message": "Test comment",
                "author": {"name": "Test User"},
                "created_time": "2025-09-14T10:00:00Z",
                "last_update_time": "2025-09-14T11:00:00Z",
            }
        ]
    }
    # Mock get_finding_comments request
    requests_mock.get(
        f"{BASE_URL}/ratings/v1/companies/test-guid/findings/dummy_id_1/comments",
        json=mock_comments_response,
        status_code=200,
    )

    args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"}

    result = bitsight.get_remote_data_command(client, args)

    # Assertions
    # Verify HTTP requests were made correctly
    assert requests_mock.call_count == 3  # get_company_findings, get_remediations, get_finding_comments

    # Verify the results
    assert result.mirrored_object["temporary_id"] == "A9jq"
    assert result.mirrored_object["remediation_status"] == "Open"
    assert len(result.entries) == 1
    assert "Test comment" in result.entries[0]["Contents"]
    assert result.entries[0]["Type"] == EntryType.NOTE


def test_get_remote_data_command_finding_not_found(mocker):
    """
    Given:
    - A client object.
    - Valid mirror ID but finding doesn't exist.

    When:
    - Mocking the 'get_company_findings' function to return empty results.

    Then:
    - Calling the 'get_remote_data_command' function should return empty GetRemoteDataResponse object.
    """
    client = bitsight.Client(base_url=BASE_URL)

    mocker.patch.object(client, "get_company_findings", return_value={"results": []})

    args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"}

    result = bitsight.get_remote_data_command(client, args)

    assert result.mirrored_object == {}
    assert result.entries == []


def test_get_remote_data_command_with_remediations_error(mocker):
    """
    Given:
    - A client object.
    - Valid finding data but remediations API fails.

    When:
    - Mocking the 'get_company_findings' function to return finding data.
    - Mocking the 'get_remediations' function to raise an exception.

    Then:
    - Function should continue without remediation data.
    """
    client = bitsight.Client(base_url=BASE_URL)

    finding_data = {
        "temporary_id": "A9jq",
        "affects_rating": True,
        "rolledup_observation_id": "dummy_id_1",
        "evidence_key": "test.com",
        "risk_vector": "ssl_certificates",
    }

    mocker.patch.object(client, "get_company_findings", return_value={"results": [finding_data]})

    mocker.patch.object(client, "get_remediations", side_effect=DemistoException("API Error"))
    mocker.patch.object(client, "get_finding_comments", return_value={"results": []})

    args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"}

    result = bitsight.get_remote_data_command(client, args)

    # Should not have remediation_status due to error
    assert "remediation_status" not in result.mirrored_object
    assert result.mirrored_object["temporary_id"] == "A9jq"


def test_get_remote_data_command_with_comments_error(mocker):
    """
    Given:
    - A client object.
    - Valid finding data but comments API fails.

    When:
    - Mocking the 'get_company_findings' function to return finding data.
    - Mocking the 'get_finding_comments' function to raise an exception.

    Then:
    - Function should continue without comment entries.
    """
    client = bitsight.Client(base_url=BASE_URL)
    inp_args = {
        "guid": "123",
        "close_status_of_bitsight": "Closed",
        "open_status_of_bitsight": "Work In Progress",
    }
    mocker.patch.object(demisto, "params", return_value=inp_args)

    finding_data = {
        "temporary_id": "A9jq",
        "affects_rating": True,
        "rolledup_observation_id": "dummy_id_1",
        "evidence_key": "test.com",
        "risk_vector": "ssl_certificates",
    }

    mocker.patch.object(client, "get_company_findings", return_value={"results": [finding_data]})

    mocker.patch.object(client, "get_remediations", return_value={"results": []})
    mocker.patch.object(client, "get_finding_comments", side_effect=DemistoException("API Error"))

    args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"}

    result = bitsight.get_remote_data_command(client, args)

    # Should have no comment entries due to error
    assert len(result.entries) == 0
    assert result.mirrored_object["temporary_id"] == "A9jq"


def test_get_remote_data_command_with_old_comments_filtered(mocker):
    """
    Given:
    - A client object.
    - Comments that are older than lastUpdate timestamp.

    When:
    - Mocking functions to return comments with timestamps before lastUpdate.

    Then:
    - Old comments should be filtered out.
    """
    client = bitsight.Client(base_url=BASE_URL)
    inp_args = {
        "guid": "123",
        "close_status_of_bitsight": "Closed",
        "open_status_of_bitsight": "Work In Progress",
        "close_active_incident": False,
        "reopen_closed_incident": False,
    }
    mocker.patch.object(demisto, "params", return_value=inp_args)

    finding_data = {
        "temporary_id": "A9jq",
        "affects_rating": True,
        "rolledup_observation_id": "dummy_id_1",
        "evidence_key": "test.com",
        "risk_vector": "ssl_certificates",
    }

    mocker.patch.object(client, "get_company_findings", return_value={"results": [finding_data]})

    mocker.patch.object(client, "get_remediations", return_value={"results": []})

    # Comments with timestamps before lastUpdate (2025-09-14T10:00:00Z)
    mocker.patch.object(
        client,
        "get_finding_comments",
        return_value={
            "results": [
                {
                    "guid": "old-comment",
                    "message": "Old comment",
                    "author": {"name": "Test User"},
                    "created_time": "2025-09-13T10:00:00Z",  # Before lastUpdate
                    "last_update_time": "2025-09-13T11:00:00Z",
                },
                {
                    "guid": "old-comment-2",
                    "message": "Old comment",
                    "author": {"name": "Test User"},
                    "created_time": "2025-09-13T10:00:00Z",  # Before lastUpdate
                    "last_update_time": None,
                },
                {
                    "guid": "new-comment",
                    "message": "New comment",
                    "author": {"name": "Test User"},
                    "created_time": "2025-09-14T11:00:00Z",  # After lastUpdate
                    "last_update_time": None,
                },
            ]
        },
    )

    args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"}

    result = bitsight.get_remote_data_command(client, args)

    # Should only have the new comment
    assert len(result.entries) == 1
    assert "New comment" in result.entries[0]["Contents"]
    assert result.entries[0]["Type"] == EntryType.NOTE


def test_get_remote_data_command_with_invalid_mirror_id(mocker):
    """
    Given:
    - A client object.
    - Invalid mirror ID format.

    When:
    - Providing mirror ID without proper format.

    Then:
    - Function should handle gracefully with empty identifiers.
    """
    client = bitsight.Client(base_url=BASE_URL)

    mocker.patch.object(client, "get_company_findings", return_value={"results": []})

    args = {
        "id": "invalid_id",  # Missing second part after split
        "lastUpdate": "2025-09-14T10:00:00Z",
        "guid": "test-guid",
    }

    result = bitsight.get_remote_data_command(client, args)

    assert result.mirrored_object == {}
    assert result.entries == []


@pytest.mark.parametrize(
    "close_active_incident,expected_entries",
    [
        (True, 1),  # Should close incident when enabled
        (False, 0),  # Should NOT close incident when disabled
    ],
)
def test_get_remote_data_command_with_incident_closure(mocker, close_active_incident, expected_entries):
    """
    Given:
    - A client object.
    - Remediation status matches close status.
    - Finding is in processed findings list.
    - close_active_incident parameter varies.

    When:
    - Calling get_remote_data_command with matching close status.

    Then:
    - Incident closure behavior should depend on checkbox parameter.
    """
    client = bitsight.Client(base_url=BASE_URL)
    inp_args = {
        "guid": "123",
        "close_status_of_bitsight": "Resolved",
        "open_status_of_bitsight": "Open",
        "close_active_incident": close_active_incident,
        "reopen_closed_incident": False,
    }
    mocker.patch.object(demisto, "params", return_value=inp_args)
    mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": ["dummy_id_1-#-2025-09-14"]})
    mocker.patch.object(demisto, "setIntegrationContext")

    finding_data = {
        "temporary_id": "A9jq",
        "affects_rating": True,
        "rolledup_observation_id": "dummy_id_1",
        "evidence_key": "test.com",
        "risk_vector": "ssl_certificates",
    }

    mocker.patch.object(client, "get_company_findings", return_value={"results": [finding_data]})
    mocker.patch.object(client, "get_remediations", return_value={"results": [{"status": {"value": "Resolved"}}]})
    mocker.patch.object(client, "get_finding_comments", return_value={"results": []})

    args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"}

    result = bitsight.get_remote_data_command(client, args)

    # Verify behavior based on parameter
    assert len(result.entries) == expected_entries
    if expected_entries > 0:
        assert result.entries[0]["Contents"]["dbotIncidentClose"] is True
    assert result.mirrored_object["remediation_status"] == "Resolved"


@pytest.mark.parametrize(
    "reopen_closed_incident,expected_entries",
    [
        (True, 1),  # Should reopen incident when enabled
        (False, 0),  # Should NOT reopen incident when disabled
    ],
)
def test_get_remote_data_command_with_incident_reopen(mocker, reopen_closed_incident, expected_entries):
    """
    Given:
    - A client object.
    - Remediation status matches open status.
    - Finding is NOT in processed findings list.
    - reopen_closed_incident parameter varies.

    When:
    - Calling get_remote_data_command with matching open status.

    Then:
    - Incident reopen behavior should depend on checkbox parameter.
    """
    client = bitsight.Client(base_url=BASE_URL)
    inp_args = {
        "guid": "123",
        "close_status_of_bitsight": "Resolved",
        "open_status_of_bitsight": "Open",
        "close_active_incident": False,
        "reopen_closed_incident": reopen_closed_incident,
    }
    mocker.patch.object(demisto, "params", return_value=inp_args)
    mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": []})
    mocker.patch.object(demisto, "setIntegrationContext")

    finding_data = {
        "temporary_id": "A9jq",
        "affects_rating": True,
        "rolledup_observation_id": "dummy_id_1",
        "evidence_key": "test.com",
        "risk_vector": "ssl_certificates",
    }

    mocker.patch.object(client, "get_company_findings", return_value={"results": [finding_data]})
    mocker.patch.object(client, "get_remediations", return_value={"results": [{"status": {"value": "Open"}}]})
    mocker.patch.object(client, "get_finding_comments", return_value={"results": []})

    args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"}

    result = bitsight.get_remote_data_command(client, args)

    # Verify behavior based on parameter
    assert len(result.entries) == expected_entries
    if expected_entries > 0:
        assert result.entries[0]["Contents"]["dbotIncidentReopen"] is True
    assert result.mirrored_object["remediation_status"] == "Open"


def test_update_remote_system_command_incident_closed(mocker):
    """
    Given:
    - A client object.
    - Incident status is DONE (closed).
    - Incident has changed.

    When:
    - Calling update_remote_system_command with closed incident.

    Then:
    - Should call update_external_status with close_status_of_bitsight.
    - Should return the remote incident ID.
    """
    client = bitsight.Client(base_url=BASE_URL)

    # Mock UpdateRemoteSystemArgs
    mock_parsed_args = mocker.MagicMock()
    mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14"
    mock_parsed_args.data = {
        "bitsightrolledupobservationid": "obs-123",
        "bitsightriskvector": "ssl_certificates",
        "bitsightevidencekey": "test.com",
        "id": "xsoar-456",
    }
    mock_parsed_args.inc_status = bitsight.IncidentStatus.DONE
    mock_parsed_args.delta = {}
    mock_parsed_args.incident_changed = True
    mock_parsed_args.entries = []

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": ["dummy_id_1-#-2025-09-14"]})
    mocker.patch.object(client, "update_external_status")
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123")

    args = {"guid": "company-guid", "user_email": "test@example.com"}
    close_status = "Closed"
    open_status = "Open"

    result = bitsight.update_remote_system_command(client, args, close_status, open_status)

    # Assertions
    expected_body = {
        "rolledup_observation_id": "obs-123",
        "evidence_key": "test.com",
        "risk_vector": "ssl_certificates",
        "status": {"value": "Closed", "public": False},
    }
    client.update_external_status.assert_called_once_with(company_guid="company-guid", body=expected_body)
    assert result == "dummy_id_1-#-2025-09-14"


def test_update_remote_system_command_with_comments(mocker):
    """
    Given:
    - A client object.
    - New entries (comments) to mirror to BitSight.
    - Existing comments in BitSight for thread_guid.

    When:
    - Calling update_remote_system_command with new entries.

    Then:
    - Should call get_finding_comments to get existing comments.
    - Should call create_finding_comment for each new entry.
    - Should format comment content properly with XSOAR metadata.
    """
    client = bitsight.Client(base_url=BASE_URL)

    # Mock UpdateRemoteSystemArgs with new entries
    mock_parsed_args = mocker.MagicMock()
    mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14"
    mock_parsed_args.data = {
        "bitsightrolledupobservationid": "obs-456",
        "bitsightriskvector": "ssl_certificates",
        "bitsightevidencekey": "test.com",
        "id": "xsoar-789",
    }
    mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE
    mock_parsed_args.delta = {}
    mock_parsed_args.incident_changed = True
    mock_parsed_args.entries = [
        {
            "id": "entry-1",
            "type": "note",
            "contents": "This is a test comment from XSOAR",
            "user": "admin",
        },
        {
            "id": "entry-2",
            "type": "note",
            "contents": "Another comment\nwith multiple lines",
            "user": "admin",
        },
    ]

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": ["dummy_id_1-#-2025-09-14"]})
    mocker.patch.object(demisto, "setIntegrationContext")

    # Mock existing comments response
    existing_comments = {"results": [{"thread_guid": "thread-123", "guid": "comment-1", "message": "Existing comment"}]}
    mocker.patch.object(client, "get_finding_comments", return_value=existing_comments)
    mocker.patch.object(client, "create_finding_comment")
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123")

    args = {"guid": "company-guid", "user_email": "test@example.com"}
    close_status = "Closed"
    open_status = "Open"

    result = bitsight.update_remote_system_command(client, args, close_status, open_status)

    # Verify get_finding_comments was called
    client.get_finding_comments.assert_called_once_with(company_guid="company-guid", rolledup_observation_id="obs-456")

    # Verify create_finding_comment was called for each entry
    assert client.create_finding_comment.call_count == 2

    # Check first comment call
    expected_message_1 = (
        "[Mirrored From XSOAR] XSOAR Incident ID: xsoar-789\n\nNote: This is a test comment from XSOAR\n\nAdded By: admin"
    )
    first_call_args = client.create_finding_comment.call_args_list[0]
    assert first_call_args.kwargs["company_guid"] == "company-guid"
    assert first_call_args.kwargs["rolledup_observation_id"] == "obs-456"
    assert first_call_args.kwargs["thread_guid"] == "thread-123"
    # Check the body contains the expected comment structure
    body_1 = first_call_args.kwargs["body"]
    assert body_1["author_guid"] == "user-123"
    assert body_1["message"] == expected_message_1
    assert body_1["public"] is False

    # Check second comment call
    expected_message_2 = (
        "[Mirrored From XSOAR] XSOAR Incident ID: xsoar-789\n\nNote: Another comment\n\nwith multiple lines\n\nAdded By: admin"
    )
    second_call_args = client.create_finding_comment.call_args_list[1]
    assert second_call_args.kwargs["company_guid"] == "company-guid"
    assert second_call_args.kwargs["rolledup_observation_id"] == "obs-456"
    assert second_call_args.kwargs["thread_guid"] == "thread-123"
    # Check the body contains the expected comment structure
    body_2 = second_call_args.kwargs["body"]
    assert body_2["author_guid"] == "user-123"
    assert body_2["message"] == expected_message_2
    assert body_2["public"] is False

    assert result == "dummy_id_1-#-2025-09-14"


def test_update_remote_system_command_with_comments_no_existing_thread(mocker):
    """
    Given:
    - A client object.
    - New entries to mirror to BitSight.
    - No existing comments in BitSight (empty thread_guid).

    When:
    - Calling update_remote_system_command with new entries.

    Then:
    - Should call create_finding_comment with empty thread_guid.
    """
    client = bitsight.Client(base_url=BASE_URL)

    # Mock UpdateRemoteSystemArgs with new entries
    mock_parsed_args = mocker.MagicMock()
    mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14"
    mock_parsed_args.data = {
        "bitsightrolledupobservationid": "obs-789",
        "bitsightriskvector": "ssl_certificates",
        "bitsightevidencekey": "test.com",
        "id": "xsoar-123",
    }
    mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE
    mock_parsed_args.delta = {}
    mock_parsed_args.incident_changed = False
    mock_parsed_args.entries = [
        {
            "id": "entry-1",
            "type": "note",
            "contents": "First comment in new thread",
            "user": "dbot",
        }
    ]

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": []})
    mocker.patch.object(demisto, "setIntegrationContext")

    # Mock empty comments response
    mocker.patch.object(client, "get_finding_comments", return_value={"results": []})
    mocker.patch.object(client, "create_finding_comment")
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123")

    args = {"guid": "company-guid", "user_email": "test@example.com"}
    close_status = "Closed"
    open_status = "Open"

    result = bitsight.update_remote_system_command(client, args, close_status, open_status)

    # Verify create_finding_comment was called with empty thread_guid
    expected_message = "[Mirrored From XSOAR] XSOAR Incident ID: xsoar-123\n\nNote: First comment in new thread\n\nAdded By: dbot"
    call_args = client.create_finding_comment.call_args
    assert call_args.kwargs["company_guid"] == "company-guid"
    assert call_args.kwargs["rolledup_observation_id"] == "obs-789"
    assert call_args.kwargs["thread_guid"] == ""  # Empty thread_guid for new thread
    # Check the body contains the expected comment structure
    body = call_args.kwargs["body"]
    assert body["comments"][0]["author_guid"] == "user-123"
    assert body["comments"][0]["message"] == expected_message
    assert body["comments"][0]["public"] is False

    assert result == "dummy_id_1-#-2025-09-14"


def test_update_remote_system_command_incident_active_no_delta(mocker):
    """
    Given:
    - A client object.
    - Incident status is ACTIVE.
    - No delta (no changes).
    - Incident has changed.

    When:
    - Calling update_remote_system_command with active incident and no delta.

    Then:
    - Should call update_external_status with open_status_of_bitsight.
    - Should return the remote incident ID.
    """
    client = bitsight.Client(base_url=BASE_URL)

    # Mock UpdateRemoteSystemArgs
    mock_parsed_args = mocker.MagicMock()
    mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14"
    mock_parsed_args.data = {
        "bitsightrolledupobservationid": "obs-456",
        "bitsightriskvector": "open_ports",
        "bitsightevidencekey": "0.0.0.1",
        "id": "xsoar-789",
    }
    mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE
    mock_parsed_args.delta = {}  # No changes
    mock_parsed_args.incident_changed = True
    mock_parsed_args.entries = []

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch.object(client, "update_external_status")
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123")

    args = {"guid": "company-guid", "user_email": "test@example.com"}
    close_status = "Closed"
    open_status = "Open"

    result = bitsight.update_remote_system_command(client, args, close_status, open_status)

    # Assertions
    expected_body = {
        "rolledup_observation_id": "obs-456",
        "evidence_key": "0.0.0.1",
        "risk_vector": "open_ports",
        "status": {"value": "Open", "public": False},
    }
    client.update_external_status.assert_called_once_with(company_guid="company-guid", body=expected_body)
    assert result == "dummy_id_1-#-2025-09-14"


def test_update_remote_system_command_incident_active_with_delta(mocker):
    """
    Given:
    - A client object.
    - Incident status is ACTIVE.
    - Has delta (changes present).
    - Incident has changed.

    When:
    - Calling update_remote_system_command with active incident and delta.

    Then:
    - Should NOT call update_external_status (should_update_status is False).
    - Should return the remote incident ID.
    """
    client = bitsight.Client(base_url=BASE_URL)

    # Mock UpdateRemoteSystemArgs
    mock_parsed_args = mocker.MagicMock()
    mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14"
    mock_parsed_args.data = {
        "bitsightrolledupobservationid": "obs-789",
        "bitsightriskvector": "malware",
        "bitsightevidencekey": "malware.com",
        "id": "xsoar-101",
    }
    mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE
    mock_parsed_args.delta = {"severity": "high"}  # Has changes
    mock_parsed_args.incident_changed = True
    mock_parsed_args.entries = []

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch.object(client, "update_external_status")
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123")

    args = {"guid": "company-guid", "user_email": "test@example.com"}
    close_status = "Closed"
    open_status = "Open"

    result = bitsight.update_remote_system_command(client, args, close_status, open_status)

    # Assertions - should NOT call update_external_status
    client.update_external_status.assert_not_called()
    assert result == "dummy_id_1-#-2025-09-14"


def test_update_remote_system_command_incident_not_changed(mocker):
    """
    Given:
    - A client object.
    - Incident status is DONE.
    - Incident has NOT changed.

    When:
    - Calling update_remote_system_command with unchanged incident.

    Then:
    - Should NOT call update_external_status (incident_changed is False).
    - Should return the remote incident ID.
    """
    client = bitsight.Client(base_url=BASE_URL)

    # Mock UpdateRemoteSystemArgs
    mock_parsed_args = mocker.MagicMock()
    mock_parsed_args.remote_incident_id = "remote-999"
    mock_parsed_args.data = {
        "bitsightrolledupobservationid": "obs-999",
        "bitsightriskvector": "ssl_certificates",
        "bitsightevidencekey": "test.com",
        "id": "xsoar-999",
    }
    mock_parsed_args.inc_status = bitsight.IncidentStatus.DONE
    mock_parsed_args.delta = {}
    mock_parsed_args.incident_changed = False  # Not changed
    mock_parsed_args.entries = []

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch.object(client, "update_external_status")
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123")

    args = {"guid": "company-guid", "user_email": "test@example.com"}
    close_status = "Closed"
    open_status = "Open"

    result = bitsight.update_remote_system_command(client, args, close_status, open_status)

    # Assertions - should NOT call update_external_status
    client.update_external_status.assert_not_called()
    assert result == "remote-999"


def test_update_remote_system_command_with_missing_data_fields(mocker):
    """
    Given:
    - A client object.
    - Missing some data fields in parsed_args.data.

    When:
    - Calling update_remote_system_command with missing fields.

    Then:
    - Should handle missing fields gracefully with empty strings.
    - Should call update_external_status with empty values.
    """
    client = bitsight.Client(base_url=BASE_URL)

    # Mock UpdateRemoteSystemArgs with missing fields
    mock_parsed_args = mocker.MagicMock()
    mock_parsed_args.remote_incident_id = "remote-empty"
    mock_parsed_args.data = {
        "id": "xsoar-empty"
        # Missing bitsightrolledupobservationid, bitsightriskvector, bitsightevidencekey
    }
    mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE
    mock_parsed_args.delta = {}
    mock_parsed_args.incident_changed = True
    mock_parsed_args.entries = []

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch.object(client, "update_external_status")
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123")

    args = {"guid": "company-guid", "user_email": "test@example.com"}
    close_status = "Closed"
    open_status = "Open"

    result = bitsight.update_remote_system_command(client, args, close_status, open_status)

    # Assertions - should handle missing fields with empty strings
    expected_body = {
        "rolledup_observation_id": "",
        "evidence_key": "",
        "risk_vector": "",
        "status": {"value": "Open", "public": False},
    }
    client.update_external_status.assert_called_once_with(company_guid="company-guid", body=expected_body)
    assert result == "remote-empty"


def test_update_remote_system_command_api_error(mocker):
    """
    Given:
    - A client object.
    - update_external_status raises an exception.

    When:
    - Calling update_remote_system_command and API fails.

    Then:
    - Should propagate the exception.
    """
    client = bitsight.Client(base_url=BASE_URL)

    # Mock UpdateRemoteSystemArgs
    mock_parsed_args = mocker.MagicMock()
    mock_parsed_args.remote_incident_id = "remote-error"
    mock_parsed_args.data = {
        "bitsightrolledupobservationid": "obs-error",
        "bitsightriskvector": "ssl_certificates",
        "bitsightevidencekey": "test.com",
        "id": "xsoar-error",
    }
    mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE
    mock_parsed_args.delta = {}
    mock_parsed_args.incident_changed = True
    mock_parsed_args.entries = []

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123")
    mocker.patch.object(client, "update_external_status", side_effect=DemistoException("API Error"))

    args = {"guid": "company-guid", "user_email": "test@example.com"}
    close_status = "Closed"
    open_status = "Open"

    # Should raise the exception
    with pytest.raises(DemistoException):
        bitsight.update_remote_system_command(client, args, close_status, open_status)


def test_update_remote_system_command_incident_reopen(mocker):
    """
    Given:
    - A client object.
    - Incident status is ACTIVE and incident has been reopened after being closed.
    - Delta contains closingUserId="" and runStatus="" indicating reopen.

    When:
    - Calling update_remote_system_command with reopened incident.

    Then:
    - Should call update_external_status with open_status_of_bitsight.
    - Should return the remote incident ID.
    """
    client = bitsight.Client(base_url=BASE_URL)

    # Mock UpdateRemoteSystemArgs
    mock_parsed_args = mocker.MagicMock()
    mock_parsed_args.remote_incident_id = "remote-reopen-123"
    mock_parsed_args.data = {
        "bitsightrolledupobservationid": "obs-reopen-456",
        "bitsightriskvector": "web_application_security",
        "bitsightevidencekey": "example.com",
        "id": "xsoar-reopen-789",
    }
    mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE
    mock_parsed_args.delta = {"closingUserId": "", "runStatus": ""}  # Indicates incident reopen
    mock_parsed_args.incident_changed = True
    mock_parsed_args.entries = []

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch.object(client, "update_external_status")
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123")

    args = {"guid": "company-guid-reopen"}
    close_status = "Risk Accepted"
    open_status = "Open"

    result = bitsight.update_remote_system_command(client, args, close_status, open_status)

    # Assertions
    expected_body = {
        "rolledup_observation_id": "obs-reopen-456",
        "evidence_key": "example.com",
        "risk_vector": "web_application_security",
        "status": {"value": "Open", "public": False},
    }
    client.update_external_status.assert_called_once_with(company_guid="company-guid-reopen", body=expected_body)
    assert result == "remote-reopen-123"


def test_get_current_user_guid_success_with_valid_email(mocker, requests_mock):
    """
    Given:
    - A client object and valid user email address.

    When:
    - Calling get_current_user_guid with valid email.

    Then:
    - Should return the user GUID from API response and cache it.
    """
    from BitSightForSecurityPerformanceManagement import get_current_user_guid

    client = bitsight.Client(base_url=BASE_URL)
    user_email = "test@example.com"
    expected_guid = "user-guid-123"

    # Mock HTTP request using requests_mock
    mock_response = {"results": [{"guid": expected_guid, "email": user_email}]}
    requests_mock.get(f"{BASE_URL}/ratings/v2/users?email={user_email}", json=mock_response, status_code=200)

    mocker.patch.object(demisto, "getIntegrationContext", return_value={"users_guids": {}})
    mocker.patch.object(demisto, "setIntegrationContext")

    result = get_current_user_guid(client, user_email)

    assert result == expected_guid
    assert requests_mock.call_count == 1


def test_test_module_with_mirroring_missing_user_email(requests_mock):
    """
    Given:
    - Integration parameters with mirroring enabled but missing user email.

    When:
    - Running test_module function.

    Then:
    - Should raise ValueError with USER_EMAIL_REQUIRED message.
    """
    client = bitsight.Client(base_url=BASE_URL)

    params = {"apikey": "test-key", "mirror_direction": "Outgoing", "user_email": "", "isFetch": False}

    # Mock HTTP request for companies
    companies_response = {"companies": [{"guid": "valid-guid-123", "name": "Test Company"}]}
    requests_mock.get(f"{BASE_URL}/v1/companies", json=companies_response, status_code=200)

    with pytest.raises(ValueError) as e:
        bitsight.test_module(client, params)

    assert str(e.value) == bitsight.ERROR_MESSAGES["USER_EMAIL_REQUIRED"]


def test_test_module_with_mirroring_invalid_user_email(mocker, requests_mock):
    """
    Given:
    - Integration parameters with mirroring enabled and invalid user email.

    When:
    - Running test_module function with non-existent email.

    Then:
    - Should raise ValueError with USER_GUID_NOT_FOUND message.
    """
    client = bitsight.Client(base_url=BASE_URL)

    params = {"apikey": "test-key", "mirror_direction": "Outgoing", "user_email": "invalid@example.com", "isFetch": False}

    # Mock HTTP requests using requests_mock
    companies_response = {"companies": [{"guid": "valid-guid-123", "name": "Test Company"}]}
    user_response = {"results": []}  # No user found

    requests_mock.get(f"{BASE_URL}/v1/companies", json=companies_response, status_code=200)
    requests_mock.get(f"{BASE_URL}/ratings/v2/users?email=invalid@example.com", json=user_response, status_code=200)

    mocker.patch.object(demisto, "getIntegrationContext", return_value={"users_guids": {}})
    mocker.patch.object(demisto, "setIntegrationContext")

    with pytest.raises(ValueError) as e:
        bitsight.test_module(client, params)

    assert str(e.value) == bitsight.ERROR_MESSAGES["USER_GUID_NOT_FOUND"]


def test_update_remote_system_command_adds_closing_note_existing_thread(mocker):
    """
    Given:
    - Mirroring arguments that mark the incident as closed with closing details.
    - Existing BitSight comment thread for the finding.

    When:
    - update_remote_system_command is executed.

    Then:
    - A closing note is appended to the existing thread and the remote ID is returned.
    """
    from BitSightForSecurityPerformanceManagement import update_remote_system_command

    client = bitsight.Client(base_url=BASE_URL)
    mock_parsed_args = mocker.MagicMock()
    closing_args = {
        "bitsightrolledupobservationid": "rolledup-123",
        "bitsightriskvector": "web_application_security",
        "bitsightevidencekey": "example.com",
        "id": "xsoar-incident-1",
        "closeNotes": "resolved",
        "closeReason": "Resolved",
        "closingUserId": "admin",
    }

    mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14"
    mock_parsed_args.data = closing_args
    mock_parsed_args.inc_status = bitsight.IncidentStatus.DONE
    mock_parsed_args.delta = {"closingUserId": "admin"}
    mock_parsed_args.incident_changed = True
    mock_parsed_args.entries = []

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123")
    mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": ["dummy_id_1-#-2025-09-14"]})
    mocker.patch.object(demisto, "setIntegrationContext")
    mocker.patch.object(client, "update_external_status")
    mocker.patch.object(client, "get_finding_comments", return_value={"results": [{"thread_guid": "thread-456"}]})
    mock_create_comment = mocker.patch.object(client, "create_finding_comment")

    args = {"guid": "company-guid", "user_email": "user@example.com"}

    result = update_remote_system_command(client, args, "Resolved", "Open")

    closing_note = (
        "[Mirrored From XSOAR] XSOAR Incident ID: xsoar-incident-1\n\n"
        "Close Reason: Resolved\n\n"
        "Closed By: admin\n\n"
        "Close Notes: resolved"
    )
    mock_create_comment.assert_called_once_with(
        company_guid="company-guid",
        rolledup_observation_id="rolledup-123",
        thread_guid="thread-456",
        body={"author_guid": "user-123", "message": closing_note, "public": False},
    )
    assert result == "dummy_id_1-#-2025-09-14"


def test_update_remote_system_command_adds_closing_note_new_thread(mocker):
    """
    Given:
    - Mirroring arguments that mark the incident as closed with closing details.
    - No existing BitSight comment thread for the finding.

    When:
    - update_remote_system_command is executed.

    Then:
    - A closing note is created in a new thread and the remote ID is returned.
    """
    from BitSightForSecurityPerformanceManagement import update_remote_system_command

    client = bitsight.Client(base_url=BASE_URL)
    mock_parsed_args = mocker.MagicMock()
    closing_args = {
        "bitsightrolledupobservationid": "rolledup-456",
        "bitsightriskvector": "malware",
        "bitsightevidencekey": "malware.example.com",
        "id": "xsoar-incident-2",
        "closeNotes": "handled",
        "closeReason": "Mitigated",
        "closingUserId": "analyst",
    }

    mock_parsed_args.remote_incident_id = "dummy_id_2-#-2025-11-25"
    mock_parsed_args.data = closing_args
    mock_parsed_args.inc_status = bitsight.IncidentStatus.DONE
    mock_parsed_args.delta = {"closingUserId": "analyst"}
    mock_parsed_args.incident_changed = True
    mock_parsed_args.entries = []

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-456")
    mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": ["dummy_id_2-#-2025-11-25"]})
    mocker.patch.object(demisto, "setIntegrationContext")
    mocker.patch.object(client, "update_external_status")
    mocker.patch.object(client, "get_finding_comments", return_value={})
    mock_create_comment = mocker.patch.object(client, "create_finding_comment")

    args = {"guid": "company-guid", "user_email": "user@example.com"}

    result = update_remote_system_command(client, args, "Resolved", "Open")

    closing_note = (
        "[Mirrored From XSOAR] XSOAR Incident ID: xsoar-incident-2\n\n"
        "Close Reason: Mitigated\n\n"
        "Closed By: analyst\n\n"
        "Close Notes: handled"
    )
    mock_create_comment.assert_called_once_with(
        company_guid="company-guid",
        rolledup_observation_id="rolledup-456",
        thread_guid="",
        body={"comments": [{"author_guid": "user-456", "message": closing_note, "public": False}]},
    )
    assert result == "dummy_id_2-#-2025-11-25"


def test_update_remote_system_command_does_not_add_closing_note_when_status_not_done(mocker):
    """
    Given:
    - Mirroring arguments where closing user is provided but the incident status is not DONE.

    When:
    - update_remote_system_command is executed.

    Then:
    - No closing note is mirrored to BitSight.
    """
    from BitSightForSecurityPerformanceManagement import update_remote_system_command

    client = bitsight.Client(base_url=BASE_URL)
    mock_parsed_args = mocker.MagicMock()
    closing_args = {
        "bitsightrolledupobservationid": "rolledup-789",
        "bitsightriskvector": "botnet_infections",
        "bitsightevidencekey": "bot.example.com",
        "id": "xsoar-incident-3",
        "closeNotes": "monitor",
        "closeReason": "Monitoring",
        "closingUserId": "responder",
    }

    mock_parsed_args.remote_incident_id = "Bitsight Finding - rolledup-789"
    mock_parsed_args.data = closing_args
    mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE
    mock_parsed_args.delta = {"closingUserId": "responder"}
    mock_parsed_args.incident_changed = True
    mock_parsed_args.entries = []

    mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args)
    mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-789")
    mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": []})
    mocker.patch.object(demisto, "setIntegrationContext")
    mocker.patch.object(client, "update_external_status")
    mock_get_comments = mocker.patch.object(client, "get_finding_comments")
    mock_create_comment = mocker.patch.object(client, "create_finding_comment")

    args = {"guid": "company-guid", "user_email": "user@example.com"}

    result = update_remote_system_command(client, args, "Resolved", "Open")

    mock_get_comments.assert_not_called()
    mock_create_comment.assert_not_called()
    assert result == "Bitsight Finding - rolledup-789"