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
150 changes: 150 additions & 0 deletions pkg/detectors/blackduck/blackduck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package blackduck

import (
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
"strings"

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

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

type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}

// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)

var (
defaultClient = detectors.DetectorHttpClientWithLocalAddresses

// Black Duck API tokens are base64("<uuid>:<uuid>"), i.e. 100 base64 chars
// ending in "==". isValidTokenFormat confirms the decoded uuid pair below.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"blackduck", "black_duck"}) + `\b([A-Za-z0-9+/]{96,140}={0,2})`)
// Black Duck is self-hosted, so we need the server URL to verify.
endpointPat = regexp.MustCompile(detectors.PrefixRegex([]string{"blackduck", "black_duck"}) + `\b(https?://[a-zA-Z0-9.-]+(?::[0-9]{2,5})?)`)

uuidPat = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
)

// 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{"blackduck", "black_duck"}
}
Comment thread
cursor[bot] marked this conversation as resolved.

// isValidTokenFormat checks the candidate base64-decodes to a "uuid:uuid" pair.
func isValidTokenFormat(candidate string) bool {
decoded, err := base64.StdEncoding.DecodeString(candidate)
if err != nil {
decoded, err = base64.RawStdEncoding.DecodeString(strings.TrimRight(candidate, "="))
if err != nil {
return false
}
}

parts := strings.Split(string(decoded), ":")
if len(parts) != 2 {
return false
}
return uuidPat.MatchString(parts[0]) && uuidPat.MatchString(parts[1])
}

// FromData will find and optionally verify Black Duck 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)

endpointMatches := endpointPat.FindAllStringSubmatch(dataStr, -1)

uniqueTokens := make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
token := strings.TrimSpace(match[1])
if isValidTokenFormat(token) {
uniqueTokens[token] = struct{}{}
}
}

for token := range uniqueTokens {
for _, endpointMatch := range endpointMatches {
resEndpointMatch := strings.TrimSpace(endpointMatch[1])

u, err := detectors.ParseURLAndStripPathAndParams(resEndpointMatch)
if err != nil {
// skip invalid URLs
continue
}
u.Path = "/api/tokens/authenticate"

s1 := detectors.Result{
DetectorType: detector_typepb.DetectorType_BlackDuck,
Raw: []byte(token),
RawV2: []byte(token + resEndpointMatch),
SecretParts: map[string]string{
"key": token,
"url": resEndpointMatch,
},
}

if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verErr := verifyToken(ctx, client, u.String(), token)
s1.Verified = isVerified
if verErr != nil {
s1.SetVerificationError(verErr, token)
}
}

results = append(results, s1)
}
}

return results, nil
}

// verifyToken calls the Black Duck token-auth endpoint. 200 means valid,
// 401/403 mean invalid, anything else is treated as an indeterminate error.
func verifyToken(ctx context.Context, client *http.Client, url, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
if err != nil {
return false, err
}
req.Header.Set("Authorization", "token "+token)
req.Header.Set("Accept", "application/vnd.blackducksoftware.user-4+json")

res, err := client.Do(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, http.StatusForbidden:
// invalid token, nothing to do
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_BlackDuck
}

func (s Scanner) Description() string {
return "Black Duck is a software composition analysis (SCA) tool used to identify security and license risks in open-source dependencies. Black Duck API tokens authenticate to the Black Duck server's REST API and can expose scan results and project data."
}
96 changes: 96 additions & 0 deletions pkg/detectors/blackduck/blackduck_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package blackduck

import (
"context"
"fmt"
"testing"

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

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)

var (
// base64("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d:f0e1d2c3-b4a5-4968-8778-695a4b3c2d1e")
validToken = "YTFiMmMzZDQtZTVmNi00YTdiLThjOWQtMGUxZjJhM2I0YzVkOmYwZTFkMmMzLWI0YTUtNDk2OC04Nzc4LTY5NWE0YjNjMmQxZQ=="
// Correct length/alphabet, but decodes to plain text instead of "<uuid>:<uuid>".
invalidToken = "dGhpcy1pcy1ub3QtYS1yZWFsLWJsYWNrZHVjay10b2tlbi1qdXN0LWZpbGxlci1jb250ZW50LXBhZGRpbmcteHl6MDEyMzQ1Ng=="
validEndpoint = "https://blackduck.example.com"
keyword = "blackduck"
)

func TestBlackduck_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern - token and url",
input: fmt.Sprintf("%s api token - '%s'\n%s url - '%s'\n", keyword, validToken, keyword, validEndpoint),
want: []string{validToken + validEndpoint},
},
{
name: "invalid pattern - token does not decode to <uuid>:<uuid>",
input: fmt.Sprintf("%s api token - '%s'\n%s url - '%s'\n", keyword, invalidToken, keyword, validEndpoint),
want: []string{},
},
{
name: "no result without an endpoint - verification needs the server url",
input: fmt.Sprintf("%s api token - '%s'\n", keyword, validToken),
want: []string{},
},
{
// Underscore form only, and a host with no "blackduck" substring, so
// the chunk matches solely on the "black_duck" keyword.
name: "valid pattern - black_duck underscore form (env-var style)",
input: fmt.Sprintf("BLACK_DUCK_API_TOKEN='%s'\nBLACK_DUCK_URL='https://bd.example.com'\n", validToken),
want: []string{validToken + "https://bd.example.com"},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
return
}

results, err := d.FromData(context.Background(), false, []byte(test.input))
if err != nil {
t.Errorf("error = %v", err)
return
}

if len(results) != len(test.want) {
if len(results) == 0 {
t.Errorf("did not receive result")
} else {
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
}
return
}

actual := make(map[string]struct{}, len(results))
for _, r := range results {
if len(r.RawV2) > 0 {
actual[string(r.RawV2)] = struct{}{}
} else {
actual[string(r.Raw)] = struct{}{}
}
}
expected := make(map[string]struct{}, len(test.want))
for _, v := range test.want {
expected[v] = struct{}{}
}

if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
}
})
}
}
2 changes: 2 additions & 0 deletions pkg/engine/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bitfinex"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bitlyaccesstoken"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bitmex"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/blackduck"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/blazemeter"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/blitapp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/blogger"
Expand Down Expand Up @@ -987,6 +988,7 @@ func buildDetectorList() []detectors.Detector {
&bitfinex.Scanner{},
&bitlyaccesstoken.Scanner{},
&bitmex.Scanner{},
&blackduck.Scanner{},
&blazemeter.Scanner{},
&blitapp.Scanner{},
// &blocknative.Scanner{}, // temporary disabled due to API issue
Expand Down
8 changes: 6 additions & 2 deletions pkg/pb/detector_typepb/detector_type.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions proto/detector_type.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1054,4 +1054,5 @@ enum DetectorType {
GitLabOauth2 = 1050;
SpectralOps = 1051;
AWSAppSync = 1052;
BlackDuck = 1053;
}