github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/php/interpreter_cataloger.go (about)

     1  package php
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path"
     7  	"strings"
     8  
     9  	"github.com/anchore/packageurl-go"
    10  	"github.com/anchore/syft/internal"
    11  	"github.com/anchore/syft/internal/unknown"
    12  	"github.com/anchore/syft/syft/artifact"
    13  	"github.com/anchore/syft/syft/cpe"
    14  	"github.com/anchore/syft/syft/file"
    15  	"github.com/anchore/syft/syft/pkg"
    16  	"github.com/anchore/syft/syft/pkg/cataloger/internal/binutils"
    17  )
    18  
    19  type interpreterCataloger struct {
    20  	name                   string
    21  	extensionsGlob         string
    22  	interpreterClassifiers []binutils.Classifier
    23  }
    24  
    25  // NewInterpreterCataloger returns a new cataloger for PHP interpreters (php and php-fpm) as well as any installed C extensions.
    26  func NewInterpreterCataloger() pkg.Cataloger { //nolint:funlen
    27  	name := "php-interpreter-cataloger"
    28  	m := binutils.ContextualEvidenceMatchers{CatalogerName: name}
    29  	return interpreterCataloger{
    30  		name: name,
    31  		// example matches:
    32  		// - as found in php-fpm docker library images: /usr/local/lib/php/extensions/no-debug-non-zts-20230831/bcmath.so
    33  		// - as found in alpine images: /usr/lib/php83/modules/bcmath.so
    34  		extensionsGlob: "**/php*/**/*.so",
    35  		interpreterClassifiers: []binutils.Classifier{
    36  			{
    37  				Class:    "php-cli-binary",
    38  				FileGlob: "**/php*",
    39  				EvidenceMatcher: m.FileNameTemplateVersionMatcher(
    40  					`(.*/|^)php[0-9]*$`,
    41  					`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
    42  				Package: "php-cli",
    43  				PURL: packageurl.PackageURL{
    44  					Type: packageurl.TypeGeneric,
    45  					Name: "php-cli",
    46  					// the version will be filled in dynamically
    47  				},
    48  				CPEs: []cpe.CPE{
    49  					{
    50  						Attributes: cpe.Attributes{
    51  							Part:    "a",
    52  							Vendor:  "php",
    53  							Product: "php",
    54  						},
    55  						Source: cpe.NVDDictionaryLookupSource,
    56  					},
    57  				},
    58  			},
    59  			{
    60  				Class:    "php-fpm-binary",
    61  				FileGlob: "**/php-fpm*",
    62  				EvidenceMatcher: m.FileContentsVersionMatcher(
    63  					`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
    64  				Package: "php-fpm",
    65  				PURL: packageurl.PackageURL{
    66  					Type: packageurl.TypeGeneric,
    67  					Name: "php-fpm",
    68  					// the version will be filled in dynamically
    69  				},
    70  				CPEs: []cpe.CPE{
    71  					{
    72  						Attributes: cpe.Attributes{
    73  							Part:    "a",
    74  							Vendor:  "php",
    75  							Product: "php",
    76  						},
    77  						Source: cpe.NVDDictionaryLookupSource,
    78  					},
    79  				},
    80  			},
    81  			{
    82  				Class:    "php-apache-binary",
    83  				FileGlob: "**/apache*/**/libphp*.so",
    84  				EvidenceMatcher: m.FileContentsVersionMatcher(
    85  					`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
    86  				Package: "libphp",
    87  				PURL: packageurl.PackageURL{
    88  					Type: packageurl.TypeGeneric,
    89  					Name: "php",
    90  					// the version will be filled in dynamically
    91  				},
    92  				CPEs: []cpe.CPE{
    93  					{
    94  						Attributes: cpe.Attributes{
    95  							Part:    "a",
    96  							Vendor:  "php",
    97  							Product: "php",
    98  						},
    99  						Source: cpe.NVDDictionaryLookupSource,
   100  					},
   101  				},
   102  			},
   103  		},
   104  	}
   105  }
   106  
   107  func (p interpreterCataloger) Name() string {
   108  	return p.name
   109  }
   110  
   111  func (p interpreterCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
   112  	interpreterPkgs, intErrs := p.catalogInterpreters(resolver)
   113  	extensionPkgs, extErrs := p.catalogExtensions(resolver)
   114  
   115  	// TODO: a future iteration of this cataloger could be to read all php.ini / php/conf.d/*.ini files and indicate which extensions are enabled
   116  	// and attempt to resolve the extension_dir. This can be tricky as it is a #define in the php source code and not always available
   117  	// in configuration. For the meantime we report all extensions present
   118  
   119  	// create a relationship for each interpreter package to the extensions
   120  	var relationships []artifact.Relationship
   121  	for _, interpreter := range interpreterPkgs {
   122  		for _, extension := range extensionPkgs {
   123  			relationships = append(relationships, artifact.Relationship{
   124  				From: extension,
   125  				To:   interpreter,
   126  				Type: artifact.DependencyOfRelationship,
   127  			})
   128  		}
   129  	}
   130  
   131  	var allPkgs []pkg.Package
   132  	allPkgs = append(allPkgs, interpreterPkgs...)
   133  	allPkgs = append(allPkgs, extensionPkgs...)
   134  
   135  	return allPkgs, relationships, unknown.Join(intErrs, extErrs)
   136  }
   137  
   138  func (p interpreterCataloger) catalogInterpreters(resolver file.Resolver) ([]pkg.Package, error) {
   139  	var errs error
   140  	var packages []pkg.Package
   141  	for _, cls := range p.interpreterClassifiers {
   142  		locations, err := resolver.FilesByGlob(cls.FileGlob)
   143  		if err != nil {
   144  			// convert any file.Resolver path errors to unknowns with locations
   145  			errs = unknown.Join(errs, unknown.ProcessPathErrors(err))
   146  			continue
   147  		}
   148  		for _, location := range locations {
   149  			pkgs, err := cls.EvidenceMatcher(cls, binutils.MatcherContext{Resolver: resolver, Location: location})
   150  			if err != nil {
   151  				errs = unknown.Append(errs, location, err)
   152  				continue
   153  			}
   154  			packages = append(packages, pkgs...)
   155  		}
   156  	}
   157  	return packages, errs
   158  }
   159  
   160  func (p interpreterCataloger) catalogExtensions(resolver file.Resolver) ([]pkg.Package, error) {
   161  	locations, err := resolver.FilesByGlob(p.extensionsGlob)
   162  	if err != nil {
   163  		// convert any file.Resolver path errors to unknowns with locations
   164  		return nil, unknown.ProcessPathErrors(err)
   165  	}
   166  
   167  	var packages []pkg.Package
   168  	var errs error
   169  	for _, location := range locations {
   170  		pkgs, err := p.catalogExtension(resolver, location)
   171  		if err != nil {
   172  			errs = unknown.Append(errs, location, err)
   173  			continue
   174  		}
   175  		packages = append(packages, pkgs...)
   176  	}
   177  	return packages, errs
   178  }
   179  
   180  func (p interpreterCataloger) catalogExtension(resolver file.Resolver, location file.Location) ([]pkg.Package, error) {
   181  	reader, err := resolver.FileContentsByLocation(location)
   182  	defer internal.CloseAndLogError(reader, location.RealPath)
   183  	if err != nil {
   184  		return nil, unknown.ProcessPathErrors(err)
   185  	}
   186  
   187  	name, cls := p.getClassifier(location.RealPath)
   188  	if name == "" || cls == nil {
   189  		return nil, nil
   190  	}
   191  
   192  	pkgs, err := cls.EvidenceMatcher(*cls, binutils.MatcherContext{Resolver: resolver, Location: location})
   193  	if err != nil {
   194  		return nil, unknown.New(location, err)
   195  	}
   196  
   197  	return pkgs, err
   198  }
   199  
   200  func (p interpreterCataloger) getClassifier(realPath string) (string, *binutils.Classifier) {
   201  	if !strings.HasSuffix(realPath, ".so") {
   202  		return "", nil
   203  	}
   204  
   205  	base := path.Base(realPath)
   206  	name := strings.TrimSuffix(base, ".so")
   207  
   208  	var match string
   209  	switch name {
   210  	case "mysqli":
   211  		match = `(mysqlnd|mysqli)?\s*\x00*(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00+API`
   212  	case "opcache":
   213  		match = `(?m)\x00+(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00+`
   214  	case "zip":
   215  		match = `\x00+(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00+Zip`
   216  	default:
   217  		match = fmt.Sprintf(`(?m)(\x00+%s)?\x00+(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00+API`, name)
   218  	}
   219  
   220  	return name, &binutils.Classifier{
   221  		Class:           fmt.Sprintf("php-ext-%s-binary", name),
   222  		EvidenceMatcher: binutils.FileContentsVersionMatcher(p.name, match),
   223  		Package:         name,
   224  		PURL: packageurl.PackageURL{
   225  			Type: packageurl.TypeGeneric,
   226  			Name: name,
   227  			// the version will be filled in dynamically
   228  		},
   229  		CPEs: []cpe.CPE{
   230  			{
   231  				Attributes: cpe.Attributes{
   232  					Part:    "a",
   233  					Vendor:  fmt.Sprintf("php-%s", name),
   234  					Product: fmt.Sprintf("php-%s", name),
   235  				},
   236  				Source: cpe.GeneratedSource,
   237  			},
   238  		},
   239  	}
   240  }