Vodiči

Native PHP: Secure Deployments with Automated Website Security Audits

Native PHP: Sigurne implementacije s automatiziranim sigurnosnim auditima web-stranica

Implementacija nije završena kada datoteke stignu u produkciju. Završena je kada javna stranica odgovara sa sigurnosnim stanjem koje ste namjeravali isporučiti. Promjena proxyja, istekli certifikat ili zaglavlje preglednika koje nedostaje mogu inače uspješno izdanje pretvoriti u tihu regresiju.

Ovaj vodič izrađuje provjeru implementacije u izvornom PHP-u 8.3 oko Website Security Analyzera. Nakon što svako produkcijsko izdanje postane dostupno, naredba šalje njegov javni HTTPS URL, preslikava rezultat u strogi domenski objekt i završava s posebnom izlaznom šifrom kada se pojave nalazi visoke ozbiljnosti.

Analyzer provodi ograničenu, neinvazivnu provjeru javnog HTTPS-a i sigurnosnog stanja preglednika. To je operativni signal, a ne penetracijski test, iskorištavanje ranjivosti, autentificirano skeniranje aplikacije ili zamjena za pregled koda.

Pribavite pristup i izradite servisni token

  1. Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.
  2. Otvorite stranicu usluge Website Security Analyzer.
  3. Odaberite dostupni plan Free, Plus ili Pro i dovršite njegovu aktivaciju.
  4. Otvorite službenu dokumentaciju usluge.
  5. Pronađite ploču Service token i kopirajte ondje prikazani token ograničen na uslugu.

Ova usluga zahtijeva autentifikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Implementacija u nastavku koristi Bearer token kako se vjerodajnica ne bi pojavila u URL-u, zapisnicima pristupa proxyju ili povijesti preglednika.

Ponovno generiranje servisnog tokena opoziva prethodno aktivni token. Rotaciju tretirajte kao atomsku promjenu implementacije: instalirajte novu vrijednost svugdje gdje se pokreće revizija, provjerite je i tek tada odbacite pretpostavke o staroj vjerodajnici.

Potvrdite točan API poziv

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

Prije pisanja PHP-a pošaljite jedan minimalni zahtjev s pouzdanog terminala. Unos tokena bez njegova izravnog stavljanja u naredbu smanjuje slučajno izlaganje u povijesti ljuske.

read -r -s WEBSITE_SECURITY_TOKEN
export WEBSITE_SECURITY_TOKEN

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

unset WEBSITE_SECURITY_TOKEN

Sada smjestite produkcijske vrijednosti u datoteku okruženja koju je izradio vaš spremnik tajni za implementaciju, izvan javnog korijena dokumenta. Ne predajte stvarnu datoteku u repozitorij. Verzijski praćena datoteka .env.production.example može sadržavati samo rezervirana mjesta.

WEBSITE_SECURITY_TOKEN=YOUR_SERVICE_TOKEN
SECURITY_AUDIT_URL=https://www.example.com

Na uobičajenom poslužitelju stvarna datoteka može biti /etc/my-site/security-audit.env, u vlasništvu korisnika za implementaciju i čitljiva samo tom računu. Izvorni PHP ne učitava automatski dotenv datoteke; skripta nakon implementacije izričito će izvesti ovu datoteku u okruženje naredbe.

Odaberite malu arhitekturu koja se može testirati

Projekt treba četiri granice: cURL transport, API klijent, domenski rezultat i naredbu koja rezultate prevodi u izlazne šifre implementacije. Zadržavanje cURL-a izvan API klijenta omogućuje PHPUnitu upotrebu determinističkog lažnog objekta bez otvaranja mrežnih veza.

security-audit/
├── bin/security-audit.php
├── deploy/post-deploy.sh
├── src/Http/CurlTransport.php
├── src/Http/HttpTransport.php
├── src/Security/SecurityAnalyzer.php
├── src/Security/SecurityAuditResult.php
├── tests/SecurityAnalyzerTest.php
└── composer.json

