greynoise 2.1.0__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.
- {greynoise-2.1.0 → greynoise-2.2.0}/CHANGELOG.rst +31 -1
- {greynoise-2.1.0/src/greynoise.egg-info → greynoise-2.2.0}/PKG-INFO +32 -2
- {greynoise-2.1.0 → greynoise-2.2.0}/setup.py +2 -2
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/__version__.py +1 -1
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/api/__init__.py +228 -12
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/decorator.py +123 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/formatter.py +29 -2
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/subcommand.py +74 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/analyze.txt.j2 +29 -1
- greynoise-2.2.0/src/greynoise/cli/templates/personadetails.txt.j2 +38 -0
- greynoise-2.2.0/src/greynoise/cli/templates/sensoractivity.txt.j2 +25 -0
- greynoise-2.2.0/src/greynoise/cli/templates/sensorlist.txt.j2 +30 -0
- {greynoise-2.1.0 → greynoise-2.2.0/src/greynoise.egg-info}/PKG-INFO +32 -2
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise.egg-info/SOURCES.txt +3 -1
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise.egg-info/requires.txt +1 -1
- {greynoise-2.1.0 → greynoise-2.2.0}/tests/test_api.py +122 -85
- greynoise-2.1.0/src/greynoise/api/analyzer.py +0 -158
- {greynoise-2.1.0 → greynoise-2.2.0}/LICENSE +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/MANIFEST.in +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/README.rst +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/setup.cfg +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/__init__.py +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/api/filter.py +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/__init__.py +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/helper.py +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/parameter.py +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/gnql_query.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/gnql_stats.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/interesting.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/ip_community.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/ip_context.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/ip_context_result.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/ip_multi_context.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/ip_quick_check.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/macros.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/riot.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/similarity.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/stats.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/timeline.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/cli/templates/timelinehourly.txt.j2 +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/exceptions.py +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise/util.py +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise.egg-info/dependency_links.txt +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise.egg-info/entry_points.txt +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise.egg-info/not-zip-safe +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/src/greynoise.egg-info/top_level.txt +0 -0
- {greynoise-2.1.0 → greynoise-2.2.0}/tests/test_util.py +0 -0
|
@@ -2,6 +2,35 @@
|
|
|
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
|
+
|
|
5
34
|
Version `2.1.0`_
|
|
6
35
|
================
|
|
7
36
|
**Date**: February 07, 2024
|
|
@@ -380,6 +409,7 @@ Version `0.2.0`_
|
|
|
380
409
|
.. _`1.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.1.0...1.2.0
|
|
381
410
|
.. _`1.2.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.2.0...1.2.1
|
|
382
411
|
.. _`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...
|
|
412
|
+
.. _`2.0.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.3.0...2.0.0
|
|
384
413
|
.. _`2.0.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.0...2.0.1
|
|
385
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.
|
|
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,35 @@ 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
|
+
|
|
127
156
|
Version `2.1.0`_
|
|
128
157
|
================
|
|
129
158
|
**Date**: February 07, 2024
|
|
@@ -502,6 +531,7 @@ Version `0.2.0`_
|
|
|
502
531
|
.. _`1.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.1.0...1.2.0
|
|
503
532
|
.. _`1.2.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.2.0...1.2.1
|
|
504
533
|
.. _`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...
|
|
534
|
+
.. _`2.0.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.3.0...2.0.0
|
|
506
535
|
.. _`2.0.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.0...2.0.1
|
|
507
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
|
|
@@ -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.
|
|
30
|
+
version="2.2.0",
|
|
31
31
|
description="Abstraction to interact with GreyNoise API.",
|
|
32
32
|
url="https://greynoise.io/",
|
|
33
33
|
author="GreyNoise Intelligence",
|
|
@@ -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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
{"message": "Quick Lookup not supported with Community offering"}
|
|
226
|
-
]
|
|
249
|
+
text_stats = [{"message": "Analyze not supported with Community offering"}]
|
|
227
250
|
else:
|
|
228
|
-
|
|
229
|
-
|
|
251
|
+
text_stats = {
|
|
252
|
+
"query": [],
|
|
253
|
+
"count": 0,
|
|
254
|
+
"stats": {},
|
|
255
|
+
}
|
|
230
256
|
|
|
231
|
-
|
|
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
|
|
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
|
|
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
|
}
|