github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/java/internal/maven/resolver.go (about)

     1  package maven
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"reflect"
    14  	"regexp"
    15  	"slices"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/vifraa/gopom"
    20  
    21  	"github.com/anchore/syft/internal"
    22  	"github.com/anchore/syft/internal/cache"
    23  	"github.com/anchore/syft/internal/log"
    24  	"github.com/anchore/syft/syft/file"
    25  )
    26  
    27  // ID is the unique identifier for a package in Maven
    28  type ID struct {
    29  	GroupID    string
    30  	ArtifactID string
    31  	Version    string
    32  }
    33  
    34  func NewID(groupID, artifactID, version string) ID {
    35  	return ID{
    36  		GroupID:    groupID,
    37  		ArtifactID: artifactID,
    38  		Version:    version,
    39  	}
    40  }
    41  
    42  func (m ID) String() string {
    43  	return fmt.Sprintf("(groupId: %s artifactId: %s version: %s)", m.GroupID, m.ArtifactID, m.Version)
    44  }
    45  
    46  // Valid indicates that the given maven ID has values for groupId, artifactId, and version
    47  func (m ID) Valid() bool {
    48  	return m.GroupID != "" && m.ArtifactID != "" && m.Version != ""
    49  }
    50  
    51  var expressionMatcher = regexp.MustCompile("[$][{][^}]+[}]")
    52  
    53  // Resolver is a short-lived utility to resolve maven poms from multiple sources, including:
    54  // the scanned filesystem, local maven cache directories, remote maven repositories, and the syft cache
    55  type Resolver struct {
    56  	cfg                  Config
    57  	cache                cache.Cache
    58  	resolved             map[ID]*Project
    59  	remoteRequestTimeout time.Duration
    60  	checkedLocalRepo     bool
    61  	// fileResolver and pomLocations are used to resolve parent poms by relativePath
    62  	fileResolver file.Resolver
    63  	pomLocations map[*Project]file.Location
    64  }
    65  
    66  // NewResolver constructs a new Resolver with the given configuration.
    67  // NOTE: the fileResolver is optional and if provided will be used to resolve parent poms by relative path
    68  func NewResolver(fileResolver file.Resolver, cfg Config) *Resolver {
    69  	return &Resolver{
    70  		cfg:                  cfg,
    71  		cache:                cache.GetManager().GetCache("java/maven/repo", "v1"),
    72  		resolved:             map[ID]*Project{},
    73  		remoteRequestTimeout: time.Second * 10,
    74  		fileResolver:         fileResolver,
    75  		pomLocations:         map[*Project]file.Location{},
    76  	}
    77  }
    78  
    79  // ResolveProperty gets property values by emulating maven property resolution logic, looking in the project's variables
    80  // as well as supporting the project expressions like ${project.parent.groupId}.
    81  // Properties which are not resolved result in empty string ""
    82  func (r *Resolver) ResolveProperty(ctx context.Context, pom *Project, propertyValue *string) string {
    83  	return r.resolvePropertyValue(ctx, propertyValue, nil, pom)
    84  }
    85  
    86  // resolvePropertyValue resolves property values by emulating maven property resolution logic, looking in the project's variables
    87  // as well as supporting the project expressions like ${project.parent.groupId}.
    88  // Properties which are not resolved result in empty string ""
    89  func (r *Resolver) resolvePropertyValue(ctx context.Context, propertyValue *string, resolvingProperties []string, resolutionContext ...*Project) string {
    90  	if propertyValue == nil {
    91  		return ""
    92  	}
    93  	resolved, err := r.resolveExpression(ctx, resolutionContext, *propertyValue, resolvingProperties)
    94  	if err != nil {
    95  		log.WithFields("error", err, "propertyValue", *propertyValue).Trace("error resolving maven property")
    96  		return ""
    97  	}
    98  	return resolved
    99  }
   100  
   101  // resolveExpression resolves an expression, which may be a plain string or a string with ${ property.references }
   102  func (r *Resolver) resolveExpression(ctx context.Context, resolutionContext []*Project, expression string, resolvingProperties []string) (string, error) {
   103  	log.Tracef("resolving expression: '%v' in context: %v", expression, resolutionContext)
   104  
   105  	var errs error
   106  	return expressionMatcher.ReplaceAllStringFunc(expression, func(match string) string {
   107  		log.Tracef("resolving property: '%v' in context: %v", expression, resolutionContext)
   108  		propertyExpression := strings.TrimSpace(match[2 : len(match)-1]) // remove leading ${ and trailing }
   109  		resolved, err := r.resolveProperty(ctx, resolutionContext, propertyExpression, resolvingProperties)
   110  		if err != nil {
   111  			errs = errors.Join(errs, err)
   112  			return ""
   113  		}
   114  		return resolved
   115  	}), errs
   116  }
   117  
   118  // resolveProperty resolves properties recursively from the root project
   119  func (r *Resolver) resolveProperty(ctx context.Context, resolutionContext []*Project, propertyExpression string, resolvingProperties []string) (string, error) {
   120  	// prevent cycles
   121  	if slices.Contains(resolvingProperties, propertyExpression) {
   122  		return "", fmt.Errorf("cycle detected resolving: %s", propertyExpression)
   123  	}
   124  	if len(resolutionContext) == 0 {
   125  		return "", fmt.Errorf("no project variable resolution context provided for expression: '%s'", propertyExpression)
   126  	}
   127  	resolvingProperties = append(resolvingProperties, propertyExpression)
   128  
   129  	// only resolve project. properties in the context of the current project pom
   130  	value, err := r.resolveProjectProperty(ctx, resolutionContext, resolutionContext[len(resolutionContext)-1], propertyExpression, resolvingProperties)
   131  	if err != nil {
   132  		return value, err
   133  	}
   134  	if value != "" {
   135  		return value, nil
   136  	}
   137  
   138  	var resolvingParents []*Project
   139  	for _, pom := range resolutionContext {
   140  		current := pom
   141  		for parentDepth := 0; current != nil; parentDepth++ {
   142  			if slices.Contains(resolvingParents, current) {
   143  				log.WithFields("property", propertyExpression, "mavenID", r.resolveID(ctx, resolvingProperties, resolvingParents...)).Error("got circular reference while resolving property")
   144  				break // some sort of circular reference -- we've already seen this project
   145  			}
   146  			if r.cfg.MaxParentRecursiveDepth > 0 && parentDepth > r.cfg.MaxParentRecursiveDepth {
   147  				return "", fmt.Errorf("maximum parent recursive depth (%v) reached resolving property: %v", r.cfg.MaxParentRecursiveDepth, propertyExpression)
   148  			}
   149  			if current.Properties != nil && current.Properties.Entries != nil {
   150  				if value, ok := current.Properties.Entries[propertyExpression]; ok {
   151  					return r.resolveExpression(ctx, resolutionContext, value, resolvingProperties) // property values can contain expressions
   152  				}
   153  			}
   154  			resolvingParents = append(resolvingParents, current)
   155  			current, err = r.resolveParent(ctx, current, resolvingProperties...)
   156  			if err != nil {
   157  				return "", err
   158  			}
   159  		}
   160  	}
   161  
   162  	return "", fmt.Errorf("unable to resolve property: %s", propertyExpression)
   163  }
   164  
   165  // resolveProjectProperty resolves properties on the project
   166  //
   167  //nolint:gocognit
   168  func (r *Resolver) resolveProjectProperty(ctx context.Context, resolutionContext []*Project, pom *Project, propertyExpression string, resolving []string) (string, error) {
   169  	// see if we have a project.x expression and process this based
   170  	// on the xml tags in gopom
   171  	parts := strings.Split(propertyExpression, ".")
   172  	numParts := len(parts)
   173  	if numParts > 1 && strings.TrimSpace(parts[0]) == "project" {
   174  		pomValue := reflect.ValueOf(pom).Elem()
   175  		pomValueType := pomValue.Type()
   176  		for partNum := 1; partNum < numParts; partNum++ {
   177  			if pomValueType.Kind() != reflect.Struct {
   178  				break
   179  			}
   180  
   181  			part := parts[partNum]
   182  			// these two fields are directly inherited from the pom parent values
   183  			if partNum == 1 && pom.Parent != nil {
   184  				switch part {
   185  				case "version":
   186  					if pom.Version == nil && pom.Parent.Version != nil {
   187  						return r.resolveExpression(ctx, resolutionContext, *pom.Parent.Version, resolving)
   188  					}
   189  				case "groupID":
   190  					if pom.GroupID == nil && pom.Parent.GroupID != nil {
   191  						return r.resolveExpression(ctx, resolutionContext, *pom.Parent.GroupID, resolving)
   192  					}
   193  				}
   194  			}
   195  			for fieldNum := 0; fieldNum < pomValueType.NumField(); fieldNum++ {
   196  				f := pomValueType.Field(fieldNum)
   197  				tag := f.Tag.Get("xml")
   198  				tag = strings.Split(tag, ",")[0]
   199  				// a segment of the property name matches the xml tag for the field,
   200  				// so we need to recurse down the nested structs or return a match
   201  				// if we're done.
   202  				if part != tag {
   203  					continue
   204  				}
   205  
   206  				pomValue = pomValue.Field(fieldNum)
   207  				pomValueType = pomValue.Type()
   208  				if pomValueType.Kind() == reflect.Ptr {
   209  					// we were recursing down the nested structs, but one of the steps
   210  					// we need to take is a nil pointer, so give up
   211  					if pomValue.IsNil() {
   212  						return "", fmt.Errorf("property undefined: %s", propertyExpression)
   213  					}
   214  					pomValue = pomValue.Elem()
   215  					if !pomValue.IsZero() {
   216  						// we found a non-zero value whose tag matches this part of the property name
   217  						pomValueType = pomValue.Type()
   218  					}
   219  				}
   220  				// If this was the last part of the property name, return the value
   221  				if partNum == numParts-1 {
   222  					value := fmt.Sprintf("%v", pomValue.Interface())
   223  					return r.resolveExpression(ctx, resolutionContext, value, resolving)
   224  				}
   225  				break
   226  			}
   227  		}
   228  	}
   229  	return "", nil
   230  }
   231  
   232  // ResolveParent resolves the parent definition, and returns a POM for the parent, which is possibly incomplete, or nil
   233  func (r *Resolver) ResolveParent(ctx context.Context, pom *Project) (*Project, error) {
   234  	if pom == nil || pom.Parent == nil {
   235  		return nil, nil
   236  	}
   237  
   238  	parent, err := r.resolveParent(ctx, pom)
   239  	if parent != nil {
   240  		return parent, err
   241  	}
   242  
   243  	groupID := r.ResolveProperty(ctx, pom, pom.Parent.GroupID)
   244  	if groupID == "" {
   245  		groupID = r.ResolveProperty(ctx, pom, pom.GroupID)
   246  	}
   247  	artifactID := r.ResolveProperty(ctx, pom, pom.Parent.ArtifactID)
   248  	version := r.ResolveProperty(ctx, pom, pom.Parent.Version)
   249  
   250  	if artifactID != "" && version != "" {
   251  		return &Project{
   252  			GroupID:    &groupID,
   253  			ArtifactID: &artifactID,
   254  			Version:    &version,
   255  		}, nil
   256  	}
   257  
   258  	return nil, fmt.Errorf("unsufficient information to create a parent pom project, id: %s", NewID(groupID, artifactID, version))
   259  }
   260  
   261  // ResolveID creates an ID from a pom, resolving parent information as necessary
   262  func (r *Resolver) ResolveID(ctx context.Context, pom *Project) ID {
   263  	return r.resolveID(ctx, nil, pom)
   264  }
   265  
   266  // resolveID creates a new ID from a pom, resolving parent information as necessary
   267  func (r *Resolver) resolveID(ctx context.Context, resolvingProperties []string, resolutionContext ...*Project) ID {
   268  	if len(resolutionContext) == 0 || resolutionContext[0] == nil {
   269  		return ID{}
   270  	}
   271  	pom := resolutionContext[len(resolutionContext)-1] // get topmost pom
   272  	if pom == nil {
   273  		return ID{}
   274  	}
   275  
   276  	groupID := r.resolvePropertyValue(ctx, pom.GroupID, resolvingProperties, resolutionContext...)
   277  	artifactID := r.resolvePropertyValue(ctx, pom.ArtifactID, resolvingProperties, resolutionContext...)
   278  	version := r.resolvePropertyValue(ctx, pom.Version, resolvingProperties, resolutionContext...)
   279  	if pom.Parent != nil {
   280  		// groupId and version are able to be inherited from the parent, but importantly: not artifactId. see:
   281  		// https://maven.apache.org/guides/introduction/introduction-to-the-pom.html#the-solution
   282  		if groupID == "" && deref(pom.GroupID) == "" {
   283  			groupID = r.resolvePropertyValue(ctx, pom.Parent.GroupID, resolvingProperties, resolutionContext...)
   284  		}
   285  		if version == "" && deref(pom.Version) == "" {
   286  			version = r.resolvePropertyValue(ctx, pom.Parent.Version, resolvingProperties, resolutionContext...)
   287  		}
   288  	}
   289  	return ID{groupID, artifactID, version}
   290  }
   291  
   292  // ResolveDependencyID creates an ID from a dependency element in a pom, resolving information as necessary
   293  func (r *Resolver) ResolveDependencyID(ctx context.Context, pom *Project, dep Dependency) ID {
   294  	if pom == nil {
   295  		return ID{}
   296  	}
   297  
   298  	groupID := r.resolvePropertyValue(ctx, dep.GroupID, nil, pom)
   299  	artifactID := r.resolvePropertyValue(ctx, dep.ArtifactID, nil, pom)
   300  	version := r.resolvePropertyValue(ctx, dep.Version, nil, pom)
   301  
   302  	var err error
   303  	if version == "" {
   304  		version, err = r.resolveInheritedVersion(ctx, pom, groupID, artifactID)
   305  	}
   306  
   307  	depID := ID{groupID, artifactID, version}
   308  
   309  	if err != nil {
   310  		log.WithFields("error", err, "ID", r.ResolveID(ctx, pom), "dependencyID", depID)
   311  	}
   312  
   313  	return depID
   314  }
   315  
   316  // FindPom gets a pom from cache, local repository, or from a remote Maven repository depending on configuration
   317  func (r *Resolver) FindPom(ctx context.Context, groupID, artifactID, version string) (*Project, error) {
   318  	if groupID == "" || artifactID == "" || version == "" {
   319  		return nil, fmt.Errorf("invalid maven pom specification, require non-empty values for groupID: '%s', artifactID: '%s', version: '%s'", groupID, artifactID, version)
   320  	}
   321  
   322  	id := ID{groupID, artifactID, version}
   323  	existingPom := r.resolved[id]
   324  
   325  	if existingPom != nil {
   326  		return existingPom, nil
   327  	}
   328  
   329  	var errs error
   330  
   331  	// try to resolve first from local maven repo
   332  	if r.cfg.UseLocalRepository {
   333  		pom, err := r.findPomInLocalRepository(groupID, artifactID, version)
   334  		if pom != nil {
   335  			r.resolved[id] = pom
   336  			return pom, nil
   337  		}
   338  		errs = errors.Join(errs, err)
   339  	}
   340  
   341  	// resolve via network maven repository
   342  	if r.cfg.UseNetwork {
   343  		pom, err := r.findPomInRemotes(ctx, groupID, artifactID, version)
   344  		if pom != nil {
   345  			r.resolved[id] = pom
   346  			return pom, nil
   347  		}
   348  		errs = errors.Join(errs, err)
   349  	}
   350  
   351  	return nil, fmt.Errorf("unable to resolve pom %s %s %s: %w", groupID, artifactID, version, errs)
   352  }
   353  
   354  // findPomInLocalRepository attempts to get the POM from the users local maven repository
   355  func (r *Resolver) findPomInLocalRepository(groupID, artifactID, version string) (*Project, error) {
   356  	groupPath := filepath.Join(strings.Split(groupID, ".")...)
   357  	pomFilePath := filepath.Join(r.cfg.LocalRepositoryDir, groupPath, artifactID, version, artifactID+"-"+version+".pom")
   358  	pomFile, err := os.Open(pomFilePath)
   359  	if err != nil {
   360  		if !r.checkedLocalRepo && errors.Is(err, os.ErrNotExist) {
   361  			r.checkedLocalRepo = true
   362  			// check if the directory exists at all, and if not just stop trying to resolve local maven files
   363  			fi, err := os.Stat(r.cfg.LocalRepositoryDir)
   364  			if errors.Is(err, os.ErrNotExist) || !fi.IsDir() {
   365  				log.WithFields("error", err, "repositoryDir", r.cfg.LocalRepositoryDir).
   366  					Info("local maven repository is not a readable directory, stopping local resolution")
   367  				r.cfg.UseLocalRepository = false
   368  			}
   369  		}
   370  		return nil, err
   371  	}
   372  	defer internal.CloseAndLogError(pomFile, pomFilePath)
   373  
   374  	return ParsePomXML(pomFile)
   375  }
   376  
   377  // findPomInRemotes download the pom file from all configured Maven repositories over HTTP
   378  func (r *Resolver) findPomInRemotes(ctx context.Context, groupID, artifactID, version string) (*Project, error) {
   379  	var errs error
   380  	for _, repo := range r.cfg.Repositories {
   381  		pom, err := r.findPomInRemoteRepository(ctx, repo, groupID, artifactID, version)
   382  		if err != nil {
   383  			errs = errors.Join(errs, err)
   384  		}
   385  		if pom != nil {
   386  			return pom, err
   387  		}
   388  	}
   389  	return nil, fmt.Errorf("pom for %v not found in any remote repository: %w", ID{groupID, artifactID, version}, errs)
   390  }
   391  
   392  // findPomInRemoteRepository download the pom file from a (remote) Maven repository over HTTP
   393  func (r *Resolver) findPomInRemoteRepository(ctx context.Context, repo string, groupID, artifactID, version string) (*Project, error) {
   394  	if groupID == "" || artifactID == "" || version == "" {
   395  		return nil, fmt.Errorf("missing/incomplete maven artifact coordinates -- groupId: '%s' artifactId: '%s', version: '%s'", groupID, artifactID, version)
   396  	}
   397  
   398  	requestURL, err := remotePomURL(repo, groupID, artifactID, version)
   399  	if err != nil {
   400  		return nil, fmt.Errorf("unable to find pom in remote due to: %w", err)
   401  	}
   402  
   403  	// Downloading snapshots requires additional steps to determine the latest snapshot version.
   404  	// See: https://maven.apache.org/ref/3-LATEST/maven-repository-metadata/
   405  	if strings.HasSuffix(version, "-SNAPSHOT") {
   406  		return nil, fmt.Errorf("downloading snapshot artifacts is not supported, got: %s", requestURL)
   407  	}
   408  
   409  	cacheKey := strings.TrimPrefix(strings.TrimPrefix(requestURL, "http://"), "https://")
   410  	reader, err := r.cacheResolveReader(cacheKey, func() (io.ReadCloser, error) {
   411  		if err != nil {
   412  			return nil, err
   413  		}
   414  		log.WithFields("url", requestURL).Info("fetching parent pom from remote maven repository")
   415  
   416  		req, err := http.NewRequest(http.MethodGet, requestURL, nil)
   417  		if err != nil {
   418  			return nil, fmt.Errorf("unable to create request for Maven central: %w", err)
   419  		}
   420  
   421  		req = req.WithContext(ctx)
   422  
   423  		client := http.Client{
   424  			Timeout: r.remoteRequestTimeout,
   425  		}
   426  
   427  		resp, err := client.Do(req)
   428  		if err != nil {
   429  			return nil, fmt.Errorf("unable to get pom from Maven repository %v: %w", requestURL, err)
   430  		}
   431  		if resp.StatusCode == http.StatusNotFound {
   432  			return nil, fmt.Errorf("pom not found in Maven repository at: %v", requestURL)
   433  		}
   434  		return resp.Body, err
   435  	})
   436  	if err != nil {
   437  		return nil, err
   438  	}
   439  	if reader, ok := reader.(io.Closer); ok {
   440  		defer internal.CloseAndLogError(reader, requestURL)
   441  	}
   442  	pom, err := ParsePomXML(reader)
   443  	if err != nil {
   444  		return nil, fmt.Errorf("unable to parse pom from Maven repository url %v: %w", requestURL, err)
   445  	}
   446  	return pom, nil
   447  }
   448  
   449  // cacheResolveReader attempts to get a reader from cache, otherwise caches the contents of the resolve() function.
   450  // this function is guaranteed to return an unread reader for the correct contents.
   451  // NOTE: this could be promoted to the internal cache package as a specialized version of the cache.Resolver
   452  // if there are more users of this functionality
   453  func (r *Resolver) cacheResolveReader(key string, resolve func() (io.ReadCloser, error)) (io.Reader, error) {
   454  	reader, err := r.cache.Read(key)
   455  	if err == nil && reader != nil {
   456  		return reader, err
   457  	}
   458  
   459  	contentReader, err := resolve()
   460  	if err != nil {
   461  		return nil, err
   462  	}
   463  	defer internal.CloseAndLogError(contentReader, key)
   464  
   465  	// store the contents to return a new reader with the same content
   466  	contents, err := io.ReadAll(contentReader)
   467  	if err != nil {
   468  		return nil, err
   469  	}
   470  	err = r.cache.Write(key, bytes.NewBuffer(contents))
   471  	return bytes.NewBuffer(contents), err
   472  }
   473  
   474  // resolveParent attempts to resolve the parent for the given pom
   475  func (r *Resolver) resolveParent(ctx context.Context, pom *Project, resolvingProperties ...string) (*Project, error) {
   476  	if pom == nil || pom.Parent == nil {
   477  		return nil, nil
   478  	}
   479  	parent := pom.Parent
   480  	pomWithoutParent := *pom
   481  	pomWithoutParent.Parent = nil
   482  	groupID := r.resolvePropertyValue(ctx, parent.GroupID, resolvingProperties, &pomWithoutParent)
   483  	artifactID := r.resolvePropertyValue(ctx, parent.ArtifactID, resolvingProperties, &pomWithoutParent)
   484  	version := r.resolvePropertyValue(ctx, parent.Version, resolvingProperties, &pomWithoutParent)
   485  
   486  	// check cache before resolving
   487  	parentID := ID{groupID, artifactID, version}
   488  	if resolvedParent, ok := r.resolved[parentID]; ok {
   489  		return resolvedParent, nil
   490  	}
   491  
   492  	// check if the pom exists in the fileResolver
   493  	parentPom := r.findParentPomByRelativePath(ctx, pom, parentID, resolvingProperties)
   494  	if parentPom != nil {
   495  		return parentPom, nil
   496  	}
   497  
   498  	// find POM normally
   499  	return r.FindPom(ctx, groupID, artifactID, version)
   500  }
   501  
   502  // resolveInheritedVersion attempts to find the version of a dependency (groupID, artifactID) by searching all parent poms and imported managed dependencies
   503  //
   504  //nolint:gocognit
   505  func (r *Resolver) resolveInheritedVersion(ctx context.Context, pom *Project, groupID, artifactID string, resolutionContext ...*Project) (string, error) {
   506  	if pom == nil {
   507  		return "", fmt.Errorf("nil pom provided to findInheritedVersion")
   508  	}
   509  	if r.cfg.MaxParentRecursiveDepth > 0 && len(resolutionContext) > r.cfg.MaxParentRecursiveDepth {
   510  		return "", fmt.Errorf("maximum depth reached attempting to resolve version for: %s:%s at: %v", groupID, artifactID, r.ResolveID(ctx, pom))
   511  	}
   512  	if slices.Contains(resolutionContext, pom) {
   513  		return "", fmt.Errorf("cycle detected attempting to resolve version for: %s:%s at: %v", groupID, artifactID, r.ResolveID(ctx, pom))
   514  	}
   515  	resolutionContext = append(resolutionContext, pom)
   516  
   517  	var err error
   518  	var version string
   519  
   520  	// check for entries in dependencyManagement first
   521  	for _, dep := range pomManagedDependencies(pom) {
   522  		depGroupID := r.resolvePropertyValue(ctx, dep.GroupID, nil, resolutionContext...)
   523  		depArtifactID := r.resolvePropertyValue(ctx, dep.ArtifactID, nil, resolutionContext...)
   524  		if depGroupID == groupID && depArtifactID == artifactID {
   525  			version = r.resolvePropertyValue(ctx, dep.Version, nil, resolutionContext...)
   526  			if version != "" {
   527  				return version, nil
   528  			}
   529  		}
   530  
   531  		// imported pom files should be treated just like parent poms, they are used to define versions of dependencies
   532  		if deref(dep.Type) == "pom" && deref(dep.Scope) == "import" {
   533  			depVersion := r.resolvePropertyValue(ctx, dep.Version, nil, resolutionContext...)
   534  
   535  			depPom, err := r.FindPom(ctx, depGroupID, depArtifactID, depVersion)
   536  			if err != nil || depPom == nil {
   537  				log.WithFields("error", err, "ID", r.ResolveID(ctx, pom), "dependencyID", ID{depGroupID, depArtifactID, depVersion}).
   538  					Debug("unable to find imported pom looking for managed dependencies")
   539  				continue
   540  			}
   541  			version, err = r.resolveInheritedVersion(ctx, depPom, groupID, artifactID, resolutionContext...)
   542  			if err != nil {
   543  				log.WithFields("error", err, "ID", r.ResolveID(ctx, pom), "dependencyID", ID{depGroupID, depArtifactID, depVersion}).
   544  					Debug("error during findInheritedVersion")
   545  			}
   546  			if version != "" {
   547  				return version, nil
   548  			}
   549  		}
   550  	}
   551  
   552  	// recursively check parents
   553  	parent, err := r.resolveParent(ctx, pom)
   554  	if err != nil {
   555  		return "", err
   556  	}
   557  	if parent != nil {
   558  		version, err = r.resolveInheritedVersion(ctx, parent, groupID, artifactID, resolutionContext...)
   559  		if err != nil {
   560  			return "", err
   561  		}
   562  		if version != "" {
   563  			return version, nil
   564  		}
   565  	}
   566  
   567  	// check for inherited dependencies
   568  	for _, dep := range DirectPomDependencies(pom) {
   569  		depGroupID := r.resolvePropertyValue(ctx, dep.GroupID, nil, resolutionContext...)
   570  		depArtifactID := r.resolvePropertyValue(ctx, dep.ArtifactID, nil, resolutionContext...)
   571  		if depGroupID == groupID && depArtifactID == artifactID {
   572  			version = r.resolvePropertyValue(ctx, dep.Version, nil, resolutionContext...)
   573  			if version != "" {
   574  				return version, nil
   575  			}
   576  		}
   577  	}
   578  
   579  	return "", nil
   580  }
   581  
   582  // FindLicenses attempts to find a pom, and once found attempts to resolve licenses traversing
   583  // parent poms as necessary
   584  func (r *Resolver) FindLicenses(ctx context.Context, groupID, artifactID, version string) ([]gopom.License, error) {
   585  	pom, err := r.FindPom(ctx, groupID, artifactID, version)
   586  	if pom == nil || err != nil {
   587  		return nil, err
   588  	}
   589  	return r.resolveLicenses(ctx, pom)
   590  }
   591  
   592  // ResolveLicenses searches the pom for license, resolving and traversing parent poms if needed
   593  func (r *Resolver) ResolveLicenses(ctx context.Context, pom *Project) ([]License, error) {
   594  	return r.resolveLicenses(ctx, pom)
   595  }
   596  
   597  // resolveLicenses searches the pom for license, traversing parent poms if needed
   598  func (r *Resolver) resolveLicenses(ctx context.Context, pom *Project, processing ...ID) ([]License, error) {
   599  	id := r.ResolveID(ctx, pom)
   600  	if slices.Contains(processing, id) {
   601  		return nil, fmt.Errorf("cycle detected resolving licenses for: %v", id)
   602  	}
   603  	if r.cfg.MaxParentRecursiveDepth > 0 && len(processing) > r.cfg.MaxParentRecursiveDepth {
   604  		return nil, fmt.Errorf("maximum parent recursive depth (%v) reached: %v", r.cfg.MaxParentRecursiveDepth, processing)
   605  	}
   606  
   607  	directLicenses := r.pomLicenses(ctx, pom)
   608  	if len(directLicenses) > 0 {
   609  		return directLicenses, nil
   610  	}
   611  
   612  	parent, err := r.resolveParent(ctx, pom)
   613  	if err != nil {
   614  		return nil, err
   615  	}
   616  	if parent == nil {
   617  		return nil, nil
   618  	}
   619  	return r.resolveLicenses(ctx, parent, append(processing, id)...)
   620  }
   621  
   622  // pomLicenses appends the directly specified licenses with non-empty name or url
   623  func (r *Resolver) pomLicenses(ctx context.Context, pom *Project) []License {
   624  	var out []License
   625  	for _, license := range deref(pom.Licenses) {
   626  		// if we find non-empty licenses, return them
   627  		name := r.resolvePropertyValue(ctx, license.Name, nil, pom)
   628  		url := r.resolvePropertyValue(ctx, license.URL, nil, pom)
   629  		if name != "" || url != "" {
   630  			out = append(out, license)
   631  		}
   632  	}
   633  	return out
   634  }
   635  
   636  func (r *Resolver) findParentPomByRelativePath(ctx context.Context, pom *Project, parentID ID, resolvingProperties []string) *Project {
   637  	// can't resolve without a file resolver
   638  	if r.fileResolver == nil {
   639  		return nil
   640  	}
   641  
   642  	pomLocation, hasPomLocation := r.pomLocations[pom]
   643  	if !hasPomLocation || pom == nil || pom.Parent == nil {
   644  		return nil
   645  	}
   646  	relativePath := r.resolvePropertyValue(ctx, pom.Parent.RelativePath, resolvingProperties, pom)
   647  	if relativePath == "" {
   648  		return nil
   649  	}
   650  	p := pomLocation.Path()
   651  	p = path.Dir(p)
   652  	p = path.Join(p, relativePath)
   653  	p = path.Clean(p)
   654  	if !strings.HasSuffix(p, ".xml") {
   655  		p = path.Join(p, "pom.xml")
   656  	}
   657  	parentLocations, err := r.fileResolver.FilesByPath(p)
   658  	if err != nil || len(parentLocations) == 0 {
   659  		log.WithFields("error", err, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "relativePath", relativePath).
   660  			Trace("parent pom not found by relative path")
   661  		return nil
   662  	}
   663  	parentLocation := parentLocations[0]
   664  
   665  	parentContents, err := r.fileResolver.FileContentsByLocation(parentLocation)
   666  	if err != nil || parentContents == nil {
   667  		log.WithFields("error", err, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation).
   668  			Debug("unable to get contents of parent pom by relative path")
   669  		return nil
   670  	}
   671  	defer internal.CloseAndLogError(parentContents, parentLocation.RealPath)
   672  	parentPom, err := ParsePomXML(parentContents)
   673  	if err != nil || parentPom == nil {
   674  		log.WithFields("error", err, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation).
   675  			Debug("unable to parse parent pom")
   676  		return nil
   677  	}
   678  	// ensure parent matches
   679  	newParentID := r.resolveID(ctx, resolvingProperties, parentPom)
   680  	if newParentID.ArtifactID != parentID.ArtifactID {
   681  		log.WithFields("newParentID", newParentID, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation).
   682  			Debug("parent IDs do not match resolving parent by relative path")
   683  		return nil
   684  	}
   685  
   686  	r.resolved[parentID] = parentPom
   687  	r.pomLocations[parentPom] = parentLocation // for any future parent relativePath lookups
   688  
   689  	return parentPom
   690  }
   691  
   692  // AddPom allows for adding known pom files with locations within the file resolver, these locations may be used
   693  // while resolving parent poms by relative path
   694  func (r *Resolver) AddPom(ctx context.Context, pom *Project, location file.Location) {
   695  	r.pomLocations[pom] = location
   696  	// by calling resolve ID here, this will lookup necessary parent poms by relative path, and
   697  	// track any poms we found with complete version information if enough is available to resolve
   698  	id := r.ResolveID(ctx, pom)
   699  	if id.Valid() {
   700  		_, existing := r.resolved[id]
   701  		if !existing {
   702  			r.resolved[id] = pom
   703  		}
   704  	}
   705  }
   706  
   707  // DirectPomDependencies returns all dependencies directly defined in a project, including all defined in profiles.
   708  // This does not resolve any parent or transitive dependencies
   709  func DirectPomDependencies(pom *Project) []Dependency {
   710  	dependencies := deref(pom.Dependencies)
   711  	for _, profile := range deref(pom.Profiles) {
   712  		dependencies = append(dependencies, deref(profile.Dependencies)...)
   713  	}
   714  	return dependencies
   715  }
   716  
   717  // pomManagedDependencies returns all directly defined managed dependencies in a project pom, including all defined in profiles.
   718  // does not resolve parent managed dependencies
   719  func pomManagedDependencies(pom *Project) []Dependency {
   720  	var dependencies []Dependency
   721  	if pom.DependencyManagement != nil {
   722  		dependencies = append(dependencies, deref(pom.DependencyManagement.Dependencies)...)
   723  	}
   724  	for _, profile := range deref(pom.Profiles) {
   725  		if profile.DependencyManagement != nil {
   726  			dependencies = append(dependencies, deref(profile.DependencyManagement.Dependencies)...)
   727  		}
   728  	}
   729  	return dependencies
   730  }
   731  
   732  // deref dereferences ptr if not nil, or returns the type default value if ptr is nil
   733  func deref[T any](ptr *T) T {
   734  	if ptr == nil {
   735  		var t T
   736  		return t
   737  	}
   738  	return *ptr
   739  }