@indicated/vibeguard 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 (109) hide show
  1. package/.claude/settings.local.json +5 -0
  2. package/.github/workflows/ci.yml +65 -0
  3. package/.github/workflows/release.yml +85 -0
  4. package/PROGRESS.md +192 -0
  5. package/README.md +183 -0
  6. package/dist/api/license.d.ts +13 -0
  7. package/dist/api/license.d.ts.map +1 -0
  8. package/dist/api/license.js +138 -0
  9. package/dist/api/license.js.map +1 -0
  10. package/dist/api/rules.d.ts +13 -0
  11. package/dist/api/rules.d.ts.map +1 -0
  12. package/dist/api/rules.js +57 -0
  13. package/dist/api/rules.js.map +1 -0
  14. package/dist/cli/commands/init.d.ts +3 -0
  15. package/dist/cli/commands/init.d.ts.map +1 -0
  16. package/dist/cli/commands/init.js +145 -0
  17. package/dist/cli/commands/init.js.map +1 -0
  18. package/dist/cli/commands/login.d.ts +4 -0
  19. package/dist/cli/commands/login.d.ts.map +1 -0
  20. package/dist/cli/commands/login.js +121 -0
  21. package/dist/cli/commands/login.js.map +1 -0
  22. package/dist/cli/commands/mcp.d.ts +3 -0
  23. package/dist/cli/commands/mcp.d.ts.map +1 -0
  24. package/dist/cli/commands/mcp.js +14 -0
  25. package/dist/cli/commands/mcp.js.map +1 -0
  26. package/dist/cli/commands/rules.d.ts +3 -0
  27. package/dist/cli/commands/rules.d.ts.map +1 -0
  28. package/dist/cli/commands/rules.js +52 -0
  29. package/dist/cli/commands/rules.js.map +1 -0
  30. package/dist/cli/commands/scan.d.ts +3 -0
  31. package/dist/cli/commands/scan.d.ts.map +1 -0
  32. package/dist/cli/commands/scan.js +114 -0
  33. package/dist/cli/commands/scan.js.map +1 -0
  34. package/dist/cli/config.d.ts +4 -0
  35. package/dist/cli/config.d.ts.map +1 -0
  36. package/dist/cli/config.js +88 -0
  37. package/dist/cli/config.js.map +1 -0
  38. package/dist/cli/index.d.ts +3 -0
  39. package/dist/cli/index.d.ts.map +1 -0
  40. package/dist/cli/index.js +25 -0
  41. package/dist/cli/index.js.map +1 -0
  42. package/dist/cli/output.d.ts +15 -0
  43. package/dist/cli/output.d.ts.map +1 -0
  44. package/dist/cli/output.js +152 -0
  45. package/dist/cli/output.js.map +1 -0
  46. package/dist/mcp/server.d.ts +2 -0
  47. package/dist/mcp/server.d.ts.map +1 -0
  48. package/dist/mcp/server.js +188 -0
  49. package/dist/mcp/server.js.map +1 -0
  50. package/dist/scanner/index.d.ts +15 -0
  51. package/dist/scanner/index.d.ts.map +1 -0
  52. package/dist/scanner/index.js +207 -0
  53. package/dist/scanner/index.js.map +1 -0
  54. package/dist/scanner/parsers/javascript.d.ts +12 -0
  55. package/dist/scanner/parsers/javascript.d.ts.map +1 -0
  56. package/dist/scanner/parsers/javascript.js +266 -0
  57. package/dist/scanner/parsers/javascript.js.map +1 -0
  58. package/dist/scanner/parsers/python.d.ts +3 -0
  59. package/dist/scanner/parsers/python.d.ts.map +1 -0
  60. package/dist/scanner/parsers/python.js +108 -0
  61. package/dist/scanner/parsers/python.js.map +1 -0
  62. package/dist/scanner/rules/definitions.d.ts +5 -0
  63. package/dist/scanner/rules/definitions.d.ts.map +1 -0
  64. package/dist/scanner/rules/definitions.js +584 -0
  65. package/dist/scanner/rules/definitions.js.map +1 -0
  66. package/dist/scanner/rules/loader.d.ts +8 -0
  67. package/dist/scanner/rules/loader.d.ts.map +1 -0
  68. package/dist/scanner/rules/loader.js +45 -0
  69. package/dist/scanner/rules/loader.js.map +1 -0
  70. package/dist/scanner/rules/matcher.d.ts +11 -0
  71. package/dist/scanner/rules/matcher.d.ts.map +1 -0
  72. package/dist/scanner/rules/matcher.js +53 -0
  73. package/dist/scanner/rules/matcher.js.map +1 -0
  74. package/dist/types.d.ts +33 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +3 -0
  77. package/dist/types.js.map +1 -0
  78. package/package.json +48 -0
  79. package/src/api/license.ts +120 -0
  80. package/src/api/rules.ts +70 -0
  81. package/src/cli/commands/init.ts +123 -0
  82. package/src/cli/commands/login.ts +92 -0
  83. package/src/cli/commands/mcp.ts +12 -0
  84. package/src/cli/commands/rules.ts +58 -0
  85. package/src/cli/commands/scan.ts +94 -0
  86. package/src/cli/config.ts +54 -0
  87. package/src/cli/index.ts +28 -0
  88. package/src/cli/output.ts +159 -0
  89. package/src/mcp/server.ts +195 -0
  90. package/src/scanner/index.ts +195 -0
  91. package/src/scanner/parsers/javascript.ts +285 -0
  92. package/src/scanner/parsers/python.ts +126 -0
  93. package/src/scanner/rules/definitions.ts +592 -0
  94. package/src/scanner/rules/loader.ts +59 -0
  95. package/src/scanner/rules/matcher.ts +68 -0
  96. package/src/types.ts +36 -0
  97. package/test-samples/secure.js +52 -0
  98. package/test-samples/vulnerable.js +56 -0
  99. package/test-samples/vulnerable.py +39 -0
  100. package/tests/helpers.ts +43 -0
  101. package/tests/rules/critical.test.ts +186 -0
  102. package/tests/rules/definitions.test.ts +167 -0
  103. package/tests/rules/high.test.ts +377 -0
  104. package/tests/rules/low.test.ts +172 -0
  105. package/tests/rules/medium.test.ts +224 -0
  106. package/tests/scanner/scanner.test.ts +161 -0
  107. package/tsconfig.json +19 -0
  108. package/vibe-coding-security-checklist.md +245 -0
  109. package/vitest.config.ts +15 -0
