@@ -66,8 +66,8 @@ fn key_from_spki(key: &GitHubPublicKey) -> Result<Vec<u8>, std::io::Error> {
66
66
/// Check if cache of public keys is populated and not expired
67
67
fn is_cache_valid ( timestamp : Option < chrono:: DateTime < chrono:: Utc > > ) -> bool {
68
68
timestamp. is_some ( )
69
- && chrono:: Utc :: now ( ) - timestamp . unwrap ( )
70
- < chrono:: Duration :: seconds ( PUBLIC_KEY_CACHE_LIFETIME_SECONDS )
69
+ && chrono:: Utc :: now ( )
70
+ < timestamp . unwrap ( ) + chrono:: Duration :: seconds ( PUBLIC_KEY_CACHE_LIFETIME_SECONDS )
71
71
}
72
72
73
73
// Fetches list of public keys from GitHub API
@@ -82,8 +82,7 @@ fn get_public_keys(req: &dyn RequestExt) -> Result<Vec<GitHubPublicKey>, Box<dyn
82
82
let app = req. app ( ) ;
83
83
let keys = app
84
84
. github
85
- . public_keys ( & app. config . gh_client_id , & app. config . gh_client_secret )
86
- . unwrap ( ) ;
85
+ . public_keys ( & app. config . gh_client_id , & app. config . gh_client_secret ) ?;
87
86
88
87
// Populate cache
89
88
if let Ok ( mut cache) = PUBLIC_KEY_CACHE . lock ( ) {
@@ -110,32 +109,31 @@ fn verify_github_signature(req: &dyn RequestExt, json: &[u8]) -> Result<(), Box<
110
109
let public_keys = get_public_keys ( req)
111
110
. map_err ( |e| bad_request ( & format ! ( "failed to fetch GitHub public keys: {e:?}" ) ) ) ?;
112
111
113
- for key in public_keys {
114
- if key. key_identifier == req_key_id {
115
- if !key. is_current {
116
- return Err ( bad_request ( & format ! (
117
- "key id {req_key_id} is not a current key"
118
- ) ) ) ;
119
- }
120
- let key_bytes =
121
- key_from_spki ( & key) . map_err ( |_| bad_request ( "cannot parse public key" ) ) ?;
122
- let gh_key =
123
- signature:: UnparsedPublicKey :: new ( & signature:: ECDSA_P256_SHA256_ASN1 , & key_bytes) ;
124
-
125
- return match gh_key. verify ( json, & sig) {
126
- Ok ( v) => {
127
- info ! (
128
- "GitHub secret alert request validated with key id {}" ,
129
- key. key_identifier
130
- ) ;
131
- Ok ( v)
132
- }
133
- Err ( e) => Err ( bad_request ( & format ! ( "invalid signature: {e:?}" ) ) ) ,
134
- } ;
135
- }
112
+ let key = public_keys
113
+ . iter ( )
114
+ . find ( |key| key. key_identifier == req_key_id) ;
115
+
116
+ let Some ( key) = key else {
117
+ return Err ( bad_request ( & format ! ( "unknown key id {req_key_id}" ) ) ) ;
118
+ } ;
119
+
120
+ if !key. is_current {
121
+ let error = bad_request ( & format ! ( "key id {req_key_id} is not a current key" ) ) ;
122
+ return Err ( error) ;
136
123
}
137
124
138
- return Err ( bad_request ( & format ! ( "unknown key id {req_key_id}" ) ) ) ;
125
+ let key_bytes = key_from_spki ( key) . map_err ( |_| bad_request ( "cannot parse public key" ) ) ?;
126
+ let gh_key = signature:: UnparsedPublicKey :: new ( & signature:: ECDSA_P256_SHA256_ASN1 , & key_bytes) ;
127
+
128
+ gh_key
129
+ . verify ( json, & sig)
130
+ . map_err ( |e| bad_request ( & format ! ( "invalid signature: {e:?}" ) ) ) ?;
131
+
132
+ debug ! (
133
+ key_id = %key. key_identifier,
134
+ "GitHub secret alert request validated" ,
135
+ ) ;
136
+ Ok ( ( ) )
139
137
}
140
138
141
139
#[ derive( Deserialize , Serialize ) ]
@@ -150,45 +148,64 @@ struct GitHubSecretAlert {
150
148
fn alert_revoke_token (
151
149
req : & dyn RequestExt ,
152
150
alert : & GitHubSecretAlert ,
153
- ) -> Result < ( ) , Box < dyn AppError > > {
151
+ ) -> Result < GitHubSecretAlertFeedbackLabel , Box < dyn AppError > > {
154
152
let conn = req. db_write ( ) ?;
155
153
156
154
// not using ApiToken::find_by_api_token in order to preserve last_used_at
157
155
// the token field has a uniqueness constraint so get_result() should be safe to use
158
- let token: ApiToken = diesel:: update ( api_tokens:: table)
156
+ let token = diesel:: update ( api_tokens:: table)
159
157
. filter ( api_tokens:: token. eq ( alert. token . as_bytes ( ) ) )
160
158
. set ( api_tokens:: revoked. eq ( true ) )
161
- . get_result :: < ApiToken > ( & * conn) ?;
159
+ . get_result :: < ApiToken > ( & * conn)
160
+ . optional ( ) ?;
161
+
162
+ let Some ( token) = token else {
163
+ return Ok ( GitHubSecretAlertFeedbackLabel :: FalsePositive ) ;
164
+ } ;
162
165
163
166
// send email notification to the token owner
164
167
let user = User :: find ( & conn, token. user_id ) ?;
165
- info ! (
168
+ warn ! (
166
169
"Revoked API token '{}' for user {} ({})" ,
167
170
alert. token, user. gh_login, user. id
168
171
) ;
169
- match user. email ( & conn) ? {
170
- None => {
171
- info ! (
172
- "No email address for user {} ({}), cannot send email notification" ,
173
- user. gh_login, user. id
174
- ) ;
175
- Ok ( ( ) )
176
- }
177
- Some ( email) => req. app ( ) . emails . send_token_exposed_notification (
172
+
173
+ if let Some ( email) = user. email ( & conn) ? {
174
+ let result = req. app ( ) . emails . send_token_exposed_notification (
178
175
& email,
179
176
& alert. url ,
180
177
"GitHub" ,
181
178
& alert. source ,
182
179
& token. name ,
183
- ) ,
184
- }
180
+ ) ;
181
+ if let Err ( error) = result {
182
+ warn ! (
183
+ gh_login = %user. gh_login, user_id = %user. id, ?error,
184
+ "Failed to send email notification" ,
185
+ ) ;
186
+ }
187
+ } else {
188
+ warn ! (
189
+ gh_login = %user. gh_login, user_id = %user. id,
190
+ "Failed to send email notification: No address found" ,
191
+ ) ;
192
+ } ;
193
+
194
+ Ok ( GitHubSecretAlertFeedbackLabel :: TruePositive )
185
195
}
186
196
187
197
#[ derive( Deserialize , Serialize ) ]
188
198
pub struct GitHubSecretAlertFeedback {
189
199
pub token_raw : String ,
190
200
pub token_type : String ,
191
- pub label : String ,
201
+ pub label : GitHubSecretAlertFeedbackLabel ,
202
+ }
203
+
204
+ #[ derive( Copy , Clone , Debug , Eq , PartialEq , Deserialize , Serialize ) ]
205
+ #[ serde( rename_all = "snake_case" ) ]
206
+ pub enum GitHubSecretAlertFeedbackLabel {
207
+ TruePositive ,
208
+ FalsePositive ,
192
209
}
193
210
194
211
/// Handles the `POST /api/github/secret-scanning/verify` route.
@@ -207,28 +224,20 @@ pub fn verify(req: &mut dyn RequestExt) -> EndpointResult {
207
224
verify_github_signature ( req, & json)
208
225
. map_err ( |e| bad_request ( & format ! ( "failed to verify request signature: {e:?}" ) ) ) ?;
209
226
210
- let json = String :: from_utf8 ( json)
211
- . map_err ( |e| bad_request ( & format ! ( "failed to decode request body: {e:?}" ) ) ) ?;
212
- let alerts: Vec < GitHubSecretAlert > = json:: from_str ( & json)
227
+ let alerts: Vec < GitHubSecretAlert > = json:: from_slice ( & json)
213
228
. map_err ( |e| bad_request ( & format ! ( "invalid secret alert request: {e:?}" ) ) ) ?;
214
229
215
- let feedback: Vec < GitHubSecretAlertFeedback > = alerts
230
+ let feedback = alerts
216
231
. into_iter ( )
217
- . map ( |alert| GitHubSecretAlertFeedback {
218
- token_raw : alert. token . clone ( ) ,
219
- token_type : alert. r#type . clone ( ) ,
220
- label : match alert_revoke_token ( req, & alert) {
221
- Ok ( ( ) ) => "true_positive" . to_string ( ) ,
222
- Err ( e) => {
223
- warn ! (
224
- "Error revoking API token in GitHub secret alert: {} ({e:?})" ,
225
- alert. token
226
- ) ;
227
- "false_positive" . to_string ( )
228
- }
229
- } ,
232
+ . map ( |alert| {
233
+ let label = alert_revoke_token ( req, & alert) ?;
234
+ Ok ( GitHubSecretAlertFeedback {
235
+ token_raw : alert. token ,
236
+ token_type : alert. r#type ,
237
+ label,
238
+ } )
230
239
} )
231
- . collect ( ) ;
240
+ . collect :: < Result < Vec < GitHubSecretAlertFeedback > , Box < dyn AppError > > > ( ) ? ;
232
241
233
242
Ok ( req. json ( & feedback) )
234
243
}
0 commit comments