github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/secrets/pgpass/pgpass.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 pgpass provides an extractor for identifying secrets in .pgpass files.
    16  package pgpass
    17  
    18  import (
    19  	"bufio"
    20  	"context"
    21  	"fmt"
    22  	"path/filepath"
    23  	"regexp"
    24  	"strings"
    25  
    26  	"github.com/google/osv-scalibr/extractor/filesystem"
    27  	"github.com/google/osv-scalibr/inventory"
    28  	"github.com/google/osv-scalibr/plugin"
    29  )
    30  
    31  // Pgpass is a Veles Secret that holds relevant information for a [Postgres Pgpass](https://www.postgresql.org/docs/current/libpq-pgpass.html).
    32  type Pgpass struct {
    33  	Hostname string
    34  	Port     string
    35  	Database string
    36  	Username string
    37  	Password string
    38  }
    39  
    40  const (
    41  	// Name is the unique name of this extractor.
    42  	Name = "secrets/pgpass"
    43  )
    44  
    45  var (
    46  	// pgpassRe is a regular expression that matches the content of a pgpass file entry
    47  	//
    48  	// Reference:
    49  	// - https://www.postgresql.org/docs/current/libpq-pgpass.html
    50  	//
    51  	// Every entry in the pgpass file is composed by the following fields:
    52  	// hostname:port:database:username:password
    53  	//
    54  	//   - hostname: matches any character except the `:` (that is currently used for separating fields)
    55  	//   - port: matches numbers until 5 digits and * (wildcard)
    56  	//     this group can match ports > 65535 but it is a compromise for regex performance
    57  	//   - database: same as hostname
    58  	//   - username: same as hostname
    59  	//   - password: can match any allowed characters but colons must be escaped
    60  	pgpassRe = regexp.MustCompile(`^([ -9;-~]+):(\*|[0-9]{1,5}):([ -9;-~]+):([ -9;-~]+):((?:\\:|[ -9;-~])+)$`)
    61  )
    62  
    63  // Extractor extracts postres credentials from .pgpass files.
    64  type Extractor struct{}
    65  
    66  // New returns a pgpass extractor.
    67  func New() filesystem.Extractor { return &Extractor{} }
    68  
    69  // Name of the extractor.
    70  func (e Extractor) Name() string { return Name }
    71  
    72  // Version of the extractor.
    73  func (e Extractor) Version() int { return 0 }
    74  
    75  // Requirements of the extractor.
    76  func (e Extractor) Requirements() *plugin.Capabilities {
    77  	return &plugin.Capabilities{}
    78  }
    79  
    80  // FileRequired returns true if the specified file is a .pgpass file.
    81  func (e Extractor) FileRequired(api filesystem.FileAPI) bool {
    82  	return filepath.Base(api.Path()) == ".pgpass"
    83  }
    84  
    85  // Extract extracts PostgreSQL credentials from .pgpass file.
    86  func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) {
    87  	scanner := bufio.NewScanner(input.Reader)
    88  	var secrets []*inventory.Secret
    89  
    90  	for scanner.Scan() {
    91  		line := strings.TrimSpace(scanner.Text())
    92  		// Skip empty lines and comments.
    93  		if line == "" || strings.HasPrefix(line, "#") {
    94  			continue
    95  		}
    96  
    97  		matches := pgpassRe.FindStringSubmatch(line)
    98  
    99  		if len(matches) == 6 {
   100  			password := matches[5]
   101  			// Skip entries where the password is a single '*'
   102  			if password == "*" {
   103  				continue
   104  			}
   105  
   106  			pgpassSecret := Pgpass{
   107  				Hostname: matches[1],
   108  				Port:     matches[2],
   109  				Database: matches[3],
   110  				Username: matches[4],
   111  				Password: matches[5],
   112  			}
   113  
   114  			secrets = append(secrets, &inventory.Secret{
   115  				Secret:   pgpassSecret,
   116  				Location: input.Path,
   117  			})
   118  		}
   119  	}
   120  
   121  	if err := scanner.Err(); err != nil {
   122  		return inventory.Inventory{}, fmt.Errorf("error reading .pgpass file: %w", err)
   123  	}
   124  
   125  	return inventory.Inventory{Secrets: secrets}, nil
   126  }