Skip to content

Commit ccecaa6

Browse files
committed
Add basic auditing for filesystem actions
Specifically skipping read actions since there isn't much to say there, and it generally wouldn't be very helpful (plus, likely to generate lots of logs).
1 parent b15679d commit ccecaa6

File tree

5 files changed

+164
-64
lines changed

5 files changed

+164
-64
lines changed

app/Http/Controllers/Api/Client/Servers/BackupController.php

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Pterodactyl\Models\Backup;
66
use Pterodactyl\Models\Server;
7+
use Pterodactyl\Models\AuditLog;
78
use Illuminate\Http\JsonResponse;
89
use Pterodactyl\Services\Backups\DeleteBackupService;
910
use Pterodactyl\Repositories\Eloquent\BackupRepository;
@@ -61,6 +62,7 @@ public function __construct(
6162
public function index(GetBackupsRequest $request, Server $server)
6263
{
6364
$limit = min($request->query('per_page') ?? 20, 50);
65+
6466
return $this->fractal->collection($server->backups()->paginate($limit))
6567
->transformWith($this->getTransformer(BackupTransformer::class))
6668
->toArray();
@@ -77,11 +79,18 @@ public function index(GetBackupsRequest $request, Server $server)
7779
*/
7880
public function store(StoreBackupRequest $request, Server $server)
7981
{
80-
$backup = $this->initiateBackupService
81-
->setIgnoredFiles(
82-
explode(PHP_EOL, $request->input('ignored') ?? '')
83-
)
84-
->handle($server, $request->input('name'));
82+
/** @var \Pterodactyl\Models\Backup $backup */
83+
$backup = $server->audit(AuditLog::ACTION_SERVER_BACKUP_STARTED, function (AuditLog $model, Server $server) use ($request) {
84+
$backup = $this->initiateBackupService
85+
->setIgnoredFiles(
86+
explode(PHP_EOL, $request->input('ignored') ?? '')
87+
)
88+
->handle($server, $request->input('name'));
89+
90+
$model->metadata = ['backup_uuid' => $backup->uuid];
91+
92+
return $backup;
93+
});
8594

8695
return $this->fractal->item($backup)
8796
->transformWith($this->getTransformer(BackupTransformer::class))
@@ -116,8 +125,10 @@ public function view(GetBackupsRequest $request, Server $server, Backup $backup)
116125
*/
117126
public function delete(DeleteBackupRequest $request, Server $server, Backup $backup)
118127
{
119-
$this->deleteBackupService->handle($backup);
128+
$server->audit(AuditLog::ACTION_SERVER_BACKUP_DELETED, function () use ($backup) {
129+
$this->deleteBackupService->handle($backup);
130+
});
120131

121-
return JsonResponse::create([], JsonResponse::HTTP_NO_CONTENT);
132+
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
122133
}
123134
}

app/Http/Controllers/Api/Client/Servers/FileController.php

Lines changed: 93 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
use Carbon\CarbonImmutable;
66
use Illuminate\Http\Response;
77
use Pterodactyl\Models\Server;
8+
use Pterodactyl\Models\AuditLog;
89
use Illuminate\Http\JsonResponse;
9-
use Illuminate\Support\Collection;
1010
use Pterodactyl\Services\Nodes\NodeJWTService;
1111
use Illuminate\Contracts\Routing\ResponseFactory;
1212
use Pterodactyl\Repositories\Wings\DaemonFileRepository;
@@ -87,18 +87,15 @@ public function directory(ListFilesRequest $request, Server $server): array
8787
* @param \Pterodactyl\Models\Server $server
8888
* @return \Illuminate\Http\Response
8989
*
90-
* @throws \Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException
91-
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
90+
* @throws \Throwable
9291
*/
9392
public function contents(GetFileContentsRequest $request, Server $server): Response
9493
{
95-
return new Response(
96-
$this->fileRepository->setServer($server)->getContent(
97-
$request->get('file'), config('pterodactyl.files.max_edit_size')
98-
),
99-
Response::HTTP_OK,
100-
['Content-Type' => 'text/plain']
94+
$response = $this->fileRepository->setServer($server)->getContent(
95+
$request->get('file'), config('pterodactyl.files.max_edit_size')
10196
);
97+
98+
return new Response($response, Response::HTTP_OK, ['Content-Type' => 'text/plain']);
10299
}
103100

104101
/**
@@ -109,17 +106,21 @@ public function contents(GetFileContentsRequest $request, Server $server): Respo
109106
* @param \Pterodactyl\Models\Server $server
110107
* @return array
111108
*
112-
* @throws \Exception
109+
* @throws \Throwable
113110
*/
114111
public function download(GetFileContentsRequest $request, Server $server)
115112
{
116-
$token = $this->jwtService
117-
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
118-
->setClaims([
119-
'file_path' => rawurldecode($request->get('file')),
120-
'server_uuid' => $server->uuid,
121-
])
122-
->handle($server->node, $request->user()->id . $server->uuid);
113+
$token = $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_DOWNLOAD, function (AuditLog $audit, Server $server) use ($request) {
114+
$audit->metadata = ['file' => $request->get('file')];
115+
116+
return $this->jwtService
117+
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
118+
->setClaims([
119+
'file_path' => rawurldecode($request->get('file')),
120+
'server_uuid' => $server->uuid,
121+
])
122+
->handle($server->node, $request->user()->id . $server->uuid);
123+
});
123124

124125
return [
125126
'object' => 'signed_url',
@@ -140,11 +141,20 @@ public function download(GetFileContentsRequest $request, Server $server)
140141
* @param \Pterodactyl\Models\Server $server
141142
* @return \Illuminate\Http\JsonResponse
142143
*
143-
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
144+
* @throws \Throwable
144145
*/
145146
public function write(WriteFileContentRequest $request, Server $server): JsonResponse
146147
{
147-
$this->fileRepository->setServer($server)->putContent($request->get('file'), $request->getContent());
148+
$server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) {
149+
$audit->metadata = [
150+
'file' => $request->get('file'),
151+
'sub_action' => 'write_content',
152+
];
153+
154+
$this->fileRepository
155+
->setServer($server)
156+
->putContent($request->get('file'), $request->getContent());
157+
});
148158

149159
return new JsonResponse([], Response::HTTP_NO_CONTENT);
150160
}
@@ -156,13 +166,20 @@ public function write(WriteFileContentRequest $request, Server $server): JsonRes
156166
* @param \Pterodactyl\Models\Server $server
157167
* @return \Illuminate\Http\JsonResponse
158168
*
159-
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
169+
* @throws \Throwable
160170
*/
161171
public function create(CreateFolderRequest $request, Server $server): JsonResponse
162172
{
163-
$this->fileRepository
164-
->setServer($server)
165-
->createDirectory($request->input('name'), $request->input('root', '/'));
173+
$server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) {
174+
$audit->metadata = [
175+
'file' => $request->input('root', '/') . $request->input('name'),
176+
'sub_action' => 'create_folder',
177+
];
178+
179+
$this->fileRepository
180+
->setServer($server)
181+
->createDirectory($request->input('name'), $request->input('root', '/'));
182+
});
166183

167184
return new JsonResponse([], Response::HTTP_NO_CONTENT);
168185
}
@@ -174,13 +191,17 @@ public function create(CreateFolderRequest $request, Server $server): JsonRespon
174191
* @param \Pterodactyl\Models\Server $server
175192
* @return \Illuminate\Http\JsonResponse
176193
*
177-
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
194+
* @throws \Throwable
178195
*/
179196
public function rename(RenameFileRequest $request, Server $server): JsonResponse
180197
{
181-
$this->fileRepository
182-
->setServer($server)
183-
->renameFiles($request->input('root'), $request->input('files'));
198+
$server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_RENAME, function (AuditLog $audit, Server $server) use ($request) {
199+
$audit->metadata = ['root' => $request->input('root'), 'files' => $request->input('files')];
200+
201+
$this->fileRepository
202+
->setServer($server)
203+
->renameFiles($request->input('root'), $request->input('files'));
204+
});
184205

