@fentz26/envcp 1.1.0 → 1.2.0-exp.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/README.md +59 -422
- package/VERSION +1 -0
- package/dist/adapters/base.d.ts +13 -0
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +153 -9
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +7 -4
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +9 -6
- package/dist/adapters/openai.js.map +1 -1
- package/dist/adapters/rest.d.ts +2 -0
- package/dist/adapters/rest.d.ts.map +1 -1
- package/dist/adapters/rest.js +72 -4
- package/dist/adapters/rest.js.map +1 -1
- package/dist/cli/index.js +519 -103
- package/dist/cli/index.js.map +1 -1
- package/dist/config/config-guard.d.ts.map +1 -1
- package/dist/config/config-guard.js +3 -2
- package/dist/config/config-guard.js.map +1 -1
- package/dist/config/config-hmac.d.ts +5 -0
- package/dist/config/config-hmac.d.ts.map +1 -0
- package/dist/config/config-hmac.js +29 -0
- package/dist/config/config-hmac.js.map +1 -0
- package/dist/config/manager.d.ts +14 -0
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +72 -13
- package/dist/config/manager.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +2 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/server/unified.d.ts +4 -1
- package/dist/server/unified.d.ts.map +1 -1
- package/dist/server/unified.js +102 -33
- package/dist/server/unified.js.map +1 -1
- package/dist/service/config.d.ts +21 -0
- package/dist/service/config.d.ts.map +1 -0
- package/dist/service/config.js +94 -0
- package/dist/service/config.js.map +1 -0
- package/dist/service/generators.d.ts +14 -0
- package/dist/service/generators.d.ts.map +1 -0
- package/dist/service/generators.js +92 -0
- package/dist/service/generators.js.map +1 -0
- package/dist/service/index.d.ts +23 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +215 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/platform.d.ts +5 -0
- package/dist/service/platform.d.ts.map +1 -0
- package/dist/service/platform.js +25 -0
- package/dist/service/platform.js.map +1 -0
- package/dist/storage/index.d.ts +39 -10
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +318 -31
- package/dist/storage/index.js.map +1 -1
- package/dist/types.d.ts +545 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +74 -2
- package/dist/types.js.map +1 -1
- package/dist/utils/crypto.d.ts +9 -0
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js +88 -19
- package/dist/utils/crypto.js.map +1 -1
- package/dist/utils/hsm.d.ts +82 -0
- package/dist/utils/hsm.d.ts.map +1 -0
- package/dist/utils/hsm.js +344 -0
- package/dist/utils/hsm.js.map +1 -0
- package/dist/utils/http.d.ts.map +1 -1
- package/dist/utils/http.js +14 -4
- package/dist/utils/http.js.map +1 -1
- package/dist/utils/keychain.d.ts.map +1 -1
- package/dist/utils/keychain.js +0 -8
- package/dist/utils/keychain.js.map +1 -1
- package/dist/utils/lock.d.ts +1 -1
- package/dist/utils/lock.d.ts.map +1 -1
- package/dist/utils/lock.js +4 -2
- package/dist/utils/lock.js.map +1 -1
- package/dist/utils/lockout.d.ts +56 -0
- package/dist/utils/lockout.d.ts.map +1 -0
- package/dist/utils/lockout.js +209 -0
- package/dist/utils/lockout.js.map +1 -0
- package/dist/utils/prompt.d.ts +13 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js +124 -0
- package/dist/utils/prompt.js.map +1 -0
- package/dist/utils/secure-memory.d.ts +20 -0
- package/dist/utils/secure-memory.d.ts.map +1 -0
- package/dist/utils/secure-memory.js +122 -0
- package/dist/utils/secure-memory.js.map +1 -0
- package/dist/utils/session.d.ts.map +1 -1
- package/dist/utils/session.js +14 -4
- package/dist/utils/session.js.map +1 -1
- package/dist/utils/update-checker.d.ts +1 -1
- package/dist/utils/update-checker.d.ts.map +1 -1
- package/dist/utils/update-checker.js +7 -5
- package/dist/utils/update-checker.js.map +1 -1
- package/dist/vault/index.d.ts.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/package.json +18 -9
- package/scripts/sync-version.js +88 -0
- package/scripts/install.sh +0 -54
package/dist/cli/index.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import inquirer from 'inquirer';
|
|
3
2
|
import chalk from 'chalk';
|
|
4
3
|
import * as path from 'path';
|
|
5
4
|
import * as os from 'os';
|
|
6
5
|
import * as fs from 'fs/promises';
|
|
6
|
+
import { promptPassword, promptInput, promptConfirm, promptList } from '../utils/prompt.js';
|
|
7
7
|
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
8
8
|
import { loadConfig, initConfig, saveConfig, parseEnvFile, registerMcpConfig, isBlacklisted, canAccess } from '../config/manager.js';
|
|
9
9
|
import { ConfigGuard } from '../config/config-guard.js';
|
|
10
|
-
import { StorageManager } from '../storage/index.js';
|
|
10
|
+
import { StorageManager, LogManager } from '../storage/index.js';
|
|
11
|
+
import { VERSION } from '../version.js';
|
|
11
12
|
import { SessionManager } from '../utils/session.js';
|
|
12
13
|
import { maskValue, validatePassword, encrypt, decrypt, generateRecoveryKey, createRecoveryData, recoverPassword } from '../utils/crypto.js';
|
|
13
14
|
import { KeychainManager } from '../utils/keychain.js';
|
|
15
|
+
import { HsmManager } from '../utils/hsm.js';
|
|
14
16
|
import { checkForUpdate, formatUpdateMessage, logUpdateCheck } from '../utils/update-checker.js';
|
|
17
|
+
import { LockoutManager } from '../utils/lockout.js';
|
|
18
|
+
import { initMemoryProtection } from '../utils/secure-memory.js';
|
|
15
19
|
import { getGlobalVaultPath, getProjectVaultPath, resolveVaultPath, setActiveVault, listVaults, initNamedVault, } from '../vault/index.js';
|
|
20
|
+
initMemoryProtection();
|
|
16
21
|
async function withSession(fn, vaultOverride) {
|
|
17
22
|
const projectPath = process.cwd();
|
|
18
23
|
const config = await loadConfig(projectPath);
|
|
@@ -23,27 +28,81 @@ async function withSession(fn, vaultOverride) {
|
|
|
23
28
|
: await resolveVaultPath(projectPath, config);
|
|
24
29
|
if (config.encryption?.enabled === false) {
|
|
25
30
|
const storage = new StorageManager(vaultPath, false);
|
|
26
|
-
|
|
31
|
+
const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
|
|
32
|
+
await logManager.init();
|
|
33
|
+
await fn(storage, '', config, projectPath, logManager);
|
|
27
34
|
return;
|
|
28
35
|
}
|
|
29
36
|
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
30
37
|
await sessionManager.init();
|
|
38
|
+
// Initialize audit logging
|
|
39
|
+
const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
|
|
40
|
+
await logManager.init();
|
|
31
41
|
let session = await sessionManager.load();
|
|
32
42
|
let password = '';
|
|
33
43
|
if (!session) {
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
const authMethod = config.auth?.method ?? 'password';
|
|
45
|
+
// --- HSM-only or multi-factor auth ---
|
|
46
|
+
if (authMethod === 'hsm' || authMethod === 'multi') {
|
|
47
|
+
const hsm = HsmManager.fromConfig(config, projectPath);
|
|
48
|
+
const hsmAvailable = await hsm.isAvailable();
|
|
49
|
+
if (hsmAvailable) {
|
|
50
|
+
try {
|
|
51
|
+
const hsmSecret = await hsm.retrieveVaultPassword();
|
|
52
|
+
if (authMethod === 'multi') {
|
|
53
|
+
const factors = config.auth?.multi_factors ?? ['password', 'hsm'];
|
|
54
|
+
let userPassword = '';
|
|
55
|
+
if (factors.includes('password')) {
|
|
56
|
+
userPassword = await promptPassword('Enter password (multi-factor):');
|
|
57
|
+
}
|
|
58
|
+
else if (factors.includes('keychain')) {
|
|
59
|
+
const keychain = new KeychainManager(config.keychain?.service || 'envcp');
|
|
60
|
+
const stored = await keychain.retrievePassword(projectPath);
|
|
61
|
+
if (stored) {
|
|
62
|
+
userPassword = stored;
|
|
63
|
+
console.log(chalk.gray('Password retrieved from OS keychain (multi-factor)'));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
password = HsmManager.combineSecrets(hsmSecret, userPassword);
|
|
67
|
+
console.log(chalk.gray(`Authenticated via ${hsm.backendName} + password`));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
password = hsmSecret;
|
|
71
|
+
console.log(chalk.gray(`Authenticated via ${hsm.backendName}`));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
76
|
+
const fallback = config.auth?.fallback ?? 'password';
|
|
77
|
+
if (fallback === 'none') {
|
|
78
|
+
console.log(chalk.red(`HSM authentication failed: ${msg}`));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(chalk.yellow(`HSM unavailable: ${msg}`));
|
|
82
|
+
console.log(chalk.gray('Falling back to password...'));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const fallback = config.auth?.fallback ?? 'password';
|
|
87
|
+
if (fallback === 'none') {
|
|
88
|
+
console.log(chalk.red(`HSM device (${hsm.backendName}) not found. No fallback configured.`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk.yellow(`HSM device (${hsm.backendName}) not available. Falling back to password...`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// --- OS keychain ---
|
|
95
|
+
if (!password && (authMethod === 'keychain' || config.keychain?.enabled)) {
|
|
96
|
+
const keychain = new KeychainManager(config.keychain?.service || 'envcp');
|
|
36
97
|
const stored = await keychain.retrievePassword(projectPath);
|
|
37
98
|
if (stored) {
|
|
38
99
|
password = stored;
|
|
39
100
|
console.log(chalk.gray('Password retrieved from OS keychain'));
|
|
40
101
|
}
|
|
41
102
|
}
|
|
103
|
+
// --- Password prompt fallback ---
|
|
42
104
|
if (!password) {
|
|
43
|
-
|
|
44
|
-
{ type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
|
|
45
|
-
]);
|
|
46
|
-
password = answer.password;
|
|
105
|
+
password = await promptPassword('Enter password:');
|
|
47
106
|
const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
|
|
48
107
|
if (!passwordValid) {
|
|
49
108
|
console.log(chalk.red("Invalid password"));
|
|
@@ -59,13 +118,13 @@ async function withSession(fn, vaultOverride) {
|
|
|
59
118
|
const storage = new StorageManager(vaultPath, config.storage.encrypted);
|
|
60
119
|
if (password)
|
|
61
120
|
storage.setPassword(password);
|
|
62
|
-
await fn(storage, password, config, projectPath);
|
|
121
|
+
await fn(storage, password, config, projectPath, logManager);
|
|
63
122
|
}
|
|
64
123
|
const program = new Command();
|
|
65
124
|
program
|
|
66
125
|
.name('envcp')
|
|
67
126
|
.description('Secure environment variable management for AI-assisted coding')
|
|
68
|
-
.version(
|
|
127
|
+
.version(VERSION);
|
|
69
128
|
program
|
|
70
129
|
.command('init')
|
|
71
130
|
.description('Initialize EnvCP in the current project')
|
|
@@ -73,6 +132,10 @@ program
|
|
|
73
132
|
.option('--no-encrypt', 'Skip encryption (passwordless mode)')
|
|
74
133
|
.option('--skip-env', 'Skip .env auto-import')
|
|
75
134
|
.option('--skip-mcp', 'Skip MCP auto-registration')
|
|
135
|
+
.option('--auth-method <method>', 'Authentication method: password | keychain | hsm | multi (default: password)')
|
|
136
|
+
.option('--hsm-type <type>', 'HSM type for --auth-method hsm|multi: yubikey | gpg | pkcs11')
|
|
137
|
+
.option('--key-id <id>', 'GPG key ID or PKCS#11 key label for HSM auth')
|
|
138
|
+
.option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library for --hsm-type pkcs11')
|
|
76
139
|
.action(async (options) => {
|
|
77
140
|
const projectPath = process.cwd();
|
|
78
141
|
const projectName = options.project || path.basename(projectPath);
|
|
@@ -85,42 +148,56 @@ program
|
|
|
85
148
|
securityChoice = 'none';
|
|
86
149
|
}
|
|
87
150
|
else {
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
choices: [
|
|
94
|
-
{ name: 'No encryption (fastest setup, for local dev)', value: 'none' },
|
|
95
|
-
{ name: 'Encrypted with recovery key (recommended)', value: 'recoverable' },
|
|
96
|
-
{ name: 'Encrypted hard-lock (max security, no recovery)', value: 'hard-lock' },
|
|
97
|
-
],
|
|
98
|
-
default: 'recoverable',
|
|
99
|
-
}
|
|
100
|
-
]);
|
|
101
|
-
securityChoice = mode;
|
|
151
|
+
securityChoice = await promptList('How would you like to secure your variables?', [
|
|
152
|
+
{ name: 'No encryption (fastest setup, for local dev)', value: 'none' },
|
|
153
|
+
{ name: 'Encrypted with recovery key (recommended)', value: 'recoverable' },
|
|
154
|
+
{ name: 'Encrypted hard-lock (max security, no recovery)', value: 'hard-lock' },
|
|
155
|
+
], 'recoverable');
|
|
102
156
|
}
|
|
103
157
|
// Apply security choice to config
|
|
104
158
|
if (securityChoice === 'none') {
|
|
105
159
|
config.encryption = { enabled: false };
|
|
106
160
|
config.storage.encrypted = false;
|
|
107
|
-
config.security = {
|
|
161
|
+
config.security = {
|
|
162
|
+
mode: 'recoverable',
|
|
163
|
+
recovery_file: '.envcp/.recovery',
|
|
164
|
+
brute_force_protection: {
|
|
165
|
+
enabled: true,
|
|
166
|
+
max_attempts: 5,
|
|
167
|
+
lockout_duration: 300,
|
|
168
|
+
progressive_delay: true,
|
|
169
|
+
max_delay: 60,
|
|
170
|
+
permanent_lockout_threshold: 50,
|
|
171
|
+
permanent_lockout_action: 'require_recovery_key',
|
|
172
|
+
notifications: {}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
108
175
|
}
|
|
109
176
|
else {
|
|
110
177
|
config.encryption = { enabled: true };
|
|
111
178
|
config.storage.encrypted = true;
|
|
112
|
-
config.security = {
|
|
179
|
+
config.security = {
|
|
180
|
+
mode: securityChoice,
|
|
181
|
+
recovery_file: '.envcp/.recovery',
|
|
182
|
+
brute_force_protection: {
|
|
183
|
+
enabled: true,
|
|
184
|
+
max_attempts: 5,
|
|
185
|
+
lockout_duration: 300,
|
|
186
|
+
progressive_delay: true,
|
|
187
|
+
max_delay: 60,
|
|
188
|
+
permanent_lockout_threshold: 50,
|
|
189
|
+
permanent_lockout_action: 'require_recovery_key',
|
|
190
|
+
notifications: {},
|
|
191
|
+
}
|
|
192
|
+
};
|
|
113
193
|
}
|
|
114
194
|
// For encrypted modes: get password now
|
|
115
195
|
let pwd = '';
|
|
116
196
|
if (securityChoice !== 'none') {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{ type: 'password', name: 'confirm', message: 'Confirm password:', mask: '*' }
|
|
122
|
-
]);
|
|
123
|
-
if (password !== confirm) {
|
|
197
|
+
const password = await promptPassword('Set encryption password:');
|
|
198
|
+
const confirmPwd = await promptPassword('Confirm password:');
|
|
199
|
+
// eslint-disable-next-line security/detect-possible-timing-attacks -- comparing two user-typed confirm fields, not a secret-vs-known value
|
|
200
|
+
if (password !== confirmPwd) {
|
|
124
201
|
console.log(chalk.red('Passwords do not match. Aborting.'));
|
|
125
202
|
return;
|
|
126
203
|
}
|
|
@@ -146,15 +223,17 @@ program
|
|
|
146
223
|
if (pwd)
|
|
147
224
|
storage.setPassword(pwd);
|
|
148
225
|
const now = new Date().toISOString();
|
|
226
|
+
const existing = await storage.load();
|
|
149
227
|
for (const [name, value] of Object.entries(vars)) {
|
|
150
|
-
|
|
228
|
+
existing[name] = {
|
|
151
229
|
name, value,
|
|
152
230
|
encrypted: config.storage.encrypted,
|
|
153
231
|
created: now, updated: now,
|
|
154
232
|
sync_to_env: true,
|
|
155
233
|
protected: false,
|
|
156
|
-
}
|
|
234
|
+
};
|
|
157
235
|
}
|
|
236
|
+
await storage.save(existing);
|
|
158
237
|
// Create session for encrypted mode
|
|
159
238
|
if (pwd) {
|
|
160
239
|
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
@@ -202,6 +281,45 @@ program
|
|
|
202
281
|
console.log(chalk.gray(' No AI tools detected for auto-registration'));
|
|
203
282
|
}
|
|
204
283
|
}
|
|
284
|
+
// Setup HSM authentication if --auth-method hsm|multi provided
|
|
285
|
+
const authMethod = options.authMethod;
|
|
286
|
+
if (pwd && (authMethod === 'hsm' || authMethod === 'multi')) {
|
|
287
|
+
const hsmType = options.hsmType || 'yubikey';
|
|
288
|
+
config.hsm = {
|
|
289
|
+
...config.hsm,
|
|
290
|
+
enabled: true,
|
|
291
|
+
type: hsmType,
|
|
292
|
+
key_id: options.keyId ?? config.hsm?.key_id,
|
|
293
|
+
pkcs11_lib: options.pkcs11Lib ?? config.hsm?.pkcs11_lib,
|
|
294
|
+
require_touch: config.hsm?.require_touch ?? true,
|
|
295
|
+
protected_key_path: config.hsm?.protected_key_path ?? '.envcp/.hsm-key',
|
|
296
|
+
};
|
|
297
|
+
config.auth = {
|
|
298
|
+
method: authMethod,
|
|
299
|
+
multi_factors: authMethod === 'multi' ? ['password', 'hsm'] : ['hsm'],
|
|
300
|
+
fallback: 'password',
|
|
301
|
+
};
|
|
302
|
+
const hsm = HsmManager.fromConfig(config, projectPath);
|
|
303
|
+
if (await hsm.isAvailable()) {
|
|
304
|
+
try {
|
|
305
|
+
await hsm.protectVaultPassword(pwd);
|
|
306
|
+
await saveConfig(config, projectPath);
|
|
307
|
+
console.log('');
|
|
308
|
+
console.log(chalk.green(` Vault password protected by ${hsm.backendName}`));
|
|
309
|
+
console.log(chalk.gray(` Future sessions will authenticate via hardware`));
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
console.log(chalk.yellow(` HSM setup warning: ${err instanceof Error ? err.message : String(err)}`));
|
|
313
|
+
console.log(chalk.gray(' Falling back to password authentication'));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
console.log(chalk.yellow(` HSM device (${hsm.backendName}) not available at init time.`));
|
|
318
|
+
console.log(chalk.gray(' Run "envcp unlock --setup-hsm" later to enable hardware authentication.'));
|
|
319
|
+
config.auth = { method: authMethod, multi_factors: ['password', 'hsm'], fallback: 'password' };
|
|
320
|
+
await saveConfig(config, projectPath);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
205
323
|
console.log('');
|
|
206
324
|
console.log(chalk.green('Done! Your AI tools can now use EnvCP.'));
|
|
207
325
|
});
|
|
@@ -209,21 +327,18 @@ program
|
|
|
209
327
|
.command('unlock')
|
|
210
328
|
.description('Unlock EnvCP session with password')
|
|
211
329
|
.option('-p, --password <password>', 'Password (will prompt if not provided)')
|
|
330
|
+
.option('--recovery-key <key>', 'Recovery key to clear permanent lockout')
|
|
212
331
|
.option('--save-to-keychain', 'Save password to OS keychain for auto-unlock')
|
|
332
|
+
.option('--setup-hsm', 'Protect vault password with hardware security module')
|
|
333
|
+
.option('--hsm-type <type>', 'HSM type: yubikey | gpg | pkcs11 (default: yubikey)')
|
|
334
|
+
.option('--key-id <id>', 'GPG key ID or PKCS#11 key label')
|
|
335
|
+
.option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library (.so / .dll)')
|
|
213
336
|
.action(async (options) => {
|
|
214
337
|
const projectPath = process.cwd();
|
|
215
338
|
const config = await loadConfig(projectPath);
|
|
216
339
|
let password = options.password;
|
|
217
340
|
if (!password) {
|
|
218
|
-
|
|
219
|
-
{
|
|
220
|
-
type: 'password',
|
|
221
|
-
name: 'password',
|
|
222
|
-
message: 'Enter password:',
|
|
223
|
-
mask: '*'
|
|
224
|
-
}
|
|
225
|
-
]);
|
|
226
|
-
password = answer.password;
|
|
341
|
+
password = await promptPassword('Enter password:');
|
|
227
342
|
}
|
|
228
343
|
const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
|
|
229
344
|
if (!passwordValid) {
|
|
@@ -234,16 +349,115 @@ program
|
|
|
234
349
|
if (passwordValid && passwordWarning) {
|
|
235
350
|
console.log(chalk.yellow('⚠ Weak password detected'));
|
|
236
351
|
}
|
|
352
|
+
const sessionDir = path.join(projectPath, path.dirname(config.session?.path || '.envcp/.session'));
|
|
353
|
+
let lockoutManager = new LockoutManager(path.join(sessionDir, '.lockout'));
|
|
354
|
+
// Handle recovery key if provided
|
|
355
|
+
if (options.recoveryKey) {
|
|
356
|
+
const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
|
|
357
|
+
if (!await pathExists(recoveryPath)) {
|
|
358
|
+
console.log(chalk.red('No recovery file found.'));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const recoveryData = await fs.readFile(recoveryPath, 'utf8');
|
|
362
|
+
try {
|
|
363
|
+
await recoverPassword(recoveryData, options.recoveryKey);
|
|
364
|
+
// Recovery key is valid - clear permanent lockout
|
|
365
|
+
await lockoutManager.clearPermanentLockout();
|
|
366
|
+
console.log(chalk.green('Permanent lockout cleared.'));
|
|
367
|
+
// Log recovery event
|
|
368
|
+
const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
|
|
369
|
+
await logManager.init();
|
|
370
|
+
await logManager.log({
|
|
371
|
+
timestamp: new Date().toISOString(),
|
|
372
|
+
operation: 'unlock',
|
|
373
|
+
variable: '',
|
|
374
|
+
source: 'cli',
|
|
375
|
+
success: true,
|
|
376
|
+
message: 'Permanent lockout cleared with recovery key (via --recovery-key flag)',
|
|
377
|
+
session_id: '',
|
|
378
|
+
client_id: 'cli',
|
|
379
|
+
client_type: 'terminal',
|
|
380
|
+
ip: '127.0.0.1'
|
|
381
|
+
});
|
|
382
|
+
console.log(chalk.yellow('Note: You still need to enter the correct password.'));
|
|
383
|
+
// Continue with password prompt
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
console.log(chalk.red('Invalid recovery key.'));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Use new brute_force_protection config if available, fall back to session config
|
|
391
|
+
const bfpConfig = config.security?.brute_force_protection;
|
|
392
|
+
const lockoutThreshold = bfpConfig?.max_attempts ?? config.session?.lockout_threshold ?? 5;
|
|
393
|
+
const lockoutBaseSeconds = bfpConfig?.lockout_duration ?? config.session?.lockout_base_seconds ?? 60;
|
|
394
|
+
const progressiveDelay = bfpConfig?.progressive_delay ?? true;
|
|
395
|
+
const maxDelay = bfpConfig?.max_delay ?? 60;
|
|
396
|
+
const permanentThreshold = bfpConfig?.permanent_lockout_threshold ?? 0;
|
|
237
397
|
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
238
398
|
await sessionManager.init();
|
|
399
|
+
// Initialize audit logging
|
|
400
|
+
const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
|
|
401
|
+
await logManager.init();
|
|
239
402
|
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
240
403
|
storage.setPassword(password);
|
|
241
404
|
const storeExists = await storage.exists();
|
|
405
|
+
// Check lockout before attempting password verification on existing stores
|
|
406
|
+
if (storeExists) {
|
|
407
|
+
const lockoutStatus = await lockoutManager.check();
|
|
408
|
+
if (lockoutStatus.locked) {
|
|
409
|
+
if (lockoutStatus.permanent_locked) {
|
|
410
|
+
console.log(chalk.red.bold('PERMANENT LOCKOUT: Too many failed attempts.'));
|
|
411
|
+
console.log(chalk.red('Recovery key or administrator intervention required.'));
|
|
412
|
+
const useRecovery = await promptConfirm('Use recovery key to clear lockout?', true);
|
|
413
|
+
if (useRecovery) {
|
|
414
|
+
const recoveryKey = await promptPassword('Enter recovery key:');
|
|
415
|
+
const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
|
|
416
|
+
if (!await pathExists(recoveryPath)) {
|
|
417
|
+
console.log(chalk.red('No recovery file found.'));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const recoveryData = await fs.readFile(recoveryPath, 'utf8');
|
|
421
|
+
try {
|
|
422
|
+
await recoverPassword(recoveryData, recoveryKey);
|
|
423
|
+
// Recovery key is valid - clear permanent lockout
|
|
424
|
+
await lockoutManager.clearPermanentLockout();
|
|
425
|
+
console.log(chalk.green('Permanent lockout cleared. You can now attempt to unlock.'));
|
|
426
|
+
console.log(chalk.yellow('Note: You still need to enter the correct password.'));
|
|
427
|
+
// Log recovery event
|
|
428
|
+
await logManager.log({
|
|
429
|
+
timestamp: new Date().toISOString(),
|
|
430
|
+
operation: 'unlock',
|
|
431
|
+
variable: '',
|
|
432
|
+
source: 'cli',
|
|
433
|
+
success: true,
|
|
434
|
+
message: 'Permanent lockout cleared with recovery key',
|
|
435
|
+
session_id: '',
|
|
436
|
+
client_id: 'cli',
|
|
437
|
+
client_type: 'terminal',
|
|
438
|
+
ip: '127.0.0.1'
|
|
439
|
+
});
|
|
440
|
+
// Continue with password prompt
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
console.log(chalk.red('Invalid recovery key.'));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
console.log(chalk.red(`Too many failed attempts. Try again in ${lockoutStatus.remaining_seconds} second(s).`));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
242
457
|
if (!storeExists) {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (confirm.password !== password) {
|
|
458
|
+
const confirmPasswordValue = await promptPassword('Confirm password:');
|
|
459
|
+
// eslint-disable-next-line security/detect-possible-timing-attacks -- comparing two user-typed confirm fields, not a secret-vs-known value
|
|
460
|
+
if (confirmPasswordValue !== password) {
|
|
247
461
|
console.log(chalk.red('Passwords do not match'));
|
|
248
462
|
return;
|
|
249
463
|
}
|
|
@@ -267,10 +481,85 @@ program
|
|
|
267
481
|
await storage.load();
|
|
268
482
|
}
|
|
269
483
|
catch (error) {
|
|
270
|
-
|
|
484
|
+
if (storeExists) {
|
|
485
|
+
const status = await lockoutManager.recordFailure(lockoutThreshold, lockoutBaseSeconds, progressiveDelay, maxDelay, permanentThreshold);
|
|
486
|
+
// Log the failed attempt
|
|
487
|
+
await logManager.log({
|
|
488
|
+
timestamp: new Date().toISOString(),
|
|
489
|
+
operation: 'auth_failure',
|
|
490
|
+
variable: '',
|
|
491
|
+
source: 'cli',
|
|
492
|
+
success: false,
|
|
493
|
+
message: `Failed unlock attempt (attempt ${status.attempts})`,
|
|
494
|
+
session_id: '',
|
|
495
|
+
client_id: 'cli',
|
|
496
|
+
client_type: 'terminal',
|
|
497
|
+
ip: '127.0.0.1'
|
|
498
|
+
});
|
|
499
|
+
if (status.permanent_locked) {
|
|
500
|
+
console.log(chalk.red.bold('PERMANENT LOCKOUT TRIGGERED: Too many failed attempts.'));
|
|
501
|
+
console.log(chalk.red('Recovery key or administrator intervention required.'));
|
|
502
|
+
// Log permanent lockout event
|
|
503
|
+
await logManager.log({
|
|
504
|
+
timestamp: new Date().toISOString(),
|
|
505
|
+
operation: 'permanent_lockout',
|
|
506
|
+
variable: '',
|
|
507
|
+
source: 'cli',
|
|
508
|
+
success: false,
|
|
509
|
+
message: `Permanent lockout triggered after ${status.permanent_lockout_count} lockouts`,
|
|
510
|
+
session_id: '',
|
|
511
|
+
client_id: 'cli',
|
|
512
|
+
client_type: 'terminal',
|
|
513
|
+
ip: '127.0.0.1'
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
else if (status.locked) {
|
|
517
|
+
console.log(chalk.red(`Invalid password. Too many failed attempts — locked out for ${status.remaining_seconds} second(s).`));
|
|
518
|
+
// Log lockout event
|
|
519
|
+
await logManager.log({
|
|
520
|
+
timestamp: new Date().toISOString(),
|
|
521
|
+
operation: 'lockout_triggered',
|
|
522
|
+
variable: '',
|
|
523
|
+
source: 'cli',
|
|
524
|
+
success: false,
|
|
525
|
+
message: `Lockout triggered for ${status.remaining_seconds}s (lockout #${status.lockout_count})`,
|
|
526
|
+
session_id: '',
|
|
527
|
+
client_id: 'cli',
|
|
528
|
+
client_type: 'terminal',
|
|
529
|
+
ip: '127.0.0.1'
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
const remaining = lockoutThreshold - status.attempts;
|
|
534
|
+
let message = `Invalid password. ${remaining} attempt(s) remaining before lockout.`;
|
|
535
|
+
// Show progressive delay if applied
|
|
536
|
+
if (status.delay_seconds && status.delay_seconds > 0) {
|
|
537
|
+
message += ` (Delayed ${status.delay_seconds}s)`;
|
|
538
|
+
}
|
|
539
|
+
console.log(chalk.red(message));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
console.log(chalk.red('Invalid password'));
|
|
544
|
+
}
|
|
271
545
|
return;
|
|
272
546
|
}
|
|
547
|
+
// Successful unlock — clear any lockout state
|
|
548
|
+
await lockoutManager.reset();
|
|
273
549
|
const session = await sessionManager.create(password);
|
|
550
|
+
// Log successful unlock
|
|
551
|
+
await logManager.log({
|
|
552
|
+
timestamp: new Date().toISOString(),
|
|
553
|
+
operation: 'unlock',
|
|
554
|
+
variable: '',
|
|
555
|
+
source: 'cli',
|
|
556
|
+
success: true,
|
|
557
|
+
message: 'Session unlocked successfully',
|
|
558
|
+
session_id: session.id,
|
|
559
|
+
client_id: 'cli',
|
|
560
|
+
client_type: 'terminal',
|
|
561
|
+
ip: '127.0.0.1'
|
|
562
|
+
});
|
|
274
563
|
console.log(chalk.green('Session unlocked!'));
|
|
275
564
|
console.log(chalk.gray(` Session ID: ${session.id}`));
|
|
276
565
|
console.log(chalk.gray(` Expires in: ${config.session?.timeout_minutes || 30} minutes`));
|
|
@@ -296,6 +585,35 @@ program
|
|
|
296
585
|
console.log(chalk.red(`OS keychain not available (${keychain.backendName})`));
|
|
297
586
|
}
|
|
298
587
|
}
|
|
588
|
+
// Setup HSM if requested
|
|
589
|
+
if (options.setupHsm) {
|
|
590
|
+
const hsmType = options.hsmType || 'yubikey';
|
|
591
|
+
config.hsm = {
|
|
592
|
+
...config.hsm,
|
|
593
|
+
enabled: true,
|
|
594
|
+
type: hsmType,
|
|
595
|
+
key_id: options.keyId ?? config.hsm?.key_id,
|
|
596
|
+
pkcs11_lib: options.pkcs11Lib ?? config.hsm?.pkcs11_lib,
|
|
597
|
+
require_touch: config.hsm?.require_touch ?? true,
|
|
598
|
+
protected_key_path: config.hsm?.protected_key_path ?? '.envcp/.hsm-key',
|
|
599
|
+
};
|
|
600
|
+
config.auth = { ...config.auth, method: 'hsm', fallback: config.auth?.fallback ?? 'password' };
|
|
601
|
+
const hsm = HsmManager.fromConfig(config, projectPath);
|
|
602
|
+
if (!await hsm.isAvailable()) {
|
|
603
|
+
console.log(chalk.red(`HSM device (${hsm.backendName}) not available. Aborting HSM setup.`));
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
await hsm.protectVaultPassword(password);
|
|
608
|
+
await saveConfig(config, projectPath);
|
|
609
|
+
console.log(chalk.green(`Vault password protected by ${hsm.backendName}`));
|
|
610
|
+
console.log(chalk.gray(` Key file: ${config.hsm.protected_key_path}`));
|
|
611
|
+
console.log(chalk.gray(' Future sessions will authenticate via hardware'));
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
console.log(chalk.red(`HSM setup failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
299
617
|
});
|
|
300
618
|
program
|
|
301
619
|
.command('lock')
|
|
@@ -316,10 +634,8 @@ program
|
|
|
316
634
|
const config = await loadConfig(projectPath);
|
|
317
635
|
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
318
636
|
await sessionManager.init();
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
]);
|
|
322
|
-
const session = await sessionManager.load(answer.password);
|
|
637
|
+
const password = await promptPassword('Enter password:');
|
|
638
|
+
const session = await sessionManager.load(password);
|
|
323
639
|
if (!session) {
|
|
324
640
|
console.log(chalk.yellow('No active session (expired, invalid password, or not unlocked)'));
|
|
325
641
|
console.log(chalk.gray('Run: envcp unlock'));
|
|
@@ -340,9 +656,7 @@ program
|
|
|
340
656
|
.action(async () => {
|
|
341
657
|
const projectPath = process.cwd();
|
|
342
658
|
const configGuard = new ConfigGuard(projectPath);
|
|
343
|
-
const
|
|
344
|
-
{ type: 'password', name: 'password', message: 'Enter password to reload config:', mask: '*' }
|
|
345
|
-
]);
|
|
659
|
+
const password = await promptPassword('Enter password to reload config:');
|
|
346
660
|
const result = await configGuard.reload(password);
|
|
347
661
|
if (result.success) {
|
|
348
662
|
console.log(chalk.green('Config reloaded successfully'));
|
|
@@ -360,10 +674,8 @@ program
|
|
|
360
674
|
const config = await loadConfig(projectPath);
|
|
361
675
|
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
362
676
|
await sessionManager.init();
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
]);
|
|
366
|
-
const loaded = await sessionManager.load(answer.password);
|
|
677
|
+
const password = await promptPassword('Enter password:');
|
|
678
|
+
const loaded = await sessionManager.load(password);
|
|
367
679
|
if (!loaded) {
|
|
368
680
|
console.log(chalk.red('Cannot extend session. No active session or invalid password.'));
|
|
369
681
|
return;
|
|
@@ -394,9 +706,7 @@ program
|
|
|
394
706
|
console.log(chalk.red('No recovery file found. Recovery is not available.'));
|
|
395
707
|
return;
|
|
396
708
|
}
|
|
397
|
-
const
|
|
398
|
-
{ type: 'password', name: 'recoveryKey', message: 'Enter your recovery key:', mask: '*' }
|
|
399
|
-
]);
|
|
709
|
+
const recoveryKey = await promptPassword('Enter your recovery key:');
|
|
400
710
|
const recoveryData = await fs.readFile(recoveryPath, 'utf8');
|
|
401
711
|
let oldPassword;
|
|
402
712
|
try {
|
|
@@ -419,12 +729,8 @@ program
|
|
|
419
729
|
}
|
|
420
730
|
console.log(chalk.green('Recovery key verified. Store contains ' + Object.keys(variables).length + ' variables.'));
|
|
421
731
|
// Set new password
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
]);
|
|
425
|
-
const { confirmPassword } = await inquirer.prompt([
|
|
426
|
-
{ type: 'password', name: 'confirmPassword', message: 'Confirm new password:', mask: '*' }
|
|
427
|
-
]);
|
|
732
|
+
const newPassword = await promptPassword('Set new password:');
|
|
733
|
+
const confirmPassword = await promptPassword('Confirm new password:');
|
|
428
734
|
if (newPassword !== confirmPassword) {
|
|
429
735
|
console.log(chalk.red('Passwords do not match'));
|
|
430
736
|
return;
|
|
@@ -489,14 +795,10 @@ program
|
|
|
489
795
|
let tags = [];
|
|
490
796
|
let description = options.description;
|
|
491
797
|
if (!value) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
]);
|
|
497
|
-
value = answers.value;
|
|
498
|
-
tags = answers.tags.split(',').map((t) => t.trim()).filter(Boolean);
|
|
499
|
-
description = answers.description;
|
|
798
|
+
value = await promptPassword('Enter value:');
|
|
799
|
+
const tagsInput = await promptInput('Tags (comma-separated):');
|
|
800
|
+
tags = tagsInput.split(',').map((t) => t.trim()).filter(Boolean);
|
|
801
|
+
description = await promptInput('Description:');
|
|
500
802
|
}
|
|
501
803
|
else if (options.tags) {
|
|
502
804
|
tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
|
|
@@ -606,7 +908,8 @@ program
|
|
|
606
908
|
if (!variable.sync_to_env)
|
|
607
909
|
continue;
|
|
608
910
|
const excluded = config.sync.exclude?.some((pattern) => {
|
|
609
|
-
|
|
911
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- glob pattern from config; metacharacters escaped above
|
|
912
|
+
const regex = new RegExp('^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$');
|
|
610
913
|
return regex.test(name);
|
|
611
914
|
});
|
|
612
915
|
if (excluded)
|
|
@@ -631,7 +934,8 @@ program
|
|
|
631
934
|
if (!variable.sync_to_env)
|
|
632
935
|
continue;
|
|
633
936
|
const excluded = config.sync.exclude?.some((pattern) => {
|
|
634
|
-
|
|
937
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- glob pattern from config; metacharacters escaped above
|
|
938
|
+
const regex = new RegExp('^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$');
|
|
635
939
|
return regex.test(name);
|
|
636
940
|
});
|
|
637
941
|
if (excluded)
|
|
@@ -709,10 +1013,7 @@ program
|
|
|
709
1013
|
process.stderr.write('Error: No active session. Run `envcp unlock` first, or use --password flag.\n');
|
|
710
1014
|
process.exit(1);
|
|
711
1015
|
}
|
|
712
|
-
|
|
713
|
-
{ type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
|
|
714
|
-
]);
|
|
715
|
-
password = answer.password;
|
|
1016
|
+
password = await promptPassword('Enter password:');
|
|
716
1017
|
const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
|
|
717
1018
|
if (!passwordValid) {
|
|
718
1019
|
// nosem: no tainted data flows to log
|
|
@@ -804,12 +1105,8 @@ program
|
|
|
804
1105
|
console.log(chalk.red('--output <path> is required with --encrypted'));
|
|
805
1106
|
return;
|
|
806
1107
|
}
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
]);
|
|
810
|
-
const { confirmExport } = await inquirer.prompt([
|
|
811
|
-
{ type: 'password', name: 'confirmExport', message: 'Confirm export password:', mask: '*' }
|
|
812
|
-
]);
|
|
1108
|
+
const exportPassword = await promptPassword('Set export password:');
|
|
1109
|
+
const confirmExport = await promptPassword('Confirm export password:');
|
|
813
1110
|
if (exportPassword !== confirmExport) {
|
|
814
1111
|
console.log(chalk.red('Passwords do not match'));
|
|
815
1112
|
return;
|
|
@@ -857,9 +1154,7 @@ program
|
|
|
857
1154
|
console.log(chalk.red(`File not found: ${file}`));
|
|
858
1155
|
return;
|
|
859
1156
|
}
|
|
860
|
-
const
|
|
861
|
-
{ type: 'password', name: 'importPassword', message: 'Enter export file password:', mask: '*' }
|
|
862
|
-
]);
|
|
1157
|
+
const importPassword = await promptPassword('Enter export file password:');
|
|
863
1158
|
const fileContent = await fs.readFile(file, 'utf8');
|
|
864
1159
|
let importData;
|
|
865
1160
|
try {
|
|
@@ -920,9 +1215,7 @@ program
|
|
|
920
1215
|
console.log(chalk.gray('\nNo files were modified.'));
|
|
921
1216
|
return;
|
|
922
1217
|
}
|
|
923
|
-
const
|
|
924
|
-
{ type: 'confirm', name: 'confirm', message: options.merge ? 'Merge into current store?' : 'Replace current store?', default: false }
|
|
925
|
-
]);
|
|
1218
|
+
const confirm = await promptConfirm(options.merge ? 'Merge into current store?' : 'Replace current store?', false);
|
|
926
1219
|
if (!confirm) {
|
|
927
1220
|
console.log(chalk.yellow('Import cancelled'));
|
|
928
1221
|
return;
|
|
@@ -1004,9 +1297,7 @@ program
|
|
|
1004
1297
|
console.log(chalk.gray(` Created: ${meta.timestamp}`));
|
|
1005
1298
|
console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
|
|
1006
1299
|
}
|
|
1007
|
-
const
|
|
1008
|
-
{ type: 'confirm', name: 'confirm', message: options.merge ? 'Merge backup into current store?' : 'Replace current store with backup?', default: false }
|
|
1009
|
-
]);
|
|
1300
|
+
const confirm = await promptConfirm(options.merge ? 'Merge backup into current store?' : 'Replace current store with backup?', false);
|
|
1010
1301
|
if (!confirm) {
|
|
1011
1302
|
console.log(chalk.yellow('Restore cancelled'));
|
|
1012
1303
|
return;
|
|
@@ -1168,7 +1459,7 @@ program
|
|
|
1168
1459
|
.option('--name <name>', 'Operate on a named vault')
|
|
1169
1460
|
.addCommand(new Command('init')
|
|
1170
1461
|
.description('Initialize a vault')
|
|
1171
|
-
.action(async (
|
|
1462
|
+
.action(async (_options, cmd) => {
|
|
1172
1463
|
const parentOpts = cmd.parent.opts();
|
|
1173
1464
|
const projectPath = process.cwd();
|
|
1174
1465
|
const config = await loadConfig(projectPath);
|
|
@@ -1199,7 +1490,7 @@ program
|
|
|
1199
1490
|
const parentOpts = cmd.parent.opts();
|
|
1200
1491
|
const vaultOverride = parentOpts.global ? 'global' : parentOpts.project ? 'project' : undefined;
|
|
1201
1492
|
await withSession(async (storage) => {
|
|
1202
|
-
const value = options.value ||
|
|
1493
|
+
const value = options.value || await promptInput('Value:');
|
|
1203
1494
|
const tags = options.tags ? options.tags.split(',').map((t) => t.trim()) : [];
|
|
1204
1495
|
const now = new Date().toISOString();
|
|
1205
1496
|
await storage.set(name, {
|
|
@@ -1275,7 +1566,6 @@ program
|
|
|
1275
1566
|
.argument('<name>', 'Vault name (global, project, or named vault)')
|
|
1276
1567
|
.action(async (name) => {
|
|
1277
1568
|
const projectPath = process.cwd();
|
|
1278
|
-
const config = await loadConfig(projectPath);
|
|
1279
1569
|
if (name !== 'global' && name !== 'project') {
|
|
1280
1570
|
const vaultDir = path.join(projectPath, '.envcp/vaults', name);
|
|
1281
1571
|
if (!await pathExists(vaultDir)) {
|
|
@@ -1334,7 +1624,7 @@ program
|
|
|
1334
1624
|
.addCommand(new Command('save')
|
|
1335
1625
|
.description('Save current password to OS keychain')
|
|
1336
1626
|
.action(async () => {
|
|
1337
|
-
await withSession(async (
|
|
1627
|
+
await withSession(async (_storage, password, config, projectPath) => {
|
|
1338
1628
|
if (!password) {
|
|
1339
1629
|
console.log(chalk.red('No password available (encryption disabled?)'));
|
|
1340
1630
|
return;
|
|
@@ -1422,5 +1712,131 @@ if (!await pathExists(firstRunMarker)) {
|
|
|
1422
1712
|
Docs: https://github.com/fentz26/EnvCP
|
|
1423
1713
|
`);
|
|
1424
1714
|
}
|
|
1715
|
+
program
|
|
1716
|
+
.command('logs')
|
|
1717
|
+
.description('View audit logs')
|
|
1718
|
+
.option('--date <date>', 'Log date (YYYY-MM-DD, default: today)')
|
|
1719
|
+
.option('--operation <op>', 'Filter by operation (add, get, update, delete, list, sync, export, unlock, lock, check_access, run, auth_failure)')
|
|
1720
|
+
.option('--variable <name>', 'Filter by variable name')
|
|
1721
|
+
.option('--source <source>', 'Filter by source (cli, mcp, api)')
|
|
1722
|
+
.option('--success', 'Show only successful operations')
|
|
1723
|
+
.option('--failure', 'Show only failed operations')
|
|
1724
|
+
.option('--tail <n>', 'Show last N entries', parseInt)
|
|
1725
|
+
.option('--dates', 'List all available log dates')
|
|
1726
|
+
.option('--verify', 'Verify HMAC integrity of log entries')
|
|
1727
|
+
.action(async (options) => {
|
|
1728
|
+
const projectPath = process.cwd();
|
|
1729
|
+
const config = await loadConfig(projectPath);
|
|
1730
|
+
const logDir = path.join(projectPath, '.envcp', 'logs');
|
|
1731
|
+
const logs = new LogManager(logDir, config.audit);
|
|
1732
|
+
await logs.init();
|
|
1733
|
+
if (options.dates) {
|
|
1734
|
+
const dates = await logs.getLogDates();
|
|
1735
|
+
if (dates.length === 0) {
|
|
1736
|
+
console.log(chalk.gray('No log files found.'));
|
|
1737
|
+
}
|
|
1738
|
+
else {
|
|
1739
|
+
console.log(chalk.bold('Available log dates:'));
|
|
1740
|
+
dates.forEach(d => console.log(chalk.gray(` ${d}`)));
|
|
1741
|
+
}
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
const filter = {};
|
|
1745
|
+
if (options.date)
|
|
1746
|
+
filter.date = options.date;
|
|
1747
|
+
if (options.operation)
|
|
1748
|
+
filter.operation = options.operation;
|
|
1749
|
+
if (options.variable)
|
|
1750
|
+
filter.variable = options.variable;
|
|
1751
|
+
if (options.source)
|
|
1752
|
+
filter.source = options.source;
|
|
1753
|
+
if (options.success)
|
|
1754
|
+
filter.success = true;
|
|
1755
|
+
if (options.failure)
|
|
1756
|
+
filter.success = false;
|
|
1757
|
+
if (options.tail)
|
|
1758
|
+
filter.tail = options.tail;
|
|
1759
|
+
const entries = await logs.getLogs(filter);
|
|
1760
|
+
if (entries.length === 0) {
|
|
1761
|
+
console.log(chalk.gray('No log entries found.'));
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
let failed = 0;
|
|
1765
|
+
entries.forEach(entry => {
|
|
1766
|
+
const ts = chalk.gray(entry.timestamp);
|
|
1767
|
+
const op = entry.success ? chalk.green(entry.operation) : chalk.red(entry.operation);
|
|
1768
|
+
const src = chalk.blue(entry.source);
|
|
1769
|
+
const varName = entry.variable ? chalk.yellow(` [${entry.variable}]`) : '';
|
|
1770
|
+
const msg = entry.message ? chalk.gray(` — ${entry.message}`) : '';
|
|
1771
|
+
if (options.verify && entry.hmac !== undefined) {
|
|
1772
|
+
const valid = logs.verifyEntry(entry);
|
|
1773
|
+
if (!valid) {
|
|
1774
|
+
failed++;
|
|
1775
|
+
console.log(`${ts} ${chalk.red('[TAMPERED]')} ${op} ${src}${varName}${msg}`);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
console.log(`${ts} ${op} ${src}${varName}${msg}`);
|
|
1780
|
+
});
|
|
1781
|
+
if (options.verify) {
|
|
1782
|
+
if (failed === 0) {
|
|
1783
|
+
console.log(chalk.green(`\nAll ${entries.length} entries verified OK.`));
|
|
1784
|
+
}
|
|
1785
|
+
else {
|
|
1786
|
+
console.log(chalk.red(`\n${failed}/${entries.length} entries failed HMAC verification.`));
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
program
|
|
1791
|
+
.command('verify-logs')
|
|
1792
|
+
.description('Verify HMAC chain integrity of audit logs')
|
|
1793
|
+
.option('--date <date>', 'Verify specific date (YYYY-MM-DD, default: all dates)')
|
|
1794
|
+
.action(async (options) => {
|
|
1795
|
+
const projectPath = process.cwd();
|
|
1796
|
+
const config = await loadConfig(projectPath);
|
|
1797
|
+
const logDir = path.join(projectPath, '.envcp', 'logs');
|
|
1798
|
+
const logs = new LogManager(logDir, config.audit);
|
|
1799
|
+
await logs.init();
|
|
1800
|
+
if (!config.audit.hmac_chain) {
|
|
1801
|
+
console.log(chalk.yellow('HMAC chain is not enabled in audit config.'));
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
console.log(chalk.bold('Verifying log chain integrity...'));
|
|
1805
|
+
const result = await logs.verifyLogChain(options.date);
|
|
1806
|
+
if (result.valid) {
|
|
1807
|
+
console.log(chalk.green(`✓ Chain integrity verified: ${result.entries} entries OK`));
|
|
1808
|
+
}
|
|
1809
|
+
else {
|
|
1810
|
+
console.log(chalk.red(`✗ Chain integrity FAILED: ${result.tampered.length}/${result.entries} entries tampered`));
|
|
1811
|
+
console.log(chalk.red(` Tampered indices: ${result.tampered.join(', ')}`));
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
program
|
|
1815
|
+
.command('protect-logs')
|
|
1816
|
+
.description('Apply OS-level protection to audit log files (Linux only)')
|
|
1817
|
+
.action(async () => {
|
|
1818
|
+
const projectPath = process.cwd();
|
|
1819
|
+
const config = await loadConfig(projectPath);
|
|
1820
|
+
const logDir = path.join(projectPath, '.envcp', 'logs');
|
|
1821
|
+
const logs = new LogManager(logDir, config.audit);
|
|
1822
|
+
await logs.init();
|
|
1823
|
+
if (config.audit.protection === 'none') {
|
|
1824
|
+
console.log(chalk.yellow('Log protection is disabled in config (protection: none).'));
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
console.log(chalk.bold(`Applying ${config.audit.protection} protection to log files...`));
|
|
1828
|
+
const result = await logs.protectLogFiles();
|
|
1829
|
+
if (result.protected.length > 0) {
|
|
1830
|
+
console.log(chalk.green(`✓ Protected ${result.protected.length} files:`));
|
|
1831
|
+
result.protected.forEach((f) => console.log(chalk.gray(` ${f}`)));
|
|
1832
|
+
}
|
|
1833
|
+
if (result.failed.length > 0) {
|
|
1834
|
+
console.log(chalk.red(`✗ Failed to protect ${result.failed.length} files:`));
|
|
1835
|
+
result.failed.forEach((f) => console.log(chalk.gray(` ${f}`)));
|
|
1836
|
+
}
|
|
1837
|
+
if (result.protected.length === 0 && result.failed.length === 0) {
|
|
1838
|
+
console.log(chalk.gray('No log files to protect.'));
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1425
1841
|
program.parse();
|
|
1426
1842
|
//# sourceMappingURL=index.js.map
|