@vulcn/driver-browser 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { RecordOptions, RecordingHandle, Session, RunContext, RunResult, CrawlOptions, FormCredentials, VulcnDriver } from '@vulcn/engine';
2
+ import { RecordOptions, RecordingHandle, Session, RunContext, RunResult, CapturedRequest, CrawlOptions, Finding, PayloadCategory, RuntimePayload, FormCredentials, VulcnDriver } from '@vulcn/engine';
3
3
  import { Browser, Page, BrowserContext } from 'playwright';
4
4
 
5
5
  /**
@@ -103,6 +103,7 @@ declare function checkBrowsers(): Promise<{
103
103
  * - Links to follow for deeper crawling
104
104
  *
105
105
  * Outputs Session[] that are directly compatible with BrowserRunner.
106
+ * Also generates CapturedRequest[] metadata for Tier 1 HTTP fast scanning.
106
107
  *
107
108
  * This is the "auto-record" mode — instead of a human clicking around,
108
109
  * the crawler automatically discovers injection points.
@@ -121,12 +122,100 @@ interface BrowserCrawlConfig {
121
122
  height: number;
122
123
  };
123
124
  }
125
+ /** Result from crawling — sessions for browser replay + requests for HTTP scanning */
126
+ interface CrawlResult {
127
+ sessions: Session[];
128
+ capturedRequests: CapturedRequest[];
129
+ }
124
130
  /**
125
131
  * Crawl a URL and generate sessions.
126
132
  *
127
133
  * This is called by the browser driver's recorder.crawl() method.
134
+ * Returns both Session[] for Tier 2 browser replay and
135
+ * CapturedRequest[] for Tier 1 HTTP fast scanning.
128
136
  */
129
- declare function crawlAndBuildSessions(config: BrowserCrawlConfig, options?: CrawlOptions): Promise<Session[]>;
137
+ declare function crawlAndBuildSessions(config: BrowserCrawlConfig, options?: CrawlOptions): Promise<CrawlResult>;
138
+
139
+ /**
140
+ * HTTP Scanner — Tier 1 Fast Scan
141
+ *
142
+ * Replays captured HTTP requests via fetch() instead of Playwright.
143
+ * Substitutes security payloads into injectable form fields and checks
144
+ * the response body for payload reflection or error patterns.
145
+ *
146
+ * Speed: ~50ms per payload (vs 2-5s for browser replay)
147
+ *
148
+ * Catches:
149
+ * - Reflected XSS (payload appears in response body)
150
+ * - Error-based SQLi (SQL error patterns in response)
151
+ * - Server-side reflection of any payload
152
+ *
153
+ * Misses:
154
+ * - DOM-based XSS (requires JS execution)
155
+ * - Client-side state bugs
156
+ * - CSP-blocked attacks
157
+ *
158
+ * When reflection IS found, the finding is marked as `needsBrowserConfirmation`
159
+ * so the caller can escalate to Tier 2 for execution-based proof.
160
+ */
161
+
162
+ interface HttpScanResult {
163
+ /** Total HTTP requests sent */
164
+ requestsSent: number;
165
+ /** How long the scan took (ms) */
166
+ duration: number;
167
+ /** Findings from reflection detection */
168
+ findings: Finding[];
169
+ /** Requests where reflection was found — should be escalated to Tier 2 */
170
+ reflectedRequests: ReflectedRequest[];
171
+ }
172
+ interface ReflectedRequest {
173
+ /** Original captured request */
174
+ request: CapturedRequest;
175
+ /** Payload that caused reflection */
176
+ payload: string;
177
+ /** Category of the payload */
178
+ category: PayloadCategory;
179
+ }
180
+ interface HttpScanOptions {
181
+ /** Request timeout in ms (default: 10000) */
182
+ timeout?: number;
183
+ /** Max concurrent requests (default: 10) */
184
+ concurrency?: number;
185
+ /** Cookie header to send with requests (for auth) */
186
+ cookies?: string;
187
+ /** Extra headers to send */
188
+ headers?: Record<string, string>;
189
+ /** Callback for progress reporting */
190
+ onProgress?: (completed: number, total: number) => void;
191
+ }
192
+ /**
193
+ * Run Tier 1 HTTP-level scan on captured requests.
194
+ *
195
+ * For each CapturedRequest × each payload:
196
+ * 1. Substitute the payload into the injectable field
197
+ * 2. Send via fetch()
198
+ * 3. Check response body for reflection patterns
199
+ * 4. If reflected, add finding + mark for Tier 2 escalation
200
+ */
201
+ declare function httpScan(requests: CapturedRequest[], payloads: RuntimePayload[], options?: HttpScanOptions): Promise<HttpScanResult>;
202
+ /**
203
+ * Convert discovered forms into CapturedRequest metadata.
204
+ *
205
+ * Called by the crawler after form discovery. Each injectable form
206
+ * produces one CapturedRequest per injectable input field.
207
+ */
208
+ declare function buildCapturedRequests(forms: Array<{
209
+ pageUrl: string;
210
+ action: string;
211
+ method: string;
212
+ inputs: Array<{
213
+ name: string;
214
+ injectable: boolean;
215
+ type: string;
216
+ }>;
217
+ sessionName: string;
218
+ }>): CapturedRequest[];
130
219
 