Jedina produkcijska proširenja su cURL i JSON. PHPUnit je samo za razvoj.

{
  "require": {
    "php": "^8.3",
    "ext-curl": "*",
    "ext-json": "*"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
}

Instalirajte ovisnosti naredbom composer install. PHPUnit 11 podržava PHP 8.3, dok produkcijski kod ostaje neovisan o paketu HTTP klijenta.

Izradite ograničeni izvorni cURL transport

Izradite src/Http/HttpTransport.php. Uski ugovor povratne vrijednosti dovoljan je za preslikavanje odgovora, odluke o ponovnom pokušaju i determinističke testove.

<?php
declare(strict_types=1);

namespace App\Http;

interface HttpTransport
{
    /**
     * @param list<string> $headers
     * @param array<string, mixed> $body
     * @return array{
     *     status: int,
     *     body: string,
     *     headers: array<string, string>
     * }
     */
    public function postJson(
        string $url,
        array $headers,
        array $body,
        int $connectTimeoutSeconds,
        int $timeoutSeconds
    ): array;
}

Izradite src/Http/CurlTransport.php. Provjera certifikata i naziva hosta ostaje uključena, preusmjeravanja su onemogućena, a ograničeni su i vrijeme povezivanja i ukupno vrijeme odgovora.

<?php
declare(strict_types=1);

namespace App\Http;

use RuntimeException;

final class CurlTransport implements HttpTransport
{
    public function postJson(
        string $url,
        array $headers,
        array $body,
        int $connectTimeoutSeconds,
        int $timeoutSeconds
    ): array {
        $handle = curl_init($url);

        if ($handle === false) {
            throw new RuntimeException('Unable to initialize cURL.');
        }

        $responseHeaders = [];

        curl_setopt_array($handle, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($body, JSON_THROW_ON_ERROR),
            CURLOPT_HTTPHEADER => array_merge([
                'Accept: application/json',
                'Content-Type: application/json',
            ], $headers),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => $connectTimeoutSeconds,
            CURLOPT_TIMEOUT => $timeoutSeconds,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_USERAGENT => 'production-security-audit/1.0',
            CURLOPT_HEADERFUNCTION => static function (
                $curl,
                string $line
            ) use (&$responseHeaders): int {
                $length = strlen($line);
                $parts = explode(':', $line, 2);

                if (count($parts) === 2) {
                    $responseHeaders[strtolower(trim($parts[0]))] =
                        trim($parts[1]);
                }

                return $length;
            },
        ]);

        $responseBody = curl_exec($handle);

        if ($responseBody === false) {
            $reason = curl_error($handle);
            throw new RuntimeException('HTTP transport failure: ' . $reason);
        }

        return [
            'status' => (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE),
            'body' => $responseBody,
            'headers' => $responseHeaders,
        ];
    }
}

Provjerite odgovor na granici aplikacije

Rezultat usluge uključuje ocjenu, nalaze grupirane prema ozbiljnosti, TLS pojedinosti i preporuke. Ugniježđeni atributi nalaza i TLS-a mogu se razvijati, stoga aplikacija provjerava stabilni ugovor na najvišoj razini bez izmišljanja dubljih polja ili pretpostavljanja raspona ocjene.

Izradite src/Security/SecurityAuditResult.php.

<?php
declare(strict_types=1);

namespace App\Security;

