Skip to content

Commit ba715f2

Browse files
committed
[SOAR-18202] [Salesforce] Ensure date.now includes microseconds (#2960)
1 parent f4c6a3d commit ba715f2

File tree

8 files changed

+57
-34
lines changed

8 files changed

+57
-34
lines changed

plugins/salesforce/.CHECKSUM

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"spec": "e182e26e61f7d3375dc3a9bc3df8fc11",
3-
"manifest": "4d555de9a1d8b4ead1868f7d8ae7c1b5",
4-
"setup": "8d0731798d9f79b7c98d821e8abf0001",
2+
"spec": "63b7270d95683b98e315808e4df20354",
3+
"manifest": "391ed2bce80fc53ee24774278259d26e",
4+
"setup": "295d03a5efdf6658a6a10babe80a9a06",
55
"schemas": [
66
{
77
"identifier": "advanced_search/schema.py",

plugins/salesforce/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM --platform=linux/amd64 rapid7/insightconnect-python-3-plugin:6.1.4
1+
FROM --platform=linux/amd64 rapid7/insightconnect-python-3-plugin:6.2.0
22

33
LABEL organization=rapid7
44
LABEL sdk=python

plugins/salesforce/bin/komand_salesforce

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ from sys import argv
66

77
Name = "Salesforce"
88
Vendor = "rapid7"
9-
Version = "2.1.11"
9+
Version = "2.1.12"
1010
Description = "[Salesforce](https://www.salesforce.com) is a CRM solution that brings together all customer information in a single, integrated platform that enables building a customer-centered business from marketing right through to sales, customer service and business analysis. The Salesforce plugin allows you to search, update, and manage salesforce records. This plugin utilizes the [Salesforce API](https://developer.salesforce.com/docs/atlas.en-us.216.0.api_rest.meta/api_rest/intro_what_is_rest_api.htm)"
1111

1212

plugins/salesforce/help.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,10 +529,11 @@ Example output:
529529

530530
## Troubleshooting
531531

532-
*There is no troubleshooting for this plugin.*
532+
*This plugin does not contain a troubleshooting.*
533533

534534
# Version History
535535

536+
* 2.1.12 - Task Monitor Users: ensure datetime includes microseconds | Bump SDK to 6.2.0
536537
* 2.1.11 - Task Monitor Users: Return 500 for retry your request error | Bump SDK to 6.1.4
537538
* 2.1.10 - Set Monitor Users task output length | Fix to remove whitespace from connection inputs
538539
* 2.1.9 - SDK Bump to 6.1.0 | Task Connection test added

plugins/salesforce/komand_salesforce/tasks/monitor_users/task.py

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
DEFAULT_CUTOFF_HOURS = 24 * 7
1111
INITIAL_LOOKBACK = 24
1212

13+
exp_frmt = "%Y-%m-%d %H:%M:%S.%f%z" # the format the state should be in
14+
old_frmt = "%Y-%m-%dT%H:%M:%S.%f%z" # an old backwards compatible state
15+
bugged_frmt = "%Y-%m-%d %H:%M:%S%z" # str value without the microseconds - SOAR-18202
16+
SUPPORTED_STR_TYPES = [exp_frmt, old_frmt, bugged_frmt]
17+
1318

1419
class MonitorUsers(insightconnect_plugin_runtime.Task):
1520
USER_LOGIN_QUERY = "SELECT LoginTime, UserId, LoginType, LoginUrl, SourceIp, Status, Application, Browser FROM LoginHistory WHERE LoginTime >= {start_timestamp} AND LoginTime < {end_timestamp}"
@@ -63,15 +68,17 @@ def run(self, params={}, state={}, custom_config={}): # noqa: C901
6368

6469
# we only check Salesforce for new users every 24 hours / first run
6570
get_users = True
66-
state[self.NEXT_USER_COLLECTION_TIMESTAMP] = str(now + timedelta(hours=24))
71+
state[self.NEXT_USER_COLLECTION_TIMESTAMP] = self.convert_dt_to_string(now + timedelta(hours=24))
6772

6873
# we check for any user profile updates every task execution
69-
state[self.LAST_USER_UPDATE_COLLECTION_TIMESTAMP] = str(user_update_last_collection)
74+
state[self.LAST_USER_UPDATE_COLLECTION_TIMESTAMP] = self.convert_dt_to_string(
75+
user_update_last_collection
76+
)
7077

7178
# we only check for login data every hour
7279
get_user_login_history = True
73-
state[self.NEXT_USER_LOGIN_COLLECTION_TIMESTAMP] = str(now + timedelta(hours=1))
74-
state[self.LAST_USER_LOGIN_COLLECTION_TIMESTAMP] = str(user_login_end_timestamp)
80+
state[self.NEXT_USER_LOGIN_COLLECTION_TIMESTAMP] = self.convert_dt_to_string(now + timedelta(hours=1))
81+
state[self.LAST_USER_LOGIN_COLLECTION_TIMESTAMP] = self.convert_dt_to_string(user_login_end_timestamp)
7582
elif users_next_page_id or user_login_next_page_id or updated_users_next_page_id:
7683
self.logger.info("Getting next page of results...")
7784
if users_next_page_id:
@@ -82,9 +89,11 @@ def run(self, params={}, state={}, custom_config={}): # noqa: C901
8289
else:
8390
self.logger.info("Subsequent run")
8491

85-
is_valid_state, key = self._is_valid_state(state)
86-
if not is_valid_state:
87-
self.logger.info(f"Bad request error occurred. Invalid timestamp format for {key}")
92+
valid_state, key = self._make_valid_state(state)
93+
if not valid_state:
94+
self.logger.info(
95+
f"Bad request error occurred. Invalid timestamp format for {key}. Got value {state[key]}"
96+
)
8897
return (
8998
[],
9099
state,
@@ -100,29 +109,34 @@ def run(self, params={}, state={}, custom_config={}): # noqa: C901
100109
state, cut_off_time, self.LAST_USER_UPDATE_COLLECTION_TIMESTAMP
101110
)
102111
# move the end time stamp to now
103-
state[self.LAST_USER_UPDATE_COLLECTION_TIMESTAMP] = str(user_update_last_collection)
112+
state[self.LAST_USER_UPDATE_COLLECTION_TIMESTAMP] = self.convert_dt_to_string(
113+
user_update_last_collection
114+
)
104115

105116
# this allows us to poll for new users every 24 hours
106117
next_user_collection_timestamp = state.get(self.NEXT_USER_COLLECTION_TIMESTAMP)
107118
if next_user_collection_timestamp and self.compare_timestamp(
108119
now, self.convert_to_datetime(next_user_collection_timestamp)
109120
):
110121
get_users = True
111-
state[self.NEXT_USER_COLLECTION_TIMESTAMP] = str(now + timedelta(hours=24)) # poll again in 24 hrs
122+
# poll again in 24 hrs
123+
state[self.NEXT_USER_COLLECTION_TIMESTAMP] = self.convert_dt_to_string(now + timedelta(hours=24))
112124

113125
# this allows us to poll for user login data every hour
114126
next_user_login_collection_timestamp = state.get(self.NEXT_USER_LOGIN_COLLECTION_TIMESTAMP)
115127
if next_user_login_collection_timestamp and self.compare_timestamp(
116128
now, self.convert_to_datetime(next_user_login_collection_timestamp)
117129
):
118130
get_user_login_history = True
119-
state[self.NEXT_USER_LOGIN_COLLECTION_TIMESTAMP] = str(
131+
state[self.NEXT_USER_LOGIN_COLLECTION_TIMESTAMP] = self.convert_dt_to_string(
120132
now + timedelta(hours=1)
121133
) # poll again in 1 hr
122134
user_login_start_timestamp = self._get_recent_timestamp(
123135
state, cut_off_time, self.LAST_USER_LOGIN_COLLECTION_TIMESTAMP
124136
)
125-
state[self.LAST_USER_LOGIN_COLLECTION_TIMESTAMP] = str(user_login_end_timestamp)
137+
state[self.LAST_USER_LOGIN_COLLECTION_TIMESTAMP] = self.convert_dt_to_string(
138+
user_login_end_timestamp
139+
)
126140

127141
try:
128142
records = []
@@ -134,8 +148,8 @@ def run(self, params={}, state={}, custom_config={}): # noqa: C901
134148
self.logger.info(msg)
135149
response = self.connection.api.query(
136150
self.UPDATED_USERS_QUERY.format(
137-
start_timestamp=user_update_start_timestamp.isoformat(),
138-
end_timestamp=user_update_last_collection.isoformat(),
151+
start_timestamp=user_update_start_timestamp.isoformat(timespec="microseconds"),
152+
end_timestamp=user_update_last_collection.isoformat(timespec="microseconds"),
139153
),
140154
updated_users_next_page_id,
141155
)
@@ -169,8 +183,8 @@ def run(self, params={}, state={}, custom_config={}): # noqa: C901
169183
self.logger.info(msg)
170184
response = self.connection.api.query(
171185
self.USER_LOGIN_QUERY.format(
172-
start_timestamp=user_login_start_timestamp.isoformat(),
173-
end_timestamp=user_login_end_timestamp.isoformat(),
186+
start_timestamp=user_login_start_timestamp.isoformat(timespec="microseconds"),
187+
end_timestamp=user_login_end_timestamp.isoformat(timespec="microseconds"),
174188
),
175189
user_login_next_page_id,
176190
)
@@ -197,16 +211,21 @@ def run(self, params={}, state={}, custom_config={}): # noqa: C901
197211
self.connection.api.unset_token()
198212
return [], state, False, 500, PluginException(preset=PluginException.Preset.UNKNOWN, data=error)
199213

200-
def _is_valid_state(self, state: dict) -> Tuple[bool, str]:
214+
def _make_valid_state(self, state: dict) -> Tuple[bool, str]:
215+
# it looks like we used to store the timestamp in the state with the `T` delimiter and then swapped when we
216+
# started to do str(datetime) which does not have this delimiter but then swap it back and forward throughout
217+
# the task logic. Allow the state to have any and a time without the microseconds also.
218+
last_attempt = len(SUPPORTED_STR_TYPES) - 1
201219
for key, value in state.items():
202-
try:
203-
self.convert_to_datetime(value)
204-
except ValueError:
220+
for attempt_x, str_format in enumerate(SUPPORTED_STR_TYPES):
205221
try:
206-
state[key] = str(self.convert_to_datetime_from_old_format(value))
222+
dt_value = datetime.strptime(value, str_format)
223+
state[key] = self.convert_dt_to_string(dt_value)
224+
break # we're happy with this state value now move to the next one
207225
except ValueError:
208-
state[key] = str(self.get_current_time())
209-
return False, key
226+
if attempt_x != last_attempt:
227+
continue # try the next type
228+
return False, key
210229
return True, ""
211230

212231
def _get_recent_timestamp(self, state: dict, fallback_timestamp: datetime, key: str) -> datetime:
@@ -331,5 +350,7 @@ def add_data_type_field(records: list, field_value: str) -> list:
331350
record["DataType"] = field_value
332351
return records
333352

334-
def convert_to_datetime_from_old_format(self, timestamp: str) -> datetime:
335-
return datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f%z")
353+
@staticmethod
354+
def convert_dt_to_string(timestamp: datetime) -> str:
355+
# Force microseconds to keep microseconds in the string but remove the `T`
356+
return timestamp.isoformat(timespec="microseconds").replace("T", " ")

plugins/salesforce/plugin.spec.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ products: [insightconnect]
44
name: salesforce
55
title: Salesforce
66
description: "[Salesforce](https://www.salesforce.com) is a CRM solution that brings together all customer information in a single, integrated platform that enables building a customer-centered business from marketing right through to sales, customer service and business analysis. The Salesforce plugin allows you to search, update, and manage salesforce records. This plugin utilizes the [Salesforce API](https://developer.salesforce.com/docs/atlas.en-us.216.0.api_rest.meta/api_rest/intro_what_is_rest_api.htm)"
7-
version: 2.1.11
7+
version: 2.1.12
88
connection_version: 2
99
vendor: rapid7
1010
support: community
@@ -13,7 +13,7 @@ status: []
1313
supported_versions: ["Salesforce API v58 2023-06-30"]
1414
sdk:
1515
type: full
16-
version: 6.1.4
16+
version: 6.2.0
1717
user: nobody
1818
resources:
1919
source_url: https://github.com/rapid7/insightconnect-plugins/tree/master/plugins/salesforce
@@ -37,6 +37,7 @@ references:
3737
- "[Connecting your app to the API](https://developer.salesforce.com/docs/atlas.en-us.216.0.api_rest.meta/api_rest/quickstart.htm)"
3838
- "[SOQL](https://developer.salesforce.com/docs/atlas.en-us.216.0.soql_sosl.meta/soql_sosl/sforce_api_calls_soql.htm)"
3939
version_history:
40+
- "2.1.12 - Task Monitor Users: ensure datetime includes microseconds | Bump SDK to 6.2.0"
4041
- "2.1.11 - Task Monitor Users: Return 500 for retry your request error | Bump SDK to 6.1.4"
4142
- "2.1.10 - Set Monitor Users task output length | Fix to remove whitespace from connection inputs"
4243
- "2.1.9 - SDK Bump to 6.1.0 | Task Connection test added"

plugins/salesforce/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44

55
setup(name="salesforce-rapid7-plugin",
6-
version="2.1.11",
6+
version="2.1.12",
77
description="[Salesforce](https://www.salesforce.com) is a CRM solution that brings together all customer information in a single, integrated platform that enables building a customer-centered business from marketing right through to sales, customer service and business analysis. The Salesforce plugin allows you to search, update, and manage salesforce records. This plugin utilizes the [Salesforce API](https://developer.salesforce.com/docs/atlas.en-us.216.0.api_rest.meta/api_rest/intro_what_is_rest_api.htm)",
88
author="rapid7",
99
author_email="",

plugins/salesforce/unit_test/expected/monitor_users_bad_request.json.exp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"state": {
3-
"last_user_update_collection_timestamp": "2023-07-20 16:21:15.340262+00:00",
3+
"last_user_update_collection_timestamp": "invalid",
44
"next_user_collection_timestamp": "2023-07-20 16:21:15.340262+00:00",
55
"next_user_login_collection_timestamp": "2023-07-20 16:21:15.340262+00:00",
66
"last_user_login_collection_timestamp": "2023-07-20 15:21:15.340262+00:00"

0 commit comments

Comments
 (0)