greynoise 2.1.0__tar.gz → 2.3.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 (48) hide show
  1. {greynoise-2.1.0 → greynoise-2.3.0}/CHANGELOG.rst +48 -1
  2. {greynoise-2.1.0/src/greynoise.egg-info → greynoise-2.3.0}/PKG-INFO +49 -2
  3. {greynoise-2.1.0 → greynoise-2.3.0}/setup.py +2 -2
  4. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/__version__.py +1 -1
  5. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/api/__init__.py +253 -12
  6. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/decorator.py +159 -0
  7. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/formatter.py +38 -2
  8. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/subcommand.py +94 -0
  9. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/analyze.txt.j2 +29 -1
  10. greynoise-2.3.0/src/greynoise/cli/templates/cvedetails.txt.j2 +53 -0
  11. greynoise-2.3.0/src/greynoise/cli/templates/personadetails.txt.j2 +38 -0
  12. greynoise-2.3.0/src/greynoise/cli/templates/sensoractivity.txt.j2 +25 -0
  13. greynoise-2.3.0/src/greynoise/cli/templates/sensorlist.txt.j2 +30 -0
  14. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/util.py +19 -0
  15. {greynoise-2.1.0 → greynoise-2.3.0/src/greynoise.egg-info}/PKG-INFO +49 -2
  16. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise.egg-info/SOURCES.txt +4 -1
  17. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise.egg-info/requires.txt +1 -1
  18. {greynoise-2.1.0 → greynoise-2.3.0}/tests/test_api.py +122 -85
  19. greynoise-2.1.0/src/greynoise/api/analyzer.py +0 -158
  20. {greynoise-2.1.0 → greynoise-2.3.0}/LICENSE +0 -0
  21. {greynoise-2.1.0 → greynoise-2.3.0}/MANIFEST.in +0 -0
  22. {greynoise-2.1.0 → greynoise-2.3.0}/README.rst +0 -0
  23. {greynoise-2.1.0 → greynoise-2.3.0}/setup.cfg +0 -0
  24. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/__init__.py +0 -0
  25. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/api/filter.py +0 -0
  26. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/__init__.py +0 -0
  27. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/helper.py +0 -0
  28. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/parameter.py +0 -0
  29. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/gnql_query.txt.j2 +0 -0
  30. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/gnql_stats.txt.j2 +0 -0
  31. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/interesting.txt.j2 +0 -0
  32. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/ip_community.txt.j2 +0 -0
  33. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/ip_context.txt.j2 +0 -0
  34. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/ip_context_result.txt.j2 +0 -0
  35. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/ip_multi_context.txt.j2 +0 -0
  36. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/ip_quick_check.txt.j2 +0 -0
  37. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/macros.txt.j2 +0 -0
  38. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/riot.txt.j2 +0 -0
  39. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/similarity.txt.j2 +0 -0
  40. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/stats.txt.j2 +0 -0
  41. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/timeline.txt.j2 +0 -0
  42. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/cli/templates/timelinehourly.txt.j2 +0 -0
  43. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise/exceptions.py +0 -0
  44. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise.egg-info/dependency_links.txt +0 -0
  45. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise.egg-info/entry_points.txt +0 -0
  46. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise.egg-info/not-zip-safe +0 -0
  47. {greynoise-2.1.0 → greynoise-2.3.0}/src/greynoise.egg-info/top_level.txt +0 -0
  48. {greynoise-2.1.0 → greynoise-2.3.0}/tests/test_util.py +0 -0
@@ -2,6 +2,51 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version `2.3.0`_
6
+ ================
7
+ **Date**: July 30, 2024
8
+
9
+ * API client:
10
+
11
+ * Add `cve` command to query the CVE lookup API
12
+
13
+ * CLI:
14
+
15
+ * Add `cve` command to display result from CVE lookup API
16
+
17
+ * Dependencies:
18
+
19
+ * Updated cachetools to version 5.4.0
20
+
21
+ Version `2.2.0`_
22
+ ================
23
+ **Date**: June 11, 2024
24
+
25
+ * API client:
26
+
27
+ * Update `analyze` command to use analysis endpoint
28
+ * Add `persona_details` command to pull a sensor persona's details based on ID
29
+ * Add `sensor_list` command to pull list of sensors from a users workspace, based on API key used
30
+ * Add `sensor_activity` command to pull sensor activity feed from users workspace
31
+ * Add `sensor_activity_ips` command to pull list of source ips from sensor activity feed
32
+
33
+ * CLI:
34
+
35
+ * Update `analyze` output to match that of new endpoint response
36
+ * Add `persona-details` command to display command response
37
+ * Add `sensor-list` command to display command response
38
+ * Add `sensor-activity` command to display command response
39
+
40
+ * Dependencies:
41
+
42
+ * Updated Clic to version 8.1.7
43
+ * Updated ansimarkup to version 2.1.0
44
+ * Updated click-repl to version 0.3.0
45
+ * Updated dict2xml to version 1.7.5
46
+ * Updated jinja2 to version 3.1.4
47
+ * Updated more-itertools to version 10.3.0 (for py3.8 and above only)
48
+ * Updated requests to version 2.32.3
49
+
5
50
  Version `2.1.0`_
