@govtechsg/oobee 0.10.61 → 0.10.65

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/src/index.ts CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  cleanUp,
8
8
  getUserDataTxt,
9
9
  writeToUserDataTxt,
10
+ listenForCleanUp,
11
+ cleanUpAndExit,
10
12
  } from './utils.js';
11
13
  import {
12
14
  prepareData,
@@ -106,19 +108,19 @@ const runScan = async (answers: Answers) => {
106
108
  answers.metadata = '{}';
107
109
 
108
110
  const data: Data = await prepareData(answers);
111
+
112
+ // Executes cleanUp script if error encountered
113
+ listenForCleanUp(data.randomToken);
114
+
109
115
  data.userDataDirectory = getClonedProfilesWithRandomToken(data.browser, data.randomToken);
110
116
 
111
117
  printMessage(['Scanning website...'], messageOptions);
112
118
 
113
119
  await combineRun(data, screenToScan);
114
120
 
115
- // Delete cloned directory
116
- deleteClonedProfiles(data.browser, data.randomToken);
117
-
118
121
  // Delete dataset and request queues
119
- cleanUp(data.randomToken);
122
+ cleanUpAndExit(0, data.randomToken);
120
123
 
121
- process.exit(0);
122
124
  };
123
125
 
124
126
  if (userData) {
package/src/logs.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  /* eslint-disable no-shadow */
3
3
  import { createLogger, format, transports } from 'winston';
4
4
  import { guiInfoStatusTypes } from './constants/constants.js';
5
+ import path from 'path';
6
+ import { randomUUID } from 'crypto';
5
7
 
6
8
  const { combine, timestamp, printf } = format;
7
9
 
@@ -20,12 +22,32 @@ const logFormat = printf(({ timestamp, level, message }) => {
20
22
  // transport: storage device for logs
21
23
  // Enabled for console and storing into files; Files are overwritten each time
22
24
  // All logs in combined.txt, error in errors.txt
25
+ const uuid = randomUUID();
26
+ let basePath: string;
27
+
28
+ if (process.env.OOBEE_LOGS_PATH) {
29
+ basePath = process.env.OOBEE_LOGS_PATH;
30
+ } else if (process.platform === 'win32') {
31
+ basePath = path.join(process.env.APPDATA, 'Oobee');
32
+ } else if (process.platform === 'darwin') {
33
+ basePath = path.join(process.env.HOME, 'Library', 'Application Support', 'Oobee');
34
+ } else {
35
+ basePath = path.join(process.cwd());
36
+ }
37
+
38
+ export const errorsTxtPath = path.join(basePath, `${uuid}.txt`);
23
39
 
24
40
  const consoleLogger = createLogger({
25
41
  silent: !(process.env.RUNNING_FROM_PH_GUI || process.env.OOBEE_VERBOSE),
26
42
  format: combine(timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), logFormat),
27
- transports:
28
- process.env.RUNNING_FROM_PH_GUI || process.env.OOBEE_VERBOSE ? [new transports.Console()] : [],
43
+ transports: [
44
+ new transports.Console({ level: 'info' }),
45
+ new transports.File({
46
+ filename: errorsTxtPath,
47
+ level: 'info',
48
+ handleExceptions: true,
49
+ }),
50
+ ],
29
51
  });
30
52
 
31
53
  // No display in consoles, this will mostly be used within the interactive script to avoid disrupting the flow
@@ -34,9 +56,10 @@ const consoleLogger = createLogger({
34
56
  const silentLogger = createLogger({
35
57
  format: combine(timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), logFormat),
36
58
  transports: [
37
- process.env.OOBEE_VERBOSE || process.env.RUNNING_FROM_PH_GUI
38
- ? new transports.Console({ handleExceptions: true })
39
- : new transports.File({ filename: 'errors.txt', level: 'warn', handleExceptions: true }),
59
+ new transports.File({
60
+ filename: errorsTxtPath,
61
+ level: 'warn',
62
+ handleExceptions: true }),
40
63
  ].filter(Boolean),
41
64
  });
42
65
 
@@ -46,16 +69,17 @@ export const guiInfoLog = (status: string, data: { numScanned?: number; urlScann
46
69
  switch (status) {
47
70
  case guiInfoStatusTypes.COMPLETED:
48
71
  console.log('Scan completed');
72
+ silentLogger.info('Scan completed');
49
73
  break;
50
74
  case guiInfoStatusTypes.SCANNED:
51
75
  case guiInfoStatusTypes.SKIPPED:
52
76
  case guiInfoStatusTypes.ERROR:
53
77
  case guiInfoStatusTypes.DUPLICATE:
54
- console.log(
55
- `crawling::${data.numScanned || 0}::${status}::${
78
+ const msg = `crawling::${data.numScanned || 0}::${status}::${
56
79
  data.urlScanned || 'no url provided'
57
- }`,
58
- );
80
+ }`;
81
+ console.log(msg);
82
+ silentLogger.info(msg);
59
83
  break;
60
84
  default:
61
85
  console.log(`Status provided to gui info log not recognized: ${status}`);
@@ -64,4 +88,6 @@ export const guiInfoLog = (status: string, data: { numScanned?: number; urlScann
64
88
  }
65
89
  };
66
90
 
91
+ consoleLogger.info(`Logger writing to: ${errorsTxtPath}`);
92
+
67
93
  export { logFormat, consoleLogger, silentLogger };
@@ -14,7 +14,7 @@ import { Base64Encode } from 'base64-stream';
14
14
  import { pipeline } from 'stream/promises';
15
15
  // @ts-ignore
16
16
  import * as Sentry from '@sentry/node';
17
- import constants, { ScannerTypes, sentryConfig, setSentryUser } from './constants/constants.js';
17
+ import constants, { BrowserTypes, ScannerTypes, sentryConfig, setSentryUser } from './constants/constants.js';
18
18
  import { getBrowserToRun, getPlaywrightLaunchOptions } from './constants/common.js';
19
19
 
20
20
  import {
@@ -29,6 +29,7 @@ import {
29
29
  getWcagCriteriaMap,
30
30
  categorizeWcagCriteria,
31
31
  getUserDataTxt,
32
+ register
32
33
  } from './utils.js';
33
34
  import { consoleLogger, silentLogger } from './logs.js';
34
35
  import itemTypeDescription from './constants/itemTypeDescription.js';
@@ -961,8 +962,6 @@ const writeScanDetailsCsv = async (
961
962
  });
962
963
  };
963
964
 
964
- let browserChannel = getBrowserToRun().browserToRun;
965
-
966
965
  const writeSummaryPdf = async (storagePath: string, pagesScanned: number, filename = 'summary', browser: string, userDataDirectory: string) => {
967
966
  const htmlFilePath = `${storagePath}/${filename}.html`;
968
967
  const fileDestinationPath = `${storagePath}/${filename}.pdf`;
@@ -975,6 +974,8 @@ const writeSummaryPdf = async (storagePath: string, pagesScanned: number, filena
975
974
  ...getPlaywrightLaunchOptions(browser),
976
975
  });
977
976
 
977
+ register(context);
978
+
978
979
  const page = await context.newPage();
979
980
 
980
981
  const data = fs.readFileSync(htmlFilePath, { encoding: 'utf-8' });
@@ -1710,9 +1711,9 @@ const generateArtifacts = async (
1710
1711
  zip: string = undefined, // optional
1711
1712
  generateJsonFiles = false,
1712
1713
  ) => {
1713
- const intermediateDatasetsPath = `${getStoragePath(randomToken)}/crawlee`;
1714
- const oobeeAppVersion = getVersion();
1715
1714
  const storagePath = getStoragePath(randomToken);
1715
+ const intermediateDatasetsPath = `${storagePath}/crawlee`;
1716
+ const oobeeAppVersion = getVersion();
1716
1717
 
1717
1718
  const formatAboutStartTime = (dateString: string) => {
1718
1719
  const utcStartTimeDate = new Date(dateString);
@@ -1982,15 +1983,21 @@ const generateArtifacts = async (
1982
1983
  ]);
1983
1984
  }
1984
1985
 
1986
+ let browserChannel = getBrowserToRun(randomToken, BrowserTypes.CHROME, false).browserToRun;
1987
+
1985
1988
  // Should consider refactor constants.userDataDirectory to be a parameter in future
1986
1989
  await retryFunction(() => writeSummaryPdf(storagePath, pagesScanned.length, 'summary', browserChannel, constants.userDataDirectory), 1);
1987
1990
 
1988
- const foldersToRemove = ['crawlee', 'logs'];
1989
- for (const folder of foldersToRemove) {
1990
- const folderPath = path.join(storagePath, folder);
1991
- if (await fs.pathExists(folderPath)) {
1992
- await fs.remove(folderPath);
1993
- }
1991
+ try {
1992
+ fs.rmSync(path.join(storagePath, 'crawlee'), { recursive: true, force: true });
1993
+ } catch (error) {
1994
+ consoleLogger.warn(`Unable to force remove crawlee folder: ${error.message}`);
1995
+ }
1996
+
1997
+ try {
1998
+ fs.rmSync(path.join(storagePath, 'pdfs'), { recursive: true, force: true });
1999
+ } catch (error) {
2000
+ consoleLogger.warn(`Unable to force remove pdfs folder: ${error.message}`);
1994
2001
  }
1995
2002
 
1996
2003
  // Take option if set
@@ -2000,6 +2007,11 @@ const generateArtifacts = async (
2000
2007
  if (!zip.endsWith('.zip')) {
2001
2008
  constants.cliZipFileName += '.zip';
2002
2009
  }
2010
+
2011
+ }
2012
+
2013
+ if (!path.isAbsolute(constants.cliZipFileName) || path.dirname(constants.cliZipFileName) === '.') {
2014
+ constants.cliZipFileName = path.join(storagePath, constants.cliZipFileName);
2003
2015
  }
2004
2016
 
2005
2017
  await fs
package/src/npmIndex.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  submitForm,
13
13
  } from './constants/common.js';
14
14
  import { createCrawleeSubFolders, filterAxeResults } from './crawlers/commonCrawlerFunc.js';
15
- import { createAndUpdateResultsFolders, createDetailsAndLogs } from './utils.js';
15
+ import { createAndUpdateResultsFolders } from './utils.js';
16
16
  import generateArtifacts from './mergeAxeResults.js';
17
17
  import { takeScreenshotForHTMLElements } from './screenshotFunc/htmlScreenshotFunc.js';
18
18
  import { consoleLogger, silentLogger } from './logs.js';
@@ -205,7 +205,7 @@ export const init = async ({
205
205
  throwErrorIfTerminated();
206
206
  if (includeScreenshots) {
207
207
  // use chrome by default
208
- const { browserToRun, clonedBrowserDataDir } = getBrowserToRun(BrowserTypes.CHROME, false, randomToken);
208
+ const { browserToRun, clonedBrowserDataDir } = getBrowserToRun(randomToken, BrowserTypes.CHROME, false);
209
209
  const browserContext = await constants.launcher.launchPersistentContext(
210
210
  clonedBrowserDataDir,
211
211
  { viewport: viewportSettings, ...getPlaywrightLaunchOptions(browserToRun) },
@@ -271,7 +271,6 @@ export const init = async ({
271
271
  if (urlsCrawled.scanned.length === 0) {
272
272
  printMessage([`No pages were scanned.`], alertMessageOptions);
273
273
  } else {
274
- await createDetailsAndLogs(randomToken);
275
274
  await createAndUpdateResultsFolders(randomToken);
276
275
  const pagesNotScanned = [
277
276
  ...scanDetails.urlsCrawled.error,
@@ -0,0 +1,405 @@
1
+ // getProxyInfo.ts
2
+ // Cross-platform proxy detector for Playwright/Chromium with PAC + credentials + macOS Keychain support.
3
+ //
4
+ // Windows: WinINET registry (HKCU → HKLM)
5
+ // macOS: env vars first, then `scutil --proxy` (reads per-protocol usernames/passwords;
6
+ // if password is missing, fetch it from Keychain via `/usr/bin/security`)
7
+ // Linux/others: env vars only
8
+ //
9
+ // Output precedence in proxyInfoToArgs():
10
+ // 1) pacUrl → ["--proxy-pac-url=<url>", ...bypass]
11
+ // 2) manual proxies → ["--proxy-server=...", ...bypass] (embeds creds if provided/available)
12
+ // 3) autoDetect → ["--proxy-auto-detect", ...bypass]
13
+
14
+ import os from 'os';
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import { spawnSync } from 'child_process';
18
+
19
+ export interface ProxyInfo {
20
+ // host:port OR user:pass@host:port (no scheme)
21
+ http?: string;
22
+ https?: string;
23
+ socks?: string;
24
+ // PAC + autodetect
25
+ pacUrl?: string;
26
+ autoDetect?: boolean;
27
+ // optional bypass list (semicolon-separated)
28
+ bypassList?: string;
29
+ // optional global credentials to embed (URL-encoded)
30
+ username?: string;
31
+ password?: string;
32
+ }
33
+
34
+ export interface ProxySettings {
35
+ server: string;
36
+ username?: string;
37
+ password?: string;
38
+ bypass?: string;
39
+ }
40
+
41
+ // New discriminated union that the launcher can switch on
42
+ export type ProxyResolution =
43
+ | { kind: 'manual'; settings: ProxySettings } // Playwright proxy option
44
+ | { kind: 'pac'; pacUrl: string; bypass?: string } // Use --proxy-pac-url
45
+ | { kind: 'none' };
46
+
47
+ /* ============================ helpers ============================ */
48
+
49
+ function stripScheme(u: string): string {
50
+ return u.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, '');
51
+ }
52
+ function semiJoin(arr?: string[]): string | undefined {
53
+ if (!arr) return undefined;
54
+ const cleaned = arr.map(s => s.trim()).filter(Boolean);
55
+ return cleaned.length ? cleaned.join(';') : undefined;
56
+ }
57
+ function readCredsFromEnv(): { username?: string; password?: string } {
58
+ const username = process.env.PROXY_USERNAME || process.env.HTTP_PROXY_USERNAME || undefined;
59
+ const password = process.env.PROXY_PASSWORD || process.env.HTTP_PROXY_PASSWORD || undefined;
60
+ return { username, password };
61
+ }
62
+ function hasUserinfo(v: string | undefined): boolean {
63
+ return !!(v && /@/.test(v.split('/')[0]));
64
+ }
65
+ function pick(map: Record<string, string>, keys: string[]): string | undefined {
66
+ for (const k of keys) {
67
+ const v = map[k];
68
+ if (v && String(v).trim().length) return String(v).trim();
69
+ }
70
+ return undefined;
71
+ }
72
+ function extractHost(valueHostPort: string): string {
73
+ // valueHostPort may be "user:pass@host:port" or "host:port"
74
+ const noUser = valueHostPort.includes('@') ? valueHostPort.split('@', 2)[1] : valueHostPort;
75
+ return noUser.split(':', 2)[0];
76
+ }
77
+
78
+ /* ============================ env (macOS + Linux) ============================ */
79
+
80
+ function parseEnvProxyCommon(): ProxyInfo | null {
81
+ const http = process.env.HTTP_PROXY || process.env.http_proxy || '';
82
+ const https = process.env.HTTPS_PROXY || process.env.https_proxy || '';
83
+ const socks = process.env.ALL_PROXY || process.env.all_proxy || '';
84
+ const noProxy = process.env.NO_PROXY || process.env.no_proxy || '';
85
+
86
+ const info: ProxyInfo = {};
87
+ if (http) info.http = stripScheme(http);
88
+ if (https) info.https = stripScheme(https);
89
+ if (socks) info.socks = stripScheme(socks);
90
+ if (noProxy) info.bypassList = semiJoin(noProxy.split(/[,;]/));
91
+
92
+ const { username, password } = readCredsFromEnv();
93
+ if (username && password) { info.username = username; info.password = password; }
94
+
95
+ return (info.http || info.https || info.socks || info.bypassList) ? info : null;
96
+ }
97
+
98
+ /* ============================ macOS Keychain ============================ */
99
+ /**
100
+ * Try to read an Internet Password from macOS Keychain for a given host/account.
101
+ * We intentionally avoid passing any user-controlled strings; host/account come from scutil.
102
+ * Returns the password (raw) or undefined.
103
+ */
104
+ export function keychainFindInternetPassword(host: string, account?: string): string | undefined {
105
+ console.log("Attempting to find internet proxy password in macOS keychain...");
106
+
107
+ // Only attempt on macOS, in an interactive session, or when explicitly allowed.
108
+ if (process.platform !== 'darwin') return undefined;
109
+ const allow = process.stdin.isTTY || process.env.ENABLE_KEYCHAIN_LOOKUP === '1';
110
+ if (!allow) return undefined;
111
+
112
+ const SECURITY_BIN = '/usr/bin/security';
113
+ const OUTPUT_LIMIT = 64 * 1024; // 64 KiB
114
+
115
+ // Verify absolute binary and realpath
116
+ try {
117
+ if (!fs.existsSync(SECURITY_BIN)) return undefined;
118
+ const real = fs.realpathSync(SECURITY_BIN);
119
+ if (real !== SECURITY_BIN) return undefined;
120
+ } catch {
121
+ return undefined;
122
+ }
123
+
124
+ // Minimal sanitized env (avoid proxy/env influence)
125
+ const env = {
126
+ PATH: '/usr/bin:/bin',
127
+ http_proxy: '', https_proxy: '', all_proxy: '', no_proxy: '',
128
+ HTTP_PROXY: '', HTTPS_PROXY: '', ALL_PROXY: '', NO_PROXY: '',
129
+ NODE_OPTIONS: '', NODE_PATH: '', DYLD_LIBRARY_PATH: '', LD_LIBRARY_PATH: '',
130
+ } as NodeJS.ProcessEnv;
131
+
132
+ const baseArgs = ['find-internet-password', '-s', host, '-w'];
133
+ const args = account ? [...baseArgs, '-a', account] : baseArgs;
134
+
135
+ // No timeout: allow user to respond to Keychain prompt
136
+ const res = spawnSync(SECURITY_BIN, args, {
137
+ encoding: 'utf8',
138
+ windowsHide: true,
139
+ shell: false,
140
+ env,
141
+ stdio: ['ignore', 'pipe', 'pipe'],
142
+ });
143
+
144
+ if (!res.error && res.status === 0 && res.stdout) {
145
+ const out = res.stdout.slice(0, OUTPUT_LIMIT).replace(/\r?\n$/, '');
146
+ return out || undefined;
147
+ }
148
+
149
+ // Retry without account if first try used one
150
+ if (account) {
151
+ const retry = spawnSync(SECURITY_BIN, baseArgs, {
152
+ encoding: 'utf8',
153
+ windowsHide: true,
154
+ shell: false,
155
+ env,
156
+ stdio: ['ignore', 'pipe', 'pipe'],
157
+ });
158
+ if (!retry.error && retry.status === 0 && retry.stdout) {
159
+ const out = retry.stdout.slice(0, OUTPUT_LIMIT).replace(/\r?\n$/, '');
160
+ return out || undefined;
161
+ }
162
+ }
163
+
164
+ // Common Keychain errors you may see in retry.stderr:
165
+ // - "User interaction is not allowed." (Keychain locked / non-interactive / no UI permission)
166
+ // - "The specified item could not be found in the keychain."
167
+ return undefined;
168
+ }
169
+
170
+ /* ============================ macOS fallback (scutil) ============================ */
171
+
172
+ function parseMacScutil(): ProxyInfo | null {
173
+ const out = spawnSync('/usr/sbin/scutil', ['--proxy'], {
174
+ encoding: 'utf8',
175
+ windowsHide: true,
176
+ shell: false,
177
+ });
178
+ if (out.error || !out.stdout) return null;
179
+
180
+ const map: Record<string, string> = {};
181
+ for (const line of out.stdout.split(/\r?\n/)) {
182
+ const m = line.match(/^\s*([A-Za-z0-9]+)\s*:\s*(.+?)\s*$/);
183
+ if (m) map[m[1]] = m[2];
184
+ }
185
+
186
+ const info: ProxyInfo = {};
187
+
188
+ // PAC + autodetect
189
+ if (map['ProxyAutoConfigEnable'] === '1' && map['ProxyAutoConfigURLString']) {
190
+ info.pacUrl = map['ProxyAutoConfigURLString'].trim();
191
+ }
192
+ if (map['ProxyAutoDiscoveryEnable'] === '1') info.autoDetect = true;
193
+
194
+ // Collect per-protocol creds (Apple keys vary by macOS version)
195
+ const httpUser = pick(map, ['HTTPProxyUsername', 'HTTPUser', 'HTTPUsername']);
196
+ let httpPass = pick(map, ['HTTPProxyPassword', 'HTTPPassword']);
197
+ const httpsUser = pick(map, ['HTTPSProxyUsername', 'HTTPSUser', 'HTTPSUsername']);
198
+ let httpsPass = pick(map, ['HTTPSProxyPassword', 'HTTPSPassword']);
199
+ const socksUser = pick(map, ['SOCKSProxyUsername', 'SOCKSUser', 'SOCKSUsername']);
200
+ let socksPass = pick(map, ['SOCKSProxyPassword', 'SOCKSPassword']);
201
+
202
+ // Manual proxies (always set host:port only; never include creds)
203
+ if (map['HTTPEnable'] === '1' && map['HTTPProxy'] && map['HTTPPort']) {
204
+ const hostPort = `${map['HTTPProxy']}:${map['HTTPPort']}`;
205
+ info.http = hostPort;
206
+ // If macOS has username but no password, try Keychain for this host/account
207
+ if (httpUser && !httpPass) httpPass = keychainFindInternetPassword(extractHost(hostPort), httpUser);
208
+ }
209
+
210
+ if (map['HTTPSEnable'] === '1' && map['HTTPSProxy'] && map['HTTPSPort']) {
211
+ const hostPort = `${map['HTTPSProxy']}:${map['HTTPSPort']}`;
212
+ info.https = hostPort;
213
+ if (httpsUser && !httpsPass) httpsPass = keychainFindInternetPassword(extractHost(hostPort), httpsUser);
214
+ }
215
+
216
+ if (map['SOCKSEnable'] === '1' && map['SOCKSProxy'] && map['SOCKSPort']) {
217
+ const hostPort = `${map['SOCKSProxy']}:${map['SOCKSPort']}`;
218
+ info.socks = hostPort;
219
+ if (socksUser && !socksPass) socksPass = keychainFindInternetPassword(extractHost(hostPort), socksUser);
220
+ }
221
+
222
+ // Choose one set of creds to expose globally: prefer HTTP, else HTTPS, else SOCKS
223
+ // (Do not overwrite if env already provided username/password.)
224
+ if (!info.username && !info.password) {
225
+ if (httpUser && httpPass) {
226
+ info.username = httpUser;
227
+ info.password = httpPass;
228
+ } else if (httpsUser && httpsPass) {
229
+ info.username = httpsUser;
230
+ info.password = httpsPass;
231
+ } else if (socksUser && socksPass) {
232
+ info.username = socksUser;
233
+ info.password = socksPass;
234
+ }
235
+ }
236
+
237
+ // Bypass list
238
+ let exceptions: string[] = [];
239
+ if (map['ExceptionsList']) {
240
+ const quoted = [...map['ExceptionsList'].matchAll(/"([^"]+)"/g)].map(m => m[1]);
241
+ exceptions = quoted.length
242
+ ? quoted
243
+ : map['ExceptionsList'].replace(/[<>{}()]/g, ' ')
244
+ .split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
245
+ }
246
+ if (map['ExcludeSimpleHostnames'] === '1' && !exceptions.includes('<local>')) exceptions.unshift('<local>');
247
+ const bypassList = semiJoin(exceptions);
248
+ if (bypassList) info.bypassList = bypassList;
249
+
250
+ // If scutil did not provide creds anywhere, still allow env creds as global fallback
251
+ if ((!info.username || !info.password)) {
252
+ const envCreds = readCredsFromEnv();
253
+ if (envCreds.username && envCreds.password) {
254
+ info.username = info.username || envCreds.username;
255
+ info.password = info.password || envCreds.password;
256
+ }
257
+ }
258
+
259
+ return (info.pacUrl || info.http || info.https || info.socks || info.autoDetect || info.bypassList) ? info : null;
260
+ }
261
+
262
+ /* ============================ Windows (Registry only) ============================ */
263
+
264
+ function regExeCandidates(): string[] {
265
+ const root = process.env.SystemRoot || 'C:\\Windows';
266
+ const sys32 = path.join(root, 'System32', 'reg.exe');
267
+ const syswow64 = path.join(root, 'SysWOW64', 'reg.exe');
268
+ const sysnative = path.join(root, 'Sysnative', 'reg.exe'); // for 32-bit node on 64-bit OS
269
+ const set = new Set([sysnative, sys32, syswow64]);
270
+ return [...set].filter(p => p && fs.existsSync(p));
271
+ }
272
+ function execReg(args: string[]): string | null {
273
+ for (const exe of regExeCandidates()) {
274
+ const out = spawnSync(exe, args, {
275
+ encoding: 'utf8',
276
+ windowsHide: true,
277
+ shell: false,
278
+ });
279
+ if (!out.error && out.stdout) return out.stdout;
280
+ }
281
+ return null;
282
+ }
283
+
284
+ type WinVals = {
285
+ ProxyEnable?: string;
286
+ ProxyServer?: string;
287
+ ProxyOverride?: string;
288
+ AutoConfigURL?: string;
289
+ AutoDetect?: string;
290
+ };
291
+
292
+ function readRegVals(hiveKey: string): WinVals | null {
293
+ const stdout = execReg(['query', hiveKey]);
294
+ if (!stdout) return null;
295
+
296
+ const take = (name: string) => {
297
+ const m = stdout.match(new RegExp(`\\s${name}\\s+REG_\\w+\\s+(.+)$`, 'mi'));
298
+ return m ? m[1].trim() : '';
299
+ };
300
+
301
+ return {
302
+ ProxyEnable: take('ProxyEnable'),
303
+ ProxyServer: take('ProxyServer'),
304
+ ProxyOverride: take('ProxyOverride'),
305
+ AutoConfigURL: take('AutoConfigURL'),
306
+ AutoDetect: take('AutoDetect'),
307
+ };
308
+ }
309
+
310
+ function parseWindowsRegistry(): ProxyInfo | null {
311
+ const HKCU = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings';
312
+ const HKLM = 'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings';
313
+
314
+ const cu = readRegVals(HKCU);
315
+ const lm = readRegVals(HKLM);
316
+ const v = cu || lm;
317
+ if (!v) return null;
318
+
319
+ const info: ProxyInfo = {};
320
+ const enabledManual = !!v.ProxyEnable && v.ProxyEnable !== '0' && v.ProxyEnable.toLowerCase() !== '0x0';
321
+ const enabledAuto = !!v.AutoDetect && v.AutoDetect !== '0' && v.AutoDetect.toLowerCase() !== '0x0';
322
+ const anyEnabled = enabledManual || enabledAuto || !!v.AutoConfigURL;
323
+
324
+ // PAC + autodetect (only when something is actually enabled)
325
+ if (v.AutoConfigURL) info.pacUrl = v.AutoConfigURL.trim(); // PAC stands on its own
326
+ if (enabledAuto) info.autoDetect = true; // autodetect still gated
327
+
328
+ // Manual proxies
329
+ if (enabledManual && v.ProxyServer) {
330
+ const s = v.ProxyServer.trim();
331
+ if (s.includes('=')) {
332
+ for (const part of s.split(';')) {
333
+ const [proto, addr] = part.split('=');
334
+ if (!proto || !addr) continue;
335
+ const p = proto.trim().toLowerCase();
336
+ const a = stripScheme(addr.trim());
337
+ if (p === 'http') info.http = a;
338
+ else if (p === 'https') info.https = a;
339
+ else if (p === 'socks' || p === 'socks5') info.socks = a;
340
+ }
341
+ } else {
342
+ const a = stripScheme(s);
343
+ info.http = a;
344
+ info.https = a;
345
+ }
346
+ }
347
+
348
+ // Bypass list
349
+ if (anyEnabled && v.ProxyOverride) info.bypassList = semiJoin(v.ProxyOverride.split(/[,;]/));
350
+
351
+ // Env creds as global fallback (Windows does not store proxy creds in ProxyServer)
352
+ const { username, password } = readCredsFromEnv();
353
+ if (username && password) { info.username = username; info.password = password; }
354
+
355
+ return (info.pacUrl || info.http || info.https || info.socks || info.autoDetect || info.bypassList) ? info : null;
356
+ }
357
+
358
+ /* ============================ Public API ============================ */
359
+
360
+ export function getProxyInfo(): ProxyInfo | null {
361
+ const plat = os.platform();
362
+ if (plat === 'win32') return parseEnvProxyCommon() || parseWindowsRegistry();
363
+ if (plat === 'darwin') return parseEnvProxyCommon() || parseMacScutil();
364
+ return parseEnvProxyCommon(); // Linux/others
365
+ }
366
+
367
+ export function proxyInfoToResolution(info: ProxyInfo | null): ProxyResolution {
368
+ if (!info) return { kind: 'none' };
369
+
370
+ // Prefer manual proxies first (these work with Playwright's proxy option)
371
+ if (info.http) {
372
+ return { kind: 'manual', settings: {
373
+ server: `http://${info.http}`,
374
+ username: info.username,
375
+ password: info.password,
376
+ bypass: info.bypassList,
377
+ }};
378
+ }
379
+ if (info.https) {
380
+ return { kind: 'manual', settings: {
381
+ server: `http://${info.https}`,
382
+ username: info.username,
383
+ password: info.password,
384
+ bypass: info.bypassList,
385
+ }};
386
+ }
387
+ if (info.socks) {
388
+ return { kind: 'manual', settings: {
389
+ server: `socks5://${info.socks}`,
390
+ username: info.username,
391
+ password: info.password,
392
+ bypass: info.bypassList,
393
+ }};
394
+ }
395
+
396
+ // PAC → handle via Chromium args; do NOT try proxy.server = 'pac+...'
397
+ if (info.pacUrl) {
398
+ // Minor hardening: prefer 127.0.0.1 over localhost for loopback PAC
399
+ const pacUrl = info.pacUrl.replace('://localhost', '://127.0.0.1');
400
+ return { kind: 'pac', pacUrl, bypass: info.bypassList };
401
+ }
402
+
403
+ // Auto-detect not supported directly
404
+ return { kind: 'none' };
405
+ }
@@ -22,7 +22,7 @@ export const takeScreenshotForHTMLElements = async (
22
22
  for (const violation of violations) {
23
23
  if (screenshotCount >= maxScreenshots) {
24
24
  /*
25
- silentLogger.warn(
25
+ consoleLogger.warn(
26
26
  `Skipping screenshots for ${violation.id} as maxScreenshots (${maxScreenshots}) exceeded. You can increase it by specifying a higher value when calling takeScreenshotForHTMLElements.`,
27
27
  );
28
28
  */
@@ -34,7 +34,7 @@ export const takeScreenshotForHTMLElements = async (
34
34
 
35
35
  // Check if rule ID is 'oobee-grading-text-contents' and skip screenshot logic
36
36
  if (rule === 'oobee-grading-text-contents') {
37
- // silentLogger.info('Skipping screenshot for rule oobee-grading-text-contents');
37
+ // consoleLogger.info('Skipping screenshot for rule oobee-grading-text-contents');
38
38
  newViolations.push(violation); // Make sure it gets added
39
39
  continue;
40
40
  }
@@ -59,13 +59,13 @@ export const takeScreenshotForHTMLElements = async (
59
59
  nodeWithScreenshotPath.screenshotPath = screenshotPath;
60
60
  screenshotCount++;
61
61
  } else {
62
- // silentLogger.info(`Element at ${currLocator} is not visible`);
62
+ // consoleLogger.info(`Element at ${currLocator} is not visible`);
63
63
  }
64
64
 
65
65
  break; // Stop looping after finding the first visible locator
66
66
  }
67
67
  } catch (e) {
68
- // silentLogger.info(`Unable to take element screenshot at ${selector}`);
68
+ // consoleLogger.info(`Unable to take element screenshot at ${selector}`);
69
69
  }
70
70
  }
71
71
  newViolationNodes.push(nodeWithScreenshotPath);