Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
569fa1d
added flyio detector
lonmarsDev Feb 2, 2024
fbbbc6d
Merge branch 'main' into detectors/flyio_detector
lonmarsDev Feb 5, 2024
13f862f
Merge branch 'main' into detectors/flyio_detector
amanfcp Jun 25, 2025
e459c3a
Merge branch 'main' into detectors/flyio_detector
amanfcp Jun 26, 2025
312b317
Merge branch 'main' into detectors/flyio_detector
amanfcp Jun 26, 2025
e997122
Merge branch 'main' of github.com:trufflesecurity/trufflehog into det…
amanfcp Jul 1, 2025
1b6e9b0
Add Fly.io Detector with Integration Tests
amanfcp Jul 2, 2025
b2cf7f9
Merge branch 'main' into detectors/flyio_detector
amanfcp Jul 2, 2025
3a613f5
incorporated feedback on PR
amanfcp Jul 3, 2025
f3f3a71
Merge branch 'main' into detectors/flyio_detector
amanfcp Jul 3, 2025
ecc0118
Merge branch 'main' into detectors/flyio_detector
kashifkhan0771 Jul 8, 2025
355c838
Merge branch 'main' into detectors/flyio_detector
amanfcp Jul 11, 2025
e54443d
Merge branch 'main' into detectors/flyio_detector
shahzadhaider1 Jul 14, 2025
b8c72f8
Remove "flyio" keyword from Fly.io detector keywords for improved acc…
amanfcp Jul 14, 2025
4179216
Merge branch 'detectors/flyio_detector' of https://github.com/lonmars…
amanfcp Jul 14, 2025
0c8b987
Merge branch 'main' into detectors/flyio_detector
amanfcp Jul 17, 2025
6ca3711
Merge branch 'main' into detectors/flyio_detector
amanfcp Aug 18, 2025
bd3b3f8
use existing false positive filtering flow while excluding AAAAAA whi…
amanfcp Aug 19, 2025
c0d8175
Merge branch 'detectors/flyio_detector' of https://github.com/lonmars…
amanfcp Aug 19, 2025
076b8db
Merge branch 'main' into detectors/flyio_detector
amanfcp Aug 19, 2025
dfb4395
Merge branch 'main' into detectors/flyio_detector
amanfcp Aug 19, 2025
e1f041f
Merge branch 'main' into detectors/flyio_detector
kashifkhan0771 Aug 25, 2025
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
123 changes: 123 additions & 0 deletions pkg/detectors/flyio/flyio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package flyio

import (
"context"
"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/detectorspb"
)

type Scanner struct {
client *http.Client
}

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

var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(FlyV1 fm\d+_[A-Za-z0-9+\/=,_-]{500,700})\b`)
)

// 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{"FlyV1"}
}

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

uniqueMatches := make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueMatches[match[1]] = struct{}{}
}

for match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_FlyIO,
Raw: []byte(match),
}

if verify {
client := s.client
if client == nil {
client = defaultClient
}

isVerified, verificationErr := verifyMatch(ctx, client, match)

s1.Verified = isVerified
if verificationErr != nil {
s1.SetVerificationError(verificationErr, match)
}
}

results = append(results, s1)
}

return
}

func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
// Not setting org_slug intentionally, as it's not required for the token to be valid.
// Initially, an organization named "personal" is created by FlyIO when the user signs up for an account. We cannot rely on this as it can be deleted.
// 403 is returned if incorrect org_slug is sent.
// 401 is returned if the token is invalid.
// 400 is returned if the token is valid but no org_slug is sent.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.machines.dev/v1/apps?org_slug=", http.NoBody)
if err != nil {
return false, nil
}
req.Header.Add("accept", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))

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.StatusBadRequest:
// Not setting org_slug returns a 400 error, which is expected.
return true, nil
case http.StatusUnauthorized:
// The secret is determinately not verified (nothing to do)
return false, nil
default:
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
return false, err
}
}

func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_FlyIO
}

func (s Scanner) Description() string {
return "Fly.io is a platform for running applications globally. Fly.io tokens can be used to access the Fly.io API and manage applications."
}

func (s Scanner) IsFalsePositive(result detectors.Result) (bool, string) {
// ignore AAAAAA for Flyio detector
if strings.Contains(string(result.Raw), "AAAAAA") {
return false, ""
}

// For non-matching patterns, fall back to default false positive logic
return detectors.IsKnownFalsePositive(string(result.Raw), detectors.DefaultFalsePositives, true)
}
148 changes: 148 additions & 0 deletions pkg/detectors/flyio/flyio_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//go:build detectors
// +build detectors

package flyio

import (
"context"
"fmt"
"testing"
"time"

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

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

func TestFlyio_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
secret := testSecrets.MustGetField("FLYIO")
inactiveSecret := testSecrets.MustGetField("FLYIO_INACTIVE")

type args struct {
ctx context.Context
data []byte
verify bool
}
tests := []struct {
name string
s Scanner
args args
want []detectors.Result
wantErr bool
wantVerificationErr bool
}{
{
name: "found, verified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flyio secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlyIO,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flyio secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlyIO,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "not found",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte("You cannot find the secret within"),
verify: true,
},
want: nil,
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, would be verified if not for timeout",
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a flyio secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_FlyIO,
Verified: false,
},
},
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 flyio secret %s within", secret)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_FlyIO,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 404"))
return []detectors.Result{r}
}(),
wantErr: false,
wantVerificationErr: true,
},
}
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("Flyio.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatalf("no raw secret present: \n %+v", got[i])
}
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "AnalysisInfo")
ignoreUnexported := cmpopts.IgnoreUnexported(detectors.Result{})
if diff := cmp.Diff(got, tt.want, ignoreOpts, ignoreUnexported); diff != "" {
t.Errorf("Flyio.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}
})
}
}
Loading