Laravel korisničke nadzorne ploče: Integrirajte analizator sigurnosti web-mjesta za korisne uvide za klijente
Sigurnosno izvješće postaje korisno tek kada netko može vidjeti što se promijenilo, razumjeti što je važno i preporuke pretvoriti u dovršeni posao. Za malu web agenciju to znači više od smještanja API odgovora u lijepu karticu. Nadzorna ploča treba trajnu povijest, izvršavanje u pozadini, eksplicitna stanja neuspjeha i zadatke sanacije koji preživljavaju sljedeće skeniranje.
Ovaj vodič izrađuje taj tijek rada u Laravelu i PHP-u 8.3+. Svaki klijent ima HTTPS web-mjesto, kronološku povijest skeniranja, nalaze grupirane prema ozbiljnosti, TLS pojedinosti i provedive preporuke. Website Security Analyzer provodi ograničenu, neinvazivnu analizu javnog HTTPS-a i sigurnosnog stanja preglednika. Trebao bi podržavati rutinsku higijenu i određivanje prioriteta, ali nikada se ne smije opisivati kao penetracijski test.
Pribavite pristup prije pisanja integracijskog koda
- Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.
- Otvorite stranicu usluge Website Security Analyzer. Odaberite dostupni Free, Plus ili Pro plan i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge. Pronađite ploču Service token i kopirajte token ograničen na uslugu.
- Spremite taj token u okruženje aplikacije. Njegovo ponovno generiranje opoziva prethodno aktivni token, stoga uskladite rotaciju s implementacijom umjesto da ga nepromišljeno ponovno generirate.
API prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Ovaj projekt upotrebljava Bearer token jer se tako autentikacija ne nalazi u URL-ovima, zapisima pristupa i povijesti preglednika.
Točan zahtjev je POST https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website, s JSON tijelom koje sadrži url. Provjerite pristup minimalnim zahtjevom:
curl --fail-with-body \
--request POST \
--url https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website \
--header "Authorization: Bearer YOUR_SERVICE_TOKEN" \
--header "Content-Type: application/json" \
--data '{"url":"https://client.example"}'
Sada postavite vjerodajnicu u .env, nikada u PHP izvorni kod, fixtureove, snimke zaslona ili zapise:
WEBSITE_SECURITY_ANALYZER_TOKEN=YOUR_SERVICE_TOKEN
QUEUE_CONNECTION=database
Dodajte konfiguraciju koja se temelji na okruženju u config/services.php:
'website_security_analyzer' => [
'endpoint' => 'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website',
'token' => env('WEBSITE_SECURITY_ANALYZER_TOKEN'),
],
Odaberite arhitekturu koja čuva povijest
Zahtjev ne bi trebao čekati završetak vanjske analize. Kontroler stvara skeniranje na čekanju, a zatim šalje posao u red. Posao poziva namjenski API klijent, provjerava odgovor na granici aplikacije, sprema nepromjenjivu snimku i iz preporuka stvara otvorene zadatke sanacije.
Takav dizajn zahtijeva nekoliko tablica baze podataka i radnika reda, ali pruža brže odgovore pregledniku, kontrolirane ponovne pokušaje i revizijski trag. Također odvaja transportne brige od kontrolera i prikaza.
Preduvjeti su PHP 8.3+, Composer, podržana Laravel baza podataka i pozadinski sustav reda. Počnite s uobičajenom Laravel aplikacijom i generirajte glavne klase:
composer create-project laravel/laravel agency-security-dashboard
cd agency-security-dashboard
php artisan make:model Client -m
php artisan make:model SecurityScan -m
php artisan make:model RemediationTask -m
php artisan make:controller ClientSecurityController
php artisan make:job AnalyzeClientWebsite
php artisan make:policy ClientPolicy --model=Client
php artisan make:test WebsiteSecurityAnalyzerTest --unit
Relevantna struktura namjerno je mala:
app/
Data/WebsiteAnalysis.php
Exceptions/AnalyzerFailure.php
Http/Controllers/ClientSecurityController.php
Jobs/AnalyzeClientWebsite.php
Models/Client.php
Models/SecurityScan.php
Models/RemediationTask.php
Services/WebsiteSecurityAnalyzer.php
resources/views/clients/security.blade.php
routes/web.php
tests/Unit/WebsiteSecurityAnalyzerTest.php
Spremite snimke i zadatke odvojeno
Skeniranje je dokaz zabilježen u određenom trenutku. Zadatak sanacije je promjenjiv posao. Njihovo odvajanje omogućuje agenciji da zatvori zadatak bez prepisivanja povijesnog odgovora koji ga je stvorio.
Schema::create('clients', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('website_url', 2048);
$table->timestamps();
});
Schema::create('security_scans', function (Blueprint $table) {
$table->id();
$table->foreignId('client_id')->constrained()->cascadeOnDelete();
$table->string('status')->index(); // pending, running, completed, failed
$table->double('score')->nullable();
$table->json('findings')->nullable();
$table->json('tls_details')->nullable();
$table->json('recommendations')->nullable();
$table->string('error_code')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
Schema::create('remediation_tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('client_id')->constrained()->cascadeOnDelete();
$table->foreignId('security_scan_id')->constrained()->cascadeOnDelete();
$table->text('description');
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
Konfigurirajte modele odgovarajućim odnosima hasMany i belongsTo. Pretvorite JSON stupce u array, a vremenske oznake u datetime. Ili deklarirajte dodijeljena polja u $fillable ili dosljedno upotrebljavajte eksplicitno dodjeljivanje svojstava.
Provjerite API odgovor na jednoj granici
Isporučeni ugovor izlaže rezultat, nalaze grupirane prema ozbiljnosti, TLS pojedinosti i preporuke. Prilagodnik ne bi smio dopustiti da neočekivana HTML stranica s pogreškom ili promijenjeni JSON oblik prodru u domenu.
<?php
namespace App\Data;
use App\Exceptions\AnalyzerFailure;
final readonly class WebsiteAnalysis
{
public function __construct(
public int|float $score,
public array $findings,
public array $tlsDetails,
public array $recommendations,
) {}
public static function fromPayload(mixed $payload): self
{
if (! is_array($payload)
|| ! is_int($payload['score'] ?? null) && ! is_float($payload['score'] ?? null)
|| ! is_array($payload['findings'] ?? null)
|| ! is_array($payload['tls'] ?? null)
|| ! is_array($payload['recommendations'] ?? null)
) {
throw new AnalyzerFailure('schema', 'Unexpected analyzer response.');
}
foreach ($payload['findings'] as $group => $items) {
if (! is_string($group) || ! is_array($items)) {
throw new AnalyzerFailure('schema', 'Invalid findings grouping.');
}
}
$recommendations = array_values(array_filter(
$payload['recommendations'],
static fn (mixed $value): bool => is_string($value) && trim($value) !== ''
));
return new self(
$payload['score'],
$payload['findings'],
$payload['tls'],
$recommendations,
);
}
}
Ako službena dokumentacija vraća ta polja unutar dokumentirane omotnice, raspakirajte tu omotnicu ovdje i nigdje drugdje. Nemojte raspršivati spekulativne zamjenske opcije po poslovima i prikazima.
Upotrijebite ograničena vremenska ograničenja i klasificirane neuspjehe
<?php
namespace App\Services;
use App\Data\WebsiteAnalysis;
use App\Exceptions\AnalyzerFailure;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
final class WebsiteSecurityAnalyzer
{
public function analyze(string $url): WebsiteAnalysis
{
$token = config('services.website_security_analyzer.token');
if (! is_string($token) || $token === '') {
throw new AnalyzerFailure('configuration', 'Analyzer token is missing.');
}
try {
$response = Http::acceptJson()
->asJson()
->withToken($token)
->connectTimeout(5)
->timeout(30)
->post(
config('services.website_security_analyzer.endpoint'),
['url' => $url],
);
} catch (ConnectionException $exception) {
throw new AnalyzerFailure('temporary', 'Analyzer connection failed.', previous: $exception);
}
if ($response->successful()) {
return WebsiteAnalysis::fromPayload($response->json());
}
$status = $response->status();
if (in_array($status, [401, 403], true)) {
throw new AnalyzerFailure('authentication', 'Analyzer authentication failed.');
}
if ($status === 429) {
$header = $response->header('Retry-After');
$delay = ctype_digit((string) $header)
? max(30, min(900, (int) $header))
: 120;
throw new AnalyzerFailure('rate_limit', 'Analyzer rate limit reached.', $delay);
}
if ($status === 422) {
throw new AnalyzerFailure('validation', 'Analyzer rejected the website URL.');
}
if ($status >= 500) {
throw new AnalyzerFailure('temporary', 'Analyzer is temporarily unavailable.');
}
throw new AnalyzerFailure('upstream', "Unexpected analyzer status: {$status}.");
}
}
AnalyzerFailure je mala podklasa RuntimeException koja izlaže javna readonly svojstva kind i nullable retryAfter. Njezin konstruktor trebao bi prihvatiti PHP-ov imenovani argument previous kako je prikazano.
Pokrenite skeniranja u redu
Samo neuspjesi veze, neuspjesi poslužitelja i ograničenja brzine zaslužuju novi pokušaj. Neuspjesi provjere valjanosti i autentikacije zahtijevaju ljudsku intervenciju, stoga njihovo ponovno pokušavanje samo troši kvotu i skriva stvarni problem.
final class AnalyzeClientWebsite implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 4;
public function __construct(public SecurityScan $scan) {}
public function backoff(): array
{
return [30, 120, 300];
}
public function handle(WebsiteSecurityAnalyzer $analyzer): void
{
$this->scan->update(['status' => 'running']);
try {
$result = $analyzer->analyze($this->scan->client->website_url);
} catch (AnalyzerFailure $failure) {
Log::warning('Website analysis failed', [
'scan_id' => $this->scan->id,
'client_id' => $this->scan->client_id,
'kind' => $failure->kind,
'attempt' => $this->attempts(),
]);
if ($failure->kind === 'rate_limit' && $this->attempts() < $this->tries) {
$this->release($failure->retryAfter ?? 120);
return;
}
if ($failure->kind === 'temporary') {
throw $failure;
}
$this->scan->update([
'status' => 'failed',
'error_code' => $failure->kind,
'completed_at' => now(),
]);
return;
}
DB::transaction(function () use ($result): void {
$this->scan->update([
'status' => 'completed',
'score' => $result->score,
'findings' => $result->findings,
'tls_details' => $result->tlsDetails,
'recommendations' => $result->recommendations,
'completed_at' => now(),
]);
foreach ($result->recommendations as $recommendation) {
$this->scan->remediationTasks()->create([
'client_id' => $this->scan->client_id,
'description' => $recommendation,
]);
}
});
}
public function failed(?Throwable $failure): void
{
$this->scan->update([
'status' => 'failed',
'error_code' => 'retry_exhausted',
'completed_at' => now(),
]);
}
}
Akcija kontrolera trebala bi autorizirati pristup, zaključati red klijenta u transakciji, odbiti drugo skeniranje na čekanju ili u tijeku, stvoriti zapis na čekanju i poslati posao nakon potvrde transakcije:
public function store(Client $client): RedirectResponse
{
Gate::authorize('update', $client);
DB::transaction(function () use ($client): void {
$locked = Client::query()->lockForUpdate()->findOrFail($client->id);
abort_if(
$locked->securityScans()
->whereIn('status', ['pending', 'running'])
->exists(),
409,
'A scan is already in progress.'
);
$scan = $locked->securityScans()->create(['status' => 'pending']);
AnalyzeClientWebsite::dispatch($scan)->afterCommit();
});
return back()->with('status', 'Security analysis queued.');
}
Definirajte rute za nadzornu ploču, stvaranje skeniranja i dovršavanje zadataka. Pravilo mora osigurati da autentikirani korisnik posjeduje klijenta; povezivanje modela ruta samo po sebi nije autorizacija.
Route::middleware('auth')->group(function (): void {
Route::get('/clients/{client}/security', [ClientSecurityController::class, 'show'])
->name('clients.security.show');
Route::post('/clients/{client}/security/scans', [ClientSecurityController::class, 'store'])
->name('clients.security.scans.store');
Route::patch('/clients/{client}/tasks/{task}', [ClientSecurityController::class, 'complete'])
->scopeBindings()
->name('clients.security.tasks.complete');
});
Blade prikaz može ostati jednostavan: prikažite najnoviji rezultat i vrijeme dovršetka, iterirajte kroz findings po grupi ozbiljnosti, prikažite TLS pojedinosti kao označene vrijednosti, navedite otvorene zadatke sanacije s obrascima za dovršavanje zaštićenima CSRF-om i postavite ranija skeniranja ispod trenutačne snimke. Escapeajte uobičajene vrijednosti Bladeovim {{ }}; nemojte prikazivati API tekst putem {!! !!}.
Testirajte granicu bez pozivanja produkcije
Laravelov HTTP fake čini transportne testove determinističkima i dokazuje da su tajne i tijela zahtjeva ispravno sastavljeni.
public function test_it_maps_a_successful_analysis(): void
{
config()->set('services.website_security_analyzer.token', 'test-token');
config()->set(
'services.website_security_analyzer.endpoint',
'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website'
);
Http::fake([
'https://ai.mihajlo.mk/*' => Http::response([
'score' => 82,
'findings' => ['high' => [], 'medium' => []],
'tls' => [],
'recommendations' => ['Review the site security configuration.'],
], 200),
]);
$result = app(WebsiteSecurityAnalyzer::class)
->analyze('https://client.example');
$this->assertSame(82, $result->score);
$this->assertCount(1, $result->recommendations);
Http::assertSent(fn ($request) =>
$request->method() === 'POST'
&& $request['url'] === 'https://client.example'
&& $request->hasHeader('Authorization', 'Bearer test-token')
);
}
public function test_authentication_failure_is_not_retried(): void
{
config()->set('services.website_security_analyzer.token', 'invalid');
Http::fake(['https://ai.mihajlo.mk/*' => Http::response([], 401)]);
try {
app(WebsiteSecurityAnalyzer::class)->analyze('https://client.example');
$this->fail('Expected an authentication failure.');
} catch (AnalyzerFailure $failure) {
$this->assertSame('authentication', $failure->kind);
}
Http::assertSentCount(1);
}
Dodajte testove poslova s Queue::fake() za slanje i Http::fake() za dovršavanje, ograničavanje brzine, neispravan JSON i iscrpljene privremene neuspjehe. Provjeravajte stanje baze podataka, a ne interne pozive metoda.
Zaštitne mjere u produkciji i implementacija
- Prihvaćajte samo normalizirane javne
https://URL-ove klijenata. Odbijte vjerodajnice u URL-ovima, nazive localhosta i privatna ili rezervirana IP odredišta. - Šifrirajte tajne okruženja putem platforme za implementaciju i ograničite tko ih može vidjeti. Tijekom rotacije implementirajte ponovno generirani token svugdje gdje se red koristi prije nego što stari radnici nastave obradu.
- Nikada ne zapisujte token, autorizacijsko zaglavlje, potpuni odgovor ni URL klijenta. Stabilni ID-ovi skeniranja, ID-ovi klijenata, kategorije neuspjeha, pokušaji, trajanje i kategorije HTTP statusa pružaju korisnu vidljivost.
- Postavite upozorenja za ponovljene neuspjehe autentikacije, neuspjehe sheme, iscrpljene pokušaje i rastući red. Neuspjeh sheme alarm je integracije, a ne prazno uspješno izvješće.
- Jasno označite nadzornu ploču kao ograničenu, neinvazivnu analizu sigurnosti web-mjesta. Izbjegavajte jezik koji implicira provjeru iskorištavanja ranjivosti, pokrivenost interne mreže ili penetracijsko testiranje.
Implementirajte migracije baze podataka, ponovno izgradite konfiguraciju i ponovno pokrenite radnike kako bi učitali novi token i kod:
php artisan migrate --force
php artisan config:cache
php artisan queue:restart
php artisan queue:work --tries=4 --timeout=90
Pokrenite radnika pod nadzorom procesa u produkciji. Njegovo vremensko ograničenje procesa mora ostati znatno dulje od vremenskog ograničenja HTTP odgovora. Uobičajeni neuspjesi najčešće su izravni: 401 ili 403 označava problem s konfiguracijom ili rotacijom tokena; 422 označava neprihvatljiv URL; 429 zahtijeva odgođeni ponovni pokušaj ili pregled plana; ponovljeni 5xx ili neuspjesi veze označavaju privremeni problem uzvodne usluge; a neuspjeh sheme znači da se prilagodnik mora usporediti s trenutačnom službenom dokumentacijom.
Završni kontrolni popis za provjeru
- Klijent može staviti jedno skeniranje u red bez zadržavanja otvorenog zahtjeva preglednika.
- Istodobni klikovi ne mogu stvoriti više aktivnih skeniranja za istog klijenta.
- Uspješni rezultati zadržavaju rezultat, grupirane nalaze, TLS pojedinosti, preporuke i vrijeme dovršetka.
- Preporuke postaju zadaci koji se mogu zatvoriti, dok izvorno skeniranje ostaje nepromjenjivo.
- Autorizacija sprječava jednog korisnika agencije da pregledava ili ažurira klijente drugog korisnika.
- Neuspjesi autentikacije i provjere valjanosti odmah se zaustavljaju; prolazni neuspjesi koriste ograničeni backoff.
- Testovi ne upućuju mrežne pozive i ne sadrže stvarni servisni token.
- Nadzorna ploča točno opisuje rezultat i nikada ga ne naziva penetracijskim testom.
Trajna vrijednost nije gumb za skeniranje. To je ciklus oko njega: zabilježiti ograničenu procjenu, sačuvati ono što je opaženo, preporuke pretvoriti u dodijeljeni posao i kasnije se vratiti kako bi se izmjerilo što se promijenilo. Time se sigurnosni API pretvara iz jednokratnog izvješća u mirnu, ponovljivu uslugu za klijente.