Vodiči

Symfony Security Dashboard: Client Audits, History, and Remediation with AI Analyzer

Symfony sigurnosna nadzorna ploča: revizije klijenata, povijest i otklanjanje problema pomoću AI analizatora

Sigurnosno skeniranje korisno je jednom. Sigurnosna povijest korisna je svaki tjedan. Za malu agenciju koja upravlja s nekoliko klijentskih web-mjesta razlika je važna: nadzorna ploča trebala bi pokazati poboljšava li se sigurnosni položaj, sačuvati prethodne rezultate i preporuke pretvoriti u zadatke koje netko zaista može dovršiti.

Ovaj vodič izrađuje tu značajku u PHP 8.3+ Symfony aplikaciji. Svako se skeniranje izvodi u pozadini, poziva Website Security Analyzer, pohranjuje normalizirani snimak stanja te prikazuje rezultat, nalaze grupirane prema ozbiljnosti, TLS pojedinosti i zadatke otklanjanja problema. Rezultat je namjerno opisan kao ograničeni pregled javnog HTTPS-a i sigurnosnog položaja preglednika — a ne penetracijski test ili dokaz da je web-mjesto sigurno.

Dobijte pristup i izradite servisni token

  1. Izradite račun na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako ga već imate.
  2. Otvorite stranicu usluge Website Security Analyzer. Odaberite dostupni plan Free, Plus ili Pro i dovršite njegovu aktivaciju.
  3. Posjetite službenu dokumentaciju usluge. Pronađite ploču Service token i kopirajte ondje prikazani token ograničen na uslugu.
  4. Pohranite token u konfiguraciju projekta koja se temelji na varijablama okruženja. Nikada ga nemojte predati u repozitorij niti lijepiti u zapisnike, snimke zaslona, fixture podatke ili izvorni kod.

Ponovno generiranje servisnog tokena opoziva prethodno aktivni token. Rotaciju tretirajte kao promjenu pri implementaciji: ažurirajte svaku pokrenutu aplikaciju i radnik prije nego što pretpostavite da su skeniranja ponovno ispravna.

Provjerite točan HTTP ugovor

Operacija je POST https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website. Prihvaća JSON koji sadrži url. Autentikacija može koristiti Bearer token, zaglavlje X-API-Token ili parametar tokena u upitu. Ovaj projekt koristi oblik Bearer kako se vjerodajnice ne bi pojavile u URL-ovima ili uobičajenim zapisnicima pristupa.

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"}'

Pokrenite taj zahtjev s web-mjestom za čiju procjenu imate ovlaštenje. Zatim vjerodajnicu stavite u nepredanu datoteku .env.local:

WEBSITE_ANALYZER_TOKEN=YOUR_SERVICE_TOKEN
MESSENGER_TRANSPORT_DSN=doctrine://default
DATABASE_URL="postgresql://app:[email protected]:5432/agency?serverVersion=16&charset=utf8"

Arhitektura prikladna za malu agenciju

Zahtjev preglednika ne bi trebao čekati dok se izvodi vanjska analiza. Kontroler zato bilježi reviziju na čekanju i šalje Messenger poruku. Radnik poziva API, granični mapper provjerava odgovor, a mala usluga perzistencije dovršava ili ne uspijeva reviziju. Nadzorna ploča čita samo lokalne podatke, pa ostaje responzivna tijekom API latencije ili prekida rada.

Pohranjivanje JSON snimaka stanja pragmatičan je kompromis. Sigurnosni nalazi i TLS pojedinosti mogu se razvijati brže od sheme nadzorne ploče, dok rezultat, status, URL i vremenske oznake ostaju korisni relacijski stupci. Ako izvještavanje kasnije bude zahtijevalo upite kroz pojedinačne nalaze, premjestite te vrijednosti u namjenske tablice.

Relevantna struktura projekta je:

src/
  Controller/SecurityDashboardController.php
  Message/AnalyzeWebsite.php
  MessageHandler/AnalyzeWebsiteHandler.php
  Security/AnalysisResult.php
  Security/WebsiteAnalyzer.php
  Security/AnalyzerException.php
  Repository/AuditStore.php
templates/security/index.html.twig
tests/Security/WebsiteAnalyzerTest.php
config/packages/messenger.yaml
config/services.yaml

Polazeći od Symfony aplikacije koja već autentificira osoblje agencije, instalirajte komponente prvog proizvođača:

composer require symfony/http-client symfony/messenger symfony/doctrine-messenger \
  symfony/orm-pack symfony/twig-bundle symfony/security-bundle
