code-auditor-mcp 1.20.0 → 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/dist/adapters/GoAdapter.d.ts +53 -0
- package/dist/adapters/GoAdapter.d.ts.map +1 -0
- package/dist/adapters/GoAdapter.js +602 -0
- package/dist/adapters/GoAdapter.js.map +1 -0
- package/dist/adapters/GoConfig.d.ts +35 -0
- package/dist/adapters/GoConfig.d.ts.map +1 -0
- package/dist/adapters/GoConfig.js +155 -0
- package/dist/adapters/GoConfig.js.map +1 -0
- package/dist/adapters/LanguageAdapter.d.ts +135 -0
- package/dist/adapters/LanguageAdapter.d.ts.map +1 -0
- package/dist/adapters/LanguageAdapter.js +47 -0
- package/dist/adapters/LanguageAdapter.js.map +1 -0
- package/dist/adapters/TypeScriptAdapter.d.ts +43 -0
- package/dist/adapters/TypeScriptAdapter.d.ts.map +1 -0
- package/dist/adapters/TypeScriptAdapter.js +386 -0
- package/dist/adapters/TypeScriptAdapter.js.map +1 -0
- package/dist/adapters/UniversalAnalyzer.d.ts +42 -0
- package/dist/adapters/UniversalAnalyzer.d.ts.map +1 -0
- package/dist/adapters/UniversalAnalyzer.js +103 -0
- package/dist/adapters/UniversalAnalyzer.js.map +1 -0
- package/dist/adapters/UniversalSOLIDAnalyzer.d.ts +48 -0
- package/dist/adapters/UniversalSOLIDAnalyzer.d.ts.map +1 -0
- package/dist/adapters/UniversalSOLIDAnalyzer.js +178 -0
- package/dist/adapters/UniversalSOLIDAnalyzer.js.map +1 -0
- package/dist/adapters/solidAnalyzerCompat.d.ts +21 -0
- package/dist/adapters/solidAnalyzerCompat.d.ts.map +1 -0
- package/dist/adapters/solidAnalyzerCompat.js +78 -0
- package/dist/adapters/solidAnalyzerCompat.js.map +1 -0
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +103 -0
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/cross-language/APIContractAnalyzer.js +425 -0
- package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -0
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.d.ts +117 -0
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.js +618 -0
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.js.map +1 -0
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +129 -0
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -0
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +473 -0
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -0
- package/dist/analyzers/cross-language/SchemaValidator.d.ts +126 -0
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -0
- package/dist/analyzers/cross-language/SchemaValidator.js +522 -0
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -0
- package/dist/analyzers/dataAccessAnalyzerCompat.d.ts +20 -0
- package/dist/analyzers/dataAccessAnalyzerCompat.d.ts.map +1 -0
- package/dist/analyzers/dataAccessAnalyzerCompat.js +59 -0
- package/dist/analyzers/dataAccessAnalyzerCompat.js.map +1 -0
- package/dist/analyzers/documentationAnalyzerCompat.d.ts +20 -0
- package/dist/analyzers/documentationAnalyzerCompat.d.ts.map +1 -0
- package/dist/analyzers/documentationAnalyzerCompat.js +52 -0
- package/dist/analyzers/documentationAnalyzerCompat.js.map +1 -0
- package/dist/analyzers/dryAnalyzerCompat.d.ts +20 -0
- package/dist/analyzers/dryAnalyzerCompat.d.ts.map +1 -0
- package/dist/analyzers/dryAnalyzerCompat.js +44 -0
- package/dist/analyzers/dryAnalyzerCompat.js.map +1 -0
- package/dist/analyzers/schemaAnalyzer.d.ts +21 -0
- package/dist/analyzers/schemaAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/schemaAnalyzer.js +405 -0
- package/dist/analyzers/schemaAnalyzer.js.map +1 -0
- package/dist/analyzers/schemaAnalyzerCompat.d.ts +20 -0
- package/dist/analyzers/schemaAnalyzerCompat.d.ts.map +1 -0
- package/dist/analyzers/schemaAnalyzerCompat.js +69 -0
- package/dist/analyzers/schemaAnalyzerCompat.js.map +1 -0
- package/dist/analyzers/solidAnalyzerCompat.d.ts +20 -0
- package/dist/analyzers/solidAnalyzerCompat.d.ts.map +1 -0
- package/dist/analyzers/solidAnalyzerCompat.js +78 -0
- package/dist/analyzers/solidAnalyzerCompat.js.map +1 -0
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +71 -0
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +276 -0
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -0
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +82 -0
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +489 -0
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -0
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +48 -0
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +175 -0
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -0
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +64 -0
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +286 -0
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -0
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +119 -0
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +812 -0
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -0
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +23 -7
- package/dist/auditRunner.js.map +1 -1
- package/dist/codeIndexDB-enhanced.d.ts +80 -0
- package/dist/codeIndexDB-enhanced.d.ts.map +1 -0
- package/dist/codeIndexDB-enhanced.js +488 -0
- package/dist/codeIndexDB-enhanced.js.map +1 -0
- package/dist/codeIndexDb.d.ts +70 -1
- package/dist/codeIndexDb.d.ts.map +1 -1
- package/dist/codeIndexDb.js +199 -2
- package/dist/codeIndexDb.js.map +1 -1
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/languages/LanguageOrchestrator.d.ts +181 -0
- package/dist/languages/LanguageOrchestrator.d.ts.map +1 -0
- package/dist/languages/LanguageOrchestrator.js +365 -0
- package/dist/languages/LanguageOrchestrator.js.map +1 -0
- package/dist/languages/LanguageRegistry.d.ts +43 -0
- package/dist/languages/LanguageRegistry.d.ts.map +1 -0
- package/dist/languages/LanguageRegistry.js +86 -0
- package/dist/languages/LanguageRegistry.js.map +1 -0
- package/dist/languages/RuntimeManager.d.ts +181 -0
- package/dist/languages/RuntimeManager.d.ts.map +1 -0
- package/dist/languages/RuntimeManager.js +841 -0
- package/dist/languages/RuntimeManager.js.map +1 -0
- package/dist/languages/UniversalAnalyzer.d.ts +37 -0
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -0
- package/dist/languages/UniversalAnalyzer.js +101 -0
- package/dist/languages/UniversalAnalyzer.js.map +1 -0
- package/dist/languages/go/GoAdapter.d.ts +57 -0
- package/dist/languages/go/GoAdapter.d.ts.map +1 -0
- package/dist/languages/go/GoAdapter.js +360 -0
- package/dist/languages/go/GoAdapter.js.map +1 -0
- package/dist/languages/index.d.ts +13 -0
- package/dist/languages/index.d.ts.map +1 -0
- package/dist/languages/index.js +23 -0
- package/dist/languages/index.js.map +1 -0
- package/dist/languages/types.d.ts +182 -0
- package/dist/languages/types.d.ts.map +1 -0
- package/dist/languages/types.js +5 -0
- package/dist/languages/types.js.map +1 -0
- package/dist/languages/typescript/TypeScriptAdapter.d.ts +108 -0
- package/dist/languages/typescript/TypeScriptAdapter.d.ts.map +1 -0
- package/dist/languages/typescript/TypeScriptAdapter.js +567 -0
- package/dist/languages/typescript/TypeScriptAdapter.js.map +1 -0
- package/dist/languages/typescript/test-extraction.d.ts +2 -0
- package/dist/languages/typescript/test-extraction.d.ts.map +1 -0
- package/dist/languages/typescript/test-extraction.js +36 -0
- package/dist/languages/typescript/test-extraction.js.map +1 -0
- package/dist/mcp-hybrid.d.ts +10 -0
- package/dist/mcp-hybrid.d.ts.map +1 -0
- package/dist/mcp-hybrid.js +41 -0
- package/dist/mcp-hybrid.js.map +1 -0
- package/dist/mcp-index.d.ts +11 -0
- package/dist/mcp-index.d.ts.map +1 -0
- package/dist/mcp-index.js +101 -0
- package/dist/mcp-index.js.map +1 -0
- package/dist/mcp-tools/workflowGuide.d.ts.map +1 -1
- package/dist/mcp-tools/workflowGuide.js +67 -10
- package/dist/mcp-tools/workflowGuide.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts +49 -0
- package/dist/mcp-tools-shared.d.ts.map +1 -0
- package/dist/mcp-tools-shared.js +1282 -0
- package/dist/mcp-tools-shared.js.map +1 -0
- package/dist/mcp-ui-server.d.ts +27 -0
- package/dist/mcp-ui-server.d.ts.map +1 -0
- package/dist/mcp-ui-server.js +484 -0
- package/dist/mcp-ui-server.js.map +1 -0
- package/dist/mcp-ui-simple.d.ts +27 -0
- package/dist/mcp-ui-simple.d.ts.map +1 -0
- package/dist/mcp-ui-simple.js +425 -0
- package/dist/mcp-ui-simple.js.map +1 -0
- package/dist/mcp.js +7 -4
- package/dist/mcp.js.map +1 -1
- package/dist/services/SchemaParser.d.ts +89 -0
- package/dist/services/SchemaParser.d.ts.map +1 -0
- package/dist/services/SchemaParser.js +434 -0
- package/dist/services/SchemaParser.js.map +1 -0
- package/dist/types/crossLanguage.d.ts +267 -0
- package/dist/types/crossLanguage.d.ts.map +1 -0
- package/dist/types/crossLanguage.js +6 -0
- package/dist/types/crossLanguage.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/fileDiscovery.d.ts +6 -0
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js +12 -1
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/examples/schema-example.json +364 -0
- package/examples/schema-example.yaml +137 -0
- package/package.json +10 -3
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Schema Analyzer
|
|
3
|
+
* Works across multiple programming languages using the adapter pattern
|
|
4
|
+
* Analyzes code against database schemas
|
|
5
|
+
*/
|
|
6
|
+
import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
|
|
7
|
+
export const DEFAULT_SCHEMA_CONFIG = {
|
|
8
|
+
enableTableUsageTracking: true,
|
|
9
|
+
checkMissingReferences: true,
|
|
10
|
+
checkNamingConventions: true,
|
|
11
|
+
detectUnusedTables: false,
|
|
12
|
+
validateQueryPatterns: true,
|
|
13
|
+
maxQueriesPerFunction: 5,
|
|
14
|
+
requiredSchemas: [],
|
|
15
|
+
schemas: [],
|
|
16
|
+
validateJsonSchemas: true,
|
|
17
|
+
jsonSchemaVersion: 'draft-07',
|
|
18
|
+
allowedJsonTypes: ['string', 'number', 'integer', 'boolean', 'array', 'object', 'null'],
|
|
19
|
+
schemaFilePatterns: ['*.schema.json', '*-schema.json'],
|
|
20
|
+
dataFilePatterns: ['*.data.json', '*.example.json'],
|
|
21
|
+
strictMode: false,
|
|
22
|
+
allowAdditionalProperties: true
|
|
23
|
+
};
|
|
24
|
+
import { promises as fs } from 'fs';
|
|
25
|
+
export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
26
|
+
name = 'schema';
|
|
27
|
+
description = 'Analyzes code against database schemas and validates JSON schemas';
|
|
28
|
+
category = 'database';
|
|
29
|
+
// Track references across files
|
|
30
|
+
tableReferences = new Map();
|
|
31
|
+
columnReferences = new Map();
|
|
32
|
+
/**
|
|
33
|
+
* Override analyze to handle both TypeScript files and JSON files
|
|
34
|
+
*/
|
|
35
|
+
async analyze(files, config) {
|
|
36
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
37
|
+
const codeFiles = files.filter(f => !f.endsWith('.json'));
|
|
38
|
+
// Analyze code files using the standard Universal approach
|
|
39
|
+
const codeResult = codeFiles.length > 0 ? await super.analyze(codeFiles, config) : {
|
|
40
|
+
violations: [],
|
|
41
|
+
errors: [],
|
|
42
|
+
filesProcessed: 0,
|
|
43
|
+
executionTime: 0
|
|
44
|
+
};
|
|
45
|
+
// Analyze JSON schema files
|
|
46
|
+
const jsonResult = await this.analyzeJsonSchemas(jsonFiles, config);
|
|
47
|
+
// Combine results
|
|
48
|
+
return {
|
|
49
|
+
violations: [...codeResult.violations, ...jsonResult.violations],
|
|
50
|
+
errors: [...(codeResult.errors || []), ...(jsonResult.errors || [])],
|
|
51
|
+
filesProcessed: codeResult.filesProcessed + jsonResult.filesProcessed,
|
|
52
|
+
executionTime: (codeResult.executionTime || 0) + (jsonResult.executionTime || 0)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async analyzeAST(ast, adapter, config, sourceCode) {
|
|
56
|
+
const violations = [];
|
|
57
|
+
const finalConfig = { ...DEFAULT_SCHEMA_CONFIG, ...config };
|
|
58
|
+
// Get available schemas
|
|
59
|
+
const schemas = finalConfig.schemas || [];
|
|
60
|
+
const allTables = new Set();
|
|
61
|
+
const tableColumns = new Map();
|
|
62
|
+
// Build lookup maps
|
|
63
|
+
for (const schema of schemas) {
|
|
64
|
+
for (const table of schema.tables) {
|
|
65
|
+
allTables.add(table.name);
|
|
66
|
+
const columns = new Set(table.columns.map(c => c.name));
|
|
67
|
+
tableColumns.set(table.name, columns);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Check if schemas are required but missing
|
|
71
|
+
if (finalConfig.requiredSchemas && finalConfig.requiredSchemas.length > 0 && schemas.length === 0) {
|
|
72
|
+
violations.push(this.createViolation(ast.filePath, { line: 1, column: 1 }, 'No database schemas loaded for analysis', 'warning', 'missing-schemas'));
|
|
73
|
+
return violations;
|
|
74
|
+
}
|
|
75
|
+
// Find table references in the code
|
|
76
|
+
const tableRefs = this.findTableReferences(ast, adapter, sourceCode);
|
|
77
|
+
const columnRefs = this.findColumnReferences(ast, adapter, sourceCode);
|
|
78
|
+
// Check for missing table references
|
|
79
|
+
if (finalConfig.checkMissingReferences) {
|
|
80
|
+
for (const ref of tableRefs) {
|
|
81
|
+
if (!allTables.has(ref.table) && !this.isSystemTable(ref.table)) {
|
|
82
|
+
violations.push(this.createViolation(ast.filePath, ref.location, `Reference to unknown table '${ref.table}'`, 'warning', 'unknown-table'));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Check column references
|
|
86
|
+
for (const ref of columnRefs) {
|
|
87
|
+
const columnSet = tableColumns.get(ref.table);
|
|
88
|
+
if (columnSet && !columnSet.has(ref.column)) {
|
|
89
|
+
violations.push(this.createViolation(ast.filePath, ref.location, `Reference to unknown column '${ref.column}' in table '${ref.table}'`, 'warning', 'unknown-column'));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Check naming conventions
|
|
94
|
+
if (finalConfig.checkNamingConventions) {
|
|
95
|
+
violations.push(...this.checkNamingConventions(tableRefs, ast.filePath));
|
|
96
|
+
}
|
|
97
|
+
// Check query patterns
|
|
98
|
+
if (finalConfig.validateQueryPatterns) {
|
|
99
|
+
violations.push(...this.checkQueryPatterns(ast, adapter, sourceCode, finalConfig));
|
|
100
|
+
}
|
|
101
|
+
// Check for SQL injection patterns
|
|
102
|
+
violations.push(...this.checkSQLInjection(ast, adapter, sourceCode));
|
|
103
|
+
return violations;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Find table references in the code
|
|
107
|
+
*/
|
|
108
|
+
findTableReferences(ast, adapter, sourceCode) {
|
|
109
|
+
const references = [];
|
|
110
|
+
// Common patterns for table references
|
|
111
|
+
const patterns = [
|
|
112
|
+
// ORM patterns
|
|
113
|
+
/from\s*\(\s*["'`]?(\w+)["'`]?\s*\)/gi,
|
|
114
|
+
/table\s*[:=]\s*["'`](\w+)["'`]/gi,
|
|
115
|
+
/\.(\w+Table)\s*\(/gi,
|
|
116
|
+
// SQL patterns
|
|
117
|
+
/FROM\s+["'`]?(\w+)["'`]?/gi,
|
|
118
|
+
/JOIN\s+["'`]?(\w+)["'`]?/gi,
|
|
119
|
+
/UPDATE\s+["'`]?(\w+)["'`]?/gi,
|
|
120
|
+
/INSERT\s+INTO\s+["'`]?(\w+)["'`]?/gi,
|
|
121
|
+
/DELETE\s+FROM\s+["'`]?(\w+)["'`]?/gi,
|
|
122
|
+
/CREATE\s+TABLE\s+["'`]?(\w+)["'`]?/gi,
|
|
123
|
+
// Schema definition patterns
|
|
124
|
+
/pgTable\s*\(\s*["'`](\w+)["'`]/gi,
|
|
125
|
+
/mysqlTable\s*\(\s*["'`](\w+)["'`]/gi,
|
|
126
|
+
/sqliteTable\s*\(\s*["'`](\w+)["'`]/gi
|
|
127
|
+
];
|
|
128
|
+
for (const pattern of patterns) {
|
|
129
|
+
let match;
|
|
130
|
+
while ((match = pattern.exec(sourceCode)) !== null) {
|
|
131
|
+
const table = match[1];
|
|
132
|
+
const index = match.index;
|
|
133
|
+
const lines = sourceCode.substring(0, index).split('\n');
|
|
134
|
+
const line = lines.length;
|
|
135
|
+
const column = lines[lines.length - 1].length + 1;
|
|
136
|
+
references.push({
|
|
137
|
+
table,
|
|
138
|
+
type: this.getQueryType(match[0]),
|
|
139
|
+
location: { line, column },
|
|
140
|
+
context: match[0]
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return references;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Find column references
|
|
148
|
+
*/
|
|
149
|
+
findColumnReferences(ast, adapter, sourceCode) {
|
|
150
|
+
const references = [];
|
|
151
|
+
// Patterns for column references
|
|
152
|
+
const patterns = [
|
|
153
|
+
/(\w+)\.(\w+)\s*[=<>]/gi, // table.column comparison
|
|
154
|
+
/SELECT\s+.*?(\w+)\.(\w+)/gi, // SELECT table.column
|
|
155
|
+
/WHERE\s+.*?(\w+)\.(\w+)/gi, // WHERE table.column
|
|
156
|
+
];
|
|
157
|
+
for (const pattern of patterns) {
|
|
158
|
+
let match;
|
|
159
|
+
while ((match = pattern.exec(sourceCode)) !== null) {
|
|
160
|
+
const table = match[1];
|
|
161
|
+
const column = match[2];
|
|
162
|
+
const index = match.index;
|
|
163
|
+
const lines = sourceCode.substring(0, index).split('\n');
|
|
164
|
+
const line = lines.length;
|
|
165
|
+
const col = lines[lines.length - 1].length + 1;
|
|
166
|
+
references.push({
|
|
167
|
+
table,
|
|
168
|
+
column,
|
|
169
|
+
location: { line, column: col }
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return references;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Check naming conventions
|
|
177
|
+
*/
|
|
178
|
+
checkNamingConventions(references, filePath) {
|
|
179
|
+
const violations = [];
|
|
180
|
+
for (const ref of references) {
|
|
181
|
+
// Check for camelCase tables (should be snake_case)
|
|
182
|
+
if (/[A-Z]/.test(ref.table) && !ref.table.endsWith('Table')) {
|
|
183
|
+
violations.push(this.createViolation(filePath, ref.location, `Table name '${ref.table}' should use snake_case convention`, 'suggestion', 'naming-convention'));
|
|
184
|
+
}
|
|
185
|
+
// Check for reserved words
|
|
186
|
+
const reserved = ['user', 'order', 'group', 'table', 'column', 'index'];
|
|
187
|
+
if (reserved.includes(ref.table.toLowerCase())) {
|
|
188
|
+
violations.push(this.createViolation(filePath, ref.location, `Table name '${ref.table}' is a reserved word. Consider using a different name.`, 'warning', 'reserved-word'));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return violations;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Check query patterns
|
|
195
|
+
*/
|
|
196
|
+
checkQueryPatterns(ast, adapter, sourceCode, config) {
|
|
197
|
+
const violations = [];
|
|
198
|
+
// Count queries per function
|
|
199
|
+
const functions = adapter.extractFunctions(ast);
|
|
200
|
+
for (const func of functions) {
|
|
201
|
+
const funcNode = this.findNodeByLocation(ast.root, func.location.start);
|
|
202
|
+
if (!funcNode)
|
|
203
|
+
continue;
|
|
204
|
+
const funcText = adapter.getNodeText(funcNode, sourceCode);
|
|
205
|
+
const queryCount = this.countQueries(funcText);
|
|
206
|
+
if (queryCount > (config.maxQueriesPerFunction || 5)) {
|
|
207
|
+
violations.push(this.createViolation(ast.filePath, func.location.start, `Function '${func.name}' has ${queryCount} queries, exceeding the maximum of ${config.maxQueriesPerFunction}`, 'warning', 'too-many-queries'));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Check for N+1 query patterns
|
|
211
|
+
if (sourceCode.includes('.map') && (sourceCode.includes('.query') || sourceCode.includes('SELECT'))) {
|
|
212
|
+
violations.push(this.createViolation(ast.filePath, { line: 1, column: 1 }, 'Potential N+1 query pattern detected. Consider using a join or batch query.', 'warning', 'n-plus-one'));
|
|
213
|
+
}
|
|
214
|
+
return violations;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Check for SQL injection vulnerabilities
|
|
218
|
+
*/
|
|
219
|
+
checkSQLInjection(ast, adapter, sourceCode) {
|
|
220
|
+
const violations = [];
|
|
221
|
+
// Patterns that indicate potential SQL injection
|
|
222
|
+
const dangerousPatterns = [
|
|
223
|
+
/query\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*`/g, // Template literals in queries
|
|
224
|
+
/query\s*\(\s*['"][^'"]*['"]?\s*\+/g, // String concatenation
|
|
225
|
+
/execute\s*\(\s*['"][^'"]*['"]?\s*\+/g, // String concatenation in execute
|
|
226
|
+
];
|
|
227
|
+
for (const pattern of dangerousPatterns) {
|
|
228
|
+
if (pattern.test(sourceCode)) {
|
|
229
|
+
violations.push(this.createViolation(ast.filePath, { line: 1, column: 1 }, 'Potential SQL injection vulnerability. Use parameterized queries.', 'critical', 'sql-injection'));
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return violations;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Helper methods
|
|
237
|
+
*/
|
|
238
|
+
getQueryType(context) {
|
|
239
|
+
const upper = context.toUpperCase();
|
|
240
|
+
if (upper.includes('SELECT'))
|
|
241
|
+
return 'select';
|
|
242
|
+
if (upper.includes('INSERT'))
|
|
243
|
+
return 'insert';
|
|
244
|
+
if (upper.includes('UPDATE'))
|
|
245
|
+
return 'update';
|
|
246
|
+
if (upper.includes('DELETE'))
|
|
247
|
+
return 'delete';
|
|
248
|
+
if (upper.includes('CREATE'))
|
|
249
|
+
return 'create';
|
|
250
|
+
return 'reference';
|
|
251
|
+
}
|
|
252
|
+
isSystemTable(table) {
|
|
253
|
+
const systemTables = [
|
|
254
|
+
'information_schema',
|
|
255
|
+
'pg_catalog',
|
|
256
|
+
'mysql',
|
|
257
|
+
'performance_schema',
|
|
258
|
+
'sys'
|
|
259
|
+
];
|
|
260
|
+
return systemTables.some(st => table.toLowerCase().startsWith(st));
|
|
261
|
+
}
|
|
262
|
+
countQueries(text) {
|
|
263
|
+
const patterns = [
|
|
264
|
+
/\.query\s*\(/g,
|
|
265
|
+
/\.execute\s*\(/g,
|
|
266
|
+
/SELECT\s+/gi,
|
|
267
|
+
/INSERT\s+INTO/gi,
|
|
268
|
+
/UPDATE\s+/gi,
|
|
269
|
+
/DELETE\s+FROM/gi
|
|
270
|
+
];
|
|
271
|
+
let count = 0;
|
|
272
|
+
for (const pattern of patterns) {
|
|
273
|
+
const matches = text.match(pattern);
|
|
274
|
+
if (matches)
|
|
275
|
+
count += matches.length;
|
|
276
|
+
}
|
|
277
|
+
return count;
|
|
278
|
+
}
|
|
279
|
+
findNodeByLocation(root, location) {
|
|
280
|
+
const queue = [root];
|
|
281
|
+
while (queue.length > 0) {
|
|
282
|
+
const node = queue.shift();
|
|
283
|
+
if (node.location.start.line === location.line &&
|
|
284
|
+
node.location.start.column === location.column) {
|
|
285
|
+
return node;
|
|
286
|
+
}
|
|
287
|
+
if (node.children) {
|
|
288
|
+
queue.push(...node.children);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Analyze JSON Schema files
|
|
295
|
+
*/
|
|
296
|
+
async analyzeJsonSchemas(files, config) {
|
|
297
|
+
const violations = [];
|
|
298
|
+
const errors = [];
|
|
299
|
+
let filesProcessed = 0;
|
|
300
|
+
const startTime = Date.now();
|
|
301
|
+
const finalConfig = { ...DEFAULT_SCHEMA_CONFIG, ...config };
|
|
302
|
+
if (!finalConfig.validateJsonSchemas) {
|
|
303
|
+
return { violations, errors, filesProcessed, executionTime: 0 };
|
|
304
|
+
}
|
|
305
|
+
// Categorize files
|
|
306
|
+
const schemaFiles = this.identifySchemaFiles(files, finalConfig);
|
|
307
|
+
const dataFiles = this.identifyDataFiles(files, finalConfig);
|
|
308
|
+
const unknownJsonFiles = files.filter(f => !schemaFiles.includes(f) && !dataFiles.includes(f));
|
|
309
|
+
// Load and validate schema files
|
|
310
|
+
const schemas = new Map();
|
|
311
|
+
for (const file of schemaFiles) {
|
|
312
|
+
try {
|
|
313
|
+
const content = await fs.readFile(file, 'utf8');
|
|
314
|
+
const schema = JSON.parse(content);
|
|
315
|
+
schemas.set(file, schema);
|
|
316
|
+
// Validate the JSON Schema itself
|
|
317
|
+
const fileViolations = this.validateJsonSchema(schema, file, finalConfig);
|
|
318
|
+
violations.push(...fileViolations);
|
|
319
|
+
filesProcessed++;
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (error instanceof SyntaxError) {
|
|
323
|
+
violations.push({
|
|
324
|
+
file,
|
|
325
|
+
line: 1,
|
|
326
|
+
column: 1,
|
|
327
|
+
severity: 'critical',
|
|
328
|
+
message: `Invalid JSON: ${error.message}`,
|
|
329
|
+
rule: 'invalid-json',
|
|
330
|
+
analyzer: 'schema'
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
errors.push({
|
|
335
|
+
file,
|
|
336
|
+
error: error instanceof Error ? error.message : String(error)
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
filesProcessed++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Validate data files against schemas
|
|
343
|
+
if (finalConfig.schemaDataPairs) {
|
|
344
|
+
// Use explicit schema-data pairs
|
|
345
|
+
for (const pair of finalConfig.schemaDataPairs) {
|
|
346
|
+
const schema = schemas.get(pair.schema) || await this.loadSchema(pair.schema);
|
|
347
|
+
if (schema) {
|
|
348
|
+
const dataFiles = Array.isArray(pair.data) ? pair.data : [pair.data];
|
|
349
|
+
for (const dataFile of dataFiles) {
|
|
350
|
+
if (files.includes(dataFile)) {
|
|
351
|
+
const dataViolations = await this.validateDataAgainstSchema(dataFile, schema, finalConfig);
|
|
352
|
+
violations.push(...dataViolations);
|
|
353
|
+
filesProcessed++;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// Try to match data files with schemas based on naming conventions
|
|
361
|
+
for (const dataFile of [...dataFiles, ...unknownJsonFiles]) {
|
|
362
|
+
const matchedSchema = this.findMatchingSchema(dataFile, schemas, finalConfig);
|
|
363
|
+
if (matchedSchema) {
|
|
364
|
+
const dataViolations = await this.validateDataAgainstSchema(dataFile, matchedSchema, finalConfig);
|
|
365
|
+
violations.push(...dataViolations);
|
|
366
|
+
}
|
|
367
|
+
else if (unknownJsonFiles.includes(dataFile)) {
|
|
368
|
+
// For unknown files, just check if they're valid JSON
|
|
369
|
+
try {
|
|
370
|
+
const content = await fs.readFile(dataFile, 'utf8');
|
|
371
|
+
JSON.parse(content);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
violations.push({
|
|
375
|
+
file: dataFile,
|
|
376
|
+
line: 1,
|
|
377
|
+
column: 1,
|
|
378
|
+
severity: 'critical',
|
|
379
|
+
message: `Invalid JSON: ${error instanceof Error ? error.message : 'Parse error'}`,
|
|
380
|
+
rule: 'invalid-json',
|
|
381
|
+
analyzer: 'schema'
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
filesProcessed++;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
violations,
|
|
390
|
+
errors,
|
|
391
|
+
filesProcessed,
|
|
392
|
+
executionTime: Date.now() - startTime
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Validate a JSON Schema
|
|
397
|
+
*/
|
|
398
|
+
validateJsonSchema(schema, filePath, config) {
|
|
399
|
+
const violations = [];
|
|
400
|
+
// Check for $schema property
|
|
401
|
+
if (!schema.$schema && config.jsonSchemaVersion) {
|
|
402
|
+
violations.push({
|
|
403
|
+
file: filePath,
|
|
404
|
+
line: 1,
|
|
405
|
+
column: 1,
|
|
406
|
+
severity: 'suggestion',
|
|
407
|
+
message: 'JSON Schema missing $schema declaration',
|
|
408
|
+
rule: 'missing-schema-declaration',
|
|
409
|
+
analyzer: 'schema'
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
// Validate types recursively
|
|
413
|
+
this.validateSchemaTypes(schema, filePath, config, violations);
|
|
414
|
+
// Check for common issues
|
|
415
|
+
if (schema.type === 'object' && schema.properties) {
|
|
416
|
+
// Check for inconsistent required fields
|
|
417
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
418
|
+
for (const field of schema.required) {
|
|
419
|
+
if (!schema.properties[field]) {
|
|
420
|
+
violations.push({
|
|
421
|
+
file: filePath,
|
|
422
|
+
line: 1,
|
|
423
|
+
column: 1,
|
|
424
|
+
severity: 'critical',
|
|
425
|
+
message: `Required field "${field}" not defined in properties`,
|
|
426
|
+
rule: 'undefined-required-field',
|
|
427
|
+
analyzer: 'schema'
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return violations;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Recursively validate schema types
|
|
437
|
+
*/
|
|
438
|
+
validateSchemaTypes(schema, filePath, config, violations, path = '') {
|
|
439
|
+
if (!schema || typeof schema !== 'object')
|
|
440
|
+
return;
|
|
441
|
+
// Check type validity
|
|
442
|
+
if (schema.type && config.allowedJsonTypes) {
|
|
443
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
444
|
+
for (const type of types) {
|
|
445
|
+
if (!config.allowedJsonTypes.includes(type)) {
|
|
446
|
+
violations.push({
|
|
447
|
+
file: filePath,
|
|
448
|
+
line: 1,
|
|
449
|
+
column: 1,
|
|
450
|
+
severity: 'critical',
|
|
451
|
+
message: `Invalid type "${type}" at ${path || 'root'}. Allowed types: ${config.allowedJsonTypes.join(', ')}`,
|
|
452
|
+
rule: 'invalid-type',
|
|
453
|
+
analyzer: 'schema'
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Check numeric constraints
|
|
459
|
+
if (schema.type === 'integer' || schema.type === 'number') {
|
|
460
|
+
if (schema.minimum !== undefined && schema.maximum !== undefined) {
|
|
461
|
+
if (schema.minimum > schema.maximum) {
|
|
462
|
+
violations.push({
|
|
463
|
+
file: filePath,
|
|
464
|
+
line: 1,
|
|
465
|
+
column: 1,
|
|
466
|
+
severity: 'critical',
|
|
467
|
+
message: `Invalid range at ${path}: minimum (${schema.minimum}) > maximum (${schema.maximum})`,
|
|
468
|
+
rule: 'invalid-range',
|
|
469
|
+
analyzer: 'schema'
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Recursively check properties
|
|
475
|
+
if (schema.properties) {
|
|
476
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
477
|
+
this.validateSchemaTypes(value, filePath, config, violations, `${path}.${key}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// Check array items
|
|
481
|
+
if (schema.items) {
|
|
482
|
+
this.validateSchemaTypes(schema.items, filePath, config, violations, `${path}[items]`);
|
|
483
|
+
}
|
|
484
|
+
// Check additional properties
|
|
485
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
486
|
+
this.validateSchemaTypes(schema.additionalProperties, filePath, config, violations, `${path}[additionalProperties]`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Identify schema files based on patterns
|
|
491
|
+
*/
|
|
492
|
+
identifySchemaFiles(files, config) {
|
|
493
|
+
if (!config.schemaFilePatterns)
|
|
494
|
+
return [];
|
|
495
|
+
return files.filter(file => {
|
|
496
|
+
const fileName = file.split('/').pop() || '';
|
|
497
|
+
return config.schemaFilePatterns.some(pattern => {
|
|
498
|
+
const regex = new RegExp(pattern.replace('*', '.*'));
|
|
499
|
+
return regex.test(fileName);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Identify data files based on patterns
|
|
505
|
+
*/
|
|
506
|
+
identifyDataFiles(files, config) {
|
|
507
|
+
if (!config.dataFilePatterns)
|
|
508
|
+
return [];
|
|
509
|
+
return files.filter(file => {
|
|
510
|
+
const fileName = file.split('/').pop() || '';
|
|
511
|
+
return config.dataFilePatterns.some(pattern => {
|
|
512
|
+
const regex = new RegExp(pattern.replace('*', '.*'));
|
|
513
|
+
return regex.test(fileName);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Load a schema from file
|
|
519
|
+
*/
|
|
520
|
+
async loadSchema(schemaPath) {
|
|
521
|
+
try {
|
|
522
|
+
const content = await fs.readFile(schemaPath, 'utf8');
|
|
523
|
+
return JSON.parse(content);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Find matching schema for a data file
|
|
531
|
+
*/
|
|
532
|
+
findMatchingSchema(dataFile, schemas, config) {
|
|
533
|
+
const dataFileName = dataFile.split('/').pop() || '';
|
|
534
|
+
const dataBaseName = dataFileName.replace(/\.(data|example|test)\.json$/, '');
|
|
535
|
+
// Look for schema with matching base name
|
|
536
|
+
for (const [schemaFile, schema] of schemas) {
|
|
537
|
+
const schemaFileName = schemaFile.split('/').pop() || '';
|
|
538
|
+
const schemaBaseName = schemaFileName.replace(/[.-]?schema\.json$/, '');
|
|
539
|
+
if (dataBaseName === schemaBaseName) {
|
|
540
|
+
return schema;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// If only one schema exists, use it for all data files
|
|
544
|
+
if (schemas.size === 1) {
|
|
545
|
+
return schemas.values().next().value;
|
|
546
|
+
}
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Validate data against a JSON Schema
|
|
551
|
+
*/
|
|
552
|
+
async validateDataAgainstSchema(dataFile, schema, config) {
|
|
553
|
+
const violations = [];
|
|
554
|
+
try {
|
|
555
|
+
const content = await fs.readFile(dataFile, 'utf8');
|
|
556
|
+
const data = JSON.parse(content);
|
|
557
|
+
// Validate the data against the schema
|
|
558
|
+
this.validateAgainstSchema(data, schema, dataFile, violations, config);
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
if (error instanceof SyntaxError) {
|
|
562
|
+
violations.push({
|
|
563
|
+
file: dataFile,
|
|
564
|
+
line: 1,
|
|
565
|
+
column: 1,
|
|
566
|
+
severity: 'critical',
|
|
567
|
+
message: `Invalid JSON: ${error.message}`,
|
|
568
|
+
rule: 'invalid-json',
|
|
569
|
+
analyzer: 'schema'
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
violations.push({
|
|
574
|
+
file: dataFile,
|
|
575
|
+
line: 1,
|
|
576
|
+
column: 1,
|
|
577
|
+
severity: 'critical',
|
|
578
|
+
message: `Error reading file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
579
|
+
rule: 'file-error',
|
|
580
|
+
analyzer: 'schema'
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return violations;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Validate data against schema recursively
|
|
588
|
+
*/
|
|
589
|
+
validateAgainstSchema(data, schema, filePath, violations, config, path = '') {
|
|
590
|
+
// Type validation
|
|
591
|
+
if (schema.type) {
|
|
592
|
+
const actualType = Array.isArray(data) ? 'array' :
|
|
593
|
+
data === null ? 'null' :
|
|
594
|
+
typeof data;
|
|
595
|
+
const allowedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
596
|
+
// Special handling for integer vs number
|
|
597
|
+
const isValidType = allowedTypes.some(type => {
|
|
598
|
+
if (type === 'integer') {
|
|
599
|
+
return typeof data === 'number' && Number.isInteger(data);
|
|
600
|
+
}
|
|
601
|
+
return type === actualType;
|
|
602
|
+
});
|
|
603
|
+
if (!isValidType) {
|
|
604
|
+
violations.push({
|
|
605
|
+
file: filePath,
|
|
606
|
+
line: 1,
|
|
607
|
+
column: 1,
|
|
608
|
+
severity: 'critical',
|
|
609
|
+
message: `Type mismatch at ${path || 'root'}: expected ${allowedTypes.join(' | ')}, got ${actualType}`,
|
|
610
|
+
rule: 'type-mismatch',
|
|
611
|
+
analyzer: 'schema'
|
|
612
|
+
});
|
|
613
|
+
return; // Don't validate further if type is wrong
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// String validations
|
|
617
|
+
if (schema.type === 'string' && typeof data === 'string') {
|
|
618
|
+
if (schema.minLength !== undefined && data.length < schema.minLength) {
|
|
619
|
+
violations.push({
|
|
620
|
+
file: filePath,
|
|
621
|
+
line: 1,
|
|
622
|
+
column: 1,
|
|
623
|
+
severity: 'warning',
|
|
624
|
+
message: `String at ${path} too short: ${data.length} < ${schema.minLength}`,
|
|
625
|
+
rule: 'string-too-short',
|
|
626
|
+
analyzer: 'schema'
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
if (schema.maxLength !== undefined && data.length > schema.maxLength) {
|
|
630
|
+
violations.push({
|
|
631
|
+
file: filePath,
|
|
632
|
+
line: 1,
|
|
633
|
+
column: 1,
|
|
634
|
+
severity: 'warning',
|
|
635
|
+
message: `String at ${path} too long: ${data.length} > ${schema.maxLength}`,
|
|
636
|
+
rule: 'string-too-long',
|
|
637
|
+
analyzer: 'schema'
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
if (schema.pattern) {
|
|
641
|
+
const regex = new RegExp(schema.pattern);
|
|
642
|
+
if (!regex.test(data)) {
|
|
643
|
+
violations.push({
|
|
644
|
+
file: filePath,
|
|
645
|
+
line: 1,
|
|
646
|
+
column: 1,
|
|
647
|
+
severity: 'warning',
|
|
648
|
+
message: `String at ${path} doesn't match pattern: ${schema.pattern}`,
|
|
649
|
+
rule: 'pattern-mismatch',
|
|
650
|
+
analyzer: 'schema'
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (schema.format) {
|
|
655
|
+
// Basic format validation
|
|
656
|
+
switch (schema.format) {
|
|
657
|
+
case 'email':
|
|
658
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data)) {
|
|
659
|
+
violations.push({
|
|
660
|
+
file: filePath,
|
|
661
|
+
line: 1,
|
|
662
|
+
column: 1,
|
|
663
|
+
severity: 'warning',
|
|
664
|
+
message: `Invalid email format at ${path}`,
|
|
665
|
+
rule: 'invalid-format',
|
|
666
|
+
analyzer: 'schema'
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
break;
|
|
670
|
+
case 'uuid':
|
|
671
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(data)) {
|
|
672
|
+
violations.push({
|
|
673
|
+
file: filePath,
|
|
674
|
+
line: 1,
|
|
675
|
+
column: 1,
|
|
676
|
+
severity: 'warning',
|
|
677
|
+
message: `Invalid UUID format at ${path}`,
|
|
678
|
+
rule: 'invalid-format',
|
|
679
|
+
analyzer: 'schema'
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
// Add more formats as needed
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// Number validations
|
|
688
|
+
if ((schema.type === 'number' || schema.type === 'integer') && typeof data === 'number') {
|
|
689
|
+
if (schema.minimum !== undefined && data < schema.minimum) {
|
|
690
|
+
violations.push({
|
|
691
|
+
file: filePath,
|
|
692
|
+
line: 1,
|
|
693
|
+
column: 1,
|
|
694
|
+
severity: 'warning',
|
|
695
|
+
message: `Value at ${path} below minimum: ${data} < ${schema.minimum}`,
|
|
696
|
+
rule: 'below-minimum',
|
|
697
|
+
analyzer: 'schema'
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
if (schema.maximum !== undefined && data > schema.maximum) {
|
|
701
|
+
violations.push({
|
|
702
|
+
file: filePath,
|
|
703
|
+
line: 1,
|
|
704
|
+
column: 1,
|
|
705
|
+
severity: 'warning',
|
|
706
|
+
message: `Value at ${path} above maximum: ${data} > ${schema.maximum}`,
|
|
707
|
+
rule: 'above-maximum',
|
|
708
|
+
analyzer: 'schema'
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Array validations
|
|
713
|
+
if (schema.type === 'array' && Array.isArray(data)) {
|
|
714
|
+
if (schema.minItems !== undefined && data.length < schema.minItems) {
|
|
715
|
+
violations.push({
|
|
716
|
+
file: filePath,
|
|
717
|
+
line: 1,
|
|
718
|
+
column: 1,
|
|
719
|
+
severity: 'warning',
|
|
720
|
+
message: `Array at ${path} has too few items: ${data.length} < ${schema.minItems}`,
|
|
721
|
+
rule: 'too-few-items',
|
|
722
|
+
analyzer: 'schema'
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
if (schema.maxItems !== undefined && data.length > schema.maxItems) {
|
|
726
|
+
violations.push({
|
|
727
|
+
file: filePath,
|
|
728
|
+
line: 1,
|
|
729
|
+
column: 1,
|
|
730
|
+
severity: 'warning',
|
|
731
|
+
message: `Array at ${path} has too many items: ${data.length} > ${schema.maxItems}`,
|
|
732
|
+
rule: 'too-many-items',
|
|
733
|
+
analyzer: 'schema'
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
if (schema.items) {
|
|
737
|
+
data.forEach((item, index) => {
|
|
738
|
+
this.validateAgainstSchema(item, schema.items, filePath, violations, config, `${path}[${index}]`);
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// Object validations
|
|
743
|
+
if (schema.type === 'object' && typeof data === 'object' && data !== null) {
|
|
744
|
+
// Check required fields
|
|
745
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
746
|
+
for (const requiredField of schema.required) {
|
|
747
|
+
if (!(requiredField in data)) {
|
|
748
|
+
violations.push({
|
|
749
|
+
file: filePath,
|
|
750
|
+
line: 1,
|
|
751
|
+
column: 1,
|
|
752
|
+
severity: 'critical',
|
|
753
|
+
message: `Missing required field "${requiredField}" at ${path}`,
|
|
754
|
+
rule: 'missing-required-field',
|
|
755
|
+
analyzer: 'schema'
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// Validate properties
|
|
761
|
+
if (schema.properties) {
|
|
762
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
763
|
+
if (key in data) {
|
|
764
|
+
this.validateAgainstSchema(data[key], propSchema, filePath, violations, config, path ? `${path}.${key}` : key);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// Check additional properties
|
|
769
|
+
if (schema.additionalProperties === false || (config.strictMode && !schema.additionalProperties)) {
|
|
770
|
+
const definedKeys = new Set(Object.keys(schema.properties || {}));
|
|
771
|
+
const actualKeys = Object.keys(data);
|
|
772
|
+
for (const key of actualKeys) {
|
|
773
|
+
if (!definedKeys.has(key)) {
|
|
774
|
+
violations.push({
|
|
775
|
+
file: filePath,
|
|
776
|
+
line: 1,
|
|
777
|
+
column: 1,
|
|
778
|
+
severity: config.strictMode ? 'critical' : 'warning',
|
|
779
|
+
message: `Unexpected property "${key}" at ${path}`,
|
|
780
|
+
rule: 'unexpected-property',
|
|
781
|
+
analyzer: 'schema'
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
else if (typeof schema.additionalProperties === 'object') {
|
|
787
|
+
// Validate additional properties against schema
|
|
788
|
+
const definedKeys = new Set(Object.keys(schema.properties || {}));
|
|
789
|
+
for (const [key, value] of Object.entries(data)) {
|
|
790
|
+
if (!definedKeys.has(key)) {
|
|
791
|
+
this.validateAgainstSchema(value, schema.additionalProperties, filePath, violations, config, path ? `${path}.${key}` : key);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// Enum validation
|
|
797
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
798
|
+
if (!schema.enum.includes(data)) {
|
|
799
|
+
violations.push({
|
|
800
|
+
file: filePath,
|
|
801
|
+
line: 1,
|
|
802
|
+
column: 1,
|
|
803
|
+
severity: 'critical',
|
|
804
|
+
message: `Value at ${path} not in enum: ${JSON.stringify(data)}. Allowed: ${schema.enum.join(', ')}`,
|
|
805
|
+
rule: 'enum-mismatch',
|
|
806
|
+
analyzer: 'schema'
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
//# sourceMappingURL=UniversalSchemaAnalyzer.js.map
|