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.
"""Main file for BitSightForSecurityPerformanceManagement Integration.""" from datetime import datetime import demistomock as demisto # noqa: F401 import requests import urllib3 from CommonServerPython import * # noqa: F401 """CONSTANTS""" BITSIGHT_DATE_TIME_FORMAT = "%Y-%m-%d" DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # ISO8601 format with UTC, default in XSOAR DEFAULT_FIRST_FETCH_DAYS = 3 DEFAULT_FETCH_LIMIT = 25 MAX_FETCH_LIMIT = 200 BASE_URL = "https://api.bitsighttech.com" MAX_LIMIT = 1000 DEFAULT_LIMIT = 100 DEFAULT_OFFSET = 0 MAX_FINDINGS_MIRRORING_LIMIT = 5000 OUTGOING_MIRROR_DIRECTION = "outgoing" INCOMING_AND_OUTGOING_MIRROR_DIRECTION = "incoming and outgoing" DEFAULT_CLOSE_STATUS_OF_BITSIGHT = "Resolved" DEFAULT_OPEN_STATUS_OF_BITSIGHT = "Open" ERROR_MESSAGES = { "GUID_REQUIRED": "Must provide a GUID.", "GUID_NOT_FETCHED": "Unable to fetch GUID.", "GUID_NOT_AVAILABLE": "Provided 'Company's GUID' is not available/valid." ' Please input a GUID retrieved using the command "bitsight-companies-guid-get".', "INVALID_SELECT": "'{}' is an invalid value for '{}'. Value must be in {}.", "INVALID_MAX_FETCH": f"Parameter 'Max Fetch' is not a valid number. Please provide a number in range 1 to {MAX_FETCH_LIMIT}.", "NEGATIVE_FIRST_FETCH": "Parameter 'First fetch time in days' should be a number greater than or equal to 0.", "LIMIT_GREATER_THAN_ALLOWED": f"Argument 'limit' should be a number less than or equal to {MAX_LIMIT}.", "USER_EMAIL_REQUIRED": "User email address is required for outgoing mirroring.", "USER_GUID_NOT_FOUND": "User GUID not found for the provided user email.", } SEVERITY_MAPPING = {"minor": 1, "moderate": 4, "material": 7, "severe": 9} ASSET_CATEGORY_MAPPING = { "low": "low,medium,high,critical", "medium": "medium,high,critical", "high": "high,critical", "critical": "critical", } VALID_AFFECT_RATING_REASON = ["Yes", "No: Grace Period", "No: Incubation Period"] AFFECT_RATING_REASON_MAPPING = { "yes": "AFFECTS_RATING", "no: grace period": "GRACE_PERIOD", "no: incubation period": "INCUBATION_PERIOD", } RISK_VECTOR_MAPPING = { "web application headers": "application_security", "botnet infections": "botnet_infections", "breaches": "data_breaches", "desktop software": "desktop_software", "dkim": "dkim", "dnssec": "dnssec", "file sharing": "file_sharing", "insecure systems": "insecure_systems", "malware servers": "malware_servers", "mobile app publications": "mobile_app_publications", "mobile application security": "mobile_application_security", "mobile software": "mobile_software", "open ports": "open_ports", "patching cadence": "patching_cadence", "potentially exploited": "potentially_exploited", "server software": "server_software", "spam propagation": "spam_propagation", "spf": "SPF", "ssl certificates": "ssl_certificates", "ssl configurations": "ssl_configurations", "unsolicited communications": "unsolicited_comm", "web application security": "web_appsec", "dmarc": "dmarc", } MIRROR_DIRECTION = { "Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both", } PACK_VERSION = get_pack_version() or "1.3.6" CALLING_PLATFORM_VERSION = "XSOAR" CONNECTOR_NAME_VERSION = f"Cortex Palo Alto - XSOAR: Bitsight - version {PACK_VERSION}" # Disable insecure warnings urllib3.disable_warnings() class Client(BaseClient): """Client will implement the service API, should not contain Cortex XSOAR logic. \ Should do requests and return data.""" def get_companies_guid(self): """Retrieve subscribed company details.""" uri = "v1/companies" return self._http_request(method="GET", url_suffix=uri) def get_company_detail(self, guid): """ Retrieve company details based on its Guid. :param guid: guid of the company whose details need to be retrieved """ uri = f"v1/companies/{encode_string_results(guid)}" return self._http_request(method="GET", url_suffix=uri) def get_company_findings(self, guid, first_seen, last_seen, optional_params=None): """ Retrieve company findings based on its Guid. :param guid: guid of the company whose findings need to be retrieved :param first_seen: first seen date (YYYY-MM-DD) of the findings :param last_seen: last seen date (YYYY-MM-DD) of the findings :param optional_params: params to be passed to the findings endpoint """ uri = f"v1/companies/{encode_string_results(guid)}/findings" params = {"first_seen_gte": first_seen, "last_seen_lte": last_seen, "unsampled": "true", "expand": "attributed_companies"} if optional_params: params.update(optional_params) remove_nulls_from_dictionary(params) return self._http_request(method="GET", url_suffix=uri, params=params) def get_finding_comments(self, company_guid: str, rolledup_observation_id: str): """ Retrieve finding comments based on its Guid. :type company_guid: str :param company_guid: guid of the company whose comments need to be retrieved :type rolledup_observation_id: str :param rolledup_observation_id: rolledup_observation_id of the finding """ encoded_company_guid = encode_string_results(company_guid) encoded_rolledup_observation_id = encode_string_results(rolledup_observation_id) uri = f"ratings/v1/companies/{encoded_company_guid}/findings/{encoded_rolledup_observation_id}/comments" return self._http_request(method="GET", url_suffix=uri) def get_remediations(self, params: dict | None = None): """ Retrieve remediations. :type params: dict | None :param params: params to be passed to the remediations endpoint """ uri = "ratings/v1/remediations" remove_nulls_from_dictionary(params) return self._http_request(method="GET", url_suffix=uri, params=params) def update_external_status(self, company_guid: str, body: dict): """ Update external status of a finding. :type company_guid: str :param company_guid: guid of the company whose findings need to be updated :type body: dict :param body: body to be passed to the findings endpoint """ uri = f"ratings/v1/remediations/{encode_string_results(company_guid)}" remove_nulls_from_dictionary(body) return self._http_request(method="PATCH", url_suffix=uri, json_data=body) def create_finding_comment(self, company_guid: str, rolledup_observation_id: str, thread_guid: str, body: dict): """ Create a finding comment based on its Guid. :type company_guid: str :param company_guid: guid of the company whose comments need to be retrieved :type rolledup_observation_id: str :param rolledup_observation_id: rolledup_observation_id of the finding :type thread_guid: str :param thread_guid: thread_guid of the finding :type body: dict :param body: body to be passed to the findings endpoint """ encoded_company_guid = encode_string_results(company_guid) encoded_rolledup_observation_id = encode_string_results(rolledup_observation_id) encoded_thread_guid = encode_string_results(thread_guid) if thread_guid: uri = f"annotations/v1/threads/{encoded_thread_guid}/comments" return self._http_request(method="POST", url_suffix=uri, json_data=body, ok_codes=[200, 201]) else: params = {"company_guid": encoded_company_guid, "record_id": encoded_rolledup_observation_id} uri = "annotations/v1/threads" return self._http_request(method="POST", url_suffix=uri, params=params, json_data=body, ok_codes=[200, 201]) def get_users(self, params: dict | None = None): """ Retrieve user details from BitSight platform. :type params: dict | None :param params: Optional parameters to be passed to the users endpoint :return: Response containing user details """ uri = "ratings/v2/users" remove_nulls_from_dictionary(params) return self._http_request(method="GET", url_suffix=uri, params=params) """HELPER FUNCTIONS""" def trim_spaces_from_args(args): """ Trim spaces from values of the args dict. :param args: Dict to trim spaces from :type args: dict :return: """ for key, val in args.items(): if isinstance(val, str): args[key] = val.strip() return args def camelize_strings_with_underscore(string: str): """ Wrap CommonServerPython's camelize_string to also convert Pascal strings. :param string: string to convert to camel case """ if string.find("_") == -1: return string[0].lower() + string[1:] else: return camelize_string(string, upper_camel=False) def camelize_dict_recursively(src): """ Camelize all the keys in a dictionary with nested dictionaries and lists. :param src: the dictionary to camelize """ destination = {} for key, value in src.items(): if isinstance(value, dict): destination[camelize_strings_with_underscore(key)] = camelize_dict_recursively(value) elif isinstance(value, list): if value and isinstance(value[0], dict): destination[camelize_strings_with_underscore(key)] = [ camelize_dict_recursively(list_value) for list_value in value ] else: destination[camelize_strings_with_underscore(key)] = value else: destination[camelize_strings_with_underscore(key)] = value return destination def get_mirroring() -> dict: """ Get the mirroring configuration parameters from the Demisto integration parameters. :return: A dictionary containing the mirroring configuration parameters. :rtype: dict """ params = demisto.params() mirror_direction = params.get("mirror_direction", "None").strip() mirror_tags = params.get("note_tag", "").strip() return { "mirror_direction": MIRROR_DIRECTION.get(mirror_direction), "mirror_tags": mirror_tags, "mirror_instance": demisto.integrationInstance(), } def prepare_and_validate_company_findings_get_filter_args(risk_vector_list, severity, asset_category): """ Prepare and validate arguments for bitsight-company-findings-get. :param risk_vector_list: input from argument risk_vector_label :param severity: input from argument severity :param asset_category: input from argument asset_category """ risk_vector = "" for vector in risk_vector_list: if vector.lower() in RISK_VECTOR_MAPPING: risk_vector += RISK_VECTOR_MAPPING[vector.lower()] + "," else: raise ValueError( ERROR_MESSAGES["INVALID_SELECT"].format( vector.lower(), "risk_vector_label", ", ".join(RISK_VECTOR_MAPPING.keys()) ) ) risk_vector = risk_vector[:-1] severity_gte = None if severity: if severity in SEVERITY_MAPPING: severity_gte = SEVERITY_MAPPING[severity] else: raise ValueError(ERROR_MESSAGES["INVALID_SELECT"].format(severity, "severity", ", ".join(SEVERITY_MAPPING.keys()))) asset_category_eq = None if asset_category: if asset_category in ASSET_CATEGORY_MAPPING: asset_category_eq = ASSET_CATEGORY_MAPPING[asset_category] else: raise ValueError( ERROR_MESSAGES["INVALID_SELECT"].format( asset_category, "asset_category", ", ".join(ASSET_CATEGORY_MAPPING.keys()) ) ) return risk_vector, severity_gte, asset_category_eq def prepare_and_validate_fetch_findings_args(client, args): """ Prepare and validate arguments for company_findings_get_command when fetch_incidents is true. :param client: client to use :param args: arguments obtained from demisto.args() """ guid = args.get("guid", None) if not guid: res = client.get_companies_guid() if res.status_code == 200: res_json = res.json() guid = res_json.get("my_company", {}).get("guid") else: raise DemistoException(ERROR_MESSAGES["GUID_NOT_FETCHED"]) severity = args.get("findings_min_severity", None) if severity: severity = severity.lower() grade_list = args.get("findings_grade", None) grade = ",".join(grade_list) if grade_list else None asset_category = args.get("findings_min_asset_category", None) if asset_category: asset_category = asset_category.lower() risk_vector_list = argToList(args.get("risk_vector")) if "All" in risk_vector_list: risk_vector_list = [] limit = arg_to_number(args.get("max_fetch", DEFAULT_FETCH_LIMIT), "Max Fetch", True) if limit and (limit < 1 or limit > MAX_FETCH_LIMIT): # type: ignore raise ValueError(ERROR_MESSAGES["INVALID_MAX_FETCH"]) affect_rating_reason_list = argToList(args.get("findings_affect_rating_reason", "")) valid_affect_rating_reasons = [] for affect_rating_reason in affect_rating_reason_list: affect_rating_reason = affect_rating_reason.strip() if affect_rating_reason and affect_rating_reason.lower() not in AFFECT_RATING_REASON_MAPPING: raise ValueError( ERROR_MESSAGES["INVALID_SELECT"].format( affect_rating_reason, "Findings Affect Rating Reason", ", ".join(VALID_AFFECT_RATING_REASON) ) ) if affect_rating_reason: valid_affect_rating_reasons.append(AFFECT_RATING_REASON_MAPPING.get(affect_rating_reason.lower())) valid_affect_rating_reasons: list = list(set(valid_affect_rating_reasons)) affect_rating_reason = ",".join(valid_affect_rating_reasons) return guid, severity, grade, asset_category, risk_vector_list, limit, affect_rating_reason def close_in_xsoar(entries: list, rolledup_observation_id: str, remidiation_status: str): """ Close the XSOAR incident for the given finding. :type entries: list :param entries: List of entries where the closing entry will be appended. :type rolledup_observation_id: str :param rolledup_observation_id: The rolledup observation ID. :type remidiation_status: str :param remidiation_status: The BitSight remediation status that triggered the closure. """ demisto.debug( f"Closing XSOAR incident for BitSight finding {rolledup_observation_id} via Incoming Mirroring. " f"Due to remediation status: {remidiation_status} is same as configured close status." ) entries.append( { "Type": EntryType.NOTE, "Contents": { "dbotIncidentClose": True, "closeReason": "Other", "closeNotes": f"Incident closed because BitSight finding remediation status '{remidiation_status}' " "matches the configured close status.", }, "ContentsFormat": EntryFormat.JSON, } ) def reopen_in_xsoar(entries: list, rolledup_observation_id: str, remidiation_status: str): """ Reopen the XSOAR incident for the given finding. :type entries: list :param entries: List of entries where the reopening entry will be appended. :type rolledup_observation_id: str :param rolledup_observation_id: The rolledup observation ID. :type remidiation_status: str :param remidiation_status: The BitSight remediation status that triggered the reopen. """ demisto.debug( f"Reopening XSOAR incident for BitSight finding {rolledup_observation_id}. " f"Due to remediation status: {remidiation_status} is same as configured open status." ) entries.append({"Type": EntryType.NOTE, "Contents": {"dbotIncidentReopen": True}, "ContentsFormat": EntryFormat.JSON}) def add_comments_to_finding( client: Client, company_guid: str, rolledup_observation_id: str, user_guid: Any, comments_to_add: list[dict] ): """ Add comments to a BitSight finding. Handles thread creation and comment addition. :type client: Client :param client: BitSight API client instance :type company_guid: str :param company_guid: GUID of the company :type rolledup_observation_id: str :param rolledup_observation_id: Rolled up observation ID of the finding :type user_guid: Any :param user_guid: GUID of the user adding comments :type comments_to_add: list[dict] :param comments_to_add: List of comment dictionaries with 'message' key :return: None """ if not user_guid: demisto.debug("Skipping the comment(s) because User GUID is not found.") return # Fetch existing comments to check if thread exists comments_response = client.get_finding_comments(company_guid=company_guid, rolledup_observation_id=rolledup_observation_id) existing_comments = comments_response.get("results", []) thread_guid = "" if existing_comments and isinstance(existing_comments, list): demisto.debug(f"Found {len(existing_comments)} existing comments for finding {rolledup_observation_id}.") thread_guid = existing_comments[0].get("thread_guid", "") # If thread exists, add comments individually; otherwise, batch create if thread_guid: # Add each comment to the existing thread for comment_data in comments_to_add: comment_info = {"author_guid": user_guid, "message": comment_data.get("message", ""), "public": False} client.create_finding_comment( company_guid=company_guid, rolledup_observation_id=rolledup_observation_id, thread_guid=thread_guid, body=comment_info, ) else: # Create new thread with all comments new_comments = [ {"author_guid": user_guid, "message": comment.get("message", ""), "public": False} for comment in comments_to_add ] comments_body = {"comments": new_comments} client.create_finding_comment( company_guid=company_guid, rolledup_observation_id=rolledup_observation_id, thread_guid=thread_guid, body=comments_body, ) """COMMAND FUNCTIONS""" def fetch_incidents(client, last_run, params): """ Fetch Bitsight Findings. :param client: client to use :param last_run: last run object obtained from demisto.getLastRun() :param params: arguments obtained from demisto.params() """ events = [] try: if "offset" in last_run: params["offset"] = last_run["offset"] last_run_date = last_run["first_fetch"] else: first_fetch = arg_to_number(params.get("first_fetch", DEFAULT_FIRST_FETCH_DAYS), "First fetch time in days", True) if first_fetch < 0: # type: ignore raise ValueError(ERROR_MESSAGES["NEGATIVE_FIRST_FETCH"]) today = datetime.now() last_run_date = (today - timedelta(days=first_fetch)).strftime(BITSIGHT_DATE_TIME_FORMAT) # type: ignore already_fetched_findings = last_run.get("already_fetched_findings") or [] report_entries = [] findings_res = company_findings_get_command(client, params, last_run_date, True) report_entries.extend(findings_res.get("results", [])) duplicate_findings = [] ingested_findings = [] for entry in report_entries: key_field = entry.get("rolledup_observation_id", "") + "-#-" + entry.get("first_seen", "") if key_field in already_fetched_findings: duplicate_findings.append(key_field) continue # Updating mirroring fields mirroring_fields = get_mirroring() mirroring_fields.update({"mirror_id": key_field}) entry.update(mirroring_fields) incident_name = "Bitsight Finding" risk_vector_label = entry.get("risk_vector_label", "") evidence_key = entry.get("evidence_key", "") rollup_start_date = entry.get("details", {}).get("rollup_start_date", "") incident_detail_name = " - ".join([x for x in [risk_vector_label, evidence_key, rollup_start_date] if x]) if incident_detail_name: incident_name += " - " + incident_detail_name # Set the Raw JSON to the event. Mapping will be done at the classification and mapping event = { "name": incident_name, "occurred": entry.get("first_seen") + "T00:00:00Z", "rawJSON": json.dumps(entry), } events.append(event) ingested_findings.append(key_field) already_fetched_findings.append(key_field) demisto.debug(f"Skipped {len(duplicate_findings)} duplicate findings: {duplicate_findings}") demisto.debug(f"Fetched {len(ingested_findings)} findings: {ingested_findings}") last_run = { "first_fetch": last_run_date, "offset": params["offset"] + len(report_entries) if params.get("offset") else len(report_entries), } demisto.debug( f"Updated the last run state - first fetch : {last_run.get('first_fetch')}, offset: {last_run.get('offset')}" ) last_run.update({"already_fetched_findings": already_fetched_findings}) except Exception as e: demisto.error("Failed to fetch events.") raise e return last_run, events def test_module(client, params): """ Returning 'ok' indicates that the integration works like it is supposed to. \ Connection to the service is successful. Anything else will fail the test. :param client: client to use :param params: parameters obtained from demisto.params() """ res = client.get_companies_guid() mirror_direction = params.get("mirror_direction", "") if mirror_direction.lower() in [OUTGOING_MIRROR_DIRECTION, INCOMING_AND_OUTGOING_MIRROR_DIRECTION]: user_email = params.get("user_email", "").strip() if not user_email: raise ValueError(ERROR_MESSAGES["USER_EMAIL_REQUIRED"]) user_guid = get_current_user_guid(client, user_email, True) if not user_guid: raise ValueError(ERROR_MESSAGES["USER_GUID_NOT_FOUND"]) if params.get("isFetch", False): available_guids = {c["guid"] for c in res["companies"]} requested_guid = params.get("guid") if not requested_guid: raise ValueError(ERROR_MESSAGES["GUID_REQUIRED"]) if requested_guid not in available_guids: raise ValueError(ERROR_MESSAGES["GUID_NOT_AVAILABLE"]) fetch_incidents(client, {}, params) return "ok" def companies_guid_get_command(client, *args): """ Retrieve subscribed company details. :param client: client to use """ res_json = client.get_companies_guid() outputs = camelize_dict_recursively(remove_empty_elements(res_json)) context_output = { "BitSight.Company(val.guid == obj.guid)": outputs.get("companies", []), "BitSight.MyCompany(val.guid == obj.guid)": outputs.get("myCompany", {}), } hr = [] companies_list = outputs.get("companies", []) for company in companies_list: hr.append( { "Company Name": company.get("name"), "Company Short Name": company.get("shortname"), "GUID": company.get("guid"), "Rating": company.get("rating"), } ) readable_output = tableToMarkdown( name="Companies:", metadata=f"My Company: {outputs.get('myCompany', {}).get('guid')}", t=hr, headers=["Company Name", "Company Short Name", "GUID", "Rating"], removeNull=True, ) return CommandResults(readable_output=readable_output, outputs=context_output, raw_response=outputs) def company_details_get_command(client, args): """ Retrieve company details based on its Guid. :param client: client to use :param args: arguments obtained from demisto.args() """ guid = args.get("guid") res_json = client.get_company_detail(guid) outputs = camelize_dict_recursively(remove_empty_elements(res_json)) outputs["ratingDetails"] = [value for _, value in outputs.get("ratingDetails", {}).items()] company_info = { "guid": res_json.get("guid"), "customId": res_json.get("custom_id"), "name": res_json.get("name"), "description": res_json.get("description"), "ipv4Count": res_json.get("ipv4_count"), "peopleCount": res_json.get("people_count"), "shortName": res_json.get("shortname"), "industry": res_json.get("industry"), "industrySlug": res_json.get("industry_slug"), "subIndustry": res_json.get("sub_industry"), "subIndustrySlug": res_json.get("sub_industry_slug"), "homePage": res_json.get("homepage"), "primaryDomain": res_json.get("primary_domain"), "type": res_json.get("type"), "displayURL": res_json.get("display_url"), } ratings = [] for rating in res_json.get("ratings", []): rating_dict = {"rating": rating.get("rating"), "rating_date": rating.get("rating_date"), "range": rating.get("range")} ratings.append(rating_dict) rating_details = [] for rating_detail_key in res_json.get("rating_details", {}): rating_detail = res_json.get("rating_details", {}).get(rating_detail_key, {}) rating_detail_dict = { "name": rating_detail.get("name"), "rating": rating_detail.get("rating"), "percentile": rating_detail.get("percentile"), "display_url": rating_detail.get("display_url"), } rating_details.append(rating_detail_dict) readable = {"Company Info": company_info, "Ratings": ratings, "Rating Details": rating_details} readable_output = tableToMarkdown( name="Company Details:", t=readable, headers=["Company Info", "Ratings", "Rating Details"], removeNull=True ) return CommandResults( readable_output=readable_output, outputs_prefix="BitSight.Company", outputs=outputs, outputs_key_field="guid", raw_response=res_json, ) def company_findings_get_command(client, args, first_seen=None, fetch_incidents=False): """ Retrieve company findings based on its Guid. :param client: client to use :param args: arguments obtained from demisto.args() :param first_seen: first seen of the finding :param fetch_incidents: whether the command is called from fetch_incidents """ last_seen = None affect_rating_reason = "" if fetch_incidents: guid, severity, grade, asset_category, risk_vector_list, limit, affect_rating_reason = ( prepare_and_validate_fetch_findings_args(client, args) ) offset = arg_to_number(args.get("offset", DEFAULT_OFFSET), "offset") else: guid = args.get("guid") severity = args.get("severity", None) grade = args.get("grade", None) asset_category = args.get("asset_category", None) limit = arg_to_number(args.get("limit", DEFAULT_LIMIT), "limit") if limit and limit > MAX_LIMIT: # type: ignore raise ValueError(ERROR_MESSAGES["LIMIT_GREATER_THAN_ALLOWED"]) offset = arg_to_number(args.get("offset", DEFAULT_OFFSET), "offset") if severity: severity = severity.lower() if grade: grade = grade.upper() if asset_category: asset_category = asset_category.lower() risk_vector_list = argToList(args.get("risk_vector_label", [])) first_seen = args.get("first_seen") last_seen = args.get("last_seen") risk_vector, severity_gte, asset_category_eq = prepare_and_validate_company_findings_get_filter_args( risk_vector_list, severity, asset_category ) res_json = client.get_company_findings( guid, first_seen, last_seen, { "severity_gte": severity_gte, "details.grade": grade, "assets.category": asset_category_eq, "risk_vector": risk_vector, "limit": limit, "offset": offset, "impacts_risk_vector_details": affect_rating_reason, }, ) if fetch_incidents: return res_json res_json_cleaned = camelize_dict_recursively(remove_empty_elements(res_json)) readable_list = [] outputs = None if res_json_cleaned.get("results", []): for finding in res_json_cleaned.get("results", []): readable = { "Evidence Key": finding.get("evidenceKey"), "Risk Vector Label": finding.get("riskVectorLabel"), "First Seen": finding.get("firstSeen"), "Last Seen": finding.get("lastSeen"), "ID": finding.get("temporaryId"), "Risk Category": finding.get("riskCategory"), "Severity": finding.get("severityCategory"), "Asset Category": "\n".join( [f"{asset.get('asset')}: {asset.get('category', '').title()}" for asset in finding.get("assets", [])] ), "Finding Grade": finding.get("details", {}).get("grade", "").title(), } readable_list.append(readable) outputs = { "BitSight.Company(val.guid == obj.guid)": { "guid": guid.lower(), "CompanyFinding": res_json_cleaned.get("results", []), }, "BitSight.Page(val.name == obj.name)": { "name": "bitsight-company-findings-get", "next": res_json_cleaned.get("links", {}).get("next"), "previous": res_json_cleaned.get("links", {}).get("previous"), "count": res_json_cleaned.get("count"), }, } readable_output = tableToMarkdown( name="Company findings:", t=readable_list, metadata=f"Total Findings: {res_json_cleaned.get('count')}", headers=[ "Evidence Key", "Risk Vector Label", "First Seen", "Last Seen", "ID", "Risk Category", "Severity", "Asset Category", "Finding Grade", ], removeNull=True, ) return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=res_json) def get_current_user_guid(client: Client, user_email: str, is_test: bool = False): """ Get the GUID of the current user based on their email. :param client: client to use :param user_email: Email address of the user :return: GUID of the user """ integration_context = demisto.getIntegrationContext() users_guids = integration_context.get("users_guids", {}) if user_email in users_guids and not is_test: return users_guids[user_email] user_guid = None user_info = client.get_users(params={"email": user_email}) if user_info.get("results") and isinstance(user_info["results"], list): user_guid = user_info["results"][0].get("guid") users_guids[user_email] = user_guid integration_context["users_guids"] = users_guids demisto.setIntegrationContext(integration_context) return user_guid def get_modified_remote_data_command(client: Client, args: Dict) -> GetModifiedRemoteDataResponse: """ Get modified findings from the BitSight platform and prepare it for mirroring. :type client: Client :param client: client to use :type args: Dict[str, str] :param args: arguments obtained from demisto.args() :return: GetModifiedRemoteDataResponse with list of modified incident IDs :rtype: GetModifiedRemoteDataResponse """ company_guid = args.get("guid", "") command_args = GetModifiedRemoteDataArgs(args) command_last_run_date = dateparser.parse( command_args.last_update, # type: ignore settings={"TIMEZONE": "UTC"}, # type: ignore ) command_last_run_date_time = command_last_run_date.strftime(DATE_FORMAT) # type: ignore demisto.debug(f"Last update date of get-modified-remote-data command is {command_last_run_date_time}.") last_update_date = command_last_run_date.strftime(BITSIGHT_DATE_TIME_FORMAT) # type: ignore optional_params = { "limit": MAX_FINDINGS_MIRRORING_LIMIT, "sort": "-last_seen", "last_remediation_status_date_gte": last_update_date, } # Fetch the findings modified since the last update. response = client.get_company_findings(guid=company_guid, first_seen=None, last_seen=None, optional_params=optional_params) modified_findings_ids = [] findings = response.get("results", []) for finding in findings: rolledup_observation_id = finding.get("rolledup_observation_id") first_seen = finding.get("first_seen") key_field = f"{rolledup_observation_id}-#-{first_seen}" modified_findings_ids.append(key_field) modified_findings_ids: List[str] = list(filter(None, modified_findings_ids)) # type: ignore demisto.debug( f"Performing get-modified-remote-data command. Numbers Findings IDs to update in XSOAR: {len(modified_findings_ids)}" ) demisto.debug(f"Performing get-modified-remote-data command. Findings IDs to update in XSOAR: {modified_findings_ids}") # Filter out duplicate findings IDs. unique_findings_ids = list(set(modified_findings_ids)) return GetModifiedRemoteDataResponse(modified_incident_ids=unique_findings_ids) def get_remote_data_command(client: Client, args: Dict) -> GetRemoteDataResponse: """ Get remote data for a specific finding from the BitSight platform for mirroring. :type client: Client :param client: client to use :type args: Dict[str, str] :param args: arguments obtained from demisto.args() :return: GetRemoteDataResponse with finding data and new entries :rtype: GetRemoteDataResponse """ new_entries_to_return: list = [] dbot_mirror_id: str = args.get("id") # type: ignore demisto.debug(f"dbot_mirror_id:{dbot_mirror_id}") dbot_mirror_id_parts = dbot_mirror_id.split("-#-") finding_rolledup_observation_id: str = dbot_mirror_id_parts[0] if len(dbot_mirror_id_parts) > 0 else "" first_seen: str = dbot_mirror_id_parts[1] if len(dbot_mirror_id_parts) > 1 else "" demisto.debug(f"Bitsight finding with rolledup_observation_id:{finding_rolledup_observation_id}") demisto.debug(f"Getting update for remote {finding_rolledup_observation_id}.") command_last_run_dt = arg_to_datetime(args.get("lastUpdate"), arg_name="lastUpdate", required=True) command_last_run_timestamp = command_last_run_dt.strftime(DATE_FORMAT) # type: ignore demisto.debug( f"The time when the last time get-remote-data command is called for current incident is {command_last_run_timestamp}." ) integration_params = demisto.params() close_status_of_bitsight = integration_params.get("close_status_of_bitsight") or DEFAULT_CLOSE_STATUS_OF_BITSIGHT open_status_of_bitsight = integration_params.get("open_status_of_bitsight") or DEFAULT_OPEN_STATUS_OF_BITSIGHT close_active_incident = integration_params.get("close_active_incident", False) reopen_closed_incident = integration_params.get("reopen_closed_incident", False) company_guid = args.get("guid", "") params = { "rolledup_observation_id": finding_rolledup_observation_id, "first_seen": first_seen, "sort": "-last_seen", } # Fetch the findings modified since the last update. remote_incident_data = client.get_company_findings(guid=company_guid, first_seen=None, last_seen=None, optional_params=params) findings = remote_incident_data.get("results", []) finding = {} for f in findings: if f.get("affects_rating", ""): finding = f break # Handle the scenario where all findings have affects_rating as false if not finding: finding = findings[0] if findings else {} if not finding: return GetRemoteDataResponse({}, []) params = { "company_guid": company_guid, "rolledup_observation_id": finding_rolledup_observation_id, "evidence_key": finding.get("evidence_key", ""), "risk_vector": finding.get("risk_vector", ""), } integration_context = demisto.getIntegrationContext() processed_findings = integration_context.get("processed_findings") or [] try: remidiations_response = client.get_remediations(params=params) remidiations = remidiations_response.get("results") remidiation_status = "" if remidiations: remidiation_status = remidiations[0].get("status", {}).get("value", "") finding.update({"remediation_status": remidiation_status}) if remidiation_status == close_status_of_bitsight and dbot_mirror_id in processed_findings and close_active_incident: close_in_xsoar(new_entries_to_return, finding_rolledup_observation_id, remidiation_status) processed_findings.remove(dbot_mirror_id) demisto.debug(f"Removed {dbot_mirror_id} finding from processed findings.") elif ( remidiation_status == open_status_of_bitsight and dbot_mirror_id not in processed_findings and reopen_closed_incident ): reopen_in_xsoar(new_entries_to_return, finding_rolledup_observation_id, remidiation_status) processed_findings.append(dbot_mirror_id) demisto.debug(f"Added {dbot_mirror_id} finding to processed findings.") except DemistoException as e: demisto.debug( f"Failed to fetch remediation status for finding {finding_rolledup_observation_id}: {str(e)}. " "Continuing without remediation data." ) try: comments = client.get_finding_comments(company_guid=company_guid, rolledup_observation_id=finding_rolledup_observation_id) comments = comments.get("results") except DemistoException as e: demisto.debug( f"Failed to fetch comments for finding {finding_rolledup_observation_id}: {str(e)}. Continuing without comments data." ) comments = None if comments: for comment in comments: if "[Mirrored From XSOAR]" in comment.get("message"): demisto.debug(f"Skipping the comment {comment.get('guid')} as it is mirrored from XSOAR.") continue comment_created_time = arg_to_datetime(comment.get("created_time")) comment_updated_time = arg_to_datetime(comment.get("last_update_time")) if comment_updated_time: if comment_updated_time <= command_last_run_dt: # type: ignore demisto.debug( f"Skipping comment {comment.get('guid')} - updated time {comment_updated_time} is " f"before sync timestamp {command_last_run_dt}." ) continue else: if comment_created_time <= command_last_run_dt: # type: ignore demisto.debug( f"Skipping the comment {comment.get('guid')} - created time {comment_created_time} is " f"before sync timestamp {command_last_run_dt}." ) continue new_entries_to_return.append( { "Type": EntryType.NOTE, "Contents": f"[Mirrored From Bitsight]\n" f"Added By: {comment.get('author', {}).get('name', '')}\n" f"Added At: {comment_created_time} UTC\n" f"Note: {comment.get('message')}", "ContentsFormat": EntryFormat.TEXT, "Note": True, } ) demisto.debug(f"remote_incident_data_finding:{finding}") processed_findings = processed_findings[-MAX_FINDINGS_MIRRORING_LIMIT:] integration_context["processed_findings"] = processed_findings demisto.setIntegrationContext(integration_context) return GetRemoteDataResponse(finding, new_entries_to_return) def update_remote_system_command(client: Client, args: Dict, close_status_of_bitsight: str, open_status_of_bitsight: str) -> str: """ Update the remote system with the provided data. :type client: Client :param client: client to use. :type args: Dict[str, str] :param args: arguments obtained from demisto.args() :type close_status_of_bitsight: str :param close_status_of_bitsight: Remediation status value for closing findings :type open_status_of_bitsight: str :param open_status_of_bitsight: Remediation status value for opening findings :return: remote incident ID :rtype: str """ company_guid = args.get("guid", "") user_email = args.get("user_email", "").strip() user_guid = None if user_email: user_guid = get_current_user_guid(client, user_email) else: demisto.debug(ERROR_MESSAGES["USER_EMAIL_REQUIRED"]) parsed_args = UpdateRemoteSystemArgs(args) # Get remote incident ID remote_incident_id = parsed_args.remote_incident_id rolledup_observation_id = parsed_args.data.get("bitsightrolledupobservationid", "") risk_vector = parsed_args.data.get("bitsightriskvector", "") evidence_key = parsed_args.data.get("bitsightevidencekey", "") incident_status = parsed_args.inc_status delta = parsed_args.delta xsoar_incident_id = parsed_args.data.get("id", "") new_entries = parsed_args.entries or [] if new_entries: demisto.debug(f"Updating remote system with {len(new_entries)} new entries for incident {remote_incident_id}.") # Prepare comments from entries comments_to_add = [] for entry in new_entries: entry_id = entry.get("id") demisto.debug(f"Sending the entry with ID: {entry_id} and Type: {entry.get('type')}") # Get note content and user entry_content = re.sub(r"([^\n])\n", r"\1\n\n", entry.get("contents", "")) entry_user = entry.get("user", "dbot") or "dbot" note_str = ( f"[Mirrored From XSOAR] XSOAR Incident ID: {xsoar_incident_id}\n\nNote: {entry_content}\n\nAdded By: {entry_user}" ) comments_to_add.append({"message": note_str}) # Add all comments using the helper function add_comments_to_finding(client, company_guid, rolledup_observation_id, user_guid, comments_to_add) # Get integration context to track processed findings integration_context = demisto.getIntegrationContext() processed_findings = integration_context.get("processed_findings") or [] incident_reopened = False # Check if incident is reopened if delta and delta.get("closingUserId") == "" and delta.get("runStatus") == "": demisto.debug(f"Incident {xsoar_incident_id} is reopened.") incident_reopened = True if (incident_status == IncidentStatus.ACTIVE and remote_incident_id in processed_findings) or ( incident_status == IncidentStatus.DONE and remote_incident_id not in processed_findings ): demisto.debug(f"Skipping status update for finding {remote_incident_id} to prevent mirroring loop.") return remote_incident_id # Update external status when incident is closed or when incident is active with no changes should_update_status = ( incident_status == IncidentStatus.DONE or (incident_status == IncidentStatus.ACTIVE and not delta) or incident_reopened ) if parsed_args.incident_changed and should_update_status: demisto.debug(f"Remote Incident ID: {remote_incident_id}") demisto.debug(f"XSOAR Incident ID: {xsoar_incident_id}") demisto.debug(f"Delta information for incident: {delta}") company_guid = args.get("guid", "") # Set status value based on incident status (closed if DONE, opened otherwise) status_value = close_status_of_bitsight if incident_status == IncidentStatus.DONE else open_status_of_bitsight demisto.debug(f"Updating remediation status of finding {remote_incident_id} to {status_value}") body = { "rolledup_observation_id": rolledup_observation_id, "evidence_key": evidence_key, "risk_vector": risk_vector, "status": {"value": status_value, "public": False}, } client.update_external_status(company_guid=company_guid, body=body) if incident_status == IncidentStatus.ACTIVE: processed_findings.append(remote_incident_id) demisto.debug(f"Added {remote_incident_id} finding to processed findings.") elif incident_status == IncidentStatus.DONE: if remote_incident_id in processed_findings: processed_findings.remove(remote_incident_id) demisto.debug(f"Removed {remote_incident_id} finding from processed findings.") processed_findings = processed_findings[-MAX_FINDINGS_MIRRORING_LIMIT:] integration_context["processed_findings"] = processed_findings demisto.setIntegrationContext(integration_context) # For Closing notes delta_keys = parsed_args.delta.keys() if "closingUserId" in delta_keys and parsed_args.incident_changed and parsed_args.inc_status == IncidentStatus.DONE: # Check if incident status is Done close_notes = parsed_args.data.get("closeNotes", "") close_reason = parsed_args.data.get("closeReason", "") close_user_id = parsed_args.data.get("closingUserId", "") closing_note = ( f"[Mirrored From XSOAR] XSOAR Incident ID: {xsoar_incident_id}\n\n" f"Close Reason: {close_reason}\n\n" f"Closed By: {close_user_id}\n\n" f"Close Notes: {close_notes}" ) demisto.debug(f"Closing Comment: {closing_note}") # Add closing comment using the helper function add_comments_to_finding(client, company_guid, rolledup_observation_id, user_guid, [{"message": closing_note}]) return remote_incident_id def main(): """PARSE AND VALIDATE INTEGRATION PARAMS.""" command = demisto.command() params = demisto.params() verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) api_key = params.get("apikey", {}) demisto.info(f"Command being called is {command}") client = Client( base_url=BASE_URL, verify=verify_certificate, proxy=proxy, ok_codes=[200], auth=requests.auth.HTTPBasicAuth(api_key, ""), headers={ "X-BITSIGHT-CALLING-PLATFORM_VERSION": CALLING_PLATFORM_VERSION, "X-BITSIGHT-CONNECTOR-NAME-VERSION": CONNECTOR_NAME_VERSION, }, ) try: """EXECUTION CODE""" args = demisto.args() args.update({"guid": params.get("guid")}) args.update({"user_email": params.get("user_email")}) close_status_of_bitsight = params.get("close_status_of_bitsight") or DEFAULT_CLOSE_STATUS_OF_BITSIGHT open_status_of_bitsight = params.get("open_status_of_bitsight") or DEFAULT_OPEN_STATUS_OF_BITSIGHT if demisto.command() == "test-module": # This is the call made when pressing the integration Test button. return_results(test_module(client, params)) elif demisto.command() == "fetch-incidents": last_run = demisto.getLastRun() last_run_curr, events = fetch_incidents(client, last_run, params) demisto.setLastRun(last_run_curr) demisto.incidents(events) elif command == "get-remote-data": return_results(get_remote_data_command(client, args)) elif command == "get-modified-remote-data": return_results(get_modified_remote_data_command(client, args)) elif command == "update-remote-system": return_results(update_remote_system_command(client, args, close_status_of_bitsight, open_status_of_bitsight)) else: COMMAND_TO_FUNCTION = { "bitsight-company-details-get": company_details_get_command, "bitsight-company-findings-get": company_findings_get_command, "bitsight-companies-guid-get": companies_guid_get_command, } if COMMAND_TO_FUNCTION.get(demisto.command()): args = demisto.args() remove_nulls_from_dictionary(trim_spaces_from_args(args)) return_results(COMMAND_TO_FUNCTION[demisto.command()](client, args)) # type: ignore else: raise NotImplementedError(f"Command {demisto.command()} is not implemented") # Log exceptions except Exception as e: return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e}") if __name__ in ["__main__", "builtin", "builtins"]: main()