From 33ab762f5aa35d33de5df9d5160a89104571d538 Mon Sep 17 00:00:00 2001
From: DaneEveritt <dane@daneeveritt.com>
Date: Sat, 9 Jul 2022 19:30:38 -0400
Subject: [PATCH] Add support for tracking more SFTP specific events

---
 .../Remote/SftpAuthenticationController.php   | 21 +++++++++++++++++++
 .../Api/Client/ActivityLogTransformer.php     |  2 +-
 resources/lang/en/activity.php                |  7 +++++++
 .../elements/activity/ActivityLogEntry.tsx    | 13 +++++++-----
 .../elements/activity/style.module.css        |  6 +-----
 5 files changed, 38 insertions(+), 11 deletions(-)

diff --git a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php
index ceaa83eb..c71c4a59 100644
--- a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php
+++ b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php
@@ -6,6 +6,7 @@ use Illuminate\Http\Request;
 use Pterodactyl\Models\User;
 use Pterodactyl\Models\Server;
 use Illuminate\Http\JsonResponse;
+use Pterodactyl\Facades\Activity;
 use Pterodactyl\Models\Permission;
 use phpseclib3\Crypt\PublicKeyLoader;
 use Pterodactyl\Http\Controllers\Controller;
@@ -51,6 +52,8 @@ class SftpAuthenticationController extends Controller
 
         if ($request->input('type') !== 'public_key') {
             if (!password_verify($request->input('password'), $user->password)) {
+                Activity::event('auth:sftp.fail')->property('method', 'password')->subject($user)->log();
+
                 $this->reject($request);
             }
         } else {
@@ -62,13 +65,29 @@ class SftpAuthenticationController extends Controller
             }
 
             if (!$key || !$user->sshKeys()->where('fingerprint', $key->getFingerprint('sha256'))->exists()) {
+                // We don't log here because of the way the SFTP system works. This endpoint
+                // will get hit for every key the user provides, which could be 4 or 5. That is
+                // a lot of unnecessary log noise.
+                //
+                // For now, we'll only log failures due to a bad password as those are not likely
+                // to occur more than once in a session for the user, and are more likely to be of
+                // value to the end user.
                 $this->reject($request, is_null($key));
             }
         }
 
         $this->validateSftpAccess($user, $server);
 
+        Activity::event('auth:sftp.success')->actor($user)
+            ->subject($user)
+            ->property(array_filter([
+                'method' => isset($key) ? 'ssh_key' : 'password',
+                'fingerprint' => isset($key) ? 'SHA256:' . $key->getFingerprint('sha256') : null,
+            ]))
+            ->log();
+
         return new JsonResponse([
+            'user' => $user->uuid,
             'server' => $server->uuid,
             'permissions' => $this->permissions->handle($server, $user),
         ]);
@@ -136,6 +155,8 @@ class SftpAuthenticationController extends Controller
             $permissions = $this->permissions->handle($server, $user);
 
             if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) {
+                Activity::event('server:sftp.denied')->actor($user)->subject($server)->log();
+
                 throw new HttpForbiddenException('You do not have permission to access SFTP for this server.');
             }
         }
diff --git a/app/Transformers/Api/Client/ActivityLogTransformer.php b/app/Transformers/Api/Client/ActivityLogTransformer.php
index 090527a3..25a7562f 100644
--- a/app/Transformers/Api/Client/ActivityLogTransformer.php
+++ b/app/Transformers/Api/Client/ActivityLogTransformer.php
@@ -92,7 +92,7 @@ class ActivityLogTransformer extends BaseClientTransformer
         $str = trans('activity.' . str_replace(':', '.', $model->event));
         preg_match_all('/:(?<key>[\w.-]+\w)(?:[^\w:]?|$)/', $str, $matches);
 
-        $exclude = array_merge($matches['key'], ['ip', 'useragent']);
+        $exclude = array_merge($matches['key'], ['ip', 'useragent', 'using_sftp']);
         foreach ($model->properties->keys() as $key) {
             if (!in_array($key, $exclude, true)) {
                 return true;
diff --git a/resources/lang/en/activity.php b/resources/lang/en/activity.php
index 63d9929e..51bf7733 100644
--- a/resources/lang/en/activity.php
+++ b/resources/lang/en/activity.php
@@ -16,6 +16,10 @@ return [
         'recovery-token' => 'Used two-factor recovery token',
         'token' => 'Solved two-factor challenge',
         'ip-blocked' => 'Blocked request from unlisted IP address for :identifier',
+        'sftp' => [
+            'success' => 'Logged in using SFTP',
+            'fail' => 'Failed SFTP log in',
+        ],
     ],
     'user' => [
         'account' => [
@@ -96,6 +100,9 @@ return [
             'update' => 'Updated the ":action" task for the :name schedule',
             'delete' => 'Deleted a task for the :name schedule',
         ],
+        'sftp' => [
+            'denied' => 'Blocked SFTP access due to permissions',
+        ],
         'settings' => [
             'rename' => 'Renamed the server from :old to :new',
         ],
diff --git a/resources/scripts/components/elements/activity/ActivityLogEntry.tsx b/resources/scripts/components/elements/activity/ActivityLogEntry.tsx
index d6f75157..b1292353 100644
--- a/resources/scripts/components/elements/activity/ActivityLogEntry.tsx
+++ b/resources/scripts/components/elements/activity/ActivityLogEntry.tsx
@@ -5,7 +5,7 @@ import Translate from '@/components/elements/Translate';
 import { format, formatDistanceToNowStrict } from 'date-fns';
 import { ActivityLog } from '@definitions/user';
 import ActivityLogMetaButton from '@/components/elements/activity/ActivityLogMetaButton';
-import { TerminalIcon } from '@heroicons/react/solid';
+import { FolderOpenIcon, TerminalIcon } from '@heroicons/react/solid';
 import classNames from 'classnames';
 import style from './style.module.css';
 import Avatar from '@/components/Avatar';
@@ -65,10 +65,13 @@ export default ({ activity, children }: Props) => {
                         </Link>
                         <div className={classNames(style.icons, 'group-hover:text-gray-300')}>
                             {activity.isApi && (
-                                <Tooltip placement={'top'} content={'Performed using API Key'}>
-                                    <span>
-                                        <TerminalIcon />
-                                    </span>
+                                <Tooltip placement={'top'} content={'Using API Key'}>
+                                    <TerminalIcon />
+                                </Tooltip>
+                            )}
+                            {activity.properties.using_sftp && (
+                                <Tooltip placement={'top'} content={'Using SFTP'}>
+                                    <FolderOpenIcon />
                                 </Tooltip>
                             )}
                             {children}
diff --git a/resources/scripts/components/elements/activity/style.module.css b/resources/scripts/components/elements/activity/style.module.css
index b779bce0..066f8cb3 100644
--- a/resources/scripts/components/elements/activity/style.module.css
+++ b/resources/scripts/components/elements/activity/style.module.css
@@ -1,12 +1,8 @@
 .icons {
     @apply flex space-x-1 mx-2 transition-colors duration-100 text-gray-400;
 
-    & > span {
-        @apply px-1 py-px cursor-pointer hover:text-gray-50;
-    }
-
     & svg {
-        @apply w-4 h-4;
+        @apply px-1 py-px cursor-pointer hover:text-gray-50 h-5 w-auto;
     }
 }