greynoise 2.0.1__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {greynoise-2.0.1 → greynoise-2.2.0}/CHANGELOG.rst +51 -1
  2. {greynoise-2.0.1/src/greynoise.egg-info → greynoise-2.2.0}/PKG-INFO +52 -2
  3. {greynoise-2.0.1 → greynoise-2.2.0}/setup.py +2 -3
  4. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/__version__.py +1 -1
  5. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/api/__init__.py +228 -12
  6. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/decorator.py +123 -0
  7. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/formatter.py +29 -2
  8. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/subcommand.py +74 -0
  9. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/analyze.txt.j2 +29 -1
  10. greynoise-2.2.0/src/greynoise/cli/templates/personadetails.txt.j2 +38 -0
  11. greynoise-2.2.0/src/greynoise/cli/templates/sensoractivity.txt.j2 +25 -0
  12. greynoise-2.2.0/src/greynoise/cli/templates/sensorlist.txt.j2 +30 -0
  13. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/stats.txt.j2 +2 -2
  14. {greynoise-2.0.1 → greynoise-2.2.0/src/greynoise.egg-info}/PKG-INFO +52 -2
  15. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise.egg-info/SOURCES.txt +6 -2
  16. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise.egg-info/requires.txt +1 -2
  17. greynoise-2.2.0/tests/test_api.py +819 -0
  18. greynoise-2.2.0/tests/test_util.py +426 -0
  19. greynoise-2.0.1/src/greynoise/api/analyzer.py +0 -158
  20. {greynoise-2.0.1 → greynoise-2.2.0}/LICENSE +0 -0
  21. {greynoise-2.0.1 → greynoise-2.2.0}/MANIFEST.in +0 -0
  22. {greynoise-2.0.1 → greynoise-2.2.0}/README.rst +0 -0
  23. {greynoise-2.0.1 → greynoise-2.2.0}/setup.cfg +0 -0
  24. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/__init__.py +0 -0
  25. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/api/filter.py +0 -0
  26. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/__init__.py +0 -0
  27. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/helper.py +0 -0
  28. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/parameter.py +0 -0
  29. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/gnql_query.txt.j2 +0 -0
  30. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/gnql_stats.txt.j2 +0 -0
  31. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/interesting.txt.j2 +0 -0
  32. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/ip_community.txt.j2 +0 -0
  33. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/ip_context.txt.j2 +0 -0
  34. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/ip_context_result.txt.j2 +0 -0
  35. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/ip_multi_context.txt.j2 +0 -0
  36. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/ip_quick_check.txt.j2 +0 -0
  37. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/macros.txt.j2 +0 -0
  38. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/riot.txt.j2 +0 -0
  39. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/similarity.txt.j2 +0 -0
  40. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/timeline.txt.j2 +0 -0
  41. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/cli/templates/timelinehourly.txt.j2 +0 -0
  42. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/exceptions.py +0 -0
  43. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise/util.py +0 -0
  44. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise.egg-info/dependency_links.txt +0 -0
  45. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise.egg-info/entry_points.txt +0 -0
  46. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise.egg-info/not-zip-safe +0 -0
  47. {greynoise-2.0.1 → greynoise-2.2.0}/src/greynoise.egg-info/top_level.txt +0 -0
@@ -2,6 +2,54 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version `2.2.0`_
6
+ ================
7
+ **Date**: June 11, 2024
8
+
9
+ * API client:
10
+
11
+ * Update `analyze` command to use analysis endpoint
12
+ * Add `persona_details` command to pull a sensor persona's details based on ID
13
+ * Add `sensor_list` command to pull list of sensors from a users workspace, based on API key used
14
+ * Add `sensor_activity` command to pull sensor activity feed from users workspace
15
+ * Add `sensor_activity_ips` command to pull list of source ips from sensor activity feed
16
+
17
+ * CLI:
18
+
19
+ * Update `analyze` output to match that of new endpoint response
20
+ * Add `persona-details` command to display command response
21
+ * Add `sensor-list` command to display command response
22
+ * Add `sensor-activity` command to display command response
23
+
24
+ * Dependencies:
25
+
26
+ * Updated Clic to version 8.1.7
27
+ * Updated ansimarkup to version 2.1.0
28
+ * Updated click-repl to version 0.3.0
29
+ * Updated dict2xml to version 1.7.5
30
+ * Updated jinja2 to version 3.1.4
31
+ * Updated more-itertools to version 10.3.0 (for py3.8 and above only)
32
+ * Updated requests to version 2.32.3
33
+
34
+ Version `2.1.0`_
35
+ ================
36
+ **Date**: February 07, 2024
37
+
38
+ * API client:
39
+
40
+ * None
41
+
42
+ * CLI:
43
+
44
+ * Fixed stats command display to show correct countries
45
+ * Fixed similar command to handle non-noise IP response
46
+
47
+ * Dependencies:
48
+
49
+ * Updated requests to version 2.31.0
50
+ * Updated cachetools to version 5.3.1
51
+ * Removed ipaddress as a required package
52
+
5
53
  Version `2.0.1`_
