Skip to content

Commit 5c9d7da

Browse files
author
soeren
committed
Harden security model, enforce authorization workflow, and add audit automation
1 parent 0beeb66 commit 5c9d7da

66 files changed

Lines changed: 1942 additions & 1005 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ SESSION_LIFETIME=120
3232
SESSION_ENCRYPT=false
3333
SESSION_PATH=/
3434
SESSION_DOMAIN=null
35+
SESSION_SECURE_COOKIE=true
36+
SESSION_SAME_SITE=lax
37+
38+
TRUSTED_HOSTS=
39+
TRUSTED_PROXIES=
40+
API_RATE_LIMIT_PER_MINUTE=60
41+
SECURITY_FORCE_HSTS=false
42+
UPLOAD_MAX_KB=10240
43+
UPLOAD_ALLOWED_EXTENSIONS=pdf,txt,csv,json,xml,md,doc,docx,xls,xlsx,png,jpg,jpeg,zip
44+
CORS_ALLOWED_ORIGINS=
3545

3646
BROADCAST_CONNECTION=log
3747
FILESYSTEM_DISK=local
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: Security Audit
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
- master
9+
schedule:
10+
- cron: "0 5 * * 1"
11+
12+
jobs:
13+
dependency-audit:
14+
name: Dependency Audit
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Setup PHP
21+
uses: shivammathur/setup-php@v2
22+
with:
23+
php-version: "8.3"
24+
tools: composer
25+
extensions: mbstring, sqlite, pdo_sqlite
26+
27+
- name: Setup Node.js
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version: "22"
31+
cache: npm
32+
33+
- name: Install PHP dependencies
34+
run: composer install --no-interaction --no-progress --prefer-dist
35+
36+
- name: Install Node dependencies
37+
run: npm ci --no-audit --fund=false
38+
39+
- name: Run Composer Audit
40+
run: composer audit --no-interaction
41+
42+
- name: Run NPM Audit (production)
43+
run: npm audit --omit=dev --audit-level=high
44+
45+
secret-scan:
46+
name: Secret Scan
47+
runs-on: ubuntu-latest
48+
steps:
49+
- name: Checkout
50+
uses: actions/checkout@v4
51+
with:
52+
fetch-depth: 0
53+
54+
- name: Gitleaks
55+
uses: gitleaks/gitleaks-action@v2
56+
env:
57+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58+
with:
59+
args: detect --source . --redact --verbose
60+
61+
sast:
62+
name: SAST
63+
runs-on: ubuntu-latest
64+
steps:
65+
- name: Checkout
66+
uses: actions/checkout@v4
67+
68+
- name: Semgrep
69+
uses: returntocorp/semgrep-action@v1
70+
with:
71+
config: |
72+
p/php
73+
p/owasp-top-ten
74+
p/secrets
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Enums\UserRole;
6+
use App\Models\User;
7+
use Illuminate\Console\Command;
8+
9+
class SetUserRole extends Command
10+
{
11+
protected $signature = 'user:set-role
12+
{user : User ID or email address}
13+
{role : admin, editor, or viewer}
14+
{--abilities= : Comma-separated custom abilities to set}';
15+
16+
protected $description = 'Set role and optional custom abilities for an existing user.';
17+
18+
public function handle(): int
19+
{
20+
$identifier = (string) $this->argument('user');
21+
$roleValue = strtolower((string) $this->argument('role'));
22+
$abilitiesOption = trim((string) $this->option('abilities'));
23+
24+
$user = User::query()
25+
->when(is_numeric($identifier), fn ($query) => $query->whereKey((int) $identifier))
26+
->when(! is_numeric($identifier), fn ($query) => $query->where('email', $identifier))
27+
->first();
28+
29+
if (! $user) {
30+
$this->error('User not found.');
31+
32+
return self::FAILURE;
33+
}
34+
35+
$role = UserRole::tryFrom($roleValue);
36+
if (! $role) {
37+
$this->error('Invalid role. Allowed: admin, editor, viewer.');
38+
39+
return self::FAILURE;
40+
}
41+
42+
$abilities = null;
43+
if ($abilitiesOption !== '') {
44+
$validAbilities = config('authorization.abilities', []);
45+
$parsedAbilities = array_values(array_unique(array_filter(array_map(
46+
static fn (string $ability): string => trim($ability),
47+
explode(',', $abilitiesOption)
48+
))));
49+
50+
$invalidAbilities = array_values(array_diff($parsedAbilities, $validAbilities));
51+
if (! empty($invalidAbilities)) {
52+
$this->error('Invalid abilities: '.implode(', ', $invalidAbilities));
53+
54+
return self::FAILURE;
55+
}
56+
57+
$abilities = $parsedAbilities;
58+
}
59+
60+
$user->forceFill([
61+
'role' => $role,
62+
'abilities' => $abilities,
63+
])->save();
64+
65+
$this->info(sprintf(
66+
'Updated user %s (%s) to role %s.',
67+
(string) $user->id,
68+
(string) $user->email,
69+
$role->value
70+
));
71+
72+
return self::SUCCESS;
73+
}
74+
}

