Symfony: Automatizirajte sigurnosne revizije nakon implementacije uz AI
Implementacija može biti zdrava, brza i potpuno dostupna, a da pritom neprimjetno oslabi sigurnosni položaj preglednika. Nedostajuće sigurnosno zaglavlje, neočekivana promjena TLS-a ili previše dopuštajuće pravilo možda uopće neće pokrenuti testove aplikacije. Praktičan je odgovor učiniti javnu sigurnosnu reviziju dijelom životnog ciklusa implementacije, odmah nakon što nova verzija postane dostupna.
Ovaj vodič gradi tu integraciju u Symfonyju na PHP-u 8.3 ili novijem. Konzolna naredba šalje produkcijski URL alatu Website Security Analyzer, mapira odgovor u tipizirani objekt domene, prikazuje sažet izvještaj o implementaciji i jasno ne uspijeva kada se sama revizija ne može dovršiti. Analiza je ograničena i neinvazivna: procjenjuje javni HTTPS i sigurnosni položaj preglednika, ali nije penetracijsko testiranje.
Dobijte pristup i kopirajte token usluge
Prije pisanja integracijskog koda stvorite račun koji posjeduje pretplatu na uslugu ili mu pristupite:
- 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 plan Free, Plus ili Pro i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Service token i kopirajte token ograničen na uslugu koji je ondje prikazan.
Analizator zahtijeva autentikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Upotrijebit ćemo Bearer token jer ga Symfony može izravno smjestiti u autorizacijsko zaglavlje. Izbjegavajte parametar upita u produkciji: veća je vjerojatnost da će se URL-ovi nego zaglavlja pojaviti u proxy i pristupnim zapisnicima.
Ponovno generiranje tokena usluge opoziva prethodno aktivni token. Rotaciju tretirajte kao koordiniranu promjenu konfiguracije: instalirajte novu vrijednost u spremište tajni za implementaciju, implementirajte je, provjerite reviziju i tek tada smatrate rotaciju dovršenom.
Potvrdite krajnju točku prije izrade funkcionalnosti
Točan API poziv jest POST https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website. Njegovo JSON tijelo zahtjeva sadrži javni url:
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://www.example.com"}'
Pokrenite ovo nad javnom HTTPS adresom, a ne nad localhost, internim nazivom hosta ili pregledom zaštićenim privatnim umrežavanjem. Usluga mora moći doseći istu web-stranicu koju dosežu posjetitelji.
Za lokalni razvoj stavite rezervirana mjesta u .env, a stvarne lokalne vrijednosti u nepraćeni .env.local. U produkciji umetnite obje varijable putem upravitelja tajni hosta ili CI-ja umjesto da predajete produkcijsku datoteku okruženja:
# .env
PUBLIC_SITE_URL=https://www.example.com
WEBSITE_SECURITY_ANALYZER_TOKEN=YOUR_SERVICE_TOKEN
# .env.local, never committed
WEBSITE_SECURITY_ANALYZER_TOKEN=replace-with-your-service-scoped-token
Odaberite namjerno malu arhitekturu
Ovom projektu ne trebaju kontroler ni Messenger red. Sustav implementacije već pruža pouzdanu orkestraciju, a naredba za reviziju ima koristan sinkroni ugovor: uspjeh znači da je odgovor dohvaćen i mapiran; neuspjeh znači da provjera nije dovršena.
Rezultirajuća struktura projekta je sažeta:
src/
Command/AuditPublicWebsiteCommand.php
SecurityAudit/SecurityAuditResult.php
SecurityAudit/AnalyzerException.php
SecurityAudit/WebsiteSecurityAnalyzer.php
SecurityAudit/WebsiteSecurityAnalyzerClient.php
tests/
SecurityAudit/WebsiteSecurityAnalyzerClientTest.php
config/
services.yaml
HTTP klijent upravlja transportnim aspektima, objekt rezultata predstavlja odgovor domene, a naredba upravlja prezentacijom usmjerenom na implementaciju. To razdvajanje također čini vanjski poziv determinističkim u testovima.
Mapirajte odgovor na granici aplikacije
Vanjski JSON nikada ne smije nekontrolirano prolaziti kroz aplikaciju. Odgovor analizatora pruža rezultat, nalaze grupirane po ozbiljnosti, TLS pojedinosti i preporuke. Mapper u nastavku provjerava njihove tipove spremnika bez pretpostavljanja nedokumentiranih polja unutar pojedinačnih nalaza ili TLS pojedinosti.
<?php
// src/SecurityAudit/SecurityAuditResult.php
namespace App\SecurityAudit;
final readonly class SecurityAuditResult
{
/**
* @param array<string, list<array<string, mixed>>> $findings
* @param array<string, mixed> $tls
* @param list<string> $recommendations
*/
public function __construct(
public int|float $score,
public array $findings,
public array $tls,
public array $recommendations,
) {
}
/** @param array<string, mixed> $payload */
public static function fromPayload(array $payload): self
{
if (!isset($payload['score']) || !is_numeric($payload['score'])) {
throw AnalyzerException::invalidResponse('Missing or invalid score.');
}
if (!isset($payload['findings']) || !is_array($payload['findings'])) {
throw AnalyzerException::invalidResponse('Missing severity-grouped findings.');
}
$findings = [];
foreach ($payload['findings'] as $severity => $items) {
if (!is_string($severity) || !is_array($items)) {
throw AnalyzerException::invalidResponse('Invalid findings group.');
}
foreach ($items as $item) {
if (!is_array($item)) {
throw AnalyzerException::invalidResponse('Invalid finding.');
}
}
$findings[$severity] = array_values($items);
}
if (!isset($payload['tls']) || !is_array($payload['tls'])) {
throw AnalyzerException::invalidResponse('Missing or invalid TLS details.');
}
if (!isset($payload['recommendations']) || !is_array($payload['recommendations'])) {
throw AnalyzerException::invalidResponse('Missing recommendations.');
}
$recommendations = [];
foreach ($payload['recommendations'] as $recommendation) {
if (!is_string($recommendation)) {
throw AnalyzerException::invalidResponse('Invalid recommendation.');
}
$recommendations[] = $recommendation;
}
return new self(
score: $payload['score'] + 0,
findings: $findings,
tls: $payload['tls'],
recommendations: $recommendations,
);
}
}
Namjerno je zadržati unutarnje vrijednosti nalaza i TLS-a kao provjerene nizove. Time se izbjegava izmišljanje nedokumentiranih naziva polja, a istodobno sprječava da neispravni podaci najviše razine kontaminiraju naredbu.
Implementirajte ograničeno HTTP ponašanje
Produkcijske integracije trebaju izričita vremenska ograničenja i selektivna ponavljanja. Neuspjesi autentikacije i provjere valjanosti trebali bi se vratiti odmah; njihovo ponavljanje troši kvotu i skriva pogreške konfiguracije. Ograničenja brzine, pogreške poslužitelja i prekidi transporta mogu biti prolazni, pa ih ovaj klijent ponavlja uz ograničeni eksponencijalni povratni odmak.
<?php
// src/SecurityAudit/AnalyzerException.php
namespace App\SecurityAudit;
final class AnalyzerException extends \RuntimeException
{
public function __construct(
string $message,
public readonly string $category,
public readonly ?int $statusCode = null,
public readonly bool $retryable = false,
?\Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public static function invalidResponse(string $message): self
{
return new self($message, 'invalid_response');
}
}
<?php
// src/SecurityAudit/WebsiteSecurityAnalyzer.php
namespace App\SecurityAudit;
interface WebsiteSecurityAnalyzer
{
public function analyze(string $url): SecurityAuditResult;
}
<?php
// src/SecurityAudit/WebsiteSecurityAnalyzerClient.php
namespace App\SecurityAudit;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class WebsiteSecurityAnalyzerClient implements WebsiteSecurityAnalyzer
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website';
public function __construct(
private HttpClientInterface $httpClient,
private LoggerInterface $logger,
private string $token,
) {
if ($this->token === '' || $this->token === 'YOUR_SERVICE_TOKEN') {
throw new \InvalidArgumentException('Analyzer token is not configured.');
}
}
public function analyze(string $url): SecurityAuditResult
{
if (filter_var($url, FILTER_VALIDATE_URL) === false
|| parse_url($url, PHP_URL_SCHEME) !== 'https') {
throw new \InvalidArgumentException('PUBLIC_SITE_URL must be a valid HTTPS URL.');
}
for ($attempt = 1; $attempt <= 3; ++$attempt) {
try {
$response = $this->httpClient->request('POST', self::ENDPOINT, [
'auth_bearer' => $this->token,
'json' => ['url' => $url],
'timeout' => 10.0,
'max_duration' => 30.0,
]);
$status = $response->getStatusCode();
if ($status >= 200 && $status < 300) {
return SecurityAuditResult::fromPayload($response->toArray(false));
}
$retryable = $status === 429 || $status >= 500;
if ($retryable && $attempt < 3) {
$this->logger->warning('Security audit request will be retried.', [
'attempt' => $attempt,
'status' => $status,
'host' => parse_url($url, PHP_URL_HOST),
]);
$this->backoff($attempt);
continue;
}
$category = match ($status) {
401, 403 => 'authentication',
400, 422 => 'validation',
429 => 'rate_limit',
default => $status >= 500 ? 'service' : 'http',
};
throw new AnalyzerException(
"Analyzer request failed with HTTP {$status}.",
$category,
$status,
$retryable,
);
} catch (DecodingExceptionInterface $exception) {
throw new AnalyzerException(
'Analyzer returned invalid JSON.',
'invalid_response',
previous: $exception,
);
} catch (TransportExceptionInterface $exception) {
if ($attempt < 3) {
$this->logger->warning('Security audit transport failure; retrying.', [
'attempt' => $attempt,
'host' => parse_url($url, PHP_URL_HOST),
]);
$this->backoff($attempt);
continue;
}
throw new AnalyzerException(
'Analyzer transport failed after three attempts.',
'transport',
retryable: true,
previous: $exception,
);
}
}
throw new \LogicException('Unreachable retry state.');
}
private function backoff(int $attempt): void
{
usleep(250_000 * (2 ** ($attempt - 1)));
}
}
Zapisnici uključuju odredišni host, pokušaj i status, ali nikada token, autorizacijsko zaglavlje, cijelo tijelo odgovora ni osjetljivi kontekst iznimke. Maksimalni povratni odmak ostaje kratak, dok timeout ograničava neaktivnost, a max_duration ograničava svaku HTTP operaciju.
Konfigurirajte ubrizgavanje ovisnosti
Središnje povežite vrijednosti okruženja i dodijelite sučelju njegov konkretni klijent:
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
bind:
$token: '%env(WEBSITE_SECURITY_ANALYZER_TOKEN)%'
$publicUrl: '%env(PUBLIC_SITE_URL)%'
App\:
resource: '../src/'
App\SecurityAudit\WebsiteSecurityAnalyzer:
alias: App\SecurityAudit\WebsiteSecurityAnalyzerClient
Ovo koristi Symfonyjev HTTP klijent prve strane putem HttpClientInterface. U postojećoj Symfony aplikaciji osigurajte prisutnost komponente pomoću composer require symfony/http-client.
Izradite naredbu za implementaciju
Naredba ispisuje samo operativni sažetak. Ne označava nalaze kao dokaz iskorištavanja ranjivosti i ne pretvara se da rezultat zamjenjuje autentificirano skeniranje, pregled koda, reviziju ovisnosti ili penetracijsko testiranje.
<?php
// src/Command/AuditPublicWebsiteCommand.php
namespace App\Command;
use App\SecurityAudit\AnalyzerException;
use App\SecurityAudit\WebsiteSecurityAnalyzer;
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\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:security:audit-public-site',
description: 'Analyzes the deployed public website security posture.',
)]
final class AuditPublicWebsiteCommand extends Command
{
public function __construct(
private readonly WebsiteSecurityAnalyzer $analyzer,
private readonly LoggerInterface $logger,
private readonly string $publicUrl,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
try {
$result = $this->analyzer->analyze($this->publicUrl);
} catch (AnalyzerException|\InvalidArgumentException $exception) {
$this->logger->error('Post-deployment security audit failed.', [
'category' => $exception instanceof AnalyzerException
? $exception->category
: 'configuration',
'status' => $exception instanceof AnalyzerException
? $exception->statusCode
: null,
]);
$io->error($exception->getMessage());
return Command::FAILURE;
}
$counts = [];
foreach ($result->findings as $severity => $findings) {
$counts[$severity] = count($findings);
}
$this->logger->notice('Post-deployment security audit completed.', [
'score' => $result->score,
'finding_counts' => $counts,
'recommendation_count' => count($result->recommendations),
]);
$io->success(sprintf('Security audit completed. Score: %s', $result->score));
foreach ($counts as $severity => $count) {
$io->writeln(sprintf('%s findings: %d', ucfirst($severity), $count));
}
$io->writeln(sprintf(
'Recommendations: %d; TLS details received: %s',
count($result->recommendations),
$result->tls === [] ? 'no' : 'yes',
));
return Command::SUCCESS;
}
}
Testirajte granicu bez mrežnih poziva
MockHttpClient pruža deterministički transport prve strane. Ovi testovi provjeravaju krajnju točku, autentikaciju, tijelo zahtjeva, mapiranje, ponašanje ponavljanja i odbacivanje neispravnih odgovora.
<?php
// tests/SecurityAudit/WebsiteSecurityAnalyzerClientTest.php
namespace App\Tests\SecurityAudit;
use App\SecurityAudit\AnalyzerException;
use App\SecurityAudit\WebsiteSecurityAnalyzerClient;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class WebsiteSecurityAnalyzerClientTest extends TestCase
{
public function testItSendsAndMapsAnAudit(): void
{
$http = new MockHttpClient(
function (string $method, string $url, array $options): MockResponse {
self::assertSame('POST', $method);
self::assertSame(
'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website',
$url,
);
self::assertSame('Bearer test-token', $options['normalized_headers']['authorization'][0]);
self::assertSame(
['url' => 'https://www.example.com'],
json_decode($options['body'], true, flags: JSON_THROW_ON_ERROR),
);
return new MockResponse(json_encode([
'score' => 91,
'findings' => ['high' => [], 'medium' => [['id' => 'example']]],
'tls' => ['enabled' => true],
'recommendations' => ['Review the reported medium finding.'],
], JSON_THROW_ON_ERROR));
},
);
$result = (new WebsiteSecurityAnalyzerClient(
$http,
new NullLogger(),
'test-token',
))->analyze('https://www.example.com');
self::assertSame(91, $result->score);
self::assertCount(1, $result->findings['medium']);
self::assertTrue($result->tls['enabled']);
}
public function testItRetriesAServiceFailure(): void
{
$http = new MockHttpClient([
new MockResponse('', ['http_code' => 503]),
new MockResponse(json_encode([
'score' => 100,
'findings' => [],
'tls' => [],
'recommendations' => [],
], JSON_THROW_ON_ERROR)),
]);
$result = (new WebsiteSecurityAnalyzerClient(
$http,
new NullLogger(),
'test-token',
))->analyze('https://www.example.com');
self::assertSame(100, $result->score);
self::assertSame(2, $http->getRequestsCount());
}
public function testItRejectsMalformedPayloads(): void
{
$http = new MockHttpClient(new MockResponse('{"score":90}'));
$client = new WebsiteSecurityAnalyzerClient($http, new NullLogger(), 'test-token');
$this->expectException(AnalyzerException::class);
$client->analyze('https://www.example.com');
}
}
Pokrenite skup testova s php bin/phpunit. Ako vaša aplikacija pokreće PHPUnit drugom standardnom naredbom projekta, upotrijebite taj omotač, uz nepromijenjen testni transport.
Postavite reviziju nakon prebacivanja produkcijskog prometa
Analizator mora pregledati novu implementiranu javnu verziju. Pokrenite naredbu nakon migracija, zagrijavanja predmemorije, provjera zdravlja i prebacivanja balansirača opterećenja ili simboličke poveznice — ne dok stara verzija još poslužuje javni naziv hosta.
curl --fail --silent --show-error \
--max-time 15 \
'https://www.example.com/health'
php bin/console app:security:audit-public-site \
--env=prod \
--no-debug
Priložite ove naredbe fazi nakon implementacije koju koristi vaša hosting platforma. Rezultat naredbe različit od nule trebao bi označiti fazu revizije neuspješnom i pokrenuti obavijest, ali ne bi smio automatski uništiti inače zdravu verziju. Prekid rada vanjskog analizatora i neispravna implementacija aplikacije različite su domene neuspjeha. Ponovite fazu revizije zasebno ili neka operater pregleda njezinu strukturiranu kategoriju neuspjeha.
Ako bi nalazi u budućnosti trebali blokirati promicanje, uvedite to pravilo izričito. Primjerice, odlučite koje su normalizirane skupine ozbiljnosti blokirajuće i uzmite u obzir odobrene iznimke. Nemojte potajno pretvoriti promjenjiv rezultat treće strane u neprijavljeno pravilo povrata.
Sigurnost, vidljivost i česti neuspjesi
Ograničite token na uslugu za koju je izdan, čuvajte ga u upravitelju tajni i ograničite tko može čitati produkcijske vrijednosti okruženja. Redigirajte autorizacijska zaglavlja u integracijama za HTTP praćenje i izvještavanje o pogreškama. Ako je token izložen, ponovno ga generirajte, imajući na umu da se prethodno aktivni token odmah opoziva.
Pratite dovršetak revizije kao i nalaze. Korisne dimenzije uključuju identifikator implementacije, ciljni host, rezultat, broj po ozbiljnosti, latenciju, broj ponavljanja, HTTP status i kategoriju neuspjeha. Nikada nemojte upotrebljavati token kao korelacijsku vrijednost zapisnika.
- HTTP 401 ili 403: potvrdite da je aktivni token ograničen na uslugu stigao do produkcijskog procesa. Nemojte slijepo ponavljati; ponovno generiranje je možda opozvalo implementiranu vrijednost.
- HTTP 400 ili 422: provjerite sadrži li zahtjev JSON
urli je liPUBLIC_SITE_URLvaljan javni HTTPS URL. - HTTP 429: trenutačni plan ili stopa zahtjeva možda ograničavaju pozive. Istodobno ponavljanje pri svakoj implementaciji može pogoršati problem; prema potrebi serijalizirajte revizije implementacija.
- Prekid transporta: razlikujte povezivost analizatora od zdravlja aplikacije. Ograničena ponavljanja trebala bi predvidljivo završiti i ostaviti vidljivu neuspješnu fazu revizije.
- Neočekivani oblik odgovora: tretirajte ga kao neuspjeh integracije. Zadržite tipiziranu granicu i ažurirajte je tek nakon provjere službene dokumentacije.
- Rezultati opisuju prethodnu verziju: premjestite naredbu nakon prebacivanja DNS-a, proxyja ili verzije te pričekajte samo da uobičajena provjera zdravlja potvrdi da se nova verzija javno poslužuje.
Završni kontrolni popis za provjeru
- Produkcijski URL je javan, koristi HTTPS i razrješava se na novopostavljenu verziju.
- Plan Free, Plus ili Pro aktivan je za uslugu analizatora.
- Token usluge dolazi iz ploče Service token na stranici dokumentacije.
- Token se umeće putem konfiguracije podržane varijablama okruženja i nije prisutan u kontroli izvornog koda ni zapisnicima.
- Klijent šalje
POSTna točnu krajnju točku analizatora s JSON-omurl. - Vremenska ograničenja i ponavljanja su ograničeni, dok se neuspjesi autentikacije i provjere valjanosti ne ponavljaju.
- Rezultat, nalazi grupirani po ozbiljnosti, TLS pojedinosti i preporuke provjeravaju se na granici API-ja.
php bin/phpunitprolazi bez ovisnosti o aktivnoj mreži.- Naredba za reviziju pokreće se nakon svakog prebacivanja produkcijskog prometa, a njezin je neuspjeh vidljiv.
- Rezultat se priopćava kao analiza javnog sigurnosnog položaja, nikada kao penetracijsko testiranje.
Revizija nakon implementacije najvrjednija je kada se ponaša kao uobičajena inženjerska infrastruktura: predvidljivi ulazi, ograničeno izvršavanje, tipizirani izlazi, izričita semantika neuspjeha i bez potrebe za herojskim tumačenjem. Kada je ova naredba dio puta izdanja, položaj preglednika i TLS-a prestaju biti nešto što se tim povremeno sjeti pregledati. Postaju još jedno svojstvo koje se provjerava svaki put kada se produkcija promijeni — upravo tu sigurnosna automatizacija zaslužuje svoje mjesto.