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.

"""Main file for BitSightForSecurityPerformanceManagement Integration."""

from datetime import datetime

import demistomock as demisto  # noqa: F401
import requests
import urllib3
from CommonServerPython import *  # noqa: F401

"""CONSTANTS"""
BITSIGHT_DATE_TIME_FORMAT = "%Y-%m-%d"
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR
DEFAULT_FIRST_FETCH_DAYS = 3
DEFAULT_FETCH_LIMIT = 25
MAX_FETCH_LIMIT = 200
BASE_URL = "https://api.bitsighttech.com"
MAX_LIMIT = 1000
DEFAULT_LIMIT = 100
DEFAULT_OFFSET = 0
MAX_FINDINGS_MIRRORING_LIMIT = 5000
OUTGOING_MIRROR_DIRECTION = "outgoing"
INCOMING_AND_OUTGOING_MIRROR_DIRECTION = "incoming and outgoing"
DEFAULT_CLOSE_STATUS_OF_BITSIGHT = "Resolved"
DEFAULT_OPEN_STATUS_OF_BITSIGHT = "Open"

ERROR_MESSAGES = {
    "GUID_REQUIRED": "Must provide a GUID.",
    "GUID_NOT_FETCHED": "Unable to fetch GUID.",
    "GUID_NOT_AVAILABLE": "Provided 'Company's GUID' is not available/valid."
    ' Please input a GUID retrieved using the command "bitsight-companies-guid-get".',
    "INVALID_SELECT": "'{}' is an invalid value for '{}'. Value must be in {}.",
    "INVALID_MAX_FETCH": f"Parameter 'Max Fetch' is not a valid number. Please provide a number in range 1 to {MAX_FETCH_LIMIT}.",
    "NEGATIVE_FIRST_FETCH": "Parameter 'First fetch time in days' should be a number greater than or equal to 0.",
    "LIMIT_GREATER_THAN_ALLOWED": f"Argument 'limit' should be a number less than or equal to {MAX_LIMIT}.",
    "USER_EMAIL_REQUIRED": "User email address is required for outgoing mirroring.",
    "USER_GUID_NOT_FOUND": "User GUID not found for the provided user email.",
}

SEVERITY_MAPPING = {"minor": 1, "moderate": 4, "material": 7, "severe": 9}

ASSET_CATEGORY_MAPPING = {
    "low": "low,medium,high,critical",
    "medium": "medium,high,critical",
    "high": "high,critical",
    "critical": "critical",
}

VALID_AFFECT_RATING_REASON = ["Yes", "No: Grace Period", "No: Incubation Period"]
AFFECT_RATING_REASON_MAPPING = {
    "yes": "AFFECTS_RATING",
    "no: grace period": "GRACE_PERIOD",
    "no: incubation period": "INCUBATION_PERIOD",
}

RISK_VECTOR_MAPPING = {
    "web application headers": "application_security",
    "botnet infections": "botnet_infections",
    "breaches": "data_breaches",
    "desktop software": "desktop_software",
    "dkim": "dkim",
    "dnssec": "dnssec",
    "file sharing": "file_sharing",
    "insecure systems": "insecure_systems",
    "malware servers": "malware_servers",
    "mobile app publications": "mobile_app_publications",
    "mobile application security": "mobile_application_security",
    "mobile software": "mobile_software",
    "open ports": "open_ports",
    "patching cadence": "patching_cadence",
    "potentially exploited": "potentially_exploited",
    "server software": "server_software",
    "spam propagation": "spam_propagation",
    "spf": "SPF",
    "ssl certificates": "ssl_certificates",
    "ssl configurations": "ssl_configurations",
    "unsolicited communications": "unsolicited_comm",
    "web application security": "web_appsec",
    "dmarc": "dmarc",
}
MIRROR_DIRECTION = {
    "Incoming": "In",
    "Outgoing": "Out",
    "Incoming And Outgoing": "Both",
}

PACK_VERSION = get_pack_version() or "1.3.6"
CALLING_PLATFORM_VERSION = "XSOAR"
CONNECTOR_NAME_VERSION = f"Cortex Palo Alto - XSOAR: Bitsight - version {PACK_VERSION}"
# Disable insecure warnings
urllib3.disable_warnings()