6
54
  ================
7
55
  **Date**: April 14, 2023
@@ -361,5 +409,7 @@ Version `0.2.0`_
361
409
  .. _`1.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.1.0...1.2.0
362
410
  .. _`1.2.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.2.0...1.2.1
363
411
  .. _`1.3.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.2.1...1.3.0
364
- .. _`2.0.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.3.0...HEAD
412
+ .. _`2.0.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.3.0...2.0.0
365
413
  .. _`2.0.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.0...2.0.1
414
+ .. _`2.1.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.1...2.1.0
415
+ .. _`2.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.1.0...HEAD
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: greynoise
3
- Version: 2.0.1
3
+ Version: 2.2.0
4
4
  Summary: Abstraction to interact with GreyNoise API.
5
5
  Home-page: https://greynoise.io/
6
6
  Download-URL: https://github.com/GreyNoise-Intelligence/pygreynoise
@@ -124,6 +124,54 @@ Usage
124
124
  Changelog
125
125
  =========
126
126
 
127
+ Version `2.2.0`_
128
+ ================
129
+ **Date**: June 11, 2024
130
+
131
+ * API client:
132
+
133
+ * Update `analyze` command to use analysis endpoint
134
+ * Add `persona_details` command to pull a sensor persona's details based on ID
135
+ * Add `sensor_list` command to pull list of sensors from a users workspace, based on API key used
136
+ * Add `sensor_activity` command to pull sensor activity feed from users workspace
137
+ * Add `sensor_activity_ips` command to pull list of source ips from sensor activity feed
138
+
139
+ * CLI:
140
+
141
+ * Update `analyze` output to match that of new endpoint response
142
+ * Add `persona-details` command to display command response
143
+ * Add `sensor-list` command to display command response
144
+ * Add `sensor-activity` command to display command response
145
+
146
+ * Dependencies:
147
+
148
+ * Updated Clic to version 8.1.7
149
+ * Updated ansimarkup to version 2.1.0
150
+ * Updated click-repl to version 0.3.0
151
+ * Updated dict2xml to version 1.7.5
152
+ * Updated jinja2 to version 3.1.4
153
+ * Updated more-itertools to version 10.3.0 (for py3.8 and above only)
154
+ * Updated requests to version 2.32.3
155
+
156
+ Version `2.1.0`_
157
+ ================
158
+ **Date**: February 07, 2024
159
+
160
+ * API client:
161
+
162
+ * None
163
+
164
+ * CLI:
165
+
166
+ * Fixed stats command display to show correct countries
167
+ * Fixed similar command to handle non-noise IP response
168
+
169
+ * Dependencies:
170
+
171
+ * Updated requests to version 2.31.0
172
+ * Updated cachetools to version 5.3.1
173
+ * Removed ipaddress as a required package
174
+
127
175
  Version `2.0.1`_
128
176
  ================
129
177
  **Date**: April 14, 2023
@@ -483,5 +531,7 @@ Version `0.2.0`_
483
531
  .. _`1.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.1.0...1.2.0
484
532
  .. _`1.2.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.2.0...1.2.1
485
533
  .. _`1.3.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.2.1...1.3.0
486
- .. _`2.0.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.3.0...HEAD
534
+ .. _`2.0.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.3.0...2.0.0
487
535
  .. _`2.0.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.0...2.0.1
