diff --git a/stix_shifter_modules/ibm_security_verify/README.md b/stix_shifter_modules/ibm_security_verify/README.md new file mode 100644 index 000000000..c4c83808b --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/README.md @@ -0,0 +1,152 @@ +# IBM Security Verify + +This is a connector for searching IBM Security Verify events. Connector uses stix-patterns and IBM event verify REST API to make a convert and execute the qurey. + +* To know more about IBM Security Verify API refer to the [API Reference](https://docs.verify.ibm.com/verify/reference/getallevents) +* Connector uses the stix schema defined as per [stix-extension/stix2.0/x-oca-event](https://github.ibm.com/IBM-Security-STIX/stix-extensions/blob/verify/STIX%202.0/x-oca-event.md) +* Connector supports ` Equal, AND, IN ` stix operations +* Possible event types are ` sso,authentication,management,risk,adaptive_risk ` + +### Some of Stix pattern examples + + * Event type `[x-oca-event:category='authentication']` + * IPv4-Addr `[ ipv4-addr:value IN ('192.168.1.1', '192.168.1.2', '192.168.1.3') ]"` + * oca-event `[x-oca-event:extensions.'x-iam-ext'.application_name='Bane']` +` +` + +### Format for making STIX translation calls via the CLI + +`python main.py +` + +### Converting from STIX patterns to verify_event queries + +CLI example of stix input pattern for TRANSLATE + + +` + python main.py translate ibm_security_verify query "{}" "[x-oca-event:category='sso']" +` + +Returns the following search query: + +` + { + "queries": [ + "event_type=\"sso\"&limit=10000" + ] + } +` + +### Transmit functions + +Transmit offers several functions: ping, query, results and execute. +### Ping +Uses the data source API to ping the connection. + + +` +python main.py transmit ibm_security_verify '{ "host": "","port" :}' '{ "auth": { "clientId": ", "clientSecret": ""}}' ping +` + +If connection is established, Connector will return the following response: + +` +{ + "success": true +} +` +### Results + +Uses the data source API to fetch the query results based on the search ID, offset, and length. + +CLI Command + +` +python main.py transmit ibm_security_verify '{ "host": "" ,"port" :}' '{ "auth": { "clientId": ", "clientSecret": ""}}' +` + +Response + +` +{ + "success": true, + "search_id": "event_type=\"sso\"&limit=10000" +} +` + +### Execute + +``` +python main.py execute ibm_security_verify ibm_security_verify '{"type": "identity","id": "","name":"verify","identity_class":"events"}' '{ }' '{ "host": "" ,"port" :}' '{ "auth": { "clientId": ", "clientSecret": ""}}' "[x-oca-event:category = 'sso']" +``` + +Response object + +```json +{ + "type": "bundle", + "id": "bundle--65fc22ff-0063-4afc-a61e-b9b50c0b1e18", + "objects": [ + { + "type": "identity", + "id": "32a23267-52fb-4e82-859b-0a15d6a2d334", + "name": "verify", + "identity_class": "events" + }, + { + "id": "observed-data--63964544-6b66-4673-ad37-bbeab66d328d", + "type": "observed-data", + "created_by_ref": "32a23267-52fb-4e82-859b-0a15d6a2d334", + "created": "2022-02-17T07:33:21.969Z", + "modified": "2022-02-17T07:33:21.969Z", + "objects": { + "0": { + "type": "x-oca-event", + "extensions": { + "x-iam-ext": { + "continent_name": "Asia", + "city_name": "mumbai", + "country_iso_code": "IN", + "country_name": "India", + "subcategory": "saml", + "provider_id": "http://ibm.com", + "realm": "www.google.com", + "application_id": "6773634223410562472", + "application_type": "Custom Application", + "browser_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36", + "applicationname": "Bane" + } + }, + "ip_refs": [ + "1" + ], + "outcome": "success", + "user_ref": "2", + "category": "sso", + "provider": "IBM Security Verify IAM", + "domain_ref": "3", + "module": "saml_runtime", + "created": "2022-02-17T07:31:38.824Z" + }, + "1": { + "type": "ipv4-addr", + "value": "192.168.1.1" + }, + "2": { + "type": "user-account", + "user_id": "123456" + }, + "3": { + "type": "domain-name", + "value": "ibmcloud.com" + } + }, + "first_observed": "2022-02-17T07:33:21.969Z", + "last_observed": "2022-02-17T07:33:21.969Z", + "number_observed": 1 + } +} +``` + diff --git a/stix_shifter_modules/ibm_security_verify/__init__.py b/stix_shifter_modules/ibm_security_verify/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/stix_shifter_modules/ibm_security_verify/configuration/config.json b/stix_shifter_modules/ibm_security_verify/configuration/config.json new file mode 100644 index 000000000..9a031bb4b --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/configuration/config.json @@ -0,0 +1,37 @@ +{ + "connection": { + "type": { + "displayName": "IBM Security Verify", + "type": "connectorType" + }, + "host": { + "type": "text", + "regex": "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9_:/\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9_:/\\-]*[A-Za-z0-9])$" + }, + "port": { + "type": "number", + "default": 443, + "min": 1, + "max": 65535 + }, + "sni": { + "type": "text", + "optional": true + }, + "selfSignedCert": { + "type": "password", + "optional": true + } + }, + "configuration": { + "auth": { + "type": "fields", + "clientId": { + "type": "password" + }, + "clientSecret": { + "type": "password" + } + } + } +} \ No newline at end of file diff --git a/stix_shifter_modules/ibm_security_verify/configuration/lang_en.json b/stix_shifter_modules/ibm_security_verify/configuration/lang_en.json new file mode 100644 index 000000000..9d1df265a --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/configuration/lang_en.json @@ -0,0 +1,38 @@ +{ + "connection": { + "host": { + "label": "Management IP address or Hostname", + "placeholder": "192.168.1.1", + "description": "Specify the IBM Security verify IP address or Hostname." + }, + "port": { + "label": "Host Port", + "placeholder": "443", + "description": "Specify the associated port number of the data source." + }, + "help": { + "label": "Need additional help?" + }, + "selfSignedCert": { + "label": "IBM Security Verify Certificate", + "placeholder": "Paste your certificate" + }, + "sni": { + "label": "Server name indicator", + "placeholder": "Add a server name indicator", + "description": "If your hostname or IP address does not match the common name you will need to supply a Server Name Indicator (SNI). This is used to allow a separate hostname to be provided to the TLS handshake of the resource connection." + } + }, + "configuration": { + "auth": { + "clientId": { + "label": "Client Id", + "description": "Client ID of IBM Seurity Verify" + }, + "clientSecret": { + "label": "Client Secret", + "description": "Client secret of Client ID of IBM Seurity Verify" + } + } + } +} \ No newline at end of file diff --git a/stix_shifter_modules/ibm_security_verify/entry_point.py b/stix_shifter_modules/ibm_security_verify/entry_point.py new file mode 100644 index 000000000..be618f427 --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/entry_point.py @@ -0,0 +1,11 @@ +from stix_shifter_utils.utils.base_entry_point import BaseEntryPoint + + +class EntryPoint(BaseEntryPoint): + + def __init__(self, connection={}, configuration={}, options={}): + super().__init__(connection, configuration, options) + if connection: + self.setup_transmission_simple(connection, configuration) + + self.setup_translation_simple(dialect_default='default') \ No newline at end of file diff --git a/stix_shifter_modules/ibm_security_verify/stix_translation/__init__.py b/stix_shifter_modules/ibm_security_verify/stix_translation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/stix_shifter_modules/ibm_security_verify/stix_translation/json/from_stix_map.json b/stix_shifter_modules/ibm_security_verify/stix_translation/json/from_stix_map.json new file mode 100644 index 000000000..eda222733 --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_translation/json/from_stix_map.json @@ -0,0 +1,157 @@ +{ + "user-account": { + "fields": { + "user_id": [ + "data.user_id" + ], + "account_login": [ + "data.username" + ], + "account_type": [ + "data.sourcetype" + ] + } + }, + "ipv4-addr": { + "fields": { + "value": [ + "data.origin" + ] + } + }, + "domain-name": { + "fields": { + "type": [ + "domain-name" + ], + "value": [ + "tenantname" + ] + } + }, + "x-oca-event": { + "fields": { + "action": [ + "data.action" + ], + "category": [ + "event_type" + ], + "module": [ + "servicename" + ], + "outcome": [ + "data.result" + ], + "agent": [ + "data.sourcetype" + ], + "ip_refs[*].value": [ + "ip" + ], + "domain_ref.value": [ + "tenantname" + ], + "user_ref": [ + "username" + ], + "provider": "'IBM Security Verify Event'", + "extensions.'x-iam-ext'.subcategory": [ + "data.subtype" + ], + "extensions.'x-iam-ext'.realm": [ + "data.realm" + ], + "extensions.'x-iam-ext'.browser_agent": [ + "data.devicetype" + ], + "extensions.'x-iam-ext'.provider_id": [ + "data.providerid" + ], + "extensions.'x-iam-ext'.application_id": [ + "data.applicationid" + ], + "extensions.'x-iam-ext'.application_type": [ + "data.applicationtype" + ], + "extensions.'x-iam-ext'.application_name": [ + "data.applicationname" + ], + "extensions.'x-iam-ext'.cause": [ + "data.cause" + ], + "extensions.'x-iam-ext'.target": [ + "data.target" + ], + "extensions.'x-iam-ext'.deleted": [ + "data.deleted" + ], + "extensions.'x-iam-ext'.performedby_clientname": [ + "data.performedby_clientname" + ], + "extensions.'x-iam-ext'.performedby_realm": [ + "data.performedby_realm" + ], + "extensions.'x-iam-ext'.performedby_username": [ + "data.performedby_username" + ], + "extensions.'x-iam-ext'.targetid": [ + "data.targetid" + ], + "extensions.'x-iam-ext'.targetid_realm": [ + "data.targetid_realm" + ], + "extensions.'x-iam-ext'.targetid_username": [ + "data.targetid_username" + ], + "extensions.'x-iam-ext'.continent_name": [ + "geoip.continent_name" + ], + "extensions.'x-iam-ext'.country_iso_code": [ + "geoip.country_iso_code" + ], + "extensions.'x-iam-ext'.country_name": [ + "geoip.country_name" + ], + "extensions.x-iam-ext.location_lon": [ + "geoip.lon" + ], + "extensions.'x-iam-ext'.location_lat": [ + "geoip.lat" + ], + "extensions.'x-iam-ext'.city_name": [ + "geoip.city_name" + ], + "extensions.'x-iam-ext'.policy_action": [ + "data.policy_action" + ], + "extensions.'x-iam-ext'.policy_name": [ + "data.policy_name" + ], + "extensions.'x-iam-ext'.rule_name": [ + "data.rule_name" + ], + "extensions.'x-iam-ext'.decision_reason": [ + "data.decision_reason" + ], + "extensions.'x-iam-ext'.risk_level": [ + "data.risk_level" + ], + "extensions.'x-iam-ext'.risk_score": [ + "data.risk_score" + ], + "extensions.'x-iam-ext'.deviceid": [ + "data.deviceid" + ], + "extensions.'x-iam-ext'.is_device_compliant": [ + "data.mdmiscompliant" + ], + "extensions.'x-iam-ext'.is_device_managed": [ + "data.mdmismanaged" + ], + "extensions.'x-iam-ext'.mdm_customerid": [ + "data.billingid" + ] + } + } +} \ No newline at end of file diff --git a/stix_shifter_modules/ibm_security_verify/stix_translation/json/operators.json b/stix_shifter_modules/ibm_security_verify/stix_translation/json/operators.json new file mode 100644 index 000000000..154c2fae2 --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_translation/json/operators.json @@ -0,0 +1,6 @@ +{ + "ComparisonExpressionOperators.And": "&", + "ComparisonComparators.Equal": "=", + "ObservationOperators.And": "=", + "ComparisonComparators.In": "=" +} \ No newline at end of file diff --git a/stix_shifter_modules/ibm_security_verify/stix_translation/json/stix_2_1/from_stix_map.json b/stix_shifter_modules/ibm_security_verify/stix_translation/json/stix_2_1/from_stix_map.json new file mode 100644 index 000000000..eda222733 --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_translation/json/stix_2_1/from_stix_map.json @@ -0,0 +1,157 @@ +{ + "user-account": { + "fields": { + "user_id": [ + "data.user_id" + ], + "account_login": [ + "data.username" + ], + "account_type": [ + "data.sourcetype" + ] + } + }, + "ipv4-addr": { + "fields": { + "value": [ + "data.origin" + ] + } + }, + "domain-name": { + "fields": { + "type": [ + "domain-name" + ], + "value": [ + "tenantname" + ] + } + }, + "x-oca-event": { + "fields": { + "action": [ + "data.action" + ], + "category": [ + "event_type" + ], + "module": [ + "servicename" + ], + "outcome": [ + "data.result" + ], + "agent": [ + "data.sourcetype" + ], + "ip_refs[*].value": [ + "ip" + ], + "domain_ref.value": [ + "tenantname" + ], + "user_ref": [ + "username" + ], + "provider": "'IBM Security Verify Event'", + "extensions.'x-iam-ext'.subcategory": [ + "data.subtype" + ], + "extensions.'x-iam-ext'.realm": [ + "data.realm" + ], + "extensions.'x-iam-ext'.browser_agent": [ + "data.devicetype" + ], + "extensions.'x-iam-ext'.provider_id": [ + "data.providerid" + ], + "extensions.'x-iam-ext'.application_id": [ + "data.applicationid" + ], + "extensions.'x-iam-ext'.application_type": [ + "data.applicationtype" + ], + "extensions.'x-iam-ext'.application_name": [ + "data.applicationname" + ], + "extensions.'x-iam-ext'.cause": [ + "data.cause" + ], + "extensions.'x-iam-ext'.target": [ + "data.target" + ], + "extensions.'x-iam-ext'.deleted": [ + "data.deleted" + ], + "extensions.'x-iam-ext'.performedby_clientname": [ + "data.performedby_clientname" + ], + "extensions.'x-iam-ext'.performedby_realm": [ + "data.performedby_realm" + ], + "extensions.'x-iam-ext'.performedby_username": [ + "data.performedby_username" + ], + "extensions.'x-iam-ext'.targetid": [ + "data.targetid" + ], + "extensions.'x-iam-ext'.targetid_realm": [ + "data.targetid_realm" + ], + "extensions.'x-iam-ext'.targetid_username": [ + "data.targetid_username" + ], + "extensions.'x-iam-ext'.continent_name": [ + "geoip.continent_name" + ], + "extensions.'x-iam-ext'.country_iso_code": [ + "geoip.country_iso_code" + ], + "extensions.'x-iam-ext'.country_name": [ + "geoip.country_name" + ], + "extensions.x-iam-ext.location_lon": [ + "geoip.lon" + ], + "extensions.'x-iam-ext'.location_lat": [ + "geoip.lat" + ], + "extensions.'x-iam-ext'.city_name": [ + "geoip.city_name" + ], + "extensions.'x-iam-ext'.policy_action": [ + "data.policy_action" + ], + "extensions.'x-iam-ext'.policy_name": [ + "data.policy_name" + ], + "extensions.'x-iam-ext'.rule_name": [ + "data.rule_name" + ], + "extensions.'x-iam-ext'.decision_reason": [ + "data.decision_reason" + ], + "extensions.'x-iam-ext'.risk_level": [ + "data.risk_level" + ], + "extensions.'x-iam-ext'.risk_score": [ + "data.risk_score" + ], + "extensions.'x-iam-ext'.deviceid": [ + "data.deviceid" + ], + "extensions.'x-iam-ext'.is_device_compliant": [ + "data.mdmiscompliant" + ], + "extensions.'x-iam-ext'.is_device_managed": [ + "data.mdmismanaged" + ], + "extensions.'x-iam-ext'.mdm_customerid": [ + "data.billingid" + ] + } + } +} \ No newline at end of file diff --git a/stix_shifter_modules/ibm_security_verify/stix_translation/json/stix_2_1/to_stix_map.json b/stix_shifter_modules/ibm_security_verify/stix_translation/json/stix_2_1/to_stix_map.json new file mode 100644 index 000000000..1d5d8365c --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_translation/json/stix_2_1/to_stix_map.json @@ -0,0 +1,219 @@ +{ + "username": [ + { + "key": "user-account.user_id", + "object": "useraccount" + }, + { + "key": "user-account.account_login", + "object": "useraccount" + }, + { + "key": "user-account.account_type", + "object": "useraccount" + }, + { + "key": "x-oca-event.user_ref", + "object": "ocaevent", + "references": "useraccount" + } + ], + "servicename": { + "key": "x-oca-event.module", + "object": "ocaevent" + }, + "sourcetype": { + "key": "x-oca-event.agent", + "object": "ocaevent" + }, + "ip": [ + { + "key": "ipv4-addr.value", + "object": "ipdata" + }, + { + "key": "x-oca-event.ip_refs", + "object": "ocaevent", + "references": [ + "ipdata" + ], + "group": true + } + ], + "tenantname": [ + { + "key": "domain-name.value", + "object": "domaindata", + "transformer": "ToDomainName" + }, + { + "key": "x-oca-event.domain_ref", + "object": "ocaevent", + "references": "domaindata" + } + ], + "result": [ + { + "object": "ocaevent", + "key": "x-oca-event.outcome" + } + ], + "subtype": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.subcategory" + }, + "cause": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.cause" + }, + "action": [ + { + "key": "x-oca-event.action", + "object": "ocaevent" + } + ], + "realm": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.realm" + }, + "devicetype": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.browser_agent" + }, + "applicationid": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.application_id" + }, + "applicationtype": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.application_type" + }, + "applicationname": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.applicationname" + }, + "target": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.target" + }, + "event_type": [ + { + "key": "x-oca-event.category", + "object": "ocaevent" + }, + { + "key": "x-oca-event.provider", + "object": "ocaevent", + "transformer": "VerifyStaticTransformer" + } + ], + "time": [ + { + "key": "x-oca-event.created", + "transformer": "EpochToTimestamp", + "object": "ocaevent" + } + ], + "performedby_username": { + "key": "x-oca-event.extensions.x-iam-ext.performedby_username", + "object": "ocaevent" + }, + "deleted": { + "key": "x-oca-event.extensions.x-iam-ext.deleted", + "object": "ocaevent" + }, + "performedby_clientname": { + "key": "x-oca-event.extensions.x-iam-ext.performedby_clientname", + "object": "ocaevent" + }, + "performedby_realm": { + "key": "x-oca-event.extensions.x-iam-ext.performedby_realm", + "object": "ocaevent" + }, + "targetid": { + "key": "x-oca-event.extensions.x-iam-ext.targetid", + "object": "ocaevent" + }, + "targetid_realm": { + "key": "x-oca-event.extensions.x-iam-ext.targetid_realm", + "object": "ocaevent" + }, + "targetid_username": { + "key": "x-oca-event.extensions.x-iam-ext.taregetid_username", + "object": "ocaevent" + }, + "userid": [ + { + "key": "user-account.user_id", + "object": "useraccount" + } + ], + "continent_name": { + "key": "x-oca-event.extensions.x-iam-ext.continent_name", + "object": "ocaevent" + }, + "city_name": { + "key": "x-oca-event.extensions.x-iam-ext.city_name", + "object": "ocaevent" + }, + "country_iso_code": { + "key": "x-oca-event.extensions.x-iam-ext.country_iso_code", + "object": "ocaevent" + }, + "country_name": { + "key": "x-oca-event.extensions.x-iam-ext.country_name", + "object": "ocaevent" + }, + "providerid": { + "key": "x-oca-event.extensions.x-iam-ext.provider_id", + "object": "ocaevent" + }, + "rule_name": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.rule_name" + }, + "policy_name": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.policy_name" + }, + "decision_reason": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.decision_reason" + }, + "policy_action": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.policy_action" + }, + "risk_level": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.risk_level" + }, + "risk_score": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.risk_score" + }, + "deviceid": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.deviceid" + }, + "mdmiscompliant": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.is_device_compliant" + }, + "mdmismanaged": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.is_device_managed" + }, + "billingid": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.mdm_customerid" + }, + "lat": { + "key": "x-oca-event.extensions.x-iam-ext.location_lat", + "object": "ocaevent" + }, + "lon": { + "key": "x-oca-event.extensions.x-iam-ext.location_lon", + "object": "ocaevent" + } +} \ No newline at end of file diff --git a/stix_shifter_modules/ibm_security_verify/stix_translation/json/to_stix_map.json b/stix_shifter_modules/ibm_security_verify/stix_translation/json/to_stix_map.json new file mode 100644 index 000000000..1d5d8365c --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_translation/json/to_stix_map.json @@ -0,0 +1,219 @@ +{ + "username": [ + { + "key": "user-account.user_id", + "object": "useraccount" + }, + { + "key": "user-account.account_login", + "object": "useraccount" + }, + { + "key": "user-account.account_type", + "object": "useraccount" + }, + { + "key": "x-oca-event.user_ref", + "object": "ocaevent", + "references": "useraccount" + } + ], + "servicename": { + "key": "x-oca-event.module", + "object": "ocaevent" + }, + "sourcetype": { + "key": "x-oca-event.agent", + "object": "ocaevent" + }, + "ip": [ + { + "key": "ipv4-addr.value", + "object": "ipdata" + }, + { + "key": "x-oca-event.ip_refs", + "object": "ocaevent", + "references": [ + "ipdata" + ], + "group": true + } + ], + "tenantname": [ + { + "key": "domain-name.value", + "object": "domaindata", + "transformer": "ToDomainName" + }, + { + "key": "x-oca-event.domain_ref", + "object": "ocaevent", + "references": "domaindata" + } + ], + "result": [ + { + "object": "ocaevent", + "key": "x-oca-event.outcome" + } + ], + "subtype": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.subcategory" + }, + "cause": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.cause" + }, + "action": [ + { + "key": "x-oca-event.action", + "object": "ocaevent" + } + ], + "realm": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.realm" + }, + "devicetype": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.browser_agent" + }, + "applicationid": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.application_id" + }, + "applicationtype": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.application_type" + }, + "applicationname": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.applicationname" + }, + "target": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.target" + }, + "event_type": [ + { + "key": "x-oca-event.category", + "object": "ocaevent" + }, + { + "key": "x-oca-event.provider", + "object": "ocaevent", + "transformer": "VerifyStaticTransformer" + } + ], + "time": [ + { + "key": "x-oca-event.created", + "transformer": "EpochToTimestamp", + "object": "ocaevent" + } + ], + "performedby_username": { + "key": "x-oca-event.extensions.x-iam-ext.performedby_username", + "object": "ocaevent" + }, + "deleted": { + "key": "x-oca-event.extensions.x-iam-ext.deleted", + "object": "ocaevent" + }, + "performedby_clientname": { + "key": "x-oca-event.extensions.x-iam-ext.performedby_clientname", + "object": "ocaevent" + }, + "performedby_realm": { + "key": "x-oca-event.extensions.x-iam-ext.performedby_realm", + "object": "ocaevent" + }, + "targetid": { + "key": "x-oca-event.extensions.x-iam-ext.targetid", + "object": "ocaevent" + }, + "targetid_realm": { + "key": "x-oca-event.extensions.x-iam-ext.targetid_realm", + "object": "ocaevent" + }, + "targetid_username": { + "key": "x-oca-event.extensions.x-iam-ext.taregetid_username", + "object": "ocaevent" + }, + "userid": [ + { + "key": "user-account.user_id", + "object": "useraccount" + } + ], + "continent_name": { + "key": "x-oca-event.extensions.x-iam-ext.continent_name", + "object": "ocaevent" + }, + "city_name": { + "key": "x-oca-event.extensions.x-iam-ext.city_name", + "object": "ocaevent" + }, + "country_iso_code": { + "key": "x-oca-event.extensions.x-iam-ext.country_iso_code", + "object": "ocaevent" + }, + "country_name": { + "key": "x-oca-event.extensions.x-iam-ext.country_name", + "object": "ocaevent" + }, + "providerid": { + "key": "x-oca-event.extensions.x-iam-ext.provider_id", + "object": "ocaevent" + }, + "rule_name": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.rule_name" + }, + "policy_name": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.policy_name" + }, + "decision_reason": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.decision_reason" + }, + "policy_action": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.policy_action" + }, + "risk_level": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.risk_level" + }, + "risk_score": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.risk_score" + }, + "deviceid": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.deviceid" + }, + "mdmiscompliant": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.is_device_compliant" + }, + "mdmismanaged": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.is_device_managed" + }, + "billingid": { + "object": "ocaevent", + "key": "x-oca-event.extensions.x-iam-ext.mdm_customerid" + }, + "lat": { + "key": "x-oca-event.extensions.x-iam-ext.location_lat", + "object": "ocaevent" + }, + "lon": { + "key": "x-oca-event.extensions.x-iam-ext.location_lon", + "object": "ocaevent" + } +} \ No newline at end of file diff --git a/stix_shifter_modules/ibm_security_verify/stix_translation/query_constructor.py b/stix_shifter_modules/ibm_security_verify/stix_translation/query_constructor.py new file mode 100644 index 000000000..d7b532b4b --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_translation/query_constructor.py @@ -0,0 +1,255 @@ +import json +import logging +import re +from typing import Union + +from stix_shifter_utils.stix_translation.src.json_to_stix import observable +from stix_shifter_utils.stix_translation.src.patterns.pattern_objects import ( + CombinedComparisonExpression, CombinedObservationExpression, + ComparisonComparators, ComparisonExpression, ComparisonExpressionOperators, + ObservationExpression, ObservationOperators, Pattern) +from stix_shifter_utils.stix_translation.src.utils.transformers import \ + TimestampToMilliseconds + +# Source and destination reference mapping for ip and mac addresses. +# Change the keys to match the data source fields. The value array indicates the possible data type that can come into from field. +REFERENCE_DATA_TYPES = {"ipaddr": ["ipv4", "ipv4_cidr", "ipv6", "ipv6_cidr"], + "proxy_ip": ["ipv4", "ipv4_cidr"], + } +REFERENCE_FILTER_TYPE = ["user-account", + "ipv4-addr", "domain-name", "x-oca-event"] +REFERENCE_EXCLUDE_FILTER_TYPE = ["category"] +logger = logging.getLogger(__name__) + + +class QueryStringPatternTranslator: + QUERIES = [] + + def __init__(self, pattern: Pattern, data_model_mapper): + self.dmm = data_model_mapper + self.comparator_lookup = self.dmm.map_comparator() + self.pattern = pattern + self.translated = self.parse_expression(pattern) + + @staticmethod + def _escape_value(value, comparator=None) -> str: + if isinstance(value, str): + return '{}'.format(value.replace("\'", "'").replace('\\', '\\\\').replace('(', '\\(').replace(')', '\\)').replace("$", "\"").replace('&', '%26') + .replace('""', '"')) + else: + return value + + @staticmethod + def _convert_list_string_in_condition(value)->str: + if isinstance(value, list): + contcated_string = ''.join( + f'"{str}",'.format(str) for str in value) + contcated_string = contcated_string[1:-2] + return contcated_string + + @staticmethod + def _negate_comparison(comparison_string): + return "NOT ({})".format(comparison_string) + + @staticmethod + def _check_value_type(value): + value = str(value) + for key, pattern in observable.REGEX.items(): + if key != 'date' and bool(re.search(pattern, value)): + return key + return None + + # TODO remove self reference from static methods + @staticmethod + def _parse_reference(self, stix_field, value_type, mapped_field, value, comparator): + if value_type not in REFERENCE_DATA_TYPES["{}".format(mapped_field)]: + return None + else: + return "{mapped_field}{comparator}{value}".format( + mapped_field=mapped_field, comparator=comparator, value=value) + + @staticmethod + def _parse_mapped_fields(self, expression, value, comparator, stix_field, mapped_fields_array, stix_object): + comparison_string = "" + is_reference_value = self._is_reference_value(stix_field) + is_object_filter_typeValue = self._is_object_filterType( + stix_object, stix_field) + # Need to use expression.value to match against regex since the passed-in value has already been formated. + value_type = self._check_value_type( + expression.value) if is_reference_value else None + mapped_fields_count = 1 if is_reference_value else len( + mapped_fields_array) + + for mapped_field in mapped_fields_array: + if is_reference_value: + parsed_reference = self._parse_reference( + self, stix_field, value_type, mapped_field, value, comparator) + if not parsed_reference: + continue + comparison_string += parsed_reference + elif is_object_filter_typeValue: + comparison_string += 'filter_key={mapped_field}&filter_value="{value}"'.format( + mapped_field=mapped_field, comparator=comparator, value=value) + else: + comparison_string += '{mapped_field}{comparator}"{value}"'.format( + mapped_field=mapped_field, comparator=comparator, value=value) + + if mapped_fields_count > 1: + comparison_string += "&" + mapped_fields_count -= 1 + return comparison_string + + @staticmethod + def _is_reference_value(stix_field): + return stix_field == 'src_ref.value' or stix_field == 'dst_ref.value' + + @staticmethod + def _is_object_filterType(stix_object, stix_field): + if (stix_object in REFERENCE_FILTER_TYPE) and (stix_field not in REFERENCE_EXCLUDE_FILTER_TYPE): + return True + else: + return False + + @staticmethod + def _check_filter_value_type(value, stix_object): + """ + Function returning value type of event type object in double quotes + :param value: str + :return: string + """ + event_object = ['event_type'] + if stix_object in event_object: + return '"{}"'.format(value) + else: + return value + + @staticmethod + def _lookup_comparison_operator(self, expression_operator): + if str(expression_operator) not in self.comparator_lookup: + raise NotImplementedError( + "Comparison operator {} unsupported for verify connector".format(expression_operator.name)) + return self.comparator_lookup[str(expression_operator)] + + @classmethod + def _format_start_stop_qualifier(self, expression, qualifier) -> str: + """ + Convert a STIX start stop qualifier into a query string. + """ + transformer = TimestampToMilliseconds() + qualifier_split = qualifier.split("'") + start = qualifier_split[1] + stop = qualifier_split[3] + # convert timepestamp to millisecond which will be passed to rest service + start_epoach = transformer.transform(start) + stop_epoach = transformer.transform(stop) + + qualified_query = "%s&from=%s&to=%s" % ( + expression, start_epoach, stop_epoach) + return qualified_query + + def _parse_expression(self, expression, qualifier=None) -> Union[str, list]: + if isinstance(expression, ComparisonExpression): # Base Case + # Resolve STIX Object Path to a field in the target Data Model + stix_object, stix_field = expression.object_path.split(':') + # Multiple data source fields may map to the same STIX Object + mapped_fields_array = self.dmm.map_field(stix_object, stix_field) + # Resolve the comparison symbol to use in the query string (usually just ':') + comparator = self._lookup_comparison_operator( + self, expression.comparator) + + # Some values are formatted differently based on how they're being compared + if expression.comparator == ComparisonComparators.Equal or expression.comparator == ComparisonComparators.NotEqual: + # Should be in single-quotes + value = self._escape_value(expression.value) + # check if belongs to event object type. This require sepecial treatment. + value = self._check_filter_value_type(value, stix_object) + elif expression.comparator == ComparisonComparators.In: + in_string = expression.value.values if hasattr( + expression.value, 'values') else expression.value + values = self._convert_list_string_in_condition(in_string) + # apply escape value to remove unwanted char in string. + value = self._escape_value(values) + + else: + value = self._escape_value(expression.value) + + comparison_string = self._parse_mapped_fields( + self, expression, value, comparator, stix_field, mapped_fields_array, stix_object) + if(len(mapped_fields_array) > 1 and not self._is_reference_value(stix_field)): + # More than one data source field maps to the STIX attribute, so group comparisons together. + grouped_comparison_string = comparison_string + comparison_string = grouped_comparison_string + + if expression.negated: + comparison_string = self._negate_comparison(comparison_string) + if qualifier is not None: + return self._format_start_stop_qualifier(comparison_string, qualifier) + else: + return "{}".format(comparison_string) + + elif isinstance(expression, CombinedComparisonExpression): + operator = self._lookup_comparison_operator( + self, expression.operator) + expression_01 = self._parse_expression(expression.expr1) + expression_02 = self._parse_expression(expression.expr2) + + if not expression_01 or not expression_02: + return '' + if isinstance(expression.expr1, CombinedComparisonExpression): + expression_01 = "{}".format(expression_01) + if isinstance(expression.expr2, CombinedComparisonExpression): + expression_02 = "{}".format(expression_02) + + query_string = "{}{}{}".format( + expression_01, operator, expression_02) + if qualifier is not None: + return self._format_start_stop_qualifier(query_string, qualifier) + else: + return "{}".format(query_string) + elif isinstance(expression, ObservationExpression): + return self._parse_expression(expression.comparison_expression, qualifier) + elif hasattr(expression, 'qualifier') and hasattr(expression, 'observation_expression'): + if isinstance(expression.observation_expression, CombinedObservationExpression): + operator = self._lookup_comparison_operator( + self, expression.observation_expression.operator) + expression_01 = self._parse_expression( + expression.observation_expression.expr1) + # qualifier only needs to be passed into the parse expression once since it will be the same for both expressions + expression_02 = self._parse_expression( + expression.observation_expression.expr2, expression.qualifier) + return "{} {} {}".format(expression_01, operator, expression_02) + else: + return self._parse_expression(expression.observation_expression.comparison_expression, expression.qualifier) + elif isinstance(expression, CombinedObservationExpression): + operator = self._lookup_comparison_operator( + self, expression.operator) + expression_01 = self._parse_expression(expression.expr1) + expression_02 = self._parse_expression(expression.expr2) + if not isinstance(expression_01, list): + QueryStringPatternTranslator.QUERIES.extend([expression_01]) + if not isinstance(expression_02, list): + QueryStringPatternTranslator.QUERIES.extend([expression_02]) + return QueryStringPatternTranslator.QUERIES + elif isinstance(expression, Pattern): + return self._parse_expression(expression.expression) + else: + raise RuntimeError("Unknown Recursion Case for expression={}, type(expression)={}".format( + expression, type(expression))) + + def parse_expression(self, pattern: Pattern): + return self._parse_expression(pattern) + + +def translate_pattern(pattern: Pattern, data_model_mapping, options): + # Query result limit and time range can be passed into the QueryStringPatternTranslator if supported by the data source. + result_limit = options['result_limit'] + list_final_query = [] + # time_range = options['time_range'] + query = QueryStringPatternTranslator( + pattern, data_model_mapping).translated + query = query if isinstance(query, list) else [query] + for each_query in query: + base_query = f"{each_query}&size={result_limit}" + list_final_query.append(base_query) + + return list_final_query diff --git a/stix_shifter_modules/ibm_security_verify/stix_translation/query_translator.py b/stix_shifter_modules/ibm_security_verify/stix_translation/query_translator.py new file mode 100644 index 000000000..575372d07 --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_translation/query_translator.py @@ -0,0 +1,26 @@ +import logging + +from stix_shifter_utils.modules.base.stix_translation.base_query_translator import BaseQueryTranslator +from . import query_constructor + +logger = logging.getLogger(__name__) + + +class QueryTranslator(BaseQueryTranslator): + + def transform_antlr(self, data, antlr_parsing_object): + """ + Transforms STIX pattern into a different query format. Based on a mapping file + :param antlr_parsing_object: Antlr parsing objects for the STIX pattern + :type antlr_parsing_object: object + :param mapping: The mapping file path to use as instructions on how to transform the given STIX query into another format. This should default to something if one isn't passed in + :type mapping: str (filepath) + :return: transformed query string + :rtype: str + """ + + logger.info("Converting STIX2 Pattern to data source query") + + query_string = query_constructor.translate_pattern( + antlr_parsing_object, self, self.options) + return query_string diff --git a/stix_shifter_modules/ibm_security_verify/stix_translation/results_translator.py b/stix_shifter_modules/ibm_security_verify/stix_translation/results_translator.py new file mode 100644 index 000000000..ac0a65a33 --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_translation/results_translator.py @@ -0,0 +1,5 @@ +from stix_shifter_utils.stix_translation.src.json_to_stix.json_to_stix import JSONToStix + + +class ResultsTranslator(JSONToStix): + pass \ No newline at end of file diff --git a/stix_shifter_modules/ibm_security_verify/stix_translation/transformers.py b/stix_shifter_modules/ibm_security_verify/stix_translation/transformers.py new file mode 100644 index 000000000..dde3aad43 --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_translation/transformers.py @@ -0,0 +1,8 @@ +from stix_shifter_utils.stix_translation.src.utils.transformers import ValueTransformer + +class VerifyStaticTransformer(ValueTransformer): + """A value transformer that always returns the string 'IBM Security Verify Event'""" + + @staticmethod + def transform(value): + return "IBM Security Verify Event" \ No newline at end of file diff --git a/stix_shifter_modules/ibm_security_verify/stix_transmission/__init__.py b/stix_shifter_modules/ibm_security_verify/stix_transmission/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/stix_shifter_modules/ibm_security_verify/stix_transmission/api_client.py b/stix_shifter_modules/ibm_security_verify/stix_transmission/api_client.py new file mode 100644 index 000000000..875745f2b --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_transmission/api_client.py @@ -0,0 +1,187 @@ +import json +from datetime import datetime, timedelta +from urllib.parse import urlencode + +import requests +from requests.adapters import Response +from stix_shifter_utils.stix_transmission.utils.RestApiClient import \ + RestApiClient +from stix_shifter_utils.utils import logger + + +class APIClient: + + endpoint_start = '/v1.0/events' + toeken_endpoint = '/v1.0/endpoint/default/token' + + def __init__(self, connection, configuration): + self.logger = logger.set_logger(__name__) + + headers = dict() + url_modifier_function = None + auth = configuration.get('auth') + # self.endpoint_start = 'incidents/' + self.host = connection.get('host') + self.client = RestApiClient(connection.get('host'), connection.get('port', None), + headers,url_modifier_function=url_modifier_function, + cert_verify=connection.get('selfSignedCert', False), + sni=connection.get('sni', None) + ) + self.timeout = connection['options'].get('timeout') + self._client_id = auth['clientId'] + self._client_secret = auth['clientSecret'] + self._token = None + self._token_time = None + + def get_token(self): + """get the token and if expired re-generate and store in token variable""" + tokenResponse = self.generate_token() + return tokenResponse.json().get('access_token') + + def generate_token(self): + """To generate the Token""" + if self.token_expired(): + resp = requests.request( + 'POST', + 'https://'+self.host+self.toeken_endpoint, + headers={ + 'accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data=( + f'client_id={self._client_id}' + f'&client_secret={self._client_secret}' + f'&grant_type=client_credentials' + f'&scope=openid' + ) + ) + token = resp.json().get('access_token') + self._token = token + self._token_time = datetime.now() + self.resp =resp + return self.resp + + def token_expired(self) -> bool: + """Check if the verify token is expired. + :return: True if token is expired, False if not expired + :return type: bool + """ + expired = True + if self._token: + expired = (datetime.now() - self._token_time) >= timedelta(minutes=30) + return expired + + + def run_search(self, query_expr, range_end=None): + """get the response from verify endpoints + :param quary_expr: dict, filter parameters + :param range_end: int,length value + :return: response, json object""" + events = [] + # if self._token : + events = self.get_events(query_expr) + return self.response_handler(events,query_expr) + + + def get_events(self,query_expr): + self.headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer {0}'.format(self.get_token())} + if query_expr is None: + data=None + return self.client.call_api(self.endpoint_start,'GET',self.headers,urldata= query_expr) + + def response_handler(self, data=None,query_expr=None): + if data is None: + data = [] + response = dict() + response['data'] =json.loads(data.read()) + response['error'] = data.code + response['code'] =data.code + response['error_msg'] = data.response.reason + response['success'] =data.code + if response['code'] ==200: + + response['search_after'] = response.get("data")['response']['events']['search_after'] + + try: + response['event_data'] = self.parseJson(response.get("data")['response']['events']['events']) + except KeyError: + self.logger.debug('events data not found in respose object',response) + response['event_data'] = [] + + elif response['error'] == 500 and "true" in response['error_msg']: + response.update({"code": 200, "data": []}) + else: + response["message"] = data.response.reason + + return response + + + def parseJson(self, response): + ''' + Iterate through the response and read the nested data like geoip and data object. + ''' + jsonObj = response + finalJson = dict() + parsedJson = [] + for obj in jsonObj: + dictC = obj + if "geoip" in dictC: + dictA= json.loads(json.dumps(obj["geoip"])) + del dictC["geoip"] + dictB= json.loads(json.dumps(obj["data"])) + del dictC["data"] + dict_geo_location = json.loads(json.dumps(dictA.get('location'))) + del dictA['location'] + finalJson = {**dictA,**dictB,**dict_geo_location} + else: + dictB= json.loads(json.dumps(obj["data"])) + del dictC["data"] + finalJson = dictB + remainingJson = json.loads(json.dumps(dictC)) + finalJson = {**finalJson,**remainingJson} + parsedJson.append(finalJson) + return parsedJson + + + def key_exist(self,data_element, *keysarr) : + ''' + Check if *keys (nested) exists in `element` (dict). + ''' + if not isinstance(data_element, dict): + raise AttributeError('keys_exists() expects dict as first argument.') + if len(keysarr) == 0: + raise AttributeError('keys_exists() expects at least two arguments, one given.') + + _element = data_element + for key in list(keysarr): + try: + _element = _element[key] + except KeyError: + self.logger.debug('key not found ',key ) + return False + return True + + def get_search_results(self, search_id, response_type, range_start=None, range_end=None): + # Sends a GET request to + # https://// + # response object body should contain information pertaining to search. + #https://isrras.ice.ibmcloud.com/v1.0/events?event_type="sso"&size=10&after="1640104162523","eeb40fd5-6b84-4dc9-9251-3f7a4cfd91c0" + headers = dict() + headers['Accept'] = response_type + size = 1000 + if ((range_start is None) and (range_end is None)): + size = range_end - range_start + + request_param = search_id+"& size="+str(size) + endpoint = self.endpoint_start+ request_param + + return self.run_search(search_id) + + def get_search(self, search_id): + # Sends a GET request to + # https:///api/ariel/searches/ + response = self.run_search(search_id) + return response + + def delete_search(self,search_id): + return self.run_search(search_id) diff --git a/stix_shifter_modules/ibm_security_verify/stix_transmission/delete_connector.py b/stix_shifter_modules/ibm_security_verify/stix_transmission/delete_connector.py new file mode 100644 index 000000000..ea07d78c8 --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_transmission/delete_connector.py @@ -0,0 +1,11 @@ +from stix_shifter_utils.modules.base.stix_transmission.base_delete_connector import BaseDeleteConnector +from stix_shifter_utils.utils.error_response import ErrorResponder +import json + + +class DeleteConnector(BaseDeleteConnector): + def __init__(self, api_client): + self.api_client = api_client + + def delete_query_connection(self, search_id): + return {"success": True} diff --git a/stix_shifter_modules/ibm_security_verify/stix_transmission/error_mapper.py b/stix_shifter_modules/ibm_security_verify/stix_transmission/error_mapper.py new file mode 100644 index 000000000..363bcf0ca --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_transmission/error_mapper.py @@ -0,0 +1,34 @@ +from stix_shifter_utils.utils.error_mapper_base import ErrorMapperBase +from stix_shifter_utils.utils.error_response import ErrorCode +from stix_shifter_utils.utils import logger + +error_mapping = { + # No Route Exists + 404: ErrorCode.TRANSMISSION_REMOTE_SYSTEM_IS_UNAVAILABLE, + # Authentication Failure + 401: ErrorCode.TRANSMISSION_AUTH_CREDENTIALS, + # A request parameter is not valid + 400: ErrorCode.TRANSMISSION_INVALID_PARAMETER, +} + +class ErrorMapper(): + logger = logger.set_logger(__name__) + DEFAULT_ERROR = ErrorCode.TRANSMISSION_MODULE_DEFAULT_ERROR + + @staticmethod + def set_error_code(json_data, return_obj): + code = None + try: + code = json_data['code'] + except Exception: + pass + + error_code = ErrorMapper.DEFAULT_ERROR + + if code in error_mapping: + error_code = error_mapping[code] + + if error_code == ErrorMapper.DEFAULT_ERROR: + ErrorMapper.logger.error("failed to map: " + str(json_data)) + + ErrorMapperBase.set_error_code(return_obj, error_code) diff --git a/stix_shifter_modules/ibm_security_verify/stix_transmission/ping_connector.py b/stix_shifter_modules/ibm_security_verify/stix_transmission/ping_connector.py new file mode 100644 index 000000000..72378517b --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_transmission/ping_connector.py @@ -0,0 +1,33 @@ +import datetime +import json + +from stix_shifter_modules.ibm_security_verify.stix_transmission.api_client import \ + APIClient +from stix_shifter_utils.modules.base.stix_transmission.base_ping_connector import \ + BasePingConnector +from stix_shifter_utils.utils import logger +from stix_shifter_utils.utils.error_response import ErrorResponder + + +class PingConnector(BasePingConnector): + + def __init__(self, api_client): + self.api_client = api_client + self.logger = logger.set_logger(__name__) + + def ping_connection(self): + ''' + ping the connection and return status details. + ''' + try: + response = self.api_client.generate_token() + # Construct a response object + return_obj = dict() + if response.status_code== 200: + return_obj['success'] = True + else: + ErrorResponder.fill_error(return_obj, response, ['message']) + return return_obj + except Exception as err: + self.logger.error('error when pinging datasource {}:'.format(err)) + raise diff --git a/stix_shifter_modules/ibm_security_verify/stix_transmission/query_connector.py b/stix_shifter_modules/ibm_security_verify/stix_transmission/query_connector.py new file mode 100644 index 000000000..ad48c85ab --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_transmission/query_connector.py @@ -0,0 +1,13 @@ +from stix_shifter_utils.modules.base.stix_transmission.base_connector import BaseQueryConnector +from stix_shifter_utils.utils.error_response import ErrorResponder +from stix_shifter_utils.utils import logger +import json + + +class QueryConnector(BaseQueryConnector): + def __init__(self, api_client): + self.api_client = api_client + self.logger = logger.set_logger(__name__) + + def create_query_connection(self, query): + return {"success": True, "search_id": query} \ No newline at end of file diff --git a/stix_shifter_modules/ibm_security_verify/stix_transmission/results_connector.py b/stix_shifter_modules/ibm_security_verify/stix_transmission/results_connector.py new file mode 100644 index 000000000..f1caae49a --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_transmission/results_connector.py @@ -0,0 +1,40 @@ +import json +import traceback + +from stix_shifter_utils.modules.base.stix_transmission.base_results_connector import \ + BaseResultsConnector +from stix_shifter_utils.utils import logger +from stix_shifter_utils.utils.error_response import ErrorResponder + + +class ResultsConnector(BaseResultsConnector): + + def __init__(self, api_client): + self.api_client = api_client + self.logger = logger.set_logger(__name__) + + def create_results_connection(self, search_id, offset, length): + offset = int(offset) + length = int(length) + + try: + response = self.api_client.run_search(search_id, length) + response_code = response['code'] + # Construct a response object + return_obj = dict() + if response_code == 200: + return_obj['success'] = True + return_obj['data'] = response.get("event_data", []) + return_obj['search_after'] = response.get("search_after", []) + # filter data based on filter_attr + # slice the records as per the provided offset and length(limit) + return_obj['data'] = return_obj['data'][offset:length] + else: + ErrorResponder.fill_error(return_obj, response, ['message']) + + except Exception as err: + self.logger.error( + 'error when getting search results: {}'.format(err)) + self.logger.error(traceback.print_stack()) + raise + return return_obj diff --git a/stix_shifter_modules/ibm_security_verify/stix_transmission/status_connector.py b/stix_shifter_modules/ibm_security_verify/stix_transmission/status_connector.py new file mode 100644 index 000000000..834e0c8c8 --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/stix_transmission/status_connector.py @@ -0,0 +1,16 @@ + +from stix_shifter_utils.modules.base.stix_transmission.base_status_connector import BaseStatusConnector +from stix_shifter_utils.modules.base.stix_transmission.base_status_connector import Status +from stix_shifter_utils.utils.error_response import ErrorResponder +from stix_shifter_utils.utils import logger as utils_logger +from enum import Enum +import json + + +class StatusConnector(BaseStatusConnector): + def __init__(self, api_client): + self.api_client = api_client + self.logger = utils_logger.set_logger(__name__) + + def create_status_connection(self, search_id): + return {"success": True, "status": "COMPLETED", "progress": 100} diff --git a/stix_shifter_modules/ibm_security_verify/test/stix_translation/___init__.py b/stix_shifter_modules/ibm_security_verify/test/stix_translation/___init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/stix_shifter_modules/ibm_security_verify/test/stix_translation/test_ibm_verify_stix_to_query.py b/stix_shifter_modules/ibm_security_verify/test/stix_translation/test_ibm_verify_stix_to_query.py new file mode 100644 index 000000000..3b1dcf0ea --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/test/stix_translation/test_ibm_verify_stix_to_query.py @@ -0,0 +1,106 @@ +import json +import logging +import unittest +from unicodedata import category + +from stix_shifter.stix_translation import stix_translation +from stix_shifter_modules.ibm_security_verify.entry_point import EntryPoint +from stix_shifter_utils.stix_translation.src.utils.transformer_utils import ( + get_module_transformers, +) + +translation = stix_translation.StixTranslation() +# config_file = open('stix_shifter_modules/verify_event/configuration/config.json').read() +# from_stix_file = open('stix_shifter_modules/verify_event/stix_translation/json/from_stix_map.json').read() +# to_stix_file = open('stix_shifter_modules/verify_event/stix_translation/json/to_stix_map.json').read() +# OPTIONS = json.loads(from_stix_file) + +# logging.basicConfig(level=logging.DEBUG) +# logger = logging.getLogger() +MODULE = "ibm_security_verify" +RESULTS = "results" +TRANSFORMERS = get_module_transformers(MODULE) +epoch_to_timestamp_class = TRANSFORMERS.get("EpochToTimestamp") +EPOCH_START = 1531169112 +EPOCH_END = 1531169254 +START_TIMESTAMP = epoch_to_timestamp_class.transform(EPOCH_START) +END_TIMESTAMP = epoch_to_timestamp_class.transform(EPOCH_END) +entry_point = EntryPoint() +MAP_DATA = entry_point.get_results_translator().map_data + + +DATA_SOURCE = { + "type": "identity", + "id": "identity--32a23267-52fb-4e82-859b-0a15d6a2d334", + "name": "ibm_security_verify", + "identity_class": "events", +} +OPTION = json.dumps(DATA_SOURCE) + + +def _test_query_assertions(query, queries): + assert query["queries"] == [queries] + + +def _translate_query(stix_pattern): + return translation.translate("ibm_security_verify", "query", "{}", stix_pattern) + + +class TestStixToQuery(unittest.TestCase, object): + def test_event_type(self): + stix_pattern = "[x-oca-event:category='authentication']" + query = _translate_query(stix_pattern) + queries = 'event_type="authentication"&size=10000' + _test_query_assertions(query, queries) + + def test_IPV4_query(self): + stix_pattern = "[ipv4-addr:value='192.168.1.1']" + query = _translate_query(stix_pattern) + expected_queries = ( + 'filter_key=data.origin&filter_value="192.168.1.1"&size=10000' + ) + _test_query_assertions(query, expected_queries) + + def test_event_type_and_fiter(self): + stix_pattern = "[x-oca-event:category = 'sso' AND x-oca-event:domain_ref.value = '192.168.1.1']START t'2022-01-17T18:24:00.000Z' STOP t'2022-01-20T18:24:00.000Z' " + query = _translate_query(stix_pattern) + expected_queries = 'filter_key=tenantname&filter_value="192.168.1.1"&event_type="sso"&from=1642443840000&to=1642703040000&size=10000' + _test_query_assertions(query, expected_queries) + + def test_domain_name_query(self): + stix_pattern = "[domain-name:value = 'ibmcloud.com']" + query = _translate_query(stix_pattern) + expected_queries = ( + 'filter_key=tenantname&filter_value="ibmcloud.com"&size=10000' + ) + _test_query_assertions(query, expected_queries) + + def test_applicationname_with_special_char_query(self): + stix_pattern = ( + "[x-oca-event:extensions.'x-iam-ext'.application_name='Bane & Ox VPN']" + ) + query = _translate_query(stix_pattern) + expected_query = ( + 'filter_key=data.applicationname&filter_value="Bane %26 Ox VPN"&size=10000' + ) + _test_query_assertions(query, expected_query) + + def test_in_statement(self): + stix_pattern = ( + "[ ipv4-addr:value IN ('192.168.1.1', '192.168.1.2', '192.168.1.3') ]" + ) + query = _translate_query(stix_pattern) + expected_query = 'filter_key=data.origin&filter_value="192.168.1.1","192.168.1.2","192.168.1.3"&size=10000' + _test_query_assertions(query, expected_query) + + def test_in_statement_with_start_stop(self): + stix_pattern = "[ ipv4-addr:value IN ('192.168.1.1', '192.168.1.2', '192.168.1.3') ] START t'2022-02-06T07:19:00.000Z' STOP t'2022-02-08T07:19:00.000Z' " + query = _translate_query(stix_pattern) + expected_query = 'filter_key=data.origin&filter_value="192.168.1.1","192.168.1.2","192.168.1.3"&from=1644131940000&to=1644304740000&size=10000' + _test_query_assertions(query, expected_query) + + def test_in_statement_with_event_type_start_stop(self): + stix_pattern = "[ x-oca-event:category IN ('sso', 'authentication') ] START t'2022-02-06T07:19:00.000Z' STOP t'2022-02-08T07:19:00.000Z' " + query = _translate_query(stix_pattern) + expected_query = 'event_type="sso","authentication"&from=1644131940000&to=1644304740000&size=10000' + _test_query_assertions(query, expected_query) diff --git a/stix_shifter_modules/ibm_security_verify/test/stix_translation/test_ibm_verify_transform.py b/stix_shifter_modules/ibm_security_verify/test/stix_translation/test_ibm_verify_transform.py new file mode 100644 index 000000000..5a275b404 --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/test/stix_translation/test_ibm_verify_transform.py @@ -0,0 +1,123 @@ +import json +import logging +import unittest +from unicodedata import category + +from stix_shifter.stix_translation import stix_translation +from stix_shifter_modules.ibm_security_verify.entry_point import EntryPoint +from stix_shifter_utils.stix_translation.src.utils.transformer_utils import ( + get_module_transformers, +) + +translation = stix_translation.StixTranslation() +# config_file = open('stix_shifter_modules/verify_event/configuration/config.json').read() +# from_stix_file = open('stix_shifter_modules/verify_event/stix_translation/json/from_stix_map.json').read() +# to_stix_file = open('stix_shifter_modules/verify_event/stix_translation/json/to_stix_map.json').read() +# OPTIONS = json.loads(from_stix_file) + +# logging.basicConfig(level=logging.DEBUG) +# logger = logging.getLogger() +MODULE = "ibm_security_verify" +RESULTS = "results" +TRANSFORMERS = get_module_transformers(MODULE) +epoch_to_timestamp_class = TRANSFORMERS.get("EpochToTimestamp") +entry_point = EntryPoint() +MAP_DATA = entry_point.get_results_translator().map_data + + +DATA_SOURCE = { + "type": "identity", + "id": "32a23267-52fb-4e82-859b-0a15d6a2d334", + "name": "verify", + "identity_class": "events", +} +OPTION = json.dumps(DATA_SOURCE) + + +class TestTransformQuery(unittest.TestCase, object): + @staticmethod + def get_first(itr, constraint): + return next((obj for obj in itr if constraint(obj)), None) + + @staticmethod + def get_first_of_type(itr, typ): + return TestTransformQuery.get_first( + itr, lambda o: type(o) == dict and o.get("type") == typ + ) + + @staticmethod + def get_object_keys(objects): + for k, v in objects.items(): + if k == "type": + yield v + elif isinstance(v, dict): + for id_val in TestTransformQuery.get_object_keys(v): + yield id_val + + def test_oca_event(self): + data = [ + { + "continent_name": "Asia", + "city_name": "Kolkata", + "country_iso_code": "IN", + "ip": "47.15.98.56", + "country_name": "India", + "region_name": "West Bengal", + "location": {"lon": "88.3697", "lat": "22.5697"}, + "result": "success", + "subtype": "saml", + "providerid": "https://portal.baneandox.org:443/SAML20/SP", + "origin": "47.15.98.56", + "realm": "www.ibm.com", + "applicationid": "6773634223410562472", + "userid": "652001LT0R", + "applicationtype": "Custom Application", + "devicetype": "PAN GlobalProtect/5.2.4-21 (Microsoft Windows 10 Enterprise , 64-bit) Mozilla/5.0 (Windows NT 6.2; Win64; x64; Trident/7.0; rv:11.0) like Gecko", + "username": "dinepal1@in.ibm.com", + "applicationname": "Bane & Ox VPN", + "year": 2022, + "billingid": "12345", + "mdmismanaged": "true", + "mdmiscompliant": "true", + "deviceid": "abc_device", + "@metadata": { + "group_id": "event-transform-prod-eu01a-prod-eu01a-01", + "source_dc": "prod-eu01a", + }, + "event_type": "sso", + "month": 1, + "indexed_at": 1642413142906, + "@processing_time": 1012, + "tenantid": "c92ce528-293f-4e84-8307-c4fe188b9461", + "tenantname": "isrras.ice.ibmcloud.com", + "correlationid": "CORR_ID-a134f569-8d73-45ac-8d44-2457448c9101", + "servicename": "saml_runtime", + "id": "dc4523e6-6260-4349-83f8-3320365a5f25", + "time": 1642413141894, + "day": 17, + } + ] + + user_ref = "2" + category = "sso" + domain_ref = "3" + module = "saml_runtime" + extensions_user_id = "652001LT0R" + result_bundle = entry_point.translate_results( + json.dumps(DATA_SOURCE), json.dumps(data) + ) + observed_data = result_bundle["objects"][1] + objects = observed_data["objects"] + + event = TestTransformQuery.get_first_of_type(objects.values(), "x-oca-event") + + assert (event["type"]) == "x-oca-event" + assert event["user_ref"] == user_ref + assert event["category"] == category + assert event["domain_ref"] == domain_ref + assert event["module"] == module + assert event["category"] == category + assert event["extensions"]["x-iam-ext"]["is_device_managed"] == "true" + assert event["extensions"]["x-iam-ext"]["mdm_customerid"] == "12345" + assert event["extensions"]["x-iam-ext"]["is_device_compliant"] == "true" + assert event["extensions"]["x-iam-ext"]["deviceid"] == "abc_device" diff --git a/stix_shifter_modules/ibm_security_verify/test/stix_transmission/___init__.py b/stix_shifter_modules/ibm_security_verify/test/stix_transmission/___init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/stix_shifter_modules/ibm_security_verify/test/stix_transmission/test_ibm_verify_transmission.py b/stix_shifter_modules/ibm_security_verify/test/stix_transmission/test_ibm_verify_transmission.py new file mode 100644 index 000000000..1de86821a --- /dev/null +++ b/stix_shifter_modules/ibm_security_verify/test/stix_transmission/test_ibm_verify_transmission.py @@ -0,0 +1,143 @@ +import json +import unittest +from sre_constants import ASSERT_NOT +from unittest.mock import ANY, patch + +from stix_shifter.stix_transmission import stix_transmission +from stix_shifter_modules.ibm_security_verify.entry_point import EntryPoint +from stix_shifter_utils.modules.base.stix_transmission.base_status_connector import \ + Status +from stix_shifter_utils.stix_transmission.utils.RestApiClient import \ + ResponseWrapper + + +class VerifyMockResponse: + def __init__(self, response_code, object): + self.code = response_code + self.object = object + + def read(self): + return self.object + + +class VerifyMockPingResponse: + def __init__(self, response_code, status_code): + self.code = response_code + self.status_code = status_code + + def read(self): + return self.object + + +@patch( + "stix_shifter_modules.ibm_security_verify.stix_transmission.api_client.APIClient.__init__", + autospec=True, +) +class TestVerifyConnection(unittest.TestCase, object): + def test_is_async(self, mock_api_client): + mock_api_client.return_value = None + entry_point = EntryPoint() + + config = {"auth": {"sec": "bla"}} + connection = { + "host": "hostbla", + "port": 8080, + } + check_async = entry_point.is_async() + assert check_async is True + + @patch( + "stix_shifter_modules.ibm_security_verify.stix_transmission.api_client.APIClient.get_search", + autospec=True, + ) + def test_status_response(self, mock_status_response, mock_api_client): + mock_api_client.return_value = None + mocked_return_value = '{"search_id": "108cb8b0-0744-4dd9-8e35-ea8311cd6211", "status": "COMPLETED", "progress": "100"}' + mock_status_response.return_value = VerifyMockResponse(200, mocked_return_value) + + config = {"host": "connection.com"} + connection = {"auth": {"clientId": "clientId", "clientSecret": "clientscred"}} + + search_id = "108cb8b0-0744-4dd9-8e35-ea8311cd6211" + transmission = stix_transmission.StixTransmission("ibm_security_verify", config, connection) + status_response = transmission.status(search_id) + + assert status_response["success"] + assert status_response is not None + assert "status" in status_response + assert status_response["status"] == Status.COMPLETED.value + + @patch( + "stix_shifter_modules.ibm_security_verify.stix_transmission.api_client.APIClient.run_search" + ) + def test_query_response(self, mock_query_response, mock_api_client): + mock_api_client.return_value = None + mock_query_response.return_value = {"success": 200} + + config = {"host": "cloudsecurity.com"} + connection = {"auth": {"clientId": "clientid", "clientSecret": "secret"}} + + query = '{"query":"event_type="sso""}' + transmission = stix_transmission.StixTransmission("ibm_security_verify", config, connection) + query_response = transmission.query(query) + + assert query_response is not None + assert "search_id" in query_response + assert query_response["search_id"] == query + + @patch( + "stix_shifter_modules.ibm_security_verify.stix_transmission.api_client.APIClient.generate_token" + ) + def test_ping(self, mock_generate_token, mock_api_client): + + config = {"host": "cloudsecurity.com"} + connection = {"auth": {"clientId": "clientid", "clientSecret": "secret"}} + mocked_return_value = VerifyMockPingResponse(200, 200) + mock_generate_token.return_value = mocked_return_value + mock_api_client.return_value = None + entry_point = EntryPoint(config, connection) + ping_result = entry_point.ping_connection() + assert ping_result["success"] is True + + @patch( + "stix_shifter_modules.ibm_security_verify.stix_transmission.api_client.APIClient.generate_token" + ) + @patch( + "stix_shifter_modules.ibm_security_verify.stix_transmission.api_client.APIClient.run_search", + autospec=True, + ) + def test_results_all_response( + self, mock_results_response, mock_generate_token, mock_api_client + ): + mock_api_client.return_value = None + mocked_return_value = {"code": 200} + mock_generate_token.return_value = mocked_return_value + config = {"host": "ibmcloud.com"} + connection = {"auth": {"clientId": "clientId", "clientSecret": "secret"}} + + mocked_return_value = { + "code": 200, + "success": 200, + "data": [ + { + "id": 123, + "created_at": "2022-01-16T16:45:16.112Z", + "account_id": 123, + "ipaddr": "12.22.33.44", + } + ], + } + + mock_results_response.return_value = mocked_return_value + + query = 'event_type="sso"&limit=10000' + + offset = 0 + length = 101 + entry_point = EntryPoint(config, connection) + results_response = entry_point.create_results_connection(query, offset, length) + + assert results_response is not None + assert results_response["success"] + assert "data" in results_response + assert results_response["data"] is not None