131
220
  /**
132
221
  * Login Form Auto-Detection & Auth Replay
@@ -403,4 +492,4 @@ type BrowserStep = z.infer<typeof BrowserStepSchema>;
403
492
  */
404
493
  declare const browserDriver: VulcnDriver;
405
494
 
406
- export { BROWSER_STEP_TYPES, type BrowserConfig, BrowserRecorder, BrowserRunner, type BrowserStep, BrowserStepSchema, type BrowserStepType, type LoginForm, type LoginResult, checkBrowsers, checkSessionAlive, configSchema, crawlAndBuildSessions, browserDriver as default, detectLoginForm, installBrowsers, launchBrowser, performLogin };
495
+ export { BROWSER_STEP_TYPES, type BrowserConfig, BrowserRecorder, BrowserRunner, type BrowserStep, BrowserStepSchema, type BrowserStepType, type CrawlResult, type HttpScanOptions, type HttpScanResult, type LoginForm, type LoginResult, type ReflectedRequest, buildCapturedRequests, checkBrowsers, checkSessionAlive, configSchema, crawlAndBuildSessions, browserDriver as default, detectLoginForm, httpScan, installBrowsers, launchBrowser, performLogin };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { RecordOptions, RecordingHandle, Session, RunContext, RunResult, CrawlOptions, FormCredentials, VulcnDriver } from '@vulcn/engine';
2
+ import { RecordOptions, RecordingHandle, Session, RunContext, RunResult, CapturedRequest, CrawlOptions, Finding, PayloadCategory, RuntimePayload, FormCredentials, VulcnDriver } from '@vulcn/engine';
3
3
  import { Browser, Page, BrowserContext } from 'playwright';
4
4
 
5
5
  /**
@@ -103,6 +103,7 @@ declare function checkBrowsers(): Promise<{
103
103
  * - Links to follow for deeper crawling
104
104
  *
105
105
  * Outputs Session[] that are directly compatible with BrowserRunner.
106
+ * Also generates CapturedRequest[] metadata for Tier 1 HTTP fast scanning.
106
107
  *
107
108
  * This is the "auto-record" mode — instead of a human clicking around,
108
109
  * the crawler automatically discovers injection points.
@@ -121,12 +122,100 @@ interface BrowserCrawlConfig {
121
122
  height: number;
122
123
  };
123
124
  }
125
+ /** Result from crawling — sessions for browser replay + requests for HTTP scanning */
126
+ interface CrawlResult {
127
+ sessions: Session[];
128
+ capturedRequests: CapturedRequest[];
129
+ }
124
130
  /**
125
131
  * Crawl a URL and generate sessions.
126
132
  *
127
133
  * This is called by the browser driver's recorder.crawl() method.
134
+ * Returns both Session[] for Tier 2 browser replay and
135
+ * CapturedRequest[] for Tier 1 HTTP fast scanning.
128
136
  */