use UnexpectedValueException;

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

    public static function fromPayload(array $payload): self
    {
        if (
            !array_key_exists('score', $payload) ||
            !is_int($payload['score']) && !is_float($payload['score'])
        ) {
            throw new UnexpectedValueException('Invalid or missing score.');
        }

        if (!isset($payload['findings']) || !is_array($payload['findings'])) {
            throw new UnexpectedValueException('Invalid or missing findings.');
        }

        foreach ($payload['findings'] as $severity => $findings) {
            if (!is_string($severity) || !is_array($findings)) {
                throw new UnexpectedValueException(
                    'Findings must be grouped by severity.'
                );
            }
        }

        if (!isset($payload['tls']) || !is_array($payload['tls'])) {
            throw new UnexpectedValueException('Invalid or missing TLS details.');
        }

        if (
            !isset($payload['recommendations']) ||
            !is_array($payload['recommendations'])
        ) {
            throw new UnexpectedValueException(
                'Invalid or missing recommendations.'
            );
        }

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

Dodajte promišljene ponovne pokušaje i klasifikaciju neuspjeha

Izradite src/Security/SecurityAnalyzer.php. Ponovno pokušava kod pogrešaka transporta, HTTP 408, HTTP 429 i pogrešaka poslužitelja. Autentifikacija i uobičajene pogreške klijenta odmah ne uspijevaju jer ponovni pokušaj istog nevaljanog zahtjeva troši kvotu i odgađa implementaciju.

<?php
declare(strict_types=1);

namespace App\Security;

use App\Http\HttpTransport;
use Closure;
use JsonException;
use RuntimeException;

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

    private Closure $sleep;
    private Closure $log;

    public function __construct(
        private readonly HttpTransport $transport,
        private readonly string $token,
        ?Closure $sleep = null,
        ?Closure $log = null
    ) {
        if (trim($token) === '') {
            throw new RuntimeException('The service token is empty.');
        }

        $this->sleep = $sleep
            ?? static fn (int $microseconds) => usleep($microseconds);

        $this->log = $log
            ?? static fn (array $event) => error_log(
                json_encode($event, JSON_THROW_ON_ERROR)
            );
    }

    public function analyze(string $url): SecurityAuditResult
    {
        for ($attempt = 1; $attempt <= 3; $attempt++) {
            try {
                $response = $this->transport->postJson(
                    self::ENDPOINT,
                    ['Authorization: Bearer ' . $this->token],
                    ['url' => $url],
                    5,
                    20
                );
            } catch (RuntimeException $exception) {
                if ($attempt === 3) {
                    throw new RuntimeException(
                        'Analyzer transport failed after retry budget.',
                        0,
                        $exception
                    );
                }

                $this->pause($attempt, 'transport');
                continue;
            }

            $status = $response['status'];

            if ($status >= 200 && $status < 300) {
                try {
                    $payload = json_decode(
                        $response['body'],
                        true,
                        512,
                        JSON_THROW_ON_ERROR
                    );
                } catch (JsonException $exception) {
                    throw new RuntimeException(
                        'Analyzer returned invalid JSON.',
                        0,
                        $exception
                    );
                }

                if (!is_array($payload)) {
                    throw new RuntimeException(
                        'Analyzer returned an invalid response document.'
                    );
                }

                return SecurityAuditResult::fromPayload($payload);
            }

            if ($status === 401 || $status === 403) {
                throw new RuntimeException(
                    'Analyzer authentication was rejected.'
                );
            }

            $retryable = in_array($status, [408, 429], true)
                || ($status >= 500 && $status <= 599);

            if (!$retryable) {
                throw new RuntimeException(
                    'Analyzer rejected the request with HTTP ' . $status . '.'
                );
            }

            if ($attempt === 3) {
                throw new RuntimeException(
                    'Analyzer remained unavailable after retry budget.'
                );
            }

            $this->pause(
                $attempt,
                'http_' . $status,
                $response['headers']['retry-after'] ?? null
            );
        }

        throw new RuntimeException('Unreachable analyzer state.');
    }

    private function pause(
        int $attempt,
        string $reason,
        ?string $retryAfter = null
    ): void {
        $delay = 250_000 * (2 ** ($attempt - 1));

        if (
            $retryAfter !== null &&
            preg_match('/^\d+$/', trim($retryAfter)) === 1
        ) {
            $delay = min(2, (int) $retryAfter) * 1_000_000;
        }

        ($this->log)([
            'event' => 'security_audit_retry',
            'attempt' => $attempt,
            'reason' => $reason,
            'delay_ms' => intdiv($delay, 1000),
        ]);

        ($this->sleep)($delay);
    }
}

