From 7d54cb0d85c54712a5b7416c600ee61c3cbdbbc5 Mon Sep 17 00:00:00 2001 From: awedist <68296344+awedist@users.noreply.github.com> Date: Sat, 15 Jun 2024 00:03:11 +0200 Subject: [PATCH] feat: Implement dependency review and dependency submission APIs (#2932) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement dependency review and dependency submission Co-authored-by: André Pereira --- .../IObservableDependencyGraphClient.cs | 22 +++ .../IObservableDependencyReviewClient.cs | 39 ++++++ .../IObservableDependencySubmissionClient.cs | 39 ++++++ .../ObservableDependencyGraphClient.cs | 29 ++++ .../ObservableDependencyReviewClient.cs | 63 +++++++++ .../ObservableDependencySubmissionClient.cs | 60 +++++++++ Octokit.Reactive/IObservableGitHubClient.cs | 1 + Octokit.Reactive/ObservableGitHubClient.cs | 4 +- .../Clients/DependencyReviewClientTests.cs | 55 ++++++++ .../DependencySubmissionClientTests.cs | 114 ++++++++++++++++ .../ObservableDependencyReviewClientTests.cs | 51 +++++++ ...servableDependencySubmissionClientTests.cs | 115 ++++++++++++++++ .../Clients/DependencyGraphClientTests.cs | 17 +++ .../Clients/DependencyReviewClientTests.cs | 74 ++++++++++ .../DependencySubmissionClientTests.cs | 73 ++++++++++ Octokit.Tests/Models/DependencyDiffTests.cs | 56 ++++++++ .../DependencySnapshotSubmissionTests.cs | 32 +++++ .../ObservableDependencyGraphClientTests.cs | 18 +++ .../ObservableDependencyReviewClientTests.cs | 72 ++++++++++ ...servableDependencySubmissionClientTests.cs | 71 ++++++++++ Octokit/Clients/DependencyGraphClient.cs | 31 +++++ Octokit/Clients/DependencyReviewClient.cs | 66 +++++++++ Octokit/Clients/DependencySubmissionClient.cs | 127 ++++++++++++++++++ Octokit/Clients/IDependencyGraphClient.cs | 21 +++ Octokit/Clients/IDependencyReviewClient.cs | 41 ++++++ .../Clients/IDependencySubmissionClient.cs | 38 ++++++ Octokit/GitHubClient.cs | 8 +- Octokit/Helpers/ApiUrls.cs | 56 +++++++- Octokit/IGitHubClient.cs | 7 +- .../Models/Request/NewDependencySnapshot.cs | 86 ++++++++++++ .../Request/NewDependencySnapshotDetector.cs | 45 +++++++ .../Request/NewDependencySnapshotJob.cs | 45 +++++++ .../Request/NewDependencySnapshotManifest.cs | 49 +++++++ .../NewDependencySnapshotManifestFile.cs | 22 +++ ...NewDependencySnapshotResolvedDependency.cs | 75 +++++++++++ Octokit/Models/Response/ChangeType.cs | 13 ++ Octokit/Models/Response/DependencyDiff.cs | 127 ++++++++++++++++++ .../Response/DependencySnapshotSubmission.cs | 70 ++++++++++ Octokit/Models/Response/Scope.cs | 25 ++++ 39 files changed, 1949 insertions(+), 8 deletions(-) create mode 100644 Octokit.Reactive/Clients/IObservableDependencyGraphClient.cs create mode 100644 Octokit.Reactive/Clients/IObservableDependencyReviewClient.cs create mode 100644 Octokit.Reactive/Clients/IObservableDependencySubmissionClient.cs create mode 100644 Octokit.Reactive/Clients/ObservableDependencyGraphClient.cs create mode 100644 Octokit.Reactive/Clients/ObservableDependencyReviewClient.cs create mode 100644 Octokit.Reactive/Clients/ObservableDependencySubmissionClient.cs create mode 100644 Octokit.Tests.Integration/Clients/DependencyReviewClientTests.cs create mode 100644 Octokit.Tests.Integration/Clients/DependencySubmissionClientTests.cs create mode 100644 Octokit.Tests.Integration/Reactive/ObservableDependencyReviewClientTests.cs create mode 100644 Octokit.Tests.Integration/Reactive/ObservableDependencySubmissionClientTests.cs create mode 100644 Octokit.Tests/Clients/DependencyGraphClientTests.cs create mode 100644 Octokit.Tests/Clients/DependencyReviewClientTests.cs create mode 100644 Octokit.Tests/Clients/DependencySubmissionClientTests.cs create mode 100644 Octokit.Tests/Models/DependencyDiffTests.cs create mode 100644 Octokit.Tests/Models/DependencySnapshotSubmissionTests.cs create mode 100644 Octokit.Tests/Reactive/ObservableDependencyGraphClientTests.cs create mode 100644 Octokit.Tests/Reactive/ObservableDependencyReviewClientTests.cs create mode 100644 Octokit.Tests/Reactive/ObservableDependencySubmissionClientTests.cs create mode 100644 Octokit/Clients/DependencyGraphClient.cs create mode 100644 Octokit/Clients/DependencyReviewClient.cs create mode 100644 Octokit/Clients/DependencySubmissionClient.cs create mode 100644 Octokit/Clients/IDependencyGraphClient.cs create mode 100644 Octokit/Clients/IDependencyReviewClient.cs create mode 100644 Octokit/Clients/IDependencySubmissionClient.cs create mode 100644 Octokit/Models/Request/NewDependencySnapshot.cs create mode 100644 Octokit/Models/Request/NewDependencySnapshotDetector.cs create mode 100644 Octokit/Models/Request/NewDependencySnapshotJob.cs create mode 100644 Octokit/Models/Request/NewDependencySnapshotManifest.cs create mode 100644 Octokit/Models/Request/NewDependencySnapshotManifestFile.cs create mode 100644 Octokit/Models/Request/NewDependencySnapshotResolvedDependency.cs create mode 100644 Octokit/Models/Response/ChangeType.cs create mode 100644 Octokit/Models/Response/DependencyDiff.cs create mode 100644 Octokit/Models/Response/DependencySnapshotSubmission.cs create mode 100644 Octokit/Models/Response/Scope.cs diff --git a/Octokit.Reactive/Clients/IObservableDependencyGraphClient.cs b/Octokit.Reactive/Clients/IObservableDependencyGraphClient.cs new file mode 100644 index 0000000000..7a22d179f3 --- /dev/null +++ b/Octokit.Reactive/Clients/IObservableDependencyGraphClient.cs @@ -0,0 +1,22 @@ +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's Dependency Graph API. + /// + /// + /// See the Git Dependency Graph API documentation for more information. + /// + public interface IObservableDependencyGraphClient + { + /// + /// Client for getting a dependency comparison between two commits. + /// + IObservableDependencyReviewClient DependencyReview { get; } + + /// + /// Client for submitting dependency snapshots. + /// + IObservableDependencySubmissionClient DependencySubmission { get; } + } +} + diff --git a/Octokit.Reactive/Clients/IObservableDependencyReviewClient.cs b/Octokit.Reactive/Clients/IObservableDependencyReviewClient.cs new file mode 100644 index 0000000000..d87a4de001 --- /dev/null +++ b/Octokit.Reactive/Clients/IObservableDependencyReviewClient.cs @@ -0,0 +1,39 @@ + +using System; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's Dependency Review API. + /// + /// + /// See the Git Dependency Review API documentation for more information. + /// + public interface IObservableDependencyReviewClient + { + /// + /// Gets all s for the specified repository. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The base revision + /// The head revision + /// Thrown when a general API error occurs. + IObservable GetAll(string owner, string name, string @base, string head); + + /// + /// Gets all s for the specified repository. + /// + /// + /// See the API documentation for more information. + /// + /// The Id of the repository + /// The base revision + /// The head revision + /// Thrown when a general API error occurs. + IObservable GetAll(long repositoryId, string @base, string head); + } +} diff --git a/Octokit.Reactive/Clients/IObservableDependencySubmissionClient.cs b/Octokit.Reactive/Clients/IObservableDependencySubmissionClient.cs new file mode 100644 index 0000000000..ed27fd853a --- /dev/null +++ b/Octokit.Reactive/Clients/IObservableDependencySubmissionClient.cs @@ -0,0 +1,39 @@ +using System; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's Dependency Submission API. + /// + /// + /// See the Dependency Submission API documentation for more details. + /// + public interface IObservableDependencySubmissionClient + { + /// + /// Creates a new dependency snapshot. + /// + /// + /// See the API documentation for more information. + /// + /// The repository's owner + /// The repository's name + /// The dependency snapshot to create + /// Thrown when a general API error occurs + /// A instance for the created snapshot + IObservable Create(string owner, string name, NewDependencySnapshot snapshot); + + /// + /// Creates a new dependency snapshot. + /// + /// + /// See the API documentation for more information. + /// + /// The Id of the repository + /// The dependency snapshot to create + /// Thrown when a general API error occurs + /// A instance for the created snapshot + IObservable Create(long repositoryId, NewDependencySnapshot snapshot); + } +} + diff --git a/Octokit.Reactive/Clients/ObservableDependencyGraphClient.cs b/Octokit.Reactive/Clients/ObservableDependencyGraphClient.cs new file mode 100644 index 0000000000..0aadea0313 --- /dev/null +++ b/Octokit.Reactive/Clients/ObservableDependencyGraphClient.cs @@ -0,0 +1,29 @@ +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's Dependency Graph API. + /// + /// + /// See the Git Dependency Graph API documentation for more information. + /// + public class ObservableDependencyGraphClient : IObservableDependencyGraphClient + { + public ObservableDependencyGraphClient(IGitHubClient client) + { + Ensure.ArgumentNotNull(client, nameof(client)); + + //DependencyReview = new ObservableDependencyReviewClient(client); + DependencySubmission = new ObservableDependencySubmissionClient(client); + } + + /// + /// Client for getting a dependency comparison between two commits. + /// + public IObservableDependencyReviewClient DependencyReview { get; private set; } + + /// + /// Client for submitting dependency snapshots. + /// + public IObservableDependencySubmissionClient DependencySubmission { get; private set; } + } +} diff --git a/Octokit.Reactive/Clients/ObservableDependencyReviewClient.cs b/Octokit.Reactive/Clients/ObservableDependencyReviewClient.cs new file mode 100644 index 0000000000..c7bfcc1145 --- /dev/null +++ b/Octokit.Reactive/Clients/ObservableDependencyReviewClient.cs @@ -0,0 +1,63 @@ +using System; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's Dependency Review API. + /// + /// + /// See the Git Dependency Review API documentation for more information. + /// + public class ObservableDependencyReviewClient : IObservableDependencyReviewClient + { + readonly IDependencyReviewClient _client; + + public ObservableDependencyReviewClient(IGitHubClient client) + { + Ensure.ArgumentNotNull(client, nameof(client)); + + _client = client.DependencyGraph.DependencyReview; + } + + /// + /// Gets all s for the specified repository. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The base revision + /// The head revision + /// Thrown when a general API error occurs. + public IObservable GetAll(string owner, string name, string @base, string head) + { + Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner)); + Ensure.ArgumentNotNullOrEmptyString(name, nameof(name)); + Ensure.ArgumentNotNullOrEmptyString(@base, nameof(@base)); + Ensure.ArgumentNotNullOrEmptyString(head, nameof(head)); + + return _client.GetAll(owner, name, @base, head).ToObservable().SelectMany(x => x); + } + + /// + /// Gets all s for the specified repository. + /// + /// + /// See the API documentation for more information. + /// + /// The Id of the repository + /// The base revision + /// The head revision + /// Thrown when a general API error occurs. + public IObservable GetAll(long repositoryId, string @base, string head) + { + Ensure.ArgumentNotNullOrEmptyString(@base, nameof(@base)); + Ensure.ArgumentNotNullOrEmptyString(head, nameof(head)); + + return _client.GetAll(repositoryId, @base, head).ToObservable().SelectMany(x => x); + } + } +} diff --git a/Octokit.Reactive/Clients/ObservableDependencySubmissionClient.cs b/Octokit.Reactive/Clients/ObservableDependencySubmissionClient.cs new file mode 100644 index 0000000000..23702457e6 --- /dev/null +++ b/Octokit.Reactive/Clients/ObservableDependencySubmissionClient.cs @@ -0,0 +1,60 @@ +using System; +using System.Reactive.Threading.Tasks; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's Dependency Submission API. + /// + /// + /// See the Dependency Submission API documentation for more details. + /// + public class ObservableDependencySubmissionClient : IObservableDependencySubmissionClient + { + readonly IDependencySubmissionClient _client; + + public ObservableDependencySubmissionClient(IGitHubClient client) + { + Ensure.ArgumentNotNull(client, nameof(client)); + + _client = client.DependencyGraph.DependencySubmission; + } + + /// + /// Creates a new dependency snapshot. + /// + /// + /// See the API documentation for more information. + /// + /// The repository's owner + /// The repository's name + /// The dependency snapshot to create + /// Thrown when a general API error occurs + /// A instance for the created snapshot + public IObservable Create(string owner, string name, NewDependencySnapshot snapshot) + { + Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner)); + Ensure.ArgumentNotNullOrEmptyString(name, nameof(name)); + Ensure.ArgumentNotNull(snapshot, nameof(snapshot)); + + return _client.Create(owner, name, snapshot).ToObservable(); + } + + /// + /// Creates a new dependency snapshot. + /// + /// + /// See the API documentation for more information. + /// + /// The Id of the repository + /// The dependency snapshot to create + /// Thrown when a general API error occurs + /// A instance for the created snapshot + public IObservable Create(long repositoryId, NewDependencySnapshot snapshot) + { + Ensure.ArgumentNotNull(snapshot, nameof(snapshot)); + + return _client.Create(repositoryId, snapshot).ToObservable(); + } + } +} diff --git a/Octokit.Reactive/IObservableGitHubClient.cs b/Octokit.Reactive/IObservableGitHubClient.cs index 68fb81a6a5..5fdb152a3d 100644 --- a/Octokit.Reactive/IObservableGitHubClient.cs +++ b/Octokit.Reactive/IObservableGitHubClient.cs @@ -44,5 +44,6 @@ public interface IObservableGitHubClient : IApiInfoProvider IObservableActionsClient Actions { get; } IObservableCodespacesClient Codespaces { get; } IObservableCopilotClient Copilot { get; } + IObservableDependencyGraphClient DependencyGraph { get; } } } diff --git a/Octokit.Reactive/ObservableGitHubClient.cs b/Octokit.Reactive/ObservableGitHubClient.cs index 2713ad12ce..130e5aca52 100644 --- a/Octokit.Reactive/ObservableGitHubClient.cs +++ b/Octokit.Reactive/ObservableGitHubClient.cs @@ -59,6 +59,7 @@ public ObservableGitHubClient(IGitHubClient gitHubClient) Actions = new ObservableActionsClient(gitHubClient); Codespaces = new ObservableCodespacesClient(gitHubClient); Copilot = new ObservableCopilotClient(gitHubClient); + DependencyGraph = new ObservableDependencyGraphClient(gitHubClient); } public IConnection Connection @@ -108,7 +109,8 @@ public void SetRequestTimeout(TimeSpan timeout) public IObservableActionsClient Actions { get; private set; } public IObservableCodespacesClient Codespaces { get; private set; } public IObservableCopilotClient Copilot { get; set; } - + public IObservableDependencyGraphClient DependencyGraph { get; } + /// /// Gets the latest API Info - this will be null if no API calls have been made /// diff --git a/Octokit.Tests.Integration/Clients/DependencyReviewClientTests.cs b/Octokit.Tests.Integration/Clients/DependencyReviewClientTests.cs new file mode 100644 index 0000000000..b19f6f610d --- /dev/null +++ b/Octokit.Tests.Integration/Clients/DependencyReviewClientTests.cs @@ -0,0 +1,55 @@ +using Octokit; +using Octokit.Tests.Integration; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +/// +/// Base and head must have different dependencies +/// +public class DependencyReviewClientTests +{ + + public class TheGetAllMethod + { + readonly IDependencyReviewClient _fixture; + readonly IGitHubClient _github; + readonly string _owner; + readonly string _repo; + readonly string _base; + readonly string _head; + readonly long _repoId; + + public TheGetAllMethod() + { + _github = Helper.GetAuthenticatedClient(); + _fixture = _github.DependencyGraph.DependencyReview; + + _owner = "octokit"; + _repo = "octokit.net"; + _base = "main"; + _head = "brave-new-codegen-world"; + _repoId = _github.Repository.Get(_owner, _repo).Result.Id; + } + + [IntegrationTest] + public async Task GetDependencyDiffs() + { + var diffs = await _fixture.GetAll(_owner, _repo, _base, _head); + + Assert.NotEmpty(diffs); + Assert.NotNull(diffs); + Assert.IsType(diffs.First()); + } + + [IntegrationTest] + public async Task GetDependencyDiffsWithRepositoryId() + { + var diffs = await _fixture.GetAll(_repoId, _base, _head); + + Assert.NotEmpty(diffs); + Assert.NotNull(diffs); + Assert.IsType(diffs.First()); + } + } +} diff --git a/Octokit.Tests.Integration/Clients/DependencySubmissionClientTests.cs b/Octokit.Tests.Integration/Clients/DependencySubmissionClientTests.cs new file mode 100644 index 0000000000..b86dcad1a6 --- /dev/null +++ b/Octokit.Tests.Integration/Clients/DependencySubmissionClientTests.cs @@ -0,0 +1,114 @@ +using Octokit; +using Octokit.Tests.Integration; +using Octokit.Tests.Integration.Helpers; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Xunit; + +public class DependencySubmissionClientTests +{ + public class TheCreateMethod + { + private readonly IDependencySubmissionClient _dependencySubmissionClient; + private readonly RepositoryContext _context; + private readonly NewDependencySnapshot _newDependencySnapshot; + + public TheCreateMethod() + { + var github = Helper.GetAuthenticatedClient(); + + _dependencySubmissionClient = github.DependencyGraph.DependencySubmission; + _context = github.CreateRepositoryContextWithAutoInit("public-repo").Result; + + var job = new NewDependencySnapshotJob("runid", "example-correlator"); + var detector = new NewDependencySnapshotDetector("example-detector", "1.0.0", "https://github.com/example/detector"); + + var resolvedMetadata = new Dictionary + { + { "License", "MIT" } + }; + + var manifests = new Dictionary + { + { + "package-lock.json", + new NewDependencySnapshotManifest("package-lock.json") + { + File = new NewDependencySnapshotManifestFile + { + SourceLocation = "src/package-lock.json" + }, + Resolved = new Dictionary + { + { + "@actions/core", + new NewDependencySnapshotResolvedDependency + { + PackageUrl = "pkg:/npm/%40actions/core@1.1.9", + Dependencies = new Collection { "@actions/http-client" }, + Scope = ResolvedPackageKeyScope.Runtime, + Relationship = ResolvedPackageKeyRelationship.Indirect, + Metadata = resolvedMetadata + } + }, + { + "@actions/http-client", + new NewDependencySnapshotResolvedDependency + { + PackageUrl = "pkg:/npm/%40actions/http-client@1.0.7", + Scope = ResolvedPackageKeyScope.Development, + Relationship = ResolvedPackageKeyRelationship.Direct + } + }, + { + "tunnel", + new NewDependencySnapshotResolvedDependency + { + PackageUrl = "pkg:/npm/tunnel@0.0.6", + Dependencies = new Collection(), + Relationship = ResolvedPackageKeyRelationship.Direct, + } + } + } + } + } + }; + + var snapshotMetadata = new Dictionary + { + { "Author", "John Doe" }, + { "Version", "1.0.0" }, + { "License", "MIT" } + }; + + _newDependencySnapshot = new NewDependencySnapshot( + 1, + "ce587453ced02b1526dfb4cb910479d431683101", + "refs/heads/main", + "2022-06-14T20:25:00Z", + job, + detector) + { + Metadata = snapshotMetadata, + Manifests = manifests + }; + } + + [IntegrationTest] + public async Task CanCreateDependencySnapshot() + { + var submission = await _dependencySubmissionClient.Create(_context.RepositoryOwner, _context.RepositoryName, _newDependencySnapshot); + + Assert.Equal(DependencySnapshotSubmissionResult.Accepted, submission.Result); + } + + [IntegrationTest] + public async Task CanCreateDependencySnapshotWithRepositoryId() + { + var submission = await _dependencySubmissionClient.Create(_context.Repository.Id, _newDependencySnapshot); + + Assert.Equal(DependencySnapshotSubmissionResult.Accepted, submission.Result); + } + } +} diff --git a/Octokit.Tests.Integration/Reactive/ObservableDependencyReviewClientTests.cs b/Octokit.Tests.Integration/Reactive/ObservableDependencyReviewClientTests.cs new file mode 100644 index 0000000000..c64eda0cdd --- /dev/null +++ b/Octokit.Tests.Integration/Reactive/ObservableDependencyReviewClientTests.cs @@ -0,0 +1,51 @@ +using Octokit; +using Octokit.Reactive; +using Octokit.Tests.Integration; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Xunit; + +/// +/// Base and head must have different dependencies +/// +public class ObservableDependencyReviewClientTests +{ + public class TheGetAllMethod + { + readonly ObservableDependencyReviewClient _DependencyReviewClient; + readonly string owner = "octokit"; + readonly string repo = "octokit.net"; + readonly string @base = "main"; + readonly string head = "brave-new-codegen-world"; + readonly long repoId; + + public TheGetAllMethod() + { + var github = Helper.GetAuthenticatedClient(); + _DependencyReviewClient = new ObservableDependencyReviewClient(github); + + repoId = github.Repository.Get(owner, repo).Result.Id; + } + + [IntegrationTest] + public async Task GetDependencyDiffs() + { + var diffs = await _DependencyReviewClient.GetAll(owner, repo, @base, head).ToList(); + + Assert.NotEmpty(diffs); + Assert.NotNull(diffs); + Assert.IsType(diffs.First()); + } + + [IntegrationTest] + public async Task GetDependencyDiffsWithRepositoryId() + { + var diffs = await _DependencyReviewClient.GetAll(repoId, @base, head).ToList(); + + Assert.NotEmpty(diffs); + Assert.NotNull(diffs); + Assert.IsType(diffs.First()); + } + } +} diff --git a/Octokit.Tests.Integration/Reactive/ObservableDependencySubmissionClientTests.cs b/Octokit.Tests.Integration/Reactive/ObservableDependencySubmissionClientTests.cs new file mode 100644 index 0000000000..63431e5dbb --- /dev/null +++ b/Octokit.Tests.Integration/Reactive/ObservableDependencySubmissionClientTests.cs @@ -0,0 +1,115 @@ +using Octokit; +using Octokit.Reactive; +using Octokit.Tests.Integration; +using Octokit.Tests.Integration.Helpers; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Xunit; + +public class ObservableDependencySubmissionClientTests +{ + public class TheCreateMethod + { + private readonly ObservableGitHubClient _client; + private readonly RepositoryContext _context; + private readonly NewDependencySnapshot _newDependencySnapshot; + + public TheCreateMethod() + { + var github = Helper.GetAuthenticatedClient(); + _client = new ObservableGitHubClient(github); + _context = github.CreateRepositoryContextWithAutoInit("public-repo").Result; + + var job = new NewDependencySnapshotJob("runid", "example-correlator"); + var detector = new NewDependencySnapshotDetector("example-detector", "1.0.0", "https://github.com/example/detector"); + + var resolvedMetadata = new Dictionary + { + { "License", "MIT" } + }; + + var manifests = new Dictionary + { + { + "package-lock.json", + new NewDependencySnapshotManifest("package-lock.json") + { + File = new NewDependencySnapshotManifestFile + { + SourceLocation = "src/package-lock.json" + }, + Resolved = new Dictionary + { + { + "@actions/core", + new NewDependencySnapshotResolvedDependency + { + PackageUrl = "pkg:/npm/%40actions/core@1.1.9", + Dependencies = new Collection { "@actions/http-client" }, + Scope = ResolvedPackageKeyScope.Runtime, + Relationship = ResolvedPackageKeyRelationship.Indirect, + Metadata = resolvedMetadata + } + }, + { + "@actions/http-client", + new NewDependencySnapshotResolvedDependency + { + PackageUrl = "pkg:/npm/%40actions/http-client@1.0.7", + Scope = ResolvedPackageKeyScope.Development, + Relationship = ResolvedPackageKeyRelationship.Direct + } + }, + { + "tunnel", + new NewDependencySnapshotResolvedDependency + { + PackageUrl = "pkg:/npm/tunnel@0.0.6", + Dependencies = new Collection(), + Relationship = ResolvedPackageKeyRelationship.Direct, + } + } + } + } + } + }; + + var snapshotMetadata = new Dictionary + { + { "Author", "John Doe" }, + { "Version", "1.0.0" }, + { "License", "MIT" } + }; + + _newDependencySnapshot = new NewDependencySnapshot( + 1, + "ce587453ced02b1526dfb4cb910479d431683101", + "refs/heads/main", + "2022-06-14T20:25:00Z", + job, + detector) + { + Metadata = snapshotMetadata, + Manifests = manifests + }; + } + + [IntegrationTest] + public async Task CanCreateDependencySnapshot() + { + var submission = await _client.DependencyGraph.DependencySubmission.Create(_context.RepositoryOwner, _context.RepositoryName, _newDependencySnapshot); + + Assert.Equal(DependencySnapshotSubmissionResult.Accepted, submission.Result); + } + + [IntegrationTest] + public async Task CanCreateDependencySnapshotWithRepositoryId() + { + var submission = await _client.DependencyGraph.DependencySubmission.Create(_context.Repository.Id, _newDependencySnapshot); + + Assert.Equal(DependencySnapshotSubmissionResult.Accepted, submission.Result); + } + } +} diff --git a/Octokit.Tests/Clients/DependencyGraphClientTests.cs b/Octokit.Tests/Clients/DependencyGraphClientTests.cs new file mode 100644 index 0000000000..fd76c1e6bb --- /dev/null +++ b/Octokit.Tests/Clients/DependencyGraphClientTests.cs @@ -0,0 +1,17 @@ +using System; +using Xunit; + +namespace Octokit +{ + public class DependencyGraphClientTests + { + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new DependencyGraphClient(null)); + } + } + } +} diff --git a/Octokit.Tests/Clients/DependencyReviewClientTests.cs b/Octokit.Tests/Clients/DependencyReviewClientTests.cs new file mode 100644 index 0000000000..ef1331739c --- /dev/null +++ b/Octokit.Tests/Clients/DependencyReviewClientTests.cs @@ -0,0 +1,74 @@ +using NSubstitute; +using Octokit.Tests; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace Octokit +{ + public class DependencyReviewClientTests + { + public class TheGetAllMethod + { + [Fact] + public async Task RequestsCorrectUrl() + { + var connection = Substitute.For(); + var client = new DependencyReviewClient(connection); + + await client.GetAll("fake", "repo", "base", "head"); + + connection.Received().GetAll(Arg.Is(u => u.ToString() == "repos/fake/repo/dependency-graph/compare/base...head")); + } + + [Fact] + public async Task RequestsCorrectUrlWithRepositoryId() + { + var connection = Substitute.For(); + var client = new DependencyReviewClient(connection); + + await client.GetAll(1, "base", "head"); + + connection.Received().GetAll(Arg.Is(u => u.ToString() == "repositories/1/dependency-graph/compare/base...head")); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new DependencyReviewClient(Substitute.For()); + + await Assert.ThrowsAsync(() => client.GetAll(null, "name", "base", "head")); + await Assert.ThrowsAsync(() => client.GetAll("owner", null, "base", "head")); + await Assert.ThrowsAsync(() => client.GetAll("owner", "name", null, "head")); + await Assert.ThrowsAsync(() => client.GetAll("owner", "name", "base", null)); + + await Assert.ThrowsAsync(() => client.GetAll(1, null, "head")); + await Assert.ThrowsAsync(() => client.GetAll(1, "base", null)); + } + + [Fact] + public async Task EnsuresNonEmptyArguments() + { + var client = new DependencyReviewClient(Substitute.For()); + + await Assert.ThrowsAsync(() => client.GetAll("", "name", "base", "head")); + await Assert.ThrowsAsync(() => client.GetAll("owner", "", "base", "head")); + await Assert.ThrowsAsync(() => client.GetAll("owner", "name", "", "head")); + await Assert.ThrowsAsync(() => client.GetAll("owner", "name", "head", "")); + + await Assert.ThrowsAsync(() => client.GetAll(1, "", "head")); + await Assert.ThrowsAsync(() => client.GetAll(1, "base", "")); + } + } + + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new DependencyReviewClient(null)); + } + } + } +} diff --git a/Octokit.Tests/Clients/DependencySubmissionClientTests.cs b/Octokit.Tests/Clients/DependencySubmissionClientTests.cs new file mode 100644 index 0000000000..9e3252214c --- /dev/null +++ b/Octokit.Tests/Clients/DependencySubmissionClientTests.cs @@ -0,0 +1,73 @@ +using NSubstitute; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Octokit +{ + public class DependencySubmissionClientTests + { + public class TheCreateMethod + { + private NewDependencySnapshot newDependencySnapshot = new NewDependencySnapshot( + 1, + "sha", + "ref", + "scanned", + new NewDependencySnapshotJob("runId", "jobCorrelator"), + new NewDependencySnapshotDetector("detectorName", "detectorVersion", "detectorUrl")); + + [Fact] + public void PostsToTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new DependencySubmissionClient(connection); + var expectedUrl = "repos/owner/name/dependency-graph/snapshots"; + + client.Create("owner", "name", newDependencySnapshot); + + connection.Received(1).Post(Arg.Is(u => u.ToString() == expectedUrl), Arg.Any()); + } + + [Fact] + public void PostsToTheCorrectUrlWithRepositoryId() + { + var connection = Substitute.For(); + var client = new DependencySubmissionClient(connection); + var expectedUrl = "repositories/1/dependency-graph/snapshots"; + + client.Create(1, newDependencySnapshot); + + connection.Received(1).Post(Arg.Is(u => u.ToString() == expectedUrl), Arg.Any()); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new DependencySubmissionClient(Substitute.For()); + + await Assert.ThrowsAsync(() => client.Create(null, "name", newDependencySnapshot)); + await Assert.ThrowsAsync(() => client.Create("owner", null, newDependencySnapshot)); + await Assert.ThrowsAsync(() => client.Create("owner", "name", null)); + } + + [Fact] + public async Task EnsuresNonEmptyArguments() + { + var client = new DependencySubmissionClient(Substitute.For()); + + await Assert.ThrowsAsync(() => client.Create("", "name", newDependencySnapshot)); + await Assert.ThrowsAsync(() => client.Create("owner", "", newDependencySnapshot)); + } + } + + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new DependencySubmissionClient(null)); + } + } + } +} diff --git a/Octokit.Tests/Models/DependencyDiffTests.cs b/Octokit.Tests/Models/DependencyDiffTests.cs new file mode 100644 index 0000000000..be46ef832a --- /dev/null +++ b/Octokit.Tests/Models/DependencyDiffTests.cs @@ -0,0 +1,56 @@ +using Octokit.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Octokit +{ + public class DependencyDiffTests + { + [Fact] + public void CanDeserialize() + { + const string json = @"{ + ""change_type"": ""removed"", + ""manifest"": ""Cargo.lock"", + ""ecosystem"": ""cargo"", + ""name"": ""libsqlite3-sys"", + ""version"": ""0.22.2"", + ""package_url"": ""pkg:cargo/libsqlite3-sys@0.22.2"", + ""license"": ""MIT"", + ""source_repository_url"": ""https://github.com/rusqlite/rusqlite"", + ""scope"": ""runtime"", + ""vulnerabilities"": [ + { + ""severity"": ""high"", + ""advisory_ghsa_id"": ""GHSA-jw36-hf63-69r9"", + ""advisory_summary"": ""`libsqlite3-sys` via C SQLite improperly validates array index"", + ""advisory_url"": ""https://github.com/advisories/GHSA-jw36-hf63-69r9"" + } + ] + }"; + + var actual = new SimpleJsonSerializer().Deserialize(json); + + Assert.Equal("removed", actual.ChangeType); + Assert.Equal("Cargo.lock", actual.Manifest); + Assert.Equal("cargo", actual.Ecosystem); + Assert.Equal("libsqlite3-sys", actual.Name); + Assert.Equal("0.22.2", actual.Version); + Assert.Equal("pkg:cargo/libsqlite3-sys@0.22.2", actual.PackageUrl); + Assert.Equal("MIT", actual.License); + Assert.Equal("https://github.com/rusqlite/rusqlite", actual.SourceRepositoryUrl); + Assert.Equal("runtime", actual.Scope); + Assert.NotNull(actual.Vulnerabilities); + Assert.Single(actual.Vulnerabilities); + var vulnerability = actual.Vulnerabilities.First(); + Assert.Equal("high", vulnerability.Severity); + Assert.Equal("GHSA-jw36-hf63-69r9", vulnerability.AdvisoryGhsaId); + Assert.Equal("`libsqlite3-sys` via C SQLite improperly validates array index", vulnerability.AdvisorySummary); + Assert.Equal("https://github.com/advisories/GHSA-jw36-hf63-69r9", vulnerability.AdvisoryUrl); + } + } +} diff --git a/Octokit.Tests/Models/DependencySnapshotSubmissionTests.cs b/Octokit.Tests/Models/DependencySnapshotSubmissionTests.cs new file mode 100644 index 0000000000..cbcac0a502 --- /dev/null +++ b/Octokit.Tests/Models/DependencySnapshotSubmissionTests.cs @@ -0,0 +1,32 @@ +using Octokit.Internal; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Xunit; + +namespace Octokit +{ + public class DependencySnapshotSubmissionTests + { + [Fact] + public void CanDeserialize() + { + const string json = @" + { + ""id"": 12345, + ""created_at"": ""2018-05-04T01:14:52Z"", + ""message"": ""Dependency results for the repo have been successfully updated."", + ""result"": ""SUCCESS"" + }"; + + var serializer = new SimpleJsonSerializer(); + + var actual = serializer.Deserialize(json); + + Assert.Equal(12345, actual.Id); + Assert.Equal(DateTimeOffset.Parse("2018-05-04T01:14:52Z"), actual.CreatedAt); + Assert.Equal("Dependency results for the repo have been successfully updated.", actual.Message); + Assert.Equal("SUCCESS", actual.Result); + } + } +} diff --git a/Octokit.Tests/Reactive/ObservableDependencyGraphClientTests.cs b/Octokit.Tests/Reactive/ObservableDependencyGraphClientTests.cs new file mode 100644 index 0000000000..8788b78dd2 --- /dev/null +++ b/Octokit.Tests/Reactive/ObservableDependencyGraphClientTests.cs @@ -0,0 +1,18 @@ +using Octokit.Reactive; +using System; +using Xunit; + +namespace Octokit.Tests.Reactive +{ + public class ObservableDependencyGraphClientTests + { + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new ObservableDependencyGraphClient(null)); + } + } + } +} \ No newline at end of file diff --git a/Octokit.Tests/Reactive/ObservableDependencyReviewClientTests.cs b/Octokit.Tests/Reactive/ObservableDependencyReviewClientTests.cs new file mode 100644 index 0000000000..fffc8958f9 --- /dev/null +++ b/Octokit.Tests/Reactive/ObservableDependencyReviewClientTests.cs @@ -0,0 +1,72 @@ +using NSubstitute; +using Octokit.Reactive; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Octokit.Tests.Reactive +{ + public class ObservableDependencyReviewClientTests + { + public class TheGetAllMethod + { + [Fact] + public async Task RequestsCorrectUrl() + { + var gitHubClient = Substitute.For(); + var client = new ObservableDependencyReviewClient(gitHubClient); + + client.GetAll("fake", "repo", "base", "head"); + + gitHubClient.Received().DependencyGraph.DependencyReview.GetAll("fake", "repo", "base", "head"); + } + + [Fact] + public void RequestsCorrectUrlWithRepositoryId() + { + var gitHubClient = Substitute.For(); + var client = new ObservableDependencyReviewClient(gitHubClient); + + client.GetAll(1, "base", "head"); + + gitHubClient.Received().DependencyGraph.DependencyReview.GetAll(1, "base", "head"); + } + + [Fact] + public void EnsuresNonNullArguments() + { + var client = new ObservableDependencyReviewClient(Substitute.For()); + + Assert.Throws(() => client.GetAll(null, "name", "base", "head")); + Assert.Throws(() => client.GetAll("owner", null, "base", "head")); + Assert.Throws(() => client.GetAll("owner", "name", null, "head")); + Assert.Throws(() => client.GetAll("owner", "name", "base", null)); + + Assert.Throws(() => client.GetAll(1, null, "head")); + Assert.Throws(() => client.GetAll(1, "base", null)); + } + + [Fact] + public void EnsuresNonEmptyArguments() + { + var client = new ObservableDependencyReviewClient(Substitute.For()); + + Assert.Throws(() => client.GetAll("", "name", "base", "head")); + Assert.Throws(() => client.GetAll("owner", "", "base", "head")); + Assert.Throws(() => client.GetAll("owner", "name", "", "head")); + Assert.Throws(() => client.GetAll("owner", "name", "base", "")); + + Assert.Throws(() => client.GetAll(1, "", "head")); + Assert.Throws(() => client.GetAll(1, "base", "")); + } + } + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new ObservableDependencyReviewClient(null)); + } + } + } +} diff --git a/Octokit.Tests/Reactive/ObservableDependencySubmissionClientTests.cs b/Octokit.Tests/Reactive/ObservableDependencySubmissionClientTests.cs new file mode 100644 index 0000000000..35eb5c45cd --- /dev/null +++ b/Octokit.Tests/Reactive/ObservableDependencySubmissionClientTests.cs @@ -0,0 +1,71 @@ +using NSubstitute; +using Octokit.Reactive; +using System; +using Xunit; + +namespace Octokit.Tests.Reactive +{ + public class ObservableDependencySubmissionClientTests + { + public class TheCreateMethod + { + private NewDependencySnapshot newDependencySnapshot = new NewDependencySnapshot( + 1, + "sha", + "ref", + "scanned", + new NewDependencySnapshotJob("runId", "jobCorrelator"), + new NewDependencySnapshotDetector("detectorName", "detectorVersion", "detectorUrl")); + + [Fact] + public void PostsToTheCorrectUrl() + { + var gitHubClient = Substitute.For(); + var client = new ObservableDependencySubmissionClient(gitHubClient); + + client.Create("fake", "repo", newDependencySnapshot); + + gitHubClient.DependencyGraph.DependencySubmission.Received().Create("fake", "repo", newDependencySnapshot); + } + + [Fact] + public void PostsToTheCorrectUrlWithRepositoryId() + { + var gitHubClient = Substitute.For(); + var client = new ObservableDependencySubmissionClient(gitHubClient); + + client.Create(1, newDependencySnapshot); + + gitHubClient.DependencyGraph.DependencySubmission.Received().Create(1, newDependencySnapshot); + } + + [Fact] + public void EnsuresNonNullArguments() + { + var client = new ObservableDependencySubmissionClient(Substitute.For()); + + Assert.Throws(() => client.Create(null, "name", newDependencySnapshot)); + Assert.Throws(() => client.Create("owner", null, newDependencySnapshot)); + Assert.Throws(() => client.Create("owner", "name", null)); + } + + [Fact] + public void EnsuresNonEmptyArguments() + { + var client = new ObservableDependencySubmissionClient(Substitute.For()); + + Assert.Throws(() => client.Create("", "name", newDependencySnapshot)); + Assert.Throws(() => client.Create("owner", "", newDependencySnapshot)); + } + } + + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new ObservableDependencySubmissionClient(null)); + } + } + } +} diff --git a/Octokit/Clients/DependencyGraphClient.cs b/Octokit/Clients/DependencyGraphClient.cs new file mode 100644 index 0000000000..d36d78fa04 --- /dev/null +++ b/Octokit/Clients/DependencyGraphClient.cs @@ -0,0 +1,31 @@ +namespace Octokit +{ + /// + /// A client for GitHub's Dependency Graph API. + /// + /// + /// See the Git Dependency Graph API documentation for more information. + /// + public class DependencyGraphClient : IDependencyGraphClient + { + /// + /// Instantiates a new GitHub Dependency Graph API client. + /// + /// An API connection + public DependencyGraphClient(IApiConnection apiConnection) + { + DependencyReview = new DependencyReviewClient(apiConnection); + DependencySubmission = new DependencySubmissionClient(apiConnection); + } + + /// + /// Client for getting a dependency comparison between two commits. + /// + public IDependencyReviewClient DependencyReview { get; private set; } + + /// + /// Client for submitting dependency snapshots. + /// + public IDependencySubmissionClient DependencySubmission { get; private set; } + } +} \ No newline at end of file diff --git a/Octokit/Clients/DependencyReviewClient.cs b/Octokit/Clients/DependencyReviewClient.cs new file mode 100644 index 0000000000..caf52b4cec --- /dev/null +++ b/Octokit/Clients/DependencyReviewClient.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Octokit +{ + /// + /// A client for GitHub's Dependency Review API. + /// + /// + /// See the Git Dependency Review API documentation for more information. + /// + public class DependencyReviewClient : ApiClient, IDependencyReviewClient + { + /// + /// Instantiate a new GitHub Dependency Review API client. + /// + /// An API connection + public DependencyReviewClient(IApiConnection apiConnection) : base(apiConnection) + { + } + + /// + /// Gets all s for the specified repository. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The base revision + /// The head revision + /// Thrown when a general API error occurs. + [ManualRoute("GET", "/repos/{owner}/{repo}/dependency-graph/compare/{base}...{head}")] + public Task> GetAll(string owner, string name, string @base, string head) + { + Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner)); + Ensure.ArgumentNotNullOrEmptyString(name, nameof(name)); + Ensure.ArgumentNotNullOrEmptyString(@base, nameof(@base)); + Ensure.ArgumentNotNullOrEmptyString(head, nameof(head)); + + return ApiConnection.GetAll(ApiUrls.DependencyReview(owner, name, @base, head)); + } + + /// + /// Gets all s for the specified repository. + /// + /// + /// See the API documentation for more information. + /// + /// The Id of the repository + /// The base revision + /// The head revision + /// Thrown when a general API error occurs. + [ManualRoute("GET", "/repositories/{id}/dependency-graph/compare/{base}...{head}")] + public Task> GetAll(long repositoryId, string @base, string head) + { + Ensure.ArgumentNotNullOrEmptyString(@base, nameof(@base)); + Ensure.ArgumentNotNullOrEmptyString(head, nameof(head)); + + return ApiConnection.GetAll(ApiUrls.DependencyReview(repositoryId, @base, head)); + } + } +} diff --git a/Octokit/Clients/DependencySubmissionClient.cs b/Octokit/Clients/DependencySubmissionClient.cs new file mode 100644 index 0000000000..968972e84a --- /dev/null +++ b/Octokit/Clients/DependencySubmissionClient.cs @@ -0,0 +1,127 @@ +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's Dependency Submission API. + /// + /// + /// See the Dependency Submission API documentation for more details. + /// + public class DependencySubmissionClient : ApiClient, IDependencySubmissionClient + { + /// + /// Initializes a new GitHub Dependency Submission API client. + /// + /// An API connection + public DependencySubmissionClient(IApiConnection apiConnection) : base(apiConnection) { } + + /// + /// Creates a new dependency snapshot. + /// + /// + /// See the API documentation for more information. + /// + /// The repository's owner + /// The repository's name + /// The dependency snapshot to create + /// Thrown when a general API error occurs + /// A instance for the created snapshot + [ManualRoute("POST", "/repos/{owner}/{repo}/dependency-graph/snapshots")] + public Task Create(string owner, string name, NewDependencySnapshot snapshot) + { + Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner)); + Ensure.ArgumentNotNullOrEmptyString(name, nameof(name)); + Ensure.ArgumentNotNull(snapshot, nameof(snapshot)); + + var newDependencySnapshotAsObject = ConvertToJsonObject(snapshot); + + return ApiConnection.Post(ApiUrls.DependencySubmission(owner, name), newDependencySnapshotAsObject); + } + + /// + /// Creates a new dependency snapshot. + /// + /// + /// See the API documentation for more information. + /// + /// The Id of the repository + /// The dependency snapshot to create + /// Thrown when a general API error occurs + /// A instance for the created snapshot + [ManualRoute("POST", "/repositories/{id}/dependency-graph/snapshots")] + public Task Create(long repositoryId, NewDependencySnapshot snapshot) + { + Ensure.ArgumentNotNull(snapshot, nameof(snapshot)); + + var newDependencySnapshotAsObject = ConvertToJsonObject(snapshot); + + return ApiConnection.Post(ApiUrls.DependencySubmission(repositoryId), newDependencySnapshotAsObject); + } + + /// + /// Dependency snapshots dictionaries such as Manifests need to be passed as JsonObject in order to be serialized correctly + /// + private JsonObject ConvertToJsonObject(NewDependencySnapshot snapshot) + { + var newSnapshotAsObject = new JsonObject(); + newSnapshotAsObject.Add("version", snapshot.Version); + newSnapshotAsObject.Add("sha", snapshot.Sha); + newSnapshotAsObject.Add("ref", snapshot.Ref); + newSnapshotAsObject.Add("scanned", snapshot.Scanned); + newSnapshotAsObject.Add("job", snapshot.Job); + newSnapshotAsObject.Add("detector", snapshot.Detector); + + if (snapshot.Metadata != null) + { + var metadataAsObject = new JsonObject(); + foreach (var kvp in snapshot.Metadata) + { + metadataAsObject.Add(kvp.Key, kvp.Value); + } + + newSnapshotAsObject.Add("metadata", metadataAsObject); + } + + if (snapshot.Manifests != null) + { + var manifestsAsObject = new JsonObject(); + foreach (var manifestKvp in snapshot.Manifests) + { + var manifest = manifestKvp.Value; + + var manifestAsObject = new JsonObject(); + manifestAsObject.Add("name", manifest.Name); + + if (manifest.File.SourceLocation != null) + { + var manifestFileAsObject = new { SourceLocation = manifest.File.SourceLocation }; + manifestAsObject.Add("file", manifestFileAsObject); + } + + if (manifest.Metadata != null) + { + manifestAsObject.Add("metadata", manifest.Metadata); + } + + if (manifest.Resolved != null) + { + var resolvedAsObject = new JsonObject(); + foreach (var resolvedKvp in manifest.Resolved) + { + resolvedAsObject.Add(resolvedKvp.Key, resolvedKvp.Value); + } + + manifestAsObject.Add("resolved", resolvedAsObject); + } + + manifestsAsObject.Add(manifestKvp.Key, manifestAsObject); + } + + newSnapshotAsObject.Add("manifests", manifestsAsObject); + } + + return newSnapshotAsObject; + } + } +} diff --git a/Octokit/Clients/IDependencyGraphClient.cs b/Octokit/Clients/IDependencyGraphClient.cs new file mode 100644 index 0000000000..4729ae8b28 --- /dev/null +++ b/Octokit/Clients/IDependencyGraphClient.cs @@ -0,0 +1,21 @@ +namespace Octokit +{ + /// + /// A client for GitHub's Dependency Graph API. + /// + /// + /// See the Git Dependency Graph API documentation for more information. + /// + public interface IDependencyGraphClient + { + /// + /// Client for getting a dependency comparison between two commits. + /// + IDependencyReviewClient DependencyReview { get; } + + /// + /// Client for submitting dependency snapshots. + /// + IDependencySubmissionClient DependencySubmission { get; } + } +} diff --git a/Octokit/Clients/IDependencyReviewClient.cs b/Octokit/Clients/IDependencyReviewClient.cs new file mode 100644 index 0000000000..9e6a6e63d7 --- /dev/null +++ b/Octokit/Clients/IDependencyReviewClient.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's Dependency Review API. + /// + /// + /// See the Git Dependency Review API documentation for more information. + /// + public interface IDependencyReviewClient + { + /// + /// Gets all s for the specified repository. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The base revision + /// The head revision + /// Thrown when a general API error occurs. + [ExcludeFromPaginationApiOptionsConventionTest("Pagination not supported by GitHub API.")] + Task> GetAll(string owner, string name, string @base, string head); + + /// + /// Gets all s for the specified repository. + /// + /// + /// See the API documentation for more information. + /// + /// The Id of the repository + /// The base revision + /// The head revision + /// Thrown when a general API error occurs. + [ExcludeFromPaginationApiOptionsConventionTest("Pagination not supported by GitHub API.")] + Task> GetAll(long repositoryId, string @base, string head); + } +} diff --git a/Octokit/Clients/IDependencySubmissionClient.cs b/Octokit/Clients/IDependencySubmissionClient.cs new file mode 100644 index 0000000000..4d51b87b76 --- /dev/null +++ b/Octokit/Clients/IDependencySubmissionClient.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's Dependency Submission API. + /// + /// + /// See the Dependency Submission API documentation for more details. + /// + public interface IDependencySubmissionClient + { + /// + /// Creates a new dependency snapshot. + /// + /// + /// See the API documentation for more information. + /// + /// The repository's owner + /// The repository's name + /// The dependency snapshot to create + /// Thrown when a general API error occurs + /// A instance for the created snapshot + Task Create(string owner, string name, NewDependencySnapshot snapshot); + + /// + /// Creates a new dependency snapshot. + /// + /// + /// See the API documentation for more information. + /// + /// The Id of the repository + /// The dependency snapshot to create + /// Thrown when a general API error occurs + /// A instance for the created snapshot + Task Create(long repositoryId, NewDependencySnapshot snapshot); + } +} diff --git a/Octokit/GitHubClient.cs b/Octokit/GitHubClient.cs index d7b8bea36f..97f0474493 100644 --- a/Octokit/GitHubClient.cs +++ b/Octokit/GitHubClient.cs @@ -122,6 +122,7 @@ public GitHubClient(IConnection connection) Actions = new ActionsClient(apiConnection); Codespaces = new CodespacesClient(apiConnection); Copilot = new CopilotClient(apiConnection); + DependencyGraph = new DependencyGraphClient(apiConnection); } /// @@ -396,12 +397,17 @@ public Uri BaseAddress public IActionsClient Actions { get; private set; } public ICodespacesClient Codespaces { get; private set; } - + /// /// Access GitHub's Copilot for Business API /// public ICopilotClient Copilot { get; private set; } + /// + /// Access GitHub's Dependency Graph API + /// + public IDependencyGraphClient DependencyGraph { get; private set; } + static Uri FixUpBaseUri(Uri uri) { Ensure.ArgumentNotNull(uri, nameof(uri)); diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index 06849637ec..44eefc029e 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -922,7 +922,7 @@ public static Uri OrganizationMemberships(string org, string name) { return "orgs/{0}/memberships/{1}".FormatUri(org, name); } - + /// /// Returns the for the organization's invitations /// @@ -2568,6 +2568,30 @@ public static Uri RepositoryVulnerabilityAlerts(string owner, string name) return "repos/{0}/{1}/vulnerability-alerts".FormatUri(owner, name); } + /// + /// Returns the for getting Dependency-Diffs between two revisions. + /// + /// The owner of the repository + /// The name of the repository + /// The base revision + /// The head revision + /// The for getting Dependency-Diffs between two revisions for the given repository. + public static Uri DependencyReview(string owner, string name, string @base, string head) + { + return "repos/{0}/{1}/dependency-graph/compare/{2}...{3}".FormatUri(owner, name, @base, head); + } + + /// + /// Returns the for submitting a Dependency Snapshot for the given repository. + /// + /// The owner of the repository + /// The name of the repository + /// The for submitting a Dependency Snapshot for the given repository. + public static Uri DependencySubmission(string owner, string name) + { + return "repos/{0}/{1}/dependency-graph/snapshots".FormatUri(owner, name); + } + /// /// Returns the for the Deployments API for the given repository. /// @@ -3244,6 +3268,28 @@ public static Uri CreateTag(long repositoryId) return "repositories/{0}/git/tags".FormatUri(repositoryId); } + /// + /// Returns the for getting Dependency-Diffs between two revisions. + /// + /// The Id of the repository + /// The base revision + /// The head revision + /// The for getting Dependency-Diffs between two revisions for the given repository. + public static Uri DependencyReview(long repositoryId, string @base, string head) + { + return "repositories/{0}/dependency-graph/compare/{1}...{2}".FormatUri(repositoryId, @base, head); + } + + /// + /// Returns the for submitting a Dependency Snapshot for the given repository. + /// + /// The Id of the repository + /// The for submitting a Dependency Snapshot for the given repository. + public static Uri DependencySubmission(long repositoryId) + { + return "repositories/{0}/dependency-graph/snapshots".FormatUri(repositoryId); + } + /// /// Returns the for the Deployments API for the given repository. /// @@ -5502,7 +5548,7 @@ public static Uri ActionsListOrganizationRunnerGroupRepositories(string org, lon { return "orgs/{0}/actions/runner-groups/{1}/repositories".FormatUri(org, runnerGroupId); } - + /// /// Returns the that handles adding or removing of copilot licenses for an organisation /// @@ -5512,7 +5558,7 @@ public static Uri CopilotBillingLicense(string org) { return $"orgs/{org}/copilot/billing/selected_users".FormatUri(org); } - + /// /// Returns the that handles reading copilot billing settings for an organization /// @@ -5522,7 +5568,7 @@ public static Uri CopilotBillingSettings(string org) { return $"orgs/{org}/copilot/billing".FormatUri(org); } - + /// /// Returns the that allows for searching across all licenses for an organisation /// @@ -5532,7 +5578,7 @@ public static Uri CopilotAllocatedLicenses(string org) { return $"orgs/{org}/copilot/billing/seats".FormatUri(org); } - + public static Uri Codespaces() { return _currentUserAllCodespaces; diff --git a/Octokit/IGitHubClient.cs b/Octokit/IGitHubClient.cs index 3ff9f320c7..643aac636a 100644 --- a/Octokit/IGitHubClient.cs +++ b/Octokit/IGitHubClient.cs @@ -217,10 +217,15 @@ public interface IGitHubClient : IApiInfoProvider IEmojisClient Emojis { get; } ICodespacesClient Codespaces { get; } - + /// /// Access to the GitHub Copilot for Business API /// ICopilotClient Copilot { get; } + + /// + /// Access GitHub's Dependency Graph API + /// + IDependencyGraphClient DependencyGraph { get; } } } diff --git a/Octokit/Models/Request/NewDependencySnapshot.cs b/Octokit/Models/Request/NewDependencySnapshot.cs new file mode 100644 index 0000000000..fc259203a4 --- /dev/null +++ b/Octokit/Models/Request/NewDependencySnapshot.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + /// + /// Describes a new dependency snapshot to create via the method. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class NewDependencySnapshot + { + /// + /// Creates a new Dependency Snapshot. + /// + /// Required. The version of the repository snapshot submission. + /// Required. The commit SHA associated with this dependency snapshot. Maximum length: 40 characters. + /// Required. The repository branch that triggered this snapshot. + /// Required. The time at which the snapshot was scanned. + /// Required. The job associated with this dependency snapshot. + /// Required. A description of the detector used. + public NewDependencySnapshot(long version, string sha, string @ref, string scanned, NewDependencySnapshotJob job, NewDependencySnapshotDetector detector) + { + Ensure.ArgumentNotNullOrEmptyString(sha, nameof(sha)); + Ensure.ArgumentNotNullOrEmptyString(@ref, nameof(@ref)); + Ensure.ArgumentNotNullOrEmptyString(scanned, nameof(scanned)); + Ensure.ArgumentNotNull(job, nameof(job)); + Ensure.ArgumentNotNull(detector, nameof(detector)); + + Version = version; + Sha = sha; + Ref = @ref; + Scanned = scanned; + Job = job; + Detector = detector; + } + + /// + /// Required. The version of the repository snapshot submission. + /// + public long Version { get; private set; } + + /// + /// Required. The job associated with this dependency snapshot. + /// + public NewDependencySnapshotJob Job { get; private set; } + + /// + /// Required. The commit SHA associated with this dependency snapshot. Maximum length: 40 characters. + /// + public string Sha { get; private set; } + + /// + /// Required. The repository branch that triggered this snapshot. + /// + public string Ref { get; private set; } + + /// + /// Required. A description of the detector used. + /// + public NewDependencySnapshotDetector Detector { get; private set; } + + /// + /// Optional. User-defined metadata to store domain-specific information limited to 8 keys with scalar values. + /// + public IDictionary Metadata { get; set; } + + /// + /// Optional. A collection of package manifests. + /// + public IDictionary Manifests { get; set; } + + /// + /// Required. The time at which the snapshot was scanned. + /// + public string Scanned { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Sha: {0}, Version: {1}", Sha, Version); + } + } + } +} diff --git a/Octokit/Models/Request/NewDependencySnapshotDetector.cs b/Octokit/Models/Request/NewDependencySnapshotDetector.cs new file mode 100644 index 0000000000..00f562b6e6 --- /dev/null +++ b/Octokit/Models/Request/NewDependencySnapshotDetector.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class NewDependencySnapshotDetector + { + /// + /// Creates a new Dependency Snapshot Detector. + /// + /// Required. The name of the detector used. + /// Required. The version of the detector used. + /// Required. The url of the detector used. + public NewDependencySnapshotDetector(string name, string version, string url) + { + Name = name; + Version = version; + Url = url; + } + + /// + /// Required. The name of the detector used. + /// + public string Name { get; private set; } + + /// + /// Required. The version of the detector used. + /// + public string Version { get; private set; } + + /// + /// Required. The url of the detector used. + /// + public string Url { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Name: {0}, Version: {1}", Name, Version); + } + } + } +} diff --git a/Octokit/Models/Request/NewDependencySnapshotJob.cs b/Octokit/Models/Request/NewDependencySnapshotJob.cs new file mode 100644 index 0000000000..9ee9163860 --- /dev/null +++ b/Octokit/Models/Request/NewDependencySnapshotJob.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class NewDependencySnapshotJob + { + /// + /// Creates a new Dependency Snapshot Job. + /// + /// Required. The external ID of the job. + /// Required. Correlator that is used to group snapshots submitted over time. + public NewDependencySnapshotJob(string id, string correlator) + { + Ensure.ArgumentNotNullOrEmptyString(correlator, nameof(correlator)); + + Id = id; + Correlator = correlator; + } + + /// + /// Required. The external ID of the job. + /// + public string Id { get; private set; } + + /// + /// Required. Correlator that is used to group snapshots submitted over time. + /// + public string Correlator { get; private set; } + + /// + /// Optional. The URL for the job. + /// + public string HtmlUrl { get; set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Id: {0}, HtmlUrl: {1}", Id, HtmlUrl); + } + } + } +} diff --git a/Octokit/Models/Request/NewDependencySnapshotManifest.cs b/Octokit/Models/Request/NewDependencySnapshotManifest.cs new file mode 100644 index 0000000000..5fca4a4e72 --- /dev/null +++ b/Octokit/Models/Request/NewDependencySnapshotManifest.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class NewDependencySnapshotManifest + { + /// + /// Creates a new Dependency Manifest Item. + /// + /// Required. The name of the manifest. + public NewDependencySnapshotManifest(string name) + { + Ensure.ArgumentNotNullOrEmptyString(name, nameof(name)); + + Name = name; + } + + /// + /// Required. The name of the manifest. + /// + public string Name { get; private set; } + + /// + /// Optional. The manifest file. + /// + public NewDependencySnapshotManifestFile File { get; set; } + + /// + /// Optional. User-defined metadata to store domain-specific information limited to 8 keys with scalar values. + /// + public IDictionary Metadata { get; set; } + + /// + /// Optional. A collection of resolved package dependencies. + /// + public IDictionary Resolved { get; set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Name: {0}", Name); + } + } + } +} diff --git a/Octokit/Models/Request/NewDependencySnapshotManifestFile.cs b/Octokit/Models/Request/NewDependencySnapshotManifestFile.cs new file mode 100644 index 0000000000..3f4ac22671 --- /dev/null +++ b/Octokit/Models/Request/NewDependencySnapshotManifestFile.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class NewDependencySnapshotManifestFile + { + /// + /// Optional. The path of the manifest file relative to the root of the Git repository. + /// + public string SourceLocation { get; set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Source Location: {0}", SourceLocation); + } + } + } +} diff --git a/Octokit/Models/Request/NewDependencySnapshotResolvedDependency.cs b/Octokit/Models/Request/NewDependencySnapshotResolvedDependency.cs new file mode 100644 index 0000000000..ab48314a7a --- /dev/null +++ b/Octokit/Models/Request/NewDependencySnapshotResolvedDependency.cs @@ -0,0 +1,75 @@ +using Octokit.Internal; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class NewDependencySnapshotResolvedDependency + { + /// + /// Optional. Package-url (PURL) of the dependency. + /// + public string PackageUrl { get; set; } + + /// + /// Optional. User-defined metadata to store domain-specific information limited to 8 keys with scalar values.. + /// + public IDictionary Metadata { get; set; } + + /// + /// Optional. A notation of whether a dependency is requested directly by this manifest or is a dependency of another dependency. + /// + public ResolvedPackageKeyRelationship Relationship { get; set; } + + /// + /// Optional. A notation of whether the dependency is required for the primary build artifact (runtime) or is only used for development. + /// + public ResolvedPackageKeyScope Scope { get; set; } + + /// + /// Optional. Array of package-url (PURLs) of direct child dependencies. + /// + public Collection Dependencies { get; set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Package Url: {0}", PackageUrl); + } + } + } + + public enum ResolvedPackageKeyRelationship + { + /// + /// The dependency is requested directly by the manifest. + /// + [Parameter(Value = "direct")] + Direct, + + /// + /// The dependency is a dependency of another dependency. + /// + [Parameter(Value = "indirect")] + Indirect, + } + + public enum ResolvedPackageKeyScope + { + /// + /// The dependency is required for the primary build artifact. + /// + [Parameter(Value = "runtime")] + Runtime, + + /// + /// The dependency is only used for development. + /// + [Parameter(Value = "development")] + Development, + } +} diff --git a/Octokit/Models/Response/ChangeType.cs b/Octokit/Models/Response/ChangeType.cs new file mode 100644 index 0000000000..8daf475475 --- /dev/null +++ b/Octokit/Models/Response/ChangeType.cs @@ -0,0 +1,13 @@ +using Octokit.Internal; + +namespace Octokit +{ + public enum ChangeType + { + [Parameter(Value = "Added")] + Added, + + [Parameter(Value = "Removed")] + Removed + } +} \ No newline at end of file diff --git a/Octokit/Models/Response/DependencyDiff.cs b/Octokit/Models/Response/DependencyDiff.cs new file mode 100644 index 0000000000..3521d55107 --- /dev/null +++ b/Octokit/Models/Response/DependencyDiff.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class DependencyDiff + { + public DependencyDiff() { } + + public DependencyDiff(ChangeType changeType, string manifest, string ecosystem, string name, string version, string packageUrl, string license, string sourceRepositoryUrl, IReadOnlyList vulnerabilities, Scope scope) + { + ChangeType = changeType; + Manifest = manifest; + Ecosystem = ecosystem; + Name = name; + Version = version; + PackageUrl = packageUrl; + License = license; + SourceRepositoryUrl = sourceRepositoryUrl; + Vulnerabilities = vulnerabilities; + Scope = scope; + } + + /// + /// The type of the change. + /// + public StringEnum ChangeType { get; private set; } + + /// + /// The package manifest of the dependency. + /// + public string Manifest { get; private set; } + + /// + /// The package ecosystem of the dependency. + /// + public string Ecosystem { get; private set; } + + /// + /// The package name of the dependency. + /// + public string Name { get; private set; } + + /// + /// The package version of the dependency. + /// + public string Version { get; private set; } + + /// + /// The package URL of the dependency. + /// + public string PackageUrl { get; private set; } + + /// + /// The license of the dependency. + /// + public string License { get; private set; } + + /// + /// The URL of the source repository of the dependency. + /// + public string SourceRepositoryUrl { get; private set; } + + /// + /// A list of vulnerabilities for the dependency. + /// + public IReadOnlyList Vulnerabilities { get; private set; } + + /// + /// The scope of the dependency. + /// + public StringEnum Scope { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Name: {0}, Version: {1}", Name, Version); + } + } + } + + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class DependencyVulnerability + { + public DependencyVulnerability() { } + + public DependencyVulnerability(string severity, string advisoryGhsaId, string advisorySummary, string advisoryUrl) + { + Severity = severity; + AdvisoryGhsaId = advisoryGhsaId; + AdvisorySummary = advisorySummary; + AdvisoryUrl = advisoryUrl; + } + + /// + /// The severity of the vulnerability. + /// + public string Severity { get; private set; } + + /// + /// The GHSA Id of the advisory. + /// + public string AdvisoryGhsaId { get; private set; } + + /// + /// "A summary of the advisory." + /// + public string AdvisorySummary { get; private set; } + + /// + /// The URL of the advisory. + /// + public string AdvisoryUrl { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Advisory URL: {0}", AdvisoryUrl); + } + } + } + +} diff --git a/Octokit/Models/Response/DependencySnapshotSubmission.cs b/Octokit/Models/Response/DependencySnapshotSubmission.cs new file mode 100644 index 0000000000..c40403b789 --- /dev/null +++ b/Octokit/Models/Response/DependencySnapshotSubmission.cs @@ -0,0 +1,70 @@ +using Octokit.Internal; +using System; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class DependencySnapshotSubmission + { + public DependencySnapshotSubmission() { } + + public DependencySnapshotSubmission(long id, DateTimeOffset createdAt, DependencySnapshotSubmissionResult result, string message) + { + Id = id; + CreatedAt = createdAt; + Result = result; + Message = message; + } + + /// + /// ID of the created snapshot. + /// + public long Id { get; private set; } + + /// + /// The time at which the snapshot was created. + /// + public DateTimeOffset CreatedAt { get; private set; } + + /// + /// The outcome of the dependency snapshot creation. + /// + public StringEnum Result { get; private set; } + + /// + /// A message providing further details about the result, such as why the dependencies were not updated. + /// + public string Message { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Id: {0} Created at: {1}", Id, CreatedAt); + } + } + } + + public enum DependencySnapshotSubmissionResult + { + /// + /// The snapshot was successfully created and the repository's dependencies were updated. + /// + [Parameter(Value = "Success")] + Success, + + /// + /// The snapshot was successfully created, but the repository's dependencies were not updated. + /// + [Parameter(Value = "Accepted")] + Accepted, + + /// + /// The snapshot was malformed. + /// + [Parameter(Value = "Invalid")] + Invalid + } +} diff --git a/Octokit/Models/Response/Scope.cs b/Octokit/Models/Response/Scope.cs new file mode 100644 index 0000000000..2d687b3849 --- /dev/null +++ b/Octokit/Models/Response/Scope.cs @@ -0,0 +1,25 @@ +using Octokit.Internal; + +namespace Octokit +{ + public enum Scope + { + /// + /// The scope of the dependency is not specified or cannot be determined. + /// + [Parameter(Value = "Unknown")] + Unknown, + + /// + /// Dependency is utilized at runtime and in the development environment. + /// + [Parameter(Value = "Runtime")] + Runtime, + + /// + /// Dependency is only utilized in the development environment. + /// + [Parameter(Value = "Development")] + Development, + } +}