app/Enums/UserRole.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace App\Enums;
4+
5+
enum UserRole: string
6+
{
7+
case ADMIN = 'admin';
8+
case EDITOR = 'editor';
9+
case VIEWER = 'viewer';
10+
}

app/Filament/Resources/Software/RelationManagers/VersionsRelationManager.php

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22

33
namespace App\Filament\Resources\Software\RelationManagers;
44

5-
use App\Enums\ApprovalStatus;
6-
use App\Enums\VersionStatus;
75
use Filament\Actions\BulkActionGroup;
86
use Filament\Actions\CreateAction;
97
use Filament\Actions\DeleteAction;
108
use Filament\Actions\DeleteBulkAction;
119
use Filament\Actions\EditAction;
1210
use Filament\Forms\Components\DatePicker;
13-
use Filament\Forms\Components\Select;
1411
use Filament\Forms\Components\TextInput;
1512
use Filament\Resources\RelationManagers\RelationManager;
1613
use Filament\Schemas\Schema;
@@ -29,14 +26,6 @@ public function form(Schema $schema): Schema
2926
->required(),
3027
DatePicker::make('release_date')
3128
->required(),
32-
Select::make('status')
33-
->options(VersionStatus::class)
34-
->default('draft')
35-
->required(),
36-
Select::make('approval_status')
37-
->options(ApprovalStatus::class)
38-
->default('pending')
39-
->required(),
4029
DatePicker::make('eol_date'),
4130
DatePicker::make('lts_date'),
4231
TextInput::make('support_status'),

app/Filament/Resources/Software/Schemas/SoftwareForm.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace App\Filament\Resources\Software\Schemas;
44

55
use App\Enums\SoftwareStatus;
6-
use Filament\Forms\Components\DatePicker;
76
use Filament\Forms\Components\Select;
87
use Filament\Forms\Components\Textarea;
98
use Filament\Forms\Components\TextInput;
@@ -32,11 +31,6 @@ public static function configure(Schema $schema): Schema
3231
->options($statusOptions)
3332
->default(SoftwareStatus::ACTIVE->value)
3433
->required(),
35-
TextInput::make('current_version')
36-
->label(__('filament.software.fields.current_version'))
37-
->maxLength(50),
38-
DatePicker::make('last_release_date')
39-
->label(__('filament.software.fields.last_release_date')),
4034
TextInput::make('license_type')
4135
->label(__('filament.software.fields.license_type'))
4236
->maxLength(255),

app/Filament/Resources/Versions/Schemas/VersionForm.php

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
namespace App\Filament\Resources\Versions\Schemas;
44

