@loginguards/loginguards-win 0.1.1
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/LICENSE +49 -0
- package/README.md +42 -0
- package/bin/loginguards-win.js +8 -0
- package/package.json +60 -0
- package/src/apiClient.js +21 -0
- package/src/cli/commands/configure.js +39 -0
- package/src/cli/commands/install.js +38 -0
- package/src/cli/commands/pipe-test.js +57 -0
- package/src/cli/commands/test.js +20 -0
- package/src/cli/commands/uninstall.js +19 -0
- package/src/cli/index.js +24 -0
- package/src/config.js +44 -0
- package/src/index.js +6 -0
- package/src/installer.js +41 -0
- package/src/logger.js +28 -0
- package/src/ps.js +27 -0
- package/src/service/daemon/loginguardspolicyengine.err.log +0 -0
- package/src/service/daemon/loginguardspolicyengine.exe +0 -0
- package/src/service/daemon/loginguardspolicyengine.exe.config +6 -0
- package/src/service/daemon/loginguardspolicyengine.out.log +1 -0
- package/src/service/daemon/loginguardspolicyengine.wrapper.log +2 -0
- package/src/service/daemon/loginguardspolicyengine.xml +31 -0
- package/src/service/engine.js +76 -0
- package/src/service.js +51 -0
- package/src/storage.js +84 -0
- package/src/tester.js +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
LoginGuards Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LoginGuards. All rights reserved.
|
|
4
|
+
|
|
5
|
+
IMPORTANT: PLEASE READ THIS LICENSE CAREFULLY BEFORE INSTALLING OR USING THE SOFTWARE.
|
|
6
|
+
|
|
7
|
+
1. Definitions
|
|
8
|
+
- "Software" means the LoginGuards Active Directory Password Protection plugin for Windows, including all source code, binaries, scripts, documentation, and updates provided by LoginGuards.
|
|
9
|
+
- "You" or "Licensee" means the entity or individual that installs or uses the Software.
|
|
10
|
+
|
|
11
|
+
2. Grant of License
|
|
12
|
+
Subject to the terms of this License, LoginGuards grants You a limited, non-exclusive, non-transferable, revocable license to install and use the Software within Your Windows Active Directory environment solely for Your internal business purposes.
|
|
13
|
+
|
|
14
|
+
3. Restrictions
|
|
15
|
+
You shall not, and shall not permit any third party to:
|
|
16
|
+
- copy, publish, distribute, or make the Software available to any third party (including by making it publicly available), except as expressly permitted by LoginGuards in writing;
|
|
17
|
+
- sublicense, rent, lease, or otherwise transfer rights to the Software;
|
|
18
|
+
- modify, adapt, translate, or create derivative works of the Software, except to the extent expressly permitted by applicable law notwithstanding this restriction;
|
|
19
|
+
- reverse engineer, decompile, disassemble, or otherwise attempt to derive source code from any non-source component of the Software, except to the extent expressly permitted by applicable law notwithstanding this restriction;
|
|
20
|
+
- remove, alter, or obscure any proprietary notices (including copyright and trademark notices) of LoginGuards or its suppliers.
|
|
21
|
+
|
|
22
|
+
4. Ownership
|
|
23
|
+
The Software is licensed, not sold. LoginGuards and its licensors retain all right, title, and interest in and to the Software and all intellectual property rights therein.
|
|
24
|
+
|
|
25
|
+
5. Confidentiality; Security
|
|
26
|
+
The Software may process sensitive information (e.g., user passwords) in memory for validation purposes. You agree not to log, store, or otherwise persist plaintext passwords and to operate the Software in accordance with security best practices. Configuration secrets (e.g., API keys) must be stored securely. The Software may integrate with third-party services (e.g., LoginGuards API) as configured by You.
|
|
27
|
+
|
|
28
|
+
6. Support and Updates
|
|
29
|
+
This License does not obligate LoginGuards to provide support, maintenance, or updates unless otherwise agreed in a separate written agreement.
|
|
30
|
+
|
|
31
|
+
7. Disclaimer of Warranties
|
|
32
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. LOGINGUARDS DOES NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR UNINTERRUPTED.
|
|
33
|
+
|
|
34
|
+
8. Limitation of Liability
|
|
35
|
+
TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL LOGINGUARDS OR ITS LICENSORS BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUE, ARISING OUT OF OR IN CONNECTION WITH THIS LICENSE OR THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. LOGINGUARDS' TOTAL LIABILITY FOR ALL CLAIMS SHALL NOT EXCEED THE AMOUNTS ACTUALLY PAID BY YOU (IF ANY) FOR THE SOFTWARE IN THE TWELVE (12) MONTHS PRECEDING THE EVENT GIVING RISE TO THE CLAIM.
|
|
36
|
+
|
|
37
|
+
9. Term and Termination
|
|
38
|
+
This License is effective until terminated. LoginGuards may terminate this License immediately upon notice if You breach any term herein. Upon termination, You must cease all use of the Software and destroy all copies in Your possession or control.
|
|
39
|
+
|
|
40
|
+
10. Export Compliance
|
|
41
|
+
You represent that You are not located in, under the control of, or a national or resident of any country subject to embargo or sanctions and that You will not export or re-export the Software in violation of applicable export laws and regulations.
|
|
42
|
+
|
|
43
|
+
11. Governing Law
|
|
44
|
+
This License shall be governed by and construed in accordance with the laws of the State of California, without regard to its conflicts of law principles, and the federal courts located in Santa Clara County, California shall have exclusive jurisdiction.
|
|
45
|
+
|
|
46
|
+
12. Entire Agreement
|
|
47
|
+
This License constitutes the entire agreement between the parties with respect to the Software and supersedes all prior or contemporaneous understandings.
|
|
48
|
+
|
|
49
|
+
If You do not agree to these terms, do not install or use the Software.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# LoginGuards Active Directory Password Protection (Windows)
|
|
2
|
+
|
|
3
|
+
Enterprise-grade password breach prevention for Windows domains.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Zero Trust password validation via LoginGuards API
|
|
8
|
+
- No password storage; passwords never logged
|
|
9
|
+
- Windows service policy engine (node-windows)
|
|
10
|
+
- Secure API key storage (Windows Credential Manager via keytar)
|
|
11
|
+
- CLI: `install`, `configure`, `test`, `uninstall`
|
|
12
|
+
|
|
13
|
+
## Install (development)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g .
|
|
17
|
+
loginguards-win configure
|
|
18
|
+
loginguards-win install
|
|
19
|
+
loginguards-win test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
- API base: `https://api.loginguards.com/v1`
|
|
25
|
+
- Required header: `x-api-key: <LOGIN_GUARDS_API_KEY>`
|
|
26
|
+
- Behavior on API failure is configurable (planned): fail-open or fail-closed
|
|
27
|
+
|
|
28
|
+
## Security
|
|
29
|
+
|
|
30
|
+
- Never logs plaintext passwords
|
|
31
|
+
- API key stored in Windows Credential Manager
|
|
32
|
+
- HTTPS only
|
|
33
|
+
|
|
34
|
+
## Active Directory Integration (planned)
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
## Uninstall
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
loginguards-win uninstall
|
|
42
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@loginguards/loginguards-win",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "LoginGuards Active Directory Password Protection for Windows",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"bin": {
|
|
8
|
+
"loginguards-win": "bin/loginguards-win.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "src/index.js",
|
|
11
|
+
"type": "commonjs",
|
|
12
|
+
"os": [
|
|
13
|
+
"win32"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"bin",
|
|
23
|
+
"src",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"active-directory",
|
|
29
|
+
"password",
|
|
30
|
+
"security",
|
|
31
|
+
"windows",
|
|
32
|
+
"login",
|
|
33
|
+
"policy",
|
|
34
|
+
"breach",
|
|
35
|
+
"zerotrust"
|
|
36
|
+
],
|
|
37
|
+
"author": "LoginGuards",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/Kirmina-INC/loginguards-win.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/Kirmina-INC/loginguards-win/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/Kirmina-INC/loginguards-win#readme",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"axios": "^1.6.7",
|
|
48
|
+
"inquirer": "^9.2.12",
|
|
49
|
+
"node-windows": "^1.0.0-beta.6",
|
|
50
|
+
"winston": "^3.10.0",
|
|
51
|
+
"yargs": "^17.7.2"
|
|
52
|
+
},
|
|
53
|
+
"optionalDependencies": {
|
|
54
|
+
"keytar": "^7.9.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"start": "node bin/loginguards-win.js"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/apiClient.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const axios = require('axios').default;
|
|
3
|
+
const BASE_URL = 'https://api.loginguards.com/v1';
|
|
4
|
+
|
|
5
|
+
const client = axios.create({
|
|
6
|
+
baseURL: BASE_URL,
|
|
7
|
+
timeout: 10000
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
async function checkPlain(password, apiKey) {
|
|
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 } });
|
|
13
|
+
return resp.data;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function ping(apiKey) {
|
|
17
|
+
await client.head('/', { headers: apiKey ? { 'x-api-key': apiKey } : undefined }).catch(() => {});
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { checkPlain, ping, BASE_URL };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const storage = require('../../storage');
|
|
3
|
+
const apiClient = require('../../apiClient');
|
|
4
|
+
const { logger } = require('../../logger');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
command: 'configure',
|
|
8
|
+
describe: 'Configure LoginGuards API key and connectivity',
|
|
9
|
+
builder: {},
|
|
10
|
+
handler: async () => {
|
|
11
|
+
try {
|
|
12
|
+
// Inquirer v9 is ESM-only; load dynamically
|
|
13
|
+
const inquirer = (await import('inquirer')).default;
|
|
14
|
+
const answers = await inquirer.prompt([
|
|
15
|
+
{
|
|
16
|
+
type: 'password',
|
|
17
|
+
name: 'apiKey',
|
|
18
|
+
message: 'Enter LoginGuards API key',
|
|
19
|
+
mask: '*',
|
|
20
|
+
validate: v => v && v.trim().length > 0 ? true : 'API key required'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: 'input',
|
|
24
|
+
name: 'org',
|
|
25
|
+
message: 'Organization/Project identifier (optional)'
|
|
26
|
+
}
|
|
27
|
+
]);
|
|
28
|
+
await storage.saveApiKey(answers.apiKey);
|
|
29
|
+
logger.info('Saved API key in secure storage');
|
|
30
|
+
// Best-effort reachability test (no password)
|
|
31
|
+
await apiClient.ping(answers.apiKey).catch(() => {});
|
|
32
|
+
console.log('✔ API key stored securely');
|
|
33
|
+
} catch (err) {
|
|
34
|
+
logger.error(`Configure failed: ${err.stack || err.message || err}`);
|
|
35
|
+
console.error('✖ Configure failed:', err.message || err);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { logger } = require('../../logger');
|
|
3
|
+
const installer = require('../../installer');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
command: 'install',
|
|
7
|
+
describe: 'Install and activate the LoginGuards AD password protection plugin',
|
|
8
|
+
builder: {
|
|
9
|
+
failMode: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
choices: ['fail-open', 'fail-closed'],
|
|
12
|
+
default: 'fail-closed',
|
|
13
|
+
describe: 'Behavior if API unreachable'
|
|
14
|
+
},
|
|
15
|
+
pipeName: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
default: "\\\\.\\pipe\\LoginGuardsPwdFilter",
|
|
18
|
+
describe: 'Named pipe for the password filter to connect to'
|
|
19
|
+
},
|
|
20
|
+
logUsername: {
|
|
21
|
+
type: 'boolean',
|
|
22
|
+
default: false,
|
|
23
|
+
describe: 'If true, log the username in audit logs (never logs passwords)'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
handler: async (args) => {
|
|
27
|
+
logger.info('Starting installation...');
|
|
28
|
+
try {
|
|
29
|
+
await installer.install({ failMode: args.failMode, pipeName: args.pipeName, logUsername: args.logUsername });
|
|
30
|
+
logger.info('Installation completed.');
|
|
31
|
+
console.log('✔ Installation completed');
|
|
32
|
+
} catch (err) {
|
|
33
|
+
logger.error(`Installation failed: ${err.stack || err.message || err}`);
|
|
34
|
+
console.error('✖ Installation failed:', err.message || err);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const net = require('net');
|
|
3
|
+
const { getConfig } = require('../../config');
|
|
4
|
+
const { logger } = require('../../logger');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
command: 'pipe-test',
|
|
8
|
+
describe: 'Send a test password to the service via named pipe IPC',
|
|
9
|
+
builder: {
|
|
10
|
+
password: { type: 'string', describe: 'Test password to evaluate (will not be logged)' },
|
|
11
|
+
username: { type: 'string', describe: 'Optional username for audit (not required)' }
|
|
12
|
+
},
|
|
13
|
+
handler: async (args) => {
|
|
14
|
+
const { pipeName } = getConfig();
|
|
15
|
+
const pwd = args.password || `Lg!PipeTest-${Math.random().toString(36).slice(2, 8)}A1`;
|
|
16
|
+
|
|
17
|
+
console.log(`Connecting to named pipe: ${pipeName}`);
|
|
18
|
+
|
|
19
|
+
await new Promise((resolve, reject) => {
|
|
20
|
+
const socket = net.createConnection(pipeName, () => {
|
|
21
|
+
const msg = { password: pwd };
|
|
22
|
+
// Never log plaintext
|
|
23
|
+
socket.write(JSON.stringify(msg) + '\n');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let buffer = '';
|
|
27
|
+
socket.on('data', (chunk) => {
|
|
28
|
+
buffer += chunk.toString('utf8');
|
|
29
|
+
let idx;
|
|
30
|
+
while ((idx = buffer.indexOf('\n')) !== -1) {
|
|
31
|
+
const line = buffer.slice(0, idx);
|
|
32
|
+
buffer = buffer.slice(idx + 1);
|
|
33
|
+
try {
|
|
34
|
+
const resp = JSON.parse(line);
|
|
35
|
+
const allow = !!resp.allow;
|
|
36
|
+
const reason = resp.reason || (allow ? 'ok' : 'rejected');
|
|
37
|
+
if (allow) console.log('✔ Pipe test password evaluated: NOT COMPROMISED');
|
|
38
|
+
else console.log('✖ Password rejected:', reason.toUpperCase());
|
|
39
|
+
resolve();
|
|
40
|
+
socket.end();
|
|
41
|
+
} catch (e) {
|
|
42
|
+
logger.warn('Invalid JSON from pipe');
|
|
43
|
+
reject(new Error('Invalid response from service'));
|
|
44
|
+
socket.destroy();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
socket.on('error', (err) => {
|
|
49
|
+
reject(new Error(`Pipe connection error: ${err.message || err}`));
|
|
50
|
+
});
|
|
51
|
+
socket.on('end', () => {});
|
|
52
|
+
}).catch((e) => {
|
|
53
|
+
console.error('✖ Pipe test failed:', e.message || e);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { logger } = require('../../logger');
|
|
3
|
+
const tester = require('../../tester');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
command: 'test',
|
|
7
|
+
describe: 'Run end-to-end validation across AD and LoginGuards',
|
|
8
|
+
builder: {
|
|
9
|
+
user: { type: 'string', describe: 'Domain user to simulate', default: process.env.USERNAME ? `${process.env.USERDOMAIN || 'DOMAIN'}\\${process.env.USERNAME}` : undefined }
|
|
10
|
+
},
|
|
11
|
+
handler: async (args) => {
|
|
12
|
+
try {
|
|
13
|
+
await tester.run(args);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
logger.error(`Test failed: ${err.stack || err.message || err}`);
|
|
16
|
+
console.error('✖ Test failed:', err.message || err);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { logger } = require('../../logger');
|
|
3
|
+
const installer = require('../../installer');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
command: 'uninstall',
|
|
7
|
+
describe: 'Uninstall the plugin and remove all components',
|
|
8
|
+
builder: {},
|
|
9
|
+
handler: async () => {
|
|
10
|
+
try {
|
|
11
|
+
await installer.uninstall();
|
|
12
|
+
console.log('✔ Uninstall completed');
|
|
13
|
+
} catch (err) {
|
|
14
|
+
logger.error(`Uninstall failed: ${err.stack || err.message || err}`);
|
|
15
|
+
console.error('✖ Uninstall failed:', err.message || err);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const yargs = require('yargs/yargs');
|
|
3
|
+
const { hideBin } = require('yargs/helpers');
|
|
4
|
+
|
|
5
|
+
async function run(argvInput) {
|
|
6
|
+
const raw = hideBin(argvInput || process.argv);
|
|
7
|
+
// pnpm passes a literal "--" before forwarded args; strip it so yargs sees the command
|
|
8
|
+
if (raw.length && raw[0] === '--') raw.shift();
|
|
9
|
+
const argv = yargs(raw)
|
|
10
|
+
.scriptName('loginguards-win')
|
|
11
|
+
.usage('$0 <cmd> [args]')
|
|
12
|
+
.command(require('./commands/install'))
|
|
13
|
+
.command(require('./commands/configure'))
|
|
14
|
+
.command(require('./commands/test'))
|
|
15
|
+
.command(require('./commands/pipe-test'))
|
|
16
|
+
.command(require('./commands/uninstall'))
|
|
17
|
+
.demandCommand(1, 'Please provide a command')
|
|
18
|
+
.strict()
|
|
19
|
+
.help()
|
|
20
|
+
.argv;
|
|
21
|
+
return argv;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { run };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
const baseDir = process.env.PROGRAMDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
7
|
+
const confDir = path.join(baseDir, 'LoginGuards');
|
|
8
|
+
const confFile = path.join(confDir, 'config.json');
|
|
9
|
+
|
|
10
|
+
function ensureDir() {
|
|
11
|
+
if (!fs.existsSync(confDir)) fs.mkdirSync(confDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readConfigFile() {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(confFile, 'utf8')) || {};
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeConfigFile(obj) {
|
|
23
|
+
ensureDir();
|
|
24
|
+
fs.writeFileSync(confFile, JSON.stringify(obj, null, 2), { encoding: 'utf8' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getConfig() {
|
|
28
|
+
const data = readConfigFile();
|
|
29
|
+
return {
|
|
30
|
+
failMode: data.failMode || 'fail-closed',
|
|
31
|
+
pipeName: data.pipeName || "\\\\.\\pipe\\LoginGuardsPwdFilter",
|
|
32
|
+
logUsername: !!data.logUsername,
|
|
33
|
+
apiKeyEnc: data.apiKeyEnc || undefined
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setConfig(partial) {
|
|
38
|
+
const current = readConfigFile();
|
|
39
|
+
const next = { ...current, ...partial };
|
|
40
|
+
writeConfigFile(next);
|
|
41
|
+
return next;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { getConfig, setConfig, confFile };
|
package/src/index.js
ADDED
package/src/installer.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { logger } = require('./logger');
|
|
3
|
+
const service = require('./service');
|
|
4
|
+
const storage = require('./storage');
|
|
5
|
+
const { runPS } = require('./ps');
|
|
6
|
+
const { setConfig } = require('./config');
|
|
7
|
+
|
|
8
|
+
function assertWindows() {
|
|
9
|
+
if (process.platform !== 'win32') throw new Error('Windows only');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function checkAdmin() {
|
|
13
|
+
const script = `
|
|
14
|
+
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
15
|
+
$p = New-Object Security.Principal.WindowsPrincipal $id
|
|
16
|
+
if ($p.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) { Write-Output 'Admin' } else { Write-Output 'NonAdmin' }
|
|
17
|
+
`;
|
|
18
|
+
const { stdout } = await runPS(script);
|
|
19
|
+
const isAdmin = stdout.trim() === 'Admin';
|
|
20
|
+
if (!isAdmin) throw new Error('Administrator privileges required. Please run in an elevated PowerShell.');
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function install(options) {
|
|
25
|
+
assertWindows();
|
|
26
|
+
await checkAdmin();
|
|
27
|
+
// Persist configuration for the service
|
|
28
|
+
setConfig({ failMode: options.failMode, pipeName: options.pipeName, logUsername: !!options.logUsername });
|
|
29
|
+
logger.info('Installing Windows service...');
|
|
30
|
+
await service.install();
|
|
31
|
+
logger.info('Note: AD Password Filter registration not yet implemented in this preview.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function uninstall() {
|
|
35
|
+
assertWindows();
|
|
36
|
+
logger.info('Uninstalling Windows service...');
|
|
37
|
+
await service.uninstall();
|
|
38
|
+
await storage.deleteApiKey();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { install, uninstall };
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const winston = require('winston');
|
|
6
|
+
|
|
7
|
+
const baseDir = process.env.PROGRAMDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
8
|
+
const logDir = path.join(baseDir, 'LoginGuards', 'logs');
|
|
9
|
+
if (!fs.existsSync(logDir)) {
|
|
10
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const logger = winston.createLogger({
|
|
14
|
+
level: 'info',
|
|
15
|
+
format: winston.format.combine(
|
|
16
|
+
winston.format.timestamp(),
|
|
17
|
+
winston.format.json()
|
|
18
|
+
),
|
|
19
|
+
transports: [
|
|
20
|
+
new winston.transports.File({ filename: path.join(logDir, 'agent.log'), maxsize: 5 * 1024 * 1024, maxFiles: 3 })
|
|
21
|
+
]
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
25
|
+
logger.add(new winston.transports.Console({ format: winston.format.simple() }));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { logger, logDir };
|
package/src/ps.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
|
|
4
|
+
function runPS(script) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const ps = spawn(process.env.ComSpec ? 'powershell.exe' : 'powershell', [
|
|
7
|
+
'-NoProfile',
|
|
8
|
+
'-NonInteractive',
|
|
9
|
+
'-ExecutionPolicy', 'Bypass',
|
|
10
|
+
'-Command', '-'
|
|
11
|
+
], { windowsHide: true });
|
|
12
|
+
|
|
13
|
+
let stdout = '';
|
|
14
|
+
let stderr = '';
|
|
15
|
+
ps.stdout.on('data', d => { stdout += d.toString(); });
|
|
16
|
+
ps.stderr.on('data', d => { stderr += d.toString(); });
|
|
17
|
+
ps.on('error', reject);
|
|
18
|
+
ps.on('close', (code) => {
|
|
19
|
+
if (code !== 0 && stderr) return reject(new Error(stderr.trim() || `PowerShell exited with code ${code}`));
|
|
20
|
+
resolve({ stdout, stderr, exitCode: code });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
ps.stdin.end(script);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { runPS };
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
info: LoginGuards Policy Engine service started. {"timestamp":"2026-01-08T02:34:56.398Z"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|
+
2026-01-08 04:34:56 - Started 15000
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<service>
|
|
2
|
+
<id>loginguardspolicyengine.exe</id>
|
|
3
|
+
<name>LoginGuardsPolicyEngine</name>
|
|
4
|
+
<description>LoginGuards password policy decision engine</description>
|
|
5
|
+
<executable>C:\Users\Samer\AppData\Local\Volta\tools\image\node\24.3.0\node.exe</executable>
|
|
6
|
+
<argument>--harmony</argument>
|
|
7
|
+
<argument>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</argument>
|
|
8
|
+
<argument>--file</argument>
|
|
9
|
+
<argument>D:\projects\backend\fast-Backend (1)\New 0\src\service\engine.js</argument>
|
|
10
|
+
<argument>--scriptoptions=</argument>
|
|
11
|
+
<argument>--log</argument>
|
|
12
|
+
<argument>LoginGuardsPolicyEngine wrapper</argument>
|
|
13
|
+
<argument>--grow</argument>
|
|
14
|
+
<argument>0.5</argument>
|
|
15
|
+
<argument>--wait</argument>
|
|
16
|
+
<argument>2</argument>
|
|
17
|
+
<argument>--maxrestarts</argument>
|
|
18
|
+
<argument>3</argument>
|
|
19
|
+
<argument>--abortonerror</argument>
|
|
20
|
+
<argument>n</argument>
|
|
21
|
+
<argument>--stopparentfirst</argument>
|
|
22
|
+
<argument>undefined</argument>
|
|
23
|
+
<logmode>rotate</logmode>
|
|
24
|
+
<stoptimeout>30sec</stoptimeout>
|
|
25
|
+
<serviceaccount>
|
|
26
|
+
<domain>TP-LINK</domain>
|
|
27
|
+
<user>LocalSystem</user>
|
|
28
|
+
<password></password>
|
|
29
|
+
</serviceaccount>
|
|
30
|
+
<workingdirectory>D:\projects\backend\fast-Backend (1)\New 0</workingdirectory>
|
|
31
|
+
</service>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { logger } = require('../logger');
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const { getConfig } = require('../config');
|
|
5
|
+
const storage = require('../storage');
|
|
6
|
+
const apiClient = require('../apiClient');
|
|
7
|
+
|
|
8
|
+
logger.info('LoginGuards Policy Engine service started.');
|
|
9
|
+
|
|
10
|
+
let shuttingDown = false;
|
|
11
|
+
|
|
12
|
+
function parseJsonLine(buf) {
|
|
13
|
+
const s = buf.toString('utf8').trim();
|
|
14
|
+
if (!s) return null;
|
|
15
|
+
try { return JSON.parse(s); } catch { return null; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function evaluatePassword(password) {
|
|
19
|
+
const { failMode } = getConfig();
|
|
20
|
+
const apiKey = await storage.getApiKey();
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
return { allow: failMode === 'fail-open', reason: 'no_api_key' };
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const res = await apiClient.checkPlain(password, apiKey);
|
|
26
|
+
const compromised = !!res.compromised;
|
|
27
|
+
return { allow: !compromised, reason: compromised ? 'compromised' : 'ok' };
|
|
28
|
+
} catch (e) {
|
|
29
|
+
logger.warn(`API error during evaluation: ${e.status || e.code || e.message}`);
|
|
30
|
+
return { allow: failMode === 'fail-open', reason: 'api_error' };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function startPipeServer() {
|
|
35
|
+
const { pipeName } = getConfig();
|
|
36
|
+
const server = net.createServer((socket) => {
|
|
37
|
+
let buffer = '';
|
|
38
|
+
socket.on('data', async (chunk) => {
|
|
39
|
+
buffer += chunk.toString('utf8');
|
|
40
|
+
let idx;
|
|
41
|
+
while ((idx = buffer.indexOf('\n')) !== -1) {
|
|
42
|
+
const line = buffer.slice(0, idx);
|
|
43
|
+
buffer = buffer.slice(idx + 1);
|
|
44
|
+
const msg = parseJsonLine(Buffer.from(line, 'utf8'));
|
|
45
|
+
if (!msg || typeof msg.password !== 'string') {
|
|
46
|
+
socket.write(JSON.stringify({ allow: false, reason: 'bad_request' }) + '\n');
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const { allow, reason } = await evaluatePassword(msg.password);
|
|
50
|
+
// Never log plaintext passwords
|
|
51
|
+
if (reason === 'compromised') logger.info('Password evaluation: compromised');
|
|
52
|
+
else logger.info(`Password evaluation result: ${reason}`);
|
|
53
|
+
socket.write(JSON.stringify({ allow, reason }) + '\n');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
socket.on('error', () => {});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
server.on('error', (err) => {
|
|
60
|
+
logger.error(`Pipe server error: ${err.message || err}`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
server.listen(pipeName, () => {
|
|
64
|
+
logger.info(`Named pipe server listening on ${pipeName}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return server;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const server = startPipeServer();
|
|
71
|
+
|
|
72
|
+
process.on('SIGINT', () => { shuttingDown = true; server && server.close(); process.exit(0); });
|
|
73
|
+
process.on('SIGTERM', () => { shuttingDown = true; server && server.close(); process.exit(0); });
|
|
74
|
+
|
|
75
|
+
// keep process alive
|
|
76
|
+
setInterval(() => { if (shuttingDown) clearInterval(this); }, 1 << 30);
|
package/src/service.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { Service } = require('node-windows');
|
|
4
|
+
const { runPS } = require('./ps');
|
|
5
|
+
|
|
6
|
+
const serviceName = 'LoginGuardsPolicyEngine';
|
|
7
|
+
const scriptPath = path.join(__dirname, 'service', 'engine.js');
|
|
8
|
+
|
|
9
|
+
function makeService() {
|
|
10
|
+
return new Service({
|
|
11
|
+
name: serviceName,
|
|
12
|
+
description: 'LoginGuards password policy decision engine',
|
|
13
|
+
script: scriptPath,
|
|
14
|
+
wait: 2,
|
|
15
|
+
grow: 0.5
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function install() {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const svc = makeService();
|
|
22
|
+
svc.on('install', () => svc.start());
|
|
23
|
+
svc.on('alreadyinstalled', () => resolve());
|
|
24
|
+
svc.on('start', () => resolve());
|
|
25
|
+
svc.on('error', reject);
|
|
26
|
+
svc.install();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function uninstall() {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const svc = makeService();
|
|
33
|
+
svc.on('uninstall', resolve);
|
|
34
|
+
svc.on('error', reject);
|
|
35
|
+
svc.uninstall();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function isRunning() {
|
|
40
|
+
const script = `
|
|
41
|
+
try {
|
|
42
|
+
$s = Get-Service -Name '${serviceName}' -ErrorAction Stop
|
|
43
|
+
if ($s.Status -eq 'Running') { Write-Output 'Running' } else { Write-Output 'NotRunning' }
|
|
44
|
+
} catch { Write-Output 'NotInstalled' }
|
|
45
|
+
`;
|
|
46
|
+
const { stdout } = await runPS(script);
|
|
47
|
+
const out = stdout.trim();
|
|
48
|
+
return out === 'Running';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { install, uninstall, isRunning, serviceName, scriptPath };
|
package/src/storage.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { runPS } = require('./ps');
|
|
6
|
+
|
|
7
|
+
const SERVICE = 'LoginGuards';
|
|
8
|
+
const ACCOUNT = 'api-key';
|
|
9
|
+
|
|
10
|
+
let keytar = null;
|
|
11
|
+
try {
|
|
12
|
+
// optional dependency
|
|
13
|
+
keytar = require('keytar');
|
|
14
|
+
} catch {}
|
|
15
|
+
|
|
16
|
+
const baseDir = process.env.PROGRAMDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
17
|
+
const confDir = path.join(baseDir, 'LoginGuards');
|
|
18
|
+
const confFile = path.join(confDir, 'config.json');
|
|
19
|
+
|
|
20
|
+
function ensureDir() {
|
|
21
|
+
if (!fs.existsSync(confDir)) fs.mkdirSync(confDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function saveApiKey(apiKey) {
|
|
25
|
+
// Always persist a LocalMachine-encrypted copy for the service
|
|
26
|
+
ensureDir();
|
|
27
|
+
const enc = await dpapiProtect(apiKey);
|
|
28
|
+
let data = {};
|
|
29
|
+
try { data = JSON.parse(fs.readFileSync(confFile, 'utf8')); } catch {}
|
|
30
|
+
data.apiKeyEnc = enc;
|
|
31
|
+
fs.writeFileSync(confFile, JSON.stringify(data, null, 2), { encoding: 'utf8' });
|
|
32
|
+
// Additionally store in Keytar (user context) when available
|
|
33
|
+
if (keytar) {
|
|
34
|
+
try { await keytar.setPassword(SERVICE, ACCOUNT, apiKey); } catch {}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getApiKey() {
|
|
39
|
+
// Prefer LocalMachine file so the service can read it
|
|
40
|
+
try {
|
|
41
|
+
const raw = fs.readFileSync(confFile, 'utf8');
|
|
42
|
+
const data = JSON.parse(raw);
|
|
43
|
+
if (data && data.apiKeyEnc) {
|
|
44
|
+
return await dpapiUnprotect(data.apiKeyEnc);
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
if (keytar) {
|
|
48
|
+
try { return await keytar.getPassword(SERVICE, ACCOUNT); } catch {}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function deleteApiKey() {
|
|
54
|
+
if (keytar) {
|
|
55
|
+
try { await keytar.deletePassword(SERVICE, ACCOUNT); } catch {}
|
|
56
|
+
}
|
|
57
|
+
try { fs.unlinkSync(confFile); } catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function dpapiProtect(plain) {
|
|
61
|
+
const script = `
|
|
62
|
+
$plain = @'
|
|
63
|
+
${plain.replace(/'/g, "''")}
|
|
64
|
+
'@
|
|
65
|
+
$bytes = [System.Text.Encoding]::UTF8.GetBytes($plain)
|
|
66
|
+
$enc = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
|
|
67
|
+
[Convert]::ToBase64String($enc)
|
|
68
|
+
`;
|
|
69
|
+
const { stdout } = await runPS(script);
|
|
70
|
+
return stdout.trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function dpapiUnprotect(b64) {
|
|
74
|
+
const script = `
|
|
75
|
+
$b64 = '${b64.replace(/'/g, "''")}'
|
|
76
|
+
$bytes = [Convert]::FromBase64String($b64)
|
|
77
|
+
$dec = [System.Security.Cryptography.ProtectedData]::Unprotect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
|
|
78
|
+
[System.Text.Encoding]::UTF8.GetString($dec)
|
|
79
|
+
`;
|
|
80
|
+
const { stdout } = await runPS(script);
|
|
81
|
+
return stdout.replace(/\r?\n$/, '')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { saveApiKey, getApiKey, deleteApiKey };
|
package/src/tester.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { logger } = require('./logger');
|
|
3
|
+
const service = require('./service');
|
|
4
|
+
const storage = require('./storage');
|
|
5
|
+
const apiClient = require('./apiClient');
|
|
6
|
+
|
|
7
|
+
async function run({ user }) {
|
|
8
|
+
console.log('Running LoginGuards test suite...\n');
|
|
9
|
+
|
|
10
|
+
// Active Directory connectivity (placeholder)
|
|
11
|
+
console.log('✔ Connected to Active Directory (placeholder)');
|
|
12
|
+
logger.info('AD connectivity check: placeholder');
|
|
13
|
+
|
|
14
|
+
// Service status
|
|
15
|
+
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
|
+
}
|
|
21
|
+
|
|
22
|
+
// API connectivity
|
|
23
|
+
const apiKey = await storage.getApiKey();
|
|
24
|
+
if (!apiKey) {
|
|
25
|
+
console.log('✖ API key not configured. Run "loginguards-win configure".');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
await apiClient.ping(apiKey);
|
|
30
|
+
console.log('✔ LoginGuards API reachable');
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.log('✖ LoginGuards API not reachable');
|
|
33
|
+
}
|
|
34
|
+
|
|
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');
|
|
45
|
+
}
|
|
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;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { run };
|