6
51
  ================
7
52
  **Date**: February 07, 2024
@@ -380,6 +425,8 @@ Version `0.2.0`_
380
425
  .. _`1.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.1.0...1.2.0
381
426
  .. _`1.2.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.2.0...1.2.1
382
427
  .. _`1.3.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.2.1...1.3.0
383
- .. _`2.0.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.3.0...HEAD
428
+ .. _`2.0.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.3.0...2.0.0
384
429
  .. _`2.0.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.0...2.0.1
385
430
  .. _`2.1.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.1...2.1.0
431
+ .. _`2.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.1.0...2.2.0
432
+ .. _`2.3.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.2.0...HEAD
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: greynoise
3
- Version: 2.1.0
3
+ Version: 2.3.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,51 @@ Usage
124
124
  Changelog
125
125
  =========
126
126
 
127
+ Version `2.3.0`_
128
+ ================
129
+ **Date**: July 30, 2024
130
+
131
+ * API client:
132
+
133
+ * Add `cve` command to query the CVE lookup API
134
+
135
+ * CLI:
136
+
137
+ * Add `cve` command to display result from CVE lookup API
138
+
139
+ * Dependencies:
140
+
141
+ * Updated cachetools to version 5.4.0
142
+
143
+ Version `2.2.0`_
144
+ ================
145
+ **Date**: June 11, 2024
146
+
147
+ * API client:
148
+
149
+ * Update `analyze` command to use analysis endpoint
150
+ * Add `persona_details` command to pull a sensor persona's details based on ID
151
+ * Add `sensor_list` command to pull list of sensors from a users workspace, based on API key used
152
+ * Add `sensor_activity` command to pull sensor activity feed from users workspace
153
+ * Add `sensor_activity_ips` command to pull list of source ips from sensor activity feed
154
+
155
+ * CLI:
156
+
157
+ * Update `analyze` output to match that of new endpoint response
158
+ * Add `persona-details` command to display command response
159
+ * Add `sensor-list` command to display command response
160
+ * Add `sensor-activity` command to display command response
161
+
162
+ * Dependencies:
163
+
164
+ * Updated Clic to version 8.1.7
165
+ * Updated ansimarkup to version 2.1.0
166
+ * Updated click-repl to version 0.3.0
167
+ * Updated dict2xml to version 1.7.5
168
+ * Updated jinja2 to version 3.1.4
169
+ * Updated more-itertools to version 10.3.0 (for py3.8 and above only)
170
+ * Updated requests to version 2.32.3
171
+
127
172
  Version `2.1.0`_
128
173
  ================
129
174
  **Date**: February 07, 2024
@@ -502,6 +547,8 @@ Version `0.2.0`_
502
547
  .. _`1.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.1.0...1.2.0
503
548
  .. _`1.2.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.2.0...1.2.1
504
549
  .. _`1.3.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.2.1...1.3.0
505
- .. _`2.0.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.3.0...HEAD
550
+ .. _`2.0.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.3.0...2.0.0
506
551
  .. _`2.0.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.0...2.0.1
507
552
  .. _`2.1.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.1...2.1.0
553
+ .. _`2.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.1.0...2.2.0
554
+ .. _`2.3.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.2.0...HEAD
@@ -20,14 +20,14 @@ INSTALL_REQUIRES = [
20
20
  "click-repl",
21
21
  "dict2xml",
22
22
  "jinja2",
23
- "more-itertools",
23
+ "more-itertools>=8.14.0",
24
24
  "requests",
25
25
  "six",
26
26
  ]
27
27
 
