← Alla inlägg Använd ZapScan som proxy för dina Playwright-tester

Använd ZapScan som proxy för dina Playwright-tester

Playwright är ett trevligt verktyg för e2e-tester men det kan göra så mycket mer om man kombinerar det med lite annat i verktygslådan, som ZapScan.

securitytestplaywrightzapscan

Att slå två flugor i en smäll

När vi skriver e2e-tester med Playwright lägger vi ner mycket energi på att klicka runt i gränssnittet, fylla i formulär och verifiera flöden. Vi gör det för att säkerställa att applikationen fungerar som tänkt för användaren.

Men tänk om vi samtidigt kunde leta efter säkerhetshål? Genom att koppla samman dina funktionella tester med OWASP ZAP (Zed Attack Proxy) – ofta kallat ZapScan – förvandlas dina vanliga testkörningar till en automatiserad säkerhetsgranskning. Du återanvänder helt enkelt de klick och flöden du redan byggt för att mata säkerhetsskannern med trafik.

Hur det fungerar under huven?

Idén är lika enkel som den är genial. Istället för att låta Playwright prata direkt med din testmiljö, konfigurerar du webbläsaren i Playwright till att routas genom ZAP som en lokal proxy.

När dina e2e-tester körs sker följande:

  1. Playwright utför sina vanliga scenarier (loggar in, klickar på knappar, laddar sidor).
  2. ZAP Proxy sitter tyst i bakgrunden och lyssnar på all HTTP- och HTTPS-trafik som passerar.
  3. Passiv skanning sker i realtid. ZAP analyserar headrar, kakor och svarstexter utan att störa eller förändra anropen. Den letar efter saknade säkerhetsheadrar, felaktiga flaggor på sessionskakor eller läckta systemdetaljer.

Det bästa av allt? Det här förlänger knappt testtiden och riskerar inte att förstöra någon testdata eftersom den passiva skanningen är helt ofarlig.

Shift Left på riktigt

Genom att baka in det här i din CI/CD-pipeline uppnår du det som säkerhetsbranschen kallar för Shift Left – att flytta säkerhetsarbetet så tidigt som möjligt i utvecklingskedjan. Istället för att vänta på en årlig, dyr penetrationstestning upptäcker du basala sårbarheter (som XSS-risker eller felkonfigurerade servrar) vid varje enskild commit eller release.

När Playwright-testerna har kört klart kan du via ZAP:s API plocka ut en färdig rapport över eventuella sårbarheter. Hittar ZAP något kritiskt kan du välja att låta pipelinen rödflaggas och stoppa bygget direkt. Det ger en enorm trygghet och lyfter ditt automatiserade testarbete till en helt ny nivå.

Aktiv scanning efteråt

Att stanna vid passiv skanning ger mycket gratis, men för att verkligen sätta applikationen på prov vill vi ta steget vidare till aktiv skanning. Skillnaden är fundamental: där den passiva skanningen bara tyst granskar trafiken, går den aktiva skanningen till attack. Den modifierar anropen, injicerar skadlig kod och försöker aktivt provocera fram fel i systemet för att hitta sårbarheter som SQL-injektioner, Cross-Site Scripting (XSS) eller trasig behörighetskontroll. Eftersom en aktiv skanning skickar tusentals aggressiva anrop vill du aldrig köra den samtidigt som dina Playwright-tester är igång – det skulle sänka prestandan och få dina funktionella e2e-tester att braka samman direkt. Strategin ser istället ut så här:

  1. Plöj marken: Playwright-testerna körs först som vanligt genom proxyn. Detta gör att ZAP lär känna applikationens struktur, hittar alla dolda URL:er, formulär och parametrar samt fångar upp giltiga autentiseringskakor.
  2. Gå till attack: När Playwright har stängt ner webbläsaren triggar vi ZAP via dess API att starta en aktiv skanning baserat på den karta (tree) som precis ritades upp under testkörningen. Genom att sköta den aktiva skanningen som ett separat steg efteråt får du det bästa av två världar: stabila, snabba e2e-tester och en djupgående säkerhetsanalys som faktiskt hittar de allvarliga hålen.

