github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/nix/nix.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package nix extracts packages from the Nix store directory. 16 package nix 17 18 import ( 19 "context" 20 "fmt" 21 "regexp" 22 "strings" 23 24 "github.com/google/osv-scalibr/extractor" 25 "github.com/google/osv-scalibr/extractor/filesystem" 26 nixmeta "github.com/google/osv-scalibr/extractor/filesystem/os/nix/metadata" 27 "github.com/google/osv-scalibr/extractor/filesystem/os/osrelease" 28 "github.com/google/osv-scalibr/inventory" 29 "github.com/google/osv-scalibr/log" 30 "github.com/google/osv-scalibr/plugin" 31 "github.com/google/osv-scalibr/purl" 32 ) 33 34 const ( 35 // Name is the unique name of this extractor. 36 Name = "os/nix" 37 ) 38 39 // Extractor extracts packages from the nix store directory. 40 type Extractor struct { 41 // visitedDir tracks already visited directories. 42 visitedDir map[string]bool 43 } 44 45 // New returns a new instance of the extractor. 46 func New() filesystem.Extractor { 47 return &Extractor{ 48 visitedDir: make(map[string]bool), 49 } 50 } 51 52 // Name of the extractor. 53 func (e Extractor) Name() string { return Name } 54 55 // Version of the extractor. 56 func (e Extractor) Version() int { return 0 } 57 58 // Requirements of the extractor. 59 func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} } 60 61 // FileRequired returns true if a given path corresponds to a unique, unprocessed 62 // directory under the nixStoreDir path. 63 func (e *Extractor) FileRequired(api filesystem.FileAPI) bool { 64 path := api.Path() 65 66 if !strings.HasPrefix(path, "nix/store/") { 67 return false 68 } 69 70 pathParts := strings.Split(path, "/") 71 if len(pathParts) <= 3 { 72 return false 73 } 74 75 // e.g. 76 // path: nix/store/1ddf3x30m0z6kknmrmapsc7liz8npi1w-perl-5.38.2/bin/ptar 77 // uniquePath: 1ddf3x30m0z6kknmrmapsc7liz8npi1w-perl-5.38.2 78 uniquePath := pathParts[2] 79 80 // Optimization note: In case this plugin becomes too heavy in terms of CPU 81 // or memory, consider optimizing by storing only the last processed 82 // package name as a string, assuming files are walked using DFS. 83 84 // Check if the uniquePath has already been processed 85 if _, exists := e.visitedDir[uniquePath]; exists { 86 return false 87 } 88 89 e.visitedDir[uniquePath] = true 90 91 return true 92 } 93 94 var packageStoreRegex = regexp.MustCompile(`^([a-zA-Z0-9]{32})-([a-zA-Z0-9.-]+)-([0-9.]+)(?:-(\S+))?$`) 95 var packageStoreUnstableRegex = regexp.MustCompile(`^([a-zA-Z0-9]{32})-([a-zA-Z0-9.-]+)-(unstable-[0-9]{4}-[0-9]{2}-[0-9]{2})$`) 96 97 // Extract extracts packages from the filenames of the directories in the nix 98 // store path. 99 func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 100 // Check for cancellation or timeout. 101 if err := ctx.Err(); err != nil { 102 return inventory.Inventory{}, fmt.Errorf("%s halted due to context error: %w", e.Name(), err) 103 } 104 105 m, err := osrelease.GetOSRelease(input.FS) 106 if err != nil { 107 log.Errorf("osrelease.GetOSRelease(): %v", err) 108 } 109 110 pkgs := strings.Split(input.Path, "/")[2] 111 112 var matches []string 113 if strings.Contains(pkgs, "unstable") { 114 matches = packageStoreUnstableRegex.FindStringSubmatch(pkgs) 115 } else { 116 matches = packageStoreRegex.FindStringSubmatch(pkgs) 117 } 118 119 if len(matches) == 0 { 120 return inventory.Inventory{}, nil 121 } 122 123 pkgHash := matches[1] 124 pkgName := matches[2] 125 pkgVersion := matches[3] 126 if pkgHash == "" || pkgName == "" || pkgVersion == "" { 127 log.Warnf("NIX package name/version/hash is empty (name: %v, version: %v, hash: %v)", pkgName, pkgVersion, pkgHash) 128 return inventory.Inventory{}, nil 129 } 130 131 p := &extractor.Package{ 132 Name: pkgName, 133 Version: pkgVersion, 134 PURLType: purl.TypeNix, 135 Metadata: &nixmeta.Metadata{ 136 PackageName: pkgName, 137 PackageVersion: pkgVersion, 138 PackageHash: pkgHash, 139 OSID: m["ID"], 140 OSVersionCodename: m["VERSION_CODENAME"], 141 OSVersionID: m["VERSION_ID"], 142 }, 143 Locations: []string{input.Path}, 144 } 145 146 if len(matches) > 4 { 147 pkgOutput := matches[4] 148 p.Metadata.(*nixmeta.Metadata).PackageOutput = pkgOutput 149 } 150 151 return inventory.Inventory{Packages: []*extractor.Package{p}}, nil 152 }