28
28
  setup(
29
29
  name="greynoise",
30
- version="2.1.0",
30
+ version="2.3.0",
31
31
  description="Abstraction to interact with GreyNoise API.",
32
32
  url="https://greynoise.io/",
33
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.1.0"
8
+ __version__ = "2.3.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,11 +11,11 @@ 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 (
16
17
  load_config,
18
+ validate_cve_id,
17
19
  validate_ip,
18
20
  validate_similar_min_score,
19
21
  validate_timeline_days,
@@ -31,7 +33,6 @@ def initialize_cache(cache_max_size, cache_ttl):
31
33
 
32
34
 
33
35
  class GreyNoise(object): # pylint: disable=R0205,R0902
34
-
35
36
  """GreyNoise API client.
36
37
 
37
38
  :param api_key: Key use to access the API.
@@ -58,6 +59,12 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
58
59
  EP_META_METADATA = "v2/meta/metadata"
59
60
  EP_PING = "ping"
60
61
  EP_RIOT = "v2/riot/{ip_address}"
62
+ EP_SENSOR_ACTIVITY = "v1/workspaces/{workspace_id}/sensors/activity"
63
+ EP_SENSOR_LIST = "v1/workspaces/{workspace_id}/sensors"
64
+ EP_PERSONA_DETAILS = "v1/personas/{persona_id}"
65
+ EP_CVE_LOOKUP = "v1/cve/{cve_id}"
66
+ EP_ANALYZE_UPLOAD = "v2/analyze/upload"
67
+ EP_ANALYZE = "v2/analyze/{id}"
61
68
  EP_NOT_IMPLEMENTED = "v2/request/{subcommand}"
62
69
  UNKNOWN_CODE_MESSAGE = "Code message unknown: {}"
63
70
  CODE_MESSAGES = {
@@ -143,7 +150,15 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
143
150
  self.ip_quick_check_cache = initialize_cache(cache_max_size, cache_ttl)
144
151
  self.ip_context_cache = initialize_cache(cache_max_size, cache_ttl)
145
152
 
146
- def _request(self, endpoint, params=None, json=None, method="get"):
153
+ def _request(
154
+ self,
155
+ endpoint,
156
+ params=None,
157
+ json=None,
158
+ files=None,
159
+ method="get",
160
+ include_headers=False,
161
+ ):
147
162
  """Handle the requesting of information from the API.
148
163
 
149
164
  :param endpoint: Endpoint to send the request to
@@ -152,6 +167,8 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
152
167
  :type param: dict
153
168
  :param json: Request's JSON payload
154
169
  :type json: dict
170
+ :param files: Request's file for update
171
+ :type files: text file
155
172
  :param method: Request method name
156
173
  :type method: str
157
174
  :returns: Response's JSON payload
@@ -179,6 +196,7 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
179
196
  LOGGER.debug("Sending API request...headers: %s", headers)
180
197
  LOGGER.debug("Sending API request...params: %s", params)
181
198
  LOGGER.debug("Sending API request...json: %s", json)
199
+ LOGGER.debug("Sending API request...files: %s", files)
182
200
  LOGGER.debug("Sending API request...proxy: %s", self.proxy)
183
201
 
184
202
  request_method = getattr(self.session, method)
@@ -190,13 +208,20 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
190
208
  timeout=self.timeout,
191
209
  params=params,
192
210
  json=json,
211
+ files=files,
193
212
  proxies=proxies,
194
213
  )
195
214
  else:
196
215
  response = request_method(
197
- url, headers=headers, timeout=self.timeout, params=params, json=json
216
+ url,
217
+ headers=headers,
218
+ timeout=self.timeout,
219
+ params=params,
220
+ json=json,
221
+ files=files,
198
222
  )
199
223
  content_type = response.headers.get("Content-Type", "")
224
+ headers = response.headers
200
225
  if "application/json" in content_type:
201
226
  body = response.json()
202
227
  else:
@@ -208,8 +233,10 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
208
233
  raise RateLimitError()
209
234
  if response.status_code >= 400 and response.status_code != 404:
210
235
  raise RequestFailure(response.status_code, body)
211
-
212
- return body
236
+ if include_headers:
237
+ return body, headers
238
+ else:
239
+ return body
213
240
 
214
241
  def analyze(self, text):
215
242
  """Aggregate stats related to IP addresses from a given text.
@@ -221,14 +248,48 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
221
248
 
222
249
  """
223
250
  if self.offering == "community":
224
- response = [
225
- {"message": "Quick Lookup not supported with Community offering"}
226
- ]
251
+ text_stats = [{"message": "Analyze not supported with Community offering"}]
227
252
  else:
228
- analyzer = Analyzer(self)
229
- response = analyzer.analyze(text)
253
+ text_stats = {
254
+ "query": [],
255
+ "count": 0,
256
+ "stats": {},
257
+ }
230
258
 
