@fentz26/envcp 1.1.0 → 1.2.0-exp.8d5b91b
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 +65 -421
- package/VERSION +1 -0
- package/dist/adapters/base.d.ts +31 -3
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +235 -26
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/gemini.d.ts +1 -1
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +12 -7
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +14 -9
- package/dist/adapters/openai.js.map +1 -1
- package/dist/adapters/rest.d.ts +3 -1
- package/dist/adapters/rest.d.ts.map +1 -1
- package/dist/adapters/rest.js +99 -15
- package/dist/adapters/rest.js.map +1 -1
- package/dist/cli/index.js +678 -138
- 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 +20 -2
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +103 -26
- package/dist/config/manager.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +8 -4
- 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 +108 -39
- 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 +52 -10
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +350 -31
- package/dist/storage/index.js.map +1 -1
- package/dist/types.d.ts +574 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +79 -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/fs.d.ts +1 -0
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +12 -0
- package/dist/utils/fs.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 +2 -0
- package/dist/vault/index.d.ts.map +1 -1
- package/dist/vault/index.js +12 -2
- package/dist/vault/index.js.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,21 +1,40 @@
|
|
|
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, resolveLogPath } 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';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
+
import { LockoutManager } from '../utils/lockout.js';
|
|
18
|
+
import { initMemoryProtection } from '../utils/secure-memory.js';
|
|
19
|
+
import { getGlobalVaultPath, getProjectVaultPath, resolveVaultPath, resolveSessionPath, setActiveVault, listVaults, initNamedVault, } from '../vault/index.js';
|
|
20
|
+
import { findProjectRoot } from '../utils/fs.js';
|
|
21
|
+
initMemoryProtection();
|
|
22
|
+
async function resolveCliContext(vaultOverride) {
|
|
23
|
+
if (vaultOverride === 'global') {
|
|
24
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
25
|
+
const config = await loadConfig(home);
|
|
26
|
+
config.vault = { ...config.vault, mode: 'global' };
|
|
27
|
+
return { projectPath: home, config };
|
|
28
|
+
}
|
|
17
29
|
const projectPath = process.cwd();
|
|
18
30
|
const config = await loadConfig(projectPath);
|
|
31
|
+
if (vaultOverride === 'project') {
|
|
32
|
+
config.vault = { ...config.vault, mode: 'project' };
|
|
33
|
+
}
|
|
34
|
+
return { projectPath, config };
|
|
35
|
+
}
|
|
36
|
+
async function withSession(fn, vaultOverride) {
|
|
37
|
+
const { projectPath, config } = await resolveCliContext(vaultOverride);
|
|
19
38
|
const vaultPath = vaultOverride
|
|
20
39
|
? vaultOverride === 'global'
|
|
21
40
|
? getGlobalVaultPath(config)
|
|
@@ -23,27 +42,81 @@ async function withSession(fn, vaultOverride) {
|
|
|
23
42
|
: await resolveVaultPath(projectPath, config);
|
|
24
43
|
if (config.encryption?.enabled === false) {
|
|
25
44
|
const storage = new StorageManager(vaultPath, false);
|
|
26
|
-
|
|
45
|
+
const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
|
|
46
|
+
await logManager.init();
|
|
47
|
+
await fn(storage, '', config, projectPath, logManager);
|
|
27
48
|
return;
|
|
28
49
|
}
|
|
29
|
-
const sessionManager = new SessionManager(
|
|
50
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
30
51
|
await sessionManager.init();
|
|
52
|
+
// Initialize audit logging
|
|
53
|
+
const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
|
|
54
|
+
await logManager.init();
|
|
31
55
|
let session = await sessionManager.load();
|
|
32
56
|
let password = '';
|
|
33
57
|
if (!session) {
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
const authMethod = config.auth?.method ?? 'password';
|
|
59
|
+
// --- HSM-only or multi-factor auth ---
|
|
60
|
+
if (authMethod === 'hsm' || authMethod === 'multi') {
|
|
61
|
+
const hsm = HsmManager.fromConfig(config, projectPath);
|
|
62
|
+
const hsmAvailable = await hsm.isAvailable();
|
|
63
|
+
if (hsmAvailable) {
|
|
64
|
+
try {
|
|
65
|
+
const hsmSecret = await hsm.retrieveVaultPassword();
|
|
66
|
+
if (authMethod === 'multi') {
|
|
67
|
+
const factors = config.auth?.multi_factors ?? ['password', 'hsm'];
|
|
68
|
+
let userPassword = '';
|
|
69
|
+
if (factors.includes('password')) {
|
|
70
|
+
userPassword = await promptPassword('Enter password (multi-factor):');
|
|
71
|
+
}
|
|
72
|
+
else if (factors.includes('keychain')) {
|
|
73
|
+
const keychain = new KeychainManager(config.keychain?.service || 'envcp');
|
|
74
|
+
const stored = await keychain.retrievePassword(projectPath);
|
|
75
|
+
if (stored) {
|
|
76
|
+
userPassword = stored;
|
|
77
|
+
console.log(chalk.gray('Password retrieved from OS keychain (multi-factor)'));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
password = HsmManager.combineSecrets(hsmSecret, userPassword);
|
|
81
|
+
console.log(chalk.gray(`Authenticated via ${hsm.backendName} + password`));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
password = hsmSecret;
|
|
85
|
+
console.log(chalk.gray(`Authenticated via ${hsm.backendName}`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
+
const fallback = config.auth?.fallback ?? 'password';
|
|
91
|
+
if (fallback === 'none') {
|
|
92
|
+
console.log(chalk.red(`HSM authentication failed: ${msg}`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log(chalk.yellow(`HSM unavailable: ${msg}`));
|
|
96
|
+
console.log(chalk.gray('Falling back to password...'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const fallback = config.auth?.fallback ?? 'password';
|
|
101
|
+
if (fallback === 'none') {
|
|
102
|
+
console.log(chalk.red(`HSM device (${hsm.backendName}) not found. No fallback configured.`));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log(chalk.yellow(`HSM device (${hsm.backendName}) not available. Falling back to password...`));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// --- OS keychain ---
|
|
109
|
+
if (!password && (authMethod === 'keychain' || config.keychain?.enabled)) {
|
|
110
|
+
const keychain = new KeychainManager(config.keychain?.service || 'envcp');
|
|
36
111
|
const stored = await keychain.retrievePassword(projectPath);
|
|
37
112
|
if (stored) {
|
|
38
113
|
password = stored;
|
|
39
114
|
console.log(chalk.gray('Password retrieved from OS keychain'));
|
|
40
115
|
}
|
|
41
116
|
}
|
|
117
|
+
// --- Password prompt fallback ---
|
|
42
118
|
if (!password) {
|
|
43
|
-
|
|
44
|
-
{ type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
|
|
45
|
-
]);
|
|
46
|
-
password = answer.password;
|
|
119
|
+
password = await promptPassword('Enter password:');
|
|
47
120
|
const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
|
|
48
121
|
if (!passwordValid) {
|
|
49
122
|
console.log(chalk.red("Invalid password"));
|
|
@@ -59,13 +132,13 @@ async function withSession(fn, vaultOverride) {
|
|
|
59
132
|
const storage = new StorageManager(vaultPath, config.storage.encrypted);
|
|
60
133
|
if (password)
|
|
61
134
|
storage.setPassword(password);
|
|
62
|
-
await fn(storage, password, config, projectPath);
|
|
135
|
+
await fn(storage, password, config, projectPath, logManager);
|
|
63
136
|
}
|
|
64
137
|
const program = new Command();
|
|
65
138
|
program
|
|
66
139
|
.name('envcp')
|
|
67
140
|
.description('Secure environment variable management for AI-assisted coding')
|
|
68
|
-
.version(
|
|
141
|
+
.version(VERSION);
|
|
69
142
|
program
|
|
70
143
|
.command('init')
|
|
71
144
|
.description('Initialize EnvCP in the current project')
|
|
@@ -73,60 +146,98 @@ program
|
|
|
73
146
|
.option('--no-encrypt', 'Skip encryption (passwordless mode)')
|
|
74
147
|
.option('--skip-env', 'Skip .env auto-import')
|
|
75
148
|
.option('--skip-mcp', 'Skip MCP auto-registration')
|
|
149
|
+
.option('--auth-method <method>', 'Authentication method: password | keychain | hsm | multi (default: password)')
|
|
150
|
+
.option('--hsm-type <type>', 'HSM type for --auth-method hsm|multi: yubikey | gpg | pkcs11')
|
|
151
|
+
.option('--key-id <id>', 'GPG key ID or PKCS#11 key label for HSM auth')
|
|
152
|
+
.option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library for --hsm-type pkcs11')
|
|
153
|
+
.option('--global', 'Initialize a global vault at ~/.envcp/ (config + store live in $HOME)')
|
|
154
|
+
.option('--force', 'Overwrite existing envcp.yaml (DESTRUCTIVE: existing vault may become inaccessible)')
|
|
76
155
|
.action(async (options) => {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
156
|
+
const useGlobal = !!options.global;
|
|
157
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
158
|
+
const projectPath = useGlobal ? home : process.cwd();
|
|
159
|
+
const projectName = options.project || (useGlobal ? 'global' : path.basename(projectPath));
|
|
160
|
+
const configPath = path.join(projectPath, 'envcp.yaml');
|
|
161
|
+
if (await pathExists(configPath) && !options.force) {
|
|
162
|
+
const vaultRelPath = path.join('.envcp', 'store.enc');
|
|
163
|
+
console.log(chalk.yellow('EnvCP is already initialized here.'));
|
|
164
|
+
console.log(chalk.gray(` Config: ${configPath}`));
|
|
165
|
+
if (await pathExists(path.join(projectPath, vaultRelPath))) {
|
|
166
|
+
console.log(chalk.gray(` Vault: ${path.join(projectPath, vaultRelPath)}`));
|
|
167
|
+
}
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log(chalk.gray('Running init again would overwrite your config and may make'));
|
|
170
|
+
console.log(chalk.gray('existing encrypted variables permanently inaccessible.'));
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log(chalk.gray('To reconfigure: edit envcp.yaml directly'));
|
|
173
|
+
console.log(chalk.gray('To force re-init (DESTRUCTIVE): envcp init --force'));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
console.log(chalk.blue(useGlobal ? 'Initializing EnvCP (global vault)...' : 'Initializing EnvCP...'));
|
|
80
177
|
console.log('');
|
|
81
|
-
const config = await initConfig(projectPath, projectName);
|
|
178
|
+
const config = await initConfig(projectPath, projectName, { global: useGlobal });
|
|
82
179
|
// Single security question (or skip if --no-encrypt)
|
|
83
180
|
let securityChoice;
|
|
84
181
|
if (options.encrypt === false) {
|
|
85
182
|
securityChoice = 'none';
|
|
86
183
|
}
|
|
87
184
|
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;
|
|
185
|
+
securityChoice = await promptList('How would you like to secure your variables?', [
|
|
186
|
+
{ name: 'No encryption (fastest setup, for local dev)', value: 'none' },
|
|
187
|
+
{ name: 'Encrypted with recovery key (recommended)', value: 'recoverable' },
|
|
188
|
+
{ name: 'Encrypted hard-lock (max security, no recovery)', value: 'hard-lock' },
|
|
189
|
+
], 'recoverable');
|
|
102
190
|
}
|
|
103
191
|
// Apply security choice to config
|
|
104
192
|
if (securityChoice === 'none') {
|
|
105
193
|
config.encryption = { enabled: false };
|
|
106
194
|
config.storage.encrypted = false;
|
|
107
|
-
config.security = {
|
|
195
|
+
config.security = {
|
|
196
|
+
mode: 'recoverable',
|
|
197
|
+
recovery_file: '.envcp/.recovery',
|
|
198
|
+
brute_force_protection: {
|
|
199
|
+
enabled: true,
|
|
200
|
+
max_attempts: 5,
|
|
201
|
+
lockout_duration: 300,
|
|
202
|
+
progressive_delay: true,
|
|
203
|
+
max_delay: 60,
|
|
204
|
+
permanent_lockout_threshold: 50,
|
|
205
|
+
permanent_lockout_action: 'require_recovery_key',
|
|
206
|
+
notifications: {}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
108
209
|
}
|
|
109
210
|
else {
|
|
110
211
|
config.encryption = { enabled: true };
|
|
111
212
|
config.storage.encrypted = true;
|
|
112
|
-
config.security = {
|
|
213
|
+
config.security = {
|
|
214
|
+
mode: securityChoice,
|
|
215
|
+
recovery_file: '.envcp/.recovery',
|
|
216
|
+
brute_force_protection: {
|
|
217
|
+
enabled: true,
|
|
218
|
+
max_attempts: 5,
|
|
219
|
+
lockout_duration: 300,
|
|
220
|
+
progressive_delay: true,
|
|
221
|
+
max_delay: 60,
|
|
222
|
+
permanent_lockout_threshold: 50,
|
|
223
|
+
permanent_lockout_action: 'require_recovery_key',
|
|
224
|
+
notifications: {},
|
|
225
|
+
}
|
|
226
|
+
};
|
|
113
227
|
}
|
|
114
228
|
// For encrypted modes: get password now
|
|
115
229
|
let pwd = '';
|
|
116
230
|
if (securityChoice !== 'none') {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{ type: 'password', name: 'confirm', message: 'Confirm password:', mask: '*' }
|
|
122
|
-
]);
|
|
123
|
-
if (password !== confirm) {
|
|
231
|
+
const password = await promptPassword('Set encryption password:');
|
|
232
|
+
const confirmPwd = await promptPassword('Confirm password:');
|
|
233
|
+
// eslint-disable-next-line security/detect-possible-timing-attacks -- comparing two user-typed confirm fields, not a secret-vs-known value
|
|
234
|
+
if (password !== confirmPwd) {
|
|
124
235
|
console.log(chalk.red('Passwords do not match. Aborting.'));
|
|
125
236
|
return;
|
|
126
237
|
}
|
|
127
238
|
pwd = password;
|
|
128
239
|
}
|
|
129
|
-
await saveConfig(config, projectPath);
|
|
240
|
+
await saveConfig(config, projectPath, { global: useGlobal });
|
|
130
241
|
const modeLabel = securityChoice === 'none' ? 'no encryption' : securityChoice;
|
|
131
242
|
console.log(chalk.green('EnvCP initialized!'));
|
|
132
243
|
console.log(chalk.gray(` Project: ${config.project}`));
|
|
@@ -146,18 +257,20 @@ program
|
|
|
146
257
|
if (pwd)
|
|
147
258
|
storage.setPassword(pwd);
|
|
148
259
|
const now = new Date().toISOString();
|
|
260
|
+
const existing = await storage.load();
|
|
149
261
|
for (const [name, value] of Object.entries(vars)) {
|
|
150
|
-
|
|
262
|
+
existing[name] = {
|
|
151
263
|
name, value,
|
|
152
264
|
encrypted: config.storage.encrypted,
|
|
153
265
|
created: now, updated: now,
|
|
154
266
|
sync_to_env: true,
|
|
155
267
|
protected: false,
|
|
156
|
-
}
|
|
268
|
+
};
|
|
157
269
|
}
|
|
270
|
+
await storage.save(existing);
|
|
158
271
|
// Create session for encrypted mode
|
|
159
272
|
if (pwd) {
|
|
160
|
-
const sessionManager = new SessionManager(
|
|
273
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
161
274
|
await sessionManager.init();
|
|
162
275
|
await sessionManager.create(pwd);
|
|
163
276
|
}
|
|
@@ -202,6 +315,45 @@ program
|
|
|
202
315
|
console.log(chalk.gray(' No AI tools detected for auto-registration'));
|
|
203
316
|
}
|
|
204
317
|
}
|
|
318
|
+
// Setup HSM authentication if --auth-method hsm|multi provided
|
|
319
|
+
const authMethod = options.authMethod;
|
|
320
|
+
if (pwd && (authMethod === 'hsm' || authMethod === 'multi')) {
|
|
321
|
+
const hsmType = options.hsmType || 'yubikey';
|
|
322
|
+
config.hsm = {
|
|
323
|
+
...config.hsm,
|
|
324
|
+
enabled: true,
|
|
325
|
+
type: hsmType,
|
|
326
|
+
key_id: options.keyId ?? config.hsm?.key_id,
|
|
327
|
+
pkcs11_lib: options.pkcs11Lib ?? config.hsm?.pkcs11_lib,
|
|
328
|
+
require_touch: config.hsm?.require_touch ?? true,
|
|
329
|
+
protected_key_path: config.hsm?.protected_key_path ?? '.envcp/.hsm-key',
|
|
330
|
+
};
|
|
331
|
+
config.auth = {
|
|
332
|
+
method: authMethod,
|
|
333
|
+
multi_factors: authMethod === 'multi' ? ['password', 'hsm'] : ['hsm'],
|
|
334
|
+
fallback: 'password',
|
|
335
|
+
};
|
|
336
|
+
const hsm = HsmManager.fromConfig(config, projectPath);
|
|
337
|
+
if (await hsm.isAvailable()) {
|
|
338
|
+
try {
|
|
339
|
+
await hsm.protectVaultPassword(pwd);
|
|
340
|
+
await saveConfig(config, projectPath, { global: useGlobal });
|
|
341
|
+
console.log('');
|
|
342
|
+
console.log(chalk.green(` Vault password protected by ${hsm.backendName}`));
|
|
343
|
+
console.log(chalk.gray(` Future sessions will authenticate via hardware`));
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
console.log(chalk.yellow(` HSM setup warning: ${err instanceof Error ? err.message : String(err)}`));
|
|
347
|
+
console.log(chalk.gray(' Falling back to password authentication'));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
console.log(chalk.yellow(` HSM device (${hsm.backendName}) not available at init time.`));
|
|
352
|
+
console.log(chalk.gray(' Run "envcp unlock --setup-hsm" later to enable hardware authentication.'));
|
|
353
|
+
config.auth = { method: authMethod, multi_factors: ['password', 'hsm'], fallback: 'password' };
|
|
354
|
+
await saveConfig(config, projectPath, { global: useGlobal });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
205
357
|
console.log('');
|
|
206
358
|
console.log(chalk.green('Done! Your AI tools can now use EnvCP.'));
|
|
207
359
|
});
|
|
@@ -209,21 +361,18 @@ program
|
|
|
209
361
|
.command('unlock')
|
|
210
362
|
.description('Unlock EnvCP session with password')
|
|
211
363
|
.option('-p, --password <password>', 'Password (will prompt if not provided)')
|
|
364
|
+
.option('--recovery-key <key>', 'Recovery key to clear permanent lockout')
|
|
212
365
|
.option('--save-to-keychain', 'Save password to OS keychain for auto-unlock')
|
|
366
|
+
.option('--setup-hsm', 'Protect vault password with hardware security module')
|
|
367
|
+
.option('--hsm-type <type>', 'HSM type: yubikey | gpg | pkcs11 (default: yubikey)')
|
|
368
|
+
.option('--key-id <id>', 'GPG key ID or PKCS#11 key label')
|
|
369
|
+
.option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library (.so / .dll)')
|
|
370
|
+
.option('--global', 'Unlock the global vault at ~/.envcp')
|
|
213
371
|
.action(async (options) => {
|
|
214
|
-
const projectPath =
|
|
215
|
-
const config = await loadConfig(projectPath);
|
|
372
|
+
const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
|
|
216
373
|
let password = options.password;
|
|
217
374
|
if (!password) {
|
|
218
|
-
|
|
219
|
-
{
|
|
220
|
-
type: 'password',
|
|
221
|
-
name: 'password',
|
|
222
|
-
message: 'Enter password:',
|
|
223
|
-
mask: '*'
|
|
224
|
-
}
|
|
225
|
-
]);
|
|
226
|
-
password = answer.password;
|
|
375
|
+
password = await promptPassword('Enter password:');
|
|
227
376
|
}
|
|
228
377
|
const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
|
|
229
378
|
if (!passwordValid) {
|
|
@@ -234,16 +383,116 @@ program
|
|
|
234
383
|
if (passwordValid && passwordWarning) {
|
|
235
384
|
console.log(chalk.yellow('⚠ Weak password detected'));
|
|
236
385
|
}
|
|
237
|
-
const
|
|
386
|
+
const sessionDir = path.dirname(resolveSessionPath(projectPath, config));
|
|
387
|
+
let lockoutManager = new LockoutManager(path.join(sessionDir, '.lockout'));
|
|
388
|
+
// Handle recovery key if provided
|
|
389
|
+
if (options.recoveryKey) {
|
|
390
|
+
const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
|
|
391
|
+
if (!await pathExists(recoveryPath)) {
|
|
392
|
+
console.log(chalk.red('No recovery file found.'));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const recoveryData = await fs.readFile(recoveryPath, 'utf8');
|
|
396
|
+
try {
|
|
397
|
+
await recoverPassword(recoveryData, options.recoveryKey);
|
|
398
|
+
// Recovery key is valid - clear permanent lockout
|
|
399
|
+
await lockoutManager.clearPermanentLockout();
|
|
400
|
+
console.log(chalk.green('Permanent lockout cleared.'));
|
|
401
|
+
// Log recovery event
|
|
402
|
+
const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
|
|
403
|
+
await logManager.init();
|
|
404
|
+
await logManager.log({
|
|
405
|
+
timestamp: new Date().toISOString(),
|
|
406
|
+
operation: 'unlock',
|
|
407
|
+
variable: '',
|
|
408
|
+
source: 'cli',
|
|
409
|
+
success: true,
|
|
410
|
+
message: 'Permanent lockout cleared with recovery key (via --recovery-key flag)',
|
|
411
|
+
session_id: '',
|
|
412
|
+
client_id: 'cli',
|
|
413
|
+
client_type: 'terminal',
|
|
414
|
+
ip: '127.0.0.1'
|
|
415
|
+
});
|
|
416
|
+
console.log(chalk.yellow('Note: You still need to enter the correct password.'));
|
|
417
|
+
// Continue with password prompt
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
console.log(chalk.red('Invalid recovery key.'));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Use new brute_force_protection config if available, fall back to session config
|
|
425
|
+
const bfpConfig = config.security?.brute_force_protection;
|
|
426
|
+
const lockoutThreshold = bfpConfig?.max_attempts ?? config.session?.lockout_threshold ?? 5;
|
|
427
|
+
const lockoutBaseSeconds = bfpConfig?.lockout_duration ?? config.session?.lockout_base_seconds ?? 60;
|
|
428
|
+
const progressiveDelay = bfpConfig?.progressive_delay ?? true;
|
|
429
|
+
const maxDelay = bfpConfig?.max_delay ?? 60;
|
|
430
|
+
const permanentThreshold = bfpConfig?.permanent_lockout_threshold ?? 0;
|
|
431
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
238
432
|
await sessionManager.init();
|
|
239
|
-
|
|
433
|
+
// Initialize audit logging
|
|
434
|
+
const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
|
|
435
|
+
await logManager.init();
|
|
436
|
+
const vaultPathForUnlock = await resolveVaultPath(projectPath, config);
|
|
437
|
+
const storage = new StorageManager(vaultPathForUnlock, config.storage.encrypted);
|
|
240
438
|
storage.setPassword(password);
|
|
241
439
|
const storeExists = await storage.exists();
|
|
440
|
+
// Check lockout before attempting password verification on existing stores
|
|
441
|
+
if (storeExists) {
|
|
442
|
+
const lockoutStatus = await lockoutManager.check();
|
|
443
|
+
if (lockoutStatus.locked) {
|
|
444
|
+
if (lockoutStatus.permanent_locked) {
|
|
445
|
+
console.log(chalk.red.bold('PERMANENT LOCKOUT: Too many failed attempts.'));
|
|
446
|
+
console.log(chalk.red('Recovery key or administrator intervention required.'));
|
|
447
|
+
const useRecovery = await promptConfirm('Use recovery key to clear lockout?', true);
|
|
448
|
+
if (useRecovery) {
|
|
449
|
+
const recoveryKey = await promptPassword('Enter recovery key:');
|
|
450
|
+
const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
|
|
451
|
+
if (!await pathExists(recoveryPath)) {
|
|
452
|
+
console.log(chalk.red('No recovery file found.'));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const recoveryData = await fs.readFile(recoveryPath, 'utf8');
|
|
456
|
+
try {
|
|
457
|
+
await recoverPassword(recoveryData, recoveryKey);
|
|
458
|
+
// Recovery key is valid - clear permanent lockout
|
|
459
|
+
await lockoutManager.clearPermanentLockout();
|
|
460
|
+
console.log(chalk.green('Permanent lockout cleared. You can now attempt to unlock.'));
|
|
461
|
+
console.log(chalk.yellow('Note: You still need to enter the correct password.'));
|
|
462
|
+
// Log recovery event
|
|
463
|
+
await logManager.log({
|
|
464
|
+
timestamp: new Date().toISOString(),
|
|
465
|
+
operation: 'unlock',
|
|
466
|
+
variable: '',
|
|
467
|
+
source: 'cli',
|
|
468
|
+
success: true,
|
|
469
|
+
message: 'Permanent lockout cleared with recovery key',
|
|
470
|
+
session_id: '',
|
|
471
|
+
client_id: 'cli',
|
|
472
|
+
client_type: 'terminal',
|
|
473
|
+
ip: '127.0.0.1'
|
|
474
|
+
});
|
|
475
|
+
// Continue with password prompt
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
console.log(chalk.red('Invalid recovery key.'));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
console.log(chalk.red(`Too many failed attempts. Try again in ${lockoutStatus.remaining_seconds} second(s).`));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
242
492
|
if (!storeExists) {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (confirm.password !== password) {
|
|
493
|
+
const confirmPasswordValue = await promptPassword('Confirm password:');
|
|
494
|
+
// eslint-disable-next-line security/detect-possible-timing-attacks -- comparing two user-typed confirm fields, not a secret-vs-known value
|
|
495
|
+
if (confirmPasswordValue !== password) {
|
|
247
496
|
console.log(chalk.red('Passwords do not match'));
|
|
248
497
|
return;
|
|
249
498
|
}
|
|
@@ -267,10 +516,85 @@ program
|
|
|
267
516
|
await storage.load();
|
|
268
517
|
}
|
|
269
518
|
catch (error) {
|
|
270
|
-
|
|
519
|
+
if (storeExists) {
|
|
520
|
+
const status = await lockoutManager.recordFailure(lockoutThreshold, lockoutBaseSeconds, progressiveDelay, maxDelay, permanentThreshold);
|
|
521
|
+
// Log the failed attempt
|
|
522
|
+
await logManager.log({
|
|
523
|
+
timestamp: new Date().toISOString(),
|
|
524
|
+
operation: 'auth_failure',
|
|
525
|
+
variable: '',
|
|
526
|
+
source: 'cli',
|
|
527
|
+
success: false,
|
|
528
|
+
message: `Failed unlock attempt (attempt ${status.attempts})`,
|
|
529
|
+
session_id: '',
|
|
530
|
+
client_id: 'cli',
|
|
531
|
+
client_type: 'terminal',
|
|
532
|
+
ip: '127.0.0.1'
|
|
533
|
+
});
|
|
534
|
+
if (status.permanent_locked) {
|
|
535
|
+
console.log(chalk.red.bold('PERMANENT LOCKOUT TRIGGERED: Too many failed attempts.'));
|
|
536
|
+
console.log(chalk.red('Recovery key or administrator intervention required.'));
|
|
537
|
+
// Log permanent lockout event
|
|
538
|
+
await logManager.log({
|
|
539
|
+
timestamp: new Date().toISOString(),
|
|
540
|
+
operation: 'permanent_lockout',
|
|
541
|
+
variable: '',
|
|
542
|
+
source: 'cli',
|
|
543
|
+
success: false,
|
|
544
|
+
message: `Permanent lockout triggered after ${status.permanent_lockout_count} lockouts`,
|
|
545
|
+
session_id: '',
|
|
546
|
+
client_id: 'cli',
|
|
547
|
+
client_type: 'terminal',
|
|
548
|
+
ip: '127.0.0.1'
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
else if (status.locked) {
|
|
552
|
+
console.log(chalk.red(`Invalid password. Too many failed attempts — locked out for ${status.remaining_seconds} second(s).`));
|
|
553
|
+
// Log lockout event
|
|
554
|
+
await logManager.log({
|
|
555
|
+
timestamp: new Date().toISOString(),
|
|
556
|
+
operation: 'lockout_triggered',
|
|
557
|
+
variable: '',
|
|
558
|
+
source: 'cli',
|
|
559
|
+
success: false,
|
|
560
|
+
message: `Lockout triggered for ${status.remaining_seconds}s (lockout #${status.lockout_count})`,
|
|
561
|
+
session_id: '',
|
|
562
|
+
client_id: 'cli',
|
|
563
|
+
client_type: 'terminal',
|
|
564
|
+
ip: '127.0.0.1'
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const remaining = lockoutThreshold - status.attempts;
|
|
569
|
+
let message = `Invalid password. ${remaining} attempt(s) remaining before lockout.`;
|
|
570
|
+
// Show progressive delay if applied
|
|
571
|
+
if (status.delay_seconds && status.delay_seconds > 0) {
|
|
572
|
+
message += ` (Delayed ${status.delay_seconds}s)`;
|
|
573
|
+
}
|
|
574
|
+
console.log(chalk.red(message));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
console.log(chalk.red('Invalid password'));
|
|
579
|
+
}
|
|
271
580
|
return;
|
|
272
581
|
}
|
|
582
|
+
// Successful unlock — clear any lockout state
|
|
583
|
+
await lockoutManager.reset();
|
|
273
584
|
const session = await sessionManager.create(password);
|
|
585
|
+
// Log successful unlock
|
|
586
|
+
await logManager.log({
|
|
587
|
+
timestamp: new Date().toISOString(),
|
|
588
|
+
operation: 'unlock',
|
|
589
|
+
variable: '',
|
|
590
|
+
source: 'cli',
|
|
591
|
+
success: true,
|
|
592
|
+
message: 'Session unlocked successfully',
|
|
593
|
+
session_id: session.id,
|
|
594
|
+
client_id: 'cli',
|
|
595
|
+
client_type: 'terminal',
|
|
596
|
+
ip: '127.0.0.1'
|
|
597
|
+
});
|
|
274
598
|
console.log(chalk.green('Session unlocked!'));
|
|
275
599
|
console.log(chalk.gray(` Session ID: ${session.id}`));
|
|
276
600
|
console.log(chalk.gray(` Expires in: ${config.session?.timeout_minutes || 30} minutes`));
|
|
@@ -296,14 +620,43 @@ program
|
|
|
296
620
|
console.log(chalk.red(`OS keychain not available (${keychain.backendName})`));
|
|
297
621
|
}
|
|
298
622
|
}
|
|
623
|
+
// Setup HSM if requested
|
|
624
|
+
if (options.setupHsm) {
|
|
625
|
+
const hsmType = options.hsmType || 'yubikey';
|
|
626
|
+
config.hsm = {
|
|
627
|
+
...config.hsm,
|
|
628
|
+
enabled: true,
|
|
629
|
+
type: hsmType,
|
|
630
|
+
key_id: options.keyId ?? config.hsm?.key_id,
|
|
631
|
+
pkcs11_lib: options.pkcs11Lib ?? config.hsm?.pkcs11_lib,
|
|
632
|
+
require_touch: config.hsm?.require_touch ?? true,
|
|
633
|
+
protected_key_path: config.hsm?.protected_key_path ?? '.envcp/.hsm-key',
|
|
634
|
+
};
|
|
635
|
+
config.auth = { ...config.auth, method: 'hsm', fallback: config.auth?.fallback ?? 'password' };
|
|
636
|
+
const hsm = HsmManager.fromConfig(config, projectPath);
|
|
637
|
+
if (!await hsm.isAvailable()) {
|
|
638
|
+
console.log(chalk.red(`HSM device (${hsm.backendName}) not available. Aborting HSM setup.`));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
await hsm.protectVaultPassword(password);
|
|
643
|
+
await saveConfig(config, projectPath);
|
|
644
|
+
console.log(chalk.green(`Vault password protected by ${hsm.backendName}`));
|
|
645
|
+
console.log(chalk.gray(` Key file: ${config.hsm.protected_key_path}`));
|
|
646
|
+
console.log(chalk.gray(' Future sessions will authenticate via hardware'));
|
|
647
|
+
}
|
|
648
|
+
catch (err) {
|
|
649
|
+
console.log(chalk.red(`HSM setup failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
299
652
|
});
|
|
300
653
|
program
|
|
301
654
|
.command('lock')
|
|
302
655
|
.description('Lock EnvCP session')
|
|
303
|
-
.
|
|
304
|
-
|
|
305
|
-
const config = await
|
|
306
|
-
const sessionManager = new SessionManager(
|
|
656
|
+
.option('--global', 'Lock the global vault session at ~/.envcp/.session')
|
|
657
|
+
.action(async (options) => {
|
|
658
|
+
const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
|
|
659
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
307
660
|
await sessionManager.init();
|
|
308
661
|
await sessionManager.destroy();
|
|
309
662
|
console.log(chalk.green('Session locked'));
|
|
@@ -311,15 +664,13 @@ program
|
|
|
311
664
|
program
|
|
312
665
|
.command('status')
|
|
313
666
|
.description('Check session status')
|
|
314
|
-
.
|
|
315
|
-
|
|
316
|
-
const config = await
|
|
317
|
-
const sessionManager = new SessionManager(
|
|
667
|
+
.option('--global', 'Check status of the global vault session at ~/.envcp/.session')
|
|
668
|
+
.action(async (options) => {
|
|
669
|
+
const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
|
|
670
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
318
671
|
await sessionManager.init();
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
]);
|
|
322
|
-
const session = await sessionManager.load(answer.password);
|
|
672
|
+
const password = await promptPassword('Enter password:');
|
|
673
|
+
const session = await sessionManager.load(password);
|
|
323
674
|
if (!session) {
|
|
324
675
|
console.log(chalk.yellow('No active session (expired, invalid password, or not unlocked)'));
|
|
325
676
|
console.log(chalk.gray('Run: envcp unlock'));
|
|
@@ -340,9 +691,7 @@ program
|
|
|
340
691
|
.action(async () => {
|
|
341
692
|
const projectPath = process.cwd();
|
|
342
693
|
const configGuard = new ConfigGuard(projectPath);
|
|
343
|
-
const
|
|
344
|
-
{ type: 'password', name: 'password', message: 'Enter password to reload config:', mask: '*' }
|
|
345
|
-
]);
|
|
694
|
+
const password = await promptPassword('Enter password to reload config:');
|
|
346
695
|
const result = await configGuard.reload(password);
|
|
347
696
|
if (result.success) {
|
|
348
697
|
console.log(chalk.green('Config reloaded successfully'));
|
|
@@ -355,15 +704,13 @@ program
|
|
|
355
704
|
program
|
|
356
705
|
.command('extend')
|
|
357
706
|
.description('Extend session timeout')
|
|
358
|
-
.
|
|
359
|
-
|
|
360
|
-
const config = await
|
|
361
|
-
const sessionManager = new SessionManager(
|
|
707
|
+
.option('--global', 'Extend the global vault session at ~/.envcp/.session')
|
|
708
|
+
.action(async (options) => {
|
|
709
|
+
const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
|
|
710
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
362
711
|
await sessionManager.init();
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
]);
|
|
366
|
-
const loaded = await sessionManager.load(answer.password);
|
|
712
|
+
const password = await promptPassword('Enter password:');
|
|
713
|
+
const loaded = await sessionManager.load(password);
|
|
367
714
|
if (!loaded) {
|
|
368
715
|
console.log(chalk.red('Cannot extend session. No active session or invalid password.'));
|
|
369
716
|
return;
|
|
@@ -394,9 +741,7 @@ program
|
|
|
394
741
|
console.log(chalk.red('No recovery file found. Recovery is not available.'));
|
|
395
742
|
return;
|
|
396
743
|
}
|
|
397
|
-
const
|
|
398
|
-
{ type: 'password', name: 'recoveryKey', message: 'Enter your recovery key:', mask: '*' }
|
|
399
|
-
]);
|
|
744
|
+
const recoveryKey = await promptPassword('Enter your recovery key:');
|
|
400
745
|
const recoveryData = await fs.readFile(recoveryPath, 'utf8');
|
|
401
746
|
let oldPassword;
|
|
402
747
|
try {
|
|
@@ -419,12 +764,8 @@ program
|
|
|
419
764
|
}
|
|
420
765
|
console.log(chalk.green('Recovery key verified. Store contains ' + Object.keys(variables).length + ' variables.'));
|
|
421
766
|
// Set new password
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
]);
|
|
425
|
-
const { confirmPassword } = await inquirer.prompt([
|
|
426
|
-
{ type: 'password', name: 'confirmPassword', message: 'Confirm new password:', mask: '*' }
|
|
427
|
-
]);
|
|
767
|
+
const newPassword = await promptPassword('Set new password:');
|
|
768
|
+
const confirmPassword = await promptPassword('Confirm new password:');
|
|
428
769
|
if (newPassword !== confirmPassword) {
|
|
429
770
|
console.log(chalk.red('Passwords do not match'));
|
|
430
771
|
return;
|
|
@@ -443,7 +784,7 @@ program
|
|
|
443
784
|
console.log(chalk.yellow.bold(` ${newRecoveryKey}`));
|
|
444
785
|
console.log(chalk.gray('Your old recovery key no longer works.'));
|
|
445
786
|
// Create a session with new password
|
|
446
|
-
const sessionManager = new SessionManager(
|
|
787
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
447
788
|
await sessionManager.init();
|
|
448
789
|
await sessionManager.create(newPassword);
|
|
449
790
|
console.log(chalk.green('Session unlocked with new password.'));
|
|
@@ -480,27 +821,82 @@ program
|
|
|
480
821
|
program
|
|
481
822
|
.command('add <name>')
|
|
482
823
|
.description('Add a new environment variable')
|
|
483
|
-
.option('-v, --value <value>', 'Variable value')
|
|
824
|
+
.option('-v, --value <value>', 'Variable value (WARNING: leaks in shell history; prefer --from-env/--from-file/--stdin)')
|
|
825
|
+
.option('--from-env <envVar>', 'Read value from the named environment variable')
|
|
826
|
+
.option('--from-file <path>', 'Read value from a file (trailing newline trimmed)')
|
|
827
|
+
.option('--stdin', 'Read value from piped stdin')
|
|
484
828
|
.option('-t, --tags <tags>', 'Tags (comma-separated)')
|
|
485
829
|
.option('-d, --description <desc>', 'Description')
|
|
486
830
|
.action(async (name, options) => {
|
|
831
|
+
const sourceFlags = [
|
|
832
|
+
options.value !== undefined ? '--value' : null,
|
|
833
|
+
options.fromEnv !== undefined ? '--from-env' : null,
|
|
834
|
+
options.fromFile !== undefined ? '--from-file' : null,
|
|
835
|
+
options.stdin ? '--stdin' : null,
|
|
836
|
+
].filter(Boolean);
|
|
837
|
+
if (sourceFlags.length > 1) {
|
|
838
|
+
console.error(chalk.red(`Error: ${sourceFlags.join(', ')} are mutually exclusive — pick one`));
|
|
839
|
+
process.exit(1);
|
|
840
|
+
}
|
|
841
|
+
let value;
|
|
842
|
+
let sourced = false;
|
|
843
|
+
if (options.value !== undefined) {
|
|
844
|
+
value = options.value;
|
|
845
|
+
sourced = true;
|
|
846
|
+
if (process.stdout.isTTY) {
|
|
847
|
+
console.warn(chalk.yellow('⚠ --value is visible in shell history and process list. Use --from-env, --from-file, or --stdin for secrets.'));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
else if (options.fromEnv) {
|
|
851
|
+
const envValue = process.env[options.fromEnv];
|
|
852
|
+
if (envValue === undefined) {
|
|
853
|
+
console.error(chalk.red(`Error: environment variable '${options.fromEnv}' is not set`));
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
value = envValue;
|
|
857
|
+
sourced = true;
|
|
858
|
+
}
|
|
859
|
+
else if (options.fromFile) {
|
|
860
|
+
try {
|
|
861
|
+
const raw = await fs.readFile(options.fromFile, 'utf-8');
|
|
862
|
+
value = raw.replace(/\r?\n$/, '');
|
|
863
|
+
sourced = true;
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
867
|
+
console.error(chalk.red(`Error: cannot read '${options.fromFile}': ${msg}`));
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
else if (options.stdin) {
|
|
872
|
+
if (process.stdin.isTTY) {
|
|
873
|
+
console.error(chalk.red('Error: --stdin requires piped input (e.g., `echo "$SECRET" | envcp add NAME --stdin`)'));
|
|
874
|
+
process.exit(1);
|
|
875
|
+
}
|
|
876
|
+
value = await new Promise((resolve, reject) => {
|
|
877
|
+
const chunks = [];
|
|
878
|
+
process.stdin.on('data', (c) => chunks.push(typeof c === 'string' ? Buffer.from(c) : c));
|
|
879
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8').replace(/\r?\n$/, '')));
|
|
880
|
+
process.stdin.on('error', reject);
|
|
881
|
+
});
|
|
882
|
+
sourced = true;
|
|
883
|
+
}
|
|
487
884
|
await withSession(async (storage, _password, config) => {
|
|
488
|
-
let value = options.value;
|
|
489
885
|
let tags = [];
|
|
490
886
|
let description = options.description;
|
|
491
|
-
if (!
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
]);
|
|
497
|
-
value = answers.value;
|
|
498
|
-
tags = answers.tags.split(',').map((t) => t.trim()).filter(Boolean);
|
|
499
|
-
description = answers.description;
|
|
887
|
+
if (!sourced) {
|
|
888
|
+
value = await promptPassword('Enter value:');
|
|
889
|
+
const tagsInput = await promptInput('Tags (comma-separated):');
|
|
890
|
+
tags = tagsInput.split(',').map((t) => t.trim()).filter(Boolean);
|
|
891
|
+
description = await promptInput('Description:');
|
|
500
892
|
}
|
|
501
893
|
else if (options.tags) {
|
|
502
894
|
tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
|
|
503
895
|
}
|
|
896
|
+
if (value === undefined) {
|
|
897
|
+
console.error(chalk.red('Error: no value provided'));
|
|
898
|
+
process.exit(1);
|
|
899
|
+
}
|
|
504
900
|
const now = new Date().toISOString();
|
|
505
901
|
const variable = {
|
|
506
902
|
name,
|
|
@@ -606,7 +1002,8 @@ program
|
|
|
606
1002
|
if (!variable.sync_to_env)
|
|
607
1003
|
continue;
|
|
608
1004
|
const excluded = config.sync.exclude?.some((pattern) => {
|
|
609
|
-
|
|
1005
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- glob pattern from config; metacharacters escaped above
|
|
1006
|
+
const regex = new RegExp('^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$');
|
|
610
1007
|
return regex.test(name);
|
|
611
1008
|
});
|
|
612
1009
|
if (excluded)
|
|
@@ -631,7 +1028,8 @@ program
|
|
|
631
1028
|
if (!variable.sync_to_env)
|
|
632
1029
|
continue;
|
|
633
1030
|
const excluded = config.sync.exclude?.some((pattern) => {
|
|
634
|
-
|
|
1031
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- glob pattern from config; metacharacters escaped above
|
|
1032
|
+
const regex = new RegExp('^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$');
|
|
635
1033
|
return regex.test(name);
|
|
636
1034
|
});
|
|
637
1035
|
if (excluded)
|
|
@@ -680,10 +1078,40 @@ program
|
|
|
680
1078
|
.option('--port <port>', 'HTTP port (for non-MCP modes)', '3456')
|
|
681
1079
|
.option('--host <host>', 'HTTP host', '127.0.0.1')
|
|
682
1080
|
.option('-k, --api-key <key>', 'API key for HTTP authentication')
|
|
1081
|
+
.option('--global', 'Force the global vault at ~/.envcp (skip project lookup)')
|
|
683
1082
|
.action(async (options) => {
|
|
684
|
-
const
|
|
1083
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
1084
|
+
const globalConfigPath = path.join(home, '.envcp', 'config.yaml');
|
|
1085
|
+
// Resolve where the vault lives: --global forces home; otherwise walk up
|
|
1086
|
+
// from cwd looking for envcp.yaml; otherwise fall back to a global install.
|
|
1087
|
+
let projectPath;
|
|
1088
|
+
let forceGlobalMode = false;
|
|
1089
|
+
if (options.global) {
|
|
1090
|
+
projectPath = home;
|
|
1091
|
+
forceGlobalMode = true;
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
const found = await findProjectRoot(process.cwd());
|
|
1095
|
+
if (found) {
|
|
1096
|
+
projectPath = found;
|
|
1097
|
+
}
|
|
1098
|
+
else if (await pathExists(globalConfigPath)) {
|
|
1099
|
+
projectPath = home;
|
|
1100
|
+
forceGlobalMode = true;
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
process.stderr.write('Error: No envcp.yaml found in cwd or any ancestor, and no global config at ~/.envcp/config.yaml.\n' +
|
|
1104
|
+
'Run `envcp init` (project) or `envcp init --global` first.\n');
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
685
1108
|
const configGuard = new ConfigGuard(projectPath);
|
|
686
1109
|
const config = await configGuard.loadAndLock();
|
|
1110
|
+
if (forceGlobalMode) {
|
|
1111
|
+
config.vault = { ...config.vault, mode: 'global' };
|
|
1112
|
+
}
|
|
1113
|
+
const vaultPath = await resolveVaultPath(projectPath, config);
|
|
1114
|
+
const sessionPath = resolveSessionPath(projectPath, config);
|
|
687
1115
|
const mode = options.mode;
|
|
688
1116
|
const port = parseInt(options.port, 10);
|
|
689
1117
|
const host = options.host;
|
|
@@ -693,26 +1121,23 @@ program
|
|
|
693
1121
|
if (config.encryption?.enabled === false) {
|
|
694
1122
|
if (mode === 'mcp') {
|
|
695
1123
|
const { EnvCPServer } = await import('../mcp/server.js');
|
|
696
|
-
const server = new EnvCPServer(config, projectPath);
|
|
1124
|
+
const server = new EnvCPServer(config, projectPath, undefined, vaultPath, sessionPath);
|
|
697
1125
|
await server.start();
|
|
698
1126
|
return;
|
|
699
1127
|
}
|
|
700
1128
|
}
|
|
701
1129
|
else {
|
|
702
1130
|
// Encrypted mode: need password
|
|
703
|
-
const sessionManager = new SessionManager(
|
|
1131
|
+
const sessionManager = new SessionManager(sessionPath, config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
704
1132
|
await sessionManager.init();
|
|
705
1133
|
let session = await sessionManager.load();
|
|
706
1134
|
if (!session && !password) {
|
|
707
1135
|
// MCP mode uses stdio — can't prompt interactively
|
|
708
1136
|
if (mode === 'mcp') {
|
|
709
|
-
process.stderr.write(
|
|
1137
|
+
process.stderr.write(`Error: No active session at ${sessionPath}. Run \`envcp unlock${forceGlobalMode ? ' --global' : ''}\` first, or use --password flag.\n`);
|
|
710
1138
|
process.exit(1);
|
|
711
1139
|
}
|
|
712
|
-
|
|
713
|
-
{ type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
|
|
714
|
-
]);
|
|
715
|
-
password = answer.password;
|
|
1140
|
+
password = await promptPassword('Enter password:');
|
|
716
1141
|
const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
|
|
717
1142
|
if (!passwordValid) {
|
|
718
1143
|
// nosem: no tainted data flows to log
|
|
@@ -728,7 +1153,7 @@ program
|
|
|
728
1153
|
// MCP mode uses stdio
|
|
729
1154
|
if (mode === 'mcp') {
|
|
730
1155
|
const { EnvCPServer } = await import('../mcp/server.js');
|
|
731
|
-
const server = new EnvCPServer(config, projectPath, password);
|
|
1156
|
+
const server = new EnvCPServer(config, projectPath, password, vaultPath, sessionPath);
|
|
732
1157
|
await server.start();
|
|
733
1158
|
return;
|
|
734
1159
|
}
|
|
@@ -804,12 +1229,8 @@ program
|
|
|
804
1229
|
console.log(chalk.red('--output <path> is required with --encrypted'));
|
|
805
1230
|
return;
|
|
806
1231
|
}
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
]);
|
|
810
|
-
const { confirmExport } = await inquirer.prompt([
|
|
811
|
-
{ type: 'password', name: 'confirmExport', message: 'Confirm export password:', mask: '*' }
|
|
812
|
-
]);
|
|
1232
|
+
const exportPassword = await promptPassword('Set export password:');
|
|
1233
|
+
const confirmExport = await promptPassword('Confirm export password:');
|
|
813
1234
|
if (exportPassword !== confirmExport) {
|
|
814
1235
|
console.log(chalk.red('Passwords do not match'));
|
|
815
1236
|
return;
|
|
@@ -857,9 +1278,7 @@ program
|
|
|
857
1278
|
console.log(chalk.red(`File not found: ${file}`));
|
|
858
1279
|
return;
|
|
859
1280
|
}
|
|
860
|
-
const
|
|
861
|
-
{ type: 'password', name: 'importPassword', message: 'Enter export file password:', mask: '*' }
|
|
862
|
-
]);
|
|
1281
|
+
const importPassword = await promptPassword('Enter export file password:');
|
|
863
1282
|
const fileContent = await fs.readFile(file, 'utf8');
|
|
864
1283
|
let importData;
|
|
865
1284
|
try {
|
|
@@ -920,9 +1339,7 @@ program
|
|
|
920
1339
|
console.log(chalk.gray('\nNo files were modified.'));
|
|
921
1340
|
return;
|
|
922
1341
|
}
|
|
923
|
-
const
|
|
924
|
-
{ type: 'confirm', name: 'confirm', message: options.merge ? 'Merge into current store?' : 'Replace current store?', default: false }
|
|
925
|
-
]);
|
|
1342
|
+
const confirm = await promptConfirm(options.merge ? 'Merge into current store?' : 'Replace current store?', false);
|
|
926
1343
|
if (!confirm) {
|
|
927
1344
|
console.log(chalk.yellow('Import cancelled'));
|
|
928
1345
|
return;
|
|
@@ -1004,9 +1421,7 @@ program
|
|
|
1004
1421
|
console.log(chalk.gray(` Created: ${meta.timestamp}`));
|
|
1005
1422
|
console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
|
|
1006
1423
|
}
|
|
1007
|
-
const
|
|
1008
|
-
{ type: 'confirm', name: 'confirm', message: options.merge ? 'Merge backup into current store?' : 'Replace current store with backup?', default: false }
|
|
1009
|
-
]);
|
|
1424
|
+
const confirm = await promptConfirm(options.merge ? 'Merge backup into current store?' : 'Replace current store with backup?', false);
|
|
1010
1425
|
if (!confirm) {
|
|
1011
1426
|
console.log(chalk.yellow('Restore cancelled'));
|
|
1012
1427
|
return;
|
|
@@ -1049,7 +1464,7 @@ program
|
|
|
1049
1464
|
}
|
|
1050
1465
|
// 5. Session status
|
|
1051
1466
|
if (encrypted) {
|
|
1052
|
-
const sessionManager = new SessionManager(
|
|
1467
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
1053
1468
|
await sessionManager.init();
|
|
1054
1469
|
const session = await sessionManager.load();
|
|
1055
1470
|
if (session) {
|
|
@@ -1168,7 +1583,7 @@ program
|
|
|
1168
1583
|
.option('--name <name>', 'Operate on a named vault')
|
|
1169
1584
|
.addCommand(new Command('init')
|
|
1170
1585
|
.description('Initialize a vault')
|
|
1171
|
-
.action(async (
|
|
1586
|
+
.action(async (_options, cmd) => {
|
|
1172
1587
|
const parentOpts = cmd.parent.opts();
|
|
1173
1588
|
const projectPath = process.cwd();
|
|
1174
1589
|
const config = await loadConfig(projectPath);
|
|
@@ -1199,7 +1614,7 @@ program
|
|
|
1199
1614
|
const parentOpts = cmd.parent.opts();
|
|
1200
1615
|
const vaultOverride = parentOpts.global ? 'global' : parentOpts.project ? 'project' : undefined;
|
|
1201
1616
|
await withSession(async (storage) => {
|
|
1202
|
-
const value = options.value ||
|
|
1617
|
+
const value = options.value || await promptInput('Value:');
|
|
1203
1618
|
const tags = options.tags ? options.tags.split(',').map((t) => t.trim()) : [];
|
|
1204
1619
|
const now = new Date().toISOString();
|
|
1205
1620
|
await storage.set(name, {
|
|
@@ -1275,7 +1690,6 @@ program
|
|
|
1275
1690
|
.argument('<name>', 'Vault name (global, project, or named vault)')
|
|
1276
1691
|
.action(async (name) => {
|
|
1277
1692
|
const projectPath = process.cwd();
|
|
1278
|
-
const config = await loadConfig(projectPath);
|
|
1279
1693
|
if (name !== 'global' && name !== 'project') {
|
|
1280
1694
|
const vaultDir = path.join(projectPath, '.envcp/vaults', name);
|
|
1281
1695
|
if (!await pathExists(vaultDir)) {
|
|
@@ -1334,7 +1748,7 @@ program
|
|
|
1334
1748
|
.addCommand(new Command('save')
|
|
1335
1749
|
.description('Save current password to OS keychain')
|
|
1336
1750
|
.action(async () => {
|
|
1337
|
-
await withSession(async (
|
|
1751
|
+
await withSession(async (_storage, password, config, projectPath) => {
|
|
1338
1752
|
if (!password) {
|
|
1339
1753
|
console.log(chalk.red('No password available (encryption disabled?)'));
|
|
1340
1754
|
return;
|
|
@@ -1422,5 +1836,131 @@ if (!await pathExists(firstRunMarker)) {
|
|
|
1422
1836
|
Docs: https://github.com/fentz26/EnvCP
|
|
1423
1837
|
`);
|
|
1424
1838
|
}
|
|
1839
|
+
program
|
|
1840
|
+
.command('logs')
|
|
1841
|
+
.description('View audit logs')
|
|
1842
|
+
.option('--date <date>', 'Log date (YYYY-MM-DD, default: today)')
|
|
1843
|
+
.option('--operation <op>', 'Filter by operation (add, get, update, delete, list, sync, export, unlock, lock, check_access, run, auth_failure)')
|
|
1844
|
+
.option('--variable <name>', 'Filter by variable name')
|
|
1845
|
+
.option('--source <source>', 'Filter by source (cli, mcp, api)')
|
|
1846
|
+
.option('--success', 'Show only successful operations')
|
|
1847
|
+
.option('--failure', 'Show only failed operations')
|
|
1848
|
+
.option('--tail <n>', 'Show last N entries', parseInt)
|
|
1849
|
+
.option('--dates', 'List all available log dates')
|
|
1850
|
+
.option('--verify', 'Verify HMAC integrity of log entries')
|
|
1851
|
+
.action(async (options) => {
|
|
1852
|
+
const projectPath = process.cwd();
|
|
1853
|
+
const config = await loadConfig(projectPath);
|
|
1854
|
+
const logDir = resolveLogPath(config.audit, projectPath);
|
|
1855
|
+
const logs = new LogManager(logDir, config.audit);
|
|
1856
|
+
await logs.init();
|
|
1857
|
+
if (options.dates) {
|
|
1858
|
+
const dates = await logs.getLogDates();
|
|
1859
|
+
if (dates.length === 0) {
|
|
1860
|
+
console.log(chalk.gray('No log files found.'));
|
|
1861
|
+
}
|
|
1862
|
+
else {
|
|
1863
|
+
console.log(chalk.bold('Available log dates:'));
|
|
1864
|
+
dates.forEach(d => console.log(chalk.gray(` ${d}`)));
|
|
1865
|
+
}
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
const filter = {};
|
|
1869
|
+
if (options.date)
|
|
1870
|
+
filter.date = options.date;
|
|
1871
|
+
if (options.operation)
|
|
1872
|
+
filter.operation = options.operation;
|
|
1873
|
+
if (options.variable)
|
|
1874
|
+
filter.variable = options.variable;
|
|
1875
|
+
if (options.source)
|
|
1876
|
+
filter.source = options.source;
|
|
1877
|
+
if (options.success)
|
|
1878
|
+
filter.success = true;
|
|
1879
|
+
if (options.failure)
|
|
1880
|
+
filter.success = false;
|
|
1881
|
+
if (options.tail)
|
|
1882
|
+
filter.tail = options.tail;
|
|
1883
|
+
const entries = await logs.getLogs(filter);
|
|
1884
|
+
if (entries.length === 0) {
|
|
1885
|
+
console.log(chalk.gray('No log entries found.'));
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
let failed = 0;
|
|
1889
|
+
entries.forEach(entry => {
|
|
1890
|
+
const ts = chalk.gray(entry.timestamp);
|
|
1891
|
+
const op = entry.success ? chalk.green(entry.operation) : chalk.red(entry.operation);
|
|
1892
|
+
const src = chalk.blue(entry.source);
|
|
1893
|
+
const varName = entry.variable ? chalk.yellow(` [${entry.variable}]`) : '';
|
|
1894
|
+
const msg = entry.message ? chalk.gray(` — ${entry.message}`) : '';
|
|
1895
|
+
if (options.verify && entry.hmac !== undefined) {
|
|
1896
|
+
const valid = logs.verifyEntry(entry);
|
|
1897
|
+
if (!valid) {
|
|
1898
|
+
failed++;
|
|
1899
|
+
console.log(`${ts} ${chalk.red('[TAMPERED]')} ${op} ${src}${varName}${msg}`);
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
console.log(`${ts} ${op} ${src}${varName}${msg}`);
|
|
1904
|
+
});
|
|
1905
|
+
if (options.verify) {
|
|
1906
|
+
if (failed === 0) {
|
|
1907
|
+
console.log(chalk.green(`\nAll ${entries.length} entries verified OK.`));
|
|
1908
|
+
}
|
|
1909
|
+
else {
|
|
1910
|
+
console.log(chalk.red(`\n${failed}/${entries.length} entries failed HMAC verification.`));
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
program
|
|
1915
|
+
.command('verify-logs')
|
|
1916
|
+
.description('Verify HMAC chain integrity of audit logs')
|
|
1917
|
+
.option('--date <date>', 'Verify specific date (YYYY-MM-DD, default: all dates)')
|
|
1918
|
+
.action(async (options) => {
|
|
1919
|
+
const projectPath = process.cwd();
|
|
1920
|
+
const config = await loadConfig(projectPath);
|
|
1921
|
+
const logDir = resolveLogPath(config.audit, projectPath);
|
|
1922
|
+
const logs = new LogManager(logDir, config.audit);
|
|
1923
|
+
await logs.init();
|
|
1924
|
+
if (!config.audit.hmac_chain) {
|
|
1925
|
+
console.log(chalk.yellow('HMAC chain is not enabled in audit config.'));
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
console.log(chalk.bold('Verifying log chain integrity...'));
|
|
1929
|
+
const result = await logs.verifyLogChain(options.date);
|
|
1930
|
+
if (result.valid) {
|
|
1931
|
+
console.log(chalk.green(`✓ Chain integrity verified: ${result.entries} entries OK`));
|
|
1932
|
+
}
|
|
1933
|
+
else {
|
|
1934
|
+
console.log(chalk.red(`✗ Chain integrity FAILED: ${result.tampered.length}/${result.entries} entries tampered`));
|
|
1935
|
+
console.log(chalk.red(` Tampered indices: ${result.tampered.join(', ')}`));
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
program
|
|
1939
|
+
.command('protect-logs')
|
|
1940
|
+
.description('Apply OS-level protection to audit log files (Linux only)')
|
|
1941
|
+
.action(async () => {
|
|
1942
|
+
const projectPath = process.cwd();
|
|
1943
|
+
const config = await loadConfig(projectPath);
|
|
1944
|
+
const logDir = resolveLogPath(config.audit, projectPath);
|
|
1945
|
+
const logs = new LogManager(logDir, config.audit);
|
|
1946
|
+
await logs.init();
|
|
1947
|
+
if (config.audit.protection === 'none') {
|
|
1948
|
+
console.log(chalk.yellow('Log protection is disabled in config (protection: none).'));
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
console.log(chalk.bold(`Applying ${config.audit.protection} protection to log files...`));
|
|
1952
|
+
const result = await logs.protectLogFiles();
|
|
1953
|
+
if (result.protected.length > 0) {
|
|
1954
|
+
console.log(chalk.green(`✓ Protected ${result.protected.length} files:`));
|
|
1955
|
+
result.protected.forEach((f) => console.log(chalk.gray(` ${f}`)));
|
|
1956
|
+
}
|
|
1957
|
+
if (result.failed.length > 0) {
|
|
1958
|
+
console.log(chalk.red(`✗ Failed to protect ${result.failed.length} files:`));
|
|
1959
|
+
result.failed.forEach((f) => console.log(chalk.gray(` ${f}`)));
|
|
1960
|
+
}
|
|
1961
|
+
if (result.protected.length === 0 && result.failed.length === 0) {
|
|
1962
|
+
console.log(chalk.gray('No log files to protect.'));
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1425
1965
|
program.parse();
|
|
1426
1966
|
//# sourceMappingURL=index.js.map
|