Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
132 changes: 132 additions & 0 deletions pkg/detectors/artifactory/artifactory.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ type Scanner struct {
detectors.EndpointSetter
}

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)
Expand All @@ -31,6 +38,9 @@ var (
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b([a-zA-Z0-9]{64,73})\b`)
URLPat = regexp.MustCompile(`\b([A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]\.jfrog\.io)`)
basicAuthURLPattern = regexp.MustCompile(
`https?://(?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"'<>]*)?`,
)

invalidHosts = simple.NewCache[struct{}]()

Expand Down Expand Up @@ -113,6 +123,86 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result

}

// ----------------------------------------
// 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]
}
}

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_ArtifactoryAccessToken,
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.host)
}

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"
}
}

results = append(results, r)
}

return results, nil
}

Expand Down Expand Up @@ -159,6 +249,48 @@ func verifyArtifactory(ctx context.Context, client *http.Client, resURLMatch, re
}
}

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.Contains(string(body), "OK") {
return true, nil
}

return false, nil
case http.StatusUnauthorized, http.StatusForbidden, 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_ArtifactoryAccessToken
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/detectors/artifactory/artifactory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ func TestArtifactory_Pattern(t *testing.T) {
useFoundEndpoint: true,
want: nil,
},
{
name: "valid pattern - basic auth uri",
input: `https://user123:[email protected]/artifactory/api/pypi/pypi/simple`,
cloudEndpoint: "https://cloudendpoint.jfrog.io",
useCloudEndpoint: false,
useFoundEndpoint: false,
want: []string{"user123:[email protected]"},
},
}

for _, test := range tests {
Expand Down
Loading