Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
sadnub committed Apr 2, 2024
1 parent 5153940 commit cf169e7
Show file tree
Hide file tree
Showing 15 changed files with 730 additions and 173 deletions.
4 changes: 4 additions & 0 deletions api/tacticalrmm/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ class Role(BaseAuditModel):
can_run_urlactions = models.BooleanField(default=False)
can_view_customfields = models.BooleanField(default=False)
can_manage_customfields = models.BooleanField(default=False)
can_view_servertasks = models.BooleanField(default=False)
can_manage_servertasks = models.BooleanField(default=False)
can_run_servertasks = models.BooleanField(default=False)
can_run_servercli = models.BooleanField(default=False)

# checks
can_list_checks = models.BooleanField(default=False)
Expand Down
13 changes: 10 additions & 3 deletions api/tacticalrmm/accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pyotp
import datetime
from django.conf import settings
from django.contrib.auth import login
from django.db import IntegrityError
Expand Down Expand Up @@ -29,6 +30,10 @@
class CheckCreds(KnoxLoginView):
permission_classes = (AllowAny,)

# restrict time on tokens issued by this view to 3 min
def get_token_ttl(self):
return datetime.timedelta(seconds=180)

def post(self, request, format=None):
# check credentials
serializer = AuthTokenSerializer(data=request.data)
Expand All @@ -47,10 +52,10 @@ def post(self, request, format=None):
if not user.totp_key:
login(request, user)
response = super(CheckCreds, self).post(request, format=None)
response.data["totp"] = "totp not set"
response.data["totp"] = False
return response

return Response("ok")
return Response({"totp": True})


class LoginView(KnoxLoginView):
Expand Down Expand Up @@ -89,7 +94,9 @@ def post(self, request, format=None):
AuditLog.audit_user_login_successful(
request.data["username"], debug_info={"ip": request._client_ip}
)
return super(LoginView, self).post(request, format=None)
response = super(LoginView, self).post(request, format=None)
response.data["username"]= request.user.username
return Response(response.data)
else:
AuditLog.audit_user_failed_twofactor(
request.data["username"], debug_info={"ip": request._client_ip}
Expand Down
195 changes: 132 additions & 63 deletions api/tacticalrmm/alerts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
DebugLogType,
)
from tacticalrmm.models import PermissionQuerySet
from tacticalrmm.utils import RE_DB_VALUE, get_db_value
from core.utils import run_server_script, run_url_rest_action

if TYPE_CHECKING:
from agents.models import Agent
Expand Down Expand Up @@ -454,23 +456,39 @@ def handle_alert_failure(
and run_script_action
and not alert.action_run
):
hist = AgentHistory.objects.create(
agent=agent,
type=AgentHistoryType.SCRIPT_RUN,
script=alert_template.action,
username="alert-action-failure",
)
r = agent.run_script(
scriptpk=alert_template.action.pk,
args=alert.parse_script_args(alert_template.action_args),
timeout=alert_template.action_timeout,
wait=True,
history_pk=hist.pk,
full=True,
run_on_any=True,
run_as_user=False,
env_vars=alert_template.action_env_vars,
)
if alert_template.action_type == "script":
hist = AgentHistory.objects.create(
agent=agent,
type=AgentHistoryType.SCRIPT_RUN,
script=alert_template.action,
username="alert-action-failure",
)
r = agent.run_script(
scriptpk=alert_template.action.pk,
args=alert.parse_script_args(alert_template.action_args),
timeout=alert_template.action_timeout,
wait=True,
history_pk=hist.pk,
full=True,
run_on_any=True,
run_as_user=False,
env_vars=alert.parse_script_args(alert_template.action_env_vars),
)
elif alert_template.action_type == "server":
r = run_server_script(
script_id=alert_template.action,
args=alert.parse_script_args(alert_template.action_args),
timeout=alert_template.action_timeout,
env_vars=alert.parse_script_args(alert_template.action_env_vars),
)

elif alert_template.action_type == "rest":
output, status = run_url_rest_action(action_id=alert_template.action, instance=alert)
alert.action_retcode = status
alert.action_stdout = output
alert.action_run = djangotime.now()
alert.save()
return

# command was successful
if isinstance(r, dict):
Expand All @@ -481,11 +499,17 @@ def handle_alert_failure(
alert.action_run = djangotime.now()
alert.save()
else:
DebugLog.error(
agent=agent,
log_type=DebugLogType.SCRIPTING,
message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert",
)
if (alert_template.action_type == "script"):
DebugLog.error(
agent=agent,
log_type=DebugLogType.SCRIPTING,
message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert",
)
else:
DebugLog.error(
log_type=DebugLogType.SCRIPTING,
message=f"Failure action: {alert_template.action.name} failed to run on server for failure alert",
)

@classmethod
def handle_alert_resolve(
Expand Down Expand Up @@ -585,23 +609,48 @@ def handle_alert_resolve(
and run_script_action
and not alert.resolved_action_run
):
hist = AgentHistory.objects.create(
agent=agent,
type=AgentHistoryType.SCRIPT_RUN,
script=alert_template.action,
username="alert-action-resolved",
)
r = agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert.parse_script_args(alert_template.resolved_action_args),
timeout=alert_template.resolved_action_timeout,
wait=True,
history_pk=hist.pk,
full=True,
run_on_any=True,
run_as_user=False,
env_vars=alert_template.resolved_action_env_vars,
)
if alert_template.resolved_action_type == "script":
hist = AgentHistory.objects.create(
agent=agent,
type=AgentHistoryType.SCRIPT_RUN,
script=alert_template.resolved_action,
username="alert-action-resolved",
)
r = agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert.parse_script_args(alert_template.resolved_action_args),
timeout=alert_template.resolved_action_timeout,
wait=True,
history_pk=hist.pk,
full=True,
run_on_any=True,
run_as_user=False,
env_vars=alert_template.resolved_action_env_vars,
)
elif alert_template.resolved_action_type == "server":
stdout, stderr, execution_time, retcode = run_server_script(
script_id=alert_template.resolved_action,
args=alert.parse_script_args(alert_template.resolved_action_args),
timeout=alert_template.resolved_action_timeout,
env_vars=alert.parse_script_args(alert_template.resolved_action_env_vars),
)
r = {
"stdout": stdout,
"stderr": stderr,
"execution_time": execution_time,
"retcode": retcode
}

