mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2024-11-10 15:14:38 +00:00
b1675194ca
* Implement EndpointCustomizer Add the EndpointCustomizer interface and EndpointSetter convenience struct, implement EndpointCustomizer for github and gitlab detectors, and add parsing, verification, and applying user-supplied configuration. * Check error from SetEndpoints * Rename variable for clarity
231 lines
6.8 KiB
Go
231 lines
6.8 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
|
dpb "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
|
)
|
|
|
|
var (
|
|
specialGroups = map[string][]DetectorID{
|
|
"all": allDetectors(),
|
|
}
|
|
|
|
detectorTypeValue = make(map[string]dpb.DetectorType, len(dpb.DetectorType_value))
|
|
validDetectors = make(map[dpb.DetectorType]struct{}, len(dpb.DetectorType_value))
|
|
maxDetectorType dpb.DetectorType
|
|
)
|
|
|
|
// Setup package local global variables.
|
|
func init() {
|
|
for k, v := range dpb.DetectorType_value {
|
|
dt := dpb.DetectorType(v)
|
|
detectorTypeValue[strings.ToLower(k)] = dt
|
|
validDetectors[dt] = struct{}{}
|
|
if dt > maxDetectorType {
|
|
maxDetectorType = dt
|
|
}
|
|
}
|
|
}
|
|
|
|
// DetectorID identifies a detector type and version. This struct is used as a
|
|
// way for users to identify detectors, whether unique or not. A DetectorID
|
|
// with Version = 0 indicates all possible versions of a detector.
|
|
type DetectorID struct {
|
|
ID dpb.DetectorType
|
|
Version int
|
|
}
|
|
|
|
// GetDetectorID extracts the DetectorID from a Detector.
|
|
func GetDetectorID(d detectors.Detector) DetectorID {
|
|
var version int
|
|
if v, ok := d.(detectors.Versioner); ok {
|
|
version = v.Version()
|
|
}
|
|
return DetectorID{
|
|
ID: d.Type(),
|
|
Version: version,
|
|
}
|
|
}
|
|
|
|
// ParseDetectors parses user supplied string into a list of detectors types.
|
|
// "all" will return the list of all available detectors. The input is comma
|
|
// separated and may use the case-insensitive detector name defined in the
|
|
// protobuf, or the protobuf enum number. A range may be used as well in the
|
|
// form "start-end". Order is preserved and duplicates are ignored.
|
|
func ParseDetectors(input string) ([]DetectorID, error) {
|
|
var output []DetectorID
|
|
seenDetector := map[DetectorID]struct{}{}
|
|
for _, item := range strings.Split(input, ",") {
|
|
item = strings.TrimSpace(item)
|
|
if item == "" {
|
|
continue
|
|
}
|
|
detectors, ok := specialGroups[strings.ToLower(item)]
|
|
if !ok {
|
|
var err error
|
|
detectors, err = asRange(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for _, d := range detectors {
|
|
if _, ok := seenDetector[d]; ok {
|
|
continue
|
|
}
|
|
seenDetector[d] = struct{}{}
|
|
output = append(output, d)
|
|
}
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
// ParseDetector parses a user supplied string into a single DetectorID. Input
|
|
// is case-insensitive and either the detector name or ID may be used.
|
|
func ParseDetector(input string) (DetectorID, error) {
|
|
return asDetectorID(strings.TrimSpace(input))
|
|
}
|
|
|
|
// ParseVerifierEndpoints parses a map of user supplied verifier URLs. The
|
|
// input keys are detector IDs and the values are a comma separated list of
|
|
// URLs. The URLs are validated as HTTPS endpoints.
|
|
func ParseVerifierEndpoints(verifierURLs map[string]string) (map[DetectorID][]string, error) {
|
|
verifiers := make(map[DetectorID][]string, len(verifierURLs))
|
|
for detectorID, urls := range verifierURLs {
|
|
key, err := ParseDetector(detectorID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid detector ID for verifier: %w", err)
|
|
}
|
|
verifierURLs := strings.Split(urls, ",")
|
|
for i, rawEndpoint := range verifierURLs {
|
|
rawEndpoint := strings.TrimSpace(rawEndpoint)
|
|
verifierURLs[i] = rawEndpoint
|
|
if endpoint, err := url.Parse(rawEndpoint); err != nil {
|
|
return nil, fmt.Errorf("invalid verifier url %q: %w", rawEndpoint, err)
|
|
} else if endpoint.Scheme != "https" {
|
|
return nil, fmt.Errorf("verifier url must be https: %q", rawEndpoint)
|
|
}
|
|
}
|
|
verifiers[key] = append(verifiers[key], verifierURLs...)
|
|
}
|
|
return verifiers, nil
|
|
}
|
|
|
|
func (id DetectorID) String() string {
|
|
name := dpb.DetectorType_name[int32(id.ID)]
|
|
if name == "" {
|
|
name = "<invalid ID>"
|
|
}
|
|
if id.Version == 0 {
|
|
return name
|
|
}
|
|
return fmt.Sprintf("%s.v%d", name, id.Version)
|
|
}
|
|
|
|
// allDetectors reutrns an ordered slice of all detector types.
|
|
func allDetectors() []DetectorID {
|
|
all := make([]DetectorID, 0, len(dpb.DetectorType_name))
|
|
for id := range dpb.DetectorType_name {
|
|
all = append(all, DetectorID{ID: dpb.DetectorType(id)})
|
|
}
|
|
sort.Slice(all, func(i, j int) bool { return all[i].ID < all[j].ID })
|
|
return all
|
|
}
|
|
|
|
// asRange converts a single input into a slice of detector types. If the input
|
|
// is not in range format, a slice of length 1 is returned. Unbounded ranges
|
|
// are allowed.
|
|
func asRange(input string) ([]DetectorID, error) {
|
|
// Check if it's a single detector type.
|
|
dt, err := asDetectorID(input)
|
|
if err == nil {
|
|
return []DetectorID{dt}, nil
|
|
}
|
|
|
|
// Check if it's a range; if not return the error from above.
|
|
start, end, found := strings.Cut(input, "-")
|
|
if !found {
|
|
return nil, err
|
|
}
|
|
start, end = strings.TrimSpace(start), strings.TrimSpace(end)
|
|
|
|
// Convert the range start and end to a DetectorType.
|
|
dtStart, err := asDetectorID(start)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dtEnd, err := asDetectorID(end)
|
|
// If end is empty it's an unbounded range.
|
|
if err != nil && end != "" {
|
|
return nil, err
|
|
}
|
|
if end == "" {
|
|
dtEnd.ID = maxDetectorType
|
|
}
|
|
|
|
// Ensure these ranges don't have versions.
|
|
if dtEnd.Version != 0 || dtStart.Version != 0 {
|
|
return nil, fmt.Errorf("versions within ranges are not supported: %s", input)
|
|
}
|
|
|
|
step := dpb.DetectorType(1)
|
|
if dtStart.ID > dtEnd.ID {
|
|
step = -1
|
|
}
|
|
var output []DetectorID
|
|
for dt := dtStart.ID; dt != dtEnd.ID; dt += step {
|
|
if _, ok := validDetectors[dt]; !ok {
|
|
continue
|
|
}
|
|
output = append(output, DetectorID{ID: dt})
|
|
}
|
|
return append(output, dtEnd), nil
|
|
}
|
|
|
|
// asDetectorID converts the case-insensitive input into a DetectorID. Name or
|
|
// ID may be used. Input is expected to be already trimmed of whitespace.
|
|
func asDetectorID(input string) (DetectorID, error) {
|
|
if input == "" {
|
|
return DetectorID{}, fmt.Errorf("empty detector")
|
|
}
|
|
var detectorID DetectorID
|
|
// Separate the version if there is one.
|
|
if detector, version, hasVersion := strings.Cut(input, "."); hasVersion {
|
|
parsedVersion, err := parseVersion(version)
|
|
if err != nil {
|
|
return DetectorID{}, fmt.Errorf("invalid version for input: %q error: %w", input, err)
|
|
}
|
|
detectorID.Version = parsedVersion
|
|
// Because there was a version, the detector type input is the part before the '.'
|
|
input = detector
|
|
}
|
|
|
|
// Check if it's a named detector.
|
|
if dt, ok := detectorTypeValue[strings.ToLower(input)]; ok {
|
|
detectorID.ID = dt
|
|
return detectorID, nil
|
|
}
|
|
// Check if it's a detector ID.
|
|
if i, err := strconv.ParseInt(input, 10, 32); err == nil {
|
|
dt := dpb.DetectorType(i)
|
|
if _, ok := validDetectors[dt]; !ok {
|
|
return DetectorID{}, fmt.Errorf("invalid detector ID: %s", input)
|
|
}
|
|
detectorID.ID = dt
|
|
return detectorID, nil
|
|
}
|
|
return DetectorID{}, fmt.Errorf("unrecognized detector type: %s", input)
|
|
}
|
|
|
|
func parseVersion(v string) (int, error) {
|
|
if !strings.HasPrefix(strings.ToLower(v), "v") {
|
|
return 0, fmt.Errorf("version must start with 'v'")
|
|
}
|
|
version := strings.TrimLeft(v, "vV")
|
|
return strconv.Atoi(version)
|
|
}
|