Skip to content

Commit

Permalink
ペネトレーションテストを含めたE2Eテスト追加
Browse files Browse the repository at this point in the history
  • Loading branch information
nanasess committed Oct 1, 2021
1 parent 81f2386 commit 9c0ad2f
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docker-compose.owaspzap.daemon.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: "3"

services:
zap:
command: bash -c "zap.sh -daemon -addonupdate -addoninstall help_ja_JP -addoninstall wappalyzer -addoninstall sequence -addonuninstall hud -configfile /zap/wrk/options.properties -certpubdump /zap/wrk/owasp_zap_root_ca.cer -host 0.0.0.0 -port 8090 -config api.addrs.addr.name=.* -config api.addrs.addr.regex=true"
135 changes: 135 additions & 0 deletions e2e-tests/test/front_login/contact.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Builder, By, until } from 'selenium-webdriver'
import { ZapClient, Mode, ContextType, Risk } from '../../utils/ZapClient';
import { intervalRepeater } from '../../utils/Progress';
import { SeleniumCapabilities } from '../../utils/SeleniumCapabilities';
const zapClient = new ZapClient('http://127.0.0.1:8090');

jest.setTimeout(6000000);

const inputNames = [
'name01', 'name02', 'kana01', 'kana02', 'zip01', 'zip02', 'addr01', 'addr02',
'tel01', 'tel02', 'tel03'
] as const;
type InputName = {
[key in typeof inputNames[number]]?: string
};

const baseURL = 'https://ec-cube';
const url = baseURL + '/contact/';

beforeAll(async () => {
await zapClient.setMode(Mode.Protect);
await zapClient.newSession('/zap/wrk/sessions/front_login_contact', true);
await zapClient.importContext(ContextType.FrontLogin);

if (!await zapClient.isForcedUserModeEnabled()) {
await zapClient.setForcedUserModeEnabled();
expect(await zapClient.isForcedUserModeEnabled()).toBeTruthy();
}
});

describe.skip('お問い合わせページを表示する', () => {
test('[E2E] お問い合わせページを表示し、ログイン状態を確認する', async () => {
const driver = await new Builder()
.withCapabilities(SeleniumCapabilities)
.build();
try {
expect.assertions(15);
await driver.get(url);
await driver.wait(
until.elementLocated(By.css('h2.title')), 10000)
.getText().then(title => expect(title).toBe('お問い合わせ(入力ページ)'));

inputNames.forEach(
async (name) => await driver.findElement(By.name(name)).getAttribute('value')
.then(value => expect(value).toEqual(expect.anything()))
);
await driver.findElement(By.name('email')).getAttribute('value').then(value => expect(value).toBe('[email protected]'));
await driver.findElement(By.name('email02')).getAttribute('value').then(value => expect(value).toBe('[email protected]'));

} finally {
driver && await driver.quit();
}
});

describe('[ATTACK] お問い合わせページの表示をスキャンする', () => {
test('GET でお問い合わせページをスキャンする', async () => {
const scanId = await zapClient.activeScanAsUser(url, 2, 110, false, null, 'GET');

await intervalRepeater(async () => await zapClient.getActiveScanStatus(scanId), 5000);

const alerts = await zapClient.getAlerts(url, 0, 1, Risk.High);
alerts.forEach(alert => {
throw new Error(alert.name);
});
expect(alerts).toHaveLength(0);
});
});
});

