github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/dotnet/packagesconfig/packagesconfig.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 // Copyright 2024 Google LLC 16 // 17 // Licensed under the Apache License, Version 2.0 (the "License"); 18 // you may not use this file except in compliance with the License. 19 // You may obtain a copy of the License at 20 // 21 // http://www.apache.org/licenses/LICENSE-2.0 22 // 23 // Unless required by applicable law or agreed to in writing, software 24 // distributed under the License is distributed on an "AS IS" BASIS, 25 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 // See the License for the specific language governing permissions and 27 // limitations under the License. 28 29 // Package packagesconfig extracts packages from .NET packages.config files. 30 package packagesconfig 31 32 import ( 33 "context" 34 "encoding/xml" 35 "path/filepath" 36 37 "github.com/google/osv-scalibr/extractor" 38 "github.com/google/osv-scalibr/extractor/filesystem" 39 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 40 "github.com/google/osv-scalibr/inventory" 41 "github.com/google/osv-scalibr/log" 42 "github.com/google/osv-scalibr/plugin" 43 "github.com/google/osv-scalibr/purl" 44 "github.com/google/osv-scalibr/stats" 45 ) 46 47 const ( 48 // Name is the unique name of this extractor. 49 Name = "dotnet/packagesconfig" 50 51 // defaultMaxFileSizeBytes is the maximum file size this extractor will process. 52 defaultMaxFileSizeBytes = 20 * units.MiB // 20 MB 53 ) 54 55 // Config is the configuration for the .NET packages.config extractor. 56 type Config struct { 57 // Stats is a stats collector for reporting metrics. 58 Stats stats.Collector 59 // MaxFileSizeBytes is the maximum file size this extractor will unmarshal. If 60 // `FileRequired` gets a bigger file, it will return false, 61 MaxFileSizeBytes int64 62 } 63 64 // DefaultConfig returns the default configuration for the .NET packages.config extractor. 65 func DefaultConfig() Config { 66 return Config{ 67 MaxFileSizeBytes: defaultMaxFileSizeBytes, 68 } 69 } 70 71 // Extractor structure for .NET packages.config files. 72 type Extractor struct { 73 stats stats.Collector 74 maxFileSizeBytes int64 75 } 76 77 // New returns a .NET packages.config extractor. 78 func New(cfg Config) *Extractor { 79 return &Extractor{ 80 stats: cfg.Stats, 81 maxFileSizeBytes: cfg.MaxFileSizeBytes, 82 } 83 } 84 85 // NewDefault returns an extractor with the default config settings. 86 func NewDefault() filesystem.Extractor { return New(DefaultConfig()) } 87 88 // Config returns the configuration of the extractor. 89 func (e Extractor) Config() Config { 90 return Config{ 91 Stats: e.stats, 92 MaxFileSizeBytes: e.maxFileSizeBytes, 93 } 94 } 95 96 // Name of the extractor. 97 func (e Extractor) Name() string { return Name } 98 99 // Version of the extractor. 100 func (e Extractor) Version() int { return 0 } 101 102 // Requirements of the extractor. 103 func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} } 104 105 // FileRequired returns true if the specified file matches the .NET packages.config pattern. 106 func (e Extractor) FileRequired(api filesystem.FileAPI) bool { 107 path := api.Path() 108 if filepath.Base(path) != "packages.config" { 109 return false 110 } 111 112 fileinfo, err := api.Stat() 113 if err != nil || (e.maxFileSizeBytes > 0 && fileinfo.Size() > e.maxFileSizeBytes) { 114 e.reportFileRequired(path, stats.FileRequiredResultSizeLimitExceeded) 115 return false 116 } 117 118 e.reportFileRequired(path, stats.FileRequiredResultOK) 119 return true 120 } 121 122 func (e Extractor) reportFileRequired(path string, result stats.FileRequiredResult) { 123 if e.stats == nil { 124 return 125 } 126 e.stats.AfterFileRequired(e.Name(), &stats.FileRequiredStats{ 127 Path: path, 128 Result: result, 129 }) 130 } 131 132 // Extract parses the packages.config file to extract .NET package dependencies. 133 func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 134 packages, err := e.extractFromInput(input) 135 if e.stats != nil { 136 var fileSizeBytes int64 137 if input.Info != nil { 138 fileSizeBytes = input.Info.Size() 139 } 140 e.stats.AfterFileExtracted(e.Name(), &stats.FileExtractedStats{ 141 Path: input.Path, 142 Result: filesystem.ExtractorErrorToFileExtractedResult(err), 143 FileSizeBytes: fileSizeBytes, 144 }) 145 } 146 return inventory.Inventory{Packages: packages}, err 147 } 148 149 type dotNETPackage struct { 150 ID string `xml:"id,attr"` 151 Version string `xml:"version,attr"` 152 } 153 154 type dotNETPackages struct { 155 XMLName xml.Name `xml:"packages"` 156 Packages []dotNETPackage `xml:"package"` 157 } 158 159 func (e Extractor) extractFromInput(input *filesystem.ScanInput) ([]*extractor.Package, error) { 160 var packages dotNETPackages 161 decoder := xml.NewDecoder(input.Reader) 162 if err := decoder.Decode(&packages); err != nil { 163 log.Errorf("Error parsing packages.config: %v", err) 164 return nil, err 165 } 166 167 var result []*extractor.Package 168 for _, pkg := range packages.Packages { 169 if pkg.ID == "" || pkg.Version == "" { 170 log.Warnf("Skipping package with missing name or version: %+v", pkg) 171 continue 172 } 173 174 result = append(result, &extractor.Package{ 175 Name: pkg.ID, 176 Version: pkg.Version, 177 PURLType: purl.TypeNuget, 178 Locations: []string{input.Path}, 179 }) 180 } 181 182 return result, nil 183 }