185206
return new JsonResponse([], Response::HTTP_NO_CONTENT);
186207
}
@@ -192,13 +213,19 @@ public function rename(RenameFileRequest $request, Server $server): JsonResponse
192213
* @param \Pterodactyl\Models\Server $server
193214
* @return \Illuminate\Http\JsonResponse
194215
*
195-
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
216+
* @throws \Throwable
196217
*/
197218
public function copy(CopyFileRequest $request, Server $server): JsonResponse
198219
{
199-
$this->fileRepository
200-
->setServer($server)
201-
->copyFile($request->input('location'));
220+
$server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) {
221+
$audit->metadata = [
222+
'file' => $request->input('location'),
223+
'sub_action' => 'copy_file',
224+
];
225+
$this->fileRepository
226+
->setServer($server)
227+
->copyFile($request->input('location'));
228+
});
202229

203230
return new JsonResponse([], Response::HTTP_NO_CONTENT);
204231
}
@@ -208,17 +235,21 @@ public function copy(CopyFileRequest $request, Server $server): JsonResponse
208235
* @param \Pterodactyl\Models\Server $server
209236
* @return array
210237
*
211-
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
238+
* @throws \Throwable
212239
*/
213240
public function compress(CompressFilesRequest $request, Server $server): array
214241
{
215-
// Allow up to five minutes for this request to process before timing out.
216-
set_time_limit(300);
242+
$file = $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_COMPRESS, function (AuditLog $audit, Server $server) use ($request) {
243+
// Allow up to five minutes for this request to process before timing out.
244+
set_time_limit(300);
217245

218-
$file = $this->fileRepository->setServer($server)
219-
->compressFiles(
220-
$request->input('root'), $request->input('files')
221-
);
246+
$audit->metadata = ['root' => $request->input('root'), 'files' => $request->input('files')];
247+
248+
return $this->fileRepository->setServer($server)
249+
->compressFiles(
250+
$request->input('root'), $request->input('files')
251+
);
252+
});
222253

