@hatem427/code-guard-ci 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +27 -0
- package/LICENSE +21 -0
- package/README.md +646 -0
- package/config/angular.config.ts +223 -0
- package/config/guidelines.config.ts +229 -0
- package/config/nextjs.config.ts +160 -0
- package/config/react.config.ts +330 -0
- package/dist/config/angular.config.d.ts +15 -0
- package/dist/config/angular.config.d.ts.map +1 -0
- package/dist/config/angular.config.js +187 -0
- package/dist/config/angular.config.js.map +1 -0
- package/dist/config/guidelines.config.d.ts +63 -0
- package/dist/config/guidelines.config.d.ts.map +1 -0
- package/dist/config/guidelines.config.js +167 -0
- package/dist/config/guidelines.config.js.map +1 -0
- package/dist/config/nextjs.config.d.ts +18 -0
- package/dist/config/nextjs.config.d.ts.map +1 -0
- package/dist/config/nextjs.config.js +133 -0
- package/dist/config/nextjs.config.js.map +1 -0
- package/dist/config/react.config.d.ts +15 -0
- package/dist/config/react.config.d.ts.map +1 -0
- package/dist/config/react.config.js +287 -0
- package/dist/config/react.config.js.map +1 -0
- package/dist/scripts/auto-fix.d.ts +16 -0
- package/dist/scripts/auto-fix.d.ts.map +1 -0
- package/dist/scripts/auto-fix.js +130 -0
- package/dist/scripts/auto-fix.js.map +1 -0
- package/dist/scripts/cli.d.ts +17 -0
- package/dist/scripts/cli.d.ts.map +1 -0
- package/dist/scripts/cli.js +255 -0
- package/dist/scripts/cli.js.map +1 -0
- package/dist/scripts/delete-bypass-logs.d.ts +17 -0
- package/dist/scripts/delete-bypass-logs.d.ts.map +1 -0
- package/dist/scripts/delete-bypass-logs.js +242 -0
- package/dist/scripts/delete-bypass-logs.js.map +1 -0
- package/dist/scripts/generate-doc.d.ts +18 -0
- package/dist/scripts/generate-doc.d.ts.map +1 -0
- package/dist/scripts/generate-doc.js +300 -0
- package/dist/scripts/generate-doc.js.map +1 -0
- package/dist/scripts/generate-pr-checklist.d.ts +20 -0
- package/dist/scripts/generate-pr-checklist.d.ts.map +1 -0
- package/dist/scripts/generate-pr-checklist.js +276 -0
- package/dist/scripts/generate-pr-checklist.js.map +1 -0
- package/dist/scripts/precommit-check.d.ts +23 -0
- package/dist/scripts/precommit-check.d.ts.map +1 -0
- package/dist/scripts/precommit-check.js +331 -0
- package/dist/scripts/precommit-check.js.map +1 -0
- package/dist/scripts/set-admin-password.d.ts +14 -0
- package/dist/scripts/set-admin-password.d.ts.map +1 -0
- package/dist/scripts/set-admin-password.js +116 -0
- package/dist/scripts/set-admin-password.js.map +1 -0
- package/dist/scripts/set-bypass-password.d.ts +11 -0
- package/dist/scripts/set-bypass-password.d.ts.map +1 -0
- package/dist/scripts/set-bypass-password.js +106 -0
- package/dist/scripts/set-bypass-password.js.map +1 -0
- package/dist/scripts/utils/auto-fixer.d.ts +28 -0
- package/dist/scripts/utils/auto-fixer.d.ts.map +1 -0
- package/dist/scripts/utils/auto-fixer.js +177 -0
- package/dist/scripts/utils/auto-fixer.js.map +1 -0
- package/dist/scripts/utils/bypass-manager.d.ts +101 -0
- package/dist/scripts/utils/bypass-manager.d.ts.map +1 -0
- package/dist/scripts/utils/bypass-manager.js +496 -0
- package/dist/scripts/utils/bypass-manager.js.map +1 -0
- package/dist/scripts/utils/code-analyzer.d.ts +34 -0
- package/dist/scripts/utils/code-analyzer.d.ts.map +1 -0
- package/dist/scripts/utils/code-analyzer.js +323 -0
- package/dist/scripts/utils/code-analyzer.js.map +1 -0
- package/dist/scripts/utils/file-checker.d.ts +93 -0
- package/dist/scripts/utils/file-checker.d.ts.map +1 -0
- package/dist/scripts/utils/file-checker.js +248 -0
- package/dist/scripts/utils/file-checker.js.map +1 -0
- package/dist/scripts/utils/logger.d.ts +26 -0
- package/dist/scripts/utils/logger.d.ts.map +1 -0
- package/dist/scripts/utils/logger.js +86 -0
- package/dist/scripts/utils/logger.js.map +1 -0
- package/dist/scripts/utils/project-detector.d.ts +34 -0
- package/dist/scripts/utils/project-detector.d.ts.map +1 -0
- package/dist/scripts/utils/project-detector.js +124 -0
- package/dist/scripts/utils/project-detector.js.map +1 -0
- package/dist/scripts/utils/rule-engine.d.ts +57 -0
- package/dist/scripts/utils/rule-engine.d.ts.map +1 -0
- package/dist/scripts/utils/rule-engine.js +158 -0
- package/dist/scripts/utils/rule-engine.js.map +1 -0
- package/dist/scripts/view-bypass-log.d.ts +13 -0
- package/dist/scripts/view-bypass-log.d.ts.map +1 -0
- package/dist/scripts/view-bypass-log.js +117 -0
- package/dist/scripts/view-bypass-log.js.map +1 -0
- package/package.json +74 -0
- package/scripts/auto-fix.ts +115 -0
- package/scripts/cli.ts +246 -0
- package/scripts/delete-bypass-logs.ts +253 -0
- package/scripts/generate-doc.ts +317 -0
- package/scripts/generate-pr-checklist.ts +285 -0
- package/scripts/precommit-check.ts +349 -0
- package/scripts/set-admin-password.ts +90 -0
- package/scripts/set-bypass-password.ts +80 -0
- package/scripts/utils/auto-fixer.ts +181 -0
- package/scripts/utils/bypass-manager.ts +566 -0
- package/scripts/utils/code-analyzer.ts +341 -0
- package/scripts/utils/file-checker.ts +253 -0
- package/scripts/utils/logger.ts +88 -0
- package/scripts/utils/project-detector.ts +115 -0
- package/scripts/utils/rule-engine.ts +186 -0
- package/scripts/view-bypass-log.ts +92 -0
- package/templates/feature-doc-api.md +101 -0
- package/templates/feature-doc-service.md +113 -0
- package/templates/feature-doc-ui.md +91 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* bypass-manager.ts — Bypass audit logging with authentication
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Tracks all bypass attempts with:
|
|
7
|
+
* - Author name (from git config)
|
|
8
|
+
* - Timestamp
|
|
9
|
+
* - Reason
|
|
10
|
+
* - Password verification
|
|
11
|
+
* - Commit hash
|
|
12
|
+
*
|
|
13
|
+
* Bypass log saved to: .code-guardian/bypass-log.json
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { execSync } from 'child_process';
|
|
19
|
+
import * as crypto from 'crypto';
|
|
20
|
+
|
|
21
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface BypassEntry {
|
|
24
|
+
/** Unique ID for this bypass */
|
|
25
|
+
id: string;
|
|
26
|
+
/** Git author name */
|
|
27
|
+
author: string;
|
|
28
|
+
/** Git author email */
|
|
29
|
+
email: string;
|
|
30
|
+
/** When the bypass occurred */
|
|
31
|
+
timestamp: string;
|
|
32
|
+
/** Reason for bypass */
|
|
33
|
+
reason: string;
|
|
34
|
+
/** Commit message */
|
|
35
|
+
commitMessage: string;
|
|
36
|
+
/** Commit hash (after commit) */
|
|
37
|
+
commitHash?: string;
|
|
38
|
+
/** Branch name */
|
|
39
|
+
branch: string;
|
|
40
|
+
/** Files that were bypassed */
|
|
41
|
+
files: string[];
|
|
42
|
+
/** How bypass was triggered (message flag or env var) */
|
|
43
|
+
method: 'commit-message' | 'env-variable' | 'password';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BypassLog {
|
|
47
|
+
/** All bypass entries */
|
|
48
|
+
entries: BypassEntry[];
|
|
49
|
+
/** Last updated timestamp */
|
|
50
|
+
lastUpdated: string;
|
|
51
|
+
/** Integrity checksum to detect tampering */
|
|
52
|
+
checksum?: string;
|
|
53
|
+
/** Flag indicating if log is immutable (cannot be deleted) */
|
|
54
|
+
immutable: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Configuration ───────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
const BYPASS_DIR = '.code-guardian';
|
|
60
|
+
const BYPASS_LOG_FILE = 'bypass-log.json';
|
|
61
|
+
const PASSWORD_FILE = 'bypass-password.hash';
|
|
62
|
+
const ADMIN_FILE = 'admin-credentials.hash';
|
|
63
|
+
const LOG_ARCHIVE_DIR = 'bypass-archive';
|
|
64
|
+
|
|
65
|
+
// Default password hash (password: "bypass123" - CHANGE THIS!)
|
|
66
|
+
const DEFAULT_PASSWORD_HASH = '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92'; // SHA-256 of "bypass123"
|
|
67
|
+
// Default admin password hash (password: "admin123" - CHANGE THIS IMMEDIATELY!)
|
|
68
|
+
const DEFAULT_ADMIN_PASSWORD_HASH = '240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9'; // SHA-256 of "admin123"
|
|
69
|
+
|
|
70
|
+
// ── Password Management ─────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Hash a password using SHA-256
|
|
74
|
+
*/
|
|
75
|
+
function hashPassword(password: string): string {
|
|
76
|
+
return crypto.createHash('sha256').update(password).digest('hex');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the stored password hash, or create default if not exists
|
|
81
|
+
*/
|
|
82
|
+
function getPasswordHash(): string {
|
|
83
|
+
const passwordPath = path.join(process.cwd(), BYPASS_DIR, PASSWORD_FILE);
|
|
84
|
+
|
|
85
|
+
if (!fs.existsSync(passwordPath)) {
|
|
86
|
+
// Create default password file
|
|
87
|
+
const dir = path.join(process.cwd(), BYPASS_DIR);
|
|
88
|
+
if (!fs.existsSync(dir)) {
|
|
89
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fs.writeFileSync(passwordPath, DEFAULT_PASSWORD_HASH, 'utf-8');
|
|
93
|
+
console.warn('⚠️ Default bypass password created. Change it by running:');
|
|
94
|
+
console.warn(' npm run set-bypass-password');
|
|
95
|
+
return DEFAULT_PASSWORD_HASH;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return fs.readFileSync(passwordPath, 'utf-8').trim();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Verify a password against the stored hash
|
|
103
|
+
*/
|
|
104
|
+
export function verifyBypassPassword(password: string): boolean {
|
|
105
|
+
const storedHash = getPasswordHash();
|
|
106
|
+
const inputHash = hashPassword(password);
|
|
107
|
+
return inputHash === storedHash;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Set a new bypass password
|
|
112
|
+
*/
|
|
113
|
+
export function setBypassPassword(newPassword: string): void {
|
|
114
|
+
const dir = path.join(process.cwd(), BYPASS_DIR);
|
|
115
|
+
if (!fs.existsSync(dir)) {
|
|
116
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const passwordPath = path.join(dir, PASSWORD_FILE);
|
|
120
|
+
const hash = hashPassword(newPassword);
|
|
121
|
+
fs.writeFileSync(passwordPath, hash, 'utf-8');
|
|
122
|
+
|
|
123
|
+
console.log('✅ Bypass password updated successfully.');
|
|
124
|
+
console.log('⚠️ Keep this password secure and share only with authorized team members.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Admin Management ────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get the stored admin password hash, or create default if not exists
|
|
131
|
+
*/
|
|
132
|
+
function getAdminPasswordHash(): string {
|
|
133
|
+
const adminPath = path.join(process.cwd(), BYPASS_DIR, ADMIN_FILE);
|
|
134
|
+
|
|
135
|
+
if (!fs.existsSync(adminPath)) {
|
|
136
|
+
// Create default admin password file
|
|
137
|
+
const dir = path.join(process.cwd(), BYPASS_DIR);
|
|
138
|
+
if (!fs.existsSync(dir)) {
|
|
139
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(adminPath, DEFAULT_ADMIN_PASSWORD_HASH, 'utf-8');
|
|
143
|
+
console.warn('⚠️ Default admin password created. CHANGE IT IMMEDIATELY by running:');
|
|
144
|
+
console.warn(' npm run set-admin-password');
|
|
145
|
+
return DEFAULT_ADMIN_PASSWORD_HASH;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return fs.readFileSync(adminPath, 'utf-8').trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Verify admin password
|
|
153
|
+
*/
|
|
154
|
+
export function verifyAdminPassword(password: string): boolean {
|
|
155
|
+
const storedHash = getAdminPasswordHash();
|
|
156
|
+
const inputHash = hashPassword(password);
|
|
157
|
+
return inputHash === storedHash;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Set a new admin password
|
|
162
|
+
*/
|
|
163
|
+
export function setAdminPassword(newPassword: string): void {
|
|
164
|
+
const dir = path.join(process.cwd(), BYPASS_DIR);
|
|
165
|
+
if (!fs.existsSync(dir)) {
|
|
166
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const adminPath = path.join(dir, ADMIN_FILE);
|
|
170
|
+
const hash = hashPassword(newPassword);
|
|
171
|
+
fs.writeFileSync(adminPath, hash, 'utf-8');
|
|
172
|
+
|
|
173
|
+
console.log('✅ Admin password updated successfully.');
|
|
174
|
+
console.log('🔐 Admin privileges: View and delete bypass logs');
|
|
175
|
+
console.log('⚠️ Keep this password highly secure!');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Log Integrity ───────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Calculate checksum for bypass log to detect tampering
|
|
182
|
+
*/
|
|
183
|
+
function calculateLogChecksum(log: BypassLog): string {
|
|
184
|
+
// Create a stable string representation (excluding checksum itself)
|
|
185
|
+
const data = JSON.stringify({
|
|
186
|
+
entries: log.entries,
|
|
187
|
+
lastUpdated: log.lastUpdated,
|
|
188
|
+
});
|
|
189
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Verify log integrity
|
|
194
|
+
*/
|
|
195
|
+
function verifyLogIntegrity(log: BypassLog): boolean {
|
|
196
|
+
if (!log.checksum) {
|
|
197
|
+
// Legacy log without checksum - consider valid but warn
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
const expectedChecksum = calculateLogChecksum(log);
|
|
201
|
+
return expectedChecksum === log.checksum;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Git Helpers ─────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
function getGitAuthor(): { name: string; email: string } {
|
|
207
|
+
try {
|
|
208
|
+
const name = execSync('git config user.name', { encoding: 'utf-8' }).trim();
|
|
209
|
+
const email = execSync('git config user.email', { encoding: 'utf-8' }).trim();
|
|
210
|
+
return { name, email };
|
|
211
|
+
} catch {
|
|
212
|
+
return { name: 'Unknown', email: 'unknown@example.com' };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getCurrentBranch(): string {
|
|
217
|
+
try {
|
|
218
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
219
|
+
} catch {
|
|
220
|
+
return 'unknown';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getStagedFiles(): string[] {
|
|
225
|
+
try {
|
|
226
|
+
const output = execSync('git diff --cached --name-only', { encoding: 'utf-8' }).trim();
|
|
227
|
+
return output ? output.split('\n') : [];
|
|
228
|
+
} catch {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Bypass Log Management ───────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Load the bypass log from disk
|
|
237
|
+
*/
|
|
238
|
+
function loadBypassLog(): BypassLog {
|
|
239
|
+
const logPath = path.join(process.cwd(), BYPASS_DIR, BYPASS_LOG_FILE);
|
|
240
|
+
|
|
241
|
+
if (!fs.existsSync(logPath)) {
|
|
242
|
+
return {
|
|
243
|
+
entries: [],
|
|
244
|
+
lastUpdated: new Date().toISOString(),
|
|
245
|
+
immutable: true,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const content = fs.readFileSync(logPath, 'utf-8');
|
|
251
|
+
const log = JSON.parse(content);
|
|
252
|
+
|
|
253
|
+
// Ensure immutable flag exists (for backward compatibility)
|
|
254
|
+
if (log.immutable === undefined) {
|
|
255
|
+
log.immutable = true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Verify integrity
|
|
259
|
+
if (!verifyLogIntegrity(log)) {
|
|
260
|
+
console.warn('⚠️ WARNING: Bypass log integrity check failed!');
|
|
261
|
+
console.warn(' The log may have been tampered with.');
|
|
262
|
+
console.warn(' Location: ' + logPath);
|
|
263
|
+
// Archive the potentially tampered log
|
|
264
|
+
archiveTamperedLog(log);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return log;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error('❌ Error loading bypass log:', err);
|
|
270
|
+
return {
|
|
271
|
+
entries: [],
|
|
272
|
+
lastUpdated: new Date().toISOString(),
|
|
273
|
+
immutable: true,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Save the bypass log to disk
|
|
280
|
+
*/
|
|
281
|
+
function saveBypassLog(log: BypassLog): void {
|
|
282
|
+
const dir = path.join(process.cwd(), BYPASS_DIR);
|
|
283
|
+
if (!fs.existsSync(dir)) {
|
|
284
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const logPath = path.join(dir, BYPASS_LOG_FILE);
|
|
288
|
+
log.lastUpdated = new Date().toISOString();
|
|
289
|
+
log.immutable = true; // Always mark as immutable
|
|
290
|
+
|
|
291
|
+
// Calculate and store checksum for integrity verification
|
|
292
|
+
log.checksum = calculateLogChecksum(log);
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
// Temporarily make file writable if it exists and is read-only.
|
|
296
|
+
// This allows the log to remain effectively "read-only" to users while
|
|
297
|
+
// still letting Code Guardian append new entries.
|
|
298
|
+
if (fs.existsSync(logPath)) {
|
|
299
|
+
try {
|
|
300
|
+
fs.chmodSync(logPath, 0o644); // rw-r--r--
|
|
301
|
+
} catch {
|
|
302
|
+
// If chmod fails (e.g., filesystem does not support it), continue and
|
|
303
|
+
// rely on the write below to surface any real permission problems.
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Write the log file
|
|
308
|
+
fs.writeFileSync(logPath, JSON.stringify(log, null, 2), 'utf-8');
|
|
309
|
+
|
|
310
|
+
// After a successful write, set file permissions back to read-only
|
|
311
|
+
// (444 = r--r--r--) to discourage manual edits.
|
|
312
|
+
try {
|
|
313
|
+
fs.chmodSync(logPath, 0o444);
|
|
314
|
+
} catch {
|
|
315
|
+
// Non-fatal: if we cannot change permissions, keep going.
|
|
316
|
+
}
|
|
317
|
+
} catch (err: any) {
|
|
318
|
+
// If we cannot write the log (for example, the file was made truly
|
|
319
|
+
// immutable with OS-level flags), do NOT block the bypass or commit.
|
|
320
|
+
// Instead, warn and continue so the hook stays usable.
|
|
321
|
+
console.warn('⚠️ Code Guardian: Failed to update bypass log file.');
|
|
322
|
+
console.warn(
|
|
323
|
+
' Reason: ' + (err && err.message ? err.message : String(err))
|
|
324
|
+
);
|
|
325
|
+
console.warn(
|
|
326
|
+
' Tip: Ensure `.code-guardian/bypass-log.json` is writable by git user, '
|
|
327
|
+
+ 'or remove OS-level immutability flags if you want new bypasses to be recorded.'
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Archive a potentially tampered log file
|
|
334
|
+
*/
|
|
335
|
+
function archiveTamperedLog(log: BypassLog): void {
|
|
336
|
+
try {
|
|
337
|
+
const archiveDir = path.join(process.cwd(), BYPASS_DIR, LOG_ARCHIVE_DIR);
|
|
338
|
+
if (!fs.existsSync(archiveDir)) {
|
|
339
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
343
|
+
const archivePath = path.join(archiveDir, `tampered-log-${timestamp}.json`);
|
|
344
|
+
|
|
345
|
+
fs.writeFileSync(archivePath, JSON.stringify(log, null, 2), 'utf-8');
|
|
346
|
+
console.log(` Archived to: ${archivePath}`);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
console.error(' Failed to archive tampered log:', err);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Record a bypass attempt in the audit log
|
|
354
|
+
*/
|
|
355
|
+
export function recordBypass(
|
|
356
|
+
reason: string,
|
|
357
|
+
commitMessage: string,
|
|
358
|
+
method: 'commit-message' | 'env-variable' | 'password'
|
|
359
|
+
): void {
|
|
360
|
+
const author = getGitAuthor();
|
|
361
|
+
const branch = getCurrentBranch();
|
|
362
|
+
const files = getStagedFiles();
|
|
363
|
+
|
|
364
|
+
const entry: BypassEntry = {
|
|
365
|
+
id: crypto.randomUUID(),
|
|
366
|
+
author: author.name,
|
|
367
|
+
email: author.email,
|
|
368
|
+
timestamp: new Date().toISOString(),
|
|
369
|
+
reason,
|
|
370
|
+
commitMessage,
|
|
371
|
+
branch,
|
|
372
|
+
files,
|
|
373
|
+
method,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const log = loadBypassLog();
|
|
377
|
+
log.entries.push(entry);
|
|
378
|
+
saveBypassLog(log);
|
|
379
|
+
|
|
380
|
+
console.log('');
|
|
381
|
+
console.log('📝 Bypass recorded in audit log:');
|
|
382
|
+
console.log(` Author: ${author.name} <${author.email}>`);
|
|
383
|
+
console.log(` Reason: ${reason}`);
|
|
384
|
+
console.log(` Method: ${method}`);
|
|
385
|
+
console.log(` Files: ${files.length} file(s)`);
|
|
386
|
+
console.log('');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Update a bypass entry with the commit hash after commit succeeds
|
|
391
|
+
*/
|
|
392
|
+
export function updateBypassWithCommitHash(entryId: string, commitHash: string): void {
|
|
393
|
+
const log = loadBypassLog();
|
|
394
|
+
const entry = log.entries.find((e) => e.id === entryId);
|
|
395
|
+
|
|
396
|
+
if (entry) {
|
|
397
|
+
entry.commitHash = commitHash;
|
|
398
|
+
saveBypassLog(log);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get all bypass entries for a specific author
|
|
404
|
+
*/
|
|
405
|
+
export function getBypassesByAuthor(authorEmail: string): BypassEntry[] {
|
|
406
|
+
const log = loadBypassLog();
|
|
407
|
+
return log.entries.filter((e) => e.email === authorEmail);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Get recent bypass entries (last N days)
|
|
412
|
+
*/
|
|
413
|
+
export function getRecentBypasses(days: number = 30): BypassEntry[] {
|
|
414
|
+
const log = loadBypassLog();
|
|
415
|
+
const cutoff = new Date();
|
|
416
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
417
|
+
|
|
418
|
+
return log.entries.filter((e) => new Date(e.timestamp) >= cutoff);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Delete bypass entries (ADMIN ONLY)
|
|
423
|
+
* @param entryIds - Array of entry IDs to delete
|
|
424
|
+
* @param adminPassword - Admin password for verification
|
|
425
|
+
* @returns Success status and message
|
|
426
|
+
*/
|
|
427
|
+
export function deleteBypassEntries(
|
|
428
|
+
entryIds: string[],
|
|
429
|
+
adminPassword: string
|
|
430
|
+
): { success: boolean; message: string; deletedCount: number } {
|
|
431
|
+
// Verify admin password
|
|
432
|
+
if (!verifyAdminPassword(adminPassword)) {
|
|
433
|
+
return {
|
|
434
|
+
success: false,
|
|
435
|
+
message: '❌ Invalid admin password. Access denied.',
|
|
436
|
+
deletedCount: 0,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const logPath = path.join(process.cwd(), BYPASS_DIR, BYPASS_LOG_FILE);
|
|
441
|
+
|
|
442
|
+
// Remove read-only protection temporarily
|
|
443
|
+
try {
|
|
444
|
+
fs.chmodSync(logPath, 0o644);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
// Ignore if chmod fails
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const log = loadBypassLog();
|
|
450
|
+
const initialCount = log.entries.length;
|
|
451
|
+
|
|
452
|
+
// Archive deleted entries before removal
|
|
453
|
+
const deletedEntries = log.entries.filter((e) => entryIds.includes(e.id));
|
|
454
|
+
if (deletedEntries.length > 0) {
|
|
455
|
+
archiveDeletedEntries(deletedEntries, adminPassword);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Remove entries
|
|
459
|
+
log.entries = log.entries.filter((e) => !entryIds.includes(e.id));
|
|
460
|
+
const deletedCount = initialCount - log.entries.length;
|
|
461
|
+
|
|
462
|
+
if (deletedCount === 0) {
|
|
463
|
+
return {
|
|
464
|
+
success: false,
|
|
465
|
+
message: '⚠️ No matching entries found to delete.',
|
|
466
|
+
deletedCount: 0,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Save updated log
|
|
471
|
+
saveBypassLog(log);
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
success: true,
|
|
475
|
+
message: `✅ Deleted ${deletedCount} bypass entry(ies) successfully.`,
|
|
476
|
+
deletedCount,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Archive deleted entries for audit trail
|
|
482
|
+
*/
|
|
483
|
+
function archiveDeletedEntries(entries: BypassEntry[], adminPassword: string): void {
|
|
484
|
+
try {
|
|
485
|
+
const archiveDir = path.join(process.cwd(), BYPASS_DIR, LOG_ARCHIVE_DIR);
|
|
486
|
+
if (!fs.existsSync(archiveDir)) {
|
|
487
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
491
|
+
const archivePath = path.join(archiveDir, `deleted-entries-${timestamp}.json`);
|
|
492
|
+
|
|
493
|
+
const archiveData = {
|
|
494
|
+
deletedAt: new Date().toISOString(),
|
|
495
|
+
deletedBy: 'admin',
|
|
496
|
+
adminPasswordHash: hashPassword(adminPassword), // For audit purposes
|
|
497
|
+
entries,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
fs.writeFileSync(archivePath, JSON.stringify(archiveData, null, 2), 'utf-8');
|
|
501
|
+
console.log(`📦 Deleted entries archived to: ${archivePath}`);
|
|
502
|
+
} catch (err) {
|
|
503
|
+
console.error('⚠️ Warning: Failed to archive deleted entries:', err);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Get all archived logs (deleted and tampered)
|
|
509
|
+
*/
|
|
510
|
+
export function getArchivedLogs(): { deleted: string[]; tampered: string[] } {
|
|
511
|
+
const archiveDir = path.join(process.cwd(), BYPASS_DIR, LOG_ARCHIVE_DIR);
|
|
512
|
+
|
|
513
|
+
if (!fs.existsSync(archiveDir)) {
|
|
514
|
+
return { deleted: [], tampered: [] };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const files = fs.readdirSync(archiveDir);
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
deleted: files.filter((f) => f.startsWith('deleted-entries-')),
|
|
521
|
+
tampered: files.filter((f) => f.startsWith('tampered-log-')),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Generate a bypass audit report
|
|
527
|
+
*/
|
|
528
|
+
export function generateBypassReport(): string {
|
|
529
|
+
const log = loadBypassLog();
|
|
530
|
+
|
|
531
|
+
if (log.entries.length === 0) {
|
|
532
|
+
return '📊 Bypass Audit Report\n\nNo bypass entries found.';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
let report = '📊 Bypass Audit Report\n';
|
|
536
|
+
report += '═'.repeat(60) + '\n\n';
|
|
537
|
+
report += `Total bypasses: ${log.entries.length}\n`;
|
|
538
|
+
report += `Last updated: ${log.lastUpdated}\n\n`;
|
|
539
|
+
|
|
540
|
+
// Group by author
|
|
541
|
+
const byAuthor = new Map<string, BypassEntry[]>();
|
|
542
|
+
for (const entry of log.entries) {
|
|
543
|
+
const key = `${entry.author} <${entry.email}>`;
|
|
544
|
+
const existing = byAuthor.get(key) || [];
|
|
545
|
+
existing.push(entry);
|
|
546
|
+
byAuthor.set(key, existing);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
report += '📈 Bypasses by Author:\n';
|
|
550
|
+
report += '─'.repeat(60) + '\n';
|
|
551
|
+
for (const [author, entries] of byAuthor) {
|
|
552
|
+
report += `\n${author}: ${entries.length} bypass(es)\n`;
|
|
553
|
+
|
|
554
|
+
for (const entry of entries.slice(-5)) {
|
|
555
|
+
// Show last 5
|
|
556
|
+
const date = new Date(entry.timestamp).toLocaleString();
|
|
557
|
+
report += ` • ${date} - ${entry.reason}\n`;
|
|
558
|
+
report += ` Branch: ${entry.branch}, Files: ${entry.files.length}\n`;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
report += '\n' + '═'.repeat(60) + '\n';
|
|
563
|
+
report += `\nFull log: ${path.join(BYPASS_DIR, BYPASS_LOG_FILE)}\n`;
|
|
564
|
+
|
|
565
|
+
return report;
|
|
566
|
+
}
|