-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Update artifactory detector with basic auth support #4605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
zacharyyun
wants to merge
18
commits into
trufflesecurity:main
Choose a base branch
from
zacharyyun:feat/artifactory-basic-auth-detection-enhancement
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+534
−9
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
1dbc37d
Update artifactory detector with basic auth support
zacharyyun a93b60f
V2 organization
zacharyyun 439ec41
split basic auth logic into v2
zacharyyun ad0b271
update integration_test
zacharyyun 78c8ae4
separated by access token and basic auth types for clarity
zacharyyun 85d98c6
remove versioning
zacharyyun 4a1104e
add exclusion for artifactorybasicauth from cloud endpoint check sinc…
zacharyyun 75a1951
update missed sessiontoken to basicauth
zacharyyun 060c8fc
add unconditional false positive check
zacharyyun 39ecc61
update status code check for 200 and 403
zacharyyun c1feb50
remove unused imports
zacharyyun c0ce67e
Merge branch 'main' into feat/artifactory-basic-auth-detection-enhanc…
kashifkhan0771 80771e6
remove endpoint customizer, add custom fp checker var, set http/s pre…
zacharyyun b9e6752
resolve proto conflicts
zacharyyun 7418a15
Merge branch 'main' into feat/artifactory-basic-auth-detection-enhanc…
zacharyyun 42e76b3
remove unnecessary endpoint engine test, add filter for 403 rate limi…
zacharyyun b0ad39d
Merge remote-tracking branch 'origin/feat/artifactory-basic-auth-dete…
zacharyyun bb25b38
remove optional http/https prefix, add password for redact
zacharyyun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
211 changes: 211 additions & 0 deletions
211
pkg/detectors/artifactory/basicauth/artifactorybasicauth.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| package artifactory | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "strings" | ||
|
|
||
| regexp "github.com/wasilibs/go-re2" | ||
|
|
||
| "github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" | ||
| ) | ||
|
|
||
| type Scanner struct { | ||
| client *http.Client | ||
| detectors.DefaultMultiPartCredentialProvider | ||
| } | ||
zacharyyun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| type basicArtifactoryCredential struct { | ||
| username string | ||
| password string | ||
| host string | ||
| raw string | ||
| } | ||
|
|
||
| var ( | ||
| // Ensure the Scanner satisfies the interface at compile time. | ||
| _ detectors.Detector = (*Scanner)(nil) | ||
| _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil) | ||
|
|
||
zacharyyun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses | ||
|
|
||
| basicAuthURLPattern = regexp.MustCompile( | ||
| `(?P<username>[^:@\s\/]+):(?P<password>[^:@\s\/]+)@(?P<host>[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]\.jfrog\.io)(?P<path>/[^\s"'<>]*)?`, | ||
| ) | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| invalidHosts = simple.NewCache[struct{}]() | ||
|
|
||
| errNoHost = errors.New("no such host") | ||
| ) | ||
|
|
||
|
|
||
| // Keywords are used for efficiently pre-filtering chunks. | ||
| // Use identifiers in the secret preferably, or the provider name. | ||
| func (s Scanner) Keywords() []string { | ||
| return []string{"artifactory", "jfrog.io"} | ||
| } | ||
|
|
||
| func (s Scanner) getClient() *http.Client { | ||
| if s.client != nil { | ||
| return s.client | ||
| } | ||
| return defaultClient | ||
| } | ||
|
|
||
| // FromData will find and optionally verify Artifactory secrets in a given set of bytes. | ||
| func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { | ||
| dataStr := string(data) | ||
|
|
||
| // ---------------------------------------- | ||
| // Basic Auth URI detection & verification | ||
| // ---------------------------------------- | ||
| basicCreds := make(map[string]basicArtifactoryCredential) | ||
|
|
||
| for _, match := range basicAuthURLPattern.FindAllStringSubmatch(dataStr, -1) { | ||
| if len(match) == 0 { | ||
| continue | ||
| } | ||
| subexpNames := basicAuthURLPattern.SubexpNames() | ||
|
|
||
| var username, password, host string | ||
| for i, name := range subexpNames { | ||
| if i == 0 || name == "" { | ||
| continue | ||
| } | ||
| switch name { | ||
| case "username": | ||
| username = match[i] | ||
| case "password": | ||
| password = match[i] | ||
| case "host": | ||
| host = match[i] | ||
| } | ||
zacharyyun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| if username == "" || password == "" || host == "" { | ||
| continue | ||
| } | ||
|
|
||
| key := username + ":" + password + "@" + host | ||
| if _, exists := basicCreds[key]; exists { | ||
| continue | ||
| } | ||
|
|
||
| basicCreds[key] = basicArtifactoryCredential{ | ||
| username: username, | ||
| password: password, | ||
| host: host, | ||
| raw: match[0], | ||
| } | ||
| } | ||
|
|
||
| for _, cred := range basicCreds { | ||
| if invalidHosts.Exists(cred.host) { | ||
| continue | ||
| } | ||
|
|
||
| r := detectors.Result{ | ||
| DetectorType: detectorspb.DetectorType_ArtifactoryBasicAuth, | ||
| Raw: []byte(cred.raw), | ||
| RawV2: []byte(cred.username + ":" + cred.password + "@" + cred.host), | ||
| } | ||
|
|
||
| if verify { | ||
| isVerified, vErr := verifyArtifactoryBasicAuth(ctx, s.getClient(), cred.host, cred.username, cred.password) | ||
| r.Verified = isVerified | ||
|
|
||
| if vErr != nil { | ||
| if errors.Is(vErr, errNoHost) { | ||
| invalidHosts.Set(cred.host, struct{}{}) | ||
| continue | ||
| } | ||
| r.SetVerificationError(vErr, cred.username, cred.password) | ||
| } | ||
|
|
||
| if isVerified { | ||
| if r.AnalysisInfo == nil { | ||
| r.AnalysisInfo = make(map[string]string) | ||
| } | ||
| r.AnalysisInfo["domain"] = cred.host | ||
| r.AnalysisInfo["username"] = cred.username | ||
| r.AnalysisInfo["password"] = cred.password | ||
| r.AnalysisInfo["authType"] = "basic" | ||
zacharyyun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| results = append(results, r) | ||
| } | ||
|
|
||
| return results, nil | ||
| } | ||
|
|
||
| func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) { | ||
| return false, "" | ||
| } | ||
|
|
||
| func verifyArtifactoryBasicAuth(ctx context.Context, client *http.Client, host, username, password string) (bool, error) { | ||
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+host+"/artifactory/api/system/ping", nil) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
|
|
||
| // Use HTTP Basic authentication with the parsed username and password. | ||
| req.SetBasicAuth(username, password) | ||
|
|
||
| resp, err := client.Do(req) | ||
| if err != nil { | ||
| if strings.Contains(err.Error(), "no such host") { | ||
| return false, errNoHost | ||
| } | ||
|
|
||
| return false, err | ||
| } | ||
|
|
||
| defer func() { | ||
| _, _ = io.Copy(io.Discard, resp.Body) | ||
| _ = resp.Body.Close() | ||
| }() | ||
|
|
||
| switch resp.StatusCode { | ||
| case http.StatusOK: | ||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
|
|
||
| if strings.TrimSpace(string(body)) == "OK" { | ||
| return true, nil | ||
| } | ||
|
|
||
| return false, nil | ||
zacharyyun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| case http.StatusForbidden: | ||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
|
|
||
| // Ignore rate-limit / temporary block 403s | ||
| if strings.Contains(strings.ToLower(string(body)), "blocked due to recurrent request failures") { | ||
| return false, nil | ||
| } | ||
|
|
||
| return true, nil | ||
| case http.StatusUnauthorized, http.StatusFound: | ||
| return false, nil | ||
| default: | ||
| return false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode) | ||
| } | ||
| } | ||
|
|
||
| func (s Scanner) Type() detectorspb.DetectorType { | ||
| return detectorspb.DetectorType_ArtifactoryBasicAuth | ||
| } | ||
zacharyyun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| func (s Scanner) Description() string { | ||
| return "Artifactory is a repository manager that supports all major package formats. Artifactory Basic Auth credentials can be used to authenticate and perform operations on repositories." | ||
| } | ||
146 changes: 146 additions & 0 deletions
146
pkg/detectors/artifactory/basicauth/artifactorybasicauth_integration_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| //go:build detectors | ||
| // +build detectors | ||
|
|
||
| package artifactory | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
|
|
||
| "github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" | ||
| ) | ||
|
|
||
| func TestArtifactory_FromChunk(t *testing.T) { | ||
| ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) | ||
| defer cancel() | ||
| testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5") | ||
| if err != nil { | ||
| t.Fatalf("could not get test secrets from GCP: %s", err) | ||
| } | ||
| basicAuthValid := testSecrets.MustGetField("ARTIFACTORY_BASIC_AUTH_VALID") | ||
| basicAuthInactive := testSecrets.MustGetField("ARTIFACTORY_BASIC_AUTH_INACTIVE") | ||
|
|
||
| type args struct { | ||
| ctx context.Context | ||
| data []byte | ||
| verify bool | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| s Scanner | ||
| args args | ||
| want []detectors.Result | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "found, verified", | ||
| s: Scanner{}, | ||
| args: args{ | ||
| ctx: context.Background(), | ||
| data: []byte(fmt.Sprintf( | ||
| "You can find an Artifactory basic auth URL https://%s/artifactory/api/pypi/pypi/simple", | ||
| basicAuthValid, | ||
| )), | ||
| verify: true, | ||
| }, | ||
| want: []detectors.Result{ | ||
| { | ||
| DetectorType: detectorspb.DetectorType_ArtifactoryBasicAuth, | ||
| Verified: true, | ||
| }, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| { | ||
| name: "found, unverified", | ||
| s: Scanner{}, | ||
| args: args{ | ||
| ctx: context.Background(), | ||
| data: []byte(fmt.Sprintf( | ||
| "You can find an Artifactory basic auth URL https://%s/artifactory/api/pypi/pypi/simple but it's not valid", | ||
| basicAuthInactive, | ||
| )), | ||
| verify: true, | ||
| }, | ||
| want: []detectors.Result{ | ||
| { | ||
| DetectorType: detectorspb.DetectorType_ArtifactoryBasicAuth, | ||
| Verified: false, | ||
| }, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| { | ||
| name: "not found", | ||
| s: Scanner{}, | ||
| args: args{ | ||
| ctx: context.Background(), | ||
| data: []byte("You cannot find any Artifactory basic auth URL within this chunk"), | ||
| verify: true, | ||
| }, | ||
| want: nil, | ||
| wantErr: false, | ||
| }, | ||
| } | ||
| 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.Errorf("Artifactory.FromData() error = %v, wantErr %v", err, tt.wantErr) | ||
| return | ||
| } | ||
|
|
||
| if len(tt.want) == 0 { | ||
| if len(got) != 0 { | ||
| t.Fatalf("expected no results, got %d", len(got)) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| for i := range got { | ||
| if len(got[i].Raw) == 0 && len(got[i].RawV2) == 0 { | ||
| t.Fatalf("no raw secret present: \n %+v", got[i]) | ||
| } | ||
| gotErr := "" | ||
| if got[i].VerificationError() != nil { | ||
| gotErr = got[i].VerificationError().Error() | ||
| } | ||
| wantErr := "" | ||
| if tt.want[i].VerificationError() != nil { | ||
| wantErr = tt.want[i].VerificationError().Error() | ||
| } | ||
| if gotErr != wantErr { | ||
| t.Fatalf("wantVerificationError = %v, verification error = %v", | ||
| tt.want[i].VerificationError(), got[i].VerificationError()) | ||
| } | ||
| } | ||
| ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "primarySecret") | ||
| if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" { | ||
| t.Errorf("Artifactory.FromData() %s diff: (-got +want)\n%s", tt.name, diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkFromData(benchmark *testing.B) { | ||
| ctx := context.Background() | ||
| s := Scanner{} | ||
| for name, data := range detectors.MustGetBenchmarkData() { | ||
| benchmark.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) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.