Skip to content

Commit c610290

Browse files
Merge pull request #1306 from theztefan/theztefan/secretscan-alerts-matching-update
Secret Scanning Alerts migration - update to new location types
2 parents 9f82b40 + 8e88dd9 commit c610290

File tree

6 files changed

+594
-95
lines changed

6 files changed

+594
-95
lines changed

RELEASENOTES.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
- Update validation error messages for `gh bbs2gh migrate-repo` command when generating an archive is not required.
22
- `gh gei migrate-code-scanning-alerts` now skips a not found code scanning analysis and continues with the rest.
3+
- Updated Secret Scanning Alerts migration (`gh gei migrate-secret-alerts`) command to match on all location types. Now includes: issues, pull requests.

src/Octoshift/Models/GithubSecretScanningAlert.cs

+13
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,29 @@ public class GithubSecretScanningAlert
44
public int Number { get; set; }
55
public string State { get; set; }
66
public string Resolution { get; set; }
7+
public string ResolutionComment { get; set; }
78
public string SecretType { get; set; }
89
public string Secret { get; set; }
910
}
1011

1112
public class GithubSecretScanningAlertLocation
1213
{
14+
public string LocationType { get; set; }
1315
public string Path { get; set; }
1416
public int StartLine { get; set; }
1517
public int EndLine { get; set; }
1618
public int StartColumn { get; set; }
1719
public int EndColumn { get; set; }
1820
public string BlobSha { get; set; }
21+
public string IssueTitleUrl { get; set; }
22+
public string IssueBodyUrl { get; set; }
23+
public string IssueCommentUrl { get; set; }
24+
public string DiscussionTitleUrl { get; set; }
25+
public string DiscussionBodyUrl { get; set; }
26+
public string DiscussionCommentUrl { get; set; }
27+
public string PullRequestTitleUrl { get; set; }
28+
public string PullRequestBodyUrl { get; set; }
29+
public string PullRequestCommentUrl { get; set; }
30+
public string PullRequestReviewUrl { get; set; }
31+
public string PullRequestReviewCommentUrl { get; set; }
1932
}

src/Octoshift/Services/GithubApi.cs

+15-2
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,7 @@ public virtual async Task<IEnumerable<GithubSecretScanningAlertLocation>> GetSec
908908
.ToListAsync();
909909
}
910910

