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 }