Vodiči

Secure Your Laravel Deployments: Automate Website Security Scans Post-Launch

Osigurajte svoje Laravel implementacije: automatizirajte sigurnosna skeniranja web-stranica nakon pokretanja

Implementacija nije dovršena samo zato što novo izdanje odgovara na provjeru stanja. Promjene konfiguracije, pravila proxyja, obnavljanja certifikata i nadogradnje okvira mogu tiho promijeniti sigurnosni položaj inače zdrave stranice.

Ovaj vodič dodaje ograničenu, neinvazivnu sigurnosnu analizu web-stranice u Laravelov cjevovod implementacije. Nakon svakog produkcijskog izdanja, Artisan naredba provjerava javnu HTTPS krajnju točku, preslikava rezultat u tipizirani domenski objekt, pohranjuje najnovije izvješće i vraća smislen izlazni kôd. Skeniranje procjenjuje položaj preglednika i TLS-a; ne smije se predstavljati kao penetracijski test ili zamjena za autentificirano sigurnosno testiranje.

Dobijte pristup Website Security Analyzeru

Započnite registracijom računa ili upotrijebite stranicu za prijavu ako ga već imate.

  1. Otvorite stranicu usluge Website Security Analyzer.
  2. Odaberite dostupni Free, Plus ili Pro paket i dovršite njegovu aktivaciju.
  3. Otvorite službenu dokumentaciju usluge.
  4. Pronađite ploču Service token i kopirajte njezin token ograničen na uslugu.
  5. Pohranite taj token u konfiguraciju projekta podržanu varijablama okruženja.

Ponovno generiranje tokena usluge opoziva prethodno aktivni token. Uskladite rotaciju s konfiguracijom implementacije kako produkcija nikada ne bi ovisila o tokenu koji je već poništen.

API prihvaća Bearer token, zaglavlje X-API-Token ili parametar tokena u upitu. Ovaj projekt koristi Bearer autentifikaciju. Zaglavlja su poželjnija od parametara upita jer je veća vjerojatnost da će se URL-ovi pojaviti u zapisnicima pristupa, alatima za nadzor, povijesti preglednika i dijagnostičkom izlazu.

Potvrdite točnu krajnju točku

Integracija šalje POST zahtjeve na https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website. JSON tijelo sadrži jednu obaveznu vrijednost: url.

Prije pisanja Laravel koda, pošaljite jedan minimalan zahtjev s pouzdanog terminala. Zamijenite oba rezervirana mjesta i izbjegavajte spremanje naredbe u zajedničke shell skripte ili dokumentaciju:

curl --request POST \
  --url 'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer YOUR_SERVICE_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{"url":"https://www.example.com"}'

Stavite vjerodajnicu i cilj implementacije u lokalnu konfiguraciju .env tijekom razvoja. U produkciji ubrizgajte ekvivalentne vrijednosti putem platforme za implementaciju ili upravitelja tajni. Nikada nemojte predati popunjenu datoteku okruženja u repozitorij.

WEBSITE_ANALYZER_TOKEN=YOUR_SERVICE_TOKEN
WEBSITE_ANALYZER_URL=https://www.example.com
WEBSITE_ANALYZER_CONNECT_TIMEOUT=3
WEBSITE_ANALYZER_TIMEOUT=15

Odaberite arhitekturu prilagođenu implementaciji

Projektu su potrebne samo četiri aplikacijske komponente: konfiguracija podržana varijablama okruženja, API klijent, domenski rezultat i Artisan naredba. Naredba je namjerno sinkrona. Posao u redu čekanja omogućio bi raniji završetak implementacije, ali bi također odvojio rezultat skeniranja od izdanja koje ga je pokrenulo. Sinkrona naredba sustavu implementacije daje trenutačni izlazni kôd uspjeha ili neuspjeha.

Skeniranje bi se trebalo pokrenuti nakon što je novo izdanje aktivno i njegova javna provjera stanja uspije. Pokretanje prije nego što promet dosegne izdanje pregledalo bi staru implementaciju ili interni naziv hosta umjesto stvarne putanje vidljive pregledniku.

Relevantna struktura projekta je:

app/
  Console/Commands/AnalyzeProductionWebsite.php
  Domain/Security/WebsiteAnalysis.php
  Services/WebsiteAnalyzerClient.php
config/
  services.php
tests/
  Feature/AnalyzeProductionWebsiteTest.php

Povežite Laravel konfiguraciju s okruženjem

Dodajte namjenski unos u config/services.php. Konfiguracijske datoteke mogu čitati varijable okruženja; aplikacijske klase trebale bi čitati Laravel konfiguraciju. Ta razlika održava config:cache pouzdanim.

