clckernel 1.2.18 → 2.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.
- package/.agents/skills/clckernel_cli/SKILL.md +23 -6
- package/.agents/skills/clckernel_cli/reference/audit.md +13 -3
- package/.agents/skills/clckernel_cli/reference/commit.md +1 -1
- package/.agents/skills/clckernel_cli/reference/doctor.md +5 -3
- package/.agents/skills/clckernel_cli/reference/feature.md +3 -3
- package/.agents/skills/clckernel_cli/reference/guard.md +48 -12
- package/.agents/skills/clckernel_cli/reference/phase.md +6 -3
- package/.agents/skills/clckernel_cli/reference/start.md +14 -4
- package/.agents/skills/clckernel_cli/reference/test.md +1 -1
- package/.agents/skills/clckernel_cli/reference/update.md +19 -19
- package/CHANGELOG.md +244 -0
- package/LICENSE +21 -0
- package/README.es.md +280 -51
- package/README.md +274 -50
- package/bin/cli.js +8 -0
- package/package.json +12 -2
- package/src/adapters/agent-os.js +161 -84
- package/src/adapters/astro.js +8 -6
- package/src/adapters/base.js +27 -46
- package/src/adapters/django.js +14 -13
- package/src/adapters/fastapi.js +16 -15
- package/src/adapters/go.js +11 -9
- package/src/adapters/laravel.js +12 -10
- package/src/adapters/nextjs.js +7 -5
- package/src/adapters/rails.js +13 -11
- package/src/adapters/rust.js +11 -9
- package/src/catalog.js +58 -49
- package/src/config.js +38 -240
- package/src/detector.js +243 -44
- package/src/doctor.js +18 -9
- package/src/generator.js +737 -309
- package/src/gitignore.js +144 -0
- package/src/index.js +257 -18
- package/src/io.js +409 -0
- package/src/known-hashes.json +309 -0
- package/src/onboarding.js +15 -0
- package/src/technologies/detector.js +1 -1
- package/src/technologies/guards/_shared.js +1 -1
- package/src/ui.js +233 -40
- package/src/updater.js +271 -36
- package/tools/_changes.js +279 -0
- package/tools/_changes.py +233 -0
- package/tools/_config.js +294 -0
- package/tools/_config.py +271 -0
- package/tools/_yaml.py +489 -0
- package/tools/audit.js +511 -111
- package/tools/audit.py +506 -128
- package/tools/celery_guard.py +13 -35
- package/tools/check_a11y.js +8 -28
- package/tools/check_api_contracts.js +13 -39
- package/tools/check_architecture.js +35 -44
- package/tools/check_architecture.py +172 -105
- package/tools/check_custom.js +108 -61
- package/tools/check_custom.py +108 -271
- package/tools/check_data_files.js +88 -0
- package/tools/check_db_efficiency.py +80 -65
- package/tools/check_hitl.js +112 -69
- package/tools/check_links.js +138 -0
- package/tools/check_migrations.py +143 -94
- package/tools/check_performance.js +35 -44
- package/tools/check_placeholders.js +104 -0
- package/tools/check_responsive.js +8 -28
- package/tools/check_scope.py +40 -31
- package/tools/check_seo.js +8 -28
- package/tools/check_skills.js +158 -0
- package/tools/check_storybook.js +8 -21
- package/tools/check_symlinks.js +40 -22
- package/tools/check_ui_reuse.js +17 -29
- package/tools/docker_guard.py +19 -41
- package/tools/postgres_guard.py +15 -36
- package/tools/redis_guard.py +13 -35
- package/tools/scan_secrets.js +134 -67
- package/tools/scan_secrets.py +163 -72
- package/tools/verify_tdd.js +867 -0
- package/tools/verify_tdd.py +407 -165
- package/tools/yaml.js +404 -132
- package/tools/check_domain_data.js +0 -100
- package/tools/check_test_calendar_sync.js +0 -111
package/tools/postgres_guard.py
CHANGED
|
@@ -9,33 +9,14 @@ from __future__ import annotations
|
|
|
9
9
|
import ast
|
|
10
10
|
import json
|
|
11
11
|
import os
|
|
12
|
-
import subprocess
|
|
13
12
|
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 = [
|
|
31
|
-
f.strip() for f in out.splitlines()
|
|
32
|
-
if f.strip().endswith('.py')
|
|
33
|
-
]
|
|
34
|
-
if files:
|
|
35
|
-
return files
|
|
36
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
37
|
-
continue
|
|
38
|
-
return None
|
|
14
|
+
# Sibling helper shipped next to the guards; works both as a script and when
|
|
15
|
+
# loaded by audit.py through importlib.
|
|
16
|
+
_TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
if _TOOLS_DIR not in sys.path:
|
|
18
|
+
sys.path.insert(0, _TOOLS_DIR)
|
|
19
|
+
import _changes # noqa: E402
|
|
39
20
|
|
|
40
21
|
|
|
41
22
|
class PostgresVisitor(ast.NodeVisitor):
|
|
@@ -85,7 +66,8 @@ class PostgresVisitor(ast.NodeVisitor):
|
|
|
85
66
|
|
|
86
67
|
def visit_Call(self, node: ast.Call):
|
|
87
68
|
# Check for cursor.execute(), connection.execute(), etc.
|
|
88
|
-
|
|
69
|
+
# The method name lives on the callee (node.func), not on the Call node.
|
|
70
|
+
if self._is_sql_method(node.func):
|
|
89
71
|
# Check for raw SQL with string formatting (f-string, %, .format())
|
|
90
72
|
if self._has_formatted_sql(node):
|
|
91
73
|
self._add_violation(
|
|
@@ -138,8 +120,10 @@ def check(target_dir: str | None = None) -> dict:
|
|
|
138
120
|
"""
|
|
139
121
|
dir_path = target_dir or os.getcwd()
|
|
140
122
|
|
|
141
|
-
|
|
142
|
-
|
|
123
|
+
# The snapshot being verified: the index, or HEAD when nothing is staged.
|
|
124
|
+
changes = _changes.changed_files(dir_path, extensions=[".py"])
|
|
125
|
+
mode, files = changes["mode"], changes["files"]
|
|
126
|
+
if mode is None:
|
|
143
127
|
return {
|
|
144
128
|
"exit_code": 0, "skipped": True, "name": "postgres_guard",
|
|
145
129
|
"data": {"findings": [], "message": "No git diff available"},
|
|
@@ -154,17 +138,12 @@ def check(target_dir: str | None = None) -> dict:
|
|
|
154
138
|
all_violations: list[dict] = []
|
|
155
139
|
|
|
156
140
|
for filepath in files:
|
|
157
|
-
|
|
158
|
-
if
|
|
159
|
-
continue
|
|
160
|
-
|
|
161
|
-
try:
|
|
162
|
-
content = full_path.read_text(encoding="utf-8", errors="replace")
|
|
163
|
-
except (OSError, PermissionError):
|
|
141
|
+
content = _changes.read_file(dir_path, filepath, mode)
|
|
142
|
+
if content is None:
|
|
164
143
|
continue
|
|
165
144
|
|
|
166
145
|
try:
|
|
167
|
-
tree = ast.parse(content, filename=
|
|
146
|
+
tree = ast.parse(content, filename=filepath)
|
|
168
147
|
except SyntaxError:
|
|
169
148
|
continue
|
|
170
149
|
|
package/tools/redis_guard.py
CHANGED
|
@@ -9,33 +9,14 @@ from __future__ import annotations
|
|
|
9
9
|
import ast
|
|
10
10
|
import json
|
|
11
11
|
import os
|
|
12
|
-
import subprocess
|
|
13
12
|
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 = [
|
|
31
|
-
f.strip() for f in out.splitlines()
|
|
32
|
-
if f.strip().endswith('.py')
|
|
33
|
-
]
|
|
34
|
-
if files:
|
|
35
|
-
return files
|
|
36
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
37
|
-
continue
|
|
38
|
-
return None
|
|
14
|
+
# Sibling helper shipped next to the guards; works both as a script and when
|
|
15
|
+
# loaded by audit.py through importlib.
|
|
16
|
+
_TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
if _TOOLS_DIR not in sys.path:
|
|
18
|
+
sys.path.insert(0, _TOOLS_DIR)
|
|
19
|
+
import _changes # noqa: E402
|
|
39
20
|
|
|
40
21
|
|
|
41
22
|
class RedisVisitor(ast.NodeVisitor):
|
|
@@ -143,8 +124,10 @@ def check(target_dir: str | None = None) -> dict:
|
|
|
143
124
|
"""
|
|
144
125
|
dir_path = target_dir or os.getcwd()
|
|
145
126
|
|
|
146
|
-
|
|
147
|
-
|
|
127
|
+
# The snapshot being verified: the index, or HEAD when nothing is staged.
|
|
128
|
+
changes = _changes.changed_files(dir_path, extensions=[".py"])
|
|
129
|
+
mode, files = changes["mode"], changes["files"]
|
|
130
|
+
if mode is None:
|
|
148
131
|
return {
|
|
149
132
|
"exit_code": 0, "skipped": True, "name": "redis_guard",
|
|
150
133
|
"data": {"findings": [], "message": "No git diff available"},
|
|
@@ -159,17 +142,12 @@ def check(target_dir: str | None = None) -> dict:
|
|
|
159
142
|
all_violations: list[dict] = []
|
|
160
143
|
|
|
161
144
|
for filepath in files:
|
|
162
|
-
|
|
163
|
-
if
|
|
164
|
-
continue
|
|
165
|
-
|
|
166
|
-
try:
|
|
167
|
-
content = full_path.read_text(encoding="utf-8", errors="replace")
|
|
168
|
-
except (OSError, PermissionError):
|
|
145
|
+
content = _changes.read_file(dir_path, filepath, mode)
|
|
146
|
+
if content is None:
|
|
169
147
|
continue
|
|
170
148
|
|
|
171
149
|
try:
|
|
172
|
-
tree = ast.parse(content, filename=
|
|
150
|
+
tree = ast.parse(content, filename=filepath)
|
|
173
151
|
except SyntaxError:
|
|
174
152
|
continue
|
|
175
153
|
|
package/tools/scan_secrets.js
CHANGED
|
@@ -4,51 +4,146 @@
|
|
|
4
4
|
* Detects hardcoded secrets, API keys, tokens, and private keys in staged/changed files.
|
|
5
5
|
* Self-contained: uses only Node.js built-ins.
|
|
6
6
|
*
|
|
7
|
+
* Rules (sdds/secret-patterns) are tried in order; the first match labels the
|
|
8
|
+
* line (one finding per line). They mirror RULES in scan_secrets.py exactly.
|
|
9
|
+
* The two generic rules skip placeholder values (see isPlaceholder):
|
|
10
|
+
* - Quoted Key Secret: a quoted JSON/YAML key ending in a secret word, with a
|
|
11
|
+
* quoted value of 8+ non-space characters holding a letter and a digit;
|
|
12
|
+
* - Dotenv Secret: in `.env`, `.env.*` and `*.env` files (not `.env.example`,
|
|
13
|
+
* `.env.sample`, `.env.template`), `NAME=value` where a `_` segment of NAME
|
|
14
|
+
* is a secret word and the value is not numeric-only or a boolean/null word.
|
|
15
|
+
*
|
|
7
16
|
* @param {string} targetDir - Directory to scan (defaults to cwd)
|
|
8
17
|
* @returns {{ exitCode: number, skipped: boolean, name: string, data: object }}
|
|
9
18
|
*/
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const { execSync } = require('child_process');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { changedFiles, readFile } = require('./_changes');
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
const SECRET_KEY_WORDS = 'api[_-]?key|apikey|secret[_-]?key|api[_-]?secret|client[_-]?secret|secret|'
|
|
23
|
+
+ 'access[_-]?token|auth[_-]?token|refresh[_-]?token|token|password|passwd|private[_-]?key';
|
|
24
|
+
const DOTENV_SECRET_NAME = /(?:^|_)(?:API_?KEY|KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD|PASS|CREDENTIALS?|DSN|PRIVATE)(?:_|$)/i;
|
|
25
|
+
const PLACEHOLDER_WORDS = ['xxx', 'your', 'changeme', 'change_me', 'change-me', 'placeholder', 'example',
|
|
26
|
+
'dummy', 'replace', 'insert', 'redacted', 'fake', 'sample', 'todo', '...', '***'];
|
|
27
|
+
const NON_SECRET_WORDS = ['true', 'false', 'yes', 'no', 'on', 'off', 'null', 'none'];
|
|
28
|
+
const TEMPLATE_SEGMENTS = ['example', 'sample', 'template'];
|
|
29
|
+
|
|
30
|
+
/** True for values that are obviously not a real secret. */
|
|
31
|
+
function isPlaceholder(value) {
|
|
32
|
+
const v = value.toLowerCase();
|
|
33
|
+
if (v.startsWith('$') || v.includes('${') || v.includes('$(') || v.includes('{{')) return true;
|
|
34
|
+
if (v.includes('<') && v.includes('>')) return true;
|
|
35
|
+
if (/^(.)\1*$/.test(v)) return true;
|
|
36
|
+
return PLACEHOLDER_WORDS.some((w) => v.includes(w));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The value of a dotenv assignment: quotes removed, or an unquoted value without its ` #` comment. */
|
|
40
|
+
function dotenvValue(raw) {
|
|
41
|
+
const quoted = /^(["'])([^\n]*?)\1/.exec(raw);
|
|
42
|
+
if (quoted) return quoted[2];
|
|
43
|
+
const comment = raw.search(/\s#/);
|
|
44
|
+
return (comment === -1 ? raw : raw.slice(0, comment)).trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `.env`, `.env.<suffix>` or `<name>.env`, unless a dot segment marks it as a template. */
|
|
48
|
+
function isDotenvFile(filename) {
|
|
49
|
+
const base = path.basename(filename || '');
|
|
50
|
+
if (!(base === '.env' || base.startsWith('.env.') || (base.endsWith('.env') && base.length > 4))) return false;
|
|
51
|
+
return !base.toLowerCase().split('.').some((seg) => TEMPLATE_SEGMENTS.includes(seg));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const RULES = [
|
|
55
|
+
{ source: '-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', flags: '', label: 'Private Key' },
|
|
56
|
+
{ source: 'AKIA[0-9A-Z]{16}', flags: '', label: 'AWS Access Key' },
|
|
57
|
+
{ source: '(?:api[_-]?key|apikey|secret[_-]?key|api[_-]?secret)\\s*[=:]\\s*[\'"][A-Za-z0-9_\\-]{16,}[\'"]', flags: 'i', label: 'API Key' },
|
|
58
|
+
{ source: '(?:password|passwd|pwd)\\s*[=:]\\s*[\'"][^\'"]{6,}[\'"]', flags: 'i', label: 'Hardcoded Password' },
|
|
59
|
+
{ source: '(?:secret|token|auth[_-]?token|bearer)\\s*[=:]\\s*[\'"][A-Za-z0-9_\\-\\.]{16,}[\'"]', flags: 'i', label: 'Secret/Token' },
|
|
60
|
+
{ source: 'ghp_[A-Za-z0-9]{36}', flags: '', label: 'GitHub Personal Access Token' },
|
|
61
|
+
{ source: 'sk-[A-Za-z0-9]{20,}', flags: '', label: 'OpenAI API Key' },
|
|
62
|
+
{ source: '(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}', flags: '', label: 'Stripe API Key' },
|
|
63
|
+
{ source: 'eyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+', flags: '', label: 'JWT Token' },
|
|
64
|
+
{ source: '(?:SUPABASE|NEXT_PUBLIC|VITE)_\\w*_KEY\\s*=\\s*[\'"][^\'"]{8,}[\'"]', flags: 'i', label: 'Framework Secret Key' },
|
|
65
|
+
{ source: 'sk-ant-[A-Za-z0-9_-]{20,}', flags: '', label: 'Anthropic API Key' },
|
|
66
|
+
{ source: 'sk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,}', flags: '', label: 'OpenAI Project Key' },
|
|
67
|
+
{ source: 'AIza[0-9A-Za-z_-]{35}', flags: '', label: 'Google API Key' },
|
|
68
|
+
{ source: 'xox[abprs]-[0-9]+-[0-9A-Za-z-]{8,}', flags: '', label: 'Slack Token' },
|
|
69
|
+
{ source: 'gh[ousr]_[A-Za-z0-9]{36}', flags: '', label: 'GitHub Token' },
|
|
70
|
+
{ source: 'github_pat_[A-Za-z0-9_]{40,}', flags: '', label: 'GitHub Fine-Grained PAT' },
|
|
71
|
+
{
|
|
72
|
+
source: `(["'])[A-Za-z0-9_.-]*(?:${SECRET_KEY_WORDS})\\1\\s*:\\s*(["'])([^"'\\s]{8,})\\2`,
|
|
73
|
+
flags: 'i',
|
|
74
|
+
label: 'Quoted Key Secret',
|
|
75
|
+
accept: (m) => /[A-Za-z]/.test(m[3]) && /[0-9]/.test(m[3]) && !isPlaceholder(m[3]),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
source: '^\\s*(?:export\\s+)?([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*([^\\n]*)$',
|
|
79
|
+
flags: '',
|
|
80
|
+
label: 'Dotenv Secret',
|
|
81
|
+
dotenvOnly: true,
|
|
82
|
+
accept: (m) => {
|
|
83
|
+
if (!DOTENV_SECRET_NAME.test(m[1])) return false;
|
|
84
|
+
const value = dotenvValue(m[2].trim());
|
|
85
|
+
if (!value || /^[0-9]+$/.test(value) || NON_SECRET_WORDS.includes(value.toLowerCase())) return false;
|
|
86
|
+
return !isPlaceholder(value);
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
].map((r) => ({ ...r, regex: new RegExp(r.source, r.flags) }));
|
|
90
|
+
|
|
91
|
+
const REDACTED = '…[REDACTED]';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The line with every match of every applicable rule cut to its first 4
|
|
95
|
+
* characters plus a marker, trimmed and capped at 120 characters. Filters are
|
|
96
|
+
* ignored here: over-redaction is safe. The full matched value never leaves the guard.
|
|
97
|
+
* @param {string} line
|
|
98
|
+
* @param {{ regex: RegExp }[]} rules
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
function redact(line, rules) {
|
|
102
|
+
let out = line;
|
|
103
|
+
for (const { regex } of rules) {
|
|
104
|
+
const global = new RegExp(regex.source, regex.flags.includes('g') ? regex.flags : regex.flags + 'g');
|
|
105
|
+
out = out.replace(global, (m) => m.slice(0, 4) + REDACTED);
|
|
106
|
+
}
|
|
107
|
+
return out.trim().substring(0, 120);
|
|
108
|
+
}
|
|
16
109
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Pure matcher: the findings for `text` as the content of `filename`.
|
|
112
|
+
* @param {string} text
|
|
113
|
+
* @param {string} filename - repo-relative path; decides whether dotenv rules apply
|
|
114
|
+
* @returns {{ lineNo: number, col: number, label: string, snippet: string }[]}
|
|
115
|
+
*/
|
|
116
|
+
function findSecrets(text, filename) {
|
|
117
|
+
const dotenv = isDotenvFile(filename);
|
|
118
|
+
const rules = RULES.filter((r) => dotenv || !r.dotenvOnly);
|
|
119
|
+
const findings = [];
|
|
120
|
+
const lines = String(text).split('\n');
|
|
121
|
+
for (let i = 0; i < lines.length; i++) {
|
|
122
|
+
const line = lines[i];
|
|
123
|
+
for (const rule of rules) {
|
|
124
|
+
const match = rule.regex.exec(line);
|
|
125
|
+
if (match && (!rule.accept || rule.accept(match))) {
|
|
126
|
+
findings.push({ lineNo: i + 1, col: match.index, label: rule.label, snippet: redact(line, rules) });
|
|
127
|
+
break; // One finding per line
|
|
128
|
+
}
|
|
33
129
|
}
|
|
34
130
|
}
|
|
131
|
+
return findings;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const check = async function scanSecrets(targetDir) {
|
|
135
|
+
const dir = targetDir || process.cwd();
|
|
136
|
+
|
|
137
|
+
// The snapshot being verified: the index, or HEAD when nothing is staged.
|
|
138
|
+
const { mode, files } = changedFiles(dir);
|
|
139
|
+
if (mode === null) {
|
|
140
|
+
return { exitCode: 0, skipped: true, name: 'scan_secrets', data: { findings: {}, message: 'No git diff available' } };
|
|
141
|
+
}
|
|
35
142
|
|
|
36
143
|
if (files.length === 0) {
|
|
37
144
|
return { exitCode: 0, skipped: false, name: 'scan_secrets', data: { findings: {}, message: 'No changed files to scan' } };
|
|
38
145
|
}
|
|
39
146
|
|
|
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
147
|
// Skip binary/generated files
|
|
53
148
|
const skipExtensions = ['.png', '.jpg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.lock', '.min.js', '.min.css'];
|
|
54
149
|
|
|
@@ -57,41 +152,11 @@ const check = async function scanSecrets(targetDir) {
|
|
|
57
152
|
const ext = path.extname(file);
|
|
58
153
|
if (skipExtensions.includes(ext)) continue;
|
|
59
154
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
let content;
|
|
64
|
-
try {
|
|
65
|
-
const buffer = fs.readFileSync(filePath);
|
|
66
|
-
// Skip binary files (check for null byte in first 512 bytes)
|
|
67
|
-
let isBinary = false;
|
|
68
|
-
const checkLen = Math.min(buffer.length, 512);
|
|
69
|
-
for (let b = 0; b < checkLen; b++) {
|
|
70
|
-
if (buffer[b] === 0) {
|
|
71
|
-
isBinary = true;
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (isBinary) continue;
|
|
76
|
-
content = buffer.toString('utf-8');
|
|
77
|
-
} catch {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const lines = content.split('\n');
|
|
82
|
-
const fileFindings = [];
|
|
83
|
-
|
|
84
|
-
for (let i = 0; i < lines.length; i++) {
|
|
85
|
-
const line = lines[i];
|
|
86
|
-
for (const { regex, label } of patterns) {
|
|
87
|
-
if (regex.test(line)) {
|
|
88
|
-
const col = line.search(regex);
|
|
89
|
-
fileFindings.push({ lineNo: i + 1, col, label, line: line.trim().substring(0, 120) });
|
|
90
|
-
break; // One finding per line
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
155
|
+
// Binary or missing in the snapshot reads as null.
|
|
156
|
+
const content = readFile(dir, file, mode);
|
|
157
|
+
if (content === null) continue;
|
|
94
158
|
|
|
159
|
+
const fileFindings = findSecrets(content, file);
|
|
95
160
|
if (fileFindings.length > 0) {
|
|
96
161
|
findings[file] = fileFindings;
|
|
97
162
|
}
|
|
@@ -111,6 +176,8 @@ const check = async function scanSecrets(targetDir) {
|
|
|
111
176
|
};
|
|
112
177
|
};
|
|
113
178
|
module.exports = check;
|
|
179
|
+
module.exports.findSecrets = findSecrets;
|
|
180
|
+
module.exports.RULES = RULES;
|
|
114
181
|
|
|
115
182
|
if (require.main === module) {
|
|
116
183
|
check(process.argv[2]).then(result => {
|