Expanse Deprecated
Deprecated. Use the Expanse v2 integration instead. The Expanse App for Demisto leverages the Expander API to retrieve network exposures and risky flows to create incidents in Demisto. This application also allows for IP, Domain, Certificate, Behavior, and Exposure enrichment, retrieving assets and exposures information drawn from Expanse’s unparalleled view of the Internet.
Data Enrichment & Threat Intelligence · Expanse (Deprecated)
Details
| ID | Expanse |
|---|---|
| Provider | Palo Alto Networks |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 5.0.0 |
| Docker Image | demisto/python3:3.9.5.21272 |
README
Overview
The Expanse App for Cortex XSOAR leverages the Expander API to retrieve network exposures and create incidents in Cortex XSOAR. This application also allows for IP, Domain, Certificate, and Behavior enrichment, retrieving assets and exposures information drawn from Expanse’s unparalleled view of the Internet.
This integration was integrated and tested with Expanse Events API v1, Assets API v2, and Behavior API v1.
Configure Expanse on Cortex XSOAR
- Navigate to Settings > Integrations > Servers & Services.
- Search for Expanse.
- Click Add instance to create and configure a new integration instance.
- Name: a textual name for the integration instance.
- API Key
- Fetch incidents
- Include Behavior data in incidents
- Trust any certificate (not secure)
- Use system proxy settings
- How many events to pull from Expander per run
- How many days to pull past events on first run
- Minimum severity of Expanse Exposure to create an incident for
- Click Test to validate the URLs, token, and connection.
Fetched Incidents Data
{
'eventType': 'ON_PREM_EXPOSURE_APPEARANCE',
'eventTime': '2020-02-05T00:00:00Z',
'businessUnit': {
'id': 'a1f0f39b-f358-3c8c-947b-926887871b88',
'name': 'VanDelay Import-Export'
},
'payload': {
'_type': 'ExposurePayload',
'id': 'b0acfbc5-4d55-3fdb-9155-4927eab91218',
'exposureType': 'NTP_SERVER',
'ip': '203.215.173.113',
'port': 123,
'portProtocol': 'UDP',
'exposureId': '6bedf636-5b6a-3b47-82a5-92b511c0649b',
'domainName': None,
'scanned': '2020-02-05T00:00:00Z',
'geolocation': {
'latitude': 33.7,
'longitude': 73.17,
'city': 'ISLAMABAD',
'regionCode': '',
'countryCode':
'PK'
},
'configuration': {
'_type': 'NtpServerConfiguration',
'response': {
'ntp': {
'leapIndicator': 0,
'mode': 4,
'poll': 4,
'precision': -19,
'stratum': 5,
'delay': 0,
'dispersion': 22,
'version': 4,
'originateTime': '2004-11-24T15:12:11.444Z',
'receiveTime': '2020-02-05T14:25:08.963Z',
'updateTime': '2020-02-05T14:25:01.597Z',
'transmitTime': '2020-02-05T14:25:08.963Z',
'reference': {
'ref_ip': {
'reference': {
'ipv4': '127.127.1.1'
}
}
},
'extentionData': None,
'keyIdentifier': None,
'messageDigest': None
}
}
},
'severity': 'ROUTINE',
'tags': {
'ipRange': ['untagged']
},
'providers': ['InternallyHosted'],
'certificatePem': None,
'remediationStatuses': []
},
'id': 'b4a1e2e6-165a-31a5-9e6a-af286adc3dcd'
}
Fetched Behavior Incident Data
{
"id": "c9704240-5021-321e-a82b-32865e07d541",
"tenantBusinessUnitId": "04b5140e-bbe2-3e9c-9318-a39a3b547ed5",
"businessUnit": {
"id": "6b73ef6c-b230-3797-b321-c4a340169eb7",
"name": "Acme Latex Supply"
},
"riskRule": {
"id": "02b6c647-65f4-4b69-b4b0-64af34fd1b29",
"name": "Connections to and from countries on block list",
"description": "Connections to and from countries on block list (Belarus, Côte d'Ivoire, Cuba, Democratic Republic of the Congo, Iran, Iraq, Liberia, North Korea, South Sudan, Sudan, Syria, Zimbabwe)",
"additionalDataFields": "[]"
},
"internalAddress": "184.174.38.51",
"internalPort": 35125,
"externalAddress": "217.218.108.188",
"externalPort": 443,
"flowDirection": "OUTBOUND",
"acked": true,
"protocol": "TCP",
"externalCountryCodes": [
"IR"
],
"internalCountryCodes": [
"US"
],
"externalCountryCode": "IR",
"internalCountryCode": "US",
"internalExposureTypes": [],
"internalDomains": [],
"internalTags": {
"ipRange": []
},
"observationTimestamp": "2020-03-23T14:59:04.211Z",
"created": "2020-03-24T02:45:28.450131Z"
}
Commands
You can execute these commands from the Cortex XSOAR 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.
- ip
- domain
- expanse-get-certificate
- expanse-get-behavior
- expanse-get-exposures
- expanse-get-domains-for-certificate
1. ip
ip command
Required Permissions
none
Base Command
ip
Input
| Argument Name | Description | Required |
|---|---|---|
| ip | ip address | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| IP.Address | String | Internet Protocol Address |
| IP.Geo.Location | String | The geolocation where the IP address is located, in the format: latitude:longitude |
| IP.Geo.Country | String | The country in which the IP address is located. |
| IP.Geo.Description | String | Additional information about the location |
| Expanse.IP.Address | String | Internet Protocol Address |
| Expanse.IP.Version | String | Internet Protocol Address Version |
| Expanse.IP.BusinessUnits | String | Expanse Business Units this IP belongs to |
| Expanse.IP.IPRange.StartAdress | String | First IP address in IP Network this IP address belongs to |
| Expanse.IP.IPRange.EndAddress | String | Last IP address in IP Network this IP address belongs to |
| Expanse.IP.IPRange.RangeSize | Number | Number of IP addresses in IP Network this IP address belongs to |
| Expanse.IP.IPRange.ResponsiveIPCount | Number | Number of responsive IP addresses in IP Network this IP address belongs to |
| Expanse.IP.IPRange.RangeIntroduced | Date | Date the IP network this IP address belongs to was introduced to Expanse |
| Expanse.IP.IPRange.AttributionReasons | String | The reason why this IP belongs to the IP Range |
| Expanse.IP.Geo.Latitude | String | Geo coordinates: Latitude of IP address |
| Expanse.IP.Geo.Longitude | String | Geo coordinates: Longitude of IP address |
| Expanse.IP.Geo.City | String | Geo coordinates city for this IP address |
| Expanse.IP.Geo.RegionCode | String | Geo coordinates Region Code for this IP address |
| Expanse.IP.Geo.CountryCode | String | Geo coordinates Country Code for this IP address |
| Expanse.IP.Annotations.Tags | String | Customer defined Tags from Expanse related to this IP Range |
| Expanse.IP.Annotations.AdditionalNotes | String | Customer defined Notes from Expanse related to this IP Range |
| Expanse.IP.Annotations.PointsOfContact | String | Customer defined Points of Contact from Expanse related to this IP Range |
| Expanse.IP.SeverityCounts.CRITICAL | Number | Count of CRITICAL Events for this IP address |
| Expanse.IP.SeverityCounts.ROUTINE | Number | Count of ROUTINE Events for this IP address |
| Expanse.IP.SeverityCounts.WARNING | Number | Count of WARNING Events for this IP address |
| Expanse.IP.Geo.Description | String | Additional information about the location |
| Expanse.IP.Geo.Country | String | The country in which the IP address is located. |
Command Example
!ip ip=74.142.119.130
Context Example
{
"IP": {
"Geo": {
"Country": "US",
"Description": "AKRON",
"Location": "41.0433:-81.5239"
},
"Address": "74.142.119.130"
},
"DBotScore": {
"Vendor": "Expanse",
"Indicator": "74.142.119.130",
"Score": 0,
"Type": "ip"
},
"Expanse.IP": {
"Version": "4",
"Annotations": {
"AdditionalNotes": "",
"Tags": [],
"PointsOfContact": []
},
"BusinessUnits": [
"Acme Latex Supply"
],
"SeverityCounts": {
"CRITICAL": 2,
"WARNING": 4,
"ROUTINE": 2
},
"Address": "74.142.119.130",
"Geo": {
"City": "AKRON",
"Description": "AKRON",
"CountryCode": "US",
"Longitude": -81.5239,
"RegionCode": "OH",
"Location": "41.0433:-81.5239",
"Latitude": 41.0433
},
"IPRange": {
"AttributionReasons": [
"This parent range is attributed via IP network registration records for 74.142.119.128\u201374.142.119.135"
],
"ResponsiveIPCount": 1,
"EndAddress": "74.142.119.135",
"RangeIntroduced": "2019-08-02",
"StartAddress": "74.142.119.128",
"RangeSize": 8
}
}
}
Human Readable Output
IP information for: 74.142.119.130
| Address | Annotations | BusinessUnits | Geo | IPRange | SeverityCounts | Version |
|---|---|---|---|---|---|---|
| 74.142.119.130 | AdditionalNotes: null PointsOfContact: null Tags: null |
Acme Latex Supply | Description: AKRON Latitude: 41.0433 Longitude: -81.5239 City: AKRON RegionCode: OH CountryCode: US Location: 41.0433:-81.5239 |
StartAddress: 74.142.119.128 EndAddress: 74.142.119.135 RangeSize: 8 ResponsiveIPCount: 2 RangeIntroduced: 2019-08-02 AttributionReasons: This parent range is attributed via IP network registration records for 74.142.119.128–74.142.119.135 |
CRITICAL: 1 ROUTINE: 4 WARNING: 2 |
4 |
2. domain
domain command
Required Permissions
none
Base Command
domain
Input
| Argument Name | Description | Required |
|---|---|---|
| domain | domain to search | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Domain.Name | String | The domain name, for example: “google.com |
| Domain.DNS | String | A list of IP objects resolved by DNS |
| Domain.CreationDate | Date | The date that the domain was created |
| Domain.DomainStatus | String | The status of the domain |
| Domain.ExpirationDate | Date | The expiration date of the domain |
| Domain.NameServers | String | Name servers of the domain |
| Domain.Organization | String | The organization of the domain |
| Domain.Admin.Country | String | The country of the domain administrator |
| Domain.Admin.Email | String | The email of the domain administrator |
| Domain.Admin.Name | String | The name of the domain administrator |
| Domain.Admin.Phone | String | The phone of the domain administrator |
| Domain.Registrant.Country | String | The country of the registrant |
| Domain.Registrant.Email | String | The email of the registrant |
| Domain.Registrant.Name | String | The name of the registrant |
| Domain.Registrant.Phone | String | The phone of the registrant |
| Domain.WHOIS.DomainStatus | String | The status of the domain |
| Domain.WHOIS.NameServers | String | A list of name servers, for example: “ns1.bla.com, ns2.bla.com” |
| Domain.WHOIS.CreationDate | Date | The date that the domain was created |
| Domain.WHOIS.UpdatedDate | Date | The date that the domain was last updated |
| Domain.WHOIS.ExpirationDate | Date | The date that the domain expires |
| Domain.WHOIS.Registrant.Email | String | The email address of the registrant |
| Domain.WHOIS.Registrant.Name | String | The name of the registrant |
| Domain.WHOIS.Registrant.Phone | String | The phone of the registrant |
| Domain.WHOIS.Registrar.Name | String | The name of the registrar, for example: “GoDaddy” |
| Domain.WHOIS.Registrar.AbuseEmail | String | The email address of the contact for reporting abuse |
| Domain.WHOIS.Registrar.AbusePhone | Unknown | The phone number of contact for reporting abuse |
| Domain.WHOIS.Admin.Name | String | The name of the domain administrator |
| Domain.WHOIS.Admin.Email | String | The email address of the domain administrator |
| Domain.WHOIS.Admin.Phone | Unknown | The phone number of the domain administrator |
| Expanse.Domain.Name | String | The domain name, for example: “google.com |
| Expanse.Domain.DateAdded | Date | Date the domain was added to Expanse |
| Expanse.Domain.FirstObserved | Date | Date Expanse first observed the domain |
| Expanse.Domain.LastObserved | Date | Date Expanse last observed the domain |
| Expanse.Domain.HasLinkedCloudResources | Boolean | Does this domain have linked cloud resources ? |
| Expanse.Domain.SourceDomain | String | Top level domain |
| Expanse.Domain.Tenant | String | Customer defined Tenant from Expanse |
| Expanse.Domain.BusinessUnits | String | Customer defined Business Units from Expanse |
| Expanse.Domain.DNSSEC | String | DNSSEC info |
| Expanse.Domain.RecentIPs | String | Any recent IP addresses Expanse has seen for this domain |
| Expanse.Domain.CloudResources | String | Any Cloud Resources Expanse has seen for this domain |
| Expanse.Domain.LastSubdomainMetadata | String | Any recent subdomain metadata Expanse has seen for this domain |
| Expanse.Domain.ServiceStatus | String | Service Status Expanse sees for this domain |
| Expanse.Domain.LastSampledIP | String | Last seen IP address for this domain |
| Expanse.Domain.DNS | String | A list of IP objects resolved by DNS |
| Expanse.Domain.CreationDate | Date | The date that the domain was created |
| Expanse.Domain.DomainStatus | String | The status of the domain |
| Expanse.Domain.ExpirationDate | Date | The expiration date of the domain |
| Expanse.Domain.NameServers | String | Name servers of the domain |
| Expanse.Domain.Organization | String | The organization of the domain |
| Expanse.Domain.Admin.Country | String | The country of the domain administrator |
| Expanse.Domain.Admin.Email | String | The email address of the domain administrator |
| Expanse.Domain.Admin.Name | String | The name of the domain administrator |
| Expanse.Domain.Admin.Phone | String | The phone number of the domain administrator |
| Expanse.Domain.Registrant.Country | String | The country of the registrant |
| Expanse.Domain.Registrant.Email | String | The email address of the registrant |
| Expanse.Domain.Registrant.Name | String | The name of the registrant |
| Expanse.Domain.Registrant.Phone | String | The phone number for receiving abuse reports |
| Expanse.Domain.WHOIS.DomainStatus | String | The status of the domain |
| Expanse.Domain.WHOIS.NameServers | String | A list of name servers, for example: “ns1.bla.com, ns2.bla.com” |
| Expanse.Domain.WHOIS.CreationDate | Date | The date that the domain was created |
| Expanse.Domain.WHOIS.UpdatedDate | String | The date that the domain was last updated |
| Expanse.Domain.WHOIS.ExpirationDate | String | The date that the domain expires |
| Expanse.Domain.WHOIS.Registrant.Email | String | The email address of the registrant |
| Expanse.Domain.WHOIS.Registrant.Name | String | The name of the registrant |
| Expanse.Domain.WHOIS.Registrant.Phone | String | The phone number of the registrant |
| Expanse.Domain.WHOIS.Registrar.Name | String | The name of the registrar, for example: “GoDaddy” |
| Expanse.Domain.WHOIS.Registrar.AbuseEmail | String | The email address of the contact for reporting abuse |
| Expanse.Domain.WHOIS.Registrar.AbusePhone | String | The phone number of contact for reporting abuse |
| Expanse.Domain.WHOIS.Admin.Name | String | The name of the domain administrator |
| Expanse.Domain.WHOIS.Admin.Email | String | The email address of the domain administrator |
| Expanse.Domain.WHOIS.Admin.Phone | String | The phone number of the domain administrator |
Command Example
!domain domain=atlas.enron.com
Context Example
{
"Domain": {
"Name": "atlas.enron.com",
"Admin": {
"Phone": "14806242599",
"Country": "UNITED STATES",
"Email": "ENRON.COM@domainsbyproxy.com",
"Name": "Registration Private"
},
"DomainStatus": [
"HAS_DNS_RESOLUTION"
],
"NameServers": [
"NS73.DOMAINCONTROL.COM",
"NS74.DOMAINCONTROL.COM"
],
"ExpirationDate": "2019-10-10T04:00:00Z",
"DNS": [],
"Organization": "Domains By Proxy, LLC",
"CreationDate": "1995-10-10T04:00:00Z",
"Registrant": {
"Phone": "14806242599",
"Country": "UNITED STATES",
"Email": "ENRON.COM@domainsbyproxy.com",
"Name": "Registration Private"
},
"WHOIS": {
"Admin": {
"Phone": "14806242599",
"Email": "ENRON.COM@domainsbyproxy.com",
"Name": "Registration Private"
},
"DomainStatus": [
"clientDeleteProhibited clientRenewProhibited clientTransferProhibited clientUpdateProhibited"
],
"NameServers": [
"NS73.DOMAINCONTROL.COM",
"NS74.DOMAINCONTROL.COM"
],
"UpdatedDate": "2015-07-29T16:20:56Z",
"Registrar": {
"AbuseEmail": null,
"AbusePhone": null,
"Name": "GoDaddy.com, LLC"
},
"ExpirationDate": "2019-10-10T04:00:00Z",
"CreationDate": "1995-10-10T04:00:00Z",
"Registrant": {
"Phone": "14806242599",
"Email": "ENRON.COM@domainsbyproxy.com",
"Name": "Registration Private"
}
}
},
"Expanse.Domain": {
"LastSubdomainMetadata": null,
"WHOIS": {
"Admin": {
"Phone": "14806242599",
"Email": "ENRON.COM@domainsbyproxy.com",
"Name": "Registration Private"
},
"DomainStatus": [
"clientDeleteProhibited clientRenewProhibited clientTransferProhibited clientUpdateProhibited"
],
"NameServers": [
"NS73.DOMAINCONTROL.COM",
"NS74.DOMAINCONTROL.COM"
],
"UpdatedDate": "2015-07-29T16:20:56Z",
"Registrar": {
"AbuseEmail": null,
"AbusePhone": null,
"Name": "GoDaddy.com, LLC"
},
"ExpirationDate": "2019-10-10T04:00:00Z",
"CreationDate": "1995-10-10T04:00:00Z",
"Registrant": {
"Phone": "14806242599",
"Email": "ENRON.COM@domainsbyproxy.com",
"Name": "Registration Private"
}
},
"DNSSEC": null,
"DomainStatus": [
"HAS_DNS_RESOLUTION"
],
"HasLinkedCloudResources": false,
"SourceDomain": "enron.com",
"LastObserved": "2020-01-02T09:30:00.374Z",
"ExpirationDate": "2019-10-10T04:00:00Z",
"CloudResources": [],
"Tenant": "VanDelay Industries",
"Name": "atlas.enron.com",
"Admin": {
"Phone": "14806242599",
"Country": "UNITED STATES",
"Email": "ENRON.COM@domainsbyproxy.com",
"Name": "Registration Private"
},
"LastSampledIP": "192.64.147.150",
"BusinessUnits": [
"VanDelay Industries"
],
"DNS": [],
"RecentIPs": [],
"Organization": "Domains By Proxy, LLC",
"DateAdded": "2020-01-04T04:57:48.580Z",
"NameServers": [
"NS73.DOMAINCONTROL.COM",
"NS74.DOMAINCONTROL.COM"
],
"FirstObserved": "2020-01-02T09:30:00.374Z",
"ServiceStatus": [
"NO_ACTIVE_SERVICE",
"NO_ACTIVE_CLOUD_SERVICE",
"NO_ACTIVE_ON_PREM_SERVICE"
],
"CreationDate": "1995-10-10T04:00:00Z",
"Registrant": {
"Phone": "14806242599",
"Country": "UNITED STATES",
"Email": "ENRON.COM@domainsbyproxy.com",
"Name": "Registration Private"
}
},
"DBotScore": {
"Vendor": "Expanse",
"Indicator": "atlas.enron.com",
"Score": 0,
"Type": "url"
}
}
Human Readable Output
Domain information for: atlas.enron.com
| Admin | BusinessUnits | CloudResources | CreationDate | DNS | DNSSEC | DateAdded | DomainStatus | ExpirationDate | FirstObserved | HasLinkedCloudResources | LastObserved | LastSampledIP | LastSubdomainMetadata | Name | NameServers | Organization | RecentIPs | Registrant | ServiceStatus | SourceDomain | Tenant | WHOIS |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Country: UNITED STATES Email: ENRON.COM@domainsbyproxy.com Name: Registration Private Phone: 14806242599 |
VanDelay Industries | 1995-10-10T04:00:00Z | 2020-01-04T04:57:48.580Z | HAS_DNS_RESOLUTION | 2019-10-10T04:00:00Z | 2020-01-02T09:30:00.374Z | false | 2020-01-02T09:30:00.374Z | 192.64.147.150 | atlas.enron.com | NS73.DOMAINCONTROL.COM, NS74.DOMAINCONTROL.COM |
Domains By Proxy, LLC | Country: UNITED STATES Email: ENRON.COM@domainsbyproxy.com Name: Registration Private Phone: 14806242599 |
NO_ACTIVE_SERVICE, NO_ACTIVE_CLOUD_SERVICE, NO_ACTIVE_ON_PREM_SERVICE |
enron.com | VanDelay Industries | DomainStatus: clientDeleteProhibited clientRenewProhibited clientTransferProhibited clientUpdateProhibited NameServers: NS73.DOMAINCONTROL.COM, NS74.DOMAINCONTROL.COM CreationDate: 1995-10-10T04:00:00Z UpdatedDate: 2015-07-29T16:20:56Z ExpirationDate: 2019-10-10T04:00:00Z Registrant: {“Email”: “ENRON.COM@domainsbyproxy.com”, “Name”: “Registration Private”, “Phone”: “14806242599”} Registrar: {“Name”: “GoDaddy.com, LLC”, “AbuseEmail”: null, “AbusePhone”: null} Admin: {“Name”: “Registration Private”, “Email”: “ENRON.COM@domainsbyproxy.com”, “Phone”: “14806242599”} |
3. expanse-get-certificate
expanse-get-certificate command
Required Permissions
none
Base Command
expanse-get-certificate
Input
| Argument Name | Description | Required |
|---|---|---|
| common_name | domain to search | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Expanse.Certificate.SearchTerm | string | The common name searched for |
| Expanse.Certificate.CommonName | string | The certificate common name |
| Expanse.Certificate.FirstObserved | date | Certificate first observation date |
| Expanse.Certificate.LastObserved | date | Certificate last observation date |
| Expanse.Certificate.DateAdded | date | Date certificate was added to Expanse |
| Expanse.Certificate.Provider | string | The certificate provider |
| Expanse.Certificate.NotValidBefore | date | Certificate not-valid-before date |
| Expanse.Certificate.NotValidAfter | date | Certificate not-valid-after date |
| Expanse.Certificate.Properties | string | Certificate properties |
| Expanse.Certificate.MD5Hash | string | Certificate MD5 Hash |
| Expanse.Certificate.PublicKeyAlgorithm | string | Certificate public key algorithm used |
| Expanse.Certificate.PublicKeyBits | number | Public key size |
| Expanse.Certificate.BusinessUnits | string | Business Unit for certificate |
| Expanse.Certificate.CertificateAdvertisementStatus | string | Is Certificate advertised |
| Expanse.Certificate.ServiceStatus | string | Any detected services for the certificate |
| Expanse.Certificate.RecentIPs | string | Any recent IPs the certificate was detected on |
| Expanse.Certificate.CloudResources | string | Any cloud resources returning the certificate |
| Expanse.Certificate.PemSha1 | string | SHA1 hash of the certificate PEM |
| Expanse.Certificate.PemSha256 | string | SHA256 hash of the certificate PEM |
| Expanse.Certificate.Issuer.Name | string | Certificate Issuer name |
| Expanse.Certificate.Issuer.Email | string | Certificate Issuer email |
| Expanse.Certificate.Issuer.Country | string | Certificate Issuer country |
| Expanse.Certificate.Issuer.Org | string | Certificate Issuer Org |
| Expanse.Certificate.Issuer.Unit | string | Certificate Issuer Unit |
| Expanse.Certificate.Issuer.AltNames | string | Certificate Issuer alternative names |
| Expanse.Certificate.Issuer.Raw | string | Certificate Issuer raw details |
| Expanse.Certificate.Subject.Name | string | Certificate Subject name |
| Expanse.Certificate.Subject.Email | string | Certificate Subject email |
| Expanse.Certificate.Subject.Country | string | Certificate Subject country |
| Expanse.Certificate.Subject.Org | string | Certificate Subject Org |
| Expanse.Certificate.Subject.Unit | string | Certificate Subject Unit |
| Expanse.Certificate.Subject.AltNames | string | Certificate Subject alternative names |
| Expanse.Certificate.Subject.Raw | string | Certificate Subject raw details |
Command Example
!expanse-get-certificate common_name=atlas.enron.com
Context Example
{
"Expanse.Certificate": {
"BusinessUnits": "VanDelay Industries",
"CertificateAdvertisementStatus": "NO_CERTIFICATE_ADVERTISEMENT",
"CloudResources": "",
"CommonName": "atlas.enron.com",
"DateAdded": "2019-11-21T09:14:27.308679Z",
"FirstObserved": "2019-11-21T09:14:27.308679Z",
"Issuer": {
"AltNames": "",
"Country": "US",
"Email": null,
"Name": "Let's Encrypt Authority X3",
"Org": "Let's Encrypt",
"Raw": "C=US,O=Let's Encrypt,CN=Let's Encrypt Authority X3",
"Unit": null
},
"LastObserved": ""2019-12-19T09:13:47.208679Z",
"MD5Hash": "VEwAbJfmIFAVcZ_x4lm42g==",
"NotValidAfter": "2019-03-31T00:27:46Z",
"NotValidBefore": "2018-12-31T00:27:46Z",
"PemSha1": "3LAYlmV3xtn4ONJ3C9JN_ogz0u8=",
"PemSha256": "kyERnydF-dzOuCCpG4jDnkGr4fI2a--lBZQz2hyhb30=",
"Properties": "EXPIRED",
"Provider": "None",
"PublicKeyAlgorithm": "RSA",
"PublicKeyBits": 2048,
"RecentIPs": "",
"SearchTerm": "atlas.enron.com",
"ServiceStatus": "NO_ACTIVE_SERVICE,NO_ACTIVE_ON_PREM_SERVICE,NO_ACTIVE_CLOUD_SERVICE",
"Subject": {
"AltNames": "atlas.enron.com",
"Country": "US",
"Email": "ENRON.COM@domainsbyproxy.com",
"Name": "atlas.enron.com",
"Org": "ENRON",
"Raw": "CN=api-dev.radioshack.com",
"Unit": null
}
}
}
Human Readable Output
Certificate information for: atlas.enron.com
| BusinessUnits | CertificateAdvertisementStatus | CloudResources | CommonName | DateAdded | FirstObserved | Issuer | LastObserved | MD5Hash | NotValidAfter | NotValidBefore | PemSha1 | PemSha256 | Properties | Provider | PublicKeyAlgorithm | PublicKeyBits | RecentIPs | SearchTerm | ServiceStatus | Subject |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| VanDelay Industries | NO_CERTIFICATE_ADVERTISEMENT | atlas.enron.com | 2019-11-21T09:14:27.308679Z | Name: Let’s Encrypt Authority X3 Email: null Country: US Org: Let’s Encrypt Unit: null AltNames: Raw: C=US,O=Let’s Encrypt,CN=Let’s Encrypt Authority X3 |
VEwAbJfmIFAVcZ_x4lm42g== | 2019-03-31T00:27:46Z | 2018-12-31T00:27:46Z | 3LAYlmV3xtn4ONJ3C9JN_ogz0u8= | kyERnydF-dzOuCCpG4jDnkGr4fI2a–lBZQz2hyhb30= | EXPIRED | None | RSA | 2048 | atlas.enron.com | NO_ACTIVE_SERVICE,NO_ACTIVE_ON_PREM_SERVICE,NO_ACTIVE_CLOUD_SERVICE | Name: atlas.enron.com Email: ENRON.COM@domainsbyproxy.com Country: US Org: null Unit: null AltNames: atlas.enron.com Raw: CN=atlas.enron.com |
4. expanse-get-behavior
expanse-get-behavior command
Required Permissions
none
Base Command
expanse-get-behavior
Input
| Argument Name | Description | Required |
|---|---|---|
| ip | ip to search | Required |
| start_time | ISO-8601 UTC timestamp denoting the earliest behavior data to fetch | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Expanse.Behavior.SearchTerm | string | IP used to search |
| Expanse.Behavior.InternalAddress | string | IP internal to Organization |
| Expanse.Behavior.InternalCountryCode | string | Internal IP Country Geolocation country |
| Expanse.Behavior.BusinessUnit | string | Buisness unit of IP |
| Expanse.Behavior.InternalDomains | string | Known domains associated with IP |
| Expanse.Behavior.InternalIPRanges | string | Known Internal IP ranges containing IP |
| Expanse.Behavior.InternalExposureTypes | string | Known Exposures for IP |
| Expanse.Behavior.ExternalAddresses | string | External IP addresses with known communication to IP |
| Expanse.Behavior.FlowSummaries | string | Summaries of most recents risky flows for IP |
| Expanse.Behavior.Flows | string | Array of Flow Objects |
| Expanse.Behavior.Flows.InternalAddress | string | Internal IP address for flow |
| Expanse.Behavior.Flows.InternalPort | number | Internal Port for flow |
| Expanse.Behavior.Flows.InternalCountryCode | string | Internal country code for flow |
| Expanse.Behavior.Flows.ExternalAddress | string | External IP address for flow |
| Expanse.Behavior.Flows.ExternalPort | number | External Port for flow |
| Expanse.Behavior.Flows.ExternalCountryCode | string | External country code for flow |
| Expanse.Behavior.Flows.Timestamp | date | Timestamp of flow |
| Expanse.Behavior.Flows.Protocol | string | Protocol of flow (UDP, TCP) |
| Expanse.Behavior.Flows.Direction | string | Direction of flow |
| Expanse.Behavior.Flows.RiskRule | string | Risk rule violated by flow |
Command Example
!expanse-get-behavior ip=74.142.119.130 start_time=7
Context Example
{
"BusinessUnit": "VanDelay Industries",
"ExternalAddresses": "66.110.49.36,66.110.49.72",
"FlowSummaries": "74.142.119.130:57475 (US) -\u003e 66.110.49.72:443 (CA) TCP violates Outbound Flows from Servers at 2020-04-05T21:18:56.889Z\n74.142.119.130:61694 (US) -\u003e 66.110.49.36:443 (CA) TCP violates Outbound Flows from Servers at 2020-04-05T21:03:50.867Z\n",
"Flows": [
{
"Direction": "OUTBOUND",
"ExternalAddress": "66.110.49.72",
"ExternalCountryCode": "CA",
"ExternalPort": 443,
"InternalAddress": "74.142.119.130",
"InternalCountryCode": "US",
"InternalPort": 57475,
"Protocol": "TCP",
"RiskRule": "Outbound Flows from Servers",
"Timestamp": "2020-04-05T21:18:56.889Z"
},
{
"Direction": "OUTBOUND",
"ExternalAddress": "66.110.49.36",
"ExternalCountryCode": "CA",
"ExternalPort": 443,
"InternalAddress": "74.142.119.130",
"InternalCountryCode": "US",
"InternalPort": 61694,
"Protocol": "TCP",
"RiskRule": "Outbound Flows from Servers",
"Timestamp": "2020-04-05T21:03:50.867Z"
}
],
"InternalAddress": "74.142.119.130",
"InternalCountryCode": "US",
"InternalDomains": "",
"InternalExposureTypes": "HttpServer",
"InternalIPRanges": "",
"SearchTerm": "74.142.119.130"
}
Human Readable Output
Expanse Behavior information for: 74.142.119.130
| BusinessUnit | ExternalAddresses | FlowSummaries | InternalAddress | InternalCountryCode | InternalDomains | InternalExposureTypes | InternalIPRanges | SearchTerm |
|---|---|---|---|---|---|---|---|---|
| VanDelay Industries | 66.110.49.36,66.110.49.72 | 74.142.119.130:57475 (US) -> 66.110.49.72:443 (CA) TCP violates Outbound Flows from Servers at 2020-04-05T21:18:56.889Z 74.142.119.130:61694 (US) -> 66.110.49.36:443 (CA) TCP violates Outbound Flows from Servers at 2020-04-05T21:03:50.867Z |
74.142.119.130 | US | HttpServer | 74.142.119.130 |
4. expanse-get-exposures
expanse-get-exposures command
Required Permissions
none
Base Command
expanse-get-exposures
Input
| Argument Name | Description | Required |
|---|---|---|
| ip | ip to search | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Expanse.Exposures.SearchTerm | string | IP used to search |
| Expanse.Exposures.TotalExposureCount | number | The total count of exposures for the IP |
| Expanse.Exposures.CriticalExposureCount | number | The total count of CRITICAL exposures for the IP |
| Expanse.Exposures.WarningExposureCount | number | The total count of WARNING exposures for the IP |
| Expanse.Exposures.RoutineExposureCount | number | The total count of ROUTINE exposures for the IP |
| Expanse.Exposures.UnknownExposureCount | number | The total count of UNKNOWN exposures for the IP |
| Expanse.Exposures.ExposureSummaries | string | Summaries of exposures for the IP address |
| Expanse.Exposures.Exposures | unknown | Array of Exposures for the IP address |
| Expanse.Exposures.Exposures.ExposureType | string | Exposure type of the Exposure |
| Expanse.Exposures.Exposures.BusinessUnit | string | Business Unit of the Exposure |
| Expanse.Exposures.Exposures.Ip | string | IP Address the Exposure was found on |
| Expanse.Exposures.Exposures.Port | string | Port the Exposure was found on |
| Expanse.Exposures.Exposures.Severity | string | Severity of the Exposure |
| Expanse.Exposures.Exposures.Certificate | unknown | Certificate details associated with Exposure |
| Expanse.Exposures.Exposures.FirstObservsation | unknown | First Observation of the Exposure |
| Expanse.Exposures.Exposures.LastObservsation | unknown | Last Observation of the Exposure |
| Expanse.Exposures.Exposures.Status | unknown | Status details of the Exposure |
| Expanse.Exposures.Exposures.Provider | unknown | Provider details of the Exposure |
Command Example
!expanse-get-exposures ip=33.2.243.123
Context Example
{
"CriticalExposureCount": 0,
"ExposureSummaries": "NTP_SERVER exposure on 33.2.243.123:UDP123",
"Exposures": [
{
"BusinessUnit": "VanDelay Industries",
"Certificate": null,
"ExposureType": "NTP_SERVER",
"FirstObservsation": {
"configuration": {
"certificate": null,
"response": {
"ntp": {
"delay": 0,
"dispersion": 65537,
"extentionData": null,
"keyIdentifier": null,
"leapIndicator": 3,
"messageDigest": null,
"mode": 4,
"originateTime": "2004-11-24T15:12:11.444Z",
"poll": 4,
"precision": -18,
"receiveTime": "2019-02-01T00:32:17.693Z",
"reference": {
"ref_str": {
"reference": ""
}
},
"stratum": 0,
"transmitTime": "2019-02-01T00:32:17.693Z",
"updateTime": "2036-02-07T06:28:16Z",
"version": 4
}
}
},
"geolocation": {
"city": "VICTOR",
"countryCode": "US",
"latitude": 42.982,
"longitude": -77.4245,
"regionCode": "NY"
},
"hostname": null,
"id": "2d349139-1111-3c92-a168-557d34729bf8",
"ip": "33.2.243.123",
"portNumber": 123,
"portProtocol": "UDP",
"qrispTaskId": 21716146,
"scanned": "2019-02-01T00:19:16Z"
},
"Ip": "33.2.243.123",
"LastObservsation": {
"configuration": {
"certificate": null,
"response": {
"ntp": {
"delay": 0,
"dispersion": 65537,
"extentionData": null,
"keyIdentifier": null,
"leapIndicator": 3,
"messageDigest": null,
"mode": 4,
"originateTime": "2004-11-24T15:12:11.444Z",
"poll": 4,
"precision": -18,
"receiveTime": "2020-05-05T16:05:36.606Z",
"reference": {
"ref_str": {
"reference": ""
}
},
"stratum": 0,
"transmitTime": "2020-05-05T16:05:36.606Z",
"updateTime": "2036-02-07T06:28:16Z",
"version": 4
}
}
},
"geolocation": {
"city": "VICTOR",
"countryCode": "US",
"latitude": 42.982,
"longitude": -77.4245,
"regionCode": "NY"
},
"hostname": null,
"id": "69a0159b-facc-3c55-b71d-3e6b8ae9252b",
"ip": "33.2.243.123",
"portNumber": 123,
"portProtocol": "UDP",
"qrispTaskId": 41755001,
"scanned": "2020-05-05T16:03:30Z"
},
"Port": "UDP123",
"Provider": null,
"Severity": "ROUTINE",
"Status": {
"remediation": [],
"snooze": []
}
}
],
"RoutineExposureCount": 1,
"SearchTerm": "33.2.243.123",
"TotalExposureCount": 1,
"UnknownExposureCount": 0,
"WarningExposureCount": 0
}
Human Readable Output
Expanse Exposure information for: 33.2.243.123
| CriticalExposureCount | ExposureSummaries | RoutineExposureCount | SearchTerm | TotalExposureCount | UnknownExposureCount | WarningExposureCount |
|---|---|---|---|---|---|---|
| 0 | NTP_SERVER exposure on 33.2.243.123:UDP123 | 1 | 33.2.243.123 | 1 | 0 | 0 |
4. expanse-get-domains-for-certificate
expanse-get-domains-for-certificate command
Required Permissions
none
Base Command
expanse-get-domains-for-certificate
Input
| Argument Name | Description | Required |
|---|---|---|
| common_name | The certificate common name | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Expanse.IPDomains.SearchTerm | string | The common name that was searched |
| Expanse.IPDomains.TotalDomainCount | number | The number of domains found matching the specified certificate |
| Expanse.IPDomains.FlatDomainList | number | An array of all domain names found. This is truncated at 50 |
| Expanse.IPDomains.DomainList | number | An array of domain objects. This is truncated at 50 |
Command Example
!expanse-get-domains-for-certificate common_name="*.us.expanse.co"
Context Example
{
"SearchTerm": "*.us.expanse.co",
"TotalDomainCount": 2,
"FlatDomainList": ["california.us.expanse.co", "dc.us.expanse.co"]
"DomainList": [
{
"ip": "33.2.243.123",
"domain": "california.us.expanse.co",
"type": "DOMAIN_RESOLUTION",
"assetType": "DOMAIN",
"assetKey": "california.us.expanse.co",
"provider": {
"id": "AWS",
"name": "Amazon Web Services"
},
"lastObserved": "2020-06-22T05:20:32.883Z",
"tenant": {
"id": "4b7efca7-c595-408e-b4d1-634080e48367",
"name": "Palo Alto Networks",
"tenantId": "4b7efca7-c595-408e-b4d1-634080e48367"
},
"businessUnits": [
{
"id": "a1f0f39b-f358-3c8c-947b-926887871b88",
"name": "VanDelay Import-Export"
"tenantId": "a1f0f39b-f358-3c8c-947b-926887871b88"
}
],
"commonName": null
},
{
"ip": "33.2.243.123",
"domain": "dc.us.expanse.co",
"type": "DOMAIN_RESOLUTION",
"assetType": "DOMAIN",
"assetKey": "dc.us.expanse.co",
"provider": {
"id": "AWS",
"name": "Amazon Web Services"
},
"lastObserved": "2020-06-21T07:20:32.883Z",
"tenant": {
"id": "4b7efca7-c595-408e-b4d1-634080e48367",
"name": "Palo Alto Networks",
"tenantId": "4b7efca7-c595-408e-b4d1-634080e48367"
},
"businessUnits": [
{
"id": "a1f0f39b-f358-3c8c-947b-926887871b88",
"name": "VanDelay Import-Export"
"tenantId": "a1f0f39b-f358-3c8c-947b-926887871b88"
}
],
"commonName": null
}
]
}
Human Readable Output
Expanse Domains matching Certificate Common Name: *.us.expanse.co
| FlatDomainList | SearchTerm | TotalDomainCount |
|---|---|---|
| california.us.expanse.co, dc.us.expanse.co | *.us.expanse.co | 2 |
Contact Details
For Product Support, please contact your Technical Account Manager or email help@expanseinc.com
Configuration parameters
api_key— API Key (required)isFetch— Fetch incidentsincidentType— Incident typeinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsbehavior— Include Behavior data in incidentspage_limit— How many events to pull from Expander per runfirst_run— How many days to pull past events on first runminimum_severity— Minimum severity of Expanse Exposure to create an incident for
Commands (6)
-
domainSubmits a domain to check.
-
expanse-get-behaviorReturns Risky Flows for the specified IP address. The maximum number of results is 20.
-
expanse-get-certificateReturns information about the domain certificate.
-
expanse-get-domains-for-certificateReturns all domains which have been seen with the specified certificate.
-
expanse-get-exposuresDeprecated command by Expanse. Returns exposure information about the ip.
-
ipSubmits an IP address to check.
import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * ''' IMPORTS ''' import dateparser import json import re import requests import traceback from datetime import datetime, timedelta # Disable insecure warnings requests.packages.urllib3.disable_warnings() ''' GLOBALS/PARAMS ''' API_KEY = demisto.params().get('api_key') PAGE_LIMIT = int(demisto.params().get('page_limit', '10')) FIRST_RUN = int(demisto.params().get('first_run', '7')) SERVER = 'https://expander.expanse.co' VERIFY_CERTIFICATES = not demisto.params().get('insecure') BEHAVIOR_ENABLED = demisto.params().get('behavior', False) MINIMUM_SEVERITY = demisto.params().get('minimum_severity', 'WARNING') BASE_URL = SERVER EXPOSURE_EVENT_TYPES = "ON_PREM_EXPOSURE_APPEARANCE,ON_PREM_EXPOSURE_REAPPEARANCE," \ "CLOUD_EXPOSURE_APPEARANCE,CLOUD_EXPOSURE_REAPPEARANCE" API_ENDPOINTS = { "exposures/ip-ports": { "version": 2 }, "ip-range": { "version": 2 }, "assets/domains": { "version": 2 }, "IdToken": { "version": 1 }, "events": { "version": 1 }, "behavior/risky-flows": { "version": 1 }, "assets/certificates": { "version": 2 }, "assets/ips": { "version": 2 } } EXPOSURE_SEVERITY_MAPPING = { "NONE": 0, "UNKNOWN": 0, "CRITICAL": 3, "ROUTINE": 1, "WARNING": 2, "UNCATEGORIZED": 1 } ''' HELPER FUNCTIONS ''' def make_headers(endpoint, token): """ provides proper headers for differing authentication methods to API """ headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'Expanse_Demisto/1.1.3' } if endpoint == "IdToken": headers['Authorization'] = 'Bearer ' + token else: headers['Authorization'] = 'JWT ' + token return headers def make_url(endpoint): """ build URL based on endpoint """ url = "{BASE_URL}/api/v{version}/{endpoint}".format( BASE_URL=BASE_URL, version=API_ENDPOINTS.get(endpoint, {}).get('version', 2), endpoint=endpoint ) return url def get_page_token(url): o = {'pagetoken': False} for i in url.split("&"): r = i.split("=") o[r[0]] = r[1] return o['pageToken'] def get_next_offset(url): offset = 0 matches = re.findall(r'offset\]\=(\d+)', url) if matches != []: offset = matches[0] return offset def do_auth(): """ perform authentication using API_KEY, stores token and stored timestamp in integration context, retrieves new token when expired """ auth = demisto.getIntegrationContext() now_epoch = int(datetime.today().strftime('%s')) if ("token" in auth or "stored" in auth) and int(auth['stored']) + (60 * 60 * 2) > int(now_epoch): # if integration context contains token and stored and the token is not expired then return token return auth['token'] else: # fetch new token r = http_request('GET', 'IdToken', token=API_KEY) if r.get('token') is None: return_error("Authorization failed") demisto.setIntegrationContext({ 'token': r['token'], 'stored': now_epoch }) return r['token'] def http_request(method, endpoint, params=None, token=False): """ make api call """ if not token: return_error("No authorization token provided") head = make_headers(endpoint, token) url = make_url(endpoint) demisto.debug("Making request to {} with params: {}".format(url, params)) r = requests.request( method, url, params=params, headers=head, verify=VERIFY_CERTIFICATES ) if r.status_code != 200: demisto.error(r.text) return_error('Error in API call [%d] - %s' % (r.status_code, r.reason)) try: res_json = r.json() return res_json except json.decoder.JSONDecodeError as err: raise ValueError('Failed to parse response as JSON. Original response:\n{rtext}.\nError: {error}' .format(rtext=r.text, error=str(err))) def parse_events(events): """ build incidents from active exposures """ incidents = [] for event in events['data']: if EXPOSURE_SEVERITY_MAPPING[event['payload']['severity']] >= EXPOSURE_SEVERITY_MAPPING[MINIMUM_SEVERITY]: incident = { 'name': "{type} on {ip}:{port}/{protocol}".format( type=event['payload']['exposureType'], ip=event['payload']['ip'], protocol=event['payload']['portProtocol'], port=event['payload']['port'] ), 'occurred': event['eventTime'], 'rawJSON': json.dumps(event), 'type': 'Expanse Appearance', 'CustomFields': { 'expanserawjsonevent': json.dumps(event) }, 'severity': EXPOSURE_SEVERITY_MAPPING[event['payload']['severity']] } incidents.append(incident) return incidents def parse_behavior(risky_flows): """ build incidents from risky flows """ incidents = [] for flow in risky_flows['data']: incident = { 'name': "{rule} {int_}:{int_port} : {ext}:{ext_port}".format( rule=flow['riskRule']['name'], int_=flow['internalAddress'], int_port=flow['internalPort'], ext=flow['externalAddress'], ext_port=flow['externalPort'] ), 'occurred': flow['observationTimestamp'], 'rawJSON': json.dumps(flow), 'type': 'Expanse Behavior', 'CustomFields': { 'expanserawjsonevent': json.dumps(flow) }, 'severity': 2 # All behavior is cast to a warning, we can revisit if critically is added to flow data } incidents.append(incident) return incidents def is_not_empty_value(value): """ Checks for empty response values. Demisto recommends returning the None type if a value is empty, rather than an empty string/list. """ return value != "" and value != [] and value != [""] def get_ip_context(data): """ provide custom context information about ip address with data from Expanse API """ geo = {} if len(data.get('locationInformation', [])) > 0: if (data['locationInformation'][0].get('geolocation', {}).get('latitude') is not None and data['locationInformation'][0].get('geolocation', {}).get('longitude') is not None): geo["Location"] = "{0}:{1}".format( data['locationInformation'][0].get('geolocation', {}).get('latitude'), data['locationInformation'][0].get('geolocation', {}).get('longitude') ) geo["Country"] = data['locationInformation'][0].get('geolocation', {}).get('countryCode') geo["Description"] = data['locationInformation'][0].get('geolocation', {}).get('city') return { "Address": data['search'], "Geo": geo, } def get_expanse_ip_context(data): """ provide custom context information about ip address with data from Expanse API """ c = { "Address": data['search'], "Version": data['ipVersion'], "BusinessUnits": [], "IPRange": { "StartAddress": data['startAddress'], "EndAddress": data['endAddress'], "RangeSize": data['rangeSize'], "ResponsiveIPCount": data['responsiveIpCount'], "RangeIntroduced": data['rangeIntroduced'], "AttributionReasons": [] }, "Annotations": { "AdditionalNotes": data['annotations'].get('additionalNotes'), }, "SeverityCounts": { "CRITICAL": 0, "ROUTINE": 0, "WARNING": 0, } } for i in data['severityCounts']: if i['type'] == "CRITICAL": c['SeverityCounts']['CRITICAL'] = i['count'] elif i['type'] == "ROUTINE": c['SeverityCounts']['ROUTINE'] = i['count'] elif i['type'] == "WARNING": c['SeverityCounts']['WARNING'] = i['count'] for i in data['businessUnits']: c['BusinessUnits'].append(i['name']) for i in data['attributionReasons']: c['IPRange']['AttributionReasons'].append(i['reason']) geo = {} if len(data.get("locationInformation", [])) > 0: geo = { "Description": data['locationInformation'][0]['geolocation'].get('city'), "Latitude": data['locationInformation'][0]['geolocation'].get('latitude'), "Longitude": data['locationInformation'][0]['geolocation'].get('longitude'), "City": data['locationInformation'][0]['geolocation'].get('city'), "RegionCode": data['locationInformation'][0]['geolocation'].get('regionCode'), "CountryCode": data['locationInformation'][0]['geolocation'].get('countryCode') } if (data['locationInformation'][0].get('geolocation', {}).get('latitude') is not None and data['locationInformation'][0].get('geolocation', {}).get('longitude') is not None): geo["Location"] = "{0}:{1}".format( data['locationInformation'][0].get('geolocation', {}).get('latitude'), data['locationInformation'][0].get('geolocation', {}).get('longitude') ) c["Geo"] = geo points_of_contact = ",".join([poc["email"] for poc in data['annotations'].get('pointsOfContact', [])]) c["Annotations"]["PointsOfContact"] = points_of_contact if is_not_empty_value(points_of_contact) else None tags = ",".join([tag["name"] for tag in data['annotations'].get('tags', [])]) c["Annotations"]["Tags"] = tags if is_not_empty_value(tags) else None return c def get_domain_context(data): """ provide standard context information about domain with data from Expanse API """ return { "Name": data['domain'], "DNS": ((data.get('details') or {}).get('recentIps') if is_not_empty_value((data.get('details') or {}).get('recentIps')) else None), "CreationDate": data['whois'][0]['creationDate'], "DomainStatus": data['dnsResolutionStatus'], "ExpirationDate": data['whois'][0]['registryExpiryDate'], "NameServers": data['whois'][0]['nameServers'], "Organization": data['whois'][0]['registrant']['organization'], "Admin": { "Country": data['whois'][0]['admin'].get('country'), "Email": data['whois'][0]['admin'].get('emailAddress'), "Name": data['whois'][0]['admin'].get('name'), "Phone": data['whois'][0]['admin'].get('phoneNumber') }, "Registrant": { "Country": data['whois'][0]['registrant'].get('country'), "Email": data['whois'][0]['registrant'].get('emailAddress'), "Name": data['whois'][0]['registrant'].get('name'), "Phone": data['whois'][0]['registrant'].get('phoneNumber') }, "WHOIS": { "DomainStatus": data['whois'][0]['domainStatuses'], "NameServers": data['whois'][0]['nameServers'], "CreationDate": (data['whois'][0].get('creationDate') if is_not_empty_value(data['whois'][0].get('creationDate')) else None), "UpdatedDate": (data['whois'][0].get('updatedDate') if is_not_empty_value(data['whois'][0].get('updatedDate')) else None), "ExpirationDate": (data['whois'][0].get('registryExpiryDate') if is_not_empty_value(data['whois'][0].get('registryExpiryDate')) else None), "Registrant": { "Email": data['whois'][0]['registrant'].get('emailAddress'), "Name": data['whois'][0]['registrant'].get('name'), "Phone": data['whois'][0]['registrant'].get('phoneNumber') }, "Registrar": { "Name": data['whois'][0]['registrar'].get('name'), "AbuseEmail": data['whois'][0]['registrar'].get('abuseContactEmail'), "AbusePhone": data['whois'][0]['registrar'].get('abuseContactPhone') }, "Admin": { "Name": data['whois'][0]['admin'].get('name'), "Email": data['whois'][0]['admin'].get('emailAddress'), "Phone": data['whois'][0]['admin'].get('phoneNumber') } }, } def get_expanse_domain_context(data): """ provide custom context information about domain with data from Expanse API """ c = { "Name": data['domain'], "DNS": ((data.get('details') or {}).get('recentIps') if is_not_empty_value(((data.get('details') or {}).get('recentIps'))) else None), "CreationDate": data['whois'][0].get('creationDate'), "DomainStatus": data.get('dnsResolutionStatus'), "ExpirationDate": (data['whois'][0].get('registryExpiryDate') if is_not_empty_value(data['whois'][0].get('registryExpiryDate')) else None), "NameServers": data['whois'][0].get('nameServers'), "Organization": data['whois'][0]['registrant'].get('organization'), "Admin": { "Country": (data['whois'][0]['admin'].get('country') if is_not_empty_value(data['whois'][0]['admin'].get('country')) else None), "Email": (data['whois'][0]['admin'].get('emailAddress') if is_not_empty_value(data['whois'][0]['admin'].get('emailAddress')) else None), "Name": (data['whois'][0]['admin'].get('name') if is_not_empty_value(data['whois'][0]['admin'].get('name')) else None), "Phone": (data['whois'][0]['admin'].get('phoneNumber') if is_not_empty_value(data['whois'][0]['admin'].get('phoneNumber')) else None) }, "Registrant": { "Country": (data['whois'][0]['registrant'].get('country') if is_not_empty_value(data['whois'][0]['registrant'].get('country')) else None), "Email": (data['whois'][0]['registrant'].get('emailAddress') if is_not_empty_value(data['whois'][0]['registrant'].get('emailAddress')) else None), "Name": (data['whois'][0]['registrant'].get('name') if is_not_empty_value(data['whois'][0]['registrant'].get('name')) else None), "Phone": (data['whois'][0]['registrant'].get('phoneNumber') if is_not_empty_value(data['whois'][0]['registrant'].get('phoneNumber')) else None) }, "WHOIS": { "DomainStatus": (data['whois'][0].get('domainStatuses') if is_not_empty_value(data['whois'][0].get('domainStatuses')) else None), "NameServers": (data['whois'][0].get('nameServers') if is_not_empty_value(data['whois'][0].get('nameServers')) else None), "CreationDate": (data['whois'][0].get('creationDate') if is_not_empty_value(data['whois'][0].get('creationDate')) else None), "UpdatedDate": (data['whois'][0].get('updatedDate') if is_not_empty_value(data['whois'][0].get('updatedDate')) else None), "ExpirationDate": (data['whois'][0].get('registryExpiryDate') if is_not_empty_value(data['whois'][0].get('registryExpiryDate')) else None), "Registrant": { "Email": (data['whois'][0]['registrant'].get('emailAddress') if is_not_empty_value(data['whois'][0]['registrant'].get('emailAddress')) else None), "Name": (data['whois'][0]['registrant'].get('name') if is_not_empty_value(data['whois'][0]['registrant'].get('name')) else None), "Phone": (data['whois'][0]['registrant'].get('phoneNumber') if is_not_empty_value(data['whois'][0]['registrant'].get('phoneNumber')) else None) }, "Registrar": { "Name": (data['whois'][0]['registrar'].get('name') if is_not_empty_value(data['whois'][0]['registrar'].get('name')) else None), "AbuseEmail": (data['whois'][0]['registrar'].get('abuseContactEmail') if is_not_empty_value(data['whois'][0]['registrar'].get('abuseContactEmail')) else None), "AbusePhone": (data['whois'][0]['registrar'].get('abuseContactPhone') if is_not_empty_value(data['whois'][0]['registrar'].get('abuseContactPhone')) else None) }, "Admin": { "Name": (data['whois'][0]['admin'].get('name') if is_not_empty_value(data['whois'][0]['admin'].get('name')) else None), "Email": (data['whois'][0]['admin'].get('emailAddress') if is_not_empty_value(data['whois'][0]['admin'].get('emailAddress')) else None), "Phone": (data['whois'][0]['admin'].get('phoneNumber') if is_not_empty_value(data['whois'][0]['admin'].get('phoneNumber')) else None) } }, "DateAdded": data['dateAdded'], "FirstObserved": data['firstObserved'], "LastObserved": data['lastObserved'], "HasLinkedCloudResources": data['hasLinkedCloudResources'], "SourceDomain": data.get('sourceDomain') if is_not_empty_value(data.get('sourceDomain')) else None, "Tenant": data['tenant'].get('name') if is_not_empty_value(data['tenant'].get('name')) else None, "BusinessUnits": [], "DNSSEC": data['whois'][0].get('dnssec') if is_not_empty_value(data['whois'][0].get('dnssec')) else None, "RecentIPs": ((data.get('details') or {}).get('recentIps') if is_not_empty_value(((data.get('details') or {}).get('recentIps'))) else None), "CloudResources": ((data['details'] or {}).get('cloudResources') if is_not_empty_value(((data['details'] or {}).get('cloudResources'))) else None), "LastSubdomainMetadata": (data.get('lastSubdomainMetadata') if is_not_empty_value(data.get('lastSubdomainMetadata')) else None), "ServiceStatus": data.get('serviceStatus') if is_not_empty_value(data.get('serviceStatus')) else None, "LastSampledIP": data.get('lastSampledIp') if is_not_empty_value(data.get('lastSampledIp')) else None } for i in data['businessUnits']: c['BusinessUnits'].append(i['name']) return c def get_expanse_certificate_context(data): """ provide custom context information about certificate with data from Expanse API """ return { "SearchTerm": data['search'], "CommonName": data['commonName'], "FirstObserved": data['firstObserved'], "LastObserved": data['lastObserved'], "DateAdded": data['dateAdded'], "Provider": data['providers'][0]['name'], "NotValidBefore": data['certificate']['validNotBefore'], "NotValidAfter": data['certificate']['validNotAfter'], "Issuer": { "Name": data['certificate']['issuerName'], "Email": data['certificate']['issuerEmail'], "Country": data['certificate']['issuerCountry'], "Org": data['certificate']['issuerOrg'], "Unit": data['certificate']['issuerOrgUnit'], "AltNames": data['certificate']['issuerAlternativeNames'], "Raw": data['certificate']['issuer'] }, "Subject": { "Name": data['certificate']['subjectName'], "Email": data['certificate']['subjectEmail'], "Country": data['certificate']['subjectCountry'], "Org": data['certificate']['subjectOrg'], "Unit": data['certificate']['subjectOrgUnit'], "AltNames": data['certificate']['subjectAlternativeNames'], "Raw": data['certificate']['subject'] }, "Properties": data['properties'][0], "MD5Hash": data['certificate']['md5Hash'], "PublicKeyAlgorithm": data['certificate']['publicKeyAlgorithm'], "PublicKeyBits": data['certificate']['publicKeyBits'], "BusinessUnits": data['businessUnits'][0]['name'], "CertificateAdvertisementStatus": data['certificateAdvertisementStatus'][0], "ServiceStatus": ','.join(data['serviceStatus']), "RecentIPs": ','.join(data['details']['recentIps']), "CloudResources": ','.join(data['details']['cloudResources']), "PemSha1": data['certificate']['pemSha1'], "PemSha256": data['certificate']['pemSha256'] } def get_expanse_behavior_context(data): """ provides custom context information from the Expanse Behavior API """ def flow_to_str(flow): """ Reduces a risky flow to a summary string """ return "{in_ip}:{in_port} ({in_co}) {direction} {ex_ip}:{ex_port} ({ex_co}) {pro} violates {rule} at {t}".format( in_ip=flow['internalAddress'], in_port=flow['internalPort'], in_co=flow['internalCountryCode'], direction="<-" if flow['flowDirection'] == "INBOUND" else "->", ex_ip=flow['externalAddress'], ex_port=flow['externalPort'], ex_co=flow['externalCountryCode'], pro=flow['protocol'], rule=flow['riskRule']['name'], t=flow['observationTimestamp'] ) def flow_to_obj(flow): return { "InternalAddress": flow['internalAddress'], "InternalPort": flow['internalPort'], "InternalCountryCode": flow['internalCountryCode'], "ExternalAddress": flow['externalAddress'], "ExternalPort": flow['externalPort'], "ExternalCountryCode": flow['externalCountryCode'], "Protocol": flow['protocol'], "Timestamp": flow['observationTimestamp'], "Direction": flow['flowDirection'], "RiskRule": flow['riskRule']['name'] } return { "SearchTerm": data[0]['internalAddress'], "InternalAddress": data[0]['internalAddress'], "InternalCountryCode": data[0]['internalCountryCode'], "BusinessUnit": data[0]['businessUnit']['name'], "FlowSummaries": '\n'.join([flow_to_str(flow) for flow in data]), "Flows": [flow_to_obj(flow) for flow in data], "ExternalAddresses": ','.join(set([flow['externalAddress'] for flow in data])), "InternalDomains": ','.join(data[0]['internalDomains']), "InternalIPRanges": ','.join(data[0]['internalTags']['ipRange']), "InternalExposureTypes": ','.join(data[0]['internalExposureTypes']) } def get_expanse_exposure_context(data): """ provides custom context information from the Expanse Exposure API """ def exposure_to_obj(exposure): return { "ExposureType": exposure['exposureType'], "BusinessUnit": exposure['businessUnit']['name'], "Ip": exposure['ip'], "Port": exposure['port'], "Severity": exposure['severity'], "Certificate": exposure['certificate'], "FirstObservsation": exposure['firstObservation'], "LastObservsation": exposure['lastObservation'], "Status": exposure['statuses'], "Provider": exposure['provider'] } def exposure_to_summary(exposure): return "{exposureType} exposure on {ip}:{port}".format(**exposure) def exposure_stats(exposures): results = { "CRITICAL": 0, "WARNING": 0, "ROUTINE": 0, "UNKNOWN": 0 } for exposure in exposures: results[exposure['severity']] += 1 return results counts = exposure_stats(data) return { "SearchTerm": data[0]['ip'], "TotalExposureCount": len(data), "CriticalExposureCount": counts['CRITICAL'], "WarningExposureCount": counts['WARNING'], "RoutineExposureCount": counts['ROUTINE'], "UnknownExposureCount": counts['UNKNOWN'], "ExposureSummaries": '\n'.join([exposure_to_summary(exposure) for exposure in data]), "Exposures": [exposure_to_obj(exposure) for exposure in data] } def get_expanse_certificate_to_domain_context(common_name, data): """ Provides custom context information for domains looked up via certificate. :param common_name: The original search parameter :param data: The data returned from the API query :return: A dict of aggregated domain details """ return { "SearchTerm": common_name, "TotalDomainCount": len(data), "FlatDomainList": [domain.get('domain') for domain in data], "DomainList": data } def fetch_events_incidents_command(start_date, end_date, token, next_=None): """ retrieve active exposures from Expanse API and create incidents """ params = { 'startDateUtc': start_date, 'endDateUtc': end_date, 'eventType': EXPOSURE_EVENT_TYPES } if next_: params['pageToken'] = next_ events = http_request('GET', 'events', params=params, token=token) if events['meta']['dataAvailable'] is True: if events['pagination']['next']: # will retrieve more data with pageToken next run next_page_token = get_page_token(events['pagination']['next']) else: next_page_token = None incidents = parse_events(events) return (incidents, next_page_token, False) return ([], None, True) def fetch_behavior_incidents_command(start_date, token, offset=0): """ retrieve risky flow details from Expanse Behavior API and create incidents """ params = { 'filter[created-after]': start_date + 'T00:00:00.000Z', 'page[offset]': offset if offset is not None else 0 } flows = http_request('GET', 'behavior/risky-flows', params=params, token=token) if flows['meta']['totalCount'] is not None and flows['meta']['totalCount'] > 0: if flows['pagination']['next']: next_offset = get_next_offset(flows['pagination']['next']) else: next_offset = None incidents = parse_behavior(flows) return (incidents, next_offset) return ([], None) def fetch_incidents_command(): """ Parent command to wrap events and behavior fetch commands """ # Check if it's been run now = datetime.today() yesterday = datetime.strftime(now - timedelta(days=1), "%Y-%m-%d") last_run = demisto.getLastRun() start_date = yesterday end_date = yesterday if "start_time" not in last_run or "complete_for_today" not in last_run: # first time integration is running start_date = datetime.strftime(now - timedelta(days=FIRST_RUN), "%Y-%m-%d") if last_run.get('complete_for_today') is True and last_run.get('start_time') == yesterday: # wait until tomorrow to try again demisto.incidents([]) return # Refresh JWT token = do_auth() # Fetch Events more_events = True no_events_found = True page_token = None incidents = [] # Check if we've stored any events in the integration cache cache = demisto.getIntegrationContext() stored_incidents = cache.get("incidents") if stored_incidents is None: demisto.debug("Did not detect any stored incidents") while more_events: event_incidents, page_token, no_events_found = fetch_events_incidents_command(start_date, end_date, token, page_token) for incident in event_incidents: demisto.debug("Adding event incident name={name}, type={type}, severity={severity}".format(**incident)) incidents += event_incidents if page_token is None: more_events = False # Fetch Behavior if BEHAVIOR_ENABLED: more_behavior = True next_offset = None while more_behavior: behavior_incidents, next_offset = fetch_behavior_incidents_command(start_date, token, next_offset) for incident in behavior_incidents: demisto.debug("Adding behavior incident name={name}, type={type}, severity={severity}".format(**incident)) incidents += behavior_incidents if next_offset is None: more_behavior = False if len(incidents) == 0 and no_events_found: # return and try again later, API may not have updated. demisto.debug("Will retry - no events returned") demisto.incidents([]) return elif len(incidents) > PAGE_LIMIT: incidents_to_send = incidents[:PAGE_LIMIT] del incidents[:PAGE_LIMIT] demisto.incidents(incidents_to_send) else: demisto.incidents(incidents) incidents = [] # Add remaining incidents to cache if len(incidents) > 0: demisto.debug("Updating cache to store {} incidents".format(len(incidents))) cache["incidents"] = incidents demisto.setIntegrationContext(cache) demisto.setLastRun({ "complete_for_today": False, "start_time": yesterday }) else: cache["incidents"] = None demisto.setIntegrationContext(cache) demisto.setLastRun({ "complete_for_today": True, "start_time": yesterday }) else: demisto.debug("Found {} stored incidents".format(len(stored_incidents))) # Send next PAGE_LIMIT number of incidents to demisto if len(stored_incidents) > PAGE_LIMIT: incidents_to_send = stored_incidents[:PAGE_LIMIT] del stored_incidents[:PAGE_LIMIT] demisto.debug("Updating cache to store {} incidents".format(len(stored_incidents))) demisto.setLastRun({ "complete_for_today": False, "start_time": yesterday }) else: incidents_to_send = list(stored_incidents) stored_incidents = None demisto.debug("Updating cache to store 0 incidents") demisto.setLastRun({ "complete_for_today": True, "start_time": yesterday }) demisto.incidents(incidents_to_send) # Update Cache cache["incidents"] = stored_incidents demisto.setIntegrationContext(cache) def ip_command(): """ searches by IP address in Expanse API for asset information """ search = demisto.args()['ip'] params = { "include": "annotations,severityCounts,attributionReasons,relatedRegistrationInformation,locationInformation", "inet": search } token = do_auth() results = http_request('GET', 'ip-range', params, token=token) try: ip = results['data'][0] except Exception: demisto.results("No data found") return ip['search'] = search dbot_context = { "Indicator": search, "Type": "ip", "Vendor": "Expanse", "Score": 0 } ip_context = get_ip_context(ip) expanse_ip_context = get_expanse_ip_context(ip) ec = { 'DBotScore': dbot_context, 'IP(val.Address == obj.Address)': ip_context, 'Expanse.IP(val.Address == obj.Address)': expanse_ip_context } human_readable = tableToMarkdown("IP information for: {search}".format(search=search), expanse_ip_context) return_outputs(human_readable, ec, ip) def domain_command(): """ searches Expanse IP for asset information for a domain """ search = demisto.args()['domain'] params = { 'domainSearch': search } token = do_auth() results = http_request('GET', "assets/domains", params, token=token) try: domain = results['data'][0] except Exception: # no results, exit gracefully demisto.results("No data found") return dbot_context = { "Indicator": search, "Type": "url", "Vendor": "Expanse", "Score": 0 } domain_context = get_domain_context(domain) expanse_domain_context = get_expanse_domain_context(domain) ec = { 'DBotScore': dbot_context, 'Domain(val.Name == obj.Name)': domain_context, 'Expanse.Domain(val.Name == obj.Name)': expanse_domain_context } human_readable = tableToMarkdown("Domain information for: {search}".format(search=search), expanse_domain_context) return_outputs(human_readable, ec, domain) def certificate_command(): """ searches by common name for certificate information """ common_name = demisto.args()['common_name'] params = { "commonNameSearch": common_name } token = do_auth() certs = fetch_certificates(params=params, token=token) if len(certs) == 0: demisto.results("No data found") return cert = certs[0] # just return the first one cert['search'] = common_name expanse_cert_context = get_expanse_certificate_context(cert) ec = { 'Expanse.Certificate(val.SearchTerm == obj.SearchTerm)': expanse_cert_context } human_readable = tableToMarkdown("Certificate information for: {search}".format(search=common_name), expanse_cert_context) return_outputs(human_readable, ec, cert) def behavior_command(): """ searches by ip for behavior details from Expanse """ search = demisto.args()['ip'] start_time = arg_to_timestamp( demisto.args().get('start_time'), arg_name='start_time', required=False ) now = datetime.today() time_range = datetime.strftime(now - timedelta(days=FIRST_RUN), "%Y-%m-%d") if start_time is None: start_time = time_range + 'T00:00:00.000Z' params = { "filter[internal-ip-range]": search, 'page[limit]': 20, 'filter[created-after]': start_time, } token = do_auth() results = http_request('GET', 'behavior/risky-flows', params, token=token) try: behaviors = results['data'] if len(behaviors) == 0: demisto.results("No data found") return except Exception: demisto.results("No data found") return expanse_behavior_context = get_expanse_behavior_context(behaviors) ec = { 'Expanse.Behavior(val.SearchTerm == obj.SearchTerm)': expanse_behavior_context } raw_flows = expanse_behavior_context['Flows'] del expanse_behavior_context['Flows'] # Remove flow objects from human readable response human_readable = tableToMarkdown("Expanse Behavior information for: {search}".format(search=search), expanse_behavior_context) expanse_behavior_context['Flows'] = raw_flows return_outputs(human_readable, ec, behaviors) def exposures_command(): """ searches by ip for exposure data from Expanse """ search = demisto.args()['ip'] params = { "inet": search, "activityStatus": "active" } token = do_auth() results = http_request('GET', 'exposures/ip-ports', params, token=token) try: exposures = results['data'] if len(exposures) == 0: demisto.results("No data found") return except Exception: demisto.results("No data found") return expanse_exposure_context = get_expanse_exposure_context(exposures) ec = { 'Expanse.Exposures(val.SearchTerm == obj.SearchTerm)': expanse_exposure_context } raw_exposures = expanse_exposure_context['Exposures'] del expanse_exposure_context['Exposures'] # Remove exposure objects from human readable response human_readable = tableToMarkdown("Expanse Exposure information for: {search}".format(search=search), expanse_exposure_context) expanse_exposure_context['Exposures'] = raw_exposures return_outputs(human_readable, ec, exposures) def domains_for_certificate_command(): """ Returns all domains that have resolved to IP addresses a certificate has been seen on. There is no direct way to correlate between certificates and domains in Expanse this does so indirectly. """ search = demisto.args()['common_name'] params = { "commonNameSearch": search } token = do_auth() matching_domains = [] certificates = fetch_certificates(params=params, token=token) for certificate in certificates: certificate_details = \ fetch_certificate(md5_hash=certificate.get('certificate', {}).get('md5Hash'), token=token) for ip in certificate_details.get('details', {}).get('recentIps', []): params = { 'inetSearch': ip.get('ip'), 'assetType': 'DOMAIN' } matching_domains += fetch_ips(params=params, token=token) if len(matching_domains) == 0: demisto.results("No data found") return context = get_expanse_certificate_to_domain_context(common_name=search, data=matching_domains) ec = { 'Expanse.IPDomains(val.SearchTerm == obj.SearchTerm)': context } hr_context = context.copy() del hr_context['DomainList'] # Remove full objects from human readable response human_readable = tableToMarkdown("Expanse Domains matching Certificate Common Name: {search}".format(search=search), hr_context) return_outputs(human_readable, ec, matching_domains) def fetch_certificates(params, token): """ Fetches all certificates that match the provided params. :param params: Search parameters :param token: Expanse Refresh token :return: List of certificate objects """ certificates = [] results = http_request('GET', 'assets/certificates', params, token=token) try: if len(results.get('data', [])) > 0: certificates += results.get('data', []) next_page = results.get('pagination', {}).get('next', None) while next_page is not None: params['pageToken'] = get_page_token(next_page) results = http_request('GET', 'assets/certificates', params, token=token) certificates += results['data'] next_page = results.get('pagination', {}).get('next', None) return certificates except Exception as err: demisto.error("Error fetching certificates: {}".format(err)) return [] def fetch_certificate(md5_hash, token): """ Returns details for a single certificate. :param md5_hash: Search term for certificates :param token: Expanse Refresh token :return: Certificate details objects """ try: return http_request('GET', 'assets/certificates/{}'.format(md5_hash), {}, token=token) except Exception as err: demisto.error("Error fetching certificate: {}".format(err)) return {} def fetch_ips(params, token): """ Returns all ip results matching search params. :param params: Search parameters :param token: Expanse Refresh token :return: List of ip objects """ ips = [] results = http_request('GET', 'assets/ips', params, token=token) try: if len(results.get('data', [])) > 0: ips += results.get('data', []) next_page = results.get('pagination', {}).get('next', None) while next_page is not None: params['pageToken'] = get_page_token(next_page) results = http_request('GET', 'assets/ips', params, token=token) ips += results['data'] next_page = results.get('pagination', {}).get('next', None) return ips except Exception as err: demisto.error("Error fetching ips: {}".format(err)) return [] def arg_to_timestamp(arg, arg_name: str, required: bool = False): if arg is None: if required is True: raise ValueError(f'Missing "{arg_name}"') return None if isinstance(arg, str) and arg.isdigit(): # timestamp that str - we just convert it to int return int(arg) if isinstance(arg, str): # if the arg is string of date format 2019-10-23T00:00:00 or "3 days", etc date = dateparser.parse(arg, settings={'TIMEZONE': 'UTC'}) if date is None: # if d is None it means dateparser failed to parse it raise ValueError(f'Invalid date: {arg_name}') return int(date.timestamp()) if isinstance(arg, (int, float)): return arg def test_module(): token = do_auth() now = datetime.today() yesterday = datetime.strftime(now - timedelta(days=1), "%Y-%m-%d") params = { 'startDateUtc': yesterday, 'endDateUtc': yesterday, 'eventType': EXPOSURE_EVENT_TYPES, 'limit': 1 } events = http_request('GET', 'events', params=params, token=token) parse_events(events) return True def main(): try: handle_proxy() active_command = demisto.command() if active_command == 'test-module': test_module() demisto.results('ok') elif active_command == 'fetch-incidents': fetch_incidents_command() elif active_command == 'ip': ip_command() elif active_command == 'domain': domain_command() elif active_command == 'expanse-get-certificate': certificate_command() elif active_command == 'expanse-get-behavior': behavior_command() elif active_command == 'expanse-get-exposures': exposures_command() elif active_command == 'expanse-get-domains-for-certificate': domains_for_certificate_command() # Log exceptions except Exception as e: demisto.error(str(e) + "\n\nTrace:\n" + traceback.format_exc()) return_error(str(e)) if __name__ == "__builtin__" or __name__ == "builtins": main()