Automated security testing in CI: integrare DAST e SAST nella pipeline PHP

Automated security testing in CI: integrare DAST e SAST nella pipeline PHP

A dicembre 2025 un'azienda del settore e-commerce B2B per la distribuzione di componenti elettronici industriali - fatturato annuo circa 28 milioni di euro, 60 dipendenti interni, piattaforma Laravel 11 su Hetzner con microservizi Docker - mi ha chiesto di stabilizzare la pipeline CI/CD dal punto di vista della sicurezza, dopo un incidente che aveva evidenziato un gap operativo strutturale. La vicenda: un developer interno aveva mergeato una PR che introduceva una dipendenza Composer con CVE critico noto, nessuno se ne era accorto in review perché il pacchetto era sottomodulo di secondo livello, il CVE era stato sfruttato tre settimane dopo il deploy in un tentativo di attacco che per fortuna era stato respinto dalla difesa applicativa, ma il fatto stesso che una CVE pubblicamente conosciuta fosse arrivata in produzione dimostrava che il processo era rotto. Il CTO ha commentato "dobbiamo avere security testing automatico che fermi queste cose prima del deploy, non dopo".

In cinque settimane ho costruito un layer di security testing automatico nella pipeline GitHub Actions del cliente, combinando PHPStan con regole di sicurezza specifiche come SAST (Static Application Security Testing), composer audit come controllo sulle dipendenze, Trivy come scanner di container Docker documentato ufficialmente dalla community Aqua Security su trivy.dev per vulnerabilità nelle immagini base e nelle librerie installate, e OWASP ZAP in modalità baseline documentato sul sito ufficiale OWASP come strumento standard per DAST su ogni deploy in staging come DAST (Dynamic Application Security Testing). Il tempo totale della CI è aumentato di 4 minuti (da 11 a 15 minuti per pipeline completa), ma il tasso di vulnerabilità introdotte in produzione è sceso dal 30% delle release (misurato nei sei mesi precedenti) allo 0% nelle 23 release successive all'attivazione. Questo articolo descrive la pipeline esatta, le scelte di tool, i criteri per gestire i falsi positivi senza neutralizzare il valore del testing, e le trappole operative che ho incontrato implementandolo su quattro clienti negli ultimi 18 mesi.

SAST vs DAST: perché servono entrambi e cosa rilevano di diverso

