hackmyagent-core 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.
Files changed (73) hide show
  1. package/dist/checker/check-skill.d.ts +48 -0
  2. package/dist/checker/check-skill.d.ts.map +1 -0
  3. package/dist/checker/check-skill.js +105 -0
  4. package/dist/checker/check-skill.js.map +1 -0
  5. package/dist/checker/check-skill.test.d.ts +2 -0
  6. package/dist/checker/check-skill.test.d.ts.map +1 -0
  7. package/dist/checker/check-skill.test.js +83 -0
  8. package/dist/checker/check-skill.test.js.map +1 -0
  9. package/dist/checker/index.d.ts +12 -0
  10. package/dist/checker/index.d.ts.map +1 -0
  11. package/dist/checker/index.js +16 -0
  12. package/dist/checker/index.js.map +1 -0
  13. package/dist/checker/permission-analyzer.d.ts +12 -0
  14. package/dist/checker/permission-analyzer.d.ts.map +1 -0
  15. package/dist/checker/permission-analyzer.js +84 -0
  16. package/dist/checker/permission-analyzer.js.map +1 -0
  17. package/dist/checker/permission-analyzer.test.d.ts +2 -0
  18. package/dist/checker/permission-analyzer.test.d.ts.map +1 -0
  19. package/dist/checker/permission-analyzer.test.js +87 -0
  20. package/dist/checker/permission-analyzer.test.js.map +1 -0
  21. package/dist/checker/publisher-verifier.d.ts +34 -0
  22. package/dist/checker/publisher-verifier.d.ts.map +1 -0
  23. package/dist/checker/publisher-verifier.js +121 -0
  24. package/dist/checker/publisher-verifier.js.map +1 -0
  25. package/dist/checker/publisher-verifier.test.d.ts +2 -0
  26. package/dist/checker/publisher-verifier.test.d.ts.map +1 -0
  27. package/dist/checker/publisher-verifier.test.js +171 -0
  28. package/dist/checker/publisher-verifier.test.js.map +1 -0
  29. package/dist/checker/skill-identifier.d.ts +14 -0
  30. package/dist/checker/skill-identifier.d.ts.map +1 -0
  31. package/dist/checker/skill-identifier.js +55 -0
  32. package/dist/checker/skill-identifier.js.map +1 -0
  33. package/dist/checker/skill-identifier.test.d.ts +2 -0
  34. package/dist/checker/skill-identifier.test.d.ts.map +1 -0
  35. package/dist/checker/skill-identifier.test.js +64 -0
  36. package/dist/checker/skill-identifier.test.js.map +1 -0
  37. package/dist/hardening/index.d.ts +7 -0
  38. package/dist/hardening/index.d.ts.map +1 -0
  39. package/dist/hardening/index.js +9 -0
  40. package/dist/hardening/index.js.map +1 -0
  41. package/dist/hardening/scanner.d.ts +85 -0
  42. package/dist/hardening/scanner.d.ts.map +1 -0
  43. package/dist/hardening/scanner.js +3410 -0
  44. package/dist/hardening/scanner.js.map +1 -0
  45. package/dist/hardening/scanner.test.d.ts +2 -0
  46. package/dist/hardening/scanner.test.d.ts.map +1 -0
  47. package/dist/hardening/scanner.test.js +1103 -0
  48. package/dist/hardening/scanner.test.js.map +1 -0
  49. package/dist/hardening/security-check.d.ts +56 -0
  50. package/dist/hardening/security-check.d.ts.map +1 -0
  51. package/dist/hardening/security-check.js +6 -0
  52. package/dist/hardening/security-check.js.map +1 -0
  53. package/dist/index.d.ts +27 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +35 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/scanner/external-scanner.d.ts +13 -0
  58. package/dist/scanner/external-scanner.d.ts.map +1 -0
  59. package/dist/scanner/external-scanner.js +299 -0
  60. package/dist/scanner/external-scanner.js.map +1 -0
  61. package/dist/scanner/external-scanner.test.d.ts +2 -0
  62. package/dist/scanner/external-scanner.test.d.ts.map +1 -0
  63. package/dist/scanner/external-scanner.test.js +302 -0
  64. package/dist/scanner/external-scanner.test.js.map +1 -0
  65. package/dist/scanner/index.d.ts +6 -0
  66. package/dist/scanner/index.d.ts.map +1 -0
  67. package/dist/scanner/index.js +9 -0
  68. package/dist/scanner/index.js.map +1 -0
  69. package/dist/scanner/types.d.ts +32 -0
  70. package/dist/scanner/types.d.ts.map +1 -0
  71. package/dist/scanner/types.js +6 -0
  72. package/dist/scanner/types.js.map +1 -0
  73. package/package.json +37 -0
