sec-gate 0.1.2 → 0.1.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sec-gate",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Pre-commit security gate for OWASP Top 10 2021 — SAST, SCA and misconfig checks for Node/Express, Go and React codebases",
5
5
  "author": {
6
6
  "name": "Sundram Bhardwaj",
@@ -38,7 +38,7 @@
38
38
  ],
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "https://github.com/SUNDRAMBHARDWAJ/sec-gate.git"
41
+ "url": "git+https://github.com/SUNDRAMBHARDWAJ/sec-gate.git"
42
42
  },
43
43
  "homepage": "https://github.com/SUNDRAMBHARDWAJ/sec-gate#readme",
44
44
  "bugs": {
@@ -48,6 +48,7 @@
48
48
  "bin/",
49
49
  "src/",
50
50
  "scripts/",
51
+ "rules/",
51
52
  "vendor-bin/.gitkeep",
52
53
  "README.md"
53
54
  ]
@@ -0,0 +1,199 @@
1
+ /**
2
+ * sec-gate custom security rules
3
+ *
4
+ * These rules cover patterns NOT caught by @pensar/semgrep-node's owasp-top10 ruleset:
5
+ * 1. Hardcoded secrets (API keys, passwords, JWT secrets)
6
+ * 2. Insecure randomness (Math.random for tokens/sessions)
7
+ * 3. Prototype pollution
8
+ * 4. Sensitive data in localStorage
9
+ * 5. console.log with passwords/secrets
10
+ * 6. new Function() with dynamic input
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ // ─────────────────────────────────────────────────────────
17
+ // Rule definitions
18
+ // Each rule: { id, description, owasp, severity, test(line, lineNum, allLines) }
19
+ // Returns a finding object or null
20
+ // ─────────────────────────────────────────────────────────
21
+
22
+ const RULES = [
23
+
24
+ // ── 1. Hardcoded secrets ──────────────────────────────
25
+ {
26
+ id: 'hardcoded-secret-assignment',
27
+ description: 'Hardcoded secret detected. Secrets should be loaded from environment variables, not hardcoded in source code.',
28
+ owasp: 'A02:2021 Cryptographic Failures',
29
+ severity: 'critical',
30
+ test(line) {
31
+ // Match: const/let/var API_KEY = "...", DB_PASSWORD = '...', etc.
32
+ return /(?:const|let|var)\s+(?:\w*(?:key|secret|password|passwd|pwd|token|api_key|jwt|auth|credential|private_key)\w*)\s*=\s*["'`][^"'`\s]{6,}/i.test(line);
33
+ }
34
+ },
35
+
36
+ {
37
+ id: 'hardcoded-secret-object',
38
+ description: 'Hardcoded secret in object literal. Use environment variables instead.',
39
+ owasp: 'A02:2021 Cryptographic Failures',
40
+ severity: 'critical',
41
+ test(line) {
42
+ // Match: { password: "...", apiKey: "...", secret: "..." }
43
+ return /(?:password|passwd|pwd|secret|api_key|apikey|jwt_secret|private_key|auth_token)\s*:\s*["'`][^"'`\s]{6,}/i.test(line);
44
+ }
45
+ },
46
+
47
+ // ── 2. Insecure randomness ────────────────────────────
48
+ {
49
+ id: 'insecure-random-token',
50
+ // security-scan: disable rule-id: insecure-random-token reason: this is a rule description string, not actual Math.random() usage
51
+ description: 'Math.random() is not cryptographically secure. For tokens, session IDs or passwords use crypto.randomBytes() or crypto.getRandomValues() instead.',
52
+ owasp: 'A02:2021 Cryptographic Failures',
53
+ severity: 'high',
54
+ test(line) {
55
+ return /Math\.random\(\)/.test(line) &&
56
+ /(?:token|session|id|key|secret|password|nonce|salt|otp|code|csrf)/i.test(line);
57
+ }
58
+ },
59
+
60
+ {
61
+ id: 'insecure-random-standalone',
62
+ // security-scan: disable rule-id: insecure-random-standalone reason: rule description string, not actual usage
63
+ description: 'Math.random() used in a security-sensitive context. Use crypto.randomBytes() for cryptographic purposes.',
64
+ owasp: 'A02:2021 Cryptographic Failures',
65
+ severity: 'medium',
66
+ test(line, lineNum, allLines) {
67
+ if (!(/Math\.random\(\)/.test(line))) return false;
68
+ // Check surrounding 3 lines for security context
69
+ const ctx = allLines.slice(Math.max(0, lineNum - 3), lineNum + 3).join(' ');
70
+ return /(?:token|session|secret|key|auth|crypto|password|nonce|salt)/i.test(ctx);
71
+ }
72
+ },
73
+
74
+ // ── 3. Prototype pollution ────────────────────────────
75
+ {
76
+ id: 'prototype-pollution',
77
+ description: 'Possible prototype pollution: assigning to a bracket-notation property using a variable key. Validate or whitelist keys before assignment.',
78
+ owasp: 'A03:2021 Injection',
79
+ severity: 'high',
80
+ test(line) {
81
+ // obj[userKey] = value or target[key] = val where key is variable
82
+ return /\w+\[\s*\w+\s*\]\s*=/.test(line) &&
83
+ !/\/\//.test(line.split('=')[0]); // not in a comment
84
+ }
85
+ },
86
+
87
+ {
88
+ id: 'proto-direct-access',
89
+ // security-scan: disable rule-id: proto-direct-access reason: description string contains __proto__ as text only, not as code
90
+ description: 'Direct __proto__ access detected. This can lead to prototype pollution.',
91
+ owasp: 'A03:2021 Injection',
92
+ severity: 'critical',
93
+ test(line) {
94
+ // security-scan: disable rule-id: proto-direct-access reason: __proto__ is inside a regex literal used as a detection pattern, not actual prototype access
95
+ return /__proto__/.test(line);
96
+ }
97
+ },
98
+
99
+ // ── 4. Sensitive data in localStorage ────────────────
100
+ {
101
+ id: 'localstorage-sensitive-data',
102
+ description: 'Sensitive data stored in localStorage. localStorage is accessible to any JS on the page (XSS). Use httpOnly cookies for tokens and passwords.',
103
+ owasp: 'A02:2021 Cryptographic Failures',
104
+ severity: 'high',
105
+ test(line) {
106
+ return /localStorage\.setItem\s*\(/.test(line) &&
107
+ /(?:password|passwd|pwd|token|secret|key|auth|jwt|session|credential)/i.test(line);
108
+ }
109
+ },
110
+
111
+ {
112
+ id: 'sessionstorage-sensitive-data',
113
+ description: 'Sensitive data stored in sessionStorage. sessionStorage is accessible to XSS attacks. Use httpOnly cookies instead.',
114
+ owasp: 'A02:2021 Cryptographic Failures',
115
+ severity: 'high',
116
+ test(line) {
117
+ return /sessionStorage\.setItem\s*\(/.test(line) &&
118
+ /(?:password|passwd|pwd|token|secret|key|auth|jwt|credential)/i.test(line);
119
+ }
120
+ },
121
+
122
+ // ── 5. console.log with sensitive data ───────────────
123
+ {
124
+ id: 'console-log-sensitive',
125
+ description: 'Possible logging of sensitive data. Passwords, tokens and secrets should never be logged as they appear in log files and monitoring tools.',
126
+ owasp: 'A09:2021 Security Logging and Monitoring Failures',
127
+ severity: 'high',
128
+ test(line) {
129
+ return /console\.\s*(?:log|info|warn|error|debug)\s*\(/.test(line) &&
130
+ /(?:password|passwd|pwd|secret|token|api_?key|jwt|credential|private)/i.test(line);
131
+ }
132
+ },
133
+
134
+ // ── 6. new Function() with dynamic input ─────────────
135
+ {
136
+ id: 'new-function-injection',
137
+ // security-scan: disable rule-id: new-function-injection reason: this is a rule description string, not actual new Function() usage
138
+ description: 'new Function() with dynamic input is equivalent to eval(). An attacker can execute arbitrary JavaScript. Use a safe alternative.',
139
+ owasp: 'A03:2021 Injection',
140
+ severity: 'critical',
141
+ test(line) {
142
+ // new Function(variable) or new Function("..." + variable)
143
+ return /new\s+Function\s*\(/.test(line) &&
144
+ !/new\s+Function\s*\(\s*["'`][^"'`]*["'`]\s*\)/.test(line); // not pure string literal
145
+ }
146
+ },
147
+
148
+ ];
149
+
150
+ // ─────────────────────────────────────────────────────────
151
+ // Scanner: run all rules against a file
152
+ // ─────────────────────────────────────────────────────────
153
+ function scanFileWithCustomRules(filePath) {
154
+ let content;
155
+ try {
156
+ // security-scan: disable rule-id: detect-non-literal-fs-filename reason: filePath comes from `git diff --cached --name-only`, not from user input
157
+ content = fs.readFileSync(filePath, 'utf8');
158
+ } catch {
159
+ return [];
160
+ }
161
+
162
+ const lines = content.split(/\r?\n/);
163
+ const findings = [];
164
+
165
+ for (let i = 0; i < lines.length; i++) {
166
+ const line = lines[i];
167
+ const lineNum = i + 1;
168
+ const trimmed = line.trim();
169
+
170
+ // Skip blank lines and pure comments
171
+ if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
172
+
173
+ // Skip lines with suppression tag
174
+ if (/security-scan:\s*disable/i.test(line)) continue;
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)) {
182
+ findings.push({
183
+ checkId: rule.id,
184
+ path: filePath,
185
+ line: lineNum,
186
+ message: rule.description,
187
+ severity: rule.severity,
188
+ owasp: rule.owasp,
189
+ raw: { line: trimmed }
190
+ });
191
+ break; // one finding per line per pass — avoid duplicates
192
+ }
193
+ }
194
+ }
195
+
196
+ return findings;
197
+ }
198
+
199
+ module.exports = { scanFileWithCustomRules, RULES };
@@ -4,10 +4,12 @@ const { runSemgrep } = require('../scanners/semgrep');
4
4
  const { runOsvScanner } = require('../scanners/osv');
5
5
  const { runGovulncheck } = require('../scanners/govulncheck');
6
6
  const { applyInlineSuppressions } = require('../suppressions/inlineTag');
7
+ const { scanFileWithCustomRules } = require('../../rules/custom-security');
7
8
 
8
9
  function formatFinding(f) {
9
10
  const loc = f.line ? `${f.path}:${f.line}` : f.path;
10
- return `- ${loc} [${f.checkId}] ${f.message}`;
11
+ const owasp = f.owasp ? ` (${f.owasp})` : '';
12
+ return `- ${loc} [${f.checkId}]${owasp}\n ${f.message}`;
11
13
  }
12
14
 
13
15
  function isSemgrepTargetPath(p) {
@@ -34,16 +36,21 @@ async function scan({ staged }) {
34
36
  console.log(`sec-gate: scan started (${staged ? 'staged files' : 'tracked files'})`);
35
37
 
36
38
  const allFindings = [];
37
-
38
39
  const semgrepTargets = (files || []).filter(isSemgrepTargetPath);
39
40
 
40
- // SAST / misconfig
41
41
  if (semgrepTargets.length > 0) {
42
+ // SAST — owasp-top10 via @pensar/semgrep-node
42
43
  const sast = await runSemgrep({ files: semgrepTargets });
43
44
  allFindings.push(...sast);
45
+
46
+ // Custom rules — patterns not covered by owasp-top10 ruleset
47
+ for (const filePath of semgrepTargets) {
48
+ const custom = scanFileWithCustomRules(filePath);
49
+ allFindings.push(...custom);
50
+ }
44
51
  } else {
45
52
  // eslint-disable-next-line no-console
46
- console.log('sec-gate: no relevant staged/tracked source files for Semgrep; skipping SAST');
53
+ console.log('sec-gate: no relevant staged/tracked source files; skipping SAST');
47
54
  }
48
55
 
49
56
  // SCA (only when dependency lockfiles or go module files are staged)