<?php

return [
    // Existing service configuration...

    'website_analyzer' => [
        'token' => env('WEBSITE_ANALYZER_TOKEN'),
        'url' => env('WEBSITE_ANALYZER_URL'),
        'connect_timeout' => (int) env('WEBSITE_ANALYZER_CONNECT_TIMEOUT', 3),
        'timeout' => (int) env('WEBSITE_ANALYZER_TIMEOUT', 15),
    ],
];

Preslikajte odgovor na granici aplikacije

Udaljeni JSON ne bi smio prolaziti kroz aplikaciju bez provjere. Rezultat analizatora sadrži ocjenu, nalaze grupirane prema ozbiljnosti, TLS pojedinosti i preporuke. Mali nepromjenjivi objekt provjerava te oblike prije nego što se kôd implementacije osloni na njih.

<?php

namespace App\Domain\Security;

use UnexpectedValueException;

final readonly class WebsiteAnalysis
{
    public function __construct(
        public int|float $score,
        public array $findings,
        public array $tls,
        public array $recommendations,
    ) {}

    public static function fromArray(array $payload): self
    {
        $score = $payload['score'] ?? null;
        $findings = $payload['findings'] ?? null;
        $tls = $payload['tls'] ?? null;
        $recommendations = $payload['recommendations'] ?? null;

        if (! is_int($score) && ! is_float($score)) {
            throw new UnexpectedValueException('Analyzer score is missing or invalid.');
        }

        if (! is_array($findings) || ! is_array($tls) || ! is_array($recommendations)) {
            throw new UnexpectedValueException('Analyzer result sections are missing or invalid.');
        }

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

        foreach ($recommendations as $recommendation) {
            if (! is_string($recommendation)) {
                throw new UnexpectedValueException('Analyzer recommendations are invalid.');
            }
        }

        return new self($score, $findings, $tls, array_values($recommendations));
    }

    public function toArray(): array
    {
        return [
            'score' => $this->score,
            'findings' => $this->findings,
            'tls' => $this->tls,
            'recommendations' => $this->recommendations,
        ];
    }
}

Ovo preslikavanje namjerno izbjegava pretpostavke o nedokumentiranim pragovima ocjene, nazivima nalaza ili TLS potpoljima. Ako se ugovor usluge razvije, klijent će proizvesti vidljivi kvar protokola umjesto tihog spremanja obmanjujućih djelomičnih podataka.

Izradite ograničen API klijent svjestan ponovnih pokušaja

Laravelov ugrađeni HTTP klijent pruža JSON kodiranje, Bearer autentifikaciju, vremenska ograničenja i determinističke lažne odgovore za testove. Donji klijent ponovno pokušava nakon neuspjeha povezivanja, HTTP 429 i odabranih prolaznih odgovora poslužitelja. Ne pokušava ponovno nakon neuspjeha autentifikacije ili provjere valjanosti, jer ponovljeni identični zahtjevi ne mogu popraviti loš token ili URL.

<?php

namespace App\Services;

use App\Domain\Security\WebsiteAnalysis;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Throwable;
use UnexpectedValueException;

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

    public function __construct(private LoggerInterface $logger) {}

    public function analyze(string $url): WebsiteAnalysis
    {
        $token = config('services.website_analyzer.token');

        if (! is_string($token) || $token === '') {
            throw new RuntimeException('Website analyzer token is not configured.');
        }

        if (filter_var($url, FILTER_VALIDATE_URL) === false
            || parse_url($url, PHP_URL_SCHEME) !== 'https'
            || ! is_string(parse_url($url, PHP_URL_HOST))) {
            throw new RuntimeException('The analyzer target must be a valid HTTPS URL.');
        }

        for ($attempt = 1; $attempt <= 3; $attempt++) {
            try {
                $response = Http::withToken($token)
                    ->acceptJson()
                    ->asJson()
                    ->connectTimeout((int) config(
                        'services.website_analyzer.connect_timeout',
                        3
                    ))
                    ->timeout((int) config('services.website_analyzer.timeout', 15))
                    ->post(self::ENDPOINT, ['url' => $url]);
            } catch (ConnectionException $exception) {
                if ($attempt === 3) {
                    throw new RuntimeException(
                        'Could not connect to the website analyzer.',
                        previous: $exception
                    );
                }

                $this->pauseBeforeRetry($attempt, null);
                continue;
            }

            if ($response->successful()) {
                $payload = $response->json();

                if (! is_array($payload)) {
                    throw new RuntimeException('Analyzer returned malformed JSON.');
                }

                try {
                    return WebsiteAnalysis::fromArray($payload);
                } catch (UnexpectedValueException $exception) {
                    throw new RuntimeException(
                        'Analyzer returned an unexpected response contract.',
                        previous: $exception
                    );
                }
            }

            if (in_array($response->status(), [401, 403], true)) {
                throw new RuntimeException('Analyzer authentication was rejected.');
            }

            if ($response->status() === 422) {
                throw new RuntimeException('Analyzer rejected the target URL.');
            }

            $retryable = $response->status() === 429
                || in_array($response->status(), [500, 502, 503, 504], true);

            if (! $retryable || $attempt === 3) {
                throw new RuntimeException(
                    'Analyzer request failed with HTTP '.$response->status().'.'
                );
            }

            $retryAfter = $response->header('Retry-After');
            $this->pauseBeforeRetry(
                $attempt,
                is_string($retryAfter) && ctype_digit($retryAfter)
                    ? (int) $retryAfter
                    : null
            );
        }

        throw new RuntimeException('Analyzer request ended unexpectedly.');
    }

    private function pauseBeforeRetry(int $attempt, ?int $retryAfterSeconds): void
    {
        $milliseconds = $retryAfterSeconds === null
            ? 250 * (2 ** ($attempt - 1))
            : $retryAfterSeconds * 1000;

        $milliseconds = min(2000, max(250, $milliseconds));

        $this->logger->warning('Retrying website security analysis.', [
            'attempt' => $attempt + 1,
            'delay_ms' => $milliseconds,
        ]);

        usleep($milliseconds * 1000);
    }
}