@@ -0,0 +1,1103 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const scanner_1 = require("./scanner");
38
+ const fs = __importStar(require("fs/promises"));
39
+ const path = __importStar(require("path"));
40
+ const os = __importStar(require("os"));
41
+ (0, vitest_1.describe)('HardeningScanner', () => {
42
+ let scanner;
43
+ let tempDir;
44
+ (0, vitest_1.beforeEach)(async () => {
45
+ scanner = new scanner_1.HardeningScanner();
46
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
47
+ });
48
+ (0, vitest_1.afterEach)(async () => {
49
+ await fs.rm(tempDir, { recursive: true, force: true });
50
+ });
51
+ (0, vitest_1.describe)('scan', () => {
52
+ (0, vitest_1.it)('returns a scan result with findings', async () => {
53
+ const result = await scanner.scan({ targetDir: tempDir });
54
+ (0, vitest_1.expect)(result).toMatchObject({
55
+ timestamp: vitest_1.expect.any(Date),
56
+ platform: vitest_1.expect.any(String),
57
+ findings: vitest_1.expect.any(Array),
58
+ score: vitest_1.expect.any(Number),
59
+ maxScore: vitest_1.expect.any(Number),
60
+ });
61
+ });
62
+ (0, vitest_1.it)('calculates security score based on findings', async () => {
63
+ const result = await scanner.scan({ targetDir: tempDir });
64
+ (0, vitest_1.expect)(result.score).toBeGreaterThanOrEqual(0);
65
+ (0, vitest_1.expect)(result.score).toBeLessThanOrEqual(result.maxScore);
66
+ });
67
+ (0, vitest_1.it)('groups findings by category', async () => {
68
+ const result = await scanner.scan({ targetDir: tempDir });
69
+ const findings = result.findings;
70
+ // Each finding should have a category
71
+ for (const finding of findings) {
72
+ (0, vitest_1.expect)(finding.category).toBeDefined();
73
+ (0, vitest_1.expect)(typeof finding.category).toBe('string');
74
+ }
75
+ });
76
+ });
77
+ (0, vitest_1.describe)('credential exposure checks', () => {
78
+ (0, vitest_1.it)('detects API keys in config files', async () => {
79
+ // Create a config file with exposed API key
80
+ const configPath = path.join(tempDir, 'config.json');
81
+ await fs.writeFile(configPath, JSON.stringify({
82
+ apiKey: 'sk-ant-api03-xxxxxxxxxxxxxxxxxxxxx',
83
+ name: 'test',
84
+ }));
85
+ const result = await scanner.scan({ targetDir: tempDir });
86
+ const credFindings = result.findings.filter((f) => f.category === 'credentials');
87
+ (0, vitest_1.expect)(credFindings.some((f) => !f.passed)).toBe(true);
88
+ });
89
+ (0, vitest_1.it)('detects OpenAI API keys', async () => {
90
+ const configPath = path.join(tempDir, '.env');
91
+ await fs.writeFile(configPath, 'OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxx\n');
92
+ const result = await scanner.scan({ targetDir: tempDir });
93
+ const finding = result.findings.find((f) => f.checkId === 'CRED-001' && !f.passed);
94
+ (0, vitest_1.expect)(finding).toBeDefined();
95
+ (0, vitest_1.expect)(finding?.details?.keys).toContain('OPENAI_API_KEY');
96
+ });
97
+ (0, vitest_1.it)('detects AWS credentials', async () => {
98
+ const configPath = path.join(tempDir, 'config.yaml');
99
+ await fs.writeFile(configPath, 'aws_access_key_id: AKIAIOSFODNN7EXAMPLE\naws_secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n');
100
+ const result = await scanner.scan({ targetDir: tempDir });
101
+ const finding = result.findings.find((f) => f.checkId === 'CRED-001' && !f.passed);
102
+ (0, vitest_1.expect)(finding).toBeDefined();
103
+ });
104
+ (0, vitest_1.it)('passes when no credentials exposed', async () => {
105
+ const configPath = path.join(tempDir, 'config.json');
106
+ await fs.writeFile(configPath, JSON.stringify({
107
+ name: 'safe-config',
108
+ apiKey: '${ANTHROPIC_API_KEY}', // env var reference, not actual key
109
+ }));
110
+ const result = await scanner.scan({ targetDir: tempDir });
111
+ const credFindings = result.findings.filter((f) => f.category === 'credentials' && !f.passed);
112
+ (0, vitest_1.expect)(credFindings).toHaveLength(0);
113
+ });
114
+ });
115
+ (0, vitest_1.describe)('file permission checks', () => {
116
+ (0, vitest_1.it)('detects world-readable sensitive files', async () => {
117
+ const secretPath = path.join(tempDir, 'secrets.json');
118
+ await fs.writeFile(secretPath, '{"secret": "value"}');
119
+ await fs.chmod(secretPath, 0o644); // world-readable
120
+ const result = await scanner.scan({ targetDir: tempDir });
121
+ const permFindings = result.findings.filter((f) => f.category === 'permissions' && !f.passed);
122
+ // Should detect overly permissive files
123
+ (0, vitest_1.expect)(permFindings.length).toBeGreaterThanOrEqual(0);
124
+ });
125
+ });
126
+ (0, vitest_1.describe)('claude code specific checks', () => {
127
+ (0, vitest_1.it)('detects CLAUDE.md with sensitive content', async () => {
128
+ const claudeMdPath = path.join(tempDir, 'CLAUDE.md');
129
+ await fs.writeFile(claudeMdPath, '# Instructions\n\nAPI Key: sk-ant-api03-testsecretkey1234567890abc\n\nDo not share this.');
130
+ const result = await scanner.scan({ targetDir: tempDir });
131
+ const finding = result.findings.find((f) => f.checkId === 'CLAUDE-001' && !f.passed);
132
+ (0, vitest_1.expect)(finding).toBeDefined();
133
+ (0, vitest_1.expect)(finding?.message).toContain('CLAUDE.md');
134
+ });
135
+ (0, vitest_1.it)('passes for safe CLAUDE.md', async () => {
136
+ const claudeMdPath = path.join(tempDir, 'CLAUDE.md');
137
+ await fs.writeFile(claudeMdPath, '# Instructions\n\nUse environment variables for API keys.\n');
138
+ const result = await scanner.scan({ targetDir: tempDir });
139
+ const finding = result.findings.find((f) => f.checkId === 'CLAUDE-001');
140
+ (0, vitest_1.expect)(finding?.passed).toBe(true);
141
+ });
142
+ });
143
+ (0, vitest_1.describe)('MCP configuration checks', () => {
144
+ (0, vitest_1.it)('detects insecure MCP server configurations', async () => {
145
+ const mcpConfigPath = path.join(tempDir, 'mcp.json');
146
+ await fs.writeFile(mcpConfigPath, JSON.stringify({
147
+ servers: {
148
+ filesystem: {
149
+ command: 'mcp-server-filesystem',
150
+ args: ['/'], // Root access - dangerous
151
+ },
152
+ },
153
+ }));
154
+ const result = await scanner.scan({ targetDir: tempDir });
155
+ const finding = result.findings.find((f) => f.checkId === 'MCP-001' && !f.passed);
156
+ (0, vitest_1.expect)(finding).toBeDefined();
157
+ });
158
+ (0, vitest_1.it)('detects shell MCP server without restrictions', async () => {
159
+ const mcpConfigPath = path.join(tempDir, 'mcp.json');
160
+ await fs.writeFile(mcpConfigPath, JSON.stringify({
161
+ servers: {
162
+ shell: {
163
+ command: 'mcp-server-shell',
164
+ // No command restrictions
165
+ },
166
+ },
167
+ }));
168
+ const result = await scanner.scan({ targetDir: tempDir });
169
+ const finding = result.findings.find((f) => f.checkId === 'MCP-002' && !f.passed);
170
+ (0, vitest_1.expect)(finding).toBeDefined();
171
+ });
172
+ });
173
+ (0, vitest_1.describe)('auto-fix capabilities', () => {
174
+ (0, vitest_1.it)('can fix file permission issues', async () => {
175
+ const secretPath = path.join(tempDir, 'secrets.json');
176
+ await fs.writeFile(secretPath, '{"secret": "value"}');
177
+ await fs.chmod(secretPath, 0o644);
178
+ const result = await scanner.scan({
179
+ targetDir: tempDir,
180
+ autoFix: true,
181
+ });
182
+ // Check if fix was attempted for permission issues
183
+ const permFinding = result.findings.find((f) => f.category === 'permissions' && f.fixable);
184
+ if (permFinding && permFinding.fixed) {
185
+ const stats = await fs.stat(secretPath);
186
+ const mode = stats.mode & 0o777;
187
+ (0, vitest_1.expect)(mode).toBe(0o600);
188
+ }
189
+ });
190
+ (0, vitest_1.it)('reports which fixes were applied', async () => {
191
+ const result = await scanner.scan({
192
+ targetDir: tempDir,
193
+ autoFix: true,
194
+ });
195
+ for (const finding of result.findings) {
196
+ if (finding.fixable && !finding.passed) {
197
+ (0, vitest_1.expect)(finding.fixed).toBeDefined();
198
+ if (finding.fixed) {
199
+ (0, vitest_1.expect)(finding.fixMessage).toBeDefined();
200
+ }
201
+ }
202
+ }
203
+ });
204
+ (0, vitest_1.it)('does not auto-fix when disabled', async () => {
205
+ const secretPath = path.join(tempDir, 'secrets.json');
206
+ await fs.writeFile(secretPath, '{"secret": "value"}');
207
+ await fs.chmod(secretPath, 0o644);
208
+ const result = await scanner.scan({
209
+ targetDir: tempDir,
210
+ autoFix: false,
211
+ });
212
+ const fixedFindings = result.findings.filter((f) => f.fixed);
213
+ (0, vitest_1.expect)(fixedFindings).toHaveLength(0);
214
+ });
215
+ });
216
+ (0, vitest_1.describe)('score calculation', () => {
217
+ (0, vitest_1.it)('gives higher score when more checks pass', async () => {
218
+ // Safe directory
219
+ const safeDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-safe-'));
220
+ await fs.writeFile(path.join(safeDir, 'config.json'), JSON.stringify({ name: 'safe' }));
221
+ // Unsafe directory
222
+ const unsafeDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-unsafe-'));
223
+ await fs.writeFile(path.join(unsafeDir, 'config.json'), JSON.stringify({ apiKey: 'sk-ant-api03-secretkey1234567890def' }));
224
+ const safeResult = await scanner.scan({ targetDir: safeDir });
225
+ const unsafeResult = await scanner.scan({ targetDir: unsafeDir });
226
+ (0, vitest_1.expect)(safeResult.score).toBeGreaterThanOrEqual(unsafeResult.score);
227
+ await fs.rm(safeDir, { recursive: true, force: true });
228
+ await fs.rm(unsafeDir, { recursive: true, force: true });
229
+ });
230
+ (0, vitest_1.it)('weights critical issues higher than low', async () => {
231
+ // The score should penalize critical issues more than low issues
232
+ const result = await scanner.scan({ targetDir: tempDir });
233
+ // Verify score calculation logic exists
234
+ (0, vitest_1.expect)(result.maxScore).toBeGreaterThan(0);
235
+ });
236
+ });
237
+ });
238
+ (0, vitest_1.describe)('Git security checks', () => {
239
+ let scanner;
240
+ let tempDir;
241
+ (0, vitest_1.beforeEach)(async () => {
242
+ scanner = new scanner_1.HardeningScanner();
243
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
244
+ });
245
+ (0, vitest_1.afterEach)(async () => {
246
+ await fs.rm(tempDir, { recursive: true, force: true });
247
+ });
248
+ (0, vitest_1.it)('detects missing .gitignore', async () => {
249
+ // No .gitignore file
250
+ const result = await scanner.scan({ targetDir: tempDir });
251
+ const finding = result.findings.find((f) => f.checkId === 'GIT-001');
252
+ (0, vitest_1.expect)(finding).toBeDefined();
253
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
254
+ });
255
+ (0, vitest_1.it)('detects .gitignore missing sensitive patterns', async () => {
256
+ // Create .gitignore without .env pattern
257
+ await fs.writeFile(path.join(tempDir, '.gitignore'), 'node_modules/\n');
258
+ const result = await scanner.scan({ targetDir: tempDir });
259
+ const finding = result.findings.find((f) => f.checkId === 'GIT-002');
260
+ (0, vitest_1.expect)(finding).toBeDefined();
261
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
262
+ (0, vitest_1.expect)(finding?.message).toContain('.env');
263
+ });
264
+ (0, vitest_1.it)('passes when .gitignore has all sensitive patterns', async () => {
265
+ await fs.writeFile(path.join(tempDir, '.gitignore'), '.env\n.env.*\nsecrets.json\n*.pem\n*.key\n');
266
+ const result = await scanner.scan({ targetDir: tempDir });
267
+ const finding = result.findings.find((f) => f.checkId === 'GIT-002');
268
+ (0, vitest_1.expect)(finding?.passed).toBe(true);
269
+ });
270
+ (0, vitest_1.it)('detects .env file when .gitignore missing .env pattern', async () => {
271
+ await fs.writeFile(path.join(tempDir, '.gitignore'), 'node_modules/\n');
272
+ await fs.writeFile(path.join(tempDir, '.env'), 'SECRET=value\n');
273
+ const result = await scanner.scan({ targetDir: tempDir });
274
+ const finding = result.findings.find((f) => f.checkId === 'GIT-003');
275
+ (0, vitest_1.expect)(finding).toBeDefined();
276
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
277
+ (0, vitest_1.expect)(finding?.severity).toBe('critical');
278
+ });
279
+ });
280
+ (0, vitest_1.describe)('Network security checks', () => {
281
+ let scanner;
282
+ let tempDir;
283
+ (0, vitest_1.beforeEach)(async () => {
284
+ scanner = new scanner_1.HardeningScanner();
285
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
286
+ });
287
+ (0, vitest_1.afterEach)(async () => {
288
+ await fs.rm(tempDir, { recursive: true, force: true });
289
+ });
290
+ (0, vitest_1.it)('detects MCP server bound to 0.0.0.0', async () => {
291
+ await fs.writeFile(path.join(tempDir, 'mcp.json'), JSON.stringify({
292
+ servers: {
293
+ myserver: {
294
+ command: 'mcp-server',
295
+ args: ['--host', '0.0.0.0'],
296
+ },
297
+ },
298
+ }));
299
+ const result = await scanner.scan({ targetDir: tempDir });
300
+ const finding = result.findings.find((f) => f.checkId === 'NET-001');
301
+ (0, vitest_1.expect)(finding).toBeDefined();
302
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
303
+ (0, vitest_1.expect)(finding?.severity).toBe('critical');
304
+ });
305
+ (0, vitest_1.it)('detects remote MCP server without TLS', async () => {
306
+ await fs.writeFile(path.join(tempDir, 'mcp.json'), JSON.stringify({
307
+ servers: {
308
+ remote: {
309
+ url: 'http://api.example.com/mcp',
310
+ },
311
+ },
312
+ }));
313
+ const result = await scanner.scan({ targetDir: tempDir });
314
+ const finding = result.findings.find((f) => f.checkId === 'NET-002');
315
+ (0, vitest_1.expect)(finding).toBeDefined();
316
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
317
+ (0, vitest_1.expect)(finding?.severity).toBe('high');
318
+ });
319
+ (0, vitest_1.it)('passes for remote MCP server with HTTPS', async () => {
320
+ await fs.writeFile(path.join(tempDir, 'mcp.json'), JSON.stringify({
321
+ servers: {
322
+ remote: {
323
+ url: 'https://api.example.com/mcp',
324
+ },
325
+ },
326
+ }));
327
+ const result = await scanner.scan({ targetDir: tempDir });
328
+ const finding = result.findings.find((f) => f.checkId === 'NET-002');
329
+ (0, vitest_1.expect)(finding?.passed).toBe(true);
330
+ });
331
+ });
332
+ (0, vitest_1.describe)('Additional MCP checks', () => {
333
+ let scanner;
334
+ let tempDir;
335
+ (0, vitest_1.beforeEach)(async () => {
336
+ scanner = new scanner_1.HardeningScanner();
337
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
338
+ });
339
+ (0, vitest_1.afterEach)(async () => {
340
+ await fs.rm(tempDir, { recursive: true, force: true });
341
+ });
342
+ (0, vitest_1.it)('detects secrets passed as environment variables to MCP', async () => {
343
+ await fs.writeFile(path.join(tempDir, 'mcp.json'), JSON.stringify({
344
+ servers: {
345
+ myserver: {
346
+ command: 'mcp-server',
347
+ env: {
348
+ API_KEY: 'sk-ant-api03-hardcodedsecretkey1234567890',
349
+ },
350
+ },
351
+ },
352
+ }));
353
+ const result = await scanner.scan({ targetDir: tempDir });
354
+ const finding = result.findings.find((f) => f.checkId === 'MCP-003');
355
+ (0, vitest_1.expect)(finding).toBeDefined();
356
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
357
+ (0, vitest_1.expect)(finding?.severity).toBe('critical');
358
+ });
359
+ (0, vitest_1.it)('passes when env vars use references', async () => {
360
+ await fs.writeFile(path.join(tempDir, 'mcp.json'), JSON.stringify({
361
+ servers: {
362
+ myserver: {
363
+ command: 'mcp-server',
364
+ env: {
365
+ API_KEY: '${ANTHROPIC_API_KEY}',
366
+ },
367
+ },
368
+ },
369
+ }));
370
+ const result = await scanner.scan({ targetDir: tempDir });
371
+ const finding = result.findings.find((f) => f.checkId === 'MCP-003');
372
+ (0, vitest_1.expect)(finding?.passed).toBe(true);
373
+ });
374
+ (0, vitest_1.it)('detects database MCP server with default credentials', async () => {
375
+ await fs.writeFile(path.join(tempDir, 'mcp.json'), JSON.stringify({
376
+ servers: {
377
+ postgres: {
378
+ command: 'mcp-server-postgres',
379
+ args: ['--password', 'postgres'],
380
+ },
381
+ },
382
+ }));
383
+ const result = await scanner.scan({ targetDir: tempDir });
384
+ const finding = result.findings.find((f) => f.checkId === 'MCP-004');
385
+ (0, vitest_1.expect)(finding).toBeDefined();
386
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
387
+ });
388
+ (0, vitest_1.it)('detects MCP server allowing all tools', async () => {
389
+ await fs.writeFile(path.join(tempDir, 'mcp.json'), JSON.stringify({
390
+ servers: {
391
+ myserver: {
392
+ command: 'mcp-server',
393
+ allowedTools: ['*'],
394
+ },
395
+ },
396
+ }));
397
+ const result = await scanner.scan({ targetDir: tempDir });
398
+ const finding = result.findings.find((f) => f.checkId === 'MCP-005');
399
+ (0, vitest_1.expect)(finding).toBeDefined();
400
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
401
+ (0, vitest_1.expect)(finding?.severity).toBe('high');
402
+ });
403
+ });
404
+ (0, vitest_1.describe)('Claude Code additional checks', () => {
405
+ let scanner;
406
+ let tempDir;
407
+ (0, vitest_1.beforeEach)(async () => {
408
+ scanner = new scanner_1.HardeningScanner();
409
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
410
+ });
411
+ (0, vitest_1.afterEach)(async () => {
412
+ await fs.rm(tempDir, { recursive: true, force: true });
413
+ });
414
+ (0, vitest_1.it)('detects overly permissive allowed commands', async () => {
415
+ await fs.mkdir(path.join(tempDir, '.claude'), { recursive: true });
416
+ await fs.writeFile(path.join(tempDir, '.claude', 'settings.json'), JSON.stringify({
417
+ permissions: {
418
+ allow: ['Bash(*)', 'Read(*)', 'Write(*)'],
419
+ },
420
+ }));
421
+ const result = await scanner.scan({ targetDir: tempDir });
422
+ const finding = result.findings.find((f) => f.checkId === 'CLAUDE-002');
423
+ (0, vitest_1.expect)(finding).toBeDefined();
424
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
425
+ (0, vitest_1.expect)(finding?.severity).toBe('high');
426
+ });
427
+ (0, vitest_1.it)('detects dangerous Bash patterns', async () => {
428
+ await fs.mkdir(path.join(tempDir, '.claude'), { recursive: true });
429
+ await fs.writeFile(path.join(tempDir, '.claude', 'settings.json'), JSON.stringify({
430
+ permissions: {
431
+ allow: ['Bash(rm -rf *)'],
432
+ },
433
+ }));
434
+ const result = await scanner.scan({ targetDir: tempDir });
435
+ const finding = result.findings.find((f) => f.checkId === 'CLAUDE-003');
436
+ (0, vitest_1.expect)(finding).toBeDefined();
437
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
438
+ (0, vitest_1.expect)(finding?.severity).toBe('critical');
439
+ });
440
+ (0, vitest_1.it)('passes for scoped permissions', async () => {
441
+ await fs.mkdir(path.join(tempDir, '.claude'), { recursive: true });
442
+ await fs.writeFile(path.join(tempDir, '.claude', 'settings.json'), JSON.stringify({
443
+ permissions: {
444
+ allow: ['Read(./src/**)', 'Write(./src/**)'],
445
+ deny: ['Bash(rm *)'],
446
+ },
447
+ }));
448
+ const result = await scanner.scan({ targetDir: tempDir });
449
+ const finding = result.findings.find((f) => f.checkId === 'CLAUDE-002');
450
+ (0, vitest_1.expect)(finding?.passed).toBe(true);
451
+ });
452
+ });
453
+ (0, vitest_1.describe)('Cursor configuration checks', () => {
454
+ let scanner;
455
+ let tempDir;
456
+ (0, vitest_1.beforeEach)(async () => {
457
+ scanner = new scanner_1.HardeningScanner();
458
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
459
+ });
460
+ (0, vitest_1.afterEach)(async () => {
461
+ await fs.rm(tempDir, { recursive: true, force: true });
462
+ });
463
+ (0, vitest_1.it)('detects credentials in Cursor rules', async () => {
464
+ await fs.mkdir(path.join(tempDir, '.cursor'), { recursive: true });
465
+ await fs.writeFile(path.join(tempDir, '.cursor', 'rules'), 'Use API key sk-ant-api03-secretkey1234567890xyz for all requests\n');
466
+ const result = await scanner.scan({ targetDir: tempDir });
467
+ const finding = result.findings.find((f) => f.checkId === 'CURSOR-001');
468
+ (0, vitest_1.expect)(finding).toBeDefined();
469
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
470
+ (0, vitest_1.expect)(finding?.severity).toBe('critical');
471
+ });
472
+ (0, vitest_1.it)('detects credentials in .cursorrules', async () => {
473
+ await fs.writeFile(path.join(tempDir, '.cursorrules'), 'API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n');
474
+ const result = await scanner.scan({ targetDir: tempDir });
475
+ const finding = result.findings.find((f) => f.checkId === 'CURSOR-001');
476
+ (0, vitest_1.expect)(finding).toBeDefined();
477
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
478
+ });
479
+ });
480
+ (0, vitest_1.describe)('VSCode configuration checks', () => {
481
+ let scanner;
482
+ let tempDir;
483
+ (0, vitest_1.beforeEach)(async () => {
484
+ scanner = new scanner_1.HardeningScanner();
485
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
486
+ });
487
+ (0, vitest_1.afterEach)(async () => {
488
+ await fs.rm(tempDir, { recursive: true, force: true });
489
+ });
490
+ (0, vitest_1.it)('detects credentials in VSCode MCP config', async () => {
491
+ await fs.mkdir(path.join(tempDir, '.vscode'), { recursive: true });
492
+ await fs.writeFile(path.join(tempDir, '.vscode', 'mcp.json'), JSON.stringify({
493
+ servers: {
494
+ myserver: {
495
+ apiKey: 'sk-ant-api03-exposedsecretkey1234567890',
496
+ },
497
+ },
498
+ }));
499
+ const result = await scanner.scan({ targetDir: tempDir });
500
+ const finding = result.findings.find((f) => f.checkId === 'VSCODE-001');
501
+ (0, vitest_1.expect)(finding).toBeDefined();
502
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
503
+ (0, vitest_1.expect)(finding?.severity).toBe('critical');
504
+ });
505
+ (0, vitest_1.it)('detects overly permissive VSCode MCP config', async () => {
506
+ await fs.mkdir(path.join(tempDir, '.vscode'), { recursive: true });
507
+ await fs.writeFile(path.join(tempDir, '.vscode', 'mcp.json'), JSON.stringify({
508
+ servers: {
509
+ filesystem: {
510
+ command: 'mcp-server-filesystem',
511
+ args: ['/'],
512
+ },
513
+ },
514
+ }));
515
+ const result = await scanner.scan({ targetDir: tempDir });
516
+ const finding = result.findings.find((f) => f.checkId === 'VSCODE-002');
517
+ (0, vitest_1.expect)(finding).toBeDefined();
518
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
519
+ });
520
+ });
521
+ (0, vitest_1.describe)('Additional credential checks', () => {
522
+ let scanner;
523
+ let tempDir;
524
+ (0, vitest_1.beforeEach)(async () => {
525
+ scanner = new scanner_1.HardeningScanner();
526
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
527
+ });
528
+ (0, vitest_1.afterEach)(async () => {
529
+ await fs.rm(tempDir, { recursive: true, force: true });
530
+ });
531
+ (0, vitest_1.it)('detects private keys in directory', async () => {
532
+ await fs.writeFile(path.join(tempDir, 'server.key'), '-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBALRi\n-----END RSA PRIVATE KEY-----\n');
533
+ const result = await scanner.scan({ targetDir: tempDir });
534
+ const finding = result.findings.find((f) => f.checkId === 'CRED-002');
535
+ (0, vitest_1.expect)(finding).toBeDefined();
536
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
537
+ (0, vitest_1.expect)(finding?.severity).toBe('critical');
538
+ });
539
+ (0, vitest_1.it)('detects .pem files', async () => {
540
+ await fs.writeFile(path.join(tempDir, 'cert.pem'), '-----BEGIN CERTIFICATE-----\nMIIBOgIBAAJBALRi\n-----END CERTIFICATE-----\n');
541
+ const result = await scanner.scan({ targetDir: tempDir });
542
+ const finding = result.findings.find((f) => f.checkId === 'CRED-002');
543
+ (0, vitest_1.expect)(finding).toBeDefined();
544
+ });
545
+ (0, vitest_1.it)('detects hardcoded secrets in package.json', async () => {
546
+ await fs.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
547
+ name: 'test',
548
+ scripts: {
549
+ deploy: 'API_KEY=sk-ant-api03-secretkey12345678901234abc npm run build',
550
+ },
551
+ }));
552
+ const result = await scanner.scan({ targetDir: tempDir });
553
+ const finding = result.findings.find((f) => f.checkId === 'CRED-003');
554
+ (0, vitest_1.expect)(finding).toBeDefined();
555
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
556
+ });
557
+ (0, vitest_1.it)('detects JWT secrets in config', async () => {
558
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({
559
+ jwt: {
560
+ secret: 'super-secret-jwt-key-that-should-not-be-here',
561
+ },
562
+ }));
563
+ const result = await scanner.scan({ targetDir: tempDir });
564
+ const finding = result.findings.find((f) => f.checkId === 'CRED-004');
565
+ (0, vitest_1.expect)(finding).toBeDefined();
566
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
567
+ });
568
+ });
569
+ (0, vitest_1.describe)('Permission boundary checks', () => {
570
+ let scanner;
571
+ let tempDir;
572
+ (0, vitest_1.beforeEach)(async () => {
573
+ scanner = new scanner_1.HardeningScanner();
574
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
575
+ });
576
+ (0, vitest_1.afterEach)(async () => {
577
+ await fs.rm(tempDir, { recursive: true, force: true });
578
+ });
579
+ (0, vitest_1.it)('detects executable config files', async () => {
580
+ const configPath = path.join(tempDir, 'config.json');
581
+ await fs.writeFile(configPath, '{}');
582
+ await fs.chmod(configPath, 0o755);
583
+ const result = await scanner.scan({ targetDir: tempDir });
584
+ const finding = result.findings.find((f) => f.checkId === 'PERM-002');
585
+ (0, vitest_1.expect)(finding).toBeDefined();
586
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
587
+ });
588
+ (0, vitest_1.it)('detects group-writable sensitive files', async () => {
589
+ const envPath = path.join(tempDir, '.env');
590
+ await fs.writeFile(envPath, 'SECRET=value');
591
+ await fs.chmod(envPath, 0o664);
592
+ const result = await scanner.scan({ targetDir: tempDir });
593
+ const finding = result.findings.find((f) => f.checkId === 'PERM-003');
594
+ (0, vitest_1.expect)(finding).toBeDefined();
595
+ (0, vitest_1.expect)(finding?.passed).toBe(false);
596
+ });
597
+ });
598
+ (0, vitest_1.describe)('Auto-fix: Git security', () => {
599
+ let scanner;
600
+ let tempDir;
601
+ (0, vitest_1.beforeEach)(async () => {
602
+ scanner = new scanner_1.HardeningScanner();
603
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
604
+ });
605
+ (0, vitest_1.afterEach)(async () => {
606
+ await fs.rm(tempDir, { recursive: true, force: true });
607
+ });
608
+ (0, vitest_1.it)('creates .gitignore when missing', async () => {
609
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
610
+ const gitignoreExists = await fs.access(path.join(tempDir, '.gitignore')).then(() => true).catch(() => false);
611
+ (0, vitest_1.expect)(gitignoreExists).toBe(true);
612
+ const content = await fs.readFile(path.join(tempDir, '.gitignore'), 'utf-8');
613
+ (0, vitest_1.expect)(content).toContain('.env');
614
+ (0, vitest_1.expect)(content).toContain('*.pem');
615
+ });
616
+ (0, vitest_1.it)('adds missing patterns to existing .gitignore', async () => {
617
+ await fs.writeFile(path.join(tempDir, '.gitignore'), 'node_modules/\n');
618
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
619
+ const content = await fs.readFile(path.join(tempDir, '.gitignore'), 'utf-8');
620
+ (0, vitest_1.expect)(content).toContain('node_modules/');
621
+ (0, vitest_1.expect)(content).toContain('.env');
622
+ (0, vitest_1.expect)(content).toContain('*.pem');
623
+ });
624
+ (0, vitest_1.it)('reports fix was applied for .gitignore', async () => {
625
+ const result = await scanner.scan({ targetDir: tempDir, autoFix: true });
626
+ const gitFinding = result.findings.find(f => f.checkId === 'GIT-001');
627
+ (0, vitest_1.expect)(gitFinding?.fixed).toBe(true);
628
+ });
629
+ });
630
+ (0, vitest_1.describe)('Auto-fix: Credential replacement', () => {
631
+ let scanner;
632
+ let tempDir;
633
+ (0, vitest_1.beforeEach)(async () => {
634
+ scanner = new scanner_1.HardeningScanner();
635
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
636
+ });
637
+ (0, vitest_1.afterEach)(async () => {
638
+ await fs.rm(tempDir, { recursive: true, force: true });
639
+ });
640
+ (0, vitest_1.it)('replaces Anthropic API key with env var reference in config.json', async () => {
641
+ const configPath = path.join(tempDir, 'config.json');
642
+ await fs.writeFile(configPath, JSON.stringify({
643
+ apiKey: 'sk-ant-api03-abcdefghijklmnopqrstuvwxyz',
644
+ name: 'test'
645
+ }, null, 2));
646
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
647
+ const content = await fs.readFile(configPath, 'utf-8');
648
+ (0, vitest_1.expect)(content).not.toContain('sk-ant-api03');
649
+ (0, vitest_1.expect)(content).toContain('${ANTHROPIC_API_KEY}');
650
+ });
651
+ (0, vitest_1.it)('replaces OpenAI API key with env var reference', async () => {
652
+ const configPath = path.join(tempDir, 'config.json');
653
+ await fs.writeFile(configPath, JSON.stringify({
654
+ openaiKey: 'sk-proj-abcdefghijklmnopqrstuvwxyz123456',
655
+ }, null, 2));
656
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
657
+ const content = await fs.readFile(configPath, 'utf-8');
658
+ (0, vitest_1.expect)(content).not.toContain('sk-proj-');
659
+ (0, vitest_1.expect)(content).toContain('${OPENAI_API_KEY}');
660
+ });
661
+ (0, vitest_1.it)('creates .env.example with placeholder', async () => {
662
+ const configPath = path.join(tempDir, 'config.json');
663
+ await fs.writeFile(configPath, JSON.stringify({
664
+ apiKey: 'sk-ant-api03-abcdefghijklmnopqrstuvwxyz',
665
+ }, null, 2));
666
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
667
+ const envExamplePath = path.join(tempDir, '.env.example');
668
+ const envExists = await fs.access(envExamplePath).then(() => true).catch(() => false);
669
+ (0, vitest_1.expect)(envExists).toBe(true);
670
+ const content = await fs.readFile(envExamplePath, 'utf-8');
671
+ (0, vitest_1.expect)(content).toContain('ANTHROPIC_API_KEY=');
672
+ });
673
+ });
674
+ (0, vitest_1.describe)('Auto-fix: Network security', () => {
675
+ let scanner;
676
+ let tempDir;
677
+ (0, vitest_1.beforeEach)(async () => {
678
+ scanner = new scanner_1.HardeningScanner();
679
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
680
+ });
681
+ (0, vitest_1.afterEach)(async () => {
682
+ await fs.rm(tempDir, { recursive: true, force: true });
683
+ });
684
+ (0, vitest_1.it)('changes 0.0.0.0 to 127.0.0.1 in mcp.json', async () => {
685
+ const mcpPath = path.join(tempDir, 'mcp.json');
686
+ await fs.writeFile(mcpPath, JSON.stringify({
687
+ servers: {
688
+ myserver: {
689
+ command: 'node',
690
+ args: ['server.js', '--host', '0.0.0.0', '--port', '3000']
691
+ }
692
+ }
693
+ }, null, 2));
694
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
695
+ const content = await fs.readFile(mcpPath, 'utf-8');
696
+ (0, vitest_1.expect)(content).not.toContain('0.0.0.0');
697
+ (0, vitest_1.expect)(content).toContain('127.0.0.1');
698
+ });
699
+ (0, vitest_1.it)('reports fix was applied for network binding', async () => {
700
+ const mcpPath = path.join(tempDir, 'mcp.json');
701
+ await fs.writeFile(mcpPath, JSON.stringify({
702
+ servers: {
703
+ myserver: {
704
+ command: 'node',
705
+ args: ['--host', '0.0.0.0']
706
+ }
707
+ }
708
+ }, null, 2));
709
+ const result = await scanner.scan({ targetDir: tempDir, autoFix: true });
710
+ const netFinding = result.findings.find(f => f.checkId === 'NET-001');
711
+ (0, vitest_1.expect)(netFinding?.fixed).toBe(true);
712
+ });
713
+ });
714
+ (0, vitest_1.describe)('Auto-fix: MCP filesystem access', () => {
715
+ let scanner;
716
+ let tempDir;
717
+ (0, vitest_1.beforeEach)(async () => {
718
+ scanner = new scanner_1.HardeningScanner();
719
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
720
+ });
721
+ (0, vitest_1.afterEach)(async () => {
722
+ await fs.rm(tempDir, { recursive: true, force: true });
723
+ });
724
+ (0, vitest_1.it)('changes root "/" to "./data" in mcp.json', async () => {
725
+ const mcpPath = path.join(tempDir, 'mcp.json');
726
+ await fs.writeFile(mcpPath, JSON.stringify({
727
+ servers: {
728
+ filesystem: {
729
+ command: 'mcp-server-filesystem',
730
+ args: ['/']
731
+ }
732
+ }
733
+ }, null, 2));
734
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
735
+ const content = await fs.readFile(mcpPath, 'utf-8');
736
+ const config = JSON.parse(content);
737
+ (0, vitest_1.expect)(config.servers.filesystem.args).not.toContain('/');
738
+ (0, vitest_1.expect)(config.servers.filesystem.args).toContain('./data');
739
+ });
740
+ (0, vitest_1.it)('changes home "~" to "./" in mcp.json', async () => {
741
+ const mcpPath = path.join(tempDir, 'mcp.json');
742
+ await fs.writeFile(mcpPath, JSON.stringify({
743
+ servers: {
744
+ filesystem: {
745
+ command: 'mcp-server-filesystem',
746
+ args: ['~']
747
+ }
748
+ }
749
+ }, null, 2));
750
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
751
+ const content = await fs.readFile(mcpPath, 'utf-8');
752
+ const config = JSON.parse(content);
753
+ (0, vitest_1.expect)(config.servers.filesystem.args).not.toContain('~');
754
+ (0, vitest_1.expect)(config.servers.filesystem.args).toContain('./');
755
+ });
756
+ });
757
+ (0, vitest_1.describe)('Auto-fix: MCP hardcoded secrets', () => {
758
+ let scanner;
759
+ let tempDir;
760
+ (0, vitest_1.beforeEach)(async () => {
761
+ scanner = new scanner_1.HardeningScanner();
762
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
763
+ });
764
+ (0, vitest_1.afterEach)(async () => {
765
+ await fs.rm(tempDir, { recursive: true, force: true });
766
+ });
767
+ (0, vitest_1.it)('replaces hardcoded API key in MCP env with reference', async () => {
768
+ const mcpPath = path.join(tempDir, 'mcp.json');
769
+ await fs.writeFile(mcpPath, JSON.stringify({
770
+ servers: {
771
+ myserver: {
772
+ command: 'node',
773
+ env: {
774
+ API_KEY: 'sk-ant-api03-secretkeyhere1234567890'
775
+ }
776
+ }
777
+ }
778
+ }, null, 2));
779
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
780
+ const content = await fs.readFile(mcpPath, 'utf-8');
781
+ (0, vitest_1.expect)(content).not.toContain('sk-ant-api03');
782
+ (0, vitest_1.expect)(content).toContain('${ANTHROPIC_API_KEY}');
783
+ });
784
+ });
785
+ (0, vitest_1.describe)('Platform detection', () => {
786
+ let scanner;
787
+ let tempDir;
788
+ (0, vitest_1.beforeEach)(async () => {
789
+ scanner = new scanner_1.HardeningScanner();
790
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
791
+ });
792
+ (0, vitest_1.afterEach)(async () => {
793
+ await fs.rm(tempDir, { recursive: true, force: true });
794
+ });
795
+ (0, vitest_1.it)('detects Claude Code environment', async () => {
796
+ await fs.writeFile(path.join(tempDir, 'CLAUDE.md'), '# Claude Instructions');
797
+ const result = await scanner.scan({ targetDir: tempDir });
798
+ (0, vitest_1.expect)(result.platform).toContain('claude');
799
+ });
800
+ (0, vitest_1.it)('detects Cursor environment', async () => {
801
+ await fs.mkdir(path.join(tempDir, '.cursor'), { recursive: true });
802
+ await fs.writeFile(path.join(tempDir, '.cursor', 'rules'), 'cursor rules');
803
+ const result = await scanner.scan({ targetDir: tempDir });
804
+ (0, vitest_1.expect)(result.platform).toContain('cursor');
805
+ });
806
+ (0, vitest_1.it)('detects MCP configuration', async () => {
807
+ await fs.writeFile(path.join(tempDir, 'mcp.json'), JSON.stringify({ servers: {} }));
808
+ const result = await scanner.scan({ targetDir: tempDir });
809
+ (0, vitest_1.expect)(result.platform).toContain('mcp');
810
+ });
811
+ (0, vitest_1.it)('returns generic for unknown platform', async () => {
812
+ const result = await scanner.scan({ targetDir: tempDir });
813
+ (0, vitest_1.expect)(result.platform).toBeDefined();
814
+ });
815
+ });
816
+ (0, vitest_1.describe)('Backup and rollback', () => {
817
+ let scanner;
818
+ let tempDir;
819
+ (0, vitest_1.beforeEach)(async () => {
820
+ scanner = new scanner_1.HardeningScanner();
821
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
822
+ });
823
+ (0, vitest_1.afterEach)(async () => {
824
+ await fs.rm(tempDir, { recursive: true, force: true });
825
+ });
826
+ (0, vitest_1.it)('creates backup before auto-fix', async () => {
827
+ // Create a config file with exposed credentials
828
+ const configPath = path.join(tempDir, 'config.json');
829
+ await fs.writeFile(configPath, JSON.stringify({ apiKey: 'sk-ant-api03-secretkey12345678901234' }));
830
+ // Run with auto-fix
831
+ const result = await scanner.scan({ targetDir: tempDir, autoFix: true });
832
+ // Check backup was created
833
+ const backupDir = path.join(tempDir, '.hackmyagent-backup');
834
+ const backupExists = await fs
835
+ .access(backupDir)
836
+ .then(() => true)
837
+ .catch(() => false);
838
+ (0, vitest_1.expect)(backupExists).toBe(true);
839
+ // Check backup contains the original file
840
+ const backups = await fs.readdir(backupDir);
841
+ (0, vitest_1.expect)(backups.length).toBeGreaterThan(0);
842
+ // Read the backup and verify it has original content
843
+ const backupContent = await fs.readFile(path.join(backupDir, backups[0], 'config.json'), 'utf-8');
844
+ (0, vitest_1.expect)(backupContent).toContain('sk-ant-api03');
845
+ });
846
+ (0, vitest_1.it)('backup contains timestamp in folder name', async () => {
847
+ const configPath = path.join(tempDir, 'config.json');
848
+ await fs.writeFile(configPath, JSON.stringify({ apiKey: 'sk-ant-api03-secretkey12345678901234' }));
849
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
850
+ const backupDir = path.join(tempDir, '.hackmyagent-backup');
851
+ const backups = await fs.readdir(backupDir);
852
+ // Backup folder should have timestamp format: YYYY-MM-DD-HHMMSS
853
+ (0, vitest_1.expect)(backups[0]).toMatch(/^\d{4}-\d{2}-\d{2}-\d{6}$/);
854
+ });
855
+ (0, vitest_1.it)('can rollback to previous state', async () => {
856
+ const configPath = path.join(tempDir, 'config.json');
857
+ const originalContent = JSON.stringify({ apiKey: 'sk-ant-api03-secretkey12345678901234' });
858
+ await fs.writeFile(configPath, originalContent);
859
+ // Run with auto-fix (modifies the file)
860
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
861
+ // Verify file was modified
862
+ const modifiedContent = await fs.readFile(configPath, 'utf-8');
863
+ (0, vitest_1.expect)(modifiedContent).not.toContain('sk-ant-api03');
864
+ // Rollback
865
+ await scanner.rollback(tempDir);
866
+ // Verify file is restored
867
+ const restoredContent = await fs.readFile(configPath, 'utf-8');
868
+ (0, vitest_1.expect)(restoredContent).toBe(originalContent);
869
+ });
870
+ (0, vitest_1.it)('rollback restores multiple files', async () => {
871
+ // Create multiple files
872
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ key: 'sk-ant-api03-secret1key1234567890abc' }));
873
+ await fs.writeFile(path.join(tempDir, 'mcp.json'), JSON.stringify({
874
+ mcpServers: {
875
+ test: { env: { API_KEY: 'sk-ant-api03-secret2key1234567890abc' } },
876
+ },
877
+ }));
878
+ // Run auto-fix
879
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
880
+ // Rollback
881
+ await scanner.rollback(tempDir);
882
+ // Check both files restored
883
+ const config = await fs.readFile(path.join(tempDir, 'config.json'), 'utf-8');
884
+ const mcp = await fs.readFile(path.join(tempDir, 'mcp.json'), 'utf-8');
885
+ (0, vitest_1.expect)(config).toContain('sk-ant-api03-secret1key1234567890abc');
886
+ (0, vitest_1.expect)(mcp).toContain('sk-ant-api03-secret2');
887
+ });
888
+ (0, vitest_1.it)('rollback removes newly created files', async () => {
889
+ // Create a file with credentials (no .gitignore exists)
890
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ key: 'sk-ant-api03-secret' }));
891
+ // Run auto-fix (creates .gitignore and .env.example)
892
+ await scanner.scan({ targetDir: tempDir, autoFix: true });
893
+ // Verify new files were created
894
+ const gitignoreExists = await fs
895
+ .access(path.join(tempDir, '.gitignore'))
896
+ .then(() => true)
897
+ .catch(() => false);
898
+ (0, vitest_1.expect)(gitignoreExists).toBe(true);
899
+ // Rollback
900
+ await scanner.rollback(tempDir);
901
+ // Verify .gitignore is removed (it didn't exist before)
902
+ const gitignoreAfterRollback = await fs
903
+ .access(path.join(tempDir, '.gitignore'))
904
+ .then(() => true)
905
+ .catch(() => false);
906
+ (0, vitest_1.expect)(gitignoreAfterRollback).toBe(false);
907
+ });
908
+ (0, vitest_1.it)('returns backup path in scan result when autoFix is true', async () => {
909
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ key: 'sk-ant-api03-secret' }));
910
+ const result = await scanner.scan({ targetDir: tempDir, autoFix: true });
911
+ (0, vitest_1.expect)(result.backupPath).toBeDefined();
912
+ (0, vitest_1.expect)(result.backupPath).toContain('.hackmyagent-backup');
913
+ });
914
+ (0, vitest_1.it)('does not create backup when autoFix is false', async () => {
915
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ key: 'sk-ant-api03-secret' }));
916
+ const result = await scanner.scan({ targetDir: tempDir, autoFix: false });
917
+ (0, vitest_1.expect)(result.backupPath).toBeUndefined();
918
+ const backupDir = path.join(tempDir, '.hackmyagent-backup');
919
+ const backupExists = await fs
920
+ .access(backupDir)
921
+ .then(() => true)
922
+ .catch(() => false);
923
+ (0, vitest_1.expect)(backupExists).toBe(false);
924
+ });
925
+ (0, vitest_1.it)('throws error when no backup exists for rollback', async () => {
926
+ await (0, vitest_1.expect)(scanner.rollback(tempDir)).rejects.toThrow(/no backup found/i);
927
+ });
928
+ });
929
+ (0, vitest_1.describe)('Dry-run mode', () => {
930
+ let scanner;
931
+ let tempDir;
932
+ (0, vitest_1.beforeEach)(async () => {
933
+ scanner = new scanner_1.HardeningScanner();
934
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
935
+ });
936
+ (0, vitest_1.afterEach)(async () => {
937
+ await fs.rm(tempDir, { recursive: true, force: true });
938
+ });
939
+ (0, vitest_1.it)('shows what would be fixed without modifying files', async () => {
940
+ const configPath = path.join(tempDir, 'config.json');
941
+ const originalContent = JSON.stringify({ apiKey: 'sk-ant-api03-secretkey12345678901234' });
942
+ await fs.writeFile(configPath, originalContent);
943
+ const result = await scanner.scan({
944
+ targetDir: tempDir,
945
+ autoFix: true,
946
+ dryRun: true,
947
+ });
948
+ // Should report what would be fixed
949
+ const credFinding = result.findings.find((f) => f.checkId === 'CRED-001');
950
+ (0, vitest_1.expect)(credFinding?.wouldFix).toBe(true);
951
+ // File should NOT be modified
952
+ const content = await fs.readFile(configPath, 'utf-8');
953
+ (0, vitest_1.expect)(content).toBe(originalContent);
954
+ });
955
+ (0, vitest_1.it)('does not create backup in dry-run mode', async () => {
956
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ apiKey: 'sk-ant-api03-secretkey1234567890def' }));
957
+ const result = await scanner.scan({
958
+ targetDir: tempDir,
959
+ autoFix: true,
960
+ dryRun: true,
961
+ });
962
+ (0, vitest_1.expect)(result.backupPath).toBeUndefined();
963
+ const backupDir = path.join(tempDir, '.hackmyagent-backup');
964
+ const backupExists = await fs
965
+ .access(backupDir)
966
+ .then(() => true)
967
+ .catch(() => false);
968
+ (0, vitest_1.expect)(backupExists).toBe(false);
969
+ });
970
+ (0, vitest_1.it)('reports all fixable issues in dry-run', async () => {
971
+ // Create multiple fixable issues
972
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ key: 'sk-ant-api03-secret1key1234567890abc' }));
973
+ // No .gitignore (fixable)
974
+ const result = await scanner.scan({
975
+ targetDir: tempDir,
976
+ autoFix: true,
977
+ dryRun: true,
978
+ });
979
+ const wouldFixFindings = result.findings.filter((f) => f.wouldFix);
980
+ (0, vitest_1.expect)(wouldFixFindings.length).toBeGreaterThanOrEqual(2);
981
+ });
982
+ (0, vitest_1.it)('returns dryRun flag in result', async () => {
983
+ const result = await scanner.scan({
984
+ targetDir: tempDir,
985
+ autoFix: true,
986
+ dryRun: true,
987
+ });
988
+ (0, vitest_1.expect)(result.dryRun).toBe(true);
989
+ });
990
+ });
991
+ (0, vitest_1.describe)('Atomic auto-fix', () => {
992
+ let scanner;
993
+ let tempDir;
994
+ (0, vitest_1.beforeEach)(async () => {
995
+ scanner = new scanner_1.HardeningScanner();
996
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
997
+ });
998
+ (0, vitest_1.afterEach)(async () => {
999
+ await fs.rm(tempDir, { recursive: true, force: true });
1000
+ });
1001
+ (0, vitest_1.it)('rolls back all changes if any fix fails', async () => {
1002
+ // Create a config file that can be fixed
1003
+ const configPath = path.join(tempDir, 'config.json');
1004
+ const originalContent = JSON.stringify({ apiKey: 'sk-ant-api03-secretkey1234567890def' });
1005
+ await fs.writeFile(configPath, originalContent);
1006
+ // Create a read-only directory to cause .gitignore creation to fail
1007
+ const readOnlyDir = path.join(tempDir, 'readonly');
1008
+ await fs.mkdir(readOnlyDir);
1009
+ // Mock a scenario where fix would fail by making config.json read-only after backup
1010
+ // For this test, we'll verify the rollback mechanism exists
1011
+ // The actual failure simulation would require more complex setup
1012
+ const result = await scanner.scan({
1013
+ targetDir: tempDir,
1014
+ autoFix: true,
1015
+ });
1016
+ // If fixes succeeded, verify atomicity flag is set
1017
+ (0, vitest_1.expect)(result.atomicFix).toBeDefined();
1018
+ });
1019
+ (0, vitest_1.it)('sets atomicFix to true when all fixes succeed', async () => {
1020
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ apiKey: 'sk-ant-api03-secretkey1234567890def' }));
1021
+ const result = await scanner.scan({
1022
+ targetDir: tempDir,
1023
+ autoFix: true,
1024
+ });
1025
+ (0, vitest_1.expect)(result.atomicFix).toBe(true);
1026
+ });
1027
+ (0, vitest_1.it)('provides rollback instructions on partial failure', async () => {
1028
+ // Create fixable content
1029
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ apiKey: 'sk-ant-api03-secretkey1234567890def' }));
1030
+ const result = await scanner.scan({
1031
+ targetDir: tempDir,
1032
+ autoFix: true,
1033
+ });
1034
+ // When autoFix is used, backupPath should be available for manual rollback
1035
+ if (result.findings.some((f) => f.fixed)) {
1036
+ (0, vitest_1.expect)(result.backupPath).toBeDefined();
1037
+ }
1038
+ });
1039
+ });
1040
+ (0, vitest_1.describe)('Ignore checks', () => {
1041
+ let scanner;
1042
+ let tempDir;
1043
+ (0, vitest_1.beforeEach)(async () => {
1044
+ scanner = new scanner_1.HardeningScanner();
1045
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hackmyagent-test-'));
1046
+ });
1047
+ (0, vitest_1.afterEach)(async () => {
1048
+ await fs.rm(tempDir, { recursive: true, force: true });
1049
+ });
1050
+ (0, vitest_1.it)('ignores specific check IDs', async () => {
1051
+ // Create file with exposed credential
1052
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ apiKey: 'sk-ant-api03-secretkey1234567890def' }));
1053
+ // Scan without ignore - should find CRED-001
1054
+ const resultWithCheck = await scanner.scan({ targetDir: tempDir });
1055
+ (0, vitest_1.expect)(resultWithCheck.findings.some((f) => f.checkId === 'CRED-001')).toBe(true);
1056
+ // Scan with ignore - should NOT find CRED-001
1057
+ const resultIgnored = await scanner.scan({
1058
+ targetDir: tempDir,
1059
+ ignore: ['CRED-001'],
1060
+ });
1061
+ (0, vitest_1.expect)(resultIgnored.findings.some((f) => f.checkId === 'CRED-001')).toBe(false);
1062
+ });
1063
+ (0, vitest_1.it)('ignore is case-insensitive', async () => {
1064
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ apiKey: 'sk-ant-api03-secretkey1234567890def' }));
1065
+ const result = await scanner.scan({
1066
+ targetDir: tempDir,
1067
+ ignore: ['cred-001'], // lowercase
1068
+ });
1069
+ (0, vitest_1.expect)(result.findings.some((f) => f.checkId === 'CRED-001')).toBe(false);
1070
+ });
1071
+ (0, vitest_1.it)('returns list of ignored checks in result', async () => {
1072
+ const result = await scanner.scan({
1073
+ targetDir: tempDir,
1074
+ ignore: ['CRED-001', 'GIT-001'],
1075
+ });
1076
+ (0, vitest_1.expect)(result.ignored).toBeDefined();
1077
+ (0, vitest_1.expect)(result.ignored).toContain('CRED-001');
1078
+ (0, vitest_1.expect)(result.ignored).toContain('GIT-001');
1079
+ });
1080
+ (0, vitest_1.it)('ignores multiple check IDs', async () => {
1081
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ apiKey: 'sk-ant-api03-secretkey1234567890def' }));
1082
+ const result = await scanner.scan({
1083
+ targetDir: tempDir,
1084
+ ignore: ['CRED-001', 'GIT-001', 'GIT-002'],
1085
+ });
1086
+ (0, vitest_1.expect)(result.findings.some((f) => f.checkId === 'CRED-001')).toBe(false);
1087
+ (0, vitest_1.expect)(result.findings.some((f) => f.checkId === 'GIT-001')).toBe(false);
1088
+ (0, vitest_1.expect)(result.findings.some((f) => f.checkId === 'GIT-002')).toBe(false);
1089
+ });
1090
+ (0, vitest_1.it)('score calculation excludes ignored checks', async () => {
1091
+ await fs.writeFile(path.join(tempDir, 'config.json'), JSON.stringify({ apiKey: 'sk-ant-api03-secretkey1234567890def' }));
1092
+ // Without ignore
1093
+ const resultFull = await scanner.scan({ targetDir: tempDir });
1094
+ // With ignore (ignoring critical check means fewer findings)
1095
+ const resultIgnored = await scanner.scan({
1096
+ targetDir: tempDir,
1097
+ ignore: ['CRED-001'],
1098
+ });
1099
+ // Should have fewer findings when ignoring a check
1100
+ (0, vitest_1.expect)(resultIgnored.findings.length).toBeLessThan(resultFull.findings.length);
1101
+ });
1102
+ });
1103
+ //# sourceMappingURL=scanner.test.js.map