From c0084d2892a413a258e77fa750b96b33957b3ddb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Jun 2025 04:24:44 +0000 Subject: [PATCH 1/8] Initial plan for issue From 30e4475eeee3da983827568f33d443d3ec7cdee2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Jun 2025 04:28:42 +0000 Subject: [PATCH 2/8] Initial plan for Dependabot Alert Dismissal support Co-authored-by: dylan-smith <1508559+dylan-smith@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index f70e88ddb..fdaac1bc6 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.404", + "version": "8.0.115", "rollForward": "minor" } } \ No newline at end of file From 66452307387aa36c427c939aca62cac737ec99c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Jun 2025 04:36:39 +0000 Subject: [PATCH 3/8] Add core Dependabot Alert migration functionality Co-authored-by: dylan-smith <1508559+dylan-smith@users.noreply.github.com> --- src/Octoshift/DependabotAlertState.cs | 23 ++++ src/Octoshift/Models/DependabotAlert.cs | 41 +++++++ .../Services/DependabotAlertService.cs | 112 ++++++++++++++++++ src/Octoshift/Services/GithubApi.cs | 77 ++++++++++++ .../MigrateDependabotAlertsCommand.cs | 95 +++++++++++++++ .../MigrateDependabotAlertsCommandArgs.cs | 30 +++++ .../MigrateDependabotAlertsCommandHandler.cs | 40 +++++++ .../DependabotAlertServiceFactory.cs | 37 ++++++ src/gei/Program.cs | 1 + 9 files changed, 456 insertions(+) create mode 100644 src/Octoshift/DependabotAlertState.cs create mode 100644 src/Octoshift/Models/DependabotAlert.cs create mode 100644 src/Octoshift/Services/DependabotAlertService.cs create mode 100644 src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs create mode 100644 src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandArgs.cs create mode 100644 src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandHandler.cs create mode 100644 src/gei/Factories/DependabotAlertServiceFactory.cs diff --git a/src/Octoshift/DependabotAlertState.cs b/src/Octoshift/DependabotAlertState.cs new file mode 100644 index 000000000..d9b4d47f7 --- /dev/null +++ b/src/Octoshift/DependabotAlertState.cs @@ -0,0 +1,23 @@ +namespace OctoshiftCLI; + +public static class DependabotAlertState +{ + public const string Open = "open"; + public const string Dismissed = "dismissed"; + public const string Fixed = "fixed"; + + // Dismissal reasons for Dependabot alerts + public const string FalsePositive = "false_positive"; + public const string Inaccurate = "inaccurate"; + public const string NotUsed = "not_used"; + public const string NoBandwidth = "no_bandwidth"; + public const string TolerableRisk = "tolerable_risk"; + + public static bool IsOpenOrDismissed(string state) => state?.Trim().ToLower() is Open or Dismissed; + + public static bool IsDismissed(string state) => state?.Trim().ToLower() is Dismissed; + + public static bool IsValidDismissedReason(string reason) => + reason?.Trim().ToLower() is + FalsePositive or Inaccurate or NotUsed or NoBandwidth or TolerableRisk; +} \ No newline at end of file diff --git a/src/Octoshift/Models/DependabotAlert.cs b/src/Octoshift/Models/DependabotAlert.cs new file mode 100644 index 000000000..efd540fa7 --- /dev/null +++ b/src/Octoshift/Models/DependabotAlert.cs @@ -0,0 +1,41 @@ +namespace Octoshift.Models; + +public class DependabotAlert +{ + public int Number { get; set; } + public string State { get; set; } + public string DismissedReason { get; set; } + public string DismissedComment { get; set; } + public string DismissedAt { get; set; } + public DependabotAlertDependency Dependency { get; set; } + public DependabotAlertSecurityAdvisory SecurityAdvisory { get; set; } + public DependabotAlertSecurityVulnerability SecurityVulnerability { get; set; } + public string Url { get; set; } + public string HtmlUrl { get; set; } + public string CreatedAt { get; set; } + public string UpdatedAt { get; set; } +} + +public class DependabotAlertDependency +{ + public string Package { get; set; } + public string Manifest { get; set; } + public string Scope { get; set; } +} + +public class DependabotAlertSecurityAdvisory +{ + public string GhsaId { get; set; } + public string CveId { get; set; } + public string Summary { get; set; } + public string Description { get; set; } + public string Severity { get; set; } +} + +public class DependabotAlertSecurityVulnerability +{ + public string Package { get; set; } + public string Severity { get; set; } + public string VulnerableVersionRange { get; set; } + public string FirstPatchedVersion { get; set; } +} \ No newline at end of file diff --git a/src/Octoshift/Services/DependabotAlertService.cs b/src/Octoshift/Services/DependabotAlertService.cs new file mode 100644 index 000000000..156b7597b --- /dev/null +++ b/src/Octoshift/Services/DependabotAlertService.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Octoshift.Models; +using OctoshiftCLI.Models; + +namespace OctoshiftCLI.Services; + +public class DependabotAlertService +{ + private readonly GithubApi _sourceGithubApi; + private readonly GithubApi _targetGithubApi; + private readonly OctoLogger _log; + + public DependabotAlertService(GithubApi sourceGithubApi, GithubApi targetGithubApi, OctoLogger octoLogger) + { + _sourceGithubApi = sourceGithubApi; + _targetGithubApi = targetGithubApi; + _log = octoLogger; + } + + public virtual async Task MigrateDependabotAlerts(string sourceOrg, string sourceRepo, string targetOrg, + string targetRepo, bool dryRun) + { + _log.LogInformation($"Migrating Dependabot Alerts from '{sourceOrg}/{sourceRepo}' to '{targetOrg}/{targetRepo}'"); + + var sourceAlerts = (await _sourceGithubApi.GetDependabotAlertsForRepository(sourceOrg, sourceRepo)).ToList(); + + // no reason to call the target on a dry run - there will be no alerts + var targetAlerts = dryRun ? + [] : + (await _targetGithubApi.GetDependabotAlertsForRepository(targetOrg, targetRepo)).ToList(); + + var successCount = 0; + var skippedCount = 0; + var notFoundCount = 0; + + _log.LogInformation($"Found {sourceAlerts.Count} source and {targetAlerts.Count} target alerts. Starting migration of alert states..."); + + foreach (var sourceAlert in sourceAlerts) + { + if (!DependabotAlertState.IsOpenOrDismissed(sourceAlert.State)) + { + _log.LogInformation($" skipping alert {sourceAlert.Number} ({sourceAlert.Url}) because state '{sourceAlert.State}' is not migratable."); + skippedCount++; + continue; + } + + if (dryRun) + { + _log.LogInformation($" running in dry-run mode. Would have tried to find target alert for {sourceAlert.Number} ({sourceAlert.Url}) and set state '{sourceAlert.State}'"); + successCount++; + // No sense in continuing here, because we don't have the target alert as it is not migrated in dryRun mode + continue; + } + + var matchingTargetAlert = FindMatchingTargetAlert(targetAlerts, sourceAlert); + + if (matchingTargetAlert == null) + { + _log.LogError($" could not find a target alert for {sourceAlert.Number} ({sourceAlert.Url})."); + notFoundCount++; + continue; + } + + if (matchingTargetAlert.State == sourceAlert.State) + { + _log.LogInformation(" skipping alert because target alert already has the same state."); + skippedCount++; + continue; + } + + _log.LogVerbose($"Setting Status {sourceAlert.State} for target alert {matchingTargetAlert.Number} ({matchingTargetAlert.Url})"); + await _targetGithubApi.UpdateDependabotAlert( + targetOrg, + targetRepo, + matchingTargetAlert.Number, + sourceAlert.State, + sourceAlert.DismissedReason, + sourceAlert.DismissedComment + ); + successCount++; + } + + _log.LogInformation($"Dependabot Alerts done!\nStatus of {sourceAlerts.Count} Alerts:\n Success: {successCount}\n Skipped (status not migratable or already matches): {skippedCount}\n No matching target found (see logs): {notFoundCount}."); + + if (notFoundCount > 0) + { + throw new OctoshiftCliException("Migration of Dependabot Alerts failed."); + } + } + + private DependabotAlert FindMatchingTargetAlert(List targetAlerts, DependabotAlert sourceAlert) + { + // Try to match based on the security advisory GHSA ID and package name + var matchingAlert = targetAlerts.FirstOrDefault(targetAlert => + targetAlert.SecurityAdvisory?.GhsaId == sourceAlert.SecurityAdvisory?.GhsaId && + targetAlert.Dependency?.Package == sourceAlert.Dependency?.Package && + targetAlert.Dependency?.Manifest == sourceAlert.Dependency?.Manifest); + + if (matchingAlert != null) + { + return matchingAlert; + } + + // Fall back to matching by CVE ID if GHSA ID doesn't match + return targetAlerts.FirstOrDefault(targetAlert => + targetAlert.SecurityAdvisory?.CveId == sourceAlert.SecurityAdvisory?.CveId && + targetAlert.Dependency?.Package == sourceAlert.Dependency?.Package && + targetAlert.Dependency?.Manifest == sourceAlert.Dependency?.Manifest); + } +} \ No newline at end of file diff --git a/src/Octoshift/Services/GithubApi.cs b/src/Octoshift/Services/GithubApi.cs index 051c06b1f..a2237ced0 100644 --- a/src/Octoshift/Services/GithubApi.cs +++ b/src/Octoshift/Services/GithubApi.cs @@ -1039,6 +1039,39 @@ public virtual async Task> GetCodeScannin .ToListAsync(); } + public virtual async Task> GetDependabotAlertsForRepository(string org, string repo) + { + var url = $"{_apiUrl}/repos/{org.EscapeDataString()}/{repo.EscapeDataString()}/dependabot/alerts?per_page=100"; + return await _client.GetAllAsync(url) + .Select(BuildDependabotAlert) + .ToListAsync(); + } + + public virtual async Task UpdateDependabotAlert(string org, string repo, int alertNumber, string state, string dismissedReason = null, string dismissedComment = null) + { + if (!DependabotAlertState.IsOpenOrDismissed(state)) + { + throw new ArgumentException($"Invalid value for {nameof(state)}"); + } + + if (DependabotAlertState.IsDismissed(state) && !DependabotAlertState.IsValidDismissedReason(dismissedReason)) + { + throw new ArgumentException($"Invalid value for {nameof(dismissedReason)}"); + } + + var url = $"{_apiUrl}/repos/{org.EscapeDataString()}/{repo.EscapeDataString()}/dependabot/alerts/{alertNumber}"; + + var payload = state == "open" + ? (new { state }) + : (object)(new + { + state, + dismissed_reason = dismissedReason, + dismissed_comment = dismissedComment ?? string.Empty + }); + await _client.PatchAsync(url, payload); + } + public virtual async Task GetEnterpriseServerVersion() { var url = $"{_apiUrl}/meta"; @@ -1246,4 +1279,48 @@ private static CodeScanningAlertInstance BuildCodeScanningAlertInstance(JToken s StartColumn = (int)scanningAlertInstance["location"]["start_column"], EndColumn = (int)scanningAlertInstance["location"]["end_column"] }; + + private static DependabotAlert BuildDependabotAlert(JToken dependabotAlert) => + new() + { + Number = (int)dependabotAlert["number"], + State = (string)dependabotAlert["state"], + DismissedReason = dependabotAlert.Value("dismissed_reason"), + DismissedComment = dependabotAlert.Value("dismissed_comment"), + DismissedAt = dependabotAlert.Value("dismissed_at"), + Url = (string)dependabotAlert["url"], + HtmlUrl = (string)dependabotAlert["html_url"], + CreatedAt = (string)dependabotAlert["created_at"], + UpdatedAt = (string)dependabotAlert["updated_at"], + Dependency = BuildDependabotAlertDependency(dependabotAlert["dependency"]), + SecurityAdvisory = BuildDependabotAlertSecurityAdvisory(dependabotAlert["security_advisory"]), + SecurityVulnerability = BuildDependabotAlertSecurityVulnerability(dependabotAlert["security_vulnerability"]) + }; + + private static DependabotAlertDependency BuildDependabotAlertDependency(JToken dependency) => + new() + { + Package = (string)dependency["package"]?["name"], + Manifest = (string)dependency["manifest_path"], + Scope = (string)dependency["scope"] + }; + + private static DependabotAlertSecurityAdvisory BuildDependabotAlertSecurityAdvisory(JToken securityAdvisory) => + new() + { + GhsaId = (string)securityAdvisory["ghsa_id"], + CveId = (string)securityAdvisory["cve_id"], + Summary = (string)securityAdvisory["summary"], + Description = (string)securityAdvisory["description"], + Severity = (string)securityAdvisory["severity"] + }; + + private static DependabotAlertSecurityVulnerability BuildDependabotAlertSecurityVulnerability(JToken securityVulnerability) => + new() + { + Package = (string)securityVulnerability["package"]?["name"], + Severity = (string)securityVulnerability["severity"], + VulnerableVersionRange = (string)securityVulnerability["vulnerable_version_range"], + FirstPatchedVersion = (string)securityVulnerability["first_patched_version"]?["identifier"] + }; } diff --git a/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs new file mode 100644 index 000000000..241940dc2 --- /dev/null +++ b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs @@ -0,0 +1,95 @@ +using System; +using System.CommandLine; +using Microsoft.Extensions.DependencyInjection; +using OctoshiftCLI.Commands; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.GithubEnterpriseImporter.Factories; +using OctoshiftCLI.Services; + +namespace OctoshiftCLI.GithubEnterpriseImporter.Commands.MigrateDependabotAlerts; + +public class MigrateDependabotAlertsCommand : CommandBase +{ + public MigrateDependabotAlertsCommand() : base( + name: "migrate-dependabot-alerts", + description: "Migrates Dependabot alert states and dismissed-reasons. This lets you migrate the dismissal state of Dependabot alerts to the target repository.") + { + AddOption(SourceOrg); + AddOption(SourceRepo); + AddOption(TargetOrg); + AddOption(TargetRepo); + AddOption(TargetApiUrl); + + AddOption(GhesApiUrl); + AddOption(NoSslVerify); + + AddOption(GithubSourcePat); + AddOption(GithubTargetPat); + AddOption(Verbose); + AddOption(DryRun); + } + + public Option SourceOrg { get; } = new("--source-org") { IsRequired = true }; + public Option SourceRepo { get; } = new("--source-repo") { IsRequired = true }; + public Option TargetOrg { get; } = new("--target-org") { IsRequired = true }; + public Option TargetRepo { get; } = new("--target-repo") + { + Description = "Defaults to the name of source-repo" + }; + + public Option TargetApiUrl { get; } = new("--target-api-url") + { + Description = "The URL of the target GitHub instance API. Defaults to https://api.github.com" + }; + public Option GhesApiUrl { get; } = new("--ghes-api-url") + { + Description = "The URL of the GitHub Enterprise Server API. Defaults to the source-api-url if provided or https://api.github.com" + }; + public Option NoSslVerify { get; } = new("--no-ssl-verify"); + + public Option GithubSourcePat { get; } = new("--github-source-pat") + { + Description = "Personal access token to use when calling the GitHub source API. Defaults to GH_SOURCE_PAT environment variable." + }; + + public Option GithubTargetPat { get; } = new("--github-target-pat") + { + Description = "Personal access token to use when calling the GitHub target API. Defaults to GH_PAT environment variable." + }; + + public Option Verbose { get; } = new("--verbose"); + + public Option DryRun { get; } = new("--dry-run") + { + Description = + "Execute in dry run mode to see what alerts will be matched and changes applied, but do not make any actual changes." + }; + + public override MigrateDependabotAlertsCommandHandler BuildHandler(MigrateDependabotAlertsCommandArgs args, IServiceProvider sp) + { + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + if (sp is null) + { + throw new ArgumentNullException(nameof(sp)); + } + + var environmentVariableProvider = sp.GetRequiredService(); + args.GithubSourcePat ??= environmentVariableProvider.SourceGithubPersonalAccessToken(false); + args.GithubTargetPat ??= environmentVariableProvider.TargetGithubPersonalAccessToken(); + + if (args.GithubSourcePat.IsNullOrWhiteSpace()) + { + args.GithubSourcePat = args.GithubTargetPat; + } + + var log = sp.GetRequiredService(); + var dependabotAlertServiceFactory = sp.GetRequiredService(); + var dependabotAlertService = dependabotAlertServiceFactory.Create(args.GhesApiUrl, args.GithubSourcePat, args.TargetApiUrl, args.GithubTargetPat, args.NoSslVerify); + + return new MigrateDependabotAlertsCommandHandler(log, dependabotAlertService); + } +} \ No newline at end of file diff --git a/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandArgs.cs b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandArgs.cs new file mode 100644 index 000000000..ceeafb329 --- /dev/null +++ b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandArgs.cs @@ -0,0 +1,30 @@ +using OctoshiftCLI.Commands; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; + +namespace OctoshiftCLI.GithubEnterpriseImporter.Commands.MigrateDependabotAlerts; + +public class MigrateDependabotAlertsCommandArgs : CommandArgs +{ + public string SourceOrg { get; set; } + public string SourceRepo { get; set; } + public string TargetOrg { get; set; } + public string TargetRepo { get; set; } + public string TargetApiUrl { get; set; } + public string GhesApiUrl { get; set; } + public bool NoSslVerify { get; set; } + public bool DryRun { get; set; } + [Secret] + public string GithubSourcePat { get; set; } + [Secret] + public string GithubTargetPat { get; set; } + + public override void Validate(OctoLogger log) + { + if (SourceRepo.HasValue() && TargetRepo.IsNullOrWhiteSpace()) + { + TargetRepo = SourceRepo; + log?.LogInformation("Since target-repo is not provided, source-repo value will be used for target-repo."); + } + } +} \ No newline at end of file diff --git a/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandHandler.cs b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandHandler.cs new file mode 100644 index 000000000..5488a5a19 --- /dev/null +++ b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandHandler.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using OctoshiftCLI.Commands; +using OctoshiftCLI.Services; + +namespace OctoshiftCLI.GithubEnterpriseImporter.Commands.MigrateDependabotAlerts; + +public class MigrateDependabotAlertsCommandHandler : ICommandHandler +{ + private readonly OctoLogger _log; + private readonly DependabotAlertService _dependabotAlertService; + + public MigrateDependabotAlertsCommandHandler(OctoLogger log, DependabotAlertService dependabotAlertService) + { + _log = log; + _dependabotAlertService = dependabotAlertService; + } + + public async Task Handle(MigrateDependabotAlertsCommandArgs args) + { + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + _log.LogInformation("Migrating Repo Dependabot Alerts..."); + + await _dependabotAlertService.MigrateDependabotAlerts( + args.SourceOrg, + args.SourceRepo, + args.TargetOrg, + args.TargetRepo, + args.DryRun); + + if (!args.DryRun) + { + _log.LogSuccess($"Dependabot alerts successfully migrated."); + } + } +} \ No newline at end of file diff --git a/src/gei/Factories/DependabotAlertServiceFactory.cs b/src/gei/Factories/DependabotAlertServiceFactory.cs new file mode 100644 index 000000000..082665020 --- /dev/null +++ b/src/gei/Factories/DependabotAlertServiceFactory.cs @@ -0,0 +1,37 @@ +using OctoshiftCLI.Contracts; +using OctoshiftCLI.Services; + +namespace OctoshiftCLI.GithubEnterpriseImporter.Factories; + +public class DependabotAlertServiceFactory +{ + private readonly OctoLogger _octoLogger; + private readonly ISourceGithubApiFactory _sourceGithubApiFactory; + private readonly ITargetGithubApiFactory _targetGithubApiFactory; + private readonly EnvironmentVariableProvider _environmentVariableProvider; + + public DependabotAlertServiceFactory( + OctoLogger octoLogger, + ISourceGithubApiFactory sourceGithubApiFactory, + ITargetGithubApiFactory targetGithubApiFactory, + EnvironmentVariableProvider environmentVariableProvider) + { + _octoLogger = octoLogger; + _sourceGithubApiFactory = sourceGithubApiFactory; + _targetGithubApiFactory = targetGithubApiFactory; + _environmentVariableProvider = environmentVariableProvider; + } + + public virtual DependabotAlertService + Create(string sourceApi, string sourceToken, string targetApi, string targetToken, bool sourceApiNoSsl = false) + { + sourceToken ??= _environmentVariableProvider.SourceGithubPersonalAccessToken(); + targetToken ??= _environmentVariableProvider.TargetGithubPersonalAccessToken(); + + var sourceGithubApi = sourceApiNoSsl + ? _sourceGithubApiFactory.CreateClientNoSsl(sourceApi, sourceToken) + : _sourceGithubApiFactory.Create(sourceApi, sourceToken); + + return new(sourceGithubApi, _targetGithubApiFactory.Create(targetApi, targetToken), _octoLogger); + } +} \ No newline at end of file diff --git a/src/gei/Program.cs b/src/gei/Program.cs index 41bb46321..9ee6c006f 100644 --- a/src/gei/Program.cs +++ b/src/gei/Program.cs @@ -44,6 +44,7 @@ public static async Task Main(string[] args) .AddSingleton(sp => sp.GetRequiredService()) .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddHttpClient("NoSSL") From b1f9b0a975896f7fc7a6861d11e3a02ea184ddc4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Jun 2025 04:46:04 +0000 Subject: [PATCH 4/8] Add comprehensive tests for Dependabot Alert migration functionality Co-authored-by: dylan-smith <1508559+dylan-smith@users.noreply.github.com> --- .../Services/DependabotAlertServiceTests.cs | 240 ++++++++++++++++++ .../Octoshift/Services/GithubApiTests.cs | 162 ++++++++++++ .../MigrateDependabotAlertsCommandTests.cs | 69 +++++ 3 files changed, 471 insertions(+) create mode 100644 src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs create mode 100644 src/OctoshiftCLI.Tests/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandTests.cs diff --git a/src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs new file mode 100644 index 000000000..ee1f353a4 --- /dev/null +++ b/src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs @@ -0,0 +1,240 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using Octoshift.Models; +using OctoshiftCLI.Models; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.Octoshift.Services; + +public class DependabotAlertServiceTests +{ + private readonly Mock _mockSourceGithubApi = TestHelpers.CreateMock(); + private readonly Mock _mockTargetGithubApi = TestHelpers.CreateMock(); + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + + private readonly DependabotAlertService _alertService; + + private const string SOURCE_ORG = "SOURCE-ORG"; + private const string SOURCE_REPO = "SOURCE-REPO"; + private const string TARGET_ORG = "TARGET-ORG"; + private const string TARGET_REPO = "TARGET-REPO"; + + public DependabotAlertServiceTests() + { + _alertService = new DependabotAlertService(_mockSourceGithubApi.Object, _mockTargetGithubApi.Object, _mockOctoLogger.Object); + } + + [Fact] + public async Task MigrateDependabotAlerts_No_Alerts_Does_Not_Update_Any_Alerts() + { + // Arrange + _mockSourceGithubApi.Setup(x => x.GetDependabotAlertsForRepository(SOURCE_ORG, SOURCE_REPO)).ReturnsAsync(Enumerable.Empty()); + _mockTargetGithubApi.Setup(x => x.GetDependabotAlertsForRepository(TARGET_ORG, TARGET_REPO)).ReturnsAsync(Enumerable.Empty()); + + // Act + await _alertService.MigrateDependabotAlerts(SOURCE_ORG, SOURCE_REPO, TARGET_ORG, TARGET_REPO, false); + + // Assert + _mockTargetGithubApi.Verify(x => x.UpdateDependabotAlert(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task MigrateDependabotAlerts_Matches_Alerts_By_Ghsa_Id_And_Package() + { + // Arrange + var sourceAlert = new DependabotAlert + { + Number = 1, + State = "dismissed", + DismissedReason = "false_positive", + DismissedComment = "Not applicable", + Url = "https://api.github.com/repos/source-org/source-repo/dependabot/alerts/1", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-1234-5678-9abc" }, + Dependency = new DependabotAlertDependency { Package = "lodash", Manifest = "package.json" } + }; + + var targetAlert = new DependabotAlert + { + Number = 2, + State = "open", + Url = "https://api.github.com/repos/target-org/target-repo/dependabot/alerts/2", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-1234-5678-9abc" }, + Dependency = new DependabotAlertDependency { Package = "lodash", Manifest = "package.json" } + }; + + _mockSourceGithubApi.Setup(x => x.GetDependabotAlertsForRepository(SOURCE_ORG, SOURCE_REPO)).ReturnsAsync(new[] { sourceAlert }); + _mockTargetGithubApi.Setup(x => x.GetDependabotAlertsForRepository(TARGET_ORG, TARGET_REPO)).ReturnsAsync(new[] { targetAlert }); + + // Act + await _alertService.MigrateDependabotAlerts(SOURCE_ORG, SOURCE_REPO, TARGET_ORG, TARGET_REPO, false); + + // Assert + _mockTargetGithubApi.Verify(x => x.UpdateDependabotAlert( + TARGET_ORG, + TARGET_REPO, + targetAlert.Number, + sourceAlert.State, + sourceAlert.DismissedReason, + sourceAlert.DismissedComment + ), Times.Once); + } + + [Fact] + public async Task MigrateDependabotAlerts_Falls_Back_To_Cve_Id_When_Ghsa_Id_Does_Not_Match() + { + // Arrange + var sourceAlert = new DependabotAlert + { + Number = 1, + State = "dismissed", + DismissedReason = "not_used", + DismissedComment = "Package not used in production", + Url = "https://api.github.com/repos/source-org/source-repo/dependabot/alerts/1", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-1111-1111-1111", CveId = "CVE-2023-1234" }, + Dependency = new DependabotAlertDependency { Package = "express", Manifest = "package.json" } + }; + + var targetAlert = new DependabotAlert + { + Number = 3, + State = "open", + Url = "https://api.github.com/repos/target-org/target-repo/dependabot/alerts/3", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-2222-2222-2222", CveId = "CVE-2023-1234" }, + Dependency = new DependabotAlertDependency { Package = "express", Manifest = "package.json" } + }; + + _mockSourceGithubApi.Setup(x => x.GetDependabotAlertsForRepository(SOURCE_ORG, SOURCE_REPO)).ReturnsAsync(new[] { sourceAlert }); + _mockTargetGithubApi.Setup(x => x.GetDependabotAlertsForRepository(TARGET_ORG, TARGET_REPO)).ReturnsAsync(new[] { targetAlert }); + + // Act + await _alertService.MigrateDependabotAlerts(SOURCE_ORG, SOURCE_REPO, TARGET_ORG, TARGET_REPO, false); + + // Assert + _mockTargetGithubApi.Verify(x => x.UpdateDependabotAlert( + TARGET_ORG, + TARGET_REPO, + targetAlert.Number, + sourceAlert.State, + sourceAlert.DismissedReason, + sourceAlert.DismissedComment + ), Times.Once); + } + + [Fact] + public async Task MigrateDependabotAlerts_Skips_Non_Migratable_States() + { + // Arrange + var sourceAlert = new DependabotAlert + { + Number = 1, + State = "fixed", // non-migratable state + Url = "https://api.github.com/repos/source-org/source-repo/dependabot/alerts/1", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-1234-5678-9abc" }, + Dependency = new DependabotAlertDependency { Package = "lodash", Manifest = "package.json" } + }; + + _mockSourceGithubApi.Setup(x => x.GetDependabotAlertsForRepository(SOURCE_ORG, SOURCE_REPO)).ReturnsAsync(new[] { sourceAlert }); + _mockTargetGithubApi.Setup(x => x.GetDependabotAlertsForRepository(TARGET_ORG, TARGET_REPO)).ReturnsAsync(Enumerable.Empty()); + + // Act + await _alertService.MigrateDependabotAlerts(SOURCE_ORG, SOURCE_REPO, TARGET_ORG, TARGET_REPO, false); + + // Assert + _mockTargetGithubApi.Verify(x => x.UpdateDependabotAlert(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task MigrateDependabotAlerts_Skips_When_Target_Already_Has_Same_State() + { + // Arrange + var sourceAlert = new DependabotAlert + { + Number = 1, + State = "dismissed", + DismissedReason = "false_positive", + DismissedComment = "Not applicable", + Url = "https://api.github.com/repos/source-org/source-repo/dependabot/alerts/1", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-1234-5678-9abc" }, + Dependency = new DependabotAlertDependency { Package = "lodash", Manifest = "package.json" } + }; + + var targetAlert = new DependabotAlert + { + Number = 2, + State = "dismissed", // Same state as source + Url = "https://api.github.com/repos/target-org/target-repo/dependabot/alerts/2", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-1234-5678-9abc" }, + Dependency = new DependabotAlertDependency { Package = "lodash", Manifest = "package.json" } + }; + + _mockSourceGithubApi.Setup(x => x.GetDependabotAlertsForRepository(SOURCE_ORG, SOURCE_REPO)).ReturnsAsync(new[] { sourceAlert }); + _mockTargetGithubApi.Setup(x => x.GetDependabotAlertsForRepository(TARGET_ORG, TARGET_REPO)).ReturnsAsync(new[] { targetAlert }); + + // Act + await _alertService.MigrateDependabotAlerts(SOURCE_ORG, SOURCE_REPO, TARGET_ORG, TARGET_REPO, false); + + // Assert + _mockTargetGithubApi.Verify(x => x.UpdateDependabotAlert(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task MigrateDependabotAlerts_Throws_When_Target_Alert_Not_Found() + { + // Arrange + var sourceAlert = new DependabotAlert + { + Number = 1, + State = "dismissed", + DismissedReason = "false_positive", + Url = "https://api.github.com/repos/source-org/source-repo/dependabot/alerts/1", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-1234-5678-9abc" }, + Dependency = new DependabotAlertDependency { Package = "lodash", Manifest = "package.json" } + }; + + // No matching target alert + var targetAlert = new DependabotAlert + { + Number = 2, + State = "open", + Url = "https://api.github.com/repos/target-org/target-repo/dependabot/alerts/2", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-9999-9999-9999" }, // Different GHSA ID + Dependency = new DependabotAlertDependency { Package = "react", Manifest = "package.json" } // Different package + }; + + _mockSourceGithubApi.Setup(x => x.GetDependabotAlertsForRepository(SOURCE_ORG, SOURCE_REPO)).ReturnsAsync(new[] { sourceAlert }); + _mockTargetGithubApi.Setup(x => x.GetDependabotAlertsForRepository(TARGET_ORG, TARGET_REPO)).ReturnsAsync(new[] { targetAlert }); + + // Act & Assert + await _alertService.Invoking(x => x.MigrateDependabotAlerts(SOURCE_ORG, SOURCE_REPO, TARGET_ORG, TARGET_REPO, false)) + .Should().ThrowAsync() + .WithMessage("Migration of Dependabot Alerts failed."); + } + + [Fact] + public async Task MigrateDependabotAlerts_In_Dry_Run_Mode_Does_Not_Update_Alerts() + { + // Arrange + var sourceAlert = new DependabotAlert + { + Number = 1, + State = "dismissed", + DismissedReason = "false_positive", + Url = "https://api.github.com/repos/source-org/source-repo/dependabot/alerts/1", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-1234-5678-9abc" }, + Dependency = new DependabotAlertDependency { Package = "lodash", Manifest = "package.json" } + }; + + _mockSourceGithubApi.Setup(x => x.GetDependabotAlertsForRepository(SOURCE_ORG, SOURCE_REPO)).ReturnsAsync(new[] { sourceAlert }); + + // Act + await _alertService.MigrateDependabotAlerts(SOURCE_ORG, SOURCE_REPO, TARGET_ORG, TARGET_REPO, true); + + // Assert + _mockTargetGithubApi.Verify(x => x.GetDependabotAlertsForRepository(It.IsAny(), It.IsAny()), Times.Never); + _mockTargetGithubApi.Verify(x => x.UpdateDependabotAlert(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } +} \ No newline at end of file diff --git a/src/OctoshiftCLI.Tests/Octoshift/Services/GithubApiTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Services/GithubApiTests.cs index 4eaaa10e8..9c2334d2e 100644 --- a/src/OctoshiftCLI.Tests/Octoshift/Services/GithubApiTests.cs +++ b/src/OctoshiftCLI.Tests/Octoshift/Services/GithubApiTests.cs @@ -3879,6 +3879,168 @@ public async Task GetSecretScanningAlertsForRepository_Populates_ResolutionComme array[0].ResolverName.Should().Be("actor"); } + [Fact] + public async Task GetDependabotAlertsForRepository_Returns_Correct_Data() + { + // Arrange + const string url = $"https://api.github.com/repos/{GITHUB_ORG}/{GITHUB_REPO}/dependabot/alerts?per_page=100"; + + var dependabotAlert = $@" + {{ + ""number"": 1, + ""state"": ""dismissed"", + ""dismissed_reason"": ""false_positive"", + ""dismissed_comment"": ""Not applicable"", + ""dismissed_at"": ""2023-05-15T10:30:00Z"", + ""url"": ""https://api.github.com/repos/{GITHUB_ORG}/{GITHUB_REPO}/dependabot/alerts/1"", + ""html_url"": ""https://github.com/{GITHUB_ORG}/{GITHUB_REPO}/security/dependabot/1"", + ""created_at"": ""2023-05-14T08:00:00Z"", + ""updated_at"": ""2023-05-15T10:30:00Z"", + ""dependency"": {{ + ""package"": {{ + ""name"": ""lodash"" + }}, + ""manifest_path"": ""package.json"", + ""scope"": ""runtime"" + }}, + ""security_advisory"": {{ + ""ghsa_id"": ""GHSA-1234-5678-9abc"", + ""cve_id"": ""CVE-2023-1234"", + ""summary"": ""Prototype Pollution in lodash"", + ""description"": ""lodash is vulnerable to prototype pollution"", + ""severity"": ""high"" + }}, + ""security_vulnerability"": {{ + ""package"": {{ + ""name"": ""lodash"" + }}, + ""severity"": ""high"", + ""vulnerable_version_range"": ""< 4.17.21"", + ""first_patched_version"": {{ + ""identifier"": ""4.17.21"" + }} + }} + }}"; + + var alerts = new[] { JToken.Parse(dependabotAlert) }; + + _githubClientMock + .Setup(m => m.GetAllAsync(url, null)) + .Returns(alerts.ToAsyncEnumerable()); + + // Act + var results = await _githubApi.GetDependabotAlertsForRepository(GITHUB_ORG, GITHUB_REPO); + + // Assert + results.Count().Should().Be(1); + var alert = results.First(); + + alert.Number.Should().Be(1); + alert.State.Should().Be("dismissed"); + alert.DismissedReason.Should().Be("false_positive"); + alert.DismissedComment.Should().Be("Not applicable"); + alert.DismissedAt.Should().NotBeNull(); + alert.Url.Should().Be($"https://api.github.com/repos/{GITHUB_ORG}/{GITHUB_REPO}/dependabot/alerts/1"); + alert.HtmlUrl.Should().Be($"https://github.com/{GITHUB_ORG}/{GITHUB_REPO}/security/dependabot/1"); + alert.CreatedAt.Should().NotBeNull(); + alert.UpdatedAt.Should().NotBeNull(); + + alert.Dependency.Package.Should().Be("lodash"); + alert.Dependency.Manifest.Should().Be("package.json"); + alert.Dependency.Scope.Should().Be("runtime"); + + alert.SecurityAdvisory.GhsaId.Should().Be("GHSA-1234-5678-9abc"); + alert.SecurityAdvisory.CveId.Should().Be("CVE-2023-1234"); + alert.SecurityAdvisory.Summary.Should().Be("Prototype Pollution in lodash"); + alert.SecurityAdvisory.Severity.Should().Be("high"); + + alert.SecurityVulnerability.Package.Should().Be("lodash"); + alert.SecurityVulnerability.Severity.Should().Be("high"); + alert.SecurityVulnerability.VulnerableVersionRange.Should().Be("< 4.17.21"); + alert.SecurityVulnerability.FirstPatchedVersion.Should().Be("4.17.21"); + } + + [Fact] + public async Task UpdateDependabotAlert_Calls_The_Right_Endpoint_With_Payload_For_Open_State() + { + // Arrange + const int alertNumber = 1; + const string state = "open"; + + var url = $"https://api.github.com/repos/{GITHUB_ORG}/{GITHUB_REPO}/dependabot/alerts/{alertNumber}"; + var payload = new { state }; + + // Act + await _githubApi.UpdateDependabotAlert(GITHUB_ORG, GITHUB_REPO, alertNumber, state); + + // Assert + _githubClientMock.Verify(m => m.PatchAsync(url, It.Is(x => x.ToJson() == payload.ToJson()), null)); + } + + [Fact] + public async Task UpdateDependabotAlert_Calls_The_Right_Endpoint_With_Payload_For_Dismissed_State() + { + // Arrange + const int alertNumber = 1; + const string state = "dismissed"; + const string dismissedReason = "false_positive"; + const string dismissedComment = "Not applicable"; + + var url = $"https://api.github.com/repos/{GITHUB_ORG}/{GITHUB_REPO}/dependabot/alerts/{alertNumber}"; + var payload = new { state, dismissed_reason = dismissedReason, dismissed_comment = dismissedComment }; + + // Act + await _githubApi.UpdateDependabotAlert(GITHUB_ORG, GITHUB_REPO, alertNumber, state, dismissedReason, dismissedComment); + + // Assert + _githubClientMock.Verify(m => m.PatchAsync(url, It.Is(x => x.ToJson() == payload.ToJson()), null)); + } + + [Fact] + public async Task UpdateDependabotAlert_Replaces_Null_Dismissed_Comment_With_Empty_String() + { + // Arrange + const int alertNumber = 1; + const string state = "dismissed"; + const string dismissedReason = "not_used"; + + var url = $"https://api.github.com/repos/{GITHUB_ORG}/{GITHUB_REPO}/dependabot/alerts/{alertNumber}"; + var payload = new { state, dismissed_reason = dismissedReason, dismissed_comment = string.Empty }; + + // Act + await _githubApi.UpdateDependabotAlert(GITHUB_ORG, GITHUB_REPO, alertNumber, state, dismissedReason); + + // Assert + _githubClientMock.Verify(m => m.PatchAsync(url, It.Is(x => x.ToJson() == payload.ToJson()), null)); + } + + [Fact] + public async Task UpdateDependabotAlert_Throws_ArgumentException_For_Invalid_State() + { + // Arrange + const int alertNumber = 1; + const string invalidState = "invalid"; + + // Act & Assert + await _githubApi.Invoking(x => x.UpdateDependabotAlert(GITHUB_ORG, GITHUB_REPO, alertNumber, invalidState)) + .Should().ThrowAsync() + .WithMessage("Invalid value for state*"); + } + + [Fact] + public async Task UpdateDependabotAlert_Throws_ArgumentException_For_Invalid_Dismissed_Reason() + { + // Arrange + const int alertNumber = 1; + const string state = "dismissed"; + const string invalidReason = "invalid_reason"; + + // Act & Assert + await _githubApi.Invoking(x => x.UpdateDependabotAlert(GITHUB_ORG, GITHUB_REPO, alertNumber, state, invalidReason)) + .Should().ThrowAsync() + .WithMessage("Invalid value for dismissedReason*"); + } + private string Compact(string source) => source .Replace("\r", "") diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandTests.cs new file mode 100644 index 000000000..5b46e3f2b --- /dev/null +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandTests.cs @@ -0,0 +1,69 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using OctoshiftCLI.GithubEnterpriseImporter.Commands.MigrateDependabotAlerts; +using OctoshiftCLI.GithubEnterpriseImporter.Factories; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.GithubEnterpriseImporter.Commands.MigrateDependabotAlerts; + +public class MigrateDependabotAlertsCommandTests +{ + private readonly Mock _mockEnvironmentVariableProvider = TestHelpers.CreateMock(); + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + private readonly Mock _mockDependabotAlertServiceFactory = TestHelpers.CreateMock(); + + private readonly ServiceProvider _serviceProvider; + private readonly MigrateDependabotAlertsCommand _command = []; + + public MigrateDependabotAlertsCommandTests() + { + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddSingleton(_mockOctoLogger.Object) + .AddSingleton(_mockEnvironmentVariableProvider.Object) + .AddSingleton(_mockDependabotAlertServiceFactory.Object); + + _serviceProvider = serviceCollection.BuildServiceProvider(); + } + + [Fact] + public void Should_Have_Options() + { + _command.Should().NotBeNull(); + _command.Name.Should().Be("migrate-dependabot-alerts"); + _command.Options.Count.Should().Be(11); + + TestHelpers.VerifyCommandOption(_command.Options, "source-org", true); + TestHelpers.VerifyCommandOption(_command.Options, "source-repo", true); + TestHelpers.VerifyCommandOption(_command.Options, "target-org", true); + TestHelpers.VerifyCommandOption(_command.Options, "target-repo", false); + TestHelpers.VerifyCommandOption(_command.Options, "target-api-url", false); + TestHelpers.VerifyCommandOption(_command.Options, "ghes-api-url", false); + TestHelpers.VerifyCommandOption(_command.Options, "no-ssl-verify", false); + TestHelpers.VerifyCommandOption(_command.Options, "github-source-pat", false); + TestHelpers.VerifyCommandOption(_command.Options, "github-target-pat", false); + TestHelpers.VerifyCommandOption(_command.Options, "verbose", false); + TestHelpers.VerifyCommandOption(_command.Options, "dry-run", false); + } + + [Fact] + public void Source_Pat_Should_Default_To_Target_Pat() + { + var targetToken = "target-token"; + + var args = new MigrateDependabotAlertsCommandArgs() + { + SourceOrg = "source-org", + SourceRepo = "source-repo", + TargetOrg = "target-org", + TargetRepo = "target-repo", + GithubTargetPat = targetToken, + }; + + _command.BuildHandler(args, _serviceProvider); + + _mockDependabotAlertServiceFactory.Verify(m => m.Create(It.IsAny(), targetToken, It.IsAny(), targetToken, It.IsAny())); + } +} \ No newline at end of file From 1ca3ec75587a23d469132d3c17d567af483f79a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Jun 2025 05:02:42 +0000 Subject: [PATCH 5/8] Revert unintentional .NET SDK version downgrade in global.json Co-authored-by: dylan-smith <1508559+dylan-smith@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index fdaac1bc6..f70e88ddb 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.115", + "version": "8.0.404", "rollForward": "minor" } } \ No newline at end of file From 2daaaba72d498c3287c054260ba9a06ce4bb3128 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Jun 2025 05:08:53 +0000 Subject: [PATCH 6/8] Fix incorrect reference to non-existent source-api-url argument in MigrateDependabotAlertsCommand Co-authored-by: dylan-smith <1508559+dylan-smith@users.noreply.github.com> --- .../MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs index 241940dc2..effb8d9bf 100644 --- a/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs +++ b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs @@ -43,7 +43,8 @@ public MigrateDependabotAlertsCommand() : base( }; public Option GhesApiUrl { get; } = new("--ghes-api-url") { - Description = "The URL of the GitHub Enterprise Server API. Defaults to the source-api-url if provided or https://api.github.com" + Description = + "Required if migrating from GHES. The API endpoint for your GHES instance. For example: http(s)://ghes.contoso.com/api/v3" }; public Option NoSslVerify { get; } = new("--no-ssl-verify"); From d0de9cc057c11e7c59d44d8db5cfc9859ee41a78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Jun 2025 05:20:13 +0000 Subject: [PATCH 7/8] Fix dry run behavior to exercise matching logic Co-authored-by: dylan-smith <1508559+dylan-smith@users.noreply.github.com> --- .../Services/DependabotAlertService.cs | 21 ++++----- .../Services/DependabotAlertServiceTests.cs | 47 ++++++++++++++++++- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/Octoshift/Services/DependabotAlertService.cs b/src/Octoshift/Services/DependabotAlertService.cs index 156b7597b..62ec582c5 100644 --- a/src/Octoshift/Services/DependabotAlertService.cs +++ b/src/Octoshift/Services/DependabotAlertService.cs @@ -25,11 +25,7 @@ public virtual async Task MigrateDependabotAlerts(string sourceOrg, string sourc _log.LogInformation($"Migrating Dependabot Alerts from '{sourceOrg}/{sourceRepo}' to '{targetOrg}/{targetRepo}'"); var sourceAlerts = (await _sourceGithubApi.GetDependabotAlertsForRepository(sourceOrg, sourceRepo)).ToList(); - - // no reason to call the target on a dry run - there will be no alerts - var targetAlerts = dryRun ? - [] : - (await _targetGithubApi.GetDependabotAlertsForRepository(targetOrg, targetRepo)).ToList(); + var targetAlerts = (await _targetGithubApi.GetDependabotAlertsForRepository(targetOrg, targetRepo)).ToList(); var successCount = 0; var skippedCount = 0; @@ -46,14 +42,6 @@ public virtual async Task MigrateDependabotAlerts(string sourceOrg, string sourc continue; } - if (dryRun) - { - _log.LogInformation($" running in dry-run mode. Would have tried to find target alert for {sourceAlert.Number} ({sourceAlert.Url}) and set state '{sourceAlert.State}'"); - successCount++; - // No sense in continuing here, because we don't have the target alert as it is not migrated in dryRun mode - continue; - } - var matchingTargetAlert = FindMatchingTargetAlert(targetAlerts, sourceAlert); if (matchingTargetAlert == null) @@ -70,6 +58,13 @@ public virtual async Task MigrateDependabotAlerts(string sourceOrg, string sourc continue; } + if (dryRun) + { + _log.LogInformation($" running in dry-run mode. Would update target alert {matchingTargetAlert.Number} ({matchingTargetAlert.Url}) from state '{matchingTargetAlert.State}' to '{sourceAlert.State}'"); + successCount++; + continue; + } + _log.LogVerbose($"Setting Status {sourceAlert.State} for target alert {matchingTargetAlert.Number} ({matchingTargetAlert.Url})"); await _targetGithubApi.UpdateDependabotAlert( targetOrg, diff --git a/src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs index ee1f353a4..ba5917a60 100644 --- a/src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs +++ b/src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs @@ -215,7 +215,7 @@ await _alertService.Invoking(x => x.MigrateDependabotAlerts(SOURCE_ORG, SOURCE_R } [Fact] - public async Task MigrateDependabotAlerts_In_Dry_Run_Mode_Does_Not_Update_Alerts() + public async Task MigrateDependabotAlerts_In_Dry_Run_Mode_Gets_Target_Alerts_But_Does_Not_Update() { // Arrange var sourceAlert = new DependabotAlert @@ -228,13 +228,56 @@ public async Task MigrateDependabotAlerts_In_Dry_Run_Mode_Does_Not_Update_Alerts Dependency = new DependabotAlertDependency { Package = "lodash", Manifest = "package.json" } }; + var targetAlert = new DependabotAlert + { + Number = 2, + State = "open", + Url = "https://api.github.com/repos/target-org/target-repo/dependabot/alerts/2", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-1234-5678-9abc" }, + Dependency = new DependabotAlertDependency { Package = "lodash", Manifest = "package.json" } + }; + _mockSourceGithubApi.Setup(x => x.GetDependabotAlertsForRepository(SOURCE_ORG, SOURCE_REPO)).ReturnsAsync(new[] { sourceAlert }); + _mockTargetGithubApi.Setup(x => x.GetDependabotAlertsForRepository(TARGET_ORG, TARGET_REPO)).ReturnsAsync(new[] { targetAlert }); // Act await _alertService.MigrateDependabotAlerts(SOURCE_ORG, SOURCE_REPO, TARGET_ORG, TARGET_REPO, true); // Assert - _mockTargetGithubApi.Verify(x => x.GetDependabotAlertsForRepository(It.IsAny(), It.IsAny()), Times.Never); + _mockTargetGithubApi.Verify(x => x.GetDependabotAlertsForRepository(TARGET_ORG, TARGET_REPO), Times.Once); _mockTargetGithubApi.Verify(x => x.UpdateDependabotAlert(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + + [Fact] + public async Task MigrateDependabotAlerts_In_Dry_Run_Mode_Still_Throws_When_Target_Alert_Not_Found() + { + // Arrange + var sourceAlert = new DependabotAlert + { + Number = 1, + State = "dismissed", + DismissedReason = "false_positive", + Url = "https://api.github.com/repos/source-org/source-repo/dependabot/alerts/1", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-1234-5678-9abc" }, + Dependency = new DependabotAlertDependency { Package = "lodash", Manifest = "package.json" } + }; + + // No matching target alert + var targetAlert = new DependabotAlert + { + Number = 2, + State = "open", + Url = "https://api.github.com/repos/target-org/target-repo/dependabot/alerts/2", + SecurityAdvisory = new DependabotAlertSecurityAdvisory { GhsaId = "GHSA-9999-9999-9999" }, // Different GHSA ID + Dependency = new DependabotAlertDependency { Package = "react", Manifest = "package.json" } // Different package + }; + + _mockSourceGithubApi.Setup(x => x.GetDependabotAlertsForRepository(SOURCE_ORG, SOURCE_REPO)).ReturnsAsync(new[] { sourceAlert }); + _mockTargetGithubApi.Setup(x => x.GetDependabotAlertsForRepository(TARGET_ORG, TARGET_REPO)).ReturnsAsync(new[] { targetAlert }); + + // Act & Assert + await _alertService.Invoking(x => x.MigrateDependabotAlerts(SOURCE_ORG, SOURCE_REPO, TARGET_ORG, TARGET_REPO, true)) + .Should().ThrowAsync() + .WithMessage("Migration of Dependabot Alerts failed."); + } } \ No newline at end of file From 3d315b147ace1ec8a57030d7d7abb3ba65c911a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Jun 2025 06:16:31 +0000 Subject: [PATCH 8/8] Fix formatting issues - add missing final newlines and apply dotnet format Co-authored-by: dylan-smith <1508559+dylan-smith@users.noreply.github.com> --- src/Octoshift/DependabotAlertState.cs | 2 +- src/Octoshift/Models/DependabotAlert.cs | 2 +- src/Octoshift/Services/DependabotAlertService.cs | 3 +-- .../Octoshift/Services/DependabotAlertServiceTests.cs | 4 +--- .../Octoshift/Services/GithubApiTests.cs | 8 ++++---- .../MigrateDependabotAlertsCommandTests.cs | 2 +- .../MigrateDependabotAlertsCommand.cs | 2 +- .../MigrateDependabotAlertsCommandArgs.cs | 2 +- .../MigrateDependabotAlertsCommandHandler.cs | 2 +- src/gei/Factories/DependabotAlertServiceFactory.cs | 2 +- 10 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Octoshift/DependabotAlertState.cs b/src/Octoshift/DependabotAlertState.cs index d9b4d47f7..1793622b4 100644 --- a/src/Octoshift/DependabotAlertState.cs +++ b/src/Octoshift/DependabotAlertState.cs @@ -20,4 +20,4 @@ public static class DependabotAlertState public static bool IsValidDismissedReason(string reason) => reason?.Trim().ToLower() is FalsePositive or Inaccurate or NotUsed or NoBandwidth or TolerableRisk; -} \ No newline at end of file +} diff --git a/src/Octoshift/Models/DependabotAlert.cs b/src/Octoshift/Models/DependabotAlert.cs index efd540fa7..8bfb21c71 100644 --- a/src/Octoshift/Models/DependabotAlert.cs +++ b/src/Octoshift/Models/DependabotAlert.cs @@ -38,4 +38,4 @@ public class DependabotAlertSecurityVulnerability public string Severity { get; set; } public string VulnerableVersionRange { get; set; } public string FirstPatchedVersion { get; set; } -} \ No newline at end of file +} diff --git a/src/Octoshift/Services/DependabotAlertService.cs b/src/Octoshift/Services/DependabotAlertService.cs index 62ec582c5..92c910cdd 100644 --- a/src/Octoshift/Services/DependabotAlertService.cs +++ b/src/Octoshift/Services/DependabotAlertService.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Threading.Tasks; using Octoshift.Models; -using OctoshiftCLI.Models; namespace OctoshiftCLI.Services; @@ -104,4 +103,4 @@ private DependabotAlert FindMatchingTargetAlert(List targetAler targetAlert.Dependency?.Package == sourceAlert.Dependency?.Package && targetAlert.Dependency?.Manifest == sourceAlert.Dependency?.Manifest); } -} \ No newline at end of file +} diff --git a/src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs index ba5917a60..7cb0f1e93 100644 --- a/src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs +++ b/src/OctoshiftCLI.Tests/Octoshift/Services/DependabotAlertServiceTests.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Moq; using Octoshift.Models; -using OctoshiftCLI.Models; using OctoshiftCLI.Services; using Xunit; @@ -280,4 +278,4 @@ await _alertService.Invoking(x => x.MigrateDependabotAlerts(SOURCE_ORG, SOURCE_R .Should().ThrowAsync() .WithMessage("Migration of Dependabot Alerts failed."); } -} \ No newline at end of file +} diff --git a/src/OctoshiftCLI.Tests/Octoshift/Services/GithubApiTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Services/GithubApiTests.cs index baa328b31..2676bcfb7 100644 --- a/src/OctoshiftCLI.Tests/Octoshift/Services/GithubApiTests.cs +++ b/src/OctoshiftCLI.Tests/Octoshift/Services/GithubApiTests.cs @@ -3923,7 +3923,7 @@ public async Task GetDependabotAlertsForRepository_Returns_Correct_Data() // Assert results.Count().Should().Be(1); var alert = results.First(); - + alert.Number.Should().Be(1); alert.State.Should().Be("dismissed"); alert.DismissedReason.Should().Be("false_positive"); @@ -3933,16 +3933,16 @@ public async Task GetDependabotAlertsForRepository_Returns_Correct_Data() alert.HtmlUrl.Should().Be($"https://github.com/{GITHUB_ORG}/{GITHUB_REPO}/security/dependabot/1"); alert.CreatedAt.Should().NotBeNull(); alert.UpdatedAt.Should().NotBeNull(); - + alert.Dependency.Package.Should().Be("lodash"); alert.Dependency.Manifest.Should().Be("package.json"); alert.Dependency.Scope.Should().Be("runtime"); - + alert.SecurityAdvisory.GhsaId.Should().Be("GHSA-1234-5678-9abc"); alert.SecurityAdvisory.CveId.Should().Be("CVE-2023-1234"); alert.SecurityAdvisory.Summary.Should().Be("Prototype Pollution in lodash"); alert.SecurityAdvisory.Severity.Should().Be("high"); - + alert.SecurityVulnerability.Package.Should().Be("lodash"); alert.SecurityVulnerability.Severity.Should().Be("high"); alert.SecurityVulnerability.VulnerableVersionRange.Should().Be("< 4.17.21"); diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandTests.cs index 5b46e3f2b..1861f59a8 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandTests.cs @@ -66,4 +66,4 @@ public void Source_Pat_Should_Default_To_Target_Pat() _mockDependabotAlertServiceFactory.Verify(m => m.Create(It.IsAny(), targetToken, It.IsAny(), targetToken, It.IsAny())); } -} \ No newline at end of file +} diff --git a/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs index effb8d9bf..9aa710866 100644 --- a/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs +++ b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommand.cs @@ -93,4 +93,4 @@ public override MigrateDependabotAlertsCommandHandler BuildHandler(MigrateDepend return new MigrateDependabotAlertsCommandHandler(log, dependabotAlertService); } -} \ No newline at end of file +} diff --git a/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandArgs.cs b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandArgs.cs index ceeafb329..953a07554 100644 --- a/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandArgs.cs +++ b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandArgs.cs @@ -27,4 +27,4 @@ public override void Validate(OctoLogger log) log?.LogInformation("Since target-repo is not provided, source-repo value will be used for target-repo."); } } -} \ No newline at end of file +} diff --git a/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandHandler.cs b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandHandler.cs index 5488a5a19..2950dc5b1 100644 --- a/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandHandler.cs +++ b/src/gei/Commands/MigrateDependabotAlerts/MigrateDependabotAlertsCommandHandler.cs @@ -37,4 +37,4 @@ await _dependabotAlertService.MigrateDependabotAlerts( _log.LogSuccess($"Dependabot alerts successfully migrated."); } } -} \ No newline at end of file +} diff --git a/src/gei/Factories/DependabotAlertServiceFactory.cs b/src/gei/Factories/DependabotAlertServiceFactory.cs index 082665020..93b942772 100644 --- a/src/gei/Factories/DependabotAlertServiceFactory.cs +++ b/src/gei/Factories/DependabotAlertServiceFactory.cs @@ -34,4 +34,4 @@ public virtual DependabotAlertService return new(sourceGithubApi, _targetGithubApiFactory.Create(targetApi, targetToken), _octoLogger); } -} \ No newline at end of file +}