@cencori/scan 0.1.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/README.md +98 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +679 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +656 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +536 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +496 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
var import_chalk = __toESM(require("chalk"));
|
|
29
|
+
var import_ora = __toESM(require("ora"));
|
|
30
|
+
|
|
31
|
+
// src/scanner/index.ts
|
|
32
|
+
var fs = __toESM(require("fs"));
|
|
33
|
+
var path = __toESM(require("path"));
|
|
34
|
+
var import_glob = require("glob");
|
|
35
|
+
|
|
36
|
+
// src/scanner/patterns.ts
|
|
37
|
+
var SECRET_PATTERNS = [
|
|
38
|
+
// OpenAI
|
|
39
|
+
{
|
|
40
|
+
name: "OpenAI API Key",
|
|
41
|
+
provider: "OpenAI",
|
|
42
|
+
pattern: /sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20}/g,
|
|
43
|
+
severity: "critical"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "OpenAI Project Key",
|
|
47
|
+
provider: "OpenAI",
|
|
48
|
+
pattern: /sk-proj-[a-zA-Z0-9_-]{80,}/g,
|
|
49
|
+
severity: "critical"
|
|
50
|
+
},
|
|
51
|
+
// Anthropic
|
|
52
|
+
{
|
|
53
|
+
name: "Anthropic API Key",
|
|
54
|
+
provider: "Anthropic",
|
|
55
|
+
pattern: /sk-ant-[a-zA-Z0-9-]{90,}/g,
|
|
56
|
+
severity: "critical"
|
|
57
|
+
},
|
|
58
|
+
// Google
|
|
59
|
+
{
|
|
60
|
+
name: "Google API Key",
|
|
61
|
+
provider: "Google",
|
|
62
|
+
pattern: /AIza[0-9A-Za-z_-]{35}/g,
|
|
63
|
+
severity: "critical"
|
|
64
|
+
},
|
|
65
|
+
// Supabase
|
|
66
|
+
{
|
|
67
|
+
name: "Supabase Service Role Key",
|
|
68
|
+
provider: "Supabase",
|
|
69
|
+
pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g,
|
|
70
|
+
severity: "critical"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "Supabase Anon Key (if hardcoded)",
|
|
74
|
+
provider: "Supabase",
|
|
75
|
+
pattern: /SUPABASE_ANON_KEY\s*[:=]\s*["']eyJ[^"']+["']/g,
|
|
76
|
+
severity: "medium"
|
|
77
|
+
},
|
|
78
|
+
// Stripe
|
|
79
|
+
{
|
|
80
|
+
name: "Stripe Secret Key",
|
|
81
|
+
provider: "Stripe",
|
|
82
|
+
pattern: /sk_live_[0-9a-zA-Z]{24,}/g,
|
|
83
|
+
severity: "critical"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "Stripe Test Key",
|
|
87
|
+
provider: "Stripe",
|
|
88
|
+
pattern: /sk_test_[0-9a-zA-Z]{24,}/g,
|
|
89
|
+
severity: "medium"
|
|
90
|
+
},
|
|
91
|
+
// AWS
|
|
92
|
+
{
|
|
93
|
+
name: "AWS Access Key ID",
|
|
94
|
+
provider: "AWS",
|
|
95
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
96
|
+
severity: "critical"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "AWS Secret Access Key",
|
|
100
|
+
provider: "AWS",
|
|
101
|
+
pattern: /aws_secret_access_key\s*[:=]\s*["'][A-Za-z0-9/+=]{40}["']/gi,
|
|
102
|
+
severity: "critical"
|
|
103
|
+
},
|
|
104
|
+
// GitHub
|
|
105
|
+
{
|
|
106
|
+
name: "GitHub Personal Access Token",
|
|
107
|
+
provider: "GitHub",
|
|
108
|
+
pattern: /ghp_[a-zA-Z0-9]{36}/g,
|
|
109
|
+
severity: "critical"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "GitHub OAuth Token",
|
|
113
|
+
provider: "GitHub",
|
|
114
|
+
pattern: /gho_[a-zA-Z0-9]{36}/g,
|
|
115
|
+
severity: "critical"
|
|
116
|
+
},
|
|
117
|
+
// Telegram
|
|
118
|
+
{
|
|
119
|
+
name: "Telegram Bot Token",
|
|
120
|
+
provider: "Telegram",
|
|
121
|
+
pattern: /[0-9]{9,10}:[a-zA-Z0-9_-]{35}/g,
|
|
122
|
+
severity: "high"
|
|
123
|
+
},
|
|
124
|
+
// Discord
|
|
125
|
+
{
|
|
126
|
+
name: "Discord Bot Token",
|
|
127
|
+
provider: "Discord",
|
|
128
|
+
pattern: /[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27}/g,
|
|
129
|
+
severity: "high"
|
|
130
|
+
},
|
|
131
|
+
// Slack
|
|
132
|
+
{
|
|
133
|
+
name: "Slack Bot Token",
|
|
134
|
+
provider: "Slack",
|
|
135
|
+
pattern: /xoxb-[0-9]{11}-[0-9]{11}-[a-zA-Z0-9]{24}/g,
|
|
136
|
+
severity: "high"
|
|
137
|
+
},
|
|
138
|
+
// SendGrid
|
|
139
|
+
{
|
|
140
|
+
name: "SendGrid API Key",
|
|
141
|
+
provider: "SendGrid",
|
|
142
|
+
pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g,
|
|
143
|
+
severity: "high"
|
|
144
|
+
},
|
|
145
|
+
// Twilio
|
|
146
|
+
{
|
|
147
|
+
name: "Twilio API Key",
|
|
148
|
+
provider: "Twilio",
|
|
149
|
+
pattern: /SK[a-fA-F0-9]{32}/g,
|
|
150
|
+
severity: "high"
|
|
151
|
+
},
|
|
152
|
+
// Mailgun
|
|
153
|
+
{
|
|
154
|
+
name: "Mailgun API Key",
|
|
155
|
+
provider: "Mailgun",
|
|
156
|
+
pattern: /key-[a-zA-Z0-9]{32}/g,
|
|
157
|
+
severity: "high"
|
|
158
|
+
},
|
|
159
|
+
// Firebase
|
|
160
|
+
{
|
|
161
|
+
name: "Firebase Database URL",
|
|
162
|
+
provider: "Firebase",
|
|
163
|
+
pattern: /https:\/\/[a-z0-9-]+\.firebaseio\.com/g,
|
|
164
|
+
severity: "medium"
|
|
165
|
+
},
|
|
166
|
+
// Generic patterns
|
|
167
|
+
{
|
|
168
|
+
name: "Private Key",
|
|
169
|
+
provider: "Generic",
|
|
170
|
+
pattern: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
|
|
171
|
+
severity: "critical"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "Generic API Key Assignment",
|
|
175
|
+
provider: "Generic",
|
|
176
|
+
pattern: /(api_key|apikey|api_secret|secret_key)\s*[:=]\s*["'][a-zA-Z0-9_-]{20,}["']/gi,
|
|
177
|
+
severity: "high"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "Password Assignment",
|
|
181
|
+
provider: "Generic",
|
|
182
|
+
pattern: /(password|passwd|pwd)\s*[:=]\s*["'][^"']{8,}["']/gi,
|
|
183
|
+
severity: "high"
|
|
184
|
+
},
|
|
185
|
+
// Replicate
|
|
186
|
+
{
|
|
187
|
+
name: "Replicate API Token",
|
|
188
|
+
provider: "Replicate",
|
|
189
|
+
pattern: /r8_[a-zA-Z0-9]{38}/g,
|
|
190
|
+
severity: "critical"
|
|
191
|
+
},
|
|
192
|
+
// Hugging Face
|
|
193
|
+
{
|
|
194
|
+
name: "Hugging Face Token",
|
|
195
|
+
provider: "Hugging Face",
|
|
196
|
+
pattern: /hf_[a-zA-Z0-9]{34}/g,
|
|
197
|
+
severity: "critical"
|
|
198
|
+
},
|
|
199
|
+
// Cohere
|
|
200
|
+
{
|
|
201
|
+
name: "Cohere API Key",
|
|
202
|
+
provider: "Cohere",
|
|
203
|
+
pattern: /[a-zA-Z0-9]{40}/g,
|
|
204
|
+
// Less specific, check context
|
|
205
|
+
severity: "medium"
|
|
206
|
+
}
|
|
207
|
+
];
|
|
208
|
+
var PII_PATTERNS = [
|
|
209
|
+
{
|
|
210
|
+
name: "Email Address",
|
|
211
|
+
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
212
|
+
severity: "medium"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "Phone Number (US)",
|
|
216
|
+
pattern: /(\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g,
|
|
217
|
+
severity: "medium"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "Phone Number (International)",
|
|
221
|
+
pattern: /\+[1-9]\d{1,14}/g,
|
|
222
|
+
severity: "medium"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: "Social Security Number",
|
|
226
|
+
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
227
|
+
severity: "high"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "Credit Card Number",
|
|
231
|
+
pattern: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
|
|
232
|
+
severity: "high"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "IP Address",
|
|
236
|
+
pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
|
237
|
+
severity: "low"
|
|
238
|
+
}
|
|
239
|
+
];
|
|
240
|
+
var ROUTE_PATTERNS = [
|
|
241
|
+
// Next.js API routes without auth
|
|
242
|
+
{
|
|
243
|
+
name: "Next.js API Route (check for auth)",
|
|
244
|
+
framework: "Next.js",
|
|
245
|
+
pattern: /export\s+(async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH)\s*\(/g,
|
|
246
|
+
severity: "medium",
|
|
247
|
+
description: "API route handler - verify authentication is implemented"
|
|
248
|
+
},
|
|
249
|
+
// Express routes
|
|
250
|
+
{
|
|
251
|
+
name: "Express Route without Auth Middleware",
|
|
252
|
+
framework: "Express",
|
|
253
|
+
pattern: /app\.(get|post|put|delete|patch)\s*\(\s*["'`][^"'`]+["'`]\s*,\s*(?!.*auth)/gi,
|
|
254
|
+
severity: "medium",
|
|
255
|
+
description: "Express route - check if auth middleware is applied"
|
|
256
|
+
}
|
|
257
|
+
];
|
|
258
|
+
var IGNORE_PATTERNS = [
|
|
259
|
+
"node_modules",
|
|
260
|
+
".git",
|
|
261
|
+
"dist",
|
|
262
|
+
"build",
|
|
263
|
+
".next",
|
|
264
|
+
".venv",
|
|
265
|
+
"__pycache__",
|
|
266
|
+
"*.min.js",
|
|
267
|
+
"*.min.css",
|
|
268
|
+
"*.map",
|
|
269
|
+
"package-lock.json",
|
|
270
|
+
"yarn.lock",
|
|
271
|
+
"pnpm-lock.yaml"
|
|
272
|
+
];
|
|
273
|
+
var SCANNABLE_EXTENSIONS = [
|
|
274
|
+
".js",
|
|
275
|
+
".jsx",
|
|
276
|
+
".ts",
|
|
277
|
+
".tsx",
|
|
278
|
+
".mjs",
|
|
279
|
+
".cjs",
|
|
280
|
+
".py",
|
|
281
|
+
".rb",
|
|
282
|
+
".go",
|
|
283
|
+
".java",
|
|
284
|
+
".php",
|
|
285
|
+
".env",
|
|
286
|
+
".json",
|
|
287
|
+
".yaml",
|
|
288
|
+
".yml",
|
|
289
|
+
".toml",
|
|
290
|
+
".xml",
|
|
291
|
+
".md",
|
|
292
|
+
".txt",
|
|
293
|
+
".sql",
|
|
294
|
+
".sh",
|
|
295
|
+
".bash",
|
|
296
|
+
".zsh"
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
// src/scanner/index.ts
|
|
300
|
+
function redact(match, showChars = 4) {
|
|
301
|
+
if (match.length <= showChars * 2) {
|
|
302
|
+
return "*".repeat(match.length);
|
|
303
|
+
}
|
|
304
|
+
return match.slice(0, showChars) + "****" + match.slice(-showChars);
|
|
305
|
+
}
|
|
306
|
+
function getPosition(content, index) {
|
|
307
|
+
const lines = content.slice(0, index).split("\n");
|
|
308
|
+
return {
|
|
309
|
+
line: lines.length,
|
|
310
|
+
column: lines[lines.length - 1].length + 1
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function shouldIgnore(filePath) {
|
|
314
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
315
|
+
return IGNORE_PATTERNS.some((pattern) => {
|
|
316
|
+
if (pattern.startsWith("*")) {
|
|
317
|
+
return normalized.endsWith(pattern.slice(1));
|
|
318
|
+
}
|
|
319
|
+
return normalized.includes(pattern);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function isScannable(filePath) {
|
|
323
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
324
|
+
return SCANNABLE_EXTENSIONS.includes(ext);
|
|
325
|
+
}
|
|
326
|
+
function scanFile(filePath, content) {
|
|
327
|
+
const issues = [];
|
|
328
|
+
const relativePath = filePath;
|
|
329
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
330
|
+
pattern.pattern.lastIndex = 0;
|
|
331
|
+
let match;
|
|
332
|
+
while ((match = pattern.pattern.exec(content)) !== null) {
|
|
333
|
+
const pos = getPosition(content, match.index);
|
|
334
|
+
issues.push({
|
|
335
|
+
type: "secret",
|
|
336
|
+
severity: pattern.severity,
|
|
337
|
+
name: pattern.name,
|
|
338
|
+
provider: pattern.provider,
|
|
339
|
+
file: relativePath,
|
|
340
|
+
line: pos.line,
|
|
341
|
+
column: pos.column,
|
|
342
|
+
match: redact(match[0])
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const pattern of PII_PATTERNS) {
|
|
347
|
+
pattern.pattern.lastIndex = 0;
|
|
348
|
+
let match;
|
|
349
|
+
while ((match = pattern.pattern.exec(content)) !== null) {
|
|
350
|
+
const matchStr = match[0];
|
|
351
|
+
if (isLikelyFalsePositive(matchStr, pattern.name)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const pos = getPosition(content, match.index);
|
|
355
|
+
issues.push({
|
|
356
|
+
type: "pii",
|
|
357
|
+
severity: pattern.severity,
|
|
358
|
+
name: pattern.name,
|
|
359
|
+
file: relativePath,
|
|
360
|
+
line: pos.line,
|
|
361
|
+
column: pos.column,
|
|
362
|
+
match: redact(matchStr, 3)
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
for (const pattern of ROUTE_PATTERNS) {
|
|
367
|
+
pattern.pattern.lastIndex = 0;
|
|
368
|
+
let match;
|
|
369
|
+
while ((match = pattern.pattern.exec(content)) !== null) {
|
|
370
|
+
const pos = getPosition(content, match.index);
|
|
371
|
+
issues.push({
|
|
372
|
+
type: "route",
|
|
373
|
+
severity: pattern.severity,
|
|
374
|
+
name: pattern.name,
|
|
375
|
+
file: relativePath,
|
|
376
|
+
line: pos.line,
|
|
377
|
+
column: pos.column,
|
|
378
|
+
match: match[0],
|
|
379
|
+
description: pattern.description
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const fileName = path.basename(filePath);
|
|
384
|
+
if (fileName.startsWith(".env") && !fileName.includes(".example")) {
|
|
385
|
+
const gitignorePath = path.join(path.dirname(filePath), ".gitignore");
|
|
386
|
+
const gitignoreExists = fs.existsSync(gitignorePath);
|
|
387
|
+
issues.push({
|
|
388
|
+
type: "config",
|
|
389
|
+
severity: "high",
|
|
390
|
+
name: "Environment file in repository",
|
|
391
|
+
file: relativePath,
|
|
392
|
+
line: 1,
|
|
393
|
+
column: 1,
|
|
394
|
+
match: fileName,
|
|
395
|
+
description: gitignoreExists ? "Verify this file is in .gitignore" : "Add .env* to .gitignore"
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return issues;
|
|
399
|
+
}
|
|
400
|
+
function isLikelyFalsePositive(match, patternName) {
|
|
401
|
+
if (patternName === "Email Address") {
|
|
402
|
+
const falseDomains = [
|
|
403
|
+
"example.com",
|
|
404
|
+
"example.org",
|
|
405
|
+
"test.com",
|
|
406
|
+
"localhost",
|
|
407
|
+
"placeholder.com"
|
|
408
|
+
];
|
|
409
|
+
if (falseDomains.some((d) => match.includes(d))) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
const publicPrefixes = [
|
|
413
|
+
"support@",
|
|
414
|
+
"help@",
|
|
415
|
+
"info@",
|
|
416
|
+
"contact@",
|
|
417
|
+
"sales@",
|
|
418
|
+
"admin@",
|
|
419
|
+
"noreply@",
|
|
420
|
+
"no-reply@",
|
|
421
|
+
"hello@",
|
|
422
|
+
"team@",
|
|
423
|
+
"partners@",
|
|
424
|
+
"enterprise@",
|
|
425
|
+
"security@",
|
|
426
|
+
"privacy@",
|
|
427
|
+
"legal@"
|
|
428
|
+
];
|
|
429
|
+
if (publicPrefixes.some((p) => match.toLowerCase().startsWith(p))) {
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (patternName === "IP Address") {
|
|
434
|
+
const falseIPs = ["0.0.0.0", "127.0.0.1", "192.168.", "10.0.", "172.16."];
|
|
435
|
+
if (falseIPs.some((ip) => match.startsWith(ip))) {
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (patternName.includes("Phone Number")) {
|
|
440
|
+
if (match.includes("555") || match.includes("123-456") || match.includes("000-000")) {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
function calculateScore(issues) {
|
|
447
|
+
const critical = issues.filter((i) => i.severity === "critical").length;
|
|
448
|
+
const high = issues.filter((i) => i.severity === "high").length;
|
|
449
|
+
const medium = issues.filter((i) => i.severity === "medium").length;
|
|
450
|
+
const total = issues.length;
|
|
451
|
+
if (critical > 0) return "F";
|
|
452
|
+
if (high >= 3) return "F";
|
|
453
|
+
if (high >= 2) return "D";
|
|
454
|
+
if (high >= 1 || medium >= 5) return "C";
|
|
455
|
+
if (medium >= 2) return "B";
|
|
456
|
+
if (total === 0) return "A";
|
|
457
|
+
return "B";
|
|
458
|
+
}
|
|
459
|
+
function getTierDescription(score) {
|
|
460
|
+
switch (score) {
|
|
461
|
+
case "A":
|
|
462
|
+
return "Excellent! No security issues detected.";
|
|
463
|
+
case "B":
|
|
464
|
+
return "Good, but minor improvements recommended.";
|
|
465
|
+
case "C":
|
|
466
|
+
return "Fair. Some security concerns need attention.";
|
|
467
|
+
case "D":
|
|
468
|
+
return "Poor. Significant security issues detected.";
|
|
469
|
+
case "F":
|
|
470
|
+
return "Critical! Your app is leaking secrets.";
|
|
471
|
+
default:
|
|
472
|
+
return "";
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function scan(targetPath) {
|
|
476
|
+
const startTime = Date.now();
|
|
477
|
+
const absolutePath = path.resolve(targetPath);
|
|
478
|
+
const files = await (0, import_glob.glob)("**/*", {
|
|
479
|
+
cwd: absolutePath,
|
|
480
|
+
nodir: true,
|
|
481
|
+
ignore: IGNORE_PATTERNS,
|
|
482
|
+
absolute: true
|
|
483
|
+
});
|
|
484
|
+
const issues = [];
|
|
485
|
+
let filesScanned = 0;
|
|
486
|
+
for (const file of files) {
|
|
487
|
+
if (!isScannable(file) || shouldIgnore(file)) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
492
|
+
const relativePath = path.relative(absolutePath, file);
|
|
493
|
+
const fileIssues = scanFile(relativePath, content);
|
|
494
|
+
issues.push(...fileIssues);
|
|
495
|
+
filesScanned++;
|
|
496
|
+
} catch {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const score = calculateScore(issues);
|
|
501
|
+
const scanDuration = Date.now() - startTime;
|
|
502
|
+
return {
|
|
503
|
+
score,
|
|
504
|
+
tierDescription: getTierDescription(score),
|
|
505
|
+
issues,
|
|
506
|
+
filesScanned,
|
|
507
|
+
scanDuration,
|
|
508
|
+
summary: {
|
|
509
|
+
secrets: issues.filter((i) => i.type === "secret").length,
|
|
510
|
+
pii: issues.filter((i) => i.type === "pii").length,
|
|
511
|
+
routes: issues.filter((i) => i.type === "route").length,
|
|
512
|
+
config: issues.filter((i) => i.type === "config").length,
|
|
513
|
+
critical: issues.filter((i) => i.severity === "critical").length,
|
|
514
|
+
high: issues.filter((i) => i.severity === "high").length,
|
|
515
|
+
medium: issues.filter((i) => i.severity === "medium").length,
|
|
516
|
+
low: issues.filter((i) => i.severity === "low").length
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/cli.ts
|
|
522
|
+
var VERSION = "0.1.0";
|
|
523
|
+
var scoreStyles = {
|
|
524
|
+
A: { color: import_chalk.default.green },
|
|
525
|
+
B: { color: import_chalk.default.blue },
|
|
526
|
+
C: { color: import_chalk.default.yellow },
|
|
527
|
+
D: { color: import_chalk.default.red },
|
|
528
|
+
F: { color: import_chalk.default.bgRed.white }
|
|
529
|
+
};
|
|
530
|
+
var severityColors = {
|
|
531
|
+
critical: import_chalk.default.bgRed.white,
|
|
532
|
+
high: import_chalk.default.red,
|
|
533
|
+
medium: import_chalk.default.yellow,
|
|
534
|
+
low: import_chalk.default.blue
|
|
535
|
+
};
|
|
536
|
+
var typeLabels = {
|
|
537
|
+
secret: "SECRETS",
|
|
538
|
+
pii: "PII",
|
|
539
|
+
route: "ROUTES",
|
|
540
|
+
config: "CONFIG"
|
|
541
|
+
};
|
|
542
|
+
function printBanner() {
|
|
543
|
+
console.log();
|
|
544
|
+
console.log(import_chalk.default.cyan.bold(" Cencori Scan"));
|
|
545
|
+
console.log(import_chalk.default.gray(` v${VERSION}`));
|
|
546
|
+
console.log();
|
|
547
|
+
}
|
|
548
|
+
function printScore(result) {
|
|
549
|
+
const style = scoreStyles[result.score];
|
|
550
|
+
const scoreText = `${result.score}-Tier`;
|
|
551
|
+
const content = ` Security Score: ${scoreText}`;
|
|
552
|
+
console.log();
|
|
553
|
+
console.log(import_chalk.default.gray(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
554
|
+
console.log(import_chalk.default.gray(" \u2502") + style.color.bold(content.padEnd(45)) + import_chalk.default.gray("\u2502"));
|
|
555
|
+
console.log(import_chalk.default.gray(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
556
|
+
console.log();
|
|
557
|
+
console.log(import_chalk.default.gray(` ${result.tierDescription}`));
|
|
558
|
+
console.log();
|
|
559
|
+
}
|
|
560
|
+
function printIssues(issues) {
|
|
561
|
+
if (issues.length === 0) {
|
|
562
|
+
console.log(import_chalk.default.green(" No security issues found."));
|
|
563
|
+
console.log();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const grouped = {};
|
|
567
|
+
for (const issue of issues) {
|
|
568
|
+
if (!grouped[issue.type]) {
|
|
569
|
+
grouped[issue.type] = [];
|
|
570
|
+
}
|
|
571
|
+
grouped[issue.type].push(issue);
|
|
572
|
+
}
|
|
573
|
+
for (const [type, typeIssues] of Object.entries(grouped)) {
|
|
574
|
+
const label = typeLabels[type] || type.toUpperCase();
|
|
575
|
+
console.log(` ${import_chalk.default.bold(label)} (${typeIssues.length})`);
|
|
576
|
+
for (let i = 0; i < typeIssues.length; i++) {
|
|
577
|
+
const issue = typeIssues[i];
|
|
578
|
+
const isLast = i === typeIssues.length - 1;
|
|
579
|
+
const prefix = isLast ? " \u2514\u2500" : " \u251C\u2500";
|
|
580
|
+
const severityColor = severityColors[issue.severity];
|
|
581
|
+
console.log(
|
|
582
|
+
import_chalk.default.gray(prefix) + " " + import_chalk.default.gray(`${issue.file}:${issue.line}`) + " " + severityColor(issue.match)
|
|
583
|
+
);
|
|
584
|
+
if (issue.description) {
|
|
585
|
+
const descPrefix = isLast ? " " : " \u2502 ";
|
|
586
|
+
console.log(import_chalk.default.gray(descPrefix) + import_chalk.default.dim(issue.description));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
console.log();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function printSummary(result) {
|
|
593
|
+
const { summary } = result;
|
|
594
|
+
console.log(import_chalk.default.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
595
|
+
console.log();
|
|
596
|
+
console.log(` ${import_chalk.default.bold("Summary")}`);
|
|
597
|
+
console.log(` Files scanned: ${import_chalk.default.cyan(result.filesScanned)}`);
|
|
598
|
+
console.log(` Scan time: ${import_chalk.default.cyan(result.scanDuration + "ms")}`);
|
|
599
|
+
console.log();
|
|
600
|
+
if (summary.critical > 0) {
|
|
601
|
+
console.log(` ${import_chalk.default.bgRed.white(" CRITICAL ")} ${summary.critical} issues`);
|
|
602
|
+
}
|
|
603
|
+
if (summary.high > 0) {
|
|
604
|
+
console.log(` ${import_chalk.default.red(" HIGH ")} ${summary.high} issues`);
|
|
605
|
+
}
|
|
606
|
+
if (summary.medium > 0) {
|
|
607
|
+
console.log(` ${import_chalk.default.yellow(" MEDIUM ")} ${summary.medium} issues`);
|
|
608
|
+
}
|
|
609
|
+
if (summary.low > 0) {
|
|
610
|
+
console.log(` ${import_chalk.default.blue(" LOW ")} ${summary.low} issues`);
|
|
611
|
+
}
|
|
612
|
+
console.log();
|
|
613
|
+
}
|
|
614
|
+
function printFixes(issues) {
|
|
615
|
+
if (issues.length === 0) return;
|
|
616
|
+
console.log(` ${import_chalk.default.bold("Recommendations:")}`);
|
|
617
|
+
const hasSecrets = issues.some((i) => i.type === "secret");
|
|
618
|
+
const hasPII = issues.some((i) => i.type === "pii");
|
|
619
|
+
const hasConfig = issues.some((i) => i.type === "config");
|
|
620
|
+
if (hasSecrets) {
|
|
621
|
+
console.log(import_chalk.default.gray(" - Use environment variables for secrets"));
|
|
622
|
+
console.log(import_chalk.default.gray(" - Never commit API keys to version control"));
|
|
623
|
+
}
|
|
624
|
+
if (hasConfig) {
|
|
625
|
+
console.log(import_chalk.default.gray(" - Add .env* to .gitignore"));
|
|
626
|
+
}
|
|
627
|
+
if (hasPII) {
|
|
628
|
+
console.log(import_chalk.default.gray(" - Remove personal data from source code"));
|
|
629
|
+
}
|
|
630
|
+
console.log();
|
|
631
|
+
}
|
|
632
|
+
function printFooter() {
|
|
633
|
+
console.log(import_chalk.default.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
634
|
+
console.log();
|
|
635
|
+
console.log(` Share: ${import_chalk.default.cyan("https://scan.cencori.com")}`);
|
|
636
|
+
console.log(` Docs: ${import_chalk.default.cyan("https://cencori.com/docs")}`);
|
|
637
|
+
console.log();
|
|
638
|
+
}
|
|
639
|
+
async function main() {
|
|
640
|
+
import_commander.program.name("cencori-scan").description("Security scanner for AI apps. Detect secrets, PII, and exposed routes.").version(VERSION).argument("[path]", "Path to scan", ".").option("-j, --json", "Output results as JSON").option("-q, --quiet", "Only output the score").option("--no-color", "Disable colored output").action(async (targetPath, options) => {
|
|
641
|
+
if (options.json) {
|
|
642
|
+
const result = await scan(targetPath);
|
|
643
|
+
console.log(JSON.stringify(result, null, 2));
|
|
644
|
+
process.exit(result.score === "A" || result.score === "B" ? 0 : 1);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
printBanner();
|
|
648
|
+
const spinner = (0, import_ora.default)({
|
|
649
|
+
text: "Scanning for security issues...",
|
|
650
|
+
color: "cyan"
|
|
651
|
+
}).start();
|
|
652
|
+
try {
|
|
653
|
+
const result = await scan(targetPath);
|
|
654
|
+
spinner.succeed(`Scanned ${result.filesScanned} files`);
|
|
655
|
+
if (options.quiet) {
|
|
656
|
+
const style = scoreStyles[result.score];
|
|
657
|
+
console.log(`
|
|
658
|
+
Score: ${style.color.bold(result.score + "-Tier")}
|
|
659
|
+
`);
|
|
660
|
+
process.exit(result.score === "A" || result.score === "B" ? 0 : 1);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
printScore(result);
|
|
664
|
+
printIssues(result.issues);
|
|
665
|
+
printSummary(result);
|
|
666
|
+
printFixes(result.issues);
|
|
667
|
+
printFooter();
|
|
668
|
+
process.exit(result.score === "A" || result.score === "B" ? 0 : 1);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
spinner.fail("Scan failed");
|
|
671
|
+
console.error(import_chalk.default.red(`
|
|
672
|
+
Error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
import_commander.program.parse();
|
|
677
|
+
}
|
|
678
|
+
main();
|
|
679
|
+
//# sourceMappingURL=cli.js.map
|