Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 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
4 changes: 2 additions & 2 deletions extractor/filesystem/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ import (
"github.com/google/osv-scalibr/extractor/filesystem/runtime/nodejs/nvm"
"github.com/google/osv-scalibr/extractor/filesystem/sbom/cdx"
"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/mariadb"
"github.com/google/osv-scalibr/extractor/filesystem/secrets/mysqlmylogin"
"github.com/google/osv-scalibr/extractor/filesystem/secrets/onepasswordconnecttoken"
"github.com/google/osv-scalibr/extractor/filesystem/secrets/pgpass"
"github.com/google/osv-scalibr/veles"
"github.com/google/osv-scalibr/veles/secrets/anthropicapikey"
"github.com/google/osv-scalibr/veles/secrets/awsaccesskey"
"github.com/google/osv-scalibr/veles/secrets/azurestorageaccountaccesskey"
"github.com/google/osv-scalibr/veles/secrets/azuretoken"
"github.com/google/osv-scalibr/veles/secrets/cratesioapitoken"
Expand Down Expand Up @@ -283,6 +283,7 @@ var (
pgpass.Name: {noCFG(pgpass.New)},
onepasswordconnecttoken.Name: {noCFG(onepasswordconnecttoken.New)},
mariadb.Name: {noCFG(mariadb.NewDefault)},
awsaccesskey.Name: {noCFG(awsaccesskey.New)},
}

// SecretDetectors for Detector interface.
Expand Down Expand Up @@ -331,7 +332,6 @@ var (
{onepasswordkeys.NewRecoveryTokenDetector(), "secrets/onepasswordrecoverycode", 0},
{gcshmackey.NewDetector(), "secrets/gcshmackey", 0},
{vapid.NewDetector(), "secrets/vapidkey", 0},
{awsaccesskey.NewDetector(), "secrets/awsaccesskey", 0},
{recaptchakey.NewDetector(), "secrets/recaptchakey", 0},
})

Expand Down
46 changes: 46 additions & 0 deletions extractor/filesystem/secrets/awsaccesskey/awsaccesskey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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 awsaccesskey extends the veles awsaccesskey.Detector to search inside the `~/.aws/credentials` file
package awsaccesskey

import (
"path/filepath"
"strings"

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

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

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

// New returns a filesystem.Extractor which extracts AWS Access Keys using the awsaccesskey.Detector
func New() filesystem.Extractor {
return convert.FromVelesDetectorWithRequire(
awsaccesskey.NewDetector(), Name, Version, FileRequired,
)
}

// FileRequired returns true if a file contains aws credentials.
func FileRequired(api filesystem.FileAPI) bool {
path := filepath.ToSlash(api.Path())
return strings.HasSuffix(path, ".aws/credentials")
}
119 changes: 119 additions & 0 deletions extractor/filesystem/secrets/awsaccesskey/awsaccesskey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// 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 awsaccesskey_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/awsaccesskey"
"github.com/google/osv-scalibr/extractor/filesystem/simplefileapi"
"github.com/google/osv-scalibr/inventory"
"github.com/google/osv-scalibr/testing/extracttest"
awsaccesskeydetector "github.com/google/osv-scalibr/veles/secrets/awsaccesskey"
)

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

// linux
{inputPath: `/Users/example-user/.aws/credentials`, want: true},
{inputPath: `/Users/example-user/bad/path`, want: false},

// windows
{inputPath: `C:\Users\USERNAME\.aws\credentials`, isWindows: true, want: true},
{inputPath: `C:\Users\USERNAME\another\bad\path`, isWindows: true, want: true},
}

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 := awsaccesskey.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: "aws_credentials",
Path: "aws_credentials",
WantSecrets: []*inventory.Secret{
{
Secret: awsaccesskeydetector.Credentials{
AccessID: "AIKA1984R439T439HTH4",
Secret: "32r923jr023rk320rk2a3rkB34tj340r32Ckt433",
},
Location: "aws_credentials",
},
},
},
{
Name: "random_content",
Path: "random_content",
WantSecrets: nil,
},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
extr := awsaccesskey.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,3 @@
[default]
aws_access_key_id = AIKA1984R439T439HTH4
aws_secret_access_key = 32r923jr023rk320rk2a3rkB34tj340r32Ckt433
Empty file.
10 changes: 10 additions & 0 deletions extractor/filesystem/secrets/awsaccesskey/testdata/random_content
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
12 changes: 11 additions & 1 deletion extractor/filesystem/secrets/convert/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func FromVelesDetector(velesDetector veles.Detector, name string, version int) f
}
}

