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

IDExpanse
ProviderPalo Alto Networks
CategoryData Enrichment & Threat Intelligence
From Version5.0.0
Docker Imagedemisto/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


  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Expanse.
  3. 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
  4. 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.

  1. ip
  2. domain
  3. expanse-get-certificate
  4. expanse-get-behavior
  5. expanse-get-exposures
  6. 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 incidents
  • incidentType — Incident type
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • behavior — Include Behavior data in incidents
  • page_limit — How many events to pull from Expander per run
  • first_run — How many days to pull past events on first run
  • minimum_severity — Minimum severity of Expanse Exposure to create an incident for

Commands (6)

  • domain

    Submits a domain to check.

  • expanse-get-behavior

    Returns Risky Flows for the specified IP address. The maximum number of results is 20.

  • expanse-get-certificate

    Returns information about the domain certificate.

  • expanse-get-domains-for-certificate

    Returns all domains which have been seen with the specified certificate.

  • expanse-get-exposures

    Deprecated command by Expanse. Returns exposure information about the ip.

  • ip

    Submits 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()