Skip to content

Commit db92812

Browse files
asivaprasad09claude
andcommitted
Add Octopus Deploy API Key detector
This commit adds a detector for Octopus Deploy API keys: - Detects API keys with the "API-" prefix followed by 26 uppercase alphanumerics - Uses context-aware regex requiring "octopus" or "X-Octopus-ApiKey" nearby - Implements CustomFalsePositiveChecker interface for additional validation - Includes comprehensive test coverage for pattern matching and edge cases The detector adds OctopusApiKey (ID 1053) to the detector type enum and follows TruffleHog's best practices for secret detection. Key features: - Context-aware pattern matching to reduce false positives - Strict uppercase alphanumeric validation (26 characters after prefix) - Deduplication of repeated keys - Proper SecretParts population - Rotation guide in ExtraData - Support for both "octopus" and "X-Octopus-ApiKey" keywords Pattern: API-[A-Z0-9]{26} with "octopus" or "x-octopus-apikey" context Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent d411fff commit db92812

5 files changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package octopusapikey
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
regexp "github.com/wasilibs/go-re2"
8+
9+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
11+
)
12+
13+
type Scanner struct {
14+
detectors.DefaultMultiPartCredentialProvider
15+
}
16+
17+
var _ detectors.Detector = (*Scanner)(nil)
18+
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
19+
20+
var (
21+
// Keep the provider keyword close to the secret pattern to reduce false positives.
22+
// Octopus Deploy API keys are commonly represented as API- followed by 26 uppercase alphanumerics.
23+
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"octopus", "x-octopus-apikey"}) + `\b(API-[A-Z0-9]{26})(?:['"|\n\r\s\x60;]|$)`)
24+
)
25+
26+
func (s Scanner) Keywords() []string {
27+
return []string{"octopus", "X-Octopus-ApiKey"}
28+
}
29+
30+
func (s Scanner) Type() detector_typepb.DetectorType {
31+
return detector_typepb.DetectorType_OctopusApiKey
32+
}
33+
34+
func (s Scanner) Description() string {
35+
return "Octopus Deploy API keys authenticate requests to Octopus REST API endpoints."
36+
}
37+
38+
func (s Scanner) FromData(_ context.Context, _ bool, data []byte) ([]detectors.Result, error) {
39+
dataStr := string(data)
40+
41+
uniqueMatches := make(map[string]struct{})
42+
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
43+
if len(match) < 2 {
44+
continue
45+
}
46+
uniqueMatches[strings.TrimSpace(match[1])] = struct{}{}
47+
}
48+
49+
results := make([]detectors.Result, 0, len(uniqueMatches))
50+
for key := range uniqueMatches {
51+
results = append(results, detectors.Result{
52+
DetectorType: detector_typepb.DetectorType_OctopusApiKey,
53+
Raw: []byte(key),
54+
ExtraData: map[string]string{
55+
"rotation_guide": "https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key",
56+
},
57+
SecretParts: map[string]string{"key": key},
58+
})
59+
}
60+
61+
return results, nil
62+
}
63+
64+
func (s Scanner) IsFalsePositive(result detectors.Result) (bool, string) {
65+
return detectors.IsKnownFalsePositive(string(result.Raw), detectors.DefaultFalsePositives, true)
66+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package octopusapikey
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/stretchr/testify/require"
9+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
12+
)
13+
14+
func TestOctopusApiKey_Pattern(t *testing.T) {
15+
d := Scanner{}
16+
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
17+
18+
tests := []struct {
19+
name string
20+
input string
21+
want []string
22+
}{
23+
{
24+
name: "valid pattern - header usage",
25+
input: `X-Octopus-ApiKey: API-ZNRMR7SL6L3ATMOIK7GKJDKLPY`,
26+
want: []string{"API-ZNRMR7SL6L3ATMOIK7GKJDKLPY"},
27+
},
28+
{
29+
name: "valid pattern - env assignment",
30+
input: `OCTOPUS_API_KEY="API-7F1M9T3D5P7Q2W4R6Y8U0I2O4A"`,
31+
want: []string{"API-7F1M9T3D5P7Q2W4R6Y8U0I2O4A"},
32+
},
33+
{
34+
name: "valid pattern - multiple keys",
35+
input: `octopus primary=API-ZNRMR7SL6L3ATMOIK7GKJDKLPY
36+
octopus backup=API-1A2B3C4D5E6F7G8H9I0J1K2L3M`,
37+
want: []string{
38+
"API-ZNRMR7SL6L3ATMOIK7GKJDKLPY",
39+
"API-1A2B3C4D5E6F7G8H9I0J1K2L3M",
40+
},
41+
},
42+
{
43+
name: "deduplication - repeated key",
44+
input: `octopus API-ZNRMR7SL6L3ATMOIK7GKJDKLPY octopus API-ZNRMR7SL6L3ATMOIK7GKJDKLPY`,
45+
want: []string{"API-ZNRMR7SL6L3ATMOIK7GKJDKLPY"},
46+
},
47+
{
48+
name: "invalid pattern - too short",
49+
input: `key = "API-ABC123"`,
50+
want: nil,
51+
},
52+
{
53+
name: "invalid pattern - lowercase characters",
54+
input: `key = "API-ZNRMR7SL6L3ATMOIK7GkJDKLPY"`,
55+
want: nil,
56+
},
57+
{
58+
name: "invalid pattern - wrong prefix",
59+
input: `key = "AP1-ZNRMR7SL6L3ATMOIK7GKJDKLPY"`,
60+
want: nil,
61+
},
62+
}
63+
64+
for _, test := range tests {
65+
t.Run(test.name, func(t *testing.T) {
66+
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
67+
if len(test.want) > 0 && len(matchedDetectors) == 0 {
68+
t.Errorf("keywords %v not found in input", d.Keywords())
69+
return
70+
}
71+
72+
results, err := d.FromData(context.Background(), false, []byte(test.input))
73+
require.NoError(t, err)
74+
75+
if len(results) != len(test.want) {
76+
t.Errorf("expected %d results, got %d", len(test.want), len(results))
77+
return
78+
}
79+
80+
actual := make(map[string]struct{}, len(results))
81+
for _, r := range results {
82+
if len(r.RawV2) > 0 {
83+
actual[string(r.RawV2)] = struct{}{}
84+
} else {
85+
actual[string(r.Raw)] = struct{}{}
86+
}
87+
}
88+
89+
expected := make(map[string]struct{}, len(test.want))
90+
for _, v := range test.want {
91+
expected[v] = struct{}{}
92+
}
93+
94+
if diff := cmp.Diff(expected, actual); diff != "" {
95+
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
96+
}
97+
})
98+
}
99+
}
100+
101+
func TestOctopusApiKey_Type(t *testing.T) {
102+
s := Scanner{}
103+
require.Equal(t, detector_typepb.DetectorType_OctopusApiKey, s.Type())
104+
}
105+
106+
func TestOctopusApiKey_Keywords(t *testing.T) {
107+
s := Scanner{}
108+
require.NotEmpty(t, s.Keywords())
109+
require.Contains(t, s.Keywords(), "octopus")
110+
require.Contains(t, s.Keywords(), "X-Octopus-ApiKey")
111+
}

pkg/engine/defaults/defaults.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ import (
521521
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/nvapi"
522522
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/nylas"
523523
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/oanda"
524+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/octopusapikey"
524525
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/okta"
525526
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/omnisend"
526527
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/onedesk"
@@ -1413,6 +1414,7 @@ func buildDetectorList() []detectors.Detector {
14131414
&nvapi.Scanner{},
14141415
&nylas.Scanner{},
14151416
&oanda.Scanner{},
1417+
&octopusapikey.Scanner{},
14161418
&okta.Scanner{},
14171419
&omnisend.Scanner{},
14181420
&onedesk.Scanner{},

pkg/pb/detector_typepb/detector_type.pb.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/detector_type.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,4 +1054,5 @@ enum DetectorType {
10541054
GitLabOauth2 = 1050;
10551055
SpectralOps = 1051;
10561056
AWSAppSync = 1052;
1057+
OctopusApiKey = 1053;
10571058
}

0 commit comments

Comments
 (0)