Skip to content
Open
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
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ func run(state overseer.State, logSync func() error) {
feature.PineconeDetectorEnabled.Store(true)
feature.CloudinaryDetectorEnabled.Store(true)
feature.GitLabOAuthDetectorEnabled.Store(true)
feature.RedHatPyxisDetectorEnabled.Store(true)

conf := &config.Config{}
if *configFilename != "" {
Expand Down
131 changes: 131 additions & 0 deletions pkg/detectors/redhatpyxis/redhatpyxis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package redhatpyxis

import (
"context"
"fmt"
"io"
"net/http"

regexp "github.com/wasilibs/go-re2"

"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)

type Scanner struct {
client *http.Client
}

// Compile-time interface check
var _ detectors.Detector = (*Scanner)(nil)

var (
defaultClient = detectors.NewClientWithDedup(common.SaneHttpClient())

pyxisAPIKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"redhat"}) + `\b([a-z0-9]{32})\b`)
)

// Keywords used for fast pre-filtering
func (s Scanner) Keywords() []string {
return []string{
"redhat",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it'll be beneficial to add pyxis to the detector/regex keywords instead of redhat or along with redhat?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think this is a good idea, just in case someone calls it "pyxis" and not redhat or redhat_pyxis. Approving so you can address along with merge conflicts when you're in.

}
}

func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}

// FromData scans for Red Hat Pyxis API keys and optionally verifies them.
func (s Scanner) FromData(
ctx context.Context,
verify bool,
data []byte,
) (results []detectors.Result, err error) {

dataStr := string(data)

uniqueTokens := make(map[string]struct{})
for _, match := range pyxisAPIKeyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[match[1]] = struct{}{}
}

for token := range uniqueTokens {
result := detectors.Result{
DetectorType: detector_typepb.DetectorType_RedHatPyxis,
Raw: []byte(token),
SecretParts: map[string]string{
"key": token,
},
}

if verify {
verified, verificationErr := verifyPyxisAPIKey(
ctx,
s.getClient(),
token,
)
result.SetVerificationError(verificationErr, token)
result.Verified = verified
}

results = append(results, result)
}

return
}

func verifyPyxisAPIKey(
ctx context.Context,
client *http.Client,
token string,
) (bool, error) {

req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"https://catalog.redhat.com/api/containers/v1/projects/certification/requests/images?page_size=1&page=0",
http.NoBody,
)
if err != nil {
return false, err
}

req.Header.Set("X-API-KEY", token)

res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_RedHatPyxis, token, req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()

switch res.StatusCode {
case http.StatusOK:
return true, nil

case http.StatusUnauthorized:
// Invalid API key
return false, nil

default:
return false, fmt.Errorf(
"unexpected HTTP response status %d",
res.StatusCode,
)
}
}

func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.DetectorType_RedHatPyxis
}

func (s Scanner) Description() string {
return "Red Hat Pyxis is a container certification and metadata management platform. Red Hat Pyxis API keys can be used to authenticate against the Red Hat Ecosystem Catalog APIs and access container certification resources and metadata."
}
211 changes: 211 additions & 0 deletions pkg/detectors/redhatpyxis/redhatpyxis_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
//go:build detectors
// +build detectors

package redhatpyxis

import (
"context"
"fmt"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"

"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)

func TestRedHatPyxis_FromData(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}

activeToken := testSecrets.MustGetField("REDHAT_PYXIS_API_KEY")

// Random inactive token matching the expected format
inactiveToken := "o9ynnj1wfw33a50g9009ti0ne1kqe8ac"

type args struct {
ctx context.Context
data []byte
verify bool
}

tests := []struct {
name string
s Scanner
args args
want []detectors.Result
wantErr bool
wantVerificationErr bool
}{
{
name: "found, verified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: fmt.Appendf(
[]byte{},
"redhat api key %s",
activeToken,
),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detector_typepb.DetectorType_RedHatPyxis,
Verified: true,
Raw: []byte(activeToken),
},
},
},
{
name: "found, real token, verification error due to timeout",
s: Scanner{
client: common.SaneHttpClientTimeOut(1 * time.Microsecond),
},
args: args{
ctx: context.Background(),
data: fmt.Appendf(
[]byte{},
"redhat api key %s",
activeToken,
),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detector_typepb.DetectorType_RedHatPyxis,
Verified: false,
Raw: []byte(activeToken),
},
},
wantVerificationErr: true,
},
{
name: "found, real token, verification error due to unexpected api surface",
s: Scanner{
client: common.ConstantResponseHttpClient(500, "{}"),
},
args: args{
ctx: context.Background(),
data: fmt.Appendf(
[]byte{},
"redhat api key %s",
activeToken,
),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detector_typepb.DetectorType_RedHatPyxis,
Verified: false,
Raw: []byte(activeToken),
},
},
wantVerificationErr: true,
},
{
name: "found, unverified (inactive token)",
s: Scanner{},
args: args{
ctx: context.Background(),
data: fmt.Appendf(
[]byte{},
"redhat api key %s",
inactiveToken,
),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detector_typepb.DetectorType_RedHatPyxis,
Verified: false,
Raw: []byte(inactiveToken),
},
},
},
{
name: "not found",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte("no secrets here"),
verify: true,
},
want: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.s.FromData(
tt.args.ctx,
tt.args.verify,
tt.args.data,
)

if (err != nil) != tt.wantErr {
t.Fatalf(
"RedHatPyxis.FromData() error = %v, wantErr %v",
err,
tt.wantErr,
)
}

for i := range got {
if len(got[i].Raw) == 0 {
t.Fatal("no raw secret present")
}

if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
t.Fatalf(
"wantVerificationError = %v, verification error = %v",
tt.wantVerificationErr,
got[i].VerificationError(),
)
}
}

ignoreOpts := cmpopts.IgnoreFields(
detectors.Result{},
"ExtraData",
"verificationError",
"primarySecret",
"SecretParts",
)

if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf(
"RedHatPyxis.FromData() %s diff: (-got +want)\n%s",
tt.name,
diff,
)
}
})
}
}

func BenchmarkRedHatPyxis_FromData(b *testing.B) {
ctx := context.Background()
s := Scanner{}

for name, data := range detectors.MustGetBenchmarkData() {
b.Run(name, func(b *testing.B) {
b.ResetTimer()

for n := 0; n < b.N; n++ {
_, err := s.FromData(ctx, false, data)
if err != nil {
b.Fatal(err)
}
}
})
}
}
Loading
Loading