Skip to content
Merged
5 changes: 5 additions & 0 deletions binary/proto/scan_result.proto
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@ message SecretData {
VapidKey vapid_key = 49;
AwsAccessKeyCredentials aws_access_key_credentials = 50;
ReCaptchaKey re_captcha_key = 51;
CodeCatalystCredentials code_catalyst_credentials = 52;
}

message GCPSAK {
Expand Down Expand Up @@ -994,6 +995,10 @@ message SecretData {
message ReCaptchaKey{
string secret = 1;
}

message CodeCatalystCredentials{
string url = 1;
}
}

message SecretStatus {
Expand Down
840 changes: 456 additions & 384 deletions binary/proto/scan_result_go_proto/scan_result.pb.go

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions binary/proto/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/google/osv-scalibr/veles/secrets/gcpoauth2client"
velesgcpsak "github.com/google/osv-scalibr/veles/secrets/gcpsak"
"github.com/google/osv-scalibr/veles/secrets/gcshmackey"
"github.com/google/osv-scalibr/veles/secrets/gitbasicauth/codecatalyst"
velesgithub "github.com/google/osv-scalibr/veles/secrets/github"
"github.com/google/osv-scalibr/veles/secrets/gitlabpat"
velesgrokxaiapikey "github.com/google/osv-scalibr/veles/secrets/grokxaiapikey"
Expand Down Expand Up @@ -215,11 +216,23 @@ func velesSecretToProto(s veles.Secret) (*spb.SecretData, error) {
return vapidKeyToProto(t), nil
case recaptchakey.Key:
return reCaptchaKeyToProto(t), nil
case codecatalyst.Credentials:
return codeCatalystCredentialsToProto(t), nil
default:
return nil, fmt.Errorf("%w: %T", ErrUnsupportedSecretType, s)
}
}

func codeCatalystCredentialsToProto(s codecatalyst.Credentials) *spb.SecretData {
return &spb.SecretData{
Secret: &spb.SecretData_CodeCatalystCredentials_{
CodeCatalystCredentials: &spb.SecretData_CodeCatalystCredentials{
Url: s.FullURL,
},
},
}
}

func awsAccessKeyCredentialToProto(s awsaccesskey.Credentials) *spb.SecretData {
return &spb.SecretData{
Secret: &spb.SecretData_AwsAccessKeyCredentials_{
Expand Down Expand Up @@ -947,6 +960,10 @@ func velesSecretToStruct(s *spb.SecretData) (veles.Secret, error) {
return recaptchakey.Key{
Secret: s.GetReCaptchaKey().GetSecret(),
}, nil
case *spb.SecretData_CodeCatalystCredentials_:
return codecatalyst.Credentials{
FullURL: s.GetCodeCatalystCredentials().GetUrl(),
}, nil
default:
return nil, fmt.Errorf("%w: %T", ErrUnsupportedSecretType, s.GetSecret())
}
Expand Down
1 change: 1 addition & 0 deletions docs/supported_inventory_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ See the docs on [how to add a new Extractor](/docs/new_extractor.md).
| Type | Extractor Plugin |
|---------------------------------------------|--------------------------------------|
| AWS access key | `secrets/awsaccesskey` |
| Amazon CodeCatalyst credentials | `secrets/codecatalystcredentials` |
| Anthropic API key | `secrets/anthropicapikey` |
| Azure Token | `secrets/azuretoken` |
| Crates.io API Token | `secrets/cratesioapitoken` |
Expand Down
2 changes: 2 additions & 0 deletions enricher/enricherlist/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"github.com/google/osv-scalibr/veles/secrets/gcpoauth2access"
"github.com/google/osv-scalibr/veles/secrets/gcpsak"
"github.com/google/osv-scalibr/veles/secrets/gcshmackey"
"github.com/google/osv-scalibr/veles/secrets/gitbasicauth/codecatalyst"
"github.com/google/osv-scalibr/veles/secrets/github"
"github.com/google/osv-scalibr/veles/secrets/gitlabpat"
"github.com/google/osv-scalibr/veles/secrets/grokxaiapikey"
Expand Down Expand Up @@ -118,6 +119,7 @@ var (
fromVeles(gcpoauth2access.NewValidator(), "secrets/gcpoauth2accesstokenvalidate", 0),
fromVeles(gcshmackey.NewValidator(), "secrets/gcshmackeyvalidate", 0),
fromVeles(awsaccesskey.NewValidator(), "secrets/awsaccesskeyvalidate", 0),
fromVeles(codecatalyst.NewValidator(), "secrets/codecatalystcredentialsvalidate", 0),
})

// SecretsEnrich lists enrichers that add data to detected secrets.
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 @@ -98,6 +98,7 @@ import (
"github.com/google/osv-scalibr/extractor/filesystem/sbom/spdx"
"github.com/google/osv-scalibr/extractor/filesystem/secrets/awsaccesskey"
"github.com/google/osv-scalibr/extractor/filesystem/secrets/convert"
"github.com/google/osv-scalibr/extractor/filesystem/secrets/gitbasicauth/codecatalyst"
"github.com/google/osv-scalibr/extractor/filesystem/secrets/mariadb"
"github.com/google/osv-scalibr/extractor/filesystem/secrets/mysqlmylogin"
"github.com/google/osv-scalibr/extractor/filesystem/secrets/onepasswordconnecttoken"
Expand Down Expand Up @@ -284,6 +285,7 @@ var (
onepasswordconnecttoken.Name: {noCFG(onepasswordconnecttoken.New)},
mariadb.Name: {noCFG(mariadb.NewDefault)},
awsaccesskey.Name: {noCFG(awsaccesskey.New)},
codecatalyst.Name: {noCFG(codecatalyst.New)},
}

// SecretDetectors for Detector interface.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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 codecatalyst extends the veles codecatalyst.Detector to search inside the git config and history files
package codecatalyst

import (
"path/filepath"
"strings"

"github.com/google/osv-scalibr/extractor/filesystem"
"github.com/google/osv-scalibr/veles/secrets/gitbasicauth/codecatalyst"

"github.com/google/osv-scalibr/extractor/filesystem/secrets/convert"
)

const (
// Name is the name of the extractor
Name = "secrets/codecatalystcredentials"
// Version is the version of the extractor
Version = 0
)

// New returns a filesystem.Extractor which extracts Amazon CodeCatalyst Credentials using the codecatalyst.Detector
func New() filesystem.Extractor {
return convert.FromVelesDetectorWithRequire(
codecatalyst.NewDetector(), Name, Version, FileRequired,
)
}

// FileRequired returns true if a file should be searched by the plugin.
func FileRequired(api filesystem.FileAPI) bool {
path := filepath.ToSlash(api.Path())
return strings.HasSuffix(path, ".git/config") ||
strings.HasSuffix(path, ".git-credentials") ||
strings.HasSuffix(path, "_history")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// 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 codecatalyst_test

import (
"runtime"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/osv-scalibr/extractor/filesystem/secrets/gitbasicauth/codecatalyst"
"github.com/google/osv-scalibr/extractor/filesystem/simplefileapi"
"github.com/google/osv-scalibr/inventory"
"github.com/google/osv-scalibr/testing/extracttest"
codecatalystdetector "github.com/google/osv-scalibr/veles/secrets/gitbasicauth/codecatalyst"
)

func TestExtractor_FileRequired(t *testing.T) {
tests := []struct {
inputPath string
want bool
isWindows bool
}{
{inputPath: "", want: false},

// linux
{inputPath: `/Users/example-user/folder/.git/config`, want: true},
{inputPath: `/Users/example-user/.git-credentials`, want: true},
{inputPath: `/Users/example-user/.zsh_history`, want: true},
{inputPath: `/Users/example-user/bad/path`, want: false},

// windows
{inputPath: `C:\Users\USERNAME\folder\.git\config`, isWindows: true, want: true},
{inputPath: `C:\Users\YourUserName\.git-credentials`, isWindows: true, want: true},
{inputPath: `C:\Users\USERNAME\another\bad\path`, isWindows: true, want: false},
}

for _, tt := range tests {
t.Run(tt.inputPath, func(t *testing.T) {
if tt.isWindows && runtime.GOOS != "windows" {
t.Skipf("Skipping test %q for %q", t.Name(), runtime.GOOS)
}
e := codecatalyst.New()
got := e.FileRequired(simplefileapi.New(tt.inputPath, nil))
if got != tt.want {
t.Errorf("FileRequired(%s) got = %v, want %v", tt.inputPath, got, tt.want)
}
})
}
}

func TestExtractor_Extract(t *testing.T) {
tests := []*struct {
Name string
Path string
WantSecrets []*inventory.Secret
WantErr error
}{
{
Name: "empty",
Path: "empty",
WantSecrets: nil,
},
{
Name: "git_credentials",
Path: "git_credentials",
WantSecrets: []*inventory.Secret{
{
Secret: codecatalystdetector.Credentials{
FullURL: `https://user:[email protected]/v1/space/project/repo`,
},
Location: "git_credentials",
},
},
},
{
Name: "git_config",
Path: "git_config",
WantSecrets: []*inventory.Secret{
{
Secret: codecatalystdetector.Credentials{
FullURL: `https://user:[email protected]/v1/space/project/repo`,
},
Location: "git_config",
},
},
},
{
Name: "history_file",
Path: ".zsh_history",
WantSecrets: []*inventory.Secret{
{
Secret: codecatalystdetector.Credentials{
FullURL: `https://user:[email protected]/v1/space/project/test-repo`,
},
Location: ".zsh_history",
},
},
},
{
Name: "random_content",
Path: "random_content",
WantSecrets: nil,
},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
extr := codecatalyst.New()

inputCfg := extracttest.ScanInputMockConfig{
Path: tt.Path,
FakeScanRoot: "testdata",
}

scanInput := extracttest.GenerateScanInputMock(t, inputCfg)
defer extracttest.CloseTestScanInput(t, scanInput)

got, err := extr.Extract(t.Context(), &scanInput)

if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" {
t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), tt.Path, diff)
return
}

wantInv := inventory.Inventory{Secrets: tt.WantSecrets}
opts := []cmp.Option{cmpopts.SortSlices(extracttest.PackageCmpLess), cmpopts.EquateEmpty()}
if diff := cmp.Diff(wantInv, got, opts...); diff != "" {
t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), tt.Path, diff)
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
: 1:0;cd /tmp
: 2:0;git clone https://user:[email protected]/v1/space/project/test-repo
: 3:0;cd test-repo
: 4:0;ls
: 5:0;cd .git
: 6:0;ls
: 7:0;cat config
: 8:0;cat .zsh_history
: 9:0;cat .zsh_history | tail -50 | pbcopy
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://user:[email protected]/v1/space/project/repo
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
https://user:[email protected]/v1/space/project/repo
https://anotheruser:[email protected]/user/repo
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
localhost:5432:mydb:myuser:mypassword
hostname:1234:testdb:testuser:testpass123
hostname:1234:testdb:testuser:passw*ord
# space inside one group (except password)
hostname:1234:testdb:testuser:passw ord
hostname:1234:db name:testuser:password
# this is a comment and should be ignored
*:*:db:admin:supersecret
# valid with escaped :
prod.example.com:5432:db:admin:pass\:word
21 changes: 21 additions & 0 deletions veles/secrets/gitbasicauth/codecatalyst/codecatalyst.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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 codecatalyst contains the logic to extract CodeCatalyst credentials.
package codecatalyst

// Credentials contains basic auth credential for CodeCatalyst.
type Credentials struct {
FullURL string
}
Loading
Loading