Ograničenje Retry-After na dvije sekunde je namjerno. Zakačka nakon implementacije trebala bi poštovati kratke signale o kvoti, ali ne bi trebala neograničeno zauzimati izvršitelja. Trajno ograničavanje stope postaje izričito neuspješna revizija koju kanal može ponovno pokušati kasnije.

Pretvorite rezultat u odluku o implementaciji

Izradite bin/security-audit.php. Ovo pravilo blokira grupe nazvane high ili critical, bez obzira na velika i mala slova. Namjerno ne odlučuje na temelju ocjene jer navedeni ugovor ne definira ljestvicu ili prag ocjene.

<?php
declare(strict_types=1);

use App\Http\CurlTransport;
use App\Security\SecurityAnalyzer;

require dirname(__DIR__) . '/vendor/autoload.php';

$environment = static function (string $name): string {
    $value = getenv($name);

    if ($value === false || trim($value) === '') {
        throw new RuntimeException('Missing environment variable: ' . $name);
    }

    return trim($value);
};

try {
    $token = $environment('WEBSITE_SECURITY_TOKEN');
    $url = $environment('SECURITY_AUDIT_URL');

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

    $analyzer = new SecurityAnalyzer(
        new CurlTransport(),
        $token
    );

    $result = $analyzer->analyze($url);
    $counts = [];
    $blockingFindings = 0;

    foreach ($result->findingsBySeverity as $severity => $findings) {
        $counts[$severity] = count($findings);

        if (in_array(strtolower($severity), ['high', 'critical'], true)) {
            $blockingFindings += count($findings);
        }
    }

    fwrite(STDOUT, json_encode([
        'event' => 'security_audit_completed',
        'target' => $url,
        'score' => $result->score,
        'findings_by_severity' => $counts,
        'tls_detail_keys' => array_keys($result->tls),
        'recommendation_count' => count($result->recommendations),
        'blocking_findings' => $blockingFindings,
    ], JSON_THROW_ON_ERROR) . PHP_EOL);

    exit($blockingFindings > 0 ? 10 : 0);
} catch (Throwable $exception) {
    fwrite(STDERR, json_encode([
        'event' => 'security_audit_failed',
        'error_type' => $exception::class,
        'message' => $exception->getMessage(),
    ], JSON_THROW_ON_ERROR) . PHP_EOL);

    exit(20);
}

Izlazna šifra 0 znači da je revizija dovršena bez blokirajućih nalaza. Šifra 10 znači da je analiza dovršena, ali je prekršila pravilo izdanja. Šifra 20 predstavlja neuspjehe konfiguracije, transporta, autentifikacije, kvote, formata odgovora ili usluge. Ta razlika omogućuje platformi za implementaciju vraćanje izdanja zbog nalaza, dok nedostupnost analyzera tretira kao operativni incident, a ne kao dokaz da je samo izdanje nesigurno.

Testirajte bez pozivanja usluge

Lažni transport bilježi odlazni zahtjev i vraća fiksni red. Umetnuto mirovanje sprječava pauziranje testova ponovnog pokušaja. Izradite tests/SecurityAnalyzerTest.php.

<?php
declare(strict_types=1);

namespace Tests;

use App\Http\HttpTransport;
use App\Security\SecurityAnalyzer;
use PHPUnit\Framework\TestCase;
use RuntimeException;

final class FakeTransport implements HttpTransport
{
    public array $requests = [];

    public function __construct(private array $responses) {}

    public function postJson(
        string $url,
        array $headers,
        array $body,
        int $connectTimeoutSeconds,
        int $timeoutSeconds
    ): array {
        $this->requests[] = compact(
            'url',
            'headers',
            'body',
            'connectTimeoutSeconds',
            'timeoutSeconds'
        );

        return array_shift($this->responses);
    }
}