Ograničenje od dvije sekunde za eksponencijalno čekanje održava vrijeme implementacije predvidljivim. Konačni 429 ostaje neuspjeh umjesto da proces izdanja čeka neograničeno. Ako odabrani paket redovito doseže kvotu, zakazivanje manjeg broja implementacija ili promjena paketa iskrenije je od skrivanja stanja iza agresivnih ponovnih pokušaja.

Izradite naredbu nakon implementacije

Naredba dohvaća konfigurirani cilj, poziva klijent, zapisuje strojno čitljivo izvješće na Laravelov lokalni disk i emitira sažet događaj zapisnika. Ne zapisuje token, potpuni odgovor ni potencijalno osjetljive pojedinosti nalaza.

<?php

namespace App\Console\Commands;

use App\Services\WebsiteAnalyzerClient;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Throwable;

final class AnalyzeProductionWebsite extends Command
{
    protected $signature = 'security:analyze-production';
    protected $description = 'Analyze the public production website security posture';

    public function handle(WebsiteAnalyzerClient $client): int
    {
        $url = config('services.website_analyzer.url');

        if (! is_string($url) || $url === '') {
            $this->error('WEBSITE_ANALYZER_URL is not configured.');
            return self::FAILURE;
        }

        try {
            $analysis = $client->analyze($url);

            $report = [
                'scanned_at' => now()->toIso8601String(),
                'url' => $url,
                ...$analysis->toArray(),
            ];

            Storage::disk('local')->put(
                'security/last-website-scan.json',
                json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)
            );

            Log::info('Production website security analysis completed.', [
                'url' => $url,
                'score' => $analysis->score,
                'finding_groups' => array_map('count', $analysis->findings),
            ]);

            $this->info('Website analysis completed. Score: '.$analysis->score);
            return self::SUCCESS;
        } catch (Throwable $exception) {
            report($exception);
            $this->error('Website analysis failed: '.$exception->getMessage());
            return self::FAILURE;
        }
    }
}

Testirajte uspjeh, pohranu i ponovne pokušaje

Http::fake() sprječava testove da kontaktiraju stvarnu uslugu. Prvi test provjerava preslikavanje odgovora i pohranu izvješća. Drugi dokazuje da se prolazni neuspjeh ponavlja i na kraju uspijeva.

<?php

namespace Tests\Feature;

use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

final class AnalyzeProductionWebsiteTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        config()->set('services.website_analyzer.token', 'test-token');
        config()->set('services.website_analyzer.url', 'https://www.example.com');
        Storage::fake('local');
    }

    public function test_it_analyzes_and_stores_the_production_report(): void
    {
        Http::fake([
            '*' => Http::response([
                'score' => 88,
                'findings' => [
                    'high' => [],
                    'medium' => [['summary' => 'Example finding']],
                ],
                'tls' => ['enabled' => true],
                'recommendations' => ['Review the reported browser policy.'],
            ], 200),
        ]);

        $this->artisan('security:analyze-production')->assertExitCode(0);

        Storage::disk('local')
            ->assertExists('security/last-website-scan.json');

        Http::assertSent(function (Request $request): bool {
            return $request->method() === 'POST'
                && $request->url()
                    === 'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website'
                && $request['url'] === 'https://www.example.com'
                && $request->hasHeader('Authorization', 'Bearer test-token');
        });
    }

    public function test_it_retries_a_transient_server_failure(): void
    {
        Http::fakeSequence()
            ->push([], 503)
            ->push([
                'score' => 95,
                'findings' => ['high' => []],
                'tls' => ['enabled' => true],
                'recommendations' => [],
            ], 200);

        $this->artisan('security:analyze-production')->assertExitCode(0);

        Http::assertSentCount(2);
    }
}

