Skip to content

Commit 9a63802

Browse files
authored
Merge pull request #36 from passivetotal/mock-requests
Mock requests & recent articles
2 parents 384f0c7 + 57e7a6d commit 9a63802

6 files changed

Lines changed: 88 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
# Changelog
22

3+
## v2.5.3
4+
5+
#### Enhancements
6+
7+
- Better support for unit tests in client libraries with ability to set a
8+
session to override default request methods.
9+
- Add flexibility to library class instantiation to prefer keyword parameters
10+
over config file keys.
11+
- Support for new `create_date` Articles API data field and query parameter. Enables
12+
searching for most recent articles instead of returning all of them at once, and
13+
provides visiblity to situations where an article published in the past was recently
14+
added to the Articles collection.
15+
16+
17+
#### Breaking Changes
18+
19+
- Previously, calls to `analyzer.AllArticles()` would return all articles without a date
20+
limit. Now, it will return only articles created after the starting date set with
21+
`analyzer.set_date_range()`. The current module-level default for all date-bounded queries
22+
is 90 days back, so now this function will return all articles created in the last 90 days.
23+
- `age` property of an Article analyzer object is now based on `create_date` instead of publish
24+
date.
25+
26+
27+
#### Bug Fixes
28+
29+
[ none ]
30+
31+
32+
333
## v2.5.2
434

535
#### Enhancements

passivetotal/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION="2.5.2"
1+
VERSION="2.5.3"

passivetotal/analyzer/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def init(**kwargs):
5656
if 'username' in kwargs and 'api_key' in kwargs:
5757
api_clients[name] = c(**kwargs)
5858
else:
59-
api_clients[name] = c.from_config()
59+
api_clients[name] = c.from_config(**kwargs)
6060
api_clients[name].exception_class = AnalyzerAPIError
6161
api_clients[name].set_context('python','passivetotal',VERSION,'analyzer')
6262
config['is_ready'] = True

passivetotal/analyzer/_common.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,13 @@ def __init__(self, response):
429429
self.json = self.response.json()
430430
except Exception:
431431
self.json = {}
432-
self.message = self.json.get('error', self.json.get('message', str(response)))
432+
if self.json is None:
433+
self.message = 'No JSON data in API response'
434+
else:
435+
try:
436+
self.message = self.json.get('error', self.json.get('message', str(response)))
437+
except Exception:
438+
self.message = ''
433439

434440
def __str__(self):
435441
return 'Error #{0.status_code} "{0.message}" ({0.url})'.format(self)

passivetotal/analyzer/articles.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from passivetotal.analyzer._common import (
44
RecordList, Record, ForPandas
55
)
6-
from passivetotal.analyzer import get_api
6+
from passivetotal.analyzer import get_api, get_config
77

88

99

