diff --git a/remediation/sca/packageupdaters/nugetpackageupdater.go b/remediation/sca/packageupdaters/nugetpackageupdater.go
index a1a170b5d..594fbc874 100644
--- a/remediation/sca/packageupdaters/nugetpackageupdater.go
+++ b/remediation/sca/packageupdaters/nugetpackageupdater.go
@@ -8,6 +8,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
+ "sort"
"strings"
"github.com/jfrog/jfrog-client-go/utils/log"
@@ -27,8 +28,20 @@ const (
// literal fixed version rather than resolved and updated at its property definition. That's a
// deliberate simplification, and a real behavior change versus how MSBuild itself would resolve
// it; Maven's updater handles the analogous ${property} case by updating the definition instead.
- nugetVersionAttrPattern = `(?is)(\bVersion\s*=\s*["'])[^"']*(["'])`
- nugetVersionElementPattern = `(?is)()[^<]*()`
+ nugetVersionAttrPattern = `(?is)(\bVersion\s*=\s*["'])[^"']*(["'])`
+ nugetVersionElementPattern = `(?is)()[^<]*()`
+ nugetVersionOverrideAttrPattern = `(?is)(\bVersionOverride\s*=\s*["'])[^"']*(["'])`
+ nugetVersionOverrideElementPattern = `(?is)()[^<]*()`
+
+ nugetPackageVersionElementPattern = `(?is)]*/>|]*[^/]>.*?`
+ nugetPackageVersionKeyAttrPattern = `(?i)\b(?:Include|Update)\s*=\s*["']%s["']`
+ nugetDirectoryPackagesPropsName = "Directory.Packages.props"
+ nugetDirectoryBuildPropsName = "Directory.Build.props"
+ nugetDirectoryBuildTargetsName = "Directory.Build.targets"
+ nugetImportProjectAttrPattern = `(?i)]*\bProject\s*=\s*["']([^"']+)["']`
+ nugetManageCpmFalsePattern = `(?is)\s*false\s*`
+ nugetManageCpmTruePattern = `(?is)\s*true\s*`
+ nugetMSBuildThisFileDirectoryMacro = "$(MSBuildThisFileDirectory)"
nugetLockFileName = "packages.lock.json"
nugetObjDirName = "obj"
@@ -96,15 +109,57 @@ func (n *NugetPackageUpdater) UpdateDependency(fixDetails *FixDetails) error {
}
func collectProjectFilePaths(fixDetails *FixDetails) []string {
+ seen := make(map[string]struct{})
var paths []string
+ add := func(path string) {
+ if path == "" {
+ return
+ }
+ if _, ok := seen[path]; ok {
+ return
+ }
+ seen[path] = struct{}{}
+ paths = append(paths, path)
+ }
+
for _, path := range GetVulnerabilityLocations(fixDetails, []string{}, []string{}) {
- if hasNugetProjectFileSuffix(path) {
- paths = append(paths, path)
+ switch {
+ case hasNugetProjectFileSuffix(path):
+ add(path)
+ case strings.EqualFold(filepath.Base(path), nugetLockFileName):
+ projectPath, err := findProjectFileInDir(filepath.Dir(path))
+ if err != nil {
+ log.Debug(fmt.Sprintf("Failed to resolve project file next to '%s': %s", path, err.Error()))
+ continue
+ }
+ add(projectPath)
}
}
return paths
}
+func findProjectFileInDir(dir string) (string, error) {
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return "", fmt.Errorf("failed to read directory %s: %w", dir, err)
+ }
+ var matches []string
+ for _, entry := range entries {
+ if !entry.IsDir() && hasNugetProjectFileSuffix(entry.Name()) {
+ matches = append(matches, filepath.Join(dir, entry.Name()))
+ }
+ }
+ if len(matches) == 0 {
+ return "", nil
+ }
+ if len(matches) > 1 {
+ sort.Strings(matches)
+ log.Debug(fmt.Sprintf("Skipping lock file in '%s': found multiple project files (%s)", dir, strings.Join(matches, ", ")))
+ return "", nil
+ }
+ return matches[0], nil
+}
+
func hasNugetProjectFileSuffix(path string) bool {
lowerPath := strings.ToLower(path)
for _, suffix := range nugetProjectFileSuffixes {
@@ -122,9 +177,12 @@ func (n *NugetPackageUpdater) fixVulnerabilityAndRestore(projectFilePath, packag
return fmt.Errorf("failed to read %s: %w", projectFilePath, err)
}
- updatedProjectFile, err := updatePackageReferenceVersion(originalProjectFile, packageName, fixedVersion)
- if err != nil {
- return fmt.Errorf("%w in %s", err, projectFilePath)
+ updatedProjectFile, fixErr := updatePackageReferenceVersion(originalProjectFile, packageName, fixedVersion)
+ if isMixedInlineAndNonInline(fixErr) {
+ return fmt.Errorf("%w in %s", fixErr, projectFilePath)
+ }
+ if fixErr != nil {
+ return n.fixViaVersionSource(projectFilePath, packageName, fixedVersion, originalWd)
}
//#nosec G703 G306 -- projectFilePath from scan workflow; 0644 for VCS-tracked sources.
@@ -132,15 +190,116 @@ func (n *NugetPackageUpdater) fixVulnerabilityAndRestore(projectFilePath, packag
return fmt.Errorf("failed to write %s: %w", projectFilePath, err)
}
+ return n.restoreLockFileAfterWrite(projectFilePath, projectFilePath, originalProjectFile, originalWd, packageName, fixedVersion)
+}
+
+func (n *NugetPackageUpdater) fixViaVersionSource(projectFilePath, packageName, fixedVersion, originalWd string) error {
+ err := n.fixViaDirectoryPackagesProps(projectFilePath, packageName, fixedVersion, originalWd)
+ if !isUnsupportedNoInlineVersion(err) {
+ return err
+ }
+ return n.fixViaDirectoryBuildProps(projectFilePath, packageName, fixedVersion, originalWd)
+}
+
+func isUnsupportedNoInlineVersion(err error) bool {
+ var unsupportedErr *ErrUnsupportedFix
+ return errors.As(err, &unsupportedErr) && unsupportedErr.ErrorType == NoInlineVersionFixNotSupported
+}
+
+func isMixedInlineAndNonInline(err error) bool {
+ var unsupportedErr *ErrUnsupportedFix
+ return errors.As(err, &unsupportedErr) && unsupportedErr.ErrorType == MixedInlineAndNonInlineVersionFixNotSupported
+}
+
+func (n *NugetPackageUpdater) fixViaDirectoryPackagesProps(projectFilePath, packageName, fixedVersion, originalWd string) error {
+ unsupported := &ErrUnsupportedFix{
+ PackageName: packageName,
+ FixedVersion: fixedVersion,
+ ErrorType: NoInlineVersionFixNotSupported,
+ }
+
+ propsPath, originalProps, err := resolveDirectoryFile(filepath.Dir(projectFilePath), originalWd, nugetDirectoryPackagesPropsName)
+ if err != nil {
+ return fmt.Errorf("%w in %s", err, projectFilePath)
+ }
+ if propsPath == "" {
+ return unsupported
+ }
+
+ absRepoRoot, err := filepath.Abs(originalWd)
+ if err != nil {
+ return fmt.Errorf("failed to resolve absolute path for %s: %w", originalWd, err)
+ }
+
+ if isCentralPackageManagementDisabledInContext(projectFilePath, originalProps, absRepoRoot) {
+ return unsupported
+ }
+
+ targetPath, originalContent, updatedContent, fixedAny, err := resolvePackageVersionUpdate(propsPath, originalProps, packageName, fixedVersion, absRepoRoot)
+ if err != nil {
+ return fmt.Errorf("%w in %s", err, projectFilePath)
+ }
+ if !fixedAny {
+ return unsupported
+ }
+
+ //#nosec G703 G306 -- targetPath resolved from descriptor discovery in the scanned repository.
+ if err = os.WriteFile(targetPath, updatedContent, 0644); err != nil {
+ return fmt.Errorf("failed to write %s: %w", targetPath, err)
+ }
+
+ return n.restoreLockFileAfterWrite(projectFilePath, targetPath, originalContent, originalWd, packageName, fixedVersion)
+}
+
+func (n *NugetPackageUpdater) fixViaDirectoryBuildProps(projectFilePath, packageName, fixedVersion, originalWd string) error {
+ unsupported := &ErrUnsupportedFix{
+ PackageName: packageName,
+ FixedVersion: fixedVersion,
+ ErrorType: NoInlineVersionFixNotSupported,
+ }
+
+ absRepoRoot, err := filepath.Abs(originalWd)
+ if err != nil {
+ return fmt.Errorf("failed to resolve absolute path for %s: %w", originalWd, err)
+ }
+
+ for _, fileName := range []string{nugetDirectoryBuildPropsName, nugetDirectoryBuildTargetsName} {
+ buildFilePath, buildFileContent, resolveErr := resolveDirectoryFile(filepath.Dir(projectFilePath), originalWd, fileName)
+ if resolveErr != nil {
+ return fmt.Errorf("%w in %s", resolveErr, projectFilePath)
+ }
+ if buildFilePath == "" {
+ continue
+ }
+
+ for _, tryFix := range []versionFixFunc{tryFixPackageReference, tryFixPackageVersionEntry} {
+ targetPath, originalContent, updatedContent, fixedAny, searchErr := searchVersionUpdate(buildFilePath, buildFileContent, packageName, fixedVersion, absRepoRoot, tryFix, map[string]struct{}{})
+ if searchErr != nil {
+ return fmt.Errorf("%w in %s", searchErr, projectFilePath)
+ }
+ if !fixedAny {
+ continue
+ }
+
+ //#nosec G703 G306 -- targetPath resolved from descriptor discovery in the scanned repository.
+ if err = os.WriteFile(targetPath, updatedContent, 0644); err != nil {
+ return fmt.Errorf("failed to write %s: %w", targetPath, err)
+ }
+ return n.restoreLockFileAfterWrite(projectFilePath, targetPath, originalContent, originalWd, packageName, fixedVersion)
+ }
+ }
+ return unsupported
+}
+
+func (n *NugetPackageUpdater) restoreLockFileAfterWrite(projectFilePath, writtenPath string, originalWritten []byte, originalWd, packageName, fixedVersion string) error {
lockFilePath := filepath.Join(filepath.Dir(projectFilePath), nugetLockFileName)
//#nosec G304 -- lockFilePath is derived from projectFilePath, itself from descriptor discovery.
originalLockFile, err := os.ReadFile(lockFilePath)
if err != nil {
if os.IsNotExist(err) {
- // No lock file for this project - nothing further to regenerate.
return nil
}
- return rollbackProjectFile(projectFilePath, originalProjectFile, fmt.Errorf("failed to read %s: %w", lockFilePath, err))
+ return rollbackProjectFile(writtenPath, originalWritten, fmt.Errorf("failed to read %s: %w", lockFilePath, err))
}
absLockFilePath := lockFilePath
@@ -159,7 +318,7 @@ func (n *NugetPackageUpdater) fixVulnerabilityAndRestore(projectFilePath, packag
if err = n.runDotnetRestore(projectFilePath); err != nil {
log.Warn(fmt.Sprintf("Failed to regenerate lock file after updating '%s' to version '%s': %s. Rolling back...", packageName, fixedVersion, err.Error()))
- return rollbackProjectFileAndLock(projectFilePath, originalProjectFile, lockFilePath, originalLockFile, err)
+ return rollbackProjectFileAndLock(writtenPath, originalWritten, lockFilePath, originalLockFile, err)
}
return nil
}
@@ -227,6 +386,8 @@ func updatePackageReferenceVersion(content []byte, packageName, fixedVersion str
keyAttr := regexp.MustCompile(fmt.Sprintf(nugetKeyAttrPattern, regexp.QuoteMeta(packageName)))
versionAttr := regexp.MustCompile(nugetVersionAttrPattern)
versionElement := regexp.MustCompile(nugetVersionElementPattern)
+ versionOverrideAttr := regexp.MustCompile(nugetVersionOverrideAttrPattern)
+ versionOverrideElement := regexp.MustCompile(nugetVersionOverrideElementPattern)
var fixedAny, foundWithoutVersion bool
updatedContent := element.ReplaceAllFunc(content, func(match []byte) []byte {
@@ -240,12 +401,25 @@ func updatePackageReferenceVersion(content []byte, packageName, fixedVersion str
case versionElement.Match(match):
fixedAny = true
return versionElement.ReplaceAll(match, []byte("${1}"+fixedVersion+"${2}"))
+ case versionOverrideAttr.Match(match):
+ fixedAny = true
+ return versionOverrideAttr.ReplaceAll(match, []byte("${1}"+fixedVersion+"${2}"))
+ case versionOverrideElement.Match(match):
+ fixedAny = true
+ return versionOverrideElement.ReplaceAll(match, []byte("${1}"+fixedVersion+"${2}"))
default:
foundWithoutVersion = true
return match
}
})
+ if foundWithoutVersion && fixedAny {
+ return nil, &ErrUnsupportedFix{
+ PackageName: packageName,
+ FixedVersion: fixedVersion,
+ ErrorType: MixedInlineAndNonInlineVersionFixNotSupported,
+ }
+ }
if foundWithoutVersion {
return nil, &ErrUnsupportedFix{
PackageName: packageName,
@@ -258,3 +432,181 @@ func updatePackageReferenceVersion(content []byte, packageName, fixedVersion str
}
return nil, fmt.Errorf("dependency %s not found", packageName)
}
+
+func updatePackageVersionEntry(content []byte, packageName, fixedVersion string) ([]byte, bool) {
+ element := regexp.MustCompile(nugetPackageVersionElementPattern)
+ keyAttr := regexp.MustCompile(fmt.Sprintf(nugetPackageVersionKeyAttrPattern, regexp.QuoteMeta(packageName)))
+ versionAttr := regexp.MustCompile(nugetVersionAttrPattern)
+ versionElement := regexp.MustCompile(nugetVersionElementPattern)
+
+ var fixedAny bool
+ updatedContent := element.ReplaceAllFunc(content, func(match []byte) []byte {
+ if !keyAttr.Match(match) {
+ return match
+ }
+ switch {
+ case versionAttr.Match(match):
+ fixedAny = true
+ return versionAttr.ReplaceAll(match, []byte("${1}"+fixedVersion+"${2}"))
+ case versionElement.Match(match):
+ fixedAny = true
+ return versionElement.ReplaceAll(match, []byte("${1}"+fixedVersion+"${2}"))
+ default:
+ return match
+ }
+ })
+ return updatedContent, fixedAny
+}
+
+type versionFixFunc func(content []byte, packageName, fixedVersion string) ([]byte, bool, error)
+
+func resolvePackageVersionUpdate(propsPath string, propsContent []byte, packageName, fixedVersion, repoRoot string) (string, []byte, []byte, bool, error) {
+ if isCentralPackageManagementDisabled(propsContent) {
+ return "", nil, nil, false, nil
+ }
+ return searchVersionUpdate(propsPath, propsContent, packageName, fixedVersion, repoRoot, tryFixPackageVersionEntry, map[string]struct{}{})
+}
+
+func isCentralPackageManagementDisabled(content []byte) bool {
+ return regexp.MustCompile(nugetManageCpmFalsePattern).Match(content)
+}
+
+func isCentralPackageManagementEnabled(content []byte) bool {
+ return regexp.MustCompile(nugetManageCpmTruePattern).Match(content)
+}
+
+func isCentralPackageManagementDisabledInContext(projectFilePath string, packagesProps []byte, repoRoot string) bool {
+ if isCentralPackageManagementDisabled(packagesProps) {
+ return true
+ }
+ if isCentralPackageManagementEnabled(packagesProps) {
+ return false
+ }
+ //#nosec G304 -- projectFilePath from descriptor discovery in the scanned repository.
+ if projectContent, err := os.ReadFile(projectFilePath); err == nil {
+ if isCentralPackageManagementDisabled(projectContent) {
+ return true
+ }
+ if isCentralPackageManagementEnabled(projectContent) {
+ return false
+ }
+ }
+ buildFilePath, buildFileContent, err := resolveDirectoryFile(filepath.Dir(projectFilePath), repoRoot, nugetDirectoryBuildPropsName)
+ if err != nil || buildFilePath == "" {
+ return false
+ }
+ return isCentralPackageManagementDisabled(buildFileContent)
+}
+
+func tryFixPackageReference(content []byte, packageName, fixedVersion string) ([]byte, bool, error) {
+ updated, err := updatePackageReferenceVersion(content, packageName, fixedVersion)
+ if err == nil {
+ return updated, true, nil
+ }
+ if isMixedInlineAndNonInline(err) {
+ return nil, false, err
+ }
+ return nil, false, nil
+}
+
+func tryFixPackageVersionEntry(content []byte, packageName, fixedVersion string) ([]byte, bool, error) {
+ updated, fixedAny := updatePackageVersionEntry(content, packageName, fixedVersion)
+ return updated, fixedAny, nil
+}
+
+func searchVersionUpdate(path string, content []byte, packageName, fixedVersion, repoRoot string, tryFix versionFixFunc, visited map[string]struct{}) (string, []byte, []byte, bool, error) {
+ absPath, err := filepath.Abs(path)
+ if err != nil {
+ return "", nil, nil, false, fmt.Errorf("failed to resolve absolute path for %s: %w", path, err)
+ }
+ if !isPathInsideRoot(repoRoot, absPath) {
+ return "", nil, nil, false, nil
+ }
+ if _, seen := visited[absPath]; seen {
+ return "", nil, nil, false, nil
+ }
+ visited[absPath] = struct{}{}
+
+ updated, fixedAny, tryErr := tryFix(content, packageName, fixedVersion)
+ if tryErr != nil {
+ return "", nil, nil, false, tryErr
+ }
+ if fixedAny {
+ return absPath, content, updated, true, nil
+ }
+
+ importAttr := regexp.MustCompile(nugetImportProjectAttrPattern)
+ for _, match := range importAttr.FindAllSubmatch(content, -1) {
+ projectRef := string(match[1])
+ if strings.HasPrefix(projectRef, nugetMSBuildThisFileDirectoryMacro) {
+ suffix := filepath.FromSlash(strings.TrimPrefix(projectRef, nugetMSBuildThisFileDirectoryMacro))
+ projectRef = filepath.Join(filepath.Dir(absPath), suffix)
+ }
+ if strings.Contains(projectRef, "$") {
+ continue
+ }
+ nextPath := projectRef
+ if !filepath.IsAbs(projectRef) {
+ nextPath = filepath.Join(filepath.Dir(absPath), filepath.FromSlash(projectRef))
+ }
+ nextAbs, absErr := filepath.Abs(nextPath)
+ if absErr != nil || !isPathInsideRoot(repoRoot, nextAbs) {
+ continue
+ }
+ //#nosec G304 -- nextAbs is an Import Project path constrained to the scanned repository.
+ nextContent, readErr := os.ReadFile(nextAbs)
+ if readErr != nil {
+ if os.IsNotExist(readErr) {
+ continue
+ }
+ return "", nil, nil, false, fmt.Errorf("failed to read %s: %w", nextAbs, readErr)
+ }
+ foundPath, original, updatedImported, ok, searchErr := searchVersionUpdate(nextAbs, nextContent, packageName, fixedVersion, repoRoot, tryFix, visited)
+ if searchErr != nil || ok {
+ return foundPath, original, updatedImported, ok, searchErr
+ }
+ }
+ return "", nil, nil, false, nil
+}
+
+func resolveDirectoryFile(startDir, repoRoot, fileName string) (path string, content []byte, err error) {
+ absRepoRoot, err := filepath.Abs(repoRoot)
+ if err != nil {
+ return "", nil, fmt.Errorf("failed to resolve absolute path for %s: %w", repoRoot, err)
+ }
+ dir, err := filepath.Abs(startDir)
+ if err != nil {
+ return "", nil, fmt.Errorf("failed to resolve absolute path for %s: %w", startDir, err)
+ }
+
+ for {
+ if !isPathInsideRoot(absRepoRoot, dir) {
+ return "", nil, nil
+ }
+ candidate := filepath.Join(dir, fileName)
+ //#nosec G304 -- candidate is built from startDir/repoRoot, from descriptor discovery.
+ candidateContent, readErr := os.ReadFile(candidate)
+ if readErr == nil {
+ return candidate, candidateContent, nil
+ }
+ if !os.IsNotExist(readErr) {
+ return "", nil, fmt.Errorf("failed to read %s: %w", candidate, readErr)
+ }
+ if dir == absRepoRoot {
+ return "", nil, nil
+ }
+ parent := filepath.Dir(dir)
+ if parent == dir {
+ return "", nil, nil
+ }
+ dir = parent
+ }
+}
+
+func isPathInsideRoot(root, path string) bool {
+ rel, err := filepath.Rel(root, path)
+ if err != nil {
+ return false
+ }
+ return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)))
+}
diff --git a/remediation/sca/packageupdaters/nugetpackageupdater_test.go b/remediation/sca/packageupdaters/nugetpackageupdater_test.go
index cff5c3f83..f61c5c8b9 100644
--- a/remediation/sca/packageupdaters/nugetpackageupdater_test.go
+++ b/remediation/sca/packageupdaters/nugetpackageupdater_test.go
@@ -920,5 +920,1133 @@ func TestUpdatePackageReferenceVersionRejectsMixedInlineAndNonInline(t *testing.
assert.Nil(t, updated)
var unsupportedErr *ErrUnsupportedFix
assert.True(t, errors.As(err, &unsupportedErr))
+ assert.Equal(t, MixedInlineAndNonInlineVersionFixNotSupported, unsupportedErr.ErrorType)
+}
+
+func TestNugetUpdateDependencyResolvesSingleDirectoryPackagesProps(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join("Cpm", "Proj", "Proj.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ propsContent, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(propsContent), `Include="Newtonsoft.Json" Version="13.0.1"`)
+
+ csprojContent, err := os.ReadFile(filepath.Join("Cpm", "Proj", "Proj.csproj"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(csprojContent), ``)
+
+ lockContent, err := os.ReadFile(filepath.Join("Cpm", "Proj", "packages.lock.json"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(lockContent), `"resolved":"13.0.1"`)
+}
+
+func TestNugetUpdateDependencyResolvesProjectFileFromLockFileEvidence(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join("Cpm", "Proj", "packages.lock.json")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ propsContent, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(propsContent), `Include="Newtonsoft.Json" Version="13.0.1"`)
+
+ lockContent, err := os.ReadFile(filepath.Join("Cpm", "Proj", "packages.lock.json"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(lockContent), `"resolved":"13.0.1"`)
+}
+
+func TestNugetUpdateDependencyResolvesDirectoryBuildProps(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join("BuildProps", "Proj", "packages.lock.json")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ buildPropsContent, err := os.ReadFile(filepath.Join("BuildProps", "Directory.Build.props"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(buildPropsContent), `Include="Newtonsoft.Json" Version="13.0.1"`)
+
+ csprojContent, err := os.ReadFile(filepath.Join("BuildProps", "Proj", "Proj.csproj"))
+ assert.NoError(t, err)
+ assert.NotContains(t, string(csprojContent), "Newtonsoft.Json")
+
+ lockContent, err := os.ReadFile(filepath.Join("BuildProps", "Proj", "packages.lock.json"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(lockContent), `"resolved":"13.0.1"`)
+}
+
+func TestNugetUpdateDependencyVersionOverrideTakesPrecedenceOverCpm(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ originalProps, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join("Cpm", "ProjWithOverride", "ProjWithOverride.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ csprojContent, err := os.ReadFile(filepath.Join("Cpm", "ProjWithOverride", "ProjWithOverride.csproj"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(csprojContent), `VersionOverride="13.0.1"`)
+ assert.NotContains(t, string(csprojContent), `VersionOverride="11.0.1"`)
+
+ propsContent, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, originalProps, propsContent)
+
+ lockContent, err := os.ReadFile(filepath.Join("Cpm", "ProjWithOverride", "packages.lock.json"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(lockContent), `"resolved":"13.0.1"`)
+}
+
+func TestNugetUpdateDependencyNestedDirectoryPackagesPropsClosestWins(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ originalRootProps, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join("Cpm", "Nested", "NestedProj", "NestedProj.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ nestedProps, err := os.ReadFile(filepath.Join("Cpm", "Nested", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(nestedProps), `Include="Newtonsoft.Json" Version="13.0.1"`)
+
+ rootProps, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, originalRootProps, rootProps)
+
+ lockContent, err := os.ReadFile(filepath.Join("Cpm", "Nested", "NestedProj", "packages.lock.json"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(lockContent), `"resolved":"13.0.1"`)
+}
+
+func TestNugetUpdateDependencyVersionOverrideChildElementTakesPrecedenceOverCpm(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ csprojPath := filepath.Join("Cpm", "ProjWithOverride", "ProjWithOverride.csproj")
+ assert.NoError(t, os.WriteFile(csprojPath, []byte(`
+
+ net8.0
+ true
+
+
+
+ 11.0.1
+
+
+
+`), 0644))
+
+ originalProps, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: csprojPath}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ csprojContent, err := os.ReadFile(csprojPath)
+ assert.NoError(t, err)
+ assert.Contains(t, string(csprojContent), `13.0.1`)
+ assert.NotContains(t, string(csprojContent), `11.0.1`)
+
+ propsContent, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, originalProps, propsContent)
+}
+
+func TestNugetUpdateDependencyDirectoryPackagesPropsWithoutMatchingEntry(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "1.0.0",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "NotInCpm.Package",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join("Cpm", "Proj", "Proj.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.Error(t, err)
+ var unsupportedErr *ErrUnsupportedFix
+ assert.True(t, errors.As(err, &unsupportedErr))
+ assert.Equal(t, NoInlineVersionFixNotSupported, unsupportedErr.ErrorType)
+}
+
+func TestResolveDirectoryPackagesPropsDoesNotWalkOutsideRepoRoot(t *testing.T) {
+ integration.InitUnitTest(t)
+
+ repoRoot := t.TempDir()
+ outsideRoot := t.TempDir()
+ projectDir := filepath.Join(outsideRoot, "app")
+ assert.NoError(t, os.MkdirAll(projectDir, 0755))
+ outsideProps := filepath.Join(outsideRoot, "Directory.Packages.props")
+ assert.NoError(t, os.WriteFile(outsideProps, []byte(``), 0644))
+
+ path, content, err := resolveDirectoryFile(projectDir, repoRoot, nugetDirectoryPackagesPropsName)
+ assert.NoError(t, err)
+ assert.Empty(t, path)
+ assert.Empty(t, content)
+}
+
+func TestNugetUpdateDependencyFollowsDirectoryPackagesPropsImport(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ importedDir := filepath.Join("Cpm", "Imported")
+ assert.NoError(t, os.MkdirAll(filepath.Join(importedDir, "Proj"), 0755))
+ assert.NoError(t, os.WriteFile(filepath.Join(importedDir, "Directory.Packages.props"), []byte(`
+
+ true
+
+
+
+`), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(importedDir, "Packages.props"), []byte(`
+
+
+
+
+`), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(importedDir, "Proj", "Proj.csproj"), []byte(`
+
+ net8.0
+ true
+
+
+
+
+
+`), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(importedDir, "Proj", "packages.lock.json"), []byte(`{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"12.0.3"}}}}`), 0644))
+
+ originalRootProps, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ originalDirectoryPackages, err := os.ReadFile(filepath.Join(importedDir, "Directory.Packages.props"))
+ assert.NoError(t, err)
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join(importedDir, "Proj", "Proj.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ importedProps, err := os.ReadFile(filepath.Join(importedDir, "Packages.props"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(importedProps), `Include="Newtonsoft.Json" Version="13.0.1"`)
+
+ directoryPackages, err := os.ReadFile(filepath.Join(importedDir, "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, originalDirectoryPackages, directoryPackages)
+
+ rootProps, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, originalRootProps, rootProps)
+}
+
+func TestNugetUpdateDependencyFollowsMSBuildThisFileDirectoryImport(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ originalDirectoryPackages, err := os.ReadFile(filepath.Join("CpmMacroImport", "Directory.Packages.props"))
+ assert.NoError(t, err)
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join("CpmMacroImport", "Proj", "packages.lock.json")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ sharedProps, err := os.ReadFile(filepath.Join("CpmMacroImport", "Shared.props"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(sharedProps), `Include="Newtonsoft.Json" Version="13.0.1"`)
+
+ directoryPackages, err := os.ReadFile(filepath.Join("CpmMacroImport", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, originalDirectoryPackages, directoryPackages)
+
+ lockContent, err := os.ReadFile(filepath.Join("CpmMacroImport", "Proj", "packages.lock.json"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(lockContent), `"resolved":"13.0.1"`)
+}
+
+func TestNugetUpdateDependencySkipsWhenManagePackageVersionsCentrallyFalse(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ disabledDir := "CpmDisabled"
+ assert.NoError(t, os.MkdirAll(filepath.Join(disabledDir, "Proj"), 0755))
+ propsContent := []byte(`
+
+ false
+
+
+
+
+
+`)
+ assert.NoError(t, os.WriteFile(filepath.Join(disabledDir, "Directory.Packages.props"), propsContent, 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(disabledDir, "Proj", "Proj.csproj"), []byte(`
+
+ net8.0
+
+
+
+
+
+`), 0644))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join(disabledDir, "Proj", "Proj.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.Error(t, err)
+ var unsupportedErr *ErrUnsupportedFix
+ assert.True(t, errors.As(err, &unsupportedErr))
+ assert.Equal(t, NoInlineVersionFixNotSupported, unsupportedErr.ErrorType)
+
+ unchangedProps, err := os.ReadFile(filepath.Join(disabledDir, "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, propsContent, unchangedProps)
+}
+
+func TestNugetUpdateDependencyRollsBackDirectoryPackagesPropsOnRestoreFailure(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ originalProps, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ originalLock, err := os.ReadFile(filepath.Join("Cpm", "Proj", "packages.lock.json"))
+ assert.NoError(t, err)
+
+ toolDir := t.TempDir()
+ writeFakeDotnetRestore(t, toolDir, 1, `{"corrupted": true}`, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join("Cpm", "Proj", "Proj.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "dotnet restore failed")
+
+ rolledBackProps, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, originalProps, rolledBackProps)
+ rolledBackLock, err := os.ReadFile(filepath.Join("Cpm", "Proj", "packages.lock.json"))
+ assert.NoError(t, err)
+ assert.Equal(t, originalLock, rolledBackLock)
+}
+
+func TestNugetUpdateDependencyTwoProjectsSharingDirectoryPackagesProps(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ otherProjDir := filepath.Join("Cpm", "OtherProj")
+ assert.NoError(t, os.MkdirAll(otherProjDir, 0755))
+ assert.NoError(t, os.WriteFile(filepath.Join(otherProjDir, "OtherProj.csproj"), []byte(`
+
+ net8.0
+ true
+
+
+
+
+
+`), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(otherProjDir, "packages.lock.json"), []byte(`{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"12.0.3"}}}}`), 0644))
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{
+ {File: filepath.Join("Cpm", "Proj", "Proj.csproj")},
+ {File: filepath.Join(otherProjDir, "OtherProj.csproj")},
+ }}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ propsContent, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(propsContent), `Include="Newtonsoft.Json" Version="13.0.1"`)
+
+ for _, lockPath := range []string{filepath.Join("Cpm", "Proj", "packages.lock.json"), filepath.Join(otherProjDir, "packages.lock.json")} {
+ lockContent, readErr := os.ReadFile(lockPath)
+ assert.NoError(t, readErr)
+ assert.Contains(t, string(lockContent), `"resolved":"13.0.1"`)
+ }
+
+ argsLog, err := os.ReadFile(filepath.Join(toolDir, "args.log"))
+ assert.NoError(t, err)
+ assert.Equal(t, 2, strings.Count(string(argsLog), "\n"))
+}
+
+func TestNugetUpdateDependencyPackageVersionChildElement(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ assert.NoError(t, os.WriteFile(filepath.Join("Cpm", "Directory.Packages.props"), []byte(`
+
+ true
+
+
+
+ 12.0.3
+
+
+
+`), 0644))
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join("Cpm", "Proj", "Proj.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ propsContent, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(propsContent), `13.0.1`)
+ assert.NotContains(t, string(propsContent), `12.0.3`)
+}
+
+func TestNugetUpdateDependencyDoesNotFallbackOnMixedInlineAndVersionless(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ csprojPath := filepath.Join("Cpm", "Proj", "Proj.csproj")
+ assert.NoError(t, os.WriteFile(csprojPath, []byte(`
+
+ net8.0
+ true
+
+
+
+
+
+
+`), 0644))
+
+ originalProps, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: csprojPath}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.Error(t, err)
+ var unsupportedErr *ErrUnsupportedFix
+ assert.True(t, errors.As(err, &unsupportedErr))
+ assert.Equal(t, MixedInlineAndNonInlineVersionFixNotSupported, unsupportedErr.ErrorType)
+
+ propsContent, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, originalProps, propsContent)
+}
+
+func TestNugetUpdateDependencySkipsAmbiguousLockFileEvidence(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ multiDir := filepath.Join("Cpm", "MultiProj")
+ assert.NoError(t, os.MkdirAll(multiDir, 0755))
+ assert.NoError(t, os.WriteFile(filepath.Join(multiDir, "App.csproj"), []byte(``), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(multiDir, "Lib.csproj"), []byte(``), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(multiDir, "packages.lock.json"), []byte(`{"version":1}`), 0644))
+
+ originalProps, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join(multiDir, "packages.lock.json")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "no NuGet project locations found")
+
+ propsContent, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, originalProps, propsContent)
+}
+
+func TestNugetUpdateDependencyPackageVersionUpdateAttribute(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ assert.NoError(t, os.WriteFile(filepath.Join("Cpm", "Directory.Packages.props"), []byte(`
+
+ true
+
+
+
+
+
+`), 0644))
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join("Cpm", "Proj", "Proj.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ propsContent, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(propsContent), `Update="Newtonsoft.Json" Version="13.0.1"`)
+}
+
+func TestNugetUpdateDependencyPackageVersionCaseInsensitive(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ assert.NoError(t, os.WriteFile(filepath.Join("Cpm", "Directory.Packages.props"), []byte(`
+
+ true
+
+
+
+
+
+`), 0644))
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join("Cpm", "Proj", "Proj.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ propsContent, err := os.ReadFile(filepath.Join("Cpm", "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(propsContent), `Include="Newtonsoft.Json" Version="13.0.1"`)
+}
+
+func TestNugetUpdateDependencySkipsWhenCpmDisabledInDirectoryBuildProps(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ disabledDir := "CpmDisabledViaBuild"
+ assert.NoError(t, os.MkdirAll(filepath.Join(disabledDir, "Proj"), 0755))
+ propsContent := []byte(`
+
+
+
+
+`)
+ assert.NoError(t, os.WriteFile(filepath.Join(disabledDir, "Directory.Packages.props"), propsContent, 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(disabledDir, "Directory.Build.props"), []byte(`
+
+ false
+
+
+`), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(disabledDir, "Proj", "Proj.csproj"), []byte(`
+
+ net8.0
+
+
+
+
+
+`), 0644))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join(disabledDir, "Proj", "Proj.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.Error(t, err)
+ var unsupportedErr *ErrUnsupportedFix
+ assert.True(t, errors.As(err, &unsupportedErr))
assert.Equal(t, NoInlineVersionFixNotSupported, unsupportedErr.ErrorType)
+
+ unchangedProps, err := os.ReadFile(filepath.Join(disabledDir, "Directory.Packages.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, propsContent, unchangedProps)
+}
+
+func TestNugetUpdateDependencyPackageVersionInDirectoryBuildProps(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ dir := "BuildPropsPackageVersion"
+ assert.NoError(t, os.MkdirAll(filepath.Join(dir, "Proj"), 0755))
+ assert.NoError(t, os.WriteFile(filepath.Join(dir, "Directory.Build.props"), []byte(`
+
+
+
+
+`), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(dir, "Proj", "Proj.csproj"), []byte(`
+
+ net8.0
+ true
+
+
+
+
+
+`), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(dir, "Proj", "packages.lock.json"), []byte(`{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"12.0.3"}}}}`), 0644))
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join(dir, "Proj", "packages.lock.json")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ buildPropsContent, err := os.ReadFile(filepath.Join(dir, "Directory.Build.props"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(buildPropsContent), `Include="Newtonsoft.Json" Version="13.0.1"`)
+}
+
+func TestNugetUpdateDependencyResolvesDirectoryBuildTargets(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ dir := "BuildTargets"
+ assert.NoError(t, os.MkdirAll(filepath.Join(dir, "Proj"), 0755))
+ assert.NoError(t, os.WriteFile(filepath.Join(dir, "Directory.Build.targets"), []byte(`
+
+
+
+
+`), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(dir, "Proj", "Proj.csproj"), []byte(`
+
+ net8.0
+ true
+
+
+`), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(dir, "Proj", "packages.lock.json"), []byte(`{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"12.0.3"}}}}`), 0644))
+
+ toolDir := t.TempDir()
+ regeneratedLock := `{"version":1,"dependencies":{"net8.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}}}`
+ writeFakeDotnetRestore(t, toolDir, 0, regeneratedLock, false)
+ t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join(dir, "Proj", "packages.lock.json")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.NoError(t, err)
+
+ targetsContent, err := os.ReadFile(filepath.Join(dir, "Directory.Build.targets"))
+ assert.NoError(t, err)
+ assert.Contains(t, string(targetsContent), `Include="Newtonsoft.Json" Version="13.0.1"`)
+}
+
+func TestNugetUpdateDependencyClosestDirectoryBuildPropsWins(t *testing.T) {
+ integration.InitUnitTest(t)
+ testProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "projects", "package-managers", "nuget", "remediation-packageupdaters")
+ currDir, err := os.Getwd()
+ assert.NoError(t, err)
+
+ tmpDir, err := os.MkdirTemp("", "nuget-test-*")
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, fileutils.RemoveTempDir(tmpDir))
+ }()
+ assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil))
+ assert.NoError(t, os.Chdir(tmpDir))
+ defer func() {
+ assert.NoError(t, os.Chdir(currDir))
+ }()
+
+ dir := "BuildPropsNested"
+ assert.NoError(t, os.MkdirAll(filepath.Join(dir, "src", "Proj"), 0755))
+ parentProps := []byte(`
+
+
+
+
+`)
+ assert.NoError(t, os.WriteFile(filepath.Join(dir, "Directory.Build.props"), parentProps, 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(dir, "src", "Directory.Build.props"), []byte(`
+`), 0644))
+ assert.NoError(t, os.WriteFile(filepath.Join(dir, "src", "Proj", "Proj.csproj"), []byte(`
+
+ net8.0
+
+
+`), 0644))
+
+ fixDetails := &FixDetails{
+ SuggestedFixedVersion: "13.0.1",
+ IsDirectDependency: true,
+ Technology: techutils.Nuget,
+ ImpactedDependencyName: "Newtonsoft.Json",
+ Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: filepath.Join(dir, "src", "Proj", "Proj.csproj")}}}},
+ }
+
+ updater := &NugetPackageUpdater{}
+ err = updater.UpdateDependency(fixDetails)
+ assert.Error(t, err)
+ var unsupportedErr *ErrUnsupportedFix
+ assert.True(t, errors.As(err, &unsupportedErr))
+ assert.Equal(t, NoInlineVersionFixNotSupported, unsupportedErr.ErrorType)
+
+ unchangedParent, err := os.ReadFile(filepath.Join(dir, "Directory.Build.props"))
+ assert.NoError(t, err)
+ assert.Equal(t, parentProps, unchangedParent)
}
diff --git a/remediation/sca/packageupdaters/types.go b/remediation/sca/packageupdaters/types.go
index 1a16a3917..876a3875c 100644
--- a/remediation/sca/packageupdaters/types.go
+++ b/remediation/sca/packageupdaters/types.go
@@ -24,13 +24,13 @@ type UnsupportedErrorType string
const (
IndirectDependencyFixNotSupported UnsupportedErrorType = "IndirectDependencyFixNotSupported"
- // NoInlineVersionFixNotSupported covers any PackageReference found with no inline version -
- // whether it's actually governed by Central Package Management, supplied via
- // Directory.Build.props, overridden elsewhere, or an SDK-implicit reference. The updater can't
- // tell these apart from the reference site alone, so it reports them all the same way rather
- // than guessing.
- NoInlineVersionFixNotSupported UnsupportedErrorType = "NoInlineVersionFixNotSupported"
- UnsupportedFixReason UnsupportedErrorType = "UnsupportedFixReason"
+ // NoInlineVersionFixNotSupported covers a PackageReference with no inline version, no
+ // VersionOverride, and no resolvable PackageVersion or PackageReference in the nearest
+ // Directory.Packages.props, Directory.Build.props, Directory.Build.targets, or a file they
+ // Import - including when Central Package Management is disabled.
+ NoInlineVersionFixNotSupported UnsupportedErrorType = "NoInlineVersionFixNotSupported"
+ MixedInlineAndNonInlineVersionFixNotSupported UnsupportedErrorType = "MixedInlineAndNonInlineVersionFixNotSupported"
+ UnsupportedFixReason UnsupportedErrorType = "UnsupportedFixReason"
)
type ErrUnsupportedFix struct {
@@ -45,7 +45,9 @@ func (err *ErrUnsupportedFix) Error() string {
case IndirectDependencyFixNotSupported:
return fmt.Sprintf("skipping fix of vulnerable package '%s' version '%s' - indirect dependency fix is not supported", err.PackageName, err.FixedVersion)
case NoInlineVersionFixNotSupported:
- return fmt.Sprintf("skipping fix of vulnerable package '%s' version '%s' - no inline version found on the reference (may be centrally managed, supplied via Directory.Build.props, overridden elsewhere, or an SDK-implicit reference) and fixing it is not yet supported", err.PackageName, err.FixedVersion)
+ return fmt.Sprintf("skipping fix of vulnerable package '%s' version '%s' - could not resolve a version to fix (no inline attribute, VersionOverride, or matching Directory.Packages.props / Directory.Build.props entry found)", err.PackageName, err.FixedVersion)
+ case MixedInlineAndNonInlineVersionFixNotSupported:
+ return fmt.Sprintf("skipping fix of vulnerable package '%s' version '%s' - package has both inline and versionless PackageReference entries", err.PackageName, err.FixedVersion)
case UnsupportedFixReason:
return fmt.Sprintf("skipping fix of vulnerable package '%s' version '%s' - %s", err.PackageName, err.FixedVersion, err.Reason)
default:
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/BuildProps/Directory.Build.props b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/BuildProps/Directory.Build.props
new file mode 100644
index 000000000..f7abc52e2
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/BuildProps/Directory.Build.props
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/BuildProps/Proj/Proj.csproj b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/BuildProps/Proj/Proj.csproj
new file mode 100644
index 000000000..6b6c8424a
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/BuildProps/Proj/Proj.csproj
@@ -0,0 +1,8 @@
+
+
+
+ net8.0
+ true
+
+
+
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/BuildProps/Proj/packages.lock.json b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/BuildProps/Proj/packages.lock.json
new file mode 100644
index 000000000..11fff8398
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/BuildProps/Proj/packages.lock.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "dependencies": {
+ "net8.0": {
+ "Newtonsoft.Json": {
+ "type": "Direct",
+ "requested": "[12.0.3, )",
+ "resolved": "12.0.3",
+ "contentHash": "6mgjfnRB4jKMlzHSl+VD+Fk4tsfGyu+CBjBTP1sOnCPQ7z+H5DwCZjxr75tGD1jXTAcgwmttSaTgWJ0t9Uu9jA=="
+ }
+ }
+ }
+}
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Directory.Packages.props b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Directory.Packages.props
new file mode 100644
index 000000000..b470562c8
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Directory.Packages.props
@@ -0,0 +1,8 @@
+
+
+ true
+
+
+
+
+
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Nested/Directory.Packages.props b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Nested/Directory.Packages.props
new file mode 100644
index 000000000..06d6fabf9
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Nested/Directory.Packages.props
@@ -0,0 +1,8 @@
+
+
+ true
+
+
+
+
+
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Nested/NestedProj/NestedProj.csproj b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Nested/NestedProj/NestedProj.csproj
new file mode 100644
index 000000000..c4755f963
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Nested/NestedProj/NestedProj.csproj
@@ -0,0 +1,12 @@
+
+
+
+ net8.0
+ true
+
+
+
+
+
+
+
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Nested/NestedProj/packages.lock.json b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Nested/NestedProj/packages.lock.json
new file mode 100644
index 000000000..fac02dbf4
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Nested/NestedProj/packages.lock.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "dependencies": {
+ "net8.0": {
+ "Newtonsoft.Json": {
+ "type": "Direct",
+ "requested": "[11.0.2, )",
+ "resolved": "11.0.2",
+ "contentHash": "6mgjfnRB4jKMlzHSl+VD+Fk4tsfGyu+CBjBTP1sOnCPQ7z+H5DwCZjxr75tGD1jXTAcgwmttSaTgWJ0t9Uu9jA=="
+ }
+ }
+ }
+}
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Proj/Proj.csproj b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Proj/Proj.csproj
new file mode 100644
index 000000000..b9e956476
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Proj/Proj.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ true
+
+
+
+
+
+
+
+
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Proj/packages.lock.json b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Proj/packages.lock.json
new file mode 100644
index 000000000..11fff8398
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/Proj/packages.lock.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "dependencies": {
+ "net8.0": {
+ "Newtonsoft.Json": {
+ "type": "Direct",
+ "requested": "[12.0.3, )",
+ "resolved": "12.0.3",
+ "contentHash": "6mgjfnRB4jKMlzHSl+VD+Fk4tsfGyu+CBjBTP1sOnCPQ7z+H5DwCZjxr75tGD1jXTAcgwmttSaTgWJ0t9Uu9jA=="
+ }
+ }
+ }
+}
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/ProjWithOverride/ProjWithOverride.csproj b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/ProjWithOverride/ProjWithOverride.csproj
new file mode 100644
index 000000000..663d209ab
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/ProjWithOverride/ProjWithOverride.csproj
@@ -0,0 +1,12 @@
+
+
+
+ net8.0
+ true
+
+
+
+
+
+
+
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/ProjWithOverride/packages.lock.json b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/ProjWithOverride/packages.lock.json
new file mode 100644
index 000000000..4f0c86dc7
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/Cpm/ProjWithOverride/packages.lock.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "dependencies": {
+ "net8.0": {
+ "Newtonsoft.Json": {
+ "type": "Direct",
+ "requested": "[11.0.1, )",
+ "resolved": "11.0.1",
+ "contentHash": "6mgjfnRB4jKMlzHSl+VD+Fk4tsfGyu+CBjBTP1sOnCPQ7z+H5DwCZjxr75tGD1jXTAcgwmttSaTgWJ0t9Uu9jA=="
+ }
+ }
+ }
+}
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Directory.Packages.props b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Directory.Packages.props
new file mode 100644
index 000000000..00902f368
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Directory.Packages.props
@@ -0,0 +1,6 @@
+
+
+ true
+
+
+
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Proj/Proj.csproj b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Proj/Proj.csproj
new file mode 100644
index 000000000..c4755f963
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Proj/Proj.csproj
@@ -0,0 +1,12 @@
+
+
+
+ net8.0
+ true
+
+
+
+
+
+
+
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Proj/packages.lock.json b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Proj/packages.lock.json
new file mode 100644
index 000000000..11fff8398
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Proj/packages.lock.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "dependencies": {
+ "net8.0": {
+ "Newtonsoft.Json": {
+ "type": "Direct",
+ "requested": "[12.0.3, )",
+ "resolved": "12.0.3",
+ "contentHash": "6mgjfnRB4jKMlzHSl+VD+Fk4tsfGyu+CBjBTP1sOnCPQ7z+H5DwCZjxr75tGD1jXTAcgwmttSaTgWJ0t9Uu9jA=="
+ }
+ }
+ }
+}
diff --git a/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Shared.props b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Shared.props
new file mode 100644
index 000000000..3881ba67a
--- /dev/null
+++ b/tests/testdata/projects/package-managers/nuget/remediation-packageupdaters/CpmMacroImport/Shared.props
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/utils/results/common.go b/utils/results/common.go
index 4febf1e57..f8d2ffcb3 100644
--- a/utils/results/common.go
+++ b/utils/results/common.go
@@ -1563,12 +1563,11 @@ func CdxEvidencesToPreferredLocation(component cyclonedx.Component) (location *f
}
}
// We need to pick the preferred location from the evidences (we prefer descriptors over lock files)
- for _, occurrence := range *component.Evidence.Occurrences {
- if techutils.IsTechnologyDescriptor(occurrence.Location) != techutils.NoTech {
- return &formats.Location{
- File: occurrence.Location,
- }
- }
+ if location := firstDescriptorLocation(*component.Evidence.Occurrences, false); location != nil {
+ return location
+ }
+ if location := firstDescriptorLocation(*component.Evidence.Occurrences, true); location != nil {
+ return location
}
// We take the first location as the main location
log.Debug(fmt.Sprintf("Multiple locations found for component %s evidence, using the first one as location", component.Name))
@@ -1577,6 +1576,19 @@ func CdxEvidencesToPreferredLocation(component cyclonedx.Component) (location *f
}
}
+func firstDescriptorLocation(occurrences []cyclonedx.EvidenceOccurrence, allowLockFile bool) *formats.Location {
+ for _, occurrence := range occurrences {
+ if techutils.IsTechnologyDescriptor(occurrence.Location) == techutils.NoTech {
+ continue
+ }
+ if !allowLockFile && techutils.IsLockFilePackageDescriptor(occurrence.Location) {
+ continue
+ }
+ return &formats.Location{File: occurrence.Location}
+ }
+ return nil
+}
+
func getExternalReferencesUrls(component cyclonedx.Component) (externalReferences []string) {
if component.ExternalReferences != nil && len(*component.ExternalReferences) > 0 {
for _, externalReference := range *component.ExternalReferences {
diff --git a/utils/results/common_test.go b/utils/results/common_test.go
index ffd47daf5..c85c12dc5 100644
--- a/utils/results/common_test.go
+++ b/utils/results/common_test.go
@@ -3057,6 +3057,16 @@ func TestCdxEvidencesToPreferredLocation(t *testing.T) {
component: cyclonedx.Component{Evidence: &cyclonedx.Evidence{Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "requirements.txt"}, {Location: "requirements.lock"}}}},
expected: &formats.Location{File: "requirements.txt"},
},
+ {
+ name: "Component with evidences (nuget prefers csproj over lock)",
+ component: cyclonedx.Component{Evidence: &cyclonedx.Evidence{Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "packages.lock.json"}, {Location: "Proj.csproj"}}}},
+ expected: &formats.Location{File: "Proj.csproj"},
+ },
+ {
+ name: "Component with nuget lock file only",
+ component: cyclonedx.Component{Evidence: &cyclonedx.Evidence{Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "src/packages.lock.json"}}}},
+ expected: &formats.Location{File: "src/packages.lock.json"},
+ },
}
for _, test := range tests {
diff --git a/utils/techutils/techutils.go b/utils/techutils/techutils.go
index cd975ae8c..8491b4c0f 100644
--- a/utils/techutils/techutils.go
+++ b/utils/techutils/techutils.go
@@ -263,9 +263,13 @@ var technologiesData = map[Technology]TechData{
language: Python,
},
Nuget: {
- formal: "NuGet",
- indicators: []string{".sln", ".slnx", ".csproj", ".fsproj", ".vbproj"},
- packageDescriptors: []string{".sln", ".slnx", ".csproj", ".fsproj", ".vbproj"},
+ formal: "NuGet",
+ indicators: []string{".sln", ".slnx", ".csproj", ".fsproj", ".vbproj"},
+ // packages.lock.json is included here too: for a package whose version comes from Central
+ // Package Management or Directory.Build.props, the .csproj itself never contains the
+ // version text, so the scanner cites only the lock file as evidence. Without it listed here,
+ // that evidence is filtered out before it ever reaches the NuGet fix updater.
+ packageDescriptors: []string{".sln", ".slnx", ".csproj", ".fsproj", ".vbproj", "packages.lock.json"},
// .NET CLI is used for NuGet projects
execCommand: "dotnet",
packageInstallationCommand: "add",
@@ -456,6 +460,10 @@ func IsTechnologyDescriptor(path string) Technology {
return NoTech
}
+func IsLockFilePackageDescriptor(path string) bool {
+ return strings.EqualFold(filepath.Base(path), "packages.lock.json")
+}
+
func DetectedTechnologiesList() (technologies []string) {
wd, err := os.Getwd()
if errorutils.CheckError(err) != nil {