Skip to content

Commit 9625276

Browse files
Phase 1 enhancement
1 parent fbaded7 commit 9625276

3 files changed

Lines changed: 276 additions & 2 deletions

File tree

internal/sbom/enhancer.go

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"encoding/hex"
66
"encoding/json"
77
"fmt"
8+
"go/parser"
9+
"go/token"
810
"os"
911
"path/filepath"
1012
"sort"
@@ -335,17 +337,152 @@ func (e *Enhancer) injectMetadata(sbom map[string]interface{}, metadata *GoenvMe
335337
func (e *Enhancer) enhanceComponents(sbom map[string]interface{}, opts EnhanceOptions) error {
336338
components, ok := sbom["components"].([]interface{})
337339
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)
339346
}
340347

341-
// TODO: Add stdlib component
342348
// TODO: Mark replaced components
343349
// TODO: Add retracted version warnings
344350

345351
sbom["components"] = components
346352
return nil
347353
}
348354

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+
349486
// makeDeterministic ensures reproducible output
350487
func (e *Enhancer) makeDeterministic(sbom map[string]interface{}) {
351488
// Sort components by name

internal/sbom/enhancer_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import (
44
"encoding/json"
55
"os"
66
"path/filepath"
7+
"strings"
78
"testing"
9+
10+
"github.com/go-nv/goenv/internal/config"
11+
"github.com/go-nv/goenv/internal/manager"
812
)
913

1014
func TestComputeSBOMDigest(t *testing.T) {
@@ -157,3 +161,74 @@ func TestGenerateDeterministicUUID(t *testing.T) {
157161
t.Errorf("UUID too short: %s", uuid1)
158162
}
159163
}
164+
165+
func TestStdlibDetection(t *testing.T) {
166+
tempDir := t.TempDir()
167+
168+
// Create a simple Go file with stdlib imports
169+
goFile := `package main
170+
171+
import (
172+
"fmt"
173+
"os"
174+
"encoding/json"
175+
"github.com/external/package"
176+
)
177+
178+
func main() {
179+
fmt.Println("test")
180+
}
181+
`
182+
if err := os.WriteFile(filepath.Join(tempDir, "main.go"), []byte(goFile), 0644); err != nil {
183+
t.Fatalf("Failed to write test file: %v", err)
184+
}
185+
186+
// Create test SBOM
187+
sbomPath := filepath.Join(tempDir, "sbom.json")
188+
sbom := map[string]interface{}{
189+
"bomFormat": "CycloneDX",
190+
"specVersion": "1.5",
191+
"version": 1,
192+
"components": []interface{}{},
193+
}
194+
data, _ := json.MarshalIndent(sbom, "", " ")
195+
os.WriteFile(sbomPath, data, 0644)
196+
197+
// Test stdlib import discovery directly
198+
enhancer := &Enhancer{
199+
config: &config.Config{Root: tempDir},
200+
manager: &manager.Manager{},
201+
}
202+
203+
stdlibImports, err := enhancer.discoverStdlibImports(tempDir)
204+
if err != nil {
205+
t.Fatalf("discoverStdlibImports failed: %v", err)
206+
}
207+
208+
if len(stdlibImports) == 0 {
209+
t.Fatal("Expected stdlib imports but got none")
210+
}
211+
212+
// Check expected stdlib packages
213+
expected := []string{"fmt", "os", "encoding/json"}
214+
for _, exp := range expected {
215+
found := false
216+
for _, imp := range stdlibImports {
217+
if imp == exp {
218+
found = true
219+
break
220+
}
221+
}
222+
if !found {
223+
t.Errorf("Expected stdlib package %s not found", exp)
224+
}
225+
}
226+
227+
// Ensure external package is NOT detected as stdlib
228+
for _, imp := range stdlibImports {
229+
if strings.Contains(imp, "github.com") {
230+
t.Errorf("External package %s incorrectly identified as stdlib", imp)
231+
}
232+
}
233+
234+
}

scripts/test-stdlib/main.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
9+
"github.com/go-nv/goenv/internal/config"
10+
"github.com/go-nv/goenv/internal/manager"
11+
"github.com/go-nv/goenv/internal/sbom"
12+
)
13+
14+
func main() {
15+
cfg := config.Load()
16+
mgr := manager.NewManager(cfg)
17+
enhancer := sbom.NewEnhancer(cfg, mgr)
18+
19+
// Test stdlib detection on current directory (goenv root)
20+
projectDirBytes, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
21+
if err != nil {
22+
fmt.Fprintf(os.Stderr, "Error getting project directory: %v\n", err)
23+
os.Exit(1)
24+
}
25+
26+
if len(projectDirBytes) == 0 {
27+
fmt.Fprintf(os.Stderr, "Could not determine project directory\n")
28+
os.Exit(1)
29+
}
30+
31+
projectDir := string(projectDirBytes)
32+
opts := sbom.EnhanceOptions{
33+
ProjectDir: projectDir,
34+
Deterministic: true,
35+
EmbedDigests: false,
36+
}
37+
38+
// Enhance the test SBOM
39+
sbomPath := fmt.Sprintf("%s/test-base-sbom.json", projectDir)
40+
err = enhancer.EnhanceCycloneDX(sbomPath, opts)
41+
if err != nil {
42+
fmt.Fprintf(os.Stderr, "Error enhancing SBOM: %v\n", err)
43+
os.Exit(1)
44+
}
45+
46+
// Read and display the enhanced SBOM
47+
data, err := os.ReadFile(sbomPath)
48+
if err != nil {
49+
fmt.Fprintf(os.Stderr, "Error reading SBOM: %v\n", err)
50+
os.Exit(1)
51+
}
52+
53+
var sbomData map[string]interface{}
54+
if err := json.Unmarshal(data, &sbomData); err != nil {
55+
fmt.Fprintf(os.Stderr, "Error parsing SBOM: %v\n", err)
56+
os.Exit(1)
57+
}
58+
59+
// Pretty print
60+
pretty, _ := json.MarshalIndent(sbomData, "", " ")
61+
fmt.Println(string(pretty))
62+
}

0 commit comments

Comments
 (0)