type extractorPlaceHolder interface {
IsPlaceholder() bool
}

// detectorWrapper is a wrapper around the veles.Detector interface that
// implements the additional functions of the filesystem Extractor interface.
type detectorWrapper struct {
Expand All @@ -45,6 +49,8 @@ type detectorWrapper struct {
version int
}

func (d *detectorWrapper) IsPlaceholder() bool { return true }

// MaxSecretLen returns the maximum length a secret from this Detector can have.
func (d *detectorWrapper) MaxSecretLen() uint32 {
return d.velesDetector.MaxSecretLen()
Expand Down Expand Up @@ -95,9 +101,13 @@ var _ filesystem.Extractor = &detectorWrapper{}
func SetupVelesExtractors(extractors []filesystem.Extractor) ([]filesystem.Extractor, error) {
result := make([]filesystem.Extractor, 0, len(extractors))
detectors := []veles.Detector{}

for _, e := range extractors {
if d, ok := e.(veles.Detector); ok {
if d, isDetector := e.(veles.Detector); isDetector {
detectors = append(detectors, d)
if _, isExtractorPlaceHolder := e.(extractorPlaceHolder); !isExtractorPlaceHolder {
result = append(result, e)
}
} else {
result = append(result, e)
}
Expand Down
94 changes: 94 additions & 0 deletions extractor/filesystem/secrets/convert/specific.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package convert

import (
"context"
"fmt"

"github.com/google/osv-scalibr/extractor/filesystem"
"github.com/google/osv-scalibr/inventory"
"github.com/google/osv-scalibr/plugin"
"github.com/google/osv-scalibr/veles"
)

// FromVelesDetectorWithRequire converts a Veles Detector into a SCALIBR FilesystemExtractor plugin.
// This allows:
// - Enabling Veles Detectors individually like regular SCALIBR plugins.
// - Using the provided detector in the detection engine with other detectors.
// - Using the detector as a standalone filesystem extractor.
func FromVelesDetectorWithRequire(velesDetector veles.Detector, name string, version int, fileRequired func(filesystem.FileAPI) bool) filesystem.Extractor {
return &detectorWithRequire{
velesDetector: velesDetector,
name: name,
version: version,
fileRequired: fileRequired,
}
}

// Assert that detectorWithRequire implements the required interfaces.
var _ veles.Detector = &detectorWithRequire{}
var _ filesystem.Extractor = &detectorWithRequire{}

// detectorWithRequire is a wrapper around the veles.Detector interface that
// implements the additional functions of the filesystem Extractor interface.
type detectorWithRequire struct {
velesDetector veles.Detector
name string
version int
fileRequired func(filesystem.FileAPI) bool
e *veles.DetectionEngine
}

// MaxSecretLen returns the maximum length a secret from this Detector can have.
func (d *detectorWithRequire) MaxSecretLen() uint32 {
return d.velesDetector.MaxSecretLen()
}

// Detect finds candidate secrets in the data and returns them alongside their
// starting positions.
func (d *detectorWithRequire) Detect(data []byte) ([]veles.Secret, []int) {
return d.velesDetector.Detect(data)
}

// Name of the secret extractor.
func (d *detectorWithRequire) Name() string {
return d.name
}

// Version of the secret extractor.
func (d *detectorWithRequire) Version() int {
return d.version
}

// Requirements of the secret extractor.
func (d *detectorWithRequire) Requirements() *plugin.Capabilities {
// Veles plugins don't have any special requirements.
return &plugin.Capabilities{}
}

// FileRequired returns the provided file required callback.
func (d *detectorWithRequire) FileRequired(api filesystem.FileAPI) bool {
return d.fileRequired(api)
}

// Extract extracts secret from the filesystem using the provided detector.
func (d *detectorWithRequire) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) {
if d.e == nil {
var err error
d.e, err = veles.NewDetectionEngine([]veles.Detector{d.velesDetector})
if err != nil {
return inventory.Inventory{}, err
}
}
secrets, err := d.e.Detect(ctx, input.Reader)
if err != nil {
return inventory.Inventory{}, fmt.Errorf("unable to scan for secrets: %w", err)
}
i := inventory.Inventory{}
for _, s := range secrets {
i.Secrets = append(i.Secrets, &inventory.Secret{
Secret: s,
Location: input.Path,
})
}
return i, nil
}
Loading