@@ -0,0 +1,68 @@
1
+ import { SecurityRule, Finding } from '../../types';
2
+
3
+ export interface MatchContext {
4
+ code: string;
5
+ lines: string[];
6
+ filePath: string;
7
+ language: string;
8
+ }
9
+
10
+ export function matchPatterns(
11
+ rule: SecurityRule,
12
+ context: MatchContext
13
+ ): Finding[] {
14
+ const findings: Finding[] = [];
15
+
16
+ if (!rule.patterns) return findings;
17
+
18
+ for (const pattern of rule.patterns) {
19
+ let match;
20
+ const regex = new RegExp(
21
+ pattern.source,
22
+ pattern.flags + (pattern.flags.includes('g') ? '' : 'g')
23
+ );
24
+
25
+ while ((match = regex.exec(context.code)) !== null) {
26
+ const beforeMatch = context.code.substring(0, match.index);
27
+ const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1;
28
+ const lineStart = beforeMatch.lastIndexOf('\n') + 1;
29
+ const column = match.index - lineStart;
30
+
31
+ findings.push({
32
+ rule,
33
+ file: context.filePath,
34
+ line: lineNumber,
35
+ column,
36
+ code: context.lines[lineNumber - 1] || '',
37
+ message: rule.description,
38
+ });
39
+
40
+ // Prevent infinite loops on zero-width matches
41
+ if (match.index === regex.lastIndex) {
42
+ regex.lastIndex++;
43
+ }
44
+ }
45
+ }
46
+
47
+ return findings;
48
+ }
49
+
50
+ export function deduplicateFindings(findings: Finding[]): Finding[] {
51
+ const seen = new Set<string>();
52
+ return findings.filter(finding => {
53
+ const key = `${finding.rule.id}:${finding.file}:${finding.line}`;
54
+ if (seen.has(key)) return false;
55
+ seen.add(key);
56
+ return true;
57
+ });
58
+ }
59
+
60
+ export function sortBySeverity(findings: Finding[]): Finding[] {
61
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
62
+ return findings.sort((a, b) => {
63
+ const severityDiff =
64
+ severityOrder[a.rule.severity] - severityOrder[b.rule.severity];
65
+ if (severityDiff !== 0) return severityDiff;
66
+ return a.file.localeCompare(b.file) || a.line - b.line;
67
+ });
68
+ }
package/src/types.ts ADDED
@@ -0,0 +1,36 @@
1
+ export type Severity = 'critical' | 'high' | 'medium' | 'low';
2
+
3
+ export interface SecurityRule {
4
+ id: string;
5
+ name: string;
6
+ description: string;
7
+ severity: Severity;
8
+ languages: ('javascript' | 'typescript' | 'python')[];
9
+ patterns?: RegExp[];
10
+ astMatcher?: string;
11
+ fix?: string;
12
+ }
13
+
14
+ export interface Finding {
15
+ rule: SecurityRule;
16
+ file: string;
17
+ line: number;
18
+ column: number;
19
+ code: string;
20
+ message: string;
21
+ }
22
+
23
+ export interface ScanResult {
24
+ files: number;
25
+ findings: Finding[];
26
+ duration: number;
27
+ }
28
+
29
+ export interface Config {
30
+ exclude?: string[];
31
+ severity?: Severity;
32
+ rules?: {
33
+ enabled?: string[];
34
+ disabled?: string[];
35
+ };
36
+ }
@@ -0,0 +1,52 @@
1
+ // Test file with secure code patterns
2
+
3
+ // Good: Using environment variables
4
+ const apiKey = process.env.API_KEY;
5
+ const dbPassword = process.env.DB_PASSWORD;
6
+
7
+ // Good: Parameterized queries
8
+ async function getUser(userId) {
9
+ return db.query('SELECT * FROM users WHERE id = $1', [userId]);
10
+ }
11
+
12
+ // Good: Safe JSON parsing
13
+ function processData(userInput) {
14
+ return JSON.parse(userInput);
15
+ }
16
+
17
+ // Good: Using textContent instead of innerHTML
18
+ function renderContent(content) {
19
+ document.getElementById('output').textContent = content;
20
+ }
21
+
22
+ // Good: httpOnly cookies must be set server-side
23
+ // This is the correct way (in Express/Node.js):
24
+ // res.cookie('token', token, { httpOnly: true, secure: true, sameSite: 'strict' });
25
+
26
+ // Client-side, we only set non-sensitive cookies:
27
+ function savePreference(theme) {
28
+ document.cookie = `theme=${theme}; SameSite=Strict`;
29
+ }
30
+
31
+ // Good: CORS with specific origin
32
+ app.use(cors({ origin: 'https://myapp.com' }));
33
+
34
+ // Good: HTTPS
35
+ fetch('https://api.example.com/data');
36
+
37
+ // Good: Strong password validation
38
+ if (password.length >= 12 && /[A-Z]/.test(password) && /[0-9]/.test(password)) {
39
+ console.log('Password is valid');
40
+ }
41
+
42
+ // Good: Generic error messages
43
+ app.use((err, req, res, next) => {
44
+ console.error(err);
45
+ res.status(500).json({ error: 'An error occurred' });
46
+ });
47
+
48
+ // Good: Route with authentication
49
+ app.post('/api/users', authMiddleware, (req, res) => {
50
+ const user = createUser(req.body);
51
+ res.json(user);
52
+ });
@@ -0,0 +1,56 @@
1
+ // Test file with intentional vulnerabilities for scanner testing
2
+
3
+ // CRITICAL: Hardcoded API key
4
+ const OPENAI_KEY = "sk-1234567890abcdefghijklmnopqrstuvwxyz";
5
+ const apiKey = "ghp_abcdefghijklmnopqrstuvwxyz1234567890";
6
+
7
+ // CRITICAL: SQL injection
8
+ async function getUser(userId) {
9
+ const query = `SELECT * FROM users WHERE id = ${userId}`;
10
+ return db.query(query);
11
+ }
12
+
13
+ // CRITICAL: eval with user input
14
+ function processData(userInput) {
15
+ return eval(userInput);
16
+ }
17
+
18
+ // HIGH: XSS via innerHTML
19
+ function renderContent(content) {
20
+ document.getElementById('output').innerHTML = content;
21
+ }
22
+
23
+ // HIGH: Storing tokens in localStorage
24
+ function saveToken(token) {
25
+ localStorage.setItem('auth_token', token);
26
+ sessionStorage.setItem('jwt', token);
27
+ }
28
+
29
+ // HIGH: Supabase without RLS consideration
30
+ async function getData() {
31
+ const { data } = await supabase.from('users').select('*');
32
+ return data;
33
+ }
34
+
35
+ // MEDIUM: Permissive CORS
36
+ app.use(cors());
37
+ const headers = { 'Access-Control-Allow-Origin': '*' };
38
+
39
+ // MEDIUM: HTTP instead of HTTPS
40
+ fetch('http://api.example.com/data');
41
+
42
+ // MEDIUM: Weak password
43
+ if (password.length >= 4) {
44
+ console.log('Password is valid');
45
+ }
46
+
47
+ // LOW: Verbose errors
48
+ app.use((err, req, res, next) => {
49
+ res.status(500).json(err.message);
50
+ });
51
+
52
+ // HIGH: API route without auth
53
+ app.post('/api/users', (req, res) => {
54
+ const user = createUser(req.body);
55
+ res.json(user);
56
+ });
@@ -0,0 +1,39 @@
1
+ # Test file with intentional Python vulnerabilities
2
+
3
+ import sqlite3
4
+ from flask import Flask, request
5
+ from flask_cors import CORS
6
+
7
+ app = Flask(__name__)
8
+ CORS(app)
9
+
10
+ # CRITICAL: Hardcoded secret
11
+ api_key = "sk-1234567890abcdefghijklmnopqrstuvwxyz"
12
+ password = "supersecret123"
13
+
14
+ # CRITICAL: SQL injection with f-string
15
+ def get_user(user_id):
16
+ cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
17
+ return cursor.fetchone()
18
+
19
+ # CRITICAL: SQL injection with string formatting
20
+ def search_users(name):
21
+ cursor.execute("SELECT * FROM users WHERE name = '%s'" % name)
22
+ return cursor.fetchall()
23
+
24
+ # CRITICAL: eval with user input
25
+ def process_input(data):
26
+ return eval(request.form['expression'])
27
+
28
+ # MEDIUM: Debug mode in production
29
+ if __name__ == '__main__':
30
+ app.run(debug=True)
31
+
32
+ # MEDIUM: Weak password validation
33
+ def validate_password(password):
34
+ if len(password) >= 4:
35
+ return True
36
+ return False
37
+
38
+ # MEDIUM: HTTP URL
39
+ response = requests.get('http://api.example.com/data')
@@ -0,0 +1,43 @@
1
+ import { securityRules } from '../src/scanner/rules/definitions';
2
+ import { SecurityRule } from '../src/types';
3
+
4
+ /**
5
+ * Test if a code snippet triggers a specific rule
6
+ */
7
+ export function testRule(ruleId: string, code: string): boolean {
8
+ const rule = securityRules.find(r => r.id === ruleId);
9
+ if (!rule) {
10
+ throw new Error(`Rule not found: ${ruleId}`);
11
+ }
12
+
13
+ if (!rule.patterns || rule.patterns.length === 0) {
14
+ // Rule uses AST matcher only, skip pattern test
15
+ return false;
16
+ }
17
+
18
+ for (const pattern of rule.patterns) {
19
+ // Reset lastIndex for global patterns
20
+ if (pattern.global) {
21
+ pattern.lastIndex = 0;
22
+ }
23
+ if (pattern.test(code)) {
24
+ return true;
25
+ }
26
+ }
27
+
28
+ return false;
29
+ }
30
+
31
+ /**
32
+ * Get a rule by ID
33
+ */
34
+ export function getRule(ruleId: string): SecurityRule | undefined {
35
+ return securityRules.find(r => r.id === ruleId);
36
+ }
37
+
38
+ /**
39
+ * Get all rule IDs
40
+ */
41
+ export function getAllRuleIds(): string[] {
42
+ return securityRules.map(r => r.id);
43
+ }
@@ -0,0 +1,186 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { testRule, getRule } from '../helpers';
3
+
4
+ describe('Critical Security Rules', () => {
5
+ describe('hardcoded-secret', () => {
6
+ const ruleId = 'hardcoded-secret';
7
+
8
+ it('should exist', () => {
9
+ expect(getRule(ruleId)).toBeDefined();
10
+ });
11
+
12
+ it('should detect OpenAI API keys', () => {
13
+ expect(testRule(ruleId, `const key = "sk-abc123def456ghi789jkl012mno345pqr678"`)).toBe(true);
14
+ });
15
+
16
+ it('should detect GitHub tokens', () => {
17
+ expect(testRule(ruleId, `const token = "ghp_1234567890abcdefghijklmnopqrstuvwxyz"`)).toBe(true);
18
+ });
19
+
20
+ it('should detect AWS access keys', () => {
21
+ expect(testRule(ruleId, `const aws = "AKIAIOSFODNN7EXAMPLE"`)).toBe(true);
22
+ });
23
+
24
+ it('should detect Slack tokens', () => {
25
+ expect(testRule(ruleId, `const slack = "xoxb-123456789012-1234567890123-abcdefghij"`)).toBe(true);
26
+ });
27
+
28
+ it('should detect JWT tokens', () => {
29
+ expect(testRule(ruleId, `const jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc123"`)).toBe(true);
30
+ });
31
+
32
+ it('should NOT detect environment variable usage', () => {
33
+ expect(testRule(ruleId, `const key = process.env.API_KEY`)).toBe(false);
34
+ });
35
+
36
+ it('should NOT detect placeholder values', () => {
37
+ expect(testRule(ruleId, `const key = "your-api-key-here"`)).toBe(false);
38
+ });
39
+ });
40
+
41
+ describe('sql-injection', () => {
42
+ const ruleId = 'sql-injection';
43
+
44
+ it('should exist', () => {
45
+ expect(getRule(ruleId)).toBeDefined();
46
+ });
47
+
48
+ // Note: sql-injection uses AST matcher, pattern tests may not apply
49
+ it('should have AST matcher configured', () => {
50
+ const rule = getRule(ruleId);
51
+ expect(rule?.astMatcher).toBe('sql-injection');
52
+ });
53
+ });
54
+
55
+ describe('eval-usage', () => {
56
+ const ruleId = 'eval-usage';
57
+
58
+ it('should exist', () => {
59
+ expect(getRule(ruleId)).toBeDefined();
60
+ });
61
+
62
+ // Note: eval-usage uses AST matcher
63
+ it('should have AST matcher configured', () => {
64
+ const rule = getRule(ruleId);
65
+ expect(rule?.astMatcher).toBe('eval-usage');
66
+ });
67
+ });
68
+
69
+ describe('command-injection', () => {
70
+ const ruleId = 'command-injection';
71
+
72
+ it('should exist', () => {
73
+ expect(getRule(ruleId)).toBeDefined();
74
+ });
75
+
76
+ it('should detect exec with template literal', () => {
77
+ expect(testRule(ruleId, 'exec(`ls ${userInput}`)'));
78
+ });
79
+
80
+ it('should detect execSync with template literal', () => {
81
+ expect(testRule(ruleId, 'execSync(`cat ${filename}`)'));
82
+ });
83
+
84
+ it('should detect Python subprocess with shell=True', () => {
85
+ expect(testRule(ruleId, 'subprocess.call(cmd, shell=True)')).toBe(true);
86
+ });
87
+
88
+ it('should detect Python os.system with f-string', () => {
89
+ expect(testRule(ruleId, 'os.system(f"rm {file}")')).toBe(true);
90
+ });
91
+
92
+ it('should NOT detect exec with static string', () => {
93
+ expect(testRule(ruleId, 'exec("ls -la")')).toBe(false);
94
+ });
95
+ });
96
+
97
+ describe('insecure-deserialization', () => {
98
+ const ruleId = 'insecure-deserialization';
99
+
100
+ it('should exist', () => {
101
+ expect(getRule(ruleId)).toBeDefined();
102
+ });
103
+
104
+ it('should detect pickle.loads', () => {
105
+ expect(testRule(ruleId, 'data = pickle.loads(user_input)')).toBe(true);
106
+ });
107
+
108
+ it('should detect pickle.load', () => {
109
+ expect(testRule(ruleId, 'data = pickle.load(file)')).toBe(true);
110
+ });
111
+
112
+ it('should detect yaml.load without Loader', () => {
113
+ expect(testRule(ruleId, 'config = yaml.load(data)')).toBe(true);
114
+ });
115
+
116
+ it('should detect node-serialize', () => {
117
+ expect(testRule(ruleId, 'const obj = node-serialize.unserialize(data)')).toBe(true);
118
+ });
119
+
120
+ it('should NOT detect yaml.safe_load', () => {
121
+ expect(testRule(ruleId, 'config = yaml.safe_load(data)')).toBe(false);
122
+ });
123
+ });
124
+
125
+ describe('django-debug-true', () => {
126
+ const ruleId = 'django-debug-true';
127
+
128
+ it('should exist', () => {
129
+ expect(getRule(ruleId)).toBeDefined();
130
+ });
131
+
132
+ it('should detect DEBUG = True', () => {
133
+ expect(testRule(ruleId, 'DEBUG = True')).toBe(true);
134
+ });
135
+
136
+ it('should NOT detect DEBUG = False', () => {
137
+ expect(testRule(ruleId, 'DEBUG = False')).toBe(false);
138
+ });
139
+
140
+ it('should NOT detect DEBUG from env', () => {
141
+ expect(testRule(ruleId, 'DEBUG = os.environ.get("DEBUG", False)')).toBe(false);
142
+ });
143
+ });
144
+
145
+ describe('django-secret-key-exposed', () => {
146
+ const ruleId = 'django-secret-key-exposed';
147
+
148
+ it('should exist', () => {
149
+ expect(getRule(ruleId)).toBeDefined();
150
+ });
151
+
152
+ it('should detect hardcoded SECRET_KEY', () => {
153
+ expect(testRule(ruleId, `SECRET_KEY = "django-insecure-abc123def456ghi789"`)).toBe(true);
154
+ });
155
+
156
+ it('should NOT detect SECRET_KEY from env', () => {
157
+ expect(testRule(ruleId, `SECRET_KEY = os.environ.get("SECRET_KEY")`)).toBe(false);
158
+ });
159
+ });
160
+
161
+ describe('django-raw-sql', () => {
162
+ const ruleId = 'django-raw-sql';
163
+
164
+ it('should exist', () => {
165
+ expect(getRule(ruleId)).toBeDefined();
166
+ });
167
+
168
+ it('should detect .raw() with f-string', () => {
169
+ expect(testRule(ruleId, 'User.objects.raw(f"SELECT * FROM users WHERE id = {user_id}")')).toBe(true);
170
+ });
171
+
172
+ it('should detect cursor.execute with f-string', () => {
173
+ expect(testRule(ruleId, 'cursor.execute(f"DELETE FROM {table}")')).toBe(true);
174
+ });
175
+
176
+ it('should detect .extra() with where clause', () => {
177
+ expect(testRule(ruleId, '.extra(where=["id = %s"])')).toBe(true);
178
+ });
179
+
180
+ it('should detect .raw() even with params (requires manual review)', () => {
181
+ // Pattern flags all .raw() calls for manual review since parameterized
182
+ // queries can still be vulnerable if params come from user input
183
+ expect(testRule(ruleId, 'User.objects.raw("SELECT * FROM users WHERE id = %s", [user_id])')).toBe(true);
184
+ });
185
+ });
186
+ });
@@ -0,0 +1,167 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { securityRules, getRuleById, getRulesBySeverity } from '../../src/scanner/rules/definitions';
3
+
4
+ describe('Security Rules Definitions', () => {
5
+ describe('rule structure', () => {
6
+ it('should have at least 40 rules', () => {
7
+ expect(securityRules.length).toBeGreaterThanOrEqual(40);
8
+ });
9
+
10
+ it('all rules should have required fields', () => {
11
+ for (const rule of securityRules) {
12
+ expect(rule.id).toBeDefined();
13
+ expect(rule.id).toMatch(/^[a-zA-Z0-9-]+$/);
14
+ expect(rule.name).toBeDefined();
15
+ expect(rule.name.length).toBeGreaterThan(0);
16
+ expect(rule.description).toBeDefined();
17
+ expect(rule.description.length).toBeGreaterThan(0);
18
+ expect(rule.severity).toBeDefined();
19
+ expect(['critical', 'high', 'medium', 'low']).toContain(rule.severity);
20
+ expect(rule.languages).toBeDefined();
21
+ expect(rule.languages.length).toBeGreaterThan(0);
22
+ expect(rule.fix).toBeDefined();
23
+ expect(rule.fix.length).toBeGreaterThan(0);
24
+ }
25
+ });
26
+
27
+ it('all rules should have either patterns or astMatcher', () => {
28
+ for (const rule of securityRules) {
29
+ const hasPatterns = rule.patterns && rule.patterns.length > 0;
30
+ const hasAstMatcher = !!rule.astMatcher;
31
+ expect(hasPatterns || hasAstMatcher).toBe(true);
32
+ }
33
+ });
34
+
35
+ it('all rule IDs should be unique', () => {
36
+ const ids = securityRules.map(r => r.id);
37
+ const uniqueIds = new Set(ids);
38
+ expect(uniqueIds.size).toBe(ids.length);
39
+ });
40
+
41
+ it('all patterns should be valid RegExp', () => {
42
+ for (const rule of securityRules) {
43
+ if (rule.patterns) {
44
+ for (const pattern of rule.patterns) {
45
+ expect(pattern).toBeInstanceOf(RegExp);
46
+ }
47
+ }
48
+ }
49
+ });
50
+ });
51
+
52
+ describe('severity distribution', () => {
53
+ it('should have critical rules', () => {
54
+ const critical = securityRules.filter(r => r.severity === 'critical');
55
+ expect(critical.length).toBeGreaterThan(0);
56
+ });
57
+
58
+ it('should have high rules', () => {
59
+ const high = securityRules.filter(r => r.severity === 'high');
60
+ expect(high.length).toBeGreaterThan(0);
61
+ });
62
+
63
+ it('should have medium rules', () => {
64
+ const medium = securityRules.filter(r => r.severity === 'medium');
65
+ expect(medium.length).toBeGreaterThan(0);
66
+ });
67
+
68
+ it('should have low rules', () => {
69
+ const low = securityRules.filter(r => r.severity === 'low');
70
+ expect(low.length).toBeGreaterThan(0);
71
+ });
72
+ });
73
+
74
+ describe('language support', () => {
75
+ it('should have JavaScript rules', () => {
76
+ const jsRules = securityRules.filter(r => r.languages.includes('javascript'));
77
+ expect(jsRules.length).toBeGreaterThan(0);
78
+ });
79
+
80
+ it('should have TypeScript rules', () => {
81
+ const tsRules = securityRules.filter(r => r.languages.includes('typescript'));
82
+ expect(tsRules.length).toBeGreaterThan(0);
83
+ });
84
+
85
+ it('should have Python rules', () => {
86
+ const pyRules = securityRules.filter(r => r.languages.includes('python'));
87
+ expect(pyRules.length).toBeGreaterThan(0);
88
+ });
89
+ });
90
+
91
+ describe('getRuleById', () => {
92
+ it('should return rule by ID', () => {
93
+ const rule = getRuleById('hardcoded-secret');
94
+ expect(rule).toBeDefined();
95
+ expect(rule?.id).toBe('hardcoded-secret');
96
+ });
97
+
98
+ it('should return undefined for non-existent ID', () => {
99
+ const rule = getRuleById('non-existent-rule');
100
+ expect(rule).toBeUndefined();
101
+ });
102
+ });
103
+
104
+ describe('getRulesBySeverity', () => {
105
+ it('should return critical rules', () => {
106
+ const rules = getRulesBySeverity('critical');
107
+ expect(rules.length).toBeGreaterThan(0);
108
+ expect(rules.every(r => r.severity === 'critical')).toBe(true);
109
+ });
110
+
111
+ it('should return high rules', () => {
112
+ const rules = getRulesBySeverity('high');
113
+ expect(rules.length).toBeGreaterThan(0);
114
+ expect(rules.every(r => r.severity === 'high')).toBe(true);
115
+ });
116
+
117
+ it('should return empty array for invalid severity', () => {
118
+ const rules = getRulesBySeverity('invalid');
119
+ expect(rules).toEqual([]);
120
+ });
121
+ });
122
+
123
+ describe('specific rules exist', () => {
124
+ const expectedRules = [
125
+ // Critical
126
+ 'hardcoded-secret',
127
+ 'sql-injection',
128
+ 'eval-usage',
129
+ 'command-injection',
130
+ 'insecure-deserialization',
131
+ 'django-debug-true',
132
+ 'django-secret-key-exposed',
133
+ 'django-raw-sql',
134
+ // High
135
+ 'missing-auth-route',
136
+ 'xss-innerhtml',
137
+ 'secrets-localstorage',
138
+ 'supabase-no-rls',
139
+ 'firebase-no-rules',
140
+ 'idor-vulnerability',
141
+ 'path-traversal',
142
+ 'ssrf-vulnerability',
143
+ 'open-redirect',
144
+ 'insecure-cookie',
145
+ 'missing-csrf',
146
+ // Medium
147
+ 'permissive-cors',
148
+ 'http-not-https',
149
+ 'weak-password',
150
+ 'hardcoded-ip',
151
+ 'xxe-vulnerability',
152
+ 'jwt-none-algorithm',
153
+ // Low
154
+ 'verbose-errors',
155
+ 'missing-rate-limit',
156
+ 'console-log-sensitive',
157
+ 'debug-mode-enabled',
158
+ 'prototype-pollution',
159
+ ];
160
+
161
+ for (const ruleId of expectedRules) {
162
+ it(`should have rule: ${ruleId}`, () => {
163
+ expect(getRuleById(ruleId)).toBeDefined();
164
+ });
165
+ }
166
+ });
167
+ });