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
63 changes: 47 additions & 16 deletions pkg/detectors/timecamp/timecamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@ package timecamp

import (
"context"
regexp "github.com/wasilibs/go-re2"
"fmt"
"io"
"net/http"
"strings"

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{}
type Scanner struct {
client *http.Client
}

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

var (
client = common.SaneHttpClient()
defaultClient = common.SaneHttpClient()

// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"timecamp"}) + `\b([0-9a-z]{26})\b`)
Expand All @@ -29,6 +34,13 @@ func (s Scanner) Keywords() []string {
return []string{"timecamp"}
}

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

// FromData will find and optionally verify TimeCamp 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)
Expand All @@ -45,19 +57,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
}

if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://app.timecamp.com/third_party/api/user?format=json", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/vnd.timecamp+json; version=3")
req.Header.Add("Authorization", resMatch)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
isVerified, verificationErr := verifyTimeCamp(ctx, s.getClient(), resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}

results = append(results, s1)
Expand All @@ -73,3 +75,32 @@ func (s Scanner) Type() detector_typepb.DetectorType {
func (s Scanner) Description() string {
return "TimeCamp is a time tracking software for teams and freelancers. TimeCamp API keys can be used to access and modify time tracking data."
}

func verifyTimeCamp(ctx context.Context, client *http.Client, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://app.timecamp.com/third_party/api/user?format=json", nil)
if err != nil {
return false, err
}
req.Header.Add("Accept", "application/vnd.timecamp+json; version=3")
req.Header.Add("Authorization", key)

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

if res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusMultipleChoices {
return true, nil
}

switch res.StatusCode {
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
82 changes: 82 additions & 0 deletions pkg/detectors/timecamp/timecamp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package timecamp

import (
"context"
"errors"
"fmt"
"net/http"
"testing"

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

"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
Expand Down Expand Up @@ -89,3 +92,82 @@ func TestTimeCamp_Pattern(t *testing.T) {
})
}
}

func TestTimeCamp_Verification(t *testing.T) {
tests := []struct {
name string
client *http.Client
wantVerified bool
wantErr string
}{
{
name: "verified",
client: common.ConstantResponseHttpClient(http.StatusOK, "{}"),
wantVerified: true,
},
{
name: "unauthorized is unverified",
client: common.ConstantResponseHttpClient(http.StatusUnauthorized, "{}"),
},
{
name: "forbidden is unverified",
client: common.ConstantResponseHttpClient(http.StatusForbidden, "{}"),
},
{
name: "unexpected status is verification error",
client: common.ConstantResponseHttpClient(http.StatusInternalServerError, "{}"),
wantErr: "unexpected HTTP response status 500",
},
{
name: "request failure is verification error",
client: &http.Client{
Transport: common.FakeTransport{
CreateResponse: func(*http.Request) (*http.Response, error) {
return nil, errors.New("network down")
},
},
},
wantErr: "Get \"https://app.timecamp.com/third_party/api/user?format=json\": network down",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotVerified, gotErr := verifyTimeCamp(context.Background(), test.client, validPattern)
if gotVerified != test.wantVerified {
t.Fatalf("verified = %v, want %v", gotVerified, test.wantVerified)
}

if gotErr == nil && test.wantErr != "" {
t.Fatalf("expected verification error %q", test.wantErr)
}
if gotErr != nil && test.wantErr == "" {
t.Fatalf("unexpected verification error: %v", gotErr)
}
if gotErr != nil && gotErr.Error() != test.wantErr {
t.Fatalf("verification error = %q, want %q", gotErr.Error(), test.wantErr)
}
})
}
}

func TestTimeCamp_FromDataSetsVerificationError(t *testing.T) {
d := Scanner{client: common.ConstantResponseHttpClient(http.StatusInternalServerError, "{}")}

results, err := d.FromData(context.Background(), true, []byte(fmt.Sprintf("%s token = '%s'", keyword, validPattern)))
if err != nil {
t.Fatalf("FromData() error = %v", err)
}
if len(results) != 1 {
t.Fatalf("results length = %d, want 1", len(results))
}
if results[0].Verified {
t.Fatal("expected result to be unverified")
}
if results[0].VerificationError() == nil {
t.Fatal("expected verification error")
}
if got, want := results[0].VerificationError().Error(), "unexpected HTTP response status 500"; got != want {
t.Fatalf("verification error = %q, want %q", got, want)
}
}
Loading