Use bad json in slackwebhooks (#2193)

* add rotation guides to SlackWebhook tests

* begin cleaning up tests

* have slack webhook detector use malformed json

* update test secrets

---------

Co-authored-by: Ahrav Dutta <ahrav.dutta@trufflesec.com>
This commit is contained in:
Cody Rose 2023-12-11 18:04:55 -05:00 committed by GitHub
parent 61c7d52a43
commit 405f356071
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 69 additions and 84 deletions

View file

@ -60,7 +60,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
client = defaultClient client = defaultClient
} }
payload := strings.NewReader(`{"text": ""}`) // We don't want to actually send anything to webhooks we find. To verify them without spamming them, we
// send an intentionally malformed message and look for a particular expected error message.
payload := strings.NewReader(`intentionally malformed JSON from Trufflehog scan`)
req, err := http.NewRequestWithContext(ctx, "POST", resMatch, payload) req, err := http.NewRequestWithContext(ctx, "POST", resMatch, payload)
if err != nil { if err != nil {
continue continue
@ -76,12 +78,18 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 || (res.StatusCode == 400 && (bytes.Equal(bodyBytes, []byte("no_text")) || bytes.Equal(bodyBytes, []byte("missing_text")))) { switch {
case res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusMultipleChoices:
// Hopefully this never happens - it means we actually sent something to a channel somewhere. But
// we at least know the secret is verified.
s1.Verified = true s1.Verified = true
} else if res.StatusCode == 401 || res.StatusCode == 403 { case res.StatusCode == http.StatusBadRequest && bytes.Equal(bodyBytes, []byte("invalid_payload")):
// The secret is determinately not verified (nothing to do) s1.Verified = true
} else { case res.StatusCode == http.StatusNotFound:
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) // Not a real webhook or the owning app's OAuth token has been revoked or the app has been deleted
// You might want to handle this case or log it.
default:
err = fmt.Errorf("unexpected HTTP response status %d: %s", res.StatusCode, bodyBytes)
s1.SetVerificationError(err, resMatch) s1.SetVerificationError(err, resMatch)
} }
} else { } else {

View file

@ -20,12 +20,14 @@ import (
func TestSlackWebhook_FromChunk(t *testing.T) { func TestSlackWebhook_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel() defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3") testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
if err != nil { if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err) t.Fatalf("could not get test secrets from GCP: %s", err)
} }
secret := testSecrets.MustGetField("SLACKWEBHOOK_TOKEN") secret := testSecrets.MustGetField("SLACKWEBHOOK_TOKEN")
inactiveSecret := testSecrets.MustGetField("SLACKWEBHOOK_INACTIVE") inactiveSecret := testSecrets.MustGetField("SLACKWEBHOOK_INACTIVE")
deletedWebhook := testSecrets.MustGetField("SLACKWEBHOOK_DELETED")
deletedUserActiveWebhook := testSecrets.MustGetField("SLACKWEBHOOK_DELETED_USER_ACTIVE_WEBHOOK")
type args struct { type args struct {
ctx context.Context ctx context.Context
@ -41,7 +43,7 @@ func TestSlackWebhook_FromChunk(t *testing.T) {
wantVerificationErr bool wantVerificationErr bool
}{ }{
{ {
name: "found, verified", name: "active webhook",
s: Scanner{}, s: Scanner{},
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@ -51,6 +53,7 @@ func TestSlackWebhook_FromChunk(t *testing.T) {
want: []detectors.Result{ want: []detectors.Result{
{ {
DetectorType: detectorspb.DetectorType_SlackWebhook, DetectorType: detectorspb.DetectorType_SlackWebhook,
ExtraData: map[string]string{"rotation_guide": "https://howtorotate.com/docs/tutorials/slack-webhook/"},
Verified: true, Verified: true,
}, },
}, },
@ -58,50 +61,35 @@ func TestSlackWebhook_FromChunk(t *testing.T) {
wantVerificationErr: false, wantVerificationErr: false,
}, },
{ {
name: "found, verified, 400 no_text", name: "active webhook created by a deactivated user",
s: Scanner{client: common.ConstantResponseHttpClient(400, "no_text")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a slackwebhook secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SlackWebhook,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, verified, 400 missing_text",
s: Scanner{client: common.ConstantResponseHttpClient(400, "missing_text")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a slackwebhook secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SlackWebhook,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{}, s: Scanner{},
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a slackwebhook secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation data: []byte(fmt.Sprintf("You can find a slackwebhook secret %s within", deletedUserActiveWebhook)),
verify: true, verify: true,
}, },
want: []detectors.Result{ want: []detectors.Result{
{ {
DetectorType: detectorspb.DetectorType_SlackWebhook, DetectorType: detectorspb.DetectorType_SlackWebhook,
ExtraData: map[string]string{"rotation_guide": "https://howtorotate.com/docs/tutorials/slack-webhook/"},
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "deleted webhook",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a slackwebhook secret %s within", deletedWebhook)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SlackWebhook,
ExtraData: map[string]string{"rotation_guide": "https://howtorotate.com/docs/tutorials/slack-webhook/"},
Verified: false, Verified: false,
}, },
}, },
@ -109,19 +97,43 @@ func TestSlackWebhook_FromChunk(t *testing.T) {
wantVerificationErr: false, wantVerificationErr: false,
}, },
{ {
name: "not found", name: "webhook from app with revoked token",
s: Scanner{}, s: Scanner{},
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
data: []byte("You cannot find the secret within"), data: []byte(fmt.Sprintf("You can find a slackwebhook secret %s within", inactiveSecret)),
verify: true, verify: true,
}, },
want: nil, want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SlackWebhook,
ExtraData: map[string]string{"rotation_guide": "https://howtorotate.com/docs/tutorials/slack-webhook/"},
Verified: false,
},
},
wantErr: false, wantErr: false,
wantVerificationErr: false, wantVerificationErr: false,
}, },
{ {
name: "found, would be verified if not for timeout", name: "unexpected webhook response",
s: Scanner{client: common.ConstantResponseHttpClient(500, "oh no")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a slackwebhook secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SlackWebhook,
ExtraData: map[string]string{"rotation_guide": "https://howtorotate.com/docs/tutorials/slack-webhook/"},
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "timeout",
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)}, s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@ -131,40 +143,7 @@ func TestSlackWebhook_FromChunk(t *testing.T) {
want: []detectors.Result{ want: []detectors.Result{
{ {
DetectorType: detectorspb.DetectorType_SlackWebhook, DetectorType: detectorspb.DetectorType_SlackWebhook,
Verified: false, ExtraData: map[string]string{"rotation_guide": "https://howtorotate.com/docs/tutorials/slack-webhook/"},
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "found, verified but unexpected api surface",
s: Scanner{client: common.ConstantResponseHttpClient(404, "")},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a slackwebhook secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SlackWebhook,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "found, account disabled",
s: Scanner{client: common.ConstantResponseHttpClient(400, disabledAccountResponse)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a slackwebhook secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SlackWebhook,
Verified: false, Verified: false,
}, },
}, },
@ -195,8 +174,6 @@ func TestSlackWebhook_FromChunk(t *testing.T) {
} }
} }
const disabledAccountResponse = `missing_text_or_fallback_or_attachments`
func BenchmarkFromData(benchmark *testing.B) { func BenchmarkFromData(benchmark *testing.B) {
ctx := context.Background() ctx := context.Background()
s := Scanner{} s := Scanner{}