From e4387bb543f7f23c33ba9ebac886c1d3e1421ff3 Mon Sep 17 00:00:00 2001 From: fmunozs Date: Thu, 18 Sep 2025 00:19:25 -0500 Subject: [PATCH 1/2] Initial support for openrouter --- binary/proto/scan_result.proto | 5 + .../scan_result_go_proto/scan_result.pb.go | 132 +++++++++---- binary/proto/secret.go | 13 ++ enricher/secrets/secrets.go | 2 + extractor/filesystem/secrets/secrets.go | 2 + veles/secrets/openrouter/detector.go | 41 ++++ veles/secrets/openrouter/detector_test.go | 182 ++++++++++++++++++ veles/secrets/openrouter/openrouter.go | 23 +++ veles/secrets/openrouter/validator.go | 156 +++++++++++++++ veles/secrets/openrouter/validator_test.go | 146 ++++++++++++++ 10 files changed, 669 insertions(+), 33 deletions(-) create mode 100644 veles/secrets/openrouter/detector.go create mode 100644 veles/secrets/openrouter/detector_test.go create mode 100644 veles/secrets/openrouter/openrouter.go create mode 100644 veles/secrets/openrouter/validator.go create mode 100644 veles/secrets/openrouter/validator_test.go diff --git a/binary/proto/scan_result.proto b/binary/proto/scan_result.proto index eb314686d..10d92d272 100644 --- a/binary/proto/scan_result.proto +++ b/binary/proto/scan_result.proto @@ -654,6 +654,7 @@ message SecretData { AzureIdentityToken azure_identity_token = 14; TinkKeyset tink_keyset = 15; GitlabPat gitlab_pat = 16; + OpenRouterAPIKey openrouter_api_key = 17; } message GCPSAK { @@ -733,6 +734,10 @@ message SecretData { string key = 1; } + message OpenRouterAPIKey { + string key = 1; + } + message DigitalOceanAPIToken { string key = 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 ea2a0ff21..b39f8a880 100644 --- a/binary/proto/scan_result_go_proto/scan_result.pb.go +++ b/binary/proto/scan_result_go_proto/scan_result.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.7 +// protoc-gen-go v1.36.9 // protoc v3.21.12 // source: proto/scan_result.proto @@ -5024,6 +5024,7 @@ type SecretData struct { // *SecretData_AzureIdentityToken_ // *SecretData_TinkKeyset_ // *SecretData_GitlabPat_ + // *SecretData_OpenrouterApiKey Secret isSecretData_Secret `protobuf_oneof:"secret"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -5210,6 +5211,15 @@ func (x *SecretData) GetGitlabPat() *SecretData_GitlabPat { return nil } +func (x *SecretData) GetOpenrouterApiKey() *SecretData_OpenRouterAPIKey { + if x != nil { + if x, ok := x.Secret.(*SecretData_OpenrouterApiKey); ok { + return x.OpenrouterApiKey + } + } + return nil +} + type isSecretData_Secret interface { isSecretData_Secret() } @@ -5278,6 +5288,10 @@ type SecretData_GitlabPat_ struct { GitlabPat *SecretData_GitlabPat `protobuf:"bytes,16,opt,name=gitlab_pat,json=gitlabPat,proto3,oneof"` } +type SecretData_OpenrouterApiKey struct { + OpenrouterApiKey *SecretData_OpenRouterAPIKey `protobuf:"bytes,17,opt,name=openrouter_api_key,json=openrouterApiKey,proto3,oneof"` +} + func (*SecretData_Gcpsak) isSecretData_Secret() {} func (*SecretData_AnthropicWorkspaceApiKey) isSecretData_Secret() {} @@ -5310,6 +5324,8 @@ func (*SecretData_TinkKeyset_) isSecretData_Secret() {} func (*SecretData_GitlabPat_) isSecretData_Secret() {} +func (*SecretData_OpenrouterApiKey) 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"` @@ -6386,6 +6402,50 @@ func (x *SecretData_PostmanCollectionAccessToken) GetKey() string { return "" } +type SecretData_OpenRouterAPIKey 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_OpenRouterAPIKey) Reset() { + *x = SecretData_OpenRouterAPIKey{} + mi := &file_proto_scan_result_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecretData_OpenRouterAPIKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretData_OpenRouterAPIKey) ProtoMessage() {} + +func (x *SecretData_OpenRouterAPIKey) ProtoReflect() protoreflect.Message { + mi := &file_proto_scan_result_proto_msgTypes[75] + 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_OpenRouterAPIKey.ProtoReflect.Descriptor instead. +func (*SecretData_OpenRouterAPIKey) Descriptor() ([]byte, []int) { + return file_proto_scan_result_proto_rawDescGZIP(), []int{53, 14} +} + +func (x *SecretData_OpenRouterAPIKey) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + type SecretData_DigitalOceanAPIToken struct { state protoimpl.MessageState `protogen:"open.v1"` Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` @@ -6395,7 +6455,7 @@ type SecretData_DigitalOceanAPIToken struct { func (x *SecretData_DigitalOceanAPIToken) Reset() { *x = SecretData_DigitalOceanAPIToken{} - mi := &file_proto_scan_result_proto_msgTypes[75] + mi := &file_proto_scan_result_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6407,7 +6467,7 @@ func (x *SecretData_DigitalOceanAPIToken) String() string { func (*SecretData_DigitalOceanAPIToken) ProtoMessage() {} func (x *SecretData_DigitalOceanAPIToken) ProtoReflect() protoreflect.Message { - mi := &file_proto_scan_result_proto_msgTypes[75] + mi := &file_proto_scan_result_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6420,7 +6480,7 @@ func (x *SecretData_DigitalOceanAPIToken) ProtoReflect() protoreflect.Message { // Deprecated: Use SecretData_DigitalOceanAPIToken.ProtoReflect.Descriptor instead. func (*SecretData_DigitalOceanAPIToken) Descriptor() ([]byte, []int) { - return file_proto_scan_result_proto_rawDescGZIP(), []int{53, 14} + return file_proto_scan_result_proto_rawDescGZIP(), []int{53, 15} } func (x *SecretData_DigitalOceanAPIToken) GetKey() string { @@ -6439,7 +6499,7 @@ type SecretData_TinkKeyset struct { func (x *SecretData_TinkKeyset) Reset() { *x = SecretData_TinkKeyset{} - mi := &file_proto_scan_result_proto_msgTypes[76] + mi := &file_proto_scan_result_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6451,7 +6511,7 @@ func (x *SecretData_TinkKeyset) String() string { func (*SecretData_TinkKeyset) ProtoMessage() {} func (x *SecretData_TinkKeyset) ProtoReflect() protoreflect.Message { - mi := &file_proto_scan_result_proto_msgTypes[76] + mi := &file_proto_scan_result_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6464,7 +6524,7 @@ func (x *SecretData_TinkKeyset) ProtoReflect() protoreflect.Message { // Deprecated: Use SecretData_TinkKeyset.ProtoReflect.Descriptor instead. func (*SecretData_TinkKeyset) Descriptor() ([]byte, []int) { - return file_proto_scan_result_proto_rawDescGZIP(), []int{53, 15} + return file_proto_scan_result_proto_rawDescGZIP(), []int{53, 16} } func (x *SecretData_TinkKeyset) GetContent() string { @@ -6879,7 +6939,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\"\xf8\x12\n" + + "\tlocations\x18\x03 \x03(\v2\x11.scalibr.LocationR\tlocations\"\xf4\x13\n" + "\n" + "SecretData\x124\n" + "\x06gcpsak\x18\x01 \x01(\v2\x1a.scalibr.SecretData.GCPSAKH\x00R\x06gcpsak\x12m\n" + @@ -6903,7 +6963,8 @@ const file_proto_scan_result_proto_rawDesc = "" + "\vtink_keyset\x18\x0f \x01(\v2\x1e.scalibr.SecretData.TinkKeysetH\x00R\n" + "tinkKeyset\x12>\n" + "\n" + - "gitlab_pat\x18\x10 \x01(\v2\x1d.scalibr.SecretData.GitlabPatH\x00R\tgitlabPat\x1a\xb0\x03\n" + + "gitlab_pat\x18\x10 \x01(\v2\x1d.scalibr.SecretData.GitlabPatH\x00R\tgitlabPat\x12T\n" + + "\x12openrouter_api_key\x18\x11 \x01(\v2$.scalibr.SecretData.OpenRouterAPIKeyH\x00R\x10openrouterApiKey\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" + @@ -6948,6 +7009,8 @@ const file_proto_scan_result_proto_rawDesc = "" + "\rPostmanAPIKey\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x1a0\n" + "\x1cPostmanCollectionAccessToken\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x1a$\n" + + "\x10OpenRouterAPIKey\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x1a(\n" + "\x14DigitalOceanAPIToken\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x1a&\n" + @@ -7011,7 +7074,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, 77) +var file_proto_scan_result_proto_msgTypes = make([]protoimpl.MessageInfo, 78) var file_proto_scan_result_proto_goTypes = []any{ (VexJustification)(0), // 0: scalibr.VexJustification (SeverityEnum)(0), // 1: scalibr.SeverityEnum @@ -7093,13 +7156,14 @@ var file_proto_scan_result_proto_goTypes = []any{ (*SecretData_GitlabPat)(nil), // 77: scalibr.SecretData.GitlabPat (*SecretData_PostmanAPIKey)(nil), // 78: scalibr.SecretData.PostmanAPIKey (*SecretData_PostmanCollectionAccessToken)(nil), // 79: scalibr.SecretData.PostmanCollectionAccessToken - (*SecretData_DigitalOceanAPIToken)(nil), // 80: scalibr.SecretData.DigitalOceanAPIToken - (*SecretData_TinkKeyset)(nil), // 81: scalibr.SecretData.TinkKeyset - (*timestamppb.Timestamp)(nil), // 82: google.protobuf.Timestamp + (*SecretData_OpenRouterAPIKey)(nil), // 80: scalibr.SecretData.OpenRouterAPIKey + (*SecretData_DigitalOceanAPIToken)(nil), // 81: scalibr.SecretData.DigitalOceanAPIToken + (*SecretData_TinkKeyset)(nil), // 82: scalibr.SecretData.TinkKeyset + (*timestamppb.Timestamp)(nil), // 83: google.protobuf.Timestamp } var file_proto_scan_result_proto_depIdxs = []int32{ - 82, // 0: scalibr.ScanResult.start_time:type_name -> google.protobuf.Timestamp - 82, // 1: scalibr.ScanResult.end_time:type_name -> google.protobuf.Timestamp + 83, // 0: scalibr.ScanResult.start_time:type_name -> google.protobuf.Timestamp + 83, // 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 9, // 4: scalibr.ScanResult.inventories_deprecated:type_name -> scalibr.Package @@ -7161,8 +7225,8 @@ var file_proto_scan_result_proto_depIdxs = []int32{ 15, // 60: scalibr.SPDXPackageMetadata.purl:type_name -> scalibr.Purl 15, // 61: scalibr.CDXPackageMetadata.purl:type_name -> scalibr.Purl 65, // 62: scalibr.PodmanMetadata.exposed_ports:type_name -> scalibr.PodmanMetadata.ExposedPortsEntry - 82, // 63: scalibr.PodmanMetadata.started_time:type_name -> google.protobuf.Timestamp - 82, // 64: scalibr.PodmanMetadata.finished_time:type_name -> google.protobuf.Timestamp + 83, // 63: scalibr.PodmanMetadata.started_time:type_name -> google.protobuf.Timestamp + 83, // 64: scalibr.PodmanMetadata.finished_time:type_name -> google.protobuf.Timestamp 55, // 65: scalibr.DockerContainersMetadata.ports:type_name -> scalibr.DockerPort 58, // 66: scalibr.Secret.secret:type_name -> scalibr.SecretData 59, // 67: scalibr.Secret.status:type_name -> scalibr.SecretStatus @@ -7175,27 +7239,28 @@ var file_proto_scan_result_proto_depIdxs = []int32{ 70, // 74: scalibr.SecretData.grok_xai_api_key:type_name -> scalibr.SecretData.GrokXAIAPIKey 71, // 75: scalibr.SecretData.grok_xai_management_api_key:type_name -> scalibr.SecretData.GrokXAIManagementAPIKey 76, // 76: scalibr.SecretData.docker_hub_pat:type_name -> scalibr.SecretData.DockerHubPat - 80, // 77: scalibr.SecretData.digitalocean:type_name -> scalibr.SecretData.DigitalOceanAPIToken + 81, // 77: scalibr.SecretData.digitalocean:type_name -> scalibr.SecretData.DigitalOceanAPIToken 75, // 78: scalibr.SecretData.openai_api_key:type_name -> scalibr.SecretData.OpenAIAPIKey 78, // 79: scalibr.SecretData.postman_api_key:type_name -> scalibr.SecretData.PostmanAPIKey 79, // 80: scalibr.SecretData.postman_collection_access_token:type_name -> scalibr.SecretData.PostmanCollectionAccessToken 73, // 81: scalibr.SecretData.azure_access_token:type_name -> scalibr.SecretData.AzureAccessToken 74, // 82: scalibr.SecretData.azure_identity_token:type_name -> scalibr.SecretData.AzureIdentityToken - 81, // 83: scalibr.SecretData.tink_keyset:type_name -> scalibr.SecretData.TinkKeyset + 82, // 83: scalibr.SecretData.tink_keyset:type_name -> scalibr.SecretData.TinkKeyset 77, // 84: scalibr.SecretData.gitlab_pat:type_name -> scalibr.SecretData.GitlabPat - 4, // 85: scalibr.SecretStatus.status:type_name -> scalibr.SecretStatus.SecretStatusEnum - 82, // 86: scalibr.SecretStatus.last_updated:type_name -> google.protobuf.Timestamp - 61, // 87: scalibr.Location.filepath:type_name -> scalibr.Filepath - 62, // 88: scalibr.Location.filepath_with_layer_details:type_name -> scalibr.FilepathWithLayerDetails - 63, // 89: scalibr.Location.environment_variable:type_name -> scalibr.EnvironmentVariable - 64, // 90: scalibr.Location.container_command:type_name -> scalibr.ContainerCommand - 11, // 91: scalibr.FilepathWithLayerDetails.layer_details:type_name -> scalibr.LayerDetails - 52, // 92: scalibr.PodmanMetadata.ExposedPortsEntry.value:type_name -> scalibr.Protocol - 93, // [93:93] is the sub-list for method output_type - 93, // [93:93] is the sub-list for method input_type - 93, // [93:93] is the sub-list for extension type_name - 93, // [93:93] is the sub-list for extension extendee - 0, // [0:93] is the sub-list for field type_name + 80, // 85: scalibr.SecretData.openrouter_api_key:type_name -> scalibr.SecretData.OpenRouterAPIKey + 4, // 86: scalibr.SecretStatus.status:type_name -> scalibr.SecretStatus.SecretStatusEnum + 83, // 87: scalibr.SecretStatus.last_updated:type_name -> google.protobuf.Timestamp + 61, // 88: scalibr.Location.filepath:type_name -> scalibr.Filepath + 62, // 89: scalibr.Location.filepath_with_layer_details:type_name -> scalibr.FilepathWithLayerDetails + 63, // 90: scalibr.Location.environment_variable:type_name -> scalibr.EnvironmentVariable + 64, // 91: scalibr.Location.container_command:type_name -> scalibr.ContainerCommand + 11, // 92: scalibr.FilepathWithLayerDetails.layer_details:type_name -> scalibr.LayerDetails + 52, // 93: scalibr.PodmanMetadata.ExposedPortsEntry.value:type_name -> scalibr.Protocol + 94, // [94:94] is the sub-list for method output_type + 94, // [94:94] is the sub-list for method input_type + 94, // [94:94] is the sub-list for extension type_name + 94, // [94:94] is the sub-list for extension extendee + 0, // [0:94] is the sub-list for field type_name } func init() { file_proto_scan_result_proto_init() } @@ -7260,6 +7325,7 @@ func file_proto_scan_result_proto_init() { (*SecretData_AzureIdentityToken_)(nil), (*SecretData_TinkKeyset_)(nil), (*SecretData_GitlabPat_)(nil), + (*SecretData_OpenrouterApiKey)(nil), } file_proto_scan_result_proto_msgTypes[55].OneofWrappers = []any{ (*Location_Filepath)(nil), @@ -7273,7 +7339,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: 77, + NumMessages: 78, NumExtensions: 0, NumServices: 0, }, diff --git a/binary/proto/secret.go b/binary/proto/secret.go index ca382b445..20fa410c5 100644 --- a/binary/proto/secret.go +++ b/binary/proto/secret.go @@ -28,6 +28,7 @@ import ( "github.com/google/osv-scalibr/veles/secrets/gitlabpat" velesgrokxaiapikey "github.com/google/osv-scalibr/veles/secrets/grokxaiapikey" velesopenai "github.com/google/osv-scalibr/veles/secrets/openai" + velesopenrouter "github.com/google/osv-scalibr/veles/secrets/openrouter" velesperplexity "github.com/google/osv-scalibr/veles/secrets/perplexityapikey" velespostmanapikey "github.com/google/osv-scalibr/veles/secrets/postmanapikey" velesprivatekey "github.com/google/osv-scalibr/veles/secrets/privatekey" @@ -123,6 +124,8 @@ func velesSecretToProto(s veles.Secret) (*spb.SecretData, error) { return tinkKeysetToProto(t), nil case velesopenai.APIKey: return openaiAPIKeyToProto(t.Key), nil + case velesopenrouter.APIKey: + return openrouterAPIKeyToProto(t.Key), nil case velespostmanapikey.PostmanAPIKey: return postmanAPIKeyToProto(t), nil case velespostmanapikey.PostmanCollectionToken: @@ -307,6 +310,16 @@ func openaiAPIKeyToProto(key string) *spb.SecretData { } } +func openrouterAPIKeyToProto(key string) *spb.SecretData { + return &spb.SecretData{ + Secret: &spb.SecretData_OpenrouterApiKey{ + OpenrouterApiKey: &spb.SecretData_OpenRouterAPIKey{ + Key: key, + }, + }, + } +} + func validationResultToProto(r inventory.SecretValidationResult) (*spb.SecretStatus, error) { status, err := validationStatusToProto(r.Status) if err != nil { diff --git a/enricher/secrets/secrets.go b/enricher/secrets/secrets.go index 5f210d75f..bd20fc8f5 100644 --- a/enricher/secrets/secrets.go +++ b/enricher/secrets/secrets.go @@ -31,6 +31,7 @@ import ( "github.com/google/osv-scalibr/veles/secrets/gitlabpat" grokxaiapikey "github.com/google/osv-scalibr/veles/secrets/grokxaiapikey" "github.com/google/osv-scalibr/veles/secrets/openai" + "github.com/google/osv-scalibr/veles/secrets/openrouter" perplexityapikey "github.com/google/osv-scalibr/veles/secrets/perplexityapikey" postmanapikey "github.com/google/osv-scalibr/veles/secrets/postmanapikey" ) @@ -62,6 +63,7 @@ func New() enricher.Enricher { veles.WithValidator(grokxaiapikey.NewManagementAPIValidator()), veles.WithValidator(gitlabpat.NewValidator()), veles.WithValidator(openai.NewProjectValidator()), + veles.WithValidator(openrouter.NewAPIKeyValidator()), veles.WithValidator(postmanapikey.NewAPIValidator()), veles.WithValidator(postmanapikey.NewCollectionValidator()), ) diff --git a/extractor/filesystem/secrets/secrets.go b/extractor/filesystem/secrets/secrets.go index 621deefd3..99399e23d 100644 --- a/extractor/filesystem/secrets/secrets.go +++ b/extractor/filesystem/secrets/secrets.go @@ -35,6 +35,7 @@ import ( "github.com/google/osv-scalibr/veles/secrets/gitlabpat" grokxaiapikey "github.com/google/osv-scalibr/veles/secrets/grokxaiapikey" "github.com/google/osv-scalibr/veles/secrets/openai" + "github.com/google/osv-scalibr/veles/secrets/openrouter" perplexityapikey "github.com/google/osv-scalibr/veles/secrets/perplexityapikey" postmanapikey "github.com/google/osv-scalibr/veles/secrets/postmanapikey" "github.com/google/osv-scalibr/veles/secrets/privatekey" @@ -88,6 +89,7 @@ func init() { //nolint:gochecknoinits azuretoken.NewDetector(), tinkkeyset.NewDetector(), openai.NewDetector(), + openrouter.NewDetector(), postmanapikey.NewAPIKeyDetector(), postmanapikey.NewCollectionTokenDetector(), }) diff --git a/veles/secrets/openrouter/detector.go b/veles/secrets/openrouter/detector.go new file mode 100644 index 000000000..639d08bd8 --- /dev/null +++ b/veles/secrets/openrouter/detector.go @@ -0,0 +1,41 @@ +// 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 openrouter + +import ( + "regexp" + + "github.com/google/osv-scalibr/veles" + "github.com/google/osv-scalibr/veles/secrets/common/simpletoken" +) + +// maxTokenLength is the maximum size of an OpenRouter API key. +const maxTokenLength = 100 + +// keyRe is a regular expression that matches OpenRouter API keys. +// OpenRouter API keys typically start with "sk-or-" followed by alphanumeric characters, +// underscores, and hyphens. The regex is designed to be specific enough to avoid false positives. +var keyRe = regexp.MustCompile(`sk-or-v1-[A-Za-z0-9_-]{20,}`) + +// NewDetector returns a new simpletoken.Detector that matches OpenRouter API keys. +func NewDetector() veles.Detector { + return simpletoken.Detector{ + MaxLen: maxTokenLength, + Re: keyRe, + FromMatch: func(b []byte) veles.Secret { + return APIKey{Key: string(b)} + }, + } +} diff --git a/veles/secrets/openrouter/detector_test.go b/veles/secrets/openrouter/detector_test.go new file mode 100644 index 000000000..c308647f4 --- /dev/null +++ b/veles/secrets/openrouter/detector_test.go @@ -0,0 +1,182 @@ +// 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 openrouter + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/osv-scalibr/veles" +) + +const ( + validAPIKey = "sk-or-v1-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqr" +) + +func TestDetector(t *testing.T) { + engine, err := veles.NewDetectionEngine([]veles.Detector{NewDetector()}) + if err != nil { + t.Fatal(err) + } + + cases := []struct { + name string + input string + want []veles.Secret + }{{ + name: "valid_openrouter_key", + input: validAPIKey, + want: []veles.Secret{ + APIKey{Key: validAPIKey}, + }, + }, { + name: "openrouter_key_in_config", + input: "OPENROUTER_API_KEY=" + validAPIKey, + want: []veles.Secret{ + APIKey{Key: validAPIKey}, + }, + }, { + name: "openrouter_key_in_env", + input: "export OPENROUTER_KEY=\"" + validAPIKey + "\"", + want: []veles.Secret{ + APIKey{Key: validAPIKey}, + }, + }, { + name: "multiple_openrouter_keys", + input: validAPIKey + "\n" + + "sk-or-v1-zyxwvutsrqponmlkjihgfedcba0987654321zyxwvutsrqponmlkjihg", + want: []veles.Secret{ + APIKey{Key: validAPIKey}, + APIKey{Key: "sk-or-v1-zyxwvutsrqponmlkjihgfedcba0987654321zyxwvutsrqponmlkjihg"}, + }, + }, { + name: "openrouter_key_with_special_chars", + input: "sk-or-v1-test_AbC_DeF-GhIjKlMnOpQrStUvWxYzAbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGh", + want: []veles.Secret{ + APIKey{Key: "sk-or-v1-test_AbC_DeF-GhIjKlMnOpQrStUvWxYzAbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGh"}, + }, + }} + + 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 TestDetector_NoMatches(t *testing.T) { + engine, err := veles.NewDetectionEngine([]veles.Detector{NewDetector()}) + if err != nil { + t.Fatal(err) + } + + cases := []struct { + name string + input string + }{{ + name: "too_short", + input: "sk-or-v1-tooshort", + }, { + name: "wrong_prefix", + input: "sk-openai-abcdefghijklmnopqrstuvwxyz1234567890", + }, { + name: "missing_sk_prefix", + input: "or-v1-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqr", + }, { + name: "wrong_or_format", + input: "sk-orv1-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqr", + }, { + name: "openai_key_format", + input: "sk-proj-abcdefghij1234567890T3BlbkFJklmnopqrstuvwxyz098765432109876", + }, { + name: "no_secrets", + input: "This is just regular text with no secrets", + }, { + name: "sk_prefix_but_not_key", + input: "skeleton key is not an API key", + }, { + name: "or_prefix_but_not_key", + input: "or something else entirely", + }} + + 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 len(got) != 0 { + t.Errorf("Detect() got %v secrets, want 0", len(got)) + } + }) + } +} + +func TestOpenRouterKeyValidation(t *testing.T) { + testCases := []struct { + name string + key string + isValid bool + }{{ + name: "valid_openrouter_key", + key: validAPIKey, + isValid: true, + }, { + name: "not_a_key", + key: "not-a-key", + isValid: false, + }, { + name: "empty_string", + key: "", + isValid: false, + }, { + name: "openai_key_format", + key: "sk-proj-123456789012345678901234567890123456789012345678", + isValid: false, + }, { + name: "too_short_openrouter_key", + key: "sk-or-v1-short", + isValid: false, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Test by trying to detect the key + engine, err := veles.NewDetectionEngine([]veles.Detector{NewDetector()}) + if err != nil { + t.Fatal(err) + } + + got, err := engine.Detect(t.Context(), strings.NewReader(tc.key)) + if err != nil { + t.Errorf("Detect() error: %v, want nil", err) + } + + isDetected := len(got) > 0 + if isDetected != tc.isValid { + t.Errorf("Key %q detected=%v, want valid=%v", + tc.key, isDetected, tc.isValid) + } + }) + } +} diff --git a/veles/secrets/openrouter/openrouter.go b/veles/secrets/openrouter/openrouter.go new file mode 100644 index 000000000..857277296 --- /dev/null +++ b/veles/secrets/openrouter/openrouter.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 openrouter contains Veles Secret types and Detectors for +// OpenRouter API keys. +package openrouter + +// APIKey is a Veles Secret that holds relevant information for an +// OpenRouter API key. +type APIKey struct { + Key string +} diff --git a/veles/secrets/openrouter/validator.go b/veles/secrets/openrouter/validator.go new file mode 100644 index 000000000..9921b4e68 --- /dev/null +++ b/veles/secrets/openrouter/validator.go @@ -0,0 +1,156 @@ +// 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 openrouter + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/google/osv-scalibr/veles" +) + +const ( + // OpenRouter API base URL + openRouterAPIBaseURL = "https://openrouter.ai/api" + // Timeout for API validation requests + validationTimeout = 10 * time.Second +) + +// ValidationConfig holds configuration for API validation +type ValidationConfig struct { + HTTPClient *http.Client + OpenRouterAPIURL string +} + +// NewValidationConfig creates a new ValidationConfig with default values +func NewValidationConfig() *ValidationConfig { + return &ValidationConfig{ + HTTPClient: &http.Client{ + Timeout: validationTimeout, + }, + OpenRouterAPIURL: openRouterAPIBaseURL, + } +} + +// WithHTTPClient configures the http.Client for validation +func (c *ValidationConfig) WithHTTPClient( + client *http.Client) *ValidationConfig { + c.HTTPClient = client + return c +} + +// WithAPIURL configures the OpenRouter API URL for validation +func (c *ValidationConfig) WithAPIURL(url string) *ValidationConfig { + c.OpenRouterAPIURL = url + return c +} + +var _ veles.Validator[APIKey] = &APIKeyValidator{} + +// APIKeyValidator is a Veles Validator for OpenRouter API keys. +// It validates API keys by making a test request to the OpenRouter API. +type APIKeyValidator struct { + config *ValidationConfig +} + +// ValidatorOption configures an APIKeyValidator when creating it via +// NewAPIKeyValidator. +type ValidatorOption func(*APIKeyValidator) + +// WithHTTPClient configures the http.Client that the APIKeyValidator uses. +// +// By default it uses http.DefaultClient with a timeout. +func WithHTTPClient(c *http.Client) ValidatorOption { + return func(v *APIKeyValidator) { + v.config.WithHTTPClient(c) + } +} + +// WithAPIURL configures the OpenRouter API URL that the APIKeyValidator uses. +// +// By default it uses the production OpenRouter API URL. +// This is useful for testing with mock servers. +func WithAPIURL(url string) ValidatorOption { + return func(v *APIKeyValidator) { + v.config.WithAPIURL(url) + } +} + +// NewAPIKeyValidator creates a new APIKeyValidator with the given +// ValidatorOptions. +func NewAPIKeyValidator(opts ...ValidatorOption) *APIKeyValidator { + v := &APIKeyValidator{ + config: NewValidationConfig(), + } + for _, opt := range opts { + opt(v) + } + return v +} + +// Validate checks whether the given APIKey is valid. +// +// It makes a request to the /v1/auth/key endpoint which is specifically +// designed for API key validation and authentication checking. +func (v *APIKeyValidator) Validate(ctx context.Context, + key APIKey) (veles.ValidationStatus, error) { + // Check for empty key + if key.Key == "" { + return veles.ValidationFailed, errors.New("empty API key") + } + + // Create HTTP request to the /v1/auth/key endpoint + req, err := http.NewRequestWithContext(ctx, http.MethodGet, + v.config.OpenRouterAPIURL+"/v1/auth/key", nil) + if err != nil { + return veles.ValidationFailed, + fmt.Errorf("unable to create HTTP request: %w", err) + } + + // Set Authorization header with Bearer token (OpenRouter format) + req.Header.Set("Authorization", "Bearer "+key.Key) + + // Make the request + res, err := v.config.HTTPClient.Do(req) + if err != nil { + return veles.ValidationFailed, + fmt.Errorf("unable to validate API key: %w", err) + } + defer res.Body.Close() + + // Check response status + switch res.StatusCode { + case http.StatusOK: + // Key is valid + return veles.ValidationValid, nil + case http.StatusUnauthorized: + // Key is invalid + return veles.ValidationInvalid, nil + case http.StatusTooManyRequests: + // Rate limited - key is likely valid but we're being throttled. + // StatusTooManyRequests indicates that the key successfully + // authenticates against the OpenRouter API and that this account + // is rate limited: https://openrouter.ai/docs/api-reference/errors + return veles.ValidationValid, nil + default: + // Other status codes indicate an error in our validation process + return veles.ValidationFailed, + fmt.Errorf("unexpected HTTP status %d during validation", + res.StatusCode) + } +} diff --git a/veles/secrets/openrouter/validator_test.go b/veles/secrets/openrouter/validator_test.go new file mode 100644 index 000000000..e3e82b605 --- /dev/null +++ b/veles/secrets/openrouter/validator_test.go @@ -0,0 +1,146 @@ +// 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 openrouter_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/osv-scalibr/veles" + "github.com/google/osv-scalibr/veles/secrets/openrouter" +) + +const ( + validatorTestKey = "sk-or-v1-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqr" +) + +// mockOpenRouterServer creates a mock OpenRouter API server for testing API keys +func mockOpenRouterServer(t *testing.T, expectedKey string, statusCode int) *httptest.Server { + t.Helper() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, + r *http.Request) { + // Check if it's a GET request to the auth/key endpoint + if r.Method != http.MethodGet || r.URL.Path != "/v1/auth/key" { + t.Errorf("unexpected request: %s %s, expected: GET /v1/auth/key", + r.Method, r.URL.Path) + http.Error(w, "not found", http.StatusNotFound) + return + } + + // Check Authorization header (Bearer token format) + expectedAuth := "Bearer " + expectedKey + if r.Header.Get("Authorization") != expectedAuth { + t.Errorf("expected Authorization: %s, got: %s", + expectedAuth, r.Header.Get("Authorization")) + } + + // Set response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + })) + + return server +} + +func TestAPIKeyValidator(t *testing.T) { + cases := []struct { + name string + statusCode int + want veles.ValidationStatus + expectError bool + }{ + { + name: "valid_key", + statusCode: http.StatusOK, + want: veles.ValidationValid, + }, + { + name: "invalid_key_unauthorized", + statusCode: http.StatusUnauthorized, + want: veles.ValidationInvalid, + }, + { + name: "forbidden_but_likely_valid", + statusCode: http.StatusForbidden, + want: veles.ValidationFailed, + expectError: true, + }, + { + name: "rate_limited_but_likely_valid", + statusCode: http.StatusTooManyRequests, + want: veles.ValidationValid, + }, + { + name: "server_error", + statusCode: http.StatusInternalServerError, + want: veles.ValidationFailed, + expectError: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Create mock server + server := mockOpenRouterServer(t, validatorTestKey, + tc.statusCode) + defer server.Close() + + // Create validator with mock client and server URL + validator := openrouter.NewAPIKeyValidator( + openrouter.WithHTTPClient(server.Client()), + openrouter.WithAPIURL(server.URL), + ) + + // Create test key + key := openrouter.APIKey{Key: validatorTestKey} + + // Test validation + got, err := validator.Validate(context.Background(), 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 TestAPIKeyValidator_EmptyKey(t *testing.T) { + validator := openrouter.NewAPIKeyValidator() + key := openrouter.APIKey{Key: ""} + + got, err := validator.Validate(context.Background(), key) + + if err == nil { + t.Errorf("Validate() expected error for empty key, got nil") + } + if got != veles.ValidationFailed { + t.Errorf("Validate() = %v, want %v", got, veles.ValidationFailed) + } +} From b34ce1e5f3273d1bf47f0d5239fd0d4b25922dd4 Mon Sep 17 00:00:00 2001 From: fmunozs Date: Fri, 26 Sep 2025 22:56:13 -0500 Subject: [PATCH 2/2] Integrate OpenRouter with new Veles plugin architecture - Fixed FromMatch signature for new simpletoken.Detector interface - Registered OpenRouter detector in extractor plugin list - Registered OpenRouter validator in enricher plugin list - Updated protobuf field number to 32 (avoiding conflicts) - Verified end-to-end functionality with individual plugins Available plugins: - secrets/openrouter (detection) - secrets/openroutervalidate (validation) --- enricher/enricherlist/list.go | 2 ++ extractor/filesystem/list/list.go | 2 ++ veles/secrets/openrouter/detector.go | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/enricher/enricherlist/list.go b/enricher/enricherlist/list.go index 01c48ab4e..4572bd759 100644 --- a/enricher/enricherlist/list.go +++ b/enricher/enricherlist/list.go @@ -41,6 +41,7 @@ import ( "github.com/google/osv-scalibr/veles/secrets/hashicorpvault" "github.com/google/osv-scalibr/veles/secrets/huggingfaceapikey" "github.com/google/osv-scalibr/veles/secrets/openai" + "github.com/google/osv-scalibr/veles/secrets/openrouter" "github.com/google/osv-scalibr/veles/secrets/perplexityapikey" "github.com/google/osv-scalibr/veles/secrets/postmanapikey" "github.com/google/osv-scalibr/veles/secrets/stripeapikeys" @@ -89,6 +90,7 @@ var ( fromVeles(hashicorpvault.NewAppRoleValidator(), "secrets/hashicorpvaultapprolevalidate", 0), fromVeles(huggingfaceapikey.NewValidator(), "secrets/huggingfaceapikeyvalidate", 0), fromVeles(openai.NewProjectValidator(), "secrets/openaivalidate", 0), + fromVeles(openrouter.NewAPIKeyValidator(), "secrets/openroutervalidate", 0), fromVeles(perplexityapikey.NewValidator(), "secrets/perplexityapikeyvalidate", 0), fromVeles(postmanapikey.NewAPIValidator(), "secrets/postmanapikeyvalidate", 0), fromVeles(postmanapikey.NewCollectionValidator(), "secrets/postmancollectiontokenvalidate", 0), diff --git a/extractor/filesystem/list/list.go b/extractor/filesystem/list/list.go index a460c3ea1..ff36b6759 100644 --- a/extractor/filesystem/list/list.go +++ b/extractor/filesystem/list/list.go @@ -105,6 +105,7 @@ import ( "github.com/google/osv-scalibr/veles/secrets/hashicorpvault" "github.com/google/osv-scalibr/veles/secrets/huggingfaceapikey" "github.com/google/osv-scalibr/veles/secrets/openai" + "github.com/google/osv-scalibr/veles/secrets/openrouter" "github.com/google/osv-scalibr/veles/secrets/perplexityapikey" "github.com/google/osv-scalibr/veles/secrets/postmanapikey" "github.com/google/osv-scalibr/veles/secrets/privatekey" @@ -269,6 +270,7 @@ var ( {hashicorpvault.NewAppRoleDetector(), "secrets/hashicorpvaultapprole", 0}, {huggingfaceapikey.NewDetector(), "secrets/huggingfaceapikey", 0}, {openai.NewDetector(), "secrets/openai", 0}, + {openrouter.NewDetector(), "secrets/openrouter", 0}, {perplexityapikey.NewDetector(), "secrets/perplexityapikey", 0}, {postmanapikey.NewAPIKeyDetector(), "secrets/postmanapikey", 0}, {postmanapikey.NewCollectionTokenDetector(), "secrets/postmancollectiontoken", 0}, diff --git a/veles/secrets/openrouter/detector.go b/veles/secrets/openrouter/detector.go index 639d08bd8..54d5a412f 100644 --- a/veles/secrets/openrouter/detector.go +++ b/veles/secrets/openrouter/detector.go @@ -34,8 +34,8 @@ func NewDetector() veles.Detector { return simpletoken.Detector{ MaxLen: maxTokenLength, Re: keyRe, - FromMatch: func(b []byte) veles.Secret { - return APIKey{Key: string(b)} + FromMatch: func(b []byte) (veles.Secret, bool) { + return APIKey{Key: string(b)}, true }, } }