github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/misc/wordpress/plugins/plugins.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 plugins extracts packages from installed Wordpress plugins. 16 package plugins 17 18 import ( 19 "bufio" 20 "context" 21 "fmt" 22 "io" 23 "strings" 24 25 "github.com/google/osv-scalibr/extractor" 26 "github.com/google/osv-scalibr/extractor/filesystem" 27 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 28 "github.com/google/osv-scalibr/inventory" 29 "github.com/google/osv-scalibr/plugin" 30 "github.com/google/osv-scalibr/purl" 31 "github.com/google/osv-scalibr/stats" 32 ) 33 34 const ( 35 // Name is the unique name of this extractor. 36 Name = "wordpress/plugins" 37 ) 38 39 // Config is the configuration for the Wordpress extractor. 40 type Config struct { 41 // Stats is a stats collector for reporting metrics. 42 Stats stats.Collector 43 // MaxFileSizeBytes is the maximum file size this extractor will unmarshal. If 44 // `FileRequired` gets a bigger file, it will return false, 45 MaxFileSizeBytes int64 46 } 47 48 // DefaultConfig returns the default configuration for the Wordpress extractor. 49 func DefaultConfig() Config { 50 return Config{ 51 Stats: nil, 52 MaxFileSizeBytes: 10 * units.MiB, 53 } 54 } 55 56 // Extractor structure for plugins files. 57 type Extractor struct { 58 stats stats.Collector 59 maxFileSizeBytes int64 60 } 61 62 // New returns a Wordpress extractor. 63 // 64 // For most use cases, initialize with: 65 // ``` 66 // e := New(DefaultConfig()) 67 // ``` 68 func New(cfg Config) *Extractor { 69 return &Extractor{ 70 stats: cfg.Stats, 71 maxFileSizeBytes: cfg.MaxFileSizeBytes, 72 } 73 } 74 75 // NewDefault returns an extractor with the default config settings. 76 func NewDefault() filesystem.Extractor { 77 return New(DefaultConfig()) 78 } 79 80 // Config returns the configuration of the extractor. 81 func (e Extractor) Config() Config { 82 return Config{ 83 Stats: e.stats, 84 MaxFileSizeBytes: e.maxFileSizeBytes, 85 } 86 } 87 88 // Name of the extractor. 89 func (e Extractor) Name() string { return Name } 90 91 // Version of the extractor. 92 func (e Extractor) Version() int { return 0 } 93 94 // Requirements of the extractor. 95 func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} } 96 97 // FileRequired returns true if the specified file matches the /wp-content/plugins/ pattern. 98 func (e Extractor) FileRequired(api filesystem.FileAPI) bool { 99 path := api.Path() 100 if !strings.HasSuffix(path, ".php") || !strings.Contains(path, "wp-content/plugins/") { 101 return false 102 } 103 104 fileinfo, err := api.Stat() 105 if err != nil { 106 return false 107 } 108 109 if e.maxFileSizeBytes > 0 && fileinfo.Size() > e.maxFileSizeBytes { 110 e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultSizeLimitExceeded) 111 return false 112 } 113 114 e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultOK) 115 return true 116 } 117 118 func (e Extractor) reportFileRequired(path string, fileSizeBytes int64, result stats.FileRequiredResult) { 119 if e.stats == nil { 120 return 121 } 122 e.stats.AfterFileRequired(e.Name(), &stats.FileRequiredStats{ 123 Path: path, 124 Result: result, 125 FileSizeBytes: fileSizeBytes, 126 }) 127 } 128 129 // Extract parses the PHP file to extract Wordpress package. 130 func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 131 pkg, err := parsePHPFile(input.Reader) 132 if err != nil { 133 return inventory.Inventory{}, err 134 } 135 136 if pkg == nil { 137 return inventory.Inventory{}, nil 138 } 139 140 return inventory.Inventory{Packages: []*extractor.Package{&extractor.Package{ 141 Name: pkg.Name, 142 Version: pkg.Version, 143 PURLType: purl.TypeWordpress, 144 Locations: []string{input.Path}, 145 }}}, nil 146 } 147 148 type wpPackage struct { 149 Name string 150 Version string 151 } 152 153 func parsePHPFile(r io.Reader) (*wpPackage, error) { 154 scanner := bufio.NewScanner(r) 155 var name, version string 156 157 for scanner.Scan() { 158 line := scanner.Text() 159 160 if strings.Contains(line, "Plugin Name:") { 161 name = strings.TrimSpace(strings.Split(line, "Plugin Name:")[1]) 162 } 163 164 if strings.Contains(line, "Version:") { 165 version = strings.TrimSpace(strings.Split(line, ":")[1]) 166 } 167 168 if name != "" && version != "" { 169 break 170 } 171 } 172 173 if err := scanner.Err(); err != nil { 174 return nil, fmt.Errorf("failed to read PHP file: %w", err) 175 } 176 177 if name == "" || version == "" { 178 return nil, nil 179 } 180 181 return &wpPackage{Name: name, Version: version}, nil 182 }