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.
- package/DOCS.md +151 -0
- package/README.md +124 -15
- package/dist/action/index.js +23 -1
- package/dist/api/dashboard.js +23 -1
- package/dist/api/dashboard.js.map +1 -1
- package/dist/api/server.js +23 -1
- package/dist/api/server.js.map +1 -1
- package/dist/cli/score.js +754 -13
- package/dist/cli/v2/index.js +23 -1
- package/dist/cli/v2/index.js.map +1 -1
- package/dist/engine/index.js +23 -1
- package/dist/engine/index.js.map +1 -1
- package/dist/govern/index.d.ts +284 -0
- package/dist/govern/index.js +155 -1
- package/dist/govern/index.js.map +1 -1
- package/dist/interact/index.js +23 -1
- package/dist/interact/index.js.map +1 -1
- package/dist/mcp/v2.js +23 -1
- package/dist/mcp/v2.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 };
|
package/dist/govern/index.js
CHANGED
|
@@ -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,
|