Files
trufflehog/pkg/detectors/netsuite/netsuite.go
Amaan Ullah 0fa069c12f Enable errcheck and staticcheck for golangci-lint v2 and resolve all issues (#4924)
* enable errcheck and staticcheck for golangci-lint v2 and resolve all issues

* skip lint on intentional reference of deprecated DetectorType values
2026-05-15 17:07:14 +05:00

297 lines
9.4 KiB
Go

package netsuite
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
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 {
detectors.DefaultMultiPartCredentialProvider
client *http.Client
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
consumerKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"netsuite", "consumer", "key"}) + `\b([a-zA-Z0-9]{64})\b`)
consumerSecretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"netsuite", "consumer", "secret"}) + `\b([a-zA-Z0-9]{64})\b`)
tokenKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"netsuite", "token", "key"}) + `\b([a-zA-Z0-9]{64})\b`)
tokenSecretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"netsuite", "token", "secret"}) + `\b([a-zA-Z0-9]{64})\b`)
accountIDPat = regexp.MustCompile(detectors.PrefixRegex([]string{"netsuite", "account", "id"}) + `\b([a-zA-Z0-9-_]{6,15})\b`)
)
type credentialSet struct {
consumerKey string
consumerSecret string
tokenKey string
tokenSecret string
accountID string
}
// 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{"netsuite"}
}
// FromData will find and optionally verify Netsuite 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)
// find for credentials
consumerKeyMatches := trimUniqueMatches(consumerKeyPat.FindAllStringSubmatch(dataStr, -1))
consumerSecretMatches := trimUniqueMatches(consumerSecretPat.FindAllStringSubmatch(dataStr, -1))
tokenKeyMatches := trimUniqueMatches(tokenKeyPat.FindAllStringSubmatch(dataStr, -1))
tokenSecretMatches := trimUniqueMatches(tokenSecretPat.FindAllStringSubmatch(dataStr, -1))
accountIDMatches := trimUniqueMatches(accountIDPat.FindAllStringSubmatch(dataStr, -1))
for consumerKey := range consumerKeyMatches {
for consumerSecret := range consumerSecretMatches {
for tokenKey := range tokenKeyMatches {
for tokenSecret := range tokenSecretMatches {
for accountID := range accountIDMatches {
cs := credentialSet{
consumerKey: consumerKey,
consumerSecret: consumerSecret,
tokenKey: tokenKey,
tokenSecret: tokenSecret,
accountID: accountID,
}
if !isUniqueKeys(cs) {
continue
}
s1 := detectors.Result{
DetectorType: detector_typepb.DetectorType_Netsuite,
Raw: []byte(consumerKey),
SecretParts: map[string]string{
"consumer_key": consumerKey,
"consumer_secret": consumerSecret,
},
RawV2: []byte(consumerKey + consumerSecret),
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, err := verifyCredentials(ctx,
client,
cs)
s1.Verified = isVerified
s1.SetVerificationError(err, consumerKey)
}
results = append(results, s1)
}
}
}
}
}
return results, nil
}
func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.DetectorType_Netsuite
}
func (s Scanner) Description() string {
return "Netsuite is a cloud business management suite. These credentials can be used to access and modify data within Netsuite."
}
func verifyCredentials(ctx context.Context, client *http.Client, cs credentialSet) (bool, error) {
// for url, filter or replace underscore in accountID if needed and lower case the accountID
urlAccountId := strings.ToLower(strings.ReplaceAll(cs.accountID, "_", "-"))
baseUrl := "https://" + urlAccountId + ".suitetalk.api.netsuite.com"
const path = "/services/rest/record/v1/metadata-catalog/check"
// nonce generate
nonce, err := netsuiteNonce(11)
if err != nil {
return false, err
}
signature := makeSignature(http.MethodGet, baseUrl, path, map[string]string{
"consumer_key": cs.consumerKey,
"consumer_secret": cs.consumerSecret,
"token_id": cs.tokenKey,
"token_secret": cs.tokenSecret,
"signature_method": "HMAC-SHA256",
"timestamp": strconv.FormatInt(time.Now().Unix(), 10),
"nonce": nonce,
"version": "1.0",
"realm": cs.accountID,
})
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseUrl+path, nil)
if err != nil {
return false, err
}
// Set required headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", signature)
// Make the request
res, err := client.Do(req)
if err != nil {
if strings.Contains(err.Error(), "no such host") {
return false, nil
}
return false, err
}
defer func() { _ = res.Body.Close() }()
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
// 401 indicates unverified credentials
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}
func generateHMACSHA256(message, secret string) string {
// Create a new HMAC by defining the hash type and the key (as byte array)
h := hmac.New(sha256.New, []byte(secret))
// Write the message to it
h.Write([]byte(message))
// Get the resulting HMAC as a byte slice
hash := h.Sum(nil)
// Encode the byte slice to a hexadecimal string
return base64.StdEncoding.EncodeToString(hash)
}
func addOauthParam(builder *strings.Builder, key, value string) {
builder.WriteString(key)
builder.WriteString("=\"")
builder.WriteString(value)
builder.WriteString("\",")
}
func makeSignature(method, baseUrl, path string, params map[string]string) string {
var paramsStringBuilder strings.Builder
paramsStringBuilder.WriteString("oauth_consumer_key=" + url.QueryEscape(params["consumer_key"]))
paramsStringBuilder.WriteString("&oauth_nonce=" + url.QueryEscape(params["nonce"]))
paramsStringBuilder.WriteString("&oauth_signature_method=" + url.QueryEscape(params["signature_method"]))
paramsStringBuilder.WriteString("&oauth_timestamp=" + url.QueryEscape(params["timestamp"]))
paramsStringBuilder.WriteString("&oauth_token=" + url.QueryEscape(params["token_id"]))
paramsStringBuilder.WriteString("&oauth_version=" + url.QueryEscape(params["version"]))
var signatureBaseBuilder strings.Builder
signatureBaseBuilder.WriteString(method)
signatureBaseBuilder.WriteString("&")
signatureBaseBuilder.WriteString(url.QueryEscape(baseUrl + path))
signatureBaseBuilder.WriteString("&")
signatureBaseBuilder.WriteString(url.QueryEscape(paramsStringBuilder.String()))
key := url.QueryEscape(params["consumer_secret"]) + "&" + url.QueryEscape(params["token_secret"])
signature := generateHMACSHA256(signatureBaseBuilder.String(), key)
var authHeaderBuilder strings.Builder
authHeaderBuilder.WriteString("OAuth")
authHeaderBuilder.WriteString(" ")
addOauthParam(&authHeaderBuilder, "realm", params["realm"])
addOauthParam(&authHeaderBuilder, "oauth_consumer_key", params["consumer_key"])
addOauthParam(&authHeaderBuilder, "oauth_token", params["token_id"])
addOauthParam(&authHeaderBuilder, "oauth_signature_method", params["signature_method"])
addOauthParam(&authHeaderBuilder, "oauth_timestamp", params["timestamp"])
addOauthParam(&authHeaderBuilder, "oauth_nonce", params["nonce"])
addOauthParam(&authHeaderBuilder, "oauth_version", params["version"])
addOauthParam(&authHeaderBuilder, "oauth_signature", url.QueryEscape(signature))
// remove trailing comma
authHeader := authHeaderBuilder.String()
authHeader = authHeader[:len(authHeader)-1]
return authHeader
}
func trimUniqueMatches(matches [][]string) (result map[string]struct{}) {
result = make(map[string]struct{})
for _, match := range matches {
if len(match) > 0 {
trimmedString := strings.TrimSpace(match[1])
result[trimmedString] = struct{}{}
}
}
return result
}
// Check if a credential set is unique, this is used to avoid duplicates.
func isUniqueKeys(cs credentialSet) bool {
seen := make(map[string]struct{})
credentials := []string{cs.consumerKey, cs.consumerSecret, cs.tokenKey, cs.tokenSecret, cs.accountID}
for _, cred := range credentials {
if _, exists := seen[cred]; exists {
return false
}
seen[cred] = struct{}{}
}
return true
}
/*
To generate a nonce, we need to generate a random string of characters.
*/
var (
charset = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// maxUnbiasedUint32 is the largest multiple of len(charset) that fits in a uint32.
// It's used to ensure unbiased sampling when selecting characters from the charset.
maxUnbiasedUint32 = uint32((1<<32 - 1) - ((1<<32 - 1) % uint64(len(charset))))
)
func netsuiteNonce(n int) (string, error) {
b := make([]byte, n)
buf := make([]byte, 4)
for i := 0; i < n; {
if _, err := rand.Read(buf); err != nil {
return "", err
}
num := binary.BigEndian.Uint32(buf)
if num < maxUnbiasedUint32 {
b[i] = charset[num%uint32(len(charset))]
i++
}
}
return string(b), nil
}