vet/code.go
abhisek e6f6288701
feat: Code analysis framework infra
feat: Building code graph

Refactor to support import processing

Handle relative import name fixup

Add docs for code analysis framework

Update docs to include additional examples

feat: Function call graph

Update code graph to link function decl and calls

Include call node in function calls

feat: Flatten vulnerabilities in CSV reporter

refactor: Maintain separation of concerns for code analysis framework

refactor: Separate storage entities in its own package

feat: Add callback support in code graph builder

docs: Fix code analysis framework docs
Signed-off-by: abhisek <abhisek.datta@gmail.com>
2024-07-11 15:09:11 +05:30

200 lines
5.2 KiB
Go

package main
import (
"fmt"
"strings"
"github.com/safedep/dry/utils"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/code"
"github.com/safedep/vet/pkg/code/languages"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/storage/graph"
"github.com/spf13/cobra"
)
var (
codeAppDirectories = []string{}
codeImportDirectories = []string{}
codeGraphDatabase string
codeLanguage string
)
func newCodeCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "code",
Short: "[EXPERIMENTAL] Perform code analysis with insights data",
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
cmd.Flags().StringArrayVarP(&codeAppDirectories, "src", "", []string{}, "Source code root directory to analyze")
cmd.Flags().StringArrayVarP(&codeImportDirectories, "imports", "", []string{}, "Language specific directory to find imported source")
cmd.Flags().StringVarP(&codeGraphDatabase, "db", "", "", "Path to the database")
cmd.Flags().StringVarP(&codeLanguage, "lang", "", "python", "Language of the source code")
err := cmd.MarkFlagRequired("db")
if err != nil {
logger.Errorf("Failed to mark flag as required: %v", err)
}
cmd.AddCommand(newCodeCreateDatabaseCommand())
cmd.AddCommand(newCodeImportReachabilityCommand())
return cmd
}
func newCodeCreateDatabaseCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create-db",
Short: "Analyse code and create a database for further analysis",
RunE: func(cmd *cobra.Command, args []string) error {
startCreateDatabase()
return nil
},
}
return cmd
}
func newCodeImportReachabilityCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "import-reachability",
Short: "Analyse import reachability",
RunE: func(cmd *cobra.Command, args []string) error {
startImportReachability()
return nil
},
}
return cmd
}
func startCreateDatabase() {
failOnError("code-create-db", internalStartCreateDatabase())
}
func startImportReachability() {
failOnError("code-import-reachability-analysis", internalStartImportReachability())
}
func internalStartImportReachability() error {
codePrintExperimentalWarning()
if utils.IsEmptyString(codeGraphDatabase) {
return fmt.Errorf("no database path provided")
}
// TODO: We need a code graph loader to load the code graph from the database
// before invoking analysis modules
return nil
}
func internalStartCreateDatabase() error {
codePrintExperimentalWarning()
logger.Debugf("Starting code analysis")
if len(codeAppDirectories) == 0 {
return fmt.Errorf("no source code directory provided")
}
if len(codeImportDirectories) == 0 {
return fmt.Errorf("no import directory provided")
}
if utils.IsEmptyString(codeGraphDatabase) {
return fmt.Errorf("no database path provided")
}
codeRepoCfg := code.FileSystemSourceRepositoryConfig{
SourcePaths: codeAppDirectories,
ImportPaths: codeImportDirectories,
}
codeRepo, err := code.NewFileSystemSourceRepository(codeRepoCfg)
if err != nil {
return fmt.Errorf("failed to create source repository: %w", err)
}
codeLang, err := codeGetLanguage()
if err != nil {
return fmt.Errorf("failed to create source language: %w", err)
}
codeRepo.ConfigureForLanguage(codeLang)
graph, err := graph.NewPropertyGraph(&graph.LocalPropertyGraphConfig{
Name: "code-analysis",
DatabasePath: codeGraphDatabase,
})
if err != nil {
return fmt.Errorf("failed to create graph database: %w", err)
}
builderConfig := code.CodeGraphBuilderConfig{
RecursiveImport: true,
}
builder, err := code.NewCodeGraphBuilder(builderConfig, codeRepo, codeLang, graph)
if err != nil {
return fmt.Errorf("failed to create code graph builder: %w", err)
}
redirectLogToFile(logFile)
var fileProcessedTracker any
var importsProcessedTracker any
var functionsProcessedTracker any
builder.RegisterEventHandler("ui-callback",
func(event code.CodeGraphBuilderEvent, metrics code.CodeGraphBuilderMetrics) error {
switch event.Kind {
case code.CodeGraphBuilderEventFileQueued:
ui.IncrementTrackerTotal(fileProcessedTracker, 1)
case code.CodeGraphBuilderEventFileProcessed:
ui.IncrementProgress(fileProcessedTracker, 1)
}
ui.UpdateValue(importsProcessedTracker, int64(metrics.ImportsCount))
ui.UpdateValue(functionsProcessedTracker, int64(metrics.FunctionsCount))
return nil
})
ui.StartProgressWriter()
fileProcessedTracker = ui.TrackProgress("Processing source files", 0)
importsProcessedTracker = ui.TrackProgress("Processing imports", 0)
functionsProcessedTracker = ui.TrackProgress("Processing functions", 0)
err = builder.Build()
if err != nil {
return fmt.Errorf("failed to build code graph: %w", err)
}
ui.MarkTrackerAsDone(fileProcessedTracker)
ui.MarkTrackerAsDone(importsProcessedTracker)
ui.MarkTrackerAsDone(functionsProcessedTracker)
ui.StopProgressWriter()
logger.Debugf("Code analysis completed")
return nil
}
func codePrintExperimentalWarning() {
ui.PrintWarning("Code analysis is experimental and may have breaking change")
}
func codeGetLanguage() (code.SourceLanguage, error) {
lang := strings.ToLower(codeLanguage)
switch lang {
case "python":
return languages.NewPythonSourceLanguage()
default:
return nil, fmt.Errorf("unsupported language: %s", codeLanguage)
}
}