@loginguards/loginguards-win 0.1.2 → 2.0.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/README.md CHANGED
@@ -5,10 +5,11 @@ Enterprise-grade password breach prevention for Windows domains.
5
5
  ## Features
6
6
 
7
7
  - Zero Trust password validation via LoginGuards API
8
+ - Windows Password Filter DLL for domain-wide enforcement on DCs
9
+ - Local Windows service policy engine (named pipe IPC)
8
10
  - No password storage; passwords never logged
9
- - Windows service policy engine (node-windows)
10
11
  - Secure API key storage (Windows Credential Manager via keytar)
11
- - CLI: `install`, `configure`, `test`, `uninstall`
12
+ - CLI: `configure`, `install`, `test`, `uninstall`, `check`, `pipe-test`
12
13
 
13
14
  ## Install (development)
14
15
 
@@ -25,7 +26,8 @@ loginguards-win --help
25
26
 
26
27
  - API base: `https://api.loginguards.com/v1`
27
28
  - Required header: `x-api-key: <LOGIN_GUARDS_API_KEY>`
28
- - Behavior on API failure is configurable (planned): fail-open or fail-closed
29
+ - Behavior on API failure is configurable: `fail-open` (default) or `fail-closed`
30
+ - Timeout default: `1500ms` (configurable)
29
31
 
30
32
  ## Security
31
33
 
@@ -33,9 +35,61 @@ loginguards-win --help
33
35
  - API key stored in Windows Credential Manager
34
36
  - HTTPS only
35
37
 
36
- ## Active Directory Integration (planned)
38
+ ## Active Directory Integration (V2)
37
39
 
38
- This preview scaffolds the Windows service and CLI. Integration via the Windows Password Filter with a PowerShell bridge to the Node.js service is planned.
40
+ V2 includes a signed x64 Windows Password Filter DLL that runs inside LSASS on Domain Controllers and communicates with the local policy engine via a named pipe (`\\.\\pipe\\LoginGuardsPwdFilter`). The service calls the LoginGuards API and returns an allow/deny decision to the DLL.
41
+
42
+ Decision mapping uses the API field `breached` and returns reasons: `SAFE`, `COMPROMISED`, `API_DOWN`, `TIMEOUT`, `NO_API_KEY`. Default policy is `fail-open`.
43
+
44
+ ## Deployment (Domain Controller only)
45
+
46
+ 1. Configure API connectivity on the DC:
47
+ ```bash
48
+ loginguards-win configure
49
+ ```
50
+ 2. Install service and register the password filter (admin required; reboot recommended):
51
+ ```bash
52
+ # A prebuilt DLL can be bundled at assets/LoginGuardsPwdFilter/x64/LoginGuardsPwdFilter.dll
53
+ # Or provide an explicit path via --dllPath
54
+ loginguards-win install \
55
+ --failMode open \
56
+ --timeoutMs 1500 \
57
+ --pipeName "\\.\\pipe\\LoginGuardsPwdFilter" \
58
+ --reboot
59
+ ```
60
+ 3. Reboot is required for the password filter to load into LSASS.
61
+
62
+ To uninstall on a DC:
63
+ ```bash
64
+ loginguards-win uninstall --reboot
65
+ ```
66
+
67
+ ## Test and Diagnostics
68
+
69
+ - Domain Controller mode:
70
+ ```bash
71
+ loginguards-win test --mode dc
72
+ ```
73
+ Validates service, pipe, registry (Notification Packages), DLL presence, API reachability, and evaluates a non-destructive test password.
74
+
75
+ - Client mode (domain-joined workstation):
76
+ ```bash
77
+ loginguards-win test --mode client
78
+ ```
79
+ Shows domain membership and logon server; enforcement validation must be run on a DC.
80
+
81
+ - Direct password check (no storage/logging):
82
+ ```bash
83
+ loginguards-win check --prompt
84
+ # or
85
+ loginguards-win check --password "YourPassword" --debug
86
+ ```
87
+
88
+ ## Recommended Rollout (Safety)
89
+
90
+ - Deploy to a secondary Domain Controller first
91
+ - Validate password resets/changes with test users
92
+ - Roll out to all Domain Controllers after validation
39
93
 
40
94
  ## Uninstall
41
95
 
