code-intelligence.com/cifuzz@v0.40.0/pkg/finding/finding.go (about)

     1  package finding
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/alexflint/go-filemutex"
    13  	"github.com/otiai10/copy"
    14  	"github.com/pkg/errors"
    15  
    16  	"code-intelligence.com/cifuzz/internal/config"
    17  	"code-intelligence.com/cifuzz/pkg/log"
    18  	"code-intelligence.com/cifuzz/pkg/parser/libfuzzer/stacktrace"
    19  	"code-intelligence.com/cifuzz/util/fileutil"
    20  	"code-intelligence.com/cifuzz/util/sliceutil"
    21  )
    22  
    23  const (
    24  	nameCrashingInput = "crashing-input"
    25  	nameJSONFile      = "finding.json"
    26  	nameFindingsDir   = ".cifuzz-findings"
    27  	lockFile          = ".lock"
    28  )
    29  
    30  type Finding struct {
    31  	Name               string        `json:"name,omitempty"`
    32  	Type               ErrorType     `json:"type,omitempty"`
    33  	InputData          []byte        `json:"input_data,omitempty"`
    34  	Logs               []string      `json:"logs,omitempty"`
    35  	Details            string        `json:"details,omitempty"`
    36  	HumanReadableInput string        `json:"human_readable_input,omitempty"`
    37  	MoreDetails        *ErrorDetails `json:"more_details,omitempty"`
    38  	Tag                uint64        `json:"tag,omitempty"`
    39  
    40  	// Note: The following fields don't exist in the protobuf
    41  	// representation used in the Code Intelligence core repository.
    42  	CreatedAt  time.Time                `json:"created_at,omitempty"`
    43  	InputFile  string                   `json:"input_file,omitempty"`
    44  	StackTrace []*stacktrace.StackFrame `json:"stack_trace,omitempty"`
    45  
    46  	seedPath string
    47  
    48  	// We also store the name of the fuzz test that found this finding so that
    49  	// we can show it in the finding overview.
    50  	FuzzTest string `json:"fuzz_test,omitempty"`
    51  }
    52  
    53  type ErrorType string
    54  
    55  // These constants must have this exact value (in uppercase) to be able
    56  // to parse JSON-marshalled reports as protobuf reports which use an
    57  // enum for this field.
    58  const (
    59  	ErrorTypeUnknownError     ErrorType = "UNKNOWN_ERROR"
    60  	ErrorTypeCompilationError ErrorType = "COMPILATION_ERROR"
    61  	ErrorTypeCrash            ErrorType = "CRASH"
    62  	ErrorTypeWarning          ErrorType = "WARNING"
    63  	ErrorTypeRuntimeError     ErrorType = "RUNTIME_ERROR"
    64  )
    65  
    66  type ErrorDetails struct {
    67  	ID           string          `json:"id,omitempty"`
    68  	Name         string          `json:"name,omitempty"`
    69  	Description  string          `json:"description,omitempty"`
    70  	Severity     *Severity       `json:"severity,omitempty"`
    71  	Mitigation   string          `json:"mitigation,omitempty"`
    72  	Links        []Link          `json:"links,omitempty"`
    73  	OwaspDetails *ExternalDetail `json:"owasp_details,omitempty"`
    74  	CweDetails   *ExternalDetail `json:"cwe_details,omitempty"`
    75  }
    76  
    77  type SeverityLevel string
    78  
    79  const (
    80  	SeverityLevelCritical SeverityLevel = "CRITICAL"
    81  	SeverityLevelHigh     SeverityLevel = "HIGH"
    82  	SeverityLevelMedium   SeverityLevel = "MEDIUM"
    83  	SeverityLevelLow      SeverityLevel = "LOW"
    84  )
    85  
    86  type Severity struct {
    87  	Level SeverityLevel `json:"description,omitempty"`
    88  	Score float32       `json:"score,omitempty"`
    89  }
    90  
    91  type ExternalDetail struct {
    92  	ID          int64  `json:"id,omitempty"`
    93  	Name        string `json:"name,omitempty"`
    94  	Description string `json:"description,omitempty"`
    95  }
    96  
    97  type Link struct {
    98  	Description string `json:"description,omitempty"`
    99  	URL         string `json:"url,omitempty"`
   100  }
   101  
   102  func (f *Finding) GetDetails() string {
   103  	if f != nil {
   104  		return f.Details
   105  	}
   106  	return ""
   107  }
   108  
   109  func (f *Finding) GetSeedPath() string {
   110  	if f != nil {
   111  		return f.seedPath
   112  	}
   113  	return ""
   114  }
   115  
   116  // Exists returns whether the JSON file of this finding already exists
   117  func (f *Finding) Exists(projectDir string) (bool, error) {
   118  	jsonPath := filepath.Join(projectDir, nameFindingsDir, f.Name, nameJSONFile)
   119  	return fileutil.Exists(jsonPath)
   120  }
   121  
   122  func (f *Finding) Save(projectDir string) error {
   123  	findingDir := filepath.Join(projectDir, nameFindingsDir, f.Name)
   124  	jsonPath := filepath.Join(findingDir, nameJSONFile)
   125  
   126  	err := os.MkdirAll(findingDir, 0o755)
   127  	if err != nil {
   128  		return errors.WithStack(err)
   129  	}
   130  
   131  	err = f.saveJSON(jsonPath)
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	return nil
   137  }
   138  
   139  func (f *Finding) saveJSON(jsonPath string) error {
   140  	bytes, err := json.MarshalIndent(f, "", "  ")
   141  	if err != nil {
   142  		return errors.WithStack(err)
   143  	}
   144  
   145  	if err := os.WriteFile(jsonPath, bytes, 0o644); err != nil {
   146  		return errors.WithStack(err)
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  // CopyInputFileAndUpdateFinding copies the input file to the finding directory and
   153  // the seed corpus directory and adjusts the finding logs accordingly.
   154  func (f *Finding) CopyInputFileAndUpdateFinding(projectDir, seedCorpusDir, buildSystem string) error {
   155  	// Acquire a file lock to avoid races with other cifuzz processes
   156  	// running in parallel
   157  	findingDir := filepath.Join(projectDir, nameFindingsDir, f.Name)
   158  	err := os.MkdirAll(findingDir, 0o755)
   159  	if err != nil {
   160  		return errors.WithStack(err)
   161  	}
   162  	lockFile := filepath.Join(findingDir, lockFile)
   163  	mutex, err := filemutex.New(lockFile)
   164  	if err != nil {
   165  		return errors.WithStack(err)
   166  	}
   167  	err = mutex.Lock()
   168  	if err != nil {
   169  		return errors.WithStack(err)
   170  	}
   171  
   172  	// Actually copy the input file
   173  	err = f.copyInputFile(projectDir, seedCorpusDir, buildSystem)
   174  
   175  	// Release the file lock
   176  	unlockErr := mutex.Unlock()
   177  	if err == nil {
   178  		return errors.WithStack(unlockErr)
   179  	}
   180  	if unlockErr != nil {
   181  		log.Error(unlockErr)
   182  	}
   183  	return err
   184  }
   185  
   186  func (f *Finding) copyInputFile(projectDir, seedCorpusDir, buildSystem string) error {
   187  	findingDir := filepath.Join(projectDir, nameFindingsDir, f.Name)
   188  	path := filepath.Join(findingDir, nameCrashingInput)
   189  
   190  	// Copy the input file to the finding dir. We don't use os.Rename to
   191  	// avoid errors when source and target are not on the same mounted
   192  	// filesystem.
   193  	err := copy.Copy(f.InputFile, path)
   194  	if err != nil {
   195  		return errors.WithStack(err)
   196  	}
   197  
   198  	if sliceutil.Contains([]string{
   199  		config.BuildSystemCMake, config.BuildSystemBazel, config.BuildSystemOther,
   200  	},
   201  		buildSystem,
   202  	) {
   203  		// Copy the input file to the seed corpus dir.
   204  		// This is only necessary for c/c++ projects.
   205  		err = os.MkdirAll(seedCorpusDir, 0o755)
   206  		if err != nil {
   207  			return errors.WithStack(err)
   208  		}
   209  		f.seedPath = filepath.Join(seedCorpusDir, f.Name)
   210  		err = copy.Copy(f.InputFile, f.seedPath)
   211  		if err != nil {
   212  			return errors.WithStack(err)
   213  		}
   214  	}
   215  
   216  	// Replace the old filename in the finding logs. Replace it with the
   217  	// relative path to not leak the directory structure of the current
   218  	// user in the finding logs (which might be shared with others).
   219  	cwd, err := os.Getwd()
   220  	if err != nil {
   221  		return errors.WithStack(err)
   222  	}
   223  	relPath, err := filepath.Rel(cwd, path)
   224  	if err != nil {
   225  		return errors.WithStack(err)
   226  	}
   227  	for i, line := range f.Logs {
   228  		f.Logs[i] = strings.ReplaceAll(line, f.InputFile, relPath)
   229  	}
   230  	log.Debugf("Copied input file from %s to %s", f.InputFile, path)
   231  
   232  	// The path in the InputFile field is expected to be relative to the
   233  	// project directory
   234  	pathRelativeToProjectDir, err := filepath.Rel(projectDir, path)
   235  	if err != nil {
   236  		return errors.WithStack(err)
   237  	}
   238  	f.InputFile = pathRelativeToProjectDir
   239  	return nil
   240  }
   241  
   242  func (f *Finding) ShortDescriptionWithName() string {
   243  	return fmt.Sprintf("[%s] %s", f.Name, f.ShortDescription())
   244  }
   245  
   246  func (f *Finding) ShortDescription() string {
   247  	return strings.Join(f.ShortDescriptionColumns(), " ")
   248  }
   249  
   250  func (f *Finding) ShortDescriptionColumns() []string {
   251  	var columns []string
   252  
   253  	// TODO this is just a naive approach to get some error types.
   254  	// This should be replace as soon as we have a list of the different error types.
   255  	var errorType string
   256  	switch f.Type {
   257  	case ErrorTypeCrash:
   258  		switch {
   259  		case f.Details == "detected memory leaks":
   260  			// Special vulnerabilities
   261  			errorType = f.Details
   262  		case strings.Contains(f.Details, "Security Issue:"):
   263  			// Jazzer findings
   264  			errorType = f.Details
   265  		case f.Details == "fuzz target exited":
   266  			// Jazzer.js findings
   267  			errorType = f.Details
   268  		default:
   269  			errorType = strings.ReplaceAll(strings.Split(f.Details, " ")[0], "-", " ")
   270  		}
   271  	case ErrorTypeRuntimeError:
   272  		errorType = strings.Split(f.Details, ":")[0]
   273  	default:
   274  		errorType = f.Details
   275  	}
   276  
   277  	columns = append(columns, errorType)
   278  
   279  	// add location (file, function, line)
   280  	if len(f.StackTrace) > 0 {
   281  		f := f.StackTrace[0]
   282  		var location string
   283  		// in some cases ASan/Libfuzzer do not include the column in the stack trace
   284  		if f.Column != 0 {
   285  			location = fmt.Sprintf("%s:%d:%d", f.SourceFile, f.Line, f.Column)
   286  		} else {
   287  			location = fmt.Sprintf("%s:%d", f.SourceFile, f.Line)
   288  		}
   289  		if f.Function != "" {
   290  			columns = append(columns, fmt.Sprintf("in %s (%s)", f.Function, location))
   291  		} else {
   292  			columns = append(columns, fmt.Sprintf("in %s", location))
   293  		}
   294  	}
   295  	return columns
   296  }
   297  
   298  // ListFindings parses the JSON files of all findings and returns the
   299  // result.
   300  func ListFindings(projectDir string, errorDetails *[]ErrorDetails) ([]*Finding, error) {
   301  	findingsDir := filepath.Join(projectDir, nameFindingsDir)
   302  	entries, err := os.ReadDir(findingsDir)
   303  	if os.IsNotExist(err) {
   304  		return []*Finding{}, nil
   305  	}
   306  	if err != nil {
   307  		return nil, errors.WithStack(err)
   308  	}
   309  
   310  	var res []*Finding
   311  	for _, e := range entries {
   312  		f, err := LoadFinding(projectDir, e.Name(), errorDetails)
   313  		if err != nil {
   314  			return nil, err
   315  		}
   316  		res = append(res, f)
   317  	}
   318  
   319  	// Sort the findings by date, starting with the newest
   320  	sort.SliceStable(res, func(i, j int) bool {
   321  		return res[i].CreatedAt.After(res[j].CreatedAt)
   322  	})
   323  
   324  	return res, nil
   325  }
   326  
   327  // LoadFinding parses the JSON file of the specified finding and returns
   328  // the result.
   329  // If the specified finding does not exist, a NotExistError is returned.
   330  // If the user is logged in, the error details are added to the finding.
   331  func LoadFinding(projectDir, findingName string, errorDetails *[]ErrorDetails) (*Finding, error) {
   332  	findingDir := filepath.Join(projectDir, nameFindingsDir, findingName)
   333  	jsonPath := filepath.Join(findingDir, nameJSONFile)
   334  	bytes, err := os.ReadFile(jsonPath)
   335  	if os.IsNotExist(err) {
   336  		return nil, WrapNotExistError(err)
   337  	}
   338  	if err != nil {
   339  		return nil, errors.WithStack(err)
   340  	}
   341  	var f Finding
   342  	err = json.Unmarshal(bytes, &f)
   343  	if err != nil {
   344  		return nil, errors.WithStack(err)
   345  	}
   346  
   347  	f.EnhanceWithErrorDetails(errorDetails)
   348  
   349  	return &f, nil
   350  }
   351  
   352  // EnhanceWithErrorDetails adds more details to the finding by parsing the
   353  // error details file.
   354  func (f *Finding) EnhanceWithErrorDetails(errorDetails *[]ErrorDetails) {
   355  	if errorDetails == nil {
   356  		return
   357  	}
   358  	for _, d := range *errorDetails {
   359  		if (f.MoreDetails != nil && f.MoreDetails.ID == d.ID) ||
   360  			strings.Contains(
   361  				strings.ToLower(f.ShortDescriptionColumns()[0]),
   362  				strings.ToLower(d.Name)) {
   363  
   364  			f.MoreDetails = &d
   365  			return
   366  		}
   367  	}
   368  
   369  	log.Debugf("No error details found for finding %s", f.Name)
   370  }