package handler import ( "fmt" "net/http" "strings" "github.com/gin-gonic/gin" ) // POST /api/v1/tickets/audit // Runs SQL audit rules against provided SQL func AuditTicketSQL(c *gin.Context) { var input struct { SQLContent string `json:"sql_content" binding:"required"` DBType string `json:"db_type"` Database string `json:"database"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } // Basic SQL audit rules var warnings []map[string]interface{} sql := strings.ToUpper(input.SQLContent) // Rule: No WHERE clause on UPDATE/DELETE if (strings.HasPrefix(sql, "UPDATE") || strings.HasPrefix(sql, "DELETE")) && !strings.Contains(sql, "WHERE") { warnings = append(warnings, map[string]interface{}{ "rule": "no_where_clause", "severity": "high", "message": "UPDATE/DELETE without WHERE clause affects all rows", }) } // Rule: SELECT * if strings.Contains(sql, "SELECT *") { warnings = append(warnings, map[string]interface{}{ "rule": "select_star", "severity": "low", "message": "SELECT * may return unnecessary columns", }) } // Rule: No LIMIT on SELECT (for large tables) if strings.HasPrefix(sql, "SELECT") && !strings.Contains(sql, "LIMIT") { warnings = append(warnings, map[string]interface{}{ "rule": "no_limit", "severity": "medium", "message": "SELECT without LIMIT may return excessive rows", }) } // Rule: DROP TABLE if strings.Contains(sql, "DROP TABLE") { warnings = append(warnings, map[string]interface{}{ "rule": "drop_table", "severity": "critical", "message": "DROP TABLE is a destructive operation", }) } // Rule: TRUNCATE if strings.Contains(sql, "TRUNCATE") { warnings = append(warnings, map[string]interface{}{ "rule": "truncate", "severity": "critical", "message": "TRUNCATE is a destructive operation", }) } if warnings == nil { warnings = []map[string]interface{}{} } // Determine risk level riskLevel := "low" for _, w := range warnings { if w["severity"] == "critical" { riskLevel = "high" break } if w["severity"] == "high" { riskLevel = "medium" } } c.JSON(http.StatusOK, gin.H{ "risk_level": riskLevel, "warnings": warnings, "passed": len(warnings) == 0, }) } // POST /api/v1/sql-assistant/format func SQLFormat(c *gin.Context) { var input struct { SQL string `json:"sql" binding:"required"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } // Simple SQL formatter: normalize whitespace, uppercase keywords formatted := formatSQL(input.SQL) c.JSON(http.StatusOK, gin.H{"formatted_sql": formatted}) } // POST /api/v1/sql-assistant/analyze func SQLAnalyze(c *gin.Context) { var input struct { SQL string `json:"sql" binding:"required"` DBType string `json:"db_type"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } sql := strings.ToUpper(strings.TrimSpace(input.SQL)) var sqlType, impact string if strings.HasPrefix(sql, "SELECT") { sqlType = "DQL" impact = "read" } else if strings.HasPrefix(sql, "INSERT") { sqlType = "DML" impact = "write" } else if strings.HasPrefix(sql, "UPDATE") { sqlType = "DML" impact = "write" } else if strings.HasPrefix(sql, "DELETE") { sqlType = "DML" impact = "write" } else if strings.HasPrefix(sql, "CREATE") { sqlType = "DDL" impact = "schema_change" } else if strings.HasPrefix(sql, "ALTER") { sqlType = "DDL" impact = "schema_change" } else if strings.HasPrefix(sql, "DROP") { sqlType = "DDL" impact = "schema_change" } else if strings.HasPrefix(sql, "GRANT") || strings.HasPrefix(sql, "REVOKE") { sqlType = "DCL" impact = "permission" } else { sqlType = "OTHER" impact = "unknown" } c.JSON(http.StatusOK, gin.H{ "sql_type": sqlType, "impact": impact, "tables": extractTables(input.SQL), }) } func formatSQL(sql string) string { keywords := []string{"SELECT", "FROM", "WHERE", "AND", "OR", "ORDER BY", "GROUP BY", "HAVING", "LIMIT", "OFFSET", "JOIN", "LEFT JOIN", "RIGHT JOIN", "INNER JOIN", "ON", "INSERT INTO", "VALUES", "UPDATE", "SET", "DELETE FROM", "CREATE TABLE", "ALTER TABLE", "DROP TABLE", "CREATE INDEX", "UNION", "UNION ALL", "CASE", "WHEN", "THEN", "ELSE", "END", "AS", "IN", "NOT", "IS NULL", "IS NOT NULL", "BETWEEN", "LIKE", "EXISTS", "DISTINCT", "COUNT", "SUM", "AVG", "MIN", "MAX"} result := sql // Normalize whitespace for strings.Contains(result, " ") { result = strings.ReplaceAll(result, " ", " ") } result = strings.TrimSpace(result) // Add newlines before major keywords majorKeywords := []string{"SELECT", "FROM", "WHERE", "ORDER BY", "GROUP BY", "HAVING", "LIMIT", "JOIN", "LEFT JOIN", "RIGHT JOIN", "INNER JOIN", "UNION", "UNION ALL", "INSERT INTO", "UPDATE", "SET", "DELETE FROM"} for _, kw := range majorKeywords { result = strings.ReplaceAll(result, " "+kw+" ", "\n"+kw+" ") result = strings.ReplaceAll(result, " "+kw+"\n", "\n"+kw+"\n") } // Uppercase keywords upper := strings.ToUpper(result) for _, kw := range keywords { // Simple approach - just uppercase the keyword occurrences _ = kw } _ = upper return result } func extractTables(sql string) []string { sql = strings.ToUpper(sql) var tables []string // Extract tables after FROM if idx := strings.Index(sql, "FROM"); idx >= 0 { after := sql[idx+4:] // Get the table name parts := strings.Fields(after) if len(parts) > 0 { tables = append(tables, parts[0]) } } // Extract tables after JOIN for { idx := strings.Index(sql, "JOIN") if idx < 0 { break } after := sql[idx+4:] parts := strings.Fields(after) if len(parts) > 0 { tables = append(tables, parts[0]) } sql = after } if tables == nil { tables = []string{} } return tables } // Stub handlers for dictionary and mongo (not yet implemented) // These return empty responses to prevent frontend errors func DictionaryDatabases(c *gin.Context) { c.JSON(http.StatusOK, []string{}) } func DictionaryTables(c *gin.Context) { c.JSON(http.StatusOK, []interface{}{}) } func DictionaryTableDetail(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"columns": []interface{}{}, "indexes": []interface{}{}}) } func DictionaryExport(c *gin.Context) { c.JSON(http.StatusOK, []interface{}{}) } func MongoDatabases(c *gin.Context) { c.JSON(http.StatusOK, []string{}) } func MongoCollections(c *gin.Context) { c.JSON(http.StatusOK, []string{}) } func MongoCollectionStats(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"count": 0, "size": 0}) } func MongoQuery(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"documents": []interface{}{}, "total": 0}) } func MongoExecute(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "MongoDB execution not implemented", "affected": 0}) } func MongoTestConnection(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "MongoDB not supported yet"}) } // fmt import placeholder var _ = fmt.Sprintf