Files
trufflehog/pkg/detectors/octopusdeploy/octopusdeploy.go
Muneeb Ullah Khan 797f02be36 [INS-334] Octopus Deploy detector (#4787)
* added octopus deploy detector

* chore: feature flag gating, regen protos

* chore: add SecretParts, fix integration test

* chore: refactors tests, updated http client and fix redacted token length for more visibility
2026-07-02 16:15:00 +05:00

155 lines
3.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package octopusdeploy
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
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
}
// Compile-time interface check
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = detectors.NewClientWithDedup(detectors.DetectorHttpClientWithNoLocalAddresses)
// Octopus Deploy API keys:
// Format: API- followed by 29–34 uppercase letters or digits
octopusTokenPat = regexp.MustCompile(
`\b(API-[A-Z0-9]{29,34})\b`,
)
urlPat = regexp.MustCompile(`\b([a-z0-9-]+\.octopus\.app)\b`)
)
// Keywords used for fast pre-filtering
func (s Scanner) Keywords() []string {
return []string{"octopus"}
}
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// FromData scans for Octopus API tokens and optionally verifies them
func (s Scanner) FromData(
ctx context.Context,
verify bool,
data []byte,
) (results []detectors.Result, err error) {
dataStr := string(data)
uniqueTokens := make(map[string]struct{})
uniqueUrls := make(map[string]struct{})
for _, urlMatch := range urlPat.FindAllStringSubmatch(dataStr, -1) {
uniqueUrls[urlMatch[1]] = struct{}{}
}
for _, match := range octopusTokenPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[match[1]] = struct{}{}
}
for url := range uniqueUrls {
for token := range uniqueTokens {
result := detectors.Result{
DetectorType: detector_typepb.DetectorType_OctopusDeploy,
Raw: []byte(token),
RawV2: []byte(fmt.Sprintf("%s:%s", url, token)),
Redacted: token[:8] + "...",
SecretParts: map[string]string{
"key": token,
"url": url,
},
}
if verify {
verified, verificationErr := verifyOctopusToken(
ctx,
s.getClient(),
url,
token,
)
result.SetVerificationError(verificationErr, token)
result.Verified = verified
}
results = append(results, result)
}
}
return
}
func verifyOctopusToken(
ctx context.Context,
client *http.Client,
baseUrl string,
token string,
) (bool, error) {
// API REFERENCE: https://trufflesec.octopus.app/api
// DOCS: https://octopus.com/docs/octopus-rest-api
url := &url.URL{
Scheme: "https",
Host: baseUrl,
Path: "/api/users/me",
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
url.String(),
http.NoBody,
)
if err != nil {
return false, err
}
req.Header.Set("X-Octopus-ApiKey", token)
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_OctopusDeploy, fmt.Sprintf("%s:%s", baseUrl, token), req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK, http.StatusForbidden:
return true, nil
case http.StatusUnauthorized:
// Invalid or revoked key
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_OctopusDeploy
}
func (s Scanner) Description() string {
return "Octopus Deploy is a DevOps deployment automation platform. Octopus Deploy API keys can be used to automate deployments, manage projects, environments, and infrastructure resources."
}