Shodan_v2
A search engine used for searching Internet-connected devices.
Data Enrichment & Threat Intelligence · Shodan
Details
| ID | Shodan_v2 |
|---|---|
| Provider | Shodan |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 5.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM EDR Cortex Cloud Attack Surface Management Cloud Runtime Security Cloud Posture Security Exposure Management |
README
Shodan is a search engine for Internet-connected devices. Unlike traditional search engines that index websites, Shodan indexes information about the devices connected to the internet, such as servers, routers, webcams, and other IoT devices.
Configure Shodan v2 in Cortex
| Parameter | Description | Required |
|---|---|---|
| API Key | False | |
| Base URL to Shodan API | True | |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False | |
| Source Reliability | Reliability of the source providing the intelligence data. | False |
| The maximum number of events per fetch | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
search
Searches Shodan using facets to get summary information on properties.
Base Command
search
Input
| Argument Name | Description | Required |
|---|---|---|
| query | The query for searching the database of banners. The search query supports filtering using the “filter:value” format to narrow your search. For example, the query “apache country:DE” returns Apache web servers located in Germany. | Required |
| facets | A CSV list of properties on which to get summary information. The search query supports filtering using the “property:count” format to define the number of facets to return for a property. For example, the query “country:100” returns the top 100 countries. | Optional |
| page | The page number of the fetched results. Each page contains a maximum of 100 results. Default is 1. | Optional |
| return_json | Whether to return a JSON file containing the full search results for further processing. Possible values are: Yes, No. Default is No. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Shodan.Banner.Org | String | The name of the organization to which the space of the IP address space for the searched device is assigned. |
| Shodan.Banner.Isp | String | The Internet Service Provider that provides the organization with the IP address space for the searched device. |
| Shodan.Banner.Transport | String | The IP address transport protocol used to fetch the summary information. Can be “UDP” or “TCP”. |
| Shodan.Banner.Asn | String | The Autonomous System Number. For example, “AS4837”. |
| Shodan.Banner.IP | String | The IP address of the host as a string. |
| Shodan.Banner.Port | Number | The port number on which the service is operating. |
| Shodan.Banner.Ssl.versions | String | The list of SSL versions that are supported by the server. Unsupported versions are prefixed with a “-“. For example, [“TLSv1”, “-SSLv2”] means that the server supports TLSv1, but does not support SSLv2. |
| Shodan.Banner.Hostnames | String | An array of strings containing all of the host names that have been assigned to the IP address for the searched device. |
| Shodan.Banner.Location.City | String | The city in which the searched device is located. |
| Shodan.Banner.Location.Longitude | Number | The longitude of the geolocation of the searched device. |
| Shodan.Banner.Location.Latitude | Number | The latitude of the geolocation of the searched device. |
| Shodan.Banner.Location.Country | String | The country in which the searched device is located. |
| Shodan.Banner.Timestamp | Date | The timestamp in UTC format indicating when the banner was fetched from the searched device. |
| Shodan.Banner.Domains | String | An array of strings containing the top-level domains for the host names of the searched device. It is a utility property for filtering by a top-level domain instead of a subdomain. It supports handling global top-level domains that have several dots in the domain. For example, “co.uk”. |
| Shodan.Banner.OS | String | The operating system that powers the searched device. |
| Shodan.Banner.Product | String | Name of the software that powers the service. |
| Shodan.Banner.Ntlm.OSBuild | String | OS build reported by the service. |
| Shodan.Banner.Ntlm.DNSForestName | String | DNS Forest Name reported by the service. |
| Shodan.Banner.Ntlm.Timestamp | Number | Timestamp. |
| Shodan.Banner.Ntlm.FQDN | String | FQDN. |
| Shodan.Banner.Ntlm.NetBIOSDomainName | String | Netbios Domain Name. |
| Shodan.Banner.Ntlm.NetBIOSComputerName | String | Netbios Computer Name. |
| Shodan.Banner.Ntlm.TargetRealm | String | Target Realm. |
| Shodan.Banner.Ntlm.OS | Unknown | OS. |
| Shodan.Banner.Ntlm.DNSDomainName | String | DNS Domain Name. |
| Shodan.Banner.Hash | Number | Numeric hash of the “data” property which is helpful for finding other IPs with the exact same information. |
| Shodan.Banner.Tags | Unknown | Tag applied by Shodan analysis. |
| Shodan.Banner.SslCert.SigAlg | String | Certificate Signature Algorithm. |
| Shodan.Banner.SslCert.Issued | Date | Timestamp of the beginning of certificate validity (Not Valid Before). |
| Shodan.Banner.SslCert.Expires | Date | Timestamp of the end of certificate validity (Not Valid After). |
| Shodan.Banner.SslCert.Version | Number | X.509 Certificate Version. |
| Shodan.Banner.SslCert.Serial | Number | Serial Number assigned by the issuer. |
| Shodan.Banner.SslCert.Subject.CN | String | Subject Common Name. |
| Shodan.Banner.SslCert.Subject.O | String | Subject Organization. |
| Shodan.Banner.SslCert.Subject.L | String | Subject Locality or City. |
| Shodan.Banner.SslCert.Subject.ST | String | Subject State or Province. |
| Shodan.Banner.SslCert.Subject.C | String | Subject Country Name. |
| Shodan.Banner.SslCert.Expired | Boolean | Boolean indicating whether the certificate is expired. |
| Shodan.Banner.SslCert.Issuer.CN | String | Issuer Certificate Common Name. |
| Shodan.Banner.SslCert.Issuer.O | String | Issuer Organization. |
| Shodan.Banner.SslCert.Issuer.OU | String | Issuer Organizational Unit. |
| Shodan.Banner.SslCert.Issuer.L | String | Issuer Locality or City. |
| Shodan.Banner.SslCert.Issuer.ST | String | Issuer State or Province. |
| Shodan.Banner.SslCert.Issuer.C | String | Issuer Country Name. |
| Shodan.Banner.Data | String | The raw data returned fro the service. |
| Shodan.Banner.CPE23 | Unknown | CPE information in the 2.3 format. |
| Shodan.Banner.Device | String | Device identified by Shodan. |
| Shodan.Banner.DeviceType | String | The Device Type identified by Shodan. |
| Shodan.Banner.Info | String | Additional information provided by Shodan. |
| Shodan.Banner.IPv6 | String | The IPv6 address of the host as a string. |
| Shodan.Banner.Link | String | The Link identified by Shodan. |
| Shodan.Banner.Platform | String | The Platform identified by Shodan. |
| Shodan.Banner.Product | String | The Product identified by Shodan. |
Command Example
!search query="country:HK org:RLL-HK -port:80 -port:443 -port:21 -port:25 has_ssl:false" using-brand=Shodan_v2
Context Example
{
"Shodan": [
{
"Banner": {
"Asn": "AS9311",
"Domains": [],
"Hostnames": [],
"IP": "1.2.3.4",
"Isp": "HITRON TECHNOLOGY INC.",
"Location": {
"City": "Hong Kong",
"Country": "Hong Kong",
"Latitude": 22.27832,
"Longitude": 114.17469
},
"OS": null,
"Org": "RLL-HK",
"Port": 5353,
"Ssl": {
"versions": []
},
"Timestamp": "2021-08-17T03:33:07.392394",
"Transport": "udp"
}
},
{
"Banner": {
"Asn": "AS9919",
"Domains": [],
"Hostnames": [],
"IP": "1.2.3.4",
"Isp": "New Century InfoComm Tech Co., Ltd.",
"Location": {
"City": "Hong Kong",
"Country": "Hong Kong",
"Latitude": 22.27832,
"Longitude": 114.17469
},
"OS": null,
"Org": "RLL-HK",
"Port": 5353,
"Ssl": {
"versions": []
},
"Timestamp": "2021-08-17T03:21:00.992437",
"Transport": "udp"
}
},
{
"Banner": {
"Asn": "AS9311",
"Domains": [],
"Hostnames": [],
"IP": "1.2.3.4",
"Isp": "HITRON TECHNOLOGY INC.",
"Location": {
"City": "Hong Kong",
"Country": "Hong Kong",
"Latitude": 22.27832,
"Longitude": 114.17469
},
"OS": null,
"Org": "RLL-HK",
"Port": 5353,
"Ssl": {
"versions": []
},
"Timestamp": "2021-08-17T03:13:54.617598",
"Transport": "udp"
}
}
]
}
Human Readable Output
Search results for query “country:HK org:RLL-HK -port:80 -port:443 -port:21 -port:25 has_ssl:false” - page 1, facets: None
IP Port Timestamp 1.2.3.4 5353 2021-08-17T03:13:54.617598
ip
Returns all services that have been found on the IP address of the searched host.
Base Command
ip
Input
| Argument Name | Description | Required |
|---|---|---|
| ip | The IP address of the host. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| IP.ASN | Unknown | The Autonomous System Number. |
| IP.Address | Unknown | The IP address. |
| IP.Geo.Country | Unknown | The country of a given IP address. |
| IP.Geo.Description | Unknown | The description of the location. |
| IP.Geo.Location | Unknown | The latitude and longitude of an IP address. |
| IP.Hostname | Unknown | The hostname of the IP address. |
| IP.Relationships | Unknown | The relationships between the ip and it’s CVEs. |
| Shodan.IP.Tags | String | The tags associated with the IP address. |
| Shodan.IP.Latitude | Number | The latitude of the geolocation of the searched device. |
| Shodan.IP.Org | String | The name of the organization to which the IP space for the searched device is assigned. |
| Shodan.IP.ASN | String | The Autonomous System Number. For example, “AS4837”. |
| Shodan.IP.ISP | String | The Internet Service Provider that provides the organization with the IP space for the searched device. |
| Shodan.IP.Longitude | Number | The longitude of the geolocation of the searched device. |
| Shodan.IP.LastUpdate | Date | The timestamp in UTC format indicating when the banner was fetched from the searched device. |
| Shodan.IP.CountryName | String | The country in which the searched device is located. |
| Shodan.IP.OS | String | The operating system on which the searched device is running. |
| Shodan.IP.Port | Number | The port number on which the service is operating. |
| Shodan.IP.Address | String | The IP address of the host as a string. |
| Shodan.IP.Vulnerabilities | Unknown | A list of Vulnerabilities. |
| DBotScore.Indicator | String | The indicator that was tested. |
| DBotScore.Score | Number | The actual score. |
| DBotScore.Type | String | The indicator type. |
| DBotScore.Vendor | String | The vendor used to calculate the score. |
Command Example
!ip ip="8.8.8.8" using-brand="Shodan_v2"
Context Example
{
"IP": {
"ASN": "AS15169",
"Address": "8.8.8.8",
"Geo": {
"Country": "United States",
"Location": "37.406,-122.078"
},
"Hostname": "dns.google",
"Relationships": [
{
"EntityA": "8.8.8.8",
"EntityAType": "IP",
"EntityB": "CVE-2016-11111",
"EntityBType": "CVE",
"Relationship": "related-to"
}
]
},
"Shodan": {
"IP": {
"ASN": "AS15169",
"Address": "8.8.8.8",
"CountryName": "United States",
"ISP": "Google LLC",
"LastUpdate": "2021-08-20T17:13:07.423800",
"Latitude": 37.4056,
"Longitude": -122.0775,
"OS": null,
"Org": "Google LLC",
"Port": [
53
],
"Tag": [],
"Vulnerabilities": ["CVE-2016-11111"]
}
},
"DBotScore": {
"Indicator": "8.8.8.8",
"Score": 0,
"Type": "ip",
"Vendor": "Shodan_v2"
}
}
Human Readable Output
Shodan details for IP 8.8.8.8
ASN Country Hostname ISP Location Ports AS15169 United States dns.google Google LLC 37.406,-122.078 53
shodan-search-count
Returns the total number of results that match only the specified query or facet settings. This command does not return host results. This command does not consume query credits.
Base Command
shodan-search-count
Input
| Argument Name | Description | Required |
|---|---|---|
| query | The query for searching the database of banners. The search query supports filtering using the “filter:value” format to narrow your search. For example, the query “apache country:DE” returns Apache web servers located in Germany. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Shodan.Search.ResultCount | Number | The number of results matched in the search query. |
Command Example
!shodan-search-count query="country:HK product:Apache"
Context Example
{
"Shodan": {
"Search": {
"ResultCount": 498645
}
}
}
Human Readable Output
498645 results for query “country:HK product:Apache”
shodan-scan-ip
Requests Shodan to crawl a network.
Base Command
shodan-scan-ip
Input
| Argument Name | Description | Required |
|---|---|---|
| ips | A CSV list of IP addresses or netblocks for Shodan to crawl defined in CIDR notation. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Shodan.Scan.ID | String | The unique ID of the scan. |
| Shodan.Scan.Status | String | The status of the scan. |
Command Example
!shodan-scan-ip ips=8.8.8.8
Context Example
{
"Shodan": {
"Scan": {
"ID": "wQEp0bIIEHklpAwa",
"Status": "PROCESSING"
}
}
}
Human Readable Output
Scanning results for scan wQEp0bIIEHklpAwa
ID Status wQEp0bIIEHklpAwa PROCESSING
shodan-scan-internet
Requests for Shodan to perform a scan on the specified port and protocol.
Base Command
shodan-scan-internet
Input
| Argument Name | Description | Required |
|---|---|---|
| port | The port for which Shodan crawls the Internet. | Required |
| protocol | The name of the protocol used to interrogate the port. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Shodan.Scan.ID | String | The ID of the initial scan. |
Command Example
#### Human Readable Output
### shodan-scan-status
***
Checks the progress of a previously submitted scan request on the specified port and protocol.
#### Base Command
`shodan-scan-status`
#### Input
| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| scanID | The unique ID of the initial scan. | Required |
#### Context Output
| **Path** | **Type** | **Description** |
| --- | --- | --- |
| Shodan.Scan.Id | String | The unique ID of the scan request checked for progress. |
| Shodan.Scan.Status | String | The status of the scan job checked for progress. |
#### Command Example
```!shodan-scan-status scanID=7rbp1CAtx91BMwcg```
#### Context Example
```json
{
"Shodan": {
"Scan": {
"ID": "7rbp1CAtx91BMwcg",
"Status": "DONE"
}
}
}
Human Readable Output
Scanning results for scan 7rbp1CAtx91BMwcg
ID Status 7rbp1CAtx91BMwcg DONE
shodan-create-network-alert
Creates a network alert for a defined IP address or netblock used for subscribing to changes or events that are discovered within the netblock’s range.
Base Command
shodan-create-network-alert
Input
| Argument Name | Description | Required |
|---|---|---|
| alertName | The name of the network alert. | Required |
| ip | A list of IP addresses or network ranges defined in CIDR notation. | Required |
| expires | The number of seconds for the network alert to remain active. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Shodan.Alert.ID | String | The ID of the subscription of the specified network alert. |
| Shodan.Alert.Expires | String | The number of seconds that the specified network alert remains active. |
Command Example
!shodan-create-network-alert alertName="test_alert" ip="1.1.1.1"
Context Example
{
"Shodan": {
"Alert": {
"Expires": 0,
"ID": "CB68M776ICCMS36L"
}
}
}
Human Readable Output
Alert ID CB68M776ICCMS36L
Expires IP Name 0 1.1.1.1 test_alert
shodan-network-get-alert-by-id
Gets the details of a network alert.
Base Command
shodan-network-get-alert-by-id
Input
| Argument Name | Description | Required |
|---|---|---|
| alertID | The ID of the network alert. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Shodan.Alert.ID | String | The ID of the subscription of the network alert. |
| Shodan.Alert.Expires | String | The number of seconds that the network alert remains active. |
Command Example
!shodan-network-get-alert-by-id alertID="0EKRH38BBQEHTQ3E"
Context Example
{
"Shodan": {
"Alert": {
"Expires": 0,
"ID": "0EKRH38BBQEHTQ3E"
}
}
}
Human Readable Output
Alert ID 0EKRH38BBQEHTQ3E
Expires IP Name 0 1.2.3.4 test_alert
shodan-network-get-alerts
Gets a list of all created network alerts.
Base Command
shodan-network-get-alerts
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| Shodan.Alert.ID | String | The IDs of the subscriptions of the network alerts. |
| Shodan.Alert.Expires | String | The number of seconds that the network alerts remain active. |
Command Example
#### Context Example
```json
{
"Shodan": [
{
"Alert": {
"Expires": 0,
"ID": "0EKRH38BBQEHTQ3E"
}
},
{
"Alert": {
"Expires": 0,
"ID": "CB68M776ICCMS36L"
}
},
{
"Alert": {
"Expires": 0,
"ID": "HTWLPTVPUHN5VAGA"
}
},
{
"Alert": {
"Expires": 0,
"ID": "VXGB6CZ536X5AWE6"
}
}
]
}
Human Readable Output
Alert ID VXGB6CZ536X5AWE6
Expires IP Name 0 1.1.1.1 test_alert
shodan-network-delete-alert
Removes the specified network alert.
Base Command
shodan-network-delete-alert
Input
| Argument Name | Description | Required |
|---|---|---|
| alertID | The ID of the network alert to remove. | Required |
Context Output
There is no context output for this command.
Command Example
!shodan-network-delete-alert alertID="0EKRH38BBQEHTQ3E"
Human Readable Output
Deleted alert 0EKRH38BBQEHTQ3E
shodan-network-alert-set-trigger
Enables receiving notifications for network alerts that are set off by the specified triggers.
Base Command
shodan-network-alert-set-trigger
Input
| Argument Name | Description | Required |
|---|---|---|
| alertID | The ID of the network alert for which to enable notifications. | Required |
| Trigger | The name of the trigger. | Required |
Context Output
There is no context output for this command.
Command Example
!shodan-network-alert-set-trigger alertID="0EKRH38BBQEHTQ3E" Trigger=any
Human Readable Output
Set trigger “any” for alert 0EKRH38BBQEHTQ3E
shodan-network-alert-remove-trigger
Disables receiving notifications for network alerts that are set off by the specified triggers.
Base Command
shodan-network-alert-remove-trigger
Input
| Argument Name | Description | Required |
|---|---|---|
| alertID | The ID of the network alert for which to disable notifications. | Required |
| Trigger | The name of the trigger. | Required |
Context Output
There is no context output for this command.
Command Example
!shodan-network-alert-remove-trigger alertID="0EKRH38BBQEHTQ3E" Trigger="any"
Human Readable Output
Deleted trigger “any” for alert 0EKRH38BBQEHTQ3E
shodan-network-alert-whitelist-service
Ignores the specified services for network alerts that are set off by the specified triggers.
Base Command
shodan-network-alert-whitelist-service
Input
| Argument Name | Description | Required |
|---|---|---|
| alertID | The ID of the network alert for which to ignore the specified services. | Required |
| trigger | The name of the trigger. | Required |
| service | The service specified in the “ip:port” format. For example, “1.1.1.1:80”. | Required |
Context Output
There is no context output for this command.
Command Example
!shodan-network-alert-whitelist-service alertID="0EKRH38BBQEHTQ3E" trigger="any" service="1.1.1.1:80"
Human Readable Output
Whitelisted service “1.1.1.1:80” for trigger any in alert 0EKRH38BBQEHTQ3E
shodan-network-alert-remove-service-from-whitelist
Resumes receiving notifications for network alerts that are set off by the specified triggers.
Base Command
shodan-network-alert-remove-service-from-whitelist
Input
| Argument Name | Description | Required |
|---|---|---|
| alertID | The ID of the alert for which to resume the specified services. | Required |
| trigger | The name of the trigger. | Required |
| service | The service specified in the “ip:port” format. For example, “1.1.1.1:80”. | Required |
Context Output
There is no context output for this command.
Command Example
!shodan-network-alert-remove-service-from-whitelist alertID="0EKRH38BBQEHTQ3E" trigger="any" service="1.1.1.1:80"
Human Readable Output
Removed service “1.1.1.1:80” for trigger any in alert 0EKRH38BBQEHTQ3E from the allow list
shodan-get-events
Retrieves events from Shodan.
Base Command
shodan-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| should_push_events | If set to ‘True’, the command will create events; otherwise, it will only display them. Possible values are: True, False. Default is False. | Optional |
| start_date | Fetch events created after this date. You can also use relative terms like “3 days ago”. Default is 3 days ago. | Optional |
| max_fetch | The maximum amount of events to return. Default is 50000. | Optional |
Context Output
There is no context output for this command.
Fetch Events
Fetch process returns a listing of all the network alerts that are currently active on the account.
To enable the Shodan integration you need to have an API key, which you can get for free by creating a Shodan account https://account.shodan.io/register
Once you have an API key, you insert it into the API Key field and click the Test button.
Rate Limits
All API plans are subject to a rate limit of 1 request per second - docs
Configuration parameters
credentials—api_key— API Keyapi_url— Base url to Shodan API (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsintegrationReliability— Source ReliabilityfeedExpirationPolicy—feedExpirationInterval—max_fetch— The maximum number of events per fetch
Commands (15)
-
ipReturns all services that have been found on the IP address of the searched host.
-
searchSearches Shodan using facets to get summary information on properties.
-
shodan-create-network-alertCreates a network alert for a defined IP address or netblock used for subscribing to changes or events that are discovered within the netblock's range.
-
shodan-get-eventsRetrieves events from Shodan.
-
shodan-network-alert-remove-service-from-whitelistResumes receiving notifications for network alerts that are set off by the specified triggers.
-
shodan-network-alert-remove-triggerDisables receiving notifications for network alerts that are set off by the specified triggers.
-
shodan-network-alert-set-triggerEnables receiving notifications for network alerts that are set off by the specified triggers.
-
shodan-network-alert-whitelist-serviceIgnores the specified services for network alerts that are set off by the specified triggers.
-
shodan-network-delete-alertRemoves the specified network alert.
-
shodan-network-get-alert-by-idGets the details of a network alert.
-
shodan-network-get-alertsGets a list of all created network alerts.
-
shodan-scan-internetRequests for Shodan to perform a scan on the specified port and protocol.
-
shodan-scan-ipRequests Shodan to crawl a network.
-
shodan-scan-statusChecks the progress of a previously submitted scan request on the specified port and protocol.
-
shodan-search-countReturns the total number of results that match only the specified query or facet settings. This command does not return host results. This command does not consume query credits.
import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * """ IMPORTS """ import json import requests import urllib3 # Disable insecure warnings urllib3.disable_warnings() """ GLOBALS/PARAMS """ API_KEY = demisto.params().get("credentials", {}).get("password") or demisto.params().get("api_key") if API_KEY is None: raise ValueError("Missing API key.") # Remove trailing slash to prevent wrong URL path to service API_URL = demisto.params()["api_url"].rstrip("/") # Should we use SSL USE_SSL = not demisto.params().get("insecure", False) VENDOR = "shodan" PRODUCT = "banner" DEFAULT_MAX_EVENTS = 50_000 DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" handle_proxy() optional_general_properties_map: dict[str, tuple[str, Any]] = { "cpe23": ("CPE23", []), "device": ("Device", ""), "devicetype": ("DeviceType", ""), "info": ("Info", ""), "ipv6": ("IPv6", ""), "link": ("Link", ""), "platform": ("Platform", ""), "product": ("Product", ""), "tags": ("Tags", []), "transport": ("Transport", ""), "vendor": ("Vendor", ""), "version": ("Version", ""), } ntlm_properties_map: dict[str, tuple[str, Any]] = { "dns_domain_name": ("DNSDomainName", ""), "dns_forest_name": ("DNSForestName", ""), "fqdn": ("FQDN", ""), "netbios_computer_name": ("NetBIOSComputerName", ""), "netbios_domain_name": ("NetBIOSDomainName", ""), "os": ("OS", []), "os_build": ("OSBuild", ""), "target_realm": ("TargetRealm", ""), "timestamp": ("Timestamp", 0), } ssl_cert_properties_map: dict[str, tuple[str, Any]] = { "expired": ("Expired", False), "expires": ("Expires", ""), "issued": ("Issued", ""), "issuer": ("Issuer", {}), "serial": ("Serial", 0), "sig_alg": ("SigAlg", ""), "subject": ("Subject", {}), "version": ("Version", 0), } """ HELPER FUNCTIONS """ def http_request(method, uri, params=None, data=None, headers=None): if params is None: params = {} params.update({"key": API_KEY}) url = f"{API_URL}{uri}" res = requests.request(method, url, params=params, data=data, headers=headers, verify=USE_SSL) if res.status_code == 404: return {} if res.status_code == 401: return_error("Error: the Shodan API key is invalid. Please check your API key.") if res.status_code != 200: error_msg = f"Error in API call {url} [{res.status_code}] - {res.reason}" if "application/json" in res.headers["content-type"] and "error" in res.json(): error_msg += f': {res.json()["error"]}' return_error(error_msg) return res.json() def alert_to_demisto_result(alert): ec = {"Shodan": {"Alert": {"ID": alert.get("id", ""), "Expires": alert.get("expires", 0)}}} human_readable = tableToMarkdown( f'Alert ID {ec["Shodan"]["Alert"]["ID"]}', { "Name": alert.get("name", ""), "IP": alert.get("filters", {"ip": ""})["ip"], "Expires": ec["Shodan"]["Alert"]["Expires"], }, ) demisto.results( { "Type": entryTypes["note"], "Contents": alert, "ContentsFormat": formats["json"], "HumanReadable": human_readable, "HumanReadableFormat": formats["markdown"], "EntryContext": ec, } ) def format_record_keys(dict_list: List[Dict]) -> List[Dict]: """ Formats dictionary keys by replacing underscores with spaces and capitalizing each word. """ new_list = [] for input_dict in dict_list: new_dict = {} for key, value in input_dict.items(): new_key = key.replace("_", " ").title() new_dict[new_key] = value new_list.append(new_dict) return new_list def add_time_to_events(events: list[dict]): """ Adds the _time key to the events. Args: events: list[dict] - list of events to add the _time key to. Returns: list: The events with the _time key. """ if events: for event in events: create_time = arg_to_datetime(event["created"]) event["_time"] = create_time.strftime(DATE_FORMAT) # type: ignore[union-attr] def filter_events(events: list[dict], limit: int, last_run: dict = {}) -> list[dict]: """ Filters and sorts events based on the last fetch time, list of excluded IDs, and a limit. Args: events (list[dict]): List of events where each event is represented as a dictionary. limit (int): The maximum number of events to return. last_run (dict, optional): Dictionary containing the last fetch time and a list of event IDs to exclude. Default is an empty dictionary. """ if last_fetch_time := arg_to_datetime(last_run.get("last_fetch_time")): events = [event for event in events if parse_event_date(event) >= last_fetch_time] if last_ids := last_run.get("last_event_ids"): events = [event for event in events if event["id"] not in last_ids] return events[:limit] def parse_event_date(event: Dict) -> datetime: """ Parses the 'created' field from an event dictionary into a datetime object. """ return datetime.strptime(event["created"], DATE_FORMAT) """ COMMANDS + REQUESTS FUNCTIONS """ def get_scan_status(scan_id): res = http_request("GET", f"/shodan/scan/{scan_id}") ec = {"Shodan": {"Scan": {"ID": res.get("id", ""), "Status": res.get("status", "")}}} human_readable = tableToMarkdown( f"Scanning results for scan {scan_id}", {"ID": ec["Shodan"]["Scan"]["ID"], "Status": ec["Shodan"]["Scan"]["Status"]} ) demisto.results( { "Type": entryTypes["note"], "Contents": res, "ContentsFormat": formats["json"], "HumanReadable": human_readable, "HumanReadableFormat": formats["markdown"], "EntryContext": ec, } ) def test_module(): """ Sends a basic GET request to verify API connectivity and performs a sample event fetch if event fetching is enabled. """ params = demisto.params() is_fetch_events = argToBoolean(params.get("isFetchEvents", False)) if is_fetch_events and not API_KEY: return_error("Missing API key - You must provide API KEY parameter.") if API_KEY: http_request("GET", "/shodan/alert/info") # Checking with API key else: http_request("GET", "/shodan/ports", {"query": "test"}) # Checking without API key def search_command(): query = demisto.args()["query"] facets = demisto.args().get("facets") page = int(demisto.args().get("page", 1)) return_json = argToBoolean(demisto.args().get("return_json", "No")) params = {"query": query} if facets: params["facets"] = facets if page: params["page"] = page res = http_request("GET", "/shodan/host/search", params) matches = res.get("matches", []) # Return the full list of match data as a JSON file if requested if return_json: formatted_json = json.dumps(matches).encode("utf-8") file_entry = fileResult( filename=f"shodan_search_matches_page{page}.json", data=formatted_json, file_type=EntryType.ENTRY_INFO_FILE ) return_results(file_entry) for match in matches: location = match.get("location", {"city": "", "country_name": "", "longitude": 0, "latitude": 0}) # Build context entry with commonly used properties and others listed as Required in Shodan Banner API spec ec = { "Shodan": { "Banner": { "Org": match.get("org", ""), "Isp": match.get("isp", ""), "Transport": match.get("transport", ""), "Asn": match.get("asn", ""), "IP": match.get("ip_str", ""), "Port": match.get("port", 0), "Ssl": {"versions": match.get("ssl", {"versions": []}).get("versions", [])}, "Hostnames": match.get("hostnames", []), "Location": { "City": location["city"], "Longitude": location["longitude"], "Latitude": location["latitude"], "Country": location["country_name"], }, "Timestamp": match.get("timestamp", ""), "Domains": match.get("domains", []), "OS": match.get("os", ""), "Data": match.get("data", ""), "Hash": match.get("hash", 0), } } } # Add any available optional Banner properties to the entry context object for optional_property, (ec_key, default_value) in optional_general_properties_map.items(): if optional_property in match: ec["Shodan"]["Banner"][ec_key] = match[optional_property] or default_value # Add SSL certificate properties, if present if match.get("ssl", {}).get("cert", {}): ec["Shodan"]["Banner"]["SslCert"] = {} for cert_property, (ec_key, default_value) in ssl_cert_properties_map.items(): value = match["ssl"]["cert"].get(cert_property, default_value) if cert_property in ["issued", "expires"]: cert_datetime = datetime.strptime(value, "%Y%m%d%H%M%SZ") value = cert_datetime.strftime(DATE_FORMAT) ec["Shodan"]["Banner"]["SslCert"][ec_key] = value # Add NTLM properties, if present if match.get("ntlm", {}): ec["Shodan"]["Banner"]["Ntlm"] = {} for ntlm_property, (ec_key, default_value) in ntlm_properties_map.items(): ec["Shodan"]["Banner"]["Ntlm"][ec_key] = match["ntlm"].get(ntlm_property, default_value) # Format returned data as markdown table human_readable = tableToMarkdown( f'Search results for query "{query}" - page {page}, facets: {facets}', flattenRow(rowDict=ec["Shodan"]["Banner"]) ) demisto.results( { "Type": entryTypes["note"], "Contents": match, "ContentsFormat": formats["json"], "HumanReadable": human_readable, "HumanReadableFormat": formats["markdown"], "EntryContext": ec, } ) def ip_command(): ips = argToList(demisto.args()["ip"]) results = [] for ip in ips: res = http_request("GET", f"/shodan/host/{ip}") if not res: results.append(CommandResults(readable_output=f"No information available for the following IP: {ip}")) else: hostnames = res.get("hostnames") # It's a list, only if it exists and not empty we take the first value. hostname = hostnames[0] if hostnames else "" location = f'{round(res.get("latitude", 0.0), 3)},{round(res.get("longitude", 0.0), 3)}' relationships_list: list[EntityRelationship] = [] vulns_list = res.get("vulns", []) for v in vulns_list: relationships_list.append( EntityRelationship( entity_a=ip, entity_a_type=FeedIndicatorType.IP, name="related-to", entity_b=v, entity_b_type=FeedIndicatorType.CVE, brand="ShodanV2", ) ) dbot_score = Common.DBotScore( indicator=ip, indicator_type=DBotScoreType.IP, reliability=demisto.params().get("integrationReliability"), score=0, integration_name="Shodan_v2", ) ip_details = Common.IP( ip=ip, dbot_score=dbot_score, asn=res.get("asn", ""), hostname=hostname, geo_country=res.get("country_name", ""), geo_latitude=round(res.get("latitude", 0.0), 3), geo_longitude=round(res.get("longitude", 0.0), 3), relationships=relationships_list, ) shodan_ip_details = { "Tag": res.get("tags", []), "Latitude": res.get("latitude", 0.0), "Longitude": res.get("longitude", 0.0), "Org": res.get("org", ""), "ASN": res.get("asn", ""), "ISP": res.get("isp", ""), "LastUpdate": res.get("last_update", ""), "CountryName": res.get("country_name", ""), "Address": ip, "OS": res.get("os", ""), "Port": res.get("ports", []), "Vulnerabilities": vulns_list, } title = f"Shodan details for IP {ip}" human_readable = { "Country": res.get("country_name", ""), "Location": location, "ASN": res.get("asn", ""), "ISP": res.get("isp", ""), "Ports": ", ".join([str(x) for x in res.get("ports", [])]), "Hostname": hostname, } readable_output = tableToMarkdown(name=title, t=human_readable, removeNull=True) results.append( CommandResults( readable_output=readable_output, raw_response=res, outputs=shodan_ip_details, relationships=relationships_list, outputs_prefix="Shodan.IP", indicator=ip_details, ) ) return results def shodan_search_count_command(): query = demisto.args()["query"] res = http_request("GET", "/shodan/host/count", {"query": query}) ec = {"Shodan": {"Search": {"ResultCount": res.get("total", 0)}}} demisto.results( { "Type": entryTypes["note"], "Contents": res, "ContentsFormat": formats["json"], "HumanReadable": f'## {ec["Shodan"]["Search"]["ResultCount"]} results for query "{query}"', "HumanReadableFormat": formats["markdown"], "EntryContext": ec, } ) def shodan_scan_ip_command(): ips = demisto.args()["ips"] res = http_request("POST", "/shodan/scan", data={"ips": ips}) if "id" not in res: demisto.results( { "Type": entryTypes["error"], "Contents": res, "ContentsFormat": formats["json"], "HumanReadable": '## Unknown answer format, no "id" field in response', "HumanReadableFormat": formats["markdown"], } ) get_scan_status(res["id"]) def shodan_scan_internet_command(): port = demisto.args()["port"] try: port = int(port) except ValueError: return_error(f"Port must be number, not {port}") protocol = demisto.args()["protocol"] res = http_request("POST", "/shodan/scan/internet", data={"port": port, "protocol": protocol}) ec = {"Shodan": {"Scan": {"ID": res.get("id", "")}}} human_readable = tableToMarkdown( f"Intenet scanning results for port {port} and protocol {protocol}", { "ID": ec["Shodan"]["Scan"]["ID"], }, ) demisto.results( { "Type": entryTypes["note"], "Contents": res, "ContentsFormat": formats["json"], "HumanReadable": human_readable, "HumanReadableFormat": formats["markdown"], "EntryContext": ec, } ) def shodan_scan_status_command(): scan_id = demisto.args()["scanID"] get_scan_status(scan_id) def shodan_create_network_alert_command(): alert_name = demisto.args()["alertName"] ip = demisto.args()["ip"] expires = demisto.args().get("expires", 0) try: expires = int(expires) except ValueError: return_error(f"Expires must be a number, not {expires}") res = http_request( "POST", "/shodan/alert", data=json.dumps({"name": alert_name, "filters": {"ip": ip}, "expires": expires}), headers={"content-type": "application/json"}, ) alert_to_demisto_result(res) def shodan_network_get_alert_by_id_command(): alert_id = demisto.args()["alertID"] res = http_request("GET", f"/shodan/alert/{alert_id}/info") alert_to_demisto_result(res) def shodan_network_get_alerts_command(): res = http_request("GET", "/shodan/alert/info") if len(res) == 0: demisto.results("No alerts") else: for alert in res: alert_to_demisto_result(alert) def shodan_network_delete_alert_command(): alert_id = demisto.args()["alertID"] http_request("DELETE", f"/shodan/alert/{alert_id}") demisto.results(f"Deleted alert {alert_id}") def shodan_network_alert_set_trigger_command(): alert_id = demisto.args()["alertID"] trigger = demisto.args()["Trigger"] res = http_request("PUT", f"/shodan/alert/{alert_id}/trigger/{trigger}") if not res.get("success", False): return_error(f"Failed setting trigger {trigger} for alert {alert_id}") demisto.results(f'Set trigger "{trigger}" for alert {alert_id}') def shodan_network_alert_remove_trigger_command(): alert_id = demisto.args()["alertID"] trigger = demisto.args()["Trigger"] res = http_request("DELETE", f"/shodan/alert/{alert_id}/trigger/{trigger}") if not res.get("success", False): return_error(f"Failed deleting trigger {trigger} for alert {alert_id}") demisto.results(f'Deleted trigger "{trigger}" for alert {alert_id}') def shodan_network_alert_whitelist_service_command(): alert_id = demisto.args()["alertID"] trigger = demisto.args()["trigger"] service = demisto.args()["service"] res = http_request("PUT", f"/shodan/alert/{alert_id}/trigger/{trigger}/ignore/{service}") if not res.get("success", False): return_error(f'Failed whitelisting service "{service}" for trigger {trigger} in alert {alert_id}') demisto.results(f'Whitelisted service "{service}" for trigger {trigger} in alert {alert_id}') def shodan_network_alert_remove_service_from_whitelist_command(): alert_id = demisto.args()["alertID"] trigger = demisto.args()["trigger"] service = demisto.args()["service"] res = http_request("DELETE", f"/shodan/alert/{alert_id}/trigger/{trigger}/ignore/{service}") if not res.get("success", False): return_error(f'Failed removing service "{service}" for trigger {trigger} in alert {alert_id} from the whitelist') demisto.results(f'Removed service "{service}" for trigger {trigger} in alert {alert_id} from the whitelist') def get_events_command(args: dict) -> tuple[str, list[dict]]: """ Get events command, used mainly for debugging """ events = http_request("GET", "/shodan/alert/info") if not isinstance(events, list): events = [events] limit = arg_to_number(args.get("max_fetch")) or DEFAULT_MAX_EVENTS events = filter_events(events, limit) hr = tableToMarkdown(f"{VENDOR.title()} - {PRODUCT.title()} Events:", format_record_keys(events)) return hr, events def fetch_events(last_run: dict, params: dict[str, str]) -> tuple[Dict, List[Dict]]: """ Fetches events from an API, filters them, and updates the last_run data with the latest event's date. Args: last_run (dict): A dictionary containing data from the last run. It should include 'last_fetch_time' and 'last_event_ids', which represent the last fetch time and IDs of the last events processed. params (dict[str, str]): Dictionary of parameters. It should include 'max_fetch' to define the maximum number of events to fetch. Returns: tuple[Dict, List[Dict]]: A tuple where the first item is the updated last_run data, including the latest fetch time and event IDs, and the second item is a list of filtered events. """ if not last_run.get("last_fetch_time"): # If this is a first run last_run = {"last_fetch_time": datetime.now().strftime(DATE_FORMAT)} demisto.debug("First run detected. Setting last_fetch_time to now.") return last_run, [] events = http_request("GET", "/shodan/alert/info") if not isinstance(events, list): events = [events] demisto.debug(f"Fetched {len(events)} events before filtering") limit = arg_to_number(params.get("max_fetch")) or DEFAULT_MAX_EVENTS events_filtered = filter_events(events, limit, last_run) demisto.debug(f"After filtering, {len(events_filtered)} events remain") if events_filtered: latest_fetch_time = max(parse_event_date(event) for event in events_filtered) latest_event_ids = [event.get("id") for event in events_filtered if parse_event_date(event) == latest_fetch_time] last_run["last_fetch_time"] = latest_fetch_time.strftime(DATE_FORMAT) last_run["last_event_ids"] = latest_event_ids else: demisto.debug("No new events found after filtering") return last_run, events_filtered """ COMMANDS MANAGER / SWITCH PANEL """ if demisto.command() == "test-module": # This is the call made when pressing the integration test button. test_module() demisto.results("ok") elif demisto.command() == "search": search_command() elif demisto.command() == "ip": return_results(ip_command()) elif demisto.command() == "shodan-search-count": shodan_search_count_command() elif demisto.command() == "shodan-scan-ip": shodan_scan_ip_command() elif demisto.command() == "shodan-scan-internet": shodan_scan_internet_command() elif demisto.command() == "shodan-scan-status": shodan_scan_status_command() elif demisto.command() == "shodan-create-network-alert": shodan_create_network_alert_command() elif demisto.command() == "shodan-network-get-alert-by-id": shodan_network_get_alert_by_id_command() elif demisto.command() == "shodan-network-get-alerts": shodan_network_get_alerts_command() elif demisto.command() == "shodan-network-delete-alert": shodan_network_delete_alert_command() elif demisto.command() == "shodan-network-alert-set-trigger": shodan_network_alert_set_trigger_command() elif demisto.command() == "shodan-network-alert-remove-trigger": shodan_network_alert_remove_trigger_command() elif demisto.command() == "shodan-network-alert-whitelist-service": shodan_network_alert_whitelist_service_command() elif demisto.command() == "shodan-network-alert-remove-service-from-whitelist": shodan_network_alert_remove_service_from_whitelist_command() elif demisto.command() == "shodan-get-events": args = demisto.args() hr, events = get_events_command(args) return_results(CommandResults(readable_output=hr)) should_push_events = argToBoolean(args.get("should_push_events")) if should_push_events: add_time_to_events(events) send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) elif demisto.command() == "fetch-events": params = demisto.params() last_run = demisto.getLastRun() demisto.debug(f"Last_run before the fetch: {last_run}") next_run, events = fetch_events(last_run, params) add_time_to_events(events) send_events_to_xsiam(events=events, vendor=VENDOR, product=PRODUCT) demisto.debug(f"last_run after the fetch {last_run}") demisto.setLastRun(next_run)