@@ -18,130 +18,140 @@ public SecretScanningAlertService(GithubApi sourceGithubApi, GithubApi targetGit
18
18
_log = logger ;
19
19
}
20
20
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
21
24
public virtual async Task MigrateSecretScanningAlerts ( string sourceOrg , string sourceRepo , string targetOrg ,
22
- string targetRepo , bool dryRun )
25
+ string targetRepo , bool dryRun )
23
26
{
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 } '") ;
26
28
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 ) ;
29
31
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 } ") ;
32
34
33
35
_log . LogInformation ( "Matching secret resolutions from source to target repository" ) ;
34
- foreach ( var alert in sourceAlerts )
36
+
37
+ foreach ( var kvp in sourceAlertsDict )
35
38
{
36
- _log . LogInformation ( $ "Processing source secret { alert . Alert . Number } ") ;
39
+ var sourceKey = kvp . Key ;
40
+ var sourceAlerts = kvp . Value ;
37
41
38
- if ( SecretScanningAlert . IsOpen ( alert . Alert . State ) )
42
+ foreach ( var sourceAlert in sourceAlerts )
39
43
{
40
- _log . LogInformation ( " secret alert is still open, nothing to do" ) ;
41
- continue ;
42
- }
44
+ _log . LogInformation ( $ "Processing source secret { sourceAlert . Alert . Number } ") ;
43
45
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
+ }
53
51
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..." ) ;
56
53
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 ) ) ;
69
57
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 } .") ;
72
61
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
+ }
79
67
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
+ }
83
73
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 } ") ;
90
75
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
101
81
{
102
- break ;
82
+ _log . LogWarning ( $ " failed to locate a matching secret to source secret { sourceAlert . Alert . Number } in { targetOrg } / { targetRepo } " ) ;
103
83
}
104
84
}
105
-
106
- if ( locationMatch )
85
+ else
107
86
{
108
- matched = target ;
87
+ _log . LogWarning ( $ " Failed to locate a matching secret to source secret { sourceAlert . Alert . Number } in { targetOrg } / { targetRepo } " ) ;
109
88
}
110
89
}
111
90
}
91
+ }
112
92
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 ) ) ;
114
101
}
115
102
116
- private bool IsMatchedSecretAlertLocation ( GithubSecretScanningAlertLocation sourceLocation ,
117
- GithubSecretScanningAlertLocation [ ] targetLocations )
103
+ private bool IsLocationMatched ( GithubSecretScanningAlertLocation sourceLocation , GithubSecretScanningAlertLocation [ ] targetLocations )
118
104
{
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
+ } ;
131
135
}
132
136
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 )
134
142
{
135
143
var alerts = await api . GetSecretScanningAlertsForRepository ( org , repo ) ;
136
- var results = new List < AlertWithLocations > ( ) ;
144
+ var alertsWithLocations = new List < AlertWithLocations > ( ) ;
137
145
foreach ( var alert in alerts )
138
146
{
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 ( ) } ) ;
142
149
}
143
150
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 ( ) ) ;
145
155
}
146
156
}
147
157
0 commit comments