536
+ .. _`2.1.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.1...2.1.0
537
+ .. _`2.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.1.0...HEAD
@@ -19,16 +19,15 @@ INSTALL_REQUIRES = [
19
19
  "click-default-group",
20
20
  "click-repl",
21
21
  "dict2xml",
22
- "ipaddress",
23
22
  "jinja2",
24
- "more-itertools",
23
+ "more-itertools>=8.14.0",
25
24
  "requests",
26
25
  "six",
27
26
  ]
28
27
 
29
28
  setup(
30
29
  name="greynoise",
31
- version="2.0.1",
30
+ version="2.2.0",
32
31
  description="Abstraction to interact with GreyNoise API.",
33
32
  url="https://greynoise.io/",
34
33
  author="GreyNoise Intelligence",
@@ -5,4 +5,4 @@ __license__ = "MIT"
5
5
  __maintainer__ = "GreyNoise Intelligence"
6
6
  __email__ = "hello@greynoise.io"
7
7
  __status__ = "BETA"
8
- __version__ = "2.0.1"
8
+ __version__ = "2.2.0"
@@ -2,6 +2,8 @@
2
2
 
3
3
  import logging
4
4
  import re
5
+ import sys
6
+ import time
5
7
  from collections import OrderedDict
6
8
 
7
9
  import cachetools
@@ -9,7 +11,6 @@ import more_itertools
9
11
  import requests
10
12
 
11
13
  from greynoise.__version__ import __version__
12
- from greynoise.api.analyzer import Analyzer
13
14
  from greynoise.api.filter import Filter
14
15
  from greynoise.exceptions import RateLimitError, RequestFailure
15
16
  from greynoise.util import (
@@ -31,7 +32,6 @@ def initialize_cache(cache_max_size, cache_ttl):
31
32
 
32
33
 
33
34
  class GreyNoise(object): # pylint: disable=R0205,R0902
34
-
35
35
  """GreyNoise API client.
36
36
 
37
37
  :param api_key: Key use to access the API.
@@ -58,6 +58,11 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
58
58
  EP_META_METADATA = "v2/meta/metadata"
59
59
  EP_PING = "ping"
60
60
  EP_RIOT = "v2/riot/{ip_address}"
61
+ EP_SENSOR_ACTIVITY = "v1/workspaces/{workspace_id}/sensors/activity"
62
+ EP_SENSOR_LIST = "v1/workspaces/{workspace_id}/sensors"
63
+ EP_PERSONA_DETAILS = "v1/personas/{persona_id}"
64
+ EP_ANALYZE_UPLOAD = "v2/analyze/upload"
65
+ EP_ANALYZE = "v2/analyze/{id}"
61
66
  EP_NOT_IMPLEMENTED = "v2/request/{subcommand}"
62
67
  UNKNOWN_CODE_MESSAGE = "Code message unknown: {}"
63
68
  CODE_MESSAGES = {
@@ -143,7 +148,15 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
143
148
  self.ip_quick_check_cache = initialize_cache(cache_max_size, cache_ttl)
144
149
  self.ip_context_cache = initialize_cache(cache_max_size, cache_ttl)
145
150
 
146
- def _request(self, endpoint, params=None, json=None, method="get"):
151
+ def _request(
152
+ self,
153
+ endpoint,
154
+ params=None,
155
+ json=None,
156
+ files=None,
157
+ method="get",
158
+ include_headers=False,
159
+ ):
147
160
  """Handle the requesting of information from the API.
148
161
 
149
162
  :param endpoint: Endpoint to send the request to
@@ -152,6 +165,8 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
152
165
  :type param: dict
153
166
  :param json: Request's JSON payload
154
167
  :type json: dict
168
+ :param files: Request's file for update
169
+ :type files: text file
155
170
  :param method: Request method name
156
171
  :type method: str
157
172
  :returns: Response's JSON payload
@@ -179,6 +194,7 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
179
194
  LOGGER.debug("Sending API request...headers: %s", headers)
180
195
  LOGGER.debug("Sending API request...params: %s", params)
181
196
  LOGGER.debug("Sending API request...json: %s", json)
197
+ LOGGER.debug("Sending API request...files: %s", files)
182
198
  LOGGER.debug("Sending API request...proxy: %s", self.proxy)
183
199
 
184
200
  request_method = getattr(self.session, method)
@@ -190,13 +206,20 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
190
206
  timeout=self.timeout,
191
207
  params=params,
192
208
  json=json,
209
+ files=files,
193
210
  proxies=proxies,
194
211
  )
195
212
  else:
196
213
  response = request_method(
197
- url, headers=headers, timeout=self.timeout, params=params, json=json
214
+ url,
215
+ headers=headers,
216
+ timeout=self.timeout,
217
+ params=params,
218
+ json=json,
219
+ files=files,
198
220
  )
199
221
  content_type = response.headers.get("Content-Type", "")
222
+ headers = response.headers
200
223
  if "application/json" in content_type:
201
224
  body = response.json()
202
225
  else:
@@ -208,8 +231,10 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
208
231
  raise RateLimitError()
209
232
  if response.status_code >= 400 and response.status_code != 404:
210
233
  raise RequestFailure(response.status_code, body)
211
-
212
- return body
234
+ if include_headers:
235
+ return body, headers
236
+ else:
237
+ return body
213
238
 
214
239
  def analyze(self, text):
215
240
  """Aggregate stats related to IP addresses from a given text.
@@ -221,14 +246,48 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
221
246
 
222
247
  """
223
248
  if self.offering == "community":
224
- response = [
225
- {"message": "Quick Lookup not supported with Community offering"}
226
- ]
249
+ text_stats = [{"message": "Analyze not supported with Community offering"}]
227
250
  else:
228
- analyzer = Analyzer(self)
229
- response = analyzer.analyze(text)
251
+ text_stats = {
252
+ "query": [],
253
+ "count": 0,
254
+ "stats": {},
255
+ }
230
256
 
231
- return response
257
+ files = {"file": text}
258
+ upload = self._request(self.EP_ANALYZE_UPLOAD, files=files, method="post")
259
+
260
+ if "uuid" in upload:
261
+ uuid = upload["uuid"]
262
+ state = upload["state"]
263
+ while state != "completed":
264
+ url = self.EP_ANALYZE.format(id=uuid)
265
+ response = self._request(url)
266
+ state = response["state"]
267
+ time.sleep(5)
268
+ unique_ip_list = (
269
+ response["details"].get("noise_ips_found", [])
270
+ + response["details"].get("unknown_ips", [])
271
+ + response["details"].get("riot_ips_found", [])
272
+ )
273
+
274
+ text_stats["summary"] = {
275
+ "ip_count": response["details"].get("unique_ips", 0),
276
+ "noise_ip_count": response["details"].get("noise_ips", 0),
277
+ "not_noise_ip_count": response["details"].get("non_noise_ips", 0),
278
+ "riot_ip_count": response["details"].get("riot_ips", 0),
279
+ "noise_ip_ratio": response["details"].get(
280
+ "percentage_of_noise_ips", 0
281
+ ),
282
+ "riot_ip_ratio": response["details"].get(
283
+ "percentage_of_riot_ips", 0
284
+ ),
285
+ }
286
+ text_stats["stats"] = response.get("stats")
287
+ text_stats["query"] = unique_ip_list
288
+ text_stats["count"] = response["details"].get("unique_ips", 0)
289
+
290
+ return text_stats
232
291
 
233
292
  def filter(self, text, noise_only=False, riot_only=False):
234
293
  """Filter lines that contain IP addresses from a given text.
@@ -580,6 +639,114 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
580
639
 
581
640
  return response
582
641
 
642
+ def sensor_activity(
643
+ self,
644
+ workspace_id,
645
+ file_format=None,
646
+ start_time=None,
647
+ end_time=None,
648
+ persona_id=None,
649
+ source_ip=None,
650
+ size=None,
651
+ scroll=None,
652
+ include_headers=False,
653
+ ):
654
+ """Get session data from sensors"""
655
+ LOGGER.debug(
656
+ "Running Sensor Activity: %s %s %s %s %s %s %s %s...",
657
+ workspace_id,
658
+ file_format,
659
+ start_time,
660
+ end_time,
661
+ persona_id,
662
+ source_ip,
663
+ size,
664
+ scroll,
665
+ )
666
+ if file_format is None or file_format == "json":
667
+ params = {"format": "json"}
668
+ elif file_format == "csv":
669
+ params = {"format": file_format}
670
+ else:
671
+ LOGGER.error(
672
+ f"Value for file_format is not valid (valid: json, csv): {file_format}"
673
+ )
674
+ sys.exit(1)
675
+
676
+ if start_time is not None:
677
+ params["start_time"] = start_time
678
+ if end_time is not None:
679
+ params["end_time"] = end_time
680
+ if persona_id is not None:
681
+ params["persona_id"] = persona_id
682
+ if source_ip is not None:
683
+ params["source_ip"] = source_ip
684
+ if size is not None:
685
+ params["size"] = size
686
+ if scroll is not None:
687
+ params["scroll"] = scroll
688
+ endpoint = self.EP_SENSOR_ACTIVITY.format(workspace_id=workspace_id)
689
+ response, headers = self._request(endpoint, params=params, include_headers=True)
690
+
691
+ if include_headers:
692
+ return response, headers
693
+ else:
694
+ return response
695
+
696
+ def sensor_activity_ips(
697
+ self,
698
+ workspace_id,
699
+ file_format=None,
700
+ start_time=None,
701
+ end_time=None,
702
+ persona_id=None,
703
+ source_ip=None,
704
+ size=None,
705
+ scroll=None,
706
+ ):
707
+ """Get session data from sensors"""
708
+ LOGGER.debug(
709
+ "Running Sensor Activity: %s %s %s %s %s %s %s %s...",
710
+ workspace_id,
711
+ file_format,
712
+ start_time,
713
+ end_time,
714
+ persona_id,
715
+ source_ip,
716
+ size,
717
+ scroll,
718
+ )
719
+ if file_format is None or file_format == "json":
720
+ params = {"format": "json"}
721
+ elif file_format == "csv":
722
+ params = {"format": file_format}
723
+ else:
724
+ LOGGER.error(
725
+ f"Value for file_format is not valid (valid: json, csv): {file_format}"
726
+ )
727
+ sys.exit(1)
728
+
729
+ if start_time is not None:
730
+ params["start_time"] = start_time
731
+ if end_time is not None:
732
+ params["end_time"] = end_time
733
+ if persona_id is not None:
734
+ params["persona_id"] = persona_id
735
+ if source_ip is not None:
736
+ params["source_ip"] = source_ip
737
+ if size is not None:
738
+ params["size"] = size
739
+ if scroll is not None:
740
+ params["scroll"] = scroll
741
+ endpoint = self.EP_SENSOR_ACTIVITY.format(workspace_id=workspace_id)
742
+ response = self._request(endpoint, params=params)
743
+ ip_list = []
744
+ for item in response:
745
+ ip_list.append(item.get("source_ip", ""))
746
+ final_ip_list = list(set(ip_list))
747
+
748
+ return final_ip_list
749
+
583
750
  def similar(self, ip_address, limit=None, min_score=None):
584
751
  """Query IP on the IP Similarity API
585
752
 
@@ -746,3 +913,52 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
746
913
  response["ip"] = ip_address
747
914
 
748
915
  return response
916
+
917
+ def sensor_list(self, workspace_id=None):
918
+ """Get list of current sensors for Workspace
919
+
920
+ :param workspace_id: ID of Workspace
921
+ :type workspace_id: str
922
+
923
+
924
+ """
925
+ if self.offering == "community":
926
+ response = {
927
+ "message": "Sensors List is not supported with Community offering"
928
+ }
929
+ else:
930
+ LOGGER.debug("Getting Sensor List for Workspace ID: %s...", workspace_id)
931
+
932
+ endpoint = self.EP_SENSOR_LIST.format(workspace_id=workspace_id)
933
+ response = self._request(endpoint)
934
+ new_response = {}
935
+ if "items" in response:
936
+ new_response["items"] = []
937
+ for sensor in response["items"]:
938
+ persona = self.persona_details(sensor["persona"])
939
+ sensor["persona_name"] = persona.get("name", "")
940
+ new_response["items"].append(sensor)
941
+ else:
942
+ new_response = response
943
+
944
+ return new_response
945
+
946
+ def persona_details(self, persona_id=None):
947
+ """Get persona details by ID
948
+
949
+ :param persona_id: ID of Persona
950
+ :type persona_id: str
951
+
952
+
953
+ """
954
+ if self.offering == "community":
955
+ response = {
956
+ "message": "Persona Details is not supported with Community offering"
957
+ }
958
+ else:
959
+ LOGGER.debug("Getting Persona Details for Workspace ID: %s...", persona_id)
960
+
961
+ endpoint = self.EP_PERSONA_DETAILS.format(persona_id=persona_id)
962
+ response = self._request(endpoint)
963
+
964
+ return response
@@ -3,6 +3,7 @@
3
3
  Decorators used to add common functionality to subcommands.
4
4
 
5
5
  """
6
+
6
7
  import functools
7
8
  import logging
8
9
 
@@ -62,11 +63,14 @@ def handle_exceptions(function):
62
63
  try:
63
64
  return function(*args, **kwargs)
64
65
  except RequestFailure as exception:
66
+ status = exception.args[0]
65
67
  body = exception.args[1]
66
68
  if "message" in body:
67
69
  error_message = "API error: {}".format(body["message"])
68
70
  elif "error" in body:
69
71
  error_message = "API error: {}".format(body["error"])
72
+ elif not body:
73
+ error_message = "API error: {}".format(status)
70
74
  else:
71
75
  error_message = "API error: {}".format(body)
72
76
  LOGGER.error(error_message)
@@ -233,3 +237,122 @@ def not_implemented_command(function):
233
237
  raise SubcommandNotImplemented(command_name)
234
238
 
235
239
  return wrapper
240
+
241
+
242
+ def workspace_command(function):
243
+ """Decorator that groups decorators common to sensor activity subcommands."""
244
+
245
+ @click.command()
246
+ @click.argument("workspace_id", required=True)
247
+ @click.option("-k", "--api-key", help="Key to include in API requests")
248
+ @click.option(
249
+ "-O",
250
+ "--offering",
251
+ help="Which API offering to use, enterprise or community, "
252
+ "defaults to enterprise",
253
+ )
254
+ @click.option("-i", "--input", "input_file", type=click.File(), help="Input file")
255
+ @click.option(
256
+ "-o", "--output", "output_file", type=click.File(mode="w"), help="Output file"
257
+ )
258
+ @click.option(
259
+ "-f",
260
+ "--format",
261
+ "output_format",
262
+ type=click.Choice(["json", "txt", "xml"]),
263
+ default="txt",
264
+ help="Output format",
265
+ )
266
+ @click.option("-v", "--verbose", count=True, help="Verbose output")
267
+ @pass_api_client
268
+ @click.pass_context
269
+ @echo_result
270
+ @handle_exceptions
271
+ @functools.wraps(function)
272
+ def wrapper(*args, **kwargs):
273
+ return function(*args, **kwargs)
274
+
275
+ return wrapper
276
+
277
+
278
+ def sensor_activity_command(function):
279
+ """Decorator that groups decorators common to sensor activity subcommands."""
280
+
281
+ @click.command()
282
+ @click.argument("workspace_id", required=True)
283
+ @click.option(
284
+ "--start_time", "start_time", help="Earliest session start time to return"
285
+ )
286
+ @click.option("--end_time", "end_time", help="Latest session start time to return")
287
+ @click.option(
288
+ "--file_format", "file_format", help="Format for output file", default="json"
289
+ )
290
+ @click.option("--persona_id", "persona_id", help="Id for the desired persona")
291
+ @click.option("--source_ip", "source_ip", help="Ip for the desired source")
292
+ @click.option("--size", "size", help="Max number of results to return")
293
+ @click.option("--scroll", "scroll", help="Scroll token for pagination")
294
+ @click.option("-k", "--api-key", help="Key to include in API requests")
295
+ @click.option(
296
+ "-O",
297
+ "--offering",
298
+ help="Which API offering to use, enterprise or community, "
299
+ "defaults to enterprise",
300
+ )
301
+ @click.option("-i", "--input", "input_file", type=click.File(), help="Input file")
302
+ @click.option(
303
+ "-o", "--output", "output_file", type=click.File(mode="w"), help="Output file"
304
+ )
305
+ @click.option(
306
+ "-f",
307
+ "--format",
308
+ "output_format",
309
+ type=click.Choice(["json", "txt", "xml"]),
310
+ default="txt",
311
+ help="Output format",
312
+ )
313
+ @click.option("-v", "--verbose", count=True, help="Verbose output")
314
+ @pass_api_client
315
+ @click.pass_context
316
+ @echo_result
317
+ @handle_exceptions
318
+ @functools.wraps(function)
319
+ def wrapper(*args, **kwargs):
320
+ return function(*args, **kwargs)
321
+
322
+ return wrapper
323
+
324
+
325
+ def persona_command(function):
326
+ """Decorator that groups decorators common to sensor activity subcommands."""
327
+
328
+ @click.command()
329
+ @click.argument("persona_id", required=True)
330
+ @click.option("-k", "--api-key", help="Key to include in API requests")
331
+ @click.option(
332
+ "-O",
333
+ "--offering",
334
+ help="Which API offering to use, enterprise or community, "
335
+ "defaults to enterprise",
336
+ )
337
+ @click.option("-i", "--input", "input_file", type=click.File(), help="Input file")
338
+ @click.option(
339
+ "-o", "--output", "output_file", type=click.File(mode="w"), help="Output file"
340
+ )
341
+ @click.option(
342
+ "-f",
343
+ "--format",
344
+ "output_format",
345
+ type=click.Choice(["json", "txt", "xml"]),
346
+ default="txt",
347
+ help="Output format",
348
+ )
349
+ @click.option("-v", "--verbose", count=True, help="Verbose output")
350
+ @pass_api_client
351
+ @click.pass_context
352
+ @echo_result
353
+ @handle_exceptions
354
+ @functools.wraps(function)
355
+ def wrapper(*args, **kwargs):
356
+ return function(*args, **kwargs)
357
+
358
+ return wrapper
@@ -180,7 +180,7 @@ def similar_formatter(results, verbose):
180
180
 
181
181
  @colored_output
182
182
  def timeline_formatter(results, verbose):
183
- """Convert IP Sim to human-readable text."""
183
+ """Convert Timeline to human-readable text."""
184
184
  template = JINJA2_ENV.get_template("timeline.txt.j2")
185
185
  max_width, _ = shutil.get_terminal_size()
186
186
  return template.render(results=results, verbose=verbose, max_width=max_width)
@@ -188,12 +188,36 @@ def timeline_formatter(results, verbose):
188
188
 
189
189
  @colored_output
190
190
  def timelinehourly_formatter(results, verbose):
191
- """Convert IP Sim to human-readable text."""
191
+ """Convert Timeline hourly/daily to human-readable text."""
192
192
  template = JINJA2_ENV.get_template("timelinehourly.txt.j2")
193
193
  max_width, _ = shutil.get_terminal_size()
194
194
  return template.render(results=results, verbose=verbose, max_width=max_width)
195
195
 
196
196
 
197
+ @colored_output
198
+ def sensoractivity_formatter(results, verbose):
199
+ """Convert Sensor Activity to human-readable text."""
200
+ template = JINJA2_ENV.get_template("sensoractivity.txt.j2")
201
+ max_width, _ = shutil.get_terminal_size()
202
+ return template.render(results=results, verbose=verbose, max_width=max_width)
203
+
204
+
205
+ @colored_output
206
+ def sensorlist_formatter(results, verbose):
207
+ """Convert Sensor List to human-readable text."""
208
+ template = JINJA2_ENV.get_template("sensorlist.txt.j2")
209
+ max_width, _ = shutil.get_terminal_size()
210
+ return template.render(results=results, verbose=verbose, max_width=max_width)
211
+
212
+
213
+ @colored_output
214
+ def personadetails_formatter(results, verbose):
215
+ """Convert Persona Details to human-readable text."""
216
+ template = JINJA2_ENV.get_template("personadetails.txt.j2")
217
+ max_width, _ = shutil.get_terminal_size()
218
+ return template.render(results=results, verbose=verbose, max_width=max_width)
219
+
220
+
197
221
  FORMATTERS = {
198
222
  "json": json_formatter,
199
223
  "xml": xml_formatter,
@@ -210,5 +234,8 @@ FORMATTERS = {
210
234
  "timeline": timeline_formatter,
211
235
  "timelinehourly": timelinehourly_formatter,
212
236
  "timelinedaily": timelinehourly_formatter,
237
+ "sensor-activity": sensoractivity_formatter,
238
+ "sensor-list": sensorlist_formatter,
239
+ "persona-details": personadetails_formatter,
213
240
  },
214
241
  }