Dodajte dodatne testove za 401, 422, ponovljene odgovore 429, iznimke povezivanja, neispravan JSON i nedostajuće odjeljke odgovora. To nisu usputni rubni slučajevi: oni određuju jesu li neuspjesi implementacije razumljivi u trenutku kada netko mora reagirati na njih.

Smjestite skeniranje u produkcijsku implementaciju

Pokrenite testove i predmemorirajte konfiguraciju prije prebacivanja izdanja. Nakon što je nova verzija aktivna, pričekajte njezinu javnu provjeru stanja i izvršite naredbu:

php artisan test
php artisan config:cache

# Activate the release and complete the public health check here.

php artisan security:analyze-production

Zadržite nenulti izlazni status naredbe vidljivim sustavu implementacije. Treba li izdanje vratiti unatrag poslovna je odluka. Nedostupnost API-ja obično opravdava upozorenje i ručnu provjeru, dok ozbiljan novi nalaz može opravdati blokiranje promocije. Nemojte nijednu od tih politika izvoditi iz nedokumentiranog praga ocjene.

Sigurnost, vidljivost i česti neuspjesi

  • Zaštitite vjerodajnicu: držite je u upravitelju tajni ili zaštićenoj postavci okruženja, ograničite pristup operatorima implementacije i namjerno je rotirajte.
  • Kontrolirajte cilj: prihvaćajte URL samo iz pouzdane konfiguracije implementacije. Ne izlažite ovaj klijent putem javnog kontrolera koji prihvaća proizvoljne URL-ove.
  • Sačuvajte korisne dokaze: zadržite generirano izvješće kao zaštićeni artefakt implementacije ako je lokalna pohrana izdanja privremena.
  • Upozoravajte na strukturirane ishode: pratite izlazne kodove naredbi, kategorije HTTP statusa, broj ponovnih pokušaja, latenciju i promjene ocjene bez bilježenja autorizacijskih zaglavlja ili cijelih tijela odgovora.
  • Ispravno tumačite opseg: rezultat opisuje ograničeni, neinvazivni sigurnosni položaj javnog HTTPS-a i preglednika. Nije dokaz da aplikacija nema ranjivosti.

401 ili 403 obično znači da token nedostaje, nije ispravno oblikovan, opozvan je ili pripada pogrešnoj aktivaciji usluge. 422 upućuje na poslani URL, a ne na pouzdanost prijenosa. Ponovljeni odgovori 429 ukazuju na pritisak kvote ili ograničenja stope. TLS nalazi koji se čine proturječnima konfiguraciji poslužitelja često potječu od javnog CDN-a, uravnoteživača opterećenja ili obrnutog proxyja — sloja do kojeg preglednik zapravo dolazi.

Završni popis za provjeru

  • Free, Plus ili Pro paket je aktivan, a token ograničen na uslugu ažuran je.
  • Produkcijska pohrana tajni sadrži token bez izlaganja u kontroli izvornog koda.
  • Konfigurirani cilj je kanonski javni HTTPS URL.
  • Zahtjev koristi POST, Bearer autentifikaciju i JSON url.
  • Vremenska ograničenja, ograničeni ponovni pokušaji te neponovljivi neuspjesi autentifikacije i provjere valjanosti ponašaju se kako je predviđeno.
  • Http::fake() testovi prolaze bez vanjskog mrežnog pristupa.
  • Naredba se pokreće nakon aktivacije izdanja i javne provjere stanja.
  • Izvješće je pohranjeno na trajnom mjestu s kontroliranim pristupom.
  • Operatori implementacije mogu vidjeti neuspjehe i znaju trebaju li upozoriti, istražiti ili vratiti izdanje unatrag.

Vrijedna navika nije prikupljanje još jedne sigurnosne ocjene. Ona je učiniti javni rub svakog izdanja vidljivim u trenutku uvođenja promjene. Mala, disciplinirana provjera nakon implementacije pretvara pogreške certifikata, oslabljena zaglavlja i regresije pravila preglednika iz tihih iznenađenja u djelotvorne signale izdanja.

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.