mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 10:36:45 +01:00
fix issue with parsing variables in csproj files
Signed-off-by: Alan Pope <alan.pope@anchore.com>
This commit is contained in:
parent
dd0e7dc20f
commit
0afe26152f
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/packageurl-go"
|
"github.com/anchore/packageurl-go"
|
||||||
@ -17,9 +18,21 @@ import (
|
|||||||
|
|
||||||
// csprojProject represents the root element of a .csproj file
|
// csprojProject represents the root element of a .csproj file
|
||||||
type csprojProject struct {
|
type csprojProject struct {
|
||||||
XMLName xml.Name `xml:"Project"`
|
XMLName xml.Name `xml:"Project"`
|
||||||
Sdk string `xml:"Sdk,attr"`
|
Sdk string `xml:"Sdk,attr"`
|
||||||
ItemGroups []csprojItemGroup `xml:"ItemGroup"`
|
PropertyGroups []csprojPropertyGroup `xml:"PropertyGroup"`
|
||||||
|
ItemGroups []csprojItemGroup `xml:"ItemGroup"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// csprojPropertyGroup represents a PropertyGroup element containing MSBuild properties
|
||||||
|
type csprojPropertyGroup struct {
|
||||||
|
Properties []csprojProperty `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// csprojProperty represents any property within a PropertyGroup
|
||||||
|
type csprojProperty struct {
|
||||||
|
XMLName xml.Name
|
||||||
|
Value string `xml:",chardata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// csprojItemGroup represents an ItemGroup element containing references
|
// csprojItemGroup represents an ItemGroup element containing references
|
||||||
@ -54,6 +67,9 @@ func parseDotnetCsproj(_ context.Context, _ file.Resolver, _ *generic.Environmen
|
|||||||
return nil, nil, fmt.Errorf("unable to parse .csproj XML: %w", err)
|
return nil, nil, fmt.Errorf("unable to parse .csproj XML: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build property map from PropertyGroups
|
||||||
|
properties := buildPropertyMap(project.PropertyGroups)
|
||||||
|
|
||||||
var pkgs []pkg.Package
|
var pkgs []pkg.Package
|
||||||
var relationships []artifact.Relationship
|
var relationships []artifact.Relationship
|
||||||
|
|
||||||
@ -65,6 +81,10 @@ func parseDotnetCsproj(_ context.Context, _ file.Resolver, _ *generic.Environmen
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve any MSBuild property variables in the version
|
||||||
|
resolvedVersion := resolveProperties(pkgRef.Version, properties)
|
||||||
|
pkgRef.Version = resolvedVersion
|
||||||
|
|
||||||
p := buildPackageFromReference(pkgRef, reader.Location)
|
p := buildPackageFromReference(pkgRef, reader.Location)
|
||||||
if p != nil {
|
if p != nil {
|
||||||
pkgs = append(pkgs, *p)
|
pkgs = append(pkgs, *p)
|
||||||
@ -99,15 +119,15 @@ func shouldSkipPackageReference(ref csprojPackageReference) bool {
|
|||||||
// Skip packages that are commonly build-time only
|
// Skip packages that are commonly build-time only
|
||||||
lowerName := strings.ToLower(ref.Include)
|
lowerName := strings.ToLower(ref.Include)
|
||||||
buildTimePackages := map[string]bool{
|
buildTimePackages := map[string]bool{
|
||||||
"microsoft.net.test.sdk": true,
|
"microsoft.net.test.sdk": true,
|
||||||
"stylecop.analyzers": true,
|
"stylecop.analyzers": true,
|
||||||
"microsoft.codeanalysis": true,
|
"microsoft.codeanalysis": true,
|
||||||
"coverlet.collector": true,
|
"coverlet.collector": true,
|
||||||
"xunit.runner.visualstudio": true,
|
"xunit.runner.visualstudio": true,
|
||||||
"nunit": true,
|
"nunit": true,
|
||||||
"nunit3testadapter": true,
|
"nunit3testadapter": true,
|
||||||
"mstest.testadapter": true,
|
"mstest.testadapter": true,
|
||||||
"mstest.testframework": true,
|
"mstest.testframework": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
for buildPkg := range buildTimePackages {
|
for buildPkg := range buildTimePackages {
|
||||||
@ -119,6 +139,48 @@ func shouldSkipPackageReference(ref csprojPackageReference) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildPropertyMap creates a map of MSBuild properties from PropertyGroups
|
||||||
|
func buildPropertyMap(propertyGroups []csprojPropertyGroup) map[string]string {
|
||||||
|
properties := make(map[string]string)
|
||||||
|
|
||||||
|
for _, group := range propertyGroups {
|
||||||
|
for _, prop := range group.Properties {
|
||||||
|
propertyName := prop.XMLName.Local
|
||||||
|
propertyValue := strings.TrimSpace(prop.Value)
|
||||||
|
if propertyName != "" && propertyValue != "" {
|
||||||
|
properties[propertyName] = propertyValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveProperties resolves MSBuild property variables like $(PropertyName) in a string
|
||||||
|
func resolveProperties(input string, properties map[string]string) string {
|
||||||
|
if input == "" {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern matches $(PropertyName)
|
||||||
|
propertyPattern := `\$\(([^)]+)\)`
|
||||||
|
re := regexp.MustCompile(propertyPattern)
|
||||||
|
|
||||||
|
result := re.ReplaceAllStringFunc(input, func(match string) string {
|
||||||
|
// Extract property name from $(PropertyName)
|
||||||
|
propertyName := match[2 : len(match)-1] // Remove $( and )
|
||||||
|
|
||||||
|
if value, exists := properties[propertyName]; exists {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return original if property not found (preserve for debugging)
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// buildPackageFromReference creates a Package from a PackageReference element
|
// buildPackageFromReference creates a Package from a PackageReference element
|
||||||
func buildPackageFromReference(ref csprojPackageReference, location file.Location) *pkg.Package {
|
func buildPackageFromReference(ref csprojPackageReference, location file.Location) *pkg.Package {
|
||||||
name := strings.TrimSpace(ref.Include)
|
name := strings.TrimSpace(ref.Include)
|
||||||
@ -134,6 +196,11 @@ func buildPackageFromReference(ref csprojPackageReference, location file.Locatio
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip packages with unresolved MSBuild properties (contains $(...))
|
||||||
|
if strings.Contains(version, "$(") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Generate PURL following the established pattern for .NET packages
|
// Generate PURL following the established pattern for .NET packages
|
||||||
purl := packageurl.NewPackageURL(
|
purl := packageurl.NewPackageURL(
|
||||||
packageurl.TypeNuget,
|
packageurl.TypeNuget,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package dotnet
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -205,6 +206,89 @@ func TestParseDotnetCsproj(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "property variable resolution",
|
||||||
|
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<SteeltoeVersion>3.2.0</SteeltoeVersion>
|
||||||
|
<TestVersion>1.0.0</TestVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Steeltoe.Common" Version="$(SteeltoeVersion)" />
|
||||||
|
<PackageReference Include="Test.Package" Version="$(TestVersion)" />
|
||||||
|
<PackageReference Include="Regular.Package" Version="2.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>`,
|
||||||
|
expected: []pkg.Package{
|
||||||
|
{
|
||||||
|
Name: "Steeltoe.Common",
|
||||||
|
Version: "3.2.0",
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
PURL: "pkg:nuget/Steeltoe.Common@3.2.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Test.Package",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
PURL: "pkg:nuget/Test.Package@1.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Regular.Package",
|
||||||
|
Version: "2.0.0",
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
PURL: "pkg:nuget/Regular.Package@2.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard versions preserved",
|
||||||
|
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<SteeltoeVersion>3.2.*</SteeltoeVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Steeltoe.Common" Version="$(SteeltoeVersion)" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>`,
|
||||||
|
expected: []pkg.Package{
|
||||||
|
{
|
||||||
|
Name: "Steeltoe.Common",
|
||||||
|
Version: "3.2.*",
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
PURL: "pkg:nuget/Steeltoe.Common@3.2.%2A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Swashbuckle.AspNetCore",
|
||||||
|
Version: "6.2.*",
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
PURL: "pkg:nuget/Swashbuckle.AspNetCore@6.2.%2A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unresolved property preserved",
|
||||||
|
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Some.Package" Version="$(UndefinedProperty)" />
|
||||||
|
<PackageReference Include="Regular.Package" Version="2.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>`,
|
||||||
|
expected: []pkg.Package{
|
||||||
|
{
|
||||||
|
Name: "Regular.Package",
|
||||||
|
Version: "2.0.0",
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
PURL: "pkg:nuget/Regular.Package@2.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "malformed XML",
|
name: "malformed XML",
|
||||||
input: `<Project><ItemGroup><PackageReference Include="Test"`,
|
input: `<Project><ItemGroup><PackageReference Include="Test"`,
|
||||||
@ -321,8 +405,8 @@ func TestShouldSkipPackageReference(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "includeAssets runtime only",
|
name: "includeAssets runtime only",
|
||||||
ref: csprojPackageReference{
|
ref: csprojPackageReference{
|
||||||
Include: "Some.Package",
|
Include: "Some.Package",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
IncludeAssets: "runtime",
|
IncludeAssets: "runtime",
|
||||||
},
|
},
|
||||||
expected: false,
|
expected: false,
|
||||||
@ -330,8 +414,8 @@ func TestShouldSkipPackageReference(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "mixed condition with release",
|
name: "mixed condition with release",
|
||||||
ref: csprojPackageReference{
|
ref: csprojPackageReference{
|
||||||
Include: "Some.Package",
|
Include: "Some.Package",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
Condition: "'$(Configuration)' == 'Debug' OR '$(Configuration)' == 'Release'",
|
Condition: "'$(Configuration)' == 'Debug' OR '$(Configuration)' == 'Release'",
|
||||||
},
|
},
|
||||||
expected: false,
|
expected: false,
|
||||||
@ -443,3 +527,132 @@ func TestBuildPackageFromReference(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildPropertyMap(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
propertyGroups []csprojPropertyGroup
|
||||||
|
expected map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty property groups",
|
||||||
|
propertyGroups: []csprojPropertyGroup{},
|
||||||
|
expected: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single property group",
|
||||||
|
propertyGroups: []csprojPropertyGroup{
|
||||||
|
{
|
||||||
|
Properties: []csprojProperty{
|
||||||
|
{XMLName: xml.Name{Local: "SteeltoeVersion"}, Value: "3.2.0"},
|
||||||
|
{XMLName: xml.Name{Local: "TestVersion"}, Value: "1.0.0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"SteeltoeVersion": "3.2.0",
|
||||||
|
"TestVersion": "1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple property groups",
|
||||||
|
propertyGroups: []csprojPropertyGroup{
|
||||||
|
{
|
||||||
|
Properties: []csprojProperty{
|
||||||
|
{XMLName: xml.Name{Local: "SteeltoeVersion"}, Value: "3.2.0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Properties: []csprojProperty{
|
||||||
|
{XMLName: xml.Name{Local: "TestVersion"}, Value: "1.0.0"},
|
||||||
|
{XMLName: xml.Name{Local: "TargetFramework"}, Value: "net6.0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"SteeltoeVersion": "3.2.0",
|
||||||
|
"TestVersion": "1.0.0",
|
||||||
|
"TargetFramework": "net6.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "properties with whitespace",
|
||||||
|
propertyGroups: []csprojPropertyGroup{
|
||||||
|
{
|
||||||
|
Properties: []csprojProperty{
|
||||||
|
{XMLName: xml.Name{Local: "Version"}, Value: " 3.2.0 "},
|
||||||
|
{XMLName: xml.Name{Local: "EmptyValue"}, Value: " "},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"Version": "3.2.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result := buildPropertyMap(test.propertyGroups)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveProperties(t *testing.T) {
|
||||||
|
properties := map[string]string{
|
||||||
|
"SteeltoeVersion": "3.2.*",
|
||||||
|
"TestVersion": "1.0.0",
|
||||||
|
"MajorVersion": "2",
|
||||||
|
"MinorVersion": "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no properties to resolve",
|
||||||
|
input: "1.2.3",
|
||||||
|
expected: "1.2.3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single property resolution",
|
||||||
|
input: "$(SteeltoeVersion)",
|
||||||
|
expected: "3.2.*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple property resolution",
|
||||||
|
input: "$(MajorVersion).$(MinorVersion).0",
|
||||||
|
expected: "2.1.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed resolved and unresolved",
|
||||||
|
input: "$(SteeltoeVersion)-$(UnknownProperty)",
|
||||||
|
expected: "3.2.*-$(UnknownProperty)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty input",
|
||||||
|
input: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unresolved property preserved",
|
||||||
|
input: "$(UnknownProperty)",
|
||||||
|
expected: "$(UnknownProperty)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex pattern",
|
||||||
|
input: "prefix-$(TestVersion)-suffix",
|
||||||
|
expected: "prefix-1.0.0-suffix",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result := resolveProperties(test.input, properties)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<SteeltoeVersion>3.2.*</SteeltoeVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Steeltoe.Common.Hosting" Version="$(SteeltoeVersion)" />
|
||||||
|
<PackageReference Include="Steeltoe.Management.EndpointCore" Version="$(SteeltoeVersion)" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Loading…
x
Reference in New Issue
Block a user