231
- return response
259
+ files = {"file": text}
260
+ upload = self._request(self.EP_ANALYZE_UPLOAD, files=files, method="post")
261
+
262
+ if "uuid" in upload:
263
+ uuid = upload["uuid"]
264
+ state = upload["state"]
265
+ while state != "completed":
266
+ url = self.EP_ANALYZE.format(id=uuid)
267
+ response = self._request(url)
268
+ state = response["state"]
269
+ time.sleep(5)
270
+ unique_ip_list = (
271
+ response["details"].get("noise_ips_found", [])
272
+ + response["details"].get("unknown_ips", [])
273
+ + response["details"].get("riot_ips_found", [])
274
+ )
275
+
276
+ text_stats["summary"] = {
277
+ "ip_count": response["details"].get("unique_ips", 0),
278
+ "noise_ip_count": response["details"].get("noise_ips", 0),
279
+ "not_noise_ip_count": response["details"].get("non_noise_ips", 0),
280
+ "riot_ip_count": response["details"].get("riot_ips", 0),
281
+ "noise_ip_ratio": response["details"].get(
282
+ "percentage_of_noise_ips", 0
283
+ ),
284
+ "riot_ip_ratio": response["details"].get(
285
+ "percentage_of_riot_ips", 0
286
+ ),
287
+ }
288
+ text_stats["stats"] = response.get("stats")
289
+ text_stats["query"] = unique_ip_list
290
+ text_stats["count"] = response["details"].get("unique_ips", 0)
291
+
292
+ return text_stats
232
293
 
233
294
  def filter(self, text, noise_only=False, riot_only=False):
234
295
  """Filter lines that contain IP addresses from a given text.
@@ -580,6 +641,114 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
580
641
 
581
642
  return response
582
643
 
644
+ def sensor_activity(
645
+ self,
646
+ workspace_id,
647
+ file_format=None,
648
+ start_time=None,
649
+ end_time=None,
650
+ persona_id=None,
651
+ source_ip=None,
652
+ size=None,
653
+ scroll=None,
654
+ include_headers=False,
655
+ ):
656
+ """Get session data from sensors"""
657
+ LOGGER.debug(
658
+ "Running Sensor Activity: %s %s %s %s %s %s %s %s...",
659
+ workspace_id,
660
+ file_format,
661
+ start_time,
662
+ end_time,
663
+ persona_id,
664
+ source_ip,
665
+ size,
666
+ scroll,
667
+ )
668
+ if file_format is None or file_format == "json":
669
+ params = {"format": "json"}
670
+ elif file_format == "csv":
671
+ params = {"format": file_format}
672
+ else:
673
+ LOGGER.error(
674
+ f"Value for file_format is not valid (valid: json, csv): {file_format}"
675
+ )
676
+ sys.exit(1)
677
+
678
+ if start_time is not None:
679
+ params["start_time"] = start_time
680
+ if end_time is not None:
681
+ params["end_time"] = end_time
682
+ if persona_id is not None:
683
+ params["persona_id"] = persona_id
684
+ if source_ip is not None:
685
+ params["source_ip"] = source_ip
686
+ if size is not None:
687
+ params["size"] = size
688
+ if scroll is not None:
689
+ params["scroll"] = scroll
690
+ endpoint = self.EP_SENSOR_ACTIVITY.format(workspace_id=workspace_id)
691
+ response, headers = self._request(endpoint, params=params, include_headers=True)
692
+
693
+ if include_headers:
694
+ return response, headers
695
+ else:
696
+ return response
697
+
698
+ def sensor_activity_ips(
699
+ self,
700
+ workspace_id,
701
+ file_format=None,
702
+ start_time=None,
703
+ end_time=None,
704
+ persona_id=None,
705
+ source_ip=None,
706
+ size=None,
707
+ scroll=None,
708
+ ):
709
+ """Get session data from sensors"""
710
+ LOGGER.debug(
711
+ "Running Sensor Activity: %s %s %s %s %s %s %s %s...",
712
+ workspace_id,
713
+ file_format,
714
+ start_time,
715
+ end_time,
716
+ persona_id,
717
+ source_ip,
718
+ size,
719
+ scroll,
720
+ )
721
+ if file_format is None or file_format == "json":
722
+ params = {"format": "json"}
723
+ elif file_format == "csv":
724
+ params = {"format": file_format}
725
+ else:
726
+ LOGGER.error(
727
+ f"Value for file_format is not valid (valid: json, csv): {file_format}"
728
+ )
729
+ sys.exit(1)
730
+
731
+ if start_time is not None:
732
+ params["start_time"] = start_time
733
+ if end_time is not None:
734
+ params["end_time"] = end_time
735
+ if persona_id is not None:
736
+ params["persona_id"] = persona_id
737
+ if source_ip is not None:
738
+ params["source_ip"] = source_ip
739
+ if size is not None:
740
+ params["size"] = size
741
+ if scroll is not None:
742
+ params["scroll"] = scroll
743
+ endpoint = self.EP_SENSOR_ACTIVITY.format(workspace_id=workspace_id)
744
+ response = self._request(endpoint, params=params)
745
+ ip_list = []
746
+ for item in response:
747
+ ip_list.append(item.get("source_ip", ""))
748
+ final_ip_list = list(set(ip_list))
749
+
750
+ return final_ip_list
751
+
583
752
  def similar(self, ip_address, limit=None, min_score=None):
584
753
  """Query IP on the IP Similarity API
