cto-ai-cli 1.3.0 → 3.0.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.
@@ -0,0 +1,662 @@
1
+ // src/govern/audit.ts
2
+ import { randomUUID, createHash } from "crypto";
3
+ import { readdir, chmod } from "fs/promises";
4
+ import { join } from "path";
5
+ import { userInfo } from "os";
6
+ import { homedir } from "os";
7
+ var CTO_DIR = ".cto-ai";
8
+ var AUDIT_DIR = "audit";
9
+ var MAX_ENTRIES_PER_FILE = 500;
10
+ function getAuditDir() {
11
+ return join(homedir(), CTO_DIR, AUDIT_DIR);
12
+ }
13
+ function getCurrentAuditFile() {
14
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
15
+ return join(getAuditDir(), `audit_${date}.json`);
16
+ }
17
+ function computeIntegrityHash(entry) {
18
+ const payload = JSON.stringify({
19
+ id: entry.id,
20
+ timestamp: entry.timestamp,
21
+ action: entry.action,
22
+ user: entry.user,
23
+ projectPath: entry.projectPath,
24
+ details: entry.details
25
+ });
26
+ return createHash("sha256").update(payload).digest("hex");
27
+ }
28
+ async function ensureDir(dirPath) {
29
+ const { mkdir } = await import("fs/promises");
30
+ await mkdir(dirPath, { recursive: true });
31
+ }
32
+ async function readJSON(filePath) {
33
+ const { readFile: readFile4 } = await import("fs/promises");
34
+ try {
35
+ const content = await readFile4(filePath, "utf-8");
36
+ return JSON.parse(content);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ async function writeJSON(filePath, data) {
42
+ const { writeFile } = await import("fs/promises");
43
+ await ensureDir(join(filePath, ".."));
44
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
45
+ }
46
+ async function logAudit(action, projectPath, details = {}) {
47
+ const auditDir = getAuditDir();
48
+ await ensureDir(auditDir);
49
+ let currentUser;
50
+ try {
51
+ currentUser = userInfo().username;
52
+ } catch {
53
+ currentUser = process.env.USER ?? process.env.USERNAME ?? "unknown";
54
+ }
55
+ const partialEntry = {
56
+ id: randomUUID().substring(0, 12),
57
+ timestamp: /* @__PURE__ */ new Date(),
58
+ action,
59
+ user: currentUser,
60
+ projectPath,
61
+ details
62
+ };
63
+ const entry = {
64
+ ...partialEntry,
65
+ integrityHash: computeIntegrityHash(partialEntry)
66
+ };
67
+ const auditFile = getCurrentAuditFile();
68
+ let entries = await readJSON(auditFile) ?? [];
69
+ entries.push(entry);
70
+ if (entries.length > MAX_ENTRIES_PER_FILE) {
71
+ entries = entries.slice(-MAX_ENTRIES_PER_FILE);
72
+ }
73
+ await writeJSON(auditFile, entries);
74
+ try {
75
+ await chmod(auditFile, 384);
76
+ } catch {
77
+ }
78
+ return entry;
79
+ }
80
+ async function getAuditEntries(options = {}) {
81
+ const auditDir = getAuditDir();
82
+ let files;
83
+ try {
84
+ files = await readdir(auditDir);
85
+ } catch {
86
+ return [];
87
+ }
88
+ const auditFiles = files.filter((f) => f.startsWith("audit_") && f.endsWith(".json")).sort().reverse();
89
+ const allEntries = [];
90
+ const limit = options.limit ?? 100;
91
+ for (const file of auditFiles) {
92
+ if (allEntries.length >= limit) break;
93
+ const entries = await readJSON(join(auditDir, file));
94
+ if (!entries) continue;
95
+ for (const entry of entries.reverse()) {
96
+ if (allEntries.length >= limit) break;
97
+ if (options.projectPath && entry.projectPath !== options.projectPath) continue;
98
+ if (options.action && entry.action !== options.action) continue;
99
+ if (options.since && new Date(entry.timestamp) < options.since) continue;
100
+ allEntries.push(entry);
101
+ }
102
+ }
103
+ return allEntries;
104
+ }
105
+ function verifyAuditEntry(entry) {
106
+ const { integrityHash, ...rest } = entry;
107
+ const expected = computeIntegrityHash(rest);
108
+ return expected === integrityHash;
109
+ }
110
+ async function verifyAuditIntegrity() {
111
+ const entries = await getAuditEntries({ limit: 1e4 });
112
+ const invalidEntries = [];
113
+ for (const entry of entries) {
114
+ if (!verifyAuditEntry(entry)) {
115
+ invalidEntries.push(entry);
116
+ }
117
+ }
118
+ return {
119
+ totalEntries: entries.length,
120
+ validEntries: entries.length - invalidEntries.length,
121
+ invalidEntries
122
+ };
123
+ }
124
+ async function purgeOldAuditEntries(retentionDays) {
125
+ const auditDir = getAuditDir();
126
+ let files;
127
+ try {
128
+ files = await readdir(auditDir);
129
+ } catch {
130
+ return 0;
131
+ }
132
+ const cutoff = /* @__PURE__ */ new Date();
133
+ cutoff.setDate(cutoff.getDate() - retentionDays);
134
+ const cutoffStr = cutoff.toISOString().split("T")[0].replace(/-/g, "");
135
+ let purged = 0;
136
+ const { unlink } = await import("fs/promises");
137
+ for (const file of files) {
138
+ if (!file.startsWith("audit_") || !file.endsWith(".json")) continue;
139
+ const dateStr = file.replace("audit_", "").replace(".json", "");
140
+ if (dateStr < cutoffStr) {
141
+ try {
142
+ await unlink(join(auditDir, file));
143
+ purged++;
144
+ } catch {
145
+ }
146
+ }
147
+ }
148
+ return purged;
149
+ }
150
+
151
+ // src/govern/secrets.ts
152
+ import { readFile } from "fs/promises";
153
+ import { resolve, relative } from "path";
154
+ var BUILTIN_PATTERNS = [
155
+ // API Keys
156
+ { type: "api-key", source: `(?:api[_-]?key|apikey)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{20,})['"]?`, flags: "gi", severity: "critical", description: "API Key" },
157
+ { type: "api-key", source: "sk-[a-zA-Z0-9]{20,}", flags: "g", severity: "critical", description: "OpenAI/Anthropic API Key" },
158
+ { type: "api-key", source: "sk-ant-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "Anthropic API Key" },
159
+ // AWS
160
+ { type: "aws-key", source: "AKIA[0-9A-Z]{16}", flags: "g", severity: "critical", description: "AWS Access Key ID" },
161
+ { type: "aws-key", source: `(?:aws_secret_access_key|aws_secret)\\s*[:=]\\s*['"]?([a-zA-Z0-9/+=]{40})['"]?`, flags: "gi", severity: "critical", description: "AWS Secret Key" },
162
+ // Private Keys
163
+ { type: "private-key", source: "-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----", flags: "g", severity: "critical", description: "Private Key" },
164
+ { type: "private-key", source: "-----BEGIN OPENSSH PRIVATE KEY-----", flags: "g", severity: "critical", description: "SSH Private Key" },
165
+ // Passwords
166
+ { type: "password", source: `(?:password|passwd|pwd)\\s*[:=]\\s*['"]([^'"]{8,})['"](?!\\s*\\{)`, flags: "gi", severity: "high", description: "Hardcoded Password" },
167
+ { type: "password", source: `(?:DB_PASSWORD|DATABASE_PASSWORD|MYSQL_PASSWORD|POSTGRES_PASSWORD)\\s*[:=]\\s*['"]?([^'"{}\\s]{4,})['"]?`, flags: "gi", severity: "high", description: "Database Password" },
168
+ // Tokens
169
+ { type: "token", source: `(?:bearer|token|auth_token|access_token|refresh_token)\\s*[:=]\\s*['"]([a-zA-Z0-9_\\-.]{20,})['"](?!\\s*\\{)`, flags: "gi", severity: "high", description: "Auth Token" },
170
+ { type: "token", source: "ghp_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub Personal Access Token" },
171
+ { type: "token", source: "gho_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub OAuth Token" },
172
+ { type: "token", source: "glpat-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "GitLab Personal Access Token" },
173
+ { type: "token", source: "npm_[a-zA-Z0-9]{36}", flags: "g", severity: "high", description: "npm Token" },
174
+ // Connection strings
175
+ { type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
176
+ { type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
177
+ // Environment variables with secrets
178
+ { type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" }
179
+ ];
180
+ function buildPatterns(customPatterns = []) {
181
+ const patterns = BUILTIN_PATTERNS.map((def) => ({
182
+ type: def.type,
183
+ pattern: new RegExp(def.source, def.flags),
184
+ severity: def.severity,
185
+ description: def.description
186
+ }));
187
+ for (const custom of customPatterns) {
188
+ try {
189
+ patterns.push({
190
+ type: "custom",
191
+ pattern: new RegExp(custom, "gi"),
192
+ severity: "medium",
193
+ description: `Custom pattern: ${custom}`
194
+ });
195
+ } catch {
196
+ }
197
+ }
198
+ return patterns;
199
+ }
200
+ function scanContentForSecrets(content, filePath, customPatterns = []) {
201
+ const findings = [];
202
+ const lines = content.split("\n");
203
+ const allPatterns = buildPatterns(customPatterns);
204
+ for (const secretPattern of allPatterns) {
205
+ for (let i = 0; i < lines.length; i++) {
206
+ const line = lines[i];
207
+ secretPattern.pattern.lastIndex = 0;
208
+ let match;
209
+ while ((match = secretPattern.pattern.exec(line)) !== null) {
210
+ const matchText = match[0];
211
+ if (isTemplateOrPlaceholder(matchText)) continue;
212
+ findings.push({
213
+ type: secretPattern.type,
214
+ file: filePath,
215
+ line: i + 1,
216
+ match: matchText,
217
+ redacted: redactSecret(matchText),
218
+ severity: secretPattern.severity
219
+ });
220
+ }
221
+ }
222
+ }
223
+ return deduplicateFindings(findings);
224
+ }
225
+ async function scanFileForSecrets(filePath, projectPath, customPatterns = []) {
226
+ try {
227
+ const content = await readFile(filePath, "utf-8");
228
+ const relPath = relative(resolve(projectPath), resolve(filePath));
229
+ return scanContentForSecrets(content, relPath, customPatterns);
230
+ } catch {
231
+ return [];
232
+ }
233
+ }
234
+ async function scanProjectForSecrets(projectPath, filePaths, customPatterns = []) {
235
+ const allFindings = [];
236
+ for (const fp of filePaths) {
237
+ const findings = await scanFileForSecrets(fp, projectPath, customPatterns);
238
+ allFindings.push(...findings);
239
+ }
240
+ return allFindings.sort((a, b) => {
241
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
242
+ return severityOrder[a.severity] - severityOrder[b.severity];
243
+ });
244
+ }
245
+ function sanitizeContent(content, customPatterns = []) {
246
+ let sanitized = content;
247
+ const allPatterns = buildPatterns(customPatterns);
248
+ for (const secretPattern of allPatterns) {
249
+ sanitized = sanitized.replace(secretPattern.pattern, (match) => {
250
+ if (isTemplateOrPlaceholder(match)) return match;
251
+ return redactSecret(match);
252
+ });
253
+ }
254
+ return sanitized;
255
+ }
256
+ function redactSecret(value) {
257
+ if (value.length <= 8) return "***REDACTED***";
258
+ const prefix = value.substring(0, 4);
259
+ const suffix = value.substring(value.length - 2);
260
+ return `${prefix}${"*".repeat(Math.min(value.length - 6, 20))}${suffix}`;
261
+ }
262
+ function isTemplateOrPlaceholder(value) {
263
+ const placeholders = [
264
+ /\$\{.*\}/,
265
+ /\{\{.*\}\}/,
266
+ /%[sd]/,
267
+ /<[A-Z_]+>/,
268
+ /YOUR_.*_HERE/i,
269
+ /\bCHANGE_ME\b/i,
270
+ /\bPLACEHOLDER\b/i,
271
+ /\bexample\b/i,
272
+ /\bTODO\b/i,
273
+ /xxx+/i,
274
+ /\breplace.?me\b/i,
275
+ /\bdummy\b/i,
276
+ /\btest_?key\b/i,
277
+ /\bsample\b/i
278
+ ];
279
+ return placeholders.some((p) => p.test(value));
280
+ }
281
+ function deduplicateFindings(findings) {
282
+ const seen = /* @__PURE__ */ new Set();
283
+ return findings.filter((f) => {
284
+ const key = `${f.file}:${f.line}:${f.type}:${f.match}`;
285
+ if (seen.has(key)) return false;
286
+ seen.add(key);
287
+ return true;
288
+ });
289
+ }
290
+
291
+ // src/engine/graph-utils.ts
292
+ function matchGlob(path, pattern) {
293
+ const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "\xA7\xA7").replace(/\*/g, "[^/]*").replace(/§§/g, ".*").replace(/\?/g, ".");
294
+ try {
295
+ return new RegExp(`^${regexStr}$`).test(path);
296
+ } catch {
297
+ return false;
298
+ }
299
+ }
300
+
301
+ // src/govern/policy.ts
302
+ var DEFAULT_POLICY = {
303
+ version: "1.0",
304
+ name: "default",
305
+ rules: [
306
+ {
307
+ id: "no-env",
308
+ type: "exclude-always",
309
+ pattern: "**/*.env*",
310
+ reason: "Environment files must never be sent to AI",
311
+ enabled: true
312
+ },
313
+ {
314
+ id: "no-secrets",
315
+ type: "secret-block",
316
+ reason: "Files with detected secrets are blocked",
317
+ enabled: true
318
+ },
319
+ {
320
+ id: "min-coverage",
321
+ type: "coverage-minimum",
322
+ threshold: 70,
323
+ reason: "Warn if context coverage drops below 70%",
324
+ enabled: true
325
+ }
326
+ ]
327
+ };
328
+ function validateSelection(selection, policies, allFiles) {
329
+ const violations = [];
330
+ const warnings = [];
331
+ const includedPaths = new Set(selection.files.map((f) => f.relativePath));
332
+ for (const rule of policies.rules) {
333
+ if (!rule.enabled) continue;
334
+ switch (rule.type) {
335
+ case "exclude-always": {
336
+ if (!rule.pattern) break;
337
+ const violatingFiles = selection.files.filter(
338
+ (f) => matchGlob(f.relativePath, rule.pattern)
339
+ );
340
+ for (const f of violatingFiles) {
341
+ violations.push({
342
+ rule,
343
+ message: `File "${f.relativePath}" is included but matches exclude-always pattern "${rule.pattern}"`,
344
+ severity: "error"
345
+ });
346
+ }
347
+ break;
348
+ }
349
+ case "include-always": {
350
+ if (!rule.pattern || !allFiles) break;
351
+ const requiredFiles = allFiles.filter(
352
+ (f) => matchGlob(f.relativePath, rule.pattern)
353
+ );
354
+ for (const f of requiredFiles) {
355
+ if (!includedPaths.has(f.relativePath)) {
356
+ violations.push({
357
+ rule,
358
+ message: `File "${f.relativePath}" matches include-always pattern "${rule.pattern}" but is not included`,
359
+ severity: "warning"
360
+ });
361
+ }
362
+ }
363
+ break;
364
+ }
365
+ case "coverage-minimum": {
366
+ const threshold = rule.threshold ?? 70;
367
+ if (selection.coverage.score < threshold) {
368
+ warnings.push({
369
+ rule,
370
+ message: `Coverage ${selection.coverage.score}% is below minimum ${threshold}%`,
371
+ currentValue: selection.coverage.score,
372
+ threshold
373
+ });
374
+ }
375
+ break;
376
+ }
377
+ case "risk-maximum": {
378
+ const threshold = rule.threshold ?? 50;
379
+ if (selection.riskScore > threshold) {
380
+ warnings.push({
381
+ rule,
382
+ message: `Exclusion risk ${selection.riskScore}/100 exceeds maximum ${threshold}`,
383
+ currentValue: selection.riskScore,
384
+ threshold
385
+ });
386
+ }
387
+ break;
388
+ }
389
+ case "budget-limit": {
390
+ if (!rule.category || !rule.threshold) break;
391
+ const categoryFiles = selection.files.filter(
392
+ (f) => fileMatchesCategory(f.relativePath, rule.category)
393
+ );
394
+ const categoryTokens = categoryFiles.reduce((s, f) => s + f.tokens, 0);
395
+ const categoryPercent = selection.totalTokens > 0 ? categoryTokens / selection.totalTokens * 100 : 0;
396
+ if (categoryPercent > rule.threshold) {
397
+ warnings.push({
398
+ rule,
399
+ message: `Category "${rule.category}" uses ${Math.round(categoryPercent)}% of budget (max: ${rule.threshold}%)`,
400
+ currentValue: Math.round(categoryPercent),
401
+ threshold: rule.threshold
402
+ });
403
+ }
404
+ break;
405
+ }
406
+ }
407
+ }
408
+ return {
409
+ passed: violations.filter((v) => v.severity === "error").length === 0,
410
+ violations,
411
+ warnings
412
+ };
413
+ }
414
+ function addRule(policies, rule) {
415
+ return {
416
+ ...policies,
417
+ rules: [...policies.rules, rule]
418
+ };
419
+ }
420
+ function removeRule(policies, ruleId) {
421
+ return {
422
+ ...policies,
423
+ rules: policies.rules.filter((r) => r.id !== ruleId)
424
+ };
425
+ }
426
+ function toggleRule(policies, ruleId, enabled) {
427
+ return {
428
+ ...policies,
429
+ rules: policies.rules.map(
430
+ (r) => r.id === ruleId ? { ...r, enabled } : r
431
+ )
432
+ };
433
+ }
434
+ function fileMatchesCategory(path, category) {
435
+ switch (category) {
436
+ case "test":
437
+ return /\.(test|spec)\.[jt]sx?$/.test(path) || /\/__tests__\//.test(path) || /\/tests?\//.test(path);
438
+ case "config":
439
+ return /\.(config|rc)\.[jt]s$/.test(path) || /\.json$/.test(path) || /\.ya?ml$/.test(path);
440
+ case "docs":
441
+ return /\.(md|txt|rst)$/.test(path);
442
+ case "types":
443
+ return /types?\//i.test(path) || /\.d\.ts$/.test(path);
444
+ default:
445
+ return path.includes(category);
446
+ }
447
+ }
448
+
449
+ // src/govern/snapshot.ts
450
+ import { randomUUID as randomUUID2, createHash as createHash2 } from "crypto";
451
+ import "fs/promises";
452
+ function createSnapshot(name, analysis, selection, metadata = {}) {
453
+ const files = selection.files.map((f) => ({
454
+ relativePath: f.relativePath,
455
+ hash: hashString(`${f.relativePath}:${f.tokens}:${f.pruneLevel}`),
456
+ tokens: f.tokens,
457
+ pruneLevel: f.pruneLevel
458
+ }));
459
+ const snapshotData = files.map((f) => `${f.relativePath}:${f.hash}:${f.pruneLevel}`).sort().join("|");
460
+ return {
461
+ id: randomUUID2().substring(0, 8),
462
+ name,
463
+ createdAt: /* @__PURE__ */ new Date(),
464
+ hash: hashString(snapshotData),
465
+ projectHash: analysis.hash,
466
+ analysisHash: analysis.hash,
467
+ selectionHash: selection.hash,
468
+ files,
469
+ totalTokens: selection.totalTokens,
470
+ coverageScore: selection.coverage.score,
471
+ riskScore: selection.riskScore,
472
+ metadata
473
+ };
474
+ }
475
+ async function verifySnapshot(snapshot, currentAnalysis, currentSelection) {
476
+ const currentFiles = new Map(
477
+ currentSelection.files.map((f) => [f.relativePath, f])
478
+ );
479
+ let filesMatched = 0;
480
+ const filesMissing = [];
481
+ const filesChanged = [];
482
+ for (const snapFile of snapshot.files) {
483
+ const current = currentFiles.get(snapFile.relativePath);
484
+ if (!current) {
485
+ filesMissing.push(snapFile.relativePath);
486
+ continue;
487
+ }
488
+ const currentHash2 = hashString(
489
+ `${current.relativePath}:${current.tokens}:${current.pruneLevel}`
490
+ );
491
+ if (currentHash2 === snapFile.hash) {
492
+ filesMatched++;
493
+ } else {
494
+ filesChanged.push(snapFile.relativePath);
495
+ }
496
+ }
497
+ const currentSnapshotData = snapshot.files.map((f) => {
498
+ const current = currentFiles.get(f.relativePath);
499
+ if (!current) return `${f.relativePath}:MISSING:MISSING`;
500
+ return `${current.relativePath}:${hashString(`${current.relativePath}:${current.tokens}:${current.pruneLevel}`)}:${current.pruneLevel}`;
501
+ }).sort().join("|");
502
+ const currentHash = hashString(currentSnapshotData);
503
+ const integrityOk = currentHash === snapshot.hash && filesMissing.length === 0 && filesChanged.length === 0;
504
+ return {
505
+ valid: integrityOk,
506
+ snapshotId: snapshot.id,
507
+ filesChecked: snapshot.files.length,
508
+ filesMatched,
509
+ filesMissing,
510
+ filesChanged,
511
+ integrityOk
512
+ };
513
+ }
514
+ function compareSnapshots(older, newer) {
515
+ const olderFiles = new Map(older.files.map((f) => [f.relativePath, f]));
516
+ const newerFiles = new Map(newer.files.map((f) => [f.relativePath, f]));
517
+ const added = [];
518
+ const removed = [];
519
+ const changed = [];
520
+ for (const [path, file] of newerFiles) {
521
+ const old = olderFiles.get(path);
522
+ if (!old) {
523
+ added.push(path);
524
+ } else if (old.hash !== file.hash) {
525
+ changed.push(path);
526
+ }
527
+ }
528
+ for (const path of olderFiles.keys()) {
529
+ if (!newerFiles.has(path)) {
530
+ removed.push(path);
531
+ }
532
+ }
533
+ return {
534
+ added,
535
+ removed,
536
+ changed,
537
+ tokenDelta: newer.totalTokens - older.totalTokens,
538
+ coverageDelta: newer.coverageScore - older.coverageScore,
539
+ riskDelta: newer.riskScore - older.riskScore
540
+ };
541
+ }
542
+ function hashString(input) {
543
+ return createHash2("sha256").update(input).digest("hex").substring(0, 16);
544
+ }
545
+
546
+ // src/govern/integrity.ts
547
+ import { createHash as createHash3 } from "crypto";
548
+ import { readFile as readFile3, chmod as chmod2, readdir as readdir2, stat } from "fs/promises";
549
+ import { join as join2 } from "path";
550
+ function hashContent(content) {
551
+ return createHash3("sha256").update(content).digest("hex");
552
+ }
553
+ async function hashFile(filePath) {
554
+ try {
555
+ const content = await readFile3(filePath);
556
+ return hashContent(content);
557
+ } catch {
558
+ return null;
559
+ }
560
+ }
561
+ async function buildManifest(projectDir) {
562
+ const entries = [];
563
+ async function scanDir(dir, type) {
564
+ let files;
565
+ try {
566
+ files = await readdir2(dir);
567
+ } catch {
568
+ return;
569
+ }
570
+ for (const file of files) {
571
+ const fullPath = join2(dir, file);
572
+ try {
573
+ const fileStat = await stat(fullPath);
574
+ if (fileStat.isFile()) {
575
+ const hash = await hashFile(fullPath);
576
+ if (hash) {
577
+ entries.push({
578
+ filePath: fullPath,
579
+ hash,
580
+ size: fileStat.size,
581
+ createdAt: fileStat.mtime,
582
+ type
583
+ });
584
+ }
585
+ }
586
+ } catch {
587
+ }
588
+ }
589
+ }
590
+ await scanDir(join2(projectDir, "snapshots"), "snapshot");
591
+ await scanDir(join2(projectDir, "audit"), "audit");
592
+ await scanDir(projectDir, "config");
593
+ return {
594
+ version: "2.0",
595
+ createdAt: /* @__PURE__ */ new Date(),
596
+ entries
597
+ };
598
+ }
599
+ async function verifyManifest(manifest) {
600
+ const invalidFiles = [];
601
+ const missingFiles = [];
602
+ for (const entry of manifest.entries) {
603
+ const currentHash = await hashFile(entry.filePath);
604
+ if (currentHash === null) {
605
+ missingFiles.push(entry.filePath);
606
+ } else if (currentHash !== entry.hash) {
607
+ invalidFiles.push(entry.filePath);
608
+ }
609
+ }
610
+ return {
611
+ totalFiles: manifest.entries.length,
612
+ validFiles: manifest.entries.length - invalidFiles.length - missingFiles.length,
613
+ invalidFiles,
614
+ missingFiles
615
+ };
616
+ }
617
+ async function securePermissions(dirPath) {
618
+ let count = 0;
619
+ try {
620
+ await chmod2(dirPath, 448);
621
+ count++;
622
+ const files = await readdir2(dirPath);
623
+ for (const file of files) {
624
+ try {
625
+ const fullPath = join2(dirPath, file);
626
+ const fileStat = await stat(fullPath);
627
+ if (fileStat.isFile()) {
628
+ await chmod2(fullPath, 384);
629
+ count++;
630
+ }
631
+ } catch {
632
+ }
633
+ }
634
+ } catch {
635
+ }
636
+ return count;
637
+ }
638
+ export {
639
+ DEFAULT_POLICY,
640
+ addRule,
641
+ buildManifest,
642
+ compareSnapshots,
643
+ createSnapshot,
644
+ getAuditEntries,
645
+ hashContent,
646
+ hashFile,
647
+ logAudit,
648
+ purgeOldAuditEntries,
649
+ removeRule,
650
+ sanitizeContent,
651
+ scanContentForSecrets,
652
+ scanFileForSecrets,
653
+ scanProjectForSecrets,
654
+ securePermissions,
655
+ toggleRule,
656
+ validateSelection,
657
+ verifyAuditEntry,
658
+ verifyAuditIntegrity,
659
+ verifyManifest,
660
+ verifySnapshot
661
+ };
662
+ //# sourceMappingURL=index.js.map