class Client(BaseClient):
    """Client will implement the service API, should not contain Cortex XSOAR logic. \
    Should do requests and return data."""

    def get_companies_guid(self):
        """Retrieve subscribed company details."""
        uri = "v1/companies"
        return self._http_request(method="GET", url_suffix=uri)

    def get_company_detail(self, guid):
        """
        Retrieve company details based on its Guid.

        :param guid: guid of the company whose details need to be retrieved
        """
        uri = f"v1/companies/{encode_string_results(guid)}"
        return self._http_request(method="GET", url_suffix=uri)

    def get_company_findings(self, guid, first_seen, last_seen, optional_params=None):
        """
        Retrieve company findings based on its Guid.

        :param guid: guid of the company whose findings need to be retrieved
        :param first_seen: first seen date (YYYY-MM-DD) of the findings
        :param last_seen: last seen date (YYYY-MM-DD) of the findings
        :param optional_params: params to be passed to the findings endpoint
        """
        uri = f"v1/companies/{encode_string_results(guid)}/findings"

        params = {"first_seen_gte": first_seen, "last_seen_lte": last_seen, "unsampled": "true", "expand": "attributed_companies"}
        if optional_params:
            params.update(optional_params)
        remove_nulls_from_dictionary(params)

        return self._http_request(method="GET", url_suffix=uri, params=params)

    def get_finding_comments(self, company_guid: str, rolledup_observation_id: str):
        """
        Retrieve finding comments based on its Guid.

        :type company_guid: str
        :param company_guid: guid of the company whose comments need to be retrieved

        :type rolledup_observation_id: str
        :param rolledup_observation_id: rolledup_observation_id of the finding
        """
        encoded_company_guid = encode_string_results(company_guid)
        encoded_rolledup_observation_id = encode_string_results(rolledup_observation_id)
        uri = f"ratings/v1/companies/{encoded_company_guid}/findings/{encoded_rolledup_observation_id}/comments"
        return self._http_request(method="GET", url_suffix=uri)

    def get_remediations(self, params: dict | None = None):
        """
        Retrieve remediations.

        :type params: dict | None
        :param params: params to be passed to the remediations endpoint
        """
        uri = "ratings/v1/remediations"
        remove_nulls_from_dictionary(params)
        return self._http_request(method="GET", url_suffix=uri, params=params)

    def update_external_status(self, company_guid: str, body: dict):
        """
        Update external status of a finding.

        :type company_guid: str
        :param company_guid: guid of the company whose findings need to be updated

        :type body: dict
        :param body: body to be passed to the findings endpoint
        """
        uri = f"ratings/v1/remediations/{encode_string_results(company_guid)}"
        remove_nulls_from_dictionary(body)
        return self._http_request(method="PATCH", url_suffix=uri, json_data=body)

    def create_finding_comment(self, company_guid: str, rolledup_observation_id: str, thread_guid: str, body: dict):
        """
        Create a finding comment based on its Guid.

        :type company_guid: str
        :param company_guid: guid of the company whose comments need to be retrieved

        :type rolledup_observation_id: str
        :param rolledup_observation_id: rolledup_observation_id of the finding

        :type thread_guid: str
        :param thread_guid: thread_guid of the finding

        :type body: dict
        :param body: body to be passed to the findings endpoint
        """
        encoded_company_guid = encode_string_results(company_guid)
        encoded_rolledup_observation_id = encode_string_results(rolledup_observation_id)
        encoded_thread_guid = encode_string_results(thread_guid)

        if thread_guid:
            uri = f"annotations/v1/threads/{encoded_thread_guid}/comments"
            return self._http_request(method="POST", url_suffix=uri, json_data=body, ok_codes=[200, 201])
        else:
            params = {"company_guid": encoded_company_guid, "record_id": encoded_rolledup_observation_id}
            uri = "annotations/v1/threads"
            return self._http_request(method="POST", url_suffix=uri, params=params, json_data=body, ok_codes=[200, 201])

    def get_users(self, params: dict | None = None):
        """
        Retrieve user details from BitSight platform.

        :type params: dict | None
        :param params: Optional parameters to be passed to the users endpoint

        :return: Response containing user details
        """
        uri = "ratings/v2/users"

        remove_nulls_from_dictionary(params)
        return self._http_request(method="GET", url_suffix=uri, params=params)


"""HELPER FUNCTIONS"""


def trim_spaces_from_args(args):
    """
    Trim spaces from values of the args dict.

    :param args: Dict to trim spaces from
    :type args: dict
    :return:
    """
    for key, val in args.items():
        if isinstance(val, str):
            args[key] = val.strip()

    return args


def camelize_strings_with_underscore(string: str):
    """
    Wrap CommonServerPython's camelize_string to also convert Pascal strings.

    :param string: string to convert to camel case
    """
    if string.find("_") == -1:
        return string[0].lower() + string[1:]
    else:
        return camelize_string(string, upper_camel=False)


def camelize_dict_recursively(src):
    """
    Camelize all the keys in a dictionary with nested dictionaries and lists.

    :param src: the dictionary to camelize
    """
    destination = {}
    for key, value in src.items():
        if isinstance(value, dict):
            destination[camelize_strings_with_underscore(key)] = camelize_dict_recursively(value)
        elif isinstance(value, list):
            if value and isinstance(value[0], dict):
                destination[camelize_strings_with_underscore(key)] = [
                    camelize_dict_recursively(list_value) for list_value in value
                ]
            else:
                destination[camelize_strings_with_underscore(key)] = value
        else:
            destination[camelize_strings_with_underscore(key)] = value
    return destination


def get_mirroring() -> dict:
    """
    Get the mirroring configuration parameters from the Demisto integration parameters.

    :return: A dictionary containing the mirroring configuration parameters.
    :rtype: dict
    """
    params = demisto.params()
    mirror_direction = params.get("mirror_direction", "None").strip()
    mirror_tags = params.get("note_tag", "").strip()
    return {
        "mirror_direction": MIRROR_DIRECTION.get(mirror_direction),
        "mirror_tags": mirror_tags,
        "mirror_instance": demisto.integrationInstance(),
    }


def prepare_and_validate_company_findings_get_filter_args(risk_vector_list, severity, asset_category):
    """
    Prepare and validate arguments for bitsight-company-findings-get.

    :param risk_vector_list: input from argument risk_vector_label
    :param severity: input from argument severity
    :param asset_category: input from argument asset_category
    """
    risk_vector = ""
    for vector in risk_vector_list:
        if vector.lower() in RISK_VECTOR_MAPPING:
            risk_vector += RISK_VECTOR_MAPPING[vector.lower()] + ","
        else:
            raise ValueError(
                ERROR_MESSAGES["INVALID_SELECT"].format(
                    vector.lower(), "risk_vector_label", ", ".join(RISK_VECTOR_MAPPING.keys())
                )
            )

    risk_vector = risk_vector[:-1]

    severity_gte = None
    if severity:
        if severity in SEVERITY_MAPPING:
            severity_gte = SEVERITY_MAPPING[severity]
        else:
            raise ValueError(ERROR_MESSAGES["INVALID_SELECT"].format(severity, "severity", ", ".join(SEVERITY_MAPPING.keys())))

    asset_category_eq = None
    if asset_category:
        if asset_category in ASSET_CATEGORY_MAPPING:
            asset_category_eq = ASSET_CATEGORY_MAPPING[asset_category]
        else:
            raise ValueError(
                ERROR_MESSAGES["INVALID_SELECT"].format(
                    asset_category, "asset_category", ", ".join(ASSET_CATEGORY_MAPPING.keys())
                )
            )
    return risk_vector, severity_gte, asset_category_eq


