diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index a1c52fbc..00000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM ubuntu:20.04 - -RUN apt-get update \ - && apt-get install -y \ - wget \ - apt-transport-https \ - && wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \ - && dpkg -i packages-microsoft-prod.deb \ - && apt-get update \ - && apt-get install -y dotnet-sdk-2.1 \ - && apt-get install -y dotnet-sdk-3.1 \ - && rm -rf /var/lib/apt/lists/* diff --git a/.devcontainer/Ubuntu22.04/Dockerfile b/.devcontainer/Ubuntu22.04/Dockerfile new file mode 100644 index 00000000..b153ad56 --- /dev/null +++ b/.devcontainer/Ubuntu22.04/Dockerfile @@ -0,0 +1,8 @@ +FROM ubuntu:22.04 + +RUN apt-get install -y --no-install-recommends wget=2.0.1 +RUN apt-get install -y --no-install-recommends apt-transport-https=2.5.6 +RUN curl -o ./packages-microsoft-prod.deb https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb +RUN dpkg -i packages-microsoft-prod.deb +RUN apt-get install -y --no-install-recommends dotnet-sdk-7.0=7.0 +RUN rm -rf /var/lib/apt/lists/* diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f38a279f..8c43a082 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ { "name": "devcontainer", "build": { - "dockerfile": "Dockerfile", + "dockerfile": "Ubuntu22.04/Dockerfile", "context": ".." }, "extensions": [ diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 5327e90f..066bb60e 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -16,15 +16,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v3.1.0 - - name: Setup dotnet 3.1 + - uses: actions/checkout@v3.5.2 + - name: Setup dotnet uses: actions/setup-dotnet@v3.0.3 with: - dotnet-version: '3.1.x' - - name: Setup dotnet 6 - uses: actions/setup-dotnet@v3.0.3 - with: - dotnet-version: '6.x' + dotnet-version: | + 7.x - name: Build run: dotnet build /WarnAsError @@ -46,15 +43,15 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - framework: ['netcoreapp3.1','net6.0'] + framework: ['net6.0','net7.0'] timeout-minutes: 30 - + steps: - - uses: actions/checkout@v3.1.0 - - name: Setup dotnet 3.1 + - uses: actions/checkout@v3.5.2 + - name: Setup dotnet 7 uses: actions/setup-dotnet@v3.0.3 with: - dotnet-version: '3.1.x' + dotnet-version: '7.x' - name: Setup dotnet 6 uses: actions/setup-dotnet@v3.0.3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 27d2ce64..4358f279 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ # # For an example commit browse to # https://github.com/CycloneDX/cyclonedx-dotnet/commit/d110af854371374460430bb8438225a7d7a84274. -# +# # The resulting release is here # https://github.com/CycloneDX/cyclonedx-dotnet/releases/tag/v1.0.0. # @@ -28,19 +28,16 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v3.1.0 - - name: Setup dotnet 3.1 - uses: actions/setup-dotnet@v3.0.3 - with: - dotnet-version: '3.1.x' - - name: Setup dotnet 6 + - uses: actions/checkout@v3.5.2 + - name: Setup dotnet uses: actions/setup-dotnet@v3.0.3 with: - dotnet-version: '6.x' - + dotnet-version: | + 7.x + # The tests should have already been run during the PR workflow, so this is really just a sanity check - name: Tests - run: dotnet test --framework net6.0 + run: dotnet test --framework net7.0 # Build and package everything, including the Docker image - name: Package release diff --git a/.gitpod.yml b/.gitpod.yml index 8f286a18..a5e0d30b 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -8,9 +8,8 @@ tasks: wget --output-document="$DOTNET_ROOT/dotnet-install.sh" https://dot.net/v1/dotnet-install.sh chmod +x "$DOTNET_ROOT/dotnet-install.sh" "$DOTNET_ROOT/dotnet-install.sh" --channel 2.1 --install-dir "$DOTNET_ROOT" - "$DOTNET_ROOT/dotnet-install.sh" --channel 3.1 --install-dir "$DOTNET_ROOT" - "$DOTNET_ROOT/dotnet-install.sh" --channel 5.0 --install-dir "$DOTNET_ROOT" "$DOTNET_ROOT/dotnet-install.sh" --channel 6.0 --install-dir "$DOTNET_ROOT" + "$DOTNET_ROOT/dotnet-install.sh" --channel 7.0 --install-dir "$DOTNET_ROOT" dotnet tool install --global dotnet-reportgenerator-globaltool dotnet restore diff --git a/CycloneDX.Tests/CycloneDX.Tests.csproj b/CycloneDX.Tests/CycloneDX.Tests.csproj index 9828a6bd..9e42244f 100644 --- a/CycloneDX.Tests/CycloneDX.Tests.csproj +++ b/CycloneDX.Tests/CycloneDX.Tests.csproj @@ -1,28 +1,29 @@ - net6.0;netcoreapp3.1 true false + net7.0;net6.0 + latest - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/CycloneDX.Tests/NugetV3ServiceTests.cs b/CycloneDX.Tests/NugetV3ServiceTests.cs index 9ead85b6..4f006ef7 100644 --- a/CycloneDX.Tests/NugetV3ServiceTests.cs +++ b/CycloneDX.Tests/NugetV3ServiceTests.cs @@ -80,37 +80,52 @@ public async Task GetComponent_FromCachedNuspecFile_ReturnsComponent() Assert.Equal("testpackage", component.Name); } - [Fact] - public async Task GetComponent_FromCachedNugetHashFile_ReturnsComponentWithHash() + public static IEnumerable VersionNormalization { - var nuspecFileContents = @" + get + { + return new List + { + new object[] { "2.5", "2.5" }, + new object[] { "2.5.0.0", "2.5.0" }, + new object[] { "2.5.0.0-beta.1", "2.5.0-beta.1" }, + new object[] { "2.5.1.0", "2.5.1" }, + new object[] { "2.5.1.1", "2.5.1.1" } + }; + } + } + + [Theory] + [MemberData(nameof(VersionNormalization))] + public async Task GetComponent_FromCachedNuspecFile_UsesNormalizedVersions(string rawVersion, string normalizedVersion) + { + var nuspecFileContents = $@" testpackage + {rawVersion} "; - byte[] sampleHash = new byte[] { 1, 2, 3, 4, 5, 6, 78, 125, 200 }; - - var nugetHashFileContents = Convert.ToBase64String(sampleHash); var mockFileSystem = new MockFileSystem(new Dictionary { - { XFS.Path(@"c:\nugetcache\testpackage\1.0.0\testpackage.nuspec"), new MockFileData(nuspecFileContents) }, - { XFS.Path(@"c:\nugetcache\testpackage\1.0.0\testpackage.1.0.0.nupkg.sha512"), new MockFileData(nugetHashFileContents) }, + { XFS.Path($@"c:\nugetcache\testpackage\{normalizedVersion}\testpackage.nuspec"), new MockFileData(nuspecFileContents) }, }); + var nugetService = new NugetV3Service(null, mockFileSystem, new List { XFS.Path(@"c:\nugetcache") }, new Mock().Object, new NullLogger(), false); - var component = await nugetService.GetComponentAsync("testpackage", "1.0.0", Component.ComponentScope.Required).ConfigureAwait(false); + var component = await nugetService.GetComponentAsync("testpackage", rawVersion, Component.ComponentScope.Required).ConfigureAwait(false); - Assert.Equal(Hash.HashAlgorithm.SHA_512, component.Hashes[0].Alg); - Assert.Equal(BitConverter.ToString(sampleHash).Replace("-", string.Empty), component.Hashes[0].Content); + Assert.Equal("testpackage", component.Name); + Assert.Equal(rawVersion, component.Version); } - [Fact] - public async Task GetComponent_FromCachedNugetFile_ReturnsComponentWithHash() + [Theory] + [MemberData(nameof(VersionNormalization))] + public async Task GetComponent_FromCachedNugetFile_ReturnsComponentWithHashUsingNormalizedVersion(string rawVersion, string normalizedVersion) { var nuspecFileContents = @" @@ -122,8 +137,8 @@ public async Task GetComponent_FromCachedNugetFile_ReturnsComponentWithHash() var nugetFileContent = "FooBarBaz"; var mockFileSystem = new MockFileSystem(new Dictionary { - { XFS.Path(@"c:\nugetcache\testpackage\1.0.0\testpackage.nuspec"), new MockFileData(nuspecFileContents) }, - { XFS.Path(@"c:\nugetcache\testpackage\1.0.0\testpackage.1.0.0.nupkg"), new MockFileData(nugetFileContent) }, + { XFS.Path($@"c:\nugetcache\testpackage\{normalizedVersion}\testpackage.nuspec"), new MockFileData(nuspecFileContents) }, + { XFS.Path($@"c:\nugetcache\testpackage\{normalizedVersion}\testpackage.{normalizedVersion}.nupkg"), new MockFileData(nugetFileContent) }, }); var nugetService = new NugetV3Service(null, @@ -132,7 +147,7 @@ public async Task GetComponent_FromCachedNugetFile_ReturnsComponentWithHash() new Mock().Object, new NullLogger(), false); - var component = await nugetService.GetComponentAsync("testpackage", "1.0.0", Component.ComponentScope.Required).ConfigureAwait(false); + var component = await nugetService.GetComponentAsync("testpackage", $"{rawVersion}", Component.ComponentScope.Required).ConfigureAwait(false); byte[] hashBytes; using (SHA512 sha = SHA512.Create()) @@ -366,5 +381,64 @@ public async Task GetComponent_GitHubLicenseLookup_FromRepository_WhenLicenseInv Assert.Single(component.Licenses); Assert.Equal("LicenseId", component.Licenses.First().License.Id); } + + [Fact] + public async Task GetComponent_SingleLicenseExpression_ReturnsComponent() + { + var nuspecFileContents = @" + + + testpackage + Apache-2.0 + + "; + var mockFileSystem = new MockFileSystem(new Dictionary + { + { XFS.Path(@"c:\nugetcache\testpackage\1.0.0\testpackage.nuspec"), new MockFileData(nuspecFileContents) }, + }); + + var mockGitHubService = new Mock(); + + var nugetService = new NugetV3Service(null, + mockFileSystem, + new List { XFS.Path(@"c:\nugetcache") }, + mockGitHubService.Object, + new NullLogger(), false); + + var component = await nugetService.GetComponentAsync("testpackage", "1.0.0", Component.ComponentScope.Required).ConfigureAwait(false); + + Assert.Single(component.Licenses); + Assert.Equal("Apache-2.0", component.Licenses.First().License.Id); + } + + [Fact] + public async Task GetComponent_MultiLicenseExpression_ReturnsComponent() + { + var nuspecFileContents = @" + + + testpackage + Apache-2.0 OR MPL-2.0 + + "; + var mockFileSystem = new MockFileSystem(new Dictionary + { + { XFS.Path(@"c:\nugetcache\testpackage\1.0.0\testpackage.nuspec"), new MockFileData(nuspecFileContents) }, + }); + + var mockGitHubService = new Mock(); + + var nugetService = new NugetV3Service(null, + mockFileSystem, + new List { XFS.Path(@"c:\nugetcache") }, + mockGitHubService.Object, + new NullLogger(), false); + + var component = await nugetService.GetComponentAsync("testpackage", "1.0.0", Component.ComponentScope.Required).ConfigureAwait(false); + + Assert.Equal(2, component.Licenses.Count); + Assert.Contains(component.Licenses, choice => choice.License.Id.Equals("Apache-2.0")); + Assert.Contains(component.Licenses, choice => choice.License.Id.Equals("MPL-2.0")); + } } } diff --git a/CycloneDX.Tests/ProgramTests.cs b/CycloneDX.Tests/ProgramTests.cs index 084211b4..06f09248 100755 --- a/CycloneDX.Tests/ProgramTests.cs +++ b/CycloneDX.Tests/ProgramTests.cs @@ -55,7 +55,7 @@ public async Task CallingCycloneDX_CreatesOutputDirectory() }); var mockSolutionFileService = new Mock(); mockSolutionFileService - .Setup(s => s.GetSolutionNugetPackages(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.GetSolutionNugetPackages(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new HashSet()); Program.fileSystem = mockFileSystem; Program.solutionFileService = mockSolutionFileService.Object; @@ -80,7 +80,7 @@ public async Task CallingCycloneDX_WithOutputFilename_CreatesOutputFilename() }); var mockSolutionFileService = new Mock(); mockSolutionFileService - .Setup(s => s.GetSolutionNugetPackages(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.GetSolutionNugetPackages(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new HashSet()); Program.fileSystem = mockFileSystem; Program.solutionFileService = mockSolutionFileService.Object; diff --git a/CycloneDX.Tests/ProjectAssetsFileServiceTests.cs b/CycloneDX.Tests/ProjectAssetsFileServiceTests.cs index 75f18a6b..372babf4 100644 --- a/CycloneDX.Tests/ProjectAssetsFileServiceTests.cs +++ b/CycloneDX.Tests/ProjectAssetsFileServiceTests.cs @@ -24,6 +24,7 @@ using Moq; using CycloneDX.Models; using CycloneDX.Services; +using NuGet.LibraryModel; using NuGet.ProjectModel; using NuGet.Packaging.Core; using NuGet.Versioning; @@ -55,6 +56,13 @@ public void GetNugetPackages_PackageAsTopLevelAndTransitive(string framework, in Name = "Package2", Version = "4.5.1", Dependencies = new Dictionary(), + }, + new NugetPackage + { + Name = "Package3", + Version = "1.0.0", + Dependencies = new Dictionary(), + IsDevDependency = true } }) }, @@ -68,19 +76,21 @@ public void GetNugetPackages_PackageAsTopLevelAndTransitive(string framework, in { ("Package1", new[]{ ("Package1", "1.5.0") }), ("Package2", new[]{ ("Package2", "4.5.1") }), + ("Package3", new[]{ ("Package3", "1.0.0") }), })); var mockAssetReader = new Mock(); mockAssetReader .Setup(m => m.Read(It.IsAny())) .Returns(() => { + var nuGetFramework = new NuGet.Frameworks.NuGetFramework(framework, new Version(frameworkMajor, frameworkMinor, 0)); return new LockFile { Targets = new[] { new LockFileTarget { - TargetFramework = new NuGet.Frameworks.NuGetFramework(framework, new Version(frameworkMajor, frameworkMinor, 0)), + TargetFramework = nuGetFramework, RuntimeIdentifier = "", Libraries = new[] { @@ -106,15 +116,117 @@ public void GetNugetPackages_PackageAsTopLevelAndTransitive(string framework, in new LockFileItem("Package2.dll") }, Dependencies = new PackageDependency[0] + }, + new LockFileTargetLibrary + { + Name = "Package3", + Version = new NuGet.Versioning.NuGetVersion("1.0.0"), + CompileTimeAssemblies = new[] + { + new LockFileItem("Package3.dll") + }, + Dependencies = new PackageDependency[0] + } + } + }, + new LockFileTarget + { + TargetFramework = nuGetFramework, + RuntimeIdentifier = "win-x64", + Libraries = new[] + { + new LockFileTargetLibrary + { + Name = "Package1", + Version = new NuGet.Versioning.NuGetVersion("1.5.0"), + CompileTimeAssemblies = new[] + { + new LockFileItem("Package1.dll") + }, + Dependencies = new[] + { + new PackageDependency("Package2", new VersionRange(minVersion: new NuGetVersion("4.5.0"), originalString:"[4.5, )")) + } + }, + new LockFileTargetLibrary + { + Name = "Package2", + Version = new NuGet.Versioning.NuGetVersion("4.5.1"), + CompileTimeAssemblies = new[] + { + new LockFileItem("Package2.dll") + }, + Dependencies = new PackageDependency[0] + }, + new LockFileTargetLibrary + { + Name = "Package3", + Version = new NuGet.Versioning.NuGetVersion("1.0.0"), + CompileTimeAssemblies = new[] + { + new LockFileItem("Package3.dll") + }, + Dependencies = new PackageDependency[0] + } + } + } + }, + PackageSpec = new PackageSpec + { + TargetFrameworks = + { + new TargetFrameworkInformation + { + FrameworkName = nuGetFramework, + TargetAlias = nuGetFramework.Framework, + Dependencies = new List + { + new() + { + SuppressParent = LibraryIncludeFlagUtils.DefaultSuppressParent, + ReferenceType = LibraryDependencyReferenceType.Direct, + LibraryRange = new LibraryRange + { + Name = "Package1", + VersionRange = VersionRange.Parse("1.5.0"), + TypeConstraint = LibraryDependencyTarget.Package + } + }, + new() + { + SuppressParent = LibraryIncludeFlagUtils.DefaultSuppressParent, + ReferenceType = LibraryDependencyReferenceType.Direct, + LibraryRange = new LibraryRange + { + Name = "Package2", + VersionRange = VersionRange.Parse("4.5.1"), + TypeConstraint = LibraryDependencyTarget.Package + } + }, + new() + { + SuppressParent = LibraryIncludeFlags.All, + ReferenceType = LibraryDependencyReferenceType.Direct, + LibraryRange = new LibraryRange + { + Name = "Package3", + VersionRange = VersionRange.Parse("1.0.0"), + TypeConstraint = LibraryDependencyTarget.Package + } + } } } } } }; }); + mockAssetReader.Setup(m => m.ReadAllText(It.IsAny())).Returns(() => + { + return "empty"; + }); var projectAssetsFileService = new ProjectAssetsFileService(mockFileSystem, mockDotnetCommandsService.Object, () => mockAssetReader.Object); - var packages = projectAssetsFileService.GetNugetPackages(XFS.Path(@"c:\SolutionPath\Project1\Project1.csproj"), XFS.Path(@"c:\SolutionPath\Project1\obj\project.assets.json"), false); + var packages = projectAssetsFileService.GetNugetPackages(XFS.Path(@"c:\SolutionPath\Project1\Project1.csproj"), XFS.Path(@"c:\SolutionPath\Project1\obj\project.assets.json"), false, false); var sortedPackages = new List(packages); sortedPackages.Sort(); @@ -137,7 +249,16 @@ public void GetNugetPackages_PackageAsTopLevelAndTransitive(string framework, in Assert.Equal(@"4.5.1", item.Version); Assert.True(item.IsDirectReference, "Package2 was expected to be a direct reference."); Assert.Empty(item.Dependencies); - }); + }, + item => + { + Assert.Equal(@"Package3", item.Name); + Assert.Equal(@"1.0.0", item.Version); + Assert.True(item.IsDirectReference, "Package3 was expected to be a direct reference."); + Assert.True(item.IsDevDependency, "Package3 was expected to be a development reference."); + Assert.Empty(item.Dependencies); + } + ); } [Theory] @@ -170,18 +291,19 @@ public void GetNugetPackages_MissingResolvedPackageVersion(string framework, int { ("Package1", new[]{ ("Package1", "1.5.0") }), })); - var mockAssetReader = new Mock(); + var mockAssetReader = new Mock(MockBehavior.Strict); mockAssetReader .Setup(m => m.Read(It.IsAny())) .Returns(() => { + var nuGetFramework = new NuGet.Frameworks.NuGetFramework(framework, new Version(frameworkMajor, frameworkMinor, 0)); return new LockFile { Targets = new[] { new LockFileTarget { - TargetFramework = new NuGet.Frameworks.NuGetFramework(framework, new Version(frameworkMajor, frameworkMinor, 0)), + TargetFramework = nuGetFramework, RuntimeIdentifier = "", Libraries = new[] { @@ -199,13 +321,63 @@ public void GetNugetPackages_MissingResolvedPackageVersion(string framework, int } } } + }, + new LockFileTarget + { + TargetFramework = nuGetFramework, + RuntimeIdentifier = "win-x64", + Libraries = new[] + { + new LockFileTargetLibrary + { + Name = "Package1", + Version = new NuGet.Versioning.NuGetVersion("1.5.0"), + CompileTimeAssemblies = new[] + { + new LockFileItem("Package1.dll") + }, + Dependencies = new[] + { + new PackageDependency("Package2", new VersionRange(minVersion: new NuGetVersion("4.5.0"), originalString:"[4.5, )")) + } + } + } + } + }, + PackageSpec = new PackageSpec + { + TargetFrameworks = + { + new TargetFrameworkInformation + { + FrameworkName = nuGetFramework, + TargetAlias = nuGetFramework.Framework, + Dependencies = new List + { + new() + { + SuppressParent = LibraryIncludeFlagUtils.DefaultSuppressParent, + ReferenceType = LibraryDependencyReferenceType.Direct, + LibraryRange = new LibraryRange + { + Name = "Package1", + VersionRange = VersionRange.Parse("1.5.0"), + TypeConstraint = LibraryDependencyTarget.Package + } + } + } + } } } }; }); + mockAssetReader.Setup(m => m.ReadAllText(It.IsAny())).Returns(() => + { + return "empty"; + }); var projectAssetsFileService = new ProjectAssetsFileService(mockFileSystem, mockDotnetCommandsService.Object, () => mockAssetReader.Object); - var packages = projectAssetsFileService.GetNugetPackages(XFS.Path(@"c:\SolutionPath\Project1\Project1.csproj"), XFS.Path(@"c:\SolutionPath\Project1\obj\project.assets.json"), false); + var packages = projectAssetsFileService.GetNugetPackages(XFS.Path(@"c:\SolutionPath\Project1\Project1.csproj"), XFS.Path(@"c:\SolutionPath\Project1\obj\project.assets.json"), false, false); var sortedPackages = new List(packages); sortedPackages.Sort(); @@ -223,5 +395,123 @@ public void GetNugetPackages_MissingResolvedPackageVersion(string framework, int }); }); } + + [Theory] + [InlineData(".NetStandard", 2, 1)] + [InlineData("net", 6, 0)] + public void GetNugetPackages_MissingDependencies(string framework, int frameworkMajor, int frameworkMinor) + { + var mockFileSystem = new MockFileSystem(new Dictionary + { + { XFS.Path(@"c:\SolutionPath\Project1\Project1.csproj"), Helpers.GetProjectFileWithPackageReferences( + new[] { + new NugetPackage + { + Name = "Package1", + Version = "1.5.0", + } + }) + }, + { XFS.Path(@"c:\SolutionPath\Project1\obj\project.assets.json"), new MockFileData("") + } + }); + var mockDotnetCommandsService = new Mock(); + mockDotnetCommandsService.Setup(m => m.Run(It.IsAny())) + .Returns(() => Helpers.GetDotnetListPackagesResult( + new[] + { + ("Package1", new[]{ ("Package1", "1.5.0") }), + })); + var mockAssetReader = new Mock(MockBehavior.Strict); + mockAssetReader + .Setup(m => m.Read(It.IsAny())) + .Returns(() => + { + var nuGetFramework = new NuGet.Frameworks.NuGetFramework(framework, new Version(frameworkMajor, frameworkMinor, 0)); + return new LockFile + { + Targets = new[] + { + new LockFileTarget + { + TargetFramework = nuGetFramework, + RuntimeIdentifier = "", + Libraries = new[] + { + new LockFileTargetLibrary + { + Name = "Package1", + Version = new NuGet.Versioning.NuGetVersion("1.5.0"), + CompileTimeAssemblies = new[] + { + new LockFileItem("Package1.dll") + } + } + } + }, + new LockFileTarget + { + TargetFramework = nuGetFramework, + RuntimeIdentifier = "win-x64", + Libraries = new[] + { + new LockFileTargetLibrary + { + Name = "Package1", + Version = new NuGet.Versioning.NuGetVersion("1.5.0"), + CompileTimeAssemblies = new[] + { + new LockFileItem("Package1.dll") + } + } + } + } + }, + PackageSpec = new PackageSpec + { + TargetFrameworks = + { + new TargetFrameworkInformation + { + FrameworkName = nuGetFramework, + TargetAlias = nuGetFramework.Framework, + Dependencies = new List + { + new() + { + SuppressParent = LibraryIncludeFlagUtils.DefaultSuppressParent, + ReferenceType = LibraryDependencyReferenceType.Direct, + LibraryRange = new LibraryRange + { + Name = "Package1", + VersionRange = VersionRange.Parse("1.5.0"), + TypeConstraint = LibraryDependencyTarget.Package + } + } + } + } + } + } + }; + }); + mockAssetReader.Setup(m => m.ReadAllText(It.IsAny())).Returns(() => + { + return "empty"; + }); + + var projectAssetsFileService = new ProjectAssetsFileService(mockFileSystem, mockDotnetCommandsService.Object, () => mockAssetReader.Object); + var packages = projectAssetsFileService.GetNugetPackages(XFS.Path(@"c:\SolutionPath\Project1\Project1.csproj"), XFS.Path(@"c:\SolutionPath\Project1\obj\project.assets.json"), false, false); + var sortedPackages = new List(packages); + sortedPackages.Sort(); + + Assert.Collection(sortedPackages, + item => + { + Assert.Equal(@"Package1", item.Name); + Assert.Equal(@"1.5.0", item.Version); + Assert.True(item.IsDirectReference, "Package1 was expected to be a direct reference."); + Assert.Empty(item.Dependencies); + }); + } } } diff --git a/CycloneDX.Tests/ProjectFileServiceTests.cs b/CycloneDX.Tests/ProjectFileServiceTests.cs index b730cbf5..4a1d06b9 100755 --- a/CycloneDX.Tests/ProjectFileServiceTests.cs +++ b/CycloneDX.Tests/ProjectFileServiceTests.cs @@ -68,7 +68,7 @@ public async Task GetProjectNugetPackages_WithProjectAssetsFile_ReturnsNugetPack var mockPackageFileService = new Mock(); var mockProjectAssetsFileService = new Mock(); mockProjectAssetsFileService - .Setup(s => s.GetNugetPackages(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.GetNugetPackages(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new HashSet { new NugetPackage { Name = "Package", Version = "1.2.3" }, @@ -79,7 +79,7 @@ public async Task GetProjectNugetPackages_WithProjectAssetsFile_ReturnsNugetPack mockPackageFileService.Object, mockProjectAssetsFileService.Object); - var packages = await projectFileService.GetProjectNugetPackagesAsync(XFS.Path(@"c:\Project\Project.csproj"), "", false, "", "").ConfigureAwait(false); + var packages = await projectFileService.GetProjectNugetPackagesAsync(XFS.Path(@"c:\Project\Project.csproj"), "", false, false, "", "").ConfigureAwait(false); Assert.Collection(packages, item => { @@ -103,7 +103,7 @@ public async Task GetProjectNugetPackages_WithProjectAssetsFileWithoutRestore_Re var mockPackageFileService = new Mock(); var mockProjectAssetsFileService = new Mock(); mockProjectAssetsFileService - .Setup(s => s.GetNugetPackages(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.GetNugetPackages(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new HashSet { new NugetPackage { Name = "Package", Version = "1.2.3" }, @@ -115,7 +115,7 @@ public async Task GetProjectNugetPackages_WithProjectAssetsFileWithoutRestore_Re mockProjectAssetsFileService.Object); projectFileService.DisablePackageRestore = true; - var packages = await projectFileService.GetProjectNugetPackagesAsync(XFS.Path(@"c:\Project\Project.csproj"), "", false, "", "").ConfigureAwait(false); + var packages = await projectFileService.GetProjectNugetPackagesAsync(XFS.Path(@"c:\Project\Project.csproj"), "", false, false, "", "").ConfigureAwait(false); Assert.Collection(packages, item => { @@ -139,7 +139,7 @@ public async Task GetProjectNugetPackages_WithProjectAssetsFile_ReturnsMultipleN var mockPackageFileService = new Mock(); var mockProjectAssetsFileService = new Mock(); mockProjectAssetsFileService - .Setup(s => s.GetNugetPackages(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.GetNugetPackages(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new HashSet { new NugetPackage { Name = "Package1", Version = "1.2.3" }, @@ -152,7 +152,7 @@ public async Task GetProjectNugetPackages_WithProjectAssetsFile_ReturnsMultipleN mockPackageFileService.Object, mockProjectAssetsFileService.Object); - var packages = await projectFileService.GetProjectNugetPackagesAsync(XFS.Path(@"c:\Project\Project.csproj"), "", false, "", "").ConfigureAwait(false); + var packages = await projectFileService.GetProjectNugetPackagesAsync(XFS.Path(@"c:\Project\Project.csproj"), "", false, false, "", "").ConfigureAwait(false); var sortedPackages = new List(packages); sortedPackages.Sort(); @@ -185,7 +185,7 @@ public async Task GetProjectNugetPackages_WithPackagesConfig_ReturnsNugetPackage ); var mockProjectAssetsFileService = new Mock(); mockProjectAssetsFileService - .Setup(s => s.GetNugetPackages(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.GetNugetPackages(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new HashSet()); var projectFileService = new ProjectFileService( mockFileSystem, @@ -193,7 +193,7 @@ public async Task GetProjectNugetPackages_WithPackagesConfig_ReturnsNugetPackage mockPackageFileService.Object, mockProjectAssetsFileService.Object); - var packages = await projectFileService.GetProjectNugetPackagesAsync(XFS.Path(@"c:\Project\Project.csproj"), "", false, "", "").ConfigureAwait(false); + var packages = await projectFileService.GetProjectNugetPackagesAsync(XFS.Path(@"c:\Project\Project.csproj"), "", false, false, "", "").ConfigureAwait(false); Assert.Collection(packages, item => { @@ -227,7 +227,7 @@ public async Task GetProjectNugetPackages_WithPackagesConfig_ReturnsMultipleNuge ); var mockProjectAssetsFileService = new Mock(); mockProjectAssetsFileService - .Setup(s => s.GetNugetPackages(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.GetNugetPackages(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new HashSet()); var projectFileService = new ProjectFileService( mockFileSystem, @@ -235,7 +235,7 @@ public async Task GetProjectNugetPackages_WithPackagesConfig_ReturnsMultipleNuge mockPackageFileService.Object, mockProjectAssetsFileService.Object); - var packages = await projectFileService.GetProjectNugetPackagesAsync(XFS.Path(@"c:\Project\Project.csproj"), "", false, "", "").ConfigureAwait(false); + var packages = await projectFileService.GetProjectNugetPackagesAsync(XFS.Path(@"c:\Project\Project.csproj"), "", false, false, "", "").ConfigureAwait(false); var sortedPackages = new List(packages); sortedPackages.Sort(); diff --git a/CycloneDX.sln b/CycloneDX.sln index 78d8a988..aa67b466 100644 --- a/CycloneDX.sln +++ b/CycloneDX.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30524.135 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33103.201 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CycloneDX", "CycloneDX\CycloneDX.csproj", "{88DFA76C-1C0A-4A83-AA48-EA1D28A9ABED}" EndProject @@ -10,6 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig build.sh = build.sh Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props Dockerfile = Dockerfile LICENSE = LICENSE nuget.config = nuget.config diff --git a/CycloneDX/CycloneDX.csproj b/CycloneDX/CycloneDX.csproj index 3f92018b..f9a26624 100644 --- a/CycloneDX/CycloneDX.csproj +++ b/CycloneDX/CycloneDX.csproj @@ -2,7 +2,6 @@ Exe - net6.0;netcoreapp3.1 CycloneDX true A .NET Core global tool to generate CycloneDX bill-of-material documents for use with Software Composition Analysis (SCA). @@ -10,6 +9,7 @@ true dotnet-CycloneDX <_SkipUpgradeNetAnalyzersNuGetWarning>true + net7.0;net6.0 @@ -23,14 +23,14 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + + + + + diff --git a/CycloneDX/Interfaces/IAssetFileReader.cs b/CycloneDX/Interfaces/IAssetFileReader.cs index ce27b9f6..84f46d62 100644 --- a/CycloneDX/Interfaces/IAssetFileReader.cs +++ b/CycloneDX/Interfaces/IAssetFileReader.cs @@ -15,6 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using System.IO; using NuGet.ProjectModel; namespace CycloneDX.Interfaces @@ -22,5 +23,9 @@ namespace CycloneDX.Interfaces public interface IAssetFileReader { LockFile Read(string filePath); + string ReadAllText(string filePath) + { + return File.ReadAllText(filePath); + } } } diff --git a/CycloneDX/Interfaces/IJsonDocs .cs b/CycloneDX/Interfaces/IJsonDocs .cs new file mode 100644 index 00000000..38e16305 --- /dev/null +++ b/CycloneDX/Interfaces/IJsonDocs .cs @@ -0,0 +1,27 @@ +// This file is part of CycloneDX Tool for .NET +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +using System.Text.Json; + +namespace CycloneDX.Interfaces +{ + public interface IJsonDocs + { + public JsonDocument Parse(string json); + + } +} diff --git a/CycloneDX/Interfaces/IProjectAssetsFileService.cs b/CycloneDX/Interfaces/IProjectAssetsFileService.cs index 9900c8e3..732fd974 100644 --- a/CycloneDX/Interfaces/IProjectAssetsFileService.cs +++ b/CycloneDX/Interfaces/IProjectAssetsFileService.cs @@ -22,6 +22,6 @@ namespace CycloneDX.Interfaces { public interface IProjectAssetsFileService { - HashSet GetNugetPackages(string projectFilePath, string projectAssetsFilePath, bool IsTestProject); + HashSet GetNugetPackages(string projectFilePath, string projectAssetsFilePath, bool IsTestProject, bool excludeDev); } } diff --git a/CycloneDX/Interfaces/IProjectFileService.cs b/CycloneDX/Interfaces/IProjectFileService.cs index 91267138..9b56e8eb 100755 --- a/CycloneDX/Interfaces/IProjectFileService.cs +++ b/CycloneDX/Interfaces/IProjectFileService.cs @@ -24,8 +24,8 @@ namespace CycloneDX.Interfaces public interface IProjectFileService { bool DisablePackageRestore { get; set; } - Task> GetProjectNugetPackagesAsync(string projectFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, string framework, string runtime); - Task> RecursivelyGetProjectNugetPackagesAsync(string projectFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, string framework, string runtime); + Task> GetProjectNugetPackagesAsync(string projectFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, bool excludeDev, string framework, string runtime); + Task> RecursivelyGetProjectNugetPackagesAsync(string projectFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, bool excludeDev, string framework, string runtime); Task> GetProjectReferencesAsync(string projectFilePath); Task> RecursivelyGetProjectReferencesAsync(string projectFilePath); } diff --git a/CycloneDX/Interfaces/ISolutionFileService.cs b/CycloneDX/Interfaces/ISolutionFileService.cs index c06b0782..3690e729 100755 --- a/CycloneDX/Interfaces/ISolutionFileService.cs +++ b/CycloneDX/Interfaces/ISolutionFileService.cs @@ -24,6 +24,6 @@ namespace CycloneDX.Interfaces public interface ISolutionFileService { Task> GetSolutionProjectReferencesAsync(string solutionFilePath); - Task> GetSolutionNugetPackages(string solutionFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, string framework, string runtime); + Task> GetSolutionNugetPackages(string solutionFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, bool excludeDev, string framework, string runtime); } } diff --git a/CycloneDX/Models/NugetPackage.cs b/CycloneDX/Models/NugetPackage.cs index 4fcdf47e..b3dc0d79 100644 --- a/CycloneDX/Models/NugetPackage.cs +++ b/CycloneDX/Models/NugetPackage.cs @@ -27,6 +27,7 @@ public class NugetPackage : IComparable public string Name { get; set; } public string Version { get; set; } public bool IsDirectReference { get; set; } + public bool IsDevDependency { get; set; } public Component.ComponentScope? Scope { get; set; } public Dictionary Dependencies { get; set; } //key: name ~ value: version diff --git a/CycloneDX/Program.cs b/CycloneDX/Program.cs index 8f402926..aadd888e 100755 --- a/CycloneDX/Program.cs +++ b/CycloneDX/Program.cs @@ -240,17 +240,17 @@ async Task OnExecuteAsync(CommandLineApplication app) { { if (SolutionOrProjectFile.ToLowerInvariant().EndsWith(".sln", StringComparison.OrdinalIgnoreCase)) { - packages = await solutionFileService.GetSolutionNugetPackages(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, framework, runtime).ConfigureAwait(false); + packages = await solutionFileService.GetSolutionNugetPackages(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, excludeDev, framework, runtime).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetFileNameWithoutExtension(SolutionOrProjectFile); } else if (Utils.IsSupportedProjectType(SolutionOrProjectFile) && scanProjectReferences) { - packages = await projectFileService.RecursivelyGetProjectNugetPackagesAsync(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, framework, runtime).ConfigureAwait(false); + packages = await projectFileService.RecursivelyGetProjectNugetPackagesAsync(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, excludeDev, framework, runtime).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetFileNameWithoutExtension(SolutionOrProjectFile); } else if (Utils.IsSupportedProjectType(SolutionOrProjectFile)) { - packages = await projectFileService.GetProjectNugetPackagesAsync(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, framework, runtime).ConfigureAwait(false); + packages = await projectFileService.GetProjectNugetPackagesAsync(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, excludeDev, framework, runtime).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetFileNameWithoutExtension(SolutionOrProjectFile); } else if (Program.fileSystem.Path.GetFileName(SolutionOrProjectFile).ToLowerInvariant().Equals("packages.config", StringComparison.OrdinalIgnoreCase)) diff --git a/CycloneDX/Services/JsonDocs.cs b/CycloneDX/Services/JsonDocs.cs new file mode 100644 index 00000000..043fe4e8 --- /dev/null +++ b/CycloneDX/Services/JsonDocs.cs @@ -0,0 +1,32 @@ +// This file is part of CycloneDX Tool for .NET +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +using System.Text.Json; +using CycloneDX.Interfaces; + +namespace CycloneDX.Services +{ + public class JsonDocs : IJsonDocs + { + + public JsonDocument Parse(string json) + { + return JsonDocument.Parse(json); + + } + } +} diff --git a/CycloneDX/Services/NugetV3Service.cs b/CycloneDX/Services/NugetV3Service.cs index 11d0fda5..4dde3c55 100644 --- a/CycloneDX/Services/NugetV3Service.cs +++ b/CycloneDX/Services/NugetV3Service.cs @@ -84,7 +84,7 @@ internal string GetCachedNuspecFilename(string name, string version) foreach (var packageCachePath in _packageCachePaths) { - var currentDirectory = _fileSystem.Path.Combine(packageCachePath, lowerName, version); + var currentDirectory = _fileSystem.Path.Combine(packageCachePath, lowerName, NormalizeVersion(version)); var currentFilename = _fileSystem.Path.Combine(currentDirectory, lowerName + _nuspecExtension); if (_fileSystem.File.Exists(currentFilename)) { @@ -96,6 +96,24 @@ internal string GetCachedNuspecFilename(string name, string version) return nuspecFilename; } + /// + /// Normalize the version string according to + /// https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#normalized-version-numbers + /// + private string NormalizeVersion(string version) + { + var separator = Math.Max(version.IndexOf('-'), version.IndexOf('+')); + var part1 = separator < 0 ? version : version.Substring(0, separator); + var part2 = separator < 0 ? string.Empty : version.Substring(separator); + if (Version.TryParse(part1, out var parsed) && parsed.Revision == 0) + { + part1 = parsed.ToString(3); + version = part1 + part2; + } + + return version; + } + private SourceRepository SetupNugetRepository(NugetInputModel nugetInput) { if (nugetInput == null || string.IsNullOrEmpty(nugetInput.nugetFeedUrl) || @@ -153,9 +171,7 @@ public async Task GetComponentAsync(string name, string version, Comp var component = SetupComponent(name, version, scope); var nuspecFilename = GetCachedNuspecFilename(name, version); - var nuspecModel = await GetNuspec(name, version, nuspecFilename, resource).ConfigureAwait(false); - if (nuspecModel.hashBytes != null) { var hex = BitConverter.ToString(nuspecModel.hashBytes).Replace("-", string.Empty); @@ -170,10 +186,11 @@ public async Task GetComponentAsync(string name, string version, Comp var licenseMetadata = nuspecModel.nuspecReader.GetLicenseMetadata(); if (licenseMetadata != null && licenseMetadata.Type == LicenseType.Expression) { - Action licenseProcessor = delegate(NuGetLicense nugetLicense) + Action licenseProcessor = delegate (NuGetLicense nugetLicense) { var license = new License { Id = nugetLicense.Identifier, Name = nugetLicense.Identifier }; - component.Licenses = new List { new LicenseChoice { License = license } }; + component.Licenses ??= new List(); + component.Licenses.Add(new LicenseChoice { License = license }); }; licenseMetadata.LicenseExpression.OnEachLeafNode(licenseProcessor, null); } @@ -231,7 +248,8 @@ public async Task GetComponentAsync(string name, string version, Comp { var externalReference = new ExternalReference { - Type = ExternalReference.ExternalReferenceType.Website, Url = projectUrl + Type = ExternalReference.ExternalReferenceType.Website, + Url = projectUrl }; component.ExternalReferences = new List { externalReference }; } @@ -243,7 +261,8 @@ public async Task GetComponentAsync(string name, string version, Comp { var externalReference = new ExternalReference { - Type = ExternalReference.ExternalReferenceType.Vcs, Url = vcsUrl + Type = ExternalReference.ExternalReferenceType.Vcs, + Url = vcsUrl }; if (null == component.ExternalReferences) { @@ -260,7 +279,7 @@ public async Task GetComponentAsync(string name, string version, Comp private static Component SetupComponentProperties(Component component, NuspecModel nuspecModel) { - component.Publisher = nuspecModel.nuspecReader.GetAuthors(); + component.Author = nuspecModel.nuspecReader.GetAuthors(); component.Copyright = nuspecModel.nuspecReader.GetCopyright(); // this prevents empty copyright values in the JSON BOM if (string.IsNullOrEmpty(component.Copyright)) @@ -301,7 +320,6 @@ await resource.CopyNupkgToStreamAsync(name, packageVersion, packageStream, _sour using PackageArchiveReader packageReader = new PackageArchiveReader(packageStream); nuspecModel.nuspecReader = await packageReader.GetNuspecReaderAsync(_cancellationToken); - if (!_disableHashComputation) { nuspecModel.hashBytes = ComputeSha215Hash(packageStream); @@ -322,8 +340,9 @@ await resource.CopyNupkgToStreamAsync(name, packageVersion, packageStream, _sour // ├─..nupkg.sha512 // └─.nuspec - string shaFilename = Path.ChangeExtension(nuspecFilename, version + _sha512Extension); - string nupkgFilename = Path.ChangeExtension(nuspecFilename, version + _nupkgExtension); + var normalizedVersion = NormalizeVersion(version); + string shaFilename = Path.ChangeExtension(nuspecFilename, normalizedVersion + _sha512Extension); + string nupkgFilename = Path.ChangeExtension(nuspecFilename, normalizedVersion + _nupkgExtension); if (_fileSystem.File.Exists(shaFilename)) { diff --git a/CycloneDX/Services/ProjectAssetsFileService.cs b/CycloneDX/Services/ProjectAssetsFileService.cs index 1b0f29b1..50e9ae9e 100644 --- a/CycloneDX/Services/ProjectAssetsFileService.cs +++ b/CycloneDX/Services/ProjectAssetsFileService.cs @@ -22,6 +22,8 @@ using System.Linq; using CycloneDX.Interfaces; using NuGet.Versioning; +using NuGet.LibraryModel; +using NuGet.ProjectModel; namespace CycloneDX.Services { @@ -38,7 +40,7 @@ public ProjectAssetsFileService(IFileSystem fileSystem, IDotnetCommandService do _assetFileReaderFactory = assetFileReaderFactory; } - public HashSet GetNugetPackages(string projectFilePath, string projectAssetsFilePath, bool isTestProject) + public HashSet GetNugetPackages(string projectFilePath, string projectAssetsFilePath, bool isTestProject, bool excludeDev) { var packages = new HashSet(); @@ -49,34 +51,27 @@ public HashSet GetNugetPackages(string projectFilePath, string pro foreach (var targetRuntime in assetsFile.Targets) { - var directPackageDependencies = GetDirectPackageDependencies(targetRuntime.Name, projectFilePath); var runtimePackages = new HashSet(); + var targetFramework = assetsFile.PackageSpec.GetTargetFramework(targetRuntime.TargetFramework); + var dependencies = targetFramework.Dependencies; + foreach (var library in targetRuntime.Libraries.Where(lib => lib.Type != "project")) { + var libs = dependencies.FirstOrDefault(ld => ld.Name.Equals(library.Name)); var package = new NugetPackage { Name = library.Name, Version = library.Version.ToNormalizedString(), Scope = Component.ComponentScope.Required, Dependencies = new Dictionary(), + IsDevDependency = SetIsDevDependency(libs), + IsDirectReference = SetIsDirectReference(libs) }; - var topLevelReferenceKey = (package.Name, package.Version); - if (directPackageDependencies.Contains(topLevelReferenceKey)) - { - package.IsDirectReference = true; - } + // is this a test project dependency or only a development dependency if ( isTestProject - || ( - library.CompileTimeAssemblies.Count == 0 - && library.ContentFiles.Count == 0 - && library.EmbedAssemblies.Count == 0 - && library.FrameworkAssemblies.Count == 0 - && library.NativeLibraries.Count == 0 - && library.ResourceAssemblies.Count == 0 - && library.ToolsAssemblies.Count == 0 - ) + || (package.IsDevDependency && excludeDev) ) { package.Scope = Component.ComponentScope.Excluded; @@ -89,7 +84,7 @@ public HashSet GetNugetPackages(string projectFilePath, string pro runtimePackages.Add(package); } - ResolveDependecyVersionRanges(runtimePackages); + ResolveDependencyVersionRanges(runtimePackages); packages.UnionWith(runtimePackages); } @@ -97,58 +92,19 @@ public HashSet GetNugetPackages(string projectFilePath, string pro return packages; } - - // Future: Instead of invoking the dotnet CLI to get direct dependencies, once asset file version 3 is available through the Nuget library, - // The direct dependencies could be retrieved from the asset file json path: .project.frameworks..dependencies - private List<(string, string)> GetDirectPackageDependencies(string targetRuntime, string projectFilePath) + public bool SetIsDirectReference(LibraryDependency dependency) { - var directPackageDependencies = new List<(string, string)>(); - var framework = TargetFrameworkToAlias(targetRuntime); - if (framework != null) - { - var output = _dotnetCommandService.Run($"list \"{projectFilePath}\" package --framework {framework}"); - var result = output.Success ? output.StdOut : null; - if (result != null) - { - directPackageDependencies = result.Split('\r', '\n').Select(line => line.Trim()) - .Where(line => line.StartsWith(">", StringComparison.InvariantCulture)) - .Select(line => line.Split(' ', StringSplitOptions.RemoveEmptyEntries)) - .Where(parts => parts.Length == 4) - .Select(parts => (parts[1], parts[3])) - .ToList(); - } - } - return directPackageDependencies; + return dependency?.ReferenceType == LibraryDependencyReferenceType.Direct; } - - /// - /// Converts an asset file's target framework value into a csproj target framework value. - /// - /// Examples: - /// .NetStandard,Version=V3.1 => netstandard3.1 - /// .NetCoreApp,Version=V3.1 => netcoreapp3.1 - /// netcoreapp3.1 => netcoreapp3.1 - /// net6.0 => net6.0 - /// - private string TargetFrameworkToAlias(string target) + public bool SetIsDevDependency(LibraryDependency dependency) { - if (!string.IsNullOrEmpty(target)) - { - target = target.ToLowerInvariant().TrimStart('.'); - var targetParts = target.Split(",version=v"); - if (targetParts.Length == 2) - { - return string.Join("", targetParts); - } - return targetParts[0]; - } - return null; + return dependency != null && dependency.SuppressParent != LibraryIncludeFlagUtils.DefaultSuppressParent; } /// /// Updates all dependencies with version ranges to the version it was resolved to. /// - private static void ResolveDependecyVersionRanges(HashSet runtimePackages) + private static void ResolveDependencyVersionRanges(HashSet runtimePackages) { var runtimePackagesLookup = runtimePackages.ToLookup(x => x.Name.ToLowerInvariant()); foreach (var runtimePackage in runtimePackages) diff --git a/CycloneDX/Services/ProjectFileService.cs b/CycloneDX/Services/ProjectFileService.cs index d1bf17bf..ac77d9c6 100755 --- a/CycloneDX/Services/ProjectFileService.cs +++ b/CycloneDX/Services/ProjectFileService.cs @@ -102,7 +102,7 @@ static internal String GetProjectProperty(string projectFilePath, string baseInt /// /// /// - public async Task> GetProjectNugetPackagesAsync(string projectFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, string framework, string runtime) + public async Task> GetProjectNugetPackagesAsync(string projectFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, bool excludeDev, string framework, string runtime) { if (!_fileSystem.File.Exists(projectFilePath)) { @@ -142,7 +142,7 @@ public async Task> GetProjectNugetPackagesAsync(string pro { Console.WriteLine($"File not found: \"{assetsFilename}\", \"{projectFilePath}\" "); } - var packages = _projectAssetsFileService.GetNugetPackages(projectFilePath, assetsFilename, isTestProject); + var packages = _projectAssetsFileService.GetNugetPackages(projectFilePath, assetsFilename, isTestProject, excludeDev); // if there are no project file package references look for a packages.config @@ -165,13 +165,13 @@ public async Task> GetProjectNugetPackagesAsync(string pro /// /// /// - public async Task> RecursivelyGetProjectNugetPackagesAsync(string projectFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, string framework, string runtime) + public async Task> RecursivelyGetProjectNugetPackagesAsync(string projectFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, bool excludeDev, string framework, string runtime) { - var nugetPackages = await GetProjectNugetPackagesAsync(projectFilePath, baseIntermediateOutputPath, excludeTestProjects, framework, runtime).ConfigureAwait(false); + var nugetPackages = await GetProjectNugetPackagesAsync(projectFilePath, baseIntermediateOutputPath, excludeTestProjects, excludeDev, framework, runtime).ConfigureAwait(false); var projectReferences = await RecursivelyGetProjectReferencesAsync(projectFilePath).ConfigureAwait(false); foreach (var project in projectReferences) { - var projectNugetPackages = await GetProjectNugetPackagesAsync(project, baseIntermediateOutputPath, excludeTestProjects, framework, runtime).ConfigureAwait(false); + var projectNugetPackages = await GetProjectNugetPackagesAsync(project, baseIntermediateOutputPath, excludeTestProjects, excludeDev, framework, runtime).ConfigureAwait(false); nugetPackages.UnionWith(projectNugetPackages); } return nugetPackages; @@ -195,7 +195,7 @@ public async Task> GetProjectReferencesAsync(string projectFileP Console.WriteLine(" Getting project references"); var projectReferences = new HashSet(); - var projectDirectory = _fileSystem.FileInfo.FromFileName(projectFilePath).Directory.FullName; + var projectDirectory = _fileSystem.FileInfo.New(projectFilePath).Directory.FullName; using (StreamReader fileReader = _fileSystem.File.OpenText(projectFilePath)) { @@ -240,7 +240,7 @@ public async Task> RecursivelyGetProjectReferencesAsync(string p // Initialize the queue with the current project file var files = new Queue(); - files.Enqueue(_fileSystem.FileInfo.FromFileName(projectFilePath).FullName); + files.Enqueue(_fileSystem.FileInfo.New(projectFilePath).FullName); var visitedProjectFiles = new HashSet(); diff --git a/CycloneDX/Services/SolutionFileService.cs b/CycloneDX/Services/SolutionFileService.cs index cf4f998f..3007778e 100755 --- a/CycloneDX/Services/SolutionFileService.cs +++ b/CycloneDX/Services/SolutionFileService.cs @@ -82,7 +82,7 @@ public async Task> GetSolutionProjectReferencesAsync(string solu /// /// /// - public async Task> GetSolutionNugetPackages(string solutionFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, string framework, string runtime) + public async Task> GetSolutionNugetPackages(string solutionFilePath, string baseIntermediateOutputPath, bool excludeTestProjects, bool excludeDev, string framework, string runtime) { if (!_fileSystem.File.Exists(solutionFilePath)) { @@ -113,7 +113,7 @@ public async Task> GetSolutionNugetPackages(string solutio foreach (var projectFilePath in projectQuery) { Console.WriteLine(); - var projectPackages = await _projectFileService.GetProjectNugetPackagesAsync(projectFilePath, baseIntermediateOutputPath, excludeTestProjects, framework, runtime).ConfigureAwait(false); + var projectPackages = await _projectFileService.GetProjectNugetPackagesAsync(projectFilePath, baseIntermediateOutputPath, excludeTestProjects, excludeDev, framework, runtime).ConfigureAwait(false); directReferencePackages.UnionWith(projectPackages.Where(p => p.IsDirectReference)); packages.UnionWith(projectPackages); } diff --git a/Directory.Build.props b/Directory.Build.props index cffe12cf..968b97c5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -19,14 +19,9 @@ AllEnableByDefault - - + true + Steve Springett & Patrick Dwyer diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..52499c36 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,27 @@ + + + true + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dockerfile b/Dockerfile index 8afd6917..d0eaea6d 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0 +FROM mcr.microsoft.com/dotnet/sdk:7.0 ARG VERSION COPY ./nupkgs /tmp/nupkgs/ @@ -6,4 +6,4 @@ RUN dotnet tool install --global CycloneDX --version ${VERSION} --add-source /tm ln -s /root/.dotnet/tools/dotnet-CycloneDX /usr/bin/CycloneDX ENTRYPOINT [ "CycloneDX" ] -CMD [ "--help" ] \ No newline at end of file +CMD [ "--help" ] diff --git a/README.md b/README.md index 81297a01..b796f27c 100755 --- a/README.md +++ b/README.md @@ -13,14 +13,14 @@ The CycloneDX module for .NET creates a valid CycloneDX bill-of-material document containing an aggregate of all project dependencies. CycloneDX is a lightweight BOM specification that is easily created, human readable, and simple to parse. This module runs on -* .NET Core 3.1 -* .NET 6.0. +* .NET 6.0 +* .NET 7.0 This module no longer runs on - * .NET Core 2.1 -* .NET5 -* see https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core for more infomation +* .NET Core 3.1 +* .NET 5.0 +* see https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core for more information ## Usage @@ -56,10 +56,12 @@ docker run cyclonedx/cyclonedx-dotnet [OPTIONS] Usage: dotnet CycloneDX [options] Arguments: - path The path to a .sln, .csproj, .fsproj, .vbproj, or packages.config file or the path to a directory which will be recursively analyzed for packages.config files + path The path to a .sln, .csproj, .fsproj, .vbproj, or packages.config file or the path to a directory which will be recursively analyzed for packages.config files Options: -v|--version Output the tool version and exit + -tfm|--framework The target framework to use. If not defined, all will be aggregated. + -rt|--runtime The runtime to use. If not defined, all will be aggregated. -o|--out The directory to write the BOM -f|--filename Optionally provide a filename for the BOM (default: bom.xml or bom.json) -j|--json Produce a JSON BOM instead of XML @@ -86,7 +88,7 @@ Options: -st|--set-type Override the default BOM metadata component type (defaults to application). Allowed values are: Null, Application, Framework, Library, OperationSystem, Device, File, Container, Firmware. Default value is: Null. - -?|-h|--help Show help information. + -?|-h|--help Show help information. ``` #### Examples diff --git a/semver.txt b/semver.txt index 24ba9a38..dbe59006 100755 --- a/semver.txt +++ b/semver.txt @@ -1 +1 @@ -2.7.0 +2.8.1