La distinzione fra SAST e DAST è formale ma ha implicazioni pratiche importanti per chi costruisce una pipeline di sicurezza automatica. SAST (Static Application Security Testing) analizza il codice sorgente senza eseguirlo: legge file PHP, identifica pattern di vulnerabilità, propone classificazioni. Trova bene le vulnerabilità che si manifestano come cattive abitudini di codifica - SQL injection da concatenazione di stringhe, XSS da output non escaped, mass assignment non protetto, deserialize non sicuro, hardcoded secrets, uso di crittografia debole. Non trova bene le vulnerabilità che emergono solo a runtime - IDOR (perché servirebbe capire la semantica del parametro), SSRF (perché servirebbe sapere quali URL sono trusted), bypass di logica di business (perché servirebbe capire cosa l'applicazione dovrebbe fare).

DAST (Dynamic Application Security Testing) fa il contrario: lancia l'applicazione in esecuzione e la attacca dall'esterno con pattern noti. Invia payload di test a ogni endpoint, osserva le risposte, identifica deviazioni. Trova bene le vulnerabilità che emergono solo con dati reali - IDOR (provando ID di utenti diversi), XSS reflected (iniettando payload nei parametri URL), SSRF (provando URL interne sensibili), header di sicurezza mancanti, configurazione errata di CORS o CSP. Non trova bene le vulnerabilità "dormienti" nel codice che non sono esposte a runtime - un hardcoded AWS key in un file di test, una SQL injection in un endpoint privato non raggiungibile dall'esterno, un uso di random insicuro in una funzione di generazione token.

I due approcci sono quindi complementari, non alternativi. Una pipeline seria ha entrambi. SAST gira su ogni PR prima del merge perché è veloce (1-3 minuti) e cattura problemi mentre sono ancora fixabili senza costo; DAST gira su staging dopo il deploy perché richiede applicazione in esecuzione e può essere più lento (5-15 minuti). Il pattern di integrazione dell'analisi statica PHP con Psalm e PHPStan in pipeline CI/CD che descrivo in un altro approfondimento copre la parte SAST generale; qui mi concentro sull'estensione security-specific e sul complemento DAST.

Il layer SAST: PHPStan, composer audit e rilevatori di segreti

Il primo livello di security testing è sul codice e sulle sue dipendenze, prima ancora che l'applicazione venga buildata. Tre strumenti compongono questo layer.

Primo: PHPStan con extension dedicata alla sicurezza. La community PHP ha pubblicato diversi set di regole PHPStan che rilevano pattern di sicurezza, il più completo è phpstan-deprecation-rules e phpstan-strict-rules come pacchetti del progetto PHPStan, documentati sul repository ufficiale GitHub di phpstan/phpstan. Combinati con il livello 9 di PHPStan, catturano la maggior parte delle SQL injection potenziali, dei tipi mixed pericolosi, delle chiamate a funzioni deprecated con implicazioni di sicurezza. La configurazione è la stessa che uso per l'analisi statica standard più due inclusioni:

# phpstan.neon - estensione per security
includes:
    - ./vendor/phpstan/phpstan-strict-rules/rules.neon
    - ./vendor/phpstan/phpstan-deprecation-rules/rules.neon
    - ./vendor/nunomaduro/larastan/extension.neon

parameters:
    level: 9
    paths: [app/, config/, routes/]
    checkUninitializedProperties: true
    checkDynamicProperties: true
    checkExplicitMixed: true

Secondo: composer audit per CVE nelle dipendenze. È il comando nativo di Composer 2.4+ che interroga il PHP Security Advisories Database mantenuto dal progetto FriendsOfPHP disponibile su GitHub e restituisce un report delle vulnerabilità conosciute nelle dipendenze installate. Il comando è parte integrante della CI con l'opzione --locked che forza l'analisi sul composer.lock specifico (non sulle versioni resolveable ottimistiche in composer.json):

composer audit --locked --abandoned=report --format=json > audit-report.json

L'output JSON è parseable e permette di fallire la CI selettivamente. Ad esempio, posso accettare CVE di severity LOW ma fallire su HIGH/CRITICAL, oppure tollerare un CVE specifico se è stato analizzato e non applicabile al contesto d'uso (con un commento motivato in un file composer-audit-exceptions.yaml versionato). Il pattern di aggiornamento automatico delle dipendenze PHP con Dependabot e Renovate che descrivo in un articolo dedicato chiude il cerchio: Renovate apre PR per aggiornare, composer audit verifica che la versione aggiornata non abbia CVE aperte, il merge automatico procede solo se entrambi sono OK.

Terzo: rilevatori di segreti hardcoded nel codice. Uso gitleaks come tool di riferimento per scansione di secret leaks in repository Git, che cerca pattern di API key, password, token, chiavi private nel diff delle PR. Il tool è configurabile con regole custom per i pattern specifici dell'azienda (ad esempio, API key del servizio di pagamento interno hanno un prefisso specifico facilmente riconoscibile). Se gitleaks trova qualcosa, la PR viene bloccata e l'autore deve risolvere il leak - tipicamente questo significa anche rotare il segreto leaked perché dal momento in cui è finito in un commit pubblico (anche se poi rimosso) va considerato compromesso.

La configurazione GitHub Actions: veloce, parallela, bloccante sui veri problemi

La pipeline GitHub Actions che combina questi tre tool è strutturata per parallelizzare il più possibile e per essere bloccante solo sui problemi veri, lasciando i warning come output informativo. Il workflow completo:

# .github/workflows/security.yml
name: Security Checks
on:
    pull_request:
    push:
        branches: [main]

jobs:
    sast-phpstan:
        runs-on: ubuntu-latest
        timeout-minutes: 5
        steps:
            - uses: actions/checkout@v4
            - uses: shivammathur/setup-php@v2
              with:
                  php-version: "8.3"
                  coverage: none
                  tools: composer:v2
            - name: Install dependencies
              run: composer install --prefer-dist --no-interaction
            - name: Restore PHPStan cache
              uses: actions/cache@v4
              with:
                  path: .phpstan-cache
                  key: phpstan-sec-${{ github.sha }}
                  restore-keys: phpstan-sec-
            - name: Run PHPStan with security rules
              run: vendor/bin/phpstan analyse --memory-limit=2G --error-format=github

    composer-audit:
        runs-on: ubuntu-latest
        timeout-minutes: 3
        steps:
            - uses: actions/checkout@v4
            - uses: shivammathur/setup-php@v2
              with:
                  php-version: "8.3"
                  tools: composer:v2
            - name: Install dependencies
              run: composer install --prefer-dist --no-interaction --no-dev
            - name: Audit dependencies for CVE
              run: composer audit --locked --no-interaction
              continue-on-error: false

    secret-scan:
        runs-on: ubuntu-latest
        timeout-minutes: 2
        steps:
            - uses: actions/checkout@v4
              with:
                  fetch-depth: 0
            - name: Run gitleaks
              uses: gitleaks/gitleaks-action@v2
              env:
                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    container-scan:
        runs-on: ubuntu-latest
        timeout-minutes: 5
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        steps:
            - uses: actions/checkout@v4
            - name: Build Docker image
              run: docker build -t myapp:${{ github.sha }} .
            - name: Scan image with Trivy
              uses: aquasecurity/trivy-action@master
              with:
                  image-ref: myapp:${{ github.sha }}
                  format: table
                  exit-code: 1
                  ignore-unfixed: true
                  severity: CRITICAL,HIGH

Due scelte di design meritano menzione. Prima: severity: CRITICAL,HIGH su Trivy - scanner di container ritornerebbe potenzialmente migliaia di CVE per un'immagine Debian standard, la maggior parte in librerie di sistema per le quali non esiste fix disponibile. Filtrare su CRITICAL e HIGH sulle sole CVE che hanno patch disponibili (ignore-unfixed: true) produce un set di finding attuabili, non rumore. Seconda: if: github.event_name == 'push' sul container scan - questo job gira solo sui merge a main, non su ogni PR, perché buildare l'immagine Docker completa aggiunge minuti alla CI. Sulle PR basta avere sicuri i tre check più leggeri (PHPStan, composer audit, secret scan).

Stai cercando un Consulente Informatico esperto per costruire una pipeline di security testing automatico calibrata sul tuo stack PHP, senza introdurre colli di bottiglia in CI/CD né generare rumore che il team finisce per ignorare? Nel mio profilo professionale trovi l'esperienza concreta su security automation, pipeline CI/CD Laravel/Symfony e integrazione di strumenti SAST/DAST in PMI italiane.

DAST con OWASP ZAP baseline: scansione dinamica su staging senza falsi allarmi

Il layer DAST è dove molti team si perdono, perché gli scanner DAST tradizionali (Burp Suite, Acunetix, Nessus) sono pensati per pentest supervisionati da umani e producono output verbose che richiede filtering esperto. OWASP ZAP ha una modalità specifica per CI/CD - il baseline scan - che è pensato per girare automaticamente su staging dopo ogni deploy, con un set di check ragionevolmente rapido (tipicamente 5-10 minuti) e basso tasso di falsi positivi.

Il baseline scan fa un crawling limitato del sito, identifica gli endpoint principali, invia un set di richieste di verifica per detect di misconfiguration comuni (header di sicurezza mancanti, CORS permissivo, informazioni di debug esposte, cookie senza flag Secure/HttpOnly, directory listing attivo), e produce un report HTML + JSON. La configurazione minima nella CI è questa:

# .github/workflows/dast-staging.yml
name: DAST Baseline
on:
    workflow_run:
        workflows: ["Deploy Staging"]
        types: [completed]

jobs:
    zap-baseline:
        if: ${{ github.event.workflow_run.conclusion == 'success' }}
        runs-on: ubuntu-latest
        timeout-minutes: 20
        steps:
            - uses: actions/checkout@v4
            - name: ZAP Baseline Scan
              uses: zaproxy/[email protected]
              with:
                  target: "https://staging.azienda.local"
                  rules_file_name: ".zap/rules.tsv"
                  cmd_options: "-a -j -T 10"
                  allow_issue_writing: false

Il file .zap/rules.tsv è dove il team definisce quali rule specifiche ignorare (es. un warning su Cookie senza flag Secure che per qualche ragione non si applica al caso specifico). La disciplina importante è non ignorare warning per default: ogni ignore deve essere commentato con il motivo e datato, altrimenti il file diventa il cimitero dove vanno a morire tutti i finding non triagiati.

Il DAST su staging è non bloccante per il deploy in staging stesso - il workflow triggera dopo il completamento del deploy, quindi il team può vedere i risultati senza che questi fermino la pipeline. I risultati vengono comunque postati come comment sulla PR associata alla release, e un processo settimanale di review copre i finding accumulati. Il deploy a produzione invece richiede che il DAST di staging dell'ultima release sia stato verde (o che i finding siano stati esplicitamente accettati): è un gate di fatto, anche se non istantaneo. Il pattern di analisi forense di attacchi Laravel con ricostruzione della kill chain che ho documentato in dettaglio mostra cosa succede quando il DAST manca: vulnerabilità scopribili in automatico vengono trovate da attaccanti in anticipo rispetto ai difensori.

Container scanning con Trivy: il problema delle immagini base stantie

Trivy è lo scanner di container più usato in ambito DevSecOps e copre tre aspetti: vulnerabilità nelle OS libraries dell'immagine base (Debian, Alpine, Ubuntu), vulnerabilità nelle application dependencies (Composer, npm, pip installate nell'immagine), misconfigurazione del Dockerfile stesso (permessi eccessivi, USER root, esposizione di porte non necessarie).

