Skip to content

Commit 37be0a4

Browse files
RosenbergYehudaContent Bot
andauthored
Add a manual fatch once in 12 hours (demisto#31123)
* fixes * http module * CSV * common server * tests * RN * link * RN * change RN * one more * pre commit * update base version * [known_words] * removing typing * swap the known words * RN * fix RN * Bump pack from version FeedMalwareBazaar to 1.0.30. * Bump pack from version AccentureCTI_Feed to 1.1.27. * Bump pack from version FeedGCPWhitelist to 2.0.30. * Bump pack from version Base to 1.32.52. * make it better * docs * CR * cr * Fixing dirty merge #1 * fixing dirty merge #2 * fix dirty merge #3 * more * fox dirty merge #4 * common * poetry * fix dirty merge #5 * fix test date * base rn * RN * fix common docstring * fix rn * fix errors in build * shirley * Bump pack from version Base to 1.32.54. * RN * mypy * fix common server * ignore type error * skip test * fix test name * add import * remove the import, test is failing * fixed function and test * space * conf * add a test for a uniq time zone * fix test * move the import into the function * move the import from the test as well * replace timezone with pytz, to fit python 2 * Bump pack from version Base to 1.33.1. * fix test comment --------- Co-authored-by: Content Bot <[email protected]>
1 parent 9e7db2f commit 37be0a4

File tree

60 files changed

+405
-62
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+405
-62
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
#### Integrations
3+
4+
##### ACTI Indicator Feed
5+
6+
Fixed an issue where indicators were expiring due to prolonged periods of inactivity in the data source by implementing a solution that enforces a bi-daily update for existing indicators, even if the corresponding resource hasn't been updated.

Packs/AccentureCTI_Feed/pack_metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "Accenture CTI Feed",
33
"description": "Accenture Cyber Threat Intelligence Feed",
44
"support": "partner",
5-
"currentVersion": "1.1.27",
5+
"currentVersion": "1.1.28",
66
"author": "Accenture",
77
"url": "https://www.accenture.com/us-en/services/security/cyber-defense",
88
"email": "[email protected]",

Packs/ApiModules/Scripts/CSVFeedApiModule/CSVFeedApiModule.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
# Globals
1515
DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
16+
THRESHOLD_IN_SECONDS = 43200 # 12 hours in seconds
1617

1718

1819
class Client(BaseClient):
@@ -140,6 +141,15 @@ def build_iterator(self, **kwargs):
140141
last_run = demisto.getLastRun()
141142
etag = last_run.get(url, {}).get('etag')
142143
last_modified = last_run.get(url, {}).get('last_modified')
144+
last_updated = last_run.get(url, {}).get('last_updated')
145+
# To avoid issues with indicators expiring, if 'last_updated' is over X hours old,
146+
# we'll refresh the indicators to ensure their expiration time is updated.
147+
# For further details, refer to : https://confluence-dc.paloaltonetworks.com/display/DemistoContent/Json+Api+Module # noqa: E501
148+
if last_updated and has_passed_time_threshold(timestamp_str=last_updated, seconds_threshold=THRESHOLD_IN_SECONDS):
149+
last_modified = None
150+
etag = None
151+
demisto.debug("Since it's been a long time with no update, to make sure we are keeping the indicators alive, \
152+
we will refetch them from scratch")
143153

144154
if etag:
145155
self.headers['If-None-Match'] = etag
@@ -250,14 +260,17 @@ def get_no_update_value(response: requests.models.Response, url: str) -> bool:
250260

251261
etag = response.headers.get('ETag')
252262
last_modified = response.headers.get('Last-Modified')
263+
current_time = datetime.utcnow()
264+
# Save the current time as the last updated time. This will be used to indicate the last time the feed was updated in XSOAR.
265+
last_updated = current_time.strftime(DATE_FORMAT)
253266

254267
if not etag and not last_modified:
255268
demisto.debug('Last-Modified and Etag headers are not exists,'
256269
'createIndicators will be executed with noUpdate=False.')
257270
return False
258271

259272
last_run = demisto.getLastRun()
260-
last_run[url] = {'last_modified': last_modified, 'etag': etag}
273+
last_run[url] = {'last_modified': last_modified, 'etag': etag, 'last_updated': last_updated}
261274
demisto.setLastRun(last_run)
262275

263276
demisto.debug('New indicators fetched - the Last-Modified value has been updated,'
@@ -344,7 +357,7 @@ def fetch_indicators_command(client: Client, default_indicator_type: str, auto_d
344357
config = client.feed_url_to_config or {}
345358

346359
# set noUpdate flag in createIndicators command True only when all the results from all the urls are True.
347-
no_update = all([next(iter(item.values())).get('no_update', False) for item in iterator])
360+
no_update = all(next(iter(item.values())).get('no_update', False) for item in iterator)
348361

349362
for url_to_reader in iterator:
350363
for url, reader in url_to_reader.items():

Packs/ApiModules/Scripts/CSVFeedApiModule/CSVFeedApiModule_test.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import requests_mock
22
from CSVFeedApiModule import *
3-
import io
43
import pytest
54

65

@@ -243,7 +242,7 @@ def test_tags_not_exists(self):
243242

244243

245244
def util_load_json(path):
246-
with io.open(path, mode='r', encoding='utf-8') as f:
245+
with open(path, 'r', encoding='utf-8') as f:
247246
return json.loads(f.read())
248247

249248

@@ -504,3 +503,36 @@ def test_build_iterator_modified_headers(mocker):
504503
result = client.build_iterator()
505504
assert 'Authorization' in mock_session.call_args[0][0].headers
506505
assert result
506+
507+
508+
@pytest.mark.parametrize('has_passed_time_threshold_response, expected_result', [
509+
(True, {}),
510+
(False, {'If-None-Match': 'etag', 'If-Modified-Since': '2023-05-29T12:34:56Z'})
511+
])
512+
def test_build_iterator__with_and_without_passed_time_threshold(mocker, has_passed_time_threshold_response, expected_result):
513+
"""
514+
Given
515+
- A boolean result from the has_passed_time_threshold function
516+
When
517+
- Running build_iterator method.
518+
Then
519+
- Ensure the next request headers will be as expected:
520+
case 1: has_passed_time_threshold_response is True, no headers will be added
521+
case 2: has_passed_time_threshold_response is False, headers containing 'last_modified' and 'etag' will be added
522+
"""
523+
mocker.patch('CommonServerPython.get_demisto_version', return_value={"version": "6.5.0"})
524+
mock_session = mocker.patch.object(requests.Session, 'send')
525+
mocker.patch('CSVFeedApiModule.has_passed_time_threshold', return_value=has_passed_time_threshold_response)
526+
mocker.patch('demistomock.getLastRun', return_value={
527+
'https://api.github.com/meta': {
528+
'etag': 'etag',
529+
'last_modified': '2023-05-29T12:34:56Z',
530+
'last_updated': '2023-05-05T09:09:06Z'
531+
}})
532+
client = Client(
533+
url='https://api.github.com/meta',
534+
credentials={'identifier': 'user', 'password': 'password'})
535+
536+
client.build_iterator()
537+
assert mock_session.call_args[0][0].headers.get('If-None-Match') == expected_result.get('If-None-Match')
538+
assert mock_session.call_args[0][0].headers.get('If-Modified-Since') == expected_result.get('If-Modified-Since')

Packs/ApiModules/Scripts/HTTPFeedApiModule/HTTPFeedApiModule.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
TAGS = 'tags'
1515
TLP_COLOR = 'trafficlightprotocol'
1616
DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
17+
THRESHOLD_IN_SECONDS = 43200 # 12 hours in seconds
1718

1819

1920
class Client(BaseClient):
@@ -216,6 +217,16 @@ def build_iterator(self, **kwargs):
216217
last_run = demisto.getLastRun()
217218
etag = last_run.get(url, {}).get('etag')
218219
last_modified = last_run.get(url, {}).get('last_modified')
220+
last_updated = last_run.get(url, {}).get('last_updated')
221+
# To avoid issues with indicators expiring, if 'last_updated' is over X hours old,
222+
# we'll refresh the indicators to ensure their expiration time is updated.
223+
# For further details, refer to : https://confluence-dc.paloaltonetworks.com/display/DemistoContent/Json+Api+Module # noqa: E501
224+
if last_updated and has_passed_time_threshold(timestamp_str=last_updated,
225+
seconds_threshold=THRESHOLD_IN_SECONDS):
226+
last_modified = None
227+
etag = None
228+
demisto.debug("Since it's been a long time with no update, to make sure we are keeping the indicators\
229+
alive, we will refetch them from scratch")
219230
if etag:
220231
if not kwargs.get('headers'):
221232
kwargs['headers'] = {}
@@ -269,15 +280,9 @@ def build_iterator(self, **kwargs):
269280
lines = res_data.get('response')
270281
result = lines.iter_lines()
271282
if self.encoding is not None:
272-
result = map(
273-
lambda x: x.decode(self.encoding).encode('utf_8'),
274-
result
275-
)
283+
result = (x.decode(self.encoding).encode('utf_8') for x in result)
276284
else:
277-
result = map(
278-
lambda x: x.decode('utf_8'),
279-
result
280-
)
285+
result = (x.decode('utf_8') for x in result)
281286
if self.ignore_regex is not None:
282287
result = filter(
283288
lambda x: self.ignore_regex.match(x) is None, # type: ignore[union-attr, arg-type]
@@ -288,8 +293,8 @@ def build_iterator(self, **kwargs):
288293

289294
def custom_fields_creator(self, attributes: dict):
290295
created_custom_fields = {}
291-
for attribute in attributes.keys():
292-
if attribute in self.custom_fields_mapping.keys() or attribute in [TAGS, TLP_COLOR]:
296+
for attribute in attributes:
297+
if attribute in self.custom_fields_mapping or attribute in [TAGS, TLP_COLOR]:
293298
if attribute in [TAGS, TLP_COLOR]:
294299
created_custom_fields[attribute] = attributes[attribute]
295300
else:
@@ -317,14 +322,17 @@ def get_no_update_value(response: requests.Response, url: str) -> bool:
317322

318323
etag = response.headers.get('ETag')
319324
last_modified = response.headers.get('Last-Modified')
325+
current_time = datetime.utcnow()
326+
# Save the current time as the last updated time. This will be used to indicate the last time the feed was updated in XSOAR.
327+
last_updated = current_time.strftime(DATE_FORMAT)
320328

321329
if not etag and not last_modified:
322330
demisto.debug('Last-Modified and Etag headers are not exists,'
323331
'createIndicators will be executed with noUpdate=False.')
324332
return False
325333

326334
last_run = demisto.getLastRun()
327-
last_run[url] = {'last_modified': last_modified, 'etag': etag}
335+
last_run[url] = {'last_modified': last_modified, 'etag': etag, 'last_updated': last_updated}
328336
demisto.setLastRun(last_run)
329337

330338
demisto.debug('New indicators fetched - the Last-Modified value has been updated,'
@@ -357,13 +365,12 @@ def get_indicator_fields(line, url, feed_tags: list, tlp_color: Optional[str], c
357365
indicator = None
358366
fields_to_extract = []
359367
feed_config = client.feed_url_to_config.get(url, {})
360-
if feed_config:
361-
if 'indicator' in feed_config:
362-
indicator = feed_config['indicator']
363-
if 'regex' in indicator:
364-
indicator['regex'] = re.compile(indicator['regex'])
365-
if 'transform' not in indicator:
366-
indicator['transform'] = r'\g<0>'
368+
if feed_config and 'indicator' in feed_config:
369+
indicator = feed_config['indicator']
370+
if 'regex' in indicator:
371+
indicator['regex'] = re.compile(indicator['regex'])
372+
if 'transform' not in indicator:
373+
indicator['transform'] = r'\g<0>'
367374

368375
if 'fields' in feed_config:
369376
fields = feed_config['fields']
@@ -418,17 +425,17 @@ def fetch_indicators_command(client, feed_tags, tlp_color, itype, auto_detect, c
418425
indicators = []
419426

420427
# set noUpdate flag in createIndicators command True only when all the results from all the urls are True.
421-
no_update = all([next(iter(iterator.values())).get('no_update', False) for iterator in iterators])
428+
no_update = all(next(iter(iterator.values())).get('no_update', False) for iterator in iterators)
422429

423430
for iterator in iterators:
424431
for url, lines in iterator.items():
425432
for line in lines.get('result', []):
426433
attributes, value = get_indicator_fields(line, url, feed_tags, tlp_color, client)
427434
if value:
428-
if 'lastseenbysource' in attributes.keys():
435+
if 'lastseenbysource' in attributes:
429436
attributes['lastseenbysource'] = datestring_to_server_format(attributes['lastseenbysource'])
430437

431-
if 'firstseenbysource' in attributes.keys():
438+
if 'firstseenbysource' in attributes:
432439
attributes['firstseenbysource'] = datestring_to_server_format(attributes['firstseenbysource'])
433440
indicator_type = determine_indicator_type(
434441
client.feed_url_to_config.get(url, {}).get('indicator_type'), itype, auto_detect, value)
@@ -450,7 +457,7 @@ def fetch_indicators_command(client, feed_tags, tlp_color, itype, auto_detect, c
450457
relationships_of_indicator = [relationships_lst.to_indicator()]
451458
indicator_data['relationships'] = relationships_of_indicator
452459

453-
if len(client.custom_fields_mapping.keys()) > 0 or TAGS in attributes.keys():
460+
if len(client.custom_fields_mapping.keys()) > 0 or TAGS in attributes:
454461
custom_fields = client.custom_fields_creator(attributes)
455462
indicator_data["fields"] = custom_fields
456463

Packs/ApiModules/Scripts/HTTPFeedApiModule/HTTPFeedApiModule_test.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
fetch_indicators_command, get_no_update_value
33
import requests_mock
44
import demistomock as demisto
5+
import pytest
6+
import requests
57

68

79
def test_get_indicators():
@@ -145,12 +147,12 @@ def test_datestring_to_server_format():
145147
datestring4 = "2020-02-10T13:39:14.123"
146148
datestring5 = "2020-02-10T13:39:14Z"
147149
datestring6 = "2020-11-01T04:16:13-04:00"
148-
assert '2020-02-10T13:39:14Z' == datestring_to_server_format(datestring1)
149-
assert '2020-02-10T13:39:14Z' == datestring_to_server_format(datestring2)
150-
assert '2020-02-10T13:39:14Z' == datestring_to_server_format(datestring3)
151-
assert '2020-02-10T13:39:14Z' == datestring_to_server_format(datestring4)
152-
assert '2020-02-10T13:39:14Z' == datestring_to_server_format(datestring5)
153-
assert '2020-11-01T08:16:13Z' == datestring_to_server_format(datestring6)
150+
assert datestring_to_server_format(datestring1) == '2020-02-10T13:39:14Z'
151+
assert datestring_to_server_format(datestring2) == '2020-02-10T13:39:14Z'
152+
assert datestring_to_server_format(datestring3) == '2020-02-10T13:39:14Z'
153+
assert datestring_to_server_format(datestring4) == '2020-02-10T13:39:14Z'
154+
assert datestring_to_server_format(datestring5) == '2020-02-10T13:39:14Z'
155+
assert datestring_to_server_format(datestring6) == '2020-11-01T08:16:13Z'
154156

155157

156158
def test_get_feed_config():
@@ -544,3 +546,35 @@ class MockResponse:
544546
assert not no_update
545547
assert demisto.debug.call_args[0][0] == 'Last-Modified and Etag headers are not exists,' \
546548
'createIndicators will be executed with noUpdate=False.'
549+
550+
551+
@pytest.mark.parametrize('has_passed_time_threshold_response, expected_result', [
552+
(True, None),
553+
(False, {'If-None-Match': 'etag', 'If-Modified-Since': '2023-05-29T12:34:56Z'})
554+
])
555+
def test_build_iterator__with_and_without_passed_time_threshold(mocker, has_passed_time_threshold_response, expected_result):
556+
"""
557+
Given
558+
- A boolean result from the has_passed_time_threshold function
559+
When
560+
- Running build_iterator method.
561+
Then
562+
- Ensure the next request headers will be as expected:
563+
case 1: has_passed_time_threshold_response is True, no headers will be added
564+
case 2: has_passed_time_threshold_response is False, headers containing 'last_modified' and 'etag' will be added
565+
"""
566+
mocker.patch('CommonServerPython.get_demisto_version', return_value={"version": "6.5.0"})
567+
mock_session = mocker.patch.object(requests, 'get')
568+
mocker.patch('HTTPFeedApiModule.has_passed_time_threshold', return_value=has_passed_time_threshold_response)
569+
mocker.patch('demistomock.getLastRun', return_value={
570+
'https://api.github.com/meta': {
571+
'etag': 'etag',
572+
'last_modified': '2023-05-29T12:34:56Z',
573+
'last_updated': '2023-05-05T09:09:06Z'
574+
}})
575+
client = Client(
576+
url='https://api.github.com/meta',
577+
credentials={'identifier': 'user', 'password': 'password'})
578+
579+
client.build_iterator()
580+
assert mock_session.call_args[1].get('headers') == expected_result

Packs/ApiModules/Scripts/JSONFeedApiModule/JSONFeedApiModule.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
# disable insecure warnings
1010
urllib3.disable_warnings()
1111

12+
DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
13+
THRESHOLD_IN_SECONDS = 43200 # 12 hours in seconds
14+
1215

1316
class Client:
1417
def __init__(self, url: str = '', credentials: dict = None,
@@ -81,7 +84,7 @@ def __init__(self, url: str = '', credentials: dict = None,
8184

8285
if isinstance(self.post_data, str):
8386
content_type_header = 'Content-Type'
84-
if content_type_header.lower() not in [k.lower() for k in self.headers.keys()]:
87+
if content_type_header.lower() not in [k.lower() for k in self.headers]:
8588
self.headers[content_type_header] = 'application/x-www-form-urlencoded'
8689

8790
@staticmethod
@@ -118,6 +121,15 @@ def build_iterator(self, feed: dict, feed_name: str, **kwargs) -> Tuple[List, bo
118121
last_run = demisto.getLastRun()
119122
etag = last_run.get(prefix_feed_name, {}).get('etag') or last_run.get(feed_name, {}).get('etag')
120123
last_modified = last_run.get(prefix_feed_name, {}).get('last_modified') or last_run.get(feed_name, {}).get('last_modified') # noqa: E501
124+
last_updated = last_run.get(prefix_feed_name, {}).get('last_updated') or last_run.get(feed_name, {}).get('last_updated') # noqa: E501
125+
# To avoid issues with indicators expiring, if 'last_updated' is over X hours old,
126+
# we'll refresh the indicators to ensure their expiration time is updated.
127+
# For further details, refer to : https://confluence-dc.paloaltonetworks.com/display/DemistoContent/Json+Api+Module
128+
if last_updated and has_passed_time_threshold(timestamp_str=last_updated, seconds_threshold=THRESHOLD_IN_SECONDS):
129+
last_modified = None
130+
etag = None
131+
demisto.debug("Since it's been a long time with no update, to make sure we are keeping the indicators alive, \
132+
we will refetch them from scratch")
121133

122134
if etag:
123135
self.headers['If-None-Match'] = etag
@@ -180,6 +192,9 @@ def get_no_update_value(response: requests.Response, feed_name: str) -> bool:
180192

181193
etag = response.headers.get('ETag')
182194
last_modified = response.headers.get('Last-Modified')
195+
current_time = datetime.utcnow()
196+
# Save the current time as the last updated time. This will be used to indicate the last time the feed was updated in XSOAR.
197+
last_updated = current_time.strftime(DATE_FORMAT)
183198

184199
if not etag and not last_modified:
185200
demisto.debug('Last-Modified and Etag headers are not exists,'
@@ -189,7 +204,8 @@ def get_no_update_value(response: requests.Response, feed_name: str) -> bool:
189204
last_run = demisto.getLastRun()
190205
last_run[feed_name] = {
191206
'last_modified': last_modified,
192-
'etag': etag
207+
'etag': etag,
208+
'last_updated': last_updated
193209
}
194210
demisto.setLastRun(last_run)
195211
demisto.debug(f'JSON: The new last run is: {last_run}')

0 commit comments

Comments
 (0)