else:
output, status = run_url_rest_action(action_id=alert_template.resolved_action, instance=alert.id)

r = {
"stdout": output,
"stderr": "",
"execution_time": 0,
"retcode": status
}


# command was successful
if isinstance(r, dict):
Expand All @@ -614,55 +663,65 @@ def handle_alert_resolve(
alert.resolved_action_run = djangotime.now()
alert.save()
else:
DebugLog.error(
agent=agent,
log_type=DebugLogType.SCRIPTING,
message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert",
)
if alert_template.resolved_action_type == "script":
DebugLog.error(
agent=agent,
log_type=DebugLogType.SCRIPTING,
message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert",
)
else:
DebugLog.error(
log_type=DebugLogType.SCRIPTING,
message=f"Resolved action: {alert_template.action.name} failed to run on server for resolved alert",
)

def parse_script_args(self, args: List[str]) -> List[str]:
if not args:
return []

temp_args = []
# pattern to match for injection
pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")

for arg in args:
if match := pattern.match(arg):
name = match.group(1)

# check if attr exists and isn't a function
if hasattr(self, name) and not callable(getattr(self, name)):
value = f"'{getattr(self, name)}'"
else:
continue

try:
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))
except re.error:
temp_args.append(re.sub("\\{\\{.*\\}\\}", re.escape(value), arg))
except Exception as e:
DebugLog.error(log_type=DebugLogType.SCRIPTING, message=str(e))
continue
temp_arg = ""
for string, model, prop in re.findall(RE_DB_VALUE, arg):
value = get_db_value(string=f"{model}.{prop}", instance=self)

temp_arg = temp_arg.replace(string, str(value))
else:
temp_args.append(arg)
temp_arg = arg

temp_args.append(temp_arg)

return temp_args



class AlertTemplateActionType(models.TextChoices):
SCRIPT = "script", "Script"
SERVER = "server", "Server"
REST = "rest", "Rest"

class AlertTemplate(BaseAuditModel):
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)

action_type = models.CharField(
max_length=10, choices=AlertTemplateActionType.choices, default="script"
)
action = models.ForeignKey(
"scripts.Script",
related_name="alert_template",
blank=True,
null=True,
on_delete=models.SET_NULL,
)
action_rest = models.ForeignKey(
"core.URLAction",
related_name="url_action_alert_template",
blank=True,
null=True,
on_delete=models.SET_NULL,
)
action_args = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
Expand All @@ -676,13 +735,23 @@ class AlertTemplate(BaseAuditModel):
default=list,
)
action_timeout = models.PositiveIntegerField(default=15)
resolved_action_type = models.CharField(
max_length=10, choices=AlertTemplateActionType.choices, default="script"
)
resolved_action = models.ForeignKey(
"scripts.Script",
related_name="resolved_alert_template",
blank=True,
null=True,
on_delete=models.SET_NULL,
)
resolved_action_rest = models.ForeignKey(
"core.URLAction",
related_name="resolved_url_action_alert_template",
blank=True,
null=True,
on_delete=models.SET_NULL,
)
resolved_action_args = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
Expand Down
7 changes: 7 additions & 0 deletions api/tacticalrmm/autotasks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class AutomatedTask(BaseAuditModel):
email_alert = models.BooleanField(default=False)
text_alert = models.BooleanField(default=False)
dashboard_alert = models.BooleanField(default=False)
server_task = models.BooleanField(default=False)

# options sent to agent for task creation
# general task settings
Expand Down Expand Up @@ -134,6 +135,10 @@ class AutomatedTask(BaseAuditModel):
run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7
task_instance_policy = models.PositiveSmallIntegerField(blank=True, default=1)

# crontab field for linux/mac tasks
# should only be the schedule, not the script
crontab_schedule = models.CharField(max_length=100, null=True, blank=True)

# deprecated
managed_by_policy = models.BooleanField(default=False)

Expand Down Expand Up @@ -472,6 +477,8 @@ class Meta:
"agents.Agent",
related_name="taskresults",
on_delete=models.CASCADE,
blank=True,
null=True,
)
task = models.ForeignKey(
"autotasks.AutomatedTask",
Expand Down
Loading

0 comments on commit cf169e7

Please sign in to comment.