clckernel 1.2.6

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 (71) hide show
  1. package/AGENTS.md +124 -0
  2. package/README.es.md +166 -0
  3. package/README.md +167 -0
  4. package/bin/cli.js +12 -0
  5. package/docs/Journal/001-adr-clckernel-governance.md +18 -0
  6. package/package.json +44 -0
  7. package/src/adapters/astro.js +67 -0
  8. package/src/adapters/base.js +109 -0
  9. package/src/adapters/django.js +70 -0
  10. package/src/adapters/fastapi.js +69 -0
  11. package/src/adapters/go.js +50 -0
  12. package/src/adapters/laravel.js +52 -0
  13. package/src/adapters/nextjs.js +72 -0
  14. package/src/adapters/rails.js +54 -0
  15. package/src/adapters/rust.js +51 -0
  16. package/src/catalog.js +217 -0
  17. package/src/config.js +250 -0
  18. package/src/detector.js +85 -0
  19. package/src/doctor.js +125 -0
  20. package/src/generator.js +473 -0
  21. package/src/index.js +52 -0
  22. package/src/technologies/detector.js +243 -0
  23. package/src/technologies/guards/_shared.js +66 -0
  24. package/src/ui.js +101 -0
  25. package/src/yaml.js +173 -0
  26. package/test/adapters/astro.test.js +102 -0
  27. package/test/adapters/base.test.js +86 -0
  28. package/test/adapters/django.test.js +95 -0
  29. package/test/adapters/fastapi.test.js +88 -0
  30. package/test/adapters/go.test.js +81 -0
  31. package/test/adapters/laravel.test.js +106 -0
  32. package/test/adapters/nextjs.test.js +113 -0
  33. package/test/adapters/rails.test.js +108 -0
  34. package/test/adapters/rust.test.js +83 -0
  35. package/test/catalog.test.js +170 -0
  36. package/test/config.test.js +512 -0
  37. package/test/detector.test.js +345 -0
  38. package/test/doctor.test.js +302 -0
  39. package/test/generator.test.js +419 -0
  40. package/test/guards_new.test.js +95 -0
  41. package/test/helpers.js +36 -0
  42. package/test/integration/cli-flow.test.js +270 -0
  43. package/test/technologies/celery_guard.test.js +185 -0
  44. package/test/technologies/detector.test.js +342 -0
  45. package/test/technologies/docker_guard.test.js +187 -0
  46. package/test/technologies/integration.test.js +123 -0
  47. package/test/technologies/postgres_guard.test.js +87 -0
  48. package/test/technologies/redis_guard.test.js +128 -0
  49. package/tools/audit.js +219 -0
  50. package/tools/audit.py +273 -0
  51. package/tools/celery_guard.py +209 -0
  52. package/tools/check_a11y.js +109 -0
  53. package/tools/check_api_contracts.js +139 -0
  54. package/tools/check_architecture.js +139 -0
  55. package/tools/check_architecture.py +264 -0
  56. package/tools/check_custom.js +163 -0
  57. package/tools/check_custom.py +395 -0
  58. package/tools/check_db_efficiency.py +190 -0
  59. package/tools/check_migrations.py +223 -0
  60. package/tools/check_performance.js +142 -0
  61. package/tools/check_responsive.js +131 -0
  62. package/tools/check_scope.py +180 -0
  63. package/tools/check_seo.js +139 -0
  64. package/tools/check_storybook.js +108 -0
  65. package/tools/check_ui_reuse.js +135 -0
  66. package/tools/docker_guard.py +210 -0
  67. package/tools/postgres_guard.py +192 -0
  68. package/tools/redis_guard.py +197 -0
  69. package/tools/scan_secrets.js +109 -0
  70. package/tools/scan_secrets.py +153 -0
  71. package/tools/verify_tdd.py +212 -0
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Secrets & API Key Scanner
4
+ * Detects hardcoded secrets, API keys, tokens, and private keys in staged/changed files.
5
+ * Self-contained: uses only Node.js built-ins.
6
+ *
7
+ * @param {string} targetDir - Directory to scan (defaults to cwd)
8
+ * @returns {{ exitCode: number, skipped: boolean, name: string, data: object }}
9
+ */
10
+ const check = async function scanSecrets(targetDir) {
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { execSync } = require('child_process');
14
+
15
+ const dir = targetDir || process.cwd();
16
+
17
+ // Collect changed files via git, fall back to package.json check
18
+ let files;
19
+ try {
20
+ const out = execSync('git diff --name-only HEAD~1', {
21
+ cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
22
+ });
23
+ files = out.split('\n').filter(f => f.trim());
24
+ } catch {
25
+ try {
26
+ const out = execSync('git diff --name-only --cached', {
27
+ cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
28
+ });
29
+ files = out.split('\n').filter(f => f.trim());
30
+ } catch {
31
+ // No git history — skip gracefully
32
+ return { exitCode: 0, skipped: true, name: 'scan_secrets', data: { findings: {}, message: 'No git diff available' } };
33
+ }
34
+ }
35
+
36
+ if (files.length === 0) {
37
+ return { exitCode: 0, skipped: false, name: 'scan_secrets', data: { findings: {}, message: 'No changed files to scan' } };
38
+ }
39
+
40
+ const patterns = [
41
+ { regex: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/, label: 'Private Key' },
42
+ { regex: /AKIA[0-9A-Z]{16}/, label: 'AWS Access Key' },
43
+ { regex: /(?:api[_-]?key|apikey|secret[_-]?key|api[_-]?secret)\s*[=:]\s*['"][A-Za-z0-9_\-]{16,}['"]/i, label: 'API Key' },
44
+ { regex: /(?:password|passwd|pwd)\s*[=:]\s*['"][^'"]{6,}['"]/i, label: 'Hardcoded Password' },
45
+ { regex: /(?:secret|token|auth[_-]?token|bearer)\s*[=:]\s*['"][A-Za-z0-9_\-\.]{16,}['"]/i, label: 'Secret/Token' },
46
+ { regex: /ghp_[A-Za-z0-9]{36}/, label: 'GitHub Personal Access Token' },
47
+ { regex: /sk-[A-Za-z0-9]{20,}/, label: 'OpenAI/Stripe API Key' },
48
+ { regex: /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/, label: 'JWT Token' },
49
+ { regex: /(?:SUPABASE|NEXT_PUBLIC|VITE)_\w*_KEY\s*=\s*['"][^'"]{8,}['"]/i, label: 'Framework Secret Key' },
50
+ ];
51
+
52
+ // Skip binary/generated files
53
+ const skipExtensions = ['.png', '.jpg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.lock', '.min.js', '.min.css'];
54
+
55
+ const findings = {};
56
+ for (const file of files) {
57
+ const ext = path.extname(file);
58
+ if (skipExtensions.includes(ext)) continue;
59
+
60
+ const filePath = path.join(dir, file);
61
+ if (!fs.existsSync(filePath)) continue;
62
+
63
+ let content;
64
+ try {
65
+ content = fs.readFileSync(filePath, 'utf-8');
66
+ } catch {
67
+ continue;
68
+ }
69
+
70
+ const lines = content.split('\n');
71
+ const fileFindings = [];
72
+
73
+ for (let i = 0; i < lines.length; i++) {
74
+ const line = lines[i];
75
+ for (const { regex, label } of patterns) {
76
+ if (regex.test(line)) {
77
+ const col = line.search(regex);
78
+ fileFindings.push({ lineNo: i + 1, col, label, line: line.trim().substring(0, 120) });
79
+ break; // One finding per line
80
+ }
81
+ }
82
+ }
83
+
84
+ if (fileFindings.length > 0) {
85
+ findings[file] = fileFindings;
86
+ }
87
+ }
88
+
89
+ const totalCount = Object.values(findings).flat().length;
90
+ return {
91
+ exitCode: totalCount > 0 ? 1 : 0,
92
+ skipped: false,
93
+ name: 'scan_secrets',
94
+ data: {
95
+ findings,
96
+ message: totalCount > 0
97
+ ? `${totalCount} potential secret(s) found in ${Object.keys(findings).length} file(s)`
98
+ : 'No secrets detected',
99
+ },
100
+ };
101
+ };
102
+ module.exports = check;
103
+
104
+ if (require.main === module) {
105
+ check(process.argv[2]).then(result => {
106
+ console.log(JSON.stringify(result, null, 2));
107
+ process.exit(result.exitCode);
108
+ });
109
+ }
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Secrets & API Key Scanner
4
+ Detects hardcoded secrets, API keys, tokens, and private keys in changed files.
5
+ Self-contained: uses only Python stdlib (re, pathlib, subprocess, json).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import pathlib
12
+ import re
13
+ import subprocess
14
+ import sys
15
+
16
+ # One finding per matching line: (line_no, label, matched_snippet)
17
+ PATTERNS: list[tuple[str, str]] = [
18
+ (r"-----BEGIN (?:RSA |EC )?PRIVATE KEY-----", "Private Key"),
19
+ (r"AKIA[0-9A-Z]{16}", "AWS Access Key"),
20
+ (
21
+ r"(?:api[_-]?key|apikey|secret[_-]?key|api[_-]?secret)\s*[=:]\s*['\"]"
22
+ r"[A-Za-z0-9_\-]{16,}['\"]",
23
+ "API Key",
24
+ ),
25
+ (
26
+ r"(?:password|passwd|pwd)\s*[=:]\s*['\"][^'\"]{6,}['\"]",
27
+ "Hardcoded Password",
28
+ ),
29
+ (
30
+ r"(?:secret|token|auth[_-]?token|bearer)\s*[=:]\s*['\"]"
31
+ r"[A-Za-z0-9_\-\.]{16,}['\"]",
32
+ "Secret/Token",
33
+ ),
34
+ (r"ghp_[A-Za-z0-9]{36}", "GitHub Personal Access Token"),
35
+ (r"sk-[A-Za-z0-9]{20,}", "OpenAI/Stripe API Key"),
36
+ (r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+", "JWT Token"),
37
+ (
38
+ r"(?:SUPABASE|NEXT_PUBLIC|VITE)_\w*_KEY\s*=\s*['\"][^'\"]{8,}['\"]",
39
+ "Framework Secret Key",
40
+ ),
41
+ ]
42
+
43
+ SKIP_EXTENSIONS = frozenset(
44
+ {
45
+ ".png", ".jpg", ".gif", ".svg", ".ico",
46
+ ".woff", ".woff2", ".ttf", ".lock",
47
+ ".min.js", ".min.css",
48
+ }
49
+ )
50
+
51
+
52
+ def _get_changed_files(target_dir: str) -> list[str] | None:
53
+ """Return list of changed files from git diff, or None on failure."""
54
+ for ref in ("HEAD~1", "--cached"):
55
+ try:
56
+ args = ["git", "diff", "--name-only"]
57
+ if ref == "--cached":
58
+ args.append("--cached")
59
+ else:
60
+ args.append(ref)
61
+ out = subprocess.check_output(
62
+ args, cwd=target_dir,
63
+ stderr=subprocess.DEVNULL, text=True,
64
+ )
65
+ files = [f.strip() for f in out.splitlines() if f.strip()]
66
+ if files:
67
+ return files
68
+ except (subprocess.CalledProcessError, FileNotFoundError):
69
+ continue
70
+ return None
71
+
72
+
73
+ def _compile_patterns() -> list[tuple[re.Pattern, str]]:
74
+ """Compile all secret-detection patterns once."""
75
+ return [(re.compile(p), label) for p, label in PATTERNS]
76
+
77
+
78
+ def check(target_dir: str | None = None) -> dict:
79
+ """
80
+ Scan changed files for secrets and API keys.
81
+
82
+ Returns:
83
+ dict with keys: exit_code, skipped, name, data
84
+ data.findings maps filepath -> list of {lineNo, label, snippet}
85
+ """
86
+ dir_path = target_dir or os.getcwd()
87
+
88
+ files = _get_changed_files(dir_path)
89
+ if files is None:
90
+ return {
91
+ "exit_code": 0, "skipped": True, "name": "scan_secrets",
92
+ "data": {"findings": {}, "message": "No git diff available"},
93
+ }
94
+
95
+ if not files:
96
+ return {
97
+ "exit_code": 0, "skipped": False, "name": "scan_secrets",
98
+ "data": {"findings": {}, "message": "No changed files to scan"},
99
+ }
100
+
101
+ compiled = _compile_patterns()
102
+ findings: dict[str, list[dict]] = {}
103
+
104
+ for filepath in files:
105
+ ext = pathlib.Path(filepath).suffix
106
+ if ext in SKIP_EXTENSIONS:
107
+ continue
108
+
109
+ full_path = pathlib.Path(dir_path) / filepath
110
+ if not full_path.exists():
111
+ continue
112
+
113
+ try:
114
+ content = full_path.read_text(encoding="utf-8", errors="replace")
115
+ except (OSError, PermissionError):
116
+ continue
117
+
118
+ file_hits: list[dict] = []
119
+ for line_no, line in enumerate(content.splitlines(), start=1):
120
+ for regex, label in compiled:
121
+ m = regex.search(line)
122
+ if m:
123
+ file_hits.append({
124
+ "lineNo": line_no,
125
+ "label": label,
126
+ "snippet": line.strip()[:120],
127
+ })
128
+ break # one finding per line
129
+
130
+ if file_hits:
131
+ findings[filepath] = file_hits
132
+
133
+ total = sum(len(v) for v in findings.values())
134
+ if total:
135
+ msg = (
136
+ f"{total} potential secret(s) found in "
137
+ f"{len(findings)} file(s)"
138
+ )
139
+ else:
140
+ msg = "No secrets detected"
141
+
142
+ return {
143
+ "exit_code": 1 if total else 0,
144
+ "skipped": False,
145
+ "name": "scan_secrets",
146
+ "data": {"findings": findings, "message": msg},
147
+ }
148
+
149
+
150
+ if __name__ == "__main__":
151
+ result = check(sys.argv[1] if len(sys.argv) > 1 else None)
152
+ print(json.dumps(result, indent=2))
153
+ sys.exit(result["exit_code"])
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ TDD Flow Validator
4
+ Validates that test files exist for changed code, checks for test patterns,
5
+ and reports on test coverage presence (pytest, unittest, manage.py test).
6
+ Self-contained: uses only Python stdlib (pathlib, subprocess, json).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import pathlib
13
+ import subprocess
14
+ import sys
15
+
16
+
17
+ def _get_changed_files(target_dir: str) -> list[str] | None:
18
+ """Return list of changed files from git diff, or None on failure."""
19
+ for ref in ("HEAD~1", "--cached"):
20
+ try:
21
+ args = ["git", "diff", "--name-only"]
22
+ if ref == "--cached":
23
+ args.append("--cached")
24
+ else:
25
+ args.append(ref)
26
+ out = subprocess.check_output(
27
+ args, cwd=target_dir,
28
+ stderr=subprocess.DEVNULL, text=True,
29
+ )
30
+ files = [f.strip() for f in out.splitlines() if f.strip()]
31
+ if files:
32
+ return files
33
+ except (subprocess.CalledProcessError, FileNotFoundError):
34
+ continue
35
+ return None
36
+
37
+
38
+ def _detect_test_framework(target_dir: str) -> str:
39
+ """Detect which test framework is in use."""
40
+ root = pathlib.Path(target_dir)
41
+
42
+ # Check for pytest.ini or pytest config
43
+ for name in ("pytest.ini", "setup.cfg", "pyproject.toml"):
44
+ fpath = root / name
45
+ if fpath.exists():
46
+ try:
47
+ text = fpath.read_text(encoding="utf-8", errors="replace").lower()
48
+ except OSError:
49
+ continue
50
+ if "pytest" in text:
51
+ return "pytest"
52
+
53
+ # Check for Django manage.py test
54
+ if (root / "manage.py").exists():
55
+ return "django"
56
+
57
+ # Check for unittest patterns
58
+ for py_file in root.rglob("test_*.py"):
59
+ try:
60
+ content = py_file.read_text(encoding="utf-8", errors="replace")
61
+ if "import unittest" in content or "unittest.TestCase" in content:
62
+ return "unittest"
63
+ except OSError:
64
+ continue
65
+
66
+ return "unknown"
67
+
68
+
69
+ def _find_test_files(target_dir: str) -> list[str]:
70
+ """Find all test files in the project."""
71
+ root = pathlib.Path(target_dir)
72
+ test_files: list[str] = []
73
+
74
+ for py_file in root.rglob("test_*.py"):
75
+ rel = str(py_file.relative_to(root))
76
+ if "__pycache__" not in rel:
77
+ test_files.append(rel)
78
+
79
+ for py_file in root.rglob("*_test.py"):
80
+ rel = str(py_file.relative_to(root))
81
+ if "__pycache__" not in rel and rel not in test_files:
82
+ test_files.append(rel)
83
+
84
+ # Check tests/ directories
85
+ for tests_dir in root.rglob("tests"):
86
+ if tests_dir.is_dir():
87
+ for py_file in tests_dir.glob("*.py"):
88
+ rel = str(py_file.relative_to(root))
89
+ if "__pycache__" not in rel and py_file.name != "__init__.py" and rel not in test_files:
90
+ test_files.append(rel)
91
+
92
+ return sorted(test_files)
93
+
94
+
95
+ def _count_test_methods(target_dir: str, filepath: str) -> int:
96
+ """Count test methods in a test file."""
97
+ full_path = pathlib.Path(target_dir) / filepath
98
+ try:
99
+ content = full_path.read_text(encoding="utf-8", errors="replace")
100
+ except OSError:
101
+ return 0
102
+
103
+ import re
104
+ # Count test_ method definitions and TestCase subclasses
105
+ test_funcs = len(re.findall(r"def (test_\w+)", content))
106
+ return test_funcs
107
+
108
+
109
+ def check(target_dir: str | None = None) -> dict:
110
+ """
111
+ Validate TDD flow: tests exist, test framework detected, coverage signals.
112
+
113
+ Returns:
114
+ dict with keys: exit_code, skipped, name, data
115
+ data.framework, data.test_files, data.changed_code_files,
116
+ data.test_count, data.has_test_coverage
117
+ """
118
+ dir_path = target_dir or os.getcwd()
119
+
120
+ changed = _get_changed_files(dir_path)
121
+ if changed is None:
122
+ return {
123
+ "exit_code": 0, "skipped": True, "name": "verify_tdd",
124
+ "data": {"message": "No git diff available"},
125
+ }
126
+
127
+ if not changed:
128
+ return {
129
+ "exit_code": 0, "skipped": False, "name": "verify_tdd",
130
+ "data": {
131
+ "framework": "unknown", "test_files": [],
132
+ "changed_code_files": [], "test_count": 0,
133
+ "has_test_coverage": False,
134
+ "message": "No changed files",
135
+ },
136
+ }
137
+
138
+ framework = _detect_test_framework(dir_path)
139
+ test_files = _find_test_files(dir_path)
140
+
141
+ # Separate test files from code files in the diff
142
+ changed_tests = [f for f in changed if f.endswith(".py") and _is_test_file(f)]
143
+ changed_code = [f for f in changed if f.endswith(".py") and not _is_test_file(f)]
144
+
145
+ # Count total test methods
146
+ total_tests = sum(_count_test_methods(dir_path, tf) for tf in test_files)
147
+
148
+ # Determine if there's meaningful test coverage
149
+ has_tests = len(test_files) > 0
150
+ has_test_in_diff = len(changed_tests) > 0
151
+ has_code_in_diff = len(changed_code) > 0
152
+
153
+ # TDD indicator: if code changed but no test changed, flag it
154
+ if has_code_in_diff and not has_test_in_diff and has_tests:
155
+ msg = (
156
+ f"Code files changed without corresponding test changes "
157
+ f"({len(changed_code)} code files, {len(changed_tests)} test files). "
158
+ f"Framework: {framework}. Total test methods: {total_tests}"
159
+ )
160
+ exit_code = 1
161
+ elif has_code_in_diff and not has_test_in_diff and not has_tests:
162
+ msg = (
163
+ f"No test files found in project. "
164
+ f"{len(changed_code)} code file(s) changed without tests. "
165
+ f"Framework: {framework}"
166
+ )
167
+ exit_code = 1
168
+ elif has_test_in_diff:
169
+ msg = (
170
+ f"TDD flow detected: {len(changed_tests)} test file(s) changed "
171
+ f"alongside {len(changed_code)} code file(s). "
172
+ f"Framework: {framework}. Total test methods: {total_tests}"
173
+ )
174
+ exit_code = 0
175
+ else:
176
+ msg = (
177
+ f"No Python code changes detected. "
178
+ f"Framework: {framework}. Total test methods: {total_tests}"
179
+ )
180
+ exit_code = 0
181
+
182
+ return {
183
+ "exit_code": exit_code,
184
+ "skipped": False,
185
+ "name": "verify_tdd",
186
+ "data": {
187
+ "framework": framework,
188
+ "test_files": test_files[:20], # Cap at 20 for readability
189
+ "changed_code_files": changed_code,
190
+ "changed_test_files": changed_tests,
191
+ "test_count": total_tests,
192
+ "has_test_coverage": has_tests,
193
+ "message": msg,
194
+ },
195
+ }
196
+
197
+
198
+ def _is_test_file(filepath: str) -> bool:
199
+ """Check if a filepath is a test file."""
200
+ basename = pathlib.Path(filepath).name
201
+ return (
202
+ basename.startswith("test_")
203
+ or basename.endswith("_test.py")
204
+ or "/tests/" in filepath
205
+ or "/test/" in filepath
206
+ )
207
+
208
+
209
+ if __name__ == "__main__":
210
+ result = check(sys.argv[1] if len(sys.argv) > 1 else None)
211
+ print(json.dumps(result, indent=2))
212
+ sys.exit(result["exit_code"])