-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathagents.py
More file actions
276 lines (245 loc) · 9.28 KB
/
agents.py
File metadata and controls
276 lines (245 loc) · 9.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
from os import environ
from pathlib import Path
from typing import List
import click
import requests
from boltons.iterutils import chunked
from rich.panel import Panel
from rich.progress import track
from _incydr_cli import console
from _incydr_cli import logging_options
from _incydr_cli import render
from _incydr_cli.cmds.models import AgentCSV
from _incydr_cli.cmds.models import AgentJSON
from _incydr_cli.cmds.options.output_options import columns_option
from _incydr_cli.cmds.options.output_options import input_format_option
from _incydr_cli.cmds.options.output_options import single_format_option
from _incydr_cli.cmds.options.output_options import SingleFormat
from _incydr_cli.cmds.options.output_options import table_format_option
from _incydr_cli.cmds.options.output_options import TableFormat
from _incydr_cli.core import incompatible_with
from _incydr_cli.core import IncydrCommand
from _incydr_cli.core import IncydrGroup
from _incydr_sdk.agents.models import Agent
from _incydr_sdk.core.client import Client
from _incydr_sdk.utils import model_as_card
@click.group(cls=IncydrGroup)
@logging_options
def agents():
"""
View and manage Incydr agents.
Incydr agents run on the endpoints in your environment and monitor for insider risk activity.
"""
@agents.command("list", cls=IncydrCommand)
@click.option(
"--active/--inactive",
default=None,
help="Filter by active or inactive agents. Defaults to returning both when when neither option is passed.",
)
@click.option(
"--healthy",
is_flag=True,
default=None,
help="Filter by healthy agents. Agents that have no health issue types are considered healthy.",
cls=incompatible_with("unhealthy"),
)
@click.option(
"--unhealthy",
is_flag=False, # is_flag=False with a flag_value indicates an optional value
flag_value="FLAG_VALUE",
default=None,
help="Filter by unhealthy agents. Defaults to returning all unhealthy agents."
" Pass a comma delimited list of health issue types to filter by unhealthy agents that have (at least) any "
"of the given health issue type(s). Health issue types include the following: NOT_CONNECTING, NOT_SENDING_SECURITY_EVENTS, SECURITY_INGEST_REJECTED, MISSING_MACOS_PERMISSION_FULL_DISK_ACCESS, MISSING_MACOS_PERMISSION_ACCESSIBILITY.",
cls=incompatible_with("healthy"),
)
@click.option(
"--agent-health-modified-within-days",
type=int,
default=None,
help="Filter agents that have had agent health modified in the last N days (starting from midnight this morning), where N is the value of the parameter.",
)
@table_format_option
@columns_option
@logging_options
def list_(
active: bool = None,
healthy: bool = None,
unhealthy: str = None,
agent_health_modified_within_days: int = None,
format_: TableFormat = None,
columns: str = None,
):
"""
List agents.
"""
agent_healthy = None
health_issues = None
if healthy:
agent_healthy = True
if unhealthy:
agent_healthy = False
if (
not unhealthy == "FLAG_VALUE"
): # If the unhealthy value is FLAG_VALUE then we know the option was passed with no values
health_issues = unhealthy.split(",")
client = Client()
agents = client.agents.v1.iter_all(
active=active,
agent_healthy=agent_healthy,
agent_health_issue_types=health_issues,
agent_health_modified_in_last_days=agent_health_modified_within_days,
)
if format_ == TableFormat.table:
render.table(Agent, agents, columns=columns, flat=False)
elif format_ == TableFormat.csv:
render.csv(Agent, agents, columns=columns, flat=True)
elif format_ == TableFormat.json_pretty:
for agent in agents:
console.print_json(agent.json())
else:
for agent in agents:
click.echo(agent.json())
@agents.command(cls=IncydrCommand)
@click.argument("agent_id")
@single_format_option
@logging_options
def show(
agent_id: str,
format_: SingleFormat,
):
"""
Show details for a single agent.
"""
client = Client()
agent = client.agents.v1.get_agent(agent_id)
if format_ == SingleFormat.rich and client.settings.use_rich:
console.print(Panel.fit(model_as_card(agent), title=f"Agent {agent.agent_id}"))
elif format_ == SingleFormat.json_pretty:
console.print_json(agent.json())
else:
click.echo(agent.json())
@agents.command(cls=IncydrCommand)
@click.argument("file", type=click.File())
@input_format_option
@logging_options
def bulk_activate(file: Path, format_: str):
"""
Activate a group of agents from a file (CSV or JSON-LINES formatted).
\b
Use `-` as filename to read from stdin.
Input files require a header (for CSV input) or JSON key for each object (for JSON-LINES input) to identify
which agent ID to activate.
Header and JSON key values that are accepted are: agentGuid, agent_id, agentId, or guid
"""
chunk_size = (
environ.get("incydr_batch_size") or environ.get("INCYDR_BATCH_SIZE") or 50
)
try:
chunk_size = int(chunk_size)
except ValueError:
console.print(
f"INCYDR_BATCH_SIZE environment variable must be an integer, found: '{chunk_size}'"
)
return
# parse CSV or JSON input
if format_ == "csv":
models = AgentCSV.parse_csv(file)
else:
models = AgentJSON.parse_json_lines(file)
try:
agent_ids = [agent.agent_id for agent in models]
except ValueError as err:
console.print(err)
return
# validate we got at least one agent_id
num_agents = len(agent_ids)
if num_agents < 1:
console.print(f"[red]No agent IDs found in {format_} input.")
return
client = Client()
batches = chunked(agent_ids, size=chunk_size)
for batch in track(batches, description="Activating agents...", console=console):
process_batch(client, batch, activate=True)
@agents.command(cls=IncydrCommand)
@click.argument("file", type=click.File())
@input_format_option
@logging_options
def bulk_deactivate(file: Path, format_: str):
"""
Deactivate a group of agents from a file (CSV or JSON-LINES formatted).
\b
Use `-` as filename to read from stdin.
Input files require a header (for CSV input) or JSON key for each object (for JSON-LINES input) to identify
which agent ID to deactivate.
Header and JSON key values that are accepted are: agentGuid, agent_id, agentId, or guid
"""
chunk_size = (
environ.get("incydr_batch_size") or environ.get("INCYDR_BATCH_SIZE") or 50
)
try:
chunk_size = int(chunk_size)
except ValueError:
console.print(
f"INCYDR_BATCH_SIZE environment variable must be an integer, found: '{chunk_size}'"
)
return
# parse CSV or JSON input
if format_ == "csv":
models = AgentCSV.parse_csv(file)
else:
models = AgentJSON.parse_json_lines(file)
try:
agent_ids = [agent.agent_id for agent in models]
except ValueError as err:
console.print(err)
return
# validate we got at least one agent_id
num_agents = len(agent_ids)
if num_agents < 1:
console.print(f"[red]No agent IDs found in {format_} input.")
return
client = Client()
batches = chunked(agent_ids, size=chunk_size)
for batch in track(batches, description="Deactivating agents...", console=console):
process_batch(client, batch, activate=False)
def process_batch(client: Client, batch: List[str], activate: bool):
action = "activation" if activate else "deactivation"
api_call = client.agents.v1.activate if activate else client.agents.v1.deactivate
process_individually = False
try:
api_call(batch)
except requests.HTTPError as err:
if err.response.status_code == 404:
invalid_agent_ids = err.response.json().get("agentsNotFound")
if invalid_agent_ids is None:
console.print(
f"[red]Unknown 404 error processing batch of {len(batch)} agent {action}s."
)
process_individually = True
else:
console.print(
f"[red]404 Error processing batch of {len(batch)} agent {action}s, agent_ids not found:[/red] {invalid_agent_ids}"
)
batch = list(set(batch) - set(invalid_agent_ids))
if len(batch) > 0:
console.print("Removing invalid agent_ids and retrying...")
try:
api_call(batch)
except requests.HTTPError as err:
console.print(f"[red]Error retrying batch. {err.response.text}")
process_individually = True
else:
console.print(
f"[red]Unknown error processing batch of {len(batch)} agent {action}s."
)
process_individually = True
if process_individually and len(batch) > 1:
console.print(f"Trying agent {action} for this batch individually.")
for agent_id in batch:
try:
api_call(agent_id)
except requests.HTTPError as err:
msg = f"Failed to process {action} for {agent_id}: {err.response.text}"
client.settings.logger.error(msg)
console.print(msg)