Exempel

Starta zap container före e2e-testerna

ZAP_CONTAINER_NAME="zap-proxy"
ZAP_IMAGE="zaproxy/zap-stable"
ZAP_PORT1=8080
ZAP_PORT2=8090
ZAP_REPORTS_DIR="$(pwd)/zap-reports"

function start_zap() {
	# Remove existing container if exists
	if [ "$(docker ps -aq -f name=^/${ZAP_CONTAINER_NAME}$)" ]; then
		echo "Removing existing ZAP container..."
		docker rm -f "$ZAP_CONTAINER_NAME"
	fi
	echo "Starting ZAP proxy..."
	docker run -d --name "$ZAP_CONTAINER_NAME" \
		-p $ZAP_PORT1:8080 -p $ZAP_PORT2:8090 \
		-v "$ZAP_REPORTS_DIR:/zap/wrk:rw" \
		"$ZAP_IMAGE" zap.sh -daemon -host 0.0.0.0 \
		-port 8080 \
		-config 'api.addrs.addr.name=.*' \
		-config api.addrs.addr.regex=true \
		-config api.disablekey=true
	echo "ZAP proxy started."
}

Exekvera scanningar (passive, spider, ajax spider, active)

try {
    // 1. Passive (before any scan)
    phaseAlerts['passive'] = await fetchAlerts(apiContext, config.targetBaseUrls);

    // 2. Spider
    if (scanMode === 'spider' || scanMode === 'all') {
      for (const url of config.targetBaseUrls) {
        await runSpiderScan(apiContext, url);
      }
      const afterSpider = await fetchAlerts(apiContext, config.targetBaseUrls);
      phaseAlerts['spider'] = diffAlerts(phaseAlerts['passive'], afterSpider);
    }

    // 3. Ajax Spider
    if (scanMode === 'ajax' || scanMode === 'all') {
      for (const url of config.targetBaseUrls) {
        await runAjaxSpiderScan(apiContext, url);
      }
      const afterAjax = await fetchAlerts(apiContext, config.targetBaseUrls);
      const prev = scanMode === 'all' && phaseAlerts['spider'] ? phaseAlerts['passive'].concat(phaseAlerts['spider']) : phaseAlerts['passive'];
      phaseAlerts['ajax'] = diffAlerts(prev, afterAjax);
    }

    // 4. Active
    if (scanMode === 'active' || scanMode === 'all') {
      for (const url of config.targetBaseUrls) {
        await runActiveScan(apiContext, url);
      }
      const afterActive = await fetchAlerts(apiContext, config.targetBaseUrls);
      let prev = phaseAlerts['passive'];
      if (scanMode === 'all') {
        if (phaseAlerts['spider']) prev = prev.concat(phaseAlerts['spider']);
        if (phaseAlerts['ajax']) prev = prev.concat(phaseAlerts['ajax']);
      }
      phaseAlerts['active'] = diffAlerts(prev, afterActive);
    }

    console.log('📊 Generating ZAP reports...\n');

    await generateHTMLReport(apiContext, config).catch(err =>
      console.error('❌ HTML report error:', err.message)
    );

    // Save phased alerts (scan_phase annotated)
    await generateJSONAlertsWithPhases(apiContext, config, phaseAlerts).catch(err =>
      console.error('❌ JSON phased alerts error:', err.message)
    );

    await printScanSummary(apiContext, config).catch(err =>
      console.error('⚠️  Summary error:', err.message)
    );

    console.log(`\n✅ ZAP reports saved to: ${config.reportsDir}\n`);
  } finally {
    await apiContext.dispose();
    console.log('✅ Global teardown completed\n');
  }

Stäng ned zap containern efteråt

function stop_zap() {
	if [ "$(docker ps -q -f name=^/${ZAP_CONTAINER_NAME}$)" ]; then
		echo "Stopping ZAP proxy..."
		docker stop "$ZAP_CONTAINER_NAME"
		docker rm "$ZAP_CONTAINER_NAME"
		echo "ZAP proxy stopped and removed."
	else
		echo "No running ZAP proxy container found."
	fi
}

Betygsätt detta inlägg