diff --git a/.gitignore b/.gitignore index 533090632..5aa2ec9b8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ ComponentDetection/src/Microsoft.ComponentDetection.Loader/Properties/launchSett node_modules/ dist/ dist-nuget/ +.nuget/ # Build results [Dd]ebug/ diff --git a/src/Microsoft.ComponentDetection.Detectors/dotnet/DotNetComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dotnet/DotNetComponentDetector.cs index 8770ce3bb..24bfdad8d 100644 --- a/src/Microsoft.ComponentDetection.Detectors/dotnet/DotNetComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/dotnet/DotNetComponentDetector.cs @@ -274,20 +274,46 @@ private bool IsApplication(string assemblyPath) if (this.fileUtilityService.Exists(globalJsonPath)) { - sdkVersion = await this.RunDotNetVersionAsync(projectDirectory, cancellationToken); - - if (string.IsNullOrWhiteSpace(sdkVersion)) + // Read the version declared in global.json first + string? globalJsonVersion = null; + using (var globalJsonDoc = await JsonDocument.ParseAsync(this.fileUtilityService.MakeFileStream(globalJsonPath), cancellationToken: cancellationToken, options: this.jsonDocumentOptions).ConfigureAwait(false)) { - var globalJson = await JsonDocument.ParseAsync(this.fileUtilityService.MakeFileStream(globalJsonPath), cancellationToken: cancellationToken, options: this.jsonDocumentOptions).ConfigureAwait(false); - if (globalJson.RootElement.TryGetProperty("sdk", out var sdk)) + if (globalJsonDoc.RootElement.TryGetProperty("sdk", out var sdk)) { if (sdk.TryGetProperty("version", out var version)) { - sdkVersion = version.GetString(); + globalJsonVersion = version.GetString(); } } } + // Try to get the version actually resolved by the SDK + var resolvedVersion = await this.RunDotNetVersionAsync(projectDirectory, cancellationToken); + + if (!string.IsNullOrWhiteSpace(resolvedVersion)) + { + sdkVersion = resolvedVersion; + + // Only register against global.json when the resolved version matches what global.json declares. + // If there is a mismatch (e.g. roll-forward selected a newer SDK), the component should not be + // attributed to global.json because changing that file would not fix the reported version. + if (!string.IsNullOrWhiteSpace(globalJsonVersion) && + !sdkVersion.Equals(globalJsonVersion, StringComparison.OrdinalIgnoreCase)) + { + this.Logger.LogInformation( + "Resolved SDK version {ResolvedVersion} does not match global.json version {GlobalJsonVersion} in {GlobalJsonPath}. Not registering component against global.json.", + resolvedVersion, + globalJsonVersion, + globalJsonPath); + return sdkVersion; + } + } + else + { + // dotnet --version failed; fall back to the version declared in global.json + sdkVersion = globalJsonVersion; + } + if (!string.IsNullOrWhiteSpace(sdkVersion)) { var globalJsonComponent = new DetectedComponent(new DotNetComponent(sdkVersion)); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/DotNetComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/DotNetComponentDetectorTests.cs index d4941512a..b5c698ef1 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/DotNetComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/DotNetComponentDetectorTests.cs @@ -367,8 +367,11 @@ public async Task TestDotNetDetectorGlobalJsonWithoutVersion() } [TestMethod] - public async Task TestDotNetDetectorGlobalJsonRollForward_ReturnsSDKVersion() + public async Task TestDotNetDetectorGlobalJsonRollForward_DoesNotRegisterAgainstGlobalJson() { + // When dotnet resolves a different SDK version than declared in global.json (roll-forward), + // the component should NOT be registered against global.json because the user would need to + // change their build environment, not global.json. var projectPath = Path.Combine(RootDir, "path", "to", "project"); var projectAssets = ProjectAssets("projectName", "does-not-exist", projectPath, "net8.0"); var globalJson = GlobalJson("8.0.100"); @@ -383,11 +386,38 @@ public async Task TestDotNetDetectorGlobalJsonRollForward_ReturnsSDKVersion() scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); var detectedComponents = componentRecorder.GetDetectedComponents(); - detectedComponents.Should().HaveCount(2); + detectedComponents.Should().ContainSingle(); var discoveredComponents = detectedComponents.ToArray(); - discoveredComponents.Where(component => component.Component.Id == "8.0.808 unknown unknown - DotNet").Should().ContainSingle(); discoveredComponents.Where(component => component.Component.Id == "8.0.808 net8.0 unknown - DotNet").Should().ContainSingle(); + discoveredComponents.Where(component => component.Component.Id == "8.0.100 unknown unknown - DotNet").Should().BeEmpty(); + discoveredComponents.Where(component => component.Component.Id == "8.0.808 unknown unknown - DotNet").Should().BeEmpty(); + } + + [TestMethod] + public async Task TestDotNetDetectorGlobalJsonMatchingVersion_RegistersAgainstGlobalJson() + { + // When dotnet resolves the same SDK version as declared in global.json, + // the component SHOULD be registered against global.json. + var projectPath = Path.Combine(RootDir, "path", "to", "project"); + var projectAssets = ProjectAssets("projectName", "does-not-exist", projectPath, "net8.0"); + var globalJson = GlobalJson("8.0.100"); + this.AddFile(projectPath, null); + this.AddFile(Path.Combine(RootDir, "path", "global.json"), globalJson); + this.SetCommandResult(0, "8.0.100"); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("project.assets.json", projectAssets) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(2); + + var discoveredComponents = detectedComponents.ToArray(); + discoveredComponents.Where(component => component.Component.Id == "8.0.100 unknown unknown - DotNet").Should().ContainSingle(); + discoveredComponents.Where(component => component.Component.Id == "8.0.100 net8.0 unknown - DotNet").Should().ContainSingle(); } [TestMethod]