129
- declare function crawlAndBuildSessions(config: BrowserCrawlConfig, options?: CrawlOptions): Promise<Session[]>;
137
+ declare function crawlAndBuildSessions(config: BrowserCrawlConfig, options?: CrawlOptions): Promise<CrawlResult>;
138
+
139
+ /**
140
+ * HTTP Scanner — Tier 1 Fast Scan
141
+ *
142
+ * Replays captured HTTP requests via fetch() instead of Playwright.
143
+ * Substitutes security payloads into injectable form fields and checks
144
+ * the response body for payload reflection or error patterns.
145
+ *
146
+ * Speed: ~50ms per payload (vs 2-5s for browser replay)
147
+ *
148
+ * Catches:
149
+ * - Reflected XSS (payload appears in response body)
150
+ * - Error-based SQLi (SQL error patterns in response)
151
+ * - Server-side reflection of any payload
152
+ *
153
+ * Misses:
154
+ * - DOM-based XSS (requires JS execution)
155
+ * - Client-side state bugs
156
+ * - CSP-blocked attacks
157
+ *
158
+ * When reflection IS found, the finding is marked as `needsBrowserConfirmation`
159
+ * so the caller can escalate to Tier 2 for execution-based proof.
160
+ */
161
+
162
+ interface HttpScanResult {
163
+ /** Total HTTP requests sent */
164
+ requestsSent: number;
165
+ /** How long the scan took (ms) */
166
+ duration: number;
167
+ /** Findings from reflection detection */
168
+ findings: Finding[];
169
+ /** Requests where reflection was found — should be escalated to Tier 2 */
170
+ reflectedRequests: ReflectedRequest[];
171
+ }
172
+ interface ReflectedRequest {
173
+ /** Original captured request */
174
+ request: CapturedRequest;
175
+ /** Payload that caused reflection */
176
+ payload: string;
177
+ /** Category of the payload */
178
+ category: PayloadCategory;
179
+ }
180
+ interface HttpScanOptions {
181
+ /** Request timeout in ms (default: 10000) */
182
+ timeout?: number;
183
+ /** Max concurrent requests (default: 10) */
184
+ concurrency?: number;
185
+ /** Cookie header to send with requests (for auth) */
186
+ cookies?: string;
187
+ /** Extra headers to send */
188
+ headers?: Record<string, string>;
189
+ /** Callback for progress reporting */
190
+ onProgress?: (completed: number, total: number) => void;
191
+ }
192
+ /**
193
+ * Run Tier 1 HTTP-level scan on captured requests.
194
+ *
195
+ * For each CapturedRequest × each payload:
196
+ * 1. Substitute the payload into the injectable field
197
+ * 2. Send via fetch()
198
+ * 3. Check response body for reflection patterns
199
+ * 4. If reflected, add finding + mark for Tier 2 escalation
200
+ */
201
+ declare function httpScan(requests: CapturedRequest[], payloads: RuntimePayload[], options?: HttpScanOptions): Promise<HttpScanResult>;
202
+ /**
203
+ * Convert discovered forms into CapturedRequest metadata.
204
+ *
205
+ * Called by the crawler after form discovery. Each injectable form
206
+ * produces one CapturedRequest per injectable input field.
207
+ */
208
+ declare function buildCapturedRequests(forms: Array<{
209
+ pageUrl: string;
210
+ action: string;
211
+ method: string;
212
+ inputs: Array<{
213
+ name: string;
214
+ injectable: boolean;
215
+ type: string;
216
+ }>;
217
+ sessionName: string;
218
+ }>): CapturedRequest[];
130
219
 
131
220
  /**
132
221
  * Login Form Auto-Detection & Auth Replay
@@ -403,4 +492,4 @@ type BrowserStep = z.infer<typeof BrowserStepSchema>;
403
492
  */
404
493
  declare const browserDriver: VulcnDriver;
405
494
 
406
- export { BROWSER_STEP_TYPES, type BrowserConfig, BrowserRecorder, BrowserRunner, type BrowserStep, BrowserStepSchema, type BrowserStepType, type LoginForm, type LoginResult, checkBrowsers, checkSessionAlive, configSchema, crawlAndBuildSessions, browserDriver as default, detectLoginForm, installBrowsers, launchBrowser, performLogin };
495
+ export { BROWSER_STEP_TYPES, type BrowserConfig, BrowserRecorder, BrowserRunner, type BrowserStep, BrowserStepSchema, type BrowserStepType, type CrawlResult, type HttpScanOptions, type HttpScanResult, type LoginForm, type LoginResult, type ReflectedRequest, buildCapturedRequests, checkBrowsers, checkSessionAlive, configSchema, crawlAndBuildSessions, browserDriver as default, detectLoginForm, httpScan, installBrowsers, launchBrowser, performLogin };
package/dist/index.js CHANGED
@@ -778,6 +778,248 @@ function interleavePayloads(payloads) {
778
778
  return result;
779
779
  }