223254
return $this->fractal->item($file)
224255
->transformWith($this->getTransformer(FileObjectTransformer::class))
@@ -230,15 +261,19 @@ public function compress(CompressFilesRequest $request, Server $server): array
230261
* @param \Pterodactyl\Models\Server $server
231262
* @return \Illuminate\Http\JsonResponse
232263
*
233-
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
264+
* @throws \Throwable
234265
*/
235266
public function decompress(DecompressFilesRequest $request, Server $server): JsonResponse
236267
{
237-
// Allow up to five minutes for this request to process before timing out.
238-
set_time_limit(300);
268+
$file = $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_DECOMPRESS, function (AuditLog $audit, Server $server) use ($request) {
269+
// Allow up to five minutes for this request to process before timing out.
270+
set_time_limit(300);
239271

240-
$this->fileRepository->setServer($server)
241-
->decompressFile($request->input('root'), $request->input('file'));
272+
$audit->metadata = ['root' => $request->input('root'), 'files' => $request->input('file')];
273+
274+
$this->fileRepository->setServer($server)
275+
->decompressFile($request->input('root'), $request->input('file'));
276+
});
242277

243278
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
244279
}
@@ -250,14 +285,18 @@ public function decompress(DecompressFilesRequest $request, Server $server): Jso
250285
* @param \Pterodactyl\Models\Server $server
251286
* @return \Illuminate\Http\JsonResponse
252287
*
253-
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
288+
* @throws \Throwable
254289
*/
255290
public function delete(DeleteFileRequest $request, Server $server): JsonResponse
256291
{
257-
$this->fileRepository->setServer($server)
258-
->deleteFiles(
259-
$request->input('root'), $request->input('files')
260-
);
292+
$server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_DELETE, function (AuditLog $audit, Server $server) use ($request) {
293+
$audit->metadata = ['root' => $request->input('root'), 'files' => $request->input('files')];
294+
295+
$this->fileRepository->setServer($server)
296+
->deleteFiles(
297+
$request->input('root'), $request->input('files')
298+
);
299+
});
261300