def prepare_and_validate_fetch_findings_args(client, args):
    """
    Prepare and validate arguments for company_findings_get_command when fetch_incidents is true.

    :param client: client to use
    :param args: arguments obtained from demisto.args()
    """
    guid = args.get("guid", None)
    if not guid:
        res = client.get_companies_guid()
        if res.status_code == 200:
            res_json = res.json()
            guid = res_json.get("my_company", {}).get("guid")
        else:
            raise DemistoException(ERROR_MESSAGES["GUID_NOT_FETCHED"])
    severity = args.get("findings_min_severity", None)
    if severity:
        severity = severity.lower()
    grade_list = args.get("findings_grade", None)
    grade = ",".join(grade_list) if grade_list else None
    asset_category = args.get("findings_min_asset_category", None)
    if asset_category:
        asset_category = asset_category.lower()
    risk_vector_list = argToList(args.get("risk_vector"))
    if "All" in risk_vector_list:
        risk_vector_list = []
    limit = arg_to_number(args.get("max_fetch", DEFAULT_FETCH_LIMIT), "Max Fetch", True)
    if limit and (limit < 1 or limit > MAX_FETCH_LIMIT):  # type: ignore
        raise ValueError(ERROR_MESSAGES["INVALID_MAX_FETCH"])

    affect_rating_reason_list = argToList(args.get("findings_affect_rating_reason", ""))
    valid_affect_rating_reasons = []
    for affect_rating_reason in affect_rating_reason_list:
        affect_rating_reason = affect_rating_reason.strip()
        if affect_rating_reason and affect_rating_reason.lower() not in AFFECT_RATING_REASON_MAPPING:
            raise ValueError(
                ERROR_MESSAGES["INVALID_SELECT"].format(
                    affect_rating_reason, "Findings Affect Rating Reason", ", ".join(VALID_AFFECT_RATING_REASON)
                )
            )

        if affect_rating_reason:
            valid_affect_rating_reasons.append(AFFECT_RATING_REASON_MAPPING.get(affect_rating_reason.lower()))

    valid_affect_rating_reasons: list = list(set(valid_affect_rating_reasons))
    affect_rating_reason = ",".join(valid_affect_rating_reasons)

    return guid, severity, grade, asset_category, risk_vector_list, limit, affect_rating_reason


def close_in_xsoar(entries: list, rolledup_observation_id: str, remidiation_status: str):
    """
    Close the XSOAR incident for the given finding.

    :type entries: list
    :param entries: List of entries where the closing entry will be appended.
    :type rolledup_observation_id: str
    :param rolledup_observation_id: The rolledup observation ID.
    :type remidiation_status: str
    :param remidiation_status: The BitSight remediation status that triggered the closure.
    """
    demisto.debug(
        f"Closing XSOAR incident for BitSight finding {rolledup_observation_id} via Incoming Mirroring. "
        f"Due to remediation status: {remidiation_status} is same as configured close status."
    )
    entries.append(
        {
            "Type": EntryType.NOTE,
            "Contents": {
                "dbotIncidentClose": True,
                "closeReason": "Other",
                "closeNotes": f"Incident closed because BitSight finding remediation status '{remidiation_status}' "
                "matches the configured close status.",
            },
            "ContentsFormat": EntryFormat.JSON,
        }
    )


def reopen_in_xsoar(entries: list, rolledup_observation_id: str, remidiation_status: str):
    """
    Reopen the XSOAR incident for the given finding.

    :type entries: list
    :param entries: List of entries where the reopening entry will be appended.
    :type rolledup_observation_id: str
    :param rolledup_observation_id: The rolledup observation ID.
    :type remidiation_status: str
    :param remidiation_status: The BitSight remediation status that triggered the reopen.
    """
    demisto.debug(
        f"Reopening XSOAR incident for BitSight finding {rolledup_observation_id}. "
        f"Due to remediation status: {remidiation_status} is same as configured open status."
    )
    entries.append({"Type": EntryType.NOTE, "Contents": {"dbotIncidentReopen": True}, "ContentsFormat": EntryFormat.JSON})


def add_comments_to_finding(
    client: Client, company_guid: str, rolledup_observation_id: str, user_guid: Any, comments_to_add: list[dict]
):
    """
    Add comments to a BitSight finding. Handles thread creation and comment addition.

    :type client: Client
    :param client: BitSight API client instance

    :type company_guid: str
    :param company_guid: GUID of the company

    :type rolledup_observation_id: str
    :param rolledup_observation_id: Rolled up observation ID of the finding

    :type user_guid: Any
    :param user_guid: GUID of the user adding comments

    :type comments_to_add: list[dict]
    :param comments_to_add: List of comment dictionaries with 'message' key

    :return: None
    """
    if not user_guid:
        demisto.debug("Skipping the comment(s) because User GUID is not found.")
        return
    # Fetch existing comments to check if thread exists
    comments_response = client.get_finding_comments(company_guid=company_guid, rolledup_observation_id=rolledup_observation_id)
    existing_comments = comments_response.get("results", [])
    thread_guid = ""

    if existing_comments and isinstance(existing_comments, list):
        demisto.debug(f"Found {len(existing_comments)} existing comments for finding {rolledup_observation_id}.")
        thread_guid = existing_comments[0].get("thread_guid", "")

    # If thread exists, add comments individually; otherwise, batch create
    if thread_guid:
        # Add each comment to the existing thread
        for comment_data in comments_to_add:
            comment_info = {"author_guid": user_guid, "message": comment_data.get("message", ""), "public": False}
            client.create_finding_comment(
                company_guid=company_guid,
                rolledup_observation_id=rolledup_observation_id,
                thread_guid=thread_guid,
                body=comment_info,
            )
    else:
        # Create new thread with all comments
        new_comments = [
            {"author_guid": user_guid, "message": comment.get("message", ""), "public": False} for comment in comments_to_add
        ]
        comments_body = {"comments": new_comments}
        client.create_finding_comment(
            company_guid=company_guid,
            rolledup_observation_id=rolledup_observation_id,
            thread_guid=thread_guid,
            body=comments_body,
        )