780
780
 
781
+ // src/http-scanner.ts
782
+ async function httpScan(requests, payloads, options = {}) {
783
+ const timeout = options.timeout ?? 1e4;
784
+ const concurrency = options.concurrency ?? 10;
785
+ const start = Date.now();
786
+ const findings = [];
787
+ const reflectedRequests = [];
788
+ let requestsSent = 0;
789
+ const tasks = [];
790
+ for (const request of requests) {
791
+ if (!request.injectableField) continue;
792
+ for (const payloadSet of payloads) {
793
+ for (const value of payloadSet.payloads) {
794
+ tasks.push({ request, payloadSet, value });
795
+ }
796
+ }
797
+ }
798
+ const totalTasks = tasks.length;
799
+ if (totalTasks === 0) {
800
+ return { requestsSent: 0, duration: 0, findings, reflectedRequests };
801
+ }
802
+ for (let i = 0; i < tasks.length; i += concurrency) {
803
+ const batch = tasks.slice(i, i + concurrency);
804
+ const results = await Promise.allSettled(
805
+ batch.map(async ({ request, payloadSet, value }) => {
806
+ try {
807
+ const body = await sendPayload(request, value, {
808
+ timeout,
809
+ cookies: options.cookies,
810
+ headers: options.headers
811
+ });
812
+ requestsSent++;
813
+ const finding = checkHttpReflection(body, request, payloadSet, value);
814
+ if (finding) {
815
+ findings.push(finding);
816
+ reflectedRequests.push({
817
+ request,
818
+ payload: value,
819
+ category: payloadSet.category
820
+ });
821
+ }
822
+ } catch {
823
+ requestsSent++;
824
+ }
825
+ })
826
+ );
827
+ const completed = Math.min(i + batch.length, totalTasks);
828
+ options.onProgress?.(completed, totalTasks);
829
+ for (const result of results) {
830
+ if (result.status === "rejected") {
831
+ }
832
+ }
833
+ }
834
+ return {
835
+ requestsSent,
836
+ duration: Date.now() - start,
837
+ findings,
838
+ reflectedRequests
839
+ };
840
+ }
841
+ async function sendPayload(request, payload, options) {
842
+ const { method, url, headers, body, contentType, injectableField } = request;
843
+ const reqHeaders = {
844
+ ...headers,
845
+ ...options.headers ?? {}
846
+ };
847
+ if (options.cookies) {
848
+ reqHeaders["Cookie"] = options.cookies;
849
+ }
850
+ delete reqHeaders["content-length"];
851
+ delete reqHeaders["Content-Length"];
852
+ let requestUrl = url;
853
+ let requestBody;
854
+ if (method.toUpperCase() === "GET") {
855
+ requestUrl = injectIntoUrl(url, injectableField, payload);
856
+ } else {
857
+ requestBody = injectIntoBody(body, contentType, injectableField, payload);
858
+ if (contentType) {
859
+ reqHeaders["Content-Type"] = contentType;
860
+ } else {
861
+ reqHeaders["Content-Type"] = "application/x-www-form-urlencoded";
862
+ }
863
+ }
864
+ const controller = new AbortController();
865
+ const timer = setTimeout(() => controller.abort(), options.timeout);
866
+ try {
867
+ const response = await fetch(requestUrl, {
868
+ method: method.toUpperCase(),
869
+ headers: reqHeaders,
870
+ body: requestBody,
871
+ signal: controller.signal,
872
+ redirect: "follow"
873
+ });
874
+ return await response.text();
875
+ } finally {
876
+ clearTimeout(timer);
877
+ }
878
+ }
879
+ function injectIntoUrl(url, field, payload) {
880
+ try {
881
+ const parsed = new URL(url);
882
+ parsed.searchParams.set(field, payload);
883
+ return parsed.toString();
884
+ } catch {
885
+ const separator = url.includes("?") ? "&" : "?";
886
+ return `${url}${separator}${encodeURIComponent(field)}=${encodeURIComponent(payload)}`;
887
+ }
888
+ }
889
+ function injectIntoBody(body, contentType, field, payload) {
890
+ if (!body) {
891
+ return `${encodeURIComponent(field)}=${encodeURIComponent(payload)}`;
892
+ }
893
+ const ct = (contentType ?? "").toLowerCase();
894
+ if (ct.includes("application/json")) {
895
+ return injectIntoJson(body, field, payload);
896
+ }
897
+ if (ct.includes("multipart/form-data")) {
898
+ return injectIntoMultipart(body, field, payload);
899
+ }
900
+ return injectIntoFormUrlEncoded(body, field, payload);
901
+ }
902
+ function injectIntoFormUrlEncoded(body, field, payload) {
903
+ const params = new URLSearchParams(body);
904
+ params.set(field, payload);
905
+ return params.toString();
906
+ }
907
+ function injectIntoJson(body, field, payload) {
908
+ try {
909
+ const parsed = JSON.parse(body);
910
+ if (typeof parsed === "object" && parsed !== null) {
911
+ parsed[field] = payload;
912
+ return JSON.stringify(parsed);
913
+ }
914
+ } catch {
915
+ }
916
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
917
+ const regex = new RegExp(`("${escaped}"\\s*:\\s*)"[^"]*"`, "g");
918
+ const replaced = body.replace(regex, `$1"${payload}"`);
919
+ if (replaced !== body) return replaced;
920
+ return body;
921
+ }
922
+ function injectIntoMultipart(body, field, payload) {
923
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
924
+ const regex = new RegExp(
925
+ `(Content-Disposition:\\s*form-data;\\s*name="${escaped}"\\r?\\n\\r?\\n)[^\\r\\n-]*`,
926
+ "i"
927
+ );
928
+ return body.replace(regex, `$1${payload}`);
929
+ }
930
+ function checkHttpReflection(responseBody, request, payloadSet, payloadValue) {
931
+ for (const pattern of payloadSet.detectPatterns) {
932
+ if (pattern.test(responseBody)) {
933
+ return {
934
+ type: payloadSet.category,
935
+ severity: getSeverity2(payloadSet.category),
936
+ title: `${payloadSet.category.toUpperCase()} reflection detected (HTTP)`,
937
+ description: `Payload pattern was reflected in HTTP response body. Needs browser confirmation for execution proof.`,
938
+ stepId: `http-${request.sessionName}`,
939
+ payload: payloadValue,
940
+ url: request.url,
941
+ evidence: responseBody.match(pattern)?.[0]?.slice(0, 200),
942
+ metadata: {
943
+ detectionMethod: "tier1-http",
944
+ needsBrowserConfirmation: true,
945
+ requestMethod: request.method,
946
+ injectableField: request.injectableField
947
+ }
948
+ };
949
+ }
950
+ }
951
+ if (responseBody.includes(payloadValue)) {
952
+ return {
953
+ type: payloadSet.category,
954
+ severity: "medium",
955
+ title: `Potential ${payloadSet.category.toUpperCase()} \u2014 payload reflected in HTTP response`,
956
+ description: `Payload was reflected in HTTP response without encoding. Escalate to browser for execution proof.`,
957
+ stepId: `http-${request.sessionName}`,
958
+ payload: payloadValue,
959
+ url: request.url,
960
+ metadata: {
961
+ detectionMethod: "tier1-http",
962
+ needsBrowserConfirmation: true,
963
+ requestMethod: request.method,
964
+ injectableField: request.injectableField
965
+ }
966
+ };
967
+ }
968
+ return void 0;
969
+ }
970
+ function getSeverity2(category) {
971
+ switch (category) {
972
+ case "sqli":
973
+ case "command-injection":
974
+ case "xxe":
975
+ return "critical";
976
+ case "xss":
977
+ case "ssrf":
978
+ case "path-traversal":
979
+ return "high";
980
+ case "open-redirect":
981
+ return "medium";
982
+ default:
983
+ return "medium";
984
+ }
985
+ }
986
+ function buildCapturedRequests(forms) {
987
+ const requests = [];
988
+ for (const form of forms) {
989
+ const injectableInputs = form.inputs.filter((i) => i.injectable);
990
+ if (injectableInputs.length === 0) continue;
991
+ let actionUrl;
992
+ try {
993
+ actionUrl = new URL(form.action, form.pageUrl).toString();
994
+ } catch {
995
+ actionUrl = form.pageUrl;
996
+ }
997
+ const method = (form.method || "GET").toUpperCase();
998
+ for (const input of injectableInputs) {
999
+ const formParams = new URLSearchParams();
1000
+ for (const inp of form.inputs) {
1001
+ formParams.set(inp.name || inp.type, inp.injectable ? "test" : "");
1002
+ }
1003
+ const request = {
1004
+ method,
1005
+ url: method === "GET" ? actionUrl : actionUrl,
1006
+ headers: {
1007
+ "User-Agent": "Vulcn/1.0 (Security Scanner)",
1008
+ Accept: "text/html,application/xhtml+xml,*/*"
1009
+ },
1010
+ ...method !== "GET" ? {
1011
+ body: formParams.toString(),
1012
+ contentType: "application/x-www-form-urlencoded"
1013
+ } : {},
1014
+ injectableField: input.name || input.type,
1015
+ sessionName: form.sessionName
1016
+ };
1017
+ requests.push(request);
1018
+ }
1019
+ }
1020
+ return requests;
1021
+ }
1022
+
781
1023
  // src/crawler.ts