final class SecurityAnalyzerTest extends TestCase
{
    public function testItMapsAValidResponse(): void
    {
        $fake = new FakeTransport([[
            'status' => 200,
            'headers' => [],
            'body' => json_encode([
                'score' => 87,
                'findings' => ['high' => [['issue' => 'example']]],
                'tls' => ['enabled' => true],
                'recommendations' => ['Review the reported issue'],
            ], JSON_THROW_ON_ERROR),
        ]]);

        $result = (new SecurityAnalyzer(
            $fake,
            'test-token',
            static fn (int $delay) => null,
            static fn (array $event) => null
        ))->analyze('https://www.example.com');

        self::assertSame(87.0, $result->score);
        self::assertCount(1, $result->findingsBySeverity['high']);
        self::assertSame(
            ['url' => 'https://www.example.com'],
            $fake->requests[0]['body']
        );
        self::assertContains(
            'Authorization: Bearer test-token',
            $fake->requests[0]['headers']
        );
    }

    public function testItRetriesAServerFailureOnce(): void
    {
        $validBody = json_encode([
            'score' => 90,
            'findings' => [],
            'tls' => [],
            'recommendations' => [],
        ], JSON_THROW_ON_ERROR);

        $fake = new FakeTransport([
            ['status' => 503, 'headers' => [], 'body' => ''],
            ['status' => 200, 'headers' => [], 'body' => $validBody],
        ]);

        $sleeps = [];

        $result = (new SecurityAnalyzer(
            $fake,
            'test-token',
            static function (int $delay) use (&$sleeps): void {
                $sleeps[] = $delay;
            },
            static fn (array $event) => null
        ))->analyze('https://www.example.com');

        self::assertSame(90.0, $result->score);
        self::assertCount(2, $fake->requests);
        self::assertSame([250000], $sleeps);
    }

    public function testItDoesNotRetryAuthenticationFailure(): void
    {
        $fake = new FakeTransport([[
            'status' => 401,
            'headers' => [],
            'body' => '',
        ]]);

        try {
            (new SecurityAnalyzer(
                $fake,
                'bad-token',
                static fn (int $delay) => null,
                static fn (array $event) => null
            ))->analyze('https://www.example.com');

            self::fail('Expected authentication failure.');
        } catch (RuntimeException $exception) {
            self::assertStringContainsString(
                'authentication',
                $exception->getMessage()
            );
            self::assertCount(1, $fake->requests);
        }
    }
}

Pokrenite paket naredbom vendor/bin/phpunit tests. Ovi testovi provjeravaju preslikavanje zahtjeva, autentifikaciju, povratno odgađanje pri ponovnom pokušaju, preslikavanje odgovora i važan izostanak ponovnih pokušaja nakon odbijenih vjerodajnica.

Pokrenite ga nakon svake produkcijske implementacije

Provjera se mora pokrenuti nakon što promet dosegne novo izdanje; inače analizira prethodnu verziju. Sljedeća datoteka deploy/post-deploy.sh učitava zaštićenu datoteku okruženja, čeka ograničenu javnu spremnost i pokreće reviziju.

#!/usr/bin/env sh
set -eu

RELEASE_DIR=${1:?Pass the deployed release directory}
AUDIT_ENV_FILE=/etc/my-site/security-audit.env

if [ ! -r "$AUDIT_ENV_FILE" ]; then
  echo "Security audit environment file is not readable." >&2
  exit 20
fi

set -a
. "$AUDIT_ENV_FILE"
set +a

cd "$RELEASE_DIR"

ready=0
attempt=1

while [ "$attempt" -le 5 ]; do
  if curl --fail --silent --show-error \
    --max-time 10 "$SECURITY_AUDIT_URL" >/dev/null; then
    ready=1
    break
  fi

  sleep $((attempt * 2))
  attempt=$((attempt + 1))
done

if [ "$ready" -ne 1 ]; then
  echo "Production URL did not become ready." >&2
  exit 30
fi

php bin/security-audit.php