"""COMMAND FUNCTIONS"""


def fetch_incidents(client, last_run, params):
    """
    Fetch Bitsight Findings.

    :param client: client to use
    :param last_run: last run object obtained from demisto.getLastRun()
    :param params: arguments obtained from demisto.params()
    """
    events = []
    try:
        if "offset" in last_run:
            params["offset"] = last_run["offset"]
            last_run_date = last_run["first_fetch"]
        else:
            first_fetch = arg_to_number(params.get("first_fetch", DEFAULT_FIRST_FETCH_DAYS), "First fetch time in days", True)
            if first_fetch < 0:  # type: ignore
                raise ValueError(ERROR_MESSAGES["NEGATIVE_FIRST_FETCH"])
            today = datetime.now()
            last_run_date = (today - timedelta(days=first_fetch)).strftime(BITSIGHT_DATE_TIME_FORMAT)  # type: ignore

        already_fetched_findings = last_run.get("already_fetched_findings") or []
        report_entries = []
        findings_res = company_findings_get_command(client, params, last_run_date, True)
        report_entries.extend(findings_res.get("results", []))

        duplicate_findings = []
        ingested_findings = []

        for entry in report_entries:
            key_field = entry.get("rolledup_observation_id", "") + "-#-" + entry.get("first_seen", "")
            if key_field in already_fetched_findings:
                duplicate_findings.append(key_field)
                continue

            # Updating mirroring fields
            mirroring_fields = get_mirroring()
            mirroring_fields.update({"mirror_id": key_field})
            entry.update(mirroring_fields)

            incident_name = "Bitsight Finding"

            risk_vector_label = entry.get("risk_vector_label", "")
            evidence_key = entry.get("evidence_key", "")
            rollup_start_date = entry.get("details", {}).get("rollup_start_date", "")

            incident_detail_name = " - ".join([x for x in [risk_vector_label, evidence_key, rollup_start_date] if x])
            if incident_detail_name:
                incident_name += " - " + incident_detail_name

            # Set the Raw JSON to the event. Mapping will be done at the classification and mapping
            event = {
                "name": incident_name,
                "occurred": entry.get("first_seen") + "T00:00:00Z",
                "rawJSON": json.dumps(entry),
            }
            events.append(event)
            ingested_findings.append(key_field)
            already_fetched_findings.append(key_field)

        demisto.debug(f"Skipped {len(duplicate_findings)} duplicate findings: {duplicate_findings}")
        demisto.debug(f"Fetched {len(ingested_findings)} findings: {ingested_findings}")
        last_run = {
            "first_fetch": last_run_date,
            "offset": params["offset"] + len(report_entries) if params.get("offset") else len(report_entries),
        }
        demisto.debug(
            f"Updated the last run state - first fetch : {last_run.get('first_fetch')}, offset: {last_run.get('offset')}"
        )
        last_run.update({"already_fetched_findings": already_fetched_findings})

    except Exception as e:
        demisto.error("Failed to fetch events.")
        raise e

    return last_run, events


def test_module(client, params):
    """
    Returning 'ok' indicates that the integration works like it is supposed to. \
    Connection to the service is successful. Anything else will fail the test.

    :param client: client to use
    :param params: parameters obtained from demisto.params()
    """
    res = client.get_companies_guid()
    mirror_direction = params.get("mirror_direction", "")

    if mirror_direction.lower() in [OUTGOING_MIRROR_DIRECTION, INCOMING_AND_OUTGOING_MIRROR_DIRECTION]:
        user_email = params.get("user_email", "").strip()
        if not user_email:
            raise ValueError(ERROR_MESSAGES["USER_EMAIL_REQUIRED"])
        user_guid = get_current_user_guid(client, user_email, True)
        if not user_guid:
            raise ValueError(ERROR_MESSAGES["USER_GUID_NOT_FOUND"])

    if params.get("isFetch", False):
        available_guids = {c["guid"] for c in res["companies"]}
        requested_guid = params.get("guid")

        if not requested_guid:
            raise ValueError(ERROR_MESSAGES["GUID_REQUIRED"])

        if requested_guid not in available_guids:
            raise ValueError(ERROR_MESSAGES["GUID_NOT_AVAILABLE"])
        fetch_incidents(client, {}, params)
    return "ok"


def companies_guid_get_command(client, *args):
    """
    Retrieve subscribed company details.

    :param client: client to use
    """
    res_json = client.get_companies_guid()
    outputs = camelize_dict_recursively(remove_empty_elements(res_json))
    context_output = {
        "BitSight.Company(val.guid == obj.guid)": outputs.get("companies", []),
        "BitSight.MyCompany(val.guid == obj.guid)": outputs.get("myCompany", {}),
    }
    hr = []
    companies_list = outputs.get("companies", [])
    for company in companies_list:
        hr.append(
            {
                "Company Name": company.get("name"),
                "Company Short Name": company.get("shortname"),
                "GUID": company.get("guid"),
                "Rating": company.get("rating"),
            }
        )

    readable_output = tableToMarkdown(
        name="Companies:",
        metadata=f"My Company: {outputs.get('myCompany', {}).get('guid')}",
        t=hr,
        headers=["Company Name", "Company Short Name", "GUID", "Rating"],
        removeNull=True,
    )

    return CommandResults(readable_output=readable_output, outputs=context_output, raw_response=outputs)