Il pattern operativo che uso è di integrare Trivy sia in CI (per bloccare push di immagini con CVE critici non accettabili) sia come scheduled scan (per rilevare CVE che emergono dopo che l'immagine è già stata pubblicata - patch di sicurezza rilasciate dalla community Debian/Alpine). Il secondo è particolarmente importante: un'immagine pubblicata 3 mesi fa potrebbe essere diventata vulnerabile anche se al momento della build era pulita, semplicemente perché una nuova CVE è stata pubblicata su una libreria che era nell'immagine.

La configurazione scheduled che uso:

# .github/workflows/trivy-nightly.yml
name: Trivy Nightly Rescan
on:
    schedule:
        - cron: "0 3 * * *" # Ogni giorno alle 03:00 UTC

jobs:
    rescan-production:
        runs-on: ubuntu-latest
        steps:
            - name: Scan production image
              uses: aquasecurity/trivy-action@master
              with:
                  image-ref: registry.azienda.local/myapp:production
                  format: sarif
                  output: trivy-results.sarif
                  severity: CRITICAL,HIGH
                  ignore-unfixed: true
            - name: Upload results to GitHub Security
              uses: github/codeql-action/upload-sarif@v3
              with:
                  sarif_file: trivy-results.sarif

I risultati vengono caricati nel GitHub Security tab, dove sono visibili come finding con severity e link alla CVE originale. Se una nuova CVE CRITICAL emerge su un'immagine in produzione, il team riceve notifica e decide se rebuildere immediatamente con l'immagine base aggiornata o attendere il prossimo ciclo di release. Questo pattern è complementare all'articolo sull'aggiornamento automatico dei container Docker in produzione senza downtime che descrivo separatamente - lo scanning rileva il problema, Watchtower (o equivalente) lo applica.

Gestione dei falsi positivi: il vero killer della security automation

Il motivo per cui la maggior parte delle implementazioni di security testing automatico falliscono in azienda è la gestione dei falsi positivi. Uno scanner che produce 200 finding per pipeline dei quali 190 sono falsi positivi o irrilevanti porta il team a due reazioni equivalentemente dannose: o ignora completamente l'output (vanificando l'investimento), o passa tempo infinito a triagiare rumore (bruciando capacità di engineering). Il pattern che applico è in tre fasi.

