Skip to content
Merged
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
128 changes: 128 additions & 0 deletions sourcecode-parser/cmd/analyze.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package cmd

import (
"fmt"
"strings"

"github.com/shivasurya/code-pathfinder/sourcecode-parser/graph"
"github.com/shivasurya/code-pathfinder/sourcecode-parser/graph/callgraph"
"github.com/spf13/cobra"
)

var analyzeCmd = &cobra.Command{
Use: "analyze",
Short: "Analyze source code for security vulnerabilities using call graph",
Run: func(cmd *cobra.Command, _ []string) {
projectInput := cmd.Flag("project").Value.String()

if projectInput == "" {
fmt.Println("Error: --project flag is required")
return
}

fmt.Println("Building code graph...")
codeGraph := graph.Initialize(projectInput)

fmt.Println("Building call graph and analyzing security patterns...")
cg, registry, patternRegistry, err := callgraph.InitializeCallGraph(codeGraph, projectInput)
if err != nil {
fmt.Println("Error building call graph:", err)
return
}

fmt.Printf("Call graph built successfully: %d functions indexed\n", len(cg.Functions))
fmt.Printf("Module registry: %d modules\n", len(registry.Modules))

// Debug: Print call graph details (commented out for production)
// fmt.Printf("\nDEBUG: Call graph statistics:\n")
// fmt.Printf(" Functions indexed: %d\n", len(cg.Functions))
// for fqn := range cg.Functions {
// fmt.Printf(" - %s\n", fqn)
// }
// fmt.Printf(" Call sites: %d callers\n", len(cg.CallSites))
// for caller, sites := range cg.CallSites {
// fmt.Printf(" %s makes %d calls:\n", caller, len(sites))
// for _, site := range sites {
// fmt.Printf(" - Target: %s, TargetFQN: %s, Resolved: %v\n", site.Target, site.TargetFQN, site.Resolved)
// }
// }
// fmt.Println()

// Run security analysis
matches := callgraph.AnalyzePatterns(cg, patternRegistry)

if len(matches) == 0 {
fmt.Println("\n✓ No security issues found!")
return
}

fmt.Printf("\n⚠ Found %d potential security issues:\n\n", len(matches))
for i, match := range matches {
fmt.Printf("%d. [%s] %s\n", i+1, match.Severity, match.PatternName)
fmt.Printf(" Description: %s\n", match.Description)
fmt.Printf(" CWE: %s, OWASP: %s\n\n", match.CWE, match.OWASP)

// Display source information
if match.SourceFQN != "" {
if match.SourceCall != "" {
fmt.Printf(" Source: %s() calls %s()\n", match.SourceFQN, match.SourceCall)
} else {
fmt.Printf(" Source: %s\n", match.SourceFQN)
}
if match.SourceFile != "" {
fmt.Printf(" at %s:%d\n", match.SourceFile, match.SourceLine)
if match.SourceCode != "" {
printCodeSnippet(match.SourceCode, int(match.SourceLine))
}
}
fmt.Println()
}

// Display sink information
if match.SinkFQN != "" {
if match.SinkCall != "" {
fmt.Printf(" Sink: %s() calls %s()\n", match.SinkFQN, match.SinkCall)
} else {
fmt.Printf(" Sink: %s\n", match.SinkFQN)
}
if match.SinkFile != "" {
fmt.Printf(" at %s:%d\n", match.SinkFile, match.SinkLine)
if match.SinkCode != "" {
printCodeSnippet(match.SinkCode, int(match.SinkLine))
}
}
fmt.Println()
}

// Display data flow path
if len(match.DataFlowPath) > 0 {
fmt.Printf(" Data flow path (%d steps):\n", len(match.DataFlowPath))
for j, step := range match.DataFlowPath {
if j == 0 {
fmt.Printf(" %s (source)\n", step)
} else if j == len(match.DataFlowPath)-1 {
fmt.Printf(" └─> %s (sink)\n", step)
} else {
fmt.Printf(" └─> %s\n", step)
}
}
fmt.Println()
}
}
},
}

func printCodeSnippet(code string, startLine int) {
lines := strings.Split(code, "\n")
for i, line := range lines {
if line != "" {
fmt.Printf(" %4d | %s\n", startLine+i, line)
}
}
}

func init() {
rootCmd.AddCommand(analyzeCmd)
analyzeCmd.Flags().StringP("project", "p", "", "Project directory to analyze (required)")
analyzeCmd.MarkFlagRequired("project") //nolint:all
}
223 changes: 223 additions & 0 deletions sourcecode-parser/cmd/resolution_report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package cmd

import (
"fmt"
"sort"

"github.com/shivasurya/code-pathfinder/sourcecode-parser/graph"
"github.com/shivasurya/code-pathfinder/sourcecode-parser/graph/callgraph"
"github.com/spf13/cobra"
)

var resolutionReportCmd = &cobra.Command{
Use: "resolution-report",
Short: "Generate a diagnostic report on call resolution statistics",
Long: `Analyze the call graph and generate a detailed report showing:
- Overall resolution statistics (resolved vs unresolved)
- Breakdown by failure category
- Top unresolved patterns with occurrence counts

This helps identify why calls are not being resolved and prioritize
improvements to the resolution logic.`,
Run: func(cmd *cobra.Command, _ []string) {
projectInput := cmd.Flag("project").Value.String()

if projectInput == "" {
fmt.Println("Error: --project flag is required")
return
}

fmt.Println("Building code graph...")
codeGraph := graph.Initialize(projectInput)

fmt.Println("Building call graph...")
cg, registry, _, err := callgraph.InitializeCallGraph(codeGraph, projectInput)
if err != nil {
fmt.Printf("Error building call graph: %v\n", err)
return
}

fmt.Printf("\nResolution Report for %s\n", projectInput)
fmt.Println("===============================================")

// Collect statistics
stats := aggregateResolutionStatistics(cg)

// Print overall statistics
printOverallStatistics(stats)
fmt.Println()

// Print failure breakdown
printFailureBreakdown(stats)
fmt.Println()

// Print top unresolved patterns
printTopUnresolvedPatterns(stats, 20)
fmt.Println()

fmt.Printf("Module registry: %d modules\n", len(registry.Modules))
},
}