@@ -71,20 +71,28 @@ class AllArticles(ArticlesList):
7171
By default, instantiating the class will automatically load the entire list
7272
of threat intelligence articles. Pass autoload=False to the constructor to disable
7373
this functionality.
74+
75+
Only articles created after the start date specified in the analyzer.set_date_range()
76+
method will be returned unless a different created_after parameter is supplied to the object
77+
constructor.
7478
"""
7579

76-
def __init__(self, autoload = True):
80+
def __init__(self, created_after=None, autoload=True):
7781
"""Initialize a list of articles; will autoload by default.
78-
7982
:param autoload: whether to automatically load articles upon instantiation (defaults to true)
8083
"""
8184
super().__init__()
8285
if autoload:
83-
self.load()
86+
self.load(created_after)
8487

85-
def load(self):
86-
"""Query the API for articles and load them into an articles list."""
87-
response = get_api('Articles').get_articles()
88+
def load(self, created_after=None):
89+
"""Query the API for articles and load them into an articles list.
90+
91+
:param created_after: only return articles created after this date (optional, defaults to date set by `analyzer.set_date_range()`
92+
"""
93+
if created_after is None:
94+
created_after = get_config('start_date')
95+
response = get_api('Articles').get_articles(createdAfter=created_after)
8896
self.parse(response)
8997

9098

@@ -98,6 +106,7 @@ def __init__(self, api_response, query=None):
98106
self._summary = api_response.get('summary')
99107
self._type = api_response.get('type')
100108
self._publishdate = api_response.get('publishedDate')
109+
self._createdate = api_response.get('createdDate')
101110
self._link = api_response.get('link')
102111
self._categories = api_response.get('categories')
103112
self._tags = api_response.get('tags')
@@ -115,13 +124,14 @@ def _api_get_details(self):
115124
response = get_api('Articles').get_details(self._guid)
116125
self._summary = response.get('summary')
117126
self._publishdate = response.get('publishedDate')
127+
self._createdate = response.get('createdDate')
118128
self._tags = response.get('tags')
119129
self._categories = response.get('categories')
120130
self._indicators = response.get('indicators')
121131

122132
def _get_dict_fields(self):
123-
return ['guid','title','type','summary','str:date_published','age',
124-
'link','categories','tags','indicators','indicator_count',
133+
return ['guid','title','type','summary','str:date_published','str:date_created',
134+
'age', 'link','categories','tags','indicators','indicator_count',
125135
'indicator_types','str:ips','str:hostnames']
126136

127137
def _ensure_details(self):
@@ -161,6 +171,7 @@ def to_dataframe(self, ensure_details=True, include_indicators=False):
161171
title = self._title,
162172
type = self._type,
163173
date_published = self._publishdate,
174+
date_created = self._createdate,
164175
summary = self._summary,
165176
link = self._link,
166177
categories = self._categories,
@@ -228,11 +239,18 @@ def date_published(self):
228239
date = datetime.fromisoformat(self._publishdate)
229240
return date
230241

242+
@property
243+
def date_created(self):
244+
"""Date the article was created in the RiskIQ database."""
245+
self._ensure_details()
246+
date = datetime.fromisoformat(self._createdate)
247+
return date
248+
231249
@property
232250
def age(self):
233-
"""Age of the article in days."""
251+
"""Age of the article in days, measured from create date."""
234252
now = datetime.now(timezone.utc)
235-
interval = now - self.date_published
253+
interval = now - self.date_created
236254
return interval.days
237255

238256
@property

passivetotal/api.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class Client(object):
2424

2525
def __init__(self, username, api_key, server=DEFAULT_SERVER,
2626
version=DEFAULT_VERSION, http_proxy=None, https_proxy=None,
27-
verify=True, headers=None, debug=False, exception_class=Exception):
27+
verify=True, headers=None, debug=False, exception_class=Exception,
28+
session=None):
2829
"""Initial loading of the client.
2930
3031
:param str username: API username in email address format
@@ -63,18 +64,25 @@ def __init__(self, username, api_key, server=DEFAULT_SERVER,
6364
self.verify = False
6465
self.exception_class = exception_class
6566
self.set_context('python','passivetotal',VERSION)
67+
self.session = session or requests.Session()
6668

6769
@classmethod
68-
def from_config(cls):
69-
"""Method to return back a loaded instance."""
70+
def from_config(cls, **kwargs):
71+
"""Method to return back a loaded instance.
72+
73+
kwargs override configuration file variables if provided and are passed to the object constructor.
74+
"""
75+
arg_keys = ['username','api_key','server','version','http_proxy','https_proxy']
76+
args = { k: kwargs.pop(k) if k in kwargs else None for k in arg_keys }
7077
config = Config()
7178
client = cls(
72-
username=config.get('username'),
73-
api_key=config.get('api_key'),
74-
server=config.get('api_server'),
75-
version=config.get('api_version'),
76-
http_proxy=config.get('http_proxy'),
77-
https_proxy=config.get('https_proxy'),
79+
username = args.get('username') or config.get('username'),
80+
api_key = args.get('api_key') or config.get('api_key'),
81+
server = args.get('server') or config.get('api_server'),
82+
version = args.get('version') or config.get('api_version'),
83+
http_proxy = args.get('http_proxy') or config.get('http_proxy'),
84+
https_proxy = args.get('https_proxy') or config.get('https_proxy'),
85+
**kwargs
7886
)
7987
return client
8088

@@ -155,7 +163,7 @@ def _get(self, endpoint, action, *url_args, **url_params):
155163
if self.proxies:
156164
kwargs['proxies'] = self.proxies
157165
self.logger.debug("Requesting: %s, %s" % (api_url, str(kwargs)))
158-
response = requests.get(api_url, **kwargs)
166+
response = self.session.get(api_url, **kwargs)
159167
return self._json(response)
160168

161169
def _get_special(self, endpoint, action, trail, data, *url_args, **url_params):
@@ -175,7 +183,7 @@ def _get_special(self, endpoint, action, trail, data, *url_args, **url_params):
175183
'auth': (self.username, self.api_key)}
176184
if self.proxies:
177185
kwargs['proxies'] = self.proxies
178-
response = requests.get(api_url, **kwargs)
186+
response = self.session.get(api_url, **kwargs)
179187
return self._json(response)
180188

181189
def _send_data(self, method, endpoint, action,
@@ -196,7 +204,7 @@ def _send_data(self, method, endpoint, action,
196204
'auth': (self.username, self.api_key)}
197205
if self.proxies:
198206
kwargs['proxies'] = self.proxies
199-
response = requests.request(method, api_url, **kwargs)
207+
response = self.session.request(method, api_url, **kwargs)
200208
return self._json(response)
201209

202210

0 commit comments

Comments
 (0)