composer require --dev symfony/test-pack
php bin/console doctrine:migrations:migrate

Izradite migraciju audit sa stupcima za id, url, status, nullable score, findings_json, tls_json, tasks_json, nullable error_code, created_at i nullable completed_at. Upotrijebite JSON tip svoje platforme baze podataka gdje je dostupan te indeksirajte created_at i status.

Izgradite strogu API granicu

Aplikacija ne bi trebala širiti pretpostavke o nizovima odgovora kroz kontrolere i predloške. Rezultat domene provjerava četiri obavezna koncepta i pretvara preporuke u otvorene zadatke otklanjanja problema.

<?php
// src/Security/AnalysisResult.php
namespace App\Security;

final readonly class AnalysisResult
{
    public function __construct(
        public float $score,
        public array $findingsBySeverity,
        public array $tls,
        public array $tasks,
    ) {}

    public static function fromApi(array $data): self
    {
        if (!is_numeric($data['score'] ?? null)
            || !is_array($data['findings'] ?? null)
            || !is_array($data['tls'] ?? null)
            || !is_array($data['recommendations'] ?? null)) {
            throw new AnalyzerException('invalid_response');
        }

        $tasks = [];
        foreach ($data['recommendations'] as $recommendation) {
            if (is_string($recommendation) && trim($recommendation) !== '') {
                $tasks[] = ['title' => trim($recommendation), 'status' => 'open'];
            }
        }

        return new self(
            (float) $data['score'],
            $data['findings'],
            $data['tls'],
            $tasks,
        );
    }
}

Ovaj mapper namjerno ne izmišlja polja unutar podataka o nalazima ili TLS-u. Zadržava te dokumentirane odjeljke odgovora, a odbacuje nedostajuće ili pogrešno tipizirane vrijednosti najviše razine. Ako službena dokumentacija promijeni primjer korisnog tereta, ažurirajte ovu jedinstvenu granicu i njezine testove.

Klijent primjenjuje ograničena vremenska ograničenja i ponovno pokušava samo prolazne transportne pogreške, ograničenja brzine i odabrane pogreške poslužitelja. Pogreške autentikacije i validacije odmah ne uspijevaju.

<?php
// src/Security/WebsiteAnalyzer.php
namespace App\Security;

use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final class WebsiteAnalyzer
{
    private const ENDPOINT =
        'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website';

    public function __construct(
        private HttpClientInterface $http,
        private LoggerInterface $logger,
        private string $token,
    ) {}

    public function analyze(string $url, string $auditId): AnalysisResult
    {
        for ($attempt = 1; $attempt <= 3; $attempt++) {
            try {
                $response = $this->http->request('POST', self::ENDPOINT, [
                    'headers' => [
                        'Authorization' => 'Bearer '.$this->token,
                        'Accept' => 'application/json',
                    ],
                    'json' => ['url' => $url],
                    'timeout' => 20.0,
                    'max_duration' => 30.0,
                ]);
                $status = $response->getStatusCode();
                $body = $response->getContent(false);
            } catch (TransportExceptionInterface $e) {
                if ($attempt === 3) {
                    throw new AnalyzerException('transport_failure', previous: $e);
                }
                $this->backoff($attempt);
                continue;
            }

            if ($status >= 200 && $status < 300) {
                try {
                    $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
                } catch (\JsonException $e) {
                    throw new AnalyzerException('invalid_json', previous: $e);
                }

                if (!is_array($data)) {
                    throw new AnalyzerException('invalid_response');
                }

                return AnalysisResult::fromApi($data);
            }

            if (in_array($status, [429, 502, 503, 504], true) && $attempt < 3) {
                $this->logger->warning('Analyzer request will be retried', [
                    'audit_id' => $auditId,
                    'status' => $status,
                    'attempt' => $attempt,
                ]);
                $this->backoff($attempt);
                continue;
            }

            $code = match ($status) {
                401, 403 => 'authentication_failure',
                400, 422 => 'request_rejected',
                429 => 'rate_limited',
                default => 'upstream_failure',
            };
            throw new AnalyzerException($code);
        }

        throw new AnalyzerException('upstream_failure');
    }

    private function backoff(int $attempt): void
    {
        usleep(250_000 * (2 ** ($attempt - 1)));
    }
}

Nemojte neselektivno bilježiti tijela odgovora: nalazi mogu otkriti operativne pojedinosti, a tijela pogrešaka mogu uključivati kontekst zahtjeva. Identifikatori revizije, statusni kodovi, pokušaji, trajanja i stabilni kodovi neuspjeha dovoljni su za većinu operativne dijagnostike.