Fase 1 - calibrazione iniziale. Nei primi 2-3 sprint post-attivazione, ogni finding viene triagiato con tag: "vero positivo da fixare", "vero ma non applicabile", "falso positivo da sopprimere". I tag sono commentati nel file di suppression appropriato (phpstan-baseline.neon, composer-audit-exceptions.yaml, .zap/rules.tsv, .trivyignore) con motivazione. Il tasso di falsi positivi misurato in questa fase determina la qualità della configurazione.

Fase 2 - tuning delle regole. Se il tasso di falsi positivi è alto (>30%), le regole sono sbagliate, non il codice. Riduco il livello di PHPStan su aree specifiche via excludePaths chirurgici, raffino il dizionario di secret detection di gitleaks per pattern aziendali, configuro rule specifiche per ZAP che escludono scenari non applicabili al caso (ad esempio se l'applicazione è deliberatamente un JSON API senza UI, le rule su CSP inline-script sono disabilitate).

Fase 3 - manutenzione periodica. Ogni trimestre revisionare il file di suppression per rimuovere entry che non sono più applicabili (codice rimosso, dipendenza aggiornata). Questa è la disciplina che impedisce al file di diventare un cimitero di vecchi ignore.

Sul cliente e-commerce del 2025-2026, dopo le prime 4 settimane di calibrazione, il tasso di falsi positivi si è attestato intorno al 12% - sufficientemente basso perché ogni finding che non è pre-suppressed merita un'occhiata reale. Il team ha sviluppato un ritmo: ogni venerdì mattina 30 minuti di triage dei finding della settimana, decisione veloce su ciascuno, merge delle suppression motivate. Questo ritmo è sostenibile e mantiene il sistema pulito nel tempo.

Il ROI misurato a sei mesi: quanto costa, quanto ripaga

Dopo sei mesi di operatività del sistema, le metriche del cliente e-commerce sono queste. Il tempo aggiunto alla CI medio è 4 minuti (da 11 a 15 minuti). Il numero di CVE critiche rilevate prima del merge in main è stato 11, tutte correttamente risolte prima della produzione. Il numero di secret leak rilevati prima del push è stato 3 (due API key di Stripe in test files, un token Telegram in un commento). Il numero di container vulnerabilities bloccate è stato 8 (principalmente librerie base del container Nginx/PHP-FPM che richiedevano rebuild con immagine base aggiornata). Il numero di DAST finding risolti prima del deploy in produzione è stato 6 (principalmente header di sicurezza mancanti su nuovi endpoint).

Il costo operativo del sistema è basso. Hardware: zero aggiuntivo (gira tutto su GitHub Actions standard). Software: zero, tutti i tool sono open source. Tempo-uomo: 30 minuti a settimana di triage, più una giornata-uomo ogni trimestre per review delle suppression e tuning. Effort di setup iniziale: 5 settimane (circa 25 giornate-uomo) distribuite fra me e il team interno.

Il valore sul business è difficile da quantificare in denaro ma è reale. L'incident iniziale che aveva motivato il lavoro (CVE in dipendenza non rilevata) non si è ripetuto. L'ansia del CTO prima di ogni release è ridotta. Il passaggio di audit di sicurezza per tre clienti enterprise della piattaforma è stato superato senza osservazioni maggiori sulla pipeline, contro le "raccomandazioni di miglioramento" che avevamo ricevuto in audit precedenti. La guida strategica all'implementazione del piano di audit tecnico iniziale nei primi 30 giorni su un progetto PHP legacy descrive come il security testing automatico si integra con la governance tecnica di un progetto nel medio periodo.

Se gestisci una codebase PHP con pipeline CI/CD esistente ma senza layer di security testing automatico, e hai vissuto anche solo un incidente dove una vulnerabilità rilevabile è sfuggita in produzione, oppure stai affrontando un audit di sicurezza da parte di un cliente enterprise e hai bisogno di dimostrare un processo di security testing ripetibile e documentato, contattami per una valutazione: in due settimane dimensiono la pipeline calibrata sul tuo stack tecnologico, configuro i quattro tool principali (PHPStan con regole security, composer audit, gitleaks, OWASP ZAP baseline), integro il tutto nel tuo workflow GitHub Actions o GitLab CI, e formo il tuo team sui pattern di triage dei finding - con l'obiettivo che dopo il mio lavoro la pipeline sia autosufficiente e sostenibile senza ulteriori interventi da parte mia.

Ultima modifica: