Vodiči

Symfony Security Dashboard: Schedule Website Health Checks

Symfony sigurnosna nadzorna ploča: Zakažite provjere web-stranice

Sigurnosna nadzorna ploča stječe povjerenje samo kada jasno odgovara na dva pitanja: što sada nije u redu i kada su dokazi posljednji put osvježeni? Jednokratno skeniranje to ne može učiniti. Produkcijski sustavi trebaju ponovljivu procjenu, sačuvane rezultate, razumljiva stanja neuspjeha i strogo razdvajanje podataka korisnika.

Ovaj vodič gradi takav sustav s PHP-om 8.3, Symfonyjem 7.4, PostgreSQL-om, Symfony HttpClientom i Mihajlo Website Security Analyzerom. Gotova aplikacija pohranjuje autorizirane korisničke web-stranice, ponovno ih procjenjuje prema rasporedu, čuva povijest procjena i prikazuje najnovije sigurnosno stanje web-stranice putem autentificirane nadzorne ploče.

Analizator provodi ograničenu, neinvazivnu analizu javnog HTTPS-a i sigurnosnog stanja preglednika. Ne smije se opisivati kao penetracijsko testiranje, certifikacija ranjivosti ili dokaz da je web-stranica sigurna.

Dobijte pristup i kopirajte servisni token

Dovršite postavljanje pristupa prije izrade Symfony klijenta:

  1. Izradite račun za AI alate ili se prijavite ako ga već imate.
  2. Otvorite stranicu usluge Website Security Analyzer, pomaknite se do planova, odaberite Free za početni test ili veći plan za produkcijsku upotrebu te dovršite aktivaciju.
  3. Otvorite interaktivnu API dokumentaciju. U ploči Service token pritisnite Copy kako biste kopirali token ograničen na uslugu.
  4. Stavite token u .env.local. Nikada ga nemojte potvrditi u repozitorij. Ponovno generiranje tokena opoziva prethodni aktivni token, stoga odmah nakon rotacije ažurirajte postavljenu tajnu.
MIHAJLO_API_TOKEN=YOUR_SERVICE_TOKEN

Provjerite token i ugovor zahtjeva izravno prije povezivanja sa Symfonyjem:

curl --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://example.com\"}"

Uspješan JSON odgovor potvrđuje da je plan aktivan, token valjan i krajnja točka dostupna s računala na kojem će se projekt pokretati.

Najprije razumite granicu API-ja

Službeni ugovor dokumentiran je u API dokumentaciji za Website Security Analyzer. Integracija koristi točno:

POST https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website

Autentikacija može koristiti Bearer token, zaglavlje X-API-Token ili parametar tokena u upitu. Ova implementacija bira Bearer oblik jer drži vjerodajnice izvan URL-ova i zapisnika pristupa. JSON zahtjev sadrži jedno polje, url. Odgovor se mapira u rezultat, nalaze grupirane prema ozbiljnosti, TLS pojedinosti i preporuke.

Udaljeni odgovor je nepouzdan ulaz. Čak i kada su polja dokumentirana, aplikacija provjerava njihove tipove prije nego što bilo što dospije u pohranu ili Twig.

Arhitektura i kompromisi

Preglednik nikada ne poziva analizator. Zakazana Symfony naredba obavlja spori vanjski posao, dok kontroler čita lokalnu snimku stanja. Korisnici zato dobivaju predvidivu latenciju nadzorne ploče čak i kada je uzvodni API spor ili privremeno nedostupan.

  • Klijent analizatora: upravlja autentikacijom, vremenskim ograničenjima, provjerom odgovora i ograničenim ponovnim pokušajima.
  • Rezultat domene: sprječava da proizvoljan uzvodni JSON procuri kroz aplikaciju.
  • Repozitorij: preuzima dospjele web-stranice zakupom baze podataka te pohranjuje snimke stanja i povijest.
  • Konzolna naredba: ponovno procjenjuje web-stranice izvan obrade zahtjeva.
  • Kontroler nadzorne ploče: provodi vlasništvo korisnika prije prikaza rezultata.

Messenger bi bio razuman pri znatno većem protoku, ali ovdje nije potreban. Zakup baze podataka i mala zakazana serija pružaju oporavak nakon rušenja i sigurno izvršavanje na više instanci bez uvođenja dodatnog transporta.

Izradite Symfony projekt

Symfony 7.4 podržava PHP 8.3. Doctrine DBAL 4 pruža eksplicitnu SQL i kontrolu transakcija bez potrebe za ORM entitetima za ovaj mali model pohrane.

composer create-project symfony/skeleton:"7.4.*" security-dashboard
cd security-dashboard

composer require symfony/http-client:^7.4 symfony/console:^7.4 \
  symfony/twig-bundle:^7.4 symfony/security-bundle:^7.4 \
  doctrine/doctrine-bundle:^2.13 doctrine/dbal:^4.0

composer require --dev symfony/test-pack

Relevantna struktura projekta namjerno je kompaktna:

src/
  Command/ReassessWebsitesCommand.php
  Controller/SecurityDashboardController.php
  Security/AnalyzerClient.php
  Security/AnalyzerException.php
  Security/AssessmentResult.php
  Security/AssessmentRepository.php
templates/security/dashboard.html.twig
tests/Security/AnalyzerClientTest.php
migrations/Version20260811000000.php
config/services.yaml

Konfigurirajte tajne i ubrizgavanje ovisnosti

Potvrdite krajnju točku kao uobičajenu konfiguraciju, ali token ubrizgajte putem okruženja. Stavite rezervirano mjesto u .env; za stvarnu vrijednost koristite .env.local, tajnu za implementaciju ili upravitelj tajnama.

MIHAJLO_ANALYZER_ENDPOINT=https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website
MIHAJLO_API_TOKEN=YOUR_SERVICE_TOKEN
DATABASE_URL=postgresql://app:[email protected]:5432/security_dashboard
# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

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

    App\Security\AnalyzerClient:
        arguments:
            $endpoint: '%env(string:MIHAJLO_ANALYZER_ENDPOINT)%'
            $token: '%env(string:MIHAJLO_API_TOKEN)%'

Nikada nemojte zapisivati token, autorizacijsko zaglavlje ni potpuni uzvodni odgovor. URL-ovi također mogu sadržavati osjetljive putanje ili nizove upita, stoga bi operativni zapisnici obično trebali navoditi samo ID web-stranice i naziv hosta.

Izradite obrambeni mapper domene

Mapper zahtijeva sva četiri područja rezultata, izbjegavajući pritom pretpostavke o pojedinačnim objektima nalaza. To čuva kompatibilnost s budućim verzijama unutar svake skupine ozbiljnosti bez prihvaćanja strukturno nepovezanog odgovora.

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

final readonly class AssessmentResult
{
    public function __construct(
        public int|float $score,
        public array $findingsBySeverity,
        public array $tlsDetails,
        public array $recommendations,
    ) {}

    public static function fromPayload(array $data): self
    {
        if (!isset($data['score']) || !is_int($data['score']) && !is_float($data['score'])) {
            throw new \UnexpectedValueException('Analyzer score is missing or invalid.');
        }

        foreach (['findings', 'tls', 'recommendations'] as $field) {
            if (!isset($data[$field]) || !is_array($data[$field])) {
                throw new \UnexpectedValueException("Analyzer field {$field} is invalid.");
            }
        }

        foreach ($data['findings'] as $severity => $findings) {
            if (!is_string($severity) || !is_array($findings)) {
                throw new \UnexpectedValueException('Findings are not grouped by severity.');
            }
        }

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

Twig prema zadanim postavkama escapea prikazane nizove. Zadržite tu zaštitu uključenom: izlaz analizatora nikada se ne smije označiti kao siguran ni prikazati kao sirovi HTML.

Implementirajte otporni HTTP klijent

Vanjski pozivi dobivaju ograničena vremenska ograničenja povezivanja i ukupnog odgovora. Klijent ponovno pokušava kod neuspjeha transporta, HTTP 429 i pogrešaka poslužitelja najviše dvaput nakon prvog pokušaja. Autentikacija, provjera valjanosti i druge pogreške klijenta odmah ne uspijevaju jer ih drugi identičan zahtjev neće popraviti.

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

final class AnalyzerException extends \RuntimeException
{
    public function __construct(
        public readonly string $category,
        public readonly bool $retryable,
        string $message,
        ?\Throwable $previous = null,
    ) {
        parent::__construct($message, 0, $previous);
    }
}
<?php
// src/Security/AnalyzerClient.php
namespace App\Security;

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

final readonly class AnalyzerClient
{
    public function __construct(
        private HttpClientInterface $http,
        private LoggerInterface $logger,
        private string $endpoint,
        private string $token,
    ) {}

    public function analyze(string $url): AssessmentResult
    {
        $host = parse_url($url, PHP_URL_HOST) ?: 'invalid-host';

        for ($attempt = 1; $attempt <= 3; $attempt++) {
            try {
                $response = $this->http->request('POST', $this->endpoint, [
                    'headers' => [
                        'Authorization' => 'Bearer '.$this->token,
                        'Accept' => 'application/json',
                    ],
                    'json' => ['url' => $url],
                    'timeout' => 10.0,
                    'max_duration' => 20.0,
                ]);

                $status = $response->getStatusCode();

                if ($status >= 200 && $status < 300) {
                    try {
                        $payload = json_decode(
                            $response->getContent(false),
                            true,
                            512,
                            JSON_THROW_ON_ERROR
                        );

                        if (!is_array($payload)) {
                            throw new \UnexpectedValueException('Expected a JSON object.');
                        }

                        return AssessmentResult::fromPayload($payload);
                    } catch (\JsonException|\UnexpectedValueException $e) {
                        throw new AnalyzerException(
                            'response_contract',
                            false,
                            'The analyzer returned an invalid response.',
                            $e
                        );
                    }
                }

                $retryable = $status === 429 || $status >= 500;
                $category = match (true) {
                    $status === 401 || $status === 403 => 'authentication',
                    $status === 400 || $status === 422 => 'invalid_target',
                    $status === 429 => 'rate_limited',
                    $status >= 500 => 'upstream',
                    default => 'http_error',
                };

                if (!$retryable || $attempt === 3) {
                    throw new AnalyzerException(
                        $category,
                        $retryable,
                        "Analyzer request failed with HTTP {$status}."
                    );
                }
            } catch (TransportExceptionInterface $e) {
                if ($attempt === 3) {
                    throw new AnalyzerException(
                        'transport',
                        true,
                        'Analyzer transport failed.',
                        $e
                    );
                }
            }

            $this->logger->warning('Website analysis will be retried.', [
                'host' => $host,
                'attempt' => $attempt,
            ]);

            sleep(2 ** ($attempt - 1));
        }

        throw new \LogicException('Retry loop terminated unexpectedly.');
    }
}

Produkcijska varijanta može poštovati numeričko zaglavlje Retry-After, ali bi trebala ograničiti odgodu na konfigurirani maksimum. Nikada nemojte dopustiti da uzvodno zaglavlje neograničeno obustavi radnik.

Pohranite snimke stanja, povijest i stanje raspoređivanja

Sljedeća PostgreSQL migracija odvaja trenutačnu snimku stanja nadzorne ploče od nepromjenjive povijesti procjena. locked_until je obnovljivi zahtjev, a ne trajno zaključavanje; srušeni proces ponovno postaje prihvatljiv nakon isteka zakupa.

CREATE TABLE monitored_site (
    id BIGSERIAL PRIMARY KEY,
    client_id VARCHAR(180) NOT NULL,
    url TEXT NOT NULL,
    interval_minutes INTEGER NOT NULL CHECK (interval_minutes >= 15),
    enabled BOOLEAN NOT NULL DEFAULT TRUE,
    next_assessment_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    locked_until TIMESTAMPTZ NULL,
    last_status VARCHAR(32) NULL,
    last_error_code VARCHAR(64) NULL,
    latest_score DOUBLE PRECISION NULL,
    latest_findings JSONB NULL,
    latest_tls JSONB NULL,
    latest_recommendations JSONB NULL,
    latest_assessed_at TIMESTAMPTZ NULL,
    UNIQUE (client_id, url)
);

CREATE INDEX monitored_site_due_idx
    ON monitored_site (next_assessment_at)
    WHERE enabled = TRUE;

CREATE TABLE security_assessment (
    id BIGSERIAL PRIMARY KEY,
    site_id BIGINT NOT NULL REFERENCES monitored_site(id) ON DELETE CASCADE,
    status VARCHAR(32) NOT NULL,
    score DOUBLE PRECISION NULL,
    findings JSONB NULL,
    tls JSONB NULL,
    recommendations JSONB NULL,
    error_code VARCHAR(64) NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Repozitorij preuzima jedan zapis odjednom. To održava zakup valjanim čak i kada raniji API zahtjev potroši cijeli proračun vremenskog ograničenja.

<?php
// Odabrane metode iz src/Security/AssessmentRepository.php
namespace App\Security;

use Doctrine\DBAL\Connection;

final readonly class AssessmentRepository
{
    public function __construct(private Connection $db) {}

    public function claimOne(): ?array
    {
        $row = $this->db->fetchAssociative(<<<'SQL'
WITH candidate AS (
    SELECT id FROM monitored_site
    WHERE enabled = TRUE
      AND next_assessment_at <= CURRENT_TIMESTAMP
      AND (locked_until IS NULL OR locked_until < CURRENT_TIMESTAMP)
    ORDER BY next_assessment_at
    FOR UPDATE SKIP LOCKED
    LIMIT 1
)
UPDATE monitored_site AS site
SET locked_until = CURRENT_TIMESTAMP + INTERVAL '2 minutes'
FROM candidate
WHERE site.id = candidate.id
RETURNING site.*
SQL);
        return $row ?: null;
    }

    public function saveSuccess(array $site, AssessmentResult $result): void
    {
        $values = [
            'id' => $site['id'],
            'score' => $result->score,
            'findings' => json_encode($result->findingsBySeverity, JSON_THROW_ON_ERROR),
            'tls' => json_encode($result->tlsDetails, JSON_THROW_ON_ERROR),
            'recommendations' => json_encode($result->recommendations, JSON_THROW_ON_ERROR),
        ];

        $this->db->transactional(function (Connection $db) use ($values): void {
            $db->executeStatement(<<<'SQL'
INSERT INTO security_assessment
    (site_id, status, score, findings, tls, recommendations)
VALUES (:id, 'success', :score, :findings::jsonb, :tls::jsonb, :recommendations::jsonb)
SQL, $values);

            $db->executeStatement(<<<'SQL'
UPDATE monitored_site
SET last_status = 'success', last_error_code = NULL,
    latest_score = :score, latest_findings = :findings::jsonb,
    latest_tls = :tls::jsonb, latest_recommendations = :recommendations::jsonb,
    latest_assessed_at = CURRENT_TIMESTAMP, locked_until = NULL,
    next_assessment_at =
        CURRENT_TIMESTAMP + interval_minutes * INTERVAL '1 minute'
WHERE id = :id
SQL, $values);
        });
    }

    public function saveFailure(array $site, AnalyzerException $e): void
    {
        $delay = $e->retryable ? 15 : (int) $site['interval_minutes'];

        $this->db->executeStatement(<<<'SQL'
UPDATE monitored_site
SET last_status = 'failed', last_error_code = :error,
    locked_until = NULL,
    next_assessment_at = CURRENT_TIMESTAMP + :delay * INTERVAL '1 minute'
WHERE id = :id
SQL, ['id' => $site['id'], 'error' => $e->category, 'delay' => $delay]);
    }

    public function dashboard(int $siteId, string $clientId): ?array
    {
        $row = $this->db->fetchAssociative(
            'SELECT * FROM monitored_site WHERE id = :id AND client_id = :client',
            ['id' => $siteId, 'client' => $clientId]
        );

        if (!$row) {
            return null;
        }

        foreach (['latest_findings', 'latest_tls', 'latest_recommendations'] as $field) {
            $row[$field] = $row[$field] === null
                ? []
                : json_decode($row[$field], true, 512, JSON_THROW_ON_ERROR);
        }

        return $row;
    }
}

Pokrenite ponovnu procjenu kao zakazanu naredbu

<?php
// src/Command/ReassessWebsitesCommand.php
namespace App\Command;

use App\Security\AnalyzerClient;
use App\Security\AnalyzerException;
use App\Security\AssessmentRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(name: 'app:security:reassess')]
final class ReassessWebsitesCommand extends Command
{
    public function __construct(
        private readonly AssessmentRepository $repository,
        private readonly AnalyzerClient $analyzer,
        private readonly LoggerInterface $logger,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Maximum sites', '20');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $limit = max(1, min(100, (int) $input->getOption('limit')));

        for ($processed = 0; $processed < $limit; $processed++) {
            $site = $this->repository->claimOne();
            if ($site === null) {
                break;
            }

            try {
                $result = $this->analyzer->analyze($site['url']);
                $this->repository->saveSuccess($site, $result);
            } catch (AnalyzerException $e) {
                $this->repository->saveFailure($site, $e);
                $this->logger->error('Website assessment failed.', [
                    'site_id' => $site['id'],
                    'category' => $e->category,
                    'retryable' => $e->retryable,
                ]);
            }
        }

        return Command::SUCCESS;
    }
}

Pokrećite naredbu svakih pet minuta; next_assessment_at svakog retka određuje njegov stvarni ritam. Na tradicionalnom hostu:

*/5 * * * * cd /srv/security-dashboard && php bin/console app:security:reassess --limit=20 --env=prod

Izložite korisničku nadzornu ploču sigurnu po vlasništvu

Upit baze podataka uključuje i ID web-stranice i ID autentificiranog korisnika. Vraćanje 404 za nepodudaranje izbjegava otkrivanje postoji li web-stranica drugog korisnika.

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

use App\Security\AssessmentRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[IsGranted('ROLE_CLIENT')]
final class SecurityDashboardController extends AbstractController
{
    #[Route('/security-health/{siteId}', name: 'security_dashboard', methods: ['GET'])]
    public function __invoke(int $siteId, AssessmentRepository $repository): Response
    {
        $user = $this->getUser();
        $site = $repository->dashboard($siteId, $user->getUserIdentifier());

        if ($site === null) {
            throw $this->createNotFoundException();
        }

        return $this->render('security/dashboard.html.twig', ['site' => $site]);
    }
}
<h2>Sigurnosno stanje web-stranice</h2>
<p>{{ site.url }}</p>
<p>Status: {{ site.last_status ?? 'Nije procijenjeno' }}</p>

{% if site.latest_assessed_at %}
  <p>Posljednja procjena: {{ site.latest_assessed_at }}</p>
  <p>Rezultat: {{ site.latest_score }}</p>

  <h3>Nalazi</h3>
  {% for severity, findings in site.latest_findings %}
    <h3>{{ severity|title }}</h3>
    <pre>{{ findings|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre>
  {% endfor %}

  <h3>TLS pojedinosti</h3>
  <pre>{{ site.latest_tls|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre>

  <h3>Preporuke</h3>
  <pre>{{ site.latest_recommendations|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre>
{% endif %}

<p>Ovo je ograničeni, neinvazivni pregled javnog HTTPS-a i sigurnosnog
stanja preglednika. Nije penetracijsko testiranje ni sigurnosna certifikacija.</p>

Testirajte bez kontaktiranja usluge

MockHttpClient čini granicu determinističkom. Pokrijte uspjeh, neispravan JSON, neuspjeh autentikacije bez ponovnog pokušaja, ograničenje stope s ograničenim ponovnim pokušajima i neuspjeh transporta. Testovi preuzimanja repozitorija trebali bi se pokretati na PostgreSQL-u jer SQLite ne može reproducirati ponašanje SKIP LOCKED ni PostgreSQL JSON-a.

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

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

final class AnalyzerClientTest extends TestCase
{
    public function testMapsAValidAssessment(): void
    {
        $http = new MockHttpClient(new MockResponse(json_encode([
            'score' => 82,
            'findings' => ['high' => [], 'medium' => [['name' => 'Example']]],
            'tls' => ['enabled' => true],
            'recommendations' => ['Review the medium-severity finding.'],
        ], JSON_THROW_ON_ERROR)));

        $client = new AnalyzerClient(
            $http,
            new NullLogger(),
            'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website',
            'test-token'
        );

        $result = $client->analyze('https://example.com');

        self::assertSame(82, $result->score);
        self::assertArrayHasKey('medium', $result->findingsBySeverity);
    }

    public function testAuthenticationFailureIsNotRetried(): void
    {
        $calls = 0;
        $http = new MockHttpClient(function () use (&$calls): MockResponse {
            $calls++;
            return new MockResponse('{}', ['http_code' => 401]);
        });

        $client = new AnalyzerClient(
            $http,
            new NullLogger(),
            'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website',
            'bad-token'
        );

        try {
            $client->analyze('https://example.com');
            self::fail('Expected AnalyzerException.');
        } catch (AnalyzerException $e) {
            self::assertSame('authentication', $e->category);
            self::assertFalse($e->retryable);
            self::assertSame(1, $calls);
        }
    }
}

Produkcijske zaštitne mjere i česti neuspjesi

Prihvaćajte samo normalizirane https URL-ove za web-stranice koje je korisnik ovlašten nadzirati. Provjera vlasništva domene razuman je zahtjev pri upisu. Odbacite ugrađene vjerodajnice, fragmente, neispravne hostove i nepodržane sheme prije pohrane. Iako vaš poslužitelj šalje URL ograničenom udaljenom analizatoru umjesto da ga izravno dohvaća, kontrole upisa i dalje sprječavaju zloupotrebu.

  • 401 ili 403: provjerite ubrizganu tajnu i oblik autentikacije; nemojte naslijepo pokušavati ponovno.
  • 400 ili 422: označite cilj nevaljanim i zahtijevajte ispravak.
  • 429: zadržite posljednju uspješnu snimku stanja, prikažite zastarjelost i zakažite odgođeni ponovni pokušaj.
  • 5xx ili vremensko ograničenje: koristite ograničeno eksponencijalno čekanje i sačuvajte strukturiranu kategoriju neuspjeha.
  • Neuspjeh ugovora: upozorite operatere; nikada ne zamjenjujte nedostajuće sigurnosne podatke izmišljenim zadanim vrijednostima.

Pratite broj uspjeha i neuspjeha, starost procjena, latenciju, broj ponovnih pokušaja i broj zakašnjelih web-stranica. Upozorite na rastući zaostatak i ponovljene neuspjehe autentikacije. Čuvajte povijesne rezultate prema deklariranoj politici zadržavanja te ograničite pristup zapisnicima i bazi podataka jer nalazi mogu otkriti obrambene slabosti.

Završni kontrolni popis za provjeru

  1. Pokrenite migraciju i potvrdite da aplikacija može dosegnuti PostgreSQL.
  2. Ubrizgajte valjan servisni token putem produkcijskog mehanizma za tajne.
  3. Upišite autoriziranu javnu HTTPS web-stranicu s ID-jem korisnika i intervalom procjene.
  4. Pokrenite php bin/console app:security:reassess --limit=1 --env=prod.
  5. Potvrdite uspješan redak povijesti i ažuriranu snimku stanja nadzorne ploče.
  6. Prijavite se kao vlasnik korisnik i provjerite rezultat, nalaze, TLS pojedinosti, preporuke i vrijeme procjene.
  7. Provjerite da drugi korisnik dobiva 404 za isti ID web-stranice.
  8. U testovima simulirajte odgovore 401, 429, neispravan JSON i vremensko ograničenje.
  9. Omogućite raspoređivač i upozorite kada procjene postanu zakašnjele.

Najjača značajka ove nadzorne ploče nije njezin rezultat. To je disciplinirani lanac iza tog rezultata: autorizirani cilj, ograničena procjena, provjeren ugovor, snimka stanja koja se može revidirati i poznato vrijeme osvježavanja. Taj lanac pretvara udaljeni sigurnosni signal u nešto što korisnici mogu razumjeti — i čemu operateri mogu vjerovati.

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.