Skip to content

Commit 885fa2d

Browse files
[INS-197] Add redhatpyxis api key detector (#4995)
* Add redhatpyxis api key detector * renamed secret parts key * regen protos and added pyxis as a keyword
1 parent c09d726 commit 885fa2d

9 files changed

Lines changed: 491 additions & 7 deletions

File tree

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ func run(state overseer.State, logSync func() error) {
544544
feature.UserDetectorEnabled.Store(true)
545545
feature.BraintrustDetectorEnabled.Store(true)
546546
feature.PgAnalyzeReadKeyDetectorEnabled.Store(true)
547+
feature.RedHatPyxisDetectorEnabled.Store(true)
547548

548549
conf := &config.Config{}
549550
if *configFilename != "" {
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package redhatpyxis
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
9+
regexp "github.com/wasilibs/go-re2"
10+
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
14+
)
15+
16+
type Scanner struct {
17+
client *http.Client
18+
}
19+
20+
// Compile-time interface check
21+
var _ detectors.Detector = (*Scanner)(nil)
22+
23+
var (
24+
defaultClient = detectors.NewClientWithDedup(common.SaneHttpClient())
25+
26+
pyxisAPIKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"redhat", "pyxis"}) + `\b([a-z0-9]{32})\b`)
27+
)
28+
29+
// Keywords used for fast pre-filtering
30+
func (s Scanner) Keywords() []string {
31+
return []string{
32+
"redhat",
33+
"pyxis",
34+
}
35+
}
36+
37+
func (s Scanner) getClient() *http.Client {
38+
if s.client != nil {
39+
return s.client
40+
}
41+
return defaultClient
42+
}
43+
44+
// FromData scans for Red Hat Pyxis API keys and optionally verifies them.
45+
func (s Scanner) FromData(
46+
ctx context.Context,
47+
verify bool,
48+
data []byte,
49+
) (results []detectors.Result, err error) {
50+
51+
dataStr := string(data)
52+
53+
uniqueTokens := make(map[string]struct{})
54+
for _, match := range pyxisAPIKeyPat.FindAllStringSubmatch(dataStr, -1) {
55+
uniqueTokens[match[1]] = struct{}{}
56+
}
57+
58+
for token := range uniqueTokens {
59+
result := detectors.Result{
60+
DetectorType: detector_typepb.DetectorType_RedHatPyxis,
61+
Raw: []byte(token),
62+
SecretParts: map[string]string{
63+
"key": token,
64+
},
65+
}
66+
67+
if verify {
68+
verified, verificationErr := verifyPyxisAPIKey(
69+
ctx,
70+
s.getClient(),
71+
token,
72+
)
73+
result.SetVerificationError(verificationErr, token)
74+
result.Verified = verified
75+
}
76+
77+
results = append(results, result)
78+
}
79+
80+
return
81+
}
82+
83+
func verifyPyxisAPIKey(
84+
ctx context.Context,
85+
client *http.Client,
86+
token string,
87+
) (bool, error) {
88+
89+
req, err := http.NewRequestWithContext(
90+
ctx,
91+
http.MethodGet,
92+
"https://catalog.redhat.com/api/containers/v1/projects/certification/requests/images?page_size=1&page=0",
93+
http.NoBody,
94+
)
95+
if err != nil {
96+
return false, err
97+
}
98+
99+
req.Header.Set("X-API-KEY", token)
100+
101+
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_RedHatPyxis, token, req)
102+
if err != nil {
103+
return false, err
104+
}
105+
defer func() {
106+
_, _ = io.Copy(io.Discard, res.Body)
107+
_ = res.Body.Close()
108+
}()
109+
110+
switch res.StatusCode {
111+
case http.StatusOK:
112+
return true, nil
113+
114+
case http.StatusUnauthorized:
115+
// Invalid API key
116+
return false, nil
117+
118+
default:
119+
return false, fmt.Errorf(
120+
"unexpected HTTP response status %d",
121+
res.StatusCode,
122+
)
123+
}
124+
}
125+
126+
func (s Scanner) Type() detector_typepb.DetectorType {
127+
return detector_typepb.DetectorType_RedHatPyxis
128+
}
129+
130+
func (s Scanner) Description() string {
131+
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."
132+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
//go:build detectors
2+
// +build detectors
3+
4+
package redhatpyxis
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"testing"
10+
"time"
11+
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/google/go-cmp/cmp/cmpopts"
14+
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
16+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
17+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
18+
)
19+
20+
func TestRedHatPyxis_FromData(t *testing.T) {
21+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
22+
defer cancel()
23+
24+
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
25+
if err != nil {
26+
t.Fatalf("could not get test secrets from GCP: %s", err)
27+
}
28+
29+
activeToken := testSecrets.MustGetField("REDHAT_PYXIS_API_KEY")
30+
31+
// Random inactive token matching the expected format
32+
inactiveToken := "o9ynnj1wfw33a50g9009ti0ne1kqe8ac"
33+
34+
type args struct {
35+
ctx context.Context
36+
data []byte
37+
verify bool
38+
}
39+
40+
tests := []struct {
41+
name string
42+
s Scanner
43+
args args
44+
want []detectors.Result
45+
wantErr bool
46+
wantVerificationErr bool
47+
}{
48+
{
49+
name: "found, verified",
50+
s: Scanner{},
51+
args: args{
52+
ctx: context.Background(),
53+
data: fmt.Appendf(
54+
[]byte{},
55+
"redhat api key %s",
56+
activeToken,
57+
),
58+
verify: true,
59+
},
60+
want: []detectors.Result{
61+
{
62+
DetectorType: detector_typepb.DetectorType_RedHatPyxis,
63+
Verified: true,
64+
Raw: []byte(activeToken),
65+
},
66+
},
67+
},
68+
{
69+
name: "found, real token, verification error due to timeout",
70+
s: Scanner{
71+
client: common.SaneHttpClientTimeOut(1 * time.Microsecond),
72+
},
73+
args: args{
74+
ctx: context.Background(),
75+
data: fmt.Appendf(
76+
[]byte{},
77+
"redhat api key %s",
78+
activeToken,
79+
),
80+
verify: true,
81+
},
82+
want: []detectors.Result{
83+
{
84+
DetectorType: detector_typepb.DetectorType_RedHatPyxis,
85+
Verified: false,
86+
Raw: []byte(activeToken),
87+
},
88+
},
89+
wantVerificationErr: true,
90+
},
91+
{
92+
name: "found, real token, verification error due to unexpected api surface",
93+
s: Scanner{
94+
client: common.ConstantResponseHttpClient(500, "{}"),
95+
},
96+
args: args{
97+
ctx: context.Background(),
98+
data: fmt.Appendf(
99+
[]byte{},
100+
"redhat api key %s",
101+
activeToken,
102+
),
103+
verify: true,
104+
},
105+
want: []detectors.Result{
106+
{
107+
DetectorType: detector_typepb.DetectorType_RedHatPyxis,
108+
Verified: false,
109+
Raw: []byte(activeToken),
110+
},
111+
},
112+
wantVerificationErr: true,
113+
},
114+
{
115+
name: "found, unverified (inactive token)",
116+
s: Scanner{},
117+
args: args{
118+
ctx: context.Background(),
119+
data: fmt.Appendf(
120+
[]byte{},
121+
"redhat api key %s",
122+
inactiveToken,
123+
),
124+
verify: true,
125+
},
126+
want: []detectors.Result{
127+
{
128+
DetectorType: detector_typepb.DetectorType_RedHatPyxis,
129+
Verified: false,
130+
Raw: []byte(inactiveToken),
131+
},
132+
},
133+
},
134+
{
135+
name: "not found",
136+
s: Scanner{},
137+
args: args{
138+
ctx: context.Background(),
139+
data: []byte("no secrets here"),
140+
verify: true,
141+
},
142+
want: nil,
143+
},
144+
}
145+
146+
for _, tt := range tests {
147+
t.Run(tt.name, func(t *testing.T) {
148+
got, err := tt.s.FromData(
149+
tt.args.ctx,
150+
tt.args.verify,
151+
tt.args.data,
152+
)
153+
154+
if (err != nil) != tt.wantErr {
155+
t.Fatalf(
156+
"RedHatPyxis.FromData() error = %v, wantErr %v",
157+
err,
158+
tt.wantErr,
159+
)
160+
}
161+
162+
for i := range got {
163+
if len(got[i].Raw) == 0 {
164+
t.Fatal("no raw secret present")
165+
}
166+
167+
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
168+
t.Fatalf(
169+
"wantVerificationError = %v, verification error = %v",
170+
tt.wantVerificationErr,
171+
got[i].VerificationError(),
172+
)
173+
}
174+
}
175+
176+
ignoreOpts := cmpopts.IgnoreFields(
177+
detectors.Result{},
178+
"ExtraData",
179+
"verificationError",
180+
"primarySecret",
181+
"SecretParts",
182+
)
183+
184+
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
185+
t.Errorf(
186+
"RedHatPyxis.FromData() %s diff: (-got +want)\n%s",
187+
tt.name,
188+
diff,
189+
)
190+
}
191+
})
192+
}
193+
}
194+
195+
func BenchmarkRedHatPyxis_FromData(b *testing.B) {
196+
ctx := context.Background()
197+
s := Scanner{}
198+
199+
for name, data := range detectors.MustGetBenchmarkData() {
200+
b.Run(name, func(b *testing.B) {
201+
b.ResetTimer()
202+
203+
for n := 0; n < b.N; n++ {
204+
_, err := s.FromData(ctx, false, data)
205+
if err != nil {
206+
b.Fatal(err)
207+
}
208+
}
209+
})
210+
}
211+
}

0 commit comments

Comments
 (0)