def company_details_get_command(client, args):
    """
    Retrieve company details based on its Guid.

    :param client: client to use
    :param args: arguments obtained from demisto.args()
    """
    guid = args.get("guid")
    res_json = client.get_company_detail(guid)

    outputs = camelize_dict_recursively(remove_empty_elements(res_json))

    outputs["ratingDetails"] = [value for _, value in outputs.get("ratingDetails", {}).items()]

    company_info = {
        "guid": res_json.get("guid"),
        "customId": res_json.get("custom_id"),
        "name": res_json.get("name"),
        "description": res_json.get("description"),
        "ipv4Count": res_json.get("ipv4_count"),
        "peopleCount": res_json.get("people_count"),
        "shortName": res_json.get("shortname"),
        "industry": res_json.get("industry"),
        "industrySlug": res_json.get("industry_slug"),
        "subIndustry": res_json.get("sub_industry"),
        "subIndustrySlug": res_json.get("sub_industry_slug"),
        "homePage": res_json.get("homepage"),
        "primaryDomain": res_json.get("primary_domain"),
        "type": res_json.get("type"),
        "displayURL": res_json.get("display_url"),
    }
    ratings = []
    for rating in res_json.get("ratings", []):
        rating_dict = {"rating": rating.get("rating"), "rating_date": rating.get("rating_date"), "range": rating.get("range")}
        ratings.append(rating_dict)

    rating_details = []
    for rating_detail_key in res_json.get("rating_details", {}):
        rating_detail = res_json.get("rating_details", {}).get(rating_detail_key, {})
        rating_detail_dict = {
            "name": rating_detail.get("name"),
            "rating": rating_detail.get("rating"),
            "percentile": rating_detail.get("percentile"),
            "display_url": rating_detail.get("display_url"),
        }
        rating_details.append(rating_detail_dict)

    readable = {"Company Info": company_info, "Ratings": ratings, "Rating Details": rating_details}

    readable_output = tableToMarkdown(
        name="Company Details:", t=readable, headers=["Company Info", "Ratings", "Rating Details"], removeNull=True
    )
    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="BitSight.Company",
        outputs=outputs,
        outputs_key_field="guid",
        raw_response=res_json,
    )


def company_findings_get_command(client, args, first_seen=None, fetch_incidents=False):
    """
    Retrieve company findings based on its Guid.

    :param client: client to use
    :param args: arguments obtained from demisto.args()
    :param first_seen: first seen of the finding
    :param fetch_incidents: whether the command is called from fetch_incidents
    """
    last_seen = None
    affect_rating_reason = ""
    if fetch_incidents:
        guid, severity, grade, asset_category, risk_vector_list, limit, affect_rating_reason = (
            prepare_and_validate_fetch_findings_args(client, args)
        )
        offset = arg_to_number(args.get("offset", DEFAULT_OFFSET), "offset")
    else:
        guid = args.get("guid")
        severity = args.get("severity", None)
        grade = args.get("grade", None)
        asset_category = args.get("asset_category", None)
        limit = arg_to_number(args.get("limit", DEFAULT_LIMIT), "limit")
        if limit and limit > MAX_LIMIT:  # type: ignore
            raise ValueError(ERROR_MESSAGES["LIMIT_GREATER_THAN_ALLOWED"])
        offset = arg_to_number(args.get("offset", DEFAULT_OFFSET), "offset")
        if severity:
            severity = severity.lower()
        if grade:
            grade = grade.upper()
        if asset_category:
            asset_category = asset_category.lower()
        risk_vector_list = argToList(args.get("risk_vector_label", []))
        first_seen = args.get("first_seen")
        last_seen = args.get("last_seen")

    risk_vector, severity_gte, asset_category_eq = prepare_and_validate_company_findings_get_filter_args(
        risk_vector_list, severity, asset_category
    )
    res_json = client.get_company_findings(
        guid,
        first_seen,
        last_seen,
        {
            "severity_gte": severity_gte,
            "details.grade": grade,
            "assets.category": asset_category_eq,
            "risk_vector": risk_vector,
            "limit": limit,
            "offset": offset,
            "impacts_risk_vector_details": affect_rating_reason,
        },
    )

    if fetch_incidents:
        return res_json
    res_json_cleaned = camelize_dict_recursively(remove_empty_elements(res_json))
    readable_list = []
    outputs = None
    if res_json_cleaned.get("results", []):
        for finding in res_json_cleaned.get("results", []):
            readable = {
                "Evidence Key": finding.get("evidenceKey"),
                "Risk Vector Label": finding.get("riskVectorLabel"),
                "First Seen": finding.get("firstSeen"),
                "Last Seen": finding.get("lastSeen"),
                "ID": finding.get("temporaryId"),
                "Risk Category": finding.get("riskCategory"),
                "Severity": finding.get("severityCategory"),
                "Asset Category": "\n".join(
                    [f"{asset.get('asset')}: {asset.get('category', '').title()}" for asset in finding.get("assets", [])]
                ),
                "Finding Grade": finding.get("details", {}).get("grade", "").title(),
            }
            readable_list.append(readable)
        outputs = {
            "BitSight.Company(val.guid == obj.guid)": {
                "guid": guid.lower(),
                "CompanyFinding": res_json_cleaned.get("results", []),
            },
            "BitSight.Page(val.name == obj.name)": {
                "name": "bitsight-company-findings-get",
                "next": res_json_cleaned.get("links", {}).get("next"),
                "previous": res_json_cleaned.get("links", {}).get("previous"),
                "count": res_json_cleaned.get("count"),
            },
        }

    readable_output = tableToMarkdown(
        name="Company findings:",
        t=readable_list,
        metadata=f"Total Findings: {res_json_cleaned.get('count')}",
        headers=[
            "Evidence Key",
            "Risk Vector Label",
            "First Seen",
            "Last Seen",
            "ID",
            "Risk Category",
            "Severity",
            "Asset Category",
            "Finding Grade",
        ],
        removeNull=True,
    )
    return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=res_json)


