Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions binary/proto/scan_result.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -1121,6 +1122,10 @@ message SecretData {
string token = 1;
}

message SendGridAPIKey {
string key = 1;
}

message SalesforceOAuth2ClientCredentials {
// Salesforce OAuth2 client ID
string id = 1;
Expand Down
216 changes: 141 additions & 75 deletions binary/proto/scan_result_go_proto/scan_result.pb.go

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions binary/proto/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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(),
Expand Down
2 changes: 2 additions & 0 deletions enricher/enricherlist/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions extractor/filesystem/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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},
Expand Down
43 changes: 43 additions & 0 deletions veles/secrets/sendgrid/detector.go
Original file line number Diff line number Diff line change
@@ -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
},
}
}
211 changes: 211 additions & 0 deletions veles/secrets/sendgrid/detector_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
23 changes: 23 additions & 0 deletions veles/secrets/sendgrid/sendgrid.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading