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
| ID | BitSight for Security Performance Management |
|---|---|
| Provider | BitSight |
| Category | Utilities |
| From Version | 6.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix 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 GUIDfirst_fetch— First fetch time in daysmirror_direction— Mirroring Directionnote_tag— Mirror Tag for Notesuser_email— Bitsight User Email Addressopen_status_of_bitsight— Bitsight Remediation Status for Incident Openingreopen_closed_incident— Reopen incident based on Bitsight Remediation Statusclose_status_of_bitsight— Bitsight Remediation Status for Incident Closureclose_active_incident— Close incident based on Bitsight Remediation Statusmax_fetch— Max Fetchfindings_affect_rating_reason— Findings Affect Rating Reasonfindings_min_severity— Findings Minimum Severityfindings_min_asset_category— Findings Minimum Asset Categoryfindings_grade— Findings Graderisk_vector— Risk Vectorinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsisFetch— Fetch incidentsincidentType— Incident typeincidentFetchInterval— Incidents Fetch Interval
Commands (3)
-
bitsight-companies-guid-getBitsight command to get list of companies and GUID.
-
bitsight-company-details-getBitsight 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-getBitsight command to get company findings.
"""Test File for BitSightForSecurityPerformanceManagement Integration.""" import json import os from datetime import datetime, timedelta from unittest.mock import patch import BitSightForSecurityPerformanceManagement as bitsight import demistomock as demisto import pytest from CommonServerPython import BaseClient, DemistoException, EntryType BASE_URL = "https://test.com" DEFAULT_FINDINGS_GRADE = "WARN,GOOD" RISK_VECTOR_INPUT = "SSL Certificates" def util_load_json(path): """Load file in JSON format.""" with open(path, encoding="utf-8") as f: return json.loads(f.read()) def test_get_companies_guid_command(mocker): """Tests success for companies_guid_get_command.""" # Positive Scenario client = bitsight.Client(base_url=BASE_URL) res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/companies_guid_get_response.json")) with open( os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/companies_guid_get_success.md"), encoding="utf-8" ) as f: hr = f.read() mocker.patch.object(BaseClient, "_http_request", return_value=res["raw_response"]) companies_guid_get_command_results = bitsight.companies_guid_get_command(client) assert companies_guid_get_command_results.outputs == res["outputs"] assert companies_guid_get_command_results.readable_output == hr def test_company_details_get_command(mocker): """Tests success for company_details_get_command.""" inp_args = {"guid": "123"} client = bitsight.Client(base_url=BASE_URL) res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/company_details_get_response.json")) with open( os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/company_details_get_success.md"), encoding="utf-8" ) as f: hr = f.read() mocker.patch.object(BaseClient, "_http_request", return_value=res["raw_response"]) company_details_get_command_results = bitsight.company_details_get_command(client, inp_args) assert company_details_get_command_results.outputs == res["outputs"] assert company_details_get_command_results.readable_output == hr def test_company_details_get_command_when_invalid_arguments_are_provided(requests_mock): """Test failure for company_details_get_command.""" inp_args = {"guid": "non-existing-guid"} client = bitsight.Client(base_url=BASE_URL) requests_mock.get(BASE_URL + "/v1/companies/non-existing-guid", json={"detail": "Not found."}, status_code=404) with pytest.raises(DemistoException) as e: bitsight.company_details_get_command(client, inp_args) assert str(e.value) == 'Error in API call [404] - None\n{"detail": "Not found."}' def test_company_findings_get_command(mocker): """Tests success for company_findings_get_command.""" inp_args = { "guid": "123", "first_seen": "2021-01-01", "last_seen": "2022-02-21", "risk_vector_label": "Open Ports", "severity": "minor", "grade": "warn,good,bad", } client = bitsight.Client(base_url=BASE_URL) res = util_load_json( os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/company_findings_get_response.json") ) with open( os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/company_findings_get_success.md"), encoding="utf-8" ) as f: hr = f.read() mocker.patch.object(BaseClient, "_http_request", return_value=res["raw_response"]) company_findings_get_command_results = bitsight.company_findings_get_command(client, inp_args) assert company_findings_get_command_results.outputs == res["outputs"] assert company_findings_get_command_results.readable_output == hr company_findings_get_on_failure_params = [ ( "invalid-severity", None, None, None, None, bitsight.ERROR_MESSAGES["INVALID_SELECT"].format( "invalid-severity", "severity", ", ".join(bitsight.SEVERITY_MAPPING.keys()) ), ), ( None, "invalid-asset-category", "", None, None, bitsight.ERROR_MESSAGES["INVALID_SELECT"].format( "invalid-asset-category", "asset_category", ", ".join(bitsight.ASSET_CATEGORY_MAPPING.keys()) ), ), ( None, None, "invalid-risk-vector", None, None, bitsight.ERROR_MESSAGES["INVALID_SELECT"].format( "invalid-risk-vector", "risk_vector_label", ", ".join(bitsight.RISK_VECTOR_MAPPING.keys()) ), ), ( None, None, "breaches,invalid-risk-vector", None, None, bitsight.ERROR_MESSAGES["INVALID_SELECT"].format( "invalid-risk-vector", "risk_vector_label", ", ".join(bitsight.RISK_VECTOR_MAPPING.keys()) ), ), (None, None, None, "abc", None, 'Invalid number: "limit"="abc"'), (None, None, None, None, "abc", 'Invalid number: "offset"="abc"'), (None, None, None, bitsight.MAX_LIMIT + 1, None, bitsight.ERROR_MESSAGES["LIMIT_GREATER_THAN_ALLOWED"]), ] @pytest.mark.parametrize( "severity, asset_category, risk_vector_label, limit, offset, error", company_findings_get_on_failure_params ) def test_company_findings_get_command_when_invalid_arguments_are_provided( severity, asset_category, risk_vector_label, limit, offset, error ): """Test failure for company_findings_get_command.""" inp_args = { "guid": "123", "first_seen": "2021-01-01", "last_seen": "2022-02-21", "risk_vector_label": risk_vector_label, "severity": severity, "asset_category": asset_category, "limit": limit, "offset": offset, } client = bitsight.Client(base_url=BASE_URL) with pytest.raises(ValueError) as e: bitsight.company_findings_get_command(client, inp_args) assert str(e.value) == error def test_fetch_incidents_success_without_last_run(mocker): """Tests success for fetch_incidents when called for the first time.""" inp_args = { "guid": "123", "first_fetch": "2", "findings_min_severity": "severe", "findings_grade": DEFAULT_FINDINGS_GRADE, "findings_asset_category": "low", "risk_vector": RISK_VECTOR_INPUT, "findings_affect_rating_reason": " ", } client = bitsight.Client(base_url=BASE_URL) mocker.patch.object(demisto, "params", return_value=inp_args) res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_incidents_response.json")) mocker.patch.object(BaseClient, "_http_request", return_value=res["response"]) last_run, events = bitsight.fetch_incidents(client=client, last_run={}, params=inp_args) curr_date = (datetime.now() - timedelta(days=int(inp_args["first_fetch"]))).strftime("%Y-%m-%d") assert curr_date == last_run["first_fetch"] assert res["response"]["count"] == last_run["offset"] assert events == res["incidents"] def test_fetch_incidents_success_with_last_run(mocker): """Tests success for fetch_incidents when called with last run.""" inp_args = { "guid": "123", "first_fetch": "2", "findings_min_severity": "severe", "findings_grade": DEFAULT_FINDINGS_GRADE, "findings_asset_category": "low", "risk_vector": RISK_VECTOR_INPUT, "findings_affect_rating_reason": "Yes,No: Grace Period,No: Incubation Period", } client = bitsight.Client(base_url=BASE_URL) mocker.patch.object(demisto, "params", return_value=inp_args) res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_incidents_response.json")) mocker.patch.object(BaseClient, "_http_request", return_value=res["response"]) last_run, events = bitsight.fetch_incidents( client=client, last_run={"first_fetch": "2022-03-27", "offset": 2}, params=inp_args ) assert res["response"]["count"] + 2 == last_run["offset"] assert last_run["first_fetch"] == "2022-03-27" assert events == res["incidents"] def test_fetch_incidents_with_duplicate_findings(mocker): """Tests fetch_incidents when duplicate findings are encountered.""" inp_args = { "guid": "123", "first_fetch": "2", "findings_min_severity": "severe", "findings_grade": DEFAULT_FINDINGS_GRADE, "findings_asset_category": "low", "risk_vector": RISK_VECTOR_INPUT, } client = bitsight.Client(base_url=BASE_URL) mocker.patch.object(demisto, "params", return_value=inp_args) # Mock response with duplicate findings (same temporary_id) res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_incidents_response.json")) mocker.patch.object(BaseClient, "_http_request", return_value=res["response"]) # Test with last_run containing already_fetched_findings with one of the duplicate IDs last_run_with_duplicates = { "first_fetch": "2025-10-05", "offset": 1, "already_fetched_findings": [ "dummy_rolledup_observation_id_1-#-2022-03-27" ], # dummy_rolledup_observation_id_1 is already fetched } last_run, events = bitsight.fetch_incidents(client=client, last_run=last_run_with_duplicates, params=inp_args) # Assertions # Should only create 1 event for dummy_rolledup_observation_id_2 assert len(events) == 1 assert events[0]["name"] == "Bitsight Finding - SSL Certificates - dummy.com - 2022-03-27" # Verify last_run is updated correctly assert last_run["first_fetch"] == "2025-10-05" assert last_run["offset"] == 3 assert "dummy_rolledup_observation_id_1-#-2022-03-27" in last_run["already_fetched_findings"] assert "dummy_rolledup_observation_id_2-#-2022-03-27" in last_run["already_fetched_findings"] assert len(last_run["already_fetched_findings"]) == 2 def test_fetch_incidents_when_empty_response(mocker): """Tests for fetch_incidents when empty response is returned.""" inp_args = { "guid": "123", "first_fetch": "2", "findings_min_severity": "severe", "findings_grade": DEFAULT_FINDINGS_GRADE, "findings_asset_category": "low", "risk_vector": RISK_VECTOR_INPUT, } client = bitsight.Client(base_url=BASE_URL) mocker.patch.object(demisto, "params", return_value=inp_args) mocker.patch.object(BaseClient, "_http_request", return_value={"count": 3, "results": []}) last_run, events = bitsight.fetch_incidents( client=client, last_run={"first_fetch": "2022-03-27", "offset": 3}, params=inp_args ) assert last_run["offset"] == 3 assert last_run["first_fetch"] == "2022-03-27" def test_fetch_incidents_with_invalid_affect_rating_reason(mocker, capfd): """Tests for fetch_incidents when invalid findings_affect_rating_reason is provided.""" inp_args = { "guid": "123", "first_fetch": "2", "findings_min_severity": "severe", "findings_grade": DEFAULT_FINDINGS_GRADE, "findings_asset_category": "low", "risk_vector": RISK_VECTOR_INPUT, "findings_affect_rating_reason": "Invalid Reason", } client = bitsight.Client(base_url=BASE_URL) mocker.patch.object(demisto, "params", return_value=inp_args) with capfd.disabled(): with pytest.raises(ValueError) as e: bitsight.fetch_incidents(client=client, last_run={}, params=inp_args) assert str(e.value) == bitsight.ERROR_MESSAGES["INVALID_SELECT"].format( "Invalid Reason", "Findings Affect Rating Reason", ", ".join(bitsight.VALID_AFFECT_RATING_REASON) ) @patch("BitSightForSecurityPerformanceManagement.return_results") # noqa: F821 def test_test_module(mock_return, mocker): """Tests success for test_module.""" # Positive Scenario res = util_load_json(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/companies_guid_get_response.json")) mocker.patch.object(BaseClient, "_http_request", return_value=res["raw_response"]) mocker.patch.object(demisto, "params", return_value={"apikey": "123"}) mocker.patch.object(demisto, "command", return_value="test-module") bitsight.main() assert mock_return.call_args.args[0] == "ok" def test_get_modified_remote_command_successful_retrieval(mocker, requests_mock): """ Given: - A client object. - Mock data for company findings. When: - Mocking the 'dateparser.parse' function to return a specific datetime. - Mocking the 'get_company_findings' function to return findings data. Then: - Calling the 'get_modified_remote_data_command' function with the provided client and arguments. - Verify the function returns the expected modified incident IDs. """ # Mock dateparser and get_company_findings client = bitsight.Client(base_url=BASE_URL) mocker.patch("BitSightForSecurityPerformanceManagement.dateparser.parse", return_value=datetime(2025, 9, 14, 10)) # Mock the HTTP request using requests_mock mock_response = { "results": [ {"temporary_id": "A9jq", "rolledup_observation_id": "dummy_id_1", "first_seen": "2025-09-14"}, {"temporary_id": "A9jr", "rolledup_observation_id": "dummy_id_2", "first_seen": "2025-09-14"}, ] } requests_mock.get( f"{BASE_URL}/v1/companies/test-guid/findings?limit=5000&sort=-last_seen&last_remediation_status_date_gte=2025-09-14", json=mock_response, status_code=200, ) args = {"lastUpdate": "2025-09-14T10:00:00+00:00", "guid": "test-guid"} result = bitsight.get_modified_remote_data_command(client, args) # Assertions # Verify the HTTP request was made correctly assert requests_mock.called bitsight.dateparser.parse.assert_called_with("2025-09-14T10:00:00+00:00", settings={"TIMEZONE": "UTC"}) assert "dummy_id_1-#-2025-09-14" in result.modified_incident_ids assert "dummy_id_2-#-2025-09-14" in result.modified_incident_ids def test_get_modified_remote_command_with_empty_results(mocker): """ Given: - A client object. When: - Mocking the 'dateparser.parse' function to return a specific datetime. - Mocking the 'get_company_findings' function to return empty results. Then: - Calling the 'get_modified_remote_data_command' function with the provided client and arguments. - Verify the function returns an empty list of modified incident IDs. """ # Mock dateparser and get_company_findings client = bitsight.Client(base_url=BASE_URL) mocker.patch("BitSightForSecurityPerformanceManagement.dateparser.parse", return_value=datetime(2025, 9, 14, 10)) mocker.patch.object(client, "get_company_findings", return_value={"results": []}) args = {"lastUpdate": "2025-09-14T10:00:00+00:00", "guid": "test-guid"} result = bitsight.get_modified_remote_data_command(client, args) # Assertions bitsight.dateparser.parse.assert_called_with("2025-09-14T10:00:00+00:00", settings={"TIMEZONE": "UTC"}) client.get_company_findings.assert_called_once_with( guid="test-guid", first_seen=None, last_seen=None, optional_params={"limit": 5000, "sort": "-last_seen", "last_remediation_status_date_gte": "2025-09-14"}, ) assert result.modified_incident_ids == [] def test_get_modified_remote_data_command_with_duplicate_findings(mocker): """ Given: - A client object. - Mock data for company findings with duplicate IDs. When: - Mocking the 'dateparser.parse' function to return a specific datetime. - Mocking the 'get_company_findings' function to return findings with duplicate IDs. Then: - Calling the 'get_modified_remote_data_command' function with the provided client and arguments. - Verify the function returns unique modified incident IDs (duplicates filtered out). """ # Mock dateparser and get_company_findings client = bitsight.Client(base_url=BASE_URL) mocker.patch("BitSightForSecurityPerformanceManagement.dateparser.parse", return_value=datetime(2025, 9, 14, 10)) mocker.patch.object( client, "get_company_findings", return_value={ "results": [ {"temporary_id": "A9jq", "rolledup_observation_id": "dummy_id_1", "first_seen": "2025-09-14"}, {"temporary_id": "A9jq", "rolledup_observation_id": "dummy_id_1", "first_seen": "2025-09-14"}, # Duplicate {"temporary_id": "A9jr", "rolledup_observation_id": "dummy_id_2", "first_seen": "2025-09-14"}, ] }, ) args = {"lastUpdate": "2025-09-14T10:00:00+00:00", "guid": "test-guid"} result = bitsight.get_modified_remote_data_command(client, args) # Assertions client.get_company_findings.assert_called_once_with( guid="test-guid", first_seen=None, last_seen=None, optional_params={"limit": 5000, "sort": "-last_seen", "last_remediation_status_date_gte": "2025-09-14"}, ) # Should have unique IDs only assert len(result.modified_incident_ids) == 2 assert "dummy_id_1-#-2025-09-14" in result.modified_incident_ids assert "dummy_id_2-#-2025-09-14" in result.modified_incident_ids def test_get_remote_data_command_successful_retrieval(requests_mock, mocker): """ Given: - A client object. - Valid mirror ID and arguments. When: - Mocking the 'get_company_findings' function to return finding data. - Mocking the 'get_remediations' function to return remediation status. - Mocking the 'get_finding_comments' function to return comments. Then: - Calling the 'get_remote_data_command' function with the provided client and arguments. - Verify the function returns the expected finding data and new entries. """ client = bitsight.Client(base_url=BASE_URL) inp_args = { "guid": "123", "close_status_of_bitsight": "Closed", "open_status_of_bitsight": "Work In Progress", } mocker.patch.object(demisto, "params", return_value=inp_args) # Mock finding data finding_data = { "temporary_id": "A9jq", "affects_rating": True, "rolledup_observation_id": "dummy_id_1", "evidence_key": "test.com", "risk_vector": "ssl_certificates", "severity": 5.0, "first_seen": "2025-09-14", } # Mock HTTP requests using requests_mock # Mock get_company_findings request requests_mock.get( f"{BASE_URL}/v1/companies/test-guid/findings?rolledup_observation_id=dummy_id_1&sort=-last_seen&first_seen=2025-09-14", json={"results": [finding_data]}, status_code=200, ) # Mock get_remediations request remediations_url = ( f"{BASE_URL}/ratings/v1/remediations?" "company_guid=test-guid&rolledup_observation_id=dummy_id_1&evidence_key=test.com&risk_vector=ssl_certificates" ) requests_mock.get( remediations_url, json={"results": [{"status": {"value": "Open"}}]}, status_code=200, ) mock_comments_response = { "results": [ { "guid": "comment-1", "message": "Test comment", "author": {"name": "Test User"}, "created_time": "2025-09-14T10:00:00Z", "last_update_time": "2025-09-14T11:00:00Z", } ] } # Mock get_finding_comments request requests_mock.get( f"{BASE_URL}/ratings/v1/companies/test-guid/findings/dummy_id_1/comments", json=mock_comments_response, status_code=200, ) args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"} result = bitsight.get_remote_data_command(client, args) # Assertions # Verify HTTP requests were made correctly assert requests_mock.call_count == 3 # get_company_findings, get_remediations, get_finding_comments # Verify the results assert result.mirrored_object["temporary_id"] == "A9jq" assert result.mirrored_object["remediation_status"] == "Open" assert len(result.entries) == 1 assert "Test comment" in result.entries[0]["Contents"] assert result.entries[0]["Type"] == EntryType.NOTE def test_get_remote_data_command_finding_not_found(mocker): """ Given: - A client object. - Valid mirror ID but finding doesn't exist. When: - Mocking the 'get_company_findings' function to return empty results. Then: - Calling the 'get_remote_data_command' function should return empty GetRemoteDataResponse object. """ client = bitsight.Client(base_url=BASE_URL) mocker.patch.object(client, "get_company_findings", return_value={"results": []}) args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"} result = bitsight.get_remote_data_command(client, args) assert result.mirrored_object == {} assert result.entries == [] def test_get_remote_data_command_with_remediations_error(mocker): """ Given: - A client object. - Valid finding data but remediations API fails. When: - Mocking the 'get_company_findings' function to return finding data. - Mocking the 'get_remediations' function to raise an exception. Then: - Function should continue without remediation data. """ client = bitsight.Client(base_url=BASE_URL) finding_data = { "temporary_id": "A9jq", "affects_rating": True, "rolledup_observation_id": "dummy_id_1", "evidence_key": "test.com", "risk_vector": "ssl_certificates", } mocker.patch.object(client, "get_company_findings", return_value={"results": [finding_data]}) mocker.patch.object(client, "get_remediations", side_effect=DemistoException("API Error")) mocker.patch.object(client, "get_finding_comments", return_value={"results": []}) args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"} result = bitsight.get_remote_data_command(client, args) # Should not have remediation_status due to error assert "remediation_status" not in result.mirrored_object assert result.mirrored_object["temporary_id"] == "A9jq" def test_get_remote_data_command_with_comments_error(mocker): """ Given: - A client object. - Valid finding data but comments API fails. When: - Mocking the 'get_company_findings' function to return finding data. - Mocking the 'get_finding_comments' function to raise an exception. Then: - Function should continue without comment entries. """ client = bitsight.Client(base_url=BASE_URL) inp_args = { "guid": "123", "close_status_of_bitsight": "Closed", "open_status_of_bitsight": "Work In Progress", } mocker.patch.object(demisto, "params", return_value=inp_args) finding_data = { "temporary_id": "A9jq", "affects_rating": True, "rolledup_observation_id": "dummy_id_1", "evidence_key": "test.com", "risk_vector": "ssl_certificates", } mocker.patch.object(client, "get_company_findings", return_value={"results": [finding_data]}) mocker.patch.object(client, "get_remediations", return_value={"results": []}) mocker.patch.object(client, "get_finding_comments", side_effect=DemistoException("API Error")) args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"} result = bitsight.get_remote_data_command(client, args) # Should have no comment entries due to error assert len(result.entries) == 0 assert result.mirrored_object["temporary_id"] == "A9jq" def test_get_remote_data_command_with_old_comments_filtered(mocker): """ Given: - A client object. - Comments that are older than lastUpdate timestamp. When: - Mocking functions to return comments with timestamps before lastUpdate. Then: - Old comments should be filtered out. """ client = bitsight.Client(base_url=BASE_URL) inp_args = { "guid": "123", "close_status_of_bitsight": "Closed", "open_status_of_bitsight": "Work In Progress", "close_active_incident": False, "reopen_closed_incident": False, } mocker.patch.object(demisto, "params", return_value=inp_args) finding_data = { "temporary_id": "A9jq", "affects_rating": True, "rolledup_observation_id": "dummy_id_1", "evidence_key": "test.com", "risk_vector": "ssl_certificates", } mocker.patch.object(client, "get_company_findings", return_value={"results": [finding_data]}) mocker.patch.object(client, "get_remediations", return_value={"results": []}) # Comments with timestamps before lastUpdate (2025-09-14T10:00:00Z) mocker.patch.object( client, "get_finding_comments", return_value={ "results": [ { "guid": "old-comment", "message": "Old comment", "author": {"name": "Test User"}, "created_time": "2025-09-13T10:00:00Z", # Before lastUpdate "last_update_time": "2025-09-13T11:00:00Z", }, { "guid": "old-comment-2", "message": "Old comment", "author": {"name": "Test User"}, "created_time": "2025-09-13T10:00:00Z", # Before lastUpdate "last_update_time": None, }, { "guid": "new-comment", "message": "New comment", "author": {"name": "Test User"}, "created_time": "2025-09-14T11:00:00Z", # After lastUpdate "last_update_time": None, }, ] }, ) args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"} result = bitsight.get_remote_data_command(client, args) # Should only have the new comment assert len(result.entries) == 1 assert "New comment" in result.entries[0]["Contents"] assert result.entries[0]["Type"] == EntryType.NOTE def test_get_remote_data_command_with_invalid_mirror_id(mocker): """ Given: - A client object. - Invalid mirror ID format. When: - Providing mirror ID without proper format. Then: - Function should handle gracefully with empty identifiers. """ client = bitsight.Client(base_url=BASE_URL) mocker.patch.object(client, "get_company_findings", return_value={"results": []}) args = { "id": "invalid_id", # Missing second part after split "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid", } result = bitsight.get_remote_data_command(client, args) assert result.mirrored_object == {} assert result.entries == [] @pytest.mark.parametrize( "close_active_incident,expected_entries", [ (True, 1), # Should close incident when enabled (False, 0), # Should NOT close incident when disabled ], ) def test_get_remote_data_command_with_incident_closure(mocker, close_active_incident, expected_entries): """ Given: - A client object. - Remediation status matches close status. - Finding is in processed findings list. - close_active_incident parameter varies. When: - Calling get_remote_data_command with matching close status. Then: - Incident closure behavior should depend on checkbox parameter. """ client = bitsight.Client(base_url=BASE_URL) inp_args = { "guid": "123", "close_status_of_bitsight": "Resolved", "open_status_of_bitsight": "Open", "close_active_incident": close_active_incident, "reopen_closed_incident": False, } mocker.patch.object(demisto, "params", return_value=inp_args) mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": ["dummy_id_1-#-2025-09-14"]}) mocker.patch.object(demisto, "setIntegrationContext") finding_data = { "temporary_id": "A9jq", "affects_rating": True, "rolledup_observation_id": "dummy_id_1", "evidence_key": "test.com", "risk_vector": "ssl_certificates", } mocker.patch.object(client, "get_company_findings", return_value={"results": [finding_data]}) mocker.patch.object(client, "get_remediations", return_value={"results": [{"status": {"value": "Resolved"}}]}) mocker.patch.object(client, "get_finding_comments", return_value={"results": []}) args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"} result = bitsight.get_remote_data_command(client, args) # Verify behavior based on parameter assert len(result.entries) == expected_entries if expected_entries > 0: assert result.entries[0]["Contents"]["dbotIncidentClose"] is True assert result.mirrored_object["remediation_status"] == "Resolved" @pytest.mark.parametrize( "reopen_closed_incident,expected_entries", [ (True, 1), # Should reopen incident when enabled (False, 0), # Should NOT reopen incident when disabled ], ) def test_get_remote_data_command_with_incident_reopen(mocker, reopen_closed_incident, expected_entries): """ Given: - A client object. - Remediation status matches open status. - Finding is NOT in processed findings list. - reopen_closed_incident parameter varies. When: - Calling get_remote_data_command with matching open status. Then: - Incident reopen behavior should depend on checkbox parameter. """ client = bitsight.Client(base_url=BASE_URL) inp_args = { "guid": "123", "close_status_of_bitsight": "Resolved", "open_status_of_bitsight": "Open", "close_active_incident": False, "reopen_closed_incident": reopen_closed_incident, } mocker.patch.object(demisto, "params", return_value=inp_args) mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": []}) mocker.patch.object(demisto, "setIntegrationContext") finding_data = { "temporary_id": "A9jq", "affects_rating": True, "rolledup_observation_id": "dummy_id_1", "evidence_key": "test.com", "risk_vector": "ssl_certificates", } mocker.patch.object(client, "get_company_findings", return_value={"results": [finding_data]}) mocker.patch.object(client, "get_remediations", return_value={"results": [{"status": {"value": "Open"}}]}) mocker.patch.object(client, "get_finding_comments", return_value={"results": []}) args = {"id": "dummy_id_1-#-2025-09-14", "lastUpdate": "2025-09-14T10:00:00Z", "guid": "test-guid"} result = bitsight.get_remote_data_command(client, args) # Verify behavior based on parameter assert len(result.entries) == expected_entries if expected_entries > 0: assert result.entries[0]["Contents"]["dbotIncidentReopen"] is True assert result.mirrored_object["remediation_status"] == "Open" def test_update_remote_system_command_incident_closed(mocker): """ Given: - A client object. - Incident status is DONE (closed). - Incident has changed. When: - Calling update_remote_system_command with closed incident. Then: - Should call update_external_status with close_status_of_bitsight. - Should return the remote incident ID. """ client = bitsight.Client(base_url=BASE_URL) # Mock UpdateRemoteSystemArgs mock_parsed_args = mocker.MagicMock() mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14" mock_parsed_args.data = { "bitsightrolledupobservationid": "obs-123", "bitsightriskvector": "ssl_certificates", "bitsightevidencekey": "test.com", "id": "xsoar-456", } mock_parsed_args.inc_status = bitsight.IncidentStatus.DONE mock_parsed_args.delta = {} mock_parsed_args.incident_changed = True mock_parsed_args.entries = [] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": ["dummy_id_1-#-2025-09-14"]}) mocker.patch.object(client, "update_external_status") mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123") args = {"guid": "company-guid", "user_email": "test@example.com"} close_status = "Closed" open_status = "Open" result = bitsight.update_remote_system_command(client, args, close_status, open_status) # Assertions expected_body = { "rolledup_observation_id": "obs-123", "evidence_key": "test.com", "risk_vector": "ssl_certificates", "status": {"value": "Closed", "public": False}, } client.update_external_status.assert_called_once_with(company_guid="company-guid", body=expected_body) assert result == "dummy_id_1-#-2025-09-14" def test_update_remote_system_command_with_comments(mocker): """ Given: - A client object. - New entries (comments) to mirror to BitSight. - Existing comments in BitSight for thread_guid. When: - Calling update_remote_system_command with new entries. Then: - Should call get_finding_comments to get existing comments. - Should call create_finding_comment for each new entry. - Should format comment content properly with XSOAR metadata. """ client = bitsight.Client(base_url=BASE_URL) # Mock UpdateRemoteSystemArgs with new entries mock_parsed_args = mocker.MagicMock() mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14" mock_parsed_args.data = { "bitsightrolledupobservationid": "obs-456", "bitsightriskvector": "ssl_certificates", "bitsightevidencekey": "test.com", "id": "xsoar-789", } mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE mock_parsed_args.delta = {} mock_parsed_args.incident_changed = True mock_parsed_args.entries = [ { "id": "entry-1", "type": "note", "contents": "This is a test comment from XSOAR", "user": "admin", }, { "id": "entry-2", "type": "note", "contents": "Another comment\nwith multiple lines", "user": "admin", }, ] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": ["dummy_id_1-#-2025-09-14"]}) mocker.patch.object(demisto, "setIntegrationContext") # Mock existing comments response existing_comments = {"results": [{"thread_guid": "thread-123", "guid": "comment-1", "message": "Existing comment"}]} mocker.patch.object(client, "get_finding_comments", return_value=existing_comments) mocker.patch.object(client, "create_finding_comment") mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123") args = {"guid": "company-guid", "user_email": "test@example.com"} close_status = "Closed" open_status = "Open" result = bitsight.update_remote_system_command(client, args, close_status, open_status) # Verify get_finding_comments was called client.get_finding_comments.assert_called_once_with(company_guid="company-guid", rolledup_observation_id="obs-456") # Verify create_finding_comment was called for each entry assert client.create_finding_comment.call_count == 2 # Check first comment call expected_message_1 = ( "[Mirrored From XSOAR] XSOAR Incident ID: xsoar-789\n\nNote: This is a test comment from XSOAR\n\nAdded By: admin" ) first_call_args = client.create_finding_comment.call_args_list[0] assert first_call_args.kwargs["company_guid"] == "company-guid" assert first_call_args.kwargs["rolledup_observation_id"] == "obs-456" assert first_call_args.kwargs["thread_guid"] == "thread-123" # Check the body contains the expected comment structure body_1 = first_call_args.kwargs["body"] assert body_1["author_guid"] == "user-123" assert body_1["message"] == expected_message_1 assert body_1["public"] is False # Check second comment call expected_message_2 = ( "[Mirrored From XSOAR] XSOAR Incident ID: xsoar-789\n\nNote: Another comment\n\nwith multiple lines\n\nAdded By: admin" ) second_call_args = client.create_finding_comment.call_args_list[1] assert second_call_args.kwargs["company_guid"] == "company-guid" assert second_call_args.kwargs["rolledup_observation_id"] == "obs-456" assert second_call_args.kwargs["thread_guid"] == "thread-123" # Check the body contains the expected comment structure body_2 = second_call_args.kwargs["body"] assert body_2["author_guid"] == "user-123" assert body_2["message"] == expected_message_2 assert body_2["public"] is False assert result == "dummy_id_1-#-2025-09-14" def test_update_remote_system_command_with_comments_no_existing_thread(mocker): """ Given: - A client object. - New entries to mirror to BitSight. - No existing comments in BitSight (empty thread_guid). When: - Calling update_remote_system_command with new entries. Then: - Should call create_finding_comment with empty thread_guid. """ client = bitsight.Client(base_url=BASE_URL) # Mock UpdateRemoteSystemArgs with new entries mock_parsed_args = mocker.MagicMock() mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14" mock_parsed_args.data = { "bitsightrolledupobservationid": "obs-789", "bitsightriskvector": "ssl_certificates", "bitsightevidencekey": "test.com", "id": "xsoar-123", } mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE mock_parsed_args.delta = {} mock_parsed_args.incident_changed = False mock_parsed_args.entries = [ { "id": "entry-1", "type": "note", "contents": "First comment in new thread", "user": "dbot", } ] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": []}) mocker.patch.object(demisto, "setIntegrationContext") # Mock empty comments response mocker.patch.object(client, "get_finding_comments", return_value={"results": []}) mocker.patch.object(client, "create_finding_comment") mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123") args = {"guid": "company-guid", "user_email": "test@example.com"} close_status = "Closed" open_status = "Open" result = bitsight.update_remote_system_command(client, args, close_status, open_status) # Verify create_finding_comment was called with empty thread_guid expected_message = "[Mirrored From XSOAR] XSOAR Incident ID: xsoar-123\n\nNote: First comment in new thread\n\nAdded By: dbot" call_args = client.create_finding_comment.call_args assert call_args.kwargs["company_guid"] == "company-guid" assert call_args.kwargs["rolledup_observation_id"] == "obs-789" assert call_args.kwargs["thread_guid"] == "" # Empty thread_guid for new thread # Check the body contains the expected comment structure body = call_args.kwargs["body"] assert body["comments"][0]["author_guid"] == "user-123" assert body["comments"][0]["message"] == expected_message assert body["comments"][0]["public"] is False assert result == "dummy_id_1-#-2025-09-14" def test_update_remote_system_command_incident_active_no_delta(mocker): """ Given: - A client object. - Incident status is ACTIVE. - No delta (no changes). - Incident has changed. When: - Calling update_remote_system_command with active incident and no delta. Then: - Should call update_external_status with open_status_of_bitsight. - Should return the remote incident ID. """ client = bitsight.Client(base_url=BASE_URL) # Mock UpdateRemoteSystemArgs mock_parsed_args = mocker.MagicMock() mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14" mock_parsed_args.data = { "bitsightrolledupobservationid": "obs-456", "bitsightriskvector": "open_ports", "bitsightevidencekey": "0.0.0.1", "id": "xsoar-789", } mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE mock_parsed_args.delta = {} # No changes mock_parsed_args.incident_changed = True mock_parsed_args.entries = [] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch.object(client, "update_external_status") mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123") args = {"guid": "company-guid", "user_email": "test@example.com"} close_status = "Closed" open_status = "Open" result = bitsight.update_remote_system_command(client, args, close_status, open_status) # Assertions expected_body = { "rolledup_observation_id": "obs-456", "evidence_key": "0.0.0.1", "risk_vector": "open_ports", "status": {"value": "Open", "public": False}, } client.update_external_status.assert_called_once_with(company_guid="company-guid", body=expected_body) assert result == "dummy_id_1-#-2025-09-14" def test_update_remote_system_command_incident_active_with_delta(mocker): """ Given: - A client object. - Incident status is ACTIVE. - Has delta (changes present). - Incident has changed. When: - Calling update_remote_system_command with active incident and delta. Then: - Should NOT call update_external_status (should_update_status is False). - Should return the remote incident ID. """ client = bitsight.Client(base_url=BASE_URL) # Mock UpdateRemoteSystemArgs mock_parsed_args = mocker.MagicMock() mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14" mock_parsed_args.data = { "bitsightrolledupobservationid": "obs-789", "bitsightriskvector": "malware", "bitsightevidencekey": "malware.com", "id": "xsoar-101", } mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE mock_parsed_args.delta = {"severity": "high"} # Has changes mock_parsed_args.incident_changed = True mock_parsed_args.entries = [] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch.object(client, "update_external_status") mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123") args = {"guid": "company-guid", "user_email": "test@example.com"} close_status = "Closed" open_status = "Open" result = bitsight.update_remote_system_command(client, args, close_status, open_status) # Assertions - should NOT call update_external_status client.update_external_status.assert_not_called() assert result == "dummy_id_1-#-2025-09-14" def test_update_remote_system_command_incident_not_changed(mocker): """ Given: - A client object. - Incident status is DONE. - Incident has NOT changed. When: - Calling update_remote_system_command with unchanged incident. Then: - Should NOT call update_external_status (incident_changed is False). - Should return the remote incident ID. """ client = bitsight.Client(base_url=BASE_URL) # Mock UpdateRemoteSystemArgs mock_parsed_args = mocker.MagicMock() mock_parsed_args.remote_incident_id = "remote-999" mock_parsed_args.data = { "bitsightrolledupobservationid": "obs-999", "bitsightriskvector": "ssl_certificates", "bitsightevidencekey": "test.com", "id": "xsoar-999", } mock_parsed_args.inc_status = bitsight.IncidentStatus.DONE mock_parsed_args.delta = {} mock_parsed_args.incident_changed = False # Not changed mock_parsed_args.entries = [] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch.object(client, "update_external_status") mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123") args = {"guid": "company-guid", "user_email": "test@example.com"} close_status = "Closed" open_status = "Open" result = bitsight.update_remote_system_command(client, args, close_status, open_status) # Assertions - should NOT call update_external_status client.update_external_status.assert_not_called() assert result == "remote-999" def test_update_remote_system_command_with_missing_data_fields(mocker): """ Given: - A client object. - Missing some data fields in parsed_args.data. When: - Calling update_remote_system_command with missing fields. Then: - Should handle missing fields gracefully with empty strings. - Should call update_external_status with empty values. """ client = bitsight.Client(base_url=BASE_URL) # Mock UpdateRemoteSystemArgs with missing fields mock_parsed_args = mocker.MagicMock() mock_parsed_args.remote_incident_id = "remote-empty" mock_parsed_args.data = { "id": "xsoar-empty" # Missing bitsightrolledupobservationid, bitsightriskvector, bitsightevidencekey } mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE mock_parsed_args.delta = {} mock_parsed_args.incident_changed = True mock_parsed_args.entries = [] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch.object(client, "update_external_status") mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123") args = {"guid": "company-guid", "user_email": "test@example.com"} close_status = "Closed" open_status = "Open" result = bitsight.update_remote_system_command(client, args, close_status, open_status) # Assertions - should handle missing fields with empty strings expected_body = { "rolledup_observation_id": "", "evidence_key": "", "risk_vector": "", "status": {"value": "Open", "public": False}, } client.update_external_status.assert_called_once_with(company_guid="company-guid", body=expected_body) assert result == "remote-empty" def test_update_remote_system_command_api_error(mocker): """ Given: - A client object. - update_external_status raises an exception. When: - Calling update_remote_system_command and API fails. Then: - Should propagate the exception. """ client = bitsight.Client(base_url=BASE_URL) # Mock UpdateRemoteSystemArgs mock_parsed_args = mocker.MagicMock() mock_parsed_args.remote_incident_id = "remote-error" mock_parsed_args.data = { "bitsightrolledupobservationid": "obs-error", "bitsightriskvector": "ssl_certificates", "bitsightevidencekey": "test.com", "id": "xsoar-error", } mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE mock_parsed_args.delta = {} mock_parsed_args.incident_changed = True mock_parsed_args.entries = [] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123") mocker.patch.object(client, "update_external_status", side_effect=DemistoException("API Error")) args = {"guid": "company-guid", "user_email": "test@example.com"} close_status = "Closed" open_status = "Open" # Should raise the exception with pytest.raises(DemistoException): bitsight.update_remote_system_command(client, args, close_status, open_status) def test_update_remote_system_command_incident_reopen(mocker): """ Given: - A client object. - Incident status is ACTIVE and incident has been reopened after being closed. - Delta contains closingUserId="" and runStatus="" indicating reopen. When: - Calling update_remote_system_command with reopened incident. Then: - Should call update_external_status with open_status_of_bitsight. - Should return the remote incident ID. """ client = bitsight.Client(base_url=BASE_URL) # Mock UpdateRemoteSystemArgs mock_parsed_args = mocker.MagicMock() mock_parsed_args.remote_incident_id = "remote-reopen-123" mock_parsed_args.data = { "bitsightrolledupobservationid": "obs-reopen-456", "bitsightriskvector": "web_application_security", "bitsightevidencekey": "example.com", "id": "xsoar-reopen-789", } mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE mock_parsed_args.delta = {"closingUserId": "", "runStatus": ""} # Indicates incident reopen mock_parsed_args.incident_changed = True mock_parsed_args.entries = [] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch.object(client, "update_external_status") mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123") args = {"guid": "company-guid-reopen"} close_status = "Risk Accepted" open_status = "Open" result = bitsight.update_remote_system_command(client, args, close_status, open_status) # Assertions expected_body = { "rolledup_observation_id": "obs-reopen-456", "evidence_key": "example.com", "risk_vector": "web_application_security", "status": {"value": "Open", "public": False}, } client.update_external_status.assert_called_once_with(company_guid="company-guid-reopen", body=expected_body) assert result == "remote-reopen-123" def test_get_current_user_guid_success_with_valid_email(mocker, requests_mock): """ Given: - A client object and valid user email address. When: - Calling get_current_user_guid with valid email. Then: - Should return the user GUID from API response and cache it. """ from BitSightForSecurityPerformanceManagement import get_current_user_guid client = bitsight.Client(base_url=BASE_URL) user_email = "test@example.com" expected_guid = "user-guid-123" # Mock HTTP request using requests_mock mock_response = {"results": [{"guid": expected_guid, "email": user_email}]} requests_mock.get(f"{BASE_URL}/ratings/v2/users?email={user_email}", json=mock_response, status_code=200) mocker.patch.object(demisto, "getIntegrationContext", return_value={"users_guids": {}}) mocker.patch.object(demisto, "setIntegrationContext") result = get_current_user_guid(client, user_email) assert result == expected_guid assert requests_mock.call_count == 1 def test_test_module_with_mirroring_missing_user_email(requests_mock): """ Given: - Integration parameters with mirroring enabled but missing user email. When: - Running test_module function. Then: - Should raise ValueError with USER_EMAIL_REQUIRED message. """ client = bitsight.Client(base_url=BASE_URL) params = {"apikey": "test-key", "mirror_direction": "Outgoing", "user_email": "", "isFetch": False} # Mock HTTP request for companies companies_response = {"companies": [{"guid": "valid-guid-123", "name": "Test Company"}]} requests_mock.get(f"{BASE_URL}/v1/companies", json=companies_response, status_code=200) with pytest.raises(ValueError) as e: bitsight.test_module(client, params) assert str(e.value) == bitsight.ERROR_MESSAGES["USER_EMAIL_REQUIRED"] def test_test_module_with_mirroring_invalid_user_email(mocker, requests_mock): """ Given: - Integration parameters with mirroring enabled and invalid user email. When: - Running test_module function with non-existent email. Then: - Should raise ValueError with USER_GUID_NOT_FOUND message. """ client = bitsight.Client(base_url=BASE_URL) params = {"apikey": "test-key", "mirror_direction": "Outgoing", "user_email": "invalid@example.com", "isFetch": False} # Mock HTTP requests using requests_mock companies_response = {"companies": [{"guid": "valid-guid-123", "name": "Test Company"}]} user_response = {"results": []} # No user found requests_mock.get(f"{BASE_URL}/v1/companies", json=companies_response, status_code=200) requests_mock.get(f"{BASE_URL}/ratings/v2/users?email=invalid@example.com", json=user_response, status_code=200) mocker.patch.object(demisto, "getIntegrationContext", return_value={"users_guids": {}}) mocker.patch.object(demisto, "setIntegrationContext") with pytest.raises(ValueError) as e: bitsight.test_module(client, params) assert str(e.value) == bitsight.ERROR_MESSAGES["USER_GUID_NOT_FOUND"] def test_update_remote_system_command_adds_closing_note_existing_thread(mocker): """ Given: - Mirroring arguments that mark the incident as closed with closing details. - Existing BitSight comment thread for the finding. When: - update_remote_system_command is executed. Then: - A closing note is appended to the existing thread and the remote ID is returned. """ from BitSightForSecurityPerformanceManagement import update_remote_system_command client = bitsight.Client(base_url=BASE_URL) mock_parsed_args = mocker.MagicMock() closing_args = { "bitsightrolledupobservationid": "rolledup-123", "bitsightriskvector": "web_application_security", "bitsightevidencekey": "example.com", "id": "xsoar-incident-1", "closeNotes": "resolved", "closeReason": "Resolved", "closingUserId": "admin", } mock_parsed_args.remote_incident_id = "dummy_id_1-#-2025-09-14" mock_parsed_args.data = closing_args mock_parsed_args.inc_status = bitsight.IncidentStatus.DONE mock_parsed_args.delta = {"closingUserId": "admin"} mock_parsed_args.incident_changed = True mock_parsed_args.entries = [] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-123") mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": ["dummy_id_1-#-2025-09-14"]}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(client, "update_external_status") mocker.patch.object(client, "get_finding_comments", return_value={"results": [{"thread_guid": "thread-456"}]}) mock_create_comment = mocker.patch.object(client, "create_finding_comment") args = {"guid": "company-guid", "user_email": "user@example.com"} result = update_remote_system_command(client, args, "Resolved", "Open") closing_note = ( "[Mirrored From XSOAR] XSOAR Incident ID: xsoar-incident-1\n\n" "Close Reason: Resolved\n\n" "Closed By: admin\n\n" "Close Notes: resolved" ) mock_create_comment.assert_called_once_with( company_guid="company-guid", rolledup_observation_id="rolledup-123", thread_guid="thread-456", body={"author_guid": "user-123", "message": closing_note, "public": False}, ) assert result == "dummy_id_1-#-2025-09-14" def test_update_remote_system_command_adds_closing_note_new_thread(mocker): """ Given: - Mirroring arguments that mark the incident as closed with closing details. - No existing BitSight comment thread for the finding. When: - update_remote_system_command is executed. Then: - A closing note is created in a new thread and the remote ID is returned. """ from BitSightForSecurityPerformanceManagement import update_remote_system_command client = bitsight.Client(base_url=BASE_URL) mock_parsed_args = mocker.MagicMock() closing_args = { "bitsightrolledupobservationid": "rolledup-456", "bitsightriskvector": "malware", "bitsightevidencekey": "malware.example.com", "id": "xsoar-incident-2", "closeNotes": "handled", "closeReason": "Mitigated", "closingUserId": "analyst", } mock_parsed_args.remote_incident_id = "dummy_id_2-#-2025-11-25" mock_parsed_args.data = closing_args mock_parsed_args.inc_status = bitsight.IncidentStatus.DONE mock_parsed_args.delta = {"closingUserId": "analyst"} mock_parsed_args.incident_changed = True mock_parsed_args.entries = [] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-456") mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": ["dummy_id_2-#-2025-11-25"]}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(client, "update_external_status") mocker.patch.object(client, "get_finding_comments", return_value={}) mock_create_comment = mocker.patch.object(client, "create_finding_comment") args = {"guid": "company-guid", "user_email": "user@example.com"} result = update_remote_system_command(client, args, "Resolved", "Open") closing_note = ( "[Mirrored From XSOAR] XSOAR Incident ID: xsoar-incident-2\n\n" "Close Reason: Mitigated\n\n" "Closed By: analyst\n\n" "Close Notes: handled" ) mock_create_comment.assert_called_once_with( company_guid="company-guid", rolledup_observation_id="rolledup-456", thread_guid="", body={"comments": [{"author_guid": "user-456", "message": closing_note, "public": False}]}, ) assert result == "dummy_id_2-#-2025-11-25" def test_update_remote_system_command_does_not_add_closing_note_when_status_not_done(mocker): """ Given: - Mirroring arguments where closing user is provided but the incident status is not DONE. When: - update_remote_system_command is executed. Then: - No closing note is mirrored to BitSight. """ from BitSightForSecurityPerformanceManagement import update_remote_system_command client = bitsight.Client(base_url=BASE_URL) mock_parsed_args = mocker.MagicMock() closing_args = { "bitsightrolledupobservationid": "rolledup-789", "bitsightriskvector": "botnet_infections", "bitsightevidencekey": "bot.example.com", "id": "xsoar-incident-3", "closeNotes": "monitor", "closeReason": "Monitoring", "closingUserId": "responder", } mock_parsed_args.remote_incident_id = "Bitsight Finding - rolledup-789" mock_parsed_args.data = closing_args mock_parsed_args.inc_status = bitsight.IncidentStatus.ACTIVE mock_parsed_args.delta = {"closingUserId": "responder"} mock_parsed_args.incident_changed = True mock_parsed_args.entries = [] mocker.patch("BitSightForSecurityPerformanceManagement.UpdateRemoteSystemArgs", return_value=mock_parsed_args) mocker.patch("BitSightForSecurityPerformanceManagement.get_current_user_guid", return_value="user-789") mocker.patch.object(demisto, "getIntegrationContext", return_value={"processed_findings": []}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(client, "update_external_status") mock_get_comments = mocker.patch.object(client, "get_finding_comments") mock_create_comment = mocker.patch.object(client, "create_finding_comment") args = {"guid": "company-guid", "user_email": "user@example.com"} result = update_remote_system_command(client, args, "Resolved", "Open") mock_get_comments.assert_not_called() mock_create_comment.assert_not_called() assert result == "Bitsight Finding - rolledup-789"