def get_current_user_guid(client: Client, user_email: str, is_test: bool = False):
    """
    Get the GUID of the current user based on their email.

    :param client: client to use
    :param user_email: Email address of the user
    :return: GUID of the user
    """
    integration_context = demisto.getIntegrationContext()
    users_guids = integration_context.get("users_guids", {})

    if user_email in users_guids and not is_test:
        return users_guids[user_email]

    user_guid = None
    user_info = client.get_users(params={"email": user_email})
    if user_info.get("results") and isinstance(user_info["results"], list):
        user_guid = user_info["results"][0].get("guid")
        users_guids[user_email] = user_guid

    integration_context["users_guids"] = users_guids
    demisto.setIntegrationContext(integration_context)
    return user_guid


def get_modified_remote_data_command(client: Client, args: Dict) -> GetModifiedRemoteDataResponse:
    """
    Get modified findings from the BitSight platform and prepare it for mirroring.

    :type client: Client
    :param client: client to use

    :type args: Dict[str, str]
    :param args: arguments obtained from demisto.args()

    :return: GetModifiedRemoteDataResponse with list of modified incident IDs
    :rtype: GetModifiedRemoteDataResponse
    """
    company_guid = args.get("guid", "")

    command_args = GetModifiedRemoteDataArgs(args)
    command_last_run_date = dateparser.parse(
        command_args.last_update,  # type: ignore
        settings={"TIMEZONE": "UTC"},  # type: ignore
    )
    command_last_run_date_time = command_last_run_date.strftime(DATE_FORMAT)  # type: ignore
    demisto.debug(f"Last update date of get-modified-remote-data command is {command_last_run_date_time}.")

    last_update_date = command_last_run_date.strftime(BITSIGHT_DATE_TIME_FORMAT)  # type: ignore

    optional_params = {
        "limit": MAX_FINDINGS_MIRRORING_LIMIT,
        "sort": "-last_seen",
        "last_remediation_status_date_gte": last_update_date,
    }
    # Fetch the findings modified since the last update.
    response = client.get_company_findings(guid=company_guid, first_seen=None, last_seen=None, optional_params=optional_params)

    modified_findings_ids = []
    findings = response.get("results", [])
    for finding in findings:
        rolledup_observation_id = finding.get("rolledup_observation_id")
        first_seen = finding.get("first_seen")
        key_field = f"{rolledup_observation_id}-#-{first_seen}"

        modified_findings_ids.append(key_field)

    modified_findings_ids: List[str] = list(filter(None, modified_findings_ids))  # type: ignore
    demisto.debug(
        f"Performing get-modified-remote-data command. Numbers Findings IDs to update in XSOAR: {len(modified_findings_ids)}"
    )
    demisto.debug(f"Performing get-modified-remote-data command. Findings IDs to update in XSOAR: {modified_findings_ids}")
    # Filter out duplicate findings IDs.
    unique_findings_ids = list(set(modified_findings_ids))

    return GetModifiedRemoteDataResponse(modified_incident_ids=unique_findings_ids)


