mirror of
https://github.com/aquasecurity/trivy.git
synced 2026-01-31 13:53:14 +08:00
359 lines
9.7 KiB
Go
359 lines
9.7 KiB
Go
package pom
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/samber/lo"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/aquasecurity/trivy/pkg/dependency/parser/utils"
|
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
|
"github.com/aquasecurity/trivy/pkg/set"
|
|
"github.com/aquasecurity/trivy/pkg/x/slices"
|
|
)
|
|
|
|
type pom struct {
|
|
filePath string
|
|
content *pomXML
|
|
}
|
|
|
|
func (p *pom) nil() bool {
|
|
return p == nil || p.content == nil
|
|
}
|
|
|
|
func (p *pom) inherit(parent *pom) {
|
|
if parent == nil {
|
|
return
|
|
}
|
|
// Merge properties
|
|
p.content.Properties = utils.MergeMaps(parent.properties(), p.content.Properties)
|
|
|
|
art := p.artifact().Inherit(parent.artifact())
|
|
|
|
p.content.GroupId = art.GroupID
|
|
p.content.ArtifactId = art.ArtifactID
|
|
p.content.Licenses = art.ToPOMLicenses()
|
|
|
|
if isProperty(art.Version.String()) {
|
|
p.content.Version = evaluateVariable(art.Version.String(), p.content.Properties, nil)
|
|
} else {
|
|
p.content.Version = art.Version.String()
|
|
}
|
|
}
|
|
|
|
func (p *pom) properties() properties {
|
|
props := p.content.Properties
|
|
return utils.MergeMaps(props, p.projectProperties())
|
|
}
|
|
|
|
func (p *pom) projectProperties() map[string]string {
|
|
val := reflect.ValueOf(p.content).Elem()
|
|
props := p.listProperties(val)
|
|
|
|
// "version" and "groupId" elements could be inherited from parent.
|
|
// https://maven.apache.org/pom.html#inheritance
|
|
props["groupId"] = p.content.GroupId
|
|
props["version"] = p.content.Version
|
|
|
|
// https://maven.apache.org/pom.html#properties
|
|
projectProperties := make(map[string]string)
|
|
for k, v := range props {
|
|
if strings.HasPrefix(k, "project.") {
|
|
continue
|
|
}
|
|
|
|
// e.g. ${project.groupId}
|
|
key := fmt.Sprintf("project.%s", k)
|
|
projectProperties[key] = v
|
|
|
|
// It is deprecated, but still available.
|
|
// e.g. ${groupId}
|
|
projectProperties[k] = v
|
|
}
|
|
|
|
return projectProperties
|
|
}
|
|
|
|
func (p *pom) listProperties(val reflect.Value) map[string]string {
|
|
props := make(map[string]string)
|
|
for i := 0; i < val.NumField(); i++ {
|
|
f := val.Type().Field(i)
|
|
|
|
tag, ok := f.Tag.Lookup("xml")
|
|
if !ok || strings.Contains(tag, ",") {
|
|
// e.g. ",chardata"
|
|
continue
|
|
}
|
|
|
|
switch f.Type.Kind() {
|
|
case reflect.Slice:
|
|
continue
|
|
case reflect.Map:
|
|
m := val.Field(i)
|
|
for _, e := range m.MapKeys() {
|
|
v := m.MapIndex(e)
|
|
props[e.String()] = v.String()
|
|
}
|
|
case reflect.Struct:
|
|
nestedProps := p.listProperties(val.Field(i))
|
|
for k, v := range nestedProps {
|
|
key := fmt.Sprintf("%s.%s", tag, k)
|
|
props[key] = v
|
|
}
|
|
default:
|
|
props[tag] = val.Field(i).String()
|
|
}
|
|
}
|
|
return props
|
|
}
|
|
|
|
func (p *pom) artifact() artifact {
|
|
return newArtifact(p.content.GroupId, p.content.ArtifactId, p.content.Version, p.licenses(), p.content.Properties)
|
|
}
|
|
|
|
func (p *pom) licenses() []string {
|
|
return slices.ZeroToNil(lo.FilterMap(p.content.Licenses.License, func(lic pomLicense, _ int) (string, bool) {
|
|
return lic.Name, lic.Name != ""
|
|
}))
|
|
}
|
|
|
|
func (p *pom) repositories(servers []Server) []repository {
|
|
return resolvePomRepos(servers, p.content.Repositories)
|
|
}
|
|
|
|
type pomXML struct {
|
|
Parent pomParent `xml:"parent"`
|
|
GroupId string `xml:"groupId"`
|
|
ArtifactId string `xml:"artifactId"`
|
|
Version string `xml:"version"`
|
|
Licenses pomLicenses `xml:"licenses"`
|
|
Modules struct {
|
|
Text string `xml:",chardata"`
|
|
Module []string `xml:"module"`
|
|
} `xml:"modules"`
|
|
Properties properties `xml:"properties"`
|
|
DependencyManagement struct {
|
|
Text string `xml:",chardata"`
|
|
Dependencies pomDependencies `xml:"dependencies"`
|
|
} `xml:"dependencyManagement"`
|
|
Dependencies pomDependencies `xml:"dependencies"`
|
|
Repositories []pomRepository `xml:"repositories>repository"`
|
|
}
|
|
|
|
type pomParent struct {
|
|
GroupId string `xml:"groupId"`
|
|
ArtifactId string `xml:"artifactId"`
|
|
Version string `xml:"version"`
|
|
RelativePath string `xml:"relativePath"`
|
|
}
|
|
|
|
type pomLicenses struct {
|
|
Text string `xml:",chardata"`
|
|
License []pomLicense `xml:"license"`
|
|
}
|
|
|
|
type pomLicense struct {
|
|
Name string `xml:"name"`
|
|
}
|
|
|
|
type pomDependencies struct {
|
|
Text string `xml:",chardata"`
|
|
Dependency []pomDependency `xml:"dependency"`
|
|
}
|
|
|
|
type pomDependency struct {
|
|
Text string `xml:",chardata"`
|
|
GroupID string `xml:"groupId"`
|
|
ArtifactID string `xml:"artifactId"`
|
|
Version string `xml:"version"`
|
|
Scope string `xml:"scope"`
|
|
Optional bool `xml:"optional"`
|
|
Exclusions pomExclusions `xml:"exclusions"`
|
|
StartLine int
|
|
EndLine int
|
|
}
|
|
|
|
type pomExclusions struct {
|
|
Text string `xml:",chardata"`
|
|
Exclusion []pomExclusion `xml:"exclusion"`
|
|
}
|
|
|
|
// ref. https://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html
|
|
type pomExclusion struct {
|
|
GroupID string `xml:"groupId"`
|
|
ArtifactID string `xml:"artifactId"`
|
|
}
|
|
|
|
func (d pomDependency) Name() string {
|
|
return fmt.Sprintf("%s:%s", d.GroupID, d.ArtifactID)
|
|
}
|
|
|
|
// Resolve evaluates variables in the dependency and inherit some fields from dependencyManagement to the dependency.
|
|
func (d pomDependency) Resolve(props map[string]string, depManagement, rootDepManagement []pomDependency) pomDependency {
|
|
// Evaluate variables
|
|
dep := pomDependency{
|
|
Text: d.Text,
|
|
GroupID: evaluateVariable(d.GroupID, props, nil),
|
|
ArtifactID: evaluateVariable(d.ArtifactID, props, nil),
|
|
Version: evaluateVariable(d.Version, props, nil),
|
|
Scope: evaluateVariable(d.Scope, props, nil),
|
|
Optional: d.Optional,
|
|
Exclusions: d.Exclusions,
|
|
StartLine: d.StartLine,
|
|
EndLine: d.EndLine,
|
|
}
|
|
|
|
// Field resolution order:
|
|
// 1. Fill empty fields with values from dependencyManagement.
|
|
// 2. Overwrite non-empty fields with values from the root dependencyManagement.
|
|
// 2.1. Exception: if a dependency has the `test` scope (either defined explicitly
|
|
// or inherited from dependencyManagement), we must NOT overwrite it
|
|
// with the value from the root dependencyManagement.
|
|
//
|
|
// See the test "don't overwrite test scope from upper depManagement" for details.
|
|
|
|
// Inherit version, scope and optional from dependencyManagement if empty
|
|
if managed, found := findDep(dep.Name(), depManagement); found { // dependencyManagement from parent
|
|
if dep.Version == "" {
|
|
dep.Version = evaluateVariable(managed.Version, props, nil)
|
|
}
|
|
if dep.Scope == "" {
|
|
dep.Scope = evaluateVariable(managed.Scope, props, nil)
|
|
}
|
|
// TODO: need to check the behavior
|
|
if !dep.Optional {
|
|
dep.Optional = managed.Optional
|
|
}
|
|
// `mvn` always merges exceptions for pom and parent
|
|
dep.Exclusions.Exclusion = append(dep.Exclusions.Exclusion, managed.Exclusions.Exclusion...)
|
|
}
|
|
|
|
// If this dependency is managed in the root POM,
|
|
// we need to overwrite fields according to the managed dependency.
|
|
if managed, found := findDep(dep.Name(), rootDepManagement); found { // dependencyManagement from the root POM
|
|
if managed.Version != "" {
|
|
dep.Version = evaluateVariable(managed.Version, props, nil)
|
|
}
|
|
|
|
if managed.Scope != "" && dep.Scope != "test" {
|
|
dep.Scope = evaluateVariable(managed.Scope, props, nil)
|
|
}
|
|
|
|
if managed.Optional {
|
|
dep.Optional = managed.Optional
|
|
}
|
|
if len(managed.Exclusions.Exclusion) != 0 {
|
|
dep.Exclusions = managed.Exclusions
|
|
}
|
|
return dep
|
|
}
|
|
return dep
|
|
}
|
|
|
|
// ToArtifact converts dependency to artifact.
|
|
// It should be called after calling Resolve() so that variables can be evaluated.
|
|
func (d pomDependency) ToArtifact(opts analysisOptions) artifact {
|
|
// To avoid shadow adding exclusions to top pom's,
|
|
// we need to initialize a new map for each new artifact
|
|
// See `exclusions in child` test for more information
|
|
exclusions := set.New[string]()
|
|
if opts.exclusions != nil {
|
|
exclusions = opts.exclusions.Clone()
|
|
}
|
|
for _, e := range d.Exclusions.Exclusion {
|
|
exclusions.Append(fmt.Sprintf("%s:%s", e.GroupID, e.ArtifactID))
|
|
}
|
|
|
|
var locations ftypes.Locations
|
|
if d.StartLine != 0 && d.EndLine != 0 {
|
|
locations = ftypes.Locations{
|
|
{
|
|
StartLine: d.StartLine,
|
|
EndLine: d.EndLine,
|
|
},
|
|
}
|
|
}
|
|
|
|
return artifact{
|
|
GroupID: d.GroupID,
|
|
ArtifactID: d.ArtifactID,
|
|
Version: newVersion(d.Version),
|
|
Exclusions: exclusions,
|
|
Locations: locations,
|
|
Relationship: ftypes.RelationshipIndirect, // default
|
|
RootFilePath: opts.rootFilePath,
|
|
}
|
|
}
|
|
|
|
type properties map[string]string
|
|
|
|
type property struct {
|
|
XMLName xml.Name
|
|
Value string `xml:",chardata"`
|
|
}
|
|
|
|
func (props *properties) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error {
|
|
*props = properties{}
|
|
for {
|
|
var p property
|
|
err := d.Decode(&p)
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return xerrors.Errorf("XML decode error: %w", err)
|
|
}
|
|
|
|
(*props)[p.XMLName.Local] = p.Value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (deps *pomDependencies) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error {
|
|
for {
|
|
token, err := d.Token()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return xerrors.Errorf("XML decode error: %w", err)
|
|
}
|
|
|
|
t, ok := token.(xml.StartElement)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if t.Name.Local == "dependency" {
|
|
var dep pomDependency
|
|
dep.StartLine, _ = d.InputPos() // <dependency> tag starts
|
|
|
|
// Decode the <dependency> element
|
|
err = d.DecodeElement(&dep, &t)
|
|
if err != nil {
|
|
return xerrors.Errorf("Error decoding dependency: %w", err)
|
|
}
|
|
|
|
dep.EndLine, _ = d.InputPos() // <dependency> tag ends
|
|
deps.Dependency = append(deps.Dependency, dep)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findDep(name string, depManagement []pomDependency) (pomDependency, bool) {
|
|
return lo.Find(depManagement, func(item pomDependency) bool {
|
|
return item.Name() == name
|
|
})
|
|
}
|
|
|
|
type pomRepository struct {
|
|
ID string `xml:"id"`
|
|
Name string `xml:"name"`
|
|
URL string `xml:"url"`
|
|
ReleasesEnabled string `xml:"releases>enabled"`
|
|
SnapshotsEnabled string `xml:"snapshots>enabled"`
|
|
}
|