fchek 1.0.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 (66) hide show
  1. package/README.md +64 -0
  2. package/bin/fchek.js +107 -0
  3. package/lib/api.js +110 -0
  4. package/lib/audit.js +211 -0
  5. package/lib/bench.js +248 -0
  6. package/lib/config.js +191 -0
  7. package/lib/context.js +356 -0
  8. package/lib/convention.js +526 -0
  9. package/lib/coverage.js +604 -0
  10. package/lib/db.js +135 -0
  11. package/lib/deps-check.js +264 -0
  12. package/lib/deps.js +374 -0
  13. package/lib/docker.js +84 -0
  14. package/lib/doctor.js +149 -0
  15. package/lib/dom.js +226 -0
  16. package/lib/fuzz.js +470 -0
  17. package/lib/git.js +290 -0
  18. package/lib/goto.js +544 -0
  19. package/lib/launch.js +182 -0
  20. package/lib/lint.js +624 -0
  21. package/lib/new_features.test.js +181 -0
  22. package/lib/output.js +46 -0
  23. package/lib/port.js +173 -0
  24. package/lib/process.js +228 -0
  25. package/lib/profile.js +453 -0
  26. package/lib/python.js +41 -0
  27. package/lib/race.js +186 -0
  28. package/lib/registry.js +179 -0
  29. package/lib/repl.js +135 -0
  30. package/lib/run.js +403 -0
  31. package/lib/screenshot.js +152 -0
  32. package/lib/secrets.js +257 -0
  33. package/lib/state.js +219 -0
  34. package/lib/test.js +471 -0
  35. package/lib/vuln.js +253 -0
  36. package/lib/watch.js +240 -0
  37. package/lib/winlog.js +123 -0
  38. package/package.json +27 -0
  39. package/skills/ACTIVATE.md +274 -0
  40. package/skills/README.md +163 -0
  41. package/skills/agent.md +444 -0
  42. package/skills/api.md +47 -0
  43. package/skills/bench.md +117 -0
  44. package/skills/context.md +116 -0
  45. package/skills/convention.md +143 -0
  46. package/skills/coverage.md +99 -0
  47. package/skills/csharp.md +97 -0
  48. package/skills/db.md +66 -0
  49. package/skills/deps-check.md +135 -0
  50. package/skills/deps.md +143 -0
  51. package/skills/docker.md +61 -0
  52. package/skills/dom.md +56 -0
  53. package/skills/fuzz.md +167 -0
  54. package/skills/goto.md +111 -0
  55. package/skills/lint.md +123 -0
  56. package/skills/port.md +57 -0
  57. package/skills/profile.md +91 -0
  58. package/skills/race.md +117 -0
  59. package/skills/repl.md +81 -0
  60. package/skills/rules.md +318 -0
  61. package/skills/run.md +135 -0
  62. package/skills/secrets.md +170 -0
  63. package/skills/security.md +360 -0
  64. package/skills/state.md +261 -0
  65. package/skills/vuln.md +57 -0
  66. package/skills/windows.md +320 -0
