feat: add stella-callgraph-node for JavaScript/TypeScript call graph extraction
- Implemented a new tool `stella-callgraph-node` that extracts call graphs from JavaScript/TypeScript projects using Babel AST. - Added command-line interface with options for JSON output and help. - Included functionality to analyze project structure, detect functions, and build call graphs. - Created a package.json file for dependency management. feat: introduce stella-callgraph-python for Python call graph extraction - Developed `stella-callgraph-python` to extract call graphs from Python projects using AST analysis. - Implemented command-line interface with options for JSON output and verbose logging. - Added framework detection to identify popular web frameworks and their entry points. - Created an AST analyzer to traverse Python code and extract function definitions and calls. - Included requirements.txt for project dependencies. chore: add framework detection for Python projects - Implemented framework detection logic to identify frameworks like Flask, FastAPI, Django, and others based on project files and import patterns. - Enhanced the AST analyzer to recognize entry points based on decorators and function definitions.
This commit is contained in:
46
bench/reachability-benchmark/cases/go/gin-exec/case.yaml
Normal file
46
bench/reachability-benchmark/cases/go/gin-exec/case.yaml
Normal file
@@ -0,0 +1,46 @@
|
||||
id: "go-gin-exec:301"
|
||||
language: go
|
||||
project: gin-exec
|
||||
version: "1.0.0"
|
||||
description: "Command injection sink reachable via GET /run in Gin handler"
|
||||
entrypoints:
|
||||
- "GET /run"
|
||||
sinks:
|
||||
- id: "CommandInjection::handleRun"
|
||||
path: "main.handleRun"
|
||||
kind: "custom"
|
||||
location:
|
||||
file: main.go
|
||||
line: 22
|
||||
notes: "os/exec.Command with user-controlled input"
|
||||
environment:
|
||||
os_image: "golang:1.22-alpine"
|
||||
runtime:
|
||||
go: "1.22"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "2Gi"
|
||||
build:
|
||||
command: "go build -o outputs/app ."
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/app
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "go test -v ./..."
|
||||
expected_coverage: []
|
||||
expected_traces: []
|
||||
ground_truth:
|
||||
summary: "Command injection reachable"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/go-gin-exec.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -0,0 +1,8 @@
|
||||
case_id: "go-gin-exec:301"
|
||||
entries:
|
||||
http:
|
||||
- id: "GET /run"
|
||||
route: "/run"
|
||||
method: "GET"
|
||||
handler: "main.handleRun"
|
||||
description: "Executes shell command from query parameter"
|
||||
5
bench/reachability-benchmark/cases/go/gin-exec/go.mod
Normal file
5
bench/reachability-benchmark/cases/go/gin-exec/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module gin-exec
|
||||
|
||||
go 1.22
|
||||
|
||||
require github.com/gin-gonic/gin v1.10.0
|
||||
41
bench/reachability-benchmark/cases/go/gin-exec/main.go
Normal file
41
bench/reachability-benchmark/cases/go/gin-exec/main.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// gin-exec benchmark case
|
||||
// Demonstrates command injection sink reachable via Gin HTTP handler
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.GET("/run", handleRun)
|
||||
r.GET("/health", handleHealth)
|
||||
r.Run(":8080")
|
||||
}
|
||||
|
||||
// handleRun - VULNERABLE: command injection sink
|
||||
// User-controlled input passed directly to exec.Command
|
||||
func handleRun(c *gin.Context) {
|
||||
cmd := c.Query("cmd")
|
||||
if cmd == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing cmd parameter"})
|
||||
return
|
||||
}
|
||||
|
||||
// SINK: os/exec.Command with user-controlled input
|
||||
output, err := exec.Command("sh", "-c", cmd).Output()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"output": string(output)})
|
||||
}
|
||||
|
||||
// handleHealth - safe endpoint, no sinks
|
||||
func handleHealth(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
37
bench/reachability-benchmark/cases/go/gin-exec/main_test.go
Normal file
37
bench/reachability-benchmark/cases/go/gin-exec/main_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestHandleHealth(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.Default()
|
||||
r.GET("/health", handleHealth)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/health", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRunMissingCmd(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.Default()
|
||||
r.GET("/run", handleRun)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/run", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# Keep this directory for build outputs
|
||||
46
bench/reachability-benchmark/cases/go/grpc-sql/case.yaml
Normal file
46
bench/reachability-benchmark/cases/go/grpc-sql/case.yaml
Normal file
@@ -0,0 +1,46 @@
|
||||
id: "go-grpc-sql:302"
|
||||
language: go
|
||||
project: grpc-sql
|
||||
version: "1.0.0"
|
||||
description: "SQL injection sink reachable via gRPC GetUser method"
|
||||
entrypoints:
|
||||
- "grpc:UserService.GetUser"
|
||||
sinks:
|
||||
- id: "SqlInjection::GetUser"
|
||||
path: "main.(*userServer).GetUser"
|
||||
kind: "custom"
|
||||
location:
|
||||
file: main.go
|
||||
line: 35
|
||||
notes: "database/sql.Query with string concatenation"
|
||||
environment:
|
||||
os_image: "golang:1.22-alpine"
|
||||
runtime:
|
||||
go: "1.22"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "2Gi"
|
||||
build:
|
||||
command: "go build -o outputs/app ."
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/app
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "go test -v ./..."
|
||||
expected_coverage: []
|
||||
expected_traces: []
|
||||
ground_truth:
|
||||
summary: "SQL injection reachable"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/go-grpc-sql.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -0,0 +1,8 @@
|
||||
case_id: "go-grpc-sql:302"
|
||||
entries:
|
||||
grpc:
|
||||
- id: "grpc:UserService.GetUser"
|
||||
service: "UserService"
|
||||
method: "GetUser"
|
||||
handler: "main.(*userServer).GetUser"
|
||||
description: "Fetches user by ID with SQL injection vulnerability"
|
||||
8
bench/reachability-benchmark/cases/go/grpc-sql/go.mod
Normal file
8
bench/reachability-benchmark/cases/go/grpc-sql/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module grpc-sql
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
google.golang.org/grpc v1.64.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
)
|
||||
86
bench/reachability-benchmark/cases/go/grpc-sql/main.go
Normal file
86
bench/reachability-benchmark/cases/go/grpc-sql/main.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// grpc-sql benchmark case
|
||||
// Demonstrates SQL injection sink reachable via gRPC handler
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// User represents a user record
|
||||
type User struct {
|
||||
ID string
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
// userServer implements the gRPC UserService
|
||||
type userServer struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// GetUser - VULNERABLE: SQL injection sink
|
||||
// User ID is concatenated directly into SQL query
|
||||
func (s *userServer) GetUser(ctx context.Context, userID string) (*User, error) {
|
||||
// SINK: database/sql.Query with string concatenation
|
||||
query := fmt.Sprintf("SELECT id, name, email FROM users WHERE id = '%s'", userID)
|
||||
row := s.db.QueryRow(query)
|
||||
|
||||
var user User
|
||||
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserSafe - SAFE: uses parameterized query
|
||||
func (s *userServer) GetUserSafe(ctx context.Context, userID string) (*User, error) {
|
||||
query := "SELECT id, name, email FROM users WHERE id = ?"
|
||||
row := s.db.QueryRow(query, userID)
|
||||
|
||||
var user User
|
||||
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Initialize schema
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
email TEXT
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create table: %v", err)
|
||||
}
|
||||
|
||||
lis, err := net.Listen("tcp", ":50051")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
|
||||
s := grpc.NewServer()
|
||||
// Register service here (simplified for benchmark)
|
||||
log.Printf("gRPC server listening on %v", lis.Addr())
|
||||
if err := s.Serve(lis); err != nil {
|
||||
log.Fatalf("failed to serve: %v", err)
|
||||
}
|
||||
}
|
||||
56
bench/reachability-benchmark/cases/go/grpc-sql/main_test.go
Normal file
56
bench/reachability-benchmark/cases/go/grpc-sql/main_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
email TEXT
|
||||
);
|
||||
INSERT INTO users (id, name, email) VALUES ('1', 'Alice', 'alice@example.com');
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to setup test data: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func TestGetUserSafe(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
server := &userServer{db: db}
|
||||
user, err := server.GetUserSafe(context.Background(), "1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if user.Name != "Alice" {
|
||||
t.Errorf("expected Alice, got %s", user.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserNotFound(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
server := &userServer{db: db}
|
||||
_, err := server.GetUserSafe(context.Background(), "999")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent user")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# Keep this directory for build outputs
|
||||
Reference in New Issue
Block a user