585
754
 
@@ -746,3 +915,75 @@ class GreyNoise(object): # pylint: disable=R0205,R0902
746
915
  response["ip"] = ip_address
747
916
 
748
917
  return response
918
+
919
+ def sensor_list(self, workspace_id=None):
920
+ """Get list of current sensors for Workspace
921
+
922
+ :param workspace_id: ID of Workspace
923
+ :type workspace_id: str
924
+
925
+
926
+ """
927
+ if self.offering == "community":
928
+ response = {
929
+ "message": "Sensors List is not supported with Community offering"
930
+ }
931
+ else:
932
+ LOGGER.debug("Getting Sensor List for Workspace ID: %s...", workspace_id)
933
+
934
+ endpoint = self.EP_SENSOR_LIST.format(workspace_id=workspace_id)
935
+ response = self._request(endpoint)
936
+ new_response = {}
937
+ if "items" in response:
938
+ new_response["items"] = []
939
+ for sensor in response["items"]:
940
+ persona = self.persona_details(sensor["persona"])
941
+ sensor["persona_name"] = persona.get("name", "")
942
+ new_response["items"].append(sensor)
943
+ else:
944
+ new_response = response
945
+
946
+ return new_response
947
+
948
+ def persona_details(self, persona_id=None):
949
+ """Get persona details by ID
950
+
951
+ :param persona_id: ID of Persona
952
+ :type persona_id: str
953
+
954
+
955
+ """
956
+ if self.offering == "community":
957
+ response = {
958
+ "message": "Persona Details is not supported with Community offering"
959
+ }
960
+ else:
961
+ LOGGER.debug("Getting Persona Details for Workspace ID: %s...", persona_id)
962
+
963
+ endpoint = self.EP_PERSONA_DETAILS.format(persona_id=persona_id)
964
+ response = self._request(endpoint)
965
+
966
+ return response
967
+
968
+ def cve(self, cve_id=None):
969
+ """Get CVE details by CVE ID
970
+
971
+ :param cve_id: ID of CVE
972
+ :type cve_id: str
973
+
974
+
975
+ """
976
+ if self.offering == "community":
977
+ response = {
978
+ "message": "CVE lookup is not supported with Community offering"
979
+ }
980
+ else:
981
+ LOGGER.debug("Getting Details for CVE ID: %s...", cve_id)
982
+
983
+ # check if CVE submitted is in correct format
984
+ validate_cve_id(cve_id)
985
+
986
+ endpoint = self.EP_CVE_LOOKUP.format(cve_id=cve_id)
987
+ response = self._request(endpoint)
988
+
989
+ 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,158 @@ 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 workspace 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 persona 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
359
+
360
+
361
+ def cve_command(function):
362
+ """Decorator that groups decorators common to cve subcommand."""
363
+
364
+ @click.command()
365
+ @click.argument("cve_id", required=True)
366
+ @click.option("-k", "--api-key", help="Key to include in API requests")
367
+ @click.option(
368
+ "-O",
369
+ "--offering",
370
+ help="Which API offering to use, enterprise or community, "
371
+ "defaults to enterprise",
372
+ )
373
+ @click.option("-i", "--input", "input_file", type=click.File(), help="Input file")
374
+ @click.option(
375
+ "-o", "--output", "output_file", type=click.File(mode="w"), help="Output file"
376
+ )
377
+ @click.option(
378
+ "-f",
379
+ "--format",
380
+ "output_format",
381
+ type=click.Choice(["json", "txt", "xml"]),
382
+ default="txt",
383
+ help="Output format",
384
+ )
385
+ @click.option("-v", "--verbose", count=True, help="Verbose output")
386
+ @pass_api_client
387
+ @click.pass_context
388
+ @echo_result
389
+ @handle_exceptions
390
+ @functools.wraps(function)
391
+ def wrapper(*args, **kwargs):
392
+ return function(*args, **kwargs)
393
+
394
+ return wrapper