* enable errcheck and staticcheck for golangci-lint v2 and resolve all issues * skip lint on intentional reference of deprecated DetectorType values
297 lines
9.4 KiB
Go
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
|
|
}
|