def get_remote_data_command(client: Client, args: Dict) -> GetRemoteDataResponse:
    """
    Get remote data for a specific finding from the BitSight platform for mirroring.

    :type client: Client
    :param client: client to use

    :type args: Dict[str, str]
    :param args: arguments obtained from demisto.args()

    :return: GetRemoteDataResponse with finding data and new entries
    :rtype: GetRemoteDataResponse
    """
    new_entries_to_return: list = []

    dbot_mirror_id: str = args.get("id")  # type: ignore
    demisto.debug(f"dbot_mirror_id:{dbot_mirror_id}")

    dbot_mirror_id_parts = dbot_mirror_id.split("-#-")
    finding_rolledup_observation_id: str = dbot_mirror_id_parts[0] if len(dbot_mirror_id_parts) > 0 else ""
    first_seen: str = dbot_mirror_id_parts[1] if len(dbot_mirror_id_parts) > 1 else ""
    demisto.debug(f"Bitsight finding with rolledup_observation_id:{finding_rolledup_observation_id}")
    demisto.debug(f"Getting update for remote {finding_rolledup_observation_id}.")

    command_last_run_dt = arg_to_datetime(args.get("lastUpdate"), arg_name="lastUpdate", required=True)
    command_last_run_timestamp = command_last_run_dt.strftime(DATE_FORMAT)  # type: ignore
    demisto.debug(
        f"The time when the last time get-remote-data command is called for current incident is {command_last_run_timestamp}."
    )
    integration_params = demisto.params()
    close_status_of_bitsight = integration_params.get("close_status_of_bitsight") or DEFAULT_CLOSE_STATUS_OF_BITSIGHT
    open_status_of_bitsight = integration_params.get("open_status_of_bitsight") or DEFAULT_OPEN_STATUS_OF_BITSIGHT
    close_active_incident = integration_params.get("close_active_incident", False)
    reopen_closed_incident = integration_params.get("reopen_closed_incident", False)

    company_guid = args.get("guid", "")
    params = {
        "rolledup_observation_id": finding_rolledup_observation_id,
        "first_seen": first_seen,
        "sort": "-last_seen",
    }

    # Fetch the findings modified since the last update.
    remote_incident_data = client.get_company_findings(guid=company_guid, first_seen=None, last_seen=None, optional_params=params)

    findings = remote_incident_data.get("results", [])
    finding = {}
    for f in findings:
        if f.get("affects_rating", ""):
            finding = f
            break

    # Handle the scenario where all findings have affects_rating as false
    if not finding:
        finding = findings[0] if findings else {}
    if not finding:
        return GetRemoteDataResponse({}, [])

    params = {
        "company_guid": company_guid,
        "rolledup_observation_id": finding_rolledup_observation_id,
        "evidence_key": finding.get("evidence_key", ""),
        "risk_vector": finding.get("risk_vector", ""),
    }
    integration_context = demisto.getIntegrationContext()
    processed_findings = integration_context.get("processed_findings") or []
    try:
        remidiations_response = client.get_remediations(params=params)
        remidiations = remidiations_response.get("results")
        remidiation_status = ""
        if remidiations:
            remidiation_status = remidiations[0].get("status", {}).get("value", "")
            finding.update({"remediation_status": remidiation_status})

        if remidiation_status == close_status_of_bitsight and dbot_mirror_id in processed_findings and close_active_incident:
            close_in_xsoar(new_entries_to_return, finding_rolledup_observation_id, remidiation_status)
            processed_findings.remove(dbot_mirror_id)
            demisto.debug(f"Removed {dbot_mirror_id} finding from processed findings.")
        elif (
            remidiation_status == open_status_of_bitsight and dbot_mirror_id not in processed_findings and reopen_closed_incident
        ):
            reopen_in_xsoar(new_entries_to_return, finding_rolledup_observation_id, remidiation_status)
            processed_findings.append(dbot_mirror_id)
            demisto.debug(f"Added {dbot_mirror_id} finding to processed findings.")
    except DemistoException as e:
        demisto.debug(
            f"Failed to fetch remediation status for finding {finding_rolledup_observation_id}: {str(e)}. "
            "Continuing without remediation data."
        )

    try:
        comments = client.get_finding_comments(company_guid=company_guid, rolledup_observation_id=finding_rolledup_observation_id)
        comments = comments.get("results")
    except DemistoException as e:
        demisto.debug(
            f"Failed to fetch comments for finding {finding_rolledup_observation_id}: {str(e)}. Continuing without comments data."
        )
        comments = None
    if comments:
        for comment in comments:
            if "[Mirrored From XSOAR]" in comment.get("message"):
                demisto.debug(f"Skipping the comment {comment.get('guid')} as it is mirrored from XSOAR.")
                continue

            comment_created_time = arg_to_datetime(comment.get("created_time"))
            comment_updated_time = arg_to_datetime(comment.get("last_update_time"))
            if comment_updated_time:
                if comment_updated_time <= command_last_run_dt:  # type: ignore
                    demisto.debug(
                        f"Skipping comment {comment.get('guid')} - updated time {comment_updated_time} is "
                        f"before sync timestamp {command_last_run_dt}."
                    )
                    continue
            else:
                if comment_created_time <= command_last_run_dt:  # type: ignore
                    demisto.debug(
                        f"Skipping the comment {comment.get('guid')} - created time {comment_created_time} is "
                        f"before sync timestamp {command_last_run_dt}."
                    )
                    continue
            new_entries_to_return.append(
                {
                    "Type": EntryType.NOTE,
                    "Contents": f"[Mirrored From Bitsight]\n"
                    f"Added By: {comment.get('author', {}).get('name', '')}\n"
                    f"Added At: {comment_created_time} UTC\n"
                    f"Note: {comment.get('message')}",
                    "ContentsFormat": EntryFormat.TEXT,
                    "Note": True,
                }
            )
    demisto.debug(f"remote_incident_data_finding:{finding}")

    processed_findings = processed_findings[-MAX_FINDINGS_MIRRORING_LIMIT:]
    integration_context["processed_findings"] = processed_findings
    demisto.setIntegrationContext(integration_context)

    return GetRemoteDataResponse(finding, new_entries_to_return)


