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 }