Povežite token kroz Symfonyjev spremnik:

# config/services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true

  App\:
    resource: '../src/'

  App\Security\WebsiteAnalyzer:
    arguments:
      $token: '%env(WEBSITE_ANALYZER_TOKEN)%'

Stavite skeniranja u red čekanja i sačuvajte njihov ishod

AuditStore, complete($id, AnalysisResult $result), fail($id, $code) i recent(). Implementirajte ih s umetnutim Doctrine DBAL Connection, parametriziranim izrazima, json_encode(..., JSON_THROW_ON_ERROR) i nepromjenjivim UTC vremenskim oznakama. Nikada ne spajajte poslani URL u SQL.

<?php
// src/Message/AnalyzeWebsite.php
namespace App\Message;

final readonly class AnalyzeWebsite
{
    public function __construct(public string $auditId, public string $url) {}
}

// src/MessageHandler/AnalyzeWebsiteHandler.php
namespace App\MessageHandler;

use App\Message\AnalyzeWebsite;
use App\Repository\AuditStore;
use App\Security\AnalyzerException;
use App\Security\WebsiteAnalyzer;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final readonly class AnalyzeWebsiteHandler
{
    public function __construct(
        private WebsiteAnalyzer $analyzer,
        private AuditStore $audits,
    ) {}

    public function __invoke(AnalyzeWebsite $message): void
    {
        try {
            $result = $this->analyzer->analyze(
                $message->url,
                $message->auditId
            );
            $this->audits->complete($message->auditId, $result);
        } catch (AnalyzerException $e) {
            $this->audits->fail($message->auditId, $e->getMessage());
        }
    }
}

Klijent već ima vlastitu kratku politiku ponovnih pokušaja, pa Messenger ne smije umnožavati te pokušaje:

# config/packages/messenger.yaml
framework:
  messenger:
    transports:
      async:
        dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
        retry_strategy:
          max_retries: 0
    routing:
      App\Message\AnalyzeWebsite: async

Dodajte zaštićenu nadzornu ploču

Prihvaćajte samo apsolutne HTTPS URL-ove, odbacite ugrađene vjerodajnice, zahtijevajte CSRF zaštitu i zadržite rutu iza postojeće autentikacije osoblja aplikacije. Za agenciju je popis dopuštenih klijenata još bolji: sprečava valjani račun osoblja da nadzornu ploču pretvori u skener opće namjene.

<?php
// src/Controller/SecurityDashboardController.php
namespace App\Controller;

use App\Message\AnalyzeWebsite;
use App\Repository\AuditStore;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\{Request, Response};
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Uid\Uuid;

#[IsGranted('ROLE_AUDITOR')]
final class SecurityDashboardController extends AbstractController
{
    #[Route('/security', name: 'security_dashboard', methods: ['GET'])]
    public function index(AuditStore $audits): Response
    {
        return $this->render('security/index.html.twig', [
            'audits' => $audits->recent(),
        ]);
    }

    #[Route('/security/audits', name: 'security_audit_create', methods: ['POST'])]
    public function create(
        Request $request,
        AuditStore $audits,
        MessageBusInterface $bus,
    ): Response {
        if (!$this->isCsrfTokenValid('new-audit', $request->request->getString('_token'))) {
            throw $this->createAccessDeniedException();
        }

        $url = trim($request->request->getString('url'));
        $parts = parse_url($url);
        if (!filter_var($url, FILTER_VALIDATE_URL)
            || ($parts['scheme'] ?? '') !== 'https'
            || isset($parts['user'])
            || !isset($parts['host'])) {
            throw $this->createNotFoundException('A public HTTPS URL is required.');
        }

        $id = Uuid::v7()->toRfc4122();
        $audits->create($id, $url);
        $bus->dispatch(new AnalyzeWebsite($id, $url));

        return $this->redirectToRoute('security_dashboard');
    }
}

Twig predložak može prikazati obrazac za slanje, a zatim nedavne revizije. Za dovršene retke prikažite rezultat, iterirajte findings_json prema ozbiljnosti, obrambeno prikažite TLS podatke ključ/vrijednost i pokažite svaku stavku u tasks_json s njezinim statusom. Za retke na čekanju i neuspjele retke prikažite pohranjeno stanje i siguran kod neuspjeha umjesto praznog izvješća. Twigovo automatsko izbjegavanje mora ostati uključeno.

Deterministički testirajte uspjeh i neuspjeh

MockHttpClient provjerava odlazni ugovor bez kontaktiranja usluge:

<?php
// tests/Security/WebsiteAnalyzerTest.php
namespace App\Tests\Security;

use App\Security\WebsiteAnalyzer;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;

final class WebsiteAnalyzerTest extends TestCase
{
    public function testMapsAValidResponse(): void
    {
        $response = new MockResponse(json_encode([
            'score' => 82,
            'findings' => ['high' => [], 'medium' => [['name' => 'Example']]],
            'tls' => ['enabled' => true],
            'recommendations' => ['Review the reported browser policy.'],
        ], JSON_THROW_ON_ERROR), ['http_code' => 200]);

        $http = new MockHttpClient(function (string $method, string $url, array $options) use ($response) {
            self::assertSame('POST', $method);
            self::assertSame(
                'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website',
                $url
            );
            self::assertSame(['url' => 'https://client.example'], $options['json']);
            self::assertContains('Authorization: Bearer test-token', $options['headers']);
            return $response;
        });

        $result = (new WebsiteAnalyzer($http, new NullLogger(), 'test-token'))
            ->analyze('https://client.example', 'audit-1');

        self::assertSame(82.0, $result->score);
        self::assertSame('open', $result->tasks[0]['status']);
    }
}

Dodajte slučajeve za neispravan JSON, nedostajući obavezni odjeljak, trenutačni neuspjeh pri 401 i ograničene ponovne pokušaje pri 429. Testirajte kontroler s autentificiranim korisnikom ROLE_AUDITOR, uključujući neispravne CSRF tokene, HTTP URL-ove, URL-ove s vjerodajnicama i neovlašteni pristup. Testirajte obrađivač s lažnim suradnicima analizatora i spremišta kako bi prijelazi uspjeha i neuspjeha bili eksplicitni.

Implementirajte, nadzirite i otklanjajte poteškoće

Pokrenite migracije tijekom implementacije, umetnite WEBSITE_ANALYZER_TOKEN kroz spremište tajni platforme za hosting te nakon rotacije ponovno pokrenite i web-procese i radnike. Pokrenite potrošača pod systemd-om, Supervisorom ili radničkim mehanizmom platforme:

php bin/console messenger:consume async \
  --time-limit=3600 \
  --memory-limit=256M

Postavite upozorenja za rastuću dubinu reda čekanja, ponovljeni authentication_failure, povišene ishode rate_limited i skeniranja zaglavljena u stanju queued dulje od očekivanog operativnog vremenskog prozora. Zadržavajte povijest revizija prema ugovorima s klijentima, ograničite pristup bazi podataka i definirajte politiku brisanja umjesto da nalaze prikupljate unedogled.

Uobičajeni su neuspjesi obično razumljivi: authentication_failure upućuje na nedostajući, opozvani ili zastarjeli token; request_rejected upućuje na poslani URL ili trenutačni dokumentirani ugovor; rate_limited znači da rad treba rasporediti prema aktivnom planu; a invalid_response zahtijeva usporedbu graničnog mappera sa službenom dokumentacijom. Trajno revizija na čekanju obično znači da Messenger radnik nedostaje, zaustavljen je ili je povezan s drugom bazom podataka.

Završni kontrolni popis provjere

  • Token je došao iz ploče Service token na stranici dokumentacije i postoji samo u konfiguraciji koja se temelji na varijablama okruženja.
  • Klijent šalje POST na točnu krajnju točku analizatora s JSON-om koji sadrži url.
  • Samo ovlašteno osoblje može slati skeniranja ili pregledavati povijest klijenata.
  • Zahtjevi imaju ograničena vremenska ograničenja, a samo prolazni neuspjesi dobivaju ograničene ponovne pokušaje s odgodom.
  • Rezultati, nalazi grupirani prema ozbiljnosti, TLS pojedinosti i zadaci otklanjanja problema preživljavaju ponovna pokretanja radnika kao lokalni snimci stanja.
  • Testovi pokrivaju valjano mapiranje, neispravne podatke, neuspjeh autentikacije, ograničavanje brzine, validaciju kontrolera i prijelaze stanja.
  • Nadzorna ploča rezultat naziva ograničenom, neinvazivnom analizom sigurnosnog položaja — a ne penetracijskim testom.

Najvrjedniji rezultat nije broj na vrhu stranice. To je trajni lanac od opažanja do preporuke, od preporuke do zadatka i od jedne revizije do sljedeće. To udaljeni analizator pretvara u odgovornu uslugu za klijente: razumljivu danas, usporedivu sutra i korisnu dugo nakon završetka prvog skeniranja.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.