hackmyagent-core 0.1.0 → 0.1.2
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/dist/hardening/scanner.d.ts +8 -0
- package/dist/hardening/scanner.d.ts.map +1 -1
- package/dist/hardening/scanner.js +401 -270
- package/dist/hardening/scanner.js.map +1 -1
- package/dist/hardening/scanner.test.js +86 -168
- package/dist/hardening/scanner.test.js.map +1 -1
- package/dist/hardening/security-check.d.ts +18 -0
- package/dist/hardening/security-check.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -40,6 +40,45 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
40
40
|
exports.HardeningScanner = void 0;
|
|
41
41
|
const fs = __importStar(require("fs/promises"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
|
+
/**
|
|
44
|
+
* Defines which checks apply to which project types
|
|
45
|
+
* Key: check ID prefix or full ID
|
|
46
|
+
* Value: array of project types this check applies to
|
|
47
|
+
*
|
|
48
|
+
* If a check ID is not in this map, it applies to 'all' project types
|
|
49
|
+
*/
|
|
50
|
+
const CHECK_PROJECT_TYPES = {
|
|
51
|
+
// Core security checks - apply to all projects
|
|
52
|
+
'CRED-': ['all'], // Credential exposure - always critical
|
|
53
|
+
'GIT-': ['all'], // Git security - always important
|
|
54
|
+
'PERM-': ['all'], // File permissions - always important
|
|
55
|
+
'DEP-': ['all'], // Dependencies - always important
|
|
56
|
+
// Environment checks - API/webapp mostly
|
|
57
|
+
'ENV-': ['webapp', 'api', 'mcp'],
|
|
58
|
+
// AI-specific checks - apply to MCP servers and AI-integrated projects
|
|
59
|
+
'CLAUDE-': ['all'], // Claude-specific (if files exist)
|
|
60
|
+
'MCP-': ['mcp'], // MCP configuration - only MCP servers
|
|
61
|
+
'PROMPT-': ['mcp', 'api'], // Prompt injection - MCP and APIs
|
|
62
|
+
'TOOL-': ['mcp'], // MCP tool boundaries
|
|
63
|
+
// Web-specific checks - only for web apps and APIs
|
|
64
|
+
'AUTH-': ['webapp', 'api'], // Authentication/authorization
|
|
65
|
+
'SESSION-': ['webapp', 'api'], // Session management
|
|
66
|
+
'NET-': ['webapp', 'api'], // Network security (HTTPS, etc.)
|
|
67
|
+
'IO-': ['webapp', 'api'], // Input/output (XSS, etc.)
|
|
68
|
+
'API-': ['api'], // API security headers
|
|
69
|
+
'RATE-': ['webapp', 'api'], // Rate limiting
|
|
70
|
+
'PROC-': ['webapp', 'api'], // Process security (headers, etc.)
|
|
71
|
+
// Database/encryption - only for apps with data storage
|
|
72
|
+
'INJ-': ['webapp', 'api'], // SQL injection, input validation
|
|
73
|
+
'ENCRYPT-': ['webapp', 'api'], // Encryption, password hashing
|
|
74
|
+
// Logging/audit - servers and MCP
|
|
75
|
+
'LOG-': ['webapp', 'api', 'mcp'],
|
|
76
|
+
'AUDIT-': ['webapp', 'api'],
|
|
77
|
+
// Sandboxing - containerized apps
|
|
78
|
+
'SANDBOX-': ['webapp', 'api', 'mcp'],
|
|
79
|
+
// Secret management - primarily for apps with secrets
|
|
80
|
+
'SEC-': ['webapp', 'api', 'mcp'],
|
|
81
|
+
};
|
|
43
82
|
// Patterns for detecting exposed credentials
|
|
44
83
|
// Each pattern is carefully tuned to minimize false positives
|
|
45
84
|
const CREDENTIAL_PATTERNS = [
|
|
@@ -86,8 +125,9 @@ class HardeningScanner {
|
|
|
86
125
|
}
|
|
87
126
|
// Track if any fix fails for atomic rollback
|
|
88
127
|
let fixFailed = false;
|
|
89
|
-
// Detect platform
|
|
128
|
+
// Detect platform and project type
|
|
90
129
|
const platform = await this.detectPlatform(targetDir);
|
|
130
|
+
const projectType = await this.detectProjectType(targetDir);
|
|
91
131
|
// Run all checks
|
|
92
132
|
const findings = [];
|
|
93
133
|
// Credential exposure checks
|
|
@@ -183,11 +223,24 @@ class HardeningScanner {
|
|
|
183
223
|
// Tool boundary checks
|
|
184
224
|
const toolFindings = await this.checkToolBoundaries(targetDir, shouldFix);
|
|
185
225
|
findings.push(...toolFindings);
|
|
186
|
-
// Filter
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
226
|
+
// Filter findings to only show real, actionable issues:
|
|
227
|
+
// 1. Only failed checks (passed: false)
|
|
228
|
+
// 2. Only checks with a file path (concrete findings, not generic advice)
|
|
229
|
+
// 3. Filter out ignored checks
|
|
230
|
+
let filteredFindings = findings.filter((f) => {
|
|
231
|
+
// Keep fixed findings (so users can see what was fixed)
|
|
232
|
+
// Otherwise, only show failed checks
|
|
233
|
+
if (!f.fixed && f.passed)
|
|
234
|
+
return false;
|
|
235
|
+
// Only show concrete findings (has a file path)
|
|
236
|
+
if (!f.file)
|
|
237
|
+
return false;
|
|
238
|
+
// Filter out ignored checks
|
|
239
|
+
if (ignoredChecks.has(f.checkId.toUpperCase()))
|
|
240
|
+
return false;
|
|
241
|
+
return true;
|
|
242
|
+
});
|
|
243
|
+
// Calculate score (only on applicable, non-ignored findings)
|
|
191
244
|
const { score, maxScore } = this.calculateScore(filteredFindings);
|
|
192
245
|
// In dry-run mode, mark fixable failed findings with wouldFix
|
|
193
246
|
if (dryRun && autoFix) {
|
|
@@ -203,6 +256,7 @@ class HardeningScanner {
|
|
|
203
256
|
return {
|
|
204
257
|
timestamp: new Date(),
|
|
205
258
|
platform,
|
|
259
|
+
projectType,
|
|
206
260
|
findings: filteredFindings,
|
|
207
261
|
score,
|
|
208
262
|
maxScore,
|
|
@@ -239,21 +293,104 @@ class HardeningScanner {
|
|
|
239
293
|
}
|
|
240
294
|
return platforms.join('+');
|
|
241
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Detect the project type based on package.json and project structure
|
|
298
|
+
*/
|
|
299
|
+
async detectProjectType(targetDir) {
|
|
300
|
+
try {
|
|
301
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
302
|
+
const content = await fs.readFile(pkgPath, 'utf-8');
|
|
303
|
+
const pkg = JSON.parse(content);
|
|
304
|
+
// Check if it's a CLI tool (has bin field)
|
|
305
|
+
if (pkg.bin) {
|
|
306
|
+
return 'cli';
|
|
307
|
+
}
|
|
308
|
+
// Check dependencies for framework detection
|
|
309
|
+
const allDeps = {
|
|
310
|
+
...pkg.dependencies,
|
|
311
|
+
...pkg.devDependencies,
|
|
312
|
+
};
|
|
313
|
+
// Check for MCP server
|
|
314
|
+
if (allDeps['@modelcontextprotocol/sdk'] ||
|
|
315
|
+
allDeps['mcp'] ||
|
|
316
|
+
pkg.name?.includes('mcp')) {
|
|
317
|
+
return 'mcp';
|
|
318
|
+
}
|
|
319
|
+
// Check for web frameworks
|
|
320
|
+
if (allDeps['react'] ||
|
|
321
|
+
allDeps['vue'] ||
|
|
322
|
+
allDeps['svelte'] ||
|
|
323
|
+
allDeps['@angular/core'] ||
|
|
324
|
+
allDeps['next'] ||
|
|
325
|
+
allDeps['nuxt']) {
|
|
326
|
+
return 'webapp';
|
|
327
|
+
}
|
|
328
|
+
// Check for API frameworks
|
|
329
|
+
if (allDeps['express'] ||
|
|
330
|
+
allDeps['fastify'] ||
|
|
331
|
+
allDeps['koa'] ||
|
|
332
|
+
allDeps['hapi'] ||
|
|
333
|
+
allDeps['@hapi/hapi'] ||
|
|
334
|
+
allDeps['restify']) {
|
|
335
|
+
return 'api';
|
|
336
|
+
}
|
|
337
|
+
// Default to library if it has main/exports but no clear type
|
|
338
|
+
if (pkg.main || pkg.exports || pkg.module) {
|
|
339
|
+
return 'library';
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// No package.json or invalid JSON
|
|
344
|
+
}
|
|
345
|
+
// Check for Python projects
|
|
346
|
+
try {
|
|
347
|
+
const setupPath = path.join(targetDir, 'setup.py');
|
|
348
|
+
await fs.access(setupPath);
|
|
349
|
+
return 'library';
|
|
350
|
+
}
|
|
351
|
+
catch { }
|
|
352
|
+
try {
|
|
353
|
+
const pyprojectPath = path.join(targetDir, 'pyproject.toml');
|
|
354
|
+
const content = await fs.readFile(pyprojectPath, 'utf-8');
|
|
355
|
+
if (content.includes('fastapi') || content.includes('flask') || content.includes('django')) {
|
|
356
|
+
return 'api';
|
|
357
|
+
}
|
|
358
|
+
return 'library';
|
|
359
|
+
}
|
|
360
|
+
catch { }
|
|
361
|
+
// Default to library for generic projects
|
|
362
|
+
return 'library';
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Check if a finding applies to the given project type
|
|
366
|
+
*/
|
|
367
|
+
findingAppliesTo(finding, projectType) {
|
|
368
|
+
// Find the matching rule based on check ID prefix
|
|
369
|
+
for (const [prefix, types] of Object.entries(CHECK_PROJECT_TYPES)) {
|
|
370
|
+
if (finding.checkId.startsWith(prefix.replace('-', ''))) {
|
|
371
|
+
// Check if 'all' is in the types array
|
|
372
|
+
if (types.includes('all')) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
return types.includes(projectType);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Default: applies to all if no rule found
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
242
381
|
async checkCredentialExposure(targetDir, autoFix) {
|
|
243
382
|
const findings = [];
|
|
244
|
-
const exposedKeys = [];
|
|
245
|
-
const fixedFiles = [];
|
|
246
383
|
const envVarsToAdd = new Set();
|
|
247
384
|
// Credential patterns with their env var names (stricter to avoid false positives)
|
|
248
385
|
const credentialPatterns = [
|
|
249
|
-
{ name: '
|
|
250
|
-
{ name: '
|
|
251
|
-
{ name: '
|
|
252
|
-
{ name: '
|
|
253
|
-
{ name: '
|
|
254
|
-
{ name: '
|
|
255
|
-
{ name: '
|
|
256
|
-
{ name: '
|
|
386
|
+
{ name: 'Anthropic API Key', pattern: /sk-ant-api\d{2}-[a-zA-Z0-9_-]{20,}/g, envVar: 'ANTHROPIC_API_KEY' },
|
|
387
|
+
{ name: 'OpenAI API Key', pattern: /sk-proj-[a-zA-Z0-9]{20,}/g, envVar: 'OPENAI_API_KEY' },
|
|
388
|
+
{ name: 'OpenAI API Key', pattern: /sk-[a-zA-Z0-9]{48,}/g, envVar: 'OPENAI_API_KEY' },
|
|
389
|
+
{ name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/g, envVar: 'AWS_ACCESS_KEY_ID' },
|
|
390
|
+
{ name: 'GitHub Token', pattern: /ghp_[a-zA-Z0-9]{36}/g, envVar: 'GITHUB_TOKEN' },
|
|
391
|
+
{ name: 'GitHub Token', pattern: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/g, envVar: 'GITHUB_TOKEN' },
|
|
392
|
+
{ name: 'Google API Key', pattern: /AIza[0-9A-Za-z_-]{35}/g, envVar: 'GOOGLE_API_KEY' },
|
|
393
|
+
{ name: 'Stripe Key', pattern: /sk_live_[0-9a-zA-Z]{24,}/g, envVar: 'STRIPE_SECRET_KEY' },
|
|
257
394
|
];
|
|
258
395
|
// Files to check for credentials
|
|
259
396
|
const filesToCheck = [
|
|
@@ -264,34 +401,52 @@ class HardeningScanner {
|
|
|
264
401
|
'settings.json',
|
|
265
402
|
'.env',
|
|
266
403
|
'.env.local',
|
|
404
|
+
'CLAUDE.md',
|
|
267
405
|
];
|
|
268
406
|
for (const filename of filesToCheck) {
|
|
269
407
|
const filePath = path.join(targetDir, filename);
|
|
270
408
|
try {
|
|
271
409
|
let content = await fs.readFile(filePath, 'utf-8');
|
|
410
|
+
const lines = content.split('\n');
|
|
272
411
|
let fileModified = false;
|
|
412
|
+
const keysFoundInFile = [];
|
|
273
413
|
for (const { name, pattern, envVar } of credentialPatterns) {
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
if (pattern.test(content)) {
|
|
277
|
-
// Check if it's already an env var reference
|
|
414
|
+
// Check each line for credentials
|
|
415
|
+
for (let i = 0; i < lines.length; i++) {
|
|
278
416
|
pattern.lastIndex = 0;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
exposedKeys.push(name);
|
|
417
|
+
if (pattern.test(lines[i]) && !lines[i].includes('${' + envVar + '}')) {
|
|
418
|
+
keysFoundInFile.push({ name, line: i + 1 });
|
|
282
419
|
if (autoFix) {
|
|
283
|
-
// Replace the credential with env var reference
|
|
284
420
|
pattern.lastIndex = 0;
|
|
285
|
-
|
|
421
|
+
lines[i] = lines[i].replace(pattern, '${' + envVar + '}');
|
|
286
422
|
fileModified = true;
|
|
287
423
|
envVarsToAdd.add(envVar);
|
|
288
424
|
}
|
|
289
425
|
}
|
|
290
426
|
}
|
|
291
427
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
428
|
+
// Report one finding per file with exposed credentials
|
|
429
|
+
if (keysFoundInFile.length > 0) {
|
|
430
|
+
const keyNames = [...new Set(keysFoundInFile.map((k) => k.name))];
|
|
431
|
+
const firstLine = keysFoundInFile[0].line;
|
|
432
|
+
if (fileModified) {
|
|
433
|
+
content = lines.join('\n');
|
|
434
|
+
await fs.writeFile(filePath, content);
|
|
435
|
+
}
|
|
436
|
+
findings.push({
|
|
437
|
+
checkId: 'CRED-001',
|
|
438
|
+
name: 'Exposed Credential',
|
|
439
|
+
description: `${keyNames.join(', ')} found in plaintext`,
|
|
440
|
+
category: 'credentials',
|
|
441
|
+
severity: 'critical',
|
|
442
|
+
passed: fileModified, // Fixed if we replaced it
|
|
443
|
+
message: keyNames.join(', '),
|
|
444
|
+
file: filename,
|
|
445
|
+
line: firstLine,
|
|
446
|
+
fixable: true,
|
|
447
|
+
fixed: fileModified,
|
|
448
|
+
fix: `Run \`hackmyagent secure --fix\` to replace the hardcoded credential with a \${ENV_VAR} reference, then store the actual value in your .env file`,
|
|
449
|
+
});
|
|
295
450
|
}
|
|
296
451
|
}
|
|
297
452
|
catch {
|
|
@@ -301,31 +456,12 @@ class HardeningScanner {
|
|
|
301
456
|
// Create .env.example if we fixed any credentials
|
|
302
457
|
if (autoFix && envVarsToAdd.size > 0) {
|
|
303
458
|
const envExamplePath = path.join(targetDir, '.env.example');
|
|
304
|
-
let envExampleContent = '# Environment variables
|
|
459
|
+
let envExampleContent = '# Environment variables\n\n';
|
|
305
460
|
for (const envVar of envVarsToAdd) {
|
|
306
|
-
envExampleContent += `${envVar}
|
|
461
|
+
envExampleContent += `${envVar}=\n`;
|
|
307
462
|
}
|
|
308
463
|
await fs.writeFile(envExamplePath, envExampleContent);
|
|
309
464
|
}
|
|
310
|
-
const passed = exposedKeys.length === 0;
|
|
311
|
-
const fixed = fixedFiles.length > 0;
|
|
312
|
-
findings.push({
|
|
313
|
-
checkId: 'CRED-001',
|
|
314
|
-
name: 'Exposed API Keys',
|
|
315
|
-
description: 'API keys or secrets found in plaintext configuration files',
|
|
316
|
-
category: 'credentials',
|
|
317
|
-
severity: 'critical',
|
|
318
|
-
passed: passed || fixed,
|
|
319
|
-
message: fixed
|
|
320
|
-
? `Replaced credentials with env var references in: ${fixedFiles.join(', ')}`
|
|
321
|
-
: passed
|
|
322
|
-
? 'No exposed API keys detected'
|
|
323
|
-
: `Found exposed credentials: ${[...new Set(exposedKeys)].join(', ')}`,
|
|
324
|
-
fixable: true,
|
|
325
|
-
fixed,
|
|
326
|
-
fixMessage: fixed ? `Created .env.example with: ${[...envVarsToAdd].join(', ')}` : undefined,
|
|
327
|
-
details: passed && !fixed ? undefined : { keys: [...new Set(exposedKeys)], fixedFiles },
|
|
328
|
-
});
|
|
329
465
|
return findings;
|
|
330
466
|
}
|
|
331
467
|
async checkClaudeMd(targetDir, autoFix) {
|
|
@@ -333,39 +469,40 @@ class HardeningScanner {
|
|
|
333
469
|
const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
|
|
334
470
|
try {
|
|
335
471
|
const content = await fs.readFile(claudeMdPath, 'utf-8');
|
|
336
|
-
|
|
472
|
+
const lines = content.split('\n');
|
|
473
|
+
let credentialLine;
|
|
474
|
+
let credentialType;
|
|
337
475
|
// Check for credentials in CLAUDE.md
|
|
338
|
-
for (const { pattern } of CREDENTIAL_PATTERNS) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
476
|
+
for (const { name, pattern } of CREDENTIAL_PATTERNS) {
|
|
477
|
+
for (let i = 0; i < lines.length; i++) {
|
|
478
|
+
if (pattern.test(lines[i])) {
|
|
479
|
+
credentialLine = i + 1;
|
|
480
|
+
credentialType = name;
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
342
483
|
}
|
|
484
|
+
if (credentialLine)
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
// Only report if credentials found
|
|
488
|
+
if (credentialLine) {
|
|
489
|
+
findings.push({
|
|
490
|
+
checkId: 'CLAUDE-001',
|
|
491
|
+
name: 'Credential in CLAUDE.md',
|
|
492
|
+
description: `${credentialType} found in CLAUDE.md`,
|
|
493
|
+
category: 'claude-code',
|
|
494
|
+
severity: 'critical',
|
|
495
|
+
passed: false,
|
|
496
|
+
message: 'Remove credentials from CLAUDE.md',
|
|
497
|
+
file: 'CLAUDE.md',
|
|
498
|
+
line: credentialLine,
|
|
499
|
+
fixable: false,
|
|
500
|
+
fix: 'Manually move the credential to a .env file and reference it as ${ENV_VAR}. CLAUDE.md may be committed to git and exposed publicly',
|
|
501
|
+
});
|
|
343
502
|
}
|
|
344
|
-
findings.push({
|
|
345
|
-
checkId: 'CLAUDE-001',
|
|
346
|
-
name: 'CLAUDE.md Sensitive Content',
|
|
347
|
-
description: 'CLAUDE.md file contains sensitive information like API keys',
|
|
348
|
-
category: 'claude-code',
|
|
349
|
-
severity: 'critical',
|
|
350
|
-
passed: !hasSecrets,
|
|
351
|
-
message: hasSecrets
|
|
352
|
-
? 'CLAUDE.md contains exposed credentials'
|
|
353
|
-
: 'CLAUDE.md does not contain sensitive credentials',
|
|
354
|
-
fixable: false,
|
|
355
|
-
});
|
|
356
503
|
}
|
|
357
504
|
catch {
|
|
358
|
-
// CLAUDE.md doesn't exist, that's fine
|
|
359
|
-
findings.push({
|
|
360
|
-
checkId: 'CLAUDE-001',
|
|
361
|
-
name: 'CLAUDE.md Sensitive Content',
|
|
362
|
-
description: 'CLAUDE.md file contains sensitive information like API keys',
|
|
363
|
-
category: 'claude-code',
|
|
364
|
-
severity: 'critical',
|
|
365
|
-
passed: true,
|
|
366
|
-
message: 'No CLAUDE.md file found (OK)',
|
|
367
|
-
fixable: false,
|
|
368
|
-
});
|
|
505
|
+
// CLAUDE.md doesn't exist, that's fine - no finding needed
|
|
369
506
|
}
|
|
370
507
|
return findings;
|
|
371
508
|
}
|
|
@@ -413,57 +550,39 @@ class HardeningScanner {
|
|
|
413
550
|
if (mcp001Fixed) {
|
|
414
551
|
await fs.writeFile(mcpConfigPath, JSON.stringify(config, null, 2));
|
|
415
552
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
: '
|
|
442
|
-
|
|
443
|
-
|
|
553
|
+
// Only report if there's an issue
|
|
554
|
+
if (hasRootAccess) {
|
|
555
|
+
findings.push({
|
|
556
|
+
checkId: 'MCP-001',
|
|
557
|
+
name: 'MCP Root Filesystem Access',
|
|
558
|
+
description: 'Server has access to / or ~ directory',
|
|
559
|
+
category: 'mcp',
|
|
560
|
+
severity: 'high',
|
|
561
|
+
passed: mcp001Fixed,
|
|
562
|
+
message: 'Restrict filesystem access to specific directories',
|
|
563
|
+
file: 'mcp.json',
|
|
564
|
+
fixable: true,
|
|
565
|
+
fixed: mcp001Fixed,
|
|
566
|
+
fix: 'Run `hackmyagent secure --fix` to restrict filesystem access from / or ~ to project-relative paths (./data or ./)',
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (hasUnrestrictedShell) {
|
|
570
|
+
findings.push({
|
|
571
|
+
checkId: 'MCP-002',
|
|
572
|
+
name: 'Unrestricted Shell Server',
|
|
573
|
+
description: 'Shell server has no command restrictions',
|
|
574
|
+
category: 'mcp',
|
|
575
|
+
severity: 'critical',
|
|
576
|
+
passed: false,
|
|
577
|
+
message: 'Add allowedCommands to restrict shell access',
|
|
578
|
+
file: 'mcp.json',
|
|
579
|
+
fixable: false,
|
|
580
|
+
fix: 'Manually add an "allowedCommands" array to your shell server config in mcp.json to whitelist specific commands (e.g., ["ls", "cat", "grep"])',
|
|
581
|
+
});
|
|
582
|
+
}
|
|
444
583
|
}
|
|
445
584
|
catch {
|
|
446
|
-
// mcp.json doesn't exist
|
|
447
|
-
findings.push({
|
|
448
|
-
checkId: 'MCP-001',
|
|
449
|
-
name: 'MCP Root Filesystem Access',
|
|
450
|
-
description: 'MCP server configured with root or home directory access',
|
|
451
|
-
category: 'mcp',
|
|
452
|
-
severity: 'high',
|
|
453
|
-
passed: true,
|
|
454
|
-
message: 'No mcp.json found (OK)',
|
|
455
|
-
fixable: true,
|
|
456
|
-
});
|
|
457
|
-
findings.push({
|
|
458
|
-
checkId: 'MCP-002',
|
|
459
|
-
name: 'MCP Unrestricted Shell',
|
|
460
|
-
description: 'MCP shell server without command restrictions',
|
|
461
|
-
category: 'mcp',
|
|
462
|
-
severity: 'critical',
|
|
463
|
-
passed: true,
|
|
464
|
-
message: 'No mcp.json found (OK)',
|
|
465
|
-
fixable: false,
|
|
466
|
-
});
|
|
585
|
+
// mcp.json doesn't exist - no findings needed
|
|
467
586
|
}
|
|
468
587
|
return findings;
|
|
469
588
|
}
|
|
@@ -550,22 +669,22 @@ dist/
|
|
|
550
669
|
gitignoreExists = true;
|
|
551
670
|
git001Fixed = true;
|
|
552
671
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
:
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
672
|
+
// Only report if .gitignore is missing
|
|
673
|
+
if (!gitignoreExists || git001Fixed) {
|
|
674
|
+
findings.push({
|
|
675
|
+
checkId: 'GIT-001',
|
|
676
|
+
name: 'Missing .gitignore',
|
|
677
|
+
description: 'No .gitignore file to prevent accidental commits',
|
|
678
|
+
category: 'git',
|
|
679
|
+
severity: 'medium',
|
|
680
|
+
passed: git001Fixed,
|
|
681
|
+
message: 'Create .gitignore to protect sensitive files',
|
|
682
|
+
file: '.gitignore',
|
|
683
|
+
fixable: true,
|
|
684
|
+
fixed: git001Fixed,
|
|
685
|
+
fix: 'Run `hackmyagent secure --fix` to create a .gitignore with security patterns (.env, secrets.json, *.pem, *.key) to prevent accidental commits',
|
|
686
|
+
});
|
|
687
|
+
}
|
|
569
688
|
// GIT-002: Check for missing sensitive patterns in .gitignore
|
|
570
689
|
const sensitivePatterns = ['.env', 'secrets.json', '*.pem', '*.key'];
|
|
571
690
|
const missingPatterns = [];
|
|
@@ -581,23 +700,22 @@ dist/
|
|
|
581
700
|
await fs.writeFile(gitignorePath, gitignoreContent);
|
|
582
701
|
git002Fixed = true;
|
|
583
702
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
: missingPatterns.
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
});
|
|
703
|
+
// Only report if patterns are missing
|
|
704
|
+
if (missingPatterns.length > 0) {
|
|
705
|
+
findings.push({
|
|
706
|
+
checkId: 'GIT-002',
|
|
707
|
+
name: 'Incomplete .gitignore',
|
|
708
|
+
description: `Missing: ${missingPatterns.join(', ')}`,
|
|
709
|
+
category: 'git',
|
|
710
|
+
severity: 'high',
|
|
711
|
+
passed: git002Fixed,
|
|
712
|
+
message: `Add patterns: ${missingPatterns.join(', ')}`,
|
|
713
|
+
file: '.gitignore',
|
|
714
|
+
fixable: true,
|
|
715
|
+
fixed: git002Fixed,
|
|
716
|
+
fix: `Run \`hackmyagent secure --fix\` to add ${missingPatterns.join(', ')} to .gitignore so sensitive files won't be accidentally committed`,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
601
719
|
// GIT-003: Check if .env exists but not in .gitignore
|
|
602
720
|
let envExists = false;
|
|
603
721
|
try {
|
|
@@ -618,23 +736,22 @@ dist/
|
|
|
618
736
|
await fs.writeFile(gitignorePath, gitignoreContent);
|
|
619
737
|
git003Fixed = true;
|
|
620
738
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
:
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
});
|
|
739
|
+
// Only report if .env is at risk
|
|
740
|
+
if (envAtRisk) {
|
|
741
|
+
findings.push({
|
|
742
|
+
checkId: 'GIT-003',
|
|
743
|
+
name: '.env Not Ignored',
|
|
744
|
+
description: '.env exists but not in .gitignore - secrets may be committed',
|
|
745
|
+
category: 'git',
|
|
746
|
+
severity: 'critical',
|
|
747
|
+
passed: git003Fixed,
|
|
748
|
+
message: 'Add .env to .gitignore',
|
|
749
|
+
file: '.env',
|
|
750
|
+
fixable: true,
|
|
751
|
+
fixed: git003Fixed,
|
|
752
|
+
fix: 'Run `hackmyagent secure --fix` to add .env to .gitignore so your environment variables won\'t be accidentally committed',
|
|
753
|
+
});
|
|
754
|
+
}
|
|
638
755
|
return findings;
|
|
639
756
|
}
|
|
640
757
|
async checkNetworkSecurity(targetDir, autoFix) {
|
|
@@ -664,22 +781,22 @@ dist/
|
|
|
664
781
|
await fs.writeFile(mcpConfigPath, fixedContent);
|
|
665
782
|
net001Fixed = true;
|
|
666
783
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
:
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
784
|
+
// Only report if bound to 0.0.0.0
|
|
785
|
+
if (boundToAllInterfaces) {
|
|
786
|
+
findings.push({
|
|
787
|
+
checkId: 'NET-001',
|
|
788
|
+
name: 'Server Bound to All Interfaces',
|
|
789
|
+
description: 'Server bound to 0.0.0.0 - accessible from any network',
|
|
790
|
+
category: 'network',
|
|
791
|
+
severity: 'critical',
|
|
792
|
+
passed: net001Fixed,
|
|
793
|
+
message: 'Change 0.0.0.0 to 127.0.0.1',
|
|
794
|
+
file: 'mcp.json',
|
|
795
|
+
fixable: true,
|
|
796
|
+
fixed: net001Fixed,
|
|
797
|
+
fix: 'Run `hackmyagent secure --fix` to change 0.0.0.0 to 127.0.0.1 so the server only accepts local connections instead of being exposed to the network',
|
|
798
|
+
});
|
|
799
|
+
}
|
|
683
800
|
// NET-002: Check for remote MCP servers without TLS
|
|
684
801
|
let hasInsecureRemote = false;
|
|
685
802
|
if (mcpConfig?.servers) {
|
|
@@ -690,18 +807,21 @@ dist/
|
|
|
690
807
|
}
|
|
691
808
|
}
|
|
692
809
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
: '
|
|
703
|
-
|
|
704
|
-
|
|
810
|
+
// Only report if insecure remote found
|
|
811
|
+
if (hasInsecureRemote) {
|
|
812
|
+
findings.push({
|
|
813
|
+
checkId: 'NET-002',
|
|
814
|
+
name: 'Remote MCP Without TLS',
|
|
815
|
+
description: 'Remote server using HTTP instead of HTTPS',
|
|
816
|
+
category: 'network',
|
|
817
|
+
severity: 'high',
|
|
818
|
+
passed: false,
|
|
819
|
+
message: 'Change http:// to https://',
|
|
820
|
+
file: 'mcp.json',
|
|
821
|
+
fixable: false,
|
|
822
|
+
fix: 'Manually change http:// to https:// in mcp.json to encrypt traffic and prevent man-in-the-middle attacks',
|
|
823
|
+
});
|
|
824
|
+
}
|
|
705
825
|
return findings;
|
|
706
826
|
}
|
|
707
827
|
async checkMcpAdvanced(targetDir, autoFix) {
|
|
@@ -754,22 +874,22 @@ dist/
|
|
|
754
874
|
await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
|
|
755
875
|
}
|
|
756
876
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
:
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
877
|
+
// Only report if hardcoded secrets found
|
|
878
|
+
if (hasHardcodedSecrets) {
|
|
879
|
+
findings.push({
|
|
880
|
+
checkId: 'MCP-003',
|
|
881
|
+
name: 'Hardcoded Secrets in MCP',
|
|
882
|
+
description: 'Secrets found in MCP env vars',
|
|
883
|
+
category: 'mcp',
|
|
884
|
+
severity: 'critical',
|
|
885
|
+
passed: mcp003Fixed,
|
|
886
|
+
message: 'Use ${ENV_VAR} references instead',
|
|
887
|
+
file: 'mcp.json',
|
|
888
|
+
fixable: true,
|
|
889
|
+
fixed: mcp003Fixed,
|
|
890
|
+
fix: 'Run `hackmyagent secure --fix` to replace hardcoded API keys with ${ENV_VAR} references, then store actual values in .env file',
|
|
891
|
+
});
|
|
892
|
+
}
|
|
773
893
|
// MCP-004: Check for default credentials
|
|
774
894
|
const defaultPasswords = ['postgres', 'password', 'admin', 'root', '123456', 'default'];
|
|
775
895
|
let hasDefaultCreds = false;
|
|
@@ -786,18 +906,21 @@ dist/
|
|
|
786
906
|
}
|
|
787
907
|
}
|
|
788
908
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
: '
|
|
799
|
-
|
|
800
|
-
|
|
909
|
+
// Only report if default credentials found
|
|
910
|
+
if (hasDefaultCreds) {
|
|
911
|
+
findings.push({
|
|
912
|
+
checkId: 'MCP-004',
|
|
913
|
+
name: 'Default Credentials',
|
|
914
|
+
description: 'MCP server using default password',
|
|
915
|
+
category: 'mcp',
|
|
916
|
+
severity: 'critical',
|
|
917
|
+
passed: false,
|
|
918
|
+
message: 'Change to strong unique password',
|
|
919
|
+
file: 'mcp.json',
|
|
920
|
+
fixable: false,
|
|
921
|
+
fix: 'Manually change the default password in mcp.json to a strong, unique password (use `openssl rand -base64 24` to generate one)',
|
|
922
|
+
});
|
|
923
|
+
}
|
|
801
924
|
// MCP-005: Check for wildcard tool access
|
|
802
925
|
let hasWildcardTools = false;
|
|
803
926
|
if (mcpConfig?.servers) {
|
|
@@ -808,18 +931,21 @@ dist/
|
|
|
808
931
|
}
|
|
809
932
|
}
|
|
810
933
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
: '
|
|
821
|
-
|
|
822
|
-
|
|
934
|
+
// Only report if wildcard tools found
|
|
935
|
+
if (hasWildcardTools) {
|
|
936
|
+
findings.push({
|
|
937
|
+
checkId: 'MCP-005',
|
|
938
|
+
name: 'Wildcard Tool Access',
|
|
939
|
+
description: 'Server allows all tools (*)',
|
|
940
|
+
category: 'mcp',
|
|
941
|
+
severity: 'high',
|
|
942
|
+
passed: false,
|
|
943
|
+
message: 'Restrict to specific tools needed',
|
|
944
|
+
file: 'mcp.json',
|
|
945
|
+
fixable: false,
|
|
946
|
+
fix: 'Manually replace "*" with a list of specific tool names you need (e.g., ["read_file", "list_directory"]) to limit what the AI can access',
|
|
947
|
+
});
|
|
948
|
+
}
|
|
823
949
|
return findings;
|
|
824
950
|
}
|
|
825
951
|
async checkClaudeAdvanced(targetDir, autoFix) {
|
|
@@ -842,18 +968,21 @@ dist/
|
|
|
842
968
|
}
|
|
843
969
|
}
|
|
844
970
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
: '
|
|
855
|
-
|
|
856
|
-
|
|
971
|
+
// Only report if overly permissive
|
|
972
|
+
if (hasOverlyPermissive) {
|
|
973
|
+
findings.push({
|
|
974
|
+
checkId: 'CLAUDE-002',
|
|
975
|
+
name: 'Overly Permissive Permissions',
|
|
976
|
+
description: 'Settings allow unrestricted tool access',
|
|
977
|
+
category: 'claude-code',
|
|
978
|
+
severity: 'high',
|
|
979
|
+
passed: false,
|
|
980
|
+
message: 'Scope permissions to specific paths',
|
|
981
|
+
file: '.claude/settings.json',
|
|
982
|
+
fixable: false,
|
|
983
|
+
fix: 'Manually replace wildcards like Bash(*) or Read(*) with specific paths (e.g., Bash(npm test) or Read(/src/**)) to limit AI access',
|
|
984
|
+
});
|
|
985
|
+
}
|
|
857
986
|
// CLAUDE-003: Check for dangerous Bash patterns
|
|
858
987
|
let hasDangerousBash = false;
|
|
859
988
|
const dangerousPatterns = ['rm -rf', 'rm -r', 'chmod 777', 'curl | sh', 'wget | sh', 'sudo'];
|
|
@@ -869,18 +998,21 @@ dist/
|
|
|
869
998
|
}
|
|
870
999
|
}
|
|
871
1000
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
: '
|
|
882
|
-
|
|
883
|
-
|
|
1001
|
+
// Only report if dangerous Bash patterns found
|
|
1002
|
+
if (hasDangerousBash) {
|
|
1003
|
+
findings.push({
|
|
1004
|
+
checkId: 'CLAUDE-003',
|
|
1005
|
+
name: 'Dangerous Bash Permissions',
|
|
1006
|
+
description: 'Allows destructive shell commands',
|
|
1007
|
+
category: 'claude-code',
|
|
1008
|
+
severity: 'critical',
|
|
1009
|
+
passed: false,
|
|
1010
|
+
message: 'Remove rm -rf, sudo, etc.',
|
|
1011
|
+
file: '.claude/settings.json',
|
|
1012
|
+
fixable: false,
|
|
1013
|
+
fix: 'Manually remove dangerous commands (rm -rf, sudo, chmod 777, etc.) from the allow list in .claude/settings.json to prevent accidental destructive operations',
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
884
1016
|
return findings;
|
|
885
1017
|
}
|
|
886
1018
|
async checkCursorConfig(targetDir, autoFix) {
|
|
@@ -3276,10 +3408,9 @@ dist/
|
|
|
3276
3408
|
}
|
|
3277
3409
|
calculateScore(findings) {
|
|
3278
3410
|
let score = 100;
|
|
3279
|
-
|
|
3411
|
+
// All findings passed in are concrete issues (already filtered)
|
|
3280
3412
|
for (const finding of findings) {
|
|
3281
3413
|
const weight = SEVERITY_WEIGHTS[finding.severity];
|
|
3282
|
-
maxDeduction += weight;
|
|
3283
3414
|
if (!finding.passed && !finding.fixed) {
|
|
3284
3415
|
score -= weight;
|
|
3285
3416
|
}
|