|
5 | 5 | "encoding/hex" |
6 | 6 | "encoding/json" |
7 | 7 | "fmt" |
| 8 | + "go/parser" |
| 9 | + "go/token" |
8 | 10 | "os" |
9 | 11 | "path/filepath" |
10 | 12 | "sort" |
@@ -335,17 +337,152 @@ func (e *Enhancer) injectMetadata(sbom map[string]interface{}, metadata *GoenvMe |
335 | 337 | func (e *Enhancer) enhanceComponents(sbom map[string]interface{}, opts EnhanceOptions) error { |
336 | 338 | components, ok := sbom["components"].([]interface{}) |
337 | 339 | if !ok { |
338 | | - return nil // No components to enhance |
| 340 | + components = []interface{}{} |
| 341 | + } |
| 342 | + |
| 343 | + // Add stdlib component if Go source files are present |
| 344 | + if stdlibComponent, err := e.createStdlibComponent(opts.ProjectDir); err == nil && stdlibComponent != nil { |
| 345 | + components = append(components, stdlibComponent) |
339 | 346 | } |
340 | 347 |
|
341 | | - // TODO: Add stdlib component |
342 | 348 | // TODO: Mark replaced components |
343 | 349 | // TODO: Add retracted version warnings |
344 | 350 |
|
345 | 351 | sbom["components"] = components |
346 | 352 | return nil |
347 | 353 | } |
348 | 354 |
|
| 355 | +// createStdlibComponent analyzes Go source files and creates a stdlib component |
| 356 | +func (e *Enhancer) createStdlibComponent(projectDir string) (map[string]interface{}, error) { |
| 357 | + if projectDir == "" { |
| 358 | + projectDir = "." |
| 359 | + } |
| 360 | + |
| 361 | + // Discover stdlib imports from Go source files |
| 362 | + stdlibImports, err := e.discoverStdlibImports(projectDir) |
| 363 | + if err != nil || len(stdlibImports) == 0 { |
| 364 | + return nil, err |
| 365 | + } |
| 366 | + |
| 367 | + // Get Go version for the component |
| 368 | + goVersion, _, err := e.manager.GetCurrentVersion() |
| 369 | + if err != nil { |
| 370 | + goVersion = "unknown" |
| 371 | + } |
| 372 | + |
| 373 | + // Create stdlib component in CycloneDX format |
| 374 | + component := map[string]interface{}{ |
| 375 | + "type": "library", |
| 376 | + "name": "golang-stdlib", |
| 377 | + "version": goVersion, |
| 378 | + "purl": fmt.Sprintf("pkg:golang/stdlib@%s", goVersion), |
| 379 | + "bom-ref": fmt.Sprintf("pkg:golang/stdlib@%s", goVersion), |
| 380 | + "description": fmt.Sprintf("Go standard library packages used by this project (%d packages)", len(stdlibImports)), |
| 381 | + "properties": []map[string]interface{}{ |
| 382 | + { |
| 383 | + "name": "goenv:stdlib_packages", |
| 384 | + "value": strings.Join(stdlibImports, ","), |
| 385 | + }, |
| 386 | + { |
| 387 | + "name": "goenv:stdlib_count", |
| 388 | + "value": fmt.Sprintf("%d", len(stdlibImports)), |
| 389 | + }, |
| 390 | + }, |
| 391 | + } |
| 392 | + |
| 393 | + return component, nil |
| 394 | +} |
| 395 | + |
| 396 | +// discoverStdlibImports scans Go source files for stdlib imports |
| 397 | +func (e *Enhancer) discoverStdlibImports(projectDir string) ([]string, error) { |
| 398 | + stdlibSet := make(map[string]bool) |
| 399 | + |
| 400 | + // Walk through all .go files |
| 401 | + err := filepath.Walk(projectDir, func(path string, info os.FileInfo, err error) error { |
| 402 | + if err != nil { |
| 403 | + return nil // Skip errors, continue walking |
| 404 | + } |
| 405 | + |
| 406 | + // Skip vendor and hidden directories |
| 407 | + if info.IsDir() { |
| 408 | + name := info.Name() |
| 409 | + if name == "vendor" || name == "testdata" || strings.HasPrefix(name, ".") { |
| 410 | + return filepath.SkipDir |
| 411 | + } |
| 412 | + return nil |
| 413 | + } |
| 414 | + |
| 415 | + // Only process .go files |
| 416 | + if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { |
| 417 | + return nil |
| 418 | + } |
| 419 | + |
| 420 | + // Parse the Go file |
| 421 | + fset := token.NewFileSet() |
| 422 | + node, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) |
| 423 | + if err != nil { |
| 424 | + return nil // Skip files with parse errors |
| 425 | + } |
| 426 | + |
| 427 | + // Extract imports |
| 428 | + for _, imp := range node.Imports { |
| 429 | + importPath := strings.Trim(imp.Path.Value, `"`) |
| 430 | + |
| 431 | + // Check if it's a stdlib package |
| 432 | + if e.isStdlibPackage(importPath) { |
| 433 | + stdlibSet[importPath] = true |
| 434 | + } |
| 435 | + } |
| 436 | + |
| 437 | + return nil |
| 438 | + }) |
| 439 | + |
| 440 | + if err != nil { |
| 441 | + return nil, err |
| 442 | + } |
| 443 | + |
| 444 | + // Convert set to sorted slice |
| 445 | + stdlibImports := make([]string, 0, len(stdlibSet)) |
| 446 | + for pkg := range stdlibSet { |
| 447 | + stdlibImports = append(stdlibImports, pkg) |
| 448 | + } |
| 449 | + sort.Strings(stdlibImports) |
| 450 | + |
| 451 | + return stdlibImports, nil |
| 452 | +} |
| 453 | + |
| 454 | +// isStdlibPackage determines if an import path is from the Go standard library |
| 455 | +func (e *Enhancer) isStdlibPackage(importPath string) bool { |
| 456 | + // Stdlib packages don't have dots in the first path element |
| 457 | + // (except for some special cases like golang.org/x/...) |
| 458 | + |
| 459 | + // Explicitly exclude known non-stdlib patterns |
| 460 | + if strings.HasPrefix(importPath, "github.com/") || |
| 461 | + strings.HasPrefix(importPath, "golang.org/x/") || |
| 462 | + strings.HasPrefix(importPath, "gopkg.in/") || |
| 463 | + strings.HasPrefix(importPath, "go.uber.org/") || |
| 464 | + strings.Contains(importPath, ".com/") || |
| 465 | + strings.Contains(importPath, ".io/") || |
| 466 | + strings.Contains(importPath, ".org/") || |
| 467 | + strings.Contains(importPath, ".net/") { |
| 468 | + return false |
| 469 | + } |
| 470 | + |
| 471 | + // Internal packages are not stdlib for third-party projects |
| 472 | + if strings.HasPrefix(importPath, e.config.Root) { |
| 473 | + return false |
| 474 | + } |
| 475 | + |
| 476 | + // Common stdlib packages (non-exhaustive, covers major ones) |
| 477 | + firstSegment := importPath |
| 478 | + if idx := strings.Index(importPath, "/"); idx > 0 { |
| 479 | + firstSegment = importPath[:idx] |
| 480 | + } |
| 481 | + |
| 482 | + // Stdlib packages typically don't have dots |
| 483 | + return !strings.Contains(firstSegment, ".") |
| 484 | +} |
| 485 | + |
349 | 486 | // makeDeterministic ensures reproducible output |
350 | 487 | func (e *Enhancer) makeDeterministic(sbom map[string]interface{}) { |
351 | 488 | // Sort components by name |
|
0 commit comments