5-
use App\Enums\ApprovalStatus;
6-
use App\Enums\VersionStatus;
75
use Filament\Forms\Components\DatePicker;
86
use Filament\Forms\Components\Select;
97
use Filament\Forms\Components\TextInput;
@@ -13,9 +11,6 @@ class VersionForm
1311
{
1412
public static function configure(Schema $schema): Schema
1513
{
16-
$statusOptions = collect(VersionStatus::cases())->mapWithKeys(fn (VersionStatus $case) => [$case->value => $case->label()])->all();
17-
$approvalOptions = collect(ApprovalStatus::cases())->mapWithKeys(fn (ApprovalStatus $case) => [$case->value => $case->label()])->all();
18-
1914
return $schema
2015
->components([
2116
Select::make('software_id')
@@ -31,16 +26,6 @@ public static function configure(Schema $schema): Schema
3126
DatePicker::make('release_date')
3227
->label(__('filament.versions.fields.release_date'))
3328
->required(),
34-
Select::make('status')
35-
->label(__('filament.versions.fields.status'))
36-
->options($statusOptions)
37-
->default(VersionStatus::DRAFT->value)
38-
->required(),
39-
Select::make('approval_status')
40-
->label(__('filament.versions.fields.approval_status'))
41-
->options($approvalOptions)
42-
->default(ApprovalStatus::PENDING->value)
43-
->required(),
4429
DatePicker::make('eol_date')
4530
->label(__('filament.versions.fields.eol_date')),
4631
DatePicker::make('lts_date')

app/Http/Controllers/Api/FileAttachmentController.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use App\Models\Version;
1111
use Illuminate\Http\JsonResponse;
1212
use Illuminate\Support\Facades\Storage;
13+
use Illuminate\Support\Str;
1314

1415
class FileAttachmentController extends Controller
1516
{
@@ -31,7 +32,7 @@ public function store(StoreFileAttachmentRequest $request, Version $version): Js
3132
$path = $uploadedFile->store("attachments/{$version->id}", $disk);
3233

3334
$attachment = $version->fileAttachments()->create([
34-
'filename' => $uploadedFile->getClientOriginalName(),
35+
'filename' => $this->sanitizeFilename($uploadedFile->getClientOriginalName()),
3536
'file_path' => $path,
3637
'mime_type' => $uploadedFile->getClientMimeType(),
3738
'size' => $uploadedFile->getSize(),
@@ -63,7 +64,7 @@ public function update(UpdateFileAttachmentRequest $request, Version $version, F
6364
$path = $uploadedFile->store("attachments/{$version->id}", $disk);
6465

6566
$fileAttachment->update([
66-
'filename' => $uploadedFile->getClientOriginalName(),
67+
'filename' => $this->sanitizeFilename($uploadedFile->getClientOriginalName()),
6768
'file_path' => $path,
6869
'mime_type' => $uploadedFile->getClientMimeType(),
6970
'size' => $uploadedFile->getSize(),
@@ -89,4 +90,15 @@ protected function ensureRelationship(Version $version, FileAttachment $attachme
8990
{
9091
abort_if($attachment->version_id !== $version->id, 404);
9192
}
93+
94+
protected function sanitizeFilename(string $originalName): string
95+
{
96+
$basename = pathinfo($originalName, PATHINFO_FILENAME);
97+
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
98+
99+
$safeBase = preg_replace('/[^A-Za-z0-9._ -]/', '-', $basename) ?: 'file';
100+
$safeBase = trim((string) Str::of($safeBase)->squish()->limit(100, ''));
101+
102+
return $extension !== '' ? $safeBase.'.'.$extension : $safeBase;
103+
}
92104
}

app/Http/Controllers/Controller.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
namespace App\Http\Controllers;
44

5-
abstract class Controller
5+
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
6+
use Illuminate\Foundation\Validation\ValidatesRequests;
7+
use Illuminate\Routing\Controller as BaseController;
8+
9+
abstract class Controller extends BaseController
610
{
7-
//
11+
use AuthorizesRequests, ValidatesRequests;
812
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Symfony\Component\HttpFoundation\Response;
8+
9+
class SecurityHeaders
10+
{
11+
public function handle(Request $request, Closure $next): Response
12+
{
13+
$response = $next($request);
14+
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
15+
$response->headers->set('X-Content-Type-Options', 'nosniff');
16+
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
17+
$response->headers->set(
18+
'Permissions-Policy',
19+
'camera=(), geolocation=(), microphone=(), payment=(), usb=()'
20+
);
21+
22+
if ($request->isSecure() || config('security.force_hsts', false)) {
23+
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
24+
}
25+
26+
return $response;
27+
}
28+
}

0 commit comments

Comments
 (0)