262301
return new JsonResponse([], Response::HTTP_NO_CONTENT);
263302
}
@@ -288,11 +327,15 @@ public function chmod(ChmodFilesRequest $request, Server $server): JsonResponse
288327
* @param \Pterodactyl\Models\Server $server
289328
* @return \Illuminate\Http\JsonResponse
290329
*
291-
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
330+
* @throws \Throwable
292331
*/
293332
public function pull(PullFileRequest $request, Server $server): JsonResponse
294333
{
295-
$this->fileRepository->setServer($server)->pull($request->input('url'), $request->input('directory'));
334+
$server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_PULL, function (AuditLog $audit, Server $server) use ($request) {
335+
$audit->metadata = ['directory' => $request->input('directory'), 'url' => $request->input('url')];
336+
337+
$this->fileRepository->setServer($server)->pull($request->input('url'), $request->input('directory'));
338+
});
296339

297340
return new JsonResponse([], Response::HTTP_NO_CONTENT);
298341
}

app/Models/AuditLog.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ class AuditLog extends Model
2828
const ACTION_USER_AUTH_FAILED = 'user:auth.failed';
2929
const ACTION_USER_AUTH_PASSWORD_CHANGED = 'user:auth.password-changed';
3030

31+
const ACTION_SERVER_FILESYSTEM_DOWNLOAD = 'server:filesystem.download';
32+
const ACTION_SERVER_FILESYSTEM_WRITE = 'server:filesystem.write';
33+
const ACTION_SERVER_FILESYSTEM_DELETE = 'server:filesystem.delete';
34+
const ACTION_SERVER_FILESYSTEM_RENAME = 'server:filesystem.rename';
35+
const ACTION_SERVER_FILESYSTEM_COMPRESS = 'server:filesystem.compress';
36+
const ACTION_SERVER_FILESYSTEM_DECOMPRESS = 'server:filesystem.decompress';
37+
const ACTION_SERVER_FILESYSTEM_PULL = 'server:filesystem.pull';
38+
39+
const ACTION_SERVER_BACKUP_STARTED = 'server:backup.started';
40+
const ACTION_SERVER_BACKUP_FAILED = 'server:backup.failed';
41+
const ACTION_SERVER_BACKUP_COMPELTED = 'server:backup.completed';
42+
const ACTION_SERVER_BACKUP_DELETED = 'server:backup.deleted';
3143
const ACTION_SERVER_BACKUP_RESTORE_STARTED = 'server:backup.restore.started';
3244
const ACTION_SERVER_BACKUP_RESTORE_COMPLETED = 'server:backup.restore.completed';
3345
const ACTION_SERVER_BACKUP_RESTORE_FAILED = 'server:backup.restore.failed';
@@ -38,7 +50,7 @@ class AuditLog extends Model
3850
public static $validationRules = [
3951
'uuid' => 'required|uuid',
4052
'action' => 'required|string',
41-
'device' => 'required|array',
53+
'device' => 'array',
4254
'device.ip_address' => 'ip',
4355
'device.user_agent' => 'string',
4456
'metadata' => 'array',
@@ -100,14 +112,14 @@ public static function factory(string $action, array $metadata, bool $isSystem =
100112
{
101113
/** @var \Illuminate\Http\Request $request */
102114
$request = Container::getInstance()->make('request');
103-
if (! $isSystem || ! $request instanceof Request) {
115+
if ($isSystem || ! $request instanceof Request) {
104116
$request = null;
105117
}
106118

107119
return (new self())->fill([
108120
'uuid' => Uuid::uuid4()->toString(),
109121
'is_system' => $isSystem,
110-
'user_id' => $request->user() ? $request->user()->id : null,
122+
'user_id' => ($request && $request->user()) ? $request->user()->id : null,
111123
'server_id' => null,
112124
'action' => $action,
113125
'device' => $request ? [

0 commit comments

Comments
 (0)