Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sast-engine/cmd/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ Examples:
builder.InitGoStdlibLoader(goRegistry, projectPath, logger)
goTypeEngine := resolution.NewGoTypeInferenceEngine(goRegistry)

goCG, err := builder.BuildGoCallGraph(codeGraph, goRegistry, goTypeEngine)
goCG, err := builder.BuildGoCallGraph(codeGraph, goRegistry, goTypeEngine, logger)
if err != nil {
logger.Warning("Failed to build Go call graph: %v", err)
} else {
Expand Down
68 changes: 67 additions & 1 deletion sast-engine/cmd/resolution_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"encoding/csv"
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -33,6 +34,7 @@ Use --csv to export unresolved calls with file, line, target, and reason.`,
Run: func(cmd *cobra.Command, _ []string) {
projectInput := cmd.Flag("project").Value.String()
csvOutput := cmd.Flag("csv").Value.String()
dumpJSON := cmd.Flag("dump-callsites-json").Value.String()

if projectInput == "" {
fmt.Println("Error: --project flag is required")
Expand All @@ -58,7 +60,7 @@ Use --csv to export unresolved calls with file, line, target, and reason.`,
builder.InitGoStdlibLoader(goRegistry, projectInput, logger)
builder.InitGoThirdPartyLoader(goRegistry, projectInput, false, logger)
goTypeEngine := resolution.NewGoTypeInferenceEngine(goRegistry)
goCG, goErr := builder.BuildGoCallGraph(codeGraph, goRegistry, goTypeEngine)
goCG, goErr := builder.BuildGoCallGraph(codeGraph, goRegistry, goTypeEngine, logger)
if goErr == nil && goCG != nil {
builder.MergeCallGraphs(cg, goCG)
}
Expand Down Expand Up @@ -115,6 +117,15 @@ Use --csv to export unresolved calls with file, line, target, and reason.`,
fmt.Printf("\nExported %d unresolved calls to %s\n", len(stats.UnresolvedDetails), csvOutput)
}
}

// Export call sites JSON for validation against ground truth
if dumpJSON != "" {
if err := dumpCallSitesJSON(cg, dumpJSON); err != nil {
fmt.Printf("Error writing call sites JSON: %v\n", err)
} else {
fmt.Printf("\nExported call sites to %s\n", dumpJSON)
}
}
},
}

Expand Down Expand Up @@ -543,6 +554,60 @@ func printTopUnresolvedPatterns(stats *resolutionStatistics, topN int) {
}
}

// callSiteRecord is the JSON record written by dumpCallSitesJSON.
type callSiteRecord struct {
File string `json:"file"`
Line int `json:"line"`
Col int `json:"col"`
CallerFQN string `json:"callerFqn"`
Target string `json:"target"`
OurFQN string `json:"ourFqn"`
Resolved bool `json:"resolved"`
TypeSource string `json:"typeSource,omitempty"` // e.g., "go_variable_binding", "thirdparty_local"
IsStdlib bool `json:"isStdlib,omitempty"`
}

// dumpCallSitesJSON writes all Go call sites (resolved + unresolved) to a JSONL file
// so they can be compared against a ground-truth extractor (e.g., go/packages).
func dumpCallSitesJSON(cg *core.CallGraph, outputPath string) error {
f, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create JSON file: %w", err)
}
defer f.Close()

enc := json.NewEncoder(f)
written := 0

for callerFQN, sites := range cg.CallSites {
funcNode := cg.Functions[callerFQN]
// Only emit Go call sites
isGoFunc := funcNode != nil && funcNode.Language == "go"
if !isGoFunc {
continue
}
for _, site := range sites {
rec := callSiteRecord{
File: site.Location.File,
Line: site.Location.Line,
Col: site.Location.Column,
CallerFQN: callerFQN,
Target: site.Target,
OurFQN: site.TargetFQN,
Resolved: site.Resolved,
TypeSource: site.TypeSource,
IsStdlib: site.IsStdlib,
}
if err := enc.Encode(rec); err != nil {
return fmt.Errorf("failed to encode record: %w", err)
}
written++
}
}
fmt.Fprintf(os.Stderr, " wrote %d Go call site records\n", written)
return nil
}

// exportUnresolvedCSV writes all unresolved call sites to a CSV file.
func exportUnresolvedCSV(stats *resolutionStatistics, outputPath string) error {
f, err := os.Create(outputPath)
Expand Down Expand Up @@ -844,4 +909,5 @@ func init() {
resolutionReportCmd.Flags().StringP("project", "p", "", "Project root directory")
resolutionReportCmd.MarkFlagRequired("project")
resolutionReportCmd.Flags().String("csv", "", "Export unresolved calls to CSV file (e.g., --csv unresolved.csv)")
resolutionReportCmd.Flags().String("dump-callsites-json", "", "Export all Go call sites as JSONL for accuracy validation (e.g., --dump-callsites-json callsites.jsonl)")
}
2 changes: 1 addition & 1 deletion sast-engine/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ Examples:

goTypeEngine := resolution.NewGoTypeInferenceEngine(goRegistry)

goCG, err := builder.BuildGoCallGraph(codeGraph, goRegistry, goTypeEngine)
goCG, err := builder.BuildGoCallGraph(codeGraph, goRegistry, goTypeEngine, logger)
if err != nil {
logger.Warning("Failed to build Go call graph: %v", err)
} else {
Expand Down
2 changes: 1 addition & 1 deletion sast-engine/cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func runServe(cmd *cobra.Command, _ []string) error {
builder.InitGoStdlibLoader(goRegistry, projectPath, logger)
server.SetGoContext(goRegistry.GoVersion, goRegistry)
goTypeEngine := resolution.NewGoTypeInferenceEngine(goRegistry)
goCG, err := builder.BuildGoCallGraph(codeGraph, goRegistry, goTypeEngine)
goCG, err := builder.BuildGoCallGraph(codeGraph, goRegistry, goTypeEngine, logger)
if err != nil {
logger.Warning("Failed to build Go call graph: %v", err)
} else {
Expand Down
Loading
Loading