782
1024
  var INJECTABLE_INPUT_TYPES = /* @__PURE__ */ new Set([
783
1025
  "text",
@@ -864,7 +1106,20 @@ async function crawlAndBuildSessions(config, options = {}) {
864
1106
  console.log(
865
1107
  `[crawler] Complete: ${visited.size} page(s), ${allForms.length} form(s)`
866
1108
  );
867
- return buildSessions(allForms);
1109
+ const sessions = buildSessions(allForms);
1110
+ const capturedRequests = buildCapturedRequests(
1111
+ allForms.filter((f) => f.inputs.some((i) => i.injectable)).map((form, idx) => ({
1112
+ pageUrl: form.pageUrl,
1113
+ action: form.action,
1114
+ method: form.method,
1115
+ inputs: form.inputs,
1116
+ sessionName: sessions[idx]?.name ?? `form-${idx + 1}`
1117
+ }))
1118
+ );
1119
+ console.log(
1120
+ `[crawler] Generated ${sessions.length} session(s), ${capturedRequests.length} HTTP request(s) for Tier 1`
1121
+ );
1122
+ return { sessions, capturedRequests };
868
1123
  }
869
1124
  async function discoverForms(page, pageUrl) {
870
1125
  const forms = [];
@@ -1377,7 +1632,7 @@ var recorderDriver = {
1377
1632
  },
1378
1633
  async crawl(config, options) {
1379
1634
  const parsedConfig = configSchema.parse(config);
1380
- return crawlAndBuildSessions(
1635
+ const result = await crawlAndBuildSessions(
1381
1636
  {
1382
1637
  startUrl: parsedConfig.startUrl ?? "",
1383
1638
  browser: parsedConfig.browser,
@@ -1386,6 +1641,7 @@ var recorderDriver = {
1386
1641
  },
1387
1642
  options
1388
1643
  );
1644
+ return result.sessions;
1389
1645
  }
1390
1646
  };
1391
1647
  var runnerDriver = {
@@ -1409,12 +1665,14 @@ export {
1409
1665
  BrowserRecorder,
1410
1666
  BrowserRunner,
1411
1667
  BrowserStepSchema,
1668
+ buildCapturedRequests,
1412
1669
  checkBrowsers,
1413
1670
  checkSessionAlive,
1414
1671
  configSchema,
1415
1672
  crawlAndBuildSessions,
1416
1673
  index_default as default,
1417
1674
  detectLoginForm,
1675
+ httpScan,
1418
1676
  installBrowsers,
1419
1677
  launchBrowser,
1420
1678
  performLogin