diff --git a/binary/proto/scan_result.proto b/binary/proto/scan_result.proto index 3930c423e..1909b7096 100644 --- a/binary/proto/scan_result.proto +++ b/binary/proto/scan_result.proto @@ -797,6 +797,7 @@ message SecretData { SquarePersonalAccessToken square_personal_access_token = 69; SquareOAuthApplicationSecret square_oauth_application_secret = 70; SalesforceOAuth2JWTCredentials salesforce_oauth2_jwt_credentials = 71; + SendGridAPIKey sendgrid_api_key = 72; } message GCPSAK { @@ -1121,6 +1122,10 @@ message SecretData { string token = 1; } + message SendGridAPIKey { + string key = 1; + } + message SalesforceOAuth2ClientCredentials { // Salesforce OAuth2 client ID string id = 1; diff --git a/binary/proto/scan_result_go_proto/scan_result.pb.go b/binary/proto/scan_result_go_proto/scan_result.pb.go index 60079c2da..d7f7d3d8b 100644 --- a/binary/proto/scan_result_go_proto/scan_result.pb.go +++ b/binary/proto/scan_result_go_proto/scan_result.pb.go @@ -5715,6 +5715,7 @@ type SecretData struct { // *SecretData_SquarePersonalAccessToken_ // *SecretData_SquareOauthApplicationSecret // *SecretData_SalesforceOauth2JwtCredentials + // *SecretData_SendgridApiKey Secret isSecretData_Secret `protobuf_oneof:"secret"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -6396,6 +6397,15 @@ func (x *SecretData) GetSalesforceOauth2JwtCredentials() *SecretData_SalesforceO return nil } +func (x *SecretData) GetSendgridApiKey() *SecretData_SendGridAPIKey { + if x != nil { + if x, ok := x.Secret.(*SecretData_SendgridApiKey); ok { + return x.SendgridApiKey + } + } + return nil +} + type isSecretData_Secret interface { isSecretData_Secret() } @@ -6684,6 +6694,10 @@ type SecretData_SalesforceOauth2JwtCredentials struct { SalesforceOauth2JwtCredentials *SecretData_SalesforceOAuth2JWTCredentials `protobuf:"bytes,71,opt,name=salesforce_oauth2_jwt_credentials,json=salesforceOauth2JwtCredentials,proto3,oneof"` } +type SecretData_SendgridApiKey struct { + SendgridApiKey *SecretData_SendGridAPIKey `protobuf:"bytes,72,opt,name=sendgrid_api_key,json=sendgridApiKey,proto3,oneof"` +} + func (*SecretData_Gcpsak) isSecretData_Secret() {} func (*SecretData_AnthropicWorkspaceApiKey) isSecretData_Secret() {} @@ -6826,6 +6840,8 @@ func (*SecretData_SquareOauthApplicationSecret) isSecretData_Secret() {} func (*SecretData_SalesforceOauth2JwtCredentials) isSecretData_Secret() {} +func (*SecretData_SendgridApiKey) isSecretData_Secret() {} + type SecretStatus struct { state protoimpl.MessageState `protogen:"open.v1"` Status SecretStatus_SecretStatusEnum `protobuf:"varint,1,opt,name=status,proto3,enum=scalibr.SecretStatus_SecretStatusEnum" json:"status,omitempty"` @@ -10666,6 +10682,50 @@ func (x *SecretData_SalesforceOAuth2AccessToken) GetToken() string { return "" } +type SecretData_SendGridAPIKey struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecretData_SendGridAPIKey) Reset() { + *x = SecretData_SendGridAPIKey{} + mi := &file_proto_scan_result_proto_msgTypes[137] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecretData_SendGridAPIKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretData_SendGridAPIKey) ProtoMessage() {} + +func (x *SecretData_SendGridAPIKey) ProtoReflect() protoreflect.Message { + mi := &file_proto_scan_result_proto_msgTypes[137] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecretData_SendGridAPIKey.ProtoReflect.Descriptor instead. +func (*SecretData_SendGridAPIKey) Descriptor() ([]byte, []int) { + return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 62} +} + +func (x *SecretData_SendGridAPIKey) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + type SecretData_SalesforceOAuth2ClientCredentials struct { state protoimpl.MessageState `protogen:"open.v1"` // Salesforce OAuth2 client ID @@ -10680,7 +10740,7 @@ type SecretData_SalesforceOAuth2ClientCredentials struct { func (x *SecretData_SalesforceOAuth2ClientCredentials) Reset() { *x = SecretData_SalesforceOAuth2ClientCredentials{} - mi := &file_proto_scan_result_proto_msgTypes[137] + mi := &file_proto_scan_result_proto_msgTypes[138] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10692,7 +10752,7 @@ func (x *SecretData_SalesforceOAuth2ClientCredentials) String() string { func (*SecretData_SalesforceOAuth2ClientCredentials) ProtoMessage() {} func (x *SecretData_SalesforceOAuth2ClientCredentials) ProtoReflect() protoreflect.Message { - mi := &file_proto_scan_result_proto_msgTypes[137] + mi := &file_proto_scan_result_proto_msgTypes[138] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10705,7 +10765,7 @@ func (x *SecretData_SalesforceOAuth2ClientCredentials) ProtoReflect() protorefle // Deprecated: Use SecretData_SalesforceOAuth2ClientCredentials.ProtoReflect.Descriptor instead. func (*SecretData_SalesforceOAuth2ClientCredentials) Descriptor() ([]byte, []int) { - return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 62} + return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 63} } func (x *SecretData_SalesforceOAuth2ClientCredentials) GetId() string { @@ -10743,7 +10803,7 @@ type SecretData_SalesforceOAuth2RefreshCredentials struct { func (x *SecretData_SalesforceOAuth2RefreshCredentials) Reset() { *x = SecretData_SalesforceOAuth2RefreshCredentials{} - mi := &file_proto_scan_result_proto_msgTypes[138] + mi := &file_proto_scan_result_proto_msgTypes[139] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10755,7 +10815,7 @@ func (x *SecretData_SalesforceOAuth2RefreshCredentials) String() string { func (*SecretData_SalesforceOAuth2RefreshCredentials) ProtoMessage() {} func (x *SecretData_SalesforceOAuth2RefreshCredentials) ProtoReflect() protoreflect.Message { - mi := &file_proto_scan_result_proto_msgTypes[138] + mi := &file_proto_scan_result_proto_msgTypes[139] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10768,7 +10828,7 @@ func (x *SecretData_SalesforceOAuth2RefreshCredentials) ProtoReflect() protorefl // Deprecated: Use SecretData_SalesforceOAuth2RefreshCredentials.ProtoReflect.Descriptor instead. func (*SecretData_SalesforceOAuth2RefreshCredentials) Descriptor() ([]byte, []int) { - return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 63} + return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 64} } func (x *SecretData_SalesforceOAuth2RefreshCredentials) GetId() string { @@ -10806,7 +10866,7 @@ type SecretData_SalesforceOAuth2JWTCredentials struct { func (x *SecretData_SalesforceOAuth2JWTCredentials) Reset() { *x = SecretData_SalesforceOAuth2JWTCredentials{} - mi := &file_proto_scan_result_proto_msgTypes[139] + mi := &file_proto_scan_result_proto_msgTypes[140] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10818,7 +10878,7 @@ func (x *SecretData_SalesforceOAuth2JWTCredentials) String() string { func (*SecretData_SalesforceOAuth2JWTCredentials) ProtoMessage() {} func (x *SecretData_SalesforceOAuth2JWTCredentials) ProtoReflect() protoreflect.Message { - mi := &file_proto_scan_result_proto_msgTypes[139] + mi := &file_proto_scan_result_proto_msgTypes[140] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10831,7 +10891,7 @@ func (x *SecretData_SalesforceOAuth2JWTCredentials) ProtoReflect() protoreflect. // Deprecated: Use SecretData_SalesforceOAuth2JWTCredentials.ProtoReflect.Descriptor instead. func (*SecretData_SalesforceOAuth2JWTCredentials) Descriptor() ([]byte, []int) { - return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 64} + return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 65} } func (x *SecretData_SalesforceOAuth2JWTCredentials) GetId() string { @@ -10864,7 +10924,7 @@ type SecretData_CursorAPIKey struct { func (x *SecretData_CursorAPIKey) Reset() { *x = SecretData_CursorAPIKey{} - mi := &file_proto_scan_result_proto_msgTypes[140] + mi := &file_proto_scan_result_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10876,7 +10936,7 @@ func (x *SecretData_CursorAPIKey) String() string { func (*SecretData_CursorAPIKey) ProtoMessage() {} func (x *SecretData_CursorAPIKey) ProtoReflect() protoreflect.Message { - mi := &file_proto_scan_result_proto_msgTypes[140] + mi := &file_proto_scan_result_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10889,7 +10949,7 @@ func (x *SecretData_CursorAPIKey) ProtoReflect() protoreflect.Message { // Deprecated: Use SecretData_CursorAPIKey.ProtoReflect.Descriptor instead. func (*SecretData_CursorAPIKey) Descriptor() ([]byte, []int) { - return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 65} + return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 66} } func (x *SecretData_CursorAPIKey) GetKey() string { @@ -10908,7 +10968,7 @@ type SecretData_CircleCIPersonalAccessToken struct { func (x *SecretData_CircleCIPersonalAccessToken) Reset() { *x = SecretData_CircleCIPersonalAccessToken{} - mi := &file_proto_scan_result_proto_msgTypes[141] + mi := &file_proto_scan_result_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10920,7 +10980,7 @@ func (x *SecretData_CircleCIPersonalAccessToken) String() string { func (*SecretData_CircleCIPersonalAccessToken) ProtoMessage() {} func (x *SecretData_CircleCIPersonalAccessToken) ProtoReflect() protoreflect.Message { - mi := &file_proto_scan_result_proto_msgTypes[141] + mi := &file_proto_scan_result_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10933,7 +10993,7 @@ func (x *SecretData_CircleCIPersonalAccessToken) ProtoReflect() protoreflect.Mes // Deprecated: Use SecretData_CircleCIPersonalAccessToken.ProtoReflect.Descriptor instead. func (*SecretData_CircleCIPersonalAccessToken) Descriptor() ([]byte, []int) { - return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 66} + return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 67} } func (x *SecretData_CircleCIPersonalAccessToken) GetToken() string { @@ -10952,7 +11012,7 @@ type SecretData_CircleCIProjectToken struct { func (x *SecretData_CircleCIProjectToken) Reset() { *x = SecretData_CircleCIProjectToken{} - mi := &file_proto_scan_result_proto_msgTypes[142] + mi := &file_proto_scan_result_proto_msgTypes[143] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10964,7 +11024,7 @@ func (x *SecretData_CircleCIProjectToken) String() string { func (*SecretData_CircleCIProjectToken) ProtoMessage() {} func (x *SecretData_CircleCIProjectToken) ProtoReflect() protoreflect.Message { - mi := &file_proto_scan_result_proto_msgTypes[142] + mi := &file_proto_scan_result_proto_msgTypes[143] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10977,7 +11037,7 @@ func (x *SecretData_CircleCIProjectToken) ProtoReflect() protoreflect.Message { // Deprecated: Use SecretData_CircleCIProjectToken.ProtoReflect.Descriptor instead. func (*SecretData_CircleCIProjectToken) Descriptor() ([]byte, []int) { - return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 67} + return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 68} } func (x *SecretData_CircleCIProjectToken) GetToken() string { @@ -10996,7 +11056,7 @@ type SecretData_URLCredentials struct { func (x *SecretData_URLCredentials) Reset() { *x = SecretData_URLCredentials{} - mi := &file_proto_scan_result_proto_msgTypes[143] + mi := &file_proto_scan_result_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11008,7 +11068,7 @@ func (x *SecretData_URLCredentials) String() string { func (*SecretData_URLCredentials) ProtoMessage() {} func (x *SecretData_URLCredentials) ProtoReflect() protoreflect.Message { - mi := &file_proto_scan_result_proto_msgTypes[143] + mi := &file_proto_scan_result_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11021,7 +11081,7 @@ func (x *SecretData_URLCredentials) ProtoReflect() protoreflect.Message { // Deprecated: Use SecretData_URLCredentials.ProtoReflect.Descriptor instead. func (*SecretData_URLCredentials) Descriptor() ([]byte, []int) { - return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 68} + return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 69} } func (x *SecretData_URLCredentials) GetUrl() string { @@ -11040,7 +11100,7 @@ type SecretData_SquarePersonalAccessToken struct { func (x *SecretData_SquarePersonalAccessToken) Reset() { *x = SecretData_SquarePersonalAccessToken{} - mi := &file_proto_scan_result_proto_msgTypes[144] + mi := &file_proto_scan_result_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11052,7 +11112,7 @@ func (x *SecretData_SquarePersonalAccessToken) String() string { func (*SecretData_SquarePersonalAccessToken) ProtoMessage() {} func (x *SecretData_SquarePersonalAccessToken) ProtoReflect() protoreflect.Message { - mi := &file_proto_scan_result_proto_msgTypes[144] + mi := &file_proto_scan_result_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11065,7 +11125,7 @@ func (x *SecretData_SquarePersonalAccessToken) ProtoReflect() protoreflect.Messa // Deprecated: Use SecretData_SquarePersonalAccessToken.ProtoReflect.Descriptor instead. func (*SecretData_SquarePersonalAccessToken) Descriptor() ([]byte, []int) { - return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 69} + return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 70} } func (x *SecretData_SquarePersonalAccessToken) GetKey() string { @@ -11085,7 +11145,7 @@ type SecretData_SquareOAuthApplicationSecret struct { func (x *SecretData_SquareOAuthApplicationSecret) Reset() { *x = SecretData_SquareOAuthApplicationSecret{} - mi := &file_proto_scan_result_proto_msgTypes[145] + mi := &file_proto_scan_result_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11097,7 +11157,7 @@ func (x *SecretData_SquareOAuthApplicationSecret) String() string { func (*SecretData_SquareOAuthApplicationSecret) ProtoMessage() {} func (x *SecretData_SquareOAuthApplicationSecret) ProtoReflect() protoreflect.Message { - mi := &file_proto_scan_result_proto_msgTypes[145] + mi := &file_proto_scan_result_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11110,7 +11170,7 @@ func (x *SecretData_SquareOAuthApplicationSecret) ProtoReflect() protoreflect.Me // Deprecated: Use SecretData_SquareOAuthApplicationSecret.ProtoReflect.Descriptor instead. func (*SecretData_SquareOAuthApplicationSecret) Descriptor() ([]byte, []int) { - return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 70} + return file_proto_scan_result_proto_rawDescGZIP(), []int{62, 71} } func (x *SecretData_SquareOAuthApplicationSecret) GetId() string { @@ -11581,7 +11641,7 @@ const file_proto_scan_result_proto_rawDesc = "" + "\x06Secret\x12+\n" + "\x06secret\x18\x01 \x01(\v2\x13.scalibr.SecretDataR\x06secret\x12-\n" + "\x06status\x18\x02 \x01(\v2\x15.scalibr.SecretStatusR\x06status\x12/\n" + - "\tlocations\x18\x03 \x03(\v2\x11.scalibr.LocationR\tlocations\"\xfcZ\n" + + "\tlocations\x18\x03 \x03(\v2\x11.scalibr.LocationR\tlocations\"\xf0[\n" + "\n" + "SecretData\x124\n" + "\x06gcpsak\x18\x01 \x01(\v2\x1a.scalibr.SecretData.GCPSAKH\x00R\x06gcpsak\x12m\n" + @@ -11665,7 +11725,8 @@ const file_proto_scan_result_proto_rawDesc = "" + "\x16circleci_project_token\x18D \x01(\v2(.scalibr.SecretData.CircleCIProjectTokenH\x00R\x14circleciProjectToken\x12p\n" + "\x1csquare_personal_access_token\x18E \x01(\v2-.scalibr.SecretData.SquarePersonalAccessTokenH\x00R\x19squarePersonalAccessToken\x12y\n" + "\x1fsquare_oauth_application_secret\x18F \x01(\v20.scalibr.SecretData.SquareOAuthApplicationSecretH\x00R\x1csquareOauthApplicationSecret\x12\x7f\n" + - "!salesforce_oauth2_jwt_credentials\x18G \x01(\v22.scalibr.SecretData.SalesforceOAuth2JWTCredentialsH\x00R\x1esalesforceOauth2JwtCredentials\x1a\xb0\x03\n" + + "!salesforce_oauth2_jwt_credentials\x18G \x01(\v22.scalibr.SecretData.SalesforceOAuth2JWTCredentialsH\x00R\x1esalesforceOauth2JwtCredentials\x12N\n" + + "\x10sendgrid_api_key\x18H \x01(\v2\".scalibr.SecretData.SendGridAPIKeyH\x00R\x0esendgridApiKey\x1a\xb0\x03\n" + "\x06GCPSAK\x12$\n" + "\x0eprivate_key_id\x18\x01 \x01(\tR\fprivateKeyId\x12!\n" + "\fclient_email\x18\x02 \x01(\tR\vclientEmail\x12\x1c\n" + @@ -11850,7 +11911,9 @@ const file_proto_scan_result_proto_rawDesc = "" + "\x10TelegramBotToken\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\x1a3\n" + "\x1bSalesforceOAuth2AccessToken\x12\x14\n" + - "\x05token\x18\x01 \x01(\tR\x05token\x1a]\n" + + "\x05token\x18\x01 \x01(\tR\x05token\x1a\"\n" + + "\x0eSendGridAPIKey\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x1a]\n" + "!SalesforceOAuth2ClientCredentials\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + "\x06secret\x18\x02 \x01(\tR\x06secret\x12\x10\n" + @@ -11964,7 +12027,7 @@ func file_proto_scan_result_proto_rawDescGZIP() []byte { } var file_proto_scan_result_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_proto_scan_result_proto_msgTypes = make([]protoimpl.MessageInfo, 147) +var file_proto_scan_result_proto_msgTypes = make([]protoimpl.MessageInfo, 148) var file_proto_scan_result_proto_goTypes = []any{ (VexJustification)(0), // 0: scalibr.VexJustification (SeverityEnum)(0), // 1: scalibr.SeverityEnum @@ -12108,22 +12171,23 @@ var file_proto_scan_result_proto_goTypes = []any{ (*SecretData_PaystackSecretKey)(nil), // 139: scalibr.SecretData.PaystackSecretKey (*SecretData_TelegramBotToken)(nil), // 140: scalibr.SecretData.TelegramBotToken (*SecretData_SalesforceOAuth2AccessToken)(nil), // 141: scalibr.SecretData.SalesforceOAuth2AccessToken - (*SecretData_SalesforceOAuth2ClientCredentials)(nil), // 142: scalibr.SecretData.SalesforceOAuth2ClientCredentials - (*SecretData_SalesforceOAuth2RefreshCredentials)(nil), // 143: scalibr.SecretData.SalesforceOAuth2RefreshCredentials - (*SecretData_SalesforceOAuth2JWTCredentials)(nil), // 144: scalibr.SecretData.SalesforceOAuth2JWTCredentials - (*SecretData_CursorAPIKey)(nil), // 145: scalibr.SecretData.CursorAPIKey - (*SecretData_CircleCIPersonalAccessToken)(nil), // 146: scalibr.SecretData.CircleCIPersonalAccessToken - (*SecretData_CircleCIProjectToken)(nil), // 147: scalibr.SecretData.CircleCIProjectToken - (*SecretData_URLCredentials)(nil), // 148: scalibr.SecretData.URLCredentials - (*SecretData_SquarePersonalAccessToken)(nil), // 149: scalibr.SecretData.SquarePersonalAccessToken - (*SecretData_SquareOAuthApplicationSecret)(nil), // 150: scalibr.SecretData.SquareOAuthApplicationSecret - nil, // 151: scalibr.ContainerImageMetadata.OsInfoEntry - (*timestamppb.Timestamp)(nil), // 152: google.protobuf.Timestamp - (*osvschema.Vulnerability)(nil), // 153: osv.Vulnerability + (*SecretData_SendGridAPIKey)(nil), // 142: scalibr.SecretData.SendGridAPIKey + (*SecretData_SalesforceOAuth2ClientCredentials)(nil), // 143: scalibr.SecretData.SalesforceOAuth2ClientCredentials + (*SecretData_SalesforceOAuth2RefreshCredentials)(nil), // 144: scalibr.SecretData.SalesforceOAuth2RefreshCredentials + (*SecretData_SalesforceOAuth2JWTCredentials)(nil), // 145: scalibr.SecretData.SalesforceOAuth2JWTCredentials + (*SecretData_CursorAPIKey)(nil), // 146: scalibr.SecretData.CursorAPIKey + (*SecretData_CircleCIPersonalAccessToken)(nil), // 147: scalibr.SecretData.CircleCIPersonalAccessToken + (*SecretData_CircleCIProjectToken)(nil), // 148: scalibr.SecretData.CircleCIProjectToken + (*SecretData_URLCredentials)(nil), // 149: scalibr.SecretData.URLCredentials + (*SecretData_SquarePersonalAccessToken)(nil), // 150: scalibr.SecretData.SquarePersonalAccessToken + (*SecretData_SquareOAuthApplicationSecret)(nil), // 151: scalibr.SecretData.SquareOAuthApplicationSecret + nil, // 152: scalibr.ContainerImageMetadata.OsInfoEntry + (*timestamppb.Timestamp)(nil), // 153: google.protobuf.Timestamp + (*osvschema.Vulnerability)(nil), // 154: osv.Vulnerability } var file_proto_scan_result_proto_depIdxs = []int32{ - 152, // 0: scalibr.ScanResult.start_time:type_name -> google.protobuf.Timestamp - 152, // 1: scalibr.ScanResult.end_time:type_name -> google.protobuf.Timestamp + 153, // 0: scalibr.ScanResult.start_time:type_name -> google.protobuf.Timestamp + 153, // 1: scalibr.ScanResult.end_time:type_name -> google.protobuf.Timestamp 7, // 2: scalibr.ScanResult.status:type_name -> scalibr.ScanStatus 8, // 3: scalibr.ScanResult.plugin_status:type_name -> scalibr.PluginStatus 6, // 4: scalibr.ScanResult.inventory:type_name -> scalibr.Inventory @@ -12183,7 +12247,7 @@ var file_proto_scan_result_proto_depIdxs = []int32{ 14, // 58: scalibr.PackageExploitabilitySignal.vuln_identifiers:type_name -> scalibr.VulnIdentifiers 0, // 59: scalibr.FindingExploitabilitySignal.justification:type_name -> scalibr.VexJustification 17, // 60: scalibr.Purl.qualifiers:type_name -> scalibr.Qualifier - 153, // 61: scalibr.PackageVuln.vuln:type_name -> osv.Vulnerability + 154, // 61: scalibr.PackageVuln.vuln:type_name -> osv.Vulnerability 15, // 62: scalibr.PackageVuln.exploitability_signals:type_name -> scalibr.FindingExploitabilitySignal 20, // 63: scalibr.GenericFinding.adv:type_name -> scalibr.GenericFindingAdvisory 22, // 64: scalibr.GenericFinding.target:type_name -> scalibr.GenericFindingTargetDetails @@ -12194,8 +12258,8 @@ var file_proto_scan_result_proto_depIdxs = []int32{ 16, // 69: scalibr.SPDXPackageMetadata.purl:type_name -> scalibr.Purl 16, // 70: scalibr.CDXPackageMetadata.purl:type_name -> scalibr.Purl 79, // 71: scalibr.PodmanMetadata.exposed_ports:type_name -> scalibr.PodmanMetadata.ExposedPortsEntry - 152, // 72: scalibr.PodmanMetadata.started_time:type_name -> google.protobuf.Timestamp - 152, // 73: scalibr.PodmanMetadata.finished_time:type_name -> google.protobuf.Timestamp + 153, // 72: scalibr.PodmanMetadata.started_time:type_name -> google.protobuf.Timestamp + 153, // 73: scalibr.PodmanMetadata.finished_time:type_name -> google.protobuf.Timestamp 62, // 74: scalibr.DockerContainersMetadata.ports:type_name -> scalibr.DockerPort 65, // 75: scalibr.UnknownBinaryMetadata.attribution:type_name -> scalibr.UnknownBinaryAttribution 67, // 76: scalibr.Secret.secret:type_name -> scalibr.SecretData @@ -12260,35 +12324,36 @@ var file_proto_scan_result_proto_depIdxs = []int32{ 137, // 135: scalibr.SecretData.bitbucket_credentials:type_name -> scalibr.SecretData.BitBucketCredentials 139, // 136: scalibr.SecretData.paystack_secret_key:type_name -> scalibr.SecretData.PaystackSecretKey 140, // 137: scalibr.SecretData.telegram_bot_api_token:type_name -> scalibr.SecretData.TelegramBotToken - 145, // 138: scalibr.SecretData.cursor_api_key:type_name -> scalibr.SecretData.CursorAPIKey + 146, // 138: scalibr.SecretData.cursor_api_key:type_name -> scalibr.SecretData.CursorAPIKey 138, // 139: scalibr.SecretData.elastic_cloud_api_key:type_name -> scalibr.SecretData.ElasticCloudAPIKey - 148, // 140: scalibr.SecretData.url_credentials:type_name -> scalibr.SecretData.URLCredentials - 142, // 141: scalibr.SecretData.salesforce_oauth2_client_credentials:type_name -> scalibr.SecretData.SalesforceOAuth2ClientCredentials + 149, // 140: scalibr.SecretData.url_credentials:type_name -> scalibr.SecretData.URLCredentials + 143, // 141: scalibr.SecretData.salesforce_oauth2_client_credentials:type_name -> scalibr.SecretData.SalesforceOAuth2ClientCredentials 141, // 142: scalibr.SecretData.salesforce_oauth2_access_token:type_name -> scalibr.SecretData.SalesforceOAuth2AccessToken - 143, // 143: scalibr.SecretData.salesforce_oauth2_refresh_credentials:type_name -> scalibr.SecretData.SalesforceOAuth2RefreshCredentials + 144, // 143: scalibr.SecretData.salesforce_oauth2_refresh_credentials:type_name -> scalibr.SecretData.SalesforceOAuth2RefreshCredentials 85, // 144: scalibr.SecretData.mistral_api_key:type_name -> scalibr.SecretData.MistralAPIKey - 146, // 145: scalibr.SecretData.circleci_personal_access_token:type_name -> scalibr.SecretData.CircleCIPersonalAccessToken - 147, // 146: scalibr.SecretData.circleci_project_token:type_name -> scalibr.SecretData.CircleCIProjectToken - 149, // 147: scalibr.SecretData.square_personal_access_token:type_name -> scalibr.SecretData.SquarePersonalAccessToken - 150, // 148: scalibr.SecretData.square_oauth_application_secret:type_name -> scalibr.SecretData.SquareOAuthApplicationSecret - 144, // 149: scalibr.SecretData.salesforce_oauth2_jwt_credentials:type_name -> scalibr.SecretData.SalesforceOAuth2JWTCredentials - 4, // 150: scalibr.SecretStatus.status:type_name -> scalibr.SecretStatus.SecretStatusEnum - 152, // 151: scalibr.SecretStatus.last_updated:type_name -> google.protobuf.Timestamp - 70, // 152: scalibr.Location.filepath:type_name -> scalibr.Filepath - 71, // 153: scalibr.Location.filepath_with_layer_details:type_name -> scalibr.FilepathWithLayerDetails - 72, // 154: scalibr.Location.environment_variable:type_name -> scalibr.EnvironmentVariable - 73, // 155: scalibr.Location.container_command:type_name -> scalibr.ContainerCommand - 12, // 156: scalibr.FilepathWithLayerDetails.layer_details:type_name -> scalibr.LayerDetails - 77, // 157: scalibr.ContainerImageMetadata.layer_metadata:type_name -> scalibr.LayerMetadata - 75, // 158: scalibr.ContainerImageMetadata.base_image_chains:type_name -> scalibr.BaseImageChain - 151, // 159: scalibr.ContainerImageMetadata.os_info:type_name -> scalibr.ContainerImageMetadata.OsInfoEntry - 76, // 160: scalibr.BaseImageChain.base_images:type_name -> scalibr.BaseImageDetails - 56, // 161: scalibr.PodmanMetadata.ExposedPortsEntry.value:type_name -> scalibr.Protocol - 162, // [162:162] is the sub-list for method output_type - 162, // [162:162] is the sub-list for method input_type - 162, // [162:162] is the sub-list for extension type_name - 162, // [162:162] is the sub-list for extension extendee - 0, // [0:162] is the sub-list for field type_name + 147, // 145: scalibr.SecretData.circleci_personal_access_token:type_name -> scalibr.SecretData.CircleCIPersonalAccessToken + 148, // 146: scalibr.SecretData.circleci_project_token:type_name -> scalibr.SecretData.CircleCIProjectToken + 150, // 147: scalibr.SecretData.square_personal_access_token:type_name -> scalibr.SecretData.SquarePersonalAccessToken + 151, // 148: scalibr.SecretData.square_oauth_application_secret:type_name -> scalibr.SecretData.SquareOAuthApplicationSecret + 145, // 149: scalibr.SecretData.salesforce_oauth2_jwt_credentials:type_name -> scalibr.SecretData.SalesforceOAuth2JWTCredentials + 142, // 150: scalibr.SecretData.sendgrid_api_key:type_name -> scalibr.SecretData.SendGridAPIKey + 4, // 151: scalibr.SecretStatus.status:type_name -> scalibr.SecretStatus.SecretStatusEnum + 153, // 152: scalibr.SecretStatus.last_updated:type_name -> google.protobuf.Timestamp + 70, // 153: scalibr.Location.filepath:type_name -> scalibr.Filepath + 71, // 154: scalibr.Location.filepath_with_layer_details:type_name -> scalibr.FilepathWithLayerDetails + 72, // 155: scalibr.Location.environment_variable:type_name -> scalibr.EnvironmentVariable + 73, // 156: scalibr.Location.container_command:type_name -> scalibr.ContainerCommand + 12, // 157: scalibr.FilepathWithLayerDetails.layer_details:type_name -> scalibr.LayerDetails + 77, // 158: scalibr.ContainerImageMetadata.layer_metadata:type_name -> scalibr.LayerMetadata + 75, // 159: scalibr.ContainerImageMetadata.base_image_chains:type_name -> scalibr.BaseImageChain + 152, // 160: scalibr.ContainerImageMetadata.os_info:type_name -> scalibr.ContainerImageMetadata.OsInfoEntry + 76, // 161: scalibr.BaseImageChain.base_images:type_name -> scalibr.BaseImageDetails + 56, // 162: scalibr.PodmanMetadata.ExposedPortsEntry.value:type_name -> scalibr.Protocol + 163, // [163:163] is the sub-list for method output_type + 163, // [163:163] is the sub-list for method input_type + 163, // [163:163] is the sub-list for extension type_name + 163, // [163:163] is the sub-list for extension extendee + 0, // [0:163] is the sub-list for field type_name } func init() { file_proto_scan_result_proto_init() } @@ -12414,6 +12479,7 @@ func file_proto_scan_result_proto_init() { (*SecretData_SquarePersonalAccessToken_)(nil), (*SecretData_SquareOauthApplicationSecret)(nil), (*SecretData_SalesforceOauth2JwtCredentials)(nil), + (*SecretData_SendgridApiKey)(nil), } file_proto_scan_result_proto_msgTypes[64].OneofWrappers = []any{ (*Location_Filepath)(nil), @@ -12427,7 +12493,7 @@ func file_proto_scan_result_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_scan_result_proto_rawDesc), len(file_proto_scan_result_proto_rawDesc)), NumEnums: 5, - NumMessages: 147, + NumMessages: 148, NumExtensions: 0, NumServices: 0, }, diff --git a/binary/proto/secret.go b/binary/proto/secret.go index 5e235a785..598c6f624 100644 --- a/binary/proto/secret.go +++ b/binary/proto/secret.go @@ -66,6 +66,7 @@ import ( "github.com/google/osv-scalibr/veles/secrets/salesforceoauth2client" "github.com/google/osv-scalibr/veles/secrets/salesforceoauth2jwt" "github.com/google/osv-scalibr/veles/secrets/salesforceoauth2refresh" + "github.com/google/osv-scalibr/veles/secrets/sendgrid" velesslacktoken "github.com/google/osv-scalibr/veles/secrets/slacktoken" velessquareapikey "github.com/google/osv-scalibr/veles/secrets/squareapikey" velesstripeapikeys "github.com/google/osv-scalibr/veles/secrets/stripeapikeys" @@ -261,6 +262,8 @@ func velesSecretToProto(s veles.Secret) (*spb.SecretData, error) { return paystackSecretKeyToProto(t), nil case velestelegrambotapitoken.TelegramBotAPIToken: return telegramBotAPITokenToProto(t), nil + case sendgrid.APIKey: + return sendgridAPIKeyToProto(t), nil case velescircleci.PersonalAccessToken: return circleCIPersonalAccessTokenToProto(t), nil case velescircleci.ProjectToken: @@ -945,6 +948,15 @@ func telegramBotAPITokenToProto(s velestelegrambotapitoken.TelegramBotAPIToken) } } +func sendgridAPIKeyToProto(s sendgrid.APIKey) *spb.SecretData { + return &spb.SecretData{ + Secret: &spb.SecretData_SendgridApiKey{ + SendgridApiKey: &spb.SecretData_SendGridAPIKey{ + Key: s.Key, + }, + }, + } +} func salesforceOAuth2JWTCredentialsToProto(creds salesforceoauth2jwt.Credentials) *spb.SecretData { return &spb.SecretData{ Secret: &spb.SecretData_SalesforceOauth2JwtCredentials{ @@ -1256,6 +1268,10 @@ func velesSecretToStruct(s *spb.SecretData) (veles.Secret, error) { return velestelegrambotapitoken.TelegramBotAPIToken{ Token: s.GetTelegramBotApiToken().GetToken(), }, nil + case *spb.SecretData_SendgridApiKey: + return sendgrid.APIKey{ + Key: s.GetSendgridApiKey().GetKey(), + }, nil case *spb.SecretData_CircleciPersonalAccessToken: return velescircleci.PersonalAccessToken{ Token: s.GetCircleciPersonalAccessToken().GetToken(), diff --git a/enricher/enricherlist/list.go b/enricher/enricherlist/list.go index c8d98dfc2..a4ec7f8d3 100644 --- a/enricher/enricherlist/list.go +++ b/enricher/enricherlist/list.go @@ -68,6 +68,7 @@ import ( "github.com/google/osv-scalibr/veles/secrets/salesforceoauth2client" "github.com/google/osv-scalibr/veles/secrets/salesforceoauth2jwt" "github.com/google/osv-scalibr/veles/secrets/salesforceoauth2refresh" + "github.com/google/osv-scalibr/veles/secrets/sendgrid" "github.com/google/osv-scalibr/veles/secrets/slacktoken" "github.com/google/osv-scalibr/veles/secrets/squareapikey" "github.com/google/osv-scalibr/veles/secrets/stripeapikeys" @@ -113,6 +114,7 @@ var ( fromVeles(digitaloceanapikey.NewValidator(), "secrets/digitaloceanapikeyvalidate", 0), fromVeles(elasticcloudapikey.NewValidator(), "secrets/elasticcloudapikeyvalidate", 0), fromVeles(pypiapitoken.NewValidator(), "secrets/pypiapitokenvalidate", 0), + fromVeles(sendgrid.NewValidator(), "secrets/sendgridvalidate", 0), fromVeles(cratesioapitoken.NewValidator(), "secrets/cratesioapitokenvalidate", 0), fromVeles(slacktoken.NewAppLevelTokenValidator(), "secrets/slackappleveltokenvalidate", 0), fromVeles(slacktoken.NewAppConfigRefreshTokenValidator(), "secrets/slackconfigrefreshtokenvalidate", 0), diff --git a/extractor/filesystem/list/list.go b/extractor/filesystem/list/list.go index 519ab9325..0266b69ff 100644 --- a/extractor/filesystem/list/list.go +++ b/extractor/filesystem/list/list.go @@ -149,6 +149,7 @@ import ( "github.com/google/osv-scalibr/veles/secrets/salesforceoauth2client" "github.com/google/osv-scalibr/veles/secrets/salesforceoauth2jwt" "github.com/google/osv-scalibr/veles/secrets/salesforceoauth2refresh" + "github.com/google/osv-scalibr/veles/secrets/sendgrid" "github.com/google/osv-scalibr/veles/secrets/slacktoken" "github.com/google/osv-scalibr/veles/secrets/squareapikey" "github.com/google/osv-scalibr/veles/secrets/stripeapikeys" @@ -352,6 +353,7 @@ var ( {postmanapikey.NewCollectionTokenDetector(), "secrets/postmancollectiontoken", 0}, {privatekey.NewDetector(), "secrets/privatekey", 0}, {rubygemsapikey.NewDetector(), "secrets/rubygemsapikey", 0}, + {sendgrid.NewDetector(), "secrets/sendgrid", 0}, {tinkkeyset.NewDetector(), "secrets/tinkkeyset", 0}, {github.NewAppRefreshTokenDetector(), "secrets/githubapprefreshtoken", 0}, {github.NewAppS2STokenDetector(), "secrets/githubapps2stoken", 0}, diff --git a/veles/secrets/sendgrid/detector.go b/veles/secrets/sendgrid/detector.go new file mode 100644 index 000000000..7c8f613b7 --- /dev/null +++ b/veles/secrets/sendgrid/detector.go @@ -0,0 +1,43 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sendgrid + +import ( + "regexp" + + "github.com/google/osv-scalibr/veles" + "github.com/google/osv-scalibr/veles/secrets/common/simpletoken" +) + +var ( + // Ensure the constructor satisfies the interface at compile time. + _ veles.Detector = NewDetector() +) + +// SendGrid API keys are exactly 69 characters: SG.<22 chars>.<43 chars> +const maxKeyLen = 69 + +var keyRe = regexp.MustCompile(`SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}`) + +// NewDetector returns a detector for SendGrid API keys (SG.xxx.yyy). +func NewDetector() veles.Detector { + return simpletoken.Detector{ + MaxLen: maxKeyLen, + Re: keyRe, + FromMatch: func(b []byte) (veles.Secret, bool) { + return APIKey{Key: string(b)}, true + }, + } +} diff --git a/veles/secrets/sendgrid/detector_test.go b/veles/secrets/sendgrid/detector_test.go new file mode 100644 index 000000000..8b899012a --- /dev/null +++ b/veles/secrets/sendgrid/detector_test.go @@ -0,0 +1,211 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sendgrid_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/osv-scalibr/veles" + "github.com/google/osv-scalibr/veles/secrets/sendgrid" + "github.com/google/osv-scalibr/veles/velestest" +) + +// Fake SendGrid API keys for testing purposes. +// These are NOT real keys and will not work with the SendGrid API. +// They follow the correct format: SG.<22 chars>.<43 chars> = 69 total characters. +const testSendGridAPIKey = "SG.aaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +const testSendGridAPIKey2 = "SG.XXXXXXXXXXXXXXXXXXXXXX.YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY" +const testSendGridAPIKeyWithSpecialChars = "SG.abc_def-ghij12345678ab.ABC_DEF-GHIJKLMNOPQRSTUVWXYZabcdefghijk1234" + +func TestDetectorAcceptance(t *testing.T) { + velestest.AcceptDetector( + t, + sendgrid.NewDetector(), + testSendGridAPIKey, + sendgrid.APIKey{Key: testSendGridAPIKey}, + ) +} + +// TestDetector_truePositives tests for cases where we know the Detector +// will find SendGrid API keys. +func TestDetector_truePositives(t *testing.T) { + engine, err := veles.NewDetectionEngine([]veles.Detector{ + sendgrid.NewDetector(), + }) + if err != nil { + t.Fatal(err) + } + cases := []struct { + name string + input string + want []veles.Secret + }{{ + name: "simple matching string", + input: testSendGridAPIKey, + want: []veles.Secret{ + sendgrid.APIKey{Key: testSendGridAPIKey}, + }, + }, { + name: "key with underscores and dashes", + input: testSendGridAPIKeyWithSpecialChars, + want: []veles.Secret{ + sendgrid.APIKey{Key: testSendGridAPIKeyWithSpecialChars}, + }, + }, { + name: "match at end of string", + input: "SENDGRID_API_KEY=" + testSendGridAPIKey, + want: []veles.Secret{ + sendgrid.APIKey{Key: testSendGridAPIKey}, + }, + }, { + name: "match in middle of string", + input: `api_key="` + testSendGridAPIKey + `"`, + want: []veles.Secret{ + sendgrid.APIKey{Key: testSendGridAPIKey}, + }, + }, { + name: "multiple matches", + input: testSendGridAPIKey + "\n" + testSendGridAPIKey2, + want: []veles.Secret{ + sendgrid.APIKey{Key: testSendGridAPIKey}, + sendgrid.APIKey{Key: testSendGridAPIKey2}, + }, + }, { + name: "key in JSON format", + input: `{"sendgrid_api_key": "` + testSendGridAPIKey + `"}`, + want: []veles.Secret{ + sendgrid.APIKey{Key: testSendGridAPIKey}, + }, + }, { + name: "key in environment variable style", + input: `export SENDGRID_API_KEY="` + testSendGridAPIKey + `"`, + want: []veles.Secret{ + sendgrid.APIKey{Key: testSendGridAPIKey}, + }, + }, { + name: "larger input containing key", + input: fmt.Sprintf(` +:test_api_key: do-test +:SENDGRID_API_KEY: %s + `, testSendGridAPIKey), + want: []veles.Secret{ + sendgrid.APIKey{Key: testSendGridAPIKey}, + }, + }, { + name: "key followed by extra characters", + input: testSendGridAPIKey + "extra", + want: []veles.Secret{ + sendgrid.APIKey{Key: testSendGridAPIKey}, + }, + }} + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := engine.Detect(t.Context(), strings.NewReader(tc.input)) + if err != nil { + t.Errorf("Detect() error: %v, want nil", err) + } + if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("Detect() diff (-want +got):\n%s", diff) + } + }) + } +} + +// TestDetector_trueNegatives tests for cases where we know the Detector +// will not find SendGrid API keys. +func TestDetector_trueNegatives(t *testing.T) { + engine, err := veles.NewDetectionEngine([]veles.Detector{ + sendgrid.NewDetector(), + }) + if err != nil { + t.Fatal(err) + } + cases := []struct { + name string + input string + want []veles.Secret + }{{ + name: "empty input", + input: "", + }, { + name: "wrong prefix - not SG.", + input: "XX.abcdefghij1234567890AB.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk1234", + }, { + name: "lowercase sg prefix should not match", + input: "sg.abcdefghij1234567890AB.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk1234", + }, { + name: "too short key_id section", + input: "SG.short.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk1234", + }, { + name: "too short key_secret section", + input: "SG.abcdefghij1234567890AB.short", + }, { + name: "invalid characters in key_id - special chars", + input: "SG.abcdefghij123456!@#$.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk1234", + }, { + name: "invalid characters in key_secret - special chars", + input: "SG.abcdefghij1234567890AB.ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$efghijk1234", + }, { + name: "missing first dot", + input: "SGabcdefghij1234567890AB.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk1234", + }, { + name: "missing second dot", + input: "SG.abcdefghij1234567890ABABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk1234", + }, { + name: "random text without any keys", + input: "this is some random text without any API keys", + }, { + name: "partial key - truncated", + input: testSendGridAPIKey[:len(testSendGridAPIKey)-1], + }} + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := engine.Detect(t.Context(), strings.NewReader(tc.input)) + if err != nil { + t.Errorf("Detect() error: %v, want nil", err) + } + if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("Detect() diff (-want +got):\n%s", diff) + } + }) + } +} + +func TestSendGridKeyFormat(t *testing.T) { + // Test that our fake keys are the correct length + tests := []struct { + name string + key string + }{ + {"testSendGridAPIKey", testSendGridAPIKey}, + {"testSendGridAPIKey2", testSendGridAPIKey2}, + {"testSendGridAPIKeyWithSpecialChars", testSendGridAPIKeyWithSpecialChars}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if len(tt.key) != 69 { + t.Errorf("%s has length %d, want 69", tt.name, len(tt.key)) + } + if tt.key[:3] != "SG." { + t.Errorf("%s doesn't start with 'SG.'", tt.name) + } + }) + } +} diff --git a/veles/secrets/sendgrid/sendgrid.go b/veles/secrets/sendgrid/sendgrid.go new file mode 100644 index 000000000..29055cf2e --- /dev/null +++ b/veles/secrets/sendgrid/sendgrid.go @@ -0,0 +1,23 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sendgrid + +// APIKey is a Veles Secret that holds relevant information for a +// SendGrid API key (prefix `SG.`). +// APIKey represents an API key used to authenticate requests to SendGrid. +// It implements veles.Secret. +type APIKey struct { + Key string +} diff --git a/veles/secrets/sendgrid/validator.go b/veles/secrets/sendgrid/validator.go new file mode 100644 index 000000000..b5d3f1164 --- /dev/null +++ b/veles/secrets/sendgrid/validator.go @@ -0,0 +1,56 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package sendgrid provides a detector and validator for SendGrid API keys. +package sendgrid + +import ( + "io" + "net/http" + "time" + + "github.com/google/osv-scalibr/veles" + "github.com/google/osv-scalibr/veles/secrets/common/simplevalidate" +) + +const ( + httpClientTimeout = 10 * time.Second + sendGridAPIEndpoint = "https://api.sendgrid.com/v3/user/account" +) + +func alwaysFailedStatus(body io.Reader) (veles.ValidationStatus, error) { + return veles.ValidationFailed, nil +} + +// NewValidator creates a validator for SendGrid API keys. +// It calls GET /v3/user/account with Bearer auth. +// 200 OK -> valid key +// 403 Forbidden -> valid key (restricted scopes, but key exists and is active) +// 401 Unauthorized -> invalid key +func NewValidator() *simplevalidate.Validator[APIKey] { + return &simplevalidate.Validator[APIKey]{ + Endpoint: sendGridAPIEndpoint, + HTTPMethod: http.MethodGet, + HTTPHeaders: func(k APIKey) map[string]string { + return map[string]string{ + "Authorization": "Bearer " + k.Key, + "Content-Type": "application/json", + } + }, + ValidResponseCodes: []int{http.StatusOK, http.StatusForbidden}, + InvalidResponseCodes: []int{http.StatusUnauthorized}, + StatusFromResponseBody: alwaysFailedStatus, + HTTPC: &http.Client{Timeout: httpClientTimeout}, + } +} diff --git a/veles/secrets/sendgrid/validator_test.go b/veles/secrets/sendgrid/validator_test.go new file mode 100644 index 000000000..e3812977e --- /dev/null +++ b/veles/secrets/sendgrid/validator_test.go @@ -0,0 +1,229 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sendgrid_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/google/osv-scalibr/veles" + "github.com/google/osv-scalibr/veles/secrets/sendgrid" +) + +// mockSendGridServer creates a mock SendGrid API server for testing. +func mockSendGridServer(t *testing.T, expectedKey string, statusCode int, expectedEndpoint string) *httptest.Server { + t.Helper() + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if it's a GET request to the expected endpoint + if r.Method != http.MethodGet { + t.Errorf("unexpected request method: got %s, expected GET", r.Method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if r.URL.Path != expectedEndpoint { + t.Errorf("unexpected request path: got %s, expected %s", r.URL.Path, expectedEndpoint) + http.Error(w, "not found", http.StatusNotFound) + return + } + + // Check Authorization header + authHeader := r.Header.Get("Authorization") + expectedAuth := "Bearer " + expectedKey + if authHeader != expectedAuth { + // Return 401 for invalid auth + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"errors":[{"message":"authorization required"}]}`)) + return + } + + // Set response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + if statusCode == http.StatusOK { + _, _ = w.Write([]byte(`{"type":"free","reputation":99.7}`)) + } + })) +} + +func TestValidator(t *testing.T) { + cases := []struct { + name string + key sendgrid.APIKey + serverExpectedKey string + statusCode int + expectedEndpoint string + want veles.ValidationStatus + expectError bool + }{ + { + name: "valid_key_returns_200", + key: sendgrid.APIKey{Key: testSendGridAPIKey}, + serverExpectedKey: testSendGridAPIKey, + statusCode: http.StatusOK, + expectedEndpoint: "/v3/user/account", + want: veles.ValidationValid, + }, + { + name: "valid_key_without_permission_returns_403", + key: sendgrid.APIKey{Key: testSendGridAPIKey}, + serverExpectedKey: testSendGridAPIKey, + statusCode: http.StatusForbidden, + expectedEndpoint: "/v3/user/account", + want: veles.ValidationValid, + }, + { + name: "invalid_key_returns_401", + key: sendgrid.APIKey{Key: "invalid_key"}, + serverExpectedKey: testSendGridAPIKey, + statusCode: http.StatusUnauthorized, + expectedEndpoint: "/v3/user/account", + want: veles.ValidationInvalid, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Create a mock server + server := mockSendGridServer(t, tc.serverExpectedKey, tc.statusCode, tc.expectedEndpoint) + defer server.Close() + + validator := sendgrid.NewValidator() + validator.HTTPC = server.Client() + validator.Endpoint = server.URL + "/v3/user/account" + + got, err := validator.Validate(t.Context(), tc.key) + + // Check error expectation + if tc.expectError { + if err == nil { + t.Errorf("Validate() expected error, got nil") + } + } else { + if err != nil { + t.Errorf("Validate() unexpected error: %v", err) + } + } + + // Check validation status + if got != tc.want { + t.Errorf("Validate() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestValidator_ContextCancellation(t *testing.T) { + // Create a server that delays response significantly + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Sleep longer than the context timeout to trigger cancellation + time.Sleep(100 * time.Millisecond) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"type":"free","reputation":99.7}`)) + })) + defer server.Close() + + validator := sendgrid.NewValidator() + validator.HTTPC = server.Client() + validator.Endpoint = server.URL + "/v3/user/account" + key := sendgrid.APIKey{Key: testSendGridAPIKey} + + // Create context with a very short timeout + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond) + defer cancel() + + // Test validation with cancelled context + got, err := validator.Validate(ctx, key) + + if err == nil { + t.Errorf("Validate() expected error due to context cancellation, got nil") + } + if got != veles.ValidationFailed { + t.Errorf("Validate() = %v, want %v", got, veles.ValidationFailed) + } +} + +func TestValidator_ServerErrors(t *testing.T) { + cases := []struct { + name string + statusCode int + want veles.ValidationStatus + }{ + { + name: "rate_limited_429", + statusCode: http.StatusTooManyRequests, + want: veles.ValidationFailed, + }, + { + name: "server_error_500", + statusCode: http.StatusInternalServerError, + want: veles.ValidationFailed, + }, + { + name: "service_unavailable_503", + statusCode: http.StatusServiceUnavailable, + want: veles.ValidationFailed, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tc.statusCode) + })) + defer server.Close() + + validator := sendgrid.NewValidator() + validator.HTTPC = server.Client() + validator.Endpoint = server.URL + "/v3/user/account" + key := sendgrid.APIKey{Key: testSendGridAPIKey} + + got, _ := validator.Validate(t.Context(), key) + + if got != tc.want { + t.Errorf("Validate() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestValidator_AuthorizationHeader(t *testing.T) { + var capturedAuth string + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedAuth = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + validator := sendgrid.NewValidator() + validator.HTTPC = server.Client() + validator.Endpoint = server.URL + "/v3/user/account" + + key := sendgrid.APIKey{Key: testSendGridAPIKey} + if _, err := validator.Validate(t.Context(), key); err != nil { + t.Fatalf("Validate() unexpected error: %v", err) + } + + expectedAuth := "Bearer " + testSendGridAPIKey + if capturedAuth != expectedAuth { + t.Errorf("Authorization header = %q, want %q", capturedAuth, expectedAuth) + } +}