github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/secrets/mariadb/mariadb.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 mariadb returns a list of secret mariadb credentials found in *.cnf and *.ini mariadb files 16 package mariadb 17 18 import ( 19 "bufio" 20 "context" 21 "errors" 22 "fmt" 23 "io/fs" 24 "path/filepath" 25 "regexp" 26 "strings" 27 28 "github.com/google/osv-scalibr/extractor/filesystem" 29 "github.com/google/osv-scalibr/inventory" 30 "github.com/google/osv-scalibr/plugin" 31 ) 32 33 const ( 34 // Name is the unique name of this extractor. 35 Name = "secrets/mariadb" 36 ) 37 38 var ( 39 keyValuePattern = regexp.MustCompile(`^\s*([^:=\s]+)\s*[:=]\s*(.+)$`) 40 ) 41 42 // Config is the extractor config 43 type Config struct { 44 // FollowInclude directive tells the extractor to follow the include or not 45 FollowInclude bool 46 } 47 48 // DefaultConfig returns the default configuration values for the Extractor. 49 func DefaultConfig() Config { 50 return Config{ 51 FollowInclude: true, 52 } 53 } 54 55 // Extractor extracts mariadb secret credentials. 56 type Extractor struct { 57 visited map[string]struct{} 58 followInclude bool 59 } 60 61 // New returns the Extractor with the specified config settings. 62 func New(cfg Config) filesystem.Extractor { 63 return &Extractor{ 64 visited: map[string]struct{}{}, 65 followInclude: cfg.FollowInclude, 66 } 67 } 68 69 // NewDefault returns the Extractor with the default config settings. 70 func NewDefault() filesystem.Extractor { 71 return New(DefaultConfig()) 72 } 73 74 // Name of the extractor. 75 func (e Extractor) Name() string { return Name } 76 77 // Version of the extractor. 78 func (e Extractor) Version() int { return 0 } 79 80 // Requirements of the extractor. 81 func (e Extractor) Requirements() *plugin.Capabilities { 82 return &plugin.Capabilities{} 83 } 84 85 // FileRequired returns true if the file contains mariadb config information 86 // ref: https://mariadb.com/docs/server/server-management/install-and-upgrade-mariadb/configuring-mariadb/configuring-mariadb-with-option-files 87 func (e *Extractor) FileRequired(api filesystem.FileAPI) bool { 88 path := api.Path() 89 return strings.HasSuffix(path, "my.cnf") || strings.HasSuffix(path, "my.ini") 90 } 91 92 // Extract returns a list of secret mariadb credentials 93 func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 94 i := inventory.Inventory{} 95 secrets, err := e.includeFile(ctx, input, input.Path) 96 if err != nil { 97 return i, err 98 } 99 i.Secrets = secrets 100 return i, nil 101 } 102 103 // include call includeDir or includeFile depending on the prefix 104 func (e *Extractor) include(ctx context.Context, input *filesystem.ScanInput, line string) ([]*inventory.Secret, error) { 105 after, isDir, err := cutIncludePrefix(line) 106 if err != nil { 107 return nil, fmt.Errorf("error in line %q: %w", line, err) 108 } 109 110 // Remove leading '/' or "C:" since SCALIBR fs paths don't include that. 111 // Remove trailing '/' if present 112 before, path, _ := strings.Cut(strings.TrimSpace(after), ":") 113 if path == "" { 114 path = before 115 } 116 path = strings.Trim(path, "/\\") 117 118 if isDir { 119 sections, err := e.includeDir(ctx, input, path) 120 return sections, err 121 } 122 123 return e.includeFile(ctx, input, path) 124 } 125 126 func cutIncludePrefix(s string) (after string, dir bool, err error) { 127 if after, ok := strings.CutPrefix(s, "!includedir"); ok { 128 return after, true, nil 129 } 130 if after, ok := strings.CutPrefix(s, "!include"); ok { 131 return after, false, nil 132 } 133 return "", false, errors.New("unknown include prefix") 134 } 135 136 // includeFile recursively extract secrets from a config file 137 func (e *Extractor) includeFile(ctx context.Context, input *filesystem.ScanInput, path string) ([]*inventory.Secret, error) { 138 // Prevent circular includes. 139 if _, seen := e.visited[path]; seen { 140 return nil, nil 141 } 142 e.visited[path] = struct{}{} 143 144 f, err := input.FS.Open(path) 145 if err != nil { 146 return nil, fmt.Errorf("could not open file %w", err) 147 } 148 defer f.Close() 149 150 curSection := "" 151 sections := map[string]*Credentials{} 152 scanner := bufio.NewScanner(f) 153 // Note: 154 // returning all the config flat instead of handling the files hierarchies 155 // because files are opened in no particular order 156 res := []*inventory.Secret{} 157 158 for scanner.Scan() { 159 if err := ctx.Err(); err != nil { 160 return nil, err 161 } 162 163 line := strings.TrimSpace(scanner.Text()) 164 165 // skip empty lines and comments 166 if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { 167 continue 168 } 169 170 // include a file or a folder 171 if strings.HasPrefix(line, "!include") { 172 if !e.followInclude { 173 continue 174 } 175 section, err := e.include(ctx, input, line) 176 if err != nil { 177 return nil, err 178 } 179 res = append(res, section...) 180 continue 181 } 182 183 // new section encountered 184 if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { 185 curSection = strings.Trim(line, "[]") 186 if _, ok := sections[curSection]; !ok { 187 sections[curSection] = &Credentials{Section: curSection} 188 } 189 continue 190 } 191 192 // add key value pair to the current section 193 matches := keyValuePattern.FindStringSubmatch(line) 194 if len(matches) != 3 { 195 continue 196 } 197 key, value := matches[1], matches[2] 198 199 if curSection == "" { 200 return nil, fmt.Errorf("bad format: key-value found outside a section in file %q", path) 201 } 202 203 // If the key is not related to credentials, ignore it silently 204 _ = sections[curSection].setField(key, value) 205 } 206 207 if err := scanner.Err(); err != nil { 208 return nil, fmt.Errorf("could not extract from file: %w", err) 209 } 210 211 // adding the current file credentials to the ones found in included files 212 for _, s := range sections { 213 if !isSecret(s) { 214 continue 215 } 216 res = append(res, &inventory.Secret{Secret: *s, Location: path}) 217 } 218 219 return res, nil 220 } 221 222 // includeDir recursively loads .cnf and .ini files from a specified directory. 223 func (e *Extractor) includeDir(ctx context.Context, input *filesystem.ScanInput, dir string) ([]*inventory.Secret, error) { 224 entries, err := fs.ReadDir(input.FS, dir) 225 if err != nil { 226 return nil, fmt.Errorf("could not read folder %s: %w", dir, err) 227 } 228 229 res := []*inventory.Secret{} 230 for _, entry := range entries { 231 if err := ctx.Err(); err != nil { 232 return nil, err 233 } 234 if entry.IsDir() { 235 continue 236 } 237 path := filepath.ToSlash(filepath.Join(dir, entry.Name())) 238 if !strings.HasSuffix(path, ".cnf") && !strings.HasSuffix(path, ".ini") { 239 continue 240 } 241 sections, err := e.includeFile(ctx, input, path) 242 if err != nil { 243 return nil, err 244 } 245 res = append(res, sections...) 246 } 247 return res, nil 248 }