911-
public virtual async Task UpdateSecretScanningAlert(string org, string repo, int alertNumber, string state, string resolution = null)
911+
public virtual async Task UpdateSecretScanningAlert(string org, string repo, int alertNumber, string state, string resolution = null, string resolutionComment = null)
912912
{
913913
if (!SecretScanningAlert.IsOpenOrResolved(state))
914914
{
@@ -922,7 +922,7 @@ public virtual async Task UpdateSecretScanningAlert(string org, string repo, int
922922

923923
var url = $"{_apiUrl}/repos/{org.EscapeDataString()}/{repo.EscapeDataString()}/secret-scanning/alerts/{alertNumber}";
924924

925-
object payload = state == SecretScanningAlert.AlertStateOpen ? new { state } : new { state, resolution };
925+
object payload = state == SecretScanningAlert.AlertStateOpen ? new { state } : new { state, resolution, resolution_comment = resolutionComment };
926926
await _client.PatchAsync(url, payload);
927927
}
928928

@@ -1179,19 +1179,32 @@ private static GithubSecretScanningAlert BuildSecretScanningAlert(JToken secretA
11791179
Number = (int)secretAlert["number"],
11801180
State = (string)secretAlert["state"],
11811181
Resolution = (string)secretAlert["resolution"],
1182+
ResolutionComment = (string)secretAlert["resolution_comment"],
11821183
SecretType = (string)secretAlert["secret_type"],
11831184
Secret = (string)secretAlert["secret"],
11841185
};
11851186

11861187
private static GithubSecretScanningAlertLocation BuildSecretScanningAlertLocation(JToken alertLocation) =>
11871188
new()
11881189
{
1190+
LocationType = (string)alertLocation["type"],
11891191
Path = (string)alertLocation["details"]["path"],
11901192
StartLine = (int)alertLocation["details"]["start_line"],
11911193
EndLine = (int)alertLocation["details"]["end_line"],
11921194
StartColumn = (int)alertLocation["details"]["start_column"],
11931195
EndColumn = (int)alertLocation["details"]["end_column"],
11941196
BlobSha = (string)alertLocation["details"]["blob_sha"],
1197+
IssueTitleUrl = (string)alertLocation["details"]["issue_title_url"],
1198+
IssueBodyUrl = (string)alertLocation["details"]["issue_body_url"],
1199+
IssueCommentUrl = (string)alertLocation["details"]["issue_comment_url"],
1200+
DiscussionTitleUrl = (string)alertLocation["details"]["discussion_title_url"],
1201+
DiscussionBodyUrl = (string)alertLocation["details"]["discussion_body_url"],
1202+
DiscussionCommentUrl = (string)alertLocation["details"]["discussion_comment_url"],
1203+
PullRequestTitleUrl = (string)alertLocation["details"]["pull_request_title_url"],
1204+
PullRequestBodyUrl = (string)alertLocation["details"]["pull_request_body_url"],
1205+
PullRequestCommentUrl = (string)alertLocation["details"]["pull_request_comment_url"],
1206+
PullRequestReviewUrl = (string)alertLocation["details"]["pull_request_review_url"],
1207+
PullRequestReviewCommentUrl = (string)alertLocation["details"]["pull_request_review_comment_url"],
11951208
};
11961209

11971210
private static CodeScanningAnalysis BuildCodeScanningAnalysis(JToken codescan) =>

src/Octoshift/Services/SecretScanningAlertService.cs

+98-88
Original file line numberDiff line numberDiff line change
@@ -18,130 +18,140 @@ public SecretScanningAlertService(GithubApi sourceGithubApi, GithubApi targetGit
1818
_log = logger;
1919
}
2020

21+
// Iterate over all source alerts by looping through the dictionary with each key (SecretType, Secret) and
22+
// try to find a matching alert in the target repository based on the same key
23+
// If potential match is found we compare the locations of the alerts and if they match a matching AlertWithLocations is returned
2124
public virtual async Task MigrateSecretScanningAlerts(string sourceOrg, string sourceRepo, string targetOrg,
22-
string targetRepo, bool dryRun)
25+
string targetRepo, bool dryRun)
2326
{
24-
_log.LogInformation(
25-
$"Migrating Secret Scanning Alerts from '{sourceOrg}/{sourceRepo}' to '{targetOrg}/{targetRepo}'");
27+
_log.LogInformation($"Migrating Secret Scanning Alerts from '{sourceOrg}/{sourceRepo}' to '{targetOrg}/{targetRepo}'");
2628

27-
var sourceAlerts = await GetAlertsWithLocations(_sourceGithubApi, sourceOrg, sourceRepo);
28-
var targetAlerts = await GetAlertsWithLocations(_targetGithubApi, targetOrg, targetRepo);
29+
var sourceAlertsDict = await GetAlertsWithLocations(_sourceGithubApi, sourceOrg, sourceRepo);
30+
var targetAlertsDict = await GetAlertsWithLocations(_targetGithubApi, targetOrg, targetRepo);
2931

30-
_log.LogInformation($"Source {sourceOrg}/{sourceRepo} secret alerts found: {sourceAlerts.Count}");
31-
_log.LogInformation($"Target {targetOrg}/{targetRepo} secret alerts found: {targetAlerts.Count}");
32+
_log.LogInformation($"Source {sourceOrg}/{sourceRepo} secret alerts found: {sourceAlertsDict.Count}");
33+
_log.LogInformation($"Target {targetOrg}/{targetRepo} secret alerts found: {targetAlertsDict.Count}");
3234

3335
_log.LogInformation("Matching secret resolutions from source to target repository");
34-
foreach (var alert in sourceAlerts)
36+
37+
foreach (var kvp in sourceAlertsDict)
3538
{
36-
_log.LogInformation($"Processing source secret {alert.Alert.Number}");
39+
var sourceKey = kvp.Key;
40+
var sourceAlerts = kvp.Value;
3741

38-
if (SecretScanningAlert.IsOpen(alert.Alert.State))
42+
foreach (var sourceAlert in sourceAlerts)
3943
{
40-
_log.LogInformation(" secret alert is still open, nothing to do");
41-
continue;
42-
}
44+
_log.LogInformation($"Processing source secret {sourceAlert.Alert.Number}");
4345

44-
_log.LogInformation(" secret is resolved, looking for matching secret in target...");
45-
var target = MatchTargetSecret(alert, targetAlerts);
46-
47-
if (target == null)
48-
{
49-
_log.LogWarning(
50-
$" failed to locate a matching secret to source secret {alert.Alert.Number} in {targetOrg}/{targetRepo}");
51-
continue;
52-
}
46+
if (SecretScanningAlert.IsOpen(sourceAlert.Alert.State))
47+
{
48+
_log.LogInformation(" secret alert is still open, nothing to do");
49+
continue;
50+
}
5351

54-
_log.LogInformation(
55-
$" source secret alert matched alert to {target.Alert.Number} in {targetOrg}/{targetRepo}.");
52+
_log.LogInformation(" secret is resolved, looking for matching secret in target...");
5653

57-
if (alert.Alert.Resolution == target.Alert.Resolution && alert.Alert.State == target.Alert.State)
58-
{
59-
_log.LogInformation(" source and target alerts are already aligned.");
60-
continue;
61-
}
62-
63-
if (dryRun)
64-
{
65-
_log.LogInformation(
66-
$" executing in dry run mode! Target alert {target.Alert.Number} would have been updated to state:{alert.Alert.State} and resolution:{alert.Alert.Resolution}");
67-
continue;
68-
}
54+
if (targetAlertsDict.TryGetValue(sourceKey, out var potentialTargets))
55+
{
56+
var targetAlert = potentialTargets.FirstOrDefault(target => DoAllLocationsMatch(sourceAlert.Locations, target.Locations));
6957

70-
_log.LogInformation(
71-
$" updating target alert:{target.Alert.Number} to state:{alert.Alert.State} and resolution:{alert.Alert.Resolution}");
58+
if (targetAlert != null)
59+
{
60+
_log.LogInformation($" source secret alert matched to {targetAlert.Alert.Number} in {targetOrg}/{targetRepo}.");
7261

73-
await _targetGithubApi.UpdateSecretScanningAlert(targetOrg, targetRepo, target.Alert.Number,
74-
alert.Alert.State, alert.Alert.Resolution);
75-
_log.LogSuccess(
76-
$" target alert successfully updated to {alert.Alert.Resolution}.");
77-
}
78-
}
62+
if (sourceAlert.Alert.Resolution == targetAlert.Alert.Resolution && sourceAlert.Alert.State == targetAlert.Alert.State)
63+
{
64+
_log.LogInformation(" source and target alerts are already aligned.");
65+
continue;
66+
}
7967

80-
private AlertWithLocations MatchTargetSecret(AlertWithLocations source, List<AlertWithLocations> targets)
81-
{
82-
AlertWithLocations matched = null;
68+
if (dryRun)
69+
{
70+
_log.LogInformation($" executing in dry run mode! Target alert {targetAlert.Alert.Number} would have been updated to state:{sourceAlert.Alert.State} and resolution:{sourceAlert.Alert.Resolution}");
71+
continue;
72+
}
8373

84-
foreach (var target in targets)
85-
{
86-
if (matched != null)
87-
{
88-
break;
89-
}
74+
_log.LogInformation($" updating target alert:{targetAlert.Alert.Number} to state:{sourceAlert.Alert.State} and resolution:{sourceAlert.Alert.Resolution}");
9075

91-
if (source.Alert.SecretType == target.Alert.SecretType
92-
&& source.Alert.Secret == target.Alert.Secret)
93-
{
94-
_log.LogVerbose(
95-
$"Secret type and value match between source:{source.Alert.Number} and target:{source.Alert.Number}");
96-
var locationMatch = true;
97-
foreach (var sourceLocation in source.Locations)
98-
{
99-
locationMatch = IsMatchedSecretAlertLocation(sourceLocation, target.Locations);
100-
if (!locationMatch)
76+
await _targetGithubApi.UpdateSecretScanningAlert(targetOrg, targetRepo, targetAlert.Alert.Number, sourceAlert.Alert.State,
77+
sourceAlert.Alert.Resolution, sourceAlert.Alert.ResolutionComment);
78+
_log.LogSuccess($" target alert successfully updated to {sourceAlert.Alert.Resolution}.");
79+
}
80+
else
10181
{
102-
break;
82+
_log.LogWarning($" failed to locate a matching secret to source secret {sourceAlert.Alert.Number} in {targetOrg}/{targetRepo}");
10383
}
10484
}
105-
106-
if (locationMatch)
85+
else
10786
{
108-
matched = target;
87+
_log.LogWarning($" Failed to locate a matching secret to source secret {sourceAlert.Alert.Number} in {targetOrg}/{targetRepo}");
10988
}
11089
}
11190
}
91+
}
11292

113-
return matched;
93+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0075: Conditional expression can be simplified", Justification = "Want to keep guard for better performance.")]
94+
private bool DoAllLocationsMatch(GithubSecretScanningAlertLocation[] sourceLocations, GithubSecretScanningAlertLocation[] targetLocations)
95+
{
96+
// Preflight check: Compare the number of locations;
97+
// If the number of locations don't match we can skip the detailed comparison as the alerts can't be considered equal
98+
return sourceLocations.Length != targetLocations.Length
99+
? false
100+
: sourceLocations.All(sourceLocation => IsLocationMatched(sourceLocation, targetLocations));
114101
}
115102

116-
private bool IsMatchedSecretAlertLocation(GithubSecretScanningAlertLocation sourceLocation,
117-
GithubSecretScanningAlertLocation[] targetLocations)
103+
private bool IsLocationMatched(GithubSecretScanningAlertLocation sourceLocation, GithubSecretScanningAlertLocation[] targetLocations)
118104
{
119-
// We cannot guarantee the ordering of things with the locations and the APIs, typically they would match, but cannot be sure
120-
// so we need to iterate over all the targets to ensure a match
121-
return targetLocations.Any(
122-
target => sourceLocation.Path == target.Path
123-
&& sourceLocation.StartLine == target.StartLine
124-
&& sourceLocation.EndLine == target.EndLine
125-
&& sourceLocation.StartColumn == target.StartColumn
126-
&& sourceLocation.EndColumn == target.EndColumn
127-
&& sourceLocation.BlobSha == target.BlobSha
128-
// Technically this wil hold, but only if there is not commit rewriting going on, so we need to make this last one optional for now
129-
// && sourceDetails.CommitSha == target.Details.CommitSha)
130-
);
105+
return targetLocations.Any(targetLocation => AreLocationsEqual(sourceLocation, targetLocation));
106+
}
107+
108+
// Check if the locations of the source and target alerts match exactly
109+
// We compare the type of location and the corresponding fields based on the type
110+
// Each type has different fields that need to be compared for equality so we use a switch statement
111+
// Note: Discussions are commented out as we don't miggate them currently
112+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0075: Conditional expression can be simplified", Justification = "Want to keep guard for better performance.")]
113+
private bool AreLocationsEqual(GithubSecretScanningAlertLocation sourceLocation, GithubSecretScanningAlertLocation targetLocation)
114+
{
115+
return sourceLocation.LocationType != targetLocation.LocationType
116+
? false
117+
: sourceLocation.LocationType switch
118+
{
119+
"commit" or "wiki_commit" => sourceLocation.Path == targetLocation.Path &&
120+
sourceLocation.StartLine == targetLocation.StartLine &&
121+
sourceLocation.EndLine == targetLocation.EndLine &&
122+
sourceLocation.StartColumn == targetLocation.StartColumn &&
123+
sourceLocation.EndColumn == targetLocation.EndColumn &&
124+
sourceLocation.BlobSha == targetLocation.BlobSha,
125+
"issue_title" => sourceLocation.IssueTitleUrl == targetLocation.IssueTitleUrl,
126+
"issue_body" => sourceLocation.IssueBodyUrl == targetLocation.IssueBodyUrl,
127+
"issue_comment" => sourceLocation.IssueCommentUrl == targetLocation.IssueCommentUrl,
128+
"pull_request_title" => sourceLocation.PullRequestTitleUrl == targetLocation.PullRequestTitleUrl,
129+
"pull_request_body" => sourceLocation.PullRequestBodyUrl == targetLocation.PullRequestBodyUrl,
130+
"pull_request_comment" => sourceLocation.PullRequestCommentUrl == targetLocation.PullRequestCommentUrl,
131+
"pull_request_review" => sourceLocation.PullRequestReviewUrl == targetLocation.PullRequestReviewUrl,
132+
"pull_request_review_comment" => sourceLocation.PullRequestReviewCommentUrl == targetLocation.PullRequestReviewCommentUrl,
133+
_ => false
134+
};
131135
}
132136

133-
private async Task<List<AlertWithLocations>> GetAlertsWithLocations(GithubApi api, string org, string repo)
137+
// Getting alerts with locations from a repository and building a dictionary with a key (SecretType, Secret)
138+
// and value List of AlertWithLocations
139+
// This method is used to get alerts from both source and target repositories
140+
private async Task<Dictionary<(string SecretType, string Secret), List<AlertWithLocations>>>
141+
GetAlertsWithLocations(GithubApi api, string org, string repo)
134142
{
135143
var alerts = await api.GetSecretScanningAlertsForRepository(org, repo);
136-
var results = new List<AlertWithLocations>();
144+
var alertsWithLocations = new List<AlertWithLocations>();
137145
foreach (var alert in alerts)
138146
{
139-
var locations =
140-
await api.GetSecretScanningAlertsLocations(org, repo, alert.Number);
141-
results.Add(new AlertWithLocations { Alert = alert, Locations = locations.ToArray() });
147+
var locations = await api.GetSecretScanningAlertsLocations(org, repo, alert.Number);
148+
alertsWithLocations.Add(new AlertWithLocations { Alert = alert, Locations = locations.ToArray() });
142149
}
143150

144-
return results;
151+
// Build the dictionary keyed by SecretType and Secret
152+
return alertsWithLocations
153+
.GroupBy(alert => (alert.Alert.SecretType, alert.Alert.Secret))
154+
.ToDictionary(group => group.Key, group => group.ToList());
145155
}
146156
}
147157

src/OctoshiftCLI.Tests/Octoshift/Services/GithubApiTests.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -2863,16 +2863,18 @@ public async Task UpdateSecretScanningAlert_Calls_The_Right_Endpoint_With_Payloa
28632863
const int alertNumber = 100;
28642864
const string alertState = "resolved";
28652865
const string alertResolution = "wont_fix";
2866+
const string alertResolutionComment = "This is a false positive";
28662867

28672868
var url = $"https://api.github.com/repos/{GITHUB_ORG}/{GITHUB_REPO}/secret-scanning/alerts/{alertNumber}";
28682869
var payload = new
28692870
{
28702871
state = alertState,
2871-
resolution = alertResolution
2872+
resolution = alertResolution,
2873+
resolution_comment = alertResolutionComment
28722874
};
28732875

28742876
// Act
2875-
await _githubApi.UpdateSecretScanningAlert(GITHUB_ORG, GITHUB_REPO, alertNumber, alertState, alertResolution);
2877+
await _githubApi.UpdateSecretScanningAlert(GITHUB_ORG, GITHUB_REPO, alertNumber, alertState, alertResolution, alertResolutionComment);
28762878

28772879
// Assert
28782880
_githubClientMock.Verify(m => m.PatchAsync(url, It.Is<object>(x => x.ToJson() == payload.ToJson()), null));

0 commit comments

Comments
 (0)