// resolutionStatistics holds aggregated statistics about call resolution.
type resolutionStatistics struct {
TotalCalls int
ResolvedCalls int
UnresolvedCalls int
FailuresByReason map[string]int // Category -> count
PatternCounts map[string]int // Target pattern -> count
FrameworkCounts map[string]int // Framework prefix -> count (for external_framework category)
UnresolvedByFQN map[string]callgraph.CallSite // For detailed inspection
}

// aggregateResolutionStatistics analyzes the call graph and collects statistics.
func aggregateResolutionStatistics(cg *callgraph.CallGraph) *resolutionStatistics {
stats := &resolutionStatistics{
FailuresByReason: make(map[string]int),
PatternCounts: make(map[string]int),
FrameworkCounts: make(map[string]int),
UnresolvedByFQN: make(map[string]callgraph.CallSite),
}

// Iterate through all call sites
for _, callSites := range cg.CallSites {
for _, site := range callSites {
stats.TotalCalls++

if site.Resolved {
stats.ResolvedCalls++
} else {
stats.UnresolvedCalls++

// Count by failure reason
if site.FailureReason != "" {
stats.FailuresByReason[site.FailureReason]++
} else {
stats.FailuresByReason["uncategorized"]++
}

// Count pattern occurrences
stats.PatternCounts[site.Target]++

// For external frameworks, track which framework
if site.FailureReason == "external_framework" {
// Extract framework prefix (first component before dot)
for idx := 0; idx < len(site.TargetFQN); idx++ {
if site.TargetFQN[idx] == '.' {
framework := site.TargetFQN[:idx]
stats.FrameworkCounts[framework]++
break
}
}
}

// Store for detailed inspection
stats.UnresolvedByFQN[site.TargetFQN] = site
}
}
}

return stats
}

// printOverallStatistics prints the overall resolution statistics.
func printOverallStatistics(stats *resolutionStatistics) {
fmt.Println("Overall Statistics:")
fmt.Printf(" Total calls: %d\n", stats.TotalCalls)
fmt.Printf(" Resolved: %d (%.1f%%)\n",
stats.ResolvedCalls,
percentage(stats.ResolvedCalls, stats.TotalCalls))
fmt.Printf(" Unresolved: %d (%.1f%%)\n",
stats.UnresolvedCalls,
percentage(stats.UnresolvedCalls, stats.TotalCalls))
}

// printFailureBreakdown prints the breakdown of failures by category.
func printFailureBreakdown(stats *resolutionStatistics) {
fmt.Println("Failure Breakdown:")

// Sort categories by count (descending)
type categoryCount struct {
category string
count int
}
categories := make([]categoryCount, 0, len(stats.FailuresByReason))
for cat, count := range stats.FailuresByReason {
categories = append(categories, categoryCount{cat, count})
}
sort.Slice(categories, func(i, j int) bool {
return categories[i].count > categories[j].count
})

// Print each category
for _, cc := range categories {
fmt.Printf(" %-20s %d (%.1f%%)\n",
cc.category+":",
cc.count,
percentage(cc.count, stats.TotalCalls))

// For external frameworks, show framework breakdown
if cc.category == "external_framework" && len(stats.FrameworkCounts) > 0 {
// Sort frameworks by count
type frameworkCount struct {
framework string
count int
}
var frameworks []frameworkCount
for fw, count := range stats.FrameworkCounts {
frameworks = append(frameworks, frameworkCount{fw, count})
}
sort.Slice(frameworks, func(i, j int) bool {
return frameworks[i].count > frameworks[j].count
})

// Print top 5 frameworks
for i, fc := range frameworks {
if i >= 5 {
break
}
fmt.Printf(" %s.*: %d\n", fc.framework, fc.count)
}
}
}
}

// printTopUnresolvedPatterns prints the most common unresolved patterns.
func printTopUnresolvedPatterns(stats *resolutionStatistics, topN int) {
fmt.Printf("Top %d Unresolved Patterns:\n", topN)

// Sort patterns by count (descending)
type patternCount struct {
pattern string
count int
}
patterns := make([]patternCount, 0, len(stats.PatternCounts))
for pattern, count := range stats.PatternCounts {
patterns = append(patterns, patternCount{pattern, count})
}
sort.Slice(patterns, func(i, j int) bool {
return patterns[i].count > patterns[j].count
})

// Print top N patterns
for i, pc := range patterns {
if i >= topN {
break
}
fmt.Printf(" %2d. %-40s %d occurrences\n", i+1, pc.pattern, pc.count)
}
}

// percentage calculates the percentage of part out of total.
func percentage(part, total int) float64 {
if total == 0 {
return 0.0
}
return float64(part) * 100.0 / float64(total)
}

func init() {
rootCmd.AddCommand(resolutionReportCmd)
resolutionReportCmd.Flags().StringP("project", "p", "", "Project root directory")
resolutionReportCmd.MarkFlagRequired("project")
}
Loading
Loading