@@ -0,0 +1,3 @@
1
+ Place the signed 64-bit LoginGuardsPwdFilter.dll in this folder before publishing.
2
+ Expected filename: LoginGuardsPwdFilter.dll
3
+ This file will be copied to %SystemRoot%\System32 on Domain Controllers during installation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loginguards/loginguards-win",
3
- "version": "0.1.2",
3
+ "version": "2.0.0",
4
4
  "description": "LoginGuards Active Directory Password Protection for Windows",
5
5
  "private": false,
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -21,6 +21,7 @@
21
21
  "files": [
22
22
  "bin",
23
23
  "src",
24
+ "assets",
24
25
  "README.md",
25
26
  "LICENSE"
26
27
  ],
package/src/apiClient.js CHANGED
@@ -7,9 +7,10 @@ const client = axios.create({
7
7
  timeout: 10000
8
8
  });
9
9
 
10
- async function checkPlain(password, apiKey) {
10
+ async function checkPlain(password, apiKey, options) {
11
11
  if (typeof password !== 'string') throw new Error('password must be string');
12
- const resp = await client.post('/check/plain', { password }, { headers: { 'x-api-key': apiKey } });
12
+ const timeout = options && typeof options.timeoutMs === 'number' ? options.timeoutMs : undefined;
13
+ const resp = await client.post('/check/plain', { password }, { headers: { 'x-api-key': apiKey }, timeout });
13
14
  return resp.data;
14
15
  }
15
16
 
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ const storage = require('../../storage');
3
+ const apiClient = require('../../apiClient');
4
+ const { logger } = require('../../logger');
5
+
6
+ module.exports = {
7
+ command: 'check',
8
+ describe: 'Check one or more passwords against LoginGuards (no passwords are logged or stored)',
9
+ builder: {
10
+ password: { type: 'string', describe: 'Password to check (avoid history; prefer --prompt or --stdin)' },
11
+ prompt: { type: 'boolean', default: false, describe: 'Securely prompt for a password' },
12
+ stdin: { type: 'boolean', default: false, describe: 'Read newline-separated passwords from stdin' },
13
+ debug: { type: 'boolean', default: false, describe: 'Print raw API response (sanitized)' }
14
+ },
15
+ handler: async (args) => {
16
+ const apiKey = await storage.getApiKey();
17
+ if (!apiKey) {
18
+ console.log('✖ API key not configured. Run "loginguards-win configure".');
19
+ process.exit(1);
20
+ }
21
+
22
+ async function evalOne(pwd) {
23
+ try {
24
+ const res = await apiClient.checkPlain(pwd, apiKey);
25
+ if (args.debug) {
26
+ try { console.log('debug response:', JSON.stringify(res)); } catch {}
27
+ }
28
+ const compromised = (typeof res.breached !== 'undefined') ? !!res.breached
29
+ : (typeof res.compromised !== 'undefined') ? !!res.compromised
30
+ : (typeof res.is_compromised !== 'undefined') ? !!res.is_compromised
31
+ : (typeof res.isCompromised !== 'undefined') ? !!res.isCompromised
32
+ : (typeof res.count === 'number') ? res.count > 0
33
+ : false;
34
+ if (compromised) {
35
+ console.log('✖ COMPROMISED');
36
+ } else {
37
+ console.log('✔ NOT COMPROMISED');
38
+ }
39
+ } catch (e) {
40
+ const status = e && e.response && e.response.status;
41
+ if (status === 401) console.log('✖ Error: Unauthorized (invalid API key)');
42
+ else if (status === 429) console.log('✖ Error: Rate limited');
43
+ else console.log('✖ Error:', e.message || String(e));
44
+ logger.warn(`check command API error: ${status || e.code || e.message}`);
45
+ process.exitCode = 1;
46
+ }
47
+ }
48
+
49
+ if (args.stdin) {
50
+ // batch mode from stdin
51
+ const chunks = [];
52
+ process.stdin.setEncoding('utf8');
53
+ process.stdin.on('data', (d) => chunks.push(d));
54
+ process.stdin.on('end', async () => {
55
+ const text = chunks.join('');
56
+ const lines = text.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
57
+ if (!lines.length) { console.log('No input passwords.'); return; }
58
+ for (const line of lines) {
59
+ await evalOne(line);
60
+ }
61
+ });
62
+ if (process.stdin.readableEnded) {
63
+ // no stdin piped
64
+ console.log('No stdin provided. Use --password or --prompt instead.');
65
+ }
66
+ return;
67
+ }
68
+
69
+ if (typeof args.password === 'string' && args.password.length > 0) {
70
+ await evalOne(args.password);
71
+ return;
72
+ }
73
+
74
+ // prompt safely
75
+ try {
76
+ const inquirer = (await import('inquirer')).default;
77
+ const ans = await inquirer.prompt([{ type: 'password', name: 'pwd', message: 'Enter password to check', mask: '*', validate: v => v ? true : 'Required' }]);
78
+ await evalOne(ans.pwd);
79
+ } catch (e) {
80
+ console.error('✖ Prompt failed:', e.message || String(e));
81
+ process.exit(1);
82
+ }
83
+ }
84
+ };
@@ -1,6 +1,13 @@
1
1
  "use strict";
2
2
  const { logger } = require('../../logger');
3
3
  const installer = require('../../installer');
4
+ function normalizeFailMode(v) {
5
+ if (!v) return 'fail-open';
6
+ const s = String(v).toLowerCase();
7
+ if (s === 'open' || s === 'fail-open') return 'fail-open';
8
+ if (s === 'closed' || s === 'fail-closed') return 'fail-closed';
9
+ return 'fail-open';
10
+ }
4
11
 
5
12
  module.exports = {
6
13
  command: 'install',
@@ -8,8 +15,8 @@ module.exports = {
8
15
  builder: {
9
16
  failMode: {
10
17
  type: 'string',
11
- choices: ['fail-open', 'fail-closed'],
12
- default: 'fail-closed',
18
+ choices: ['fail-open', 'fail-closed', 'open', 'closed'],
19
+ default: 'fail-open',
13
20
  describe: 'Behavior if API unreachable'
14
21
  },
15
22
  pipeName: {
@@ -21,12 +28,27 @@ module.exports = {
21
28
  type: 'boolean',
22
29
  default: false,
23
30
  describe: 'If true, log the username in audit logs (never logs passwords)'
31
+ },
32
+ timeoutMs: {
33
+ type: 'number',
34
+ default: 1500,
35
+ describe: 'Max time in milliseconds to wait for API decision (DLL also times out)'
36
+ },
37
+ dllPath: {
38
+ type: 'string',
39
+ describe: 'Path to prebuilt x64 LoginGuardsPwdFilter.dll (required on DC if not bundled)'
40
+ },
41
+ reboot: {
42
+ type: 'boolean',
43
+ default: false,
44
+ describe: 'If true and on DC, reboot after registering the password filter'
24
45
  }
25
46
  },
26
47
  handler: async (args) => {
27
48
  logger.info('Starting installation...');
28
49
  try {
29
- await installer.install({ failMode: args.failMode, pipeName: args.pipeName, logUsername: args.logUsername });
50
+ const fm = normalizeFailMode(args.failMode);
51
+ await installer.install({ failMode: fm, pipeName: args.pipeName, logUsername: args.logUsername, timeoutMs: args.timeoutMs, dllPath: args.dllPath, reboot: args.reboot });
30
52
  logger.info('Installation completed.');
31
53
  console.log('✔ Installation completed');
32
54
  } catch (err) {
@@ -6,7 +6,9 @@ module.exports = {
6
6
  command: 'test',
7
7
  describe: 'Run end-to-end validation across AD and LoginGuards',
8
8
  builder: {
9
- user: { type: 'string', describe: 'Domain user to simulate', default: process.env.USERNAME ? `${process.env.USERDOMAIN || 'DOMAIN'}\\${process.env.USERNAME}` : undefined }
9
+ user: { type: 'string', describe: 'Domain user to simulate', default: process.env.USERNAME ? `${process.env.USERDOMAIN || 'DOMAIN'}\\${process.env.USERNAME}` : undefined },
10
+ mode: { type: 'string', choices: ['auto', 'dc', 'client'], default: 'auto', describe: 'Validation mode: detect DC (auto), force DC or client mode' },
11
+ verbose: { type: 'boolean', default: false, describe: 'Print extra diagnostics' }
10
12
  },
11
13
  handler: async (args) => {
12
14
  try {
@@ -5,10 +5,12 @@ const installer = require('../../installer');
5
5
  module.exports = {
6
6
  command: 'uninstall',
7
7
  describe: 'Uninstall the plugin and remove all components',
8
- builder: {},
9
- handler: async () => {
8
+ builder: {
9
+ reboot: { type: 'boolean', default: false, describe: 'If true (on DC), reboot after unregistering the password filter' }
10
+ },
11
+ handler: async (args) => {
10
12
  try {
11
- await installer.uninstall();
13
+ await installer.uninstall({ reboot: args.reboot });
12
14
  console.log('✔ Uninstall completed');
13
15
  } catch (err) {
14
16
  logger.error(`Uninstall failed: ${err.stack || err.message || err}`);
package/src/cli/index.js CHANGED
@@ -11,6 +11,7 @@ async function run(argvInput) {
11
11
  .usage('$0 <cmd> [args]')
12
12
  .command(require('./commands/install'))
13
13
  .command(require('./commands/configure'))
14
+ .command(require('./commands/check'))
14
15
  .command(require('./commands/test'))
15
16
  .command(require('./commands/pipe-test'))
16
17
  .command(require('./commands/uninstall'))
package/src/config.js CHANGED
@@ -27,9 +27,10 @@ function writeConfigFile(obj) {
27
27
  function getConfig() {
28
28
  const data = readConfigFile();
29
29
  return {
30
- failMode: data.failMode || 'fail-closed',
30
+ failMode: data.failMode || 'fail-open',
31
31
  pipeName: data.pipeName || "\\\\.\\pipe\\LoginGuardsPwdFilter",
32
32
  logUsername: !!data.logUsername,
33
+ timeoutMs: typeof data.timeoutMs === 'number' && data.timeoutMs > 0 ? data.timeoutMs : 1500,
33
34
  apiKeyEnc: data.apiKeyEnc || undefined
34
35
  };
35
36
  }
package/src/installer.js CHANGED
@@ -4,6 +4,8 @@ const service = require('./service');
4
4
  const storage = require('./storage');
5
5
  const { runPS } = require('./ps');
6
6
  const { setConfig } = require('./config');
7
+ const fs = require('fs');
8
+ const path = require('path');
7
9
 
8
10
  function assertWindows() {
9
11
  if (process.platform !== 'win32') throw new Error('Windows only');
@@ -25,17 +27,97 @@ async function install(options) {
25
27
  assertWindows();
26
28
  await checkAdmin();
27
29
  // Persist configuration for the service
28
- setConfig({ failMode: options.failMode, pipeName: options.pipeName, logUsername: !!options.logUsername });
30
+ setConfig({ failMode: options.failMode, pipeName: options.pipeName, logUsername: !!options.logUsername, timeoutMs: options.timeoutMs });
29
31
  logger.info('Installing Windows service...');
30
32
  await service.install();
31
- logger.info('Note: AD Password Filter registration not yet implemented in this preview.');
33
+ // DC-only: deploy password filter DLL and register in LSA
34
+ const dc = await isDomainController().catch(() => false);
35
+ if (!dc) {
36
+ logger.info('Non-DC host detected; skipping password filter registration.');
37
+ return;
38
+ }
39
+ logger.info('Domain Controller detected; proceeding with Password Filter registration.');
40
+ const sys32 = process.env.WINDIR ? path.join(process.env.WINDIR, 'System32') : 'C\\\Windows\\System32';
41
+ const targetDll = path.join(sys32, 'LoginGuardsPwdFilter.dll');
42
+ let sourceDll = options.dllPath;
43
+ if (!sourceDll) {
44
+ // Try bundled asset
45
+ const bundled = path.join(__dirname, '..', 'assets', 'LoginGuardsPwdFilter', 'x64', 'LoginGuardsPwdFilter.dll');
46
+ if (fs.existsSync(bundled)) sourceDll = bundled;
47
+ }
48
+ if (!sourceDll || !fs.existsSync(sourceDll)) {
49
+ logger.warn('Password Filter DLL not found. Provide --dllPath to a prebuilt x64 LoginGuardsPwdFilter.dll');
50
+ } else {
51
+ logger.info(`Copying DLL to ${targetDll}`);
52
+ fs.copyFileSync(sourceDll, targetDll);
53
+ }
54
+ const changed = await registerPasswordFilter('LoginGuardsPwdFilter');
55
+ if (changed) {
56
+ logger.info('Password Filter registered in LSA (Notification Packages). Reboot required.');
57
+ if (options.reboot) {
58
+ logger.info('Reboot flag provided; rebooting now.');
59
+ await runPS('Restart-Computer -Force');
60
+ } else {
61
+ console.log('⚠ Reboot required to activate the password filter.');
62
+ }
63
+ } else {
64
+ logger.info('Password Filter already registered.');
65
+ }
32
66
  }
33
67
 
34
- async function uninstall() {
68
+ async function uninstall(options = {}) {
35
69
  assertWindows();
36
70
  logger.info('Uninstalling Windows service...');
37
71
  await service.uninstall();
38
72
  await storage.deleteApiKey();
73
+ const dc = await isDomainController().catch(() => false);
74
+ if (!dc) return;
75
+ // DC-only cleanup
76
+ const removed = await unregisterPasswordFilter('LoginGuardsPwdFilter');
77
+ const sys32 = process.env.WINDIR ? path.join(process.env.WINDIR, 'System32') : 'C\\\Windows\\System32';
78
+ const targetDll = path.join(sys32, 'LoginGuardsPwdFilter.dll');
79
+ try { fs.unlinkSync(targetDll); } catch {}
80
+ if (removed) {
81
+ logger.info('Password Filter unregistered. Reboot required to unload.');
82
+ if (options.reboot) {
83
+ logger.info('Reboot flag provided; rebooting now.');
84
+ await runPS('Restart-Computer -Force');
85
+ } else {
86
+ console.log('⚠ Reboot required to fully unload the password filter.');
87
+ }
88
+ }
89
+ }
90
+
91
+ async function isDomainController() {
92
+ const script = `
93
+ $k = Get-Item 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\NTDS' -ErrorAction SilentlyContinue
94
+ if ($null -ne $k) { 'DC' } else { 'NOTDC' }
95
+ `;
96
+ const { stdout } = await runPS(script);
97
+ return stdout.trim() === 'DC';
98
+ }
99
+
100
+ async function registerPasswordFilter(nameNoExt) {
101
+ const script = `
102
+ $path = 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa'
103
+ $name = 'Notification Packages'
104
+ $cur = (Get-ItemProperty -Path $path -Name $name -ErrorAction SilentlyContinue).$name
105
+ if ($null -eq $cur) { $cur = @() }
106
+ if ($cur -notcontains '${nameNoExt}') { $new = @($cur) + '${nameNoExt}'; Set-ItemProperty -Path $path -Name $name -Value $new; 'CHANGED' } else { 'NOCHANGE' }
107
+ `;
108
+ const { stdout } = await runPS(script);
109
+ return stdout.trim() === 'CHANGED';
110
+ }
111
+
112
+ async function unregisterPasswordFilter(nameNoExt) {
113
+ const script = `
114
+ $path = 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa'
115
+ $name = 'Notification Packages'
116
+ $cur = (Get-ItemProperty -Path $path -Name $name -ErrorAction SilentlyContinue).$name
117
+ if ($null -eq $cur) { 'NOCHANGE' } else { $new = @($cur) | Where-Object { $_ -ne '${nameNoExt}' }; Set-ItemProperty -Path $path -Name $name -Value $new; 'CHANGED' }
118
+ `;
119
+ const { stdout } = await runPS(script);
120
+ return stdout.trim() === 'CHANGED';
39
121
  }
40
122
 
41
123
  module.exports = { install, uninstall };
@@ -1 +1,7 @@
1
1
  info: LoginGuards Policy Engine service started. {"timestamp":"2026-01-08T02:34:56.398Z"}
2
+ info: LoginGuards Policy Engine service started. {"timestamp":"2026-01-08T18:19:44.254Z"}
3
+ info: Named pipe server listening on \\.\pipe\LoginGuardsPwdFilter {"timestamp":"2026-01-08T18:19:44.262Z"}
4
+ info: Password evaluation result: ok {"timestamp":"2026-01-08T18:19:50.289Z"}
5
+ info: Password evaluation result: ok {"timestamp":"2026-01-08T18:28:15.489Z"}
6
+ info: Password evaluation result: ok {"timestamp":"2026-01-08T18:30:48.150Z"}
7
+ info: Password evaluation result: ok {"timestamp":"2026-01-08T19:34:03.913Z"}
@@ -1,2 +1,18 @@
1
1
  2026-01-08 04:34:56 - Starting C:\Users\Samer\AppData\Local\Volta\tools\image\node\24.3.0\node.exe --harmony "D:\projects\backend\fast-Backend (1)\New 0\node_modules\.pnpm\node-windows@1.0.0-beta.8\node_modules\node-windows\lib\wrapper.js" --file "D:\projects\backend\fast-Backend (1)\New 0\src\service\engine.js" --scriptoptions= --log "LoginGuardsPolicyEngine wrapper" --grow 0.5 --wait 2 --maxrestarts 3 --abortonerror n --stopparentfirst undefined
2
2
  2026-01-08 04:34:56 - Started 15000
3
+ 2026-01-08 20:19:43 - Stopping loginguardspolicyengine.exe
4
+ 2026-01-08 20:19:43 - ProcessKill 15000
5
+ 2026-01-08 20:19:43 - Found child process: 7540 Name: conhost.exe
6
+ 2026-01-08 20:19:43 - Found child process: 15148 Name: node.exe
7
+ 2026-01-08 20:19:43 - Stopping process 7540
8
+ 2026-01-08 20:19:43 - Send SIGINT 7540
9
+ 2026-01-08 20:19:43 - SIGINT to 7540 failed - Killing as fallback
10
+ 2026-01-08 20:19:43 - Stopping process 15148
11
+ 2026-01-08 20:19:43 - Send SIGINT 15148
12
+ 2026-01-08 20:19:43 - SIGINT to 15148 failed - Killing as fallback
13
+ 2026-01-08 20:19:43 - Stopping process 15000
14
+ 2026-01-08 20:19:43 - Send SIGINT 15000
15
+ 2026-01-08 20:19:43 - SIGINT to 15000 failed - Killing as fallback
16
+ 2026-01-08 20:19:43 - Finished loginguardspolicyengine.exe
17
+ 2026-01-08 20:19:43 - Starting C:\Users\Samer\AppData\Local\Volta\tools\image\node\24.3.0\node.exe --harmony "D:\projects\backend\fast-Backend (1)\New 0\node_modules\.pnpm\node-windows@1.0.0-beta.8\node_modules\node-windows\lib\wrapper.js" --file "D:\projects\backend\fast-Backend (1)\New 0\src\service\engine.js" --scriptoptions= --log "LoginGuardsPolicyEngine wrapper" --grow 0.5 --wait 2 --maxrestarts 3 --abortonerror n --stopparentfirst undefined
18
+ 2026-01-08 20:19:43 - Started 13552
@@ -16,23 +16,31 @@ function parseJsonLine(buf) {
16
16
  }
17
17
 
18
18
  async function evaluatePassword(password) {
19
- const { failMode } = getConfig();
19
+ const { failMode, timeoutMs } = getConfig();
20
20
  const apiKey = await storage.getApiKey();
21
21
  if (!apiKey) {
22
- return { allow: failMode === 'fail-open', reason: 'no_api_key' };
22
+ return { allow: failMode === 'fail-open', reason: 'NO_API_KEY' };
23
23
  }
24
24
  try {
25
- const res = await apiClient.checkPlain(password, apiKey);
26
- const compromised = !!res.compromised;
27
- return { allow: !compromised, reason: compromised ? 'compromised' : 'ok' };
25
+ const res = await apiClient.checkPlain(password, apiKey, { timeoutMs });
26
+ const compromised = (typeof res.breached !== 'undefined') ? !!res.breached
27
+ : (typeof res.compromised !== 'undefined') ? !!res.compromised
28
+ : (typeof res.is_compromised !== 'undefined') ? !!res.is_compromised
29
+ : (typeof res.isCompromised !== 'undefined') ? !!res.isCompromised
30
+ : (typeof res.count === 'number') ? res.count > 0
31
+ : false;
32
+ return { allow: !compromised, reason: compromised ? 'COMPROMISED' : 'SAFE' };
28
33
  } catch (e) {
29
- logger.warn(`API error during evaluation: ${e.status || e.code || e.message}`);
30
- return { allow: failMode === 'fail-open', reason: 'api_error' };
34
+ const msg = e && (e.code || e.message || e.toString());
35
+ const isTimeout = (e && e.code === 'ECONNABORTED') || (typeof msg === 'string' && msg.toLowerCase().includes('timeout'));
36
+ const reason = isTimeout ? 'TIMEOUT' : 'API_DOWN';
37
+ logger.warn(`API error during evaluation: ${msg}`);
38
+ return { allow: failMode === 'fail-open', reason };
31
39
  }
32
40
  }
33
41
 
34
42
  function startPipeServer() {
35
- const { pipeName } = getConfig();
43
+ const { pipeName, logUsername } = getConfig();
36
44
  const server = net.createServer((socket) => {
37
45
  let buffer = '';
38
46
  socket.on('data', async (chunk) => {
@@ -43,13 +51,19 @@ function startPipeServer() {
43
51
  buffer = buffer.slice(idx + 1);
44
52
  const msg = parseJsonLine(Buffer.from(line, 'utf8'));
45
53
  if (!msg || typeof msg.password !== 'string') {
46
- socket.write(JSON.stringify({ allow: false, reason: 'bad_request' }) + '\n');
54
+ socket.write(JSON.stringify({ allow: false, reason: 'BAD_REQUEST' }) + '\n');
47
55
  continue;
48
56
  }
57
+ const user = (msg && typeof msg.username === 'string') ? msg.username : undefined;
58
+ const op = (msg && typeof msg.op === 'string') ? msg.op : 'change';
59
+
49
60
  const { allow, reason } = await evaluatePassword(msg.password);
50
61
  // Never log plaintext passwords
51
- if (reason === 'compromised') logger.info('Password evaluation: compromised');
52
- else logger.info(`Password evaluation result: ${reason}`);
62
+ if (logUsername && user) {
63
+ logger.info(`Password evaluation: ${reason}; user=${user}; op=${op}`);
64
+ } else {
65
+ logger.info(`Password evaluation: ${reason}; op=${op}`);
66
+ }
53
67
  socket.write(JSON.stringify({ allow, reason }) + '\n');
54
68
  }
55
69
  });
package/src/tester.js CHANGED
@@ -3,52 +3,122 @@ const { logger } = require('./logger');
3
3
  const service = require('./service');
4
4
  const storage = require('./storage');
5
5
  const apiClient = require('./apiClient');
6
+ const { getConfig } = require('./config');
7
+ const net = require('net');
8
+ const { runPS } = require('./ps');
6
9
 
7
- async function run({ user }) {
10
+ async function run({ user, mode = 'auto', verbose = false }) {
8
11
  console.log('Running LoginGuards test suite...\n');
9
12
 
10
- // Active Directory connectivity (placeholder)
11
- console.log('✔ Connected to Active Directory (placeholder)');
12
- logger.info('AD connectivity check: placeholder');
13
+ const isDc = await isDomainController().catch(() => false);
14
+ const execMode = mode === 'auto' ? (isDc ? 'dc' : 'client') : mode;
13
15
 
14
- // Service status
16
+ // Service status (common)
15
17
  const running = await service.isRunning().catch(() => false);
16
- if (running) {
17
- console.log('✔ Password policy engine service running');
18
- } else {
19
- console.log('✖ Password policy engine service not running');
20
- }
18
+ console.log(running ? '✔ Password policy engine service running' : '✖ Password policy engine service not running');
21
19
 
22
- // API connectivity
20
+ // API connectivity (common)
23
21
  const apiKey = await storage.getApiKey();
24
22
  if (!apiKey) {
25
23
  console.log('✖ API key not configured. Run "loginguards-win configure".');
26
24
  return;
27
25
  }
28
- try {
29
- await apiClient.ping(apiKey);
30
- console.log('✔ LoginGuards API reachable');
31
- } catch (e) {
32
- console.log('✖ LoginGuards API not reachable');
26
+ try { await apiClient.ping(apiKey); console.log('✔ LoginGuards API reachable'); } catch { console.log('✖ LoginGuards API not reachable'); }
27
+
28
+ if (execMode === 'dc') {
29
+ console.log('✔ Domain Controller detected');
30
+ // Registry check
31
+ const filterReg = await isFilterRegistered('LoginGuardsPwdFilter');
32
+ console.log(filterReg ? '✔ Password filter registered' : '✖ Password filter not registered');
33
+ // DLL presence
34
+ const dllExists = await dllPresent();
35
+ console.log(dllExists ? '✔ Password filter DLL present in System32' : '✖ Password filter DLL missing in System32');
36
+ // Pipe check
37
+ const pipeOk = await pipeHealth();
38
+ console.log(pipeOk ? '✔ Named pipe OK' : '✖ Named pipe unavailable');
39
+
40
+ // End-to-end evaluation via API (non-destructive)
41
+ const testPwd = `Lg!Test-${Math.random().toString(36).slice(2, 8)}A1`;
42
+ try {
43
+ const res = await apiClient.checkPlain(testPwd, apiKey);
44
+ const compromised = (typeof res.breached !== 'undefined') ? !!res.breached
45
+ : (typeof res.compromised !== 'undefined') ? !!res.compromised
46
+ : false;
47
+ console.log(`✔ Test password evaluated: ${compromised ? 'COMPROMISED' : 'NOT COMPROMISED'}`);
48
+ console.log(`✔ Test complete successfully for user "${user || 'DOMAIN\\test-user'}"`);
49
+ } catch (e) {
50
+ logger.warn(`Test evaluation error: ${e.message || e}`);
51
+ console.log('✖ Test evaluation failed');
52
+ }
53
+ return;
54
+ }
55
+
56
+ // client mode
57
+ const info = await getDomainInfo().catch(() => null);
58
+ if (info && info.PartOfDomain) {
59
+ console.log(`✔ Client is joined to domain: ${info.Domain || 'UNKNOWN'}`);
60
+ if (info.LogonServer) console.log(`✔ Logon server: ${info.LogonServer}`);
61
+ } else {
62
+ console.log('✖ Client is not domain-joined or unable to detect domain');
33
63
  }
64
+ console.log('ℹ Enforcement validation must be run on a Domain Controller.');
65
+ }
66
+
67
+ async function isDomainController() {
68
+ const script = `
69
+ $k = Get-Item 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\NTDS' -ErrorAction SilentlyContinue
70
+ if ($null -ne $k) { 'DC' } else { 'NOTDC' }
71
+ `;
72
+ const { stdout } = await runPS(script);
73
+ return stdout.trim() === 'DC';
74
+ }
75
+
76
+ async function isFilterRegistered(nameNoExt) {
77
+ const script = `
78
+ $path = 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa'
79
+ $name = 'Notification Packages'
80
+ $cur = (Get-ItemProperty -Path $path -Name $name -ErrorAction SilentlyContinue).$name
81
+ if ($null -eq $cur) { 'NO' } elseif ($cur -contains '${nameNoExt}') { 'YES' } else { 'NO' }
82
+ `;
83
+ const { stdout } = await runPS(script);
84
+ return stdout.trim() === 'YES';
85
+ }
86
+
87
+ async function dllPresent() {
88
+ const sys32 = process.env.WINDIR ? require('path').join(process.env.WINDIR, 'System32') : 'C\\\\Windows\\\\System32';
89
+ const dll = require('path').join(sys32, 'LoginGuardsPwdFilter.dll');
90
+ try { require('fs').accessSync(dll); return true; } catch { return false; }
91
+ }
92
+
93
+ async function pipeHealth() {
94
+ const { pipeName } = getConfig();
95
+ return new Promise((resolve) => {
96
+ const socket = net.createConnection(pipeName, () => {
97
+ // send minimal JSON and close
98
+ socket.write(JSON.stringify({ password: `Lg!Probe-${Date.now()}aA1`, op: 'change' }) + '\n');
99
+ });
100
+ let responded = false;
101
+ socket.on('data', () => { responded = true; socket.end(); });
102
+ socket.on('error', () => resolve(false));
103
+ socket.on('end', () => resolve(responded));
104
+ setTimeout(() => { try { socket.destroy(); } catch {} resolve(false); }, 1000);
105
+ });
106
+ }
34
107
 
35
- // Test password evaluation (non-destructive)
36
- const testPwd = `Lg!Test-${Math.random().toString(36).slice(2, 8)}A1`;
37
- try {
38
- const res = await apiClient.checkPlain(testPwd, apiKey);
39
- const compromised = !!res.compromised;
40
- if (compromised) {
41
- console.log('✔ Test completed');
42
- console.log('✖ Password rejected: COMPROMISED');
43
- } else {
44
- console.log('✔ Test password evaluated: NOT COMPROMISED');
108
+ async function getDomainInfo() {
109
+ const script = `
110
+ $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue
111
+ if ($null -eq $cs) { Write-Output '{}' } else {
112
+ $obj = [ordered]@{
113
+ PartOfDomain = $cs.PartOfDomain
114
+ Domain = $cs.Domain
115
+ LogonServer = $env:LOGONSERVER
45
116
  }
46
- console.log(`✔ Test completed successfully for user "${user || 'DOMAIN\\test-user'}"`);
47
- } catch (err) {
48
- logger.error(`Test request failed: ${err.stack || err.message || err}`);
49
- console.log('✖ Test request failed');
50
- throw err;
117
+ $obj | ConvertTo-Json -Compress
51
118
  }
119
+ `;
120
+ const { stdout } = await runPS(script);
121
+ try { return JSON.parse(stdout.trim()); } catch { return null; }
52
122
  }
53
123
 
54
124
  module.exports = { run };