@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.
- package/.claude/settings.local.json +5 -0
- package/.github/workflows/ci.yml +65 -0
- package/.github/workflows/release.yml +85 -0
- package/PROGRESS.md +192 -0
- package/README.md +183 -0
- package/dist/api/license.d.ts +13 -0
- package/dist/api/license.d.ts.map +1 -0
- package/dist/api/license.js +138 -0
- package/dist/api/license.js.map +1 -0
- package/dist/api/rules.d.ts +13 -0
- package/dist/api/rules.d.ts.map +1 -0
- package/dist/api/rules.js +57 -0
- package/dist/api/rules.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +145 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/login.d.ts +4 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +121 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +14 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/rules.d.ts +3 -0
- package/dist/cli/commands/rules.d.ts.map +1 -0
- package/dist/cli/commands/rules.js +52 -0
- package/dist/cli/commands/rules.js.map +1 -0
- package/dist/cli/commands/scan.d.ts +3 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/scan.js +114 -0
- package/dist/cli/commands/scan.js.map +1 -0
- package/dist/cli/config.d.ts +4 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +88 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +25 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +15 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +152 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +188 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/scanner/index.d.ts +15 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +207 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/parsers/javascript.d.ts +12 -0
- package/dist/scanner/parsers/javascript.d.ts.map +1 -0
- package/dist/scanner/parsers/javascript.js +266 -0
- package/dist/scanner/parsers/javascript.js.map +1 -0
- package/dist/scanner/parsers/python.d.ts +3 -0
- package/dist/scanner/parsers/python.d.ts.map +1 -0
- package/dist/scanner/parsers/python.js +108 -0
- package/dist/scanner/parsers/python.js.map +1 -0
- package/dist/scanner/rules/definitions.d.ts +5 -0
- package/dist/scanner/rules/definitions.d.ts.map +1 -0
- package/dist/scanner/rules/definitions.js +584 -0
- package/dist/scanner/rules/definitions.js.map +1 -0
- package/dist/scanner/rules/loader.d.ts +8 -0
- package/dist/scanner/rules/loader.d.ts.map +1 -0
- package/dist/scanner/rules/loader.js +45 -0
- package/dist/scanner/rules/loader.js.map +1 -0
- package/dist/scanner/rules/matcher.d.ts +11 -0
- package/dist/scanner/rules/matcher.d.ts.map +1 -0
- package/dist/scanner/rules/matcher.js +53 -0
- package/dist/scanner/rules/matcher.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -0
- package/src/api/license.ts +120 -0
- package/src/api/rules.ts +70 -0
- package/src/cli/commands/init.ts +123 -0
- package/src/cli/commands/login.ts +92 -0
- package/src/cli/commands/mcp.ts +12 -0
- package/src/cli/commands/rules.ts +58 -0
- package/src/cli/commands/scan.ts +94 -0
- package/src/cli/config.ts +54 -0
- package/src/cli/index.ts +28 -0
- package/src/cli/output.ts +159 -0
- package/src/mcp/server.ts +195 -0
- package/src/scanner/index.ts +195 -0
- package/src/scanner/parsers/javascript.ts +285 -0
- package/src/scanner/parsers/python.ts +126 -0
- package/src/scanner/rules/definitions.ts +592 -0
- package/src/scanner/rules/loader.ts +59 -0
- package/src/scanner/rules/matcher.ts +68 -0
- package/src/types.ts +36 -0
- package/test-samples/secure.js +52 -0
- package/test-samples/vulnerable.js +56 -0
- package/test-samples/vulnerable.py +39 -0
- package/tests/helpers.ts +43 -0
- package/tests/rules/critical.test.ts +186 -0
- package/tests/rules/definitions.test.ts +167 -0
- package/tests/rules/high.test.ts +377 -0
- package/tests/rules/low.test.ts +172 -0
- package/tests/rules/medium.test.ts +224 -0
- package/tests/scanner/scanner.test.ts +161 -0
- package/tsconfig.json +19 -0
- package/vibe-coding-security-checklist.md +245 -0
- 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')
|
package/tests/helpers.ts
ADDED
|
@@ -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
|
+
});
|