describe('お問い合わせ確認ページを表示する', () => {
test('[E2E] お問い合わせページに入力し、確認画面に進む', async () => {
const driver = await new Builder()
.withCapabilities(SeleniumCapabilities)
.build();
try {
await driver.get(url);
await driver.wait(
until.elementLocated(By.css('h2.title')), 10000)
.getText().then(title => expect(title).toBe('お問い合わせ(入力ページ)'));

// 入力値を代入しておく
let inputField: InputName = {};
inputNames.forEach(
async (name) => await driver.findElement(By.name(name)).getAttribute('value')
.then(value => inputField[name] = value)
);

await driver.findElement(By.name('contents')).sendKeys('お問い合わせ内容入力');
await driver.findElement(By.name('confirm')).click();

await driver.wait(
until.elementLocated(By.css('h2.title')), 10000)
.getText().then(title => expect(title).toBe('お問い合わせ(確認ページ)'));

// hidden に入力されているかどうか
inputNames.forEach(
async (name) => await driver.findElement(By.name(name)).getAttribute('value')
.then(value => expect(value).toBe(inputField[name]))
);
// 確認画面に表示されているかどうか
await driver.findElement(By.xpath('//*[@id="form1"]/table/tbody/tr[1]/td')).getText()
.then(value => expect(value).toBe(`${inputField.name01} ${inputField.name02}`));
await driver.findElement(By.xpath('//*[@id="form1"]/table/tbody/tr[2]/td')).getText()
.then(value => expect(value).toBe(`${inputField.kana01} ${inputField.kana02}`));
await driver.findElement(By.xpath('//*[@id="form1"]/table/tbody/tr[3]/td')).getText()
.then(value => expect(value).toBe(`〒${inputField.zip01}-${inputField.zip02}`));
await driver.findElement(By.xpath('//*[@id="form1"]/table/tbody/tr[4]/td')).getText()
.then(value => expect(value).toContain(`${inputField.addr01}${inputField.addr02}`));
await driver.findElement(By.xpath('//*[@id="form1"]/table/tbody/tr[5]/td')).getText()
.then(value => expect(value).toBe(`${inputField.tel01}-${inputField.tel02}-${inputField.tel03}`));
await driver.findElement(By.xpath('//*[@id="form1"]/table/tbody/tr[6]/td')).getText()
.then(value => expect(value).toBe('[email protected]'));
await driver.findElement(By.xpath('//*[@id="form1"]/table/tbody/tr[7]/td')).getText()
.then(value => expect(value).toBe('お問い合わせ内容入力'));
} finally {
driver && await driver.quit();
}
});

describe('[ATTACK] お問い合わせ(確認ページ)をスキャンする', () => {
test('POST でお問い合わせ(確認ページ)をスキャンする', async () => {

const message = await zapClient.getLastMessage(url);
const scanId = await zapClient.activeScanAsUser(url, 2, 110, false, null, 'GET', message.requestBody); // XXX なぜか method=POST にすると url_not_found のエラーになる. GET にしていても POST でスキャンされる

await intervalRepeater(async () => await zapClient.getActiveScanStatus(scanId), 5000);

const alerts = await zapClient.getAlerts(url, 0, 1, Risk.High);
alerts.forEach(alert => {
throw new Error(alert.name);
});
expect(alerts).toHaveLength(0);
});
});
});
10 changes: 10 additions & 0 deletions e2e-tests/utils/Progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const sleep = (msec: number) => new Promise(resolve => setTimeout(resolve, msec));
export const intervalRepeater = async (callback: any, interval: number) => {
let progress = await callback();

while (progress < 100) {
progress = await callback();
console.log(`Active Scan progress : ${progress}%`);
await sleep(interval);
}
}
18 changes: 18 additions & 0 deletions e2e-tests/utils/SeleniumCapabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Capabilities, ProxyConfig } from 'selenium-webdriver'
const proxy : ProxyConfig = {
proxyType: 'manual',
httpProxy: 'localhost:8090',
sslProxy: 'localhost:8090'
};

export const SeleniumCapabilities = Capabilities.chrome();
SeleniumCapabilities.set('chromeOptions', {
args: [
'--headless',
'--disable-gpu',
'--window-size=1024,768'
],
w3c: false
})
.setAcceptInsecureCerts(true)
.setProxy(proxy);
157 changes: 157 additions & 0 deletions e2e-tests/utils/ZapClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
const ClientApi = require('zaproxy');
export const Mode = {
Safe: 'safe',
Protect: 'protect',
Standard: 'standard',
// Attack: 'attack' denger!!
} as const;
type Mode = typeof Mode[keyof typeof Mode];

export const ContextType = {
FrontLogin: 'front_login.context',
FrontGuest: 'front_guest.context',
Admin: 'admin.context'
} as const;
type ContextType = typeof ContextType[keyof typeof ContextType];

export const Risk = {
Informational: 0,
Low: 1,
Medium: 2,
High: 3
} as const;
type Risk = typeof Risk[keyof typeof Risk];

// see https://github.com/zaproxy/zaproxy/blob/main/zap/src/main/java/org/zaproxy/zap/extension/api/ApiResponseConversionUtils.java#L80-L122
export type HttpMessage = {
id: string,
type: string,
timestamp: string,
rtt: string,
cookieParams: string,
note: string,
requestHeader: string,
requestBody: string,
responseHeader: string,
responseBody: string,
tags: string[]
};

