cto-ai-cli 3.0.2 → 3.2.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.
@@ -0,0 +1,284 @@
1
+ interface AuditEntry {
2
+ id: string;
3
+ timestamp: Date;
4
+ action: AuditAction;
5
+ user: string;
6
+ projectPath: string;
7
+ contextHash?: string;
8
+ filesIncluded?: number;
9
+ filesExcluded?: number;
10
+ tokensUsed?: number;
11
+ coverageScore?: number;
12
+ riskScore?: number;
13
+ model?: string;
14
+ estimatedCost?: number;
15
+ integrityHash: string;
16
+ details: Record<string, unknown>;
17
+ }
18
+ type AuditAction = 'init' | 'analyze' | 'interact' | 'snapshot-create' | 'snapshot-verify' | 'policy-change' | 'secret-detected' | 'integrity-check';
19
+ interface PolicySet {
20
+ version: string;
21
+ name: string;
22
+ rules: PolicyRule[];
23
+ }
24
+ interface PolicyRule {
25
+ id: string;
26
+ type: PolicyRuleType;
27
+ pattern?: string;
28
+ threshold?: number;
29
+ category?: string;
30
+ reason: string;
31
+ enabled: boolean;
32
+ }
33
+ type PolicyRuleType = 'include-always' | 'exclude-always' | 'budget-limit' | 'coverage-minimum' | 'risk-maximum' | 'secret-block';
34
+ interface PolicyValidation {
35
+ passed: boolean;
36
+ violations: PolicyViolation[];
37
+ warnings: PolicyWarning[];
38
+ }
39
+ interface PolicyViolation {
40
+ rule: PolicyRule;
41
+ message: string;
42
+ severity: 'error' | 'warning';
43
+ }
44
+ interface PolicyWarning {
45
+ rule: PolicyRule;
46
+ message: string;
47
+ currentValue: number;
48
+ threshold: number;
49
+ }
50
+ interface ContextSnapshot {
51
+ id: string;
52
+ name: string;
53
+ createdAt: Date;
54
+ hash: string;
55
+ projectHash: string;
56
+ analysisHash: string;
57
+ selectionHash: string;
58
+ files: SnapshotFile[];
59
+ totalTokens: number;
60
+ coverageScore: number;
61
+ riskScore: number;
62
+ metadata: Record<string, unknown>;
63
+ }
64
+ interface SnapshotFile {
65
+ relativePath: string;
66
+ hash: string;
67
+ tokens: number;
68
+ pruneLevel: string;
69
+ }
70
+ interface SnapshotVerification {
71
+ valid: boolean;
72
+ snapshotId: string;
73
+ filesChecked: number;
74
+ filesMatched: number;
75
+ filesMissing: string[];
76
+ filesChanged: string[];
77
+ integrityOk: boolean;
78
+ }
79
+ interface SecretFinding {
80
+ type: SecretType;
81
+ file: string;
82
+ line: number;
83
+ match: string;
84
+ redacted: string;
85
+ severity: 'critical' | 'high' | 'medium' | 'low';
86
+ }
87
+ type SecretType = 'api-key' | 'aws-key' | 'private-key' | 'password' | 'token' | 'connection-string' | 'env-variable' | 'pii' | 'high-entropy' | 'custom';
88
+ interface IntegrityManifest {
89
+ version: string;
90
+ createdAt: Date;
91
+ entries: IntegrityEntry[];
92
+ }
93
+ interface IntegrityEntry {
94
+ filePath: string;
95
+ hash: string;
96
+ size: number;
97
+ createdAt: Date;
98
+ type: 'snapshot' | 'audit' | 'config' | 'policy';
99
+ }
100
+
101
+ declare function logAudit(action: AuditAction, projectPath: string, details?: Record<string, unknown>): Promise<AuditEntry>;
102
+ declare function getAuditEntries(options?: {
103
+ projectPath?: string;
104
+ action?: AuditAction;
105
+ since?: Date;
106
+ limit?: number;
107
+ }): Promise<AuditEntry[]>;
108
+ declare function verifyAuditEntry(entry: AuditEntry): boolean;
109
+ declare function verifyAuditIntegrity(): Promise<{
110
+ totalEntries: number;
111
+ validEntries: number;
112
+ invalidEntries: AuditEntry[];
113
+ }>;
114
+ declare function purgeOldAuditEntries(retentionDays: number): Promise<number>;
115
+
116
+ declare function scanContentForSecrets(content: string, filePath: string, customPatterns?: string[]): SecretFinding[];
117
+ declare function scanFileForSecrets(filePath: string, projectPath: string, customPatterns?: string[]): Promise<SecretFinding[]>;
118
+ declare function scanProjectForSecrets(projectPath: string, filePaths: string[], customPatterns?: string[]): Promise<SecretFinding[]>;
119
+ declare function sanitizeContent(content: string, customPatterns?: string[]): string;
120
+ declare function scanContentForHighEntropy(content: string, filePath: string, threshold?: number): SecretFinding[];
121
+ interface AuditResult {
122
+ findings: SecretFinding[];
123
+ summary: {
124
+ totalFiles: number;
125
+ filesScanned: number;
126
+ filesWithSecrets: number;
127
+ totalFindings: number;
128
+ bySeverity: {
129
+ critical: number;
130
+ high: number;
131
+ medium: number;
132
+ low: number;
133
+ };
134
+ byType: Record<string, number>;
135
+ };
136
+ recommendations: string[];
137
+ }
138
+ declare function auditProject(projectPath: string, filePaths: string[], options?: {
139
+ customPatterns?: string[];
140
+ entropyThreshold?: number;
141
+ includePII?: boolean;
142
+ }): Promise<AuditResult>;
143
+
144
+ interface AnalyzedFile {
145
+ path: string;
146
+ relativePath: string;
147
+ extension: string;
148
+ size: number;
149
+ tokens: number;
150
+ lines: number;
151
+ lastModified: Date;
152
+ kind: FileKind;
153
+ imports: string[];
154
+ importedBy: string[];
155
+ isHub: boolean;
156
+ complexity: number;
157
+ riskScore: number;
158
+ riskFactors: RiskFactor[];
159
+ exclusionImpact: ExclusionImpact;
160
+ }
161
+ type FileKind = 'source' | 'type' | 'test' | 'config' | 'entry' | 'asset';
162
+ type ExclusionImpact = 'critical' | 'high' | 'medium' | 'low' | 'none';
163
+ interface ProjectAnalysis {
164
+ projectPath: string;
165
+ projectName: string;
166
+ analyzedAt: Date;
167
+ hash: string;
168
+ files: AnalyzedFile[];
169
+ totalFiles: number;
170
+ totalTokens: number;
171
+ graph: ProjectGraph;
172
+ riskProfile: RiskProfile;
173
+ stack: string[];
174
+ tokenMethod: 'chars4' | 'tiktoken';
175
+ }
176
+ interface ProjectGraph {
177
+ nodes: string[];
178
+ edges: GraphEdge[];
179
+ hubs: HubNode[];
180
+ leaves: string[];
181
+ orphans: string[];
182
+ clusters: FileCluster[];
183
+ }
184
+ interface GraphEdge {
185
+ from: string;
186
+ to: string;
187
+ type: 'import' | 'export' | 're-export';
188
+ }
189
+ interface HubNode {
190
+ relativePath: string;
191
+ dependents: number;
192
+ dependencies: number;
193
+ score: number;
194
+ }
195
+ interface FileCluster {
196
+ id: string;
197
+ name: string;
198
+ files: string[];
199
+ totalTokens: number;
200
+ internalEdges: number;
201
+ externalEdges: number;
202
+ cohesion: number;
203
+ }
204
+ interface RiskProfile {
205
+ distribution: {
206
+ critical: number;
207
+ high: number;
208
+ medium: number;
209
+ low: number;
210
+ };
211
+ topRiskFiles: AnalyzedFile[];
212
+ overallComplexity: number;
213
+ }
214
+ interface RiskFactor {
215
+ type: RiskFactorType;
216
+ score: number;
217
+ weight: number;
218
+ detail: string;
219
+ }
220
+ type RiskFactorType = 'hub' | 'type-provider' | 'complexity' | 'recency' | 'config' | 'churn';
221
+ interface CoverageResult {
222
+ score: number;
223
+ relevantFiles: string[];
224
+ includedRelevant: string[];
225
+ missingRelevant: string[];
226
+ missingCritical: string[];
227
+ explanation: string;
228
+ }
229
+ interface ContextSelection {
230
+ files: SelectedFile[];
231
+ totalTokens: number;
232
+ budget: number;
233
+ usedPercent: number;
234
+ coverage: CoverageResult;
235
+ riskScore: number;
236
+ deterministic: boolean;
237
+ hash: string;
238
+ decisions: SelectionDecision[];
239
+ }
240
+ interface SelectedFile {
241
+ relativePath: string;
242
+ tokens: number;
243
+ originalTokens: number;
244
+ pruneLevel: PruneLevel;
245
+ riskScore: number;
246
+ reason: string;
247
+ }
248
+ type PruneLevel = 'full' | 'signatures' | 'skeleton' | 'excluded';
249
+ interface SelectionDecision {
250
+ file: string;
251
+ action: 'include-full' | 'include-signatures' | 'include-skeleton' | 'exclude';
252
+ reason: string;
253
+ alternatives?: string;
254
+ }
255
+
256
+ declare const DEFAULT_POLICY: PolicySet;
257
+ declare function validateSelection(selection: ContextSelection, policies: PolicySet, allFiles?: AnalyzedFile[]): PolicyValidation;
258
+ declare function addRule(policies: PolicySet, rule: PolicyRule): PolicySet;
259
+ declare function removeRule(policies: PolicySet, ruleId: string): PolicySet;
260
+ declare function toggleRule(policies: PolicySet, ruleId: string, enabled: boolean): PolicySet;
261
+
262
+ declare function createSnapshot(name: string, analysis: ProjectAnalysis, selection: ContextSelection, metadata?: Record<string, unknown>): ContextSnapshot;
263
+ declare function verifySnapshot(snapshot: ContextSnapshot, currentAnalysis: ProjectAnalysis, currentSelection: ContextSelection): Promise<SnapshotVerification>;
264
+ declare function compareSnapshots(older: ContextSnapshot, newer: ContextSnapshot): {
265
+ added: string[];
266
+ removed: string[];
267
+ changed: string[];
268
+ tokenDelta: number;
269
+ coverageDelta: number;
270
+ riskDelta: number;
271
+ };
272
+
273
+ declare function hashContent(content: Buffer | string): string;
274
+ declare function hashFile(filePath: string): Promise<string | null>;
275
+ declare function buildManifest(projectDir: string): Promise<IntegrityManifest>;
276
+ declare function verifyManifest(manifest: IntegrityManifest): Promise<{
277
+ totalFiles: number;
278
+ validFiles: number;
279
+ invalidFiles: string[];
280
+ missingFiles: string[];
281
+ }>;
282
+ declare function securePermissions(dirPath: string): Promise<number>;
283
+
284
+ export { type AuditResult, DEFAULT_POLICY, addRule, auditProject, buildManifest, compareSnapshots, createSnapshot, getAuditEntries, hashContent, hashFile, logAudit, purgeOldAuditEntries, removeRule, sanitizeContent, scanContentForHighEntropy, scanContentForSecrets, scanFileForSecrets, scanProjectForSecrets, securePermissions, toggleRule, validateSelection, verifyAuditEntry, verifyAuditIntegrity, verifyManifest, verifySnapshot };
@@ -175,7 +175,29 @@ var BUILTIN_PATTERNS = [
175
175
  { type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
176
176
  { type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
177
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" }
178
+ { type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" },
179
+ // Stripe
180
+ { type: "api-key", source: "sk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Live Secret Key" },
181
+ { type: "api-key", source: "pk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "high", description: "Stripe Live Publishable Key" },
182
+ { type: "api-key", source: "rk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Restricted Key" },
183
+ // Slack
184
+ { type: "token", source: "xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack Bot Token" },
185
+ { type: "token", source: "xoxp-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack User Token" },
186
+ { type: "api-key", source: "https://hooks\\.slack\\.com/services/T[a-zA-Z0-9_]+/B[a-zA-Z0-9_]+/[a-zA-Z0-9_]+", flags: "g", severity: "high", description: "Slack Webhook URL" },
187
+ // Google
188
+ { type: "api-key", source: "AIza[0-9A-Za-z_-]{35}", flags: "g", severity: "high", description: "Google API Key" },
189
+ { type: "token", source: "ya29\\.[0-9A-Za-z_-]+", flags: "g", severity: "high", description: "Google OAuth Token" },
190
+ // Azure
191
+ { type: "api-key", source: "(?:AccountKey|SharedAccessKey)\\s*=\\s*[a-zA-Z0-9+/=]{40,}", flags: "g", severity: "critical", description: "Azure Storage Key" },
192
+ // Twilio
193
+ { type: "api-key", source: "AC[a-f0-9]{32}", flags: "g", severity: "high", description: "Twilio Account SID" },
194
+ // SendGrid
195
+ { type: "api-key", source: "SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}", flags: "g", severity: "critical", description: "SendGrid API Key" },
196
+ // JWT
197
+ { type: "token", source: "eyJ[a-zA-Z0-9_-]{10,}\\.eyJ[a-zA-Z0-9_-]{10,}\\.[a-zA-Z0-9_-]{10,}", flags: "g", severity: "high", description: "JSON Web Token" },
198
+ // PII
199
+ { type: "pii", source: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b", flags: "g", severity: "medium", description: "Email Address (PII)" },
200
+ { type: "pii", source: "\\b\\d{3}[-.]?\\d{2}[-.]?\\d{4}\\b", flags: "g", severity: "high", description: "Possible SSN (PII)" }
179
201
  ];
180
202
  function buildPatterns(customPatterns = []) {
181
203
  const patterns = BUILTIN_PATTERNS.map((def) => ({
@@ -287,6 +309,136 @@ function deduplicateFindings(findings) {
287
309
  return true;
288
310
  });
289
311
  }
312
+ function shannonEntropy(str) {
313
+ const freq = /* @__PURE__ */ new Map();
314
+ for (const ch of str) {
315
+ freq.set(ch, (freq.get(ch) || 0) + 1);
316
+ }
317
+ let entropy = 0;
318
+ for (const count of freq.values()) {
319
+ const p = count / str.length;
320
+ if (p > 0) entropy -= p * Math.log2(p);
321
+ }
322
+ return entropy;
323
+ }
324
+ var HIGH_ENTROPY_RE = /['"]([a-zA-Z0-9+/=_\-]{30,})['"]|=\s*['"]?([a-zA-Z0-9+/=_\-]{30,})['"]?/g;
325
+ var ENTROPY_SKIP = [
326
+ /^[a-f0-9]{32,}$/i,
327
+ // hex hashes
328
+ /^[A-Z_]{30,}$/,
329
+ // all-caps constants
330
+ /^[a-z_]{30,}$/,
331
+ // all-lowercase identifiers
332
+ /^[a-zA-Z0-9+/]+=+$/,
333
+ // base64 padding
334
+ /^[a-z]+[A-Z][a-zA-Z]+$/,
335
+ // camelCase identifiers
336
+ /sha\d+-/i
337
+ // integrity hashes (sha256-, sha512-)
338
+ ];
339
+ function scanContentForHighEntropy(content, filePath, threshold = 5) {
340
+ const findings = [];
341
+ const lines = content.split("\n");
342
+ for (let i = 0; i < lines.length; i++) {
343
+ const line = lines[i];
344
+ if (line.trim().startsWith("//") || line.trim().startsWith("#") || line.trim().startsWith("*")) continue;
345
+ HIGH_ENTROPY_RE.lastIndex = 0;
346
+ let match;
347
+ while ((match = HIGH_ENTROPY_RE.exec(line)) !== null) {
348
+ const value = match[1] || match[2];
349
+ if (!value || value.length < 40) continue;
350
+ if (isTemplateOrPlaceholder(value)) continue;
351
+ if (ENTROPY_SKIP.some((p) => p.test(value))) continue;
352
+ const entropy = shannonEntropy(value);
353
+ if (entropy >= threshold) {
354
+ findings.push({
355
+ type: "high-entropy",
356
+ file: filePath,
357
+ line: i + 1,
358
+ match: value,
359
+ redacted: redactSecret(value),
360
+ severity: entropy >= 5 ? "high" : "medium"
361
+ });
362
+ }
363
+ }
364
+ }
365
+ return deduplicateFindings(findings);
366
+ }
367
+ async function auditProject(projectPath, filePaths, options = {}) {
368
+ const { customPatterns = [], entropyThreshold = 4.5, includePII = true } = options;
369
+ const allFindings = [];
370
+ const filesWithSecrets = /* @__PURE__ */ new Set();
371
+ for (const fp of filePaths) {
372
+ try {
373
+ const content = await readFile(fp, "utf-8");
374
+ const relPath = relative(resolve(projectPath), resolve(fp));
375
+ const isTestFile = /\.(test|spec|mock)\.[jt]sx?$/.test(relPath) || relPath.includes("__tests__");
376
+ const isDtsFile = relPath.endsWith(".d.ts");
377
+ let findings = scanContentForSecrets(content, relPath, customPatterns);
378
+ if (!includePII) {
379
+ findings = findings.filter((f) => f.type !== "pii");
380
+ }
381
+ const entropyFindings = isTestFile || isDtsFile ? [] : scanContentForHighEntropy(content, relPath, entropyThreshold);
382
+ const combined = [...findings, ...entropyFindings];
383
+ if (combined.length > 0) {
384
+ filesWithSecrets.add(relPath);
385
+ allFindings.push(...combined);
386
+ }
387
+ } catch {
388
+ }
389
+ }
390
+ allFindings.sort((a, b) => {
391
+ const order = { critical: 0, high: 1, medium: 2, low: 3 };
392
+ return order[a.severity] - order[b.severity];
393
+ });
394
+ const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
395
+ const byType = {};
396
+ for (const f of allFindings) {
397
+ bySeverity[f.severity]++;
398
+ byType[f.type] = (byType[f.type] || 0) + 1;
399
+ }
400
+ const recommendations = [];
401
+ if (bySeverity.critical > 0) {
402
+ recommendations.push("CRITICAL: Rotate all detected credentials immediately. They may already be compromised.");
403
+ }
404
+ if (byType["password"] > 0) {
405
+ recommendations.push("Move passwords to environment variables or a secrets manager (AWS Secrets Manager, Vault, etc.).");
406
+ }
407
+ if (byType["api-key"] > 0 || byType["aws-key"] > 0) {
408
+ recommendations.push("Use environment variables for API keys. Never commit them to source control.");
409
+ }
410
+ if (byType["connection-string"] > 0) {
411
+ recommendations.push("Database connection strings should use environment variables, not hardcoded values.");
412
+ }
413
+ if (byType["private-key"] > 0) {
414
+ recommendations.push("Private keys should NEVER be in source code. Use a key management service.");
415
+ }
416
+ if (byType["pii"] > 0) {
417
+ recommendations.push("PII detected. Review for GDPR/CCPA compliance. Consider data anonymization.");
418
+ }
419
+ if (byType["high-entropy"] > 0) {
420
+ recommendations.push("High-entropy strings detected that may be secrets. Review manually.");
421
+ }
422
+ if (allFindings.length > 0) {
423
+ recommendations.push("Add a .gitignore entry for .env files if not already present.");
424
+ recommendations.push("Run `npx cto-ai-cli --audit` regularly or add to CI pipeline.");
425
+ }
426
+ if (allFindings.length === 0) {
427
+ recommendations.push("No secrets detected. Great job keeping your codebase clean!");
428
+ }
429
+ return {
430
+ findings: allFindings,
431
+ summary: {
432
+ totalFiles: filePaths.length,
433
+ filesScanned: filePaths.length,
434
+ filesWithSecrets: filesWithSecrets.size,
435
+ totalFindings: allFindings.length,
436
+ bySeverity,
437
+ byType
438
+ },
439
+ recommendations
440
+ };
441
+ }
290
442
 
291
443
  // src/engine/graph-utils.ts
292
444
  function matchGlob(path, pattern) {
@@ -638,6 +790,7 @@ async function securePermissions(dirPath) {
638
790
  export {
639
791
  DEFAULT_POLICY,
640
792
  addRule,
793
+ auditProject,
641
794
  buildManifest,
642
795
  compareSnapshots,
643
796
  createSnapshot,
@@ -648,6 +801,7 @@ export {
648
801
  purgeOldAuditEntries,
649
802
  removeRule,
650
803
  sanitizeContent,
804
+ scanContentForHighEntropy,
651
805
  scanContentForSecrets,
652
806
  scanFileForSecrets,
653
807
  scanProjectForSecrets,