diff --git a/README.md b/README.md index f58c783e..68fbe525 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,16 @@ About Code Pathfinder, the open-source alternative to GitHub CodeQL. Built for advanced structural search, derive insights, find vulnerabilities in code. [](https://github.com/shivasurya/code-pathfinder/actions/workflows/build.yml) [](https://goreportcard.com/report/github.com/shivasurya/code-pathfinder/sourcecode-parser) -[](https://github.com/shivasurya/code-pathfinder/blob/main/LICENSE) +[](https://github.com/shivasurya/code-pathfinder/blob/main/LICENSE) [](https://discord.gg/xmPdJC6WPX) [](https://codecov.io/gh/shivasurya/code-pathfinder) + ## :tv: Demo +Try interactive online playground [here](https://play.codepathfinder.dev/). + ```bash docker run --rm -v "./src:/src" shivasurya/code-pathfinder:stable-latest ci --project /src/code-pathfinder/test-src --ruleset cpf/java ``` diff --git a/docs/src/content/docs/atlas/index.mdx b/docs/src/content/docs/atlas/index.mdx index ad6a3df1..1e569afc 100644 --- a/docs/src/content/docs/atlas/index.mdx +++ b/docs/src/content/docs/atlas/index.mdx @@ -9,6 +9,9 @@ hero: link: /quickstart icon: right-arrow variant: primary + - text: Playground + link: https://play.codepathfinder.dev + icon: forward-slash - text: Documentation link: /overview icon: open-book diff --git a/docs/src/content/docs/blog/code-pathfinder-closure-table-hierarchical-queries.mdx b/docs/src/content/docs/blog/code-pathfinder-closure-table-hierarchical-queries.mdx index d5eaf619..0efc5bc1 100644 --- a/docs/src/content/docs/blog/code-pathfinder-closure-table-hierarchical-queries.mdx +++ b/docs/src/content/docs/blog/code-pathfinder-closure-table-hierarchical-queries.mdx @@ -159,7 +159,7 @@ import { Card } from '@astrojs/starlight/components'; ### Closing Note - Discover [Code-PathFinder](https://github.com/shivasurya/code-pathfinder), the open-source alternative to CodeQL—a powerful tool engineered to detect security vulnerabilities. Unlike grep-based scanners such as Semgrep or ast-grep, Code-PathFinder enables fine-tuning of queries to more effectively eliminate false positives, thanks to its advanced taint analysis and source-to-sink tracing capabilities. Give it a try, and if you encounter any bugs or have suggestions, please file an issue. + Discover [Code-PathFinder](https://github.com/shivasurya/code-pathfinder), the open-source alternative to CodeQL—a powerful tool engineered to detect security vulnerabilities. Unlike grep-based scanners such as ast-grep, Code-PathFinder enables fine-tuning of queries to more effectively eliminate false positives, thanks to its advanced taint analysis and source-to-sink tracing capabilities. Give it a try, and if you encounter any bugs or have suggestions, please file an issue. diff --git a/docs/src/content/docs/blog/finding-webview-misconfigurations-android.mdx b/docs/src/content/docs/blog/finding-webview-misconfigurations-android.mdx index 08464baf..50f4ec7c 100644 --- a/docs/src/content/docs/blog/finding-webview-misconfigurations-android.mdx +++ b/docs/src/content/docs/blog/finding-webview-misconfigurations-android.mdx @@ -132,7 +132,7 @@ import { Card } from '@astrojs/starlight/components'; ### Conclusion While [Code-PathFinder, the open-source alternative to CodeQL](https://codepathfinder.dev), is a powerful tool for finding security vulnerabilities in Android applications, one can always tweak the queries to filter out false positives - more effectively compared to grep-based scanners like `Semgrep` or `ast-grep`. This is because the taint analysis and source-to-sink analysis are far more powerful than grep-based scanners. Give it a try and file an [issue](https://github.com/shivasurya/code-pathfinder/issues) + more effectively compared to grep-based scanners like `ast-grep`. This is because the taint analysis and source-to-sink analysis are far more powerful than grep-based scanners. Give it a try and file an [issue](https://github.com/shivasurya/code-pathfinder/issues) if you find any bugs or have any suggestions. diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index b95d6487..b5394a2c 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -9,15 +9,15 @@ hero: link: /quickstart icon: right-arrow variant: primary + - text: Playground + link: https://play.codepathfinder.dev + icon: forward-slash - text: Browse Rules link: /atlas icon: open-book - text: Tech Blog link: /blog icon: pen - - text: Get Source - link: https://github.com/shivasurya/code-pathfinder - icon: github --- import { Card, CardGrid, Icon } from '@astrojs/starlight/components'; diff --git a/playground-Dockerfile b/playground-Dockerfile new file mode 100644 index 00000000..b0a7a9b8 --- /dev/null +++ b/playground-Dockerfile @@ -0,0 +1,45 @@ +# Use Wolfi as base image with Go support for builder stage +FROM cgr.dev/chainguard/go:latest as builder + +# Set working directory +WORKDIR /build + +# First, copy the sourcecode-parser module +COPY ./sourcecode-parser /build/sourcecode-parser + +# Copy the playground module +COPY ./playground /build/playground + +# Set working directory to playground +WORKDIR /build/playground + +# Build the application with security flags +ENV CGO_ENABLED=1 +RUN go build -o playground + +# Use distroless base image for minimal attack surface +FROM cgr.dev/chainguard/wolfi-base:latest + +# Create non-root user +USER nonroot:nonroot + +# Set working directory +WORKDIR /app + +# Copy the binary from builder +COPY --from=builder --chown=nonroot:nonroot /build/playground/playground /app/ + +# Copy static files +COPY --from=builder --chown=nonroot:nonroot /build/playground/public/static /app/public/static + +# Create and set permissions for temporary directory +RUN mkdir -p /tmp/code-analysis && \ + chmod 0750 /tmp/code-analysis && \ + chown nonroot:nonroot /tmp/code-analysis + +# Expose port 8080 +EXPOSE 8080 + +# Run the application with reduced capabilities +CMD ["/app/playground"] + diff --git a/playground/.dockerignore b/playground/.dockerignore new file mode 100644 index 00000000..75eeea0b --- /dev/null +++ b/playground/.dockerignore @@ -0,0 +1,32 @@ +# Version control +.git +.gitignore + +# Go build artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out + +# IDE directories +.idea/ +.vscode/ + +# Temporary files +*.tmp +*.temp +tmp/ +temp/ + +# Docker files +Dockerfile +.dockerignore + +# Debug files +debug/ + +# Dependencies +/vendor/ diff --git a/playground/README.md b/playground/README.md new file mode 100644 index 00000000..d43f129d --- /dev/null +++ b/playground/README.md @@ -0,0 +1,26 @@ +### Code-Pathfinder Playground + +The Code-Pathfinder Playground is a online interactive app that allows you to analyze code and execute Code-Pathfinder (CodeQL) queries on it. + + + +### Quickstart + +In the playground directory, run: + +```shell +$ go run main.go +``` + +This will start the playground server. Visit `http://localhost:8080` to access the playground. + +### Docker Build + +From the root directory, run: + +```shell +$ podman build --platform linux/amd64 -t docker.io/shivasurya/cpf-playground:latest . -f playground-Dockerfile +``` + +This will build the playground Docker image. + diff --git a/playground/go.mod b/playground/go.mod new file mode 100644 index 00000000..946dde69 --- /dev/null +++ b/playground/go.mod @@ -0,0 +1,19 @@ +module github.com/shivasurya/code-pathfinder/playground + +go 1.24.1 + +replace github.com/shivasurya/code-pathfinder/sourcecode-parser => ../sourcecode-parser + +require ( + github.com/google/uuid v1.6.0 + github.com/shivasurya/code-pathfinder/sourcecode-parser v0.0.0-00010101000000-000000000000 + github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 +) + +require ( + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/expr-lang/expr v1.16.9 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/posthog/posthog-go v1.2.24 // indirect + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect +) diff --git a/playground/go.sum b/playground/go.sum new file mode 100644 index 00000000..9802fc33 --- /dev/null +++ b/playground/go.sum @@ -0,0 +1,22 @@ +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= +github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posthog/posthog-go v1.2.24 h1:A+iG4saBJemo++VDlcWovbYf8KFFNUfrCoJtsc40RPA= +github.com/posthog/posthog-go v1.2.24/go.mod h1:uYC2l1Yktc8E+9FAHJ9QZG4vQf/NHJPD800Hsm7DzoM= +github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 h1:6C8qej6f1bStuePVkLSFxoU22XBS165D3klxlzRg8F4= +github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82/go.mod h1:xe4pgH49k4SsmkQq5OT8abwhWmnzkhpgnXeekbx2efw= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/playground/main.go b/playground/main.go new file mode 100644 index 00000000..d32c3f9d --- /dev/null +++ b/playground/main.go @@ -0,0 +1,42 @@ +// Package main implements a web server for analyzing Java source code and executing CodeQL queries. +// It provides endpoints for code analysis, AST parsing, and visualization. +package main + +import ( + "log" + "net/http" + "os" + "strings" + + "github.com/shivasurya/code-pathfinder/playground/pkg/handlers" + "github.com/shivasurya/code-pathfinder/playground/pkg/middleware" +) + +func main() { + // Create a new mux for better control over middleware + mux := http.NewServeMux() + + // Serve static files with security and logging middleware + fs := http.FileServer(http.Dir("public/static")) + mux.Handle("/", middleware.LoggingMiddleware(fs)) + + // API endpoints with security and logging middleware + mux.Handle("/api/analyze", middleware.LoggingMiddleware(http.HandlerFunc(handlers.AnalyzeHandler))) + mux.Handle("/api/parse", middleware.LoggingMiddleware(http.HandlerFunc(handlers.ParseHandler))) + + // Get port from environment variable or use default + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + // Ensure port starts with : + if !strings.HasPrefix(port, ":") { + port = ":" + port + } + + log.Printf("Starting server on port %s", port) + if err := http.ListenAndServe(port, mux); err != nil { + log.Fatalf("Server failed to start: %v", err) + } +} diff --git a/playground/pkg/ast/parser.go b/playground/pkg/ast/parser.go new file mode 100644 index 00000000..5641f97f --- /dev/null +++ b/playground/pkg/ast/parser.go @@ -0,0 +1,173 @@ +package ast + +import ( + "fmt" + "strings" + + "github.com/shivasurya/code-pathfinder/playground/pkg/models" + sitter "github.com/smacker/go-tree-sitter" + "github.com/smacker/go-tree-sitter/java" +) + +// ParseJavaSource parses Java source code into an AST using tree-sitter +func ParseJavaSource(sourceCode string) (*models.ASTNode, error) { + parser := sitter.NewParser() + parser.SetLanguage(java.GetLanguage()) + + sourceBytes := []byte(sourceCode) + tree := parser.Parse(nil, sourceBytes) + if tree == nil { + return nil, fmt.Errorf("failed to parse source code") + } + defer tree.Close() + + root := tree.RootNode() + if root == nil { + return nil, fmt.Errorf("invalid AST: no root node") + } + + ast := buildASTFromTreeSitter(root, sourceBytes) + if ast == nil { + return nil, fmt.Errorf("failed to build AST") + } + + return ast, nil +} + +// buildASTFromTreeSitter converts a tree-sitter AST into our AST structure +func buildASTFromTreeSitter(node *sitter.Node, sourceBytes []byte) *models.ASTNode { + if node == nil { + return nil + } + + // Create AST node + astNode := &models.ASTNode{ + Type: node.Type(), + Name: node.Type(), + Code: getNodeText(node, sourceBytes), + Line: int(node.StartPoint().Row + 1), + Children: make([]models.ASTNode, 0), + } + + // Process specific node types + switch node.Type() { + case "package_declaration": + astNode.Type = "PackageDeclaration" + astNode.Package = getNodeText(node, sourceBytes) + + case "import_declaration": + astNode.Type = "ImportDeclaration" + astNode.Imports = []string{getNodeText(node, sourceBytes)} + + case "class_declaration": + astNode.Type = "ClassDeclaration" + if nameNode := node.ChildByFieldName("name"); nameNode != nil { + astNode.Name = getNodeText(nameNode, sourceBytes) + } + if superNode := node.ChildByFieldName("superclass"); superNode != nil { + astNode.SuperClass = getNodeText(superNode, sourceBytes) + } + astNode.Modifier = getModifiers(node, sourceBytes) + + case "constructor_declaration": + astNode.Type = "ConstructorDeclaration" + if nameNode := node.ChildByFieldName("name"); nameNode != nil { + astNode.Name = getNodeText(nameNode, sourceBytes) + } + astNode.Arguments = getMethodParameters(node, sourceBytes) + astNode.Modifier = getModifiers(node, sourceBytes) + case "method_declaration": + astNode.Type = "MethodDeclaration" + if nameNode := node.ChildByFieldName("name"); nameNode != nil { + astNode.Name = getNodeText(nameNode, sourceBytes) + } + if typeNode := node.ChildByFieldName("type"); typeNode != nil { + astNode.ReturnType = getNodeText(typeNode, sourceBytes) + } + astNode.Arguments = getMethodParameters(node, sourceBytes) + astNode.Modifier = getModifiers(node, sourceBytes) + + case "field_declaration": + astNode.Type = "FieldDeclaration" + if nameNode := node.ChildByFieldName("name"); nameNode != nil { + astNode.Name = getNodeText(nameNode, sourceBytes) + } + if typeNode := node.ChildByFieldName("type"); typeNode != nil { + astNode.DataType = getNodeText(typeNode, sourceBytes) + } + astNode.Modifier = getModifiers(node, sourceBytes) + astNode.Value = getInitializer(node, sourceBytes) + + case "local_variable_declaration": + astNode.Type = "VariableDeclaration" + if nameNode := node.ChildByFieldName("name"); nameNode != nil { + astNode.Name = getNodeText(nameNode, sourceBytes) + } + if typeNode := node.ChildByFieldName("type"); typeNode != nil { + astNode.DataType = getNodeText(typeNode, sourceBytes) + } + astNode.Value = getInitializer(node, sourceBytes) + + case "method_invocation": + astNode.Type = "MethodInvocation" + if nameNode := node.ChildByFieldName("name"); nameNode != nil { + var args []string + if paramNode := node.ChildByFieldName("arguments"); paramNode != nil { + args = getMethodParameters(paramNode, sourceBytes) + } + // print full method invocation with arguments + astNode.Name = fmt.Sprintf("%s(%s)", getNodeText(nameNode, sourceBytes), strings.Join(args, ", ")) + } + if typeNode := node.ChildByFieldName("type"); typeNode != nil { + astNode.ReturnType = getNodeText(typeNode, sourceBytes) + } + astNode.Arguments = getMethodParameters(node, sourceBytes) + astNode.Modifier = getModifiers(node, sourceBytes) + astNode.Value = getInitializer(node, sourceBytes) + } + + // Process child nodes + for i := 0; i < int(node.NamedChildCount()); i++ { + if child := node.NamedChild(i); child != nil { + if childNode := buildASTFromTreeSitter(child, sourceBytes); childNode != nil { + astNode.Children = append(astNode.Children, *childNode) + } + } + } + + return astNode +} + +// Helper functions for tree-sitter AST processing +func getNodeText(node *sitter.Node, sourceBytes []byte) string { + if node == nil { + return "" + } + return string(node.Content(sourceBytes)) +} + +func getModifiers(node *sitter.Node, sourceBytes []byte) string { + if modNode := node.ChildByFieldName("modifiers"); modNode != nil { + return getNodeText(modNode, sourceBytes) + } + return "" +} + +func getMethodParameters(node *sitter.Node, sourceBytes []byte) []string { + var params []string + if paramsList := node.ChildByFieldName("parameters"); paramsList != nil { + for i := 0; i < int(paramsList.NamedChildCount()); i++ { + if param := paramsList.NamedChild(i); param != nil { + params = append(params, getNodeText(param, sourceBytes)) + } + } + } + return params +} + +func getInitializer(node *sitter.Node, sourceBytes []byte) string { + if initNode := node.ChildByFieldName("initializer"); initNode != nil { + return getNodeText(initNode, sourceBytes) + } + return "" +} diff --git a/playground/pkg/handlers/analyze.go b/playground/pkg/handlers/analyze.go new file mode 100644 index 00000000..e4c5040d --- /dev/null +++ b/playground/pkg/handlers/analyze.go @@ -0,0 +1,125 @@ +package handlers + +import ( + "errors" + "net/http" + "os" + "strings" + "time" + + "github.com/shivasurya/code-pathfinder/playground/pkg/models" + "github.com/shivasurya/code-pathfinder/playground/pkg/utils" + parser "github.com/shivasurya/code-pathfinder/sourcecode-parser/antlr" + "github.com/shivasurya/code-pathfinder/sourcecode-parser/graph" +) + +const ( + // QueryTimeout is the maximum time allowed for query execution + QueryTimeout = 60 * time.Second +) + +// Error definitions +var ( + ErrQueryTimeout = errors.New("query execution timed out") +) + +// Channel types +type resultChannel chan []models.QueryResult + +// AnalyzeHandler processes POST requests to /analyze endpoint. +// It accepts Java source code and a CodeQL query, executes the query using code-pathfinder, +// and returns the query results. The execution is done with a 60-second timeout. +func AnalyzeHandler(w http.ResponseWriter, r *http.Request) { + start := time.Now() + defer utils.LogRequestDuration("analyzeHandler", start) + + if !utils.ValidateMethod(w, r, http.MethodPost) { + return + } + + var req models.AnalyzeRequest + if err := utils.DecodeJSONRequest(w, r, &req); err != nil { + return + } + + tmpDir, err := utils.CreateTempWorkspace("code-analysis-*") + if err != nil { + utils.SendErrorResponse(w, "Failed to create temporary directory", err) + return + } + defer os.RemoveAll(tmpDir) + + if err := utils.WriteSourceAndQueryFiles(tmpDir, req.JavaSource, req.Query); err != nil { + utils.SendErrorResponse(w, "Failed to write files", err) + return + } + + results, err := executeQueryWithTimeout(tmpDir, req.Query) + if err != nil { + utils.SendErrorResponse(w, "Query execution failed", err) + return + } + + utils.SendJSONResponse(w, models.AnalyzeResponse{ + Results: results, + }) +} + +// executeQueryWithTimeout executes the query with a timeout +func executeQueryWithTimeout(tmpDir, queryStr string) ([]models.QueryResult, error) { + resultsChan := make(resultChannel) + errorChan := make(chan error) + + go executeQuery(tmpDir, queryStr, resultsChan, errorChan) + + select { + case results := <-resultsChan: + return results, nil + case err := <-errorChan: + return nil, err + case <-time.After(QueryTimeout): + return nil, ErrQueryTimeout + } +} + +// executeQuery performs the actual query execution +func executeQuery(tmpDir, queryStr string, resultsChan resultChannel, errorChan chan error) { + // Initialize code graph with the temporary directory + codeGraph := graph.Initialize(tmpDir) + + // Parse the query + parsedQuery, err := parser.ParseQuery(queryStr) + if err != nil { + errorChan <- err + return + } + + // Extract WHERE clause + parts := strings.SplitN(queryStr, "WHERE", 2) + if len(parts) > 1 { + parsedQuery.Expression = strings.SplitN(parts[1], "SELECT", 2)[0] + } + + // Execute query on the graph + entities, _ := graph.QueryEntities(codeGraph, parsedQuery) + + // Convert results to QueryResult format + var results []models.QueryResult + for _, entity := range entities { + for _, entityObject := range entity { + + // Create QueryResult + result := models.QueryResult{ + File: "Main.java", + Line: int64(entityObject.LineNumber), + Snippet: entityObject.CodeSnippet, + Kind: entityObject.Type, // Use the Type field from entityObject + } + + // Add the result to the list + results = append(results, result) + } + } + + resultsChan <- results +} diff --git a/playground/pkg/handlers/parse.go b/playground/pkg/handlers/parse.go new file mode 100644 index 00000000..0062bcec --- /dev/null +++ b/playground/pkg/handlers/parse.go @@ -0,0 +1,43 @@ +package handlers + +import ( + "net/http" + "strings" + "time" + + "github.com/shivasurya/code-pathfinder/playground/pkg/ast" + "github.com/shivasurya/code-pathfinder/playground/pkg/models" + "github.com/shivasurya/code-pathfinder/playground/pkg/utils" +) + +// ParseHandler processes POST requests to /parse endpoint. +// It accepts Java source code, parses it into an AST using tree-sitter, +// and returns the AST structure for visualization. +func ParseHandler(w http.ResponseWriter, r *http.Request) { + start := time.Now() + defer utils.LogRequestDuration("parseHandler", start) + + if !utils.ValidateMethod(w, r, http.MethodPost) { + return + } + + var req models.ParseRequest + if err := utils.DecodeJSONRequest(w, r, &req); err != nil { + return + } + + // Validate source code is not empty + if strings.TrimSpace(req.JavaSource) == "" { + utils.SendErrorResponse(w, "Source code cannot be empty", nil) + return + } + + // Parse the Java source code + ast, err := ast.ParseJavaSource(req.JavaSource) + if err != nil { + utils.SendErrorResponse(w, "Failed to parse source code", err) + return + } + + utils.SendJSONResponse(w, models.ParseResponse{AST: ast}) +} diff --git a/playground/pkg/middleware/logging.go b/playground/pkg/middleware/logging.go new file mode 100644 index 00000000..7d1abf37 --- /dev/null +++ b/playground/pkg/middleware/logging.go @@ -0,0 +1,82 @@ +package middleware + +import ( + "log" + "net/http" + "strings" + "time" + + "github.com/google/uuid" +) + +// LoggingResponseWriter is a custom response writer that captures the status code +type LoggingResponseWriter struct { + http.ResponseWriter + statusCode int +} + +// WriteHeader captures the status code +func (lrw *LoggingResponseWriter) WriteHeader(code int) { + lrw.statusCode = code + lrw.ResponseWriter.WriteHeader(code) +} + +// LoggingMiddleware logs request details with security context +func LoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Get client IP with security checks + clientIP := r.Header.Get("X-Real-IP") + if clientIP == "" { + clientIP = r.Header.Get("X-Forwarded-For") + if clientIP == "" { + // Extract IP without port + clientIP = strings.Split(r.RemoteAddr, ":")[0] + } + } + + // Get user agent (sanitized) + userAgent := strings.ReplaceAll(r.Header.Get("User-Agent"), "\n", "") + + // Get request ID or generate one + requestID := r.Header.Get("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + } + + // Add request ID to response headers for tracing + w.Header().Set("X-Request-ID", requestID) + + // Create a response writer that captures the status code + lrw := &LoggingResponseWriter{ + ResponseWriter: w, + statusCode: http.StatusOK, + } + + // Call the next handler + next.ServeHTTP(lrw, r) + + // Calculate request duration + duration := time.Since(start) + + // Get content length + contentLength := r.Header.Get("Content-Length") + + // Log request details with security context + log.Printf("[%s] [%s] Method=%s Path=%s IP=%s Status=%d Duration=%v Size=%s UA=%s Referer=%s Protocol=%s Host=%s", + time.Now().Format(time.RFC3339), + requestID, + r.Method, + r.URL.Path, + clientIP, + lrw.statusCode, + duration, + contentLength, + userAgent, + r.Referer(), + r.Proto, + r.Host, + ) + }) +} diff --git a/playground/pkg/models/models.go b/playground/pkg/models/models.go new file mode 100644 index 00000000..eb1e1faa --- /dev/null +++ b/playground/pkg/models/models.go @@ -0,0 +1,21 @@ +package models + +// AnalyzeRequest represents the request body for code analysis +type AnalyzeRequest struct { + JavaSource string `json:"javaSource"` + Query string `json:"query"` + Category string `json:"category"` // Technology or language-based category +} + +// AnalyzeResponse represents the response from code analysis +type AnalyzeResponse struct { + Results []QueryResult `json:"results"` +} + +// QueryResult represents a single result from query execution +type QueryResult struct { + File string `json:"file"` + Line int64 `json:"line"` + Snippet string `json:"snippet"` + Kind string `json:"kind,omitempty"` // Type of the node or result +} diff --git a/playground/pkg/models/types.go b/playground/pkg/models/types.go new file mode 100644 index 00000000..54c0f6b5 --- /dev/null +++ b/playground/pkg/models/types.go @@ -0,0 +1,28 @@ +package models + +// ParseRequest represents the input for AST parsing +type ParseRequest struct { + JavaSource string `json:"code"` +} + +// ParseResponse represents the output from AST parsing +type ParseResponse struct { + AST *ASTNode `json:"ast"` +} + +// ASTNode represents a node in the Abstract Syntax Tree +type ASTNode struct { + Type string `json:"type"` + Name string `json:"name"` + Code string `json:"code"` + Line int `json:"line"` + Modifier string `json:"modifier,omitempty"` + Value string `json:"value,omitempty"` + Package string `json:"package,omitempty"` + Imports []string `json:"imports,omitempty"` + SuperClass string `json:"superClass,omitempty"` + DataType string `json:"dataType,omitempty"` + ReturnType string `json:"returnType,omitempty"` + Arguments []string `json:"arguments,omitempty"` + Children []ASTNode `json:"children"` +} diff --git a/playground/pkg/utils/files.go b/playground/pkg/utils/files.go new file mode 100644 index 00000000..05d91a1e --- /dev/null +++ b/playground/pkg/utils/files.go @@ -0,0 +1,58 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/google/uuid" +) + +const ( + // FilePermissions for created files + FilePermissions = 0644 +) + +// CreateTempWorkspace creates a temporary directory for analysis +func CreateTempWorkspace(prefix string) (string, error) { + // Create a unique directory name + dirName := fmt.Sprintf("%s-%s", prefix, uuid.New().String()) + tmpDir := filepath.Join(os.TempDir(), dirName) + + // Create directory with secure permissions + if err := os.MkdirAll(tmpDir, 0700); err != nil { + return "", fmt.Errorf("failed to create temp directory: %v", err) + } + + return tmpDir, nil +} + +// WriteSourceAndQueryFiles writes the source and query files to the workspace +func WriteSourceAndQueryFiles(dir, source, query string) error { + if err := WriteSourceFile(dir, source); err != nil { + return err + } + return WriteFile(filepath.Join(dir, "query.ql"), query) +} + +// WriteSourceFile writes the Java source code to a file +func WriteSourceFile(dir, source string) error { + return WriteFile(filepath.Join(dir, "Main.java"), source) +} + +// WriteFile writes content to a file with proper permissions +func WriteFile(path, content string) error { + // Create or truncate the file + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, FilePermissions) + if err != nil { + return fmt.Errorf("failed to create file: %v", err) + } + defer file.Close() + + // Write content + if _, err := file.WriteString(content); err != nil { + return fmt.Errorf("failed to write content: %v", err) + } + + return nil +} diff --git a/playground/pkg/utils/http.go b/playground/pkg/utils/http.go new file mode 100644 index 00000000..fc65e0fc --- /dev/null +++ b/playground/pkg/utils/http.go @@ -0,0 +1,46 @@ +package utils + +import ( + "encoding/json" + "log" + "net/http" + "time" +) + +// LogRequestDuration logs the duration of a request +func LogRequestDuration(handler string, start time.Time) { + log.Printf("%s took %v", handler, time.Since(start)) +} + +// ValidateMethod checks if the request method matches the expected method +func ValidateMethod(w http.ResponseWriter, r *http.Request, method string) bool { + if r.Method != method { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return false + } + return true +} + +// DecodeJSONRequest decodes a JSON request body into a struct +func DecodeJSONRequest(w http.ResponseWriter, r *http.Request, v interface{}) error { + if err := json.NewDecoder(r.Body).Decode(v); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return err + } + return nil +} + +// SendJSONResponse sends a JSON response +func SendJSONResponse(w http.ResponseWriter, v interface{}) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(v); err != nil { + log.Printf("Error encoding response: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } +} + +// SendErrorResponse sends an error response +func SendErrorResponse(w http.ResponseWriter, message string, err error) { + log.Printf("Error: %s: %v", message, err) + http.Error(w, message, http.StatusInternalServerError) +} diff --git a/playground/public/static/index.html b/playground/public/static/index.html new file mode 100644 index 00000000..adc5fbbb --- /dev/null +++ b/playground/public/static/index.html @@ -0,0 +1,147 @@ + + +
+ + + +Type: ${node.type}
+Line: ${node.line || 'N/A'}
+ ${node.name ? `Name: ${node.name}
` : ''} + `; + highlightNodes([node]); + } + } + }); + + // Handle node deselection + network.on('deselectNode', function() { + const details = document.getElementById('nodeDetails'); + details.innerHTML = ''; + // Reset node highlighting + if (currentNodes.length > 0) { + highlightNodes([]); + } + }); + + // Handle zoom events + network.on('zoom', function() { + const scale = network.getScale(); + const zoomLevel = document.querySelector('.zoom-level'); + if (zoomLevel) { + zoomLevel.textContent = `${Math.round(scale * 100)}%`; + } + }); + + // Handle stabilization events + network.on('stabilizationProgress', function(params) { + const progress = Math.round(params.iterations / params.total * 100); + console.log(`Stabilizing: ${progress}%`); + }); + + network.on('stabilizationIterationsDone', function() { + console.log('Network stabilized'); + }); +} diff --git a/playground/public/static/style.css b/playground/public/static/style.css new file mode 100644 index 00000000..b394337e --- /dev/null +++ b/playground/public/static/style.css @@ -0,0 +1,765 @@ +/* Reset and base styles */ +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + margin: 0; + padding: 0; + background-color: #111111; + color: #ffffff; + font-feature-settings: 'liga' 1, 'calt' 1; /* Enable ligatures */ +} + +/* App Container */ +.app-container { + display: flex; + flex-direction: column; + height: 100vh; +} + +/* Header */ +.header { + background-color: #111111; + padding: 0.8rem 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #3d3d3d; + position: relative; +} + +/* Error Message */ +.error-message { + position: fixed; + top: 20px; + right: 20px; + background-color: #ff4444; + color: white; + padding: 12px 20px; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + z-index: 1000; + font-family: 'Inter', sans-serif; + font-size: 14px; + max-width: 400px; + word-wrap: break-word; + display: none; + animation: fadeIn 0.3s ease-in-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +.header h1 { + margin: 0; + font-family: 'Space Grotesk', sans-serif; + font-size: 1.2rem; + font-weight: 600; + color: #61dafb; + letter-spacing: 0.5px; + text-shadow: 0 0 10px rgba(97, 218, 251, 0.3); +} + +.view-controls button { + background: transparent; + border: 1px solid #4d4d4d; + color: #ffffff; + padding: 0.4rem 1rem; + margin-left: 0.5rem; + cursor: pointer; + transition: all 0.2s; + font-size: 0.9rem; + border-radius: 3px; +} + +.view-controls button:hover { + border-color: #61dafb; + background-color: rgba(97, 218, 251, 0.1); +} + +.view-controls button.active { + background-color: #61dafb; + border-color: #61dafb; + color: #1e1e1e; +} + +/* Main Content */ +.main-content { + display: flex; + flex: 1; + overflow: hidden; + position: relative; +} + +/* Gutter for resizable panels */ +.gutter { + background-color: #2d2d2d; + position: absolute; + z-index: 10; + touch-action: none; +} + +.gutter-horizontal { + cursor: col-resize; + width: 6px; + height: 100%; + left: 50%; + transform: translateX(-50%); +} + +.gutter:hover { + background-color: #61dafb; +} + +.gutter.active { + background-color: #61dafb; +} + +/* Panel Headers */ +.panel-header { + padding: 0.8rem 1rem; + background-color: #2d2d2d; + border-bottom: 1px solid #3d3d3d; + display: flex; + justify-content: space-between; + align-items: center; +} + +.panel-header h2 { + margin: 0; + font-family: 'Space Grotesk', sans-serif; + font-size: 0.9rem; + font-weight: 500; + color: #e2e2e2; + text-transform: uppercase; + letter-spacing: 0.5px; + text-shadow: 0 0 8px rgba(226, 226, 226, 0.2); +} + +/* Left Panel */ +.left-panel { + width: 50%; + background-color: #111111; + display: flex; + flex-direction: column; + transition: width 0.1s ease; +} + +.editor-container { + flex: 1; + display: flex; + flex-direction: column; + position: relative; + overflow: auto; + min-height: 0; +} + +/* Right Panel */ +.right-panel { + width: 50%; + background-color: #111111; + display: flex; + flex-direction: column; + transition: width 0.1s ease; +} + +/* Query Console */ +.query-console { + height: 300px; + background-color: #111111; + border-top: 1px solid #3d3d3d; + display: flex; + flex-direction: column; +} + +/* Results Table Styles */ +.results-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + font-family: 'Space Grotesk', sans-serif; + margin: 8px 0; + border: 1px solid #2d2d2d; + border-radius: 4px; + overflow: hidden; +} + +.results-table th, +.results-table td { + padding: 8px 12px; + text-align: left; + border-bottom: 1px solid #2d2d2d; +} + +.results-table th { + color: #8b949e; + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + background: #1e1e1e; +} + +.results-table tr:hover { + background: rgba(97, 218, 251, 0.05); +} + +/* CodeMirror Search Highlights */ +.CodeMirror-search-match { + background: rgba(97, 218, 251, 0.1); +} + +.CodeMirror-search-text { + background: rgba(97, 218, 251, 0.2); + border-radius: 2px; +} + +.CodeMirror-search-marker { + width: 8px; + height: 8px; + border-radius: 50%; + background: #61dafb; + margin-top: 6px; + box-shadow: 0 0 8px rgba(97, 218, 251, 0.4); +} + +.file-cell { + color: #e2e2e2; + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.line-cell { + color: #8b949e; + text-align: right; + width: 50px; + font-family: monospace; +} + +.kind-cell { + color: #61dafb; + font-family: monospace; + transition: color 0.2s ease; +} + +.kind-cell[data-category="java"] { + color: #61dafb; +} + +.kind-cell[data-category="java"]:hover { + color: #61dafb; + text-shadow: 0 0 8px rgba(97, 218, 251, 0.3); +} + +.results-table-row:hover { + border-left-color: #61dafb; + background: rgba(97, 218, 251, 0.05); +} + +/* Interactive hover effects */ +.results-table-row:hover { + background: rgba(97, 218, 251, 0.05); +} + +.results-table-row:hover .kind-cell[data-category="java"] { + color: #61dafb; + text-shadow: 0 0 8px rgba(97, 218, 251, 0.3); +} + +.results-table-row:hover .kind-cell[data-category="android"] { + color: #a5d6a7; + text-shadow: 0 0 8px rgba(165, 214, 167, 0.3); +} + +/* Smooth transitions */ +.results-table-row, +.kind-cell { + transition: all 0.2s ease; +} + + + +.results-table-row:not(.results-table-header):hover { + background: #2d2d2d; +} + +.header-cell { + font-weight: 600; + color: #e2e2e2; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-family: 'Space Grotesk', sans-serif; + position: relative; + padding-bottom: 0.5rem; +} + +.header-cell::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 2rem; + height: 2px; + background: #3d3d3d; + transition: width 0.2s ease; +} + +.results-table-header:hover .header-cell::after { + width: 3rem; + background: #61dafb; +} + +.file-cell { + color: #e2e2e2; + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.line-cell { + color: #8b949e; + text-align: right; + width: 50px; + font-family: monospace; +} + +.kind-cell { + color: #61dafb; + font-family: monospace; + transition: color 0.2s ease; +} + +.kind-cell[data-category="java"]:hover { + color: #61dafb; +} + +/* Category-specific colors */ +.kind-cell[data-category="java"] { + color: #61dafb; +} + +.kind-cell[data-category="android"] { + color: #a5d6a7; +} + +.kind-cell::before { + content: ''; + position: absolute; + left: 0; + width: 3px; + height: 16px; + background: currentColor; + border-radius: 2px; + opacity: 0.7; +} + + + + + +.results-table-body .results-table-row:not(:last-child) { + border-bottom: 1px solid #3d3d3d; +} + +.query-input-container { + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; + background-color: #111111; + height: 200px; + position: relative; + overflow: hidden; +} + +.query-input-container .CodeMirror { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + height: 100% !important; + font-size: 14px; + line-height: 1.6; + padding: 8px; + border-radius: 4px; + background-color: #1e1e1e; + font-family: 'Fira Code', monospace; +} + +.button-group { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.action-btn { + display: flex; + align-items: center; + gap: 8px; + background-color: #2d8632; + color: #ffffff; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.action-btn:hover { + background-color: #2d6a4f; + transform: translateY(-1px); + box-shadow: + inset 0 0 15px rgba(255, 255, 255, 0.2), + 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.btn-icon { + width: 16px; + height: 16px; + opacity: 0.9; +} + +#queryEditor { + flex-grow: 1; + height: 60px; +} + +.run-query-btn { + background-color: #1b4332; + color: #ffffff; + border: none; + padding: 6px; + border-radius: 4px; + cursor: pointer; + height: 32px; + width: 32px; + transition: all 0.2s ease; + margin-top: 4px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.1); +} + +.run-query-btn::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%); + transform: rotate(45deg); + transition: all 0.3s ease; + pointer-events: none; + opacity: 0; +} + +.query-icon { + width: 16px; + height: 16px; +} + +.run-query-btn:hover { + background-color: #2d6a4f; + transform: scale(1.05); + box-shadow: + inset 0 0 15px rgba(255, 255, 255, 0.2), + 0 0 10px rgba(45, 106, 79, 0.5); +} + +.run-query-btn:hover::before { + opacity: 1; +} + +.run-query-btn:active { + transform: scale(0.95); +} + +.query-results { + flex-grow: 1; + padding: 10px; + background-color: #111111; + overflow-y: auto; + font-family: 'Fira Code', monospace; + font-size: 13px; + color: #d4d4d4; +} + +.query-results pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +.visualization-container { + flex: 1; + position: relative; + overflow: hidden; + background-color: #111111; + min-height: 300px; + background-image: radial-gradient(circle at 2px 2px, rgba(97, 218, 251, 0.15) 2px, transparent 0); + background-size: 25px 25px; +} + +#visualization { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; +} + +/* CodeMirror Customization */ +.CodeMirror { + height: 100% !important; + font-family: 'Fira Code', monospace; + background-color: #1e1e1e !important; + font-size: 14px; + line-height: 1.6; + padding: 1rem 0; +} + +.CodeMirror-gutters { + border-right: 1px solid #3d3d3d !important; + background-color: #252526 !important; +} + +.CodeMirror-linenumber { + color: #6b6b6b !important; +} + +/* Graph Visualization */ +#visualization { + width: 100%; + height: 100%; +} + +.node circle { + stroke: #2d2d2d; + stroke-width: 2px; +} + +.node text { + fill: #ffffff; + font-size: 12px; + font-family: 'Fira Code', monospace; +} + +.link { + stroke: #4d4d4d; + stroke-width: 1px; + opacity: 0.6; +} + +/* Legend */ +.legend { + display: flex; + gap: 20px; + align-items: center; + margin-left: auto; + background-color: rgba(97, 218, 251, 0.05); + padding: 8px 12px; + border-radius: 4px; + border: 1px solid rgba(97, 218, 251, 0.1); + margin-bottom: 10px; +} + +.legend-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.75rem; + letter-spacing: 0.3px; + color: #e2e2e2; +} + +.dot { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; + margin-right: 8px; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} + +.dot.class { + background-color: #4CAF50; + border: 2px solid hsl(134, 73%, 65%); +} + +.dot.method { + background-color: #2196F3; + border: 2px solid #2196F3; +} + +.dot.field { + background-color: #FF9800; + border: 2px solid #FF9800; +} + +.dot.variable { + background-color: #FF5722; + border: 2px solid #FF5722; +} + +.dot { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} + +/* Zoom controls */ +.zoom-controls { + position: absolute; + bottom: 20px; + right: 20px; + display: flex; + gap: 0.5rem; +} + +.zoom-controls button { + background: rgba(45, 45, 45, 0.9); + border: 1px solid #4d4d4d; + color: #ffffff; + width: 32px; + height: 32px; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + transition: all 0.2s; +} + +.zoom-controls button:hover { + border-color: #61dafb; + background: rgba(45, 45, 45, 1); +} + +/* Tabbed Interface */ +.tab-container { + display: flex; + gap: 1rem; + margin-bottom: 1rem; +} + +.tab-button { + background: #2a2a2a; + border: none; + color: #ffffff; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-family: 'Inter', sans-serif; + font-weight: 500; + transition: background-color 0.2s; +} + +.tab-button:hover { + background: #3a3a3a; +} + +.tab-button.active { + background: #4a4a4a; +} + +.tab-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.tab-pane { + display: none; + flex: 1; +} + +.tab-pane.active { + display: flex; +} + +.query-console { + border-top: 1px solid #3a3a3a; +} + +.query-console .panel-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.button-group { + display: flex; + gap: 0.5rem; +} + +.action-btn { + background: #2d8632; + border: none; + color: #ffffff; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + justify-content: center; + transition: all 0.2s; + font-weight: 500; +} + +.action-btn:hover { + background: #36a03c; + transform: translateY(-1px); +} + +.btn-icon { + width: 16px; + height: 16px; +} + +.query-results { + background: #1e1e1e; + border-radius: 4px; + padding: 1rem; + overflow: auto; + flex: 1; +} + +.result-item { + margin-bottom: 1rem; + padding: 1rem; + background: #2a2a2a; + border-radius: 4px; +} + +.result-location { + color: #9e9e9e; + font-size: 0.9rem; + margin-bottom: 0.5rem; +} + +.result-snippet { + margin: 0; + padding: 0.5rem; + background: #1e1e1e; + border-radius: 2px; + font-family: monospace; + white-space: pre-wrap; +}