Pozovite ovu skriptu s platforme za implementaciju odmah nakon promocije. Sačuvajte njezinu izlaznu šifru. Konfigurirajte upozorenje za šifre 10, 20 i 30; automatsko vraćanje izdanja povežite samo s pravilima koja je vaš tim svjesno odobrio. Omogućite dovoljno vremena zadatku za tri pokušaja analyzera od 20 sekundi, uz provjere spremnosti i kratka odgađanja.

Sigurnosno i operativno jačanje

  • Držite token izvan Gita, argumenata naredbi, zapisnika, testnih podataka, snimki zaslona i javnih artefakata izgradnje.
  • Ograničite datoteku okruženja na račun za implementaciju i držite je izvan web korijena.
  • Nikada ne onemogućujte TLS provjeru kako biste zaobišli pogreške certifikata.
  • Upotrebljavajte ciljni URL kojim upravlja implementacija. Ne prihvaćajte proizvoljan URL iz javnog zahtjeva i ne prosljeđujte ga analyzeru.
  • Bilježite status, pokušaj, trajanje, cilj, ocjenu i broj nalaza, ali detaljne nalaze čuvajte u artefaktu s kontroliranim pristupom ili sigurnosnom sustavu.
  • Zasebno upozoravajte na neuspjehe autentifikacije, ponovljena ograničenja stope, neispravne odgovore i neuspjehe sigurnosnih pravila.
  • Kada ponovno generirate servisni token, ažurirajte spremnik tajni jer je prethodni aktivni token opozvan.

Uobičajeni produkcijski neuspjesi

HTTP 401 ili 403 obično znači da je instaliran pogrešan token, da je token ponovno generiran ili da odabrana usluga nije aktivirana. Ispravite vjerodajnicu umjesto povećavanja broja ponovnih pokušaja.

HTTP 429 označava kvotu ili ograničavanje stope. Klijent poštuje kratku brojčanu vrijednost Retry-After, a zatim završava nakon ograničenog proračuna za ponovne pokušaje. Provjerite kapacitet plana i osigurajte da paralelne implementacije ne pokreću duplicirane revizije.

Istek vremena transporta može značiti problem s DNS-om, pravilo izlaznog vatrozida ili privremenu nedostupnost usluge. Naredba ponovno pokušava, ali nikada ne pretvara iscrpljenost pokušaja u prolazan rezultat. Neispravan JSON ili promijenjeni ugovor na najvišoj razini također ne uspijeva zatvoreno na granici integracije.

Ako provjera javne spremnosti uspije, ali revizija prijavi neočekivane TLS ili nalaze preglednika, provjerite je li analizirani naziv hosta produkcijski naziv hosta. Revizija izvornog poslužitelja, URL-a za staging ili alternativne domene može opravdano dati drukčiji rezultat od stranice okrenute kupcima.

Završni kontrolni popis provjere

  • Aktivni plan je Free, Plus ili Pro, a token ograničen na uslugu dolazi s ploče Service token na stranici dokumentacije.
  • Tajna implementacije sadrži WEBSITE_SECURITY_TOKEN i HTTPS SECURITY_AUDIT_URL.
  • vendor/bin/phpunit tests prolazi bez pristupa mreži.
  • Implementirano izdanje javno je dostupno prije pokretanja naredbe analyzera.
  • Uspješna revizija završava s 0; blokirajući nalazi završavaju s 10; operativni neuspjesi završavaju s 20.
  • Ponovni pokušaji su ograničeni, pogreške autentifikacije se ne pokušavaju ponovno, a HTTP 429 se obrađuje izričito.
  • Zapisnici sadrže korisne sažetke, ali nikada token ni neobrađeno zaglavlje autorizacije.
  • Tim razumije da je ovo provjera javnog sigurnosnog stanja, a ne penetracijski test.

Vrijedan je pomak malen, ali trajan: uspjeh implementacije sada uključuje ono što javni internet može opažati. Kod, konfiguracija, TLS i obrane preglednika susreću se na toj granici. Provjeravanje nakon svakog izdanja pretvara sigurnosno stanje iz povremene ručne provjere u uobičajen, provjerljiv dio isporuke softvera.

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.