Skip to content

Commit

Permalink
Refactoring to be able to close the browser earlier #3622
Browse files Browse the repository at this point in the history
- add a ScriptLoginResult which contains all the session data
- update the current script login to use the ScriptLoginResult
- update and extend testcases
  • Loading branch information
winzj committed Nov 26, 2024
1 parent e4a4459 commit 9b34ad4
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 152 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.zapwrapper.scan.login;

import java.util.*;

import org.openqa.selenium.Cookie;

public class ScriptLoginResult {

private Set<Cookie> sessionCookies;
private Map<String, String> localStorage;
private Map<String, String> sessionStorage;

private boolean loginFailed;

public ScriptLoginResult() {
this.sessionCookies = new HashSet<>();
this.localStorage = new HashMap<>();
this.sessionStorage = new HashMap<>();
}

public Set<Cookie> getSessionCookies() {
return Collections.unmodifiableSet(sessionCookies);
}

public void setSessionCookies(Set<Cookie> sessionCookies) {
if (sessionCookies != null) {
this.sessionCookies = sessionCookies;
}
}

public Map<String, String> getLocalStorage() {
return Collections.unmodifiableMap(localStorage);
}

public void setLocalStorage(Map<String, String> localStorage) {
if (localStorage != null) {
this.localStorage = localStorage;
}
}

public Map<String, String> getSessionStorage() {
return Collections.unmodifiableMap(sessionStorage);
}

public void setSessionStorage(Map<String, String> sessionStorage) {
if (sessionStorage != null) {
this.sessionStorage = sessionStorage;
}
}

public boolean isLoginFailed() {
return loginFailed;
}

public void setLoginFailed(boolean loginFailed) {
this.loginFailed = loginFailed;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@
package com.mercedesbenz.sechub.zapwrapper.scan.login;

import java.io.File;
import java.io.IOException;

import javax.script.ScriptException;

import org.openqa.selenium.firefox.FirefoxDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zaproxy.clientapi.core.ClientApiException;
Expand All @@ -19,19 +15,16 @@
public class ZapScriptLogin {
private static final Logger LOG = LoggerFactory.getLogger(ZapScriptLogin.class);

private ZapScriptLoginWebDriverFactory webDriverFactory;
private ZapWrapperGroovyScriptExecutor groovyScriptExecutor;
private ZapScriptLoginSessionGrabber sessionGrabber;
private ZapScriptLoginSessionConfigurator sessionConfigurator;

public ZapScriptLogin() {
this(new ZapScriptLoginWebDriverFactory(), new ZapWrapperGroovyScriptExecutor(), new ZapScriptLoginSessionGrabber());
this(new ZapWrapperGroovyScriptExecutor(), new ZapScriptLoginSessionConfigurator());
}

ZapScriptLogin(ZapScriptLoginWebDriverFactory webDriverFactory, ZapWrapperGroovyScriptExecutor groovyScriptExecutor,
ZapScriptLoginSessionGrabber sessionGrabber) {
this.webDriverFactory = webDriverFactory;
ZapScriptLogin(ZapWrapperGroovyScriptExecutor groovyScriptExecutor, ZapScriptLoginSessionConfigurator sessionConfigurator) {
this.groovyScriptExecutor = groovyScriptExecutor;
this.sessionGrabber = sessionGrabber;
this.sessionConfigurator = sessionConfigurator;
}

/**
Expand All @@ -49,26 +42,22 @@ public String login(ZapScanContext scanContext, ClientApiWrapper clientApiWrappe
"Expected a groovy script file to perform login, but no script was found. Cannot perform script login without the script file.",
ZapWrapperExitCode.PDS_CONFIGURATION_ERROR);
}
LOG.info("Creating selenium web driver.");
FirefoxDriver firefox = webDriverFactory.createFirefoxWebdriver(scanContext.getProxyInformation(), true);

LOG.info("Calling groovy script executor to execute login script.");
ScriptLoginResult loginResult = groovyScriptExecutor.executeScript(groovyScriptLoginFile, scanContext);
if (loginResult.isLoginFailed()) {
throw new ZapWrapperRuntimeException("An error happened during script login.", ZapWrapperExitCode.PRODUCT_EXECUTION_ERROR);
}
try {
LOG.info("Calling groovy script executor to execute login script.");
groovyScriptExecutor.executeScript(groovyScriptLoginFile, firefox, scanContext);

LOG.info("Calling session grabber to read the HTTP session data and pass them to ZAP.");
return sessionGrabber.extractSessionAndPassToZAP(firefox, scanContext.getTargetUrlAsString(), clientApiWrapper);
} catch (IOException | ScriptException e) {
throw new ZapWrapperRuntimeException("An error happened while executing the script file.", e, ZapWrapperExitCode.IO_ERROR);
return sessionConfigurator.passSessionDataToZAP(loginResult, scanContext.getTargetUrlAsString(), clientApiWrapper);
} catch (ClientApiException e) {
throw new ZapWrapperRuntimeException("An error happened while grabbing the session data.", e, ZapWrapperExitCode.PRODUCT_EXECUTION_ERROR);
} finally {
firefox.quit();
}
}

public void cleanUpScriptLoginData(String targetUrl, ClientApiWrapper clientApiWrapper) throws ClientApiException {
sessionGrabber.cleanUpOldSessionDataIfNecessary(targetUrl, clientApiWrapper);
sessionConfigurator.cleanUpOldSessionDataIfNecessary(targetUrl, clientApiWrapper);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,52 @@
import java.util.Map;

import org.openqa.selenium.Cookie;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zaproxy.clientapi.core.ClientApiException;

import com.mercedesbenz.sechub.zapwrapper.internal.scan.ClientApiWrapper;

public class ZapScriptLoginSessionGrabber {
private static final Logger LOG = LoggerFactory.getLogger(ZapScriptLoginSessionGrabber.class);
public class ZapScriptLoginSessionConfigurator {
private static final Logger LOG = LoggerFactory.getLogger(ZapScriptLoginSessionConfigurator.class);

private static final String JWT_REPLACER_DESCRIPTION = "JWT";
private static final String SESSION_TOKEN_IDENTIFIER = "session-token";
private static final String SESSION_IDENTIFIER = "authenticated-session";

private static final String LOCAL_STORAGE = "localStorage";
private static final String SESSION_STORAGE = "sessionStorage";

private JWTSupport jwtSupport;

public ZapScriptLoginSessionGrabber() {
public ZapScriptLoginSessionConfigurator() {
this(new JWTSupport());
}

ZapScriptLoginSessionGrabber(JWTSupport jwtSupport) {
ZapScriptLoginSessionConfigurator(JWTSupport jwtSupport) {
this.jwtSupport = jwtSupport;
}

/**
* The sessionGrabber will add all necessary session data to ZAP.
* This method will add all necessary session data to ZAP. ZAP can use this
* authenticated session for scanning.
*
* @param firefox
* @param loginResult
* @param targetUrl
* @param clientApiWrapper
* @return the name/identifier of the authenticated session inside ZAP
* @throws ClientApiException
*/
public String extractSessionAndPassToZAP(FirefoxDriver firefox, String targetUrl, ClientApiWrapper clientApiWrapper) throws ClientApiException {
public String passSessionDataToZAP(ScriptLoginResult loginResult, String targetUrl, ClientApiWrapper clientApiWrapper) throws ClientApiException {
cleanUpOldSessionDataIfNecessary(targetUrl, clientApiWrapper);

clientApiWrapper.addHTTPSessionToken(targetUrl, SESSION_TOKEN_IDENTIFIER);
clientApiWrapper.createEmptyHTTPSession(targetUrl, SESSION_IDENTIFIER);

for (Cookie cookie : firefox.manage().getCookies()) {
for (Cookie cookie : loginResult.getSessionCookies()) {
clientApiWrapper.setHTTPSessionTokenValue(targetUrl, SESSION_IDENTIFIER, cookie.getName(), cookie.getValue());
}
clientApiWrapper.setActiveHTTPSession(targetUrl, SESSION_IDENTIFIER);

if (!addJwtAsReplacerRuleToZap(firefox, clientApiWrapper, LOCAL_STORAGE)) {
addJwtAsReplacerRuleToZap(firefox, clientApiWrapper, SESSION_STORAGE);
if (!addJwtAsReplacerRuleToZap(loginResult.getSessionStorage(), clientApiWrapper)) {
addJwtAsReplacerRuleToZap(loginResult.getLocalStorage(), clientApiWrapper);
}

boolean followRedirects = true;
Expand Down Expand Up @@ -93,7 +89,7 @@ public void cleanUpOldSessionDataIfNecessary(String targetUrl, ClientApiWrapper
}
}

private boolean addJwtAsReplacerRuleToZap(FirefoxDriver firefox, ClientApiWrapper clientApiWrapper, String storageType) throws ClientApiException {
private boolean addJwtAsReplacerRuleToZap(Map<String, String> storage, ClientApiWrapper clientApiWrapper) throws ClientApiException {
boolean enabled = true;
// "REQ_HEADER" means the header entry will be added to the requests if not
// existing or replaced if already existing
Expand All @@ -111,11 +107,10 @@ private boolean addJwtAsReplacerRuleToZap(FirefoxDriver firefox, ClientApiWrappe
// any URL
String url = null;

LOG.info("Searching: {} for JWT and add JWT as replacer rule.", storageType);
Map<String, String> localStorage = retrieveStorage(firefox, storageType);
LOG.info("Searching browser storage for JWT and add JWT as replacer rule.");

for (String key : localStorage.keySet()) {
String value = localStorage.get(key);
for (String key : storage.keySet()) {
String value = storage.get(key);
if (jwtSupport.isJWT(value)) {
replacement = "Bearer %s".formatted(value);
clientApiWrapper.addReplacerRule(JWT_REPLACER_DESCRIPTION, enabled, matchtype, matchregex, matchstring, replacement, initiators, url);
Expand All @@ -125,18 +120,4 @@ private boolean addJwtAsReplacerRuleToZap(FirefoxDriver firefox, ClientApiWrappe
return false;
}

private Map<String, String> retrieveStorage(JavascriptExecutor jsExecutor, String storageType) {
String script = """
let items = {};
for (let i = 0; i < %s.length; i++) {
let key = %s.key(i);
items[key] = %s.getItem(key);
}
return items;
""".formatted(storageType, storageType, storageType);

@SuppressWarnings("unchecked")
Map<String, String> storage = (Map<String, String>) jsExecutor.executeScript(script);
return storage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class ZapScriptLoginWebDriverFactory {
private static final Dimension DEFAULT_WEBDRIVER_RESOLUTION = new Dimension(1920, 1080);

public FirefoxDriver createFirefoxWebdriver(ProxyInformation proxyInformation, boolean headless) {

FirefoxOptions options = new FirefoxOptions();
if (headless) {
LOG.info("Using firefox in headless mode.");
Expand All @@ -32,6 +33,7 @@ public FirefoxDriver createFirefoxWebdriver(ProxyInformation proxyInformation, b
proxy.setSslProxy(proxyString);
options.setProxy(proxy);
}
LOG.info("Creating selenium firefox driver.");
FirefoxDriver firefox = new FirefoxDriver(options);
// Set the window size, some application need a windows size to render correctly
// even in headless mode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Duration;
import java.util.Map;

import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

import org.codehaus.groovy.jsr223.GroovyScriptEngineFactory;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
Expand All @@ -29,21 +31,57 @@
public class ZapWrapperGroovyScriptExecutor {
private static final Logger LOG = LoggerFactory.getLogger(ZapWrapperGroovyScriptExecutor.class);

private static final long WEBDRIVER_TIMEOUT_PER_STEP_IN_SECONDS = 30;
private static final int WEBDRIVER_TIMEOUT_PER_STEP_IN_SECONDS = 30;

public void executeScript(File scriptFile, FirefoxDriver firefox, ZapScanContext scanContext) throws IOException, ScriptException {
private static final String LOCAL_STORAGE = "localStorage";
private static final String SESSION_STORAGE = "sessionStorage";

private ZapScriptLoginWebDriverFactory webDriverFactory;

private int webdriverTimeoutInSeconds;

public ZapWrapperGroovyScriptExecutor() {
this(new ZapScriptLoginWebDriverFactory(), WEBDRIVER_TIMEOUT_PER_STEP_IN_SECONDS);
}

ZapWrapperGroovyScriptExecutor(ZapScriptLoginWebDriverFactory webDriverFactory, int webdriverTimeoutInSeconds) {
this.webDriverFactory = webDriverFactory;
this.webdriverTimeoutInSeconds = webdriverTimeoutInSeconds;
}

public ScriptLoginResult executeScript(File scriptFile, ZapScanContext scanContext) {

FirefoxDriver firefox = webDriverFactory.createFirefoxWebdriver(scanContext.getProxyInformation(), true);
WebDriverWait wait = new WebDriverWait(firefox, Duration.ofSeconds(webdriverTimeoutInSeconds));

String script = Files.readString(scriptFile.toPath());
ScriptEngine scriptEngine = new GroovyScriptEngineFactory().getScriptEngine();

LOG.info("Create bindings for groovy script.");
Bindings bindings = createBindings(scanContext, scriptEngine, firefox);

LOG.info("Execute groovy login script.");
scriptEngine.eval(script, bindings);
Bindings bindings = createBindings(scanContext, scriptEngine, firefox, wait);

ScriptLoginResult loginResult = new ScriptLoginResult();
try {
String script = Files.readString(scriptFile.toPath());
LOG.info("Execute groovy login script.");
scriptEngine.eval(script, bindings);

// load target URL to ensure the correct page is loaded in the browser
firefox.get(scanContext.getTargetUrlAsString());

LOG.info("Execution successful, perparing login result with session data.");
loginResult.setSessionCookies(firefox.manage().getCookies());
loginResult.setSessionStorage(retrieveStorage(firefox, SESSION_STORAGE));
loginResult.setLocalStorage(retrieveStorage(firefox, LOCAL_STORAGE));
} catch (IOException | ScriptException e) {
LOG.error("An error happened while executing the script file.", e);
loginResult.setLoginFailed(true);
} finally {
firefox.quit();
}
return loginResult;
}

private Bindings createBindings(ZapScanContext scanContext, ScriptEngine scriptEngine, FirefoxDriver firefox) {
private Bindings createBindings(ZapScanContext scanContext, ScriptEngine scriptEngine, FirefoxDriver firefox, WebDriverWait wait) {
// TODO 2024-11-21 jan: use templates structure from sechub webscan config
SecHubWebScanConfiguration secHubWebScanConfiguration = scanContext.getSecHubWebScanConfiguration();
WebLoginConfiguration webLoginConfiguration = secHubWebScanConfiguration.getLogin().get();
Expand All @@ -66,8 +104,6 @@ private Bindings createBindings(ZapScanContext scanContext, ScriptEngine scriptE
String user = "DUMMY";
String password = "DUMMY";

WebDriverWait wait = new WebDriverWait(firefox, Duration.ofSeconds(WEBDRIVER_TIMEOUT_PER_STEP_IN_SECONDS));

Bindings bindings = scriptEngine.createBindings();
bindings.put(FIREFOX_WEBDRIVER_KEY, firefox);
bindings.put(FIREFOX_WEBDRIVER_WAIT_KEY, wait);
Expand All @@ -82,4 +118,19 @@ private Bindings createBindings(ZapScanContext scanContext, ScriptEngine scriptE

return bindings;
}

private Map<String, String> retrieveStorage(JavascriptExecutor jsExecutor, String storageType) {
String script = """
let items = {};
for (let i = 0; i < %s.length; i++) {
let key = %s.key(i);
items[key] = %s.getItem(key);
}
return items;
""".formatted(storageType, storageType, storageType);

@SuppressWarnings("unchecked")
Map<String, String> storage = (Map<String, String>) jsExecutor.executeScript(script);
return storage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public TOTPGenerator(String seed, int totpLength, TOTPHashAlgorithm hashAlgorith
}

/**
* This method generates a TOTP for the current times stamp in milliseconds.
* This method generates a TOTP for the current timestamp in milliseconds.
*
* @return totp currently valid
*/
Expand All @@ -50,7 +50,7 @@ public String now() {
}

/**
* This method generates a TOTP for a time stamp in milliseconds.
* This method generates a TOTP for a timestamp in milliseconds.
*
* @param seed
* @param currentTimeMillis
Expand Down
Loading

0 comments on commit 9b34ad4

Please sign in to comment.