Premier commit

This commit is contained in:
jeremy bayse
2026-02-09 11:27:21 +01:00
commit 89a369964d
114 changed files with 17837 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Console\Commands;
use App\Models\Contract;
use App\Models\User;
use App\Mail\ContractExpiringNotification;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class SendContractAlerts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'contracts:check-expirations';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check for expiring contracts and notify admins';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Checking for expiring contracts...');
$expiringContracts = Contract::where('status', '!=', 'expired') // Assuming 'active' or 'pending'
->where('end_date', '<=', now()->addDays(30))
->where('end_date', '>=', now())
->get();
if ($expiringContracts->isEmpty()) {
$this->info('No contracts expiring soon.');
return;
}
$admins = User::where('role', 'admin')->get();
if ($admins->isEmpty()) {
$this->warn('No admins found to notify.');
return;
}
foreach ($admins as $admin) {
// In a real app, you might group these or send one email per contract/batch
// Here we just simulate sending a notification for the batch
// Or send individual emails
// Simplification: Send one email with list
try {
Mail::to($admin->email)->send(new ContractExpiringNotification($expiringContracts));
$this->info("Notification sent to {$admin->email}");
} catch (\Exception $e) {
$this->error("Failed to send email to {$admin->email}: " . $e->getMessage());
Log::error($e);
}
}
$this->info('Done.');
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\Console\Commands;
use App\Services\CortexXdrService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
class TestCortexConnection extends Command
{
protected $signature = 'cortex:test';
protected $description = 'Test connectivity to Cortex XDR API and display results.';
public function handle(CortexXdrService $cortexService)
{
$this->info("--- Cortex XDR Connectivity Test ---");
// 1. Config Check
$key = Config::get('services.cortex.key');
$id = Config::get('services.cortex.id');
$url = Config::get('services.cortex.url');
$this->line("\n1. Checking Environment Variables:");
$this->line(" - API Key: " . ($key ? 'OK (' . substr($key, 0, 5) . '...)' : 'MISSING'));
$this->line(" - API Key ID: " . ($id ? 'OK (' . $id . ')' : 'MISSING'));
$this->line(" - Base URL: " . ($url ? 'OK (' . $url . ')' : 'MISSING'));
if (!$key || !$id || !$url) {
$this->error("\n[ERROR] Configuration is incomplete.");
$this->line("You need to add the following variables to your .env file:");
if (!$key) $this->line(" - CORTEX_XDR_API_KEY=your_api_key");
if (!$id) $this->line(" - CORTEX_XDR_API_KEY_ID=your_key_id (Integer ID)");
if (!$url) $this->line(" - CORTEX_XDR_BASE_URL=https://api-fqdn.xdr.cortex.paloaltonetworks.com");
$this->warn("\nIMPORTANT: After editing .env, you may need to restart 'php artisan serve' for the web app to see changes.");
return 1;
}
// 2. Test Incidents
$this->info("\n2. Testing API Connection (Incidents)...");
try {
$incidents = $cortexService->getIncidents();
if (empty($incidents)) {
$this->warn("No incidents returned. (This might be normal if no incidents exist, or check API permissions).");
} else {
$this->info("SUCCESS: Fetched " . count($incidents) . " incidents.");
if (count($incidents) > 0) {
$this->line("DEBUG: First Incident ID: " . ($incidents[0]['incident_id'] ?? 'N/A'));
$this->line("DEBUG: Incident keys: " . print_r(array_keys($incidents[0]), true));
$this->line("DEBUG: Incident values: " . print_r(array_values($incidents[0]), true));
}
$this->line("Latest Incident ID: " . ($incidents[0]['incident_id'] ?? 'N/A'));
}
} catch (\Exception $e) {
$this->error("EXCEPTION: " . $e->getMessage());
}
// 3. Test Endpoints
$this->info("\n3. Testing API Connection (Endpoints)...");
try {
$endpoints = $cortexService->getEndpoints();
if (empty($endpoints)) {
$this->warn("No endpoints returned.");
} else {
$this->info("SUCCESS: Fetched " . count($endpoints) . " endpoints.");
if (count($endpoints) > 0) {
$ep = $endpoints[0];
$this->line("DEBUG: First Endpoint Status key: 'endpoint_status' => " . ($ep['endpoint_status'] ?? 'N/A'));
$this->line("DEBUG: Keys available: " . print_r(array_keys($ep), true));
$this->line("DEBUG: First Endpoint Status key: 'endpoint_status' => " . ($ep['endpoint_status'] ?? 'N/A'));
}
}
} catch (\Exception $e) {
$this->error("EXCEPTION: " . $e->getMessage());
}
return 0;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\LicenseLevel;
class LicenseLevelController extends Controller
{
public function index()
{
$levels = LicenseLevel::all();
return view('admin.license_levels.index', compact('levels'));
}
public function store(Request $request)
{
$request->validate(['name' => 'required|unique:license_levels,name']);
LicenseLevel::create($request->only('name'));
return back()->with('success', 'Niveau de licence ajouté.');
}
public function toggle(LicenseLevel $licenseLevel)
{
$licenseLevel->update(['is_active' => !$licenseLevel->is_active]);
return back()->with('success', 'Statut mis à jour.');
}
public function destroy(LicenseLevel $licenseLevel)
{
$licenseLevel->delete();
return back()->with('success', 'Niveau de licence supprimé.');
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Link;
use Illuminate\Http\Request;
class LinkController extends Controller
{
public function index()
{
$links = Link::orderBy('order', 'asc')->get();
return view('admin.links.index', compact('links'));
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'url' => 'required|url',
'icon' => 'nullable|string',
'color' => 'nullable|string',
'order' => 'integer',
]);
Link::create($validated);
return back()->with('success', 'Lien ajouté.');
}
public function edit(Link $link)
{
return view('admin.links.edit', compact('link'));
}
public function update(Request $request, Link $link)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'url' => 'required|url',
'icon' => 'nullable|string',
'color' => 'nullable|string',
'order' => 'integer',
]);
$link->update($validated);
return redirect()->route('admin.links.index')->with('success', 'Lien mis à jour.');
}
public function toggle(Link $link)
{
$link->update(['is_active' => !$link->is_active]);
return back()->with('success', 'Statut mis à jour.');
}
public function destroy(Link $link)
{
$link->delete();
return back()->with('success', 'Lien supprimé.');
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Municipality;
class MunicipalityController extends Controller
{
public function index()
{
$municipalities = Municipality::all();
return view('admin.municipalities.index', compact('municipalities'));
}
public function toggle(Municipality $municipality)
{
$municipality->update(['is_active' => !$municipality->is_active]);
return back()->with('success', 'Statut de la commune mis à jour.');
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Defaults\Password;
class AuthController extends Controller
{
public function showLogin()
{
return view('auth.login');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (Auth::attempt($credentials, $request->boolean('remember'))) {
$request->session()->regenerate();
if (!Auth::user()->is_active) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return back()->withErrors(['email' => 'Your account is pending approval by an administrator.']);
}
return redirect()->intended('/dashboard');
}
return back()->withErrors([
'email' => 'The provided credentials do not match our records.',
])->onlyInput('email');
}
public function showRegister()
{
return view('auth.register');
}
public function register(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Password::defaults()],
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
'is_active' => false, // Require approval
'role' => 'reader', // Default
]);
Auth::login($user);
// Notify admin in real app
return redirect('/dashboard')->with('status', 'Account created. Wait for approval.');
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace App\Http\Controllers;
use App\Models\Contract;
use App\Models\ContractMeta;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class ContractController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$contracts = Contract::with('municipality')->latest()->paginate(10);
return view('contracts.index', compact('contracts'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
if (!auth()->user()->isManager()) {
abort(403);
}
$municipalities = \App\Models\Municipality::active()->orderBy('name')->get();
$licenseLevels = \App\Models\LicenseLevel::active()->orderBy('name')->get();
return view('contracts.create', compact('municipalities', 'licenseLevels'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
if (!auth()->user()->isManager()) {
abort(403);
}
$validated = $request->validate([
'name' => 'required|string|max:255',
'reference' => 'nullable|string|unique:contracts',
'provider' => 'required|string',
'municipality_id' => 'nullable|exists:municipalities,id',
'start_date' => 'required|date',
'end_date' => 'nullable|date|after_or_equal:start_date',
'amount' => 'nullable|numeric',
'type' => 'required|string',
'meta' => 'nullable|array', // key-value pairs
]);
$contract = Contract::create($validated);
if ($request->has('meta')) {
foreach ($request->meta as $key => $value) {
if ($value) {
$contract->meta()->create(['key' => $key, 'value' => $value]);
}
}
}
return redirect()->route('contracts.index')
->with('success', 'Contract created successfully.');
}
/**
* Display the specified resource.
*/
public function show(Contract $contract)
{
$contract->load(['meta', 'documents', 'municipality']);
return view('contracts.show', compact('contract'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Contract $contract)
{
if (!auth()->user()->isManager()) {
abort(403);
}
$contract->load('meta');
$municipalities = \App\Models\Municipality::active()->orderBy('name')->get();
$licenseLevels = \App\Models\LicenseLevel::active()->orderBy('name')->get();
return view('contracts.edit', compact('contract', 'municipalities', 'licenseLevels'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Contract $contract)
{
if (!auth()->user()->isManager()) {
abort(403);
}
$validated = $request->validate([
'name' => 'required|string|max:255',
'reference' => 'nullable|string|unique:contracts,reference,' . $contract->id,
'provider' => 'required|string',
'municipality_id' => 'nullable|exists:municipalities,id',
'start_date' => 'required|date',
'end_date' => 'nullable|date|after_or_equal:start_date',
'amount' => 'nullable|numeric',
'status' => 'required|string',
]);
$contract->update($validated);
// Handle Meta Data Update
if ($request->has('meta')) {
foreach ($request->meta as $key => $value) {
if ($value) {
$contract->meta()->updateOrCreate(
['key' => $key],
['value' => $value]
);
} else {
// If value is empty, maybe delete? Or just leave null.
// Let's delete if empty to keep clean
$contract->meta()->where('key', $key)->delete();
}
}
}
return redirect()->route('contracts.index')
->with('success', 'Contract updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Contract $contract)
{
if (!auth()->user()->isAdmin()) { // Only admin can delete? Or manager?
abort(403);
}
$contract->delete();
return redirect()->route('contracts.index')
->with('success', 'Contract deleted.');
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers;
use App\Services\CortexXdrService;
use Illuminate\Http\Request;
class CortexXdrController extends Controller
{
protected $cortex;
public function __construct(CortexXdrService $cortex)
{
$this->cortex = $cortex;
}
public function index()
{
// Return view effectively immediately with a loading state
return view('cortex.index');
}
public function getData(CortexXdrService $cortexService)
{
try {
$summary = $cortexService->getSummary();
return response()->json($summary);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers;
use App\Models\Contract;
use App\Models\AuditLog;
use App\Models\GlobalSetting;
use App\Models\Link;
use Illuminate\Http\Request;
class DashboardController extends Controller
{
/**
* Display the dashboard.
*/
public function index()
{
$contractStats = [
'total' => Contract::count(),
'active' => Contract::active()->count(),
'expiring_soon' => Contract::expiringSoon(30)->count(),
'expired' => Contract::where('status', 'expired')->count(),
'by_type' => Contract::select('type', \DB::raw('count(*) as count'))->groupBy('type')->get()->keyBy('type'),
];
// Recent logs
$recentLogs = AuditLog::with('user')->latest()->take(10)->get();
// Upcoming Contracts (Timeline)
$upcomingContracts = Contract::whereNotNull('end_date')
->whereDate('end_date', '>=', now())
->orderBy('end_date', 'asc')
->take(6)
->get();
// Dashboard Note
$dashboardNote = GlobalSetting::get('dashboard_note');
// External Links
$links = Link::active()->get();
return view('dashboard', compact('contractStats', 'recentLogs', 'upcomingContracts', 'dashboardNote', 'links'));
}
/**
* Update the dashboard note.
*/
public function updateNote(Request $request)
{
GlobalSetting::set('dashboard_note', $request->input('note'));
return back()->with('success', 'Pense-bête mis à jour.');
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use App\Models\Contract;
use App\Models\Document;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class DocumentController extends Controller
{
/**
* Store a new document for a contract.
*/
public function store(Request $request, Contract $contract)
{
// Simple validation
$request->validate([
'file' => 'required|file|mimes:pdf,docx,jpg,png|max:10240', // 10MB limit
'description' => 'nullable|string|max:255',
]);
if ($request->hasFile('file')) {
$path = $request->file('file')->store('contracts/' . $contract->id, 'public');
$contract->documents()->create([
'filename' => $request->file('file')->getClientOriginalName(),
'path' => $path,
'mime_type' => $request->file('file')->getMimeType(),
'size' => $request->file('file')->getSize(),
'description' => $request->input('description'),
'uploaded_by' => auth()->id(),
]);
return back()->with('success', 'Document uploaded successfully.');
}
return back()->with('error', 'No file uploaded.');
}
/**
* Delete a document.
*/
public function destroy(Document $document)
{
// Check permission (manager or admin or uploader?)
if (!auth()->user()->isManager() && auth()->id() !== $document->uploaded_by) {
abort(403);
}
// Delete from storage
Storage::disk('public')->delete($document->path);
$document->delete();
return back()->with('success', 'Document deleted.');
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class MunicipalityController extends Controller
{
public function index()
{
$municipalities = \App\Models\Municipality::active()->orderBy('name')->get();
return view('municipalities.index', compact('municipalities'));
}
public function show(\App\Models\Municipality $municipality)
{
if (!$municipality->is_active) {
abort(404);
}
$municipality->load(['contracts.meta']);
$contracts = $municipality->contracts;
// M365 Statistics
$m365Contracts = $contracts->where('type', 'microsoft_365');
// Fetch all active license levels to initialize stats keys
$licenseLevels = \App\Models\LicenseLevel::active()->pluck('name')->toArray();
$m365Stats = array_fill_keys($licenseLevels, 0);
$m365Stats['Autre'] = 0; // Fallback category
foreach ($m365Contracts as $contract) {
$level = $contract->meta->where('key', 'm365_license_level')->first()?->value;
$quantity = (int) $contract->meta->where('key', 'm365_quantity')->first()?->value ?? 0;
if ($level && array_key_exists($level, $m365Stats)) {
$m365Stats[$level] += $quantity;
} else {
$m365Stats['Autre'] += $quantity;
}
}
return view('municipalities.show', compact('municipality', 'contracts', 'm365Stats'));
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsActive
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (auth()->check() && !auth()->user()->is_active) {
auth()->logout();
return redirect()->route('login')->withErrors(['email' => 'Your account is pending approval by an administrator.']);
}
return $next($request);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsAdmin
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (!$request->user() || !$request->user()->isAdmin()) {
abort(403, 'Unauthorized. Admin access only.');
}
return $next($request);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use Illuminate\Database\Eloquent\Collection;
class ContractExpiringNotification extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(
public Collection $expiringContracts
) {}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Alertes: Contrats arrivant à échéance',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'emails.contracts.expiring',
);
}
}

23
app/Models/AuditLog.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AuditLog extends Model
{
use HasFactory;
protected $guarded = ['id'];
protected $casts = [
'changes' => 'array',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

60
app/Models/Contract.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Contract extends Model
{
use HasFactory;
protected $fillable = [
'name',
'reference',
'provider',
'status',
'start_date',
'end_date',
'amount',
'currency',
'notes',
'type',
'municipality_id',
];
protected $casts = [
'start_date' => 'date',
'end_date' => 'date',
'amount' => 'decimal:2',
];
public function meta(): HasMany
{
return $this->hasMany(ContractMeta::class);
}
public function documents(): HasMany
{
return $this->hasMany(Document::class);
}
public function municipality(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Municipality::class);
}
// Helpers
public function scopeActive($query)
{
return $query->where('status', 'active');
}
public function scopeExpiringSoon($query, $days = 30)
{
return $query->where('end_date', '<=', now()->addDays($days))
->where('end_date', '>=', now())
->where('status', '!=', 'expired');
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ContractMeta extends Model
{
use HasFactory;
protected $table = 'contract_meta'; // singular/plural convention can be tricky if not explicit
protected $fillable = [
'contract_id',
'key',
'value',
];
public function contract(): BelongsTo
{
return $this->belongsTo(Contract::class);
}
}

32
app/Models/Document.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Document extends Model
{
use HasFactory;
protected $fillable = [
'contract_id',
'filename',
'path',
'mime_type',
'size',
'description',
'uploaded_by',
];
public function contract(): BelongsTo
{
return $this->belongsTo(Contract::class);
}
public function uploader(): BelongsTo
{
return $this->belongsTo(User::class, 'uploaded_by');
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class GlobalSetting extends Model
{
protected $fillable = ['key', 'value'];
/**
* Get a global setting value by key.
*/
public static function get(string $key, $default = null)
{
$setting = self::where('key', $key)->first();
return $setting ? $setting->value : $default;
}
/**
* Set a global setting value.
*/
public static function set(string $key, $value)
{
return self::updateOrCreate(
['key' => $key],
['value' => $value]
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class LicenseLevel extends Model
{
protected $fillable = ['name', 'type', 'is_active'];
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}

22
app/Models/Link.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Link extends Model
{
use HasFactory;
protected $fillable = ['title', 'url', 'icon', 'color', 'order', 'is_active'];
protected $casts = [
'is_active' => 'boolean',
];
public function scopeActive($query)
{
return $query->where('is_active', true)->orderBy('order', 'asc');
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Municipality extends Model
{
protected $fillable = ['name', 'zip_code', 'is_active'];
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function contracts()
{
return $this->hasMany(Contract::class);
}
}

66
app/Models/User.php Normal file
View File

@@ -0,0 +1,66 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
'role',
'is_active',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_active' => 'boolean',
];
}
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function isManager(): bool
{
return $this->role === 'manager' || $this->isAdmin();
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Observers;
use App\Models\Contract;
use App\Models\AuditLog;
use Illuminate\Support\Facades\Auth;
class ContractObserver
{
/**
* Handle the Contract "created" event.
*/
public function created(Contract $contract): void
{
AuditLog::create([
'user_id' => Auth::id(),
'action' => 'contract_created',
'description' => "Contract {$contract->name} created.",
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'changes' => $contract->getAttributes(),
]);
}
/**
* Handle the Contract "updated" event.
*/
public function updated(Contract $contract): void
{
AuditLog::create([
'user_id' => Auth::id(),
'action' => 'contract_updated',
'description' => "Contract {$contract->name} updated.",
'ip_address' => request()->ip(),
'changes' => $contract->getChanges(),
]);
}
/**
* Handle the Contract "deleted" event.
*/
public function deleted(Contract $contract): void
{
AuditLog::create([
'user_id' => Auth::id(),
'action' => 'contract_deleted',
'description' => "Contract {$contract->name} deleted.",
'ip_address' => request()->ip(),
]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\AuditLog;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class AuditLogPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, AuditLog $auditLog): bool
{
return false;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, AuditLog $auditLog): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, AuditLog $auditLog): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, AuditLog $auditLog): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, AuditLog $auditLog): bool
{
return false;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Contract;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ContractPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Contract $contract): bool
{
return false;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Contract $contract): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Contract $contract): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Contract $contract): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Contract $contract): bool
{
return false;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class UserPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, User $model): bool
{
return false;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, User $model): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, User $model): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, User $model): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, User $model): bool
{
return false;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class CortexXdrService
{
protected ?string $apiKey;
protected ?string $apiKeyId;
protected ?string $baseUrl;
public function __construct()
{
$this->apiKey = config('services.cortex.key');
$this->apiKeyId = config('services.cortex.id');
$this->baseUrl = config('services.cortex.url');
}
/**
* Generate headers.
* Switched to Standard Authentication (Direct Key) based on documentation screenshot.
*/
protected function getHeaders()
{
// STANDARD AUTHENTICATION
return [
'x-xdr-auth-id' => $this->apiKeyId,
'Authorization' => $this->apiKey,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
];
}
/**
* Get Incidents
* Using pagination to fetch up to $limit incidents.
*/
public function getIncidents($limit = 1000)
{
return $this->fetchAll('incidents/get_incidents', 'incidents', $limit);
}
/**
* Get Endpoints
* Using pagination to fetch up to $limit endpoints.
*/
public function getEndpoints($limit = 1000)
{
return $this->fetchAll('endpoints/get_endpoint', 'endpoints', $limit);
}
/**
* Generic fetch method with pagination.
*/
private function fetchAll($endpoint, $dataKey, $limit)
{
if (empty($this->apiKey) || empty($this->apiKeyId) || empty($this->baseUrl)) {
Log::warning('Cortex XDR credentials missing.');
return [];
}
$allResults = [];
$batchSize = 100; // API Limit
$offset = 0;
try {
while ($offset < $limit) {
// Adjust batch size for last page if needed
$currentBatchSize = min($batchSize, $limit - $offset);
// API confusingly uses search_to as count? Or offset + count?
// Documentation: "search_to: Integer representing the ending offset" ?
// Error message: "0 < search_size <= 100".
// Wait, request_data documentation usually says:
// search_from: integer
// search_to: integer (exclusive end index?) or count?
// Let's assume standard Cortex: offset based.
// If search_from=0, search_to=100 -> gets 0 to 99 (100 items).
$response = Http::withHeaders($this->getHeaders())
->post("{$this->baseUrl}/public_api/v1/{$endpoint}/", [
'request_data' => [
'search_from' => $offset,
'search_to' => $offset + $currentBatchSize,
]
]);
if ($response->successful()) {
$items = $response->json()['reply'][$dataKey] ?? [];
if (empty($items)) {
break; // No more items
}
$allResults = array_merge($allResults, $items);
$offset += count($items);
// If we got fewer items than requested, we are done
if (count($items) < $currentBatchSize) {
break;
}
} else {
$err = "Cortex API Error ({$endpoint}): " . $response->status() . ' - ' . $response->body();
Log::error($err);
throw new \Exception($err);
}
}
return $allResults;
} catch (\Exception $e) {
throw $e;
}
}
/**
* Get Dashboard Summary
*/
public function getSummary()
{
try {
$incidents = $this->getIncidents(1000); // Limit incidents to 1000 recent
$endpoints = $this->getEndpoints(1000); // Limit endpoints to 2000
} catch (\Exception $e) {
// In dashboard context, we verify configuration separately.
// Returning empty arrays here avoids crashing the page if just one call fails.
$incidents = [];
$endpoints = [];
}
// Calculations
$endpointsTotal = count($endpoints);
$endpointsConnected = collect($endpoints)->filter(fn($e) => strtolower($e['endpoint_status'] ?? '') === 'connected')->count();
$endpointsDisconnected = collect($endpoints)->filter(fn($e) => strtolower($e['endpoint_status'] ?? '') === 'disconnected')->count();
// Filter only Active Incidents (New or Under Investigation)
$activeIncidents = collect($incidents)->filter(fn($i) => in_array($i['status'] ?? '', ['new', 'under_investigation']));
//die(var_dump($activeIncidents));
$incidentsCritical = $activeIncidents->where('severity', 'critical')->count();
$incidentsHigh = $activeIncidents->where('severity', 'high')->count();
$incidentsMedium = $activeIncidents->where('severity', 'medium')->count();
$incidentsLow = $activeIncidents->where('severity', 'low')->count();
// Endpoint Types (Workstation, Server, etc.)
$endpointTypes = collect($endpoints)->groupBy(function ($e) {
return ucfirst(strtolower($e['endpoint_type'] ?? 'Unknown'));
})->map->count();
return [
'incidents_total' => $activeIncidents->count(),
'incidents_critical' => $incidentsCritical,
'incidents_high' => $incidentsHigh,
'incidents_medium' => $incidentsMedium,
'incidents_low' => $incidentsLow,
'endpoints_total' => $endpointsTotal,
'endpoints_connected' => $endpointsConnected,
'endpoints_disconnected' => $endpointsDisconnected,
'endpoints_types' => $endpointTypes, // New Data
'recent_incidents' => collect($incidents)
->filter(fn($i) => !in_array($i['status'] ?? '', ['resolved_true_positive', 'resolved_false_positive']))
->slice(0, 10)
->values()
->all()
];
}
}