@loginguards/loginguards-win 0.1.3 → 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 +59 -5
- package/assets/LoginGuardsPwdFilter/x64/README.txt +3 -0
- package/package.json +2 -1
- package/src/apiClient.js +3 -2
- package/src/cli/commands/install.js +25 -3
- package/src/cli/commands/test.js +3 -1
- package/src/cli/commands/uninstall.js +5 -3
- package/src/config.js +2 -1
- package/src/installer.js +85 -3
- package/src/service/daemon/loginguardspolicyengine.out.log +6 -0
- package/src/service/daemon/loginguardspolicyengine.wrapper.log +16 -0
- package/src/service/engine.js +19 -10
- package/src/tester.js +101 -31
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: `
|
|
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
|
|
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 (
|
|
38
|
+
## Active Directory Integration (V2)
|
|
37
39
|
|
|
38
|
-
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loginguards/loginguards-win",
|
|
3
|
-
"version": "0.
|
|
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
|
|
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
|
|
|
@@ -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-
|
|
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
|
-
|
|
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) {
|
package/src/cli/commands/test.js
CHANGED
|
@@ -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
|
-
|
|
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/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-
|
|
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
|
-
|
|
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
|
package/src/service/engine.js
CHANGED
|
@@ -16,28 +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: '
|
|
22
|
+
return { allow: failMode === 'fail-open', reason: 'NO_API_KEY' };
|
|
23
23
|
}
|
|
24
24
|
try {
|
|
25
|
-
const res = await apiClient.checkPlain(password, apiKey);
|
|
25
|
+
const res = await apiClient.checkPlain(password, apiKey, { timeoutMs });
|
|
26
26
|
const compromised = (typeof res.breached !== 'undefined') ? !!res.breached
|
|
27
27
|
: (typeof res.compromised !== 'undefined') ? !!res.compromised
|
|
28
28
|
: (typeof res.is_compromised !== 'undefined') ? !!res.is_compromised
|
|
29
29
|
: (typeof res.isCompromised !== 'undefined') ? !!res.isCompromised
|
|
30
30
|
: (typeof res.count === 'number') ? res.count > 0
|
|
31
31
|
: false;
|
|
32
|
-
return { allow: !compromised, reason: compromised ? '
|
|
32
|
+
return { allow: !compromised, reason: compromised ? 'COMPROMISED' : 'SAFE' };
|
|
33
33
|
} catch (e) {
|
|
34
|
-
|
|
35
|
-
|
|
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 };
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
function startPipeServer() {
|
|
40
|
-
const { pipeName } = getConfig();
|
|
43
|
+
const { pipeName, logUsername } = getConfig();
|
|
41
44
|
const server = net.createServer((socket) => {
|
|
42
45
|
let buffer = '';
|
|
43
46
|
socket.on('data', async (chunk) => {
|
|
@@ -48,13 +51,19 @@ function startPipeServer() {
|
|
|
48
51
|
buffer = buffer.slice(idx + 1);
|
|
49
52
|
const msg = parseJsonLine(Buffer.from(line, 'utf8'));
|
|
50
53
|
if (!msg || typeof msg.password !== 'string') {
|
|
51
|
-
socket.write(JSON.stringify({ allow: false, reason: '
|
|
54
|
+
socket.write(JSON.stringify({ allow: false, reason: 'BAD_REQUEST' }) + '\n');
|
|
52
55
|
continue;
|
|
53
56
|
}
|
|
57
|
+
const user = (msg && typeof msg.username === 'string') ? msg.username : undefined;
|
|
58
|
+
const op = (msg && typeof msg.op === 'string') ? msg.op : 'change';
|
|
59
|
+
|
|
54
60
|
const { allow, reason } = await evaluatePassword(msg.password);
|
|
55
61
|
// Never log plaintext passwords
|
|
56
|
-
if (
|
|
57
|
-
|
|
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
|
+
}
|
|
58
67
|
socket.write(JSON.stringify({ allow, reason }) + '\n');
|
|
59
68
|
}
|
|
60
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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 };
|