package/lib/secrets.js ADDED
@@ -0,0 +1,257 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * secrets.js — detect secrets and credentials in code
5
+ *
6
+ * Wrappers gitleaks and trufflehog.
7
+ * Falls back to built-in regex patterns if neither is installed.
8
+ *
9
+ * Run BEFORE committing code with SQL, DB connections, API calls.
10
+ */
11
+
12
+ const { spawnSync, execSync } = require('child_process');
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+ const crypto = require('crypto');
16
+ const { output, ok, fail } = require('./output');
17
+
18
+ const HELP = `
19
+ fchek secrets <path> [--staged] [--full-history]
20
+
21
+ Scan for secrets, credentials, and API keys in code.
22
+ Run BEFORE committing any code that touches DB, APIs, or auth.
23
+
24
+ Tools used (in order of preference):
25
+ 1. gitleaks (apt install gitleaks / brew install gitleaks)
26
+ 2. trufflehog (pip install trufflehog)
27
+ 3. Built-in regex scanner (no install required, less accurate)
28
+
29
+ Options:
30
+ --staged Scan only git staged files (pre-commit check)
31
+ --full-history Scan entire git history (slow, thorough)
32
+
33
+ Examples:
34
+ fchek secrets .
35
+ fchek secrets src/auth.py
36
+ fchek secrets . --staged
37
+ `.trim();
38
+
39
+ function commandExists(cmd) {
40
+ try {
41
+ execSync(process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`, { stdio: 'ignore' });
42
+ return true;
43
+ } catch { return false; }
44
+ }
45
+
46
+ // ─── Built-in regex patterns (fallback) ──────────────────────────────────────
47
+
48
+ const PATTERNS = [
49
+ { name: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/g, severity: 'critical' },
50
+ { name: 'AWS Secret Key', regex: /aws_secret_access_key\s*=\s*["']?([A-Za-z0-9/+=]{40})["']?/gi, severity: 'critical' },
51
+ { name: 'Generic API Key', regex: /(?:api[_-]?key|apikey)\s*[=:]\s*["']([A-Za-z0-9_\-]{20,})["']/gi, severity: 'high' },
52
+ { name: 'Generic Secret', regex: /(?:secret|password|passwd|pwd)\s*[=:]\s*["']([^"']{8,})["']/gi, severity: 'high' },
53
+ { name: 'Private Key Header', regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g, severity: 'critical' },
54
+ { name: 'GitHub Token', regex: /ghp_[A-Za-z0-9]{36}/g, severity: 'critical' },
55
+ { name: 'GitHub OAuth', regex: /gho_[A-Za-z0-9]{36}/g, severity: 'critical' },
56
+ { name: 'Slack Token', regex: /xox[baprs]-[A-Za-z0-9\-]{10,}/g, severity: 'high' },
57
+ { name: 'Stripe Key', regex: /(?:sk|pk)_(?:live|test)_[A-Za-z0-9]{24,}/g, severity: 'critical' },
58
+ { name: 'Generic Bearer Token', regex: /bearer\s+([A-Za-z0-9+/=._\-]{20,})/gi, severity: 'medium' },
59
+ { name: 'Basic Auth in URL', regex: /https?:\/\/[^:]+:[^@]{4,}@/g, severity: 'high' },
60
+ { name: 'Database URL', regex: /(?:postgres(?:ql)?|mysql|mongodb|redis|mssql|sqlite):\/\/[^:@\s]+:[^@\s]{3,}@/gi, severity: 'high' },
61
+ { name: 'Hardcoded IP+Port', regex: /\b(?:\d{1,3}\.){3}\d{1,3}:\d{4,5}\b/g, severity: 'low' },
62
+ { name: 'Hex Secret (32+ chars)',regex: /(?:secret|key|token)\s*[=:]\s*["']?[0-9a-f]{32,}["']?/gi, severity: 'medium' },
63
+ ];
64
+
65
+ const IGNORED_EXTENSIONS = new Set([
66
+ // Images / media / fonts
67
+ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp', '.bmp',
68
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
69
+ '.mp4', '.mp3', '.wav', '.ogg', '.avi', '.mov',
70
+ // Archives / binaries
71
+ '.pdf', '.zip', '.tar', '.gz', '.bz2', '.7z', '.rar', '.xz',
72
+ '.exe', '.dll', '.so', '.dylib', '.lib', '.a', '.o', '.obj', '.pdb',
73
+ // .NET / JVM compiled artifacts
74
+ '.class', '.jar', '.war', '.ear', '.nupkg', '.snupkg',
75
+ '.pdb', '.mdb', '.ilk', '.exp',
76
+ // Lock files and large generated files
77
+ '.lock', '.sum',
78
+ // Database / binary data
79
+ '.db', '.sqlite', '.sqlite3', '.mdb', '.accdb',
80
+ // Misc binary
81
+ '.bin', '.dat', '.cache', '.pyc', '.pyo',
82
+ ]);
83
+
84
+ // Directories to skip — binaries, compiled output, vendor code
85
+ const IGNORED_DIRS = new Set([
86
+ 'node_modules', '.git', 'dist', 'build', 'target', '__pycache__',
87
+ '.venv', 'vendor', 'packages', 'bin', 'obj', // bin/obj = .NET build output
88
+ '.vs', '.idea', '.gradle', 'gradle',
89
+ ]);
90
+
91
+ function scanFileBuiltin(filePath) {
92
+ const ext = path.extname(filePath).toLowerCase();
93
+ if (IGNORED_EXTENSIONS.has(ext)) return [];
94
+
95
+ // Skip large files — binaries often disguise as text but are huge
96
+ try {
97
+ const stat = fs.statSync(filePath);
98
+ if (stat.size > 1_000_000) return []; // skip files > 1MB
99
+ } catch { return []; }
100
+
101
+ let content;
102
+ try { content = fs.readFileSync(filePath, 'utf8'); }
103
+ catch { return []; }
104
+
105
+ // Skip files that look binary (high ratio of non-printable chars in first 512 bytes)
106
+ const sample = content.slice(0, 512);
107
+ const nonPrintable = (sample.match(/[\x00-\x08\x0E-\x1F\x7F-\xFF]/g) || []).length;
108
+ if (nonPrintable / sample.length > 0.1) return [];
109
+
110
+ const findings = [];
111
+ const lines = content.split('\n');
112
+
113
+ for (const pattern of PATTERNS) {
114
+ for (let i = 0; i < lines.length; i++) {
115
+ const line = lines[i];
116
+ const matches = [...line.matchAll(pattern.regex)];
117
+ for (const match of matches) {
118
+ // Redact the actual value — show first 4 chars + hash
119
+ const raw = match[1] || match[0];
120
+ const redacted = raw.slice(0, 4) + '***' + crypto.createHash('md5').update(raw).digest('hex').slice(0, 4);
121
+ findings.push({
122
+ file: filePath,
123
+ line: i + 1,
124
+ pattern: pattern.name,
125
+ severity: pattern.severity,
126
+ match_redacted: redacted,
127
+ });
128
+ }
129
+ }
130
+ }
131
+
132
+ return findings;
133
+ }
134
+
135
+ function scanDirBuiltin(dir) {
136
+ const findings = [];
137
+ function walk(d) {
138
+ let entries;
139
+ try { entries = fs.readdirSync(d); } catch { return; }
140
+ for (const name of entries) {
141
+ if (IGNORED_DIRS.has(name) || name.startsWith('.')) continue;
142
+ const full = path.join(d, name);
143
+ const stat = fs.statSync(full);
144
+ if (stat.isDirectory()) walk(full);
145
+ else if (stat.isFile()) findings.push(...scanFileBuiltin(full));
146
+ }
147
+ }
148
+ walk(dir);
149
+ return findings;
150
+ }
151
+
152
+ // ─── gitleaks ─────────────────────────────────────────────────────────────────
153
+
154
+ function runGitleaks(target, staged, fullHistory) {
155
+ const args = ['detect', '--source', target, '--report-format', 'json', '--no-banner'];
156
+ if (staged) args.push('--staged');
157
+ if (fullHistory) args.unshift('git-log', '--source', target);
158
+
159
+ const res = spawnSync('gitleaks', args, { encoding: 'utf8', timeout: 60_000 });
160
+
161
+ // gitleaks exits 1 when secrets found, 0 when clean
162
+ let findings = [];
163
+ try {
164
+ // Output may contain JSON array
165
+ const raw = res.stdout || res.stderr || '';
166
+ const jsonStart = raw.indexOf('[');
167
+ if (jsonStart !== -1) findings = JSON.parse(raw.slice(jsonStart));
168
+ } catch {}
169
+
170
+ return {
171
+ tool: 'gitleaks',
172
+ findings: findings.map(f => ({
173
+ file: f.File || f.file,
174
+ line: f.StartLine || f.line,
175
+ rule: f.RuleID || f.rule,
176
+ description: f.Description || f.description,
177
+ severity: 'high',
178
+ match_redacted: (f.Secret || '').slice(0, 4) + '***',
179
+ })).slice(0, 50),
180
+ };
181
+ }
182
+
183
+ // ─── trufflehog ───────────────────────────────────────────────────────────────
184
+
185
+ function runTrufflehog(target) {
186
+ const res = spawnSync('trufflehog', ['filesystem', target, '--json', '--no-verification'],
187
+ { encoding: 'utf8', timeout: 120_000 });
188
+
189
+ const findings = [];
190
+ for (const line of (res.stdout || '').split('\n')) {
191
+ try {
192
+ const obj = JSON.parse(line);
193
+ if (obj.DetectorName) {
194
+ findings.push({
195
+ file: obj.SourceMetadata?.Data?.Filesystem?.file || 'unknown',
196
+ line: obj.SourceMetadata?.Data?.Filesystem?.line || null,
197
+ rule: obj.DetectorName,
198
+ description: obj.DetectorDescription || obj.DetectorName,
199
+ severity: 'high',
200
+ match_redacted: (obj.Raw || '').slice(0, 4) + '***',
201
+ });
202
+ }
203
+ } catch {}
204
+ }
205
+ return { tool: 'trufflehog', findings: findings.slice(0, 50) };
206
+ }
207
+
208
+ // ─── Entry point ─────────────────────────────────────────────────────────────
209
+
210
+ async function run(args) {
211
+ if (args.length === 0 || args[0] === '--help') { console.log(HELP); return; }
212
+
213
+ const target = args[0];
214
+ const staged = args.includes('--staged');
215
+ const fullHistory = args.includes('--full-history');
216
+
217
+ if (!fs.existsSync(target)) {
218
+ return output(fail(`Not found: ${target}`));
219
+ }
220
+
221
+ let scanResult;
222
+
223
+ if (commandExists('gitleaks')) {
224
+ scanResult = runGitleaks(target, staged, fullHistory);
225
+ } else if (commandExists('trufflehog')) {
226
+ scanResult = runTrufflehog(target);
227
+ } else {
228
+ // Built-in fallback
229
+ const stat = fs.statSync(target);
230
+ const findings = stat.isDirectory()
231
+ ? scanDirBuiltin(target)
232
+ : scanFileBuiltin(target);
233
+ scanResult = { tool: 'built-in-regex', findings };
234
+ }
235
+
236
+ const critical = scanResult.findings.filter(f => f.severity === 'critical');
237
+ const high = scanResult.findings.filter(f => f.severity === 'high');
238
+ const total = scanResult.findings.length;
239
+
240
+ output(ok({
241
+ target: path.resolve(target),
242
+ tool: scanResult.tool,
243
+ total_findings: total,
244
+ verdict: total === 0 ? 'clean' : critical.length > 0 ? 'critical' : 'issues_found',
245
+ summary: {
246
+ critical: critical.length,
247
+ high: high.length,
248
+ other: total - critical.length - high.length,
249
+ },
250
+ findings: scanResult.findings,
251
+ note: scanResult.tool === 'built-in-regex'
252
+ ? 'Using built-in patterns. Install gitleaks for better coverage: https://github.com/gitleaks/gitleaks'
253
+ : null,
254
+ }));
255
+ }
256
+
257
+ module.exports = { run };
package/lib/state.js ADDED
@@ -0,0 +1,219 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * state.js — persistent project memory across sessions.
5
+ *
6
+ * Stores data in .fcheck_state.json in the nearest project root
7
+ * (where package.json / Cargo.toml / .git lives), NOT in cwd blindly.
8
+ *
9
+ * Difference from repl.js:
10
+ * state = disk-based memory that survives process restarts (long-term context)
11
+ * repl = in-memory live process for interactive use within one session
12
+ *
13
+ * SECURITY NOTE:
14
+ * .fcheck_state.json is added to .gitignore automatically on first write.
15
+ * Never store secrets (tokens, passwords) in state — use env vars for those.
16
+ */
17
+
18
+ const path = require('path');
19
+ const fs = require('fs');
20
+ const crypto = require('crypto');
21
+ const { output, ok, fail } = require('./output');
22
+
23
+ const HELP = `
24
+ fchek state <action> [key] [value]
25
+
26
+ Persistent project memory — survives between sessions (stored on disk).
27
+ Different from REPL: state persists across process restarts.
28
+
29
+ Actions:
30
+ set <key> <value> Save a value
31
+ get <key> Read a value
32
+ list Show entire state
33
+ delete <key> Remove one key
34
+ clear Wipe all state (asks for confirmation unless --yes)
35
+ snapshot Save a snapshot of project file tree + hashes
36
+
37
+ Examples:
38
+ fchek state set task "fix auth bug"
39
+ fchek state set branch "feature/auth"
40
+ fchek state get task
41
+ fchek state list
42
+ fchek state snapshot
43
+ fchek state clear --yes
44
+
45
+ State file location: <project_root>/.fcheck_state.json
46
+ `.trim();
47
+
48
+ const STATE_FILE = '.fcheck_state.json';
49
+
50
+ function findProjectRoot(startDir) {
51
+ const markers = ['package.json', 'Cargo.toml', 'CMakeLists.txt', '.git', 'pyproject.toml', 'go.mod'];
52
+ let dir = path.resolve(startDir);
53
+ for (let i = 0; i < 8; i++) {
54
+ if (markers.some(m => fs.existsSync(path.join(dir, m)))) return dir;
55
+ const parent = path.dirname(dir);
56
+ if (parent === dir) break;
57
+ dir = parent;
58
+ }
59
+ return path.resolve(startDir); // fallback: use cwd
60
+ }
61
+
62
+ function statePath() {
63
+ return path.join(findProjectRoot(process.cwd()), STATE_FILE);
64
+ }
65
+
66
+ function loadState() {
67
+ const p = statePath();
68
+ if (!fs.existsSync(p)) return {};
69
+ try {
70
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
71
+ } catch {
72
+ return {};
73
+ }
74
+ }
75
+
76
+ function saveState(state) {
77
+ const p = statePath();
78
+ fs.writeFileSync(p, JSON.stringify(state, null, 2), 'utf8');
79
+ ensureGitignore(path.dirname(p));
80
+ }
81
+
82
+ function ensureGitignore(dir) {
83
+ const gi = path.join(dir, '.gitignore');
84
+ const entry = '.fcheck_state.json';
85
+ if (!fs.existsSync(gi)) {
86
+ fs.writeFileSync(gi, `${entry}\n`, 'utf8');
87
+ return;
88
+ }
89
+ const content = fs.readFileSync(gi, 'utf8');
90
+ if (!content.includes(entry)) {
91
+ fs.appendFileSync(gi, `\n${entry}\n`, 'utf8');
92
+ }
93
+ }
94
+
95
+ function fileHash(filePath) {
96
+ try {
97
+ const content = fs.readFileSync(filePath);
98
+ return crypto.createHash('sha256').update(content).digest('hex').slice(0, 12);
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ function buildSnapshot(rootDir) {
105
+ const result = {};
106
+ const ignored = new Set(['node_modules', '.git', 'dist', 'build', 'target', '__pycache__']);
107
+
108
+ function walk(dir, prefix = '') {
109
+ let entries;
110
+ try { entries = fs.readdirSync(dir); } catch { return; }
111
+ for (const name of entries) {
112
+ if (ignored.has(name) || name.startsWith('.')) continue;
113
+ const full = path.join(dir, name);
114
+ const rel = prefix ? `${prefix}/${name}` : name;
115
+ const stat = fs.statSync(full);
116
+ if (stat.isDirectory()) {
117
+ walk(full, rel);
118
+ } else if (stat.isFile()) {
119
+ result[rel] = { size: stat.size, hash: fileHash(full), mtime: stat.mtimeMs };
120
+ }
121
+ }
122
+ }
123
+
124
+ walk(rootDir);
125
+ return result;
126
+ }
127
+
128
+ async function run(args) {
129
+ if (args.length === 0 || args[0] === '--help') {
130
+ console.log(HELP);
131
+ return;
132
+ }
133
+
134
+ const action = args[0];
135
+
136
+ switch (action) {
137
+ case 'set': {
138
+ const key = args[1];
139
+ const value = args.slice(2).join(' ');
140
+ if (!key) return output(fail('Usage: fchek state set <key> <value>'));
141
+ const state = loadState();
142
+ state[key] = value;
143
+ state.__updated_at = new Date().toISOString();
144
+ saveState(state);
145
+ output(ok({ action: 'set', key, value, state_file: statePath() }));
146
+ break;
147
+ }
148
+
149
+ case 'get': {
150
+ const key = args[1];
151
+ if (!key) return output(fail('Usage: fchek state get <key>'));
152
+ const state = loadState();
153
+ if (!(key in state)) {
154
+ return output(fail(`Key not found: "${key}"`));
155
+ }
156
+ output(ok({ action: 'get', key, value: state[key] }));
157
+ break;
158
+ }
159
+
160
+ case 'list': {
161
+ const state = loadState();
162
+ output(ok({ action: 'list', state_file: statePath(), state }));
163
+ break;
164
+ }
165
+
166
+ case 'delete': {
167
+ const key = args[1];
168
+ if (!key) return output(fail('Usage: fchek state delete <key>'));
169
+ const state = loadState();
170
+ if (!(key in state)) return output(fail(`Key not found: "${key}"`));
171
+ delete state[key];
172
+ state.__updated_at = new Date().toISOString();
173
+ saveState(state);
174
+ output(ok({ action: 'delete', key, state_file: statePath() }));
175
+ break;
176
+ }
177
+
178
+ case 'clear': {
179
+ const confirmed = args.includes('--yes');
180
+ if (!confirmed) {
181
+ return output(fail(
182
+ 'This will wipe all state. Add --yes to confirm: fchek state clear --yes'
183
+ ));
184
+ }
185
+ const p = statePath();
186
+ if (fs.existsSync(p)) fs.unlinkSync(p);
187
+ output(ok({ action: 'clear', state_file: p }));
188
+ break;
189
+ }
190
+
191
+ case 'snapshot': {
192
+ const rootDir = findProjectRoot(process.cwd());
193
+ const files = buildSnapshot(rootDir);
194
+ const count = Object.keys(files).length;
195
+ const state = loadState();
196
+ state.__snapshot = {
197
+ created_at: new Date().toISOString(),
198
+ root: rootDir,
199
+ file_count: count,
200
+ files,
201
+ };
202
+ saveState(state);
203
+ output(ok({
204
+ action: 'snapshot',
205
+ root: rootDir,
206
+ file_count: count,
207
+ state_file: statePath(),
208
+ }));
209
+ break;
210
+ }
211
+
212
+ default:
213
+ output(fail(
214
+ `Unknown action: "${action}". Valid: set, get, list, delete, clear, snapshot`
215
+ ));
216
+ }
217
+ }
218
+
219
+ module.exports = { run };