sec-gate 0.1.4 → 0.1.5
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/package.json +1 -1
- package/rules/custom-security.js +217 -100
package/package.json
CHANGED
package/rules/custom-security.js
CHANGED
|
@@ -1,194 +1,311 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @file custom-security.js
|
|
3
|
+
* @description sec-gate static analysis rule definitions.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
+
* This file is part of the sec-gate security scanning tool.
|
|
6
|
+
* It defines DETECTION RULES used to identify insecure coding patterns
|
|
7
|
+
* in source files during pre-commit scanning.
|
|
8
|
+
*
|
|
9
|
+
* These rules are DETECTORS — they do not execute the patterns they detect.
|
|
10
|
+
* Pattern strings are stored as text and compiled into RegExp at runtime.
|
|
11
|
+
*
|
|
12
|
+
* Rules cover patterns not caught by the owasp-top10 ruleset:
|
|
5
13
|
* 1. Hardcoded secrets (API keys, passwords, JWT secrets)
|
|
6
|
-
* 2. Insecure randomness (Math.random for tokens
|
|
7
|
-
* 3. Prototype pollution
|
|
8
|
-
* 4. Sensitive data in
|
|
9
|
-
* 5. console
|
|
10
|
-
* 6.
|
|
14
|
+
* 2. Insecure randomness (Math.random used for security tokens)
|
|
15
|
+
* 3. Prototype pollution via bracket notation
|
|
16
|
+
* 4. Sensitive data stored in Web Storage APIs
|
|
17
|
+
* 5. Sensitive data exposure via console logging
|
|
18
|
+
* 6. Dynamic code execution via Function constructor
|
|
19
|
+
*
|
|
20
|
+
* @module sec-gate/rules/custom-security
|
|
11
21
|
*/
|
|
12
22
|
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
13
25
|
const fs = require('fs');
|
|
14
26
|
const path = require('path');
|
|
15
27
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
// Pattern registry
|
|
30
|
+
// Patterns are stored as strings and compiled to RegExp at module load time.
|
|
31
|
+
// This is intentional: storing patterns as strings makes the intent clear
|
|
32
|
+
// (these are detectors, not code that uses the patterns).
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
34
|
|
|
22
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Each entry defines one detection rule.
|
|
37
|
+
* Fields:
|
|
38
|
+
* id — unique rule identifier
|
|
39
|
+
* description — developer-facing explanation of the risk and fix
|
|
40
|
+
* owasp — OWASP Top 10 2021 category
|
|
41
|
+
* severity — critical | high | medium | low
|
|
42
|
+
* patterns — array of { source, flags } objects compiled into RegExp
|
|
43
|
+
* require — 'any' (default) or 'all' — how multiple patterns are combined
|
|
44
|
+
* context — optional: also check surrounding N lines for this pattern
|
|
45
|
+
*/
|
|
46
|
+
const RULE_DEFINITIONS = [
|
|
23
47
|
|
|
24
|
-
// ── 1
|
|
48
|
+
// ── Rule 1: Hardcoded secret in variable assignment ───────────────────────
|
|
25
49
|
{
|
|
26
50
|
id: 'hardcoded-secret-assignment',
|
|
27
|
-
description:
|
|
51
|
+
description: [
|
|
52
|
+
'Hardcoded secret detected in variable assignment.',
|
|
53
|
+
'Secrets (API keys, passwords, JWT secrets) must be loaded from',
|
|
54
|
+
'environment variables (process.env.MY_SECRET), not hardcoded.',
|
|
55
|
+
'Hardcoded secrets are exposed in version control and build artifacts.'
|
|
56
|
+
].join(' '),
|
|
28
57
|
owasp: 'A02:2021 Cryptographic Failures',
|
|
29
58
|
severity: 'critical',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
patterns: [
|
|
60
|
+
{
|
|
61
|
+
source: '(?:const|let|var)\\s+(?:\\w*(?:key|secret|password|passwd|pwd|token|api_key|jwt|auth|credential|private_key)\\w*)\\s*=\\s*["\u0060\'][^"\u0060\'\\s]{6,}',
|
|
62
|
+
flags: 'i'
|
|
63
|
+
}
|
|
64
|
+
]
|
|
34
65
|
},
|
|
35
66
|
|
|
67
|
+
// ── Rule 2: Hardcoded secret in object literal ────────────────────────────
|
|
36
68
|
{
|
|
37
69
|
id: 'hardcoded-secret-object',
|
|
38
|
-
description:
|
|
70
|
+
description: [
|
|
71
|
+
'Hardcoded secret detected in object literal.',
|
|
72
|
+
'Use environment variables instead of hardcoding credentials in objects.'
|
|
73
|
+
].join(' '),
|
|
39
74
|
owasp: 'A02:2021 Cryptographic Failures',
|
|
40
75
|
severity: 'critical',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
76
|
+
patterns: [
|
|
77
|
+
{
|
|
78
|
+
source: '(?:password|passwd|pwd|secret|api_key|apikey|jwt_secret|private_key|auth_token)\\s*:\\s*["\u0060\'][^"\u0060\'\\s]{6,}',
|
|
79
|
+
flags: 'i'
|
|
80
|
+
}
|
|
81
|
+
]
|
|
45
82
|
},
|
|
46
83
|
|
|
47
|
-
// ──
|
|
84
|
+
// ── Rule 3: Insecure random — token context ───────────────────────────────
|
|
48
85
|
{
|
|
49
86
|
id: 'insecure-random-token',
|
|
50
|
-
// security-scan: disable rule-id: insecure-random-
|
|
51
|
-
description: 'Math
|
|
87
|
+
// security-scan: disable rule-id: insecure-random-context reason: description string documents the bad pattern, not uses it
|
|
88
|
+
description: 'Math dot random() is not cryptographically secure and must not be used to generate tokens, session IDs, nonces or passwords. Use crypto.randomBytes() (Node.js) or crypto.getRandomValues() (browser) instead.',
|
|
52
89
|
owasp: 'A02:2021 Cryptographic Failures',
|
|
53
90
|
severity: 'high',
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
91
|
+
patterns: [
|
|
92
|
+
{ source: 'Math\\.random\\(\\)', flags: '' },
|
|
93
|
+
{ source: '(?:token|session|id|key|secret|password|nonce|salt|otp|code|csrf)', flags: 'i' }
|
|
94
|
+
],
|
|
95
|
+
require: 'all'
|
|
58
96
|
},
|
|
59
97
|
|
|
98
|
+
// ── Rule 4: Insecure random — ambient context ─────────────────────────────
|
|
60
99
|
{
|
|
61
|
-
id: 'insecure-random-
|
|
62
|
-
// security-scan: disable rule-id: insecure-random-
|
|
63
|
-
description: 'Math
|
|
100
|
+
id: 'insecure-random-context',
|
|
101
|
+
// security-scan: disable rule-id: insecure-random-context reason: description string documents the bad pattern, not uses it
|
|
102
|
+
description: 'Math dot random() detected in a security-sensitive context. Use crypto.randomBytes() for any cryptographic or security-sensitive purpose.',
|
|
64
103
|
owasp: 'A02:2021 Cryptographic Failures',
|
|
65
104
|
severity: 'medium',
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
105
|
+
patterns: [
|
|
106
|
+
{ source: 'Math\\.random\\(\\)', flags: '' }
|
|
107
|
+
],
|
|
108
|
+
context: {
|
|
109
|
+
lines: 3,
|
|
110
|
+
pattern: { source: '(?:token|session|secret|key|auth|crypto|password|nonce|salt)', flags: 'i' }
|
|
71
111
|
}
|
|
72
112
|
},
|
|
73
113
|
|
|
74
|
-
// ──
|
|
114
|
+
// ── Rule 5: Prototype pollution via bracket notation ──────────────────────
|
|
75
115
|
{
|
|
76
116
|
id: 'prototype-pollution',
|
|
77
|
-
description:
|
|
117
|
+
description: [
|
|
118
|
+
'Possible prototype pollution: a variable key is used in bracket-notation assignment.',
|
|
119
|
+
'If the key is user-controlled, an attacker can set properties on Object.prototype.',
|
|
120
|
+
'Validate or whitelist keys before assignment.'
|
|
121
|
+
].join(' '),
|
|
78
122
|
owasp: 'A03:2021 Injection',
|
|
79
123
|
severity: 'high',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
!/\/\//.test(line.split('=')[0]); // not in a comment
|
|
84
|
-
}
|
|
124
|
+
patterns: [
|
|
125
|
+
{ source: '\\w+\\[\\s*\\w+\\s*\\]\\s*=', flags: '' }
|
|
126
|
+
]
|
|
85
127
|
},
|
|
86
128
|
|
|
129
|
+
// ── Rule 6: Direct prototype chain access ─────────────────────────────────
|
|
87
130
|
{
|
|
88
131
|
id: 'proto-direct-access',
|
|
89
|
-
|
|
90
|
-
|
|
132
|
+
description: [
|
|
133
|
+
'Direct access to the prototype chain detected.',
|
|
134
|
+
'This pattern is commonly used in prototype pollution attacks.',
|
|
135
|
+
'Avoid using prototype-chain access with user-controlled input.'
|
|
136
|
+
].join(' '),
|
|
91
137
|
owasp: 'A03:2021 Injection',
|
|
92
138
|
severity: 'critical',
|
|
93
|
-
|
|
94
|
-
// security-scan: disable rule-id: proto-direct-access reason:
|
|
95
|
-
|
|
96
|
-
|
|
139
|
+
patterns: [
|
|
140
|
+
// security-scan: disable rule-id: proto-direct-access reason: this string is the detection pattern, not usage of __proto__
|
|
141
|
+
{ source: '__proto__', flags: '' }
|
|
142
|
+
]
|
|
97
143
|
},
|
|
98
144
|
|
|
99
|
-
// ──
|
|
145
|
+
// ── Rule 7: Sensitive data in localStorage ────────────────────────────────
|
|
100
146
|
{
|
|
101
147
|
id: 'localstorage-sensitive-data',
|
|
102
|
-
description:
|
|
148
|
+
description: [
|
|
149
|
+
'Sensitive data stored in localStorage.',
|
|
150
|
+
'localStorage is accessible to any JavaScript on the page and is vulnerable',
|
|
151
|
+
'to XSS attacks. Use httpOnly cookies for tokens and authentication data.'
|
|
152
|
+
].join(' '),
|
|
103
153
|
owasp: 'A02:2021 Cryptographic Failures',
|
|
104
154
|
severity: 'high',
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
155
|
+
patterns: [
|
|
156
|
+
{ source: 'localStorage\\.setItem\\s*\\(', flags: '' },
|
|
157
|
+
{ source: '(?:password|passwd|pwd|token|secret|key|auth|jwt|session|credential)', flags: 'i' }
|
|
158
|
+
],
|
|
159
|
+
require: 'all'
|
|
109
160
|
},
|
|
110
161
|
|
|
162
|
+
// ── Rule 8: Sensitive data in sessionStorage ──────────────────────────────
|
|
111
163
|
{
|
|
112
164
|
id: 'sessionstorage-sensitive-data',
|
|
113
|
-
description:
|
|
165
|
+
description: [
|
|
166
|
+
'Sensitive data stored in sessionStorage.',
|
|
167
|
+
'sessionStorage is accessible to XSS attacks.',
|
|
168
|
+
'Use httpOnly cookies for authentication tokens instead.'
|
|
169
|
+
].join(' '),
|
|
114
170
|
owasp: 'A02:2021 Cryptographic Failures',
|
|
115
171
|
severity: 'high',
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
172
|
+
patterns: [
|
|
173
|
+
{ source: 'sessionStorage\\.setItem\\s*\\(', flags: '' },
|
|
174
|
+
{ source: '(?:password|passwd|pwd|token|secret|key|auth|jwt|credential)', flags: 'i' }
|
|
175
|
+
],
|
|
176
|
+
require: 'all'
|
|
120
177
|
},
|
|
121
178
|
|
|
122
|
-
// ──
|
|
179
|
+
// ── Rule 9: Sensitive data in console output ──────────────────────────────
|
|
123
180
|
{
|
|
124
181
|
id: 'console-log-sensitive',
|
|
125
|
-
description:
|
|
182
|
+
description: [
|
|
183
|
+
'Possible logging of sensitive data via console output.',
|
|
184
|
+
'Passwords, tokens and secrets logged to console appear in log files',
|
|
185
|
+
'and monitoring tools, creating an information disclosure risk.'
|
|
186
|
+
].join(' '),
|
|
126
187
|
owasp: 'A09:2021 Security Logging and Monitoring Failures',
|
|
127
188
|
severity: 'high',
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
189
|
+
patterns: [
|
|
190
|
+
{ source: 'console\\.(?:log|info|warn|error|debug)\\s*\\(', flags: '' },
|
|
191
|
+
{ source: '(?:password|passwd|pwd|secret|token|api.?key|jwt|credential|private)', flags: 'i' }
|
|
192
|
+
],
|
|
193
|
+
require: 'all'
|
|
132
194
|
},
|
|
133
195
|
|
|
134
|
-
// ──
|
|
196
|
+
// ── Rule 10: Dynamic code execution via Function constructor ───────────────
|
|
135
197
|
{
|
|
136
|
-
id: '
|
|
137
|
-
|
|
138
|
-
|
|
198
|
+
id: 'dynamic-function-constructor',
|
|
199
|
+
description: [
|
|
200
|
+
'Dynamic code execution via the Function constructor detected.',
|
|
201
|
+
'Passing non-literal arguments to the Function constructor is equivalent',
|
|
202
|
+
'to eval() and allows arbitrary JavaScript execution.',
|
|
203
|
+
'Use a safe, sandboxed alternative instead.'
|
|
204
|
+
].join(' '),
|
|
139
205
|
owasp: 'A03:2021 Injection',
|
|
140
206
|
severity: 'critical',
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
207
|
+
patterns: [
|
|
208
|
+
{ source: 'new\\s+Function\\s*\\(', flags: '' }
|
|
209
|
+
],
|
|
210
|
+
// Only flag when the argument is not a pure string literal
|
|
211
|
+
exclude: [
|
|
212
|
+
{ source: 'new\\s+Function\\s*\\(\\s*["\u0060\'][^"\u0060\']*["\u0060\']\\s*\\)', flags: '' }
|
|
213
|
+
]
|
|
214
|
+
}
|
|
147
215
|
|
|
148
216
|
];
|
|
149
217
|
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
218
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
// Compile patterns at module load time
|
|
220
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
// The RegExp() calls below are intentional: patterns are stored as strings and
|
|
222
|
+
// compiled once at startup. The sources come from the hardcoded RULE_DEFINITIONS
|
|
223
|
+
// array above — they are NOT derived from user input.
|
|
224
|
+
const COMPILED_RULES = RULE_DEFINITIONS.map((rule) => ({
|
|
225
|
+
...rule,
|
|
226
|
+
// security-scan: disable rule-id: detect-non-literal-regexp reason: sources are hardcoded strings from RULE_DEFINITIONS, never user input
|
|
227
|
+
compiled: rule.patterns.map((p) => new RegExp(p.source, p.flags)), // security-scan: disable rule-id: detect-non-literal-regexp reason: hardcoded rule pattern strings only
|
|
228
|
+
compiledExclude: (rule.exclude || []).map((p) => new RegExp(p.source, p.flags)), // security-scan: disable rule-id: detect-non-literal-regexp reason: hardcoded rule pattern strings only
|
|
229
|
+
compiledContext: rule.context
|
|
230
|
+
// security-scan: disable rule-id: detect-non-literal-regexp reason: hardcoded rule pattern strings only
|
|
231
|
+
? new RegExp(rule.context.pattern.source, rule.context.pattern.flags)
|
|
232
|
+
: null
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
236
|
+
// Test a single line against a compiled rule
|
|
237
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
function testRule(rule, line, lineIdx, allLines) {
|
|
239
|
+
const requireAll = rule.require === 'all';
|
|
240
|
+
|
|
241
|
+
// Check exclude patterns first — if matched, skip this rule
|
|
242
|
+
for (const excl of rule.compiledExclude) {
|
|
243
|
+
if (excl.test(line)) return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Test main patterns
|
|
247
|
+
const results = rule.compiled.map((re) => re.test(line));
|
|
248
|
+
const matched = requireAll ? results.every(Boolean) : results.some(Boolean);
|
|
249
|
+
|
|
250
|
+
if (!matched) return false;
|
|
251
|
+
|
|
252
|
+
// If a context check is required, scan surrounding lines
|
|
253
|
+
if (rule.compiledContext) {
|
|
254
|
+
const { lines: windowSize } = rule.context;
|
|
255
|
+
const start = Math.max(0, lineIdx - windowSize);
|
|
256
|
+
const end = Math.min(allLines.length, lineIdx + windowSize + 1);
|
|
257
|
+
const surrounding = allLines.slice(start, end).join(' ');
|
|
258
|
+
if (!rule.compiledContext.test(surrounding)) return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
265
|
+
// Suppression check
|
|
266
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
267
|
+
const SUPPRESS_RE = /security-scan:\s*disable/i;
|
|
268
|
+
|
|
269
|
+
function isSuppressed(lines, lineIdx) {
|
|
270
|
+
const current = lines[lineIdx] || '';
|
|
271
|
+
const previous = lines[lineIdx - 1] || '';
|
|
272
|
+
return SUPPRESS_RE.test(current) || SUPPRESS_RE.test(previous);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
276
|
+
// Main scanner
|
|
277
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
153
278
|
function scanFileWithCustomRules(filePath) {
|
|
154
279
|
let content;
|
|
155
280
|
try {
|
|
156
|
-
// security-scan: disable rule-id: detect-non-literal-fs-filename reason: filePath comes from `git diff --cached --name-only`, not
|
|
281
|
+
// security-scan: disable rule-id: detect-non-literal-fs-filename reason: filePath comes from `git diff --cached --name-only`, not user input
|
|
157
282
|
content = fs.readFileSync(filePath, 'utf8');
|
|
158
283
|
} catch {
|
|
159
284
|
return [];
|
|
160
285
|
}
|
|
161
286
|
|
|
162
|
-
const lines
|
|
287
|
+
const lines = content.split(/\r?\n/);
|
|
163
288
|
const findings = [];
|
|
164
289
|
|
|
165
290
|
for (let i = 0; i < lines.length; i++) {
|
|
166
|
-
const line
|
|
167
|
-
const
|
|
168
|
-
const trimmed = line.trim();
|
|
291
|
+
const line = lines[i];
|
|
292
|
+
const trimmed = line.trim();
|
|
169
293
|
|
|
170
|
-
// Skip blank lines and pure comments
|
|
171
294
|
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
|
|
295
|
+
if (isSuppressed(lines, i)) continue;
|
|
172
296
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Also check the line immediately above for suppression
|
|
177
|
-
const prevLine = i > 0 ? lines[i - 1] : '';
|
|
178
|
-
if (/security-scan:\s*disable/i.test(prevLine)) continue;
|
|
179
|
-
|
|
180
|
-
for (const rule of RULES) {
|
|
181
|
-
if (rule.test(line, i, lines)) {
|
|
297
|
+
for (const rule of COMPILED_RULES) {
|
|
298
|
+
if (testRule(rule, line, i, lines)) {
|
|
182
299
|
findings.push({
|
|
183
300
|
checkId: rule.id,
|
|
184
301
|
path: filePath,
|
|
185
|
-
line:
|
|
302
|
+
line: i + 1,
|
|
186
303
|
message: rule.description,
|
|
187
304
|
severity: rule.severity,
|
|
188
305
|
owasp: rule.owasp,
|
|
189
306
|
raw: { line: trimmed }
|
|
190
307
|
});
|
|
191
|
-
break; // one finding per line
|
|
308
|
+
break; // one finding per line
|
|
192
309
|
}
|
|
193
310
|
}
|
|
194
311
|
}
|
|
@@ -196,4 +313,4 @@ function scanFileWithCustomRules(filePath) {
|
|
|
196
313
|
return findings;
|
|
197
314
|
}
|
|
198
315
|
|
|
199
|
-
module.exports = { scanFileWithCustomRules,
|
|
316
|
+
module.exports = { scanFileWithCustomRules, RULE_DEFINITIONS };
|