// see https://github.com/zaproxy/zaproxy/blob/main/zap/src/main/java/org/parosproxy/paros/core/scanner/Alert.java#L198
export type Alert = {
alertId?: string,
pluginId?: string
name: string,
risk: string,
confidence?: string,
description?: string,
uri?: string,
param?: string,
attack?: string,
otherInfo?: string,
solution?: string,
reference?: string,
evidence?: string,
cweId?: string,
wascId?: string,
message?: any,
sourceHistoryId?: string,
historyRef?: any;
method?: string,
postData?: string,
msgUri?: string
source?: string,
alertRef?: string
};

export class ZapClientError extends Error {
constructor(message?: string) {
super(message);
};
}

export class ZapClient {

private apiKey: string | null;
private proxy: string;
private readonly zaproxy;

constructor(proxy: string, apiKey?: string | null) {
this.proxy = proxy;
this.apiKey = apiKey != undefined ? apiKey : null;
this.zaproxy = new ClientApi({
apiKey: this.apiKey,
proxy: this.proxy
});
}

public async setMode(mode: Mode): Promise<void> {
await this.zaproxy.core.setMode(mode);
}

public async newSession(name: string, override: boolean): Promise<void> {
await this.zaproxy.core.newSession(name, override);
}

public async importContext(contextType: ContextType): Promise<void> {
await this.zaproxy.context.importContext('/zap/wrk/' + contextType);
}

public async isForcedUserModeEnabled(): Promise<boolean> {
const result = await this.zaproxy.forcedUser.isForcedUserModeEnabled();
return JSON.parse(result.forcedModeEnabled);
}

public async setForcedUserModeEnabled(bool?: boolean): Promise<void> {
await this.zaproxy.forcedUser.setForcedUserModeEnabled(bool ?? true);
}

public async sendRequest(request: string, followRedirects?: boolean): Promise<HttpMessage> {
const result = await this.zaproxy.core.sendRequest(request, followRedirects ?? false);
return result.sendRequest;
}

public async getNumberOfMessages(url: string): Promise<number> {
const result = await this.zaproxy.core.numberOfMessages(url);
return JSON.parse(result.numberOfMessages);
}

public async getMessages(url: string, start?: number, count?: number): Promise<HttpMessage[]> {
const result = await this.zaproxy.core.messages(url, start, count);
return result.messages;
}

public async getLastMessage(url: string): Promise<HttpMessage> {
const result = await this.getMessages(url, await this.getNumberOfMessages(url), 10);
const message = result.pop();
if (message === undefined) {
throw new ZapClientError('Invalid response');
}

return message;
}

public async activeScanAsUser(url: string, contextId: number, userId: number, recurse?: boolean, scanPolicyName?: string | null, method?: 'GET' | 'POST' | 'PUT' | 'DELETE', postData?: string | null): Promise<number> {
const result = await this.zaproxy.ascan.scanAsUser(url, contextId, userId, recurse ?? false, scanPolicyName ?? null, method ?? 'GET', postData ?? null);
return result.scan;
}

public async activeScan(url: string, recurse?: boolean, inScopeOnly?: boolean, scanPolicyName?: string | null, method?: 'GET' | 'POST' | 'PUT' | 'DELETE', postData?: string | null, contextId?: number | null): Promise<number> {
const result = await this.zaproxy.ascan.scan(url, recurse ?? false, inScopeOnly ?? true, scanPolicyName ?? null, method ?? 'GET', postData ?? null, contextId ?? null)
return result.scan;
}

public async getActiveScanStatus(scanId: number): Promise<number> {
const result = await this.zaproxy.ascan.status(scanId);
return result.status;
}

public async snapshotSession(): Promise<void> {
await this.zaproxy.core.snapshotSession();
}

public async getAlerts(url: string, start?: number, count?:number, riskid?: Risk): Promise<Alert[]> {
const result = await this.zaproxy.core.alerts(url, start, count, riskid);
return result.alerts;
}
}
1 change: 1 addition & 0 deletions zap/options.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ anticsrf.tokens.token\(0\).name=transactionid
anticsrf.tokens.token\(0\).enabled=true
httpsessions.tokens.token\(0\).name=ecsessid
httpsessions.tokens.token\(0\).enabled=true
scanner.antiCSFR=true

0 comments on commit 9c0ad2f

Please sign in to comment.