def update_remote_system_command(client: Client, args: Dict, close_status_of_bitsight: str, open_status_of_bitsight: str) -> str:
    """
    Update the remote system with the provided data.

    :type client: Client
    :param client: client to use.

    :type args: Dict[str, str]
    :param args: arguments obtained from demisto.args()

    :type close_status_of_bitsight: str
    :param close_status_of_bitsight: Remediation status value for closing findings

    :type open_status_of_bitsight: str
    :param open_status_of_bitsight: Remediation status value for opening findings

    :return: remote incident ID
    :rtype: str
    """
    company_guid = args.get("guid", "")
    user_email = args.get("user_email", "").strip()
    user_guid = None
    if user_email:
        user_guid = get_current_user_guid(client, user_email)
    else:
        demisto.debug(ERROR_MESSAGES["USER_EMAIL_REQUIRED"])
    parsed_args = UpdateRemoteSystemArgs(args)

    # Get remote incident ID
    remote_incident_id = parsed_args.remote_incident_id

    rolledup_observation_id = parsed_args.data.get("bitsightrolledupobservationid", "")
    risk_vector = parsed_args.data.get("bitsightriskvector", "")
    evidence_key = parsed_args.data.get("bitsightevidencekey", "")

    incident_status = parsed_args.inc_status
    delta = parsed_args.delta
    xsoar_incident_id = parsed_args.data.get("id", "")
    new_entries = parsed_args.entries or []
    if new_entries:
        demisto.debug(f"Updating remote system with {len(new_entries)} new entries for incident {remote_incident_id}.")
        # Prepare comments from entries
        comments_to_add = []
        for entry in new_entries:
            entry_id = entry.get("id")
            demisto.debug(f"Sending the entry with ID: {entry_id} and Type: {entry.get('type')}")
            # Get note content and user
            entry_content = re.sub(r"([^\n])\n", r"\1\n\n", entry.get("contents", ""))
            entry_user = entry.get("user", "dbot") or "dbot"

            note_str = (
                f"[Mirrored From XSOAR] XSOAR Incident ID: {xsoar_incident_id}\n\nNote: {entry_content}\n\nAdded By: {entry_user}"
            )
            comments_to_add.append({"message": note_str})

        # Add all comments using the helper function
        add_comments_to_finding(client, company_guid, rolledup_observation_id, user_guid, comments_to_add)

    # Get integration context to track processed findings
    integration_context = demisto.getIntegrationContext()
    processed_findings = integration_context.get("processed_findings") or []

    incident_reopened = False
    # Check if incident is reopened
    if delta and delta.get("closingUserId") == "" and delta.get("runStatus") == "":
        demisto.debug(f"Incident {xsoar_incident_id} is reopened.")
        incident_reopened = True

    if (incident_status == IncidentStatus.ACTIVE and remote_incident_id in processed_findings) or (
        incident_status == IncidentStatus.DONE and remote_incident_id not in processed_findings
    ):
        demisto.debug(f"Skipping status update for finding {remote_incident_id} to prevent mirroring loop.")
        return remote_incident_id

    # Update external status when incident is closed or when incident is active with no changes
    should_update_status = (
        incident_status == IncidentStatus.DONE or (incident_status == IncidentStatus.ACTIVE and not delta) or incident_reopened
    )

    if parsed_args.incident_changed and should_update_status:
        demisto.debug(f"Remote Incident ID: {remote_incident_id}")
        demisto.debug(f"XSOAR Incident ID: {xsoar_incident_id}")
        demisto.debug(f"Delta information for incident: {delta}")

        company_guid = args.get("guid", "")
        # Set status value based on incident status (closed if DONE, opened otherwise)
        status_value = close_status_of_bitsight if incident_status == IncidentStatus.DONE else open_status_of_bitsight
        demisto.debug(f"Updating remediation status of finding {remote_incident_id} to {status_value}")

        body = {
            "rolledup_observation_id": rolledup_observation_id,
            "evidence_key": evidence_key,
            "risk_vector": risk_vector,
            "status": {"value": status_value, "public": False},
        }
        client.update_external_status(company_guid=company_guid, body=body)
        if incident_status == IncidentStatus.ACTIVE:
            processed_findings.append(remote_incident_id)
            demisto.debug(f"Added {remote_incident_id} finding to processed findings.")
        elif incident_status == IncidentStatus.DONE:
            if remote_incident_id in processed_findings:
                processed_findings.remove(remote_incident_id)
                demisto.debug(f"Removed {remote_incident_id} finding from processed findings.")

    processed_findings = processed_findings[-MAX_FINDINGS_MIRRORING_LIMIT:]
    integration_context["processed_findings"] = processed_findings
    demisto.setIntegrationContext(integration_context)

    # For Closing notes
    delta_keys = parsed_args.delta.keys()
    if "closingUserId" in delta_keys and parsed_args.incident_changed and parsed_args.inc_status == IncidentStatus.DONE:
        # Check if incident status is Done
        close_notes = parsed_args.data.get("closeNotes", "")
        close_reason = parsed_args.data.get("closeReason", "")
        close_user_id = parsed_args.data.get("closingUserId", "")

        closing_note = (
            f"[Mirrored From XSOAR] XSOAR Incident ID: {xsoar_incident_id}\n\n"
            f"Close Reason: {close_reason}\n\n"
            f"Closed By: {close_user_id}\n\n"
            f"Close Notes: {close_notes}"
        )
        demisto.debug(f"Closing Comment: {closing_note}")

        # Add closing comment using the helper function
        add_comments_to_finding(client, company_guid, rolledup_observation_id, user_guid, [{"message": closing_note}])

    return remote_incident_id


def main():
    """PARSE AND VALIDATE INTEGRATION PARAMS."""
    command = demisto.command()
    params = demisto.params()
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)
    api_key = params.get("apikey", {})

    demisto.info(f"Command being called is {command}")

    client = Client(
        base_url=BASE_URL,
        verify=verify_certificate,
        proxy=proxy,
        ok_codes=[200],
        auth=requests.auth.HTTPBasicAuth(api_key, ""),
        headers={
            "X-BITSIGHT-CALLING-PLATFORM_VERSION": CALLING_PLATFORM_VERSION,
            "X-BITSIGHT-CONNECTOR-NAME-VERSION": CONNECTOR_NAME_VERSION,
        },
    )

    try:
        """EXECUTION CODE"""
        args = demisto.args()
        args.update({"guid": params.get("guid")})
        args.update({"user_email": params.get("user_email")})
        close_status_of_bitsight = params.get("close_status_of_bitsight") or DEFAULT_CLOSE_STATUS_OF_BITSIGHT
        open_status_of_bitsight = params.get("open_status_of_bitsight") or DEFAULT_OPEN_STATUS_OF_BITSIGHT
        if demisto.command() == "test-module":
            # This is the call made when pressing the integration Test button.
            return_results(test_module(client, params))
        elif demisto.command() == "fetch-incidents":
            last_run = demisto.getLastRun()
            last_run_curr, events = fetch_incidents(client, last_run, params)

            demisto.setLastRun(last_run_curr)
            demisto.incidents(events)
        elif command == "get-remote-data":
            return_results(get_remote_data_command(client, args))
        elif command == "get-modified-remote-data":
            return_results(get_modified_remote_data_command(client, args))
        elif command == "update-remote-system":
            return_results(update_remote_system_command(client, args, close_status_of_bitsight, open_status_of_bitsight))
        else:
            COMMAND_TO_FUNCTION = {
                "bitsight-company-details-get": company_details_get_command,
                "bitsight-company-findings-get": company_findings_get_command,
                "bitsight-companies-guid-get": companies_guid_get_command,
            }
            if COMMAND_TO_FUNCTION.get(demisto.command()):
                args = demisto.args()
                remove_nulls_from_dictionary(trim_spaces_from_args(args))

                return_results(COMMAND_TO_FUNCTION[demisto.command()](client, args))  # type: ignore
            else:
                raise NotImplementedError(f"Command {demisto.command()} is not implemented")

    # Log exceptions
    except Exception as e:
        return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e}")


if __name__ in ["__main__", "builtin", "builtins"]:
    main()