github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/apk/apk.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 apk extracts packages from the APK database. 16 package apk 17 18 import ( 19 "context" 20 "fmt" 21 "path/filepath" 22 23 "github.com/google/osv-scalibr/extractor" 24 "github.com/google/osv-scalibr/extractor/filesystem" 25 "github.com/google/osv-scalibr/extractor/filesystem/os/apk/apkutil" 26 apkmeta "github.com/google/osv-scalibr/extractor/filesystem/os/apk/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 "github.com/google/osv-scalibr/stats" 33 ) 34 35 const ( 36 // Name is the unique name of this extractor. 37 Name = "os/apk" 38 ) 39 40 // Config is the configuration for the Extractor. 41 type Config struct { 42 // Stats is a stats collector for reporting metrics. 43 Stats stats.Collector 44 // MaxFileSizeBytes is the maximum file size this extractor will unmarshal. If 45 // `FileRequired` gets a bigger file, it will return false, 46 MaxFileSizeBytes int64 47 } 48 49 // DefaultConfig returns the default configuration for the extractor. 50 func DefaultConfig() Config { 51 return Config{ 52 MaxFileSizeBytes: 0, 53 Stats: nil, 54 } 55 } 56 57 // Extractor extracts packages from the APK database. 58 type Extractor struct { 59 stats stats.Collector 60 maxFileSizeBytes int64 61 } 62 63 // New returns an APK extractor. 64 // 65 // For most use cases, initialize with: 66 // ``` 67 // e := New(DefaultConfig()) 68 // ``` 69 func New(cfg Config) *Extractor { 70 return &Extractor{ 71 stats: cfg.Stats, 72 maxFileSizeBytes: cfg.MaxFileSizeBytes, 73 } 74 } 75 76 // NewDefault returns an extractor with the default config settings. 77 func NewDefault() filesystem.Extractor { return New(DefaultConfig()) } 78 79 // Name of the extractor. 80 func (e Extractor) Name() string { return Name } 81 82 // Version of the extractor. 83 func (e Extractor) Version() int { return 0 } 84 85 // Requirements of the extractor. 86 func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} } 87 88 // FileRequired returns true if the specified file matches apk status file pattern. 89 func (e Extractor) FileRequired(api filesystem.FileAPI) bool { 90 // Should match the status file. 91 if filepath.ToSlash(api.Path()) != "lib/apk/db/installed" && 92 filepath.ToSlash(api.Path()) != "var/lib/apk/db/installed" && 93 // TODO(b/428271704): Remove once we handle symlinks properly. 94 filepath.ToSlash(api.Path()) != "usr/lib/apk/db/installed" { 95 return false 96 } 97 98 fileinfo, err := api.Stat() 99 if err != nil { 100 return false 101 } 102 if e.maxFileSizeBytes > 0 && fileinfo.Size() > e.maxFileSizeBytes { 103 e.reportFileRequired(api.Path(), fileinfo.Size(), stats.FileRequiredResultSizeLimitExceeded) 104 return false 105 } 106 107 e.reportFileRequired(api.Path(), fileinfo.Size(), stats.FileRequiredResultOK) 108 return true 109 } 110 111 func (e Extractor) reportFileRequired(path string, fileSizeBytes int64, result stats.FileRequiredResult) { 112 if e.stats == nil { 113 return 114 } 115 e.stats.AfterFileRequired(e.Name(), &stats.FileRequiredStats{ 116 Path: path, 117 Result: result, 118 FileSizeBytes: fileSizeBytes, 119 }) 120 } 121 122 // Extract extracts packages from lib/apk/db/installed passed through the scan input. 123 func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 124 pkgs, err := e.extractFromInput(ctx, input) 125 if e.stats != nil { 126 var fileSizeBytes int64 127 if input.Info != nil { 128 fileSizeBytes = input.Info.Size() 129 } 130 e.stats.AfterFileExtracted(e.Name(), &stats.FileExtractedStats{ 131 Path: input.Path, 132 Result: filesystem.ExtractorErrorToFileExtractedResult(err), 133 FileSizeBytes: fileSizeBytes, 134 }) 135 } 136 return inventory.Inventory{Packages: pkgs}, err 137 } 138 139 func (e Extractor) extractFromInput(ctx context.Context, input *filesystem.ScanInput) ([]*extractor.Package, error) { 140 m, err := osrelease.GetOSRelease(input.FS) 141 if err != nil { 142 log.Errorf("osrelease.ParseOsRelease(): %v", err) 143 } 144 145 scanner := apkutil.NewScanner(input.Reader) 146 packages := []*extractor.Package{} 147 148 for scanner.Scan() { 149 if err := ctx.Err(); err != nil { 150 return nil, fmt.Errorf("%s halted due to context error: %w", e.Name(), err) 151 } 152 153 record := scanner.Record() 154 155 var sourceCode *extractor.SourceCodeIdentifier 156 if commit, ok := record["c"]; ok { 157 sourceCode = &extractor.SourceCodeIdentifier{ 158 Commit: commit, 159 } 160 } 161 162 var pkg = &extractor.Package{ 163 Name: record["P"], 164 Version: record["V"], 165 PURLType: purl.TypeApk, 166 Metadata: &apkmeta.Metadata{ 167 OSID: m["ID"], 168 OSVersionID: m["VERSION_ID"], 169 PackageName: record["P"], 170 OriginName: record["o"], 171 Architecture: record["A"], 172 Maintainer: record["m"], 173 }, 174 Licenses: []string{record["L"]}, 175 SourceCode: sourceCode, 176 Locations: []string{input.Path}, 177 } 178 179 if pkg.Name == "" || pkg.Version == "" { 180 log.Warnf("APK package name or version is empty (name: %q, version: %q)", pkg.Name, pkg.Version) 181 continue 182 } 183 184 packages = append(packages, pkg) 185 } 186 187 if err := scanner.Err(); err != nil { 188 return nil, fmt.Errorf("